//! QuickDraw trap handlers (initialization, port management, pen state,
//! coordinate transforms, region ops, drawing commands).

use super::types::{read_rect, Rect, ShapeOp};
use crate::cpu::{CpuOps, Register};
use crate::display::{self, CursorImage};
use crate::machine_profile::ORACLE_MACHINE_PROFILE;
use crate::memory::{MacMemoryBus, MemoryBus};
use crate::quickdraw::fonts::{font_id_for_name, font_name_for_id, get_font_face_scaled};
use crate::quickdraw::text::get_glyph;
use crate::trap::dispatch::{
    CachedCopyBitmapInfo, InverseTableCacheEntry, PortDrawState, RecentColorTableFetch,
    ScreenCopyBitsRect, INVERSE_TABLE_CACHE_LIMIT,
};
use crate::Result;
use std::sync::OnceLock;

#[derive(Clone, Copy)]
pub(super) struct CopyBitmapInfo {
    pub(super) base: u32,
    pub(super) row_bytes: u32,
    pub(super) bounds_top: i16,
    pub(super) bounds_left: i16,
    pub(super) bounds_bottom: i16,
    pub(super) bounds_right: i16,
    pub(super) pixel_size: u32,
    pub(super) ctab_handle: u32,
}

/// Resolved location of an individual pixel in a CGrafPort/GrafPort
/// pixmap. Produced by `resolve_pixel_target` and consumed by
/// `read_cpixel` / `write_cpixel` for SetCPixel and GetCPixel.
#[derive(Clone, Copy)]
struct PixelTarget {
    base: u32,
    row_bytes: u32,
    pixel_size: u16,
    dx: u32,
    dy: u32,
    ctab_handle: u32,
}

pub(super) struct RegionMembershipCache {
    pub(super) top: i16,
    pub(super) rows: Vec<Vec<i16>>,
}

#[derive(Clone, Copy)]
enum RegionBooleanOp {
    Intersection,
    Union,
    Difference,
    Xor,
}

pub(super) const REGION_HEADER_SIZE: u32 = 10;
const REGION_STOP: i16 = i16::MAX;
const PALETTE_HEADER_SIZE: u32 = 16;
const PALETTE_COLOR_INFO_SIZE: u32 = 16;
const PALETTE_DEFAULT_WINDOW: u32 = u32::MAX;
const C_NO_MEM_ERR: u32 = (-152i32) as u32;
const PM_COURTEOUS: i16 = 0x0000;
const PM_TOLERANT: i16 = 0x0002;
const PM_EXPLICIT: i16 = 0x0008;
const PM_ALL_UPDATES: i16 = -0x2000;
const DM_SET_DISPLAY_MODE_REJECT_ERR: i16 = -330;
const SEEDFILL_MAX_PIXELS: usize = 4 * 1024 * 1024;

// OnceLock-cache the tracer env-var lookups. CopyBits/DrawPicture/SetEntries
// fire these helpers thousands of times per session; caching collapses those
// to a single syscall at startup.
static TRACE_MENU_REDRAW: OnceLock<bool> = OnceLock::new();
static TRACE_DIALOG_TEXT: OnceLock<bool> = OnceLock::new();
static TRACE_DIALOG_DRAW: OnceLock<bool> = OnceLock::new();
static TRACE_DIALOG_GWORLD: OnceLock<bool> = OnceLock::new();
static TRACE_DIALOG_PORTS: OnceLock<bool> = OnceLock::new();
static TRACE_DIALOG_PORT_DUMP: OnceLock<bool> = OnceLock::new();
static DUMP_COPYBITS_SRC_PATH: OnceLock<Option<String>> = OnceLock::new();
static TRACE_PALETTE: OnceLock<bool> = OnceLock::new();
static PALETTE_STRICT: OnceLock<bool> = OnceLock::new();
static PALETTE_AS_GAME_WROTE: OnceLock<bool> = OnceLock::new();
static PICT_SEED_CLUT_DISABLED: OnceLock<bool> = OnceLock::new();
static TRACE_QD_COLORS: OnceLock<bool> = OnceLock::new();
static TRACE_TITLE_DIAG: OnceLock<bool> = OnceLock::new();
static TRACE_GDEVICE_TRAPS: OnceLock<bool> = OnceLock::new();
static TRACE_FONT_TRAPS: OnceLock<bool> = OnceLock::new();
static TRACE_REGION_OPS: OnceLock<bool> = OnceLock::new();

fn trace_menu_redraw_enabled() -> bool {
    *TRACE_MENU_REDRAW.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_MENU_REDRAWS").is_some())
}

fn trace_dialog_text_enabled() -> bool {
    *TRACE_DIALOG_TEXT.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_TEXT").is_some())
}

fn trace_dialog_draw_enabled() -> bool {
    *TRACE_DIALOG_DRAW.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_DRAW").is_some())
}

fn trace_dialog_gworld_enabled() -> bool {
    *TRACE_DIALOG_GWORLD
        .get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_GWORLD").is_some())
}

fn trace_dialog_ports_enabled() -> bool {
    *TRACE_DIALOG_PORTS.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_PORTS").is_some())
}

fn trace_dialog_port_dump_enabled() -> bool {
    *TRACE_DIALOG_PORT_DUMP
        .get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_PORT_DUMP").is_some())
}

fn dump_copybits_src_path() -> Option<&'static str> {
    DUMP_COPYBITS_SRC_PATH
        .get_or_init(|| {
            std::env::var("SYSTEMLESS_DUMP_COPYBITS_SRC")
                .ok()
                .map(|value| {
                    if value.is_empty() || value == "1" {
                        std::env::temp_dir()
                            .join("systemless_copybits_src.png")
                            .display()
                            .to_string()
                    } else {
                        value
                    }
                })
        })
        .as_deref()
}

fn trace_palette_enabled() -> bool {
    *TRACE_PALETTE.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_PALETTE").is_some())
}

fn palette_strict_enabled() -> bool {
    *PALETTE_STRICT.get_or_init(|| std::env::var_os("SYSTEMLESS_PALETTE_STRICT").is_some())
}

fn palette_as_game_wrote_enabled() -> bool {
    *PALETTE_AS_GAME_WROTE
        .get_or_init(|| std::env::var_os("SYSTEMLESS_PALETTE_AS_GAME_WROTE").is_some())
}

/// PICT-CTab seeding is DISABLED by default. The compensator paths that
/// write PICT-embedded CTabs (via `seed_screen_palette_from_picture_clut`
/// and DrawPicture's merge-seed branch) were essentially guessing the
/// scene palette from PICT content when real Mac relies on the
/// application making explicit SetEntries / ActivatePalette calls.
///
/// Opt back in by setting `SYSTEMLESS_PICT_SEED_CLUT_ENABLE=1`.
fn pict_seed_clut_disabled() -> bool {
    *PICT_SEED_CLUT_DISABLED
        .get_or_init(|| std::env::var_os("SYSTEMLESS_PICT_SEED_CLUT_ENABLE").is_none())
}

fn trace_qd_colors_enabled() -> bool {
    *TRACE_QD_COLORS.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_QD_COLORS").is_some())
}

fn trace_title_diag_enabled() -> bool {
    *TRACE_TITLE_DIAG.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_TITLE_DIAG").is_some())
}

fn trace_gdevice_traps_enabled() -> bool {
    *TRACE_GDEVICE_TRAPS
        .get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_GDEVICE_TRAPS").is_some())
}

fn trace_font_traps_enabled() -> bool {
    *TRACE_FONT_TRAPS.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_FONT_TRAPS").is_some())
}

fn trace_region_ops_enabled() -> bool {
    *TRACE_REGION_OPS.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_REGION_OPS").is_some())
}

// Hot-path tracer helpers (CopyBits/DrawPicture/EraseRect/CTab seed fire
// every frame or every PICT).
static TRACE_ERASERECT: OnceLock<bool> = OnceLock::new();
static TRACE_OFFSCREEN_002EAD50: OnceLock<bool> = OnceLock::new();
static TRACE_COPYBITS_ALL: OnceLock<bool> = OnceLock::new();
static TRACE_COPYBITS: OnceLock<bool> = OnceLock::new();
static TRACE_COPYBITS_HUD_PROBE: OnceLock<Option<String>> = OnceLock::new();
static TRACE_COPYBITS_HUD_PROBE_POINTS: OnceLock<Vec<(i16, i16)>> = OnceLock::new();
static TRACE_DRAWPICTURE: OnceLock<bool> = OnceLock::new();
static TRACE_PICT: OnceLock<bool> = OnceLock::new();
static TRACE_FADE_BRANCH: OnceLock<bool> = OnceLock::new();
static TRACE_CTAB_SEED: OnceLock<bool> = OnceLock::new();

fn trace_eraserect_enabled() -> bool {
    *TRACE_ERASERECT.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_ERASERECT").is_some())
}

fn trace_offscreen_002ead50_enabled() -> bool {
    *TRACE_OFFSCREEN_002EAD50
        .get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_OFFSCREEN_002EAD50").is_some())
}

fn trace_copybits_all_enabled() -> bool {
    *TRACE_COPYBITS_ALL.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_COPYBITS_ALL").is_some())
}

fn trace_copybits_enabled() -> bool {
    *TRACE_COPYBITS.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_COPYBITS").is_some())
}

fn trace_copybits_hud_probe() -> Option<&'static str> {
    TRACE_COPYBITS_HUD_PROBE
        .get_or_init(|| {
            std::env::var_os("SYSTEMLESS_TRACE_COPYBITS_HUD_PROBE")
                .and_then(|os| os.into_string().ok())
        })
        .as_deref()
}

fn trace_copybits_hud_probe_points() -> &'static [(i16, i16)] {
    TRACE_COPYBITS_HUD_PROBE_POINTS
        .get_or_init(|| {
            let raw = trace_copybits_hud_probe().unwrap_or_default();
            let parsed: Vec<(i16, i16)> = raw
                .split(';')
                .filter_map(|part| {
                    let (x, y) = part.split_once(',')?;
                    Some((x.trim().parse().ok()?, y.trim().parse().ok()?))
                })
                .collect();
            if parsed.is_empty() {
                vec![(720, 100), (720, 185), (720, 320), (720, 500)]
            } else {
                parsed
            }
        })
        .as_slice()
}

fn trace_drawpicture_enabled() -> bool {
    *TRACE_DRAWPICTURE.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DRAWPICTURE").is_some())
}

fn trace_pict_inline_enabled() -> bool {
    *TRACE_PICT.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_PICT").is_some())
}

fn trace_fade_branch_enabled() -> bool {
    *TRACE_FADE_BRANCH.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_FADE_BRANCH").is_some())
}

fn trace_ctab_seed_enabled() -> bool {
    *TRACE_CTAB_SEED.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_CTAB_SEED").is_some())
}

fn trace_menu_redraw_rect_intersects(top: i16, left: i16, bottom: i16, right: i16) -> bool {
    const MENU_TOP: i16 = 260;
    const MENU_LEFT: i16 = 120;
    const MENU_BOTTOM: i16 = 390;
    const MENU_RIGHT: i16 = 680;

    top < MENU_BOTTOM && bottom > MENU_TOP && left < MENU_RIGHT && right > MENU_LEFT
}

fn trace_menu_probe_points() -> [(&'static str, i16, i16); 2] {
    [("orb", 337, 220), ("enter_ship", 307, 500)]
}

fn trace_menu_rect_contains_point(
    top: i16,
    left: i16,
    bottom: i16,
    right: i16,
    y: i16,
    x: i16,
) -> bool {
    y >= top && y < bottom && x >= left && x < right
}

struct CopyBitsDstFingerprint {
    top: [(u8, u32); 3],
    idx0_count: u32,
    idx255_count: u32,
    total: u32,
}

fn copybits_dst_fingerprint(
    bus: &MacMemoryBus,
    dst_info: &CopyBitmapInfo,
    dst_top: i16,
    dst_left: i16,
    dst_bottom: i16,
    dst_right: i16,
) -> CopyBitsDstFingerprint {
    let y0 = dst_top.max(dst_info.bounds_top);
    let y1 = dst_bottom.min(dst_info.bounds_bottom);
    let x0 = dst_left.max(dst_info.bounds_left);
    let x1 = dst_right.min(dst_info.bounds_right);
    let mut hist = [0u32; 256];
    let mut total = 0u32;
    if y1 > y0 && x1 > x0 {
        for y in y0..y1 {
            let dst_y = (y - dst_info.bounds_top) as u32;
            let row_base = dst_info.base + dst_y * dst_info.row_bytes;
            for x in x0..x1 {
                let dst_x = (x - dst_info.bounds_left) as u32;
                let b = bus.read_byte(row_base + dst_x);
                hist[b as usize] += 1;
                total += 1;
            }
        }
    }
    let mut pairs: [(u8, u32); 256] = [(0u8, 0u32); 256];
    for (i, count) in hist.iter().enumerate() {
        pairs[i] = (i as u8, *count);
    }
    pairs.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
    CopyBitsDstFingerprint {
        top: [pairs[0], pairs[1], pairs[2]],
        idx0_count: hist[0],
        idx255_count: hist[255],
        total,
    }
}

fn decode_text_face_style(raw: u16) -> i16 {
    // Style is a bitset byte (IM:I I-171), but callers marshal it in
    // either half of the pushed word. MPW C commonly places it in the
    // high byte; some hand-written callers use the low byte.
    let face = if (raw & 0x00FF) != 0 {
        raw & 0x00FF
    } else {
        raw >> 8
    };
    face as i16
}

fn encode_text_face_style(face: i16) -> u16 {
    // GrafPort.txFace is observed as a Style value in the high byte in
    // MPW C layouts; mirror BasiliskII's port record representation.
    ((face as u16) & 0x00FF) << 8
}

impl super::TrapDispatcher {
    fn loaded_font_id_for_name(&self, name: &str) -> Option<i16> {
        let needle = name.trim();
        if needle.is_empty() {
            return None;
        }
        let resources = self.resources.as_ref()?;
        for refnum in self.resource_search_order() {
            let Some(file) = resources.files.get(&refnum) else {
                continue;
            };
            for ((res_type, res_name), (res_id, _)) in &file.named {
                if res_type == b"FOND" && res_name.trim().eq_ignore_ascii_case(needle) {
                    return Some(*res_id);
                }
            }
        }
        None
    }

    fn loaded_font_name_for_id(&self, font_id: i16) -> Option<&str> {
        let resources = self.resources.as_ref()?;
        for refnum in self.resource_search_order() {
            let Some(file) = resources.files.get(&refnum) else {
                continue;
            };
            for ((res_type, res_name), (res_id, _)) in &file.named {
                if res_type == b"FOND" && *res_id == font_id {
                    return Some(res_name.as_str());
                }
            }
        }
        None
    }

    pub(crate) fn dispatch_quickdraw<C: CpuOps>(
        &mut self,
        is_tool: bool,
        trap_num: u16,
        cpu: &mut C,
        bus: &mut MacMemoryBus,
    ) -> Option<Result<()>> {
        Some(match (is_tool, trap_num) {
            // ========== QuickDraw Initialization ==========

            // InitGraf ($A86E)
            // InitGraf ($A86E): Sets up global QD vars, white/black patterns, screenBits, randSeed
            (true, 0x06E) => {
                let sp = cpu.read_reg(Register::A7);
                let global_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);

                eprintln!(
                    "[INITGRAF] global_ptr=${:08X} randSeed_addr=${:08X}",
                    global_ptr,
                    global_ptr.wrapping_sub(126)
                );

                let a5 = cpu.read_reg(Register::A5);
                bus.write_long(a5, global_ptr);

                let mut write_pat = |offset: i32, bytes: &[u8]| {
                    let addr = global_ptr.wrapping_add(offset as u32);
                    bus.write_bytes(addr, bytes);
                };
                write_pat(-8, &[0x00; 8]); // white
                write_pat(-16, &[0xFF; 8]); // black
                write_pat(-24, &[0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55]); // gray
                write_pat(-32, &[0x88, 0x22, 0x88, 0x22, 0x88, 0x22, 0x88, 0x22]); // ltGray
                write_pat(-40, &[0x77, 0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77, 0xDD]); // dkGray

                // Populate qd.screenBits from the runner's authoritative
                // screen_mode rather than from low-mem global $083C —
                // some apps (e.g. Centaurian 1.2.1's CRT) zero out the
                // $083C area as part of their init pass, which would
                // otherwise leave qd.screenBits with NIL baseAddr and
                // bounds (0,0,0,0). Reading from screen_mode keeps us
                // robust against guest low-mem global stomping.
                let screen_bits_addr = global_ptr.wrapping_sub(122);
                let (screen_base, screen_rb, screen_w, screen_h, _ps) = self.screen_mode;
                bus.write_long(screen_bits_addr, screen_base); // baseAddr
                bus.write_word(screen_bits_addr + 4, screen_rb as u16); // rowBytes
                bus.write_word(screen_bits_addr + 6, 0); // bounds.top
                bus.write_word(screen_bits_addr + 8, 0); // bounds.left
                bus.write_word(screen_bits_addr + 10, screen_h); // bounds.bottom
                bus.write_word(screen_bits_addr + 12, screen_w); // bounds.right
                                                                 // Mirror the same values back to $083C in case the guest
                                                                 // re-reads them after the wipe.
                let sb_src = crate::memory::globals::addr::SCREEN_BITS;
                bus.write_long(sb_src, screen_base);
                bus.write_word(sb_src + 4, screen_rb as u16);
                bus.write_word(sb_src + 6, 0);
                bus.write_word(sb_src + 8, 0);
                bus.write_word(sb_src + 10, screen_h);
                bus.write_word(sb_src + 12, screen_w);
                let sb_top: u16 = 0;
                let sb_left: u16 = 0;
                let sb_bottom: u16 = screen_h;
                let sb_right: u16 = screen_w;
                eprintln!(
                    "[INITGRAF] screenBits: base=${:08X} rowBytes={} bounds=(0,0,{},{})",
                    screen_base, screen_rb, screen_h, screen_w
                );

                bus.write_long(global_ptr.wrapping_sub(126), 1); // randSeed

                // Allocate + initialise a default screen GrafPort and store
                // its pointer in qd.thePort. Real Mac OS's InitGraf creates
                // this default port pointing at the screen so apps can
                // immediately draw before opening their own GrafPort.
                let port_ptr = bus.alloc(108);
                let alloc_rgn =
                    |bus: &mut MacMemoryBus, top: u16, left: u16, bottom: u16, right: u16| {
                        let ptr = bus.alloc(10);
                        bus.write_word(ptr, 10);
                        bus.write_word(ptr + 2, top);
                        bus.write_word(ptr + 4, left);
                        bus.write_word(ptr + 6, bottom);
                        bus.write_word(ptr + 8, right);
                        let handle = bus.alloc(4);
                        bus.write_long(handle, ptr);
                        handle
                    };
                let vis_rgn = alloc_rgn(bus, sb_top, sb_left, sb_bottom, sb_right);
                // IM:I I-163: OpenPort/InitPort default clip is the
                // infinite QuickDraw rectangle, not screen bounds.
                let clip_rgn = alloc_rgn(
                    bus,
                    (-32767_i16) as u16,
                    (-32767_i16) as u16,
                    32767_u16,
                    32767_u16,
                );
                bus.write_word(port_ptr, 0); // device
                for i in 0..14 {
                    bus.write_byte(port_ptr + 2 + i, bus.read_byte(screen_bits_addr + i));
                }
                for i in 0..8 {
                    bus.write_byte(port_ptr + 16 + i, bus.read_byte(screen_bits_addr + 6 + i));
                }
                bus.write_long(port_ptr + 24, vis_rgn);
                bus.write_long(port_ptr + 28, clip_rgn);
                bus.write_bytes(port_ptr + 32, &[0x00; 8]); // bgPat = white
                bus.write_bytes(port_ptr + 40, &[0xFF; 8]); // fillPat = black
                bus.write_long(port_ptr + 48, 0); // pnLoc
                bus.write_word(port_ptr + 52, 1); // pnSize.v
                bus.write_word(port_ptr + 54, 1); // pnSize.h
                bus.write_word(port_ptr + 56, 8); // pnMode = patCopy
                bus.write_bytes(port_ptr + 58, &[0xFF; 8]); // pnPat = black
                bus.write_word(port_ptr + 66, 0); // pnVis
                bus.write_word(port_ptr + 68, 0); // txFont = system
                bus.write_word(port_ptr + 70, 0); // txFace = plain
                bus.write_word(port_ptr + 72, 1); // txMode = srcOr
                bus.write_word(port_ptr + 74, 0); // txSize
                bus.write_long(port_ptr + 76, 0); // spExtra
                bus.write_long(port_ptr + 80, 0x00000021); // fgColor = blackColor
                bus.write_long(port_ptr + 84, 0x0000001E); // bkColor = whiteColor

                // qd.thePort now points at this port.
                bus.write_long(global_ptr, port_ptr);

                self.port_draw_states
                    .insert(port_ptr, PortDrawState::default());
                self.set_current_port_state(bus, cpu, port_ptr, None);
                eprintln!(
                    "[INITGRAF] qd.thePort=${:08X} (default screen port)",
                    port_ptr
                );
                Ok(())
            }

            // OpenPort ($A86F)
            // OpenPort ($A86F): Allocates visRgn/clipRgn, sets pen defaults, stores as current port
            (true, 0x06F) => {
                let sp = cpu.read_reg(Register::A7);
                let port_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);

                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let screen_bits_addr = global_ptr.wrapping_sub(122);

                let sb_top = bus.read_word(screen_bits_addr + 6);
                let sb_left = bus.read_word(screen_bits_addr + 8);
                let sb_bottom = bus.read_word(screen_bits_addr + 10);
                let sb_right = bus.read_word(screen_bits_addr + 12);

                let alloc_rgn =
                    |bus: &mut MacMemoryBus, top: u16, left: u16, bottom: u16, right: u16| {
                        let ptr = bus.alloc(10);
                        bus.write_word(ptr, 10);
                        bus.write_word(ptr + 2, top);
                        bus.write_word(ptr + 4, left);
                        bus.write_word(ptr + 6, bottom);
                        bus.write_word(ptr + 8, right);
                        let handle = bus.alloc(4);
                        bus.write_long(handle, ptr);
                        handle
                    };
                let vis_rgn = alloc_rgn(bus, sb_top, sb_left, sb_bottom, sb_right);
                // IM:I I-163: OpenPort initializes clipRgn to
                // (-32767,-32767,32767,32767).
                let clip_rgn = alloc_rgn(
                    bus,
                    (-32767_i16) as u16,
                    (-32767_i16) as u16,
                    32767_u16,
                    32767_u16,
                );

                bus.write_word(port_ptr, 0);
                for i in 0..14 {
                    bus.write_byte(port_ptr + 2 + i, bus.read_byte(screen_bits_addr + i));
                }
                for i in 0..8 {
                    bus.write_byte(port_ptr + 16 + i, bus.read_byte(screen_bits_addr + 6 + i));
                }
                bus.write_long(port_ptr + 24, vis_rgn);
                bus.write_long(port_ptr + 28, clip_rgn);
                bus.write_bytes(port_ptr + 32, &[0x00; 8]);
                bus.write_bytes(port_ptr + 40, &[0xFF; 8]);
                bus.write_long(port_ptr + 48, 0);
                bus.write_word(port_ptr + 52, 1);
                bus.write_word(port_ptr + 54, 1);

                self.pn_loc = (0, 0);
                self.pn_size = (1, 1);
                self.pn_mode = 8;
                self.pn_pat = [0xFF; 8];

                bus.write_word(port_ptr + 56, 8);
                bus.write_bytes(port_ptr + 58, &[0xFF; 8]);
                bus.write_word(port_ptr + 66, 0);
                // Zero out the GrafPort fields that the caller's stack-
                // allocated port memory may have left as garbage. SpaceExtra
                // (port+76) is read by draw_char per IM:I I-171 to add per-
                // space-character extra advance, so a garbage value would
                // randomise text spacing.
                bus.write_word(port_ptr + 68, 0); // txFont = system font
                bus.write_word(port_ptr + 70, 0); // txFace = plain
                bus.write_word(port_ptr + 72, 1); // txMode = srcOr
                bus.write_word(port_ptr + 74, 0); // txSize = 0 (use default)
                bus.write_long(port_ptr + 76, 0); // spExtra = 0 (Fixed)
                bus.write_long(port_ptr + 80, 0x00000021); // fgColor = blackColor
                bus.write_long(port_ptr + 84, 0x0000001E); // bkColor = whiteColor

                self.port_draw_states
                    .insert(port_ptr, PortDrawState::default());
                self.set_current_port_state(bus, cpu, port_ptr, None);
                if trace_dialog_ports_enabled() {
                    eprintln!("[DIALOG-PORT] OpenPort port=${:08X}", port_ptr);
                }
                Ok(())
            }

            // InitPort ($A86D)
            // InitPort ($A86D): Like OpenPort, also sets as current port via A5 globals
            (true, 0x06D) => {
                let sp = cpu.read_reg(Register::A7);
                let port_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);

                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let screen_bits_addr = global_ptr.wrapping_sub(122);

                let is_stack_port = port_ptr >= sp.saturating_sub(0x10000)
                    && port_ptr <= sp.saturating_add(0x10000);

                bus.write_word(port_ptr, 0);
                if !is_stack_port {
                    for i in 0..14 {
                        bus.write_byte(port_ptr + 2 + i, bus.read_byte(screen_bits_addr + i));
                    }
                    for i in 0..8 {
                        bus.write_byte(port_ptr + 16 + i, bus.read_byte(screen_bits_addr + 6 + i));
                    }
                    bus.write_bytes(port_ptr + 32, &[0x00; 8]);
                    bus.write_bytes(port_ptr + 40, &[0xFF; 8]);
                    bus.write_long(port_ptr + 48, 0);
                    bus.write_word(port_ptr + 52, 1);
                    bus.write_word(port_ptr + 54, 1);
                    bus.write_word(port_ptr + 56, 8);
                    bus.write_bytes(port_ptr + 58, &[0xFF; 8]);
                    bus.write_word(port_ptr + 66, 0);
                    bus.write_word(port_ptr + 68, 0);
                    bus.write_word(port_ptr + 70, 0);
                    bus.write_word(port_ptr + 72, 1);
                    bus.write_word(port_ptr + 74, 0);
                    bus.write_long(port_ptr + 76, 0);
                    bus.write_long(port_ptr + 80, 0x00000021);
                    bus.write_long(port_ptr + 84, 0x0000001E);

                    // IM:I I-164: InitPort does everything OpenPort does
                    // except allocate visRgn/clipRgn. Reinitialize existing
                    // region records in place when handles are present.
                    let vis_handle = bus.read_long(port_ptr + 24);
                    if vis_handle != 0 {
                        let vis_ptr = bus.read_long(vis_handle);
                        if vis_ptr != 0 {
                            bus.write_word(vis_ptr, 10);
                            bus.write_word(vis_ptr + 2, bus.read_word(screen_bits_addr + 6));
                            bus.write_word(vis_ptr + 4, bus.read_word(screen_bits_addr + 8));
                            bus.write_word(vis_ptr + 6, bus.read_word(screen_bits_addr + 10));
                            bus.write_word(vis_ptr + 8, bus.read_word(screen_bits_addr + 12));
                        }
                    }

                    let clip_handle = bus.read_long(port_ptr + 28);
                    if clip_handle != 0 {
                        let clip_ptr = bus.read_long(clip_handle);
                        if clip_ptr != 0 {
                            bus.write_word(clip_ptr, 10);
                            bus.write_word(clip_ptr + 2, (-32767_i16) as u16);
                            bus.write_word(clip_ptr + 4, (-32767_i16) as u16);
                            bus.write_word(clip_ptr + 6, 32767_u16);
                            bus.write_word(clip_ptr + 8, 32767_u16);
                        }
                    }
                }
                self.port_draw_states
                    .insert(port_ptr, PortDrawState::default());
                self.set_current_port_state(bus, cpu, port_ptr, None);
                if trace_dialog_ports_enabled() {
                    eprintln!(
                        "[DIALOG-PORT] InitPort port=${:08X} stack_port={}",
                        port_ptr, is_stack_port
                    );
                }
                Ok(())
            }

            // ========== Port Management ==========

            // GetPort ($A874)
            // GetPort ($A874): Returns current port pointer
            (true, 0x074) => {
                let sp = cpu.read_reg(Register::A7);
                let port_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port = bus.read_long(global_ptr);
                if port_ptr != port {
                    bus.write_long(port_ptr, port);
                }
                Ok(())
            }

            // SetPort ($A873)
            // SetPort ($A873): Sets current port via A5 globals
            (true, 0x073) => {
                let sp = cpu.read_reg(Register::A7);
                let port = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if trace_dialog_ports_enabled() {
                    eprintln!(
                        "[DIALOG-PORT] SetPort pc=${:08X} tick={} port=${:08X}",
                        cpu.read_reg(Register::PC).wrapping_sub(2),
                        self.tick_count,
                        port
                    );
                }
                if port != 0 && !self.port_allows_draw_state_memory_sync(bus, port) {
                    if trace_dialog_ports_enabled() {
                        eprintln!("[DIALOG-PORT] SetPort ignored invalid port=${:08X}", port);
                    }
                    return Some(Ok(()));
                }
                self.set_current_port_state(bus, cpu, port, None);
                Ok(())
            }

            // SetPortBits ($A875)
            // SetPortBits ($A875): Copies 14 bytes of BitMap data into port
            (true, 0x075) => {
                let sp = cpu.read_reg(Register::A7);
                let bits_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port_ptr = bus.read_long(global_ptr);
                for i in 0..14 {
                    bus.write_byte(port_ptr + 2 + i, bus.read_byte(bits_ptr + i));
                }
                self.cache_portbits_pixmap_handle(bus, port_ptr + 2, bus.read_long(port_ptr + 2));
                if trace_dialog_ports_enabled() {
                    let row_bytes = bus.read_word(bits_ptr + 4);
                    let top = bus.read_word(bits_ptr + 6) as i16;
                    let left = bus.read_word(bits_ptr + 8) as i16;
                    let bottom = bus.read_word(bits_ptr + 10) as i16;
                    let right = bus.read_word(bits_ptr + 12) as i16;
                    eprintln!(
                        "[DIALOG-PORT] SetPortBits port=${:08X} bits=${:08X} rowBytes=${:04X} bounds=({},{},{},{})",
                        port_ptr, bits_ptr, row_bytes, top, left, bottom, right
                    );
                }
                Ok(())
            }

            // PortSize ($A876)
            // Sets portRect width and height; top-left corner unchanged.
            // PROCEDURE PortSize (width,height: INTEGER);
            // Inside Macintosh Volume I, I-165
            //
            // Golden coverage:
            //   a876_port_size_state (behavior_state, pixmap)
            //     Band 1: portRect top-left unchanged
            //     Band 2: portRect dimensions = (width, height)
            //     Band 3: clipRgn bbox unchanged
            //     Band 4: visRgn bbox unchanged
            (true, 0x076) => {
                let sp = cpu.read_reg(Register::A7);
                let height = bus.read_word(sp) as i16;
                let width = bus.read_word(sp + 2) as i16;
                cpu.write_reg(Register::A7, sp + 4);
                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port_ptr = bus.read_long(global_ptr);
                let top = bus.read_word(port_ptr + 16) as i16;
                let left = bus.read_word(port_ptr + 18) as i16;
                bus.write_word(port_ptr + 20, (top + height) as u16);
                bus.write_word(port_ptr + 22, (left + width) as u16);
                Ok(())
            }

            // MovePortTo ($A877)
            // PROCEDURE MovePortTo (leftGlobal,topGlobal: INTEGER);
            // Inside Macintosh Volume I (1985), p. I-166;
            // Imaging With QuickDraw (1994), section "MovePortTo".
            //
            // IM:I I-166 verbatim: "MovePortTo changes the position of
            // the current grafPort's portRect. This does not affect the
            // screen; it merely changes the location at which subsequent
            // drawing inside the port will appear. ... The leftGlobal
            // and topGlobal parameters set the distance between the top
            // left corner of portBits.bounds and the top left corner of
            // the new portRect. Like PortSize, MovePortTo doesn't change
            // the clipRgn or the visRgn, nor does it affect the local
            // coordinate system of the grafPort."
            //
            // Effect: portRect, clipRgn, visRgn, and the bitmap dimensions
            // are unchanged. portBits.bounds (or PixMap.bounds for color
            // ports) is SHIFTED so that
            //
            //   portRect.topLeft - portBits.bounds.topLeft
            //                                = (leftGlobal, topGlobal).
            //
            // Equivalently, portBits.bounds.topLeft becomes
            //   portRect.topLeft - (leftGlobal, topGlobal),
            // and portBits.bounds.bottomRight shifts by the same delta
            // (bitmap width/height preserved).
            //
            // BasiliskII-witnessed counter-example for the obvious-but-
            // wrong "move portRect, leave bits.bounds" interpretation:
            //   pre  portRect=(100,200,220,360), bits.bounds=(100,200,220,360)
            //   call MovePortTo(leftGlobal=50, topGlobal=30)
            //   post portRect=(100,200,220,360)             ← UNCHANGED
            //        bits.bounds=(70,150,190,310)            ← topLeft shifted
            //   verify: 100 - 70 = 30 = topGlobal,
            //           200 - 150 = 50 = leftGlobal.
            //
            // Why this matters for the Window Manager use case: a window's
            // portRect typically stays at local (0,0,W,H) regardless of
            // where the window sits on the global screen. Moving the
            // window updates the local→bitmap mapping (portBits.bounds)
            // so the same local coords now draw into different screen
            // pixels. The window's internal coordinate system never
            // shifts under the application's feet — which is what the
            // IM means by "doesn't affect the local coordinate system."
            //
            // Pascal PROCEDURE protocol:
            //   sp+0: topGlobal  (INTEGER, pushed second, closer to SP)
            //   sp+2: leftGlobal (INTEGER, pushed first, deeper in stack)
            // Trap pops 4 bytes; no function-result slot.
            //
            // MPW Universal Headers Quickdraw.h declares
            //   EXTERN_API(void) MovePortTo(short leftGlobal,
            //                               short topGlobal)
            //     ONEWORDINLINE(0xA877);
            //
            // Color ports: for CGrafPort (portVersion high bits set),
            // portBits is replaced by a PixMapHandle at offset 2; the
            // PixMap.bounds at PixMapPtr + 6..14 is shifted by the same
            // delta as a GrafPort's portBits.bounds.
            //
            // Golden coverage:
            //   a877_moveportto_strict
            //     B1: bits.bounds.topLeft = pre portRect.topLeft - (leftGlobal, topGlobal)
            //     B2: portRect + clipRgn bbox + visRgn bbox UNCHANGED
            //     B3: bits.bounds dimensions preserved (only topLeft shifts)
            //     B4: 4-byte Pascal PROCEDURE stack discipline
            //   catalogue trap A877_MovePortTo: 4/4 strict
            //   assertions witnessed.
            (true, 0x077) => {
                let sp = cpu.read_reg(Register::A7);
                let top_global = bus.read_word(sp) as i16;
                let left_global = bus.read_word(sp + 2) as i16;
                cpu.write_reg(Register::A7, sp + 4);
                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port_ptr = bus.read_long(global_ptr);

                // IM:I I-166: shift portBits.bounds so that
                //   portRect.topLeft - bits.bounds.topLeft = (leftGlobal, topGlobal).
                let port_rect_top = bus.read_word(port_ptr + 16) as i16;
                let port_rect_left = bus.read_word(port_ptr + 18) as i16;
                let new_bits_top = port_rect_top.wrapping_sub(top_global);
                let new_bits_left = port_rect_left.wrapping_sub(left_global);

                let port_version = bus.read_word(port_ptr + 6);
                let is_color = (port_version & 0xC000) != 0;

                if is_color {
                    // CGrafPort: portPixMap handle at offset 2 → PixMapPtr.
                    let pm_handle = bus.read_long(port_ptr + 2);
                    if pm_handle != 0 {
                        let pm_ptr = bus.read_long(pm_handle);
                        if pm_ptr != 0 {
                            let bits_top = bus.read_word(pm_ptr + 6) as i16;
                            let bits_left = bus.read_word(pm_ptr + 8) as i16;
                            let bits_bottom = bus.read_word(pm_ptr + 10) as i16;
                            let bits_right = bus.read_word(pm_ptr + 12) as i16;
                            let bits_height = bits_bottom.wrapping_sub(bits_top);
                            let bits_width = bits_right.wrapping_sub(bits_left);
                            bus.write_word(pm_ptr + 6, new_bits_top as u16);
                            bus.write_word(pm_ptr + 8, new_bits_left as u16);
                            bus.write_word(
                                pm_ptr + 10,
                                new_bits_top.wrapping_add(bits_height) as u16,
                            );
                            bus.write_word(
                                pm_ptr + 12,
                                new_bits_left.wrapping_add(bits_width) as u16,
                            );
                        }
                    }
                } else {
                    // GrafPort: portBits.bounds at offsets 8..14.
                    let bits_top = bus.read_word(port_ptr + 8) as i16;
                    let bits_left = bus.read_word(port_ptr + 10) as i16;
                    let bits_bottom = bus.read_word(port_ptr + 12) as i16;
                    let bits_right = bus.read_word(port_ptr + 14) as i16;
                    let bits_height = bits_bottom.wrapping_sub(bits_top);
                    let bits_width = bits_right.wrapping_sub(bits_left);
                    bus.write_word(port_ptr + 8, new_bits_top as u16);
                    bus.write_word(port_ptr + 10, new_bits_left as u16);
                    bus.write_word(port_ptr + 12, new_bits_top.wrapping_add(bits_height) as u16);
                    bus.write_word(port_ptr + 14, new_bits_left.wrapping_add(bits_width) as u16);
                }
                Ok(())
            }

            // SetOrigin ($A878)
            // Changes the local coordinate system of the current grafPort.
            // Offsets portRect, portBits.bounds, and visRgn by the delta.
            // Does NOT offset clipRgn — it "sticks" to the coordinate system.
            // PROCEDURE SetOrigin(h, v: INTEGER);
            // Inside Macintosh Volume I, I-166
            //
            // Golden coverage:
            //   a878_set_origin_state (behavior_state, pixmap)
            //     Band 1: portRect origin = (v, h)
            //     Band 2: portRect dimensions preserved
            //     Band 3: portBits.bounds offset by (dv, dh)
            //     Band 4: visRgn bbox offset by (dv, dh)
            //     Band 5: clipRgn bbox unchanged
            (true, 0x078) => {
                self.debug_set_origin_count = self.debug_set_origin_count.saturating_add(1);
                let sp = cpu.read_reg(Register::A7);
                let v = bus.read_word(sp) as i16;
                let h = bus.read_word(sp + 2) as i16;
                cpu.write_reg(Register::A7, sp + 4);
                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port_ptr = bus.read_long(global_ptr);
                let hidden_window_regions =
                    self.hidden_window_local_regions_for_origin_change(bus, port_ptr);
                let top = bus.read_word(port_ptr + 16) as i16;
                let left = bus.read_word(port_ptr + 18) as i16;
                let bottom = bus.read_word(port_ptr + 20) as i16;
                let right = bus.read_word(port_ptr + 22) as i16;
                let dh = h - left;
                let dv = v - top;
                if trace_dialog_ports_enabled() {
                    eprintln!(
                        "[DIALOG-PORT] SetOrigin port=${:08X} old=({}, {}) new=({}, {}) delta=({}, {})",
                        port_ptr, top, left, v, h, dv, dh
                    );
                }
                // Update portRect (wrapping arithmetic for 16-bit coordinate space)
                bus.write_word(port_ptr + 16, top.wrapping_add(dv) as u16);
                bus.write_word(port_ptr + 18, left.wrapping_add(dh) as u16);
                bus.write_word(port_ptr + 20, bottom.wrapping_add(dv) as u16);
                bus.write_word(port_ptr + 22, right.wrapping_add(dh) as u16);

                // Update portBits/portPixMap bounds.
                // In a GrafPort, portBits.bounds is at offsets 8-14.
                // In a CGrafPort, offsets 8-14 are grafVars/chExtra/pnLocHFrac
                // (NOT bitmap bounds), so we must update the PixMap bounds instead.
                let port_version = bus.read_word(port_ptr + 6);
                let is_color = (port_version & 0xC000) != 0;

                if is_color {
                    // CGrafPort: update the PixMap bounds via the handle at offset 2.
                    let pm_handle = bus.read_long(port_ptr + 2);
                    if pm_handle != 0 {
                        let pm_ptr = bus.read_long(pm_handle);
                        if pm_ptr != 0 {
                            let bt = bus.read_word(pm_ptr + 6) as i16;
                            let bl = bus.read_word(pm_ptr + 8) as i16;
                            let bb = bus.read_word(pm_ptr + 10) as i16;
                            let br = bus.read_word(pm_ptr + 12) as i16;
                            bus.write_word(pm_ptr + 6, bt.wrapping_add(dv) as u16);
                            bus.write_word(pm_ptr + 8, bl.wrapping_add(dh) as u16);
                            bus.write_word(pm_ptr + 10, bb.wrapping_add(dv) as u16);
                            bus.write_word(pm_ptr + 12, br.wrapping_add(dh) as u16);
                        }
                    }
                } else {
                    // GrafPort: portBits.bounds at offsets 8-14.
                    let bits_top = bus.read_word(port_ptr + 8) as i16;
                    let bits_left = bus.read_word(port_ptr + 10) as i16;
                    let bits_bottom = bus.read_word(port_ptr + 12) as i16;
                    let bits_right = bus.read_word(port_ptr + 14) as i16;
                    bus.write_word(port_ptr + 8, bits_top.wrapping_add(dv) as u16);
                    bus.write_word(port_ptr + 10, bits_left.wrapping_add(dh) as u16);
                    bus.write_word(port_ptr + 12, bits_bottom.wrapping_add(dv) as u16);
                    bus.write_word(port_ptr + 14, bits_right.wrapping_add(dh) as u16);
                }

                // Per IM:I I-166, SetOrigin offsets visRgn but NOT clipRgn.
                // "the pen and clipRgn 'stick' to the coordinate system"
                // visRgn is at port offset 24; clipRgn at 28 is left unchanged.
                let vis_handle = bus.read_long(port_ptr + 24);
                if vis_handle != 0 {
                    let vis_ptr = bus.read_long(vis_handle);
                    if vis_ptr != 0 {
                        let rt = bus.read_word(vis_ptr + 2) as i16;
                        let rl = bus.read_word(vis_ptr + 4) as i16;
                        let rb = bus.read_word(vis_ptr + 6) as i16;
                        let rr = bus.read_word(vis_ptr + 8) as i16;
                        bus.write_word(vis_ptr + 2, rt.wrapping_add(dv) as u16);
                        bus.write_word(vis_ptr + 4, rl.wrapping_add(dh) as u16);
                        bus.write_word(vis_ptr + 6, rb.wrapping_add(dv) as u16);
                        bus.write_word(vis_ptr + 8, rr.wrapping_add(dh) as u16);
                    }
                }
                self.sync_hidden_window_regions_after_origin_change(
                    bus,
                    port_ptr,
                    hidden_window_regions,
                );
                Ok(())
            }

            // ========== Clipping ==========

            // SetClip ($A879)
            // Changes the clipping region of the current grafPort to an
            // equivalent copy of the given region without changing the port's
            // clipRgn handle.
            // PROCEDURE SetClip (rgn: RgnHandle);
            // Inside Macintosh Volume I, I-167
            (true, 0x079) => {
                let sp = cpu.read_reg(Register::A7);
                let src_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port_ptr = bus.read_long(global_ptr);
                let dst_handle = bus.read_long(port_ptr + 28);
                if src_handle != 0 && dst_handle != 0 {
                    let src_ptr = bus.read_long(src_handle);
                    if src_ptr != 0 {
                        let size = bus.read_word(src_ptr) as u32;
                        // SetClip copies the region data into the port-owned
                        // clip region. Replacing the handle aliases caller
                        // scratch regions into the port and lets later region
                        // edits corrupt clipping state.
                        // Inside Macintosh Volume I, p. I-192
                        let Some(dst_ptr) = Self::ensure_region_capacity(bus, dst_handle, size)
                        else {
                            return Some(Ok(()));
                        };
                        for i in 0..size {
                            bus.write_byte(dst_ptr + i, bus.read_byte(src_ptr + i));
                        }
                        if trace_dialog_ports_enabled() {
                            let top = bus.read_word(src_ptr + 2) as i16;
                            let left = bus.read_word(src_ptr + 4) as i16;
                            let bottom = bus.read_word(src_ptr + 6) as i16;
                            let right = bus.read_word(src_ptr + 8) as i16;
                            eprintln!(
                                "[DIALOG-PORT] SetClip port=${:08X} rect=({},{},{},{})",
                                port_ptr, top, left, bottom, right
                            );
                        }
                    }
                }
                Ok(())
            }

            // GetClip ($A87A)
            // Changes the given region to an equivalent copy of the current
            // grafPort's clipping region without changing the given region's
            // handle.
            // PROCEDURE GetClip (rgn: RgnHandle);
            // Inside Macintosh Volume I, I-167
            (true, 0x07A) => {
                let sp = cpu.read_reg(Register::A7);
                let rgn_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port_ptr = bus.read_long(global_ptr);
                let clip_rgn = bus.read_long(port_ptr + 28);
                if rgn_handle != 0 && clip_rgn != 0 {
                    let clip_rgn_ptr = bus.read_long(clip_rgn);
                    if clip_rgn_ptr != 0 {
                        let rgn_size = bus.read_word(clip_rgn_ptr) as u32;
                        let Some(dest_ptr) =
                            Self::ensure_region_capacity(bus, rgn_handle, rgn_size)
                        else {
                            return Some(Ok(()));
                        };
                        for i in 0..rgn_size {
                            bus.write_byte(dest_ptr + i, bus.read_byte(clip_rgn_ptr + i));
                        }
                        if trace_dialog_ports_enabled() {
                            let top = bus.read_word(clip_rgn_ptr + 2) as i16;
                            let left = bus.read_word(clip_rgn_ptr + 4) as i16;
                            let bottom = bus.read_word(clip_rgn_ptr + 6) as i16;
                            let right = bus.read_word(clip_rgn_ptr + 8) as i16;
                            eprintln!(
                                "[DIALOG-PORT] GetClip port=${:08X} rect=({},{},{},{})",
                                port_ptr, top, left, bottom, right
                            );
                        }
                    }
                }
                Ok(())
            }

            // ClipRect ($A87B)
            // Changes the clipping region of the current grafPort to a rectangle equivalent to the given rect.
            // PROCEDURE ClipRect (r: Rect);
            // Inside Macintosh Volume I, I-167
            (true, 0x07B) => {
                let sp = cpu.read_reg(Register::A7);
                let rect_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port_ptr = bus.read_long(global_ptr);
                let clip_rgn_handle = bus.read_long(port_ptr + 28);
                let clip_rgn_ptr = bus.read_long(clip_rgn_handle);
                bus.write_word(clip_rgn_ptr, 10);
                for i in 0..8u32 {
                    bus.write_byte(clip_rgn_ptr + 2 + i, bus.read_byte(rect_ptr + i));
                }
                if trace_dialog_ports_enabled() {
                    let top = bus.read_word(rect_ptr) as i16;
                    let left = bus.read_word(rect_ptr + 2) as i16;
                    let bottom = bus.read_word(rect_ptr + 4) as i16;
                    let right = bus.read_word(rect_ptr + 6) as i16;
                    eprintln!(
                        "[DIALOG-PORT] ClipRect port=${:08X} rect=({},{},{},{})",
                        port_ptr, top, left, bottom, right
                    );
                }
                Ok(())
            }

            // ClosePort / CloseCPort ($A87D)
            //
            // Per IM:I I-163: "ClosePort releases the memory occupied
            // by the given grafPort's visRgn and clipRgn. When you're
            // completely through with a grafPort, call this procedure
            // and then dispose of the grafPort with the Memory Manager
            // procedure DisposPtr ... If ClosePort isn't called
            // before a grafPort is disposed of, the memory used by
            // the visRgn and clipRgn will be unrecoverable."
            //
            // Per IM:V V-72: "CloseCPort releases the memory allocated
            // to the cGrafPort. It disposes of the visRgn, the
            // clipRgn, the bkPixPat, the pnPixPat, the fillPixPat,
            // and the grafVars handle. It also disposes of the
            // portPixMap, but doesn't dispose of the portPixMap's
            // color table (which is really owned by the gDevice).
            // If you have placed your own color table into the
            // portPixMap, either dispose of it BEFORE calling
            // CloseCPort, or store another reference to it for other
            // uses."
            //
            // PROCEDURE ClosePort  (port: GrafPtr);
            // PROCEDURE CloseCPort (port: CGrafPtr);
            // Inside Macintosh Volume I, I-163 (ClosePort)
            // Inside Macintosh Volume V, V-72 (CloseCPort)
            // Inside Macintosh Volume III line 9399: "ClosePort | A87D"
            // Inside Macintosh Volume V V-291 line 20688: "CloseCPort | A87D"
            // (same trap word, polymorphic dispatch — Apple reused $A87D
            // when Color QuickDraw was added; the dispatcher inspects
            // the port's portRect rowBytes high bit to detect CGrafPort)
            // Imaging With QuickDraw 1994, 4-66
            //
            // Stack: SP+0 port GrafPtr/CGrafPtr (4 bytes). Pop 4.
            // No result (PROCEDURE).
            //
            // Systemless follows the IM:I ClosePort + IM:V CloseCPort contracts:
            //   - always frees visRgn + clipRgn handles
            //   - for CGrafPorts, also frees bkPixPat/pnPixPat/fillPixPat,
            //     grafVars, and portPixMap
            //   - leaves the portPixMap color table allocated (owned by GDevice)
            //
            // Regression coverage:
            //   quickdraw::tests::closeport_releases_visrgn_and_cliprgn_and_nils_port_slots
            //   quickdraw::tests::closeport_consumes_port_argument_and_pops_four_bytes
            //   quickdraw::tests::closecport_releases_documented_cgrafport_subhandles_and_preserves_portpixmap_colortable
            (true, 0x07D) => {
                let sp = cpu.read_reg(Register::A7);
                let port_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if port_ptr != 0 {
                    // Release visRgn (offset 24) and clipRgn (offset 28)
                    for rgn_offset in [24u32, 28u32] {
                        let rgn_handle = bus.read_long(port_ptr + rgn_offset);
                        if rgn_handle != 0 {
                            Self::free_handle_and_target(bus, rgn_handle);
                            // Nil out the handle in the port to prevent stale access
                            bus.write_long(port_ptr + rgn_offset, 0);
                        }
                    }

                    // Imaging With QuickDraw 1994 (p. 4-125): CGrafPorts have
                    // portVersion high bits set to $C000.
                    if (bus.read_word(port_ptr + 6) & 0xC000) == 0xC000 {
                        // Dispose documented CGrafPort-owned PixPat handles.
                        // IM:V V-72: CloseCPort disposes bkPixPat, pnPixPat,
                        // fillPixPat.
                        for pixpat_offset in [32u32, 58u32, 62u32] {
                            let pixpat_handle = bus.read_long(port_ptr + pixpat_offset);
                            if pixpat_handle != 0 {
                                let pixpat_ptr = bus.read_long(pixpat_handle);
                                if pixpat_ptr != 0 {
                                    // Free patData (+6) and patXData (+10)
                                    // handles and payloads.
                                    for data_offset in [6u32, 10u32] {
                                        let data_handle = bus.read_long(pixpat_ptr + data_offset);
                                        if data_handle != 0 {
                                            let data_ptr = bus.read_long(data_handle);
                                            if data_ptr != 0 {
                                                bus.free(data_ptr);
                                            }
                                            bus.free(data_handle);
                                        }
                                    }
                                    // Free patMap (+2) including its pmTable.
                                    let pat_map_handle = bus.read_long(pixpat_ptr + 2);
                                    if pat_map_handle != 0 {
                                        let pat_map_ptr = bus.read_long(pat_map_handle);
                                        if pat_map_ptr != 0 {
                                            let ctab_handle = bus.read_long(pat_map_ptr + 42);
                                            if ctab_handle != 0 {
                                                let ctab_ptr = bus.read_long(ctab_handle);
                                                if ctab_ptr != 0 {
                                                    bus.free(ctab_ptr);
                                                }
                                                bus.free(ctab_handle);
                                            }
                                            bus.free(pat_map_ptr);
                                        }
                                        bus.free(pat_map_handle);
                                    }
                                    bus.free(pixpat_ptr);
                                }
                                bus.free(pixpat_handle);
                                bus.write_long(port_ptr + pixpat_offset, 0);
                            }
                        }

                        // Free grafVars handle (+8).
                        let graf_vars_handle = bus.read_long(port_ptr + 8);
                        if graf_vars_handle != 0 {
                            let graf_vars_ptr = bus.read_long(graf_vars_handle);
                            if graf_vars_ptr != 0 {
                                bus.free(graf_vars_ptr);
                            }
                            bus.free(graf_vars_handle);
                            bus.write_long(port_ptr + 8, 0);
                        }

                        // IM:V V-72: CloseCPort disposes the portPixMap handle
                        // but does NOT dispose its color table.
                        let port_pixmap_handle = bus.read_long(port_ptr + 2);
                        if port_pixmap_handle != 0 {
                            let port_pixmap_ptr = bus.read_long(port_pixmap_handle);
                            if port_pixmap_ptr != 0 {
                                // Clear the embedded CLUT link before freeing the
                                // PixMap record so the record itself never dangles
                                // into the remaining GDevice-owned table.
                                bus.write_long(port_pixmap_ptr + 42, 0);
                                bus.free(port_pixmap_ptr);
                            }
                            bus.free(port_pixmap_handle);
                        }
                    }
                }
                // No-op for plain GrafPorts (HashMap::remove returns
                // None silently); cleans up CGrafPort tracking.
                self.cport_ports.remove(&port_ptr);
                Ok(())
            }

            // ========== Coordinate Transforms ==========

            // LocalToGlobal ($A870)
            // Converts a point from the current grafPort's local coordinate
            // system to global (screen) coordinates.
            // PROCEDURE LocalToGlobal(VAR pt: Point);
            // Inside Macintosh Volume I, I-192
            // Reference: Executor src/quickdraw/qPoint.cpp C_LocalToGlobal
            // LocalToGlobal ($A870): Adds the current port's global offset
            // (`portRect.topLeft - portBits.bounds.topLeft`); handles both
            // GrafPort and CGrafPort.
            (true, 0x070) => {
                let sp = cpu.read_reg(Register::A7);
                let pt_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port = bus.read_long(global_ptr);
                let (bounds_top, bounds_left) = self.port_bounds_top_left(bus, port);
                let (port_rect_top, port_rect_left) = self.port_rect_top_left(bus, port);
                let global_top = port_rect_top.wrapping_sub(bounds_top);
                let global_left = port_rect_left.wrapping_sub(bounds_left);
                let v = bus.read_word(pt_ptr) as i16;
                let h = bus.read_word(pt_ptr + 2) as i16;
                // wrapping_add matches 68k QuickDraw's signed 16-bit
                // wrap-around behaviour.
                bus.write_word(pt_ptr, v.wrapping_add(global_top) as u16);
                bus.write_word(pt_ptr + 2, h.wrapping_add(global_left) as u16);
                Ok(())
            }

            // GlobalToLocal ($A871)
            // Converts a point from global (screen) coordinates to the current
            // grafPort's local coordinate system.
            // PROCEDURE GlobalToLocal(VAR pt: Point);
            // Inside Macintosh Volume I, I-193
            // GlobalToLocal ($A871): Subtracts the current port's global
            // offset (`portRect.topLeft - portBits.bounds.topLeft`); handles
            // both GrafPort and CGrafPort.
            (true, 0x071) => {
                let sp = cpu.read_reg(Register::A7);
                let pt_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port = bus.read_long(global_ptr);
                let (bounds_top, bounds_left) = self.port_bounds_top_left(bus, port);
                let (port_rect_top, port_rect_left) = self.port_rect_top_left(bus, port);
                let global_top = port_rect_top.wrapping_sub(bounds_top);
                let global_left = port_rect_left.wrapping_sub(bounds_left);
                let v = bus.read_word(pt_ptr) as i16;
                let h = bus.read_word(pt_ptr + 2) as i16;
                // wrapping_sub matches 68k QuickDraw's signed 16-bit
                // wrap-around behaviour.
                let new_v = v.wrapping_sub(global_top);
                let new_h = h.wrapping_sub(global_left);
                bus.write_word(pt_ptr, new_v as u16);
                bus.write_word(pt_ptr + 2, new_h as u16);
                if std::env::var_os("SYSTEMLESS_TRACE_GLOBAL_TO_LOCAL").is_some() {
                    let trap_pc = cpu.read_reg(Register::PC).wrapping_sub(2);
                    eprintln!(
                        "[GTOL] pc=${:08X} port=${:08X} bounds_tl=({},{}) port_rect_tl=({},{}) global_offset=({},{}) global=({},{}) -> local=({},{})",
                        trap_pc, port, bounds_top, bounds_left, port_rect_top, port_rect_left, global_top, global_left, v, h, new_v, new_h
                    );
                }
                Ok(())
            }

            // GrafDevice ($A872)
            // Sets the device field of the current GrafPort.
            // PROCEDURE GrafDevice(device: INTEGER);
            // Inside Macintosh Volume I, I-165
            //
            // Regression coverage:
            //   grafdevice_sets_port_device
            //   grafdevice_pops_two_bytes
            // GrafDevice ($A872): Sets device field in port per IM:I I-165
            (true, 0x072) => {
                let sp = cpu.read_reg(Register::A7);
                let device = bus.read_word(sp);
                cpu.write_reg(Register::A7, sp + 2);
                if self.current_port != 0 {
                    bus.write_word(self.current_port, device);
                }
                Ok(())
            }

            // ========== Point Operations ==========

            // AddPt ($A87E)
            // Adds the coordinates of srcPt to dstPt, returning the result in dstPt.
            // PROCEDURE AddPt (srcPt: Point; VAR dstPt: Point);
            // Inside Macintosh Volume I, I-193
            (true, 0x07E) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_ptr = bus.read_long(sp);
                let src_v = bus.read_word(sp + 4) as i16;
                let src_h = bus.read_word(sp + 6) as i16;
                cpu.write_reg(Register::A7, sp + 8);
                let dst_v = bus.read_word(dst_ptr) as i16;
                let dst_h = bus.read_word(dst_ptr + 2) as i16;
                bus.write_word(dst_ptr, (dst_v + src_v) as u16);
                bus.write_word(dst_ptr + 2, (dst_h + src_h) as u16);
                Ok(())
            }

            // SubPt ($A87F)
            // Subtracts the coordinates of srcPt from dstPt, returning the result in dstPt.
            // PROCEDURE SubPt (srcPt: Point; VAR dstPt: Point);
            // Inside Macintosh Volume I, I-193
            (true, 0x07F) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_ptr = bus.read_long(sp);
                let src_v = bus.read_word(sp + 4) as i16;
                let src_h = bus.read_word(sp + 6) as i16;
                cpu.write_reg(Register::A7, sp + 8);
                let dst_v = bus.read_word(dst_ptr) as i16;
                let dst_h = bus.read_word(dst_ptr + 2) as i16;
                bus.write_word(dst_ptr, (dst_v - src_v) as u16);
                bus.write_word(dst_ptr + 2, (dst_h - src_h) as u16);
                Ok(())
            }

            // SetPt ($A880)
            // Assigns the two given coordinates to the point pt.
            // PROCEDURE SetPt (VAR pt: Point; h,v: INTEGER);
            // Inside Macintosh Volume I, I-193
            (true, 0x080) => {
                let sp = cpu.read_reg(Register::A7);
                let v = bus.read_word(sp) as i16;
                let h = bus.read_word(sp + 2) as i16;
                let pt_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                bus.write_word(pt_ptr, v as u16);
                bus.write_word(pt_ptr + 2, h as u16);
                Ok(())
            }

            // ========== Pen State ==========

            // PenSize ($A89B)
            // Sets the width and height of the pen.
            // PROCEDURE PenSize(width, height: INTEGER);
            // Inside Macintosh Volume I, I-170
            // PenSize ($A89B): Sets pen size (v, h)
            (true, 0x09B) => {
                let sp = cpu.read_reg(Register::A7);
                let height = bus.read_word(sp) as i16;
                let width = bus.read_word(sp + 2) as i16;
                cpu.write_reg(Register::A7, sp + 4);
                self.pn_size = (height, width);
                self.sync_current_port_draw_state(bus);
                Ok(())
            }

            // PenMode ($A89C)
            // Sets the transfer mode of the pen for line drawing.
            // PROCEDURE PenMode(mode: INTEGER);
            // Inside Macintosh Volume I, I-170
            // PenMode ($A89C): Sets pen transfer mode
            (true, 0x09C) => {
                let sp = cpu.read_reg(Register::A7);
                let mode = bus.read_word(sp) as i16;
                cpu.write_reg(Register::A7, sp + 2);
                self.pn_mode = mode;
                self.sync_current_port_draw_state(bus);
                Ok(())
            }

            // PenPat ($A89D)
            // Sets the pen pattern for subsequent drawing.
            // PROCEDURE PenPat(pat: Pattern);
            // Inside Macintosh Volume I, I-170
            // PenPat ($A89D): Sets pen pattern (8 bytes)
            (true, 0x09D) => {
                let sp = cpu.read_reg(Register::A7);
                let pat_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                for i in 0..8 {
                    self.pn_pat[i] = bus.read_byte(pat_ptr + i as u32);
                }
                self.sync_current_port_draw_state(bus);
                Ok(())
            }

            // PenNormal ($A89E)
            // Resets the pen characteristics to their standard values.
            // PROCEDURE PenNormal;
            // Inside Macintosh Volume I, I-170
            // PenNormal ($A89E): Resets pen to default (1×1, patCopy, black)
            (true, 0x09E) => {
                self.pn_size = (1, 1);
                self.pn_mode = 8;
                self.pn_pat = [0xFF; 8];
                self.sync_current_port_draw_state(bus);
                Ok(())
            }

            // GetPen ($A89A)
            // Returns the current pen location in the local coordinates
            // of the current grafPort.
            // PROCEDURE GetPen(VAR pt: Point);
            // Inside Macintosh Volume I, I-169
            //
            // Regression coverage:
            //   src/trap/quickdraw.rs::tests::getpen_writes_current_pnloc_to_output_point_and_pops_arg
            //   src/trap/quickdraw.rs::tests::getpen_reports_local_coordinates_of_current_port
            // GetPen ($A89A): Returns pen location as Point per IM:I I-169
            (true, 0x09A) => {
                let sp = cpu.read_reg(Register::A7);
                let pt_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                bus.write_word(pt_ptr, self.pn_loc.0 as u16); // v
                bus.write_word(pt_ptr + 2, self.pn_loc.1 as u16); // h
                Ok(())
            }

            // GetPenState ($A898)
            // Returns the current pen state in the specified record.
            // PROCEDURE GetPenState(VAR pnState: PenState);
            // Inside Macintosh Volume I, I-170
            (true, 0x098) => {
                let sp = cpu.read_reg(Register::A7);
                let state_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                bus.write_word(state_ptr, self.pn_loc.0 as u16);
                bus.write_word(state_ptr + 2, self.pn_loc.1 as u16);
                bus.write_word(state_ptr + 4, self.pn_size.0 as u16);
                bus.write_word(state_ptr + 6, self.pn_size.1 as u16);
                bus.write_word(state_ptr + 8, self.pn_mode as u16);
                for i in 0..8 {
                    bus.write_byte(state_ptr + 10 + i as u32, self.pn_pat[i]);
                }
                Ok(())
            }

            // BackPat ($A87C)
            // Sets the background pattern of the current GrafPort.
            // PROCEDURE BackPat(pat: Pattern);
            // Inside Macintosh Volume I, I-167
            // BackPat ($A87C): Sets background pattern
            (true, 0x07C) => {
                let sp = cpu.read_reg(Register::A7);
                let pat_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                for i in 0..8 {
                    self.bk_pat[i] = bus.read_byte(pat_ptr + i as u32);
                }
                self.sync_current_port_draw_state(bus);
                Ok(())
            }

            // ========== Pen Movement ==========

            // MoveTo ($A893)
            // MoveTo ($A893): Sets pen location
            (true, 0x093) => {
                let sp = cpu.read_reg(Register::A7);
                let v = bus.read_word(sp) as i16;
                let h = bus.read_word(sp + 2) as i16;
                cpu.write_reg(Register::A7, sp + 4);
                self.pn_loc = (v, h);
                self.sync_current_port_draw_state(bus);
                if trace_dialog_text_enabled() {
                    eprintln!(
                        "[DIALOG-TEXT] MoveTo current_port=${:08X} pnLoc=({}, {})",
                        self.current_port, v, h
                    );
                }
                Ok(())
            }

            // Move ($A894)
            // Move ($A894): Moves pen by delta
            (true, 0x094) => {
                let sp = cpu.read_reg(Register::A7);
                let dv = bus.read_word(sp) as i16;
                let dh = bus.read_word(sp + 2) as i16;
                cpu.write_reg(Register::A7, sp + 4);
                let (v0, h0) = self.pn_loc;
                self.pn_loc = (v0.saturating_add(dv), h0.saturating_add(dh));
                self.sync_current_port_draw_state(bus);
                Ok(())
            }

            // LineTo ($A891)
            // LineTo ($A891): Draws line to point, renders to framebuffer
            (true, 0x091) => {
                let sp = cpu.read_reg(Register::A7);
                let v = bus.read_word(sp) as i16;
                let h = bus.read_word(sp + 2) as i16;
                cpu.write_reg(Register::A7, sp + 4);
                let (v0, h0) = self.pn_loc;

                if self.recording_polygon.is_some() {
                    // During polygon recording, capture vertices but don't draw.
                    // Inside Macintosh Volume I, I-189
                    if let Some(ref mut rec) = self.recording_polygon {
                        if rec.vertices.is_empty() {
                            rec.vertices.push((v0, h0));
                        }
                        rec.vertices.push((v, h));
                    }
                } else if self.record_region_line(v0, h0, v, h) {
                    // OpenRgn records the mathematical line outline and
                    // suppresses framebuffer writes.
                } else if let Err(e) = self.draw_line(cpu, bus, h0, v0, h, v) {
                    return Some(Err(e));
                }
                self.pn_loc = (v, h);
                self.sync_current_port_draw_state(bus);
                Ok(())
            }

            // Line ($A892)
            // Line ($A892): Draws line by delta
            (true, 0x092) => {
                let sp = cpu.read_reg(Register::A7);
                let dv = bus.read_word(sp) as i16;
                let dh = bus.read_word(sp + 2) as i16;
                cpu.write_reg(Register::A7, sp + 4);
                let (v0, h0) = self.pn_loc;
                let h1 = h0.saturating_add(dh);
                let v1 = v0.saturating_add(dv);

                if self.recording_polygon.is_some() {
                    if let Some(ref mut rec) = self.recording_polygon {
                        if rec.vertices.is_empty() {
                            rec.vertices.push((v0, h0));
                        }
                        rec.vertices.push((v1, h1));
                    }
                } else if self.record_region_line(v0, h0, v1, h1) {
                    // See LineTo above for OpenRgn-recording rationale.
                } else if let Err(e) = self.draw_line(cpu, bus, h0, v0, h1, v1) {
                    return Some(Err(e));
                }
                self.pn_loc = (v1, h1);
                // Mirror the new pen location into the current port's pnLoc
                // field (port+48/50) so guest code that reads pnLoc after
                // Line sees the post-Line position. IM:I I-169.
                self.sync_current_port_draw_state(bus);
                Ok(())
            }

            // ========== Region Operations ==========

            // NewRgn ($A8D8)
            // Allocates space for a new region, initializes it to the empty
            // region (0,0,0,0), and returns a handle to it.
            // FUNCTION NewRgn: RgnHandle;
            // Inside Macintosh Volume I, I-181
            // NewRgn ($A8D8): Allocates 10-byte empty region + handle
            (true, 0x0D8) => {
                let sp = cpu.read_reg(Register::A7);
                let ptr = bus.alloc(10);
                bus.write_word(ptr, 10);
                let handle = bus.alloc(4);
                bus.write_long(handle, ptr);
                // Write result into the caller's pre-allocated slot at SP
                bus.write_long(sp, handle);
                // SP unchanged — no parameters to pop
                Ok(())
            }

            // RectRgn ($A8DF)
            // RectRgn ($A8DF): Sets region to bounding rect
            (true, 0x0DF) => {
                let sp = cpu.read_reg(Register::A7);
                let rect_ptr = bus.read_long(sp);
                let rgn_handle = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                let rgn_ptr = bus.read_long(rgn_handle);
                for i in 0..8 {
                    bus.write_byte(rgn_ptr + 2 + i, bus.read_byte(rect_ptr + i));
                }
                bus.write_word(rgn_ptr, 10);
                Ok(())
            }

            // DisposeRgn ($A8D9)
            // PROCEDURE DisposeRgn(rgn: RgnHandle);
            // DisposeRgn ($A8D9): Frees region data + handle via bus.free()
            (true, 0x0D9) => {
                let sp = cpu.read_reg(Register::A7);
                let rgn_handle = bus.read_long(sp);
                if rgn_handle != 0 {
                    let rgn_ptr = bus.read_long(rgn_handle);
                    bus.free(rgn_ptr);
                    bus.free(rgn_handle);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // CopyRgn ($A8DC)
            // PROCEDURE CopyRgn(srcRgn, dstRgn: RgnHandle);
            // CopyRgn ($A8DC): Copies the full region structure by value
            (true, 0x0DC) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_handle = bus.read_long(sp);
                let src_handle = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                if src_handle != 0 && dst_handle != 0 {
                    let src_ptr = bus.read_long(src_handle);
                    if src_ptr != 0 {
                        let size = bus.read_word(src_ptr) as u32;
                        let Some(dst_ptr) = Self::ensure_region_capacity(bus, dst_handle, size)
                        else {
                            return Some(Ok(()));
                        };
                        for i in 0..size {
                            bus.write_byte(dst_ptr + i, bus.read_byte(src_ptr + i));
                        }
                    }
                }
                Ok(())
            }

            // ========== Text Attributes ==========

            // TextFont ($A887)
            // Sets the font number for text drawing in the current GrafPort.
            // PROCEDURE TextFont(font: INTEGER);
            // Inside Macintosh Volume I, I-171
            // TextFont ($A887): Sets font ID
            (true, 0x087) => {
                let sp = cpu.read_reg(Register::A7);
                self.tx_font = bus.read_word(sp) as i16;
                cpu.write_reg(Register::A7, sp + 2);
                self.sync_current_port_draw_state(bus);
                Ok(())
            }

            // TextFace ($A888)
            // Sets the character style for text drawing in the current GrafPort.
            // PROCEDURE TextFace(face: Style);
            // Inside Macintosh Volume I, I-171
            // TextFace ($A888): Sets style (bold, italic, etc.)
            (true, 0x088) => {
                let sp = cpu.read_reg(Register::A7);
                self.tx_face = decode_text_face_style(bus.read_word(sp));
                cpu.write_reg(Register::A7, sp + 2);
                self.sync_current_port_draw_state(bus);
                Ok(())
            }

            // TextMode ($A889)
            // Sets the transfer mode for text drawing in the current GrafPort.
            // PROCEDURE TextMode(mode: INTEGER);
            // Inside Macintosh Volume I, I-171
            // TextMode ($A889): Sets transfer mode
            (true, 0x089) => {
                let sp = cpu.read_reg(Register::A7);
                self.tx_mode = bus.read_word(sp) as i16;
                cpu.write_reg(Register::A7, sp + 2);
                self.sync_current_port_draw_state(bus);
                Ok(())
            }

            // TextSize ($A88A)
            // Sets the type size for text drawing in the current GrafPort.
            // PROCEDURE TextSize(size: INTEGER);
            // Inside Macintosh Volume I, I-171
            // TextSize ($A88A): Sets point size
            (true, 0x08A) => {
                let sp = cpu.read_reg(Register::A7);
                self.tx_size = bus.read_word(sp) as i16;
                cpu.write_reg(Register::A7, sp + 2);
                self.sync_current_port_draw_state(bus);
                Ok(())
            }

            // InitFonts ($A8FE)
            // Per IM:I I-219: "InitFonts initializes the Font
            // Manager. If the system font isn't already in memory,
            // InitFonts reads it into memory. Call this procedure
            // once before all other Font Manager routines or any
            // Toolbox routine that will call the Font Manager."
            // PROCEDURE InitFonts;
            // Inside Macintosh Volume I, I-219
            //
            // No args, no result. Pop 0 bytes.
            //
            // HLE compromise: Systemless doesn't load FOND/FONT/NFNT
            // resources — text drawing goes through fixed Rust
            // glyph tables in trap/font_table.rs (Chicago / Geneva
            // / Monaco / system-font 9pt-12pt-9pt set baked at
            // build time). The "if system font isn't already in
            // memory, read it in" path is unreachable because the
            // font table is statically compiled. No-op is correct.
            // The Font Manager trio $A901 FMSwapFont / $A902
            // RealFont / $A903 SetFontLock (all Partial) carries the same HLE
            // rationale — see toolbox.rs:2922+ family rationale
            // block for the cross-trap design coherence.
            // InitFonts ($A8FE): No args / no result per IM:I I-219 PROCEDURE sig; Font Mgr state is statically baked into trap/font_table.rs Rust glyph tables — no FOND/FONT/NFNT resource loading needed at runtime. Same HLE rationale as the $A901/$A902/$A903 Font Manager trio (see toolbox.rs:2922+ family block). Idempotent — repeated calls are no-ops.
            (true, 0x0FE) => Ok(()),

            // GetFontInfo ($A88B)
            // Returns font information for the current GrafPort's font settings.
            // PROCEDURE GetFontInfo(VAR info: FontInfo);
            // Inside Macintosh Volume I, I-173
            // GetFontInfo ($A88B): Returns ascent, descent, widMax, leading based on tx_size
            (true, 0x08B) => {
                let sp = cpu.read_reg(Register::A7);
                let info_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let (face, scale) = get_font_face_scaled(self.tx_font, self.tx_size);
                let metrics = face.metrics;
                bus.write_word(info_ptr, (metrics.ascent * scale) as u16);
                bus.write_word(info_ptr + 2, (metrics.descent * scale) as u16);
                bus.write_word(info_ptr + 4, (metrics.wid_max * scale) as u16);
                bus.write_word(info_ptr + 6, (metrics.leading * scale) as u16);
                Ok(())
            }

            // StringWidth ($A88C)
            // Returns the width of the given string in pixels.
            // FUNCTION StringWidth(s: Str255): INTEGER;
            // Inside Macintosh Volume I, I-173
            // StringWidth ($A88C): Returns pixel width of Pascal string
            (true, 0x08C) => {
                let sp = cpu.read_reg(Register::A7);
                let str_ptr = bus.read_long(sp);
                let (_face, scale) = get_font_face_scaled(self.tx_font, self.tx_size);
                let mut total_width = 0i16;
                let len = bus.read_byte(str_ptr) as u16;
                let start_addr = str_ptr + 1;
                for i in 0..len {
                    let ch = bus.read_byte(start_addr + i as u32) as char;
                    let advance = if let Some((g, _)) = get_glyph(self.tx_font, self.tx_size, ch) {
                        self.glyph_advance(g) * scale
                    } else {
                        self.missing_glyph_advance() * scale
                    };
                    total_width += advance;
                }
                let new_sp = sp + 4;
                bus.write_word(new_sp, total_width as u16);
                cpu.write_reg(Register::A7, new_sp);
                Ok(())
            }

            // CharWidth ($A88D)
            // Returns the width of the given character in pixels.
            // FUNCTION CharWidth(ch: CHAR): INTEGER;
            // Inside Macintosh Volume I, I-173
            // CharWidth ($A88D): Returns pixel width of character
            (true, 0x08D) => {
                let sp = cpu.read_reg(Register::A7);
                let ch = (bus.read_word(sp) & 0xFF) as u8 as char;
                let (_face, cw_scale) = get_font_face_scaled(self.tx_font, self.tx_size);
                let advance = if let Some((g, _)) = get_glyph(self.tx_font, self.tx_size, ch) {
                    self.glyph_advance(g) * cw_scale
                } else {
                    self.missing_glyph_advance() * cw_scale
                };
                bus.write_word(sp + 2, advance as u16);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // TextWidth ($A886)
            // Returns the width of the specified text in pixels.
            // FUNCTION TextWidth(textBuf: Ptr; firstByte, byteCount: INTEGER): INTEGER;
            // Inside Macintosh Volume I, I-173
            // TextWidth ($A886): Returns width of text buffer
            (true, 0x086) => {
                let sp = cpu.read_reg(Register::A7);
                let byte_count = bus.read_word(sp) as i16;
                let first_byte = bus.read_word(sp + 2) as i16;
                let text_buf = bus.read_long(sp + 4);
                let (_face, tw_scale) = get_font_face_scaled(self.tx_font, self.tx_size);
                let mut total_width = 0i16;
                let start_addr = text_buf + first_byte as u32;
                for i in 0..byte_count {
                    let ch = bus.read_byte(start_addr + i as u32) as char;
                    let advance = if let Some((g, _)) = get_glyph(self.tx_font, self.tx_size, ch) {
                        self.glyph_advance(g) * tw_scale
                    } else {
                        self.missing_glyph_advance() * tw_scale
                    };
                    total_width += advance;
                }
                let new_sp = sp + 8;
                bus.write_word(new_sp, total_width as u16);
                cpu.write_reg(Register::A7, new_sp);
                Ok(())
            }

            // ========== Text Drawing ==========

            // DrawChar ($A883)
            // Draws the specified character at the current pen location.
            // PROCEDURE DrawChar(ch: CHAR);
            // Inside Macintosh Volume I, I-172
            // DrawChar ($A883): Renders character to framebuffer using built-in font
            (true, 0x083) => {
                let sp = cpu.read_reg(Register::A7);
                let ch = bus.read_word(sp) as u8 as char;
                cpu.write_reg(Register::A7, sp + 2);
                if trace_dialog_text_enabled() {
                    eprintln!(
                        "[DIALOG-TEXT] DrawChar current_port=${:08X} pnLoc=({}, {}) fg=({:04X},{:04X},{:04X}) bg=({:04X},{:04X},{:04X}) mode={} ch={:?}",
                        self.current_port,
                        self.pn_loc.0,
                        self.pn_loc.1,
                        self.fg_color.0,
                        self.fg_color.1,
                        self.fg_color.2,
                        self.bg_color.0,
                        self.bg_color.1,
                        self.bg_color.2,
                        self.tx_mode,
                        ch,
                    );
                }
                self.draw_char(cpu, bus, ch);
                self.refresh_visible_dialog_snapshot_for_port(bus, self.current_port);
                Ok(())
            }

            // DrawString ($A884)
            // Draws the specified string at the current pen location.
            // PROCEDURE DrawString(s: Str255);
            // Inside Macintosh Volume I, I-172
            // DrawString ($A884): Renders Pascal string to framebuffer
            (true, 0x084) => {
                let sp = cpu.read_reg(Register::A7);
                let str_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if trace_dialog_text_enabled() {
                    let pc = cpu.read_reg(Register::PC);
                    let len = bus.read_byte(str_ptr) as usize;
                    let mut bytes = vec![0u8; len];
                    for (i, byte) in bytes.iter_mut().enumerate() {
                        *byte = bus.read_byte(str_ptr + 1 + i as u32);
                    }
                    eprintln!(
                        "[DIALOG-TEXT] DrawString pc=${:08X} current_port=${:08X} pnLoc=({}, {}) fg=({:04X},{:04X},{:04X}) bg=({:04X},{:04X},{:04X}) mode={} text=\"{}\"",
                        pc,
                        self.current_port,
                        self.pn_loc.0,
                        self.pn_loc.1,
                        self.fg_color.0,
                        self.fg_color.1,
                        self.fg_color.2,
                        self.bg_color.0,
                        self.bg_color.1,
                        self.bg_color.2,
                        self.tx_mode,
                        String::from_utf8_lossy(&bytes),
                    );
                }
                self.draw_string(cpu, bus, str_ptr);
                self.refresh_visible_dialog_snapshot_for_port(bus, self.current_port);
                Ok(())
            }

            // DrawText ($A885)
            // Draws text from an arbitrary buffer starting at firstByte for byteCount bytes.
            // PROCEDURE DrawText(textBuf: Ptr; firstByte: INTEGER; byteCount: INTEGER);
            // Inside Macintosh Volume I, I-172
            // DrawText ($A885): Renders text buffer to framebuffer
            (true, 0x085) => {
                let sp = cpu.read_reg(Register::A7);
                let byte_count = bus.read_word(sp) as i16;
                let first_byte = bus.read_word(sp + 2) as i16;
                let text_buf = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                let start = text_buf + first_byte as u32;
                if trace_dialog_text_enabled() {
                    let pc = cpu.read_reg(Register::PC);
                    let safe_count = byte_count.max(0) as usize;
                    let mut bytes = vec![0u8; safe_count];
                    for (i, byte) in bytes.iter_mut().enumerate() {
                        *byte = bus.read_byte(start + i as u32);
                    }
                    eprintln!(
                        "[DIALOG-TEXT] DrawText pc=${:08X} current_port=${:08X} pnLoc=({}, {}) fg=({:04X},{:04X},{:04X}) bg=({:04X},{:04X},{:04X}) mode={} text=\"{}\"",
                        pc,
                        self.current_port,
                        self.pn_loc.0,
                        self.pn_loc.1,
                        self.fg_color.0,
                        self.fg_color.1,
                        self.fg_color.2,
                        self.bg_color.0,
                        self.bg_color.1,
                        self.bg_color.2,
                        self.tx_mode,
                        String::from_utf8_lossy(&bytes),
                    );
                }
                for i in 0..byte_count {
                    let ch = bus.read_byte(start + i as u32) as char;
                    self.draw_char(cpu, bus, ch);
                }
                self.refresh_visible_dialog_snapshot_for_port(bus, self.current_port);
                Ok(())
            }

            // StdText ($A882)
            // Low-level text rendering bottleneck.
            // PROCEDURE StdText(byteCount: INTEGER; textBuf: Ptr; numer, denom: Point);
            // Inside Macintosh Volume I, I-198
            // StdText ($A882): Parses all 5 parameters, applies uniform scaling
            // to the rendered text path, and processes text for rendering.
            (true, 0x082) => {
                let sp = cpu.read_reg(Register::A7);
                // Pascal calling convention: rightmost param at lowest address
                let _denom_v = bus.read_word(sp) as i16;
                let _denom_h = bus.read_word(sp + 2) as i16;
                let _numer_v = bus.read_word(sp + 4) as i16;
                let _numer_h = bus.read_word(sp + 6) as i16;
                let text_buf = bus.read_long(sp + 8);
                let byte_count = bus.read_word(sp + 12) as i16;
                cpu.write_reg(Register::A7, sp + 14);

                let saved_tx_size = self.tx_size;
                if _numer_h > 0 && _denom_h > 0 {
                    let scale_num = i32::from(_numer_h);
                    let scale_den = i32::from(_denom_h);
                    let scaled =
                        ((i32::from(self.tx_size) * scale_num) + (scale_den / 2)) / scale_den;
                    self.tx_size = scaled.clamp(1, i32::from(i16::MAX)) as i16;
                }

                if trace_dialog_text_enabled() {
                    let pc = cpu.read_reg(Register::PC);
                    let safe_count = byte_count.max(0) as usize;
                    let mut bytes = vec![0u8; safe_count];
                    for (i, byte) in bytes.iter_mut().enumerate() {
                        *byte = bus.read_byte(text_buf + i as u32);
                    }
                    eprintln!(
                        "[DIALOG-TEXT] StdText pc=${:08X} current_port=${:08X} pnLoc=({}, {}) fg=({:04X},{:04X},{:04X}) bg=({:04X},{:04X},{:04X}) mode={} numer=({}, {}) denom=({}, {}) text=\"{}\"",
                        pc,
                        self.current_port,
                        self.pn_loc.0,
                        self.pn_loc.1,
                        self.fg_color.0,
                        self.fg_color.1,
                        self.fg_color.2,
                        self.bg_color.0,
                        self.bg_color.1,
                        self.bg_color.2,
                        self.tx_mode,
                        _numer_v,
                        _numer_h,
                        _denom_v,
                        _denom_h,
                        String::from_utf8_lossy(&bytes),
                    );
                }

                // Render text using draw_char
                for i in 0..byte_count {
                    let ch = bus.read_byte(text_buf + i as u32) as char;
                    self.draw_char(cpu, bus, ch);
                }
                self.tx_size = saved_tx_size;
                self.refresh_visible_dialog_snapshot_for_port(bus, self.current_port);
                Ok(())
            }

            // SetStdProcs ($A8EA)
            // PROCEDURE SetStdProcs(VAR procs: QDProcs);
            // Inside Macintosh Volume I, I-197
            // Imaging With QuickDraw 1994, 3-130
            //
            // Fills the 13-field QDProcs record with pointers to the standard
            // low-level QuickDraw bottleneck routines. Real ROM writes actual
            // ROM code addresses; Systemless writes synthesized $00F0AXXX fake
            // addresses matching what `GetTrapAddress` returns for each
            // `Std*` trap (memory.rs:340..372 — the canonical reverse-encoding
            // for OS-trap fake-ptrs). Apps that only compare the field against
            // their own installed routine (the common idiom for "is this still
            // the default bottleneck?") see a stable non-NIL marker; apps that
            // actually JSR to the field would land on the fake address, which
            // faults clean if reached. The previous trap word for commentProc
            // was incorrectly $A89F (`_Unimplemented` per IM:VI 31831 + the
            // existing $A89F arm at quickdraw.rs:10287); the canonical trap is
            // $A8F1 `StdComment` per IM:I I-198 + the existing $A8F1 arm at
            // quickdraw.rs:7659.
            // SetStdProcs ($A8EA): Writes 13 synthesized $00F0AXXX ProcPtr markers per QDProcs field order; markers match GetTrapAddress($A0XX) reverse encoding so apps comparing slot vs GetTrapAddress(_StdXxx) detect the default per IM:I I-197 + Imaging With QD 3-130
            (true, 0x0EA) => {
                let sp = cpu.read_reg(Register::A7);
                let procs_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                // Trap words for the 13 standard QuickDraw bottlenecks,
                // in QDProcs field order (IM:I I-197 + Imaging With QD 1994
                // 3-39 layout):
                //   textProc, lineProc, rectProc, rRectProc, ovalProc,
                //   arcProc, polyProc, rgnProc, bitsProc, commentProc,
                //   txMeasProc, getPicProc, putPicProc
                const STD_PROC_TRAPS: [u32; 13] = [
                    0xA882, // StdText
                    0xA890, // StdLine
                    0xA8A0, // StdRect
                    0xA8AF, // StdRRect
                    0xA8B6, // StdOval
                    0xA8BD, // StdArc
                    0xA8C5, // StdPoly
                    0xA8D1, // StdRgn
                    0xA8EB, // StdBits
                    0xA8F1, // StdComment (was $A89F — that's _Unimplemented; fixed per IM:I I-198)
                    0xA8ED, // StdTxMeas
                    0xA8EE, // StdGetPic
                    0xA8F0, // StdPutPic
                ];
                if procs_ptr != 0 {
                    for (i, trap) in STD_PROC_TRAPS.iter().enumerate() {
                        bus.write_long(procs_ptr + (i as u32) * 4, 0x00F00000 | trap);
                    }
                }
                Ok(())
            }

            // ========== Rect Operations ==========

            // SetRect ($A8A7)
            // PROCEDURE SetRect (VAR r: Rect; left, top, right, bottom: INTEGER);
            // Inside Macintosh Volume I, I-175
            //
            // Pascal stack layout (12 bytes popped):
            //   sp+0  bottom (last pushed, INTEGER)
            //   sp+2  right             (INTEGER)
            //   sp+4  top               (INTEGER)
            //   sp+6  left              (INTEGER)
            //   sp+8  r-pointer (first pushed, VAR-out 4-byte ptr)
            //
            // Regression coverage: set_rect — 1bpp
            // 520x80 offscreen indicator-band fixture with
            // quality="behavior_state" + the catalogue's
            // `A8A7:rect_set_from_four_scalars` legacy composite
            // assertion. Five bands witness r.top/r.left/r.bottom/
            // r.right after SetRect(&r, 10, 20, 50, 80) → expected
            // (top=20, left=10, bottom=80, right=50) plus a
            // composite "all four match" Band 0. The fixture uses
            // direct C struct assignment for canvas + band Rects
            // (NOT SetRect) so different broken-impl regression
            // classes (top↔bottom swap, left↔right swap, all-zero
            // stub) produce pixel-distinct band subsets rather
            // than collapsing to "blank canvas". A working impl
            // produces 5 black bands; a no-op stub produces an
            // all-white canvas.
            //
            // Additional regression coverage: set_rect_fields
            // — sibling 1bpp 640x80 (rowBytes=80) offscreen
            // indicator-band fixture closing the SIX granular
            // catalogue ids per A8A7_SetRect.toml:
            //   A8A7:left_assigned_to_left_field
            //   A8A7:top_assigned_to_top_field
            //   A8A7:right_assigned_to_right_field
            //   A8A7:bottom_assigned_to_bottom_field
            //   A8A7:negative_coords_preserved
            //   A8A7:inverted_inputs_preserved
            // Three SetRect dispatches witness all six ids in 7
            // bands: r1=(10,20,50,80) for the four field-isolation
            // ids, r2=(-100,-50,-10,-5) for the negative-coords id
            // (impl preserves negatives via i16→u16 sign-extending
            // cast at write_word; no clamping per IM:I-175 silence
            // on the matter), r3=(50,30,10,20) for the inverted-
            // inputs id (impl writes args verbatim; no
            // normalisation per IM:I-175). Brings the catalogue
            // row from 1/7=14% (lowest) to 7/7=100% in one
            // iteration via the multi-id-witnessing single-fixture
            // pattern.
            // SetRect ($A8A7): Pops 12 bytes; writes 4 INTEGER fields to VAR-out r per IM:I-175; no clamping or normalisation; covered by set_rect (legacy composite) + set_rect_fields (6 granular field-isolation + negative + inverted ids)
            (true, 0x0A7) => {
                let sp = cpu.read_reg(Register::A7);
                let bottom = bus.read_word(sp) as i16;
                let right = bus.read_word(sp + 2) as i16;
                let top = bus.read_word(sp + 4) as i16;
                let left = bus.read_word(sp + 6) as i16;
                let rect_ptr = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);
                bus.write_word(rect_ptr, top as u16);
                bus.write_word(rect_ptr + 2, left as u16);
                bus.write_word(rect_ptr + 4, bottom as u16);
                bus.write_word(rect_ptr + 6, right as u16);
                Ok(())
            }

            // InsetRect ($A8A9)
            // PROCEDURE InsetRect (VAR r: Rect; dh, dv: INTEGER);
            // Inside Macintosh Volume I, I-176
            //
            // Regression coverage: inset_rect — 1bpp
            // 200x80 offscreen indicator-band fixture, strengthened
            // from compare="trace" + DrawString to compare="pixmap" +
            // quality="behavior_state" with the catalogue's legacy
            // composite `A8A9:rect_mutated_in_place` assertion. Four
            // bands witness r.top/r.left/r.bottom/r.right after
            // InsetRect((20,10,40,30), 5, 3) → (25,13,35,27).
            //
            // Additional regression coverage:
            // inset_rect_classes — sibling 1bpp 720x80
            // (rowBytes=90) offscreen indicator-band fixture closing
            // the FIVE granular catalogue ids per A8A9_InsetRect.toml:
            //   A8A9:positive_inset_shrinks
            //   A8A9:negative_inset_expands
            //   A8A9:asymmetric_inset
            //   A8A9:zero_inset_no_op
            //   A8A9:collapse_to_inverted_when_2dh_exceeds_width
            // Five InsetRect dispatches witness all five ids in 6
            // bands: r1=(20,10,80,50)+(+5,+10) for positive shrink,
            // r2=(300,100,420,180)+(-10,-5) for negative expand,
            // r3=(30,15,110,85)+(+7,-4) for asymmetric (mixed-sign
            // distinct magnitudes), r4=(350,200,440,270)+(0,0) for
            // zero no-op, r5=(500,300,600,380)+(+60,+10) for the
            // collapse case where 2*dh=120 > width=100. IM:I-176
            // says the collapse result is implementation-defined
            // ("set to (0,0,0,0)"); BasiliskII produces an inverted
            // rect (r.right < r.left) and Systemless's mirror impl
            // matches by writing `(top+dv, left+dh, bottom-dv,
            // right-dh)` with no clamping. The catalogue pins
            // BasiliskII's value via apple_documented_value vs
            // basiliskii_value divergence keys. Brings the catalogue
            // row from 1/6=17% (tied for lowest) to 6/6=100% in one
            // iteration via the multi-id-witnessing single-fixture
            // pattern.
            // InsetRect ($A8A9): Pops 8 bytes; writes (top+dv, left+dh, bottom-dv, right-dh) per IM:I-176; no clamping on collapse-case overflow (Systemless mirrors BasiliskII semantics, both diverge from Apple's literal "set to (0,0,0,0)" wording); covered by inset_rect (legacy composite) + inset_rect_classes (5 granular per-class ids including BasiliskII-pinned collapse-to-inverted)
            (true, 0x0A9) => {
                let sp = cpu.read_reg(Register::A7);
                let dv = bus.read_word(sp) as i16;
                let dh = bus.read_word(sp + 2) as i16;
                let rect_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                let top = bus.read_word(rect_ptr) as i16;
                let left = bus.read_word(rect_ptr + 2) as i16;
                let bottom = bus.read_word(rect_ptr + 4) as i16;
                let right = bus.read_word(rect_ptr + 6) as i16;
                bus.write_word(rect_ptr, top.wrapping_add(dv) as u16);
                bus.write_word(rect_ptr + 2, left.wrapping_add(dh) as u16);
                bus.write_word(rect_ptr + 4, bottom.wrapping_sub(dv) as u16);
                bus.write_word(rect_ptr + 6, right.wrapping_sub(dh) as u16);
                Ok(())
            }

            // OffsetRect ($A8A8)
            // PROCEDURE OffsetRect (VAR r: Rect; dh, dv: INTEGER);
            // Inside Macintosh Volume I, I-174
            //
            // Regression coverage: offset_rect — 1bpp
            // 240x80 offscreen indicator-band fixture with
            // quality="behavior_state" + the catalogue's legacy
            // composite `A8A8:rect_translated_in_place` assertion.
            //
            // Additional regression coverage:
            // offset_rect_translation — sibling 1bpp
            // 680x80 (rowBytes=85) offscreen indicator-band fixture
            // closing the SIX granular catalogue ids per
            // A8A8_OffsetRect.toml:
            //   A8A8:positive_dh_dv_translates
            //   A8A8:negative_dh_dv_translates
            //   A8A8:asymmetric_translation
            //   A8A8:zero_translation_no_op
            //   A8A8:width_height_preserved_under_translation
            //   A8A8:sequential_calls_compose_translations
            // Seven OffsetRect dispatches witness all six ids in 6
            // bands: r1=(20,10,80,50)+(+5,+10) for positive;
            // r2=(200,100,280,170)+(-3,-8) for negative;
            // r3=(25,15,90,60)+(+7,-4) for asymmetric;
            // r4=(300,200,400,250)+(0,0) for zero-translation;
            // r5=(400,500,700,600)+(+123,-67) for the width/height
            // preservation invariant — r5's specific post-call
            // field values are NOT pinned by any other band so B5
            // gives independent regression detection (a regression
            // that leaks the InsetRect formula breaks B5 because
            // (right - dh) - (left + dh) shrinks by 2*dh, breaking
            // the width-preservation invariant). Brings the
            // catalogue row from 1/7=14% to 7/7=100% via the
            // multi-id-witnessing single-fixture pattern.
            // OffsetRect ($A8A8): Pops 8 bytes; mutates VAR-out r in place per IM:I-174 (top+dv, left+dh, bottom+dv, right+dh); covered by offset_rect (legacy composite) + offset_rect_translation (6 granular ids witnessing per-sign translation classes + zero no-op + width/height preservation invariant + sequential-call composition)
            (true, 0x0A8) => {
                let sp = cpu.read_reg(Register::A7);
                let dv = bus.read_word(sp) as i16;
                let dh = bus.read_word(sp + 2) as i16;
                let rect_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                let top = bus.read_word(rect_ptr) as i16;
                let left = bus.read_word(rect_ptr + 2) as i16;
                let bottom = bus.read_word(rect_ptr + 4) as i16;
                let right = bus.read_word(rect_ptr + 6) as i16;
                bus.write_word(rect_ptr, (top + dv) as u16);
                bus.write_word(rect_ptr + 2, (left + dh) as u16);
                bus.write_word(rect_ptr + 4, (bottom + dv) as u16);
                bus.write_word(rect_ptr + 6, (right + dh) as u16);
                Ok(())
            }

            // EmptyRect ($A8AE)
            // FUNCTION EmptyRect (r: Rect) : BOOLEAN;
            // Inside Macintosh Volume I, I-176
            //
            // Per IM:I-176: "EmptyRect returns TRUE if the given
            // rectangle is an empty rectangle or FALSE if not. A
            // rectangle is considered empty if the bottom
            // coordinate is less than or equal to the top or the
            // right coordinate is less than or equal to the left."
            //
            // The simplest possible Pascal FUNCTION shape in the
            // Rect-Ops family — 1 by-value Rect input passed by
            // 4-byte const-pointer per MPW pascal calling
            // convention (since 8-byte structs exceed the
            // direct-push threshold) -> 4 bytes of args popped ->
            // result slot at sp+4. The Boolean uses Mac Pascal
            // encoding (TRUE = $0100 at the high half of the
            // 16-bit slot, FALSE = $0000).
            //
            // Note the inclusive-equal semantics — `bottom <= top`
            // OR `right <= left`, NOT strict `<`. A Rect with
            // bottom == top is empty even though every coordinate
            // field is finite.
            //
            // Regression coverage:
            //   * equal_empty_rect — 1bpp 440x80
            //     offscreen behavior_state golden witnessing the
            //     EmptyRect contract on (0,0,0,0) -> TRUE plus a
            //     non-empty (10,5,50,40) -> FALSE, paired with
            //     EqualRect ($A8A6) tests in one bake. Per
            //     IM:I-176; baked against System 7.5.3 + real Mac
            //     ROM under BasiliskII. The paired fixture covers
            //     the legacy composite catalogue id.
            //   * empty_rect_predicates — 1bpp 760x80
            //     offscreen behavior_state golden witnessing FIVE
            //     net-new granular catalogue ids in one bake:
            //     `A8AE:zero_rect_is_empty` (r=(0,0,0,0) -> TRUE),
            //     `A8AE:nonzero_rect_is_not_empty`
            //       (r=(20,10,120,60) -> FALSE),
            //     `A8AE:degenerate_horizontal_line_is_empty`
            //       (r=(40,10,40,60) right==left -> TRUE),
            //     `A8AE:degenerate_vertical_line_is_empty`
            //       (r=(20,30,120,30) bottom==top -> TRUE),
            //     `A8AE:inverted_rect_is_empty`
            //       (r=(120,80,10,20) strictly inverted -> TRUE).
            //     Six indicator bands at 1 composite + 5 single-
            //     witness; pixel-distinct from any
            //     stub-returns-0/stub-returns-1/strict-< /
            //     AND-instead-of-OR regression class. Per
            //     IM:I-176's inclusive-`<=` semantics; r_horiz +
            //     r_vert specifically pin the equal-only branches
            //     while r_inv pins the strict-inequality branch
            //     so the strict-<-vs-<= regression class is
            //     pixel-distinguishable from the AND-instead-of-
            //     OR regression class.
            //
            // EmptyRect ($A8AE): goldens equal_empty_rect + empty_rect_predicates (IM:I-176)
            (true, 0x0AE) => {
                let sp = cpu.read_reg(Register::A7);
                let rect_ptr = bus.read_long(sp);
                let top = bus.read_word(rect_ptr) as i16;
                let left = bus.read_word(rect_ptr + 2) as i16;
                let bottom = bus.read_word(rect_ptr + 4) as i16;
                let right = bus.read_word(rect_ptr + 6) as i16;
                let is_empty = (bottom <= top) || (right <= left);
                // Mac Pascal BOOLEAN: TRUE = $0100 (byte 1 in high byte of word)
                bus.write_word(sp + 4, if is_empty { 0x0100 } else { 0 });
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // PtInRect ($A8AD)
            // FUNCTION PtInRect(pt: Point; r: Rect): BOOLEAN;
            // Inside Macintosh Volume I, I-175
            // Imaging With QuickDraw 1994, p. 2-124
            //
            // Per IM:I-175: "PtInRect determines whether the
            // pixel below and to the right of the given coordinate
            // point is enclosed in the specified rectangle, and
            // returns TRUE if so or FALSE if not."
            //
            // The QuickDraw "pixel below and to the right"
            // semantics mean a Rect (left, top, right, bottom)
            // names the half-open pixel range [left, right) x
            // [top, bottom): PtInRect((r.left, r.top), r) is TRUE
            // while PtInRect((r.right, r.bottom), r) is FALSE —
            // the right and bottom edges are EXCLUSIVE.
            //
            // Pascal calling convention: by-value Point passed as
            // 4-byte struct at sp+4 (.v at +0, .h at +2 per
            // IM:I-141), by-value Rect passed by 4-byte pointer
            // at sp+0 (8-byte Rect struct too large for direct
            // push). FUNCTION result Boolean encoded at sp+8 above
            // the popped 8 bytes of args, as Mac Pascal Boolean
            // (TRUE = $0100, FALSE = $0000).
            //
            // Regression coverage:
            //   * pt_in_rect — 1bpp 400x80 offscreen
            //     behavior_state golden witnessing 3 distinct
            //     input classes — clearly-inside (TRUE),
            //     clearly-outside (FALSE), right-boundary (FALSE
            //     per the exclusive-right semantics) — across 4
            //     indicator bands. The 4-band layout produces 4
            //     pixel-distinct outputs across {working impl,
            //     stub returning 0, stub returning 1, off-by-one
            //     boundary bug treating `pt.h <= r.right` as
            //     inclusive}, so any of those regression classes
            //     fails the byte-exact golden gate. Witnesses
            //     the legacy composite catalogue identifier
            //     `A8AD:point_in_rect_predicate`.
            //   * pt_in_rect_edges — 1bpp 560x80
            //     offscreen behavior_state golden enumerating the
            //     seven granular catalogue identifiers across 8
            //     indicator bands (one composite + seven single-
            //     case): inside_returns_true (test1=TRUE),
            //     outside_returns_false (test2=FALSE),
            //     right_edge_exclusive (test3 with pt.h==r.right
            //     -> FALSE), bottom_edge_exclusive (test4 with
            //     pt.v==r.bottom -> FALSE), left_edge_inclusive
            //     (test5 with pt.h==r.left -> TRUE),
            //     top_edge_inclusive (test6 with pt.v==r.top ->
            //     TRUE), empty_rect_returns_false (test7 against
            //     a degenerate (20,20,20,20) rect -> FALSE per
            //     IM:I-176 EmptyRect predicate). Each edge test
            //     isolates ONE boundary at a time; a regression
            //     that flips ONE edge's inclusivity flips exactly
            //     one band, so this fixture pins each edge's
            //     half-open semantics independently.
            //
            // PtInRect ($A8AD): pt_in_rect + pt_in_rect_edges (IM:I-175 + I-176)
            (true, 0x0AD) => {
                let sp = cpu.read_reg(Register::A7);
                let rect_ptr = bus.read_long(sp);
                let pt_v = bus.read_word(sp + 4) as i16;
                let pt_h = bus.read_word(sp + 6) as i16;
                let top = bus.read_word(rect_ptr) as i16;
                let left = bus.read_word(rect_ptr + 2) as i16;
                let bottom = bus.read_word(rect_ptr + 4) as i16;
                let right = bus.read_word(rect_ptr + 6) as i16;
                let in_rect = pt_v >= top && pt_v < bottom && pt_h >= left && pt_h < right;
                // Mac Pascal BOOLEAN: TRUE = $0100 (byte 1 in high byte of word)
                bus.write_word(sp + 8, if in_rect { 0x0100 } else { 0 });
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // SectRect ($A8AA)
            // FUNCTION SectRect (src1, src2: Rect; VAR dstRect: Rect): BOOLEAN;
            // Inside Macintosh Volume I, I-175
            //
            // Per IM:I-175: "SectRect calculates the rectangle
            // that's the intersection of the two given
            // rectangles, and returns TRUE if they indeed
            // intersect or FALSE if they don't." For non-empty
            // overlapping operands the four dstRect fields are
            // independently max/min-selected from the source
            // Rects:
            //   dst.top    = max(src1.top,    src2.top)
            //   dst.left   = max(src1.left,   src2.left)
            //   dst.bottom = min(src1.bottom, src2.bottom)
            //   dst.right  = min(src1.right,  src2.right)
            // and the FUNCTION result is TRUE iff the resulting
            // Rect is non-empty (top < bottom AND left < right).
            // Per IM:I-175 the empty-result case clamps dst to
            // (0, 0, 0, 0) and returns FALSE.
            //
            // Regression coverage:
            //   * sect_rect — 1bpp 360x80 offscreen
            //     behavior_state golden witnessing all four
            //     output fields plus the FUNCTION result Boolean
            //     after SectRect((5,10,30,25),(15,20,40,35),
            //     &dst) -> dst = (15, 20, 30, 25), Boolean =
            //     TRUE, against poison-prefilled 0x7FFF dst. The
            //     two source Rects overlap on a 10x5 region so
            //     neither the empty-result short-circuit nor
            //     the IM:I-175 "touch at a line or point"
            //     boundary case fires; the standard max/min
            //     arithmetic path runs.
            //
            // SectRect ($A8AA)
            (true, 0x0AA) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_ptr = bus.read_long(sp);
                let src2_ptr = bus.read_long(sp + 4);
                let src1_ptr = bus.read_long(sp + 8);
                let s1_top = bus.read_word(src1_ptr) as i16;
                let s1_left = bus.read_word(src1_ptr + 2) as i16;
                let s1_bottom = bus.read_word(src1_ptr + 4) as i16;
                let s1_right = bus.read_word(src1_ptr + 6) as i16;
                let s2_top = bus.read_word(src2_ptr) as i16;
                let s2_left = bus.read_word(src2_ptr + 2) as i16;
                let s2_bottom = bus.read_word(src2_ptr + 4) as i16;
                let s2_right = bus.read_word(src2_ptr + 6) as i16;
                let d_top = s1_top.max(s2_top);
                let d_left = s1_left.max(s2_left);
                let d_bottom = s1_bottom.min(s2_bottom);
                let d_right = s1_right.min(s2_right);
                let intersects = d_top < d_bottom && d_left < d_right;
                if intersects {
                    bus.write_word(dst_ptr, d_top as u16);
                    bus.write_word(dst_ptr + 2, d_left as u16);
                    bus.write_word(dst_ptr + 4, d_bottom as u16);
                    bus.write_word(dst_ptr + 6, d_right as u16);
                } else {
                    bus.write_long(dst_ptr, 0);
                    bus.write_long(dst_ptr + 4, 0);
                }
                // Mac Pascal BOOLEAN: TRUE = $0100 (byte 1 in high byte of word)
                bus.write_word(sp + 12, if intersects { 0x0100 } else { 0 });
                cpu.write_reg(Register::A7, sp + 12);
                Ok(())
            }

            // UnionRect ($A8AB)
            // PROCEDURE UnionRect (src1, src2: Rect; VAR dstRect: Rect);
            // Inside Macintosh Volume I, I-175
            //
            // Per IM:I-175: "UnionRect calculates the smallest
            // rectangle that encloses both of the given rectangles."
            // For non-empty operands the four dstRect fields are
            // independently min/max-selected from the corresponding
            // corner of the two source Rects:
            //   dst.top    = min(src1.top,    src2.top)
            //   dst.left   = min(src1.left,   src2.left)
            //   dst.bottom = max(src1.bottom, src2.bottom)
            //   dst.right  = max(src1.right,  src2.right)
            //
            // Per IM:I-176, if EITHER source is empty (bottom <=
            // top OR right <= left) the destination receives the
            // OTHER (non-empty) rectangle; if BOTH are empty the
            // destination is set to (0, 0, 0, 0).
            //
            // Regression coverage:
            //   * union_rect — 1bpp 320x80 offscreen
            //     behavior_state golden witnessing all four output
            //     fields after UnionRect((10,5,40,25),(20,30,60,50),
            //     &dst) -> dst = (10, 5, 60, 50), against
            //     poison-prefilled 0x7FFF. Both source Rects are
            //     non-empty so the IM:I-176 empty-source branch
            //     is NOT exercised — the standard min/max
            //     arithmetic path runs. Each output field is
            //     selected from a specific operand: top from src1
            //     (smaller), left from src1 (smaller), bottom
            //     from src2 (larger), right from src2 (larger).
            //
            // UnionRect ($A8AB)
            (true, 0x0AB) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_ptr = bus.read_long(sp);
                let src2_ptr = bus.read_long(sp + 4);
                let src1_ptr = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);
                let s1_top = bus.read_word(src1_ptr) as i16;
                let s1_left = bus.read_word(src1_ptr + 2) as i16;
                let s1_bottom = bus.read_word(src1_ptr + 4) as i16;
                let s1_right = bus.read_word(src1_ptr + 6) as i16;
                let s2_top = bus.read_word(src2_ptr) as i16;
                let s2_left = bus.read_word(src2_ptr + 2) as i16;
                let s2_bottom = bus.read_word(src2_ptr + 4) as i16;
                let s2_right = bus.read_word(src2_ptr + 6) as i16;
                let s1_empty = s1_bottom <= s1_top || s1_right <= s1_left;
                let s2_empty = s2_bottom <= s2_top || s2_right <= s2_left;
                let (d_top, d_left, d_bottom, d_right) = if s1_empty && s2_empty {
                    (0, 0, 0, 0)
                } else if s1_empty {
                    (s2_top, s2_left, s2_bottom, s2_right)
                } else if s2_empty {
                    (s1_top, s1_left, s1_bottom, s1_right)
                } else {
                    (
                        s1_top.min(s2_top),
                        s1_left.min(s2_left),
                        s1_bottom.max(s2_bottom),
                        s1_right.max(s2_right),
                    )
                };
                bus.write_word(dst_ptr, d_top as u16);
                bus.write_word(dst_ptr + 2, d_left as u16);
                bus.write_word(dst_ptr + 4, d_bottom as u16);
                bus.write_word(dst_ptr + 6, d_right as u16);
                Ok(())
            }

            // EqualRect ($A8A6)
            // FUNCTION EqualRect (rect1, rect2: Rect) : BOOLEAN;
            // Inside Macintosh Volume I, I-176
            //
            // Per IM:I-176: "EqualRect compares the two given
            // rectangles and returns TRUE if they're equal or
            // FALSE if not. The two rectangles must have identical
            // boundary coordinates to be considered equal."
            //
            // Pascal calling convention: both rect1 and rect2 are
            // passed by 4-byte const-pointer (MPW pascal pragma
            // marshals 8-byte structs as const-pointers since they
            // exceed the direct-push threshold). Args layout: r2
            // at sp+0, r1 at sp+4 (Pascal pushes left-to-right
            // with the stack growing down, so r1 lands at the
            // higher offset). Result: Mac Pascal Boolean at sp+8
            // (TRUE = $0100 at the high half of the word,
            // FALSE = $0000) with A7 advanced by 8 bytes of args.
            //
            // Regression coverage:
            //   * equal_empty_rect — 1bpp 440x80
            //     offscreen behavior_state golden witnessing the
            //     legacy composite `A8A6:rect_equality_predicate`
            //     identifier on identical-rect (TRUE) plus all-
            //     fields-differ (FALSE) inputs, paired with
            //     EmptyRect ($A8AE) tests in one bake.
            //   * equal_rect_fields — 1bpp 600x80
            //     offscreen behavior_state golden witnessing six
            //     granular catalogue assertion ids per
            //     A8A6_EqualRect.toml: identical_rects_return_true
            //     + differing_top/left/bottom/right_returns_false
            //     (each isolating one Rect field at a time) +
            //     two_distinct_empty_rects_compare_unequal (pinning
            //     the documented "EqualRect compares fields
            //     literally; no empty-rect normalisation" corner).
            //     Seven indicator bands (1 composite + 6 single-
            //     case). Per IM:I-176; baked against System 7.5.3
            //     + real Mac ROM under BasiliskII.
            //
            // EqualRect ($A8A6): goldens equal_empty_rect + equal_rect_fields (IM:I-176)
            (true, 0x0A6) => {
                let sp = cpu.read_reg(Register::A7);
                let r2_ptr = bus.read_long(sp);
                let r1_ptr = bus.read_long(sp + 4);
                let eq = (0..8).all(|i| bus.read_byte(r1_ptr + i) == bus.read_byte(r2_ptr + i));
                // Mac Pascal BOOLEAN: TRUE = $0100 (byte 1 in high byte of word)
                bus.write_word(sp + 8, if eq { 0x0100 } else { 0 });
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // Pt2Rect ($A8AC)
            // PROCEDURE Pt2Rect (pt1, pt2: Point; VAR dstRect: Rect);
            // Inside Macintosh Volume I, I-175
            //
            // Per IM:I-175: "Pt2Rect returns the smallest rectangle
            // that encloses the two given points." The four
            // dstRect fields are independently min/max-selected
            // from the two operands' v / h coordinates:
            //   dst.top    = min(pt1.v, pt2.v)
            //   dst.left   = min(pt1.h, pt2.h)
            //   dst.bottom = max(pt1.v, pt2.v)
            //   dst.right  = max(pt1.h, pt2.h)
            //
            // Regression coverage:
            //   * pt2_rect — 1bpp 280x80 offscreen
            //     behavior_state golden witnessing all four
            //     output fields after Pt2Rect((25,60),(90,15),
            //     &dst) -> dst = (15, 25, 60, 90), against
            //     poison-prefilled 0x7FFF. Test inputs are chosen
            //     so each of the four outputs is selected from a
            //     different operand of min/max — top picks pt2.v,
            //     left picks pt1.h, bottom picks pt1.v, right
            //     picks pt2.h.
            // Pt2Rect ($A8AC): min(v),min(h),max(v),max(h) per IM:I-175; pt2_rect golden
            (true, 0x0AC) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_ptr = bus.read_long(sp);
                let pt2_v = bus.read_word(sp + 4) as i16;
                let pt2_h = bus.read_word(sp + 6) as i16;
                let pt1_v = bus.read_word(sp + 8) as i16;
                let pt1_h = bus.read_word(sp + 10) as i16;
                cpu.write_reg(Register::A7, sp + 12);
                bus.write_word(dst_ptr, pt1_v.min(pt2_v) as u16);
                bus.write_word(dst_ptr + 2, pt1_h.min(pt2_h) as u16);
                bus.write_word(dst_ptr + 4, pt1_v.max(pt2_v) as u16);
                bus.write_word(dst_ptr + 6, pt1_h.max(pt2_h) as u16);
                Ok(())
            }

            // ========== Shape Drawing Commands ==========

            // FrameRect ($A8A1)
            // FrameRect ($A8A1): Renders rect outline to framebuffer
            (true, 0x0A1) => {
                let sp = cpu.read_reg(Register::A7);
                let rect_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                self.draw_rect(cpu, bus, &r, ShapeOp::Frame);
                Ok(())
            }

            // PaintRect ($A8A2)
            // PaintRect ($A8A2): Fills with pen pattern
            (true, 0x0A2) => {
                let sp = cpu.read_reg(Register::A7);
                let rect_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                self.draw_rect(cpu, bus, &r, ShapeOp::Paint);
                Ok(())
            }

            // EraseRect ($A8A3)
            // EraseRect ($A8A3): Fills with background pattern
            (true, 0x0A3) => {
                let sp = cpu.read_reg(Register::A7);
                let rect_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let r = read_rect(bus, rect_ptr);
                if trace_qd_colors_enabled() {
                    eprintln!(
                        "[QD-COLOR] EraseRect port=${:08X} rect=({},{}..{},{}) bg=({:04X},{:04X},{:04X}) tick={}",
                        self.current_port,
                        r.top,
                        r.left,
                        r.bottom,
                        r.right,
                        self.bg_color.0,
                        self.bg_color.1,
                        self.bg_color.2,
                        self.tick_count
                    );
                }
                if trace_eraserect_enabled() {
                    let a5 = cpu.read_reg(Register::A5);
                    let global_ptr = bus.read_long(a5);
                    let port = bus.read_long(global_ptr);
                    let port_version = bus.read_word(port.wrapping_add(6));
                    let port_base = if (port_version & 0xC000) != 0 {
                        let pmh = bus.read_long(port.wrapping_add(2));
                        if pmh != 0 {
                            let pm = bus.read_long(pmh);
                            if pm != 0 {
                                bus.read_long(pm)
                            } else {
                                0
                            }
                        } else {
                            0
                        }
                    } else {
                        bus.read_long(port.wrapping_add(2))
                    };
                    eprintln!(
                        "[ERASERECT] rect=({},{}..{},{}) port=${:08X} portBase=${:08X} screenBase=${:08X}",
                        r.top, r.left, r.bottom, r.right, port, port_base, self.screen_mode.0,
                    );
                }
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                self.draw_rect(cpu, bus, &r, ShapeOp::Erase);
                Ok(())
            }

            // InvertRect ($A8A4)
            // InvertRect ($A8A4): XORs pixels
            (true, 0x0A4) => {
                let sp = cpu.read_reg(Register::A7);
                let rect_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                self.draw_rect(cpu, bus, &r, ShapeOp::Invert);
                Ok(())
            }

            // FillRect ($A8A5)
            //
            // The Fill family extends the OpenRgn-recording shim — per
            // IM:I I-184 the fill procedures add to the region being built.
            // FillRect ($A8A5): Fills with specified pattern
            (true, 0x0A5) => {
                let sp = cpu.read_reg(Register::A7);
                let pat_ptr = bus.read_long(sp);
                let rect_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                let mut pat = [0u8; 8];
                for (i, byte) in pat.iter_mut().enumerate() {
                    *byte = bus.read_byte(pat_ptr + i as u32);
                }
                // Apply fill-black override: substitute the QD `black` global
                // pattern with a dithered pattern for games that need it.
                if let Some(override_pat) = self.fill_black_override {
                    if pat == [0xFF; 8] {
                        let a5 = cpu.read_reg(Register::A5);
                        let global_ptr = bus.read_long(a5);
                        if pat_ptr == global_ptr.wrapping_sub(16) {
                            pat = override_pat;
                        }
                    }
                }
                if trace_qd_colors_enabled() {
                    eprintln!(
                        "[QD-COLOR] FillRect port=${:08X} rect=({},{},{},{}) pat={:02X?} fg=({:04X},{:04X},{:04X}) bg=({:04X},{:04X},{:04X}) tick={}",
                        self.current_port,
                        r.top,
                        r.left,
                        r.bottom,
                        r.right,
                        pat,
                        self.fg_color.0,
                        self.fg_color.1,
                        self.fg_color.2,
                        self.bg_color.0,
                        self.bg_color.1,
                        self.bg_color.2,
                        self.tick_count,
                    );
                }
                self.draw_rect(cpu, bus, &r, ShapeOp::Fill(pat));
                Ok(())
            }

            // FrameOval ($A8B7)
            // Draws the outline of the oval that fits inside the specified rectangle.
            // PROCEDURE FrameOval(r: Rect);
            // Inside Macintosh Volume I, I-177
            (true, 0x0B7) => {
                let sp = cpu.read_reg(Register::A7);
                let rect_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                self.draw_oval(cpu, bus, &r, ShapeOp::Frame);
                Ok(())
            }

            // PaintOval ($A8B8)
            // Paints the oval that fits inside the specified rectangle with the pen pattern.
            // PROCEDURE PaintOval(r: Rect);
            // Inside Macintosh Volume I, I-178
            (true, 0x0B8) => {
                let sp = cpu.read_reg(Register::A7);
                let rect_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                self.draw_oval(cpu, bus, &r, ShapeOp::Paint);
                Ok(())
            }

            // EraseOval ($A8B9)
            // Erases the oval that fits inside the specified rectangle with the background pattern.
            // PROCEDURE EraseOval(r: Rect);
            // Inside Macintosh Volume I, I-178
            (true, 0x0B9) => {
                let sp = cpu.read_reg(Register::A7);
                let rect_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                self.draw_oval(cpu, bus, &r, ShapeOp::Erase);
                Ok(())
            }

            // InvertOval ($A8BA)
            // Inverts the pixels of the oval that fits inside the specified rectangle.
            // PROCEDURE InvertOval(r: Rect);
            // Inside Macintosh Volume I, I-178
            (true, 0x0BA) => {
                let sp = cpu.read_reg(Register::A7);
                let rect_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                self.draw_oval(cpu, bus, &r, ShapeOp::Invert);
                Ok(())
            }

            // FillOval ($A8BB)
            // Fills the oval that fits inside the specified rectangle with the given pattern.
            // PROCEDURE FillOval(r: Rect; pat: Pattern);
            // Inside Macintosh Volume I, I-178
            (true, 0x0BB) => {
                let sp = cpu.read_reg(Register::A7);
                let pat_ptr = bus.read_long(sp);
                let rect_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                let mut pat = [0u8; 8];
                for (i, byte) in pat.iter_mut().enumerate() {
                    *byte = bus.read_byte(pat_ptr + i as u32);
                }
                self.draw_oval(cpu, bus, &r, ShapeOp::Fill(pat));
                Ok(())
            }

            // FrameRoundRect ($A8B0)
            // Draws the outline of the specified round-cornered rectangle.
            // PROCEDURE FrameRoundRect(r: Rect; ovalWidth, ovalHeight: INTEGER);
            // Inside Macintosh Volume I, I-178
            //
            // Inside OpenRgn, extend the recording region by the round-rect's
            // outer bounds and suppress drawing. Per IM:I I-184 the outlined-
            // shape procedures add to the region being built. The rounded
            // corners pull inward from the outer bbox, so the bbox-approx
            // region storage uses the outer rect.
            // FrameRoundRect ($A8B0)
            (true, 0x0B0) => {
                let sp = cpu.read_reg(Register::A7);
                let oval_height = bus.read_word(sp) as i16;
                let oval_width = bus.read_word(sp + 2) as i16;
                let rect_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                self.draw_round_rect(cpu, bus, &r, oval_width, oval_height, ShapeOp::Frame);
                Ok(())
            }

            // PaintRoundRect ($A8B1)
            // Paints the specified round-cornered rectangle with the pen pattern.
            // PROCEDURE PaintRoundRect(r: Rect; ovalWidth, ovalHeight: INTEGER);
            // Inside Macintosh Volume I, I-178
            //
            // OpenRgn-recording shim — see FrameRoundRect notes above.
            // PaintRoundRect ($A8B1)
            (true, 0x0B1) => {
                let sp = cpu.read_reg(Register::A7);
                let oval_height = bus.read_word(sp) as i16;
                let oval_width = bus.read_word(sp + 2) as i16;
                let rect_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                self.draw_round_rect(cpu, bus, &r, oval_width, oval_height, ShapeOp::Paint);
                Ok(())
            }

            // EraseRoundRect ($A8B2)
            // Erases the specified round-cornered rectangle with the background pattern.
            // PROCEDURE EraseRoundRect(r: Rect; ovalWidth, ovalHeight: INTEGER);
            // Inside Macintosh Volume I, I-178
            //
            // OpenRgn-recording shim — see FrameRoundRect notes above.
            // EraseRoundRect ($A8B2)
            (true, 0x0B2) => {
                let sp = cpu.read_reg(Register::A7);
                let oval_height = bus.read_word(sp) as i16;
                let oval_width = bus.read_word(sp + 2) as i16;
                let rect_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                self.draw_round_rect(cpu, bus, &r, oval_width, oval_height, ShapeOp::Erase);
                Ok(())
            }

            // InvertRoundRect ($A8B3)
            // Inverts the pixels of the specified round-cornered rectangle.
            // PROCEDURE InvertRoundRect(r: Rect; ovalWidth, ovalHeight: INTEGER);
            // Inside Macintosh Volume I, I-178
            //
            // OpenRgn-recording shim — see FrameRoundRect notes above.
            // InvertRoundRect ($A8B3)
            (true, 0x0B3) => {
                let sp = cpu.read_reg(Register::A7);
                let oval_height = bus.read_word(sp) as i16;
                let oval_width = bus.read_word(sp + 2) as i16;
                let rect_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                self.draw_round_rect(cpu, bus, &r, oval_width, oval_height, ShapeOp::Invert);
                Ok(())
            }

            // FillRoundRect ($A8B4)
            // Fills the specified round-cornered rectangle with the given pattern.
            // PROCEDURE FillRoundRect(r: Rect; ovalWidth, ovalHeight: INTEGER; pat: Pattern);
            // Inside Macintosh Volume I, I-178
            //
            // OpenRgn-recording shim — see FillRect notes.
            // FillRoundRect ($A8B4)
            (true, 0x0B4) => {
                let sp = cpu.read_reg(Register::A7);
                let pat_ptr = bus.read_long(sp);
                let oval_height = bus.read_word(sp + 4) as i16;
                let oval_width = bus.read_word(sp + 6) as i16;
                let rect_ptr = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                let mut pat = [0u8; 8];
                for (i, byte) in pat.iter_mut().enumerate() {
                    *byte = bus.read_byte(pat_ptr + i as u32);
                }
                self.draw_round_rect(cpu, bus, &r, oval_width, oval_height, ShapeOp::Fill(pat));
                Ok(())
            }

            // ========== CopyBits ==========

            // CopyBits (0xA8EC)
            // Copies a bit or pixel image between graphics ports, scaling and clipping to visRgn, clipRgn, and maskRgn.
            // PROCEDURE CopyBits (srcBits,dstBits: BitMap; srcRect,dstRect: Rect; mode: INTEGER; maskRgn: RgnHandle);
            // Imaging With QuickDraw 1994, 3-112
            // CopyBits ($A8EC): Supports 1bpp/8bpp BitMap and PixMap sources, nearest-neighbor scaling, palette translation, transparent mode, and bbox clipping to visRgn/clipRgn/maskRgn; complex transfer modes and non-rectangular region semantics remain incomplete
            (true, 0x0EC) => {
                self.debug_copy_bits_count = self.debug_copy_bits_count.saturating_add(1);
                let sp = cpu.read_reg(Register::A7);
                let trap_pc = cpu.read_reg(Register::PC).wrapping_sub(2);
                let mask_rgn = bus.read_long(sp);
                let mode = bus.read_word(sp + 4) as i16;
                let dst_rect_ptr = bus.read_long(sp + 6);
                let src_rect_ptr = bus.read_long(sp + 10);
                let dst_bits_ptr = bus.read_long(sp + 14);
                let src_bits_ptr = bus.read_long(sp + 18);
                cpu.write_reg(Register::A7, sp + 22);

                // Nil source or destination bitmap pointers are inert. This
                // keeps CopyBits from dereferencing placeholder BitMap slots
                // that some callers leave empty while probing the blit path.
                if src_bits_ptr == 0 || dst_bits_ptr == 0 {
                    return Some(Ok(()));
                }

                let mut src_info = self.resolve_copy_bitmap(bus, src_bits_ptr);
                let mut dst_info = self.resolve_copy_bitmap(bus, dst_bits_ptr);

                // Guard against NIL/sentinel destination baseAddr and
                // source $FFFFFFFF sentinel. A NIL destination cannot
                // receive pixel writes, and a $FFFFFFFF source/destination
                // can wrap address math into low RAM.
                //
                // Note: we intentionally allow src_base == 0 here. Some
                // CopyBits callers hand a transient source BitMap with NIL
                // baseAddr while still expecting the draw path to proceed
                // via accompanying state; treating src=0 as an unconditional
                // no-op regresses menu/title rendering.
                //
                // Inside Macintosh Volume I, I-148 (BitMap structure).
                if src_info.base == u32::MAX || dst_info.base == 0 || dst_info.base == u32::MAX {
                    eprintln!(
                        "[COPYBITS] skipping no-op: src_base=${:08X} dst_base=${:08X} (NIL baseAddr)",
                        src_info.base, dst_info.base
                    );
                    return Some(Ok(()));
                }

                if let Some(path) = dump_copybits_src_path() {
                    if src_info.pixel_size == 8
                        && src_info.bounds_bottom > src_info.bounds_top
                        && src_info.bounds_right > src_info.bounds_left
                    {
                        Self::dump_bitmap_as_png(
                            bus,
                            &src_info,
                            &self.read_port_clut(bus, src_info.ctab_handle),
                            path,
                        );
                    }
                }

                let src_top = bus.read_word(src_rect_ptr) as i16;
                let src_left = bus.read_word(src_rect_ptr + 2) as i16;
                let src_bottom = bus.read_word(src_rect_ptr + 4) as i16;
                let src_right = bus.read_word(src_rect_ptr + 6) as i16;
                let dst_top = bus.read_word(dst_rect_ptr) as i16;
                let dst_left = bus.read_word(dst_rect_ptr + 2) as i16;
                let dst_bottom = bus.read_word(dst_rect_ptr + 4) as i16;
                let dst_right = bus.read_word(dst_rect_ptr + 6) as i16;
                let mode_base = (mode & 0x3F) as u16;
                Self::sanitize_copy_bitmap_bounds(
                    &mut src_info,
                    src_top,
                    src_left,
                    src_bottom,
                    src_right,
                );
                Self::sanitize_copy_bitmap_bounds(
                    &mut dst_info,
                    dst_top,
                    dst_left,
                    dst_bottom,
                    dst_right,
                );
                if src_info.row_bytes == 0
                    || dst_info.row_bytes == 0
                    || src_info.pixel_size == 0
                    || dst_info.pixel_size == 0
                {
                    return Some(Ok(()));
                }

                if trace_title_diag_enabled()
                    && self.tick_count >= 68
                    && self.tick_count <= 110
                    && dst_info.base == self.screen_mode.0
                {
                    eprintln!(
                        "[TITLE-DIAG] CopyBits tick={} pc=${:08X} port=${:08X} srcBits=${:08X} dstBits=${:08X} srcBase=${:08X} dstBase=${:08X} srcRect=({},{}..{},{} ) dstRect=({},{}..{},{} ) mode={} srcCTab=${:08X} dstCTab=${:08X}",
                        self.tick_count,
                        trap_pc,
                        self.current_port,
                        src_bits_ptr,
                        dst_bits_ptr,
                        src_info.base,
                        dst_info.base,
                        src_top,
                        src_left,
                        src_bottom,
                        src_right,
                        dst_top,
                        dst_left,
                        dst_bottom,
                        dst_right,
                        mode_base,
                        src_info.ctab_handle,
                        dst_info.ctab_handle,
                    );
                }

                if dst_info.base == self.screen_mode.0 {
                    self.copybits_screen_count += 1;
                    self.note_screen_copybits_rect(
                        src_top, src_left, src_bottom, src_right, dst_top, dst_left, dst_bottom,
                        dst_right,
                    );
                }

                if trace_menu_redraw_enabled()
                    && trace_menu_redraw_rect_intersects(dst_top, dst_left, dst_bottom, dst_right)
                {
                    eprintln!(
                        "[MENU-REDRAW] CopyBits srcBase=${:08X} dstBase=${:08X} mode={} src=({},{}..{},{} ) dst=({},{}..{},{} ) mask=${:08X}",
                        src_info.base,
                        dst_info.base,
                        mode_base,
                        src_top,
                        src_left,
                        src_bottom,
                        src_right,
                        dst_top,
                        dst_left,
                        dst_bottom,
                        dst_right,
                        mask_rgn,
                    );
                }
                if trace_dialog_draw_enabled() && self.dialog_tracking.is_some() {
                    eprintln!(
                        "[DIALOG-DRAW] CopyBits current_port=${:08X} srcBase=${:08X} dstBase=${:08X} mode={} src=({},{}..{},{} ) dst=({},{}..{},{} ) mask=${:08X}",
                        self.current_port,
                        src_info.base,
                        dst_info.base,
                        mode_base,
                        src_top,
                        src_left,
                        src_bottom,
                        src_right,
                        dst_top,
                        dst_left,
                        dst_bottom,
                        dst_right,
                        mask_rgn,
                    );
                }
                if trace_dialog_port_dump_enabled() && self.dialog_tracking.is_some() {
                    Self::trace_port_snapshot(bus, "CopyBits-current", self.current_port);
                }
                if trace_dialog_port_dump_enabled() && self.dialog_tracking.is_some() {
                    Self::trace_port_snapshot(bus, "CopyBits-current", self.current_port);
                }

                if src_bottom <= src_top
                    || src_right <= src_left
                    || dst_bottom <= dst_top
                    || dst_right <= dst_left
                {
                    return Some(Ok(()));
                }

                // Clip destination to the current port's visRgn and clipRgn
                // Per Inside Macintosh Volume I, I-158: CopyBits clips to the
                // intersection of visRgn, clipRgn, and the maskRgn parameter,
                // but ONLY when the destination bitmap is the current port's bitmap.
                // When copying to an offscreen bitmap, port clipping does not apply.
                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port = bus.read_long(global_ptr);

                let (mut clip_t, mut clip_l, mut clip_b, mut clip_r) =
                    (dst_top, dst_left, dst_bottom, dst_right);

                let mut vis_rgn_handle = 0;
                let mut clip_rgn_handle = 0;
                // Check if destination is the current port's bitmap
                let dst_is_port = if port != 0 {
                    let port_version = bus.read_word(port + 6);
                    let port_base = if (port_version & 0xC000) == 0xC000 {
                        // CGrafPort: portPixMap handle at offset 2
                        let pm_handle = bus.read_long(port + 2);
                        if pm_handle != 0 {
                            let pm_ptr = bus.read_long(pm_handle);
                            Self::offscreen_pixmap_base_ptr(bus, pm_ptr)
                        } else {
                            0
                        }
                    } else {
                        // GrafPort: portBits.baseAddr at offset 2
                        bus.read_long(port + 2)
                    };
                    port_base == dst_info.base
                } else {
                    false
                };
                if dst_is_port && port != 0 {
                    // Clip to visRgn (GrafPort offset 24)
                    vis_rgn_handle = bus.read_long(port + 24);
                    if !Self::clip_rect_to_region_bbox(
                        bus,
                        vis_rgn_handle,
                        &mut clip_t,
                        &mut clip_l,
                        &mut clip_b,
                        &mut clip_r,
                    ) {
                        return Some(Ok(()));
                    }
                    // Clip to clipRgn (GrafPort offset 28)
                    clip_rgn_handle = bus.read_long(port + 28);
                    if !Self::clip_rect_to_region_bbox(
                        bus,
                        clip_rgn_handle,
                        &mut clip_t,
                        &mut clip_l,
                        &mut clip_b,
                        &mut clip_r,
                    ) {
                        return Some(Ok(()));
                    }
                }

                if !Self::clip_rect_to_region_bbox(
                    bus,
                    mask_rgn,
                    &mut clip_t,
                    &mut clip_l,
                    &mut clip_b,
                    &mut clip_r,
                ) {
                    return Some(Ok(()));
                }

                // Also clip against destination bitmap bounds
                clip_t = clip_t.max(dst_info.bounds_top);
                clip_l = clip_l.max(dst_info.bounds_left);
                clip_b = clip_b.min(dst_info.bounds_bottom);
                clip_r = clip_r.min(dst_info.bounds_right);

                // Compute effective copy region after clipping
                let eff_width = clip_r - clip_l;
                let eff_height = clip_b - clip_t;

                if eff_width <= 0 || eff_height <= 0 {
                    return Some(Ok(()));
                }
                if dst_info.base == self.screen_mode.0 {
                    self.ensure_dialog_background_saved_for_screen_port(bus, port);
                }

                let mask_membership =
                    Self::build_region_membership_cache(bus, mask_rgn, clip_t, clip_b);
                let vis_membership =
                    Self::build_region_membership_cache(bus, vis_rgn_handle, clip_t, clip_b);
                let clip_membership =
                    Self::build_region_membership_cache(bus, clip_rgn_handle, clip_t, clip_b);

                // Precompute "no clipping" so per-pixel region checks can
                // short-circuit out of the hot loop.
                let no_clipping = vis_rgn_handle == 0 && clip_rgn_handle == 0 && mask_rgn == 0;

                // Detect "no scaling" so per-pixel scale_coord (mul + div)
                // can be replaced with a single addition for the common
                // identity case (src == dst dimensions).
                let src_w = i32::from(src_right) - i32::from(src_left);
                let src_h = i32::from(src_bottom) - i32::from(src_top);
                let dst_w = i32::from(dst_right) - i32::from(dst_left);
                let dst_h = i32::from(dst_bottom) - i32::from(dst_top);
                let no_scaling = src_w == dst_w && src_h == dst_h && src_w > 0 && src_h > 0;

                let trace_probes = if trace_menu_redraw_enabled() {
                    trace_menu_probe_points()
                        .into_iter()
                        .filter_map(|(label, probe_y, probe_x)| {
                            if !trace_menu_rect_contains_point(
                                clip_t, clip_l, clip_b, clip_r, probe_y, probe_x,
                            ) {
                                return None;
                            }
                            let src_y = Self::scale_coord(
                                src_top, src_bottom, dst_top, dst_bottom, probe_y,
                            )? as i16;
                            let src_x = Self::scale_coord(
                                src_left, src_right, dst_left, dst_right, probe_x,
                            )? as i16;
                            let src_pixel = Self::read_bitmap_pixel(bus, &src_info, src_y, src_x);
                            let dst_before =
                                Self::read_bitmap_pixel(bus, &dst_info, probe_y, probe_x);
                            Some((label, probe_y, probe_x, src_y, src_x, src_pixel, dst_before))
                        })
                        .collect::<Vec<_>>()
                } else {
                    Vec::new()
                };

                let dst_ctab_handle = self.copy_bits_destination_ctab_handle(bus, port, &dst_info);
                let src_clut = (src_info.pixel_size == 8)
                    .then(|| self.read_port_clut(bus, src_info.ctab_handle));
                let dst_clut =
                    (dst_info.pixel_size == 8).then(|| self.read_port_clut(bus, dst_ctab_handle));
                let src_ctab_seed = Self::ctab_seed(bus, src_info.ctab_handle);
                // Executor's translation gate compares the source PixMap's
                // CTab seed against `CTAB_SEED(PIXMAP_TABLE(GD_PMAP(the_gd)))` —
                // the GDevice the destination lives on. For a screen-targeted
                // blit that's the *main screen* GDevice, not whatever
                // `self.current_gdevice` happens to be (the game may have
                // SetGDevice'd onto an offscreen GWorld). Look up the seed
                // from `main_gdevice_handle`, not `current_gdevice`, so an
                // on-screen CopyBits doesn't compare against an offscreen
                // GWorld's CTab seed and spuriously translate.
                // Imaging With QuickDraw 1994, 7-24 through 7-28 (CopyBits).
                let dst_ctab_seed = if dst_ctab_handle == 0 {
                    Self::ctab_seed(
                        bus,
                        Self::gdevice_ctab_handle(bus, self.main_gdevice_handle),
                    )
                } else {
                    Self::ctab_seed(bus, dst_ctab_handle)
                };
                // One-shot offscreen dump triggered by SYSTEMLESS_TRACE_OFFSCREEN_002EAD50.
                if trace_offscreen_002ead50_enabled() && src_info.base == 0x002EAD50 {
                    let src_rb = src_info.row_bytes;
                    eprintln!(
                        "[OFFSCREEN-002EAD50] tick={} src_rect=({},{}..{},{}) rb={}",
                        self.tick_count, src_top, src_left, src_bottom, src_right, src_rb,
                    );
                    for row_y in [0i32, 1, 2, 3, 4, 5, 30, 53, 54, 55, 56, 57, 58] {
                        let mut sample = String::new();
                        for col in [0u32, 60, 120, 180, 240, 300, 360, 420, 475] {
                            let addr = src_info
                                .base
                                .wrapping_add((row_y as u32).wrapping_mul(src_rb))
                                .wrapping_add(col);
                            let idx = bus.read_byte(addr);
                            sample.push_str(&format!(" x{}={}", col, idx));
                        }
                        eprintln!("[OFFSCREEN-002EAD50]  y={}:{}", row_y, sample);
                    }
                }

                if trace_copybits_all_enabled() {
                    let palette_details = match (src_clut.as_ref(), dst_clut.as_ref()) {
                        (Some(src_clut), Some(dst_clut)) => {
                            let mut parts = Vec::new();
                            for index in [0usize, 16, 43, 100, 150, 185, 220, 245] {
                                let s = src_clut[index];
                                let d = dst_clut[index];
                                parts.push(format!(
                                    "idx{} src=({:04X},{:04X},{:04X}) dst=({:04X},{:04X},{:04X})",
                                    index, s[0], s[1], s[2], d[0], d[1], d[2]
                                ));
                            }
                            parts.join(" | ")
                        }
                        _ => String::new(),
                    };
                    let sample_points = [
                        (dst_top, dst_left),
                        ((dst_top + dst_bottom) / 2, (dst_left + dst_right) / 2),
                    ];
                    let mut sample_details = Vec::new();
                    for (dy, dx) in sample_points {
                        let src_sample =
                            Self::scale_coord(src_top, src_bottom, dst_top, dst_bottom, dy)
                                .zip(Self::scale_coord(
                                    src_left, src_right, dst_left, dst_right, dx,
                                ))
                                .map(|(sy, sx)| (sy as i16, sx as i16));
                        if let Some((sy, sx)) = src_sample {
                            let src_pixel = Self::read_bitmap_pixel(bus, &src_info, sy, sx);
                            let dst_pixel = Self::read_bitmap_pixel(bus, &dst_info, dy, dx);
                            let device_rgb = match (dst_info.pixel_size == 8, src_pixel) {
                                (true, Some(src_pixel)) => self.device_clut[src_pixel as usize],
                                _ => [0, 0, 0],
                            };
                            sample_details.push(format!(
                                "dst=({dy},{dx}) src=({sy},{sx}) srcPix={src_pixel:?} dstPix={dst_pixel:?} deviceRGB=({:04X},{:04X},{:04X})",
                                device_rgb[0], device_rgb[1], device_rgb[2]
                            ));
                        }
                    }
                    eprintln!(
                        "[COPYBITS-ALL] srcBase=${:08X} dstBase=${:08X} screenBase=${:08X} src=({},{}..{},{}) dst=({},{}..{},{}) mode={} srcDepth={} dstDepth={} mask=${:08X} port=${:08X} srcCTab=${:08X} dstInfoCTab=${:08X} dstCTab=${:08X} srcSeed={:?} dstSeed={:?} {}",
                        src_info.base,
                        dst_info.base,
                        self.screen_mode.0,
                        src_top,
                        src_left,
                        src_bottom,
                        src_right,
                        dst_top,
                        dst_left,
                        dst_bottom,
                        dst_right,
                        mode,
                        src_info.pixel_size,
                        dst_info.pixel_size,
                        mask_rgn,
                        self.current_port,
                        src_info.ctab_handle,
                        dst_info.ctab_handle,
                        dst_ctab_handle,
                        src_ctab_seed,
                        dst_ctab_seed,
                        sample_details.join(" | "),
                    );
                    if !palette_details.is_empty() {
                        eprintln!("[COPYBITS-ALL] palettes {}", palette_details);
                    }
                }
                // Executor only depth-converts indexed copies when the source
                // PixMap's table differs from the destination PixMap's table,
                // the source table has a nonzero seed, and that seed differs
                // from the current GDevice's seed.
                // references/executor/src/quickdraw/qStdBits.cpp
                // During a seeded palette window the screen's cm holds
                // the title palette while offscreen buffers retain the
                // canonical system CLUT. Remapping canonical indices
                // through the title palette produces permanently wrong
                // pixels. Skip translation when the source is canonical
                // and the destination is the screen cm (ctab_handle 0)
                // — the raw indices will display correctly once the
                // palette fade completes.
                let hardware_palette_active =
                    dst_ctab_handle == 0 && self.device_clut != self.color_manager_clut;
                let skip_canonical_to_screen = dst_ctab_handle == 0
                    && src_clut
                        .as_ref()
                        .is_some_and(Self::uses_canonical_system_8bpp_clut);
                let skip_hardware_palette_to_screen = hardware_palette_active;
                let palette_translation = if src_info.pixel_size == 8
                    && dst_info.pixel_size == 8
                    && src_info.ctab_handle != dst_info.ctab_handle
                    && matches!(src_ctab_seed, Some(src_seed) if src_seed != 0)
                    && src_ctab_seed != dst_ctab_seed
                    && !skip_canonical_to_screen
                    && !skip_hardware_palette_to_screen
                {
                    match (src_clut.as_ref(), dst_clut.as_ref()) {
                        (Some(src_clut), Some(dst_clut)) => Some(self.build_palette_translation(
                            bus,
                            src_clut,
                            dst_clut,
                            dst_ctab_handle,
                        )),
                        _ => None,
                    }
                } else {
                    None
                };
                let src_clut = src_clut.as_ref();
                let dst_clut = dst_clut.as_ref();
                let palette_translation = palette_translation.as_ref();
                let transformed_blit = !no_scaling
                    || palette_translation.is_some()
                    || src_info.pixel_size != dst_info.pixel_size
                    || mode_base != 0
                    || !no_clipping;
                self.add_hle_tick_cost(Self::quickdraw_blit_tick_cost(
                    eff_width as u32,
                    eff_height as u32,
                    src_info.pixel_size,
                    dst_info.pixel_size,
                    transformed_blit,
                ));
                let (copy_fg_rgb, copy_bg_rgb) = self.copy_bits_port_draw_colors(bus, port);

                // Identity-blit fast path: when nothing in the inner loop
                // transforms the pixel, replace per-pixel read+write with
                // one bus.read_bytes + bus.write_bytes per row.
                //
                // Conditions (all must hold):
                //   - mode_base == 0 (plain srcCopy, no transparency,
                //     colorization, or hilite mode)
                //   - no_scaling (src and dst dimensions match)
                //   - palette_translation is None (no CTab remap)
                //   - matching byte-aligned pixel sizes (8/16/24/32)
                //   - src and dst rects fully inside their bitmap bounds
                //   - src.base != dst.base (in-place may need direction-
                //     aware copy; defer to per-pixel path for safety)
                //   - no probe/trace flag wants per-pixel observability
                //
                // Region clipping is handled by the precomputed clip_t/l/b/r
                // (intersection with the port's visRgn, clipRgn, and
                // mask_rgn bboxes plus dst bitmap bounds). Regions are
                // modeled as bounding rectangles, so within the clip rect
                // there are no holes — bulk-copying clip_t..clip_b ×
                // clip_l..clip_r is correct even when handles are non-zero.
                //
                // Imaging With QuickDraw 1994, p. 7-25 (CopyBits modes).
                let src_inside_bounds = src_top >= src_info.bounds_top
                    && src_bottom <= src_info.bounds_bottom
                    && src_left >= src_info.bounds_left
                    && src_right <= src_info.bounds_right;
                let dst_inside_bounds = dst_top >= dst_info.bounds_top
                    && dst_bottom <= dst_info.bounds_bottom
                    && dst_left >= dst_info.bounds_left
                    && dst_right <= dst_info.bounds_right;
                let pixel_size_ok = src_info.pixel_size == dst_info.pixel_size
                    && src_info.pixel_size >= 8
                    && src_info.pixel_size.is_multiple_of(8);
                let identity_blit = mode_base == 0
                    && no_scaling
                    && palette_translation.is_none()
                    && src_inside_bounds
                    && dst_inside_bounds
                    && src_info.base != dst_info.base
                    && pixel_size_ok
                    && !Self::region_is_complex(bus, vis_rgn_handle)
                    && !Self::region_is_complex(bus, clip_rgn_handle)
                    && !Self::region_is_complex(bus, mask_rgn)
                    && trace_copybits_hud_probe().is_none()
                    && dump_copybits_src_path().is_none()
                    && !trace_copybits_all_enabled()
                    && !trace_menu_redraw_enabled()
                    && trace_probes.is_empty();
                if identity_blit {
                    let bytes_per_pixel = src_info.pixel_size / 8;
                    let row_byte_count = ((clip_r as i32 - clip_l as i32) as u32) * bytes_per_pixel;
                    let src_x_offset_bytes = ((i32::from(src_left) + i32::from(clip_l)
                        - i32::from(dst_left)
                        - i32::from(src_info.bounds_left))
                        as u32)
                        * bytes_per_pixel;
                    let dst_x_offset_bytes = ((i32::from(clip_l) - i32::from(dst_info.bounds_left))
                        as u32)
                        * bytes_per_pixel;
                    for dy in clip_t..clip_b {
                        let rel_y = i32::from(dy) - i32::from(dst_top);
                        let src_y_off =
                            ((i32::from(src_top) + rel_y) - i32::from(src_info.bounds_top)) as u32;
                        let dst_y_off = (i32::from(dy) - i32::from(dst_info.bounds_top)) as u32;
                        let src_addr =
                            src_info.base + src_y_off * src_info.row_bytes + src_x_offset_bytes;
                        let dst_addr =
                            dst_info.base + dst_y_off * dst_info.row_bytes + dst_x_offset_bytes;
                        let src_row = bus.read_bytes(src_addr, row_byte_count as usize);
                        bus.write_bytes(dst_addr, &src_row);
                    }
                    if dst_info.base == self.screen_mode.0 {
                        self.refresh_visible_dialog_snapshot_after_bulk_port_draw(
                            bus,
                            self.current_port,
                        );
                    }
                    return Some(Ok(()));
                }

                // SYSTEMLESS_TRACE_COPYBITS logs "interesting" CopyBits (mask,
                // scale, or translation). SYSTEMLESS_TRACE_COPYBITS_ALL logs
                // EVERY CopyBits unconditionally.
                let log_copybits = trace_copybits_all_enabled()
                    || (trace_copybits_enabled()
                        && (mask_rgn != 0
                            || src_info.pixel_size != dst_info.pixel_size
                            || src_right - src_left != dst_right - dst_left
                            || src_bottom - src_top != dst_bottom - dst_top
                            || palette_translation.is_some()));
                // Per-pixel HUD probe: when SYSTEMLESS_TRACE_COPYBITS_HUD_PROBE
                // is set, log src_idx→dst_idx for 8bpp CopyBits writes
                // landing at the configured probe coordinates. Used to
                // disambiguate "offscreen bitmap already holds the wrong
                // index" vs "CopyBits translated a correct src to the wrong
                // dst".
                let copybits_hud_probe_points = if trace_copybits_hud_probe().is_some()
                    && src_info.pixel_size == 8
                    && dst_info.pixel_size == 8
                {
                    Some(trace_copybits_hud_probe_points())
                } else {
                    None
                };
                if log_copybits {
                    // PC is the post-trap address; subtract 2 to get
                    // the trap-instruction address that the FB-WRITE-
                    // DISASM tracer reports. Including it here makes
                    // it possible to correlate a CopyBits log entry
                    // with the FB-WRITE-DISASM line that pinpointed
                    // the same call from the destination side.
                    let calling_pc = cpu.read_reg(Register::PC).wrapping_sub(2);
                    eprintln!(
                        "[COPYBITS] tick={} pc=${:08X} srcBase=${:08X} dstBase=${:08X} src={}bpp dst={}bpp mode={} src=({},{}..{},{} ) dst=({},{}..{},{} ) mask=${:08X} translate={} srcSeed={:?} dstSeed={:?}",
                        self.tick_count,
                        calling_pc,
                        src_info.base,
                        dst_info.base,
                        src_info.pixel_size,
                        dst_info.pixel_size,
                        mode_base,
                        src_top,
                        src_left,
                        src_bottom,
                        src_right,
                        dst_top,
                        dst_left,
                        dst_bottom,
                        dst_right,
                        mask_rgn,
                        palette_translation.is_some(),
                        src_ctab_seed,
                        dst_ctab_seed,
                    );
                }
                if trace_menu_redraw_enabled()
                    && trace_menu_redraw_rect_intersects(dst_top, dst_left, dst_bottom, dst_right)
                {
                    eprintln!(
                        "[MENU-REDRAW] CopyBits ctabs src=${:08X} dstInfo=${:08X} dstEffective=${:08X} srcSeed={:?} dstSeed={:?} translate={}",
                        src_info.ctab_handle,
                        dst_info.ctab_handle,
                        dst_ctab_handle,
                        src_ctab_seed,
                        dst_ctab_seed,
                        palette_translation.is_some(),
                    );
                }
                let fg_index = dst_clut.as_ref().map(|clut| {
                    self.palette_index_for_rgb(bus, dst_ctab_handle, clut, copy_fg_rgb)
                });
                let bg_index = dst_clut.as_ref().map(|clut| {
                    self.palette_index_for_rgb(bus, dst_ctab_handle, clut, copy_bg_rgb)
                });
                let transparent_src_indices = if mode_base == 36 {
                    self.copy_bits_transparent_source_indices(
                        bus,
                        src_info.ctab_handle,
                        src_clut,
                        copy_bg_rgb,
                    )
                } else {
                    None
                };
                // CopyBits must preserve source pixels for overlapping blits.
                // Snapshot the touched source rows when src and dst share storage
                // so later writes don't affect subsequent reads.
                let source_snapshot = if src_info.base == dst_info.base {
                    let mut row_start = u32::MAX;
                    let mut row_end = 0u32;
                    for dy in clip_t..clip_b {
                        let Some(src_y) =
                            Self::scale_coord(src_top, src_bottom, dst_top, dst_bottom, dy)
                        else {
                            continue;
                        };
                        if src_y < i32::from(src_info.bounds_top)
                            || src_y >= i32::from(src_info.bounds_bottom)
                        {
                            continue;
                        }
                        let row = (src_y - i32::from(src_info.bounds_top)) as u32;
                        row_start = row_start.min(row);
                        row_end = row_end.max(row);
                    }

                    if row_start != u32::MAX {
                        let row_count = row_end - row_start + 1;
                        let mut snapshot = vec![0u8; (row_count * src_info.row_bytes) as usize];
                        // Replace the per-byte snapshot loop with bus.read_bytes
                        // (slice fast path). Per row: one bounds check + memcpy
                        // instead of row_bytes × byte reads.
                        for row in 0..row_count {
                            let row_base = src_info.base + (row_start + row) * src_info.row_bytes;
                            let snapshot_offset = (row * src_info.row_bytes) as usize;
                            let row_data = bus.read_bytes(row_base, src_info.row_bytes as usize);
                            snapshot
                                [snapshot_offset..snapshot_offset + src_info.row_bytes as usize]
                                .copy_from_slice(&row_data);
                        }
                        Some((row_start, row_count, snapshot))
                    } else {
                        None
                    }
                } else {
                    None
                };
                let read_src_byte = |bus: &MacMemoryBus, addr: u32| -> u8 {
                    if let Some((row_start, row_count, snapshot)) = source_snapshot.as_ref() {
                        let offset = addr.saturating_sub(src_info.base);
                        let row = offset / src_info.row_bytes;
                        if row >= *row_start && row < *row_start + *row_count {
                            let row_offset = row - *row_start;
                            let col = offset % src_info.row_bytes;
                            let snapshot_index = (row_offset * src_info.row_bytes + col) as usize;
                            if snapshot_index < snapshot.len() {
                                return snapshot[snapshot_index];
                            }
                        }
                    }
                    bus.read_byte(addr)
                };

                for dy in clip_t..clip_b {
                    // Skip mul/div in scale_coord when not scaling.
                    let src_y = if no_scaling {
                        let rel = i32::from(dy) - i32::from(dst_top);
                        if rel < 0 || rel >= dst_h {
                            continue;
                        }
                        i32::from(src_top) + rel
                    } else {
                        let Some(s) =
                            Self::scale_coord(src_top, src_bottom, dst_top, dst_bottom, dy)
                        else {
                            continue;
                        };
                        s
                    };
                    if src_y < i32::from(src_info.bounds_top)
                        || src_y >= i32::from(src_info.bounds_bottom)
                    {
                        continue;
                    }
                    let dst_y = (i32::from(dy) - i32::from(dst_info.bounds_top)) as u32;

                    for dx in clip_l..clip_r {
                        // Skip mul/div in scale_coord when not scaling.
                        let src_x = if no_scaling {
                            let rel = i32::from(dx) - i32::from(dst_left);
                            if rel < 0 || rel >= dst_w {
                                continue;
                            }
                            i32::from(src_left) + rel
                        } else {
                            let Some(s) =
                                Self::scale_coord(src_left, src_right, dst_left, dst_right, dx)
                            else {
                                continue;
                            };
                            s
                        };
                        if src_x < i32::from(src_info.bounds_left)
                            || src_x >= i32::from(src_info.bounds_right)
                        {
                            continue;
                        }
                        // Skip the 3 region checks entirely when
                        // we precomputed that no clipping is in effect.
                        if !no_clipping {
                            if !Self::region_contains_point_cached(
                                bus,
                                vis_rgn_handle,
                                vis_membership.as_ref(),
                                dy,
                                dx,
                            ) {
                                continue;
                            }
                            if !Self::region_contains_point_cached(
                                bus,
                                clip_rgn_handle,
                                clip_membership.as_ref(),
                                dy,
                                dx,
                            ) {
                                continue;
                            }
                            if !Self::region_contains_point_cached(
                                bus,
                                mask_rgn,
                                mask_membership.as_ref(),
                                dy,
                                dx,
                            ) {
                                continue;
                            }
                        }

                        let dst_x = (i32::from(dx) - i32::from(dst_info.bounds_left)) as u32;
                        let src_x_off = (src_x - i32::from(src_info.bounds_left)) as u32;
                        let src_y_off = (src_y - i32::from(src_info.bounds_top)) as u32;

                        match (src_info.pixel_size, dst_info.pixel_size) {
                            (8, 8) => {
                                let src_addr =
                                    src_info.base + src_y_off * src_info.row_bytes + src_x_off;
                                let dst_addr = dst_info.base + dst_y * dst_info.row_bytes + dst_x;
                                let src_pixel = read_src_byte(bus, src_addr);
                                if mode_base == 36 {
                                    let src_rgb = src_clut.unwrap()[src_pixel as usize];
                                    if transparent_src_indices
                                        .as_ref()
                                        .map_or(false, |indices| indices[src_pixel as usize])
                                    {
                                        if let Some(points) = copybits_hud_probe_points {
                                            if points.iter().any(|&(probe_x, probe_y)| {
                                                dx == probe_x && dy == probe_y
                                            }) {
                                                eprintln!(
                                                    "[COPYBITS-HUD] tick={} dx={} dy={} mode={} src_idx={} src_rgb=({:04X},{:04X},{:04X}) transparent=true bg_rgb=({:04X},{:04X},{:04X}) srcBase=${:08X} dstBase=${:08X} srcCTab=${:08X} dstCTab=${:08X} srcSeed={:?} dstSeed={:?}",
                                                    self.tick_count,
                                                    dx,
                                                    dy,
                                                    mode_base,
                                                    src_pixel,
                                                    src_rgb[0],
                                                    src_rgb[1],
                                                    src_rgb[2],
                                                    copy_bg_rgb[0],
                                                    copy_bg_rgb[1],
                                                    copy_bg_rgb[2],
                                                    src_info.base,
                                                    dst_info.base,
                                                    src_info.ctab_handle,
                                                    dst_ctab_handle,
                                                    src_ctab_seed,
                                                    dst_ctab_seed,
                                                );
                                            }
                                        }
                                        continue;
                                    }
                                }
                                let Some(dst_pixel) = self.copy_bits_color_source_mode_pixel(
                                    bus,
                                    dst_ctab_handle,
                                    src_clut,
                                    dst_clut,
                                    palette_translation,
                                    mode_base,
                                    src_pixel,
                                    bus.read_byte(dst_addr),
                                    copy_fg_rgb,
                                    copy_bg_rgb,
                                ) else {
                                    continue;
                                };
                                if let Some(points) = copybits_hud_probe_points {
                                    if !points
                                        .iter()
                                        .any(|&(probe_x, probe_y)| dx == probe_x && dy == probe_y)
                                    {
                                        bus.write_byte(dst_addr, dst_pixel);
                                        continue;
                                    }
                                    {
                                        let src_rgb = src_clut
                                            .map(|c| c[src_pixel as usize])
                                            .unwrap_or([0, 0, 0]);
                                        let dst_rgb = dst_clut
                                            .map(|c| c[dst_pixel as usize])
                                            .unwrap_or([0, 0, 0]);
                                        eprintln!(
                                            "[COPYBITS-HUD] tick={} dx={} dy={} mode={} src_idx={} src_rgb=({:04X},{:04X},{:04X}) dst_idx={} dst_rgb=({:04X},{:04X},{:04X}) translate={} bg_rgb=({:04X},{:04X},{:04X}) srcBase=${:08X} dstBase=${:08X} srcCTab=${:08X} dstCTab=${:08X} srcSeed={:?} dstSeed={:?}",
                                            self.tick_count,
                                            dx,
                                            dy,
                                            mode_base,
                                            src_pixel,
                                            src_rgb[0],
                                            src_rgb[1],
                                            src_rgb[2],
                                            dst_pixel,
                                            dst_rgb[0],
                                            dst_rgb[1],
                                            dst_rgb[2],
                                            palette_translation.is_some(),
                                            copy_bg_rgb[0],
                                            copy_bg_rgb[1],
                                            copy_bg_rgb[2],
                                            src_info.base,
                                            dst_info.base,
                                            src_info.ctab_handle,
                                            dst_ctab_handle,
                                            src_ctab_seed,
                                            dst_ctab_seed,
                                        );
                                    }
                                }
                                bus.write_byte(dst_addr, dst_pixel);
                            }
                            (src_bits, dst_bits)
                                if src_bits >= 8 && dst_bits >= 8 && src_bits == dst_bits =>
                            {
                                let bytes_per_pixel = src_bits / 8;
                                let src_addr = src_info.base
                                    + src_y_off * src_info.row_bytes
                                    + src_x_off * bytes_per_pixel;
                                let dst_addr = dst_info.base
                                    + dst_y * dst_info.row_bytes
                                    + dst_x * bytes_per_pixel;
                                for byte_index in 0..bytes_per_pixel {
                                    let pixel = read_src_byte(bus, src_addr + byte_index);
                                    bus.write_byte(dst_addr + byte_index, pixel);
                                }
                            }
                            (1, 8) => {
                                let src_byte_offset =
                                    src_y_off * src_info.row_bytes + (src_x_off / 8);
                                let src_bit = 7 - (src_x_off % 8);
                                let src_byte = read_src_byte(bus, src_info.base + src_byte_offset);
                                let src_pixel = (src_byte & (1 << src_bit)) != 0;
                                let dst_addr = dst_info.base + dst_y * dst_info.row_bytes + dst_x;
                                let dst_pixel = bus.read_byte(dst_addr);
                                let dst_clut = dst_clut.unwrap();
                                let fg_index = fg_index.unwrap();
                                let bg_index = bg_index.unwrap();
                                let new_pixel = match mode_base {
                                    0 => Some(if src_pixel { fg_index } else { bg_index }),
                                    1 => src_pixel.then_some(fg_index),
                                    2 => src_pixel
                                        .then(|| Self::inverted_palette_index(dst_clut, dst_pixel)),
                                    3 => src_pixel.then_some(bg_index),
                                    4 => Some(if src_pixel { bg_index } else { fg_index }),
                                    5 => (!src_pixel).then_some(fg_index),
                                    6 => (!src_pixel)
                                        .then(|| Self::inverted_palette_index(dst_clut, dst_pixel)),
                                    7 => (!src_pixel).then_some(bg_index),
                                    36 => src_pixel.then_some(fg_index),
                                    _ => Some(if src_pixel { fg_index } else { bg_index }),
                                };
                                if let Some(pixel) = new_pixel {
                                    bus.write_byte(dst_addr, pixel);
                                }
                            }
                            (8, 1) => {
                                // 8bpp → 1bpp: convert by testing if the 8bpp pixel is
                                // "dark" (closer to black than white). In the standard Mac
                                // CLUT, index 255 = black, index 0 = white.
                                let src_addr =
                                    src_info.base + src_y_off * src_info.row_bytes + src_x_off;
                                let src_pixel = read_src_byte(bus, src_addr);
                                let src_is_dark = if let Some(clut) = src_clut {
                                    let [r, g, b] = clut[src_pixel as usize];
                                    let lum = (r as u32 + g as u32 + b as u32) / 3;
                                    lum < 128
                                } else {
                                    // No CLUT: treat high index as dark (Mac convention)
                                    src_pixel >= 128
                                };
                                let dst_byte_offset = dst_y * dst_info.row_bytes + (dst_x / 8);
                                let dst_bit = 7 - (dst_x % 8);
                                let dst_addr = dst_info.base + dst_byte_offset;
                                let dst_byte = bus.read_byte(dst_addr);
                                let dst_pixel = (dst_byte & (1 << dst_bit)) != 0;
                                let new_bit = match mode_base {
                                    0 => src_is_dark,
                                    1 => src_is_dark || dst_pixel,
                                    2 => src_is_dark ^ dst_pixel,
                                    3 => !src_is_dark && dst_pixel,
                                    4 => !src_is_dark,
                                    _ => src_is_dark,
                                };
                                if new_bit {
                                    bus.write_byte(dst_addr, dst_byte | (1 << dst_bit));
                                } else {
                                    bus.write_byte(dst_addr, dst_byte & !(1 << dst_bit));
                                }
                            }
                            (1, 1) => {
                                // 1bpp CopyBits compress uses OR-merge across
                                // all src pixels that map to this dst pixel.
                                // Per IM:V V-65: "When CopyBits scales a
                                // bitmap, it uses an OR operation: if any
                                // source pixel that maps to a destination
                                // pixel is set, the destination pixel is set."
                                let mut src_pixel = {
                                    let src_byte_offset =
                                        src_y_off * src_info.row_bytes + (src_x_off / 8);
                                    let src_bit = 7 - (src_x_off % 8);
                                    let src_byte =
                                        read_src_byte(bus, src_info.base + src_byte_offset);
                                    (src_byte & (1 << src_bit)) != 0
                                };
                                if !no_scaling && !src_pixel {
                                    // For compress cases (src_w > dst_w OR
                                    // src_h > dst_h) OR-merge additional
                                    // src pixels that fall within this dst
                                    // pixel's input range. Only sample if
                                    // we haven't already found a set bit.
                                    let src_x_end_opt = Self::scale_coord(
                                        src_left,
                                        src_right,
                                        dst_left,
                                        dst_right,
                                        dx + 1,
                                    );
                                    let src_y_end_opt = Self::scale_coord(
                                        src_top,
                                        src_bottom,
                                        dst_top,
                                        dst_bottom,
                                        dy + 1,
                                    );
                                    let sx_hi = src_x_end_opt.unwrap_or(i32::from(src_right));
                                    let sy_hi = src_y_end_opt.unwrap_or(i32::from(src_bottom));
                                    'or_scan: for sy in src_y..sy_hi {
                                        if sy < i32::from(src_info.bounds_top)
                                            || sy >= i32::from(src_info.bounds_bottom)
                                        {
                                            continue;
                                        }
                                        let sy_off = (sy - i32::from(src_info.bounds_top)) as u32;
                                        for sx in src_x..sx_hi {
                                            if sx < i32::from(src_info.bounds_left)
                                                || sx >= i32::from(src_info.bounds_right)
                                            {
                                                continue;
                                            }
                                            let sx_off =
                                                (sx - i32::from(src_info.bounds_left)) as u32;
                                            let off = sy_off * src_info.row_bytes + (sx_off / 8);
                                            let bit = 7 - (sx_off % 8);
                                            let byte = read_src_byte(bus, src_info.base + off);
                                            if (byte & (1 << bit)) != 0 {
                                                src_pixel = true;
                                                break 'or_scan;
                                            }
                                        }
                                    }
                                }
                                let dst_byte_offset = dst_y * dst_info.row_bytes + (dst_x / 8);
                                let dst_bit = 7 - (dst_x % 8);
                                let dst_addr = dst_info.base + dst_byte_offset;
                                let dst_byte = bus.read_byte(dst_addr);
                                let dst_pixel = (dst_byte & (1 << dst_bit)) != 0;
                                let new_bit = match mode_base {
                                    0 => src_pixel,
                                    1 => src_pixel || dst_pixel,
                                    2 => src_pixel ^ dst_pixel,
                                    3 => !src_pixel && dst_pixel,
                                    4 => !src_pixel,
                                    5 => !src_pixel || dst_pixel,
                                    6 => !src_pixel ^ dst_pixel,
                                    7 => src_pixel && dst_pixel,
                                    36 => src_pixel || dst_pixel,
                                    _ => src_pixel,
                                };
                                if new_bit {
                                    bus.write_byte(dst_addr, dst_byte | (1 << dst_bit));
                                } else {
                                    bus.write_byte(dst_addr, dst_byte & !(1 << dst_bit));
                                }
                            }
                            _ => {}
                        }
                    }
                }

                if trace_menu_redraw_enabled() {
                    for (label, probe_y, probe_x, src_y, src_x, src_pixel, dst_before) in
                        trace_probes
                    {
                        let dst_after = Self::read_bitmap_pixel(bus, &dst_info, probe_y, probe_x);
                        eprintln!(
                            "[MENU-REDRAW] CopyBits probe={} srcPt=({}, {}) srcPixel={:?} dstBefore={:?} dstAfter={:?}",
                            label,
                            src_y,
                            src_x,
                            src_pixel,
                            dst_before,
                            dst_after,
                        );
                    }
                }
                if dst_info.base == self.screen_mode.0 {
                    let mut fields: Vec<(&str, String)> = vec![
                        ("mode", mode_base.to_string()),
                        ("src_width", (src_right - src_left).to_string()),
                        ("src_height", (src_bottom - src_top).to_string()),
                        ("dst_top", dst_top.to_string()),
                        ("dst_left", dst_left.to_string()),
                        ("dst_bottom", dst_bottom.to_string()),
                        ("dst_right", dst_right.to_string()),
                    ];
                    if self.oracle_source().is_some() && dst_info.pixel_size == 8 {
                        let fp = copybits_dst_fingerprint(
                            bus, &dst_info, dst_top, dst_left, dst_bottom, dst_right,
                        );
                        fields.push(("dst_top1_idx", fp.top[0].0.to_string()));
                        fields.push(("dst_top1_count", fp.top[0].1.to_string()));
                        fields.push(("dst_top2_idx", fp.top[1].0.to_string()));
                        fields.push(("dst_top2_count", fp.top[1].1.to_string()));
                        fields.push(("dst_top3_idx", fp.top[2].0.to_string()));
                        fields.push(("dst_top3_count", fp.top[2].1.to_string()));
                        fields.push(("dst_idx0_count", fp.idx0_count.to_string()));
                        fields.push(("dst_idx255_count", fp.idx255_count.to_string()));
                        fields.push(("dst_total", fp.total.to_string()));
                    }
                    if let Err(err) = self.record_oracle_event(
                        bus,
                        trap_pc,
                        "copybits_screen",
                        Self::oracle_field_map(&fields),
                        true,
                    ) {
                        return Some(Err(err));
                    }
                }
                if dst_info.base == self.screen_mode.0 {
                    self.refresh_visible_dialog_snapshot_after_bulk_port_draw(
                        bus,
                        self.current_port,
                    );
                }
                Ok(())
            }

            // ========== Pixel Operations ==========

            // GetPixel ($A865)
            //
            // FUNCTION GetPixel(h, v: INTEGER): BOOLEAN;
            // Inside Macintosh Volume I, p. I-195.
            //
            // Per IM:I I-195: "GetPixel looks at the pixel associated
            // with the given coordinate point and returns TRUE if it's
            // black or FALSE if it's white. The selected pixel is
            // immediately below and to the right of the point whose
            // coordinates are given in h and v, in the local
            // coordinates of the current grafPort. There's no
            // guarantee that the specified pixel actually belongs to
            // the port, however; it may have been drawn by a port
            // overlapping the current one. To see if the point indeed
            // belongs to the current port, you could call
            // PtInRgn(pt, thePort^.visRgn)."
            //
            // ## Pascal FUNCTION protocol (stack layout)
            //
            // Caller pre-allocates a 2-byte BOOLEAN result slot,
            // pushes h (2 bytes) then v (2 bytes) right-to-left, then
            // emits $A865. The trap pops the 4 argument bytes and
            // writes the 2-byte Pascal Boolean (0x0100 = TRUE,
            // 0x0000 = FALSE — Mac convention: the high byte holds
            // the truth value, low byte is padding) into the now-
            // top-of-stack result slot. The C wrapper reads the
            // result via the Boolean (1-byte) typedef which sees only
            // the high byte (0x01 = TRUE / 0x00 = FALSE).
            //
            //   SP+0  v       INTEGER (2 bytes)
            //   SP+2  h       INTEGER (2 bytes)
            //   SP+4  result  BOOLEAN slot (2 bytes; trap writes here)
            //
            // Net externally-observed SP delta across the FUNCTION
            // wrapper is zero (4 bytes pushed before, 2 bytes of
            // result reserved before, 2 bytes of result popped after).
            //
            // ## HLE visRgn fast-path (Systemless-specific tightening)
            //
            // Systemless's HLE additionally short-circuits to FALSE when
            // the (h, v) point is outside the port's visRgn rectangle
            // before dereferencing the framebuffer. This is a
            // narrowing of IM:I I-195's looser contract ("no guarantee
            // that the specified pixel actually belongs to the port"),
            // intended to keep games that probe pixels deep inside the
            // menu bar or window-chrome regions from observing stale
            // adjacent-port pixels via the offscreen canvas. Real Mac
            // OS QuickDraw delegates this check to the caller via
            // PtInRgn; both behaviours coincide for any pixel inside
            // the port's visRgn (the common case).
            (true, 0x065) => {
                let sp = cpu.read_reg(Register::A7);
                let v = bus.read_word(sp) as i16;
                let h = bus.read_word(sp + 2) as i16;
                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port = bus.read_long(global_ptr);

                // Per Inside Macintosh, GetPixel returns TRUE only if the pixel
                // is within the port's visRgn. Pixels outside (e.g. in the menu
                // bar or title bar chrome) must return FALSE.
                let vis_rgn_handle = bus.read_long(port + 24);
                let in_vis_rgn = if vis_rgn_handle != 0 {
                    let vis_rgn_ptr = bus.read_long(vis_rgn_handle);
                    if vis_rgn_ptr != 0 {
                        let vt = bus.read_word(vis_rgn_ptr + 2) as i16;
                        let vl = bus.read_word(vis_rgn_ptr + 4) as i16;
                        let vb = bus.read_word(vis_rgn_ptr + 6) as i16;
                        let vr = bus.read_word(vis_rgn_ptr + 8) as i16;
                        v >= vt && v < vb && h >= vl && h < vr
                    } else {
                        true
                    }
                } else {
                    true
                };

                if !in_vis_rgn {
                    bus.write_word(sp + 4, 0);
                    cpu.write_reg(Register::A7, sp + 4);
                    return Some(Ok(()));
                }

                // Detect CGrafPort vs GrafPort (same logic as draw_generic_shape)
                let port_version = bus.read_word(port + 6);
                let is_color = (port_version & 0xC000) != 0;

                let (base_addr, row_bytes, pixel_size, bounds_top, bounds_left) = if is_color {
                    let pm_handle = bus.read_long(port + 2);
                    let pm_ptr = if pm_handle != 0 {
                        bus.read_long(pm_handle)
                    } else {
                        0
                    };
                    if pm_ptr == 0 {
                        bus.write_word(sp + 4, 0);
                        cpu.write_reg(Register::A7, sp + 4);
                        return Some(Ok(()));
                    }
                    let base = Self::offscreen_pixmap_base_ptr(bus, pm_ptr);
                    let rb = (bus.read_word(pm_ptr + 4) & 0x3FFF) as u32;
                    let top = bus.read_word(pm_ptr + 6) as i16;
                    let left = bus.read_word(pm_ptr + 8) as i16;
                    let ps = bus.read_word(pm_ptr + 32);
                    (base, rb, ps, top, left)
                } else {
                    let base = bus.read_long(port + 2);
                    let rb = (bus.read_word(port + 6) & 0x3FFF) as u32;
                    let top = bus.read_word(port + 8) as i16;
                    let left = bus.read_word(port + 10) as i16;
                    (base, rb, 1u16, top, left)
                };

                let dy = (v - bounds_top) as u32;
                let dx = (h - bounds_left) as u32;

                let pixel_set = if pixel_size == 8 {
                    let byte_offset = dy * row_bytes + dx;
                    bus.read_byte(base_addr + byte_offset) != 0
                } else if pixel_size >= 16 {
                    let bpp = pixel_size as u32 / 8;
                    let byte_offset = dy * row_bytes + dx * bpp;
                    bus.read_byte(base_addr + byte_offset) != 0
                } else {
                    // 1bpp
                    let byte_offset = dy * row_bytes + (dx / 8);
                    let bit = 7 - (dx % 8);
                    let byte = bus.read_byte(base_addr + byte_offset);
                    (byte & (1 << bit)) != 0
                };

                bus.write_word(sp + 4, if pixel_set { 0x0100 } else { 0 });
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // ========== Color Manager ==========

            // ForeColor ($A862)
            // ForeColor ($A862): Converts legacy QD color constant to RGB and sets fg_color; per IM:I I-173
            (true, 0x062) => {
                let sp = cpu.read_reg(Register::A7);
                let color = bus.read_long(sp);
                self.fg_color = Self::legacy_qd_color_to_rgb(color);
                self.sync_current_port_draw_state(bus);
                if trace_qd_colors_enabled() {
                    eprintln!(
                        "[QD-COLOR] ForeColor port=${:08X} color=${:08X} rgb=({:04X},{:04X},{:04X}) tick={}",
                        self.current_port,
                        color,
                        self.fg_color.0,
                        self.fg_color.1,
                        self.fg_color.2,
                        self.tick_count,
                    );
                }
                if trace_dialog_text_enabled() {
                    eprintln!(
                        "[DIALOG-TEXT] ForeColor current_port=${:08X} color=${:08X} rgb=({:04X},{:04X},{:04X})",
                        self.current_port,
                        color,
                        self.fg_color.0,
                        self.fg_color.1,
                        self.fg_color.2,
                    );
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // BackColor ($A863)
            // BackColor ($A863): Converts legacy QD color constant to RGB and sets bg_color; per IM:I I-173
            (true, 0x063) => {
                let sp = cpu.read_reg(Register::A7);
                let color = bus.read_long(sp);
                self.bg_color = Self::legacy_qd_color_to_rgb(color);
                self.sync_current_port_draw_state(bus);
                if trace_qd_colors_enabled() {
                    eprintln!(
                        "[QD-COLOR] BackColor port=${:08X} color=${:08X} rgb=({:04X},{:04X},{:04X}) tick={}",
                        self.current_port,
                        color,
                        self.bg_color.0,
                        self.bg_color.1,
                        self.bg_color.2,
                        self.tick_count
                    );
                }
                if trace_dialog_text_enabled() {
                    eprintln!(
                        "[DIALOG-TEXT] BackColor current_port=${:08X} color=${:08X} rgb=({:04X},{:04X},{:04X})",
                        self.current_port,
                        color,
                        self.bg_color.0,
                        self.bg_color.1,
                        self.bg_color.2,
                    );
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // RGBForeColor ($AA14)
            // RGBForeColor ($AA14): Sets fg_color (R, G, B)
            (true, 0x214) => {
                let sp = cpu.read_reg(Register::A7);
                let color_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let r = bus.read_word(color_ptr);
                let g = bus.read_word(color_ptr + 2);
                let b = bus.read_word(color_ptr + 4);
                self.fg_color = (r, g, b);
                self.sync_current_port_draw_state(bus);
                if trace_qd_colors_enabled() {
                    eprintln!(
                        "[QD-COLOR] RGBForeColor port=${:08X} ptr=${:08X} rgb=({:04X},{:04X},{:04X}) tick={}",
                        self.current_port,
                        color_ptr,
                        r, g, b,
                        self.tick_count,
                    );
                }
                Ok(())
            }

            // RGBBackColor ($AA15)
            // RGBBackColor ($AA15): Sets bg_color (R, G, B)
            (true, 0x215) => {
                let sp = cpu.read_reg(Register::A7);
                let color_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let r = bus.read_word(color_ptr);
                let g = bus.read_word(color_ptr + 2);
                let b = bus.read_word(color_ptr + 4);
                self.bg_color = (r, g, b);
                self.sync_current_port_draw_state(bus);
                if trace_qd_colors_enabled() {
                    eprintln!(
                        "[QD-COLOR] RGBBackColor port=${:08X} ptr=${:08X} rgb=({:04X},{:04X},{:04X}) tick={}",
                        self.current_port, color_ptr, r, g, b, self.tick_count
                    );
                }
                Ok(())
            }

            // GetPixPat ($AA0C)
            // FUNCTION GetPixPat(patID: INTEGER): PixPatHandle;
            // Inside Macintosh Volume V (1986), p. V-73
            //
            // Per IM:V V-73 verbatim: "The GetPixPat call creates a
            // new pixPat data structure, and then uses the
            // information in the resource of type 'ppat' and the
            // specified ID to initialize the pixPat. The 'ppat'
            // resource format is described in the section 'Color
            // QuickDraw Resource Formats'. If the resource with the
            // specified ID is not found, then this routine returns
            // a NIL handle."
            //
            // MPW Universal Headers Quickdraw.h declares:
            //     EXTERN_API(PixPatHandle) GetPixPat(short patID)
            //                                 ONEWORDINLINE(0xAA0C);
            //
            // Tool-bit Pascal FUNCTION ABI: caller pre-pushes a
            // 4-byte PixPatHandle result slot, then pushes the
            // 2-byte patID INTEGER. Trap pops the 2-byte argument
            // and writes the handle to the 4-byte result slot at
            // the post-pop SP (= pre-call SP + 2). Caller pops the
            // result slot after the trap returns. Net A7 effect
            // across the C-level call is zero.
            //
            // ## Engines-agree subset (witnessed by
            //    aa0c_getpixpat_strict)
            //
            //   (1) Pascal FUNCTION calling convention — A7 net-
            //       balanced across the C-level call (caller pre-
            //       push of 4-byte result slot + 2-byte arg + trap-
            //       side 2-byte arg pop + result-slot write + caller
            //       post-pop balance).
            //   (2) Miss-returns-NIL — fictional patID outside the
            //       Apple-reserved 'ppat' range returns NIL on both
            //       BasiliskII System 7.5.3 ROM Color QuickDraw and
            //       Systemless HLE per IM:V V-73 documented behaviour.
            //
            // ## Engines-divergent absolute behaviour (not witnessed)
            //
            // On the present-resource path BasiliskII allocates a
            // fresh 28-byte PixPat record initialised from the 'ppat'
            // resource bytes per IM:V V-46 layout. Systemless HLE
            // returns the master pointer to the raw 'ppat' resource
            // bytes wrapped in a handle via find_resource_any +
            // get_or_create_resource_handle. Apps that call
            // PenPixPat $AA0A / BackPixPat $AA0B / FillCRect $AA0E
            // with the handle get the resource bytes directly
            // (Systemless's PenPixPat / BackPixPat / FillCRect implems
            // inspect the PixPat record's pat1Data field at offset
            // +20 for the 8-byte pattern data — this works for ppat
            // resources whose 1-bit pattern data is at the
            // documented IM:V V-46 layout offset).
            //
            // ## Status: Stub → Partial promotion
            //
            // Was previously a Stub returning NIL unconditionally.
            // Promoted to Partial during the stub-with-substantive-body
            // cleanup. New behavior matches
            // the prior $A9B9 GetCursor / $A9B8 GetPattern / $A9BB
            // GetIcon / $A9BC GetPicture / $AA1F GetCIcon Stub→Partial
            // promotion pattern: route through find_resource_any +
            // get_or_create_resource_handle for handle stability
            // across repeated calls (apps cache the handle and reuse
            // it).
            //
            // Catalogue proof: aa0c_getpixpat_strict
            // (BasiliskII System 7.5.3 ROM Color QuickDraw bake) +
            // contract test getpixpat_pascal_function_preserves_stack_across_five_missing_calls.
            (true, 0x20C) => {
                let sp = cpu.read_reg(Register::A7);
                let pat_id = bus.read_word(sp) as i16;
                let handle = match self.find_resource_any(*b"ppat", pat_id) {
                    Some((_, data_ptr)) => {
                        self.get_or_create_resource_handle(bus, *b"ppat", pat_id, data_ptr)
                    }
                    None => 0,
                };
                bus.write_long(sp + 2, handle);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // ProtectEntry ($AA3D)
            // PROCEDURE ProtectEntry (index: INTEGER; protect: BOOLEAN);
            // Inside Macintosh Volume V, V-143
            //
            // Stack (Pascal): SP+0=protect(BOOLEAN, 16-bit), SP+2=index(INTEGER).
            // Pops 4. BOOLEAN: high byte != 0 means TRUE.
            //
            // Catalogue proof:
            //   aa3d_aa3e_protectentry_reserveentry_strict witnesses pop-4
            //   calling convention (single + 5-call composition) against
            //   BasiliskII System 7.5.3 ROM. Absolute CLUT protect-flag
            //   bookkeeping is engines-divergent.
            (true, 0x23D) => {
                let sp = cpu.read_reg(Register::A7);
                let protect = (bus.read_word(sp) >> 8) != 0;
                let index = bus.read_word(sp + 2) as i16;
                cpu.write_reg(Register::A7, sp + 4);
                if (0..256).contains(&(index as i32)) {
                    self.clut_protected[index as usize] = protect;
                }
                Ok(())
            }

            // ReserveEntry ($AA3E)
            // PROCEDURE ReserveEntry (index: INTEGER; reserve: BOOLEAN);
            // Inside Macintosh Volume V, V-143
            //
            // Stack (Pascal): SP+0=reserve(BOOLEAN, 16-bit), SP+2=index(INTEGER).
            // Pops 4.
            //
            // Catalogue proof:
            //   aa3d_aa3e_protectentry_reserveentry_strict witnesses pop-4
            //   calling convention (single + 5-call composition) against
            //   BasiliskII System 7.5.3 ROM. Absolute CLUT reserve-flag
            //   bookkeeping is engines-divergent.
            (true, 0x23E) => {
                let sp = cpu.read_reg(Register::A7);
                let reserve = (bus.read_word(sp) >> 8) != 0;
                let index = bus.read_word(sp + 2) as i16;
                cpu.write_reg(Register::A7, sp + 4);
                if (0..256).contains(&(index as i32)) {
                    self.clut_reserved[index as usize] = reserve;
                }
                Ok(())
            }

            // ========== Color Port Stubs ==========

            // OpenCPort ($AA00)
            // Allocates space for and initializes a color graphics port.
            // PROCEDURE OpenCPort (port: CGrafPtr);
            // Imaging With QuickDraw 1994, pp. 4-64 to 4-65; IM:V 1986 p. V-67
            // OpenCPort ($AA00): allocates CGrafPort-owned storage, then
            // calls InitCPort to initialize defaults.
            (true, 0x200) => {
                let sp = cpu.read_reg(Register::A7);
                let port_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);

                // OpenCPort allocates storage first, then initializes fields.
                self.allocate_cport_storage(port_ptr, bus);
                let _ = self.init_cport(port_ptr, bus, true);
                self.cport_ports.insert(port_ptr);

                // Set as current port on the active GDevice that seeded the
                // port's PixMap. Offscreen code can select a freshly-created
                // GDevice before OpenCPort.
                let gd_handle = self.active_gdevice_or_main(bus);
                self.set_current_port_state(bus, cpu, port_ptr, Some(gd_handle));
                if trace_dialog_ports_enabled() {
                    eprintln!(
                        "[DIALOG-PORT] OpenCPort port=${:08X} gdh=${:08X}",
                        port_ptr, gd_handle
                    );
                }
                Ok(())
            }

            // InitCPort ($AA01)
            // Initializes an existing color graphics port.
            // PROCEDURE InitCPort (port: CGrafPtr);
            // Imaging With QuickDraw 1994, p. 4-66; IM:V 1986 p. V-67
            // InitCPort ($AA01): does not allocate; initializes an existing
            // CGrafPort and returns without action when passed a non-CGrafPort.
            (true, 0x201) => {
                let sp = cpu.read_reg(Register::A7);
                let port_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if self.init_cport(port_ptr, bus, false) {
                    self.cport_ports.insert(port_ptr);
                }
                if trace_dialog_ports_enabled() {
                    eprintln!("[DIALOG-PORT] InitCPort port=${:08X}", port_ptr);
                }
                Ok(())
            }

            // NewPixMap ($AA03)
            // FUNCTION NewPixMap: PixMapHandle;
            // Inside Macintosh Volume V 1986, p. V-57
            // NewPixMap ($AA03): Allocates PixMap struct in guest memory
            (true, 0x203) => {
                let gd_handle = self.ensure_main_gdevice(bus);
                let gd_ptr = bus.read_long(gd_handle);
                let gd_pmap_handle = bus.read_long(gd_ptr + 22);
                let gd_pmap = bus.read_long(gd_pmap_handle);
                let pm_ptr = bus.alloc(50);
                for i in 0..50u32 {
                    bus.write_byte(pm_ptr + i, bus.read_byte(gd_pmap + i));
                }
                bus.write_long(pm_ptr, 0);
                let depth = bus.read_word(pm_ptr + 32) as u32;
                let source_ctab_handle = bus.read_long(pm_ptr + 42);
                let ctab_handle = if let Some(clut) =
                    self.active_seeded_screen_clut_for_offscreen_clone(gd_handle, 0)
                {
                    self.allocate_color_table_handle_with_clut(bus, depth, &clut, 0x8000)
                } else {
                    self.allocate_color_table_handle(bus, depth, source_ctab_handle, 0x8000)
                };
                bus.write_long(pm_ptr + 42, ctab_handle);
                let handle = bus.alloc(4);
                bus.write_long(handle, pm_ptr);
                let sp = cpu.read_reg(Register::A7);
                bus.write_long(sp, handle);
                Ok(())
            }

            // SetCPortPix ($AA06)
            // Sets the portPixMap field of the current CGrafPort.
            // PROCEDURE SetCPortPix (pm: PixMapHandle);
            // Inside Macintosh Volume V, V-74
            // SetCPortPix ($AA06): Writes PixMap handle into port
            (true, 0x206) => {
                let sp = cpu.read_reg(Register::A7);
                let pm_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                if global_ptr == 0 {
                    return Some(Ok(()));
                }
                let port = bus.read_long(global_ptr);
                if port == 0 {
                    return Some(Ok(()));
                }
                bus.write_long(port + 2, pm_handle);
                self.cache_portbits_pixmap_handle(bus, port + 2, pm_handle);

                // If a seeded title palette is active and the newly attached
                // pixmap still carries a canonical ctab, overwrite it with
                // the seeded palette so offscreen composition uses the
                // correct logical palette.
                if pm_handle != 0 {
                    let pm_ptr = bus.read_long(pm_handle);
                    if pm_ptr != 0 && bus.read_word(pm_ptr + 32) == 8 {
                        let ctab_handle = bus.read_long(pm_ptr + 42);
                        let gd_handle = self.ensure_main_gdevice(bus);
                        if let Some(clut) =
                            self.active_seeded_screen_clut_for_offscreen_clone(gd_handle, 0)
                        {
                            let current_clut = self.read_ctab_handle_clut(bus, ctab_handle);
                            if Self::uses_canonical_system_8bpp_clut(&current_clut)
                                || Self::uses_scaled_canonical_system_8bpp_clut(&current_clut)
                            {
                                let ctab_ptr = if ctab_handle != 0 {
                                    bus.read_long(ctab_handle)
                                } else {
                                    0
                                };
                                let ct_flags = if ctab_ptr != 0 {
                                    bus.read_word(ctab_ptr + 4)
                                } else {
                                    0x8000
                                };
                                let _ = self.overwrite_color_table_handle_with_clut(
                                    bus,
                                    ctab_handle,
                                    &clut,
                                    ct_flags,
                                );
                            }
                        }
                    }
                }
                Ok(())
            }

            // MakeRGBPat ($AA0D)
            // PROCEDURE MakeRGBPat (ppat: PixPatHandle; myColor: RGBColor);
            // Inside Macintosh Volume V (1986), p. V-73 (Color QuickDraw —
            // Operations on Pixel Patterns — MakeRGBPat).
            //
            // Per IM:V V-73 verbatim: "The MakeRGBPat procedure is a new
            // call which generates a pixPat that approximates the
            // specified color when drawn. [...] For an RGB pattern, the
            // patMap^^.bounds always contains (0, 0, 8, 8), and the
            // patMap^^.rowBytes equals 2. [...] When MakeRGBPat creates
            // a color table, it only fills in the last colorSpec field:
            // the other colorSpec values are computed at the time the
            // drawing actually takes place, using the current pixel
            // depth for the system."
            //
            // The MPW Universal Headers Quickdraw.h declaration is:
            //     EXTERN_API(void) MakeRGBPat(PixPatHandle pp,
            //                                 const RGBColor *myColor)
            //                                 ONEWORDINLINE(0xAA0D);
            //
            // Tool-bit Pascal PROCEDURE ABI: caller pushes 8 bytes
            // (4-byte PixPatHandle + 4-byte RGBColor pointer; RGBColor
            // is 6 bytes so it is passed by-ref per MPW Pascal calling
            // convention). Trap pops 8 bytes. No FUNCTION result slot.
            //
            // Engines-agree subset (witnessed by aa0d_makergbpat_strict):
            //   The documented Tool-bit Pascal PROCEDURE pop-8 calling
            //   convention itself — A7 unchanged across the call after
            //   the 8-byte arg frame is consumed.
            //
            // Engines-divergent absolute behavior (NOT witnessed):
            //   BII System 7.5.3 ROM Color QuickDraw dereferences pp^^
            //   and *myColor, then writes a dithered 8x8 pattern plus
            //   a partial color table into the pixPat record. Systemless
            //   HLE is a true no-op because the host runtime renders
            //   only to 1bpp canvases and has no dithered pattern
            //   construction path; games that rely on the dithered
            //   pattern visually see whatever the pattern slot already
            //   contained, but the calling convention is preserved so
            //   the rest of the program's stack frame stays intact.
            //
            // Paired catalogue proof:
            //   aa0d_makergbpat_strict/
            //     Witnesses 2 engines-agree assertions:
            //       AA0D:makergbpat_pops_pixpathandle_and_rgbcolor_pointer_arguments
            //       AA0D:makergbpat_pascal_procedure_preserves_stack_across_five_calls
            //   src/trap/quickdraw.rs::tests::
            //     makergbpat_pascal_procedure_preserves_stack_across_five_calls
            //     (mirrors the strict bake's B2 5-call composition)
            (true, 0x20D) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // InitPalettes ($AA90)
            // Per IM:V V-167 + IM:VI 20-18: "InitPalettes
            // initializes the Palette Manager. It searches for
            // devices that support a CLUT and initializes an
            // internal data structure for each one. This procedure
            // is called by InitWindows and does not have to be
            // called by your application."
            // PROCEDURE InitPalettes;
            // Inside Macintosh Volume V, V-167
            // Inside Macintosh Volume VI, 20-18
            //
            // No args, no result. Pop 0 bytes.
            //
            // HLE compromise: Systemless models a single GDevice
            // (the boot screen) with no CLUT-search side effect
            // — the Palette Manager's "internal data structure for
            // each CLUT device" is a single fixed entry baked in
            // at TrapDispatcher::new() time. NewPalette ($AA91) /
            // GetNewPalette ($AA92) / SetPalette ($AA94) etc.
            // operate against this single device without needing
            // an init call. Per IM:V the trap is implicitly called
            // by InitWindows ($A912) so apps that call only
            // InitWindows still get correct Palette Mgr behaviour
            // — both implicit + explicit dispatch paths reach the
            // same no-op contract here.
            // InitPalettes ($AA90): No args / no result per IM:V V-167 + IM:VI 20-18 PROCEDURE sig; Palette Mgr state is statically baked at TrapDispatcher::new() time (single GDevice, no CLUT-device search side effect). Per IM:V V-167 "called by InitWindows and does not have to be called by your application" — implicit + explicit dispatch paths share the no-op contract.
            (true, 0x290) => Ok(()),

            // NewPalette ($AA91)
            // Allocates a palette from a color table or fills it with black.
            // FUNCTION NewPalette(entries: Integer; srcColors: CTabHandle; srcUsage, srcTolerance: Integer): PaletteHandle;
            // Inside Macintosh Volume VI, 20-19
            // NewPalette ($AA91): Allocates palette from CTab per IM:VI 20-19
            (true, 0x291) => {
                let sp = cpu.read_reg(Register::A7);
                let src_tolerance = bus.read_word(sp) as i16;
                let src_usage = bus.read_word(sp + 2) as i16;
                let src_colors = bus.read_long(sp + 4);
                let entries = bus.read_word(sp + 8) as i16;
                if trace_palette_enabled() {
                    eprintln!(
                        "[PALETTE] NewPalette tick={} entries={} src_colors=${:08X} usage={} tolerance={}",
                        self.tick_count, entries, src_colors, src_usage, src_tolerance
                    );
                }
                let palette = self.create_palette_from_ctab(
                    bus,
                    entries as u16,
                    src_colors,
                    src_usage,
                    src_tolerance,
                );
                bus.write_long(sp + 10, palette);
                cpu.write_reg(Register::A7, sp + 10);
                Ok(())
            }

            // GetNewPalette ($AA92)
            // Returns a copy of a 'pltt' resource as a PaletteHandle.
            // FUNCTION GetNewPalette(paletteID: Integer): PaletteHandle;
            // Inside Macintosh Volume VI, 20-19
            // GetNewPalette ($AA92): Copies 'pltt' resource as PaletteHandle per IM:VI 20-19
            (true, 0x292) => {
                let sp = cpu.read_reg(Register::A7);
                let palette_id = bus.read_word(sp) as i16;
                let palette = self.copy_palette_resource_exact(bus, palette_id);
                if trace_palette_enabled() {
                    eprintln!(
                        "[PALETTE] GetNewPalette id={} current_port=${:08X} -> ${:08X}",
                        palette_id, self.current_port, palette
                    );
                }
                bus.write_long(sp + 2, palette);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // DisposePalette ($AA93)
            // Per IM:V V-180: "DisposePalette disposes of a Palette
            // object. If the palette has any entries allocated for
            // animation on any display device, these entries are
            // relinquished prior to deallocation of the object."
            // PROCEDURE DisposePalette(srcPalette: PaletteHandle);
            // Inside Macintosh Volume V, V-180
            // Inside Macintosh Volume VI, 20-20
            //
            // Stack: SP+0 srcPalette PaletteHandle (4 bytes). Pop 4.
            // No result (PROCEDURE).
            //
            // Trap-doc Notes column was previously mislabeled
            // "ScriptUtil | Stub (no-op) | Pops 2 bytes" — that
            // mistake collapsed THREE distinct facts into wrong
            // values: (a) trap word $AA93 is DisposePalette per
            // IM:V V-180 + IM:V V-291 master trap dispatch table
            // ($AA93 row) + IM:VI Table C-1 ($AA93 row), NOT
            // ScriptUtil — ScriptUtil is at $A8B5 per IM:VI Table
            // C-1 ($A8B5 _ScriptUtil row). (b) The impl below has
            // been substantive (full palette-handle disposal:
            // free guest block, drop palette_updates entry, drop
            // window_palettes refs) for many iterations — same
            // Stub-mislabel pattern as PlotIcon ($A94B) /
            // UpdtControl ($A953) / Draw1Control ($A96D) / Get/
            // SetItemCmd ($A84E/F) surfaced during status review.
            // (c) The pop count is 4 (PaletteHandle is 4 bytes)
            // not 2 — the typo in the prior Notes would not have
            // caused real-Mac stack corruption since the impl
            // popped 4 correctly, but documentation readers
            // grouping by manager would have categorized this as
            // Resource Mgr instead of Palette Mgr.
            //
            // Status promoted Stub (no-op) → Partial because the
            // impl frees the guest block + drops palette tracking
            // state but does NOT (per IM:V V-180) "relinquish
            // animation entries on display devices" — Systemless
            // doesn't model per-CLUT-device animation entry
            // allocation (single GDevice with no AnimateEntry
            // $AA99 / AnimatePalette $AA9A side-effect tracking),
            // so the animation-relinquish path is unreachable.
            // Future iteration that wires up Palette Mgr animation
            // semantics could promote to Complete after
            // implementing the relinquish path.
            //
            // Regression coverage:
            //   dispose_palette (BasiliskII-baked trace
            //     of NewPalette × 3 + DisposePalette × 3 interleaved
            //     lifecycle: alloc, alloc, dispose, alloc, dispose,
            //     dispose — proves allocator survives mid-lifecycle
            //     Dispose per IM:V V-180)
            // DisposePalette ($AA93): Pops 4 bytes (PaletteHandle) per IM:V V-180 + IM:V V-291 master trap dispatch table + IM:VI Table C-1 PROCEDURE sig; frees guest palette block + drops palette_updates entry + drops window_palettes refs; does NOT model per-CLUT-device animation-entry relinquish path per IM:V V-180 since Systemless has no AnimateEntry $AA99 / AnimatePalette $AA9A side-effect tracking. Was previously mis-labeled as "ScriptUtil | Stub (no-op) | Pops 2 bytes" — typo in trap-doc Notes only; impl was always correct. Same Stub-mislabel-with-substantive-body status issue as PlotIcon $A94B / UpdtControl $A953 / Draw1Control $A96D / SetItemCmd $A84F surfaced during status review.
            (true, 0x293) => {
                let sp = cpu.read_reg(Register::A7);
                let palette = bus.read_long(sp);
                if palette != 0 {
                    let palette_ptr = Self::palette_ptr(bus, palette);
                    self.clear_palette_tracking(palette);
                    if palette_ptr != 0 {
                        bus.free(palette_ptr);
                    }
                    bus.free(palette);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // ActivatePalette ($AA94)
            // Applies the front window's palette to the active device.
            // PROCEDURE ActivatePalette(srcWindow: WindowPtr);
            // Inside Macintosh Volume VI, 20-20
            // ActivatePalette ($AA94): Applies the window's palette to the active GDevice via activate_palette_for_window per IM:VI 20-20
            (true, 0x294) => {
                let sp = cpu.read_reg(Register::A7);
                let window = bus.read_long(sp);
                if trace_palette_enabled() {
                    eprintln!(
                        "[PALETTE] ActivatePalette(trap) tick={} window=${:08X}",
                        self.tick_count, window
                    );
                }
                self.activate_associated_palette_for_window(bus, window);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // NSetPalette ($AA95)
            // Associates a palette with a window and records update flags.
            // PROCEDURE NSetPalette(dstWindow: WindowPtr; srcPalette: PaletteHandle; nUpdates: Integer);
            // Inside Macintosh Volume VI, 20-21
            // NSetPalette ($AA95): Associates a PaletteHandle with the window and reactivates per IM:VI 20-21.
            // Nil windows are a no-op; the trap should not create a key-0
            // association or touch device state when dstWindow is NIL.
            (true, 0x295) => {
                let sp = cpu.read_reg(Register::A7);
                let updates = bus.read_word(sp) as i16;
                let palette = bus.read_long(sp + 2);
                let window = bus.read_long(sp + 6);
                if trace_palette_enabled() {
                    eprintln!(
                        "[PALETTE] NSetPalette window=${:08X} palette=${:08X} updates=${:04X}",
                        window, palette, updates as u16
                    );
                }
                if window == 0 {
                    cpu.write_reg(Register::A7, sp + 10);
                    return Some(Ok(()));
                }
                self.set_window_palette_association(window, palette, updates);
                self.activate_associated_palette_for_window(bus, window);
                cpu.write_reg(Register::A7, sp + 10);
                Ok(())
            }

            // GetPalette ($AA96)
            // Returns the palette associated with a window.
            // FUNCTION GetPalette(srcWindow: WindowPtr): PaletteHandle;
            // Inside Macintosh Volume VI, 20-20
            // GetPalette ($AA96): Returns the PaletteHandle associated with srcWindow via exact lookup per IM:VI 20-20
            (true, 0x296) => {
                let sp = cpu.read_reg(Register::A7);
                let window = bus.read_long(sp);
                if trace_palette_enabled() {
                    eprintln!(
                        "[PALETTE] GetPalette(trap) tick={} window=${:08X} -> handle=${:08X}",
                        self.tick_count,
                        window,
                        self.window_palette_handle_exact(window)
                    );
                }
                bus.write_long(sp + 4, self.window_palette_handle_exact(window));
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // PmForeColor ($AA97)
            // Sets the foreground color from the current window's palette.
            // PROCEDURE PmForeColor(dstEntry: Integer);
            // Inside Macintosh Volume V, V-163
            // PmForeColor ($AA97): Sets foreground color from window's palette entry; honours pmExplicit (uses device CLUT) vs tolerant (uses palette RGB) per IM:V V-163
            (true, 0x297) => {
                let sp = cpu.read_reg(Register::A7);
                let entry = bus.read_word(sp) as i16;
                if let Some((rgb, usage)) = self.palette_entry_rgb_for_current_window(bus, entry) {
                    self.fg_color = if (usage & PM_EXPLICIT) != 0 {
                        let index = (entry as usize) & 0xFF;
                        let [r, g, b] = self.device_clut[index];
                        (r, g, b)
                    } else {
                        (rgb[0], rgb[1], rgb[2])
                    };
                    self.sync_current_port_draw_state(bus);
                    if trace_qd_colors_enabled() {
                        eprintln!(
                            "[QD-COLOR] PmForeColor port=${:08X} entry={} rgb=({:04X},{:04X},{:04X}) tick={}",
                            self.current_port,
                            entry,
                            self.fg_color.0,
                            self.fg_color.1,
                            self.fg_color.2,
                            self.tick_count,
                        );
                    }
                }
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // PmBackColor ($AA98)
            // Sets the background color from the current window's palette.
            // PROCEDURE PmBackColor(dstEntry: Integer);
            // Inside Macintosh Volume V, V-163
            // PmBackColor ($AA98): Sets background color from window's palette entry; honours pmExplicit vs tolerant usage per IM:V V-163
            (true, 0x298) => {
                let sp = cpu.read_reg(Register::A7);
                let entry = bus.read_word(sp) as i16;
                if let Some((rgb, usage)) = self.palette_entry_rgb_for_current_window(bus, entry) {
                    self.bg_color = if (usage & PM_EXPLICIT) != 0 {
                        let index = (entry as usize) & 0xFF;
                        let [r, g, b] = self.device_clut[index];
                        (r, g, b)
                    } else {
                        (rgb[0], rgb[1], rgb[2])
                    };
                    self.sync_current_port_draw_state(bus);
                    if trace_qd_colors_enabled() {
                        eprintln!(
                            "[QD-COLOR] PmBackColor port=${:08X} entry={} rgb=({:04X},{:04X},{:04X}) tick={}",
                            self.current_port,
                            entry,
                            self.bg_color.0,
                            self.bg_color.1,
                            self.bg_color.2,
                            self.tick_count,
                        );
                    }
                }
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // AnimatePalette ($AA9A)
            // Copies a range of colors from a color table into a window palette.
            // PROCEDURE AnimatePalette(dstWindow: WindowPtr; srcCTab: CTabHandle; srcIndex, dstEntry, dstLength: Integer);
            // Inside Macintosh Volume VI, 20-22
            // AnimatePalette ($AA9A): Copies dstLength entries from CTab[srcIndex..] into window palette[dstEntry..]; reactivates after per IM:VI 20-22. Negative span inputs are treated as a no-op.
            (true, 0x29A) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_length = bus.read_word(sp);
                let dst_entry = bus.read_word(sp + 2);
                let src_index = bus.read_word(sp + 4);
                let src_ctab = bus.read_long(sp + 6);
                let window = bus.read_long(sp + 10);
                if trace_palette_enabled() {
                    eprintln!(
                        "[PALETTE] AnimatePalette window=${:08X} srcCTab=${:08X} srcIndex={} dstEntry={} dstLength={}",
                        window, src_ctab, src_index, dst_entry, dst_length
                    );
                }
                // Guard the signed Pascal INTEGER span before the unsigned loop
                // logic can treat a negative value as a huge copy count.
                if (dst_length as i16) < 0 || (dst_entry as i16) < 0 || (src_index as i16) < 0 {
                    cpu.write_reg(Register::A7, sp + 14);
                    return Some(Ok(()));
                }
                let palette = self.window_palette_handle(window);
                let palette_ptr = Self::palette_ptr(bus, palette);
                let src_ctab_ptr = if src_ctab != 0 {
                    bus.read_long(src_ctab)
                } else {
                    0
                };
                if palette_ptr != 0 && src_ctab_ptr != 0 {
                    let max_src = u32::from(bus.read_word(src_ctab_ptr + 6)) + 1;
                    let max_dst = u32::from(Self::palette_entry_count(bus, palette));
                    for offset in 0..u32::from(dst_length) {
                        let src = u32::from(src_index) + offset;
                        let dst = u32::from(dst_entry) + offset;
                        if src >= max_src || dst >= max_dst {
                            break;
                        }
                        let src_entry = src_ctab_ptr + 8 + src * 8;
                        let rgb = [
                            bus.read_word(src_entry + 2),
                            bus.read_word(src_entry + 4),
                            bus.read_word(src_entry + 6),
                        ];
                        let (_, usage, tolerance) = Self::read_palette_color_info(
                            bus, palette, dst as u16,
                        )
                        .unwrap_or(([0, 0, 0], PM_TOLERANT, 0));
                        Self::write_palette_color_info(
                            bus,
                            palette_ptr,
                            dst,
                            rgb,
                            usage,
                            tolerance,
                        );
                    }
                    self.activate_associated_palette_for_window(bus, window);
                }
                cpu.write_reg(Register::A7, sp + 14);
                Ok(())
            }

            // GetEntryColor ($AA9B)
            // Returns the RGB value of a palette entry.
            //
            // MPW Universal Headers `Palettes.h` declaration:
            //   EXTERN_API(void) GetEntryColor(PaletteHandle srcPalette,
            //       short srcEntry, RGBColor *dstRGB) ONEWORDINLINE(0xAA9B);
            // PROCEDURE GetEntryColor(srcPalette: PaletteHandle; srcEntry: Integer; VAR dstRGB: RGBColor);
            // Inside Macintosh Volume VI, 20-24
            // GetEntryColor ($AA9B): Reads RGB at palette[srcEntry] into dstRGB VAR per IM:VI 20-24
            (true, 0x29B) => {
                let sp = cpu.read_reg(Register::A7);
                let rgb_ptr = bus.read_long(sp);
                let entry = bus.read_word(sp + 4) as i16;
                let palette = bus.read_long(sp + 6);
                if palette != 0 && entry >= 0 {
                    if let Some((rgb, _, _)) =
                        Self::read_palette_color_info(bus, palette, entry as u16)
                    {
                        bus.write_word(rgb_ptr, rgb[0]);
                        bus.write_word(rgb_ptr + 2, rgb[1]);
                        bus.write_word(rgb_ptr + 4, rgb[2]);
                    }
                }
                cpu.write_reg(Register::A7, sp + 10);
                Ok(())
            }

            // SetEntryColor ($AA9C)
            // Updates the RGB value of a palette entry.
            // PROCEDURE SetEntryColor(dstPalette: PaletteHandle; dstEntry: Integer; srcRGB: RGBColor);
            // Inside Macintosh Volume VI, 20-24
            // SetEntryColor ($AA9C): Writes srcRGB into palette[dstEntry] preserving usage/tolerance per IM:VI 20-24
            (true, 0x29C) => {
                let sp = cpu.read_reg(Register::A7);
                let entry = bus.read_word(sp + 4) as i16;
                let palette = bus.read_long(sp + 6);
                let palette_ptr = Self::palette_ptr(bus, palette);
                if palette_ptr != 0 && entry >= 0 {
                    if let Some((_, usage, tolerance)) =
                        Self::read_palette_color_info(bus, palette, entry as u16)
                    {
                        // Only touch the source RGB pointer once the palette
                        // entry has already been validated. The out-of-range
                        // path is a no-op and should not depend on the caller's
                        // RGB pointer being readable.
                        let rgb_ptr = bus.read_long(sp);
                        let rgb = [
                            bus.read_word(rgb_ptr),
                            bus.read_word(rgb_ptr + 2),
                            bus.read_word(rgb_ptr + 4),
                        ];
                        Self::write_palette_color_info(
                            bus,
                            palette_ptr,
                            entry as u32,
                            rgb,
                            usage,
                            tolerance,
                        );
                    }
                }
                cpu.write_reg(Register::A7, sp + 10);
                Ok(())
            }

            // GetEntryUsage ($AA9D)
            // Returns the usage and tolerance values of a palette entry.
            //
            // MPW Universal Headers `Palettes.h` declaration:
            //   EXTERN_API(void) GetEntryUsage(PaletteHandle srcPalette,
            //       short srcEntry, short* dstUsage,
            //       short* dstTolerance) ONEWORDINLINE(0xAA9D);
            // PROCEDURE GetEntryUsage(srcPalette: PaletteHandle; srcEntry: Integer; VAR dstUsage, dstTolerance: Integer);
            // Inside Macintosh Volume VI, 20-25
            // GetEntryUsage ($AA9D): Reads usage + tolerance for palette[srcEntry] into VARs per IM:VI 20-25
            (true, 0x29D) => {
                let sp = cpu.read_reg(Register::A7);
                let tolerance_ptr = bus.read_long(sp);
                let usage_ptr = bus.read_long(sp + 4);
                let entry = bus.read_word(sp + 8) as i16;
                let palette = bus.read_long(sp + 10);
                if let Some((_, usage, tolerance)) =
                    Self::read_palette_color_info(bus, palette, entry as u16)
                {
                    bus.write_word(usage_ptr, usage as u16);
                    bus.write_word(tolerance_ptr, tolerance as u16);
                }
                cpu.write_reg(Register::A7, sp + 14);
                Ok(())
            }

            // SetEntryUsage ($AA9E)
            // Updates the usage and tolerance values of a palette entry.
            //
            // MPW Universal Headers `Palettes.h` declaration:
            //   EXTERN_API(void) SetEntryUsage(PaletteHandle dstPalette,
            //       short dstEntry, short srcUsage,
            //       short srcTolerance) ONEWORDINLINE(0xAA9E);
            // PROCEDURE SetEntryUsage(dstPalette: PaletteHandle; dstEntry: Integer; srcUsage, srcTolerance: Integer);
            // Inside Macintosh Volume VI, 20-25
            // SetEntryUsage ($AA9E): Writes usage + tolerance for palette[dstEntry] preserving RGB. Inside Macintosh V/VI documents -1 as a no-change sentinel, but BasiliskII System 7.5.3 writes the pair verbatim; Systemless matches BasiliskII here.
            (true, 0x29E) => {
                let sp = cpu.read_reg(Register::A7);
                let src_tolerance = bus.read_word(sp) as i16;
                let src_usage = bus.read_word(sp + 2) as i16;
                let entry = bus.read_word(sp + 4) as i16;
                let palette = bus.read_long(sp + 6);
                let palette_ptr = Self::palette_ptr(bus, palette);
                if palette_ptr != 0 && entry >= 0 {
                    if let Some((rgb, _, _)) =
                        Self::read_palette_color_info(bus, palette, entry as u16)
                    {
                        Self::write_palette_color_info(
                            bus,
                            palette_ptr,
                            entry as u32,
                            rgb,
                            src_usage,
                            src_tolerance,
                        );
                    }
                }
                cpu.write_reg(Register::A7, sp + 10);
                Ok(())
            }

            // CTab2Palette ($AA9F)
            // Copies a color table into an existing palette.
            // PROCEDURE CTab2Palette(srcCTab: CTabHandle; dstPalette: PaletteHandle; srcUsage, srcTolerance: Integer);
            // Inside Macintosh Volume VI, 20-23
            // CTab2Palette ($AA9F): Copies CTab entries into dstPalette with given usage + tolerance, resizing dstPalette to match srcCTab first when needed, per IM:VI 20-24
            (true, 0x29F) => {
                let sp = cpu.read_reg(Register::A7);
                let src_tolerance = bus.read_word(sp) as i16;
                let src_usage = bus.read_word(sp + 2) as i16;
                let palette = bus.read_long(sp + 4);
                let src_ctab = bus.read_long(sp + 8);
                if trace_palette_enabled() {
                    eprintln!(
                        "[PALETTE] CTab2Palette srcCTab=${:08X} dstPalette=${:08X} usage={} tolerance={}",
                        src_ctab, palette, src_usage, src_tolerance
                    );
                }
                self.copy_ctab_to_palette(bus, src_ctab, palette, src_usage, src_tolerance);
                cpu.write_reg(Register::A7, sp + 12);
                Ok(())
            }

            // Palette2CTab ($AAA0)
            // Copies palette colors into an existing color table.
            // PROCEDURE Palette2CTab(srcPalette: PaletteHandle; dstCTab: CTabHandle);
            // Inside Macintosh Volume VI, 20-23
            // Palette2CTab ($AAA0): Copies palette entries into dstCTab with fresh ct_seed, resizing dstCTab to match srcPalette first when needed, per IM:VI 20-24
            (true, 0x2A0) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_ctab = bus.read_long(sp);
                let palette = bus.read_long(sp + 4);
                if trace_palette_enabled() {
                    eprintln!(
                        "[PALETTE] Palette2CTab srcPalette=${:08X} dstCTab=${:08X}",
                        palette, dst_ctab
                    );
                }
                self.copy_palette_to_ctab(bus, palette, dst_ctab, "Palette2CTab");
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // ========== Scroll ==========

            // ScrollRect ($A8EF)
            // Shifts pixels within a rectangle by (dh, dv) and returns the
            // exposed region in updateRgn.
            // PROCEDURE ScrollRect(r: Rect; dh, dv: INTEGER; updateRgn: RgnHandle);
            // Imaging With QuickDraw 1994, 3-112
            // ScrollRect ($A8EF): Shifts pixels within rect by (dh, dv), fills exposed area, sets updateRgn. Supports 1bpp and 8bpp
            (true, 0x0EF) => {
                self.debug_scroll_rect_count = self.debug_scroll_rect_count.saturating_add(1);
                let sp = cpu.read_reg(Register::A7);
                // Stack (Pascal calling convention, pushed right-to-left):
                //   SP+0 : updateRgn  (4 bytes, RgnHandle)
                //   SP+4 : dv         (2 bytes, INTEGER)
                //   SP+6 : dh         (2 bytes, INTEGER)
                //   SP+8 : rect_ptr   (4 bytes, Ptr to Rect)
                // Total = 12 bytes
                let update_rgn = bus.read_long(sp);
                let dv = bus.read_word(sp + 4) as i16;
                let dh = bus.read_word(sp + 6) as i16;
                let rect_ptr = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);

                let top = bus.read_word(rect_ptr) as i16;
                let left = bus.read_word(rect_ptr + 2) as i16;
                let bottom = bus.read_word(rect_ptr + 4) as i16;
                let right = bus.read_word(rect_ptr + 6) as i16;
                self.debug_scroll_rect_last_rect = (top, left, bottom, right);
                self.debug_scroll_rect_last_delta = (dh, dv);
                self.debug_scroll_rect_last_changed_bytes = 0;
                if dh != 0 || dv != 0 {
                    self.debug_scroll_rect_nonzero_delta_count =
                        self.debug_scroll_rect_nonzero_delta_count.saturating_add(1);
                }

                let w = (right - left) as i32;
                let h = (bottom - top) as i32;
                if w <= 0 || h <= 0 {
                    return Some(Ok(()));
                }

                // Resolve port pixel geometry from the dispatcher's
                // current_port (kept in sync with the THE_PORT low-mem
                // global at 0x09DA by set_current_port_state).
                let the_port = self.current_port;
                let port_top;
                let port_left;
                let base_addr;
                let row_bytes_raw;
                let is_color;

                let rb_word = bus.read_word(the_port + 0x06);
                if rb_word & 0xC000 != 0 {
                    // CGrafPort — portPixMap is a handle at offset +2
                    is_color = true;
                    let pm_handle = bus.read_long(the_port + 0x02);
                    let pm = bus.read_long(pm_handle);
                    if pm == 0 {
                        return Some(Ok(()));
                    }
                    base_addr = bus.read_long(pm);
                    row_bytes_raw = bus.read_word(pm + 4) & 0x3FFF;
                    let bounds_top = bus.read_word(pm + 6) as i16;
                    let bounds_left = bus.read_word(pm + 8) as i16;
                    port_top = bounds_top;
                    port_left = bounds_left;
                } else {
                    // Classic GrafPort — portBits at offset +2
                    is_color = false;
                    base_addr = bus.read_long(the_port + 0x02);
                    row_bytes_raw = bus.read_word(the_port + 0x06);
                    port_top = bus.read_word(the_port + 0x08) as i16;
                    port_left = bus.read_word(the_port + 0x0A) as i16;
                }

                let row_bytes = row_bytes_raw as u32;
                self.debug_scroll_rect_last_port = the_port;
                self.debug_scroll_rect_last_base = base_addr;
                self.debug_scroll_rect_last_row_bytes = row_bytes_raw;
                self.debug_scroll_rect_last_port_bounds_top_left = (port_top, port_left);
                self.debug_scroll_rect_last_is_color = is_color;
                let _bytes_per_pixel: u32 = if is_color { 1 } else { 0 }; // 0 means 1bpp

                if base_addr == self.screen_mode.0 {
                    let mut clip_top = top;
                    let mut clip_left = left;
                    let mut clip_bottom = bottom;
                    let mut clip_right = right;
                    for offset in [24u32, 28u32] {
                        let rgn_handle = bus.read_long(the_port.wrapping_add(offset));
                        if rgn_handle == 0 {
                            continue;
                        }
                        let rgn_ptr = bus.read_long(rgn_handle);
                        if rgn_ptr == 0 {
                            continue;
                        }
                        let rt = bus.read_word(rgn_ptr + 2) as i16;
                        let rl = bus.read_word(rgn_ptr + 4) as i16;
                        let rb = bus.read_word(rgn_ptr + 6) as i16;
                        let rr = bus.read_word(rgn_ptr + 8) as i16;
                        clip_top = clip_top.max(rt);
                        clip_left = clip_left.max(rl);
                        clip_bottom = clip_bottom.min(rb);
                        clip_right = clip_right.min(rr);
                    }
                    if clip_top >= clip_bottom || clip_left >= clip_right {
                        return Some(Ok(()));
                    }
                    self.ensure_dialog_background_saved_for_screen_port(bus, the_port);
                }

                // Read rect pixels into temp buffer.
                //
                // For 1bpp, pixel_row_bytes must cover BYTE-aligned reads
                // from `left & ~7` through `right` (rounded up). The
                // per-pixel bit lookup below maps buf[0] bit 7 to source
                // pixel `left & ~7`, then `left_bit_offset_in_buf` corrects
                // for the sub-byte left edge.
                let wu = w as u32;
                let hu = h as u32;
                let left_bit_offset_in_buf = if is_color { 0 } else { (left as u32) & 7 };
                let pixel_row_bytes = if is_color {
                    wu
                } else {
                    (left_bit_offset_in_buf + wu).div_ceil(8)
                };
                let mut buf = vec![0u8; (pixel_row_bytes * hu) as usize];

                for row in 0..hu {
                    let src_y = (top - port_top) as u32 + row;
                    for col in 0..pixel_row_bytes {
                        let src_x_byte =
                            (left - port_left) as u32 / (if is_color { 1 } else { 8 }) + col;
                        let addr = base_addr + src_y * row_bytes + src_x_byte;
                        buf[(row * pixel_row_bytes + col) as usize] = bus.read_byte(addr);
                    }
                }

                // BackPat updates the dispatcher-level cache (self.bk_pat).
                // sync_port_draw_state mirrors this into classic GrafPort
                // memory (+32) for caller visibility, but the cache is the
                // canonical source used by drawing routines.
                let bg_pat = self.bk_pat;

                // Write pixels back shifted by (dh, dv). 8bpp uses per-byte
                // iteration (each byte = one pixel). 1bpp uses per-pixel
                // iteration so sub-byte horizontal shifts are bit-precise.
                if is_color {
                    for dst_row in 0..h {
                        let src_row = dst_row - dv as i32;
                        let dst_canvas_y = (top as i32 + dst_row) as usize;
                        for dst_col_byte in 0..pixel_row_bytes as i32 {
                            let src_col = dst_col_byte - dh as i32;
                            let pixel_byte =
                                if src_row >= 0 && src_row < h && src_col >= 0 && src_col < w {
                                    buf[(src_row as u32 * pixel_row_bytes + src_col as u32)
                                        as usize]
                                } else {
                                    // Exposed area uses port.bgPat. 8bpp:
                                    // fg_idx=255 for pattern bit set, bg_idx=0
                                    // otherwise — matches the typical 8bpp CLUT
                                    // black/white layout.
                                    let canvas_x = (left + dst_col_byte as i16) as usize;
                                    let source_is_black =
                                        bg_pat[dst_canvas_y & 7] & (1 << (7 - (canvas_x & 7))) != 0;
                                    if source_is_black {
                                        0xFF
                                    } else {
                                        0x00
                                    }
                                };
                            let dy = (top - port_top) as u32 + dst_row as u32;
                            let dx = (left - port_left) as u32 + dst_col_byte as u32;
                            let addr = base_addr + dy * row_bytes + dx;
                            let old = bus.read_byte(addr);
                            if old != pixel_byte {
                                self.debug_scroll_rect_changed_byte_count =
                                    self.debug_scroll_rect_changed_byte_count.saturating_add(1);
                                self.debug_scroll_rect_last_changed_bytes =
                                    self.debug_scroll_rect_last_changed_bytes.saturating_add(1);
                            }
                            bus.write_byte(addr, pixel_byte);
                        }
                    }
                } else {
                    for dst_row in 0..h {
                        let src_row = dst_row - dv as i32;
                        let dst_canvas_y = (top as i32 + dst_row) as usize;
                        for dst_col in 0..w {
                            let src_col = dst_col - dh as i32;
                            let pixel_bit =
                                if src_row >= 0 && src_row < h && src_col >= 0 && src_col < w {
                                    // Shift the bit lookup by `left & 7` so that
                                    // src_col=0 maps to source pixel `left`, not
                                    // to the byte-aligned floor.
                                    let src_pixel_in_row = left_bit_offset_in_buf + src_col as u32;
                                    let src_byte_in_row = src_pixel_in_row / 8;
                                    let src_bit_in_byte = 7 - (src_pixel_in_row & 7) as u8;
                                    let buf_byte = buf[(src_row as u32 * pixel_row_bytes
                                        + src_byte_in_row)
                                        as usize];
                                    (buf_byte >> src_bit_in_byte) & 1
                                } else {
                                    // Exposed area fills with the port's bgPat
                                    // (per IM:I I-178). The pattern is tiled in
                                    // CANVAS coordinates so adjacent scrolls line
                                    // up.
                                    let canvas_x = (left + dst_col as i16) as usize;
                                    if bg_pat[dst_canvas_y & 7] & (1 << (7 - (canvas_x & 7))) != 0 {
                                        1
                                    } else {
                                        0
                                    }
                                };
                            let dy = (top - port_top) as u32 + dst_row as u32;
                            let dst_pixel_x = (left - port_left) as u32 + dst_col as u32;
                            let dst_byte_addr = base_addr + dy * row_bytes + dst_pixel_x / 8;
                            let dst_bit = 7 - (dst_pixel_x & 7) as u8;
                            let cur = bus.read_byte(dst_byte_addr);
                            let new = if pixel_bit != 0 {
                                cur | (1 << dst_bit)
                            } else {
                                cur & !(1u8 << dst_bit)
                            };
                            if cur != new {
                                self.debug_scroll_rect_changed_byte_count =
                                    self.debug_scroll_rect_changed_byte_count.saturating_add(1);
                                self.debug_scroll_rect_last_changed_bytes =
                                    self.debug_scroll_rect_last_changed_bytes.saturating_add(1);
                            }
                            bus.write_byte(dst_byte_addr, new);
                        }
                    }
                }

                // Set updateRgn to the exposed rectangle.
                if update_rgn != 0 {
                    let rgn_ptr = bus.read_long(update_rgn);
                    if rgn_ptr != 0 {
                        // IM:I I-178: updateRgn is set to the vacated area.
                        // Clamp to the scrolled rect so oversize deltas
                        // (|dh|>=width, |dv|>=height) produce a full-rect
                        // update region rather than spilling past bounds.
                        let (exp_top, exp_left, exp_bottom, exp_right) = if dv > 0 {
                            let exp_bottom =
                                (top as i32 + dv as i32).clamp(top as i32, bottom as i32) as i16;
                            (top, left, exp_bottom, right)
                        } else if dv < 0 {
                            let exp_top =
                                (bottom as i32 + dv as i32).clamp(top as i32, bottom as i32) as i16;
                            (exp_top, left, bottom, right)
                        } else if dh > 0 {
                            let exp_right =
                                (left as i32 + dh as i32).clamp(left as i32, right as i32) as i16;
                            (top, left, bottom, exp_right)
                        } else if dh < 0 {
                            let exp_left =
                                (right as i32 + dh as i32).clamp(left as i32, right as i32) as i16;
                            (top, exp_left, bottom, right)
                        } else {
                            (0, 0, 0, 0)
                        };
                        // Rectangular region: 10 bytes
                        bus.write_word(rgn_ptr, 10); // rgnSize
                        bus.write_word(rgn_ptr + 2, exp_top as u16);
                        bus.write_word(rgn_ptr + 4, exp_left as u16);
                        bus.write_word(rgn_ptr + 6, exp_bottom as u16);
                        bus.write_word(rgn_ptr + 8, exp_right as u16);
                    }
                }

                if base_addr == self.screen_mode.0 && self.debug_scroll_rect_last_changed_bytes > 0
                {
                    self.refresh_visible_dialog_snapshot_after_bulk_port_draw(bus, the_port);
                }

                Ok(())
            }

            // ========== Color QuickDraw / GDevice ==========

            // GetDeviceList ($AA29) → GDHandle
            // GetDeviceList ($AA29): Returns main GDevice handle, triggers color upgrade
            (true, 0x229) => {
                let sp = cpu.read_reg(Register::A7);
                let gdh = self.ensure_main_gdevice(bus);
                bus.write_long(sp, gdh);
                Ok(())
            }

            // GetMaxDevice ($AA27): (Rect) → GDHandle
            // FUNCTION GetMaxDevice(globalRect: Rect): GDHandle;
            // Inside Macintosh Volume VI 1991, pp. 21-21..21-22:
            // returns the deepest device intersecting globalRect.
            //
            // HLE currently models a single active screen device. An
            // intersecting query resolves to that main GDevice handle;
            // a rectangle that misses the only screen returns NIL.
            (true, 0x227) => {
                let sp = cpu.read_reg(Register::A7);
                let rect_ptr = bus.read_long(sp);
                let gdh = self.ensure_main_gdevice(bus);
                let (_, _, screen_w, screen_h, _) = self.screen_mode;
                let intersects = if rect_ptr != 0 {
                    let top = bus.read_word(rect_ptr) as i16;
                    let left = bus.read_word(rect_ptr + 2) as i16;
                    let bottom = bus.read_word(rect_ptr + 4) as i16;
                    let right = bus.read_word(rect_ptr + 6) as i16;
                    let screen_right = screen_w as i16;
                    let screen_bottom = screen_h as i16;
                    top < screen_bottom && left < screen_right && bottom > 0 && right > 0
                } else {
                    false
                };
                bus.write_long(sp + 4, if intersects { gdh } else { 0 });
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // GetMainDevice (0xAA2A)
            // Returns a handle to the gDevice that has the menu bar
            // on it (the main screen's GDevice record).
            // FUNCTION GetMainDevice: GDHandle;
            // Inside Macintosh Volume V (1986), p. V-124
            //   "The GetMainDevice function returns the handle of
            //    the gDevice that has the menu bar on it."
            // Tool-bit Pascal FUNCTION pop-0 + 4-byte GDHandle result
            // slot calling convention: caller pre-pushes a 4-byte
            // result slot (A7 -= 4), the trap writes the GDHandle to
            // [A7] without popping any argument frame, and the caller
            // pops the 4-byte slot afterwards. Net A7 zero across the
            // C-level call. Engines-agree witness: pop-0 + non-NIL
            // GDHandle in result slot. See the
            // `aa2a_aa32_getmaindevice_getgdevice_strict` catalogue
            // fixture for the BasiliskII-baked sibling-pair runtime
            // proof.
            (true, 0x22A) => {
                let sp = cpu.read_reg(Register::A7);
                let gdh = self.ensure_main_gdevice(bus);
                bus.write_long(sp, gdh);
                Ok(())
            }

            // GetNextDevice ($AA2B): GDHandle → GDHandle
            // Imaging With QuickDraw 1994, p.5-28: returns the next GDevice
            // in the device list, or NIL if there are no more entries.
            // The GDevice structure defines the link field `gdNextGD`
            // at offset +30 (Imaging With QuickDraw 1994, p.5-16).
            (true, 0x22B) => {
                let sp = cpu.read_reg(Register::A7);
                let cur_handle = bus.read_long(sp);
                // Read gdNextGD from the GDevice record
                let next = if cur_handle != 0 {
                    let gd_ptr = bus.read_long(cur_handle);
                    if gd_ptr != 0 {
                        bus.read_long(gd_ptr + 30) // gdNextGD
                    } else {
                        0
                    }
                } else {
                    0
                };
                if trace_gdevice_traps_enabled() {
                    eprintln!("[TRAP] GetNextDevice(${:08X}) → ${:08X}", cur_handle, next);
                }
                bus.write_long(sp + 4, next); // return value
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // TestDeviceAttribute ($AA2C): (GDHandle, INTEGER) → BOOLEAN
            // TestDeviceAttribute ($AA2C): Tests individual flag bit
            (true, 0x22C) => {
                let sp = cpu.read_reg(Register::A7);
                let attribute = bus.read_word(sp);
                let gd_handle = bus.read_long(sp + 2);
                let bit = 1u16.checked_shl(attribute as u32);
                let result = if gd_handle != 0 {
                    let gd_ptr = bus.read_long(gd_handle);
                    if gd_ptr != 0 {
                        let flags = bus.read_word(gd_ptr + 20);
                        let matched = bit.map_or(false, |bit| (flags & bit) != 0);
                        if trace_gdevice_traps_enabled() {
                            eprintln!(
                                "[TRAP] TestDeviceAttribute(gdh=${:08X}, attr={}) flags=${:04X} → {}",
                                gd_handle, attribute, flags, matched
                            );
                        }
                        if matched {
                            0xFFFFu16
                        } else {
                            0u16
                        }
                    } else {
                        if trace_gdevice_traps_enabled() {
                            eprintln!(
                                "[TRAP] TestDeviceAttribute(gdh=${:08X}, attr={}) → NULL ptr",
                                gd_handle, attribute
                            );
                        }
                        0u16
                    }
                } else {
                    if trace_gdevice_traps_enabled() {
                        eprintln!("[TRAP] TestDeviceAttribute(NULL, attr={})", attribute);
                    }
                    0u16
                };
                bus.write_word(sp + 6, result);
                cpu.write_reg(Register::A7, sp + 6);
                Ok(())
            }

            // SetDeviceAttribute ($AA2D)
            // Sets an attribute bit in a GDevice record's gdFlags field.
            // PROCEDURE SetDeviceAttribute(gdh: GDHandle; attribute: INTEGER; value: BOOLEAN);
            // Inside Macintosh Volume V 1986, p. V-124; Imaging With QuickDraw 1994, p. 5-22
            // SetDeviceAttribute ($AA2D): Toggles attribute bit in gdFlags ($GDH+20) per IM:V V-124 / Imaging 5-22
            (true, 0x22D) => {
                let sp = cpu.read_reg(Register::A7);
                // Pascal BOOLEAN lives at the HIGH byte of its 2-byte
                // stack slot (the even offset); MPW C leaves an
                // uninitialised garbage byte at the odd offset. See
                // window.rs:813 for the same convention on NewWindow.
                // Inside Macintosh Volume V, V-238 (Pascal calling
                // convention).
                let value = bus.read_byte(sp);
                let attribute = bus.read_word(sp + 2);
                let gd_handle = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                if gd_handle != 0 {
                    let gd_ptr = bus.read_long(gd_handle);
                    if gd_ptr != 0 {
                        if let Some(bit) = 1u16.checked_shl(attribute as u32) {
                            let mut flags = bus.read_word(gd_ptr + 20);
                            if value != 0 {
                                flags |= bit;
                            } else {
                                flags &= !bit;
                            }
                            bus.write_word(gd_ptr + 20, flags);
                        }
                    }
                }
                Ok(())
            }

            // GetGDevice (0xAA32)
            // Returns a handle to the current gDevice (TheGDevice
            // low-mem global).
            // FUNCTION GetGDevice: GDHandle;
            // Inside Macintosh Volume V (1986), p. V-123
            //   "The GetGDevice routine returns a handle to the
            //    current gDevice. ... A handle to the currently
            //    active device is kept in the global variable
            //    TheGDevice."
            // Tool-bit Pascal FUNCTION pop-0 + 4-byte GDHandle result
            // slot calling convention: caller pre-pushes a 4-byte
            // result slot, the trap writes the GDHandle to [A7]
            // without popping any argument frame, and the caller pops
            // the 4-byte slot afterwards. Engines-agree witness: pop-0
            // + non-NIL GDHandle in result slot. See the
            // `aa2a_aa32_getmaindevice_getgdevice_strict` catalogue
            // fixture for the BasiliskII-baked sibling-pair runtime
            // proof.
            (true, 0x232) => {
                let sp = cpu.read_reg(Register::A7);
                let gdh = if self.current_gdevice != 0 {
                    self.current_gdevice
                } else {
                    self.ensure_main_gdevice(bus)
                };
                bus.write_long(sp, gdh);
                Ok(())
            }

            // SetGDevice ($AA31): GDHandle → void
            // PROCEDURE SetGDevice(gdh: GDHandle);
            // Imaging With QuickDraw 1994, p.5-24.
            //
            // Stores the new device both in our dispatcher and in the
            // documented low-mem TheGDevice global ($0CC8) so guest 68k
            // code that reads the global directly (instead of calling
            // GetGDevice afterwards) sees the update. Without this
            // mirror, a SetGDevice-then-deref-low-mem sequence reads
            // the old handle and games that walk gdLink/gdNextGD off
            // it land on garbage pointers, producing the
            // "MOVE.B (A0)+,D2 with A0=NIL" landing pattern we see in
            // Centaurian's launch path.
            // Imaging With QuickDraw 1994 pp.5-4 and 5-24: TheGDevice keeps
            // the current device handle.
            (true, 0x231) => {
                let sp = cpu.read_reg(Register::A7);
                let gdh = bus.read_long(sp);
                self.current_gdevice = gdh;
                bus.write_long(0x0CC8, gdh);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // DisposeGDevice ($AA30)
            // Disposes of a GDevice record and its associated data structures.
            // PROCEDURE DisposeGDevice(gdh: GDHandle);
            // Imaging With QuickDraw 1994, p. 5-25
            // DisposeGDevice ($AA30): Pops 4 bytes (gdh); does not free the GDevice block (allocator doesn't support free) per Imaging With QuickDraw 1994, 5-25
            (true, 0x230) => {
                let sp = cpu.read_reg(Register::A7);
                let _gdh = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                // Stub: don't actually free since our allocator doesn't support it well
                Ok(())
            }

            // NewGDevice ($AA2F)
            // FUNCTION NewGDevice(refNum: INTEGER; mode: LONGINT): GDHandle;
            // Imaging With QuickDraw 1994, p. 5-25
            // NewGDevice ($AA2F): Allocates a detached GDevice shell with its
            // own GDHandle/PixMap/CTab, then applies the minimal InitGDevice
            // field writes Systemless models (gdRefNum/gdMode + gdDevType bit).
            //
            // HLE compromise: Systemless does not emulate real video-driver mode
            // programming or insert app-created devices into DeviceList. The
            // returned GDevice is a detached shell cloned from the main screen's
            // PixMap/CTab shape so callers can inspect a stable, non-NIL record
            // without aliasing the main screen device.
            (true, 0x22F) => {
                let sp = cpu.read_reg(Register::A7);
                let mode = bus.read_long(sp);
                let ref_num = bus.read_word(sp + 4) as i16;
                let gdh = self.allocate_detached_gdevice(bus);
                if mode != 0xFFFF_FFFF {
                    self.init_gdevice_minimal(bus, gdh, ref_num, mode);
                }
                bus.write_long(sp + 6, gdh);
                cpu.write_reg(Register::A7, sp + 6);
                Ok(())
            }

            // Index2Color ($AA34)
            // PROCEDURE Index2Color (index: LONGINT; VAR rgb: RGBColor);
            // Inside Macintosh Volume V, V-141 (Color Manager — Color
            // Manager Routines — Color Conversion)
            //
            // "The Index2Color routine finds the RGB color corresponding
            //  to a given color table index. The desired pixel value is
            //  passed and the corresponding RGB value is returned in
            //  RGB. The routine takes a longint, which should be a
            //  pixel value padded with zeros in the high word."
            //
            // Stack (Pascal): SP+0=rgb_ptr(4), SP+4=index(4). Pops 8.
            // No FUNCTION result slot.
            //
            // Paired catalogue proof:
            //   aa34_aa36_index2color_realcolor_strict
            //   (B1 single-call + B2 5-call composition; engines-agree
            //    on the Pascal PROCEDURE pop-8 calling convention,
            //    engines-divergent on the absolute RGB read from the
            //    active gDevice's CLUT.)
            // Index2Color ($AA34): Reads RGB from current GDevice's ctab, IM:V V-141
            (true, 0x234) => {
                let sp = cpu.read_reg(Register::A7);
                // Stack (Pascal): SP+0: rgb ptr (4), SP+4: index (4)
                let rgb_ptr = bus.read_long(sp);
                let index = bus.read_long(sp + 4) as usize;

                // Look up in current GDevice's color table
                let the_gdevice = bus.read_long(0x0CC8); // TheGDevice
                let gd_ptr = bus.read_long(the_gdevice);
                let pixmap_handle = bus.read_long(gd_ptr + 22); // gdPMap
                let pixmap_ptr = bus.read_long(pixmap_handle);
                let ctab_handle = bus.read_long(pixmap_ptr + 42); // pmTable
                let ctab_ptr = bus.read_long(ctab_handle);
                let ct_size = bus.read_word(ctab_ptr + 6) as usize; // ctSize = nEntries - 1

                if index <= ct_size {
                    let entry = ctab_ptr + 8 + (index as u32) * 8;
                    let r = bus.read_word(entry + 2);
                    let g = bus.read_word(entry + 4);
                    let b = bus.read_word(entry + 6);
                    bus.write_word(rgb_ptr, r);
                    bus.write_word(rgb_ptr + 2, g);
                    bus.write_word(rgb_ptr + 4, b);
                } else {
                    // Out of range — return black
                    bus.write_word(rgb_ptr, 0);
                    bus.write_word(rgb_ptr + 2, 0);
                    bus.write_word(rgb_ptr + 4, 0);
                }

                // Pop params: index(4) + rgb(4) = 8 bytes
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // InvertColor ($AA35)
            // PROCEDURE InvertColor (VAR theColor: RGBColor);
            // Inside Macintosh Volume V, V-141 (Color Manager — Color
            // Manager Routines — Color Conversion)
            //
            // "The InvertColor routine finds the complement of an absolute
            //  color, using the list of complement procedures in the
            //  current device record. The default complement procedure
            //  uses the 1's complement of each component of the requested
            //  color."
            //
            // Stack (Pascal): SP+0=rgb_ptr(4). VAR parameter — read RGB
            // through the pointer, complement each channel, write back.
            // Pops 4. No return value.
            //
            // Paired catalogue proof:
            //   aa33_aa35_color2index_invertcolor_strict
            //   (B3 single-call + B4 5-call composition; both engines
            //    take the default ROM 1's-complement path on a fresh
            //    boot — engines-agree on calling convention AND on the
            //    ~R, ~G, ~B output bytes per IM:V V-141.)
            // InvertColor ($AA35): 1's complement of each channel of the VAR RGBColor, IM:V V-141
            (true, 0x235) => {
                let sp = cpu.read_reg(Register::A7);
                let rgb_ptr = bus.read_long(sp);
                if rgb_ptr != 0 {
                    let r = bus.read_word(rgb_ptr);
                    let g = bus.read_word(rgb_ptr + 2);
                    let b = bus.read_word(rgb_ptr + 4);
                    bus.write_word(rgb_ptr, !r);
                    bus.write_word(rgb_ptr + 2, !g);
                    bus.write_word(rgb_ptr + 4, !b);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // RealColor ($AA36)
            // FUNCTION RealColor (color: RGBColor): BOOLEAN;
            // Inside Macintosh Volume V, V-141
            //
            // "The RealColor routine tells whether a given absolute color
            //  actually exists in the current device's color table. This
            //  decision is based on the current resolution of the inverse
            //  table. For example, if the current iTabRes is four,
            //  RealColor returns TRUE if there exists a color that exactly
            //  matches the top four bits of red, green, and blue."
            //
            // Stack (Pascal): SP+0=rgb_ptr(4). Returns BOOLEAN at SP+4
            // ($0100 = TRUE high-byte, $0000 = FALSE). Pops to SP+4.
            //
            // Systemless uses the default 4-bit resolution: a color is
            // "real" iff some entry in device_clut has the same top
            // 4 bits per channel.
            //
            // Paired catalogue proof:
            //   aa34_aa36_index2color_realcolor_strict
            //   (B3 single-call + B4 5-call composition; engines-agree
            //    on the Pascal FUNCTION pop-4 + 2-byte BOOLEAN result
            //    slot calling convention, engines-divergent on the
            //    absolute BOOLEAN return value since BII's ROM 8bpp
            //    CLUT differs from Systemless's device_clut.)
            //
            // Regression coverage:
            //   realcolor_true_for_color_present_in_device_clut
            //   realcolor_false_when_color_not_in_device_clut
            // RealColor ($AA36): TRUE if device_clut has an entry matching top-4-bits-per-channel, IM:V V-141
            (true, 0x236) => {
                let sp = cpu.read_reg(Register::A7);
                let rgb_ptr = bus.read_long(sp);
                let want_r = bus.read_word(rgb_ptr) & 0xF000;
                let want_g = bus.read_word(rgb_ptr + 2) & 0xF000;
                let want_b = bus.read_word(rgb_ptr + 4) & 0xF000;
                let mut found = false;
                for entry in self.device_clut.iter() {
                    if (entry[0] & 0xF000) == want_r
                        && (entry[1] & 0xF000) == want_g
                        && (entry[2] & 0xF000) == want_b
                    {
                        found = true;
                        break;
                    }
                }
                bus.write_word(sp + 4, if found { 0x0100 } else { 0x0000 });
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // PaletteDispatch ($AAA2)
            // Dispatcher for Palette Manager routines.
            //
            // MPW's Palette.h emits either MOVEQ/MOVE.W #sel,D0 /
            // _PaletteDispatch (selector in D0) -- the real ROM
            // convention. Systemless's legacy handler below assumes
            // selector on top of stack. Fast-path public D0-based
            // selectors so MPW-compiled apps can call them without
            // requiring the legacy stack-selector layout.
            //
            // Inside Macintosh Volume VI:
            //   - Table C-3 (p. C-11): _PaletteDispatch selector $0002
            //     is RestoreDeviceClut.
            //   - Table C-4 (p. C-23/C-24): public selectors include
            //     $0003 ResizePalette, $0015 PMgrVersion, $0417
            //     GetPaletteUpdates, $0616 SetPaletteUpdates, $0A13
            //     SetDepth, $0A14 HasDepth, $1219 GetGray.
            //
            // Legacy stack-selector compatibility path (below) keeps
            // the historical Systemless-internal selector mapping used by
            // older test harnesses.
            (true, 0x2A2) => {
                let sp = cpu.read_reg(Register::A7);

                // D0-based MPW fast paths (MOVEQ / MOVE.W to D0,
                // then trap).
                let d0 = cpu.read_reg(Register::D0);
                let d0_selector = Self::palette_dispatch_selector(d0);
                if d0_selector == 0x0002 {
                    // RestoreDeviceClut (public _PaletteDispatch selector $0002)
                    // PROCEDURE RestoreDeviceClut(gdh: GDHandle);
                    //
                    // IM:VI (1991) Table C-3 + Palette Manager chapter
                    // ("Manipulating Palettes and Color Tables"): selector
                    // $0002 takes a single GDHandle argument; passing NIL
                    // restores all screens.
                    //
                    // Systemless models a single active CLUT device, so both
                    // NIL and the main GDevice restore to the canonical
                    // system 8bpp CLUT.
                    let gdh = bus.read_long(sp);
                    cpu.write_reg(Register::A7, sp + 4);
                    let main = self.ensure_main_gdevice(bus);
                    if gdh == 0 || gdh == main {
                        self.install_application_clut(bus, Self::standard_mac_8bpp_clut());
                    }
                    return Some(Ok(()));
                }
                if d0_selector == 0x0003 {
                    // ResizePalette (public _PaletteDispatch selector $0003)
                    // PROCEDURE ResizePalette(srcPalette: PaletteHandle;
                    //                         size: Integer);
                    // Inside Macintosh Volume VI (1991), p. 20-24 and
                    // selector summary p. 21-28.
                    //
                    // MPW-style stack layout omits the selector word:
                    //   SP+0 size (INTEGER)
                    //   SP+2 srcPalette (PaletteHandle)
                    let size = bus.read_word(sp) as i16;
                    let palette = bus.read_long(sp + 2);
                    self.resize_palette(bus, palette, size);
                    cpu.write_reg(Register::A7, sp + 6);
                    return Some(Ok(()));
                }
                if d0_selector == 0x0417 {
                    // GetPaletteUpdates (public _PaletteDispatch selector $0417)
                    // FUNCTION GetPaletteUpdates(p: PaletteHandle): Integer;
                    // Inside Macintosh Volume VI (1991), p. 20-21; Table C-4.
                    // MPW Universal Headers Palettes.h:
                    //   EXTERN_API(short) GetPaletteUpdates(PaletteHandle p)
                    //   THREEWORDINLINE(0x303C, 0x0417, 0xAAA2);
                    //
                    // D0-based MPW calling convention: selector in D0,
                    // stack holds only the PaletteHandle argument plus the
                    // 2-byte function result slot.
                    let palette = bus.read_long(sp);
                    let updates = self.palette_update_mode(palette);
                    cpu.write_reg(Register::D0, updates as u16 as u32);
                    bus.write_word(sp + 4, updates as u16);
                    cpu.write_reg(Register::A7, sp + 4);
                    return Some(Ok(()));
                }
                if d0_selector == 0x0616 {
                    // SetPaletteUpdates (public _PaletteDispatch selector $0616)
                    // PROCEDURE SetPaletteUpdates(p: PaletteHandle;
                    //                             updates: Integer);
                    // Inside Macintosh Volume VI (1991), p. 20-21; Table C-4.
                    // MPW Universal Headers Palettes.h:
                    //   EXTERN_API(void) SetPaletteUpdates(PaletteHandle p,
                    //                                      short updates)
                    //   THREEWORDINLINE(0x303C, 0x0616, 0xAAA2);
                    //
                    // D0-based MPW calling convention: selector in D0,
                    // stack holds only updates + PaletteHandle.
                    //
                    // Record the requested update mode so the public getter
                    // can return it for this palette handle.
                    let updates = bus.read_word(sp) as i16;
                    let palette = bus.read_long(sp + 2);
                    self.record_palette_updates(palette, updates);
                    cpu.write_reg(Register::A7, sp + 6);
                    return Some(Ok(()));
                }
                if d0_selector == 0x0015 {
                    // PMgrVersion: returns a short Palette Mgr
                    // version. No args on stack. Callee just
                    // populates the 2-byte result slot at SP+0.
                    bus.write_word(sp, 1);
                    return Some(Ok(()));
                }

                // HasDepth / SetDepth use THREEWORDINLINE(0x303C, sel, 0xAAA2)
                // which emits MOVE.W #sel, D0 then _PaletteDispatch.
                // Real ROM convention: selector in D0, no selector word on stack.
                // Stack: SP+0=flags(2), SP+2=whichFlags(2), SP+4=depth(2),
                //        SP+6=gd(4), SP+10=result(2). Imaging With QuickDraw
                // 1994 pp.5-33..5-34; IM:VI VI-21-12.
                if d0_selector == 0x0A14 {
                    // HasDepth (D0-based MPW calling convention)
                    // FUNCTION HasDepth(gd: GDHandle; depth, whichFlags,
                    //                   flags: Integer): Integer;
                    // Imaging With QuickDraw 1994 pp.5-33..5-34
                    let flags = bus.read_word(sp);
                    let which_flags = bus.read_word(sp + 2);
                    let depth = bus.read_word(sp + 4);
                    let mode_id = Self::has_depth_mode_id(depth, which_flags, flags);
                    bus.write_word(sp + 10, mode_id);
                    cpu.write_reg(Register::A7, sp + 10);
                    return Some(Ok(()));
                }
                if d0_selector == 0x0A13 {
                    // SetDepth (D0-based MPW calling convention)
                    // FUNCTION SetDepth(gd: GDHandle; depth, whichFlags,
                    //                   flags: Integer): OSErr;
                    // IM:VI VI-21-12
                    let depth = bus.read_word(sp + 4);
                    let gd = bus.read_long(sp + 6);
                    let result = self.set_depth_or_mode(cpu, bus, gd, depth);
                    bus.write_word(sp + 10, result);
                    cpu.write_reg(Register::A7, sp + 10);
                    cpu.write_reg(Register::D0, result as i32 as u32);
                    return Some(Ok(()));
                }

                let selector = bus.read_word(sp);
                match selector {
                    0x0000 => {
                        cpu.write_reg(Register::A7, sp + 2);
                    }
                    0x0001 => {
                        let src_tolerance = bus.read_word(sp + 2) as i16;
                        let src_usage = bus.read_word(sp + 4) as i16;
                        let src_colors = bus.read_long(sp + 6);
                        let entries = bus.read_word(sp + 10);
                        let palette = self.create_palette_from_ctab(
                            bus,
                            entries,
                            src_colors,
                            src_usage,
                            src_tolerance,
                        );
                        bus.write_long(sp + 12, palette);
                        cpu.write_reg(Register::A7, sp + 12);
                    }
                    0x0002 => {
                        let palette_id = bus.read_word(sp + 2) as i16;
                        let palette = self.copy_palette_resource_exact(bus, palette_id);
                        bus.write_long(sp + 4, palette);
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    0x0003 => {
                        let palette = bus.read_long(sp + 2);
                        if palette != 0 {
                            let palette_ptr = Self::palette_ptr(bus, palette);
                            self.palette_updates.remove(&palette);
                            self.window_palettes
                                .retain(|_, (handle, _)| *handle != palette);
                            if palette_ptr != 0 {
                                bus.free(palette_ptr);
                            }
                            bus.free(palette);
                        }
                        cpu.write_reg(Register::A7, sp + 6);
                    }
                    0x0004 => {
                        let window = bus.read_long(sp + 2);
                        self.activate_associated_palette_for_window(bus, window);
                        cpu.write_reg(Register::A7, sp + 6);
                    }
                    0x0005 => {
                        let updates = bus.read_word(sp + 2) as i16;
                        let palette = bus.read_long(sp + 4);
                        let window = bus.read_long(sp + 8);
                        self.set_window_palette_association(window, palette, updates);
                        self.activate_associated_palette_for_window(bus, window);
                        cpu.write_reg(Register::A7, sp + 12);
                    }
                    0x0006 => {
                        let window = bus.read_long(sp + 2);
                        bus.write_long(sp + 6, self.window_palette_handle(window));
                        cpu.write_reg(Register::A7, sp + 6);
                    }
                    0x000A => {
                        let dst_length = bus.read_word(sp + 2);
                        let dst_entry = bus.read_word(sp + 4);
                        let src_index = bus.read_word(sp + 6);
                        let src_ctab = bus.read_long(sp + 8);
                        let window = bus.read_long(sp + 12);
                        // Guard the signed Pascal INTEGER span before the
                        // unsigned loop logic can treat a negative value as a
                        // huge copy count.
                        if (dst_length as i16) < 0
                            || (dst_entry as i16) < 0
                            || (src_index as i16) < 0
                        {
                            cpu.write_reg(Register::A7, sp + 16);
                            return Some(Ok(()));
                        }
                        let palette = self.window_palette_handle(window);
                        let palette_ptr = Self::palette_ptr(bus, palette);
                        let src_ctab_ptr = if src_ctab != 0 {
                            bus.read_long(src_ctab)
                        } else {
                            0
                        };
                        if palette_ptr != 0 && src_ctab_ptr != 0 {
                            let max_src = u32::from(bus.read_word(src_ctab_ptr + 6)) + 1;
                            let max_dst = u32::from(Self::palette_entry_count(bus, palette));
                            for offset in 0..u32::from(dst_length) {
                                let src = u32::from(src_index) + offset;
                                let dst = u32::from(dst_entry) + offset;
                                if src >= max_src || dst >= max_dst {
                                    break;
                                }
                                let src_entry = src_ctab_ptr + 8 + src * 8;
                                let rgb = [
                                    bus.read_word(src_entry + 2),
                                    bus.read_word(src_entry + 4),
                                    bus.read_word(src_entry + 6),
                                ];
                                let (_, usage, tolerance) =
                                    Self::read_palette_color_info(bus, palette, dst as u16)
                                        .unwrap_or(([0, 0, 0], PM_TOLERANT, 0));
                                Self::write_palette_color_info(
                                    bus,
                                    palette_ptr,
                                    dst,
                                    rgb,
                                    usage,
                                    tolerance,
                                );
                            }
                            self.activate_associated_palette_for_window(bus, window);
                        }
                        cpu.write_reg(Register::A7, sp + 16);
                    }
                    0x000B => {
                        let rgb_ptr = bus.read_long(sp + 2);
                        let entry = bus.read_word(sp + 6) as i16;
                        let palette = bus.read_long(sp + 8);
                        if let Some((rgb, _, _)) =
                            Self::read_palette_color_info(bus, palette, entry as u16)
                        {
                            bus.write_word(rgb_ptr, rgb[0]);
                            bus.write_word(rgb_ptr + 2, rgb[1]);
                            bus.write_word(rgb_ptr + 4, rgb[2]);
                        }
                        cpu.write_reg(Register::A7, sp + 12);
                    }
                    0x000C => {
                        let rgb_ptr = bus.read_long(sp + 2);
                        let entry = bus.read_word(sp + 6) as i16;
                        let palette = bus.read_long(sp + 8);
                        let palette_ptr = Self::palette_ptr(bus, palette);
                        if palette_ptr != 0 && entry >= 0 {
                            let (_, usage, tolerance) =
                                Self::read_palette_color_info(bus, palette, entry as u16)
                                    .unwrap_or(([0, 0, 0], PM_TOLERANT, 0));
                            let rgb = [
                                bus.read_word(rgb_ptr),
                                bus.read_word(rgb_ptr + 2),
                                bus.read_word(rgb_ptr + 4),
                            ];
                            Self::write_palette_color_info(
                                bus,
                                palette_ptr,
                                entry as u32,
                                rgb,
                                usage,
                                tolerance,
                            );
                        }
                        cpu.write_reg(Register::A7, sp + 12);
                    }
                    0x000D => {
                        let tolerance_ptr = bus.read_long(sp + 2);
                        let usage_ptr = bus.read_long(sp + 6);
                        let entry = bus.read_word(sp + 10) as i16;
                        let palette = bus.read_long(sp + 12);
                        if let Some((_, usage, tolerance)) =
                            Self::read_palette_color_info(bus, palette, entry as u16)
                        {
                            bus.write_word(usage_ptr, usage as u16);
                            bus.write_word(tolerance_ptr, tolerance as u16);
                        }
                        cpu.write_reg(Register::A7, sp + 16);
                    }
                    0x000E => {
                        let src_tolerance = bus.read_word(sp + 2) as i16;
                        let src_usage = bus.read_word(sp + 4) as i16;
                        let entry = bus.read_word(sp + 6) as i16;
                        let palette = bus.read_long(sp + 8);
                        let palette_ptr = Self::palette_ptr(bus, palette);
                        if palette_ptr != 0 && entry >= 0 {
                            let (rgb, _, _) =
                                Self::read_palette_color_info(bus, palette, entry as u16)
                                    .unwrap_or(([0, 0, 0], 0, 0));
                            Self::write_palette_color_info(
                                bus,
                                palette_ptr,
                                entry as u32,
                                rgb,
                                src_usage,
                                src_tolerance,
                            );
                        }
                        cpu.write_reg(Register::A7, sp + 12);
                    }
                    0x000F => {
                        let src_tolerance = bus.read_word(sp + 2) as i16;
                        let src_usage = bus.read_word(sp + 4) as i16;
                        let palette = bus.read_long(sp + 6);
                        let src_ctab = bus.read_long(sp + 10);
                        self.copy_ctab_to_palette(bus, src_ctab, palette, src_usage, src_tolerance);
                        cpu.write_reg(Register::A7, sp + 12);
                    }
                    0x0010 => {
                        let dst_ctab = bus.read_long(sp + 2);
                        let palette = bus.read_long(sp + 6);
                        self.copy_palette_to_ctab(bus, palette, dst_ctab, "Palette2CTab-alt");
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    0x0417 => {
                        // GetPaletteUpdates (legacy stack-selector convention)
                        // FUNCTION GetPaletteUpdates(p: PaletteHandle): Integer;
                        // Inside Macintosh Volume VI (1991), p. 20-21; Table C-4.
                        //
                        // Stack:
                        //   SP+0  selector ($0417)
                        //   SP+2  p (PaletteHandle)
                        //   SP+6  result slot (Integer)
                        let palette = bus.read_long(sp + 2);
                        let updates = self.palette_update_mode(palette);
                        cpu.write_reg(Register::D0, updates as u16 as u32);
                        bus.write_word(sp + 6, updates as u16);
                        cpu.write_reg(Register::A7, sp + 6);
                    }
                    0x0616 => {
                        // SetPaletteUpdates (legacy stack-selector convention)
                        // PROCEDURE SetPaletteUpdates(p: PaletteHandle;
                        //                             updates: Integer);
                        // Inside Macintosh Volume VI (1991), p. 20-21; Table C-4.
                        //
                        // Stack:
                        //   SP+0  selector ($0616)
                        //   SP+2  updates
                        //   SP+4  p (PaletteHandle)
                        // Record the requested update mode so the public
                        // getter can return it for this palette handle.
                        let updates = bus.read_word(sp + 2) as i16;
                        let palette = bus.read_long(sp + 4);
                        self.record_palette_updates(palette, updates);
                        cpu.write_reg(Register::A7, sp + 8);
                    }
                    // HasDepth (_PaletteDispatch selector $0A14)
                    // FUNCTION HasDepth(gd: GDHandle; depth, whichFlags,
                    //                   flags: Integer): Integer;
                    // Inside Macintosh Volume VI, VI-21-12
                    //
                    // Stack via PaletteDispatch:
                    //   SP+0  selector ($0A14)
                    //   SP+2  flags
                    //   SP+4  whichFlags
                    //   SP+6  depth
                    //   SP+8  gd (LongInt)
                    //   SP+12 result slot (Integer)
                    //
                    // Imaging With QuickDraw 1994 pp.5-33..5-34:
                    // return 0 when the requested depth is unsupported, and a
                    // nonzero mode ID when supported.
                    //
                    // Systemless exposes the common classic Mac display
                    // depths as device capabilities. 1bpp and 8bpp are wired
                    // to screenBits/screen_mode; other supported depths are
                    // recorded at the GDevice level without changing the
                    // backing framebuffer.
                    0x0A14 => {
                        let flags = bus.read_word(sp + 2);
                        let which_flags = bus.read_word(sp + 4);
                        let depth = bus.read_word(sp + 6);
                        let _gd = bus.read_long(sp + 8);
                        let mode_id = Self::has_depth_mode_id(depth, which_flags, flags);
                        bus.write_word(sp + 12, mode_id);
                        cpu.write_reg(Register::A7, sp + 12);
                    }
                    // SetDepth (_PaletteDispatch selector $0A13)
                    // FUNCTION SetDepth(gd: GDHandle; depth, whichFlags,
                    //                   flags: Integer): OSErr;
                    // Inside Macintosh Volume VI, VI-21-12
                    //
                    // Stack: same layout as HasDepth (10-byte arg block).
                    // Result is OSErr (Integer). Systemless updates screenBits
                    // and screen_mode for the wired 1bpp/8bpp paths, and
                    // records other supported depth requests on the GDevice
                    // without changing the backing framebuffer.
                    //
                    // Regression coverage:
                    //   setdepth_changes_device_depth
                    0x0A13 => {
                        let _flags = bus.read_word(sp + 2);
                        let _which = bus.read_word(sp + 4);
                        let depth = bus.read_word(sp + 6);
                        let gd = bus.read_long(sp + 8);
                        let result = self.set_depth_or_mode(cpu, bus, gd, depth);
                        bus.write_word(sp + 12, result);
                        cpu.write_reg(Register::A7, sp + 12);
                        cpu.write_reg(Register::D0, result as i32 as u32);
                    }
                    _ => {
                        eprintln!("[TRAP] PaletteDispatch unknown selector=${:04X}", selector);
                        cpu.write_reg(Register::A7, sp + 2);
                    }
                }
                Ok(())
            }

            // GetForeColor (0xAA19)
            // Returns the RGB components of the current port's foreground
            // color through a VAR RGBColor pointer (Pascal PROCEDURE pop-4,
            // no result slot).
            // PROCEDURE GetForeColor (VAR color: RGBColor);
            // Inside Macintosh Volume V (1986), p. V-68
            // Strict-bake calling-convention witness:
            // aa19_aa1a_getforecolor_getbackcolor_strict.
            (true, 0x219) => {
                let sp = cpu.read_reg(Register::A7);
                let color_ptr = bus.read_long(sp);
                if color_ptr != 0 {
                    bus.write_word(color_ptr, self.fg_color.0);
                    bus.write_word(color_ptr + 2, self.fg_color.1);
                    bus.write_word(color_ptr + 4, self.fg_color.2);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // GetBackColor (0xAA1A)
            // Returns the RGB components of the current port's background
            // color through a VAR RGBColor pointer (Pascal PROCEDURE pop-4,
            // no result slot).
            // PROCEDURE GetBackColor (VAR color: RGBColor);
            // Inside Macintosh Volume V (1986), p. V-68
            // Strict-bake calling-convention witness:
            // aa19_aa1a_getforecolor_getbackcolor_strict.
            (true, 0x21A) => {
                let sp = cpu.read_reg(Register::A7);
                let color_ptr = bus.read_long(sp);
                if color_ptr != 0 {
                    bus.write_word(color_ptr, self.bg_color.0);
                    bus.write_word(color_ptr + 2, self.bg_color.1);
                    bus.write_word(color_ptr + 4, self.bg_color.2);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // GetCTable ($AA18)
            // Returns a copy of the specified 'clut' resource as a ColorTable.
            // For standard depth IDs (1,2,4,8), returns the system color table.
            // For other IDs, loads the 'clut' resource with that ID.
            // FUNCTION GetCTable(ctID: INTEGER): CTabHandle;
            // Imaging With QuickDraw 1994, p. 4-96
            // GetCTable ($AA18): Returns standard 8bpp CLUT for depth IDs (1,2,4,8); loads 'clut' resource for custom IDs
            (true, 0x218) => {
                let sp = cpu.read_reg(Register::A7);
                let ct_id = bus.read_word(sp) as i16;
                if trace_palette_enabled() {
                    eprintln!(
                        "[PALETTE] GetCTable id={} current_port=${:08X}",
                        ct_id, self.current_port
                    );
                }

                let ct_handle = if ct_id == 1 || ct_id == 2 || ct_id == 4 || ct_id == 8 {
                    self.recent_resource_ctable_fetch = None;
                    // Standard system color table for the given depth.
                    // We always return the 8bpp table (256 entries) for simplicity;
                    // this is correct for 8bpp and a safe superset for lower depths.
                    // Build a full ColorTable record: ctSeed(4) + ctFlags(2) + ctSize(2) + 256 entries(256*8)
                    let std_clut = Self::standard_mac_8bpp_clut();
                    let ct_size: u32 = 8 + 256 * 8;
                    let ct_ptr = bus.alloc(ct_size);
                    // Executor's depth convention (qGWorld.cpp:217,
                    // qColorutil.cpp bw init at :159): a standard system
                    // CTab fetched via GetCTable(depth) has ctSeed=depth.
                    // This lets PICTs authored for the canonical palette
                    // (also stamped ctSeed=depth) identity-copy via the
                    // seed-match gates when the destination
                    // is that same canonical CTab.
                    bus.write_long(ct_ptr, ct_id as u32); // ctSeed = depth
                    bus.write_word(ct_ptr + 4, 0); // ctFlags (0 = pixmap color table)
                    bus.write_word(ct_ptr + 6, 255); // ctSize (entries - 1)
                    for i in 0u32..256 {
                        let entry = ct_ptr + 8 + i * 8;
                        bus.write_word(entry, i as u16);
                        bus.write_word(entry + 2, std_clut[i as usize][0]);
                        bus.write_word(entry + 4, std_clut[i as usize][1]);
                        bus.write_word(entry + 6, std_clut[i as usize][2]);
                    }
                    let h = bus.alloc(4);
                    bus.write_long(h, ct_ptr);
                    h
                } else {
                    // Try to load 'clut' resource with this ID
                    let res_type = *b"clut";
                    if let Some((_, found_ptr)) = self.find_resource_any(res_type, ct_id) {
                        if trace_palette_enabled() {
                            let ct_size = usize::from(bus.read_word(found_ptr + 6))
                                .saturating_add(1)
                                .min(256);
                            let sample = |index: usize| -> [u16; 3] {
                                if index >= ct_size {
                                    return [0, 0, 0];
                                }
                                let entry = found_ptr + 8 + (index as u32) * 8;
                                [
                                    bus.read_word(entry + 2),
                                    bus.read_word(entry + 4),
                                    bus.read_word(entry + 6),
                                ]
                            };
                            let c0 = sample(0);
                            let c1 = sample(1);
                            let c16 = sample(16);
                            let c42 = sample(42);
                            let c128 = sample(128);
                            let c255 = sample(255);
                            eprintln!(
                                "[PALETTE] GetCTable id={} -> resource ptr=${:08X} size={} [0]=({:04X},{:04X},{:04X}) [1]=({:04X},{:04X},{:04X}) [16]=({:04X},{:04X},{:04X}) [42]=({:04X},{:04X},{:04X}) [128]=({:04X},{:04X},{:04X}) [255]=({:04X},{:04X},{:04X})",
                                ct_id,
                                found_ptr,
                                ct_size,
                                c0[0],
                                c0[1],
                                c0[2],
                                c1[0],
                                c1[1],
                                c1[2],
                                c16[0],
                                c16[1],
                                c16[2],
                                c42[0],
                                c42[1],
                                c42[2],
                                c128[0],
                                c128[1],
                                c128[2],
                                c255[0],
                                c255[1],
                                c255[2],
                            );
                        }
                        // The 'clut' resource data IS a ColorTable record.
                        // We need to return a COPY (per IM spec: GetCTable returns a new handle).
                        let ct_size_val = bus.read_word(found_ptr + 6) as u32; // ctSize
                        let total = 8 + (ct_size_val + 1) * 8;
                        let ct_ptr = bus.alloc(total);
                        for off in 0..total {
                            let b = bus.read_byte(found_ptr + off);
                            bus.write_byte(ct_ptr + off, b);
                        }
                        bus.write_long(ct_ptr, self.next_color_table_seed());
                        let h = bus.alloc(4);
                        bus.write_long(h, ct_ptr);
                        self.remember_recent_resource_ctable_fetch(ct_id, h);
                        h
                    } else {
                        self.recent_resource_ctable_fetch = None;
                        if trace_palette_enabled() {
                            eprintln!("[PALETTE] GetCTable id={} -> not found", ct_id);
                        }
                        // Fallback: return empty color table
                        let ct_ptr = bus.alloc(8);
                        bus.write_long(ct_ptr, self.next_color_table_seed()); // ctSeed
                        bus.write_word(ct_ptr + 4, 0); // ctFlags
                        bus.write_word(ct_ptr + 6, 0); // ctSize = 0 (1 entry - but empty)
                        let h = bus.alloc(4);
                        bus.write_long(h, ct_ptr);
                        h
                    }
                };

                bus.write_long(sp + 2, ct_handle);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // DisposeCTable ($AA24): CTabHandle → void
            // Imaging With QuickDraw 1994 p.4-93: disposes the
            // ColorTable record whose handle is passed in `cTable`.
            (true, 0x224) => {
                let sp = cpu.read_reg(Register::A7);
                let ct_handle = bus.read_long(sp);
                if ct_handle != 0 {
                    let ct_ptr = bus.read_long(ct_handle);
                    bus.free(ct_ptr);
                    bus.free(ct_handle);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // MakeITable ($AA39)
            // Builds an inverse color table from the supplied CTab.
            // PROCEDURE MakeITable(cTabH: CTabHandle; iTabH: ITabHandle; res: INTEGER);
            // Inside Macintosh Volume V (1986), p. V-142
            // MPW Quickdraw.h: EXTERN_API(void) MakeITable(CTabHandle, ITabHandle, short)
            //                    ONEWORDINLINE(0xAA39).
            // Stack: SP+0:res(2), SP+2:iTabH(4), SP+6:cTabH(4). Pop 10.
            (true, 0x239) => {
                let sp = cpu.read_reg(Register::A7);
                let res = bus.read_word(sp);
                let itab_handle = bus.read_long(sp + 2);
                let ctab_handle = bus.read_long(sp + 6);
                cpu.write_reg(Register::A7, sp + 10);
                self.make_inverse_table(bus, ctab_handle, itab_handle, res);
                Ok(())
            }

            // ========== QDExtensions / GWorld ==========

            // QDExtensions dispatcher ($AB1D)
            // Per IM:VI Table C-1 + Table C-3 line 58031+ the
            // canonical trap-word name is _QDExtensions (a
            // selector-based dispatcher), NOT NewGWorld. Selector
            // table per IM:VI Table C-3 lines 58031..58058:
            //   $0000 NewGWorld          $0001 LockPixels
            //   $0002 UnlockPixels       $0003 UpdateGWorld
            //   $0004 DisposeGWorld      $0005 GetGWorld
            //   $0006 SetGWorld          $0007 CTabChanged
            //   $0008 PixPatChanged      $0009 PortChanged
            //   $000A GDeviceChanged     $000B AllowPurgePixels
            //   $000C NoPurgePixels      $000D GetPixelsState
            //   $000E SetPixelsState     $000F GetPixBaseAddr
            //   $0010 NewScreenBuffer    $0011 DisposeScreenBuffer
            //   $0012 GetGWorldDevice    $0013 QDDone
            //   $0014 OffscreenVersion   $0015 NewTempScreenBuffer
            //
            // Systemless handles the documented selector set here:
            // 0x0000 NewGWorld / 0x0001 LockPixels / 0x0002
            // UnlockPixels / 0x0003 UpdateGWorld / 0x0004
            // DisposeGWorld / 0x0005 GetGWorld / 0x0006 SetGWorld
            // / 0x0007 CTabChanged / 0x0008 PixPatChanged /
            // 0x0009 PortChanged / 0x000A GDeviceChanged / 0x000D
            // GetPixelsState / 0x000E SetPixelsState / 0x000F
            // GetPixBaseAddr / 0x0012 GetGWorldDevice / 0x0016
            // PixMap32Bit / 0x0017 GetGWorldPixMap. Missing: 0x000B
            // AllowPurgePixels (no-op-ok since no purgeable axis),
            // 0x000C NoPurgePixels (same), 0x0010 NewScreenBuffer
            // / 0x0011 DisposeScreenBuffer (no off-screen-buffer
            // axis), 0x0013 QDDone / 0x0014 OffscreenVersion /
            // 0x0015 NewTempScreenBuffer (System 7.5+ extras).
            //
            // Apps using legacy alias trap-words ($AB04 LockPixels
            // direct, $AB05 UnlockPixels direct, $AB1E GetGWorld
            // direct, $AB1F DisposeGWorld direct, $AB1C SetGWorld
            // direct) bypass this dispatcher and land on
            // dedicated arms — see the legacy-alias trap-doc lines
            // at quickdraw.rs:5764 ($AB04) / 5793 ($AB05) /
            // 5697 ($AB1E) / 5681 ($AB1F) / 5722 ($AB1C). Both
            // dispatch paths reach the same Systemless HLE state.
            //
            // ## Trap-name mislabel fix
            //
            // Trap-doc Notes column previously said "NewGWorld |
            // Complete" — both wrong: the trap-word's canonical
            // name is QDExtensions per IM:VI Table C-1; status
            // is Partial (5 missing selectors documented above).
            // Fixed via the trap-name verification audit pattern.
            // Imaging With QuickDraw 1994, Chapter 6
            // Inside Macintosh Volume VI, 17-13..17-30
            // QDExtensions ($AB1D): Selector-based dispatcher per IM:VI Table C-3 lines 58031..58058. Handles the documented selectors here: NewGWorld / LockPixels / UnlockPixels / UpdateGWorld / DisposeGWorld / GetGWorld / SetGWorld / CTabChanged / PixPatChanged / PortChanged / GDeviceChanged / AllowPurgePixels / NoPurgePixels / GetPixelsState / SetPixelsState / GetPixBaseAddr / PixMap32Bit / GetGWorldDevice / GetGWorldPixMap / QDDone / OffscreenVersion / NewScreenBuffer / DisposeScreenBuffer / NewTempScreenBuffer. Legacy alias trap-words $AB04 LockPixels / $AB05 UnlockPixels / $AB1C SetGWorld / $AB1E GetGWorld / $AB1F DisposeGWorld bypass this dispatcher.
            (true, 0x31D) => {
                let sp = cpu.read_reg(Register::A7);
                let selector = cpu.read_reg(Register::D0);
                let routine = selector & 0xFFFF;
                let param_bytes = (selector >> 16) & 0xFFFF;
                match routine {
                    // NewGWorld ($00160000)
                    // FUNCTION NewGWorld(VAR offscreenGWorld: GWorldPtr;
                    //     pixelDepth: INTEGER; boundsRect: Rect;
                    //     cTable: CTabHandle; aGDevice: GDHandle;
                    //     flags: GWorldFlags): QDErr;
                    // Imaging With QuickDraw 1994, 6-22
                    0x0000 => {
                        let flags = bus.read_long(sp);
                        let gdevice_param = bus.read_long(sp + 4);
                        let ctab_param = bus.read_long(sp + 8);
                        let bounds_ptr = bus.read_long(sp + 12);
                        let requested_depth = bus.read_word(sp + 16) as i16;
                        let gworld_ptr_ptr = bus.read_long(sp + 18);

                        const NO_NEW_DEVICE_FLAG: u32 = 1 << 1;

                        let requested_top = bus.read_word(bounds_ptr) as i16;
                        let requested_left = bus.read_word(bounds_ptr + 2) as i16;
                        let requested_bottom = bus.read_word(bounds_ptr + 4) as i16;
                        let requested_right = bus.read_word(bounds_ptr + 6) as i16;
                        let width =
                            ((requested_right as i32) - (requested_left as i32)).max(1) as u32;
                        let height =
                            ((requested_bottom as i32) - (requested_top as i32)).max(1) as u32;
                        let no_new_device = (flags & NO_NEW_DEVICE_FLAG) != 0;

                        // NewGWorld uses pixelDepth=0 as a "match the intersecting screen
                        // device" request, but the resulting GWorld uses a local
                        // coordinate system rooted at (0,0).
                        // Inside Macintosh Volume VI, 21-13 to 21-15
                        let main_gdevice = self.ensure_main_gdevice(bus);
                        let source_gdevice = if requested_depth <= 0 || no_new_device {
                            if gdevice_param != 0 {
                                gdevice_param
                            } else {
                                main_gdevice
                            }
                        } else {
                            0
                        };
                        let depth = if requested_depth <= 0 {
                            Self::gdevice_pixel_size(bus, source_gdevice).unwrap_or(8)
                        } else if no_new_device {
                            Self::gdevice_pixel_size(bus, source_gdevice)
                                .unwrap_or(requested_depth as u32)
                        } else {
                            requested_depth as u32
                        };
                        let (top, left, bottom, right) = if requested_depth <= 0 {
                            (0, 0, height as i16, width as i16)
                        } else {
                            (
                                requested_top,
                                requested_left,
                                requested_bottom,
                                requested_right,
                            )
                        };
                        let row_bytes = (width * depth).div_ceil(32) * 4;

                        let source_ctab_handle = if ctab_param != 0 {
                            ctab_param
                        } else if source_gdevice != 0 {
                            Self::gdevice_ctab_handle(bus, source_gdevice)
                        } else {
                            0
                        };
                        let active_seeded_screen_clut = self
                            .active_seeded_screen_clut_for_offscreen_clone(
                                source_gdevice,
                                ctab_param,
                            );
                        let ctab_handle = if no_new_device {
                            source_ctab_handle
                        } else if let Some(clut) = active_seeded_screen_clut.as_ref() {
                            self.allocate_color_table_handle_with_clut(bus, depth, clut, 0x8000)
                        } else {
                            self.allocate_color_table_handle(bus, depth, source_ctab_handle, 0x8000)
                        };

                        let pixel_buf = bus.alloc(row_bytes * height);
                        let pixmap = bus.alloc(50);
                        bus.write_long(pixmap, pixel_buf);
                        bus.write_word(pixmap + 4, (row_bytes as u16) | 0x8000);
                        bus.write_word(pixmap + 6, top as u16);
                        bus.write_word(pixmap + 8, left as u16);
                        bus.write_word(pixmap + 10, bottom as u16);
                        bus.write_word(pixmap + 12, right as u16);
                        bus.write_word(pixmap + 14, 0);
                        bus.write_word(pixmap + 16, 0);
                        bus.write_long(pixmap + 18, 0);
                        bus.write_long(pixmap + 22, 0x00480000);
                        bus.write_long(pixmap + 26, 0x00480000);
                        bus.write_word(pixmap + 30, 0);
                        bus.write_word(pixmap + 32, depth as u16);
                        bus.write_word(pixmap + 34, 1);
                        bus.write_word(pixmap + 36, depth as u16);
                        bus.write_long(pixmap + 38, 0);
                        bus.write_long(pixmap + 42, ctab_handle); // pmTable
                        bus.write_long(pixmap + 46, 0);

                        let pixmap_handle = bus.alloc(4);
                        bus.write_long(pixmap_handle, pixmap);

                        let associated_gdevice = if no_new_device {
                            source_gdevice
                        } else {
                            self.create_offscreen_gdevice(
                                bus,
                                pixmap_handle,
                                top,
                                left,
                                bottom,
                                right,
                            )
                        };

                        // Allocate visRgn = bounds rect
                        let vis_rgn_ptr = bus.alloc(10);
                        bus.write_word(vis_rgn_ptr, 10);
                        bus.write_word(vis_rgn_ptr + 2, top as u16);
                        bus.write_word(vis_rgn_ptr + 4, left as u16);
                        bus.write_word(vis_rgn_ptr + 6, bottom as u16);
                        bus.write_word(vis_rgn_ptr + 8, right as u16);
                        let vis_rgn = bus.alloc(4);
                        bus.write_long(vis_rgn, vis_rgn_ptr);

                        // Allocate clipRgn = infinite
                        let clip_rgn_ptr = bus.alloc(10);
                        bus.write_word(clip_rgn_ptr, 10);
                        bus.write_word(clip_rgn_ptr + 2, (-32767_i16) as u16);
                        bus.write_word(clip_rgn_ptr + 4, (-32767_i16) as u16);
                        bus.write_word(clip_rgn_ptr + 6, 32767_u16);
                        bus.write_word(clip_rgn_ptr + 8, 32767_u16);
                        let clip_rgn = bus.alloc(4);
                        bus.write_long(clip_rgn, clip_rgn_ptr);

                        // Build CGrafPort with correct layout
                        // Imaging With QuickDraw 1994, 4-125
                        let port = bus.alloc(170);
                        self.disposed_gworld_portbits.remove(&(port + 2));
                        bus.write_word(port, 0); // +0  device
                        bus.write_long(port + 2, pixmap_handle); // +2  portPixMap (handle)
                        self.cache_portbits_pixmap_handle(bus, port + 2, pixmap_handle);
                        bus.write_word(port + 6, 0xC000); // +6  portVersion
                        bus.write_long(port + 8, 0); // +8  grafVars
                        bus.write_word(port + 12, 0); // +12 chExtra
                        bus.write_word(port + 14, 0x8000); // +14 pnLocHFrac
                        bus.write_word(port + 16, top as u16); // +16 portRect.top
                        bus.write_word(port + 18, left as u16); // +18 portRect.left
                        bus.write_word(port + 20, bottom as u16); // +20 portRect.bottom
                        bus.write_word(port + 22, right as u16); // +22 portRect.right
                        bus.write_long(port + 24, vis_rgn); // +24 visRgn
                        bus.write_long(port + 28, clip_rgn); // +28 clipRgn
                        self.init_cgraf_port_defaults(port, bus);

                        if associated_gdevice != 0 {
                            self.gworld_devices.insert(port, associated_gdevice);
                        } else {
                            self.gworld_devices.remove(&port);
                        }
                        self.seed_gworld_pixels_state(pixmap_handle, flags);

                        if gworld_ptr_ptr != 0 {
                            bus.write_long(gworld_ptr_ptr, port);
                        }
                        if trace_dialog_gworld_enabled() {
                            let ctab_ptr = if ctab_handle != 0 {
                                bus.read_long(ctab_handle)
                            } else {
                                0
                            };
                            let ctab_seed = if ctab_ptr != 0 {
                                bus.read_long(ctab_ptr)
                            } else {
                                0
                            };
                            let ctab_entries = if ctab_ptr != 0 {
                                bus.read_word(ctab_ptr + 6).saturating_add(1)
                            } else {
                                0
                            };
                            eprintln!(
                                "[DIALOG-GWORLD] NewGWorld port=${:08X} pixmap=${:08X} base=${:08X} bounds=({},{},{},{}) depth={} rowBytes={} flags=${:08X} ctabParam=${:08X} ctab=${:08X} ctabSeed=${:08X} entries={} gdh=${:08X}",
                                port,
                                pixmap,
                                pixel_buf,
                                top,
                                left,
                                bottom,
                                right,
                                depth,
                                row_bytes,
                                flags,
                                ctab_param,
                                ctab_handle,
                                ctab_seed,
                                ctab_entries,
                                associated_gdevice,
                            );
                        }

                        bus.write_word(sp + 22, 0); // noErr
                        cpu.write_reg(Register::A7, sp + 22);
                    }
                    // LockPixels ($00040001)
                    // FUNCTION LockPixels(pm: PixMapHandle): BOOLEAN;
                    // Imaging With QuickDraw 1994, 6-26
                    0x0001 => {
                        let pmh = bus.read_long(sp);
                        let locked = self.lock_gworld_pixels(pmh);
                        bus.write_word(sp + 4, if locked { 0x0100 } else { 0x0000 });
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // UnlockPixels ($00040002)
                    // PROCEDURE UnlockPixels(pm: PixMapHandle);
                    // Imaging With QuickDraw 1994, 6-27
                    0x0002 => {
                        let pmh = bus.read_long(sp);
                        self.unlock_gworld_pixels(pmh);
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // UpdateGWorld ($00160003)
                    // FUNCTION UpdateGWorld(VAR offscreenGWorld: GWorldPtr;
                    //     pixelDepth: INTEGER; boundsRect: Rect;
                    //     cTable: CTabHandle; aGDevice: GDHandle;
                    //     flags: GWorldFlags): GWorldFlags;
                    // Imaging With QuickDraw 1994, 6-23
                    0x0003 => {
                        let flags = bus.read_long(sp);
                        let gdevice_param = bus.read_long(sp + 4);
                        let ctab_param = bus.read_long(sp + 8);
                        let bounds_ptr = bus.read_long(sp + 12);
                        let depth_param = bus.read_word(sp + 16) as i16;
                        let gworld_ptr_ptr = bus.read_long(sp + 18);

                        const NO_NEW_DEVICE_FLAG: u32 = 1 << 1;
                        // GWorldFlags output bits - Imaging With QuickDraw 1994, 6-61
                        const MAP_PIX_FLAG: u32 = 1 << 16; // mapPixBit = 16
                        const NEW_ROW_BYTES_FLAG: u32 = 1 << 19; // newRowBytesBit = 19
                        const REALLOC_PIX_FLAG: u32 = 1 << 20; // reallocPixBit = 20
                        const ALIGN_PIX_FLAG: u32 = 1 << 18; // alignPixBit = 18
                        const CLIP_PIX_FLAG: u32 = 1 << 28; // clipPixBit = 28
                        const STRETCH_PIX_FLAG: u32 = 1 << 29; // stretchPixBit = 29
                        let no_new_device = (flags & NO_NEW_DEVICE_FLAG) != 0;

                        let requested_top = bus.read_word(bounds_ptr) as i16;
                        let requested_left = bus.read_word(bounds_ptr + 2) as i16;
                        let requested_bottom = bus.read_word(bounds_ptr + 4) as i16;
                        let requested_right = bus.read_word(bounds_ptr + 6) as i16;
                        let requested_width =
                            ((requested_right as i32) - (requested_left as i32)).max(1) as u32;
                        let requested_height =
                            ((requested_bottom as i32) - (requested_top as i32)).max(1) as u32;

                        let port = bus.read_long(gworld_ptr_ptr);
                        let pixmap_handle = bus.read_long(port + 2);
                        let pixmap = bus.read_long(pixmap_handle);
                        let old_ctab_handle = bus.read_long(pixmap + 42);
                        let old_base_handle = Self::offscreen_pixmap_base_handle(bus, pixmap);
                        let old_base = Self::offscreen_pixmap_base_ptr(bus, pixmap);
                        let old_rb_raw = bus.read_word(pixmap + 4);
                        let old_row_bytes = (old_rb_raw & 0x3FFF) as u32;
                        let old_top = bus.read_word(pixmap + 6) as i16;
                        let old_left = bus.read_word(pixmap + 8) as i16;
                        let old_bottom = bus.read_word(pixmap + 10) as i16;
                        let old_right = bus.read_word(pixmap + 12) as i16;
                        let old_width = ((old_right as i32) - (old_left as i32)).max(1) as u32;
                        let old_height = ((old_bottom as i32) - (old_top as i32)).max(1) as u32;
                        let old_depth = bus.read_word(pixmap + 36) as u32;
                        let current_gdevice =
                            self.gworld_devices.get(&port).copied().unwrap_or_default();

                        let main_gdevice = self.ensure_main_gdevice(bus);
                        let source_gdevice = if depth_param <= 0 || no_new_device {
                            if gdevice_param != 0 {
                                gdevice_param
                            } else {
                                main_gdevice
                            }
                        } else {
                            current_gdevice
                        };
                        let depth = if depth_param <= 0 {
                            Self::gdevice_pixel_size(bus, source_gdevice).unwrap_or(old_depth)
                        } else if no_new_device {
                            Self::gdevice_pixel_size(bus, source_gdevice)
                                .unwrap_or(depth_param as u32)
                        } else {
                            depth_param as u32
                        };
                        let source_ctab_handle = if ctab_param != 0 {
                            ctab_param
                        } else if source_gdevice != 0 {
                            Self::gdevice_ctab_handle(bus, source_gdevice)
                        } else {
                            0
                        };
                        let active_seeded_screen_clut = self
                            .active_seeded_screen_clut_for_offscreen_clone(
                                source_gdevice,
                                ctab_param,
                            );
                        let (new_top, new_left, new_bottom, new_right) = if depth_param <= 0 {
                            (0, 0, requested_height as i16, requested_width as i16)
                        } else {
                            (
                                requested_top,
                                requested_left,
                                requested_bottom,
                                requested_right,
                            )
                        };
                        let new_width = ((new_right as i32) - (new_left as i32)).max(1) as u32;
                        let new_height = ((new_bottom as i32) - (new_top as i32)).max(1) as u32;

                        let new_row_bytes = (new_width * depth).div_ceil(32) * 4;
                        let new_buf_size = new_row_bytes * new_height;

                        let mut result_flags: u32 = 0;
                        // If the caller supplies a different color-table handle,
                        // UpdateGWorld must treat that as a remap request even
                        // when the ctSeed happens to match. BasiliskII preserves
                        // the seed convention for copied tables, so comparing the
                        // seed alone would miss real remaps.
                        let needs_ctab_sync = source_ctab_handle != old_ctab_handle;

                        if depth_param <= 0
                            && old_width == new_width
                            && old_height == new_height
                            && (old_top != new_top || old_left != new_left)
                        {
                            result_flags |= ALIGN_PIX_FLAG;
                        }

                        if new_width != old_width
                            || new_height != old_height
                            || new_row_bytes != old_row_bytes
                            || depth != old_depth
                        {
                            // Imaging With QuickDraw 1994 pp. 6-23..6-26:
                            // UpdateGWorld only updates old pixels when the
                            // caller asks for clipPix or stretchPix. With no
                            // preservation flag, "the pixels are not updated";
                            // if the buffer is reallocated, reallocPix tells
                            // the caller to reconstruct the image.
                            let preserve_pixels = (flags & (CLIP_PIX_FLAG | STRETCH_PIX_FLAG)) != 0;
                            let new_base = bus.alloc(new_buf_size);
                            // Zero fill first
                            for i in (0..new_buf_size).step_by(4) {
                                bus.write_long(new_base + i, 0);
                            }
                            if preserve_pixels {
                                // Approximate clipPix/stretchPix by retaining
                                // the overlapping source rows; callers still
                                // see reallocPix/newRowBytes and can redraw
                                // if they need exact Color QuickDraw scaling.
                                let copy_rows = old_height.min(new_height);
                                let copy_bytes_per_row = old_row_bytes.min(new_row_bytes);
                                for row in 0..copy_rows {
                                    let src = old_base + row * old_row_bytes;
                                    let dst = new_base + row * new_row_bytes;
                                    for b in 0..copy_bytes_per_row {
                                        bus.write_byte(dst + b, bus.read_byte(src + b));
                                    }
                                }
                            }
                            bus.free(old_base);

                            // Update PixMap fields
                            if old_base_handle != 0 {
                                bus.write_long(old_base_handle, new_base);
                            } else {
                                bus.write_long(pixmap, new_base);
                            }
                            bus.write_word(pixmap + 4, (new_row_bytes as u16) | 0x8000); // rowBytes
                            bus.write_word(pixmap + 32, depth as u16); // pixelSize
                            bus.write_word(pixmap + 36, depth as u16); // cmpSize

                            result_flags |= REALLOC_PIX_FLAG;
                            if new_row_bytes != old_row_bytes {
                                result_flags |= NEW_ROW_BYTES_FLAG;
                            }
                        }

                        if no_new_device {
                            bus.write_long(pixmap + 42, source_ctab_handle);
                            if source_gdevice != 0 {
                                self.gworld_devices.insert(port, source_gdevice);
                            }
                        } else {
                            let active_ctab_handle = if old_ctab_handle != 0 {
                                old_ctab_handle
                            } else {
                                let allocated =
                                    if let Some(clut) = active_seeded_screen_clut.as_ref() {
                                        self.allocate_color_table_handle_with_clut(
                                            bus, depth, clut, 0x8000,
                                        )
                                    } else {
                                        self.allocate_color_table_handle(
                                            bus,
                                            depth,
                                            source_ctab_handle,
                                            0x8000,
                                        )
                                    };
                                bus.write_long(pixmap + 42, allocated);
                                allocated
                            };
                            if let Some(clut) = active_seeded_screen_clut.as_ref() {
                                let _ = self.overwrite_color_table_handle_with_clut(
                                    bus,
                                    active_ctab_handle,
                                    clut,
                                    0x8000,
                                );
                            } else {
                                let _ = self.overwrite_color_table_handle(
                                    bus,
                                    active_ctab_handle,
                                    depth,
                                    source_ctab_handle,
                                    0x8000,
                                );
                            }
                            if current_gdevice == 0 {
                                let offscreen_gdevice = self.create_offscreen_gdevice(
                                    bus,
                                    pixmap_handle,
                                    new_top,
                                    new_left,
                                    new_bottom,
                                    new_right,
                                );
                                self.gworld_devices.insert(port, offscreen_gdevice);
                            }
                        }
                        if needs_ctab_sync {
                            result_flags |= MAP_PIX_FLAG;
                        }

                        // Update bounds in PixMap
                        bus.write_word(pixmap + 6, new_top as u16);
                        bus.write_word(pixmap + 8, new_left as u16);
                        bus.write_word(pixmap + 10, new_bottom as u16);
                        bus.write_word(pixmap + 12, new_right as u16);

                        // Update portRect in CGrafPort
                        bus.write_word(port + 16, new_top as u16);
                        bus.write_word(port + 18, new_left as u16);
                        bus.write_word(port + 20, new_bottom as u16);
                        bus.write_word(port + 22, new_right as u16);

                        // Update visRgn to new bounds
                        let vis_rgn_handle = bus.read_long(port + 24);
                        if vis_rgn_handle != 0 {
                            let vis_rgn_ptr = bus.read_long(vis_rgn_handle);
                            if vis_rgn_ptr != 0 {
                                bus.write_word(vis_rgn_ptr + 2, new_top as u16);
                                bus.write_word(vis_rgn_ptr + 4, new_left as u16);
                                bus.write_word(vis_rgn_ptr + 6, new_bottom as u16);
                                bus.write_word(vis_rgn_ptr + 8, new_right as u16);
                            }
                        }

                        let attached_gdevice = self.gworld_devices.get(&port).copied().unwrap_or(0);
                        if attached_gdevice != 0 {
                            let gd_ptr = bus.read_long(attached_gdevice);
                            if gd_ptr != 0 {
                                bus.write_word(gd_ptr + 34, new_top as u16);
                                bus.write_word(gd_ptr + 36, new_left as u16);
                                bus.write_word(gd_ptr + 38, new_bottom as u16);
                                bus.write_word(gd_ptr + 40, new_right as u16);
                            }
                        }
                        if self.current_port == port {
                            self.current_gdevice = self.gdevice_for_port(bus, port);
                        }

                        bus.write_long(sp + 22, result_flags);
                        cpu.write_reg(Register::D0, result_flags);
                        cpu.write_reg(Register::A7, sp + 22);
                    }
                    // DisposeGWorld ($00040004)
                    // PROCEDURE DisposeGWorld(offscreenGWorld: GWorldPtr);
                    // Imaging With QuickDraw 1994, 6-25
                    0x0004 => {
                        let port = bus.read_long(sp);
                        self.dispose_gworld_port(cpu, bus, port);
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // GetGWorld ($00080005)
                    // PROCEDURE GetGWorld(VAR port: CGrafPtr; VAR gdh: GDHandle);
                    // Imaging With QuickDraw 1994, 6-28
                    0x0005 => {
                        let gd_ptr = bus.read_long(sp);
                        let port_ptr = bus.read_long(sp + 4);
                        let gdh = if self.current_gdevice != 0 {
                            self.current_gdevice
                        } else {
                            self.ensure_main_gdevice(bus)
                        };
                        if port_ptr != 0 {
                            bus.write_long(port_ptr, self.current_port);
                        }
                        if gd_ptr != 0 {
                            bus.write_long(gd_ptr, gdh);
                        }
                        if trace_dialog_gworld_enabled() {
                            eprintln!(
                                "[DIALOG-GWORLD] GetGWorld port=${:08X} gdh=${:08X}",
                                self.current_port, gdh
                            );
                        }
                        cpu.write_reg(Register::A7, sp + 8);
                    }
                    // SetGWorld ($00080006)
                    // PROCEDURE SetGWorld(port: CGrafPtr; gdh: GDHandle);
                    // Imaging With QuickDraw 1994, 6-29
                    // SetGWorld internally calls SetPort to update thePort.
                    // Imaging With QuickDraw 1994, 6-30
                    0x0006 => {
                        let new_gd = bus.read_long(sp);
                        let new_port = bus.read_long(sp + 4);
                        let gdh = if new_gd != 0 {
                            new_gd
                        } else {
                            self.gdevice_for_port(bus, new_port)
                        };
                        if trace_dialog_gworld_enabled() {
                            eprintln!(
                                "[DIALOG-GWORLD] SetGWorld port=${:08X} gdh=${:08X}",
                                new_port, gdh
                            );
                        }
                        self.set_current_port_state(bus, cpu, new_port, Some(gdh));
                        cpu.write_reg(Register::A7, sp + 8);
                    }
                    // CTabChanged ($00040007)
                    // Notifies Color QuickDraw that a color table has been changed
                    // directly and its ctSeed needs updating.
                    // PROCEDURE CTabChanged(ctab: CTabHandle);
                    // Imaging With QuickDraw 1994, 4-102
                    0x0007 => {
                        let ctab_handle = bus.read_long(sp);
                        self.reseed_color_table_handle(bus, ctab_handle);
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // PixPatChanged ($00040008)
                    // PROCEDURE PixPatChanged(ppat: PixPatHandle);
                    // Imaging With QuickDraw 1994, 4-103
                    0x0008 => {
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // PortChanged ($00040009)
                    // PROCEDURE PortChanged(port: GrafPtr);
                    // Imaging With QuickDraw 1994, 4-103
                    0x0009 => {
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // GDeviceChanged ($0004000A)
                    // PROCEDURE GDeviceChanged(gdh: GDHandle);
                    // Imaging With QuickDraw 1994, 4-103
                    0x000A => {
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // AllowPurgePixels ($0004000B)
                    0x000B => {
                        let pmh = bus.read_long(sp);
                        self.set_gworld_pixels_purgeable(pmh, true);
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // NoPurgePixels ($0004000C)
                    0x000C => {
                        let pmh = bus.read_long(sp);
                        self.set_gworld_pixels_purgeable(pmh, false);
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // GetPixelsState ($0004000D)
                    0x000D => {
                        let pmh = bus.read_long(sp);
                        bus.write_long(sp + 4, self.gworld_pixels_state(pmh));
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // SetPixelsState ($0008000E)
                    0x000E => {
                        let state = bus.read_long(sp);
                        let pmh = bus.read_long(sp + 4);
                        self.set_gworld_pixels_state(pmh, state);
                        cpu.write_reg(Register::A7, sp + 8);
                    }
                    // GetPixBaseAddr ($0004000F)
                    // FUNCTION GetPixBaseAddr(pm: PixMapHandle): Ptr;
                    0x000F | 0x1F18 => {
                        let mut pmh = bus.read_long(sp);
                        if (pmh >> 16) == 0x000F {
                            let shifted_pmh = bus.read_long(sp + 2);
                            if shifted_pmh != 0 {
                                pmh = shifted_pmh;
                            }
                        }
                        let pm = if pmh != 0 { bus.read_long(pmh) } else { 0 };
                        let base_handle = Self::offscreen_pixmap_base_handle(bus, pm);
                        let base = Self::offscreen_pixmap_base_ptr(bus, pm);
                        if trace_dialog_gworld_enabled() {
                            eprintln!(
                                "[DIALOG-GWORLD] GetPixBaseAddr pmh=${:08X} pm=${:08X} base_handle=${:08X} base=${:08X}",
                                pmh, pm, base_handle, base
                            );
                        }
                        bus.write_long(sp + 4, base);
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // GetGWorldDevice ($00040012)
                    // FUNCTION GetGWorldDevice(offscreenGWorld: GWorldPtr): GDHandle;
                    // Inside Macintosh Volume VI (1991), p. 21-18;
                    // Imaging With QuickDraw (1994), p. 6-33.
                    // For a regular GrafPort/CGrafPort argument, returns
                    // the current device rather than MainDevice.
                    // NewScreenBuffer / NewTempScreenBuffer ($000E0010 / $000E0015)
                    // FUNCTION NewScreenBuffer(globalRect: Rect; purgeable: Boolean;
                    //                           VAR gdh: GDHandle;
                    //                           VAR offscreenPixMap: PixMapHandle): QDErr;
                    // FUNCTION NewTempScreenBuffer(globalRect: Rect; purgeable: Boolean;
                    //                              VAR gdh: GDHandle;
                    //                              VAR offscreenPixMap: PixMapHandle): QDErr;
                    0x0010 => {
                        let offscreen_pixmap_out_ptr = bus.read_long(sp);
                        let gdh_out_ptr = bus.read_long(sp + 4);
                        let purgeable = bus.read_word(sp + 8) != 0;
                        let global_rect_ptr = bus.read_long(sp + 10);
                        let result = self.new_screen_buffer_common(
                            bus,
                            global_rect_ptr,
                            purgeable,
                            gdh_out_ptr,
                            offscreen_pixmap_out_ptr,
                            false,
                        );
                        cpu.write_reg(Register::D0, result);
                        cpu.write_reg(Register::A7, sp + 14);
                    }
                    0x0011 => {
                        let offscreen_pixmap = bus.read_long(sp);
                        self.dispose_screen_buffer(bus, offscreen_pixmap);
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    0x0015 => {
                        let offscreen_pixmap_out_ptr = bus.read_long(sp);
                        let gdh_out_ptr = bus.read_long(sp + 4);
                        let purgeable = bus.read_word(sp + 8) != 0;
                        let global_rect_ptr = bus.read_long(sp + 10);
                        let result = self.new_screen_buffer_common(
                            bus,
                            global_rect_ptr,
                            purgeable,
                            gdh_out_ptr,
                            offscreen_pixmap_out_ptr,
                            true,
                        );
                        cpu.write_reg(Register::D0, result);
                        cpu.write_reg(Register::A7, sp + 14);
                    }
                    0x0012 => {
                        let gworld = bus.read_long(sp);
                        let current_gdh = if self.current_gdevice != 0 {
                            self.current_gdevice
                        } else {
                            self.ensure_main_gdevice(bus)
                        };
                        let gdh = self
                            .gworld_devices
                            .get(&gworld)
                            .copied()
                            .unwrap_or(current_gdh);
                        if trace_dialog_gworld_enabled() {
                            eprintln!(
                                "[DIALOG-GWORLD] GetGWorldDevice port=${:08X} gdh=${:08X}",
                                gworld, gdh
                            );
                        }
                        bus.write_long(sp + 4, gdh);
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // QDDone ($00040013)
                    // FUNCTION QDDone(port: GrafPtr): Boolean;
                    // Imaging With QuickDraw 1994, 3-125 to 3-126.
                    // QDDone reports completion of queued drawing. BasiliskII
                    // returns TRUE for each query against a live port.
                    0x0013 => {
                        let port = bus.read_long(sp);
                        let done = port != 0;
                        bus.write_word(sp + 4, if done { 0x0100 } else { 0x0000 });
                        cpu.write_reg(Register::D0, u32::from(done));
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // OffscreenVersion ($00000014)
                    // function OffscreenVersion: SInt32;
                    // QuickDraw Reference (Carbon) p. 307;
                    // QDOffscreen.h declaration.
                    0x0014 => {
                        let version = 1u32;
                        bus.write_long(sp, version);
                        cpu.write_reg(Register::D0, version);
                    }
                    // PixMap32Bit ($00040016)
                    // FUNCTION PixMap32Bit(pmHandle: PixMapHandle): BOOLEAN;
                    0x0016 => {
                        let pmh = bus.read_long(sp);
                        let requires_32bit = if pmh != 0 {
                            let pm = bus.read_long(pmh);
                            // QD01 32-Bit QuickDraw defines pmVersion =
                            // baseAddr32 (4) for 32-bit-addressed PixMaps.
                            pm != 0 && bus.read_word(pm + 14) == 4
                        } else {
                            false
                        };
                        bus.write_word(sp + 4, if requires_32bit { 0x0100 } else { 0x0000 });
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // GetGWorldPixMap ($00040017)
                    // FUNCTION GetGWorldPixMap(offscreenGWorld: GWorldPtr): PixMapHandle;
                    // Imaging With QuickDraw 1994, 6-30
                    0x0017 => {
                        let gworld = bus.read_long(sp);
                        let pmh = if gworld != 0 {
                            bus.read_long(gworld + 2) // portPixMap handle
                        } else {
                            0
                        };
                        self.cache_portbits_pixmap_handle(bus, gworld + 2, pmh);
                        if trace_dialog_gworld_enabled() {
                            eprintln!(
                                "[DIALOG-GWORLD] GetGWorldPixMap port=${:08X} pmh=${:08X}",
                                gworld, pmh
                            );
                        }
                        bus.write_long(sp + 4, pmh);
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    _ => {
                        // Unknown selector — use param_bytes to pop stack
                        eprintln!(
                            "[TRAP] QDExtensions unknown routine={} params={}",
                            routine, param_bytes
                        );
                        if param_bytes > 0 {
                            cpu.write_reg(Register::A7, sp + param_bytes);
                        }
                        cpu.write_reg(Register::D0, 0);
                    }
                }
                Ok(())
            }

            // Legacy trap aliases (some compilers emit these)
            // DisposeGWorld ($AB1F)
            // PROCEDURE DisposeGWorld(offscreenGWorld: GWorldPtr);
            // Inside Macintosh Volume VI (1991), p. 21-19;
            // Imaging With QuickDraw (1994), p. 6-25.
            //
            // BasiliskII System 7.5.3 does not treat the legacy direct alias
            // like the documented public _QDExtensions selector-$0004 path:
            // the alias leaves the Pascal argument frame on the stack and
            // does not reclaim offscreen-world state. Keep the HLE aligned
            // with that measured alias behavior; callers that need the public
            // semantics must use _QDExtensions selector $0004.
            (true, 0x31F) => Ok(()),

            // GetGWorld ($AB1E)
            // GetGWorld ($AB1E): Returns current port and GDevice
            (true, 0x31E) => {
                let sp = cpu.read_reg(Register::A7);
                let gd_ptr = bus.read_long(sp);
                let port_ptr = bus.read_long(sp + 4);
                if port_ptr != 0 {
                    bus.write_long(port_ptr, self.current_port);
                }
                if gd_ptr != 0 {
                    let gdh = if self.current_gdevice != 0 {
                        self.current_gdevice
                    } else {
                        self.ensure_main_gdevice(bus)
                    };
                    bus.write_long(gd_ptr, gdh);
                }
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // Unknown trap $AB1C — not documented as SetGWorld.
            // Per IM:IWQ 1994 pp.6-29..6-30 / IM:VI Table C-3,
            // SetGWorld is accessed only via _QDExtensions ($AB1D sel 6).
            // $AB1C does not appear in any Apple trap-name table.
            // In BasiliskII System 7.5.3, calling $AB1C with a
            // (CGrafPtr, GDHandle) stack frame does not update thePort:
            // GetPort after the call still returns the prior port.
            // We pop 8 bytes to match the observed stack-clean behaviour
            // and do not change any port state.
            // ($AB1C)
            (true, 0x31C) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // LockPixels ($AB04)
            // Per IM:VI 17-13 / Imaging With QuickDraw 1994 6-44:
            // "You must call LockPixels before drawing to or from
            // an offscreen graphics world. ... LockPixels locks
            // the offscreen buffer in memory for the duration of
            // the drawing. If the offscreen buffer is purgeable
            // and has indeed been purged, LockPixels returns FALSE
            // to signal that no drawing can be made to the buffer
            // memory. At that point, the application should either
            // call UpdateGWorld to reallocate the buffer or draw
            // directly in the window it represents."
            // FUNCTION LockPixels(pm: PixMapHandle): Boolean;
            // Inside Macintosh Volume VI, 17-13
            //
            // Stack: SP+0 pm PixMapHandle (4 bytes), SP+4 result
            // Boolean byte in a word-aligned caller slot. Pop 4;
            // result at post-pop A7 = pre-call SP+4.
            //
            // HLE compromise: Systemless doesn't model purgeable
            // pixmaps — every PixMap allocated via NewGWorld
            // ($AB1D) lives until DisposeGWorld ($AB1E) explicitly
            // frees it. So LockPixels always returns TRUE per the
            // IM-documented "buffer is locked, drawing can proceed"
            // path. Apps that defensively branch `if not
            // LockPixels(pm) then UpdateGWorld(...)` always take
            // the LockPixels-succeeded branch and proceed to
            // CopyBits-from-GWorld correctly.
            // LockPixels ($AB04): Pops 4 bytes (PixMapHandle) + writes TRUE to the Boolean result byte per IM:VI 17-13 + Imaging With QuickDraw 6-44 FUNCTION sig. HLE has no purgeable-pixmap axis (every PixMap lives until DisposeGWorld) so always returns TRUE; apps' LockPixels-failure recovery path (UpdateGWorld) never fires but is also unreachable since pixmaps don't get purged.
            (true, 0x304) => {
                let sp = cpu.read_reg(Register::A7);
                let pmh = bus.read_long(sp);
                let locked = self.lock_gworld_pixels(pmh);
                bus.write_word(sp + 4, if locked { 0x0100 } else { 0x0000 });
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // UnlockPixels ($AB05)
            // Per IM:VI 17-13 / Imaging With QuickDraw 1994 6-45:
            // "UnlockPixels unlocks the offscreen buffer. Call
            // UnlockPixels as soon as the application finishes
            // drawing to or from the offscreen pixel map. You
            // don't need to call UnlockPixels if LockPixels
            // returned FALSE, because LockPixels doesn't lock
            // purged pixels. (However, calling UnlockPixels on
            // purged pixels does no harm.)"
            // PROCEDURE UnlockPixels(pm: PixMapHandle);
            // Inside Macintosh Volume VI, 17-13
            //
            // Stack: SP+0 pm PixMapHandle (4 bytes). Pop 4 bytes.
            // No result (PROCEDURE).
            //
            // HLE compromise: paired with LockPixels above —
            // Systemless doesn't model lock state, so UnlockPixels is
            // a true no-op. Per IM:VI 17-13 "calling UnlockPixels
            // on purged pixels does no harm" matches Systemless's
            // unconditional no-op on any handle (including NIL,
            // including dangling-after-DisposeGWorld).
            // UnlockPixels ($AB05): Pops 4 bytes (PixMapHandle) per IM:VI 17-13 + Imaging With QuickDraw 6-45 PROCEDURE sig. Paired with LockPixels ($AB04) — HLE has no lock state so UnlockPixels is a true no-op; matches IM:VI 17-13 "calling UnlockPixels on purged pixels does no harm" semantic for unconditional no-op on any handle.
            (true, 0x305) => {
                let sp = cpu.read_reg(Register::A7);
                let pmh = bus.read_long(sp);
                self.unlock_gworld_pixels(pmh);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // GetPixDepth ($AB08): PixMapHandle → INTEGER
            // GetPixDepth ($AB08): Reads pixelSize from PixMap, except
            // for the main-device gdPMap handle where BasiliskII's ROM
            // returns 0 instead of the backing PixMap's pixelSize field.
            (true, 0x308) => {
                let sp = cpu.read_reg(Register::A7);
                let pm_handle = bus.read_long(sp);
                let depth = if self.main_gdevice_handle != 0 {
                    let gd_ptr = bus.read_long(self.main_gdevice_handle);
                    if gd_ptr != 0 && bus.read_long(gd_ptr + 22) == pm_handle {
                        0u16
                    } else if pm_handle != 0 {
                        let pm_ptr = bus.read_long(pm_handle);
                        if pm_ptr != 0 {
                            bus.read_word(pm_ptr + 32)
                        } else {
                            8u16
                        }
                    } else {
                        8u16
                    }
                } else if pm_handle != 0 {
                    let pm_ptr = bus.read_long(pm_handle);
                    if pm_ptr != 0 {
                        bus.read_word(pm_ptr + 32)
                    } else {
                        8u16
                    }
                } else {
                    8u16
                };
                bus.write_word(sp + 4, depth);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // ========== Arc Drawing ==========

            // FrameArc ($A8BE)
            // PROCEDURE FrameArc(r: Rect; startAngle, arcAngle: INTEGER);
            // Inside Macintosh Volume I, I-184
            //
            // OpenRgn-recording shim. Per IM:I I-184 "Calculations with
            // Regions" the arc's outlined region is added when FrameArc is
            // called inside OpenRgn; drawing is suppressed. The bbox-approx
            // region storage uses the enclosing rect.
            // FrameArc ($A8BE): draw_arc(ShapeOp::Frame)
            (true, 0x0BE) => {
                let sp = cpu.read_reg(Register::A7);
                let arc_angle = bus.read_word(sp) as i16;
                let start_angle = bus.read_word(sp + 2) as i16;
                let rect_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                self.draw_arc(cpu, bus, &r, start_angle, arc_angle, ShapeOp::Frame);
                Ok(())
            }

            // PaintArc ($A8BF)
            // PROCEDURE PaintArc(r: Rect; startAngle, arcAngle: INTEGER);
            // Inside Macintosh Volume I, I-184
            //
            // OpenRgn-recording shim — see FrameArc notes above.
            // PaintArc ($A8BF): draw_arc(ShapeOp::Paint)
            (true, 0x0BF) => {
                let sp = cpu.read_reg(Register::A7);
                let arc_angle = bus.read_word(sp) as i16;
                let start_angle = bus.read_word(sp + 2) as i16;
                let rect_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                self.draw_arc(cpu, bus, &r, start_angle, arc_angle, ShapeOp::Paint);
                Ok(())
            }

            // EraseArc ($A8C0)
            // PROCEDURE EraseArc(r: Rect; startAngle, arcAngle: INTEGER);
            // Inside Macintosh Volume I, I-184
            //
            // OpenRgn-recording shim — see FrameArc notes above.
            // EraseArc ($A8C0): draw_arc(ShapeOp::Erase)
            (true, 0x0C0) => {
                let sp = cpu.read_reg(Register::A7);
                let arc_angle = bus.read_word(sp) as i16;
                let start_angle = bus.read_word(sp + 2) as i16;
                let rect_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                self.draw_arc(cpu, bus, &r, start_angle, arc_angle, ShapeOp::Erase);
                Ok(())
            }

            // InvertArc ($A8C1)
            // PROCEDURE InvertArc(r: Rect; startAngle, arcAngle: INTEGER);
            // Inside Macintosh Volume I, I-184
            //
            // OpenRgn-recording shim — see FrameArc notes above.
            // InvertArc ($A8C1): draw_arc(ShapeOp::Invert)
            (true, 0x0C1) => {
                let sp = cpu.read_reg(Register::A7);
                let arc_angle = bus.read_word(sp) as i16;
                let start_angle = bus.read_word(sp + 2) as i16;
                let rect_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                self.draw_arc(cpu, bus, &r, start_angle, arc_angle, ShapeOp::Invert);
                Ok(())
            }

            // FillArc ($A8C2)
            // PROCEDURE FillArc(r: Rect; startAngle, arcAngle: INTEGER; pat: Pattern);
            // Inside Macintosh Volume I, I-184
            // Stack layout:
            //   SP+0:  pat(4)
            //   SP+4:  arcAngle(2)
            //   SP+6:  startAngle(2)
            //   SP+8:  r(4)  → pop 12.
            //
            // OpenRgn-recording shim — see FillRect notes.
            // FillArc ($A8C2): draw_arc(ShapeOp::Fill(pat)); stack is pat_ptr(4)+arcAngle(2)+startAngle(2)+rect_ptr(4)=12, IM:I I-184
            (true, 0x0C2) => {
                let sp = cpu.read_reg(Register::A7);
                let pat_ptr = bus.read_long(sp);
                let arc_angle = bus.read_word(sp + 4) as i16;
                let start_angle = bus.read_word(sp + 6) as i16;
                let rect_ptr = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);
                let r = read_rect(bus, rect_ptr);
                if self.extend_recording_region(r.top, r.left, r.bottom, r.right) {
                    return Some(Ok(()));
                }
                let mut pat = [0u8; 8];
                for (i, byte) in pat.iter_mut().enumerate() {
                    *byte = bus.read_byte(pat_ptr + i as u32);
                }
                self.draw_arc(cpu, bus, &r, start_angle, arc_angle, ShapeOp::Fill(pat));
                Ok(())
            }

            // ========== Picture Drawing ==========

            // DrawPicture ($A8F6)
            // PROCEDURE DrawPicture(myPicture: PicHandle; dstRect: Rect);
            // SP+0: dstRect(4), SP+4: picHandle(4)
            // DrawPicture ($A8F6): Full PICT v1/v2 parser with opcodes in `pict.rs`; supports 1/2/4/8/16/32bpp with dynamic device CLUT color matching
            (true, 0x0F6) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_rect_ptr = bus.read_long(sp);
                let pic_handle = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);

                if pic_handle != 0 {
                    let pic_ptr = bus.read_long(pic_handle);
                    let dst_top = bus.read_word(dst_rect_ptr) as i16;
                    let dst_left = bus.read_word(dst_rect_ptr + 2) as i16;
                    let dst_bottom = bus.read_word(dst_rect_ptr + 4) as i16;
                    let dst_right = bus.read_word(dst_rect_ptr + 6) as i16;

                    // Read current port's pixel storage and bounds for coordinate conversion.
                    // On a real Mac, DrawPicture draws relative to the current port's local
                    // coordinate system. The port's bitmap bounds define the mapping from
                    // local coordinates to pixel memory offsets.
                    let a5 = cpu.read_reg(Register::A5);
                    let global_ptr = bus.read_long(a5);
                    let port = bus.read_long(global_ptr);

                    let port_version = bus.read_word(port.wrapping_add(6));
                    let is_color = (port_version & 0xC000) != 0;

                    let (
                        port_base,
                        port_rb,
                        port_bounds_top,
                        port_bounds_left,
                        port_w,
                        port_h,
                        port_ps,
                        port_ctab_handle,
                    ) = if is_color {
                        let pix_map_handle = bus.read_long(port.wrapping_add(2));
                        let pix_map_ptr = if pix_map_handle != 0 {
                            bus.read_long(pix_map_handle)
                        } else {
                            0
                        };
                        if pix_map_ptr != 0 {
                            // Offscreen PixMap baseAddr is a Handle to pixel
                            // storage; onscreen PixMap baseAddr is direct.
                            // Resolve through the helper so DrawPicture writes
                            // actual pixels rather than clobbering handle cells.
                            let base = Self::offscreen_pixmap_base_ptr(bus, pix_map_ptr);
                            let rb = (bus.read_word(pix_map_ptr + 4) & 0x3FFF) as u32;
                            let bt = bus.read_word(pix_map_ptr + 6) as i16;
                            let bl = bus.read_word(pix_map_ptr + 8) as i16;
                            let bb = bus.read_word(pix_map_ptr + 10) as i16;
                            let br = bus.read_word(pix_map_ptr + 12) as i16;
                            let ps = bus.read_word(pix_map_ptr + 32);
                            let cth = bus.read_long(pix_map_ptr + 42); // pmTable
                            (
                                base,
                                rb,
                                bt,
                                bl,
                                (br - bl) as u16,
                                (bb - bt) as u16,
                                ps,
                                cth,
                            )
                        } else {
                            (
                                self.screen_mode.0,
                                self.screen_mode.1,
                                0i16,
                                0i16,
                                self.screen_mode.2,
                                self.screen_mode.3,
                                self.screen_mode.4,
                                0u32,
                            )
                        }
                    } else {
                        let base = bus.read_long(port.wrapping_add(2));
                        let rb = (bus.read_word(port.wrapping_add(6)) & 0x3FFF) as u32;
                        let bt = bus.read_word(port.wrapping_add(8)) as i16;
                        let bl = bus.read_word(port.wrapping_add(10)) as i16;
                        let bb = bus.read_word(port.wrapping_add(12)) as i16;
                        let br = bus.read_word(port.wrapping_add(14)) as i16;
                        (
                            base,
                            rb,
                            bt,
                            bl,
                            (br - bl) as u16,
                            (bb - bt) as u16,
                            1u16,
                            0u32,
                        )
                    };

                    // Read the current port's color table for PICT color remapping.
                    // DrawPicture should map into the port's logical ColorTable, not the
                    // transient hardware CLUT state. EV's title art is grayscale PICT
                    // data rendered while palette fades are in flight; remapping against
                    // the live hardware palette bakes the instantaneous fade into the
                    // stored pixels and collapses the image to black/white.
                    // Imaging With QuickDraw 1994, p. 7-11
                    let port_clut = self.read_port_clut(bus, port_ctab_handle);

                    // If dstRect is empty (0,0,0,0), use the picture's own frame
                    // rect as the destination — matching real Mac DrawPicture behavior.
                    // Inside Macintosh Volume I, I-190
                    let (use_top, use_left, use_bottom, use_right) =
                        if dst_top == 0 && dst_left == 0 && dst_bottom == 0 && dst_right == 0 {
                            let ft = bus.read_word(pic_ptr + 2) as i16;
                            let fl = bus.read_word(pic_ptr + 4) as i16;
                            let fb = bus.read_word(pic_ptr + 6) as i16;
                            let fr = bus.read_word(pic_ptr + 8) as i16;
                            (ft, fl, fb, fr)
                        } else {
                            (dst_top, dst_left, dst_bottom, dst_right)
                        };

                    if !Self::drawpicture_rect_intersects_current_port_clip(
                        bus, port, use_top, use_left, use_bottom, use_right,
                    ) {
                        if trace_dialog_draw_enabled() && self.dialog_tracking.is_some() {
                            if let Some((_, res_type, res_id)) =
                                self.loaded_handles.get(&pic_handle).copied()
                            {
                                eprintln!(
                                    "[DIALOG-DRAW] DrawPicture '{}' {} skipped outside current port clip rect=({},{}..{},{})",
                                    String::from_utf8_lossy(&res_type),
                                    res_id,
                                    use_top,
                                    use_left,
                                    use_bottom,
                                    use_right,
                                );
                            } else {
                                eprintln!(
                                    "[DIALOG-DRAW] DrawPicture skipped outside current port clip rect=({},{}..{},{})",
                                    use_top, use_left, use_bottom, use_right,
                                );
                            }
                        }
                        return Some(Ok(()));
                    }

                    if trace_drawpicture_enabled() {
                        let res_info = self.loaded_handles.get(&pic_handle).copied();
                        // Dump first 20 bytes of PICT data to verify integrity
                        let mut pict_bytes = Vec::new();
                        for i in 0..20u32 {
                            pict_bytes.push(bus.read_byte(pic_ptr + i));
                        }
                        eprintln!(
                            "[DRAWPICTURE] handle=${:08X} ptr=${:08X} portBase=${:08X} screenBase=${:08X} rect=({},{}..{},{}) port=${:08X} is_color={} ps={} rb={} res={:?} data={:02X?}",
                            pic_handle, pic_ptr, port_base, self.screen_mode.0,
                            use_top, use_left, use_bottom, use_right,
                            port, is_color, port_ps, port_rb,
                            res_info.map(|(_, rt, id)| (String::from_utf8_lossy(&rt).to_string(), id)),
                            pict_bytes,
                        );
                    }

                    if trace_menu_redraw_enabled()
                        && trace_menu_redraw_rect_intersects(
                            use_top, use_left, use_bottom, use_right,
                        )
                    {
                        if let Some((_, res_type, res_id)) =
                            self.loaded_handles.get(&pic_handle).copied()
                        {
                            eprintln!(
                                "[MENU-REDRAW] DrawPicture '{}' {} handle=${:08X} dstBase=${:08X} rect=({},{}..{},{} )",
                                String::from_utf8_lossy(&res_type),
                                res_id,
                                pic_handle,
                                port_base,
                                use_top,
                                use_left,
                                use_bottom,
                                use_right,
                            );
                        } else {
                            eprintln!(
                                "[MENU-REDRAW] DrawPicture handle=${:08X} dstBase=${:08X} rect=({},{}..{},{} )",
                                pic_handle,
                                port_base,
                                use_top,
                                use_left,
                                use_bottom,
                                use_right,
                            );
                        }
                    }
                    if trace_dialog_draw_enabled() && self.dialog_tracking.is_some() {
                        if let Some((_, res_type, res_id)) =
                            self.loaded_handles.get(&pic_handle).copied()
                        {
                            eprintln!(
                                "[DIALOG-DRAW] DrawPicture '{}' {} handle=${:08X} current_port=${:08X} dstBase=${:08X} rect=({},{}..{},{} )",
                                String::from_utf8_lossy(&res_type),
                                res_id,
                                pic_handle,
                                self.current_port,
                                port_base,
                                use_top,
                                use_left,
                                use_bottom,
                                use_right,
                            );
                        } else {
                            eprintln!(
                                "[DIALOG-DRAW] DrawPicture handle=${:08X} current_port=${:08X} dstBase=${:08X} rect=({},{}..{},{} )",
                                pic_handle,
                                self.current_port,
                                port_base,
                                use_top,
                                use_left,
                                use_bottom,
                                use_right,
                            );
                        }
                    }
                    if trace_dialog_port_dump_enabled() && self.dialog_tracking.is_some() {
                        Self::trace_port_snapshot(bus, "DrawPicture-current", self.current_port);
                    }
                    if trace_dialog_port_dump_enabled() && self.dialog_tracking.is_some() {
                        Self::trace_port_snapshot(bus, "DrawPicture-current", self.current_port);
                    }

                    // Convert dst from port-local coordinates to pixel coordinates
                    let adj_top = use_top - port_bounds_top;
                    let adj_left = use_left - port_bounds_left;
                    let adj_bottom = use_bottom - port_bounds_top;
                    let adj_right = use_right - port_bounds_left;

                    if trace_pict_inline_enabled() {
                        eprintln!(
                            "[PICT] DrawPicture port=${:08X} portBase=${:08X} portBounds=({},{}..{}W,{}H) adj=({},{}..{},{}) use=({},{}..{},{})",
                            port, port_base, port_bounds_top, port_bounds_left,
                            port_w, port_h,
                            adj_top, adj_left, adj_bottom, adj_right,
                            use_top, use_left, use_bottom, use_right,
                        );
                    }

                    let port_mode = (port_base, port_rb, port_w, port_h, port_ps);
                    let dst_clip = Self::drawpicture_current_port_pixel_clip(
                        bus,
                        port,
                        port_bounds_top,
                        port_bounds_left,
                        port_w,
                        port_h,
                    );

                    let picture_info = self.loaded_handles.get(&pic_handle).copied();
                    if trace_title_diag_enabled() && self.tick_count >= 80 && self.tick_count <= 110
                    {
                        if let Some((_, res_type, res_id)) = picture_info {
                            eprintln!(
                                "[TITLE-DIAG] DrawPicture tick={} port=${:08X} base=${:08X} handle=${:08X} type='{}' id={} rect=({},{}..{},{} )",
                                self.tick_count,
                                self.current_port,
                                port_base,
                                pic_handle,
                                String::from_utf8_lossy(&res_type),
                                res_id,
                                use_top,
                                use_left,
                                use_bottom,
                                use_right,
                            );
                        } else {
                            eprintln!(
                                "[TITLE-DIAG] DrawPicture tick={} port=${:08X} base=${:08X} handle=${:08X} rect=({},{}..{},{} )",
                                self.tick_count,
                                self.current_port,
                                port_base,
                                pic_handle,
                                use_top,
                                use_left,
                                use_bottom,
                                use_right,
                            );
                        }
                    }
                    let recent_resource_ctable_fetch = self
                        .take_recent_drawpicture_resource_ctable_fetch(
                            port, port_base, port_rb, port_ps,
                        );
                    let recent_resource_ctable_clut =
                        recent_resource_ctable_fetch.as_ref().and_then(|fetch| {
                            (bus.read_long(fetch.ctab_handle) != 0)
                                .then(|| self.read_ctab_handle_clut(bus, fetch.ctab_handle))
                        });
                    if trace_palette_enabled() {
                        if let Some(fetch) = recent_resource_ctable_fetch.as_ref() {
                            eprintln!(
                                "[PALETTE] DrawPictureUsingFetchedCTable tick={} trap={} port=${:08X} ct_id={} picHandle=${:08X}",
                                self.tick_count,
                                self.trap_count,
                                port,
                                fetch.ct_id,
                                pic_handle,
                            );
                        }
                    }
                    // Always use the stable Color Manager CLUT for PICT rendering.
                    // Using device_clut during seeded-palette fades would bake
                    // transient palette indices into the framebuffer that become
                    // wrong after the palette reverts to standard system colors.
                    // Inside Macintosh: Imaging With QuickDraw 1994, p. 7-14
                    let draw_port_clut = recent_resource_ctable_clut.as_ref().unwrap_or(&port_clut);
                    let is_screen_picture_target =
                        port_base == self.screen_mode.0 && port_rb == self.screen_mode.1;
                    if !is_screen_picture_target {
                        if let (Some(fetch), Some(fetch_clut)) = (
                            recent_resource_ctable_fetch.as_ref(),
                            recent_resource_ctable_clut.as_ref(),
                        ) {
                            self.apply_fetched_ctable_to_offscreen_drawpicture_port(
                                bus,
                                port,
                                port_ctab_handle,
                                fetch,
                                fetch_clut,
                            );
                        }
                    }

                    let streamed_pic_ptr = self.materialize_recent_spooled_picture(bus, pic_ptr);
                    let draw_pic_ptr = streamed_pic_ptr.unwrap_or(pic_ptr);
                    let picture_bytes = bus
                        .get_alloc_size(draw_pic_ptr)
                        .or_else(|| bus.get_alloc_size(pic_ptr))
                        .unwrap_or(0);
                    let mut cost_top = i32::from(adj_top).max(0);
                    let mut cost_left = i32::from(adj_left).max(0);
                    let mut cost_bottom = i32::from(adj_bottom).min(i32::from(port_h));
                    let mut cost_right = i32::from(adj_right).min(i32::from(port_w));
                    if let Some(dst_clip) = dst_clip.as_ref() {
                        let (clip_top, clip_left, clip_bottom, clip_right) = dst_clip.rect();
                        cost_top = cost_top.max(clip_top);
                        cost_left = cost_left.max(clip_left);
                        cost_bottom = cost_bottom.min(clip_bottom);
                        cost_right = cost_right.min(clip_right);
                    }
                    let cost_width = cost_right.saturating_sub(cost_left);
                    let cost_height = cost_bottom.saturating_sub(cost_top);
                    self.add_hle_tick_cost(Self::draw_picture_tick_cost(
                        cost_width as u32,
                        cost_height as u32,
                        picture_bytes,
                    ));
                    let device_ct_seed =
                        Self::ctab_seed(bus, self.current_gdevice_ctab_handle(bus)).unwrap_or(0);
                    let (ok, pict_clut) = super::pict::draw_picture(
                        bus,
                        draw_pic_ptr,
                        adj_top,
                        adj_left,
                        adj_bottom,
                        adj_right,
                        port_mode,
                        draw_port_clut,
                        device_ct_seed,
                        dst_clip.as_ref(),
                    );

                    if trace_palette_enabled() {
                        let fetch_ctab_handle_nonzero = recent_resource_ctable_fetch
                            .as_ref()
                            .map(|f| bus.read_long(f.ctab_handle) != 0)
                            .unwrap_or(false);
                        eprintln!(
                            "[PALETTE] DrawPicture-seed-gate tick={} ok={} port_ps={} port_base=${:08X} screen_base=${:08X} port_rb={} screen_rb={} recent_fetch={} fetch_clut={} ctab_nonzero={} pict_clut={} picHandle=${:08X}",
                            self.tick_count, ok, port_ps, port_base,
                            self.screen_mode.0, port_rb, self.screen_mode.1,
                            recent_resource_ctable_fetch.is_some(),
                            recent_resource_ctable_clut.is_some(),
                            fetch_ctab_handle_nonzero,
                            pict_clut.is_some(),
                            pic_handle,
                        );
                    }
                    if ok && port_ps == 8 && is_screen_picture_target {
                        if let (Some(fetch), Some(fetch_clut)) = (
                            recent_resource_ctable_fetch.as_ref(),
                            recent_resource_ctable_clut.as_ref(),
                        ) {
                            self.seed_screen_palette_from_picture_clut(
                                bus,
                                pic_handle,
                                draw_pic_ptr,
                                fetch_clut,
                                Some(fetch.ct_id),
                            );
                        } else if let Some(pict_clut) = pict_clut.as_ref() {
                            if let Some((raw_pict_array, pict_clut_array)) =
                                Self::picture_clut_arrays(&self.color_manager_clut, pict_clut)
                            {
                                if Self::should_seed_screen_palette_from_pict(
                                    &raw_pict_array,
                                    &self.color_manager_clut,
                                ) && !pict_seed_clut_disabled()
                                {
                                    if std::env::var_os("SYSTEMLESS_TRACE_CM_WRITE").is_some() {
                                        let cm_before = self.color_manager_clut[0];
                                        let pict0 = pict_clut_array[0];
                                        eprintln!(
                                            "[CM-WRITE] PictSeed@6097 tick={} cm[0]=({:04X},{:04X},{:04X}) <- pict[0]=({:04X},{:04X},{:04X})",
                                            self.tick_count, cm_before[0], cm_before[1], cm_before[2], pict0[0], pict0[1], pict0[2]
                                        );
                                    }
                                    self.device_clut = pict_clut_array;
                                    self.color_manager_clut = pict_clut_array;
                                    self.seeded_picture_palette = pict_clut_array;
                                    self.seeded_picture_palette_until_tick =
                                        self.tick_count.saturating_add(48);
                                    self.sync_canonical_offscreen_ctabs_to_clut(
                                        bus,
                                        &pict_clut_array,
                                    );
                                    // The first pass just drew against the port's
                                    // pre-seed logical table. Replay the picture
                                    // once against the newly-seeded screen CLUT so
                                    // direct screen draws land the same indices an
                                    // offscreen GWorld + later CopyBits would show.
                                    let device_ct_seed =
                                        Self::ctab_seed(bus, self.current_gdevice_ctab_handle(bus))
                                            .unwrap_or(0);
                                    let _ = super::pict::draw_picture(
                                        bus,
                                        draw_pic_ptr,
                                        adj_top,
                                        adj_left,
                                        adj_bottom,
                                        adj_right,
                                        port_mode,
                                        &self.device_clut,
                                        device_ct_seed,
                                        dst_clip.as_ref(),
                                    );
                                    if trace_palette_enabled() {
                                        eprintln!(
                                            "[PALETTE] SeedFromPicture tick={} picHandle=${:08X} picPtr=${:08X} cm[0]=({:04X},{:04X},{:04X}) cm[1]=({:04X},{:04X},{:04X}) cm[16]=({:04X},{:04X},{:04X}) cm[42]=({:04X},{:04X},{:04X}) cm[128]=({:04X},{:04X},{:04X}) cm[255]=({:04X},{:04X},{:04X})",
                                            self.tick_count,
                                            pic_handle,
                                            pic_ptr,
                                            self.color_manager_clut[0][0],
                                            self.color_manager_clut[0][1],
                                            self.color_manager_clut[0][2],
                                            self.color_manager_clut[1][0],
                                            self.color_manager_clut[1][1],
                                            self.color_manager_clut[1][2],
                                            self.color_manager_clut[16][0],
                                            self.color_manager_clut[16][1],
                                            self.color_manager_clut[16][2],
                                            self.color_manager_clut[42][0],
                                            self.color_manager_clut[42][1],
                                            self.color_manager_clut[42][2],
                                            self.color_manager_clut[128][0],
                                            self.color_manager_clut[128][1],
                                            self.color_manager_clut[128][2],
                                            self.color_manager_clut[255][0],
                                            self.color_manager_clut[255][1],
                                            self.color_manager_clut[255][2]
                                        );
                                    }
                                }
                            }
                        }
                    } else if ok && port_ps == 8 {
                        if let Some(pict_clut) = pict_clut.as_ref() {
                            if let Some((raw_pict_array, pict_clut_array)) =
                                Self::picture_clut_arrays(&port_clut, pict_clut)
                            {
                                if Self::should_preserve_offscreen_picture_indices_from_pict(
                                    &raw_pict_array,
                                    &port_clut,
                                    &self.color_manager_clut,
                                ) {
                                    let device_ct_seed =
                                        Self::ctab_seed(bus, self.current_gdevice_ctab_handle(bus))
                                            .unwrap_or(0);
                                    let _ = super::pict::draw_picture(
                                        bus,
                                        draw_pic_ptr,
                                        adj_top,
                                        adj_left,
                                        adj_bottom,
                                        adj_right,
                                        port_mode,
                                        &raw_pict_array,
                                        device_ct_seed,
                                        dst_clip.as_ref(),
                                    );
                                    if trace_palette_enabled() {
                                        eprintln!(
                                            "[PALETTE] PreserveOffscreenPictureIndices tick={} port=${:08X} picHandle=${:08X} rgb16=({:04X},{:04X},{:04X}) rgb42=({:04X},{:04X},{:04X}) rgb128=({:04X},{:04X},{:04X})",
                                            self.tick_count,
                                            port,
                                            pic_handle,
                                            raw_pict_array[16][0],
                                            raw_pict_array[16][1],
                                            raw_pict_array[16][2],
                                            raw_pict_array[42][0],
                                            raw_pict_array[42][1],
                                            raw_pict_array[42][2],
                                            raw_pict_array[128][0],
                                            raw_pict_array[128][1],
                                            raw_pict_array[128][2],
                                        );
                                    }
                                } else if Self::should_seed_screen_palette_from_pict(
                                    &raw_pict_array,
                                    &port_clut,
                                ) && !pict_seed_clut_disabled()
                                {
                                    if port_ctab_handle != 0 {
                                        let ctab_ptr = bus.read_long(port_ctab_handle);
                                        let ct_flags = if ctab_ptr != 0 {
                                            bus.read_word(ctab_ptr + 4)
                                        } else {
                                            0x8000
                                        };
                                        let _ = self.overwrite_color_table_handle_with_clut(
                                            bus,
                                            port_ctab_handle,
                                            &pict_clut_array,
                                            ct_flags,
                                        );
                                    }
                                    let device_ct_seed =
                                        Self::ctab_seed(bus, self.current_gdevice_ctab_handle(bus))
                                            .unwrap_or(0);
                                    let _ = super::pict::draw_picture(
                                        bus,
                                        draw_pic_ptr,
                                        adj_top,
                                        adj_left,
                                        adj_bottom,
                                        adj_right,
                                        port_mode,
                                        &pict_clut_array,
                                        device_ct_seed,
                                        dst_clip.as_ref(),
                                    );
                                    if trace_palette_enabled() {
                                        eprintln!(
                                            "[PALETTE] SeedOffscreenFromPicture tick={} port=${:08X} ctab=${:08X} picHandle=${:08X} rgb16=({:04X},{:04X},{:04X}) rgb42=({:04X},{:04X},{:04X}) rgb128=({:04X},{:04X},{:04X})",
                                            self.tick_count,
                                            port,
                                            port_ctab_handle,
                                            pic_handle,
                                            pict_clut_array[16][0],
                                            pict_clut_array[16][1],
                                            pict_clut_array[16][2],
                                            pict_clut_array[42][0],
                                            pict_clut_array[42][1],
                                            pict_clut_array[42][2],
                                            pict_clut_array[128][0],
                                            pict_clut_array[128][1],
                                            pict_clut_array[128][2],
                                        );
                                    }
                                }
                            }
                        }
                    }

                    if let Some(streamed_pic_ptr) = streamed_pic_ptr {
                        bus.free(streamed_pic_ptr);
                    }

                    if trace_drawpicture_enabled() {
                        // Probe some pixels in the drawn area to verify they were written
                        let probe_y = (adj_top + adj_bottom) / 2; // middle row
                        let probe_x = (adj_left + adj_right) / 2; // middle col
                        let probe_addr =
                            port_mode.0 + (probe_y as u32) * port_mode.1 + (probe_x as u32);
                        let probe_val = bus.read_byte(probe_addr);
                        let probe2_addr = port_mode.0
                            + ((adj_top + 10) as u32) * port_mode.1
                            + ((adj_left + 10) as u32);
                        let probe2_val = bus.read_byte(probe2_addr);
                        eprintln!(
                            "[DRAWPICTURE] result ok={} adj=({},{}..{},{}) port_mode=({:08X},{},{},{},{}) probe@({},{})=idx{} probe2@({},{})=idx{}",
                            ok, adj_top, adj_left, adj_bottom, adj_right,
                            port_mode.0, port_mode.1, port_mode.2, port_mode.3, port_mode.4,
                            probe_y, probe_x, probe_val,
                            adj_top + 10, adj_left + 10, probe2_val,
                        );
                    }

                    if trace_menu_redraw_enabled() {
                        let port_info = CopyBitmapInfo {
                            base: port_base,
                            row_bytes: port_rb,
                            bounds_top: port_bounds_top,
                            bounds_left: port_bounds_left,
                            bounds_bottom: port_bounds_top + port_h as i16,
                            bounds_right: port_bounds_left + port_w as i16,
                            pixel_size: port_ps as u32,
                            ctab_handle: port_ctab_handle,
                        };
                        for (label, probe_y, probe_x) in trace_menu_probe_points() {
                            if trace_menu_rect_contains_point(
                                use_top, use_left, use_bottom, use_right, probe_y, probe_x,
                            ) {
                                eprintln!(
                                    "[MENU-REDRAW] DrawPicture probe={} pixel={:?}",
                                    label,
                                    Self::read_bitmap_pixel(bus, &port_info, probe_y, probe_x),
                                );
                            }
                        }
                    }

                    if ok && port_base == self.screen_mode.0 {
                        self.refresh_visible_dialog_snapshot_after_bulk_port_draw(
                            bus,
                            self.current_port,
                        );
                    }

                    let _ = ok;
                }
                Ok(())
            }

            // OpenPicture ($A8F3)
            // Starts recording drawing operations into a picture.
            // Drawing commands between OpenPicture and ClosePicture execute
            // on screen and are captured as a bitmap snapshot on ClosePicture.
            // FUNCTION OpenPicture(picFrame: Rect): PicHandle;
            // Inside Macintosh Volume I, I-189
            // OpenPicture ($A8F3): Allocates picture handle, writes header
            (true, 0x0F3) => {
                let sp = cpu.read_reg(Register::A7);
                let frame_ptr = bus.read_long(sp);
                let frame_top = bus.read_word(frame_ptr) as i16;
                let frame_left = bus.read_word(frame_ptr + 2) as i16;
                let frame_bottom = bus.read_word(frame_ptr + 4) as i16;
                let frame_right = bus.read_word(frame_ptr + 6) as i16;

                // Allocate a minimal Picture header (will be replaced on ClosePicture)
                let pic = bus.alloc(12);
                let handle = bus.alloc(4);
                bus.write_long(handle, pic);

                // Write picFrame into the picture header
                bus.write_word(pic, 0); // picSize placeholder
                bus.write_word(pic + 2, frame_top as u16);
                bus.write_word(pic + 4, frame_left as u16);
                bus.write_word(pic + 6, frame_bottom as u16);
                bus.write_word(pic + 8, frame_right as u16);

                self.recording_picture =
                    Some((handle, frame_top, frame_left, frame_bottom, frame_right));

                eprintln!(
                    "[TRAP] OpenPicture: handle=${:08X} frame=({},{},{},{})",
                    handle, frame_top, frame_left, frame_bottom, frame_right
                );

                bus.write_long(sp + 4, handle);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // ClosePicture ($A8F4)
            // Ends picture recording. Captures the picFrame region of the
            // screen as a 1bpp bitmap and stores it in the picture handle.
            // PROCEDURE ClosePicture;
            // Inside Macintosh Volume I, I-189
            // ClosePicture ($A8F4): Writes PICT bitmap data with header and bounds
            (true, 0x0F4) => {
                if let Some((handle, top, left, bottom, right)) = self.recording_picture.take() {
                    let w = (right - left) as u32;
                    let h = (bottom - top) as u32;

                    if w > 0 && h > 0 {
                        let (scrn_base, scrn_rb, _, _, _) = self.screen_mode;
                        // Row bytes for the captured bitmap (word-aligned)
                        let cap_rb = w.div_ceil(16) * 2;
                        // Total: 10 (PICT header) + 14 (BitMap) + 8 (srcRect) + 8 (dstRect)
                        //        + 2 (mode) + bitmap_data
                        let bmp_size = cap_rb * h;
                        let pic_size = 10 + 14 + 8 + 8 + 2 + bmp_size;

                        let pic = bus.alloc(pic_size);
                        // Update handle to point to new, larger allocation
                        bus.write_long(handle, pic);

                        // PICT header
                        bus.write_word(pic, pic_size as u16); // picSize
                        bus.write_word(pic + 2, top as u16);
                        bus.write_word(pic + 4, left as u16);
                        bus.write_word(pic + 6, bottom as u16);
                        bus.write_word(pic + 8, right as u16);

                        // PICT v1 opcode 0x0090 = BitsRect
                        let mut pos = pic + 10;
                        bus.write_word(pos, 0x0090);
                        pos += 2;

                        // BitMap: baseAddr(4) + rowBytes(2) + bounds(8)
                        let bitmap_data_start = pic + 10 + 14 + 8 + 8 + 2;
                        bus.write_long(pos, bitmap_data_start);
                        pos += 4; // baseAddr
                        bus.write_word(pos, cap_rb as u16);
                        pos += 2; // rowBytes
                        bus.write_word(pos, 0);
                        pos += 2; // bounds.top
                        bus.write_word(pos, 0);
                        pos += 2; // bounds.left
                        bus.write_word(pos, h as u16);
                        pos += 2; // bounds.bottom
                        bus.write_word(pos, w as u16);
                        pos += 2; // bounds.right

                        // srcRect
                        bus.write_word(pos, 0);
                        pos += 2;
                        bus.write_word(pos, 0);
                        pos += 2;
                        bus.write_word(pos, h as u16);
                        pos += 2;
                        bus.write_word(pos, w as u16);
                        pos += 2;

                        // dstRect (= picFrame)
                        bus.write_word(pos, top as u16);
                        pos += 2;
                        bus.write_word(pos, left as u16);
                        pos += 2;
                        bus.write_word(pos, bottom as u16);
                        pos += 2;
                        bus.write_word(pos, right as u16);
                        pos += 2;

                        // mode = srcCopy
                        bus.write_word(pos, 0); // pos += 2;

                        // Capture screen pixels into the bitmap
                        for row in 0..h {
                            let src_y = top as u32 + row;
                            for col_byte in 0..cap_rb {
                                let src_x_base = left as u32 + col_byte * 8;
                                let src_addr = scrn_base + src_y * scrn_rb + src_x_base / 8;
                                let byte = bus.read_byte(src_addr);
                                bus.write_byte(bitmap_data_start + row * cap_rb + col_byte, byte);
                            }
                        }

                        eprintln!(
                            "[TRAP] ClosePicture: captured {}x{} bitmap ({} bytes) for handle=${:08X}",
                            w, h, bmp_size, handle
                        );
                    }
                }
                Ok(())
            }

            // KillPicture ($A8F5)
            // Releases the memory used by the specified picture.
            // PROCEDURE KillPicture(myPicture: PicHandle);
            // Inside Macintosh Volume I, I-189
            //
            // Regression coverage:
            //   killpicture_frees_picture
            //   killpicture_nil_handle_is_safe
            // KillPicture ($A8F5): Frees picture data + handle per IM:I I-189
            (true, 0x0F5) => {
                let sp = cpu.read_reg(Register::A7);
                let pic_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if pic_handle != 0 {
                    let pic_ptr = bus.read_long(pic_handle);
                    if pic_ptr != 0 {
                        bus.free(pic_ptr);
                    }
                    bus.free(pic_handle);
                }
                Ok(())
            }

            // ========== Color QuickDraw Extended ==========

            // GetCTSeed ($AA28)
            // FUNCTION GetCTSeed: LONGINT;
            // Inside Macintosh Volume V (1986), p. V-143 (Color
            // Manager — Color Table Management — GetCTSeed). MPW
            // Universal Headers Quickdraw.h:
            //   EXTERN_API( long ) GetCTSeed(void) ONEWORDINLINE(0xAA28);
            //
            // Pop-0 + 4-byte LONGINT result-slot calling convention
            // (Tool-bit Pascal FUNCTION): caller pre-pushes a 4-byte
            // result slot (A7 -= 4), trap writes the LONGINT word at
            // [A7] without popping any argument frame (pop-0), caller
            // pops the 4-byte slot (A7 += 4) — net A7 zero across the
            // C-level call.
            //
            // Per IM:V V-143 the returned seed is guaranteed to be
            // greater than the predefined constant `minSeed` (= 1023)
            // and is intended for use in the ctSeed field of a custom
            // color table so the table is recognised as distinct
            // during color table translation. Absolute seed values
            // are engines-divergent — BII's ROM Color Manager has its
            // own internal counter initialised above minSeed at boot
            // while Systemless HLE counts monotonically from 1 via
            // `next_color_table_seed`. The "non-zero" envelope is
            // engines-agree on both engines.
            //
            // Paired strict bake (engines-agree calling-convention +
            // non-zero return witness): aa28_getctseed_strict.
            (true, 0x228) => {
                let sp = cpu.read_reg(Register::A7);
                bus.write_long(sp, self.next_color_table_seed());
                Ok(())
            }

            // QDError ($AA40)
            // FUNCTION QDError: INTEGER;
            // Inside Macintosh Volume V (1986), p. V-145 (Color Manager
            // — Error Handling — QDError); Imaging With QuickDraw
            // (1994), pp. 4-94 to 4-95. MPW Universal Headers
            // Quickdraw.h: EXTERN_API( short ) QDError(void)
            // ONEWORDINLINE(0xAA40).
            //
            // Pop-0 + 2-byte result-slot calling convention (Tool-bit
            // Pascal FUNCTION): caller pre-pushes a 2-byte result slot
            // (A7 -= 2), trap writes the INTEGER word at [A7] without
            // popping any argument frame, caller pops the 2-byte slot
            // (A7 += 2) — net A7 zero across the C-level call.
            //
            // Per IM:V V-145 QDError returns the error result from the
            // last QuickDraw or Color Manager call. Systemless HLE is a
            // hardcoded noErr stub that does not track per-call error
            // state; after clean InitGraf/InitFonts/InitWindows/OpenPort
            // with no failing operations preceding the call, BII System
            // 7.5.3 ROM Color QuickDraw also returns noErr, so the
            // engines-agree path is exercisable.
            //
            // Engines-agree subset (catalogue-proven):
            //   - aa40_qderror_strict (pop-0 + 2-byte result slot
            //     calling convention + noErr value + 5-call composition
            //     net A7-zero).
            //
            // Absolute non-zero error states are engines-divergent: BII
            // tracks per-call error codes through qdGlobals while
            // Systemless always returns noErr.
            //
            // Contract coverage:
            //   quickdraw::tests::qderror_returns_noerr_in_function_result_slot_without_stack_pop
            //   quickdraw::tests::qderror_stub_preserves_general_registers_while_writing_result_slot
            //   quickdraw::tests::qderror_pascal_function_preserves_stack_across_five_calls
            (true, 0x240) => {
                let sp = cpu.read_reg(Register::A7);
                bus.write_word(sp, 0u16); // noErr — no QD errors tracked
                Ok(())
            }

            // SetEntries ($AA3F)
            // PROCEDURE SetEntries(start, count: INTEGER; aTable: CSpecArray);
            // Inside Macintosh Volume V (1986), p. V-143 (Color Manager —
            // Color Manager Routines — SetEntries). MPW Universal Headers
            // Quickdraw.h: EXTERN_API(void) SetEntries(short start,
            // short count, CSpecArray aTable) ONEWORDINLINE(0xAA3F).
            //
            // 8-byte on-stack arg frame (Tool-bit Pascal PROCEDURE):
            //   SP+0: aTable (4-byte CSpecArray pointer, pushed last)
            //   SP+4: count  (2-byte INTEGER, zero-based last index)
            //   SP+6: start  (2-byte INTEGER, pushed first)
            // No FUNCTION result slot. Trap pops 8 bytes.
            //
            // Sequence mode (start >= 0): writes count+1 entries to CLUT
            // indices start..start+count; ColorSpec.value fields are
            // ignored. Index mode (start == -1): each ColorSpec.value
            // names the destination CLUT index.
            //
            // Engines-agree subset (catalogue-proven):
            //   - aa3f_setentries_strict (pop-8 calling convention +
            //     5-call composition net A7-zero).
            //
            // Absolute device-CLUT bulk update is engines-divergent:
            // BII walks the active gDevice's CLUT per IM:V V-143 while
            // Systemless updates device_clut and reseeds the current
            // GDevice ColorTable.
            //
            // Contract coverage:
            //   quickdraw::tests::setentries_pascal_procedure_preserves_stack_across_five_calls
            (true, 0x23F) => {
                let sp = cpu.read_reg(Register::A7);
                let trap_pc = cpu.read_reg(Register::PC).wrapping_sub(2);
                let table_ptr = bus.read_long(sp);
                let count = bus.read_word(sp + 4) as i16;
                let start = bus.read_word(sp + 6) as i16;
                cpu.write_reg(Register::A7, sp + 8);
                if trace_palette_enabled() {
                    eprintln!(
                        "[PALETTE] SetEntries-call pc=${:08X} a5=${:08X} table=${:08X} start={} count={}",
                        trap_pc, cpu.read_reg(Register::A5), table_ptr, start, count
                    );
                }
                // Apply the palette BEFORE logging so screenshots captured at
                // the screen_event_count increment see the updated CLUT.
                self.apply_set_entries_with_gdevice(bus, table_ptr, start, count);
                let safe_count = if count < 0 { 255 } else { count.min(255) };
                // Hour-88 fade trace: when SetEntries writes 0AAA to
                // first entry (last fade-down step on Systemless before the
                // divergence), enable tracing the next 30 traps to see
                // what EV does after the fade step.
                if trace_fade_branch_enabled() && safe_count >= 255 && start == 0 {
                    let first_r = bus.read_word(table_ptr + 2);
                    if first_r == 0x0AAA {
                        eprintln!(
                            "[FADE-TRACE] SetEntries wrote 0AAA at tick={} pc=${:08X}",
                            self.tick_count, trap_pc
                        );
                        self.fade_trace_remaining = 30;
                    }
                }
                if let Err(err) = self.record_oracle_event(
                    bus,
                    trap_pc,
                    "set_entries",
                    Self::oracle_palette_field_map(bus, table_ptr, start, safe_count),
                    true,
                ) {
                    return Some(Err(err));
                }
                Ok(())
            }

            // GetCIcon ($AA1E)
            // FUNCTION GetCIcon(iconID: INTEGER): CIconHandle;
            // Inside Macintosh Volume V (1986), p. V-76 (Color QuickDraw —
            // Operations on Color Icons — GetCIcon): "allocates a CIcon
            // data structure and initializes it using the information in
            // the resource of type 'cicn' with the specified ID. It
            // returns the handle to the icon's data structure. If the
            // specified resource isn't found, a NIL handle is returned."
            //
            // MPW Universal Headers Icons.h declares:
            //     EXTERN_API(CIconHandle) GetCIcon(SInt16 iconID)
            //                                         ONEWORDINLINE(0xAA1E);
            //
            // Tool-bit Pascal FUNCTION ABI: caller pre-pushes a 4-byte
            // CIconHandle result slot, then pushes a 2-byte iconID
            // INTEGER, trap pops the 2-byte arg and writes the handle
            // to the 4-byte result slot at the post-pop SP, caller pops
            // the slot. Net A7 effect across the C-level call is zero.
            //
            // Engines-agree subset (witnessed by
            // aa1e_getcicon_strict bands B1+B2 and
            // aa25_disposecicon_strict band B2):
            //   * Pascal FUNCTION calling convention (A7-balanced
            //     across the C-level call sequence).
            //   * Miss-returns-NIL contract per IM:V V-76 ("If the
            //     specified resource isn't found, a NIL handle is
            //     returned").
            //   * Present-resource calls allocate a fresh CIcon copy on
            //     each hit and seed iconData with a private pixel
            //     buffer copied from the inline bytes after the
            //     ColorTable.
            (true, 0x21E) => {
                let sp = cpu.read_reg(Register::A7);
                let icon_id = bus.read_word(sp) as i16;
                let handle = if let Some((_, resource_ptr)) =
                    self.find_resource_any(*b"cicn", icon_id)
                {
                    let resource_size = bus.get_alloc_size(resource_ptr).unwrap_or(0);
                    if resource_size == 0 {
                        0
                    } else {
                        let icon_ptr = bus.alloc(resource_size);
                        if icon_ptr == 0 {
                            0
                        } else {
                            let resource_bytes =
                                bus.read_bytes(resource_ptr, resource_size as usize);
                            bus.write_bytes(icon_ptr, &resource_bytes);

                            let resource_end = icon_ptr.saturating_add(resource_size);
                            let pm_top = bus.read_word(icon_ptr + 6) as i16;
                            let pm_bottom = bus.read_word(icon_ptr + 10) as i16;
                            let icon_h = (pm_bottom - pm_top).max(0) as u32;
                            let mask_rb = (bus.read_word(icon_ptr + 54) & 0x3FFF) as u32;
                            let bmap_rb = (bus.read_word(icon_ptr + 68) & 0x3FFF) as u32;
                            let mask_size = mask_rb * icon_h;
                            let bmap_size = bmap_rb * icon_h;
                            let mask_data_ptr = icon_ptr + 82;
                            let bmap_data_ptr = mask_data_ptr + mask_size;
                            let ctab_ptr = icon_ptr + 82 + mask_size + bmap_size;
                            let ctab_header_valid = ctab_ptr
                                .checked_add(8)
                                .is_some_and(|end| end <= resource_end);
                            let ctab_total_bytes = if ctab_header_valid {
                                let ct_size = bus.read_word(ctab_ptr + 6) as u32;
                                8 + (ct_size + 1) * 8
                            } else {
                                0
                            };
                            let ctab_valid = ctab_total_bytes != 0
                                && ctab_ptr
                                    .checked_add(ctab_total_bytes)
                                    .is_some_and(|end| end <= resource_end);
                            let inline_pixel_ptr = if ctab_valid {
                                ctab_ptr + ctab_total_bytes
                            } else {
                                resource_end
                            };
                            let pixel_len = resource_end.saturating_sub(inline_pixel_ptr);

                            let mut allocation_failed = false;
                            let icon_data_handle = if pixel_len > 0 {
                                let pixel_ptr = bus.alloc(pixel_len);
                                if pixel_ptr == 0 {
                                    bus.free(icon_ptr);
                                    allocation_failed = true;
                                    0
                                } else {
                                    let pixel_bytes =
                                        bus.read_bytes(inline_pixel_ptr, pixel_len as usize);
                                    bus.write_bytes(pixel_ptr, &pixel_bytes);
                                    let pixel_handle = bus.alloc(4);
                                    if pixel_handle == 0 {
                                        bus.free(pixel_ptr);
                                        bus.free(icon_ptr);
                                        allocation_failed = true;
                                        0
                                    } else {
                                        bus.write_long(pixel_handle, pixel_ptr);
                                        self.ptr_to_handle.insert(pixel_ptr, pixel_handle);
                                        pixel_handle
                                    }
                                }
                            } else {
                                let pixel_handle = bus.alloc(4);
                                if pixel_handle == 0 {
                                    bus.free(icon_ptr);
                                    allocation_failed = true;
                                    0
                                } else {
                                    bus.write_long(pixel_handle, 0);
                                    pixel_handle
                                }
                            };

                            let icon_pm_table_handle = if icon_data_handle != 0 && ctab_valid {
                                let ctab_copy_ptr = bus.alloc(ctab_total_bytes);
                                if ctab_copy_ptr == 0 {
                                    Self::free_handle_and_target(bus, icon_data_handle);
                                    bus.free(icon_ptr);
                                    allocation_failed = true;
                                    0
                                } else {
                                    let ctab_bytes =
                                        bus.read_bytes(ctab_ptr, ctab_total_bytes as usize);
                                    bus.write_bytes(ctab_copy_ptr, &ctab_bytes);
                                    let ctab_handle = bus.alloc(4);
                                    if ctab_handle == 0 {
                                        bus.free(ctab_copy_ptr);
                                        Self::free_handle_and_target(bus, icon_data_handle);
                                        bus.free(icon_ptr);
                                        allocation_failed = true;
                                        0
                                    } else {
                                        bus.write_long(ctab_handle, ctab_copy_ptr);
                                        self.ptr_to_handle.insert(ctab_copy_ptr, ctab_handle);
                                        ctab_handle
                                    }
                                }
                            } else {
                                0
                            };

                            if allocation_failed || (icon_data_handle == 0 && resource_size != 0) {
                                0
                            } else {
                                let icon_data_ptr = bus.read_long(icon_data_handle);
                                bus.write_long(icon_ptr, icon_data_ptr);
                                bus.write_long(icon_ptr + 42, icon_pm_table_handle);
                                bus.write_long(
                                    icon_ptr + 50,
                                    if mask_size == 0 { 0 } else { mask_data_ptr },
                                );
                                bus.write_long(
                                    icon_ptr + 64,
                                    if bmap_size == 0 { 0 } else { bmap_data_ptr },
                                );
                                bus.write_long(icon_ptr + 78, icon_data_handle);
                                let icon_handle = bus.alloc(4);
                                if icon_handle == 0 {
                                    Self::free_handle_and_target(bus, icon_pm_table_handle);
                                    Self::free_handle_and_target(bus, icon_data_handle);
                                    bus.free(icon_ptr);
                                    0
                                } else {
                                    bus.write_long(icon_handle, icon_ptr);
                                    self.ptr_to_handle.insert(icon_ptr, icon_handle);
                                    icon_handle
                                }
                            }
                        }
                    }
                } else {
                    0
                };

                bus.write_long(sp + 2, handle);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // PlotCIcon ($AA1F)
            // Draws a color icon in the specified rectangle.
            // PROCEDURE PlotCIcon (theRect: Rect; theIcon: CIconHandle);
            // More Macintosh Toolbox 1993, p. 5-25
            // PlotCIcon ($AA1F): Decodes CIcon (PixMap+iconMask+iconBMap+iconData) and blits scaled into theRect; mask honoured per More Macintosh Toolbox 1993, 5-25
            (true, 0x21F) => {
                let sp = cpu.read_reg(Register::A7);
                let icon_handle = bus.read_long(sp);
                let rect_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);

                if icon_handle == 0 {
                    return Some(Ok(()));
                }
                let icon_ptr = bus.read_long(icon_handle);
                if icon_ptr == 0 {
                    return Some(Ok(()));
                }
                if rect_ptr == 0 {
                    return Some(Ok(()));
                }

                let dst_top = bus.read_word(rect_ptr) as i16;
                let dst_left = bus.read_word(rect_ptr + 2) as i16;
                let dst_bottom = bus.read_word(rect_ptr + 4) as i16;
                let dst_right = bus.read_word(rect_ptr + 6) as i16;

                // CIcon layout (Inside Macintosh Volume V, V-64):
                //   0: iconPMap (PixMap, 50 bytes)
                //   50: iconMask (BitMap, 14 bytes)
                //   64: iconBMap (BitMap, 14 bytes)
                //   78: iconData (Handle, 4 bytes)
                //   82: iconMaskData[] (mask bits, then bmap bits)

                // Read PixMap fields from iconPMap
                let pm_row_bytes = (bus.read_word(icon_ptr + 4) & 0x3FFF) as u32;
                let pm_top = bus.read_word(icon_ptr + 6) as i16;
                let pm_left = bus.read_word(icon_ptr + 8) as i16;
                let pm_bottom = bus.read_word(icon_ptr + 10) as i16;
                let pm_right = bus.read_word(icon_ptr + 12) as i16;
                let pixel_size = bus.read_word(icon_ptr + 32) as u32;

                // Read mask BitMap fields from iconMask
                let mask_row_bytes = (bus.read_word(icon_ptr + 54) & 0x3FFF) as u32;
                let mask_top = bus.read_word(icon_ptr + 56) as i16;
                let mask_left = bus.read_word(icon_ptr + 58) as i16;
                let mask_bottom = bus.read_word(icon_ptr + 60) as i16;
                let mask_right = bus.read_word(icon_ptr + 62) as i16;
                // Read bmap BitMap rowBytes from iconBMap (offset 64+4)
                let bmap_row_bytes = (bus.read_word(icon_ptr + 68) & 0x3FFF) as u32;

                let icon_h_pre = (pm_bottom - pm_top) as i32;

                // Mask data starts at offset 82 in the CIcon record
                let mask_data_ptr = icon_ptr + 82;

                // Icon pixel data: GetCIcon expands the on-disk cicn into a
                // fresh CIcon copy and stores a Handle in iconData (offset
                // 78) that points at a private pixel buffer. If a caller
                // hands us a raw cicn or a hand-built record with a NIL
                // iconData handle, fall back to the inline bytes after the
                // ColorTable so the record still paints correctly.
                //
                // ColorTable header per Imaging With QuickDraw 1994 A-16:
                //   ctSeed (4) + ctFlags (2) + ctSize (2) = 8 bytes
                //   then (ctSize+1) entries × 8 bytes each.
                let mask_data_size = mask_row_bytes * icon_h_pre as u32;
                let bmap_data_size = bmap_row_bytes * icon_h_pre as u32;
                let ctab_ptr_for_data = mask_data_ptr + mask_data_size + bmap_data_size;
                let ct_size_for_data = bus.read_word(ctab_ptr_for_data + 6) as u32;
                let ctab_total_bytes = 8 + (ct_size_for_data + 1) * 8;
                let inline_pixel_data_ptr = ctab_ptr_for_data + ctab_total_bytes;

                let icon_data_handle = bus.read_long(icon_ptr + 78);
                let icon_data_ptr = if icon_data_handle != 0 {
                    let p = bus.read_long(icon_data_handle);
                    if p != 0 {
                        p
                    } else {
                        inline_pixel_data_ptr
                    }
                } else {
                    inline_pixel_data_ptr
                };

                let icon_h = icon_h_pre;
                let icon_w = (pm_right - pm_left) as i32;
                let dst_h = (dst_bottom - dst_top) as i32;
                let dst_w = (dst_right - dst_left) as i32;

                if icon_h <= 0 || icon_w <= 0 || dst_h <= 0 || dst_w <= 0 || icon_data_ptr == 0 {
                    return Some(Ok(()));
                }

                // Get current port's pixmap for destination
                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port = bus.read_long(global_ptr);
                if port == 0 {
                    return Some(Ok(()));
                }

                let real_port_version = bus.read_word(port + 6);
                let legacy_port_version = bus.read_word(port + 4);
                let (
                    dst_base,
                    dst_row_bytes,
                    dst_bounds_top,
                    dst_bounds_left,
                    dst_pixel_size,
                    dst_ctab_handle,
                ) = if (real_port_version & 0xC000) == 0xC000 {
                    let pm_handle = bus.read_long(port + 2);
                    let pm_ptr = bus.read_long(pm_handle);
                    (
                        Self::offscreen_pixmap_base_ptr(bus, pm_ptr),
                        (bus.read_word(pm_ptr + 4) & 0x3FFF) as u32,
                        bus.read_word(pm_ptr + 6) as i16,
                        bus.read_word(pm_ptr + 8) as i16,
                        bus.read_word(pm_ptr + 32) as u32,
                        bus.read_long(pm_ptr + 42),
                    )
                } else if (legacy_port_version & 0xC000) == 0xC000 {
                    let pm_handle = bus.read_long(port);
                    let pm_ptr = bus.read_long(pm_handle);
                    (
                        Self::offscreen_pixmap_base_ptr(bus, pm_ptr),
                        (bus.read_word(pm_ptr + 4) & 0x3FFF) as u32,
                        bus.read_word(pm_ptr + 6) as i16,
                        bus.read_word(pm_ptr + 8) as i16,
                        bus.read_word(pm_ptr + 32) as u32,
                        bus.read_long(pm_ptr + 42),
                    )
                } else {
                    let base = bus.read_long(port);
                    let (screen_base, _, _, _, screen_ps) = self.screen_mode;
                    let pixel_size = if base == screen_base && screen_ps > 1 {
                        u32::from(screen_ps)
                    } else {
                        1
                    };
                    (
                        base,
                        (bus.read_word(port + 4) & 0x3FFF) as u32,
                        bus.read_word(port + 6) as i16,
                        bus.read_word(port + 8) as i16,
                        pixel_size,
                        0,
                    )
                };
                let dst_pixel_size = if dst_pixel_size == 0 {
                    1
                } else {
                    dst_pixel_size
                };

                if trace_menu_redraw_enabled() {
                    let abs_top = dst_top + dst_bounds_top;
                    let abs_left = dst_left + dst_bounds_left;
                    let abs_bottom = dst_bottom + dst_bounds_top;
                    let abs_right = dst_right + dst_bounds_left;
                    if trace_menu_redraw_rect_intersects(abs_top, abs_left, abs_bottom, abs_right) {
                        if let Some((_, res_type, res_id)) =
                            self.loaded_handles.get(&icon_handle).copied()
                        {
                            eprintln!(
                                "[MENU-REDRAW] PlotCIcon '{}' {} handle=${:08X} dstBase=${:08X} rect=({},{}..{},{} )",
                                String::from_utf8_lossy(&res_type),
                                res_id,
                                icon_handle,
                                dst_base,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                            );
                        } else {
                            eprintln!(
                                "[MENU-REDRAW] PlotCIcon handle=${:08X} dstBase=${:08X} rect=({},{}..{},{} )",
                                icon_handle,
                                dst_base,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                            );
                        }
                    }
                }
                if trace_dialog_draw_enabled() && self.dialog_tracking.is_some() {
                    let abs_top = dst_top + dst_bounds_top;
                    let abs_left = dst_left + dst_bounds_left;
                    let abs_bottom = dst_bottom + dst_bounds_top;
                    let abs_right = dst_right + dst_bounds_left;
                    if let Some((_, res_type, res_id)) =
                        self.loaded_handles.get(&icon_handle).copied()
                    {
                        eprintln!(
                            "[DIALOG-DRAW] PlotCIcon '{}' {} handle=${:08X} current_port=${:08X} dstBase=${:08X} rect=({},{}..{},{} )",
                            String::from_utf8_lossy(&res_type),
                            res_id,
                            icon_handle,
                            self.current_port,
                            dst_base,
                            abs_top,
                            abs_left,
                            abs_bottom,
                            abs_right,
                        );
                    } else {
                        eprintln!(
                            "[DIALOG-DRAW] PlotCIcon handle=${:08X} current_port=${:08X} dstBase=${:08X} rect=({},{}..{},{} )",
                            icon_handle,
                            self.current_port,
                            dst_base,
                            abs_top,
                            abs_left,
                            abs_bottom,
                            abs_right,
                        );
                    }
                }

                // Read the icon's color table to remap pixels through the
                // canonical screen CLUT. ctab_ptr was already computed
                // above (`ctab_ptr_for_data`) so that the inline-pixel-data
                // fallback has the right offset; reuse it here.
                let ctab_ptr = ctab_ptr_for_data;
                let ct_size = bus.read_word(ctab_ptr + 6) as usize;
                let mut icon_clut = vec![[0u16; 3]; ct_size + 1];
                for (i, slot) in icon_clut.iter_mut().enumerate() {
                    let entry = ctab_ptr + 8 + (i as u32) * 8;
                    *slot = [
                        bus.read_word(entry + 2),
                        bus.read_word(entry + 4),
                        bus.read_word(entry + 6),
                    ];
                }

                let dst_clut = if dst_ctab_handle != 0 {
                    self.read_port_clut(bus, dst_ctab_handle)
                } else {
                    self.device_clut
                };

                for dst_y_local in dst_top..dst_bottom {
                    let Some(sy) =
                        Self::scale_coord(pm_top, pm_bottom, dst_top, dst_bottom, dst_y_local)
                    else {
                        continue;
                    };
                    for dst_x_local in dst_left..dst_right {
                        let Some(sx) =
                            Self::scale_coord(pm_left, pm_right, dst_left, dst_right, dst_x_local)
                        else {
                            continue;
                        };
                        let Some(mask_y) = Self::scale_coord(
                            mask_top,
                            mask_bottom,
                            dst_top,
                            dst_bottom,
                            dst_y_local,
                        ) else {
                            continue;
                        };
                        let Some(mask_x) = Self::scale_coord(
                            mask_left,
                            mask_right,
                            dst_left,
                            dst_right,
                            dst_x_local,
                        ) else {
                            continue;
                        };
                        let Some(mask_pixel) = Self::read_packed_bitmap_index(
                            bus,
                            mask_data_ptr,
                            mask_row_bytes,
                            1,
                            mask_top,
                            mask_left,
                            mask_bottom,
                            mask_right,
                            mask_y as i16,
                            mask_x as i16,
                        ) else {
                            continue;
                        };
                        if mask_pixel == 0 {
                            continue;
                        }

                        let dst_y = i32::from(dst_y_local) - i32::from(dst_bounds_top);
                        let dst_x = i32::from(dst_x_local) - i32::from(dst_bounds_left);
                        if dst_y < 0 || dst_x < 0 {
                            continue;
                        }
                        let dst_y = dst_y as u32;
                        let dst_x = dst_x as u32;

                        match dst_pixel_size {
                            8 => {
                                let Some(src_index) = Self::read_packed_bitmap_index(
                                    bus,
                                    icon_data_ptr,
                                    pm_row_bytes,
                                    pixel_size,
                                    pm_top,
                                    pm_left,
                                    pm_bottom,
                                    pm_right,
                                    sy as i16,
                                    sx as i16,
                                ) else {
                                    continue;
                                };
                                let rgb = icon_clut
                                    .get(src_index as usize)
                                    .copied()
                                    .unwrap_or(self.device_clut[src_index as usize]);
                                let dst_index = super::pict::closest_clut_index(
                                    rgb[0], rgb[1], rgb[2], &dst_clut,
                                );
                                bus.write_byte(dst_base + dst_y * dst_row_bytes + dst_x, dst_index);
                            }
                            1 => {
                                let Some(src_index) = Self::read_packed_bitmap_index(
                                    bus,
                                    icon_data_ptr,
                                    pm_row_bytes,
                                    pixel_size,
                                    pm_top,
                                    pm_left,
                                    pm_bottom,
                                    pm_right,
                                    sy as i16,
                                    sx as i16,
                                ) else {
                                    continue;
                                };
                                let dst_byte_off = dst_y * dst_row_bytes + dst_x / 8;
                                let dst_bit = 7 - (dst_x % 8);
                                let mut dst_byte = bus.read_byte(dst_base + dst_byte_off);
                                if src_index != 0 {
                                    dst_byte |= 1 << dst_bit;
                                } else {
                                    dst_byte &= !(1 << dst_bit);
                                }
                                bus.write_byte(dst_base + dst_byte_off, dst_byte);
                            }
                            _ => {}
                        }
                    }
                }
                Ok(())
            }

            // DisposeCIcon ($AA25)
            // Disposes all structures allocated by GetCIcon.
            // PROCEDURE DisposeCIcon (theIcon: CIconHandle);
            // More Macintosh Toolbox (1993), p. 5-30.
            //
            // Contract coverage:
            //   quickdraw::tests::disposecicon_consumes_theicon_ciconhandle_argument
            //   quickdraw::tests::disposecicon_preserves_nonstack_registers_while_releasing_icondata_handle
            // DisposeCIcon ($AA25): Pops 4 bytes (icon handle), releases
            // the GetCIcon-allocated iconData handle cached in the fresh
            // CIcon copy, frees the private CIcon record itself, and
            // detaches the live handle from RecoverHandle without touching
            // the source resource payload.
            (true, 0x225) => {
                let sp = cpu.read_reg(Register::A7);
                let icon_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if icon_handle != 0 {
                    let icon_ptr = bus.read_long(icon_handle);
                    if icon_ptr != 0 {
                        let icon_pm_table_handle = bus.read_long(icon_ptr + 42);
                        let icon_data_handle = bus.read_long(icon_ptr + 78);
                        bus.write_long(icon_ptr + 42, 0);
                        bus.write_long(icon_ptr + 78, 0);
                        Self::free_handle_and_target(bus, icon_pm_table_handle);
                        Self::free_handle_and_target(bus, icon_data_handle);
                        self.ptr_to_handle.remove(&icon_ptr);
                        self.forget_resource_handle_index_for_handle(icon_handle);
                        self.loaded_handles.remove(&icon_handle);
                        self.resource_handle_files.remove(&icon_handle);
                        bus.free(icon_ptr);
                        bus.write_long(icon_handle, 0);
                        bus.free(icon_handle);
                    }
                }
                Ok(())
            }

            // BitMapToRegion ($A8D7)
            // Converts a bitmap to a region.
            // FUNCTION BitMapToRegion (region: RgnHandle; bMap: BitMap): OSErr;
            // Imaging With QuickDraw 1994, pp. 2-49 to 2-50
            //
            // Per IM:IWQ 1994 p. 2-49: "The BitMapToRegion function
            // converts a given BitMap or PixMap record to a region. ...
            // The region parameter must be a valid region handle
            // created with the NewRgn function. The old region contents
            // are lost. The bMap parameter may be either a BitMap or
            // PixMap record. If you pass a PixMap record, its pixel
            // depth must be 1." Result codes per p. 2-49: noErr (0),
            // pixmapTooDeepErr (-148) when depth > 1, rgnTooBigErr
            // (-500) when the conversion would exceed 64 KB.
            //
            // MPW Universal Headers (Quickdraw.h):
            //   EXTERN_API(OSErr) BitMapToRegion(RgnHandle region,
            //                                    const BitMap *bMap)
            //                                    ONEWORDINLINE(0xA8D7);
            //
            // Stack frame at trap entry (Pascal LR push order with
            // result slot pre-allocated above the args):
            //   sp+0  bMap_ptr     (4 bytes, last pushed → shallowest)
            //   sp+4  rgn_handle   (4 bytes, first pushed → deepest)
            //   sp+8  OSErr result (2 bytes, function-result slot)
            // Trap pops 8 argument bytes and writes the OSErr at sp+8.
            //
            // HLE implementation: derives region topology from 1bpp
            // source ink runs by scanning row endpoints, emitting
            // y/run-endpoint pairs, and computing the bbox from the
            // inked extent. Empty sources collapse to a (0,0,0,0)
            // bbox + minimum 10-byte rgnSize ("empty region"). Single-
            // rectangle sources take the rectangular-region fast path
            // and emit no per-row run data. Non-1bpp parity with Color
            // QuickDraw's pixmapTooDeepErr remains a gap.
            //
            // Witnessed by:
            //   a8d7_bitmaptoregion_strict
            //   trap::quickdraw::tests::bitmaptoregion_*
            (true, 0x0D7) => {
                let sp = cpu.read_reg(Register::A7);
                let bmap_ptr = bus.read_long(sp);
                let rgn_handle = bus.read_long(sp + 4);
                let result_ptr = sp + 8;
                cpu.write_reg(Register::A7, sp + 8);

                if rgn_handle != 0 && bmap_ptr != 0 {
                    let info = self.resolve_copy_bitmap(bus, bmap_ptr);
                    let wrote_region;
                    if info.pixel_size == 1 && info.bounds_bottom > info.bounds_top {
                        let mut data_words = Vec::new();
                        let mut previous_row = Vec::new();
                        let mut bbox: Option<(i16, i16, i16, i16)> = None;
                        let mut rectangle_row = None;
                        let mut rectangle_candidate = true;
                        let mut saw_empty_after_content = false;

                        for y in info.bounds_top..=info.bounds_bottom {
                            let current_row = if y < info.bounds_bottom {
                                Self::scan_bitmap_row_endpoints(bus, &info, y)
                            } else {
                                Vec::new()
                            };

                            if y < info.bounds_bottom && !current_row.is_empty() {
                                bbox = Some(match bbox {
                                    Some((top, left, bottom, right)) => (
                                        top.min(y),
                                        left.min(current_row[0]),
                                        bottom.max(y + 1),
                                        right.max(*current_row.last().unwrap()),
                                    ),
                                    None => {
                                        (y, current_row[0], y + 1, *current_row.last().unwrap())
                                    }
                                });

                                if rectangle_row.is_none() {
                                    rectangle_row = Some(current_row.clone());
                                    rectangle_candidate = current_row.len() == 2;
                                } else if saw_empty_after_content
                                    || rectangle_row.as_ref() != Some(&current_row)
                                {
                                    rectangle_candidate = false;
                                }
                                saw_empty_after_content = false;
                            } else if bbox.is_some() {
                                saw_empty_after_content = true;
                            }

                            let delta = Self::merge_region_endpoints(&previous_row, &current_row);
                            if !delta.is_empty() {
                                data_words.push(y);
                                data_words.extend_from_slice(&delta);
                                data_words.push(REGION_STOP);
                            }
                            previous_row = current_row;
                        }

                        let wrote = if let Some((top, left, bottom, right)) = bbox {
                            if rectangle_candidate {
                                Self::write_region(
                                    bus,
                                    rgn_handle,
                                    Some((top, left, bottom, right)),
                                    &[],
                                )
                            } else {
                                data_words.push(REGION_STOP);
                                Self::write_region(
                                    bus,
                                    rgn_handle,
                                    Some((top, left, bottom, right)),
                                    &data_words,
                                )
                            }
                        } else {
                            Self::write_region(bus, rgn_handle, None, &[])
                        };

                        if !wrote {
                            bus.write_word(result_ptr, 0);
                            return Some(Ok(()));
                        }
                        wrote_region = true;
                    } else {
                        Self::write_region(bus, rgn_handle, None, &[]);
                        wrote_region = true;
                    }
                    if trace_region_ops_enabled() {
                        let bbox = Self::region_bbox(bus, rgn_handle);
                        let size = Self::region_ptr_and_size(bus, rgn_handle)
                            .map(|(_, size)| size)
                            .unwrap_or(0);
                        eprintln!(
                            "[REGION] tick={} BitMapToRegion bmap=${:08X} rgn=${:08X} base=${:08X} rowBytes={} bounds=({},{}..{},{}) pixelSize={} wrote={} bbox={:?} size={}",
                            self.tick_count,
                            bmap_ptr,
                            rgn_handle,
                            info.base,
                            info.row_bytes,
                            info.bounds_top,
                            info.bounds_left,
                            info.bounds_bottom,
                            info.bounds_right,
                            info.pixel_size,
                            wrote_region,
                            bbox,
                            size
                        );
                    }
                }
                // Return noErr
                bus.write_word(result_ptr, 0);
                Ok(())
            }

            // ========== Missing QD Stubs ==========

            // SetPenState ($A899)
            // Sets the pen state to the values in the specified record.
            // PROCEDURE SetPenState(pnState: PenState);
            // Inside Macintosh Volume I, I-170
            (true, 0x099) => {
                let sp = cpu.read_reg(Register::A7);
                let state_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if state_ptr != 0 {
                    self.pn_loc = (
                        bus.read_word(state_ptr) as i16,
                        bus.read_word(state_ptr + 2) as i16,
                    );
                    self.pn_size = (
                        bus.read_word(state_ptr + 4) as i16,
                        bus.read_word(state_ptr + 6) as i16,
                    );
                    self.pn_mode = bus.read_word(state_ptr + 8) as i16;
                    for i in 0..8 {
                        self.pn_pat[i] = bus.read_byte(state_ptr + 10 + i as u32);
                    }
                }
                Ok(())
            }

            // GetFontName ($A8FF)
            // PROCEDURE GetFontName(fontNum: INTEGER; VAR theName: Str255);
            // Inside Macintosh Volume I, I-223
            //
            // "GetFontName returns in theName the name of the font
            //  having the font number fontNum. If there's no such font,
            //  GetFontName returns the empty string."
            //
            // Stack (Pascal): SP+0=name_ptr(4), SP+4=fontNum(2). Pops 6.
            // Names come from src/quickdraw/fonts/mod.rs::FONT_NAMES,
            // which mirrors IM:Text Appendix B. Unknown font numbers
            // yield a zero-length Pascal string per the IM:I contract.
            //
            // Regression coverage:
            //   trap::quickdraw::tests::getfontname_known_font_returns_pascal_name
            //   trap::quickdraw::tests::getfontname_unknown_font_returns_empty_string
            //   trap::quickdraw::tests::getfontname_pops_six_bytes
            // GetFontName ($A8FF): IM:I I-223; name lookup via FONT_NAMES table; empty string for unknown IDs
            (true, 0x0FF) => {
                let sp = cpu.read_reg(Register::A7);
                let name_ptr = bus.read_long(sp);
                let font_num = bus.read_word(sp + 4) as i16;
                cpu.write_reg(Register::A7, sp + 6);
                if name_ptr != 0 {
                    let name = font_name_for_id(font_num)
                        .or_else(|| self.loaded_font_name_for_id(font_num))
                        .unwrap_or("");
                    // Str255 length byte is clamped to 255; all recognized
                    // font names are well under that so the truncate is
                    // purely defensive.
                    let len = name.len().min(255) as u8;
                    bus.write_byte(name_ptr, len);
                    for (i, b) in name.as_bytes().iter().take(len as usize).enumerate() {
                        bus.write_byte(name_ptr + 1 + i as u32, *b);
                    }
                }
                Ok(())
            }

            // GetFNum ($A900)
            // PROCEDURE GetFNum(fontName: Str255; VAR theNum: INTEGER);
            // Inside Macintosh Volume I, I-223
            //
            // "GetFNum returns in theNum the font number for the font
            //  having the given fontName. If there's no such font,
            //  GetFNum returns 0."
            //
            // Stack (Pascal): SP+0=num_ptr(4), SP+4=name_ptr(4). Pops 8.
            // Name lookup is ASCII case-insensitive and trims whitespace,
            // matching classic Font Manager behavior. Returns 0 when no
            // match — distinguishable from "Chicago" (which is also 0)
            // only by the caller also calling GetFontName(0) to verify.
            //
            // Regression coverage:
            //   trap::quickdraw::tests::getfnum_known_font_returns_family_id
            //   trap::quickdraw::tests::getfnum_unknown_font_returns_zero
            //   trap::quickdraw::tests::getfnum_name_match_is_case_insensitive
            //   trap::quickdraw::tests::getfnum_pops_eight_bytes
            // GetFNum ($A900): IM:I I-223; case-insensitive reverse lookup; 0 for unknown names
            (true, 0x100) => {
                let sp = cpu.read_reg(Register::A7);
                let num_ptr = bus.read_long(sp);
                let name_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                if num_ptr != 0 {
                    let mut requested_name: Option<String> = None;
                    let font_id = if name_ptr != 0 {
                        // Read Pascal Str255: length byte + chars.
                        let len = bus.read_byte(name_ptr) as usize;
                        let mut name_bytes = Vec::with_capacity(len);
                        for i in 0..len {
                            name_bytes.push(bus.read_byte(name_ptr + 1 + i as u32));
                        }
                        match std::str::from_utf8(&name_bytes) {
                            Ok(name) => {
                                requested_name = Some(name.to_string());
                                font_id_for_name(name)
                                    .or_else(|| self.loaded_font_id_for_name(name))
                                    .unwrap_or(0)
                            }
                            Err(_) => 0,
                        }
                    } else {
                        0
                    };
                    if trace_font_traps_enabled() {
                        eprintln!(
                            "[TRAP] GetFNum({:?}) -> {}",
                            requested_name.as_deref().unwrap_or("<null>"),
                            font_id
                        );
                    }
                    bus.write_word(num_ptr, font_id as u16);
                }
                Ok(())
            }

            // SetEmptyRgn ($A8DD)
            // Destroys the previous structure of the region, then sets it to the empty region (0,0,0,0).
            // PROCEDURE SetEmptyRgn(rgn: RgnHandle);
            // Inside Macintosh Volume I, I-183
            // Golden: a8dd_set_empty_rgn
            (true, 0x0DD) => {
                let sp = cpu.read_reg(Register::A7);
                let rgn_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if rgn_handle != 0 {
                    let rgn_ptr = bus.read_long(rgn_handle);
                    if rgn_ptr != 0 {
                        bus.write_word(rgn_ptr, 10);
                        bus.write_long(rgn_ptr + 2, 0);
                        bus.write_long(rgn_ptr + 6, 0);
                    }
                }
                Ok(())
            }

            // SetRectRgn ($A8DE)
            // Sets region to 10-byte rectangular structure with specified bounds.
            // PROCEDURE SetRectRgn(rgn: RgnHandle; left, top, right, bottom: INTEGER);
            // Inside Macintosh Volume I, I-183
            (true, 0x0DE) => {
                let sp = cpu.read_reg(Register::A7);
                let bottom = bus.read_word(sp) as i16;
                let right = bus.read_word(sp + 2) as i16;
                let top = bus.read_word(sp + 4) as i16;
                let left = bus.read_word(sp + 6) as i16;
                let rgn_handle = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);
                if rgn_handle != 0 {
                    let rgn_ptr = bus.read_long(rgn_handle);
                    if rgn_ptr != 0 {
                        bus.write_word(rgn_ptr, 10);
                        bus.write_word(rgn_ptr + 2, top as u16);
                        bus.write_word(rgn_ptr + 4, left as u16);
                        bus.write_word(rgn_ptr + 6, bottom as u16);
                        bus.write_word(rgn_ptr + 8, right as u16);
                    }
                }
                Ok(())
            }

            // OpenRgn ($A8DA)
            // PROCEDURE OpenRgn;
            // Imaging With QuickDraw 1994, 3-87
            // Begins recording line and framed-shape outlines into a region;
            // drawing is suppressed until CloseRgn organizes the region.
            // OpenRgn ($A8DA): Begins region recording; subsequent drawing primitives skip framebuffer writes
            (true, 0x0DA) => {
                self.recording_region = Some(super::dispatch::RegionRecording::default());
                Ok(())
            }

            // CloseRgn ($A8DB)
            // PROCEDURE CloseRgn(dstRgn: RgnHandle);
            // Imaging With QuickDraw 1994, 3-89
            // Stops collection of lines and framed shapes, organizes them
            // into a region definition, and saves it in dstRgn.
            // CloseRgn ($A8DB): Ends region recording and serializes recorded geometry into dstRgn
            (true, 0x0DB) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if let Some(recording) = self.recording_region.take() {
                    if dst_handle != 0 {
                        if let Some((rows_top, rows)) = Self::recorded_region_rows(&recording) {
                            Self::write_region_from_rows(bus, dst_handle, rows_top, &rows);
                        } else {
                            Self::write_region(bus, dst_handle, None, &[]);
                        }
                    }
                }
                Ok(())
            }

            // OffsetRgn ($A8E0)
            // Shifts region bounding box by (dh, dv).
            // PROCEDURE OffsetRgn(rgn: RgnHandle; dh, dv: INTEGER);
            // Inside Macintosh Volume I, I-183
            (true, 0x0E0) => {
                let sp = cpu.read_reg(Register::A7);
                let dv = bus.read_word(sp) as i16;
                let dh = bus.read_word(sp + 2) as i16;
                let rgn_handle = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                Self::offset_region(bus, rgn_handle, dh, dv);
                Ok(())
            }

            // InsetRgn ($A8E1)
            // PROCEDURE InsetRgn(rgn: RgnHandle; dh, dv: INTEGER);
            // InsetRgn ($A8E1): Insets bbox by (dh, dv) on all sides per IM:I I-184
            (true, 0x0E1) => {
                let sp = cpu.read_reg(Register::A7);
                let dv = bus.read_word(sp) as i16;
                let dh = bus.read_word(sp + 2) as i16;
                let rgn_handle = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                Self::inset_region(bus, rgn_handle, dh, dv);
                Ok(())
            }

            // EmptyRgn ($A8E2)
            // FUNCTION EmptyRgn(rgn: RgnHandle): BOOLEAN;
            // EmptyRgn ($A8E2): Returns TRUE if bbox has top>=bottom or left>=right
            (true, 0x0E2) => {
                let sp = cpu.read_reg(Register::A7);
                let rgn_handle = bus.read_long(sp);
                let is_empty = if rgn_handle != 0 {
                    let rgn_ptr = bus.read_long(rgn_handle);
                    if rgn_ptr != 0 {
                        let t = bus.read_word(rgn_ptr + 2) as i16;
                        let l = bus.read_word(rgn_ptr + 4) as i16;
                        let b = bus.read_word(rgn_ptr + 6) as i16;
                        let r = bus.read_word(rgn_ptr + 8) as i16;
                        b <= t || r <= l
                    } else {
                        true
                    }
                } else {
                    true
                };
                bus.write_word(sp + 4, if is_empty { 0x0100 } else { 0 });
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // EqualRgn ($A8E3)
            // FUNCTION EqualRgn(rgnA, rgnB: RgnHandle): BOOLEAN;
            // Inside Macintosh Volume I, I-183
            //
            // Returns TRUE iff the two regions have identical sizes,
            // shapes, and locations. A region's on-bus layout is:
            //     rgnSize(2) + rgnBBox(8) + [scanline data...]
            // The canonical implementation (ROM / Executor C_EqualRgn at
            // qRegion.cpp:1493) compares the full rgnSize bytes of *r1
            // vs *r2 with memcmp. Because rgnSize is the first word of
            // the region, any size mismatch surfaces in the first word
            // of the byte-compare and short-circuits to FALSE.
            //
            // REGRESSION: the previous implementation lived at the wrong
            // dispatch slot (0x0E5, which is really UnionRgn) and only
            // compared the 10-byte bbox header. That made it silently
            // report two complex regions as equal any time they shared
            // the same bounding box — the classic clip/update-region
            // staleness bug.
            //
            // Stack: SP+0=rgnB, SP+4=rgnA, result word at SP+8 (pop 8).
            // BOOL convention matches EmptyRgn/PtInRgn/RectInRgn:
            // 0x0100 for TRUE, 0x0000 for FALSE.
            //
            // Regression coverage:
            //   equalrgn_identical_rect_regions_equal
            //   equalrgn_different_bbox_unequal
            //   equalrgn_same_bbox_different_scanlines_unequal
            //   equalrgn_different_rgn_size_unequal
            //   equalrgn_nil_handles
            //   equalrgn_pops_eight_bytes
            // EqualRgn ($A8E3): Full rgnSize byte compare (Executor C_EqualRgn, IM:I I-183); was at wrong slot + bbox-only prior to 2026-04-12
            (true, 0x0E3) => {
                let sp = cpu.read_reg(Register::A7);
                let rgn_b = bus.read_long(sp);
                let rgn_a = bus.read_long(sp + 4);
                let equal = if rgn_a != 0 && rgn_b != 0 {
                    let ptr_a = bus.read_long(rgn_a);
                    let ptr_b = bus.read_long(rgn_b);
                    if ptr_a != 0 && ptr_b != 0 {
                        // memcmp-equivalent: compare rgnSize bytes
                        // starting at the master pointer. Because the
                        // first word is rgnSize, a size mismatch trips
                        // the comparison at byte 0-1.
                        let size_a = bus.read_word(ptr_a) as u32;
                        (0..size_a).all(|i| bus.read_byte(ptr_a + i) == bus.read_byte(ptr_b + i))
                    } else {
                        ptr_a == ptr_b
                    }
                } else {
                    rgn_a == rgn_b
                };
                bus.write_word(sp + 8, if equal { 0x0100 } else { 0 });
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // SectRgn ($A8E4)
            // Calculates the intersection of two regions.
            // PROCEDURE SectRgn(srcRgnA, srcRgnB, dstRgn: RgnHandle);
            // Inside Macintosh Volume I, I-184
            (true, 0x0E4) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_handle = bus.read_long(sp);
                let src_b = bus.read_long(sp + 4);
                let src_a = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);
                if src_a != 0 && src_b != 0 && dst_handle != 0 {
                    Self::write_region_boolean_op(
                        bus,
                        dst_handle,
                        src_a,
                        src_b,
                        RegionBooleanOp::Intersection,
                    );
                    if trace_region_ops_enabled() {
                        eprintln!(
                            "[REGION] tick={} SectRgn a=${:08X} bbox={:?} b=${:08X} bbox={:?} dst=${:08X} bbox={:?} size={}",
                            self.tick_count,
                            src_a,
                            Self::region_bbox(bus, src_a),
                            src_b,
                            Self::region_bbox(bus, src_b),
                            dst_handle,
                            Self::region_bbox(bus, dst_handle),
                            Self::region_ptr_and_size(bus, dst_handle).map(|(_, size)| size).unwrap_or(0)
                        );
                    }
                }
                Ok(())
            }

            // UnionRgn ($A8E5)
            // Calculates the union of two regions.
            // PROCEDURE UnionRgn(srcRgnA, srcRgnB, dstRgn: RgnHandle);
            // Inside Macintosh Volume I, I-184
            (true, 0x0E5) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_handle = bus.read_long(sp);
                let src_b = bus.read_long(sp + 4);
                let src_a = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);
                if src_a != 0 && src_b != 0 && dst_handle != 0 {
                    Self::write_region_boolean_op(
                        bus,
                        dst_handle,
                        src_a,
                        src_b,
                        RegionBooleanOp::Union,
                    );
                    if trace_region_ops_enabled() {
                        eprintln!(
                            "[REGION] tick={} UnionRgn a=${:08X} bbox={:?} b=${:08X} bbox={:?} dst=${:08X} bbox={:?} size={}",
                            self.tick_count,
                            src_a,
                            Self::region_bbox(bus, src_a),
                            src_b,
                            Self::region_bbox(bus, src_b),
                            dst_handle,
                            Self::region_bbox(bus, dst_handle),
                            Self::region_ptr_and_size(bus, dst_handle).map(|(_, size)| size).unwrap_or(0)
                        );
                    }
                }
                Ok(())
            }

            // DiffRgn ($A8E6)
            // Subtracts srcRgnB from srcRgnA and places the difference in dstRgn.
            // PROCEDURE DiffRgn(srcRgnA, srcRgnB, dstRgn: RgnHandle);
            // Inside Macintosh Volume I, I-184
            (true, 0x0E6) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_handle = bus.read_long(sp);
                let src_b = bus.read_long(sp + 4);
                let src_a = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);
                if src_a != 0 && dst_handle != 0 {
                    Self::write_region_boolean_op(
                        bus,
                        dst_handle,
                        src_a,
                        src_b,
                        RegionBooleanOp::Difference,
                    );
                    if trace_region_ops_enabled() {
                        eprintln!(
                            "[REGION] tick={} DiffRgn a=${:08X} bbox={:?} b=${:08X} bbox={:?} dst=${:08X} bbox={:?} size={}",
                            self.tick_count,
                            src_a,
                            Self::region_bbox(bus, src_a),
                            src_b,
                            Self::region_bbox(bus, src_b),
                            dst_handle,
                            Self::region_bbox(bus, dst_handle),
                            Self::region_ptr_and_size(bus, dst_handle).map(|(_, size)| size).unwrap_or(0)
                        );
                    }
                }
                Ok(())
            }

            // XorRgn ($A8E7)
            // Calculates the symmetric difference of two regions.
            // PROCEDURE XorRgn(srcRgnA, srcRgnB, dstRgn: RgnHandle);
            // Inside Macintosh Volume I, I-185
            (true, 0x0E7) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_handle = bus.read_long(sp);
                let src_b = bus.read_long(sp + 4);
                let src_a = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);
                if src_a != 0 && src_b != 0 && dst_handle != 0 {
                    Self::write_region_boolean_op(
                        bus,
                        dst_handle,
                        src_a,
                        src_b,
                        RegionBooleanOp::Xor,
                    );
                    if trace_region_ops_enabled() {
                        eprintln!(
                            "[REGION] tick={} XorRgn a=${:08X} bbox={:?} b=${:08X} bbox={:?} dst=${:08X} bbox={:?} size={}",
                            self.tick_count,
                            src_a,
                            Self::region_bbox(bus, src_a),
                            src_b,
                            Self::region_bbox(bus, src_b),
                            dst_handle,
                            Self::region_bbox(bus, dst_handle),
                            Self::region_ptr_and_size(bus, dst_handle).map(|(_, size)| size).unwrap_or(0)
                        );
                    }
                }
                Ok(())
            }

            // PtInRgn ($A8E8)
            // FUNCTION PtInRgn(pt: Point; rgn: RgnHandle): BOOLEAN;
            // PtInRgn ($A8E8): Tests pt inside region via membership cache (differential x-span decode); rectangular short-circuit per IM:I I-185
            (true, 0x0E8) => {
                let sp = cpu.read_reg(Register::A7);
                let rgn_handle = bus.read_long(sp);
                let pt_v = bus.read_word(sp + 4) as i16;
                let pt_h = bus.read_word(sp + 6) as i16;
                let in_rgn = Self::region_contains_point(bus, rgn_handle, pt_v, pt_h);
                if trace_region_ops_enabled() {
                    eprintln!(
                        "[REGION] tick={} PtInRgn pt=({}, {}) rgn=${:08X} bbox={:?} size={} -> {}",
                        self.tick_count,
                        pt_v,
                        pt_h,
                        rgn_handle,
                        Self::region_bbox(bus, rgn_handle),
                        Self::region_ptr_and_size(bus, rgn_handle)
                            .map(|(_, size)| size)
                            .unwrap_or(0),
                        in_rgn
                    );
                }
                bus.write_word(sp + 8, if in_rgn { 0x0100 } else { 0 });
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // RectInRgn ($A8E9)
            // FUNCTION RectInRgn(r: Rect; rgn: RgnHandle): BOOLEAN;
            // "Returns TRUE if any pixel enclosed by r lies within rgn."
            // Inside Macintosh Volume I, I-185
            //
            // Stack (Pascal left-to-right push → deepest first):
            //   SP+0  rgn     RgnHandle (4 bytes, shallowest — pushed last)
            //   SP+4  r       Ptr to Rect (4 bytes)
            //   SP+8  result  BOOLEAN slot (2 bytes — caller pre-pushed)
            // Pop 8, write result at SP+8 — SP advances to SP+8.
            //
            // For rectangular regions the bounding-box intersection suffices.
            // For complex regions we build a membership cache and check per-row
            // span overlap so that donut-shaped regions don't falsely return TRUE
            // for points inside the hole.  IM:I I-142 documents the region format.
            //
            // RectInRgn ($A8E9): Rect/region intersection; complex regions use membership cache per IM:I I-185
            (true, 0x0E9) => {
                let sp = cpu.read_reg(Register::A7);
                let rgn_handle = bus.read_long(sp);
                let rect_ptr = bus.read_long(sp + 4);
                let in_rgn = Self::rect_in_rgn(bus, rgn_handle, rect_ptr);
                if trace_region_ops_enabled() {
                    eprintln!(
                        "[REGION] tick={} RectInRgn rect=({},{}..{},{}) rgn=${:08X} bbox={:?} size={} -> {}",
                        self.tick_count,
                        bus.read_word(rect_ptr) as i16,
                        bus.read_word(rect_ptr + 2) as i16,
                        bus.read_word(rect_ptr + 4) as i16,
                        bus.read_word(rect_ptr + 6) as i16,
                        rgn_handle,
                        Self::region_bbox(bus, rgn_handle),
                        Self::region_ptr_and_size(bus, rgn_handle).map(|(_, size)| size).unwrap_or(0),
                        in_rgn
                    );
                }
                bus.write_word(sp + 8, if in_rgn { 0x0100 } else { 0 });
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // ========== Std* bottleneck procs (intentional no-ops) ==========
            //
            // QuickDraw routes every shape-drawing call through a small set of
            // "low-level routines" — the bottleneck procs — exposed via the
            // grafProcs (B&W: QDProcs) or grafProcs (color: CQDProcs) field of
            // the current port. Apps customize drawing by replacing one or
            // more proc pointers; the standard procs are what runs when the
            // proc slot is NIL OR explicitly equals the standard routine.
            //
            // Per IM:I I-196..I-198 + Imaging With QuickDraw 1994 3-130..3-139
            // each Std* trap is the documented entry point for the
            // corresponding bottleneck. On a real Mac the high-level traps
            // (FrameRect, PaintRect, EraseRect, FillRect, InvertRect, etc.)
            // dispatch THROUGH grafProcs.rectProc — which by default is
            // StdRect; an app installing a custom rectProc has its routine
            // invoked instead, and the custom routine MAY chain to StdRect
            // for the actual drawing.
            //
            // Systemless's HLE compromise: every high-level QD trap (FrameRect
            // $A8A1, PaintRect $A8A2, EraseRect $A8A3, etc.) draws DIRECTLY
            // via Rust drawing code — it does NOT consult grafProcs.rectProc
            // before dispatching, and it does NOT JSR to the slot. The
            // standard bottleneck traps are therefore reachable only by:
            //   (a) An app that explicitly JSRs through grafProcs.<slot>
            //       (e.g. via SetStdProcs to capture defaults, then calls
            //       the captured ProcPtr later), OR
            //   (b) An app that installs a custom proc, does setup work,
            //       and then chains to the standard one with JSR through
            //       its saved-default-proc pointer.
            // In (a) the no-op is safe because no actual drawing was
            // intended — the call is a probe/no-op anyway. In (b) the
            // custom proc has already done whatever drawing it needed
            // (typically: route through Systemless's high-level FrameRect
            // etc. via direct trap dispatch), and the chained Std* call
            // is just for "make sure the default ALSO got to run." The
            // no-op is correct because Systemless's high-level paths have
            // already drawn — the bottleneck would be a duplicate.
            //
            // No corpus title relies on the standard bottleneck procs
            // executing real drawing — verified by tracing the Pack6 +
            // Marathon + Glider PRO + Centaurian games' trap histograms
            // (none of them invoke any Std* trap except $A8EB StdBits,
            // which IS implemented as a proper CopyBits delegation at
            // line 7621 below).
            //
            // The Pascal stack convention for these traps follows the
            // MPW / Toolbox standard:
            //   - GrafVerb (an enum) → 1 byte at source level, 2 bytes
            //     on stack (M68k 16-bit stack alignment)
            //   - INTEGER → 2 bytes
            //   - Point (4 bytes) → by VALUE, 4 bytes
            //   - Rect / BitMap / FontInfo (VAR or by-value Rect-larger-
            //     than-pointer types) → by REFERENCE, 4-byte pointer
            //   - PolyHandle / RgnHandle / Handle / Ptr → 4 bytes
            //
            // Selectors-vs-trap-words: each Std* bottleneck has its own
            // trap word (no DispatchManager packaging); selector dispatch
            // applies only to higher-level dispatchers like _PaletteDispatch
            // or _SCSIDispatch, not to the bottleneck family.

            // StdLine ($A890)
            // PROCEDURE StdLine(newPt: Point);
            // Inside Macintosh Volume I (1985), I-197 + Imaging With QuickDraw 1994 p. 3-132:
            //   "The StdLine procedure draws a line from the current pen
            //    location to newPt in local coordinates and updates the
            //    pen location to newPt. The pen state (size, pattern,
            //    mode) determines the line's appearance."
            //
            // MPW Universal Headers Quickdraw.h declares the C-callable
            // form with Point passed by value (4-byte record):
            //   EXTERN_API(void) StdLine(Point newPt) ONEWORDINLINE(0xA890);
            //
            // Stack frame at trap entry (Pascal LR push; Point is a
            // 4-byte record {v: INTEGER; h: INTEGER} pushed in declaration
            // order — v first, h second — so newPt.h ends up at the lowest
            // address):
            //   sp+0..1  newPt.v   (Point.v, shallowest)
            //   sp+2..3  newPt.h   (Point.h, deepest)
            // 4-byte total pop, no result slot (PROCEDURE).
            //
            // BII-vs-Systemless divergence:
            //   - BII System 7.5 ROM rasterises a line from the current
            //     pen location to newPt AND mutates A0/A1/D0/D1 as part
            //     of the bottleneck-proc bookkeeping.
            //   - Systemless HLE is a stub-noop: high-level Line ($A892) and
            //     LineTo ($A891) draw directly via the Rust framebuffer
            //     pen-state path (see quickdraw.rs draw_line), so this
            //     bottleneck is reachable only via SetStdProcs →
            //     JSR-through-slot (a non-corpus pattern).  The HLE
            //     leaves A0/A1/D0/D1 unchanged and pops only the 4 bytes.
            //
            // Engines-agree subset: stack discipline (4-byte pop) — pinned
            // by the strict bake a890_stdline_strict and the in-Rust
            // tests stdline_consumes_point_argument_by_value +
            // stdline_five_call_composition_pops_twenty_bytes_total.
            //
            // Systemless-only contract: register preservation A0/A1/D0/D1 —
            // pinned by stdline_stub_preserves_non_stack_registers_in_hle_compromise_path.
            (true, 0x090) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // StdRect ($A8A0)
            // PROCEDURE StdRect(verb: GrafVerb; r: Rect);
            // Inside Macintosh Volume I, I-197 + Imaging With QuickDraw 1994 3-132:
            //   "The StdRect procedure draws the rectangle specified in the r
            //    parameter according to the action specified in the verb parameter."
            //
            // MPW Universal Headers Quickdraw.h declares the C-callable form
            // with the Rect passed by pointer (not inlined by value):
            //   EXTERN_API(void) StdRect(GrafVerb verb, const Rect *r)
            //                                          ONEWORDINLINE(0xA8A0);
            //
            // Stack frame at trap entry (Pascal LR push, first arg deepest):
            //   sp+0..3  rect_ptr   (last pushed → shallowest)
            //   sp+4..5  verb_word  (first pushed → deepest)
            // 6-byte total pop, no result slot (PROCEDURE).
            //
            // Real-Mac semantic: dispatches Frame/Paint/Erase/Invert/Fill verb
            // on the rect via the current pen state / fill pattern.
            //
            // Systemless HLE compromise — every high-level QuickDraw rect trap
            // (FrameRect / PaintRect / EraseRect / InvertRect / FillRect)
            // draws directly via the Rust framebuffer code, bypassing the
            // bottleneck dispatch through grafProcs.rectProc.  StdRect
            // itself is reachable only via explicit SetStdProcs → JSR-
            // through-slot — a non-corpus pattern.  Therefore the HLE pops
            // the 6-byte stack frame and returns without performing any
            // drawing.  caller_observable = false because no corpus caller
            // depends on StdRect producing pixels.
            //
            // BasiliskII divergence: BII's System 7.5 ROM faithfully draws
            // the rect AND mutates A0/A1/D0/D1 during rasterisation.  Both
            // engines agree on the 6-byte pop; they diverge on pixel side-
            // effect and register mutation.  Witnessed by the strict bake
            // `a8a0_a8b6_stdrect_stdoval_strict` via empty-clipRgn + StackSpace
            // sandwich, which clips BII's drawing to nothing and asserts A7
            // advances by exactly 6 bytes per call (single + 5-verb-composition).
            // The Systemless-only register-preservation contract is pinned by
            // `stdrect_stub_preserves_non_stack_registers_in_hle_compromise_path`.
            (true, 0x0A0) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 6);
                Ok(())
            }

            // StdRRect ($A8AF)
            // PROCEDURE StdRRect(verb: GrafVerb; r: Rect; ovalWidth, ovalHeight: INTEGER);
            // Inside Macintosh Volume I, I-197 + Imaging With QuickDraw 1994 3-133:
            //   "The StdRRect procedure draws the specified rounded-corner
            //    rectangle, applying the indicated verb.  The ovalWidth and
            //    ovalHeight parameters specify the diameters of curvature
            //    for the corners."
            //
            // MPW Universal Headers Quickdraw.h declares the C-callable form
            // with the Rect passed by pointer plus two short INTEGER args:
            //   EXTERN_API(void) StdRRect(GrafVerb verb, const Rect *r,
            //                             short ovalWidth, short ovalHeight)
            //                                          ONEWORDINLINE(0xA8AF);
            //
            // Stack frame at trap entry (Pascal LR push, first arg deepest):
            //   sp+0..1  ovalHeight (last pushed → shallowest)
            //   sp+2..3  ovalWidth
            //   sp+4..7  rect_ptr
            //   sp+8..9  verb_word  (first pushed → deepest)
            // 10-byte total pop, no result slot (PROCEDURE).
            //
            // Real-Mac semantic: dispatches Frame/Paint/Erase/Invert/Fill verb
            // on the rounded-corner rectangle inscribed in r with corner
            // diameters (ovalWidth, ovalHeight) via the current pen state /
            // fill pattern.
            //
            // Systemless HLE compromise — every high-level QuickDraw round-rect
            // trap (FrameRoundRect / PaintRoundRect / EraseRoundRect /
            // InvertRoundRect / FillRoundRect) draws directly via the Rust
            // framebuffer code, bypassing the bottleneck dispatch through
            // grafProcs.rRectProc.  StdRRect itself is reachable only via
            // explicit SetStdProcs → JSR-through-slot — a non-corpus pattern.
            // Therefore the HLE pops the 10-byte stack frame and returns
            // without performing any drawing.  caller_observable = false
            // because no corpus caller depends on StdRRect producing pixels.
            //
            // BasiliskII divergence: BII's System 7.5 ROM faithfully draws
            // the rounded rect AND mutates A0/A1/D0/D1 during rasterisation.
            // Both engines agree on the 10-byte pop; they diverge on pixel
            // side-effect and register mutation.  Witnessed by the strict
            // bake `a8af_a8bd_stdrrect_stdarc_strict` via empty-clipRgn +
            // StackSpace sandwich, which clips BII's drawing to nothing and
            // asserts A7 advances by exactly 10 bytes per call (single +
            // 5-verb-composition).  The Systemless-only register-preservation
            // contract is pinned by
            // `stdrrect_stub_preserves_non_stack_registers_in_hle_compromise_path`.
            (true, 0x0AF) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 10);
                Ok(())
            }

            // StdOval ($A8B6)
            // PROCEDURE StdOval(verb: GrafVerb; r: Rect);
            // Inside Macintosh Volume I, I-197 + Imaging With QuickDraw 1994 3-133.
            //
            // MPW Universal Headers Quickdraw.h declares the C-callable form
            // with the Rect passed by pointer:
            //   EXTERN_API(void) StdOval(GrafVerb verb, const Rect *r)
            //                                          ONEWORDINLINE(0xA8B6);
            //
            // Same stack-frame layout as StdRect ($A8A0):
            //   sp+0..3  rect_ptr   (last pushed → shallowest)
            //   sp+4..5  verb_word  (first pushed → deepest)
            // 6-byte total pop, no result slot.
            //
            // Real-Mac semantic: draws oval inscribed in r per verb.
            //
            // Systemless HLE compromise — high-level FrameOval ($A8B7) /
            // PaintOval / EraseOval / InvertOval / FillOval draw directly via
            // Rust circle/ellipse code; StdOval itself is reachable only via
            // SetStdProcs → JSR-through-slot.  caller_observable = false.
            //
            // BasiliskII divergence: BII's real ROM rasterises the oval AND
            // mutates A0/A1/D0/D1.  Engines-agree on the 6-byte pop only.
            // Witnessed by `a8a0_a8b6_stdrect_stdoval_strict` (sibling-trap
            // combined fixture).  The Systemless-only register-preservation
            // contract is pinned by `stdoval_stub_preserves_non_stack_registers_in_hle_compromise_path`.
            (true, 0x0B6) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 6);
                Ok(())
            }

            // StdArc ($A8BD)
            // PROCEDURE StdArc(verb: GrafVerb; r: Rect; startAngle, arcAngle: INTEGER);
            // Inside Macintosh Volume I, I-197 + Imaging With QuickDraw 1994 3-134:
            //   "The StdArc procedure draws an arc or wedge of the oval that
            //    fits inside the rectangle r.  The startAngle parameter
            //    specifies where the arc begins and the arcAngle parameter
            //    specifies the angular extent of the arc.  Both angles are
            //    measured clockwise from 12 o'clock."
            //
            // MPW Universal Headers Quickdraw.h declares the C-callable form
            // with the Rect passed by pointer plus two short INTEGER args:
            //   EXTERN_API(void) StdArc(GrafVerb verb, const Rect *r,
            //                           short startAngle, short arcAngle)
            //                                          ONEWORDINLINE(0xA8BD);
            //
            // Stack frame at trap entry (Pascal LR push, first arg deepest):
            //   sp+0..1  arcAngle   (last pushed → shallowest)
            //   sp+2..3  startAngle
            //   sp+4..7  rect_ptr
            //   sp+8..9  verb_word  (first pushed → deepest)
            // 10-byte total pop, no result slot (PROCEDURE).
            //
            // Real-Mac semantic: dispatches Frame verb to draw an arc outline
            // along the oval boundary; Paint/Erase/Invert/Fill verbs draw a
            // closed wedge (pie slice) using the current pen state / fill
            // pattern.
            //
            // Systemless HLE compromise — every high-level QuickDraw arc trap
            // (FrameArc / PaintArc / EraseArc / InvertArc / FillArc) draws
            // directly via the Rust arc/wedge rasteriser, bypassing the
            // bottleneck dispatch through grafProcs.arcProc.  StdArc itself
            // is reachable only via explicit SetStdProcs → JSR-through-slot
            // — a non-corpus pattern.  Therefore the HLE pops the 10-byte
            // stack frame and returns without performing any drawing.
            // caller_observable = false because no corpus caller depends on
            // StdArc producing pixels.
            //
            // BasiliskII divergence: BII's System 7.5 ROM faithfully draws
            // the arc/wedge AND mutates A0/A1/D0/D1 during rasterisation.
            // Both engines agree on the 10-byte pop; they diverge on pixel
            // side-effect and register mutation.  Witnessed by the strict
            // bake `a8af_a8bd_stdrrect_stdarc_strict` via empty-clipRgn +
            // StackSpace sandwich, which clips BII's drawing to nothing and
            // asserts A7 advances by exactly 10 bytes per call (single +
            // 5-verb-composition with varying startAngle/arcAngle pairs).
            // The Systemless-only register-preservation contract is pinned by
            // `stdarc_stub_preserves_non_stack_registers_in_hle_compromise_path`.
            (true, 0x0BD) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 10);
                Ok(())
            }

            // StdPoly ($A8C5)
            // PROCEDURE StdPoly(verb: GrafVerb; poly: PolyHandle);
            // Inside Macintosh Volume I, I-198 + Imaging With QuickDraw 1994 3-135:
            //   "The StdPoly procedure draws the polygon specified in the poly
            //    parameter according to the action specified in the verb parameter."
            //
            // MPW Universal Headers Quickdraw.h declares the C-callable form
            // with the PolyHandle (4-byte handle):
            //   EXTERN_API(void) StdPoly(GrafVerb verb, PolyHandle poly)
            //                                          ONEWORDINLINE(0xA8C5);
            //
            // Stack frame at trap entry (Pascal LR push, first arg deepest):
            //   sp+0..3  poly_handle  (last pushed → shallowest)
            //   sp+4..5  verb_word    (first pushed → deepest)
            // 6-byte total pop, no result slot (PROCEDURE).
            //
            // Real-Mac semantic: dispatches Frame/Paint/Erase/Invert/Fill verb
            // on the polygon's recorded vertex list via the current pen state /
            // fill pattern.
            //
            // Systemless HLE compromise — every high-level QuickDraw polygon trap
            // (FramePoly / PaintPoly / ErasePoly / InvertPoly / FillPoly) draws
            // directly via the Rust polygon scanline code, bypassing the
            // bottleneck dispatch through grafProcs.polyProc.  StdPoly itself
            // is reachable only via explicit SetStdProcs → JSR-through-slot —
            // a non-corpus pattern.  Therefore the HLE pops the 6-byte stack
            // frame and returns without performing any drawing.
            // caller_observable = false because no corpus caller depends on
            // StdPoly producing pixels.
            //
            // BasiliskII divergence: BII's System 7.5 ROM faithfully draws the
            // polygon AND mutates A0/A1/D0/D1 during rasterisation.  Both
            // engines agree on the 6-byte pop; they diverge on pixel side-
            // effect and register mutation.  Witnessed by the strict bake
            // `a8c5_a8d1_stdpoly_stdrgn_strict` via empty-clipRgn + StackSpace
            // sandwich, which clips BII's drawing to nothing and asserts A7
            // advances by exactly 6 bytes per call (single + 5-verb-composition).
            // The Systemless-only register-preservation contract is pinned by
            // `stdpoly_stub_preserves_non_stack_registers_in_hle_compromise_path`.
            (true, 0x0C5) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 6);
                Ok(())
            }

            // StdRgn ($A8D1)
            // PROCEDURE StdRgn(verb: GrafVerb; rgn: RgnHandle);
            // Inside Macintosh Volume I, I-198 + Imaging With QuickDraw 1994 3-135:
            //   "The StdRgn procedure draws the region specified in the rgn
            //    parameter according to the action specified in the verb parameter."
            //
            // MPW Universal Headers Quickdraw.h declares the C-callable form
            // with the RgnHandle (4-byte handle):
            //   EXTERN_API(void) StdRgn(GrafVerb verb, RgnHandle rgn)
            //                                          ONEWORDINLINE(0xA8D1);
            //
            // Stack frame at trap entry (Pascal LR push, first arg deepest):
            //   sp+0..3  rgn_handle   (last pushed → shallowest)
            //   sp+4..5  verb_word    (first pushed → deepest)
            // 6-byte total pop, no result slot (PROCEDURE).
            //
            // Real-Mac semantic: dispatches Frame/Paint/Erase/Invert/Fill verb
            // on the region's scanline data via the current pen state / fill
            // pattern.
            //
            // Systemless HLE compromise — every high-level QuickDraw region trap
            // (FrameRgn / PaintRgn / EraseRgn / InvertRgn / FillRgn) draws
            // directly via the Rust region rasteriser, bypassing the
            // bottleneck dispatch through grafProcs.rgnProc.  StdRgn itself
            // is reachable only via explicit SetStdProcs → JSR-through-slot —
            // a non-corpus pattern.  Therefore the HLE pops the 6-byte stack
            // frame and returns without performing any drawing.
            // caller_observable = false because no corpus caller depends on
            // StdRgn producing pixels.
            //
            // BasiliskII divergence: BII's System 7.5 ROM faithfully draws the
            // region AND mutates A0/A1/D0/D1 during rasterisation.  Both
            // engines agree on the 6-byte pop; they diverge on pixel side-
            // effect and register mutation.  Witnessed by the strict bake
            // `a8c5_a8d1_stdpoly_stdrgn_strict` via empty-clipRgn + StackSpace
            // sandwich, which clips BII's drawing to nothing and asserts A7
            // advances by exactly 6 bytes per call (single + 5-verb-composition).
            // The Systemless-only register-preservation contract is pinned by
            // `stdrgn_stub_preserves_non_stack_registers_in_hle_compromise_path`.
            (true, 0x0D1) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 6);
                Ok(())
            }

            // StdBits ($A8EB)
            // PROCEDURE StdBits(srcBits: BitMap; srcRect, dstRect: Rect; mode: INTEGER; maskRgn: RgnHandle);
            // StdBits ($A8EB): Standard CopyBits bottleneck — defers to copy_bits_common targeting current port's bitmap per IM:I I-176
            (true, 0x0EB) => {
                let sp = cpu.read_reg(Register::A7);
                let mask_rgn = bus.read_long(sp);
                let mode = bus.read_word(sp + 4) as i16;
                let dst_rect_ptr = bus.read_long(sp + 6);
                let src_rect_ptr = bus.read_long(sp + 10);
                let src_bits_ptr = bus.read_long(sp + 14);
                cpu.write_reg(Register::A7, sp + 18);
                let dst_bits_ptr = self.bitmap_ptr_for_port(bus, self.current_port);
                if dst_bits_ptr == 0 {
                    return Some(Ok(()));
                }
                if let Err(err) = self.copy_bits_common(
                    cpu,
                    bus,
                    src_bits_ptr,
                    dst_bits_ptr,
                    src_rect_ptr,
                    dst_rect_ptr,
                    mode,
                    mask_rgn,
                ) {
                    return Some(Err(err));
                }
                Ok(())
            }

            // StdPutPic ($A8F0)
            // PROCEDURE StdPutPic(dataPtr: Ptr; byteCount: INTEGER);
            // Inside Macintosh Volume I, p. I-200; Imaging With QuickDraw
            // (1994), pp. 3-138 to 3-139.
            // Pascal frame: SP+0..3 = dataPtr (Ptr), SP+4..5 = byteCount, pop 6.
            // Real-Mac semantic: emits picture-recording bytes into the
            // currently-open picture (between OpenPicture and ClosePicture).
            // HLE no-op: Systemless's PICT recording (OpenPicture $A8F3,
            // ClosePicture $A8F4) is implemented end-to-end without going
            // through the bottleneck. Direct trap calls pop the documented
            // 6-byte frame and return noErr in D0 for assembly callers.
            (true, 0x0F0) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 6);
                cpu.write_reg(Register::D0, 0);
                Ok(())
            }

            // StdComment ($A8F1)
            // PROCEDURE StdComment(kind, dataSize: INTEGER; dataHandle: Handle);
            // Inside Macintosh Volume I, I-198 + Imaging With QuickDraw 1994 3-137
            // Pascal frame: SP+0..1 = kind, SP+2..3 = dataSize,
            //               SP+4..7 = dataHandle, pop 8.
            // Real-Mac semantic: processes a PICT comment opcode emitted
            // during DrawPicture playback; default behaviour ignores the
            // comment per IM:I I-198 ("StdComment simply ignores the
            // comment"). PicComment ($A8F2) is the ENCODER counterpart that
            // emits a comment INTO an open picture; StdComment is the
            // DECODER hook called during playback.
            // HLE no-op: matches IM-canonical "ignore" semantic exactly.
            // Systemless's DrawPicture ignores all comment opcodes regardless
            // of whether they reach this bottleneck.
            // StdComment ($A8F1): Pops 8 bytes (kind INTEGER + dataSize INTEGER + dataHandle Handle); IM-canonical "ignore comment" per IM:I I-198 + Imaging With QD 3-137
            (true, 0x0F1) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // StdGetPic ($A8EE)
            // PROCEDURE StdGetPic(dataPtr: Ptr; byteCount: INTEGER);
            // Inside Macintosh Volume I, p. I-200; Imaging With QuickDraw
            // (1994), pp. 3-138 to 3-139.
            // Pascal frame: SP+0..3 = dataPtr (Ptr), SP+4..5 = byteCount, pop 6.
            // Real-Mac semantic: reads picture-playback bytes during
            // DrawPicture — the inverse of StdPutPic. Systemless's DrawPicture
            // parses resident PICT bytes directly and has a data-fork
            // spooling fallback for the common "10-byte header was just
            // read from file" path. Direct StdGetPic trap calls remain a
            // no-op that pops the documented frame and returns noErr in D0.
            (true, 0x0EE) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 6);
                cpu.write_reg(Register::D0, 0);
                Ok(())
            }

            // StdTxMeas ($A8ED)
            // FUNCTION StdTxMeas(byteCount: INTEGER; textBuf: Ptr;
            //                    VAR numer, denom: Point;
            //                    VAR info: FontInfo): INTEGER;
            // Inside Macintosh Volume I, I-199; Inside Macintosh: Text
            // 1993, 3-99..3-100.
            //
            // Public MPW C declaration (Text 1993):
            //   pascal short StdTxMeas(short byteCount, const void *textAddr,
            //                          Point *numer, Point *denom,
            //                          FontInfo *info);
            //
            // Pascal frame: SP+0..3 = info FontInfo ptr,
            //               SP+4..7 = denom Point ptr,
            //               SP+8..11 = numer Point ptr,
            //               SP+12..15 = textBuf,
            //               SP+16..17 = byteCount, pop 18 + 2-byte
            //               result slot at SP+18 (caller pre-pushed before args).
            //
            // Real-Mac semantic: returns the width of scaled or unscaled
            // text and writes current-font metrics to `info`; `numer`/`denom`
            // may be adjusted on output when the Font Manager cannot fully
            // satisfy the scaling request.
            //
            // Systemless partial HLE:
            //   * width is measured using the same glyph advances as the
            //     high-level TextWidth/StringWidth/CharWidth path
            //   * explicit horizontal numer/denom scaling is applied to the
            //     returned width
            //   * `info` mirrors GetFontInfo for the current port font/size
            //   * `numer`/`denom` are left as supplied, so callers using the
            //     canonical identity-scaling path (1,1 / 1,1) or scaling
            //     `info` themselves still get coherent results
            //
            // The broader output-scaling reconciliation path remains partial,
            // and Systemless's direct StdText bottleneck path still doesn't draw
            // explicitly scaled text. The common-case measurement behavior is
            // nonetheless much more useful than the prior hard-coded zero
            // stub.
            (true, 0x0ED) => {
                let sp = cpu.read_reg(Register::A7);
                let info_ptr = bus.read_long(sp);
                let denom_ptr = bus.read_long(sp + 4);
                let numer_ptr = bus.read_long(sp + 8);
                let text_buf = bus.read_long(sp + 12);
                let byte_count = bus.read_word(sp + 16) as i16;
                let (face, scale) = get_font_face_scaled(self.tx_font, self.tx_size);
                let mut total_width = 0i32;

                for i in 0..byte_count {
                    let ch = bus.read_byte(text_buf + i as u32) as char;
                    let advance =
                        if let Some((glyph, _)) = get_glyph(self.tx_font, self.tx_size, ch) {
                            i32::from(self.glyph_advance(glyph) * scale)
                        } else {
                            i32::from(self.missing_glyph_advance() * scale)
                        };
                    total_width += advance;
                }

                let numer_h = if numer_ptr != 0 {
                    bus.read_word(numer_ptr + 2) as i16
                } else {
                    1
                };
                let denom_h = if denom_ptr != 0 {
                    bus.read_word(denom_ptr + 2) as i16
                } else {
                    1
                };
                let scaled_width = if numer_h > 0 && denom_h > 0 {
                    let numer_i32 = i32::from(numer_h);
                    let denom_i32 = i32::from(denom_h);
                    ((total_width * numer_i32) + (denom_i32 / 2)) / denom_i32
                } else {
                    total_width
                }
                .clamp(i32::from(i16::MIN), i32::from(i16::MAX))
                    as i16;

                if info_ptr != 0 {
                    let metrics = face.metrics;
                    bus.write_word(info_ptr, (metrics.ascent * scale) as u16);
                    bus.write_word(info_ptr + 2, (metrics.descent * scale) as u16);
                    bus.write_word(info_ptr + 4, (metrics.wid_max * scale) as u16);
                    bus.write_word(info_ptr + 6, (metrics.leading * scale) as u16);
                }

                // info(4)+denom(4)+numer(4)+textBuf(4)+byteCount(2) = 18 + result(2)
                bus.write_word(sp + 18, scaled_width as u16);
                cpu.write_reg(Register::A7, sp + 18);
                Ok(())
            }

            // SetStdCProcs ($AA4E)
            // PROCEDURE SetStdCProcs(VAR cProcs: CQDProcs);
            // Inside Macintosh Volume V, V-77 + V-91 (CQDProcs record layout)
            // + Imaging With QuickDraw 1994 7-83
            // Pascal frame: SP+0..3 = cProcs ptr (VAR CQDProcs), pop 4.
            // Real-Mac semantic: fills textProc..putPicProc with the 13
            // standard QuickDraw bottlenecks, writes opcodeProc with the
            // default undefined-opcode handler (StdOpcodeProc), writes
            // newProc1 with the standard StdPix handler, and leaves the
            // remaining reserved newProc2..newProc6 fields NIL. IM:V V-77
            // says SetStdCProcs returns a CQDProcs record whose fields point
            // to the standard low-level routines; IM:V V-91 / Imaging With
            // QuickDraw 4-60..4-61 define opcodeProc plus 6 extension slots
            // beyond QDProcs; IWQD 7-82 defines StdOpcodeProc; QuickTime
            // (1993) 3-137..3-138 identifies newProc1 as the StdPix entry
            // obtained via SetStdCProcs. A BasiliskII 7.5.3 ROM probe
            // matches that shape: 15 non-NIL routine pointers followed by
            // 5 NIL reserved slots.
            //
            // Systemless mirrors SetStdProcs' fake-ptr strategy for the 15
            // concrete routines: synthesize stable $00F0AXXX markers that
            // match GetTrapAddress(_StdXxx) comparisons where a trap exists,
            // plus a stable non-NIL synthetic marker for StdPix (which is
            // exposed only through SetStdCProcs rather than an A-line trap),
            // then zero the 5 remaining reserved slots. This keeps caller-
            // visible default-proc identity for code that snapshots CQDProcs,
            // patches one slot, then later compares against the saved
            // defaults.
            // SetStdCProcs ($AA4E): Writes synthesized ProcPtr markers for textProc..putPicProc + opcodeProc (StdOpcodeProc $ABF8) + newProc1 (StdPix), then zeros reserved newProc2..newProc6 per IM:V V-77/V-91 + Imaging With QuickDraw 7-82..7-83 + QuickTime 1993 3-137..3-138
            (true, 0x24E) => {
                let sp = cpu.read_reg(Register::A7);
                let procs_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if procs_ptr != 0 {
                    // CQDProcs field order per IM:V V-91 / IWQD 4-60:
                    //   13 inherited QDProcs slots + opcodeProc + newProc1
                    //   (StdPix) + 5 remaining reserved newProc slots.
                    //   Write the 15 concrete defaults as
                    //   synthesized trap markers, then leave the reserved
                    //   extension slots NIL.
                    const STD_PIX_FAKE_PROC: u32 = 0x00F05058;
                    const STD_COLOR_PROC_TRAPS: [u32; 14] = [
                        0xA882, // textProc (StdText)
                        0xA890, // lineProc (StdLine)
                        0xA8A0, // rectProc (StdRect)
                        0xA8AF, // rRectProc (StdRRect)
                        0xA8B6, // ovalProc (StdOval)
                        0xA8BD, // arcProc (StdArc)
                        0xA8C5, // polyProc (StdPoly)
                        0xA8D1, // rgnProc (StdRgn)
                        0xA8EB, // bitsProc (StdBits)
                        0xA8F1, // commentProc (StdComment)
                        0xA8ED, // txMeasProc (StdTxMeas)
                        0xA8EE, // getPicProc (StdGetPic)
                        0xA8F0, // putPicProc (StdPutPic)
                        0xABF8, // opcodeProc (StdOpcodeProc)
                    ];
                    for (i, trap) in STD_COLOR_PROC_TRAPS.iter().enumerate() {
                        bus.write_long(procs_ptr + (i as u32) * 4, 0x00F00000 | trap);
                    }
                    bus.write_long(procs_ptr + 14 * 4, STD_PIX_FAKE_PROC); // newProc1 (StdPix)
                    for i in 15..20u32 {
                        bus.write_long(procs_ptr + i * 4, 0);
                    }
                }
                Ok(())
            }

            // ========== Polygon ==========

            // OpenPoly ($A8CB)
            // FUNCTION OpenPoly: PolyHandle;
            // Inside Macintosh Volume I, I-189
            // OpenPoly ($A8CB): Begins recording polygon vertices
            (true, 0x0CB) => {
                let sp = cpu.read_reg(Register::A7);
                // Allocate a placeholder PolyRec (will be filled by ClosePoly).
                let poly = bus.alloc(14);
                bus.write_word(poly, 14); // polySize (header only, updated later)
                let handle = bus.alloc(4);
                bus.write_long(handle, poly);
                bus.write_long(sp, handle);
                self.recording_polygon = Some(super::dispatch::PolygonRecording {
                    handle,
                    vertices: Vec::new(),
                });
                Ok(())
            }

            // ClosePoly ($A8CC)
            // PROCEDURE ClosePoly;
            // Inside Macintosh Volume I, I-189
            // ClosePoly ($A8CC): Finishes polygon recording
            (true, 0x0CC) => {
                if let Some(rec) = self.recording_polygon.take() {
                    let handle = rec.handle;
                    let verts = &rec.vertices;
                    // PolyRec: polySize(2) + polyBBox(8) + polyPoints(4*N)
                    let data_size = 10 + verts.len() as u32 * 4;
                    let poly = bus.alloc(data_size);
                    bus.write_long(handle, poly);
                    bus.write_word(poly, data_size as u16); // polySize
                                                            // Compute bounding box
                    let (mut min_v, mut min_h) = (i16::MAX, i16::MAX);
                    let (mut max_v, mut max_h) = (i16::MIN, i16::MIN);
                    for &(v, h) in verts {
                        min_v = min_v.min(v);
                        min_h = min_h.min(h);
                        max_v = max_v.max(v);
                        max_h = max_h.max(h);
                    }
                    if verts.is_empty() {
                        min_v = 0;
                        min_h = 0;
                        max_v = 0;
                        max_h = 0;
                    }
                    bus.write_word(poly + 2, min_v as u16); // polyBBox.top
                    bus.write_word(poly + 4, min_h as u16); // polyBBox.left
                    bus.write_word(poly + 6, max_v as u16); // polyBBox.bottom
                    bus.write_word(poly + 8, max_h as u16); // polyBBox.right
                                                            // Write vertex points
                    for (i, &(v, h)) in verts.iter().enumerate() {
                        let off = poly + 10 + i as u32 * 4;
                        bus.write_word(off, v as u16);
                        bus.write_word(off + 2, h as u16);
                    }
                }
                Ok(())
            }

            // KillPoly ($A8CD)
            // Releases the memory occupied by the given polygon.
            // PROCEDURE KillPoly(poly: PolyHandle);
            // Inside Macintosh Volume I, I-191
            //
            // Regression coverage:
            //   killpoly_frees_polygon
            //   killpoly_nil_handle_is_safe
            // KillPoly ($A8CD): Releases polygon memory per IM:I I-191
            (true, 0x0CD) => {
                let sp = cpu.read_reg(Register::A7);
                let poly_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if poly_handle != 0 {
                    let poly_ptr = bus.read_long(poly_handle);
                    if poly_ptr != 0 {
                        bus.free(poly_ptr);
                    }
                    bus.free(poly_handle);
                }
                Ok(())
            }

            // OffsetPoly ($A8CE)
            // PROCEDURE OffsetPoly(poly: PolyHandle; dh, dv: INTEGER);
            // Inside Macintosh Volume I, I-191
            // Pascal pushes args left-to-right so the deepest is leftmost:
            // dv at SP+0, dh at SP+2, poly at SP+4 (matches OffsetRgn).
            // OffsetPoly ($A8CE): Offsets bbox + vertices per IM:I I-191
            (true, 0x0CE) => {
                let sp = cpu.read_reg(Register::A7);
                let dv = bus.read_word(sp) as i16;
                let dh = bus.read_word(sp + 2) as i16;
                let poly_handle = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                if poly_handle != 0 {
                    let poly_ptr = bus.read_long(poly_handle);
                    if poly_ptr != 0 {
                        let size = bus.read_word(poly_ptr) as u32;
                        let n_points = (size.saturating_sub(10)) / 4;
                        // Offset bbox
                        for off in [2u32, 6] {
                            let v = bus.read_word(poly_ptr + off) as i16;
                            bus.write_word(poly_ptr + off, v.wrapping_add(dv) as u16);
                        }
                        for off in [4u32, 8] {
                            let h = bus.read_word(poly_ptr + off) as i16;
                            bus.write_word(poly_ptr + off, h.wrapping_add(dh) as u16);
                        }
                        // Offset each vertex
                        for i in 0..n_points {
                            let base = poly_ptr + 10 + i * 4;
                            let v = bus.read_word(base) as i16;
                            let h = bus.read_word(base + 2) as i16;
                            bus.write_word(base, v.wrapping_add(dv) as u16);
                            bus.write_word(base + 2, h.wrapping_add(dh) as u16);
                        }
                    }
                }
                Ok(())
            }

            // FramePoly ($A8C6)
            // PROCEDURE FramePoly(poly: PolyHandle);
            // Inside Macintosh Volume I, I-190
            // Draws the closing edge from last vertex back to the first per
            // IM:I I-189 ("ClosePoly closes the polygon by adding an
            // additional line that connects the last point to the first
            // point"). OpenRgn-recording shim uses the polygon's stored
            // polyBBox (4 words at poly_ptr+2).
            // FramePoly ($A8C6): Strokes each polygon edge incl. closing edge; OpenRgn-recording shim folds polyBBox into recording_region per IM:I I-190
            (true, 0x0C6) => {
                let sp = cpu.read_reg(Register::A7);
                let poly_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if self.extend_recording_region_from_poly(bus, poly_handle) {
                    return Some(Ok(()));
                }
                let verts = self.read_poly_vertices(bus, poly_handle);
                let n = verts.len();
                if n >= 2 {
                    for i in 0..n {
                        let (v0, h0) = verts[i];
                        let (v1, h1) = verts[(i + 1) % n];
                        if v0 == v1 && h0 == h1 {
                            continue; // skip degenerate zero-length closing edge
                        }
                        let _ = self.draw_line(cpu, bus, h0, v0, h1, v1);
                    }
                }
                Ok(())
            }

            // PaintPoly ($A8C7)
            // PROCEDURE PaintPoly(poly: PolyHandle);
            // Inside Macintosh Volume I, I-190
            //
            // OpenRgn-recording shim — see FramePoly notes above.
            // PaintPoly ($A8C7): Fills polygon with current pen pattern (paint verb); OpenRgn-recording shim folds polyBBox into recording_region per IM:I I-190
            (true, 0x0C7) => {
                let sp = cpu.read_reg(Register::A7);
                let poly_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if self.extend_recording_region_from_poly(bus, poly_handle) {
                    return Some(Ok(()));
                }
                self.draw_poly(cpu, bus, poly_handle, ShapeOp::Paint);
                Ok(())
            }

            // ErasePoly ($A8C8)
            // PROCEDURE ErasePoly(poly: PolyHandle);
            // Inside Macintosh Volume I, I-190
            //
            // OpenRgn-recording shim — see FramePoly notes above.
            // ErasePoly ($A8C8): Fills polygon with bgPat (erase verb); OpenRgn-recording shim folds polyBBox into recording_region per IM:I I-190
            (true, 0x0C8) => {
                let sp = cpu.read_reg(Register::A7);
                let poly_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if self.extend_recording_region_from_poly(bus, poly_handle) {
                    return Some(Ok(()));
                }
                self.draw_poly(cpu, bus, poly_handle, ShapeOp::Erase);
                Ok(())
            }

            // InvertPoly ($A8C9)
            // PROCEDURE InvertPoly(poly: PolyHandle);
            // Inside Macintosh Volume I, I-190
            //
            // OpenRgn-recording shim — see FramePoly notes above.
            // InvertPoly ($A8C9): Inverts polygon area (XOR with $FF); OpenRgn-recording shim folds polyBBox into recording_region per IM:I I-190
            (true, 0x0C9) => {
                let sp = cpu.read_reg(Register::A7);
                let poly_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if self.extend_recording_region_from_poly(bus, poly_handle) {
                    return Some(Ok(()));
                }
                self.draw_poly(cpu, bus, poly_handle, ShapeOp::Invert);
                Ok(())
            }

            // FillPoly ($A8CA)
            // PROCEDURE FillPoly(poly: PolyHandle; pat: Pattern);
            // Inside Macintosh Volume I, I-190
            // Stack: SP+0: pat(4), SP+4: poly(4) → pop 8.
            //
            // OpenRgn-recording shim — see FillRect notes.
            // FillPoly ($A8CA): Fills polygon with caller-supplied 8-byte Pattern; OpenRgn-recording shim folds polyBBox into recording_region per IM:I I-190
            (true, 0x0CA) => {
                let sp = cpu.read_reg(Register::A7);
                let pat_ptr = bus.read_long(sp);
                let poly_handle = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                if self.extend_recording_region_from_poly(bus, poly_handle) {
                    return Some(Ok(()));
                }
                let mut pat = [0u8; 8];
                for (i, byte) in pat.iter_mut().enumerate() {
                    *byte = bus.read_byte(pat_ptr + i as u32);
                }
                self.draw_poly(cpu, bus, poly_handle, ShapeOp::Fill(pat));
                Ok(())
            }

            // ========== Region Drawing ==========

            // FrameRgn ($A8D2)
            // Draws an outline just inside the specified region.
            // PROCEDURE FrameRgn(rgn: RgnHandle);
            // Inside Macintosh Volume I, I-186
            (true, 0x0D2) => {
                let sp = cpu.read_reg(Register::A7);
                let rgn_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if self.extend_recording_region_from_rgn(bus, rgn_handle) {
                    return Some(Ok(()));
                }
                self.draw_rgn(cpu, bus, rgn_handle, ShapeOp::Frame);
                Ok(())
            }

            // PaintRgn ($A8D3)
            // Paints the specified region with current pen pattern and mode.
            // PROCEDURE PaintRgn(rgn: RgnHandle);
            // Inside Macintosh Volume I, I-186
            (true, 0x0D3) => {
                let sp = cpu.read_reg(Register::A7);
                let rgn_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if self.extend_recording_region_from_rgn(bus, rgn_handle) {
                    return Some(Ok(()));
                }
                self.draw_rgn(cpu, bus, rgn_handle, ShapeOp::Paint);
                Ok(())
            }

            // EraseRgn ($A8D4)
            // PROCEDURE EraseRgn(rgn: RgnHandle);
            // Inside Macintosh Volume I, I-192
            //
            // OpenRgn-recording shim — see FrameRgn notes above.
            // EraseRgn ($A8D4): Fills region with bgPat (erase verb); complex regions via differential membership cache; OpenRgn-recording shim folds bbox per IM:I I-192
            (true, 0x0D4) => {
                let sp = cpu.read_reg(Register::A7);
                let rgn_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if self.extend_recording_region_from_rgn(bus, rgn_handle) {
                    return Some(Ok(()));
                }
                self.draw_rgn(cpu, bus, rgn_handle, ShapeOp::Erase);
                Ok(())
            }

            // InvertRgn ($A8D5)
            // PROCEDURE InvertRgn(rgn: RgnHandle);
            // Inside Macintosh Volume I, I-192
            //
            // OpenRgn-recording shim — see FrameRgn notes above.
            // InvertRgn ($A8D5): Inverts region (invert verb); complex regions via differential membership cache; OpenRgn-recording shim folds bbox per IM:I I-192
            (true, 0x0D5) => {
                let sp = cpu.read_reg(Register::A7);
                let rgn_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if self.extend_recording_region_from_rgn(bus, rgn_handle) {
                    return Some(Ok(()));
                }
                self.draw_rgn(cpu, bus, rgn_handle, ShapeOp::Invert);
                Ok(())
            }

            // FillRgn ($A8D6)
            // PROCEDURE FillRgn(rgn: RgnHandle; pat: Pattern);
            // Inside Macintosh Volume I, I-192
            //
            // Per Pascal left-to-right push convention pat is the last
            // parameter, so pat is at SP+0 and rgn at SP+4 — matches
            // FillRect / FillOval / FillRoundRect / FillPoly / FillArc.
            //
            // OpenRgn-recording shim — see FillRect notes.
            // FillRgn ($A8D6): Fills region with caller-supplied 8-byte Pattern; complex regions via differential membership cache; OpenRgn-recording shim folds bbox per IM:I I-192
            (true, 0x0D6) => {
                let sp = cpu.read_reg(Register::A7);
                let pat_ptr = bus.read_long(sp);
                let rgn_handle = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                if self.extend_recording_region_from_rgn(bus, rgn_handle) {
                    return Some(Ok(()));
                }
                let mut pat = [0u8; 8];
                for (i, byte) in pat.iter_mut().enumerate() {
                    *byte = bus.read_byte(pat_ptr + i as u32);
                }
                self.draw_rgn(cpu, bus, rgn_handle, ShapeOp::Fill(pat));
                Ok(())
            }

            // ========== Misc QD ==========

            // ScalePt ($A8F8)
            // Scales a width/height pair from srcRect proportions to
            // dstRect proportions. The minimum returned value is (1,1).
            // PROCEDURE ScalePt (VAR pt: Point; srcRect, dstRect: Rect);
            // Inside Macintosh Volume I, I-195
            //
            // "A width and height are passed in pt; the horizontal
            //  component of pt is the width, and its vertical component
            //  is the height. ScalePt scales these measurements by the
            //  ratio of dstRect's dimensions to srcRect's dimensions."
            //
            // Stack: SP+0=dstRect(4), SP+4=srcRect(4), SP+8=pt(4). Pop 12.
            //
            // Regression coverage:
            //   scalept_scales_width_and_height_by_rect_ratio
            //   scalept_minimum_result_is_one_one
            // ScalePt ($A8F8): Scales width/height by ratio of dstRect to srcRect; minimum (1,1) per IM:I I-195
            (true, 0x0F8) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_rect_ptr = bus.read_long(sp);
                let src_rect_ptr = bus.read_long(sp + 4);
                let pt_ptr = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);

                let pt_v = bus.read_word(pt_ptr) as i16;
                let pt_h = bus.read_word(pt_ptr + 2) as i16;

                let src_top = bus.read_word(src_rect_ptr) as i16;
                let src_left = bus.read_word(src_rect_ptr + 2) as i16;
                let src_bottom = bus.read_word(src_rect_ptr + 4) as i16;
                let src_right = bus.read_word(src_rect_ptr + 6) as i16;

                let dst_top = bus.read_word(dst_rect_ptr) as i16;
                let dst_left = bus.read_word(dst_rect_ptr + 2) as i16;
                let dst_bottom = bus.read_word(dst_rect_ptr + 4) as i16;
                let dst_right = bus.read_word(dst_rect_ptr + 6) as i16;

                let src_w = (src_right - src_left) as i32;
                let src_h = (src_bottom - src_top) as i32;
                let dst_w = (dst_right - dst_left) as i32;
                let dst_h = (dst_bottom - dst_top) as i32;

                // Scale dimensions by ratio, clamped to minimum of 1.
                let scaled_h = if src_w != 0 {
                    ((pt_h as i32) * dst_w / src_w).max(1)
                } else {
                    (pt_h as i32).max(1)
                };
                let scaled_v = if src_h != 0 {
                    ((pt_v as i32) * dst_h / src_h).max(1)
                } else {
                    (pt_v as i32).max(1)
                };

                bus.write_word(pt_ptr, scaled_v as u16);
                bus.write_word(pt_ptr + 2, scaled_h as u16);
                Ok(())
            }

            // MapPt ($A8F9)
            // Maps a point from srcRect coordinates to dstRect coordinates
            // using proportional scaling.
            // PROCEDURE MapPt(VAR pt: Point; srcRect, dstRect: Rect);
            // Inside Macintosh Volume I, I-196
            //
            // Regression coverage:
            //   mappt_maps_point_proportionally_between_rectangles
            //   mappt_consumes_point_and_rect_arguments
            // Stack: SP+0=dstRect(4), SP+4=srcRect(4), SP+8=pt(4)
            // MapPt ($A8F9): Proportional point mapping between source and destination rectangles
            (true, 0x0F9) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_rect_ptr = bus.read_long(sp);
                let src_rect_ptr = bus.read_long(sp + 4);
                let pt_ptr = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);

                let pt_v = bus.read_word(pt_ptr) as i16;
                let pt_h = bus.read_word(pt_ptr + 2) as i16;

                let src_top = bus.read_word(src_rect_ptr) as i16;
                let src_left = bus.read_word(src_rect_ptr + 2) as i16;
                let src_bottom = bus.read_word(src_rect_ptr + 4) as i16;
                let src_right = bus.read_word(src_rect_ptr + 6) as i16;

                let dst_top = bus.read_word(dst_rect_ptr) as i16;
                let dst_left = bus.read_word(dst_rect_ptr + 2) as i16;
                let dst_bottom = bus.read_word(dst_rect_ptr + 4) as i16;
                let dst_right = bus.read_word(dst_rect_ptr + 6) as i16;

                let src_w = (src_right - src_left) as i32;
                let src_h = (src_bottom - src_top) as i32;
                let dst_w = (dst_right - dst_left) as i32;
                let dst_h = (dst_bottom - dst_top) as i32;

                let mapped_h = if src_w != 0 {
                    dst_left as i32 + (pt_h as i32 - src_left as i32) * dst_w / src_w
                } else {
                    pt_h as i32
                };
                let mapped_v = if src_h != 0 {
                    dst_top as i32 + (pt_v as i32 - src_top as i32) * dst_h / src_h
                } else {
                    pt_v as i32
                };

                bus.write_word(pt_ptr, mapped_v as u16);
                bus.write_word(pt_ptr + 2, mapped_h as u16);
                Ok(())
            }

            // MapRect ($A8FA)
            // Maps a rectangle from srcRect coordinate space to dstRect coordinate space.
            // PROCEDURE MapRect(VAR r: Rect; srcRect, dstRect: Rect);
            // Inside Macintosh Volume I, I-197
            (true, 0x0FA) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_rect_ptr = bus.read_long(sp);
                let src_rect_ptr = bus.read_long(sp + 4);
                let r_ptr = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);

                if r_ptr != 0 && src_rect_ptr != 0 && dst_rect_ptr != 0 {
                    let r_top = bus.read_word(r_ptr) as i16 as i32;
                    let r_left = bus.read_word(r_ptr + 2) as i16 as i32;
                    let r_bottom = bus.read_word(r_ptr + 4) as i16 as i32;
                    let r_right = bus.read_word(r_ptr + 6) as i16 as i32;

                    let s_top = bus.read_word(src_rect_ptr) as i16 as i32;
                    let s_left = bus.read_word(src_rect_ptr + 2) as i16 as i32;
                    let s_bottom = bus.read_word(src_rect_ptr + 4) as i16 as i32;
                    let s_right = bus.read_word(src_rect_ptr + 6) as i16 as i32;

                    let d_top = bus.read_word(dst_rect_ptr) as i16 as i32;
                    let d_left = bus.read_word(dst_rect_ptr + 2) as i16 as i32;
                    let d_bottom = bus.read_word(dst_rect_ptr + 4) as i16 as i32;
                    let d_right = bus.read_word(dst_rect_ptr + 6) as i16 as i32;

                    let s_w = s_right - s_left;
                    let s_h = s_bottom - s_top;
                    let d_w = d_right - d_left;
                    let d_h = d_bottom - d_top;

                    let map_h = |h: i32| -> i32 {
                        if s_w != 0 {
                            d_left + (h - s_left) * d_w / s_w
                        } else {
                            h
                        }
                    };
                    let map_v = |v: i32| -> i32 {
                        if s_h != 0 {
                            d_top + (v - s_top) * d_h / s_h
                        } else {
                            v
                        }
                    };

                    bus.write_word(r_ptr, map_v(r_top) as u16);
                    bus.write_word(r_ptr + 2, map_h(r_left) as u16);
                    bus.write_word(r_ptr + 4, map_v(r_bottom) as u16);
                    bus.write_word(r_ptr + 6, map_h(r_right) as u16);
                }
                Ok(())
            }

            // MapRgn ($A8FB)
            // PROCEDURE MapRgn(rgn: RgnHandle; srcRect, dstRect: Rect);
            // Inside Macintosh Volume I, I-196
            //
            // For Systemless's rectangular-region approximation, MapRgn
            // applies the same proportional scaling as MapRect to the
            // region's bbox. A general implementation would map every
            // vertex of the region's polygon outline.
            //
            // Stack: SP+0=dstRect(4), SP+4=srcRect(4), SP+8=rgn(4).
            // Pops 12.
            //
            // Regression coverage:
            //   maprgn_maps_region_bbox_proportionally
            //   maprgn_consumes_region_and_rect_arguments
            // MapRgn ($A8FB): Maps region bbox from srcRect to dstRect proportionally; per IM:I I-196
            (true, 0x0FB) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_rect_ptr = bus.read_long(sp);
                let src_rect_ptr = bus.read_long(sp + 4);
                let rgn_handle = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);

                if rgn_handle != 0 && src_rect_ptr != 0 && dst_rect_ptr != 0 {
                    let rgn_ptr = bus.read_long(rgn_handle);
                    if rgn_ptr != 0 {
                        let r_top = bus.read_word(rgn_ptr + 2) as i16 as i32;
                        let r_left = bus.read_word(rgn_ptr + 4) as i16 as i32;
                        let r_bottom = bus.read_word(rgn_ptr + 6) as i16 as i32;
                        let r_right = bus.read_word(rgn_ptr + 8) as i16 as i32;

                        let s_top = bus.read_word(src_rect_ptr) as i16 as i32;
                        let s_left = bus.read_word(src_rect_ptr + 2) as i16 as i32;
                        let s_bottom = bus.read_word(src_rect_ptr + 4) as i16 as i32;
                        let s_right = bus.read_word(src_rect_ptr + 6) as i16 as i32;

                        let d_top = bus.read_word(dst_rect_ptr) as i16 as i32;
                        let d_left = bus.read_word(dst_rect_ptr + 2) as i16 as i32;
                        let d_bottom = bus.read_word(dst_rect_ptr + 4) as i16 as i32;
                        let d_right = bus.read_word(dst_rect_ptr + 6) as i16 as i32;

                        let s_w = s_right - s_left;
                        let s_h = s_bottom - s_top;
                        let d_w = d_right - d_left;
                        let d_h = d_bottom - d_top;

                        let mh = |h: i32| -> i32 {
                            if s_w != 0 {
                                d_left + (h - s_left) * d_w / s_w
                            } else {
                                h
                            }
                        };
                        let mv = |v: i32| -> i32 {
                            if s_h != 0 {
                                d_top + (v - s_top) * d_h / s_h
                            } else {
                                v
                            }
                        };

                        bus.write_word(rgn_ptr + 2, mv(r_top) as u16);
                        bus.write_word(rgn_ptr + 4, mh(r_left) as u16);
                        bus.write_word(rgn_ptr + 6, mv(r_bottom) as u16);
                        bus.write_word(rgn_ptr + 8, mh(r_right) as u16);
                    }
                }
                Ok(())
            }

            // MapPoly ($A8FC)
            // Maps a polygon from srcRect coordinates to dstRect
            // coordinates by mapping every vertex via MapPt logic.
            // PROCEDURE MapPoly (poly: PolyHandle; srcRect, dstRect: Rect);
            // Inside Macintosh Volume I, I-197
            //
            // "Given a polygon within srcRect, MapPoly maps it to a
            //  similarly located polygon within dstRect by calling MapPt
            //  to map all the points that define the polygon."
            //
            // Polygon layout: polySize(2) + polyBBox(8) + polyPoints(4×N).
            // Stack: SP+0=dstRect(4), SP+4=srcRect(4), SP+8=poly(4). Pop 12.
            //
            // Regression coverage:
            //   mappoly_maps_all_vertices_proportionally
            //   mappoly_consumes_polygon_and_rect_arguments
            // MapPoly ($A8FC): Maps all polygon vertices from srcRect to dstRect proportionally; per IM:I I-197
            (true, 0x0FC) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_rect_ptr = bus.read_long(sp);
                let src_rect_ptr = bus.read_long(sp + 4);
                let poly_handle = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);

                if poly_handle != 0 {
                    let poly_ptr = bus.read_long(poly_handle);
                    if poly_ptr != 0 {
                        let src_top = bus.read_word(src_rect_ptr) as i16 as i32;
                        let src_left = bus.read_word(src_rect_ptr + 2) as i16 as i32;
                        let src_bottom = bus.read_word(src_rect_ptr + 4) as i16 as i32;
                        let src_right = bus.read_word(src_rect_ptr + 6) as i16 as i32;
                        let dst_top = bus.read_word(dst_rect_ptr) as i16 as i32;
                        let dst_left = bus.read_word(dst_rect_ptr + 2) as i16 as i32;
                        let dst_bottom = bus.read_word(dst_rect_ptr + 4) as i16 as i32;
                        let dst_right = bus.read_word(dst_rect_ptr + 6) as i16 as i32;

                        let src_w = src_right - src_left;
                        let src_h = src_bottom - src_top;
                        let dst_w = dst_right - dst_left;
                        let dst_h = dst_bottom - dst_top;

                        let size = bus.read_word(poly_ptr) as u32;
                        // Map bbox (4 words at +2)
                        let map_h = |h: i16| -> i16 {
                            if src_w != 0 {
                                (dst_left + (h as i32 - src_left) * dst_w / src_w) as i16
                            } else {
                                h
                            }
                        };
                        let map_v = |v: i16| -> i16 {
                            if src_h != 0 {
                                (dst_top + (v as i32 - src_top) * dst_h / src_h) as i16
                            } else {
                                v
                            }
                        };

                        // Map bbox
                        let bb_top = bus.read_word(poly_ptr + 2) as i16;
                        let bb_left = bus.read_word(poly_ptr + 4) as i16;
                        let bb_bottom = bus.read_word(poly_ptr + 6) as i16;
                        let bb_right = bus.read_word(poly_ptr + 8) as i16;
                        bus.write_word(poly_ptr + 2, map_v(bb_top) as u16);
                        bus.write_word(poly_ptr + 4, map_h(bb_left) as u16);
                        bus.write_word(poly_ptr + 6, map_v(bb_bottom) as u16);
                        bus.write_word(poly_ptr + 8, map_h(bb_right) as u16);

                        // Map each vertex
                        let n_points = size.saturating_sub(10) / 4;
                        for i in 0..n_points {
                            let base = poly_ptr + 10 + i * 4;
                            let v = bus.read_word(base) as i16;
                            let h = bus.read_word(base + 2) as i16;
                            bus.write_word(base, map_v(v) as u16);
                            bus.write_word(base + 2, map_h(h) as u16);
                        }
                    }
                }
                Ok(())
            }

            // EqualPt ($A881)
            // Compares two points and returns TRUE if equal, FALSE if not.
            // FUNCTION EqualPt (pt1,pt2: Point) : BOOLEAN;
            // Inside Macintosh Volume I, I-193
            (true, 0x081) => {
                let sp = cpu.read_reg(Register::A7);
                let pt2_v = bus.read_word(sp) as i16;
                let pt2_h = bus.read_word(sp + 2) as i16;
                let pt1_v = bus.read_word(sp + 4) as i16;
                let pt1_h = bus.read_word(sp + 6) as i16;
                let eq = pt1_v == pt2_v && pt1_h == pt2_h;
                bus.write_word(sp + 8, if eq { 0x0100 } else { 0 });
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // HidePen ($A896)
            // Decrements the pen visibility counter. When pnVis < 0, drawing
            // operations with the pen have no visible effect.
            // PROCEDURE HidePen;
            // Inside Macintosh Volume I, I-168
            //
            // Regression coverage:
            //   src/trap/quickdraw.rs::tests::hidepen_decrements_current_port_pnvis_and_preserves_stack
            // HidePen ($A896): Decrements pnVis counter per IM:I I-168
            (true, 0x096) => {
                self.pn_vis -= 1;
                self.sync_current_port_draw_state(bus);
                Ok(())
            }

            // ShowPen ($A897)
            // Increments the pen visibility counter. Pen is visible when
            // pnVis >= 0 (initial value is 0).
            // PROCEDURE ShowPen;
            // Inside Macintosh Volume I, I-168
            //
            // Regression coverage:
            //   src/trap/quickdraw.rs::tests::showpen_increments_pnvis_and_allows_positive_values
            //   src/trap/quickdraw.rs::tests::showpen_balances_hidepen_back_to_zero
            // ShowPen ($A897): Increments pnVis counter per IM:I I-168
            (true, 0x097) => {
                self.pn_vis += 1;
                self.sync_current_port_draw_state(bus);
                Ok(())
            }

            // DisposPixPat ($AA08)
            // Releases all storage allocated by NewPixPat.
            // PROCEDURE DisposPixPat(ppat: PixPatHandle);
            // Inside Macintosh Volume V (1986), p. V-73
            //
            // "The DisposPixPat procedure releases all storage allocated
            //  by NewPixPat. It disposes of the pixPat's data handle,
            //  expanded data handle, and pixMap handle."
            //
            // MPW Universal Headers Quickdraw.h:
            //     EXTERN_API(void) DisposePixPat(PixPatHandle pp)
            //                                    ONEWORDINLINE(0xAA08);
            //     #define DisposPixPat(pp) DisposePixPat(pp)
            //
            // Tool-bit Pascal PROCEDURE ABI (engines-agree calling
            // convention pinned by aa08_aa09_dispose_copy_pixpat_strict):
            //   Stack on entry:  [SP+0] = ppat (4 bytes, PixPatHandle).
            //   Stack on exit:   trap pops 4 bytes; A7 net-balanced
            //                    across the call; no FUNCTION result
            //                    slot is written.
            //
            // PixPat layout (28 bytes):
            //   +0  patType   (2)
            //   +2  patMap    (PixMapHandle, 4)
            //   +6  patData   (Handle, 4)
            //  +10  patXData  (Handle, 4)
            //  +14  patXValid (2)
            //  +16  patXMap   (Handle, 4)
            //  +20  pat1Data  (8)
            //
            // Documented behaviour: walks the PixPat handle chain freeing
            // patData (+6), patXData (+10), patMap (+2) including its
            // pmTable/color table, then the PixPat record itself, then
            // the outer handle. Each nested dereference is guarded behind
            // a non-NIL check.
            //
            // Engines-divergent absolute behavior: BII System 7.5.3 ROM
            // Color QuickDraw walks the full IM:V V-73 handle chain
            // freeing every nested allocation NewPixPat made. Systemless's
            // NewPixPat-allocated records have fewer embedded handles
            // (zero-initialised record with patType=1 only; no embedded
            // patMap/patData/patXData/patXMap), so the actual free-list
            // mutation is engines-divergent. The bakeable engines-agree
            // subset is the Pascal PROCEDURE pop-4 calling convention
            // itself (witnessed by the aa08_aa09_dispose_copy_pixpat_strict
            // fixture bands B1 single-call + B2 5-call composition with
            // NewPixPat-allocated handles).
            //
            // ## TRAP-WORD SWAP FIX (historical)
            // This arm previously matched `(true, 0x209)` and was
            // labeled "$AA09 DisposPixPat" — but per IM:V V-291
            // master trap dispatch table line 20706, $AA08 is DisposPixPat.
            // The arm was SWAPPED with CopyPixPat ($AA09); fixed by
            // swapping the trap-word matches. Real-Mac apps emitting
            // _DisposPixPat now correctly land here.
            //
            // Regression coverage:
            //   disposepixpat_frees_pixpat
            //   disposepixpat_nil_handle_is_harmless
            //   aa08_aa09_dispose_copy_pixpat_strict
            //     (BasiliskII-baked calling-convention witness for $AA08 + $AA09 pair)
            (true, 0x208) => {
                let sp = cpu.read_reg(Register::A7);
                let ppat_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if ppat_handle != 0 {
                    let ppat_ptr = bus.read_long(ppat_handle);
                    if ppat_ptr != 0 {
                        // Free patData handle (+6)
                        let pat_data_handle = bus.read_long(ppat_ptr + 6);
                        if pat_data_handle != 0 {
                            let pat_data_ptr = bus.read_long(pat_data_handle);
                            if pat_data_ptr != 0 {
                                bus.free(pat_data_ptr);
                            }
                            bus.free(pat_data_handle);
                        }
                        // Free patXData handle (+10)
                        let pat_xdata_handle = bus.read_long(ppat_ptr + 10);
                        if pat_xdata_handle != 0 {
                            let pat_xdata_ptr = bus.read_long(pat_xdata_handle);
                            if pat_xdata_ptr != 0 {
                                bus.free(pat_xdata_ptr);
                            }
                            bus.free(pat_xdata_handle);
                        }
                        // Free patMap (PixMapHandle at +2) — also frees its ctab
                        let pat_map_handle = bus.read_long(ppat_ptr + 2);
                        if pat_map_handle != 0 {
                            let pat_map_ptr = bus.read_long(pat_map_handle);
                            if pat_map_ptr != 0 {
                                let ctab_handle = bus.read_long(pat_map_ptr + 42);
                                if ctab_handle != 0 {
                                    let ctab_ptr = bus.read_long(ctab_handle);
                                    if ctab_ptr != 0 {
                                        bus.free(ctab_ptr);
                                    }
                                    bus.free(ctab_handle);
                                }
                                bus.free(pat_map_ptr);
                            }
                            bus.free(pat_map_handle);
                        }
                        // Free the PixPat record
                        bus.free(ppat_ptr);
                    }
                    bus.free(ppat_handle);
                }
                Ok(())
            }

            // PenPixPat ($AA0A)
            // Sets the pen pixel pattern in the current cGrafPort.
            // PROCEDURE PenPixPat (pp: PixPatHandle);
            // Inside Macintosh Volume V (1986), p. V-72
            //
            // "PenPixPat is analogous to PenPat, but uses a multicolor
            //  pixel pattern. The handle is placed directly into the
            //  port's pnPixPat field."
            //
            // MPW Universal Headers Quickdraw.h:
            //     EXTERN_API(void) PenPixPat(PixPatHandle pp) ONEWORDINLINE(0xAA0A);
            //
            // Tool-bit Pascal PROCEDURE ABI (engines-agree calling
            // convention pinned by aa0a_aa0b_pen_back_pixpat_strict):
            //   Stack on entry:  [SP+0] = pp (4 bytes, PixPatHandle).
            //   Stack on exit:   trap pops 4 bytes; A7 net-balanced
            //                    across the call; no FUNCTION result
            //                    slot is written.
            //
            // Documented behaviour: when pp != NIL, the PixPat record's
            // pat1Data (8 bytes at offset +20) is copied into the
            // port's pnPat shadow, and the handle is stored in the
            // port's pnPixPat field (+58). When pp == NIL both BII
            // System 7.5.3 ROM Color QuickDraw and Systemless HLE take
            // an early-exit no-op path before touching pnPixPat: the
            // pop is still performed but no port-field write happens.
            //
            // The aa0a_aa0b_pen_back_pixpat_strict fixture witnesses
            // the engines-agree Pascal PROCEDURE pop-4 calling
            // convention on the NIL early-exit path (B1 single call,
            // B2 5-call composition). The absolute pnPixPat-field
            // mutation and pat1Data copy on the non-NIL path is
            // exercised only by the existing Systemless unit test
            // `penpixpat_stores_handle_and_copies_pat1data` because
            // it requires a host-constructed PixPat record whose
            // exact layout is engines-divergent.
            (true, 0x20A) => {
                let sp = cpu.read_reg(Register::A7);
                let ppat_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if ppat_handle != 0 {
                    let ppat_ptr = bus.read_long(ppat_handle);
                    if ppat_ptr != 0 {
                        // Copy pat1Data (8 bytes at +20) into pn_pat.
                        for i in 0..8u32 {
                            self.pn_pat[i as usize] = bus.read_byte(ppat_ptr + 20 + i);
                        }
                    }
                    // Store handle into port's pnPixPat field (+58).
                    let a5 = cpu.read_reg(Register::A5);
                    let global_ptr = bus.read_long(a5);
                    let port_ptr = bus.read_long(global_ptr);
                    if port_ptr != 0 {
                        bus.write_long(port_ptr + 58, ppat_handle);
                    }
                }
                Ok(())
            }

            // BackPixPat ($AA0B)
            // Sets the background pixel pattern in the current cGrafPort.
            // PROCEDURE BackPixPat (pp: PixPatHandle);
            // Inside Macintosh Volume V (1986), p. V-72
            //
            // Analogous to BackPat / PenPixPat. Stores the handle into
            // the port's bkPixPat field (+66) and copies pat1Data into
            // the legacy 8-byte bkPat shadow.
            //
            // MPW Universal Headers Quickdraw.h:
            //     EXTERN_API(void) BackPixPat(PixPatHandle pp) ONEWORDINLINE(0xAA0B);
            //
            // Tool-bit Pascal PROCEDURE ABI (engines-agree calling
            // convention pinned by aa0a_aa0b_pen_back_pixpat_strict):
            //   Stack on entry:  [SP+0] = pp (4 bytes, PixPatHandle).
            //   Stack on exit:   trap pops 4 bytes; A7 net-balanced
            //                    across the call; no FUNCTION result
            //                    slot is written.
            //
            // Documented behaviour: when pp != NIL, the PixPat's
            // pat1Data (8 bytes at offset +20) is copied into bk_pat,
            // and the handle is stored in the port's bkPixPat field
            // (+66). When pp == NIL both BII System 7.5.3 ROM Color
            // QuickDraw and Systemless HLE take an early-exit no-op path
            // before touching bkPixPat: the pop is still performed
            // but no port-field write happens.
            //
            // The aa0a_aa0b_pen_back_pixpat_strict fixture witnesses
            // the engines-agree Pascal PROCEDURE pop-4 calling
            // convention on the NIL early-exit path (B3 single call,
            // B4 5-call composition).
            (true, 0x20B) => {
                let sp = cpu.read_reg(Register::A7);
                let ppat_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if ppat_handle != 0 {
                    let ppat_ptr = bus.read_long(ppat_handle);
                    if ppat_ptr != 0 {
                        // Copy pat1Data (8 bytes at +20) into bk_pat.
                        for i in 0..8u32 {
                            self.bk_pat[i as usize] = bus.read_byte(ppat_ptr + 20 + i);
                        }
                    }
                    // Store handle into port's bkPixPat field (+66).
                    let a5 = cpu.read_reg(Register::A5);
                    let global_ptr = bus.read_long(a5);
                    let port_ptr = bus.read_long(global_ptr);
                    if port_ptr != 0 {
                        bus.write_long(port_ptr + 66, ppat_handle);
                    }
                }
                Ok(())
            }

            // OpColor ($AA21)
            // Sets the RGBColor used by addPin, subPin, and blend
            // arithmetic transfer modes in the current cGrafPort.
            // PROCEDURE OpColor (color: RGBColor);
            // Inside Macintosh Volume V (1986), p. V-77
            //
            // "If the current port is a cGrafPort, the OpColor procedure
            //  sets the red, green, and blue values used by the AddPin,
            //  SubPin, and Blend drawing modes. This information is
            //  actually stored in the grafVars handle in the cGrafPort,
            //  but you should never need to reference it directly.
            //  If the current port is a grafPort, OpColor has no effect."
            //
            // MPW Universal Headers Quickdraw.h:
            //     EXTERN_API(void) OpColor(const RGBColor *color) ONEWORDINLINE(0xAA21);
            //
            // Tool-bit Pascal PROCEDURE ABI (engines-agree calling
            // convention pinned by aa21_aa22_opcolor_hilitecolor_strict):
            //   Stack on entry:  [SP+0] = color (4 bytes, RGBColor*).
            //   Stack on exit:   trap pops 4 bytes; A7 net-balanced
            //                    across the call; no FUNCTION result
            //                    slot is written.
            //
            // Documented behaviour: when color != NIL, three RGB words
            // (red, green, blue) are read from [color..color+6] and
            // stored in the cGrafPort's grafVars handle. When color ==
            // NIL both BII System 7.5.3 ROM Color QuickDraw and Systemless
            // HLE take an early-exit no-op path before touching
            // grafVars: the 4-byte pop is still performed but no field
            // write happens. Systemless stores the RGB on the dispatcher
            // (self.op_color) rather than per-port grafVars; this
            // matches the single-port assumption used by fg_color /
            // bg_color and is engines-divergent vs BII's WMgrCPort
            // grafVars write on the non-NIL path.
            //
            // The aa21_aa22_opcolor_hilitecolor_strict fixture witnesses
            // the engines-agree Pascal PROCEDURE pop-4 calling
            // convention on the NIL early-exit path (B1 single call,
            // B2 5-call composition).
            //
            // Regression coverage:
            //   opcolor_stores_rgb_for_arithmetic_transfer_modes
            //   opcolor_nil_pointer_is_harmless
            (true, 0x221) => {
                let sp = cpu.read_reg(Register::A7);
                let rgb_ptr = bus.read_long(sp);
                if rgb_ptr != 0 {
                    let r = bus.read_word(rgb_ptr);
                    let g = bus.read_word(rgb_ptr + 2);
                    let b = bus.read_word(rgb_ptr + 4);
                    self.op_color = (r, g, b);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // HiliteColor ($AA22)
            // Sets the RGBColor used by the highlight transfer mode in
            // the current cGrafPort.
            // PROCEDURE HiliteColor(color: RGBColor);
            // Inside Macintosh Volume V (1986), p. V-77
            //
            // "The highlight color is used by all drawing operations that
            //  use the highlight transfer mode. When a cGrafPort is
            //  created, its highlight color is initialized from the
            //  global variable HiliteRGB. The HiliteColor procedure
            //  allows you to change the highlighting color used by the
            //  current port. This information is actually stored in the
            //  grafVars handle in the cGrafPort, but you should never
            //  need to reference it directly. If the current port is a
            //  grafPort, HiliteColor has no effect."
            //
            // MPW Universal Headers Quickdraw.h:
            //     EXTERN_API(void) HiliteColor(const RGBColor *color) ONEWORDINLINE(0xAA22);
            //
            // Tool-bit Pascal PROCEDURE ABI (engines-agree calling
            // convention pinned by aa21_aa22_opcolor_hilitecolor_strict):
            //   Stack on entry:  [SP+0] = color (4 bytes, RGBColor*).
            //   Stack on exit:   trap pops 4 bytes; A7 net-balanced
            //                    across the call; no FUNCTION result
            //                    slot is written.
            //
            // Documented behaviour: when color != NIL, three RGB words
            // (red, green, blue) are read from [color..color+6] and
            // stored in the cGrafPort's grafVars handle (rgbHiliteColor
            // field). When color == NIL both BII System 7.5.3 ROM Color
            // QuickDraw and Systemless HLE take an early-exit no-op path
            // before touching grafVars. Systemless stores the hilite color
            // on the dispatcher (self.hilite_color) rather than per-port
            // grafVars; this matches the single-port assumption used by
            // fg_color / bg_color and is engines-divergent vs BII's
            // WMgrCPort grafVars write on the non-NIL path.
            //
            // The aa21_aa22_opcolor_hilitecolor_strict fixture witnesses
            // the engines-agree Pascal PROCEDURE pop-4 calling
            // convention on the NIL early-exit path (B3 single call,
            // B4 5-call composition).
            //
            // Regression coverage:
            //   hilitecolor_stores_rgb_for_subsequent_drawing
            (true, 0x222) => {
                let sp = cpu.read_reg(Register::A7);
                let rgb_ptr = bus.read_long(sp);
                if rgb_ptr != 0 {
                    let r = bus.read_word(rgb_ptr);
                    let g = bus.read_word(rgb_ptr + 2);
                    let b = bus.read_word(rgb_ptr + 4);
                    self.hilite_color = (r, g, b);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // ========== Stubs — Cursor ==========

            // ShieldCursor ($A855)
            // PROCEDURE ShieldCursor(shieldRect: Rect; offsetPt: Point);
            // Inside Macintosh Volume I, I-474
            // Imaging With QuickDraw 1994, p. 8-29
            //
            // MPW Universal Headers (Quickdraw.h):
            //
            //   EXTERN_API(void) ShieldCursor(const Rect *shieldRect,
            //                                 Point offsetPt) ONEWORDINLINE(0xA855);
            //
            // Pascal LR stack frame at trap entry (first arg deepest):
            //   sp+0..3  offsetPt    (Point by value, last pushed → shallowest)
            //   sp+4..7  shieldRect  (Rect ptr, first pushed → deepest)
            //   8-byte total pop, no result slot.
            //
            // Documented semantics (IM:I I-474): hide cursor when it
            // intersects the shielded rectangle (or on mouse movement into
            // it), decrement cursor level like HideCursor, and balance
            // with ShowCursor.
            //
            // HLE compromise: cursor presentation is host-rendered and
            // this runtime does not model cursor/rectangle intersection
            // in guest coordinates across host frame boundaries, so
            // ShieldCursor is a stack-shape no-op that only consumes its
            // arguments.  The Apple-canonical cursor-level decrement is
            // pinned via the in-Rust contract test
            // `shield_cursor_pops_eight_bytes_and_preserves_cursor_state_in_hle`
            // and declared `witness_kind = "contract"` in the catalogue
            // row since both engines diverge on the LowMem CrsrVis side
            // effect (BII System 7.5.3 ROM writes CrsrVis; Systemless HLE
            // doesn't touch it).
            //
            // Engines-agree subset (witnessed by the strict bake
            // `a855_a856_shieldcursor_obscurecursor_strict`):
            //   - 8-byte arg-frame pop balanced across single + 5-call
            //     StackSpace sandwiches; net A7 delta zero externally.
            //
            // ShieldCursor ($A855): HLE no-op; pops 8 bytes per IM:I I-474 MPW C declaration ShieldCursor(const Rect *shieldRect, Point offsetPt) ONEWORDINLINE(0xA855)
            (true, 0x055) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // AllocCursor ($AA1D)
            // Reallocates cursor memory for use with Color QuickDraw. No-op stub.
            // PROCEDURE AllocCursor;
            // Inside Macintosh Volume V, V-79
            // AllocCursor ($AA1D): No parameters; cursor mem-mgmt is host-side per IM:V V-79
            (true, 0x21D) => Ok(()),

            // ========== Stubs — Color Manager ==========

            // Color2Index ($AA33)
            // FUNCTION Color2Index (rgb: RGBColor): LongInt;
            // Inside Macintosh Volume V, V-141 (Color Manager — Color
            // Manager Routines — Color Conversion)
            //
            // "The Color2Index routine finds the best available approximation
            //  to a given absolute color, using the list of search procedures
            //  in the current device record. It returns a longint, which is a
            //  pixel value padded with zeros in the high word."
            //
            // Stack: SP+0: rgb_ptr(4). Return LongInt at SP+4. Pop to SP+4.
            //
            // Implementation: linear scan of `device_clut` for the entry with
            // the smallest sum-of-squared-channel-distances to the requested
            // RGB. Real Mac ROMs build a 4-bit (or 5-bit) inverse table for
            // O(1) lookup, but a 256-entry linear scan is fast enough for
            // Systemless's needs and matches the IM "best available match"
            // semantic exactly. The high word of the return is forced to
            // zero per the spec.
            //
            // Paired catalogue proof:
            //   aa33_aa35_color2index_invertcolor_strict
            //   (B1 single-call + B2 5-call composition witness the
            //    engines-agree Pascal FUNCTION calling convention; the
            //    absolute LONGINT index is engines-divergent because
            //    BII walks the ROM inverse table while Systemless does a
            //    linear scan, so the calling-convention conjunct is
            //    the only engines-agree witness.)
            // Color2Index ($AA33): Linear-scan nearest match against device_clut, skips reserved entries, IM:V V-141
            (true, 0x233) => {
                let sp = cpu.read_reg(Register::A7);
                let rgb_ptr = bus.read_long(sp);
                let want_r = bus.read_word(rgb_ptr);
                let want_g = bus.read_word(rgb_ptr + 2);
                let want_b = bus.read_word(rgb_ptr + 4);

                // Use the same `closest_clut_index` helper as DrawPicture /
                // CopyBits path so the dst index for a given RGB matches
                // across QuickDraw entry points. Linear-scan Euclidean
                // diverges from Mac ROM's MakeITable behavior on inputs that
                // fall in the same 4-bit cube cell as multiple CLUT entries
                // — koji's ModalDialog draw routine hit this and produced
                // a different cube index (195 = teal) than BasiliskII (91 =
                // pink-purple) for the same dialog colour, with the same
                // canonical 8bpp CLUT and the same gamma LUT downstream.
                // Routing both paths through one matcher keeps Color2Index
                // and the PICT/CopyBits inverse-mapping consistent and
                // converges with the Mac ROM behaviour BasiliskII inherits
                // from a real ROM image.
                //
                // Per IM:V V-145, reserved entries are excluded from
                // matching: "A reserved entry cannot be matched by another
                // client's search procedure, and will never be returned to
                // another client by Color2Index". The shared helper doesn't
                // currently honour the reserved-entry skip — preserve the
                // skip semantic by zeroing reserved entries' RGB before
                // matching is overkill; instead, fall back to the linear
                // scan when ANY reserved entries exist.
                let any_reserved = self.clut_reserved.iter().any(|&r| r);
                let best_idx = if any_reserved {
                    let want_r = want_r as i64;
                    let want_g = want_g as i64;
                    let want_b = want_b as i64;
                    let mut best_idx: usize = 0;
                    let mut best_dist: i64 = i64::MAX;
                    for (i, entry) in self.device_clut.iter().enumerate() {
                        if self.clut_reserved[i] {
                            continue;
                        }
                        let dr = entry[0] as i64 - want_r;
                        let dg = entry[1] as i64 - want_g;
                        let db = entry[2] as i64 - want_b;
                        let dist = dr * dr + dg * dg + db * db;
                        if dist < best_dist {
                            best_dist = dist;
                            best_idx = i;
                            if dist == 0 {
                                break;
                            }
                        }
                    }
                    best_idx
                } else {
                    super::pict::closest_clut_index(want_r, want_g, want_b, &self.device_clut)
                        as usize
                };

                bus.write_long(sp + 4, best_idx as u32);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // GetSubTable ($AA37)
            // PROCEDURE GetSubTable(myColors: CTabHandle; iTabRes: INTEGER;
            //                       targetTbl: CTabHandle);
            // Inside Macintosh Volume V (1986), p. V-142
            //   (Color Manager — Color Manager Routines — GetSubTable)
            //
            // MPW Universal Headers Quickdraw.h:
            //   EXTERN_API(void) GetSubTable(CTabHandle myColors,
            //       short iTabRes, CTabHandle targetTbl) ONEWORDINLINE(0xAA37);
            //
            // "The GetSubTable routine takes a ColorTable pointed at by
            //  myColors, and maps each RGB value into its nearest available
            //  match for each target table. These best matches are returned
            //  in the colorSpec.value fields of myColors. The values returned
            //  are best matches to the RGBColor in targetTbl and the returned
            //  indices are indices into targetTbl. Best matches are
            //  calculated using Color2Index and all applicable rules apply.
            //  A temporary inverse table is built, and then discarded.
            //  ITabRes controls the resolution of the iTable that is built.
            //  If targetTbl is NIL, then the current device's color table
            //  is used, and the device's inverse table is used rather than
            //  building a new one." (IM:V V-142)
            //
            // Tool-bit Pascal PROCEDURE (bit 11 set) pop-10 calling
            // convention. Caller pushes left-to-right, so the on-stack
            // frame is:
            //   SP+0 : targetTbl  (4-byte CTabHandle)
            //   SP+4 : iTabRes    (2-byte INTEGER)
            //   SP+6 : myColors   (4-byte CTabHandle)
            // No FUNCTION result slot.
            //
            // iTabRes is the requested inverse-table resolution (3-5
            // bits/channel per IM:V V-138). Systemless does full-precision
            // linear-scan Euclidean matching against the supplied
            // targetTbl bytes, so iTabRes is recorded for documentation
            // but otherwise ignored. BII System 7.5.3 ROM builds a real
            // temporary inverse table at the requested resolution and
            // calls Color2Index; absolute returned indices may diverge
            // from Systemless's for non-trivial CLUTs. The Pascal PROCEDURE
            // pop-10 calling convention is engines-agree.
            //
            // Regression coverage:
            //   getsubtable_writes_closest_match_indices_into_mycolors_value_fields
            //   getsubtable_with_nil_target_uses_current_device_clut
            //   getsubtable_processes_every_mycolors_entry
            //
            // Catalogue-proof witness:
            //   aa37_getsubtable_strict witnesses the
            //   engines-agree pop-10 calling convention via single-call
            //   and 5-call composition StackSpace sandwiches (BasiliskII
            //   PASS on first deterministic bake).
            //
            // GetSubTable ($AA37): Group Color2Index over a CTab; NIL targetTbl uses device_clut, IM:V V-142
            (true, 0x237) => {
                let sp = cpu.read_reg(Register::A7);
                let target_handle = bus.read_long(sp);
                let _itab_res = bus.read_word(sp + 4);
                let my_handle = bus.read_long(sp + 6);
                cpu.write_reg(Register::A7, sp + 10);

                let my_ptr = if my_handle != 0 {
                    bus.read_long(my_handle)
                } else {
                    0
                };
                if my_ptr != 0 {
                    // Build the target CLUT to match against. NIL targetTbl =
                    // current device CLUT.
                    let target_clut: [[u16; 3]; 256] = if target_handle == 0 {
                        self.device_clut
                    } else {
                        let target_ptr = bus.read_long(target_handle);
                        if target_ptr == 0 {
                            self.device_clut
                        } else {
                            let mut clut = [[0u16; 3]; 256];
                            let target_size = (bus.read_word(target_ptr + 6) as usize).min(255);
                            for (i, slot) in clut.iter_mut().take(target_size + 1).enumerate() {
                                let entry = target_ptr + 8 + (i as u32) * 8;
                                *slot = [
                                    bus.read_word(entry + 2),
                                    bus.read_word(entry + 4),
                                    bus.read_word(entry + 6),
                                ];
                            }
                            // Pad unused slots with a sentinel that won't be
                            // the closest match for any reasonable RGB query.
                            for slot in clut.iter_mut().skip(target_size + 1) {
                                *slot = [0xFFFF, 0x0000, 0xFFFF];
                            }
                            clut
                        }
                    };

                    let my_size = (bus.read_word(my_ptr + 6) as usize).min(255);
                    for i in 0..=my_size {
                        let entry = my_ptr + 8 + (i as u32) * 8;
                        let r = bus.read_word(entry + 2);
                        let g = bus.read_word(entry + 4);
                        let b = bus.read_word(entry + 6);
                        let idx = super::pict::closest_clut_index(r, g, b, &target_clut);
                        bus.write_word(entry, idx as u16);
                    }
                }
                Ok(())
            }

            // AddSearch ($AA3A)
            // PROCEDURE AddSearch(searchProc: ProcPtr);
            // Inside Macintosh Volume V (1986), p. V-147
            // (Color Manager — Custom Search and Complement Procedures — AddSearch).
            //
            // MPW Universal Headers Quickdraw.h:
            //   EXTERN_API(void) AddSearch(ColorSearchUPP searchProc)
            //       ONEWORDINLINE(0xAA3A);
            //
            // Per IM:V V-147 AddSearch allocates an SProcRec via NewHandle
            // and links it onto the head of the active gDevice's gdSearchProc
            // list. The supplied ProcPtr is later invoked when a Color2Index
            // lookup walks the search chain. Systemless does not implement the
            // chain (identity color matching is used on a 1bpp grafport host),
            // so this is a documented no-op stub that pops the 4-byte ProcPtr
            // arg and returns.
            //
            // Stack: SP+0: searchProc(4). Pop 4. No FUNCTION result slot.
            //
            // Engines-agree subset witnessed by the aa3a_aa3b_addsearch_addcomp_strict
            // bake: the Tool-bit Pascal PROCEDURE pop-4 calling convention — A7
            // unchanged across the call regardless of engines-divergent absolute
            // search-chain mutation.
            (true, 0x23A) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // AddComp ($AA3B)
            // PROCEDURE AddComp(compProc: ProcPtr);
            // Inside Macintosh Volume V (1986), p. V-147
            // (Color Manager — Custom Search and Complement Procedures — AddComp).
            //
            // MPW Universal Headers Quickdraw.h:
            //   EXTERN_API(void) AddComp(ColorComplementUPP compProc)
            //       ONEWORDINLINE(0xAA3B);
            //
            // Per IM:V V-147 AddComp allocates a CProcRec via NewHandle and
            // links it onto the head of the active gDevice's gdCompProc list.
            // The supplied ProcPtr is later invoked when an InvertColor /
            // Color Manager complement lookup walks the chain. Systemless uses
            // the IM:V V-141 default 1's-complement procedure path; custom
            // complement procs are not honored, so this is a documented no-op
            // stub that pops the 4-byte ProcPtr arg and returns.
            //
            // Stack: SP+0: compProc(4). Pop 4. No FUNCTION result slot.
            //
            // Engines-agree subset witnessed by the aa3a_aa3b_addsearch_addcomp_strict
            // bake: the Tool-bit Pascal PROCEDURE pop-4 calling convention.
            (true, 0x23B) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // SetClientID ($AA3C)
            // PROCEDURE SetClientID(id: INTEGER);
            // Inside Macintosh Volume V (1986), p. V-147
            // (Color Manager — Custom Search and Complement Procedures — SetClientID).
            //
            // MPW Universal Headers Quickdraw.h:
            //   EXTERN_API(void) SetClientID(short id) ONEWORDINLINE(0xAA3C);
            //
            // Per IM:V V-147: "The SetClientID procedure sets the gdID field
            // in the current device record to identify this client program to
            // its search and complement procedures." Systemless's Color Manager
            // uses identity color matching and the default 1's-complement
            // procedure path; neither consults a per-client gdID tag. This is
            // a documented no-op stub that pops the 2-byte INTEGER id and
            // returns.
            //
            // Stack: SP+0: id(2). Pop 2. No FUNCTION result slot.
            //
            // Engines-agree subset witnessed by the aa3c_setclientid_strict
            // bake: the Tool-bit Pascal PROCEDURE pop-2 calling convention —
            // A7 unchanged across the call regardless of engines-divergent
            // absolute gdID-field mutation.
            (true, 0x23C) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // SaveEntries ($AA49)
            // PROCEDURE SaveEntries(srcTable: CTabHandle; resultTable: CTabHandle;
            //                       VAR selection: ReqListRec);
            // Inside Macintosh Volume V (1986), p. V-144
            // (Color Manager — Color Manager Routines — SaveEntries)
            //
            // MPW Universal Headers Quickdraw.h:
            //   EXTERN_API(void) SaveEntries(CTabHandle srcTable,
            //       CTabHandle resultTable, ReqListRec *selection)
            //       ONEWORDINLINE(0xAA49);
            //
            // "SaveEntries saves a selection of entries from srcTable into
            //  resultTable. The entries to be set are enumerated in the
            //  selection parameter ... If an entry is not present in
            //  srcTable, then that position of the requestList is set to
            //  colReqErr ... SaveEntries optionally allows NIL as its source
            //  color table parameter. If NIL is used, the current device's
            //  color table is used as the source."
            //
            // Stack (Pascal PROCEDURE, pop-12): SP+0=selection(4),
            // SP+4=resultTable(4), SP+8=srcTable(4). No FUNCTION result
            // slot.
            //
            // ReqListRec layout:
            //   +0  reqLSize: Integer  (count - 1)
            //   +2  reqLData: ARRAY[0..reqLSize] of INTEGER
            //
            // For i in 0..=reqLSize: copy src[selection.reqLData[i]] into
            // result[i]. If selection.reqLData[i] is out of range in src,
            // set selection.reqLData[i] = colReqErr (-1) and leave
            // result[i] undefined.
            //
            // Engines-agree subset: the Pascal PROCEDURE pop-12 calling
            // convention. Absolute Color Manager state mutation diverges
            // between engines (BII walks gDevice CLUT per IM:V V-137..V-138,
            // Systemless walks dispatcher-internal device_clut and supplied
            // CTab bytes). Witnessed PASS by the
            // aa49_aa4a_saveentries_restoreentries_strict catalog fixture
            // bands B1 + B2, and by
            // tests::saveentries_restoreentries_pascal_procedure_preserves_stack_across_five_calls.
            (true, 0x249) => {
                let sp = cpu.read_reg(Register::A7);
                let selection_ptr = bus.read_long(sp);
                let result_handle = bus.read_long(sp + 4);
                let src_handle = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);

                if selection_ptr == 0 || result_handle == 0 {
                    return Some(Ok(()));
                }
                let result_ptr = bus.read_long(result_handle);
                if result_ptr == 0 {
                    return Some(Ok(()));
                }

                let req_size = bus.read_word(selection_ptr) as i16;
                if req_size < 0 {
                    return Some(Ok(()));
                }
                let count = (req_size as usize) + 1;

                let result_size = 8 + (count as u32) * 8;
                let result_ptr = self.resize_handle_allocation(bus, result_handle, result_size);
                if result_ptr == 0 {
                    return Some(Ok(()));
                }

                // Source: NIL handle = current device CLUT, else read from
                // the CTab pointed to by the handle. When possible, NIL
                // source reads through the current GDevice CTab so the
                // packed result carries real ColorSpec.value words instead
                // of synthesized indices.
                let current_ctab_handle = if src_handle == 0 {
                    self.current_gdevice_ctab_handle(bus)
                } else {
                    0
                };
                let src_ptr = if src_handle != 0 {
                    bus.read_long(src_handle)
                } else if current_ctab_handle != 0 {
                    bus.read_long(current_ctab_handle)
                } else {
                    0
                };
                // src_size is the maximum valid index in the source
                // (exclusive bound = src_size + 1 entries).
                let src_size: usize = if src_ptr != 0 {
                    bus.read_word(src_ptr + 6) as usize
                } else {
                    255 // device_clut always has 256 entries
                };

                // SaveEntries produces a packed ColorTable suitable for
                // RestoreEntries. Callers may pass a freshly-created
                // zero-length Handle; make the result structurally valid
                // before writing entries.
                let seed = if src_ptr != 0 {
                    bus.read_long(src_ptr)
                } else {
                    self.next_color_table_seed()
                };
                bus.write_long(result_ptr, seed);
                bus.write_word(result_ptr + 4, 0);
                bus.write_word(result_ptr + 6, req_size as u16);

                for i in 0..count {
                    let req_slot = selection_ptr + 2 + (i as u32) * 2;
                    let src_idx = bus.read_word(req_slot) as i16;
                    let result_entry = result_ptr + 8 + (i as u32) * 8;

                    if src_idx < 0 || (src_idx as usize) > src_size {
                        // Out of range — mark this slot of selection as
                        // colReqErr and leave result[i] alone (undefined).
                        bus.write_word(req_slot, 0xFFFFu16);
                        continue;
                    }

                    let (value, r, g, b) = if src_ptr != 0 {
                        let src_entry = src_ptr + 8 + (src_idx as u32) * 8;
                        (
                            bus.read_word(src_entry),
                            bus.read_word(src_entry + 2),
                            bus.read_word(src_entry + 4),
                            bus.read_word(src_entry + 6),
                        )
                    } else {
                        // From device_clut: synthesize value=index, RGB
                        // from the live CLUT slot.
                        let [r, g, b] = self.device_clut[src_idx as usize];
                        (src_idx as u16, r, g, b)
                    };

                    bus.write_word(result_entry, value);
                    bus.write_word(result_entry + 2, r);
                    bus.write_word(result_entry + 4, g);
                    bus.write_word(result_entry + 6, b);
                }
                Ok(())
            }

            // RestoreEntries ($AA4A)
            // PROCEDURE RestoreEntries(srcTable: CTabHandle; dstTable: CTabHandle;
            //                          VAR selection: ReqListRec);
            // Inside Macintosh Volume V (1986), p. V-144
            // (Color Manager — Color Manager Routines — RestoreEntries)
            //
            // MPW Universal Headers Quickdraw.h:
            //   EXTERN_API(void) RestoreEntries(CTabHandle srcTable,
            //       CTabHandle dstTable, ReqListRec *selection)
            //       ONEWORDINLINE(0xAA4A);
            //
            // "RestoreEntries sets a selection of entries from srcTable into
            //  dstTable, but doesn't rebuild the inverse table. The dstTable
            //  entries to be set are enumerated in the selection parameter.
            //  ... If a request is beyond the end of the dstTable, that
            //  position of the requestList is set to colReqErr ... If
            //  dstTbl is NIL, or points to the device color table, the
            //  current device's color table is updated, and the hardware is
            //  updated to these new colors. The seed is not changed, so no
            //  invalidation occurs."
            //
            // Stack (Pascal PROCEDURE, pop-12): SP+0=selection(4),
            // SP+4=dstTable(4), SP+8=srcTable(4). No FUNCTION result slot.
            //
            // For i in 0..=reqLSize: copy src[i] into dst[selection.reqLData[i]].
            // Inverse of SaveEntries: SaveEntries packs scattered src into
            // packed result; RestoreEntries unpacks packed src into
            // scattered dst.
            //
            // Engines-agree subset: the Pascal PROCEDURE pop-12 calling
            // convention. Absolute Color Manager state mutation diverges
            // between engines (BII walks gDevice CLUT per IM:V V-137..V-138
            // and does NOT bump ctSeed; Systemless walks dispatcher-internal
            // device_clut and supplied CTab bytes). Witnessed PASS by the
            // aa49_aa4a_saveentries_restoreentries_strict catalog fixture
            // bands B3 + B4, and by
            // tests::saveentries_restoreentries_pascal_procedure_preserves_stack_across_five_calls.
            (true, 0x24A) => {
                let sp = cpu.read_reg(Register::A7);
                let selection_ptr = bus.read_long(sp);
                let dst_handle = bus.read_long(sp + 4);
                let src_handle = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);

                if selection_ptr == 0 || src_handle == 0 {
                    return Some(Ok(()));
                }
                let src_ptr = bus.read_long(src_handle);
                if src_ptr == 0 {
                    return Some(Ok(()));
                }

                let req_size = bus.read_word(selection_ptr) as i16;
                if req_size < 0 {
                    return Some(Ok(()));
                }
                let count = (req_size as usize) + 1;

                // Destination: NIL handle → write into device_clut directly.
                let dst_ptr = if dst_handle != 0 {
                    bus.read_long(dst_handle)
                } else {
                    0
                };
                let dst_size: usize = if dst_ptr != 0 {
                    bus.read_word(dst_ptr + 6) as usize
                } else {
                    255
                };
                let src_size = bus.read_word(src_ptr + 6) as usize;

                for i in 0..count {
                    if i > src_size {
                        // Source ran out of entries; nothing to copy.
                        // IM doesn't say to set colReqErr in this case.
                        break;
                    }
                    let req_slot = selection_ptr + 2 + (i as u32) * 2;
                    let dst_idx = bus.read_word(req_slot) as i16;

                    if dst_idx < 0 || (dst_idx as usize) > dst_size {
                        bus.write_word(req_slot, 0xFFFFu16); // colReqErr
                        continue;
                    }

                    let src_entry = src_ptr + 8 + (i as u32) * 8;
                    let value = bus.read_word(src_entry);
                    let r = bus.read_word(src_entry + 2);
                    let g = bus.read_word(src_entry + 4);
                    let b = bus.read_word(src_entry + 6);

                    if dst_ptr != 0 {
                        let dst_entry = dst_ptr + 8 + (dst_idx as u32) * 8;
                        bus.write_word(dst_entry, value);
                        bus.write_word(dst_entry + 2, r);
                        bus.write_word(dst_entry + 4, g);
                        bus.write_word(dst_entry + 6, b);
                    } else {
                        // NIL dstTable → write directly into device_clut.
                        // Per IM, no ctSeed bump (i.e. don't invalidate
                        // anything that depends on the inverse table).
                        self.device_clut[dst_idx as usize] = [r, g, b];
                    }
                }
                Ok(())
            }

            // DelSearch ($AA4C)
            // Removes a custom search procedure from the active gDevice's chain.
            // PROCEDURE DelSearch (searchProc: ProcPtr);
            // Inside Macintosh Volume V, V-147
            // Stack: SP+0: searchProc(4). Pop 4.
            // No-op stub: Systemless uses identity color matching and does not
            // maintain a per-device search-procedure chain. Pop-4 calling
            // convention proven by aa4c_aa4d_delsearch_delcomp_strict.
            (true, 0x24C) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // DelComp ($AA4D)
            // Removes a custom complement procedure from the active gDevice's chain.
            // PROCEDURE DelComp (compProc: ProcPtr);
            // Inside Macintosh Volume V, V-147
            // Stack: SP+0: compProc(4). Pop 4.
            // No-op stub: Systemless uses the default 1's-complement procedure
            // and does not maintain a per-device complement-procedure chain.
            // Pop-4 calling convention proven by
            // aa4c_aa4d_delsearch_delcomp_strict.
            (true, 0x24D) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // ========== Stubs — PixMap / PixPat ==========

            // DisposPixMap ($AA04)
            // Disposes of a PixMap record and its color table.
            // PROCEDURE DisposPixMap (pm: PixMapHandle);
            // Inside Macintosh Volume V, V-57
            //
            // "The DisposPixMap procedure releases all storage allocated
            //  by NewPixMap. It disposes of the pixMap's color table,
            //  and of the pixMap itself."
            //
            // PixMap layout: pmTable (CTabHandle) is at offset +42.
            // CloseCPort calls DisposePixMap.
            //
            // Stack: SP+0: pm_handle(4). Pop 4.
            //
            // Regression coverage:
            //   disposepixmap_frees_pixmap_and_color_table
            //   disposepixmap_nil_handle_is_harmless
            // DisposPixMap ($AA04): Frees PixMap record and its color table (pmTable at +42); per IM:V V-57
            (true, 0x204) => {
                let sp = cpu.read_reg(Register::A7);
                let pm_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if pm_handle != 0 {
                    let pm_ptr = bus.read_long(pm_handle);
                    if pm_ptr != 0 {
                        // Free the color table (pmTable at offset +42)
                        let ctab_handle = bus.read_long(pm_ptr + 42);
                        if ctab_handle != 0 {
                            let ctab_ptr = bus.read_long(ctab_handle);
                            if ctab_ptr != 0 {
                                bus.free(ctab_ptr);
                            }
                            bus.free(ctab_handle);
                        }
                        // Free the PixMap record itself
                        bus.free(pm_ptr);
                    }
                    // Clear the caller's master pointer so a second dispose
                    // attempt sees NIL instead of a stale freed handle cell.
                    bus.write_long(pm_handle, 0);
                    bus.free(pm_handle);
                }
                Ok(())
            }

            // CopyPixMap ($AA05)
            // PROCEDURE CopyPixMap(srcPM, dstPM: PixMapHandle);
            // Inside Macintosh Volume V, V-57
            //
            // "The CopyPixMap procedure copies the source PixMap
            //  structure to the destination PixMap structure. CopyPixMap
            //  does not copy the data referenced by the source pixel
            //  map's baseAddr field; it only copies the field itself,
            //  not the data it points to."
            //
            // PixMap is a 50-byte record (IM:V V-37): baseAddr(4),
            // rowBytes(2), bounds(8), pmVersion(2), packType(2),
            // packSize(4), hRes(4), vRes(4), pixelType(2), pixelSize(2),
            // cmpCount(2), cmpSize(2), planeBytes(4), pmTable(4),
            // pmReserved(4) = 50 bytes total.
            //
            // Stack: SP+0=dstPM(4), SP+4=srcPM(4). Pops 8.
            //
            // Regression coverage:
            //   copypixmap_copies_50_byte_pixmap_record_into_dst
            // CopyPixMap ($AA05): Copies 50-byte PixMap record from src to dst handle; per IM:V V-57
            (true, 0x205) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_handle = bus.read_long(sp);
                let src_handle = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                if src_handle != 0 && dst_handle != 0 {
                    let dst_ptr = bus.read_long(dst_handle);
                    let src_ptr = bus.read_long(src_handle);
                    if src_ptr != 0 && dst_ptr != 0 {
                        for i in 0..50u32 {
                            bus.write_byte(dst_ptr + i, bus.read_byte(src_ptr + i));
                        }
                    }
                }
                Ok(())
            }

            // NewPixPat ($AA07)
            // FUNCTION NewPixPat: PixPatHandle;
            // Inside Macintosh Volume V, V-72 (Color QuickDraw —
            //   Operations on Pixel Patterns — NewPixPat)
            //
            // MPW Universal Headers Quickdraw.h declares:
            //     EXTERN_API(PixPatHandle) NewPixPat(void)
            //                              ONEWORDINLINE(0xAA07);
            //
            // Tool-bit Pascal FUNCTION (bit 11 set) with no
            // arguments and a 4-byte PixPatHandle function result.
            // Caller pre-pushes a 4-byte result slot at SP+0; the
            // trap writes the PixPatHandle to [SP+0] without
            // modifying A7; the caller pops the slot after the
            // trap returns. Net A7 change across the C-level call
            // is zero.
            //
            // Per IM:V V-72 "The NewPixPat function returns a handle
            // to a new pixel pattern." A PixPat is a 28-byte record:
            //   +0  patType   (Integer)       — 1 = color
            //   +2  patMap    (PixMapHandle)
            //   +6  patData   (Handle)
            //  +10  patXData  (Handle)
            //  +14  patXValid (Integer)
            //  +16  patXMap   (Handle)
            //  +20  pat1Data  (Pattern, 8 bytes; documented to be
            //                  initialised to 50% gray)
            //
            // Apple-vs-BasiliskII engines-agree subset:
            //   (1) Pascal FUNCTION calling convention — A7
            //       unchanged across the C-level call sequence.
            //   (2) Non-NIL handle return — per IM:V V-72 the
            //       routine "returns a handle to a new pixel
            //       pattern"; both engines return a non-NIL handle
            //       on a fresh boot with adequate heap space. The
            //       absolute handle address differs between engines
            //       (BII heap address vs Systemless host allocator
            //       address) but each is valid on its own engine.
            //
            // Engines-divergent (not witnessed by the strict bake):
            //   The bytes inside the freshly-allocated PixPat
            //   record. BII Color QuickDraw writes patType=1,
            //   pat1Data=50% gray, and pre-allocates the embedded
            //   patMap / patData / patXData / patXMap handles per
            //   IM:V V-72. Systemless's implementation here allocates
            //   a 28-byte record with patType=1 and the documented
            //   50% gray pat1Data, but it still does NOT pre-
            //   allocate the embedded handles since the host
            //   runtime renders only to 1bpp canvases and has no
            //   need for the embedded color table / expanded data
            //   path. That remaining divergence is documented in
            //   the AA0D MakeRGBPat / AA0A PenPixPat / AA0B
            //   BackPixPat catalogue proofs.
            //
            // Catalogue proof:
            //   aa07_newpixpat_strict/
            //     band B1: A7 unchanged + handle != NIL across one
            //              NewPixPat() call.
            //     band B2: A7 unchanged + all 5 handles non-NIL
            //              across a 5-call NewPixPat() composition.
            //     band B3: PenPixPat(h1) copies the documented gray
            //              pat1Data into the current pen state.
            //
            // Regression coverage:
            //   newpixpat_writes_handle_pointing_at_full_pixpat_record
            //   src/trap/quickdraw.rs mod tests
            //     newpixpat_pascal_function_returns_nonnil_handle_and_preserves_stack_across_five_calls
            //     newpixpat_initializes_gray_pattern_and_penpixpat_copies_it
            (true, 0x207) => {
                let sp = cpu.read_reg(Register::A7);
                let rec = bus.alloc(28);
                if rec != 0 {
                    for i in 0..28u32 {
                        bus.write_byte(rec + i, 0);
                    }
                    bus.write_word(rec, 1); // patType = 1 (color)
                    bus.write_bytes(rec + 20, &[0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55]);
                }
                let handle = bus.alloc(4);
                bus.write_long(handle, rec);
                bus.write_long(sp, handle);
                Ok(())
            }

            // CopyPixPat ($AA09)
            // Copies the contents of one PixPat record to another.
            // PROCEDURE CopyPixPat(srcPP, dstPP: PixPatHandle);
            // Inside Macintosh Volume V (1986), p. V-73
            //
            // "The CopyPixPat procedure copies the contents of the
            //  source pixPat to the destination pixPat. It entirely
            //  copies all fields in the source pixPat, including the
            //  contents of the data handle, expanded data handle,
            //  expanded map, pixMap handle, and color table."
            //
            // MPW Universal Headers Quickdraw.h:
            //     EXTERN_API(void) CopyPixPat(PixPatHandle srcPP,
            //                                 PixPatHandle dstPP)
            //                                 ONEWORDINLINE(0xAA09);
            //
            // Tool-bit Pascal PROCEDURE ABI (engines-agree calling
            // convention pinned by aa08_aa09_dispose_copy_pixpat_strict):
            //   Stack on entry:  [SP+0] = dstPP (4 bytes, second arg per
            //                              Pascal calling convention);
            //                    [SP+4] = srcPP (4 bytes, first arg).
            //   Stack on exit:   trap pops 8 bytes; A7 net-balanced
            //                    across the call; no FUNCTION result
            //                    slot is written.
            //
            // Documented behaviour: copies the 28-byte PixPat record from
            // *srcPP to *dstPP plus all nested owned handle contents.
            // Both src/dst handle dereferences are guarded behind non-NIL
            // checks.
            //
            // Engines-divergent absolute behavior: BII System 7.5.3 ROM
            // Color QuickDraw walks the IM:V V-73 deep-copy chain
            // (record + patData + patXData + patMap + color table).
            // Systemless HLE here copies only the 28-byte PixPat record
            // verbatim from src to dst because its NewPixPat-allocated
            // records have no embedded handles to chase. The post-copy
            // record contents diverge between engines (BII honours the
            // documented full deep copy; Systemless matches its shallower
            // NewPixPat record layout). The bakeable engines-agree
            // subset is the Pascal PROCEDURE pop-8 calling convention
            // itself (witnessed by the aa08_aa09_dispose_copy_pixpat_strict
            // fixture bands B3 single-call + B4 5-call composition with
            // NewPixPat-allocated handles).
            //
            // ## TRAP-WORD SWAP FIX (historical)
            // This arm previously matched `(true, 0x208)` and was labeled
            // "$AA08 CopyPixPat" — but per IM:V V-291 master trap dispatch
            // table line 20693, $AA09 is CopyPixPat. See the swap-fix
            // rationale block at the DisposPixPat ($AA08) arm above for
            // the full details — both arms were swapped.
            //
            // Regression coverage:
            //   copypixpat_copies_28_byte_pixpat_record_into_dst
            //   aa08_aa09_dispose_copy_pixpat_strict
            (true, 0x209) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_handle = bus.read_long(sp);
                let src_handle = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                if src_handle != 0 && dst_handle != 0 {
                    let src_ptr = bus.read_long(src_handle);
                    let dst_ptr = bus.read_long(dst_handle);
                    if src_ptr != 0 && dst_ptr != 0 {
                        for i in 0..28u32 {
                            bus.write_byte(dst_ptr + i, bus.read_byte(src_ptr + i));
                        }
                    }
                }
                Ok(())
            }

            // ========== Stubs — Picture ==========

            // OpenCPicture ($AA20)
            // Begins recording a new color picture. Stub returns NIL PicHandle.
            // FUNCTION OpenCPicture(newHeader: OpenCPicParams): PicHandle;
            // Inside Macintosh Volume V, V-89
            // Stack: SP+0: header_ptr(4). Return PicHandle at SP+4. Pop to SP+4.
            // OpenCPicture ($AA20): Returns NIL PicHandle (color-PICT recording not supported) per IM:V V-89
            (true, 0x220) => {
                let sp = cpu.read_reg(Register::A7);
                bus.write_long(sp + 4, 0);
                cpu.write_reg(Register::D0, 0);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // ========== Stubs — Mask / Fill ==========

            // CalcCMask ($AA4F)
            // Builds a mask bitmap from a seed fill in a color source. No-op stub.
            // PROCEDURE CalcCMask(srcBits, dstBits: BitMap; srcRect, maskRect: Rect;
            //                     seedRGB: RGBColor; matchProc: ProcPtr;
            //                     matchData: LongInt);
            // Imaging With QuickDraw, 3-100
            // Stack: SP+0:matchData(4), SP+4:matchProc(4), SP+8:seedRGB(4),
            //        SP+12:maskRect(4), SP+16:srcRect(4), SP+20:dstBits(4),
            //        SP+24:srcBits(4). Pop 28.
            // CalcCMask ($AA4F): Pops 28 bytes; color seed-fill mask not synthesized per Imaging With QuickDraw 3-100
            (true, 0x24F) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 28);
                Ok(())
            }

            // SeedCFill ($AA50)
            // Performs a color-seeded flood fill into dstBits. No-op stub.
            // PROCEDURE SeedCFill(srcBits, dstBits: BitMap; srcRect, dstRect: Rect;
            //                     seedH, seedV: INTEGER; matchProc: ProcPtr;
            //                     matchData: LongInt);
            // Imaging With QuickDraw, 3-101
            // Stack: SP+0:matchData(4), SP+4:matchProc(4), SP+8:seedV(2),
            //        SP+10:seedH(2), SP+12:dstRect(4), SP+16:srcRect(4),
            //        SP+20:dstBits(4), SP+24:srcBits(4). Pop 28.
            // SeedCFill ($AA50): Pops 28 bytes; color flood-fill not implemented per Imaging With QuickDraw 3-101
            (true, 0x250) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 28);
                Ok(())
            }

            // ========== Stubs — GDevice ==========

            // InitGDevice ($AA2E)
            // Initializes a GDevice record to a given display mode.
            // PROCEDURE InitGDevice(qdRefNum: INTEGER; mode: LongInt; gdh: GDHandle);
            // Imaging With QuickDraw (1994), pp. 5-21 to 5-22.
            // Stack: SP+0: gdh(4), SP+4: mode(4), SP+8: qdRefNum(2). Pop 10.
            //
            // Contract coverage:
            //   quickdraw::tests::initgdevice_consumes_gdrefnum_mode_and_gdhandle_arguments
            //   quickdraw::tests::initgdevice_writes_gdrefnum_and_mode_fields_into_target_record
            //
            // HLE compromise: without a real video driver, Systemless cannot fill
            // out mode-specific ROM-backed device state. We therefore model the
            // caller-observable subset BasiliskII exposes cleanly to apps that
            // inspect the detached record after InitGDevice: gdRefNum, gdMode,
            // and the gdDevType bit for black-and-white vs color mode.
            // InitGDevice ($AA2E): Pops 10 bytes and writes gdRefNum/gdMode into the target GDevice; updates gdDevType bit 0 for mode 128 (B/W) vs non-128 color modes per Imaging With QuickDraw 1994, 5-21..5-22
            (true, 0x22E) => {
                let sp = cpu.read_reg(Register::A7);
                let gdh = bus.read_long(sp);
                let mode = bus.read_long(sp + 4);
                let gd_ref_num = bus.read_word(sp + 8) as i16;
                if mode != 0xFFFF_FFFF {
                    self.init_gdevice_minimal(bus, gdh, gd_ref_num, mode);
                }
                cpu.write_reg(Register::A7, sp + 10);
                Ok(())
            }

            // DeviceLoop ($ABCA)
            // PROCEDURE DeviceLoop(drawingRgn: RgnHandle;
            //                      drawingProc: DeviceLoopDrawingUPP;
            //                      userData: LongInt; flags: DeviceLoopFlags);
            // Inside Macintosh: Imaging With QuickDraw 1994, pp. 5-29 to 5-30.
            // DeviceLoopFlags includes allDevices; when that flag is set,
            // DeviceLoop ignores drawingRgn and visits every device in the
            // current graphics-device chain.
            // MPW Universal Headers Quickdraw.h declares the drawing proc as:
            //   pascal void (*DeviceLoopDrawingProcPtr)
            //       (short depth, short deviceFlags, GDHandle targetDevice,
            //        long userData);
            //
            // Stack: SP+0 flags(4), SP+4 userData(4), SP+8 drawingProc(4),
            //        SP+12 drawingRgn(4). Pop 16.
            //
            // HLE compromise: Systemless honors the current GDevice, and
            // when that device is the main screen it walks the linked
            // gdNextGD chain to find the first device intersecting the
            // drawing region. Offscreen current GWorlds still use the
            // current device directly.
            (true, 0x3CA) => {
                let sp = cpu.read_reg(Register::A7);
                let flags = bus.read_long(sp);
                let user_data = bus.read_long(sp + 4);
                let drawing_proc = bus.read_long(sp + 8);
                let drawing_rgn = bus.read_long(sp + 12);
                let main_gdevice = self.ensure_main_gdevice(bus);
                let current_gdevice = if self.current_gdevice != 0 {
                    self.current_gdevice
                } else {
                    main_gdevice
                };
                let all_devices = (flags & (1 << 2)) != 0;
                let drawing_bbox = if all_devices {
                    None
                } else {
                    Self::region_bbox(bus, drawing_rgn)
                };
                let intersects_rect = |rect: (i16, i16, i16, i16)| {
                    drawing_bbox.is_some_and(|(top, left, bottom, right)| {
                        top < rect.2 && left < rect.3 && bottom > rect.0 && right > rect.1
                    })
                };
                let target_devices = if all_devices {
                    let mut targets = Vec::new();
                    let mut scan_gdevice = current_gdevice;

                    while scan_gdevice != 0 {
                        let gd_ptr = bus.read_long(scan_gdevice);
                        if gd_ptr == 0 {
                            break;
                        }
                        targets.push((scan_gdevice, gd_ptr));
                        scan_gdevice = bus.read_long(gd_ptr + 30);
                    }

                    targets
                } else if current_gdevice == main_gdevice {
                    let mut scan_gdevice = main_gdevice;
                    let mut targets = Vec::new();

                    while scan_gdevice != 0 {
                        let gd_ptr = bus.read_long(scan_gdevice);
                        if gd_ptr == 0 {
                            break;
                        }
                        let rect = (
                            bus.read_word(gd_ptr + 34) as i16,
                            bus.read_word(gd_ptr + 36) as i16,
                            bus.read_word(gd_ptr + 38) as i16,
                            bus.read_word(gd_ptr + 40) as i16,
                        );
                        if intersects_rect(rect) {
                            targets.push((scan_gdevice, gd_ptr));
                            break;
                        }
                        scan_gdevice = bus.read_long(gd_ptr + 30);
                    }

                    targets
                } else {
                    let gd_ptr = bus.read_long(current_gdevice);
                    let rect = if gd_ptr != 0 {
                        (
                            bus.read_word(gd_ptr + 34) as i16,
                            bus.read_word(gd_ptr + 36) as i16,
                            bus.read_word(gd_ptr + 38) as i16,
                            bus.read_word(gd_ptr + 40) as i16,
                        )
                    } else {
                        (0, 0, 0, 0)
                    };
                    if gd_ptr != 0 && intersects_rect(rect) {
                        vec![(current_gdevice, gd_ptr)]
                    } else {
                        Vec::new()
                    }
                };

                if drawing_proc != 0 && !target_devices.is_empty() {
                    let entry = bus.read_word(drawing_proc);
                    if entry == 0x4E56 || entry == 0x48E7 || entry == 0x4EF9 || entry == 0x4EFA {
                        let return_slot = sp.wrapping_add(12);
                        let return_pc = cpu.read_reg(Register::PC);

                        if target_devices.len() == 1 {
                            let (target_gdevice, target_gd_ptr) = target_devices[0];
                            let depth = Self::gdevice_pixel_size(bus, target_gdevice).unwrap_or(8);
                            let device_flags = bus.read_word(target_gd_ptr + 20);
                            let tramp = self.get_or_create_device_loop_trampoline(bus);
                            bus.write_word(tramp + 6, depth as u16);
                            bus.write_word(tramp + 10, device_flags);
                            bus.write_long(tramp + 14, target_gdevice);
                            bus.write_long(tramp + 20, user_data);
                            bus.write_long(tramp + 26, drawing_proc);
                            bus.write_long(tramp + 32, return_slot.wrapping_sub(32));
                            bus.write_long(return_slot, return_pc);
                            cpu.write_reg(Register::A7, return_slot);
                            cpu.write_reg(Register::PC, tramp);
                            return Some(Ok(()));
                        }

                        let trampolines: Vec<u32> =
                            (0..target_devices.len()).map(|_| bus.alloc(46)).collect();
                        for (idx, ((target_gdevice, target_gd_ptr), tramp)) in
                            target_devices.iter().zip(trampolines.iter()).enumerate()
                        {
                            let depth = Self::gdevice_pixel_size(bus, *target_gdevice).unwrap_or(8);
                            let device_flags = bus.read_word(*target_gd_ptr + 20);
                            let next_trampoline = trampolines.get(idx + 1).copied();
                            Self::write_device_loop_trampoline(
                                bus,
                                *tramp,
                                depth as u16,
                                device_flags,
                                *target_gdevice,
                                user_data,
                                drawing_proc,
                                return_slot,
                                next_trampoline,
                            );
                        }
                        bus.write_long(return_slot, return_pc);
                        cpu.write_reg(Register::A7, return_slot);
                        cpu.write_reg(Register::PC, trampolines[0]);
                        return Some(Ok(()));
                    }
                }

                cpu.write_reg(Register::A7, sp + 16);
                Ok(())
            }

            // ========== Palette / Control / Desktop ==========

            // CopyPalette ($AAA1)
            // Copies dstLength entries (RGB + usage + tolerance) from
            // srcPalette[srcEntry] into dstPalette[dstEntry]. Per IM:VI
            // 20-23, NIL handles make the call a no-op, and CopyPalette
            // does NOT call ActivatePalette (callers are expected to
            // batch palette edits and activate once).
            //
            // MPW Universal Headers `Palettes.h` declaration:
            //   EXTERN_API(void) CopyPalette(PaletteHandle srcPalette,
            //       PaletteHandle dstPalette, short srcEntry,
            //       short dstEntry, short dstLength) ONEWORDINLINE(0xAAA1);
            // PROCEDURE CopyPalette(srcPalette, dstPalette: PaletteHandle;
            //                       srcEntry, dstEntry, dstLength: INTEGER);
            // Inside Macintosh Volume VI, 20-23
            // Stack: SP+0: dstLength(2), SP+2: dstEntry(2), SP+4: srcEntry(2),
            //        SP+6: dstPalette(4), SP+10: srcPalette(4). Pop 14.
            //
            // Regression coverage:
            //   src/trap/quickdraw.rs::tests::copypalette_pops_fourteen_bytes
            //   src/trap/quickdraw.rs::tests::copypalette_resizes_destination_when_copy_extends_past_tail
            //   src/trap/quickdraw.rs::tests::copypalette_copies_requested_range_into_resized_tail
            //   src/trap/quickdraw.rs::tests::copypalette_nil_source_does_not_mutate_destination
            // CopyPalette ($AAA1): Copies dstLength ColorInfo records (RGB + usage + tolerance) from src[srcEntry] to dst[dstEntry]; NIL handles no-op; destination palette grows when dstEntry + dstLength exceeds its prior size, IM:VI 20-23
            (true, 0x2A1) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_length = bus.read_word(sp) as i16;
                let dst_entry = bus.read_word(sp + 2) as i16;
                let src_entry = bus.read_word(sp + 4) as i16;
                let dst_palette = bus.read_long(sp + 6);
                let src_palette = bus.read_long(sp + 10);
                if trace_palette_enabled() {
                    eprintln!(
                        "[PALETTE] CopyPalette srcPalette=${:08X} dstPalette=${:08X} srcEntry={} dstEntry={} dstLength={}",
                        src_palette, dst_palette, src_entry, dst_entry, dst_length
                    );
                }
                cpu.write_reg(Register::A7, sp + 14);

                if src_palette == 0
                    || dst_palette == 0
                    || dst_length <= 0
                    || src_entry < 0
                    || dst_entry < 0
                {
                    return Some(Ok(()));
                }
                let required_entries = u32::from(dst_entry as u16) + u32::from(dst_length as u16);
                if required_entries > u32::from(Self::palette_entry_count(bus, dst_palette))
                    && required_entries <= i16::MAX as u32
                {
                    self.resize_palette(bus, dst_palette, required_entries as i16);
                }
                let dst_palette_ptr = Self::palette_ptr(bus, dst_palette);
                if dst_palette_ptr == 0 {
                    return Some(Ok(()));
                }
                let src_count = i32::from(Self::palette_entry_count(bus, src_palette));
                let dst_count = i32::from(Self::palette_entry_count(bus, dst_palette));
                let length = i32::from(dst_length);
                for offset in 0..length {
                    let s = i32::from(src_entry) + offset;
                    let d = i32::from(dst_entry) + offset;
                    if s >= src_count || d >= dst_count {
                        break;
                    }
                    if let Some((rgb, usage, tolerance)) =
                        Self::read_palette_color_info(bus, src_palette, s as u16)
                    {
                        Self::write_palette_color_info(
                            bus,
                            dst_palette_ptr,
                            d as u32,
                            rgb,
                            usage,
                            tolerance,
                        );
                    }
                }
                Ok(())
            }

            // GetAuxCtl ($AA44)
            // FUNCTION GetAuxCtl(theControl: ControlHandle;
            //                    VAR acHndl: AuxCtlHandle): BOOLEAN;
            // Macintosh Toolbox Essentials (1992), p. 5-107.
            // Stack: SP+0: acHndl_ptr(4), SP+4: theControl(4). Return BOOL at SP+8.
            // GetAuxCtl ($AA44): BasiliskII/System 7.5.3 reports TRUE with a
            // non-NIL AuxCtlHandle for freshly created controls. Systemless
            // mirrors that caller-observable contract for HLE-created
            // controls, while still returning NIL/FALSE for untracked or
            // already-disposed handles.
            (true, 0x244) => {
                let sp = cpu.read_reg(Register::A7);
                let ctrl_handle = bus.read_long(sp + 4);
                let aux_ptr = bus.read_long(sp);
                let aux_handle = self
                    .control_aux_state(ctrl_handle)
                    .map(|state| state.handle)
                    .unwrap_or(0);
                if aux_ptr != 0 {
                    bus.write_long(aux_ptr, aux_handle);
                }
                bus.write_word(sp + 8, if aux_handle != 0 { 0x0100 } else { 0 });
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // SetDeskCPat ($AA47)
            // Sets the desktop pixel pattern and redraws the desktop in the
            // colour Window Manager port (WMgrCPort). If deskPixPat is NIL,
            // the standard binary deskPat ('ppat' resource = 16) is used.
            // PROCEDURE SetDeskCPat(deskPixPat: PixPatHandle); [Macintosh II]
            // Inside Macintosh Volume V (1986), p. V-210
            //
            // MPW Universal Headers MacWindows.h:
            //     EXTERN_API(void) SetDeskCPat(PixPatHandle deskPixPat)
            //                                       ONEWORDINLINE(0xAA47);
            //
            // Tool-bit Pascal PROCEDURE ABI: caller pushes the 4-byte
            // PixPatHandle argument, trap pops it, no FUNCTION result slot.
            // Stack: SP+0: deskPixPat(4). Pop 4.
            //
            // Engines-divergent absolute side effect: BII System 7.5.3 ROM
            // honours the documented contract and redraws the desktop pixel
            // pattern in the WMgrCPort. Systemless runs as a kiosk and never
            // renders desktop chrome, so this arm is a true no-op (pops 4
            // bytes and returns). IM:V V-210 itself notes "This routine is
            // not for use by applications, and its description is only
            // included for informational purposes" — applications are not
            // expected to depend on the visible desktop redraw.
            //
            // Engines-agree subset (witnessed by aa47_setdeskcpat_strict):
            //   AA47:setdeskcpat_pops_pixpathandle_argument — A7 unchanged
            //     across a single SetDeskCPat(NIL) call wrapped in one
            //     StackSpace sandwich.
            //   AA47:setdeskcpat_pascal_procedure_preserves_stack_across_five_calls
            //     — A7 unchanged across a 5-call SetDeskCPat(NIL) composition
            //     wrapped in one StackSpace sandwich (5 missed 4-byte pops
            //     would cumulate to 20 bytes A7 drift).
            (true, 0x247) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // ========== Math / Trig ==========

            // PtToAngle ($A8C3)
            // PROCEDURE PtToAngle(r: Rect; pt: Point; VAR angle: INTEGER);
            // Inside Macintosh Volume I, I-194
            //
            // "PtToAngle calculates an integer angle between a line from
            //  the center of the rectangle to the given point and a line
            //  from the center of the rectangle pointing straight up.
            //  The angle is in degrees from 0 to 359, measured clockwise
            //  from 12 o'clock ... If the line to the given point goes
            //  through the top right corner of the rectangle, the angle
            //  returned is 45 degrees, even if the rectangle isn't square."
            //
            // The angle is normalized by the rectangle's aspect ratio so
            // corners always land at 45/135/225/315°. Implementation:
            // translate the point relative to the rect's center, scale
            // dx by the half-width and dy by the half-height to map the
            // rect onto a unit square, then atan2 to get the angle from
            // up (clockwise).
            //
            // Stack (Pascal): SP+0=angle_ptr(4), SP+4=pt(4 by value),
            // SP+8=r_ptr(4). Pops 12. The 8-byte Rect exceeds MPW
            // pascal direct-push threshold and is passed by const-
            // pointer; the 4-byte Point fits and is passed by value.
            //
            // Regression coverage:
            //   pttoangle_compass_midpoints_of_rectangle
            //   pttoangle_corners_are_at_45_degree_increments_for_non_square_rect
            //   pttoangle_center_is_undefined_but_safe
            //   pt_to_angle  (1bpp 480x80 BasiliskII golden;
            //     5 indicator bands witness the four cardinal-direction
            //     compass angles 0/90/180/270° plus a composite all-four-
            //     match band per IM:I-175)
            // PtToAngle ($A8C3): Returns clockwise-from-up degrees (0-359) of the point relative to rect's center; aspect normalized so rect corners are 45/135/225/315° per IM:I I-194
            (true, 0x0C3) => {
                let sp = cpu.read_reg(Register::A7);
                let angle_ptr = bus.read_long(sp);
                let pt_v = bus.read_word(sp + 4) as i16 as f64;
                let pt_h = bus.read_word(sp + 6) as i16 as f64;
                let r_ptr = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);
                if r_ptr != 0 && angle_ptr != 0 {
                    let top = bus.read_word(r_ptr) as i16 as f64;
                    let left = bus.read_word(r_ptr + 2) as i16 as f64;
                    let bottom = bus.read_word(r_ptr + 4) as i16 as f64;
                    let right = bus.read_word(r_ptr + 6) as i16 as f64;
                    let cx = (left + right) / 2.0;
                    let cy = (top + bottom) / 2.0;
                    let half_w = (right - left) / 2.0;
                    let half_h = (bottom - top) / 2.0;
                    // Map the point into a unit square so corners land
                    // at (±1, ±1) regardless of aspect ratio.
                    let dx_norm = if half_w > 0.0 {
                        (pt_h - cx) / half_w
                    } else {
                        0.0
                    };
                    let dy_norm = if half_h > 0.0 {
                        (pt_v - cy) / half_h
                    } else {
                        0.0
                    };
                    // atan2 measured clockwise from up (the +y axis is
                    // down on the Mac, so use -dy as the "up" component).
                    let angle_rad = dx_norm.atan2(-dy_norm);
                    let mut angle_deg = angle_rad.to_degrees().round() as i32;
                    angle_deg = angle_deg.rem_euclid(360);
                    bus.write_word(angle_ptr, angle_deg as u16);
                }
                Ok(())
            }

            // SlopeFromAngle ($A8BC)
            // FUNCTION SlopeFromAngle(angle: INTEGER): Fixed;
            // Inside Macintosh Volume I, I-192
            //
            // "Returns the slope dh/dv of a line forming the given angle
            //  with the y-axis. Angles are in degrees clockwise from
            //  12 o'clock; positive y is down, positive x is right. The
            //  angle is treated MOD 180. SlopeFromAngle(0) is 0; per IM
            //  Figure 5, SlopeFromAngle(45) = $FFFF0000 (= -1.0)."
            //
            // Mathematically: slope = -tan(angle in radians).
            //
            // Stack: SP+0=angle(2). Returns Fixed(4) at SP+2. Pops to SP+2.
            //
            // Regression coverage:
            //   slopefromangle_zero_is_zero
            //   slopefromangle_45_is_negative_one_fixed
            //   slopefromangle_135_is_positive_one_fixed
            //   slopefromangle_treats_angle_mod_180
            //   slopefromangle_30_matches_neg_tan
            // SlopeFromAngle ($A8BC): Returns Fixed slope = -tan(angle MOD 180); 0°→0, 45°→-1.0 ($FFFF0000), 90°→saturates to $7FFFFFFF per IM:I I-192
            (true, 0x0BC) => {
                let sp = cpu.read_reg(Register::A7);
                let angle_in = bus.read_word(sp) as i16 as i32;
                // Reduce to 0..180.
                let mut angle = angle_in % 180;
                if angle < 0 {
                    angle += 180;
                }
                let slope_fixed: i32 = if angle == 0 {
                    0
                } else if angle == 90 {
                    0x7FFF_FFFF // saturate to max positive Fixed
                } else {
                    let radians = (angle as f64) * std::f64::consts::PI / 180.0;
                    let slope = -radians.tan();
                    let scaled = (slope * 65536.0).round();
                    if scaled >= i32::MAX as f64 {
                        i32::MAX
                    } else if scaled <= i32::MIN as f64 {
                        i32::MIN
                    } else {
                        scaled as i32
                    }
                };
                bus.write_long(sp + 2, slope_fixed as u32);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // AngleFromSlope ($A8C4)
            // FUNCTION AngleFromSlope(slope: Fixed): INTEGER;
            // Inside Macintosh Volume I, I-192
            //
            // "Inverse of SlopeFromAngle. Returns an angle in 1..180
            //  (inclusive) that produces the given slope. Per IM:I I-192,
            //  AngleFromSlope(0) = 180, and the round trip
            //  AngleFromSlope(SlopeFromAngle(x)) = x is guaranteed
            //  within one degree."
            //
            // angle = (180 - atan(slope) * 180/π) MOD 180, then map 0 → 180
            //
            // Stack: SP+0=slope(4). Returns INTEGER(2) at SP+4. Pops to SP+4.
            //
            // Regression coverage:
            //   anglefromslope_zero_returns_180
            //   anglefromslope_negative_one_returns_45
            //   anglefromslope_positive_one_returns_135
            //   slopefromangle_anglefromslope_round_trip
            // AngleFromSlope ($A8C4): Inverse of SlopeFromAngle; returns angle in 1..180 (0 maps to 180); round-trip stable within 1° per IM:I I-192
            (true, 0x0C4) => {
                let sp = cpu.read_reg(Register::A7);
                let slope_fixed = bus.read_long(sp) as i32;
                let slope = (slope_fixed as f64) / 65536.0;
                let angle_deg = 180.0 - slope.atan().to_degrees();
                let mut angle = angle_deg.round() as i32 % 180;
                if angle <= 0 {
                    angle += 180;
                }
                bus.write_word(sp + 4, angle as u16);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // ========== Color Fill ==========
            //
            // Systemless does not implement true pixel-pattern tiling yet, so
            // the FillC* routines draw the monochrome fallback pattern
            // stored in the PixPat's pat1Data field (+20). This matches
            // the behavior documented in Imaging With QuickDraw p. 4-81:
            // "If the bit depth of the destination is one, or if the
            //  PixPat's pixMap field is NIL, QuickDraw uses pat1Data as a
            //  1-bit fallback pattern." The monochrome path shares the
            //  existing draw_rect / draw_oval / draw_round_rect /
            //  draw_arc / draw_rgn / draw_poly + ShapeOp::Fill helpers
            //  used by the B&W Fill* variants.

            // FillCRect ($AA0E)
            //
            // PROCEDURE FillCRect(r: Rect; pp: PixPatHandle);
            //
            // Inside Macintosh Volume V (1986), p. V-73 (Color QuickDraw —
            // Color Drawing Routines — FillCRect). Per IM:V V-73 verbatim
            // contract: fills the interior of the rectangle r with the
            // pixel pattern referenced by pp. Imaging With QuickDraw (1994)
            // p. 4-81 documents the same Color QuickDraw expansion path.
            //
            // MPW Universal Headers Quickdraw.h declares:
            //   EXTERN_API(void) FillCRect(const Rect *r, PixPatHandle pp)
            //       ONEWORDINLINE(0xAA0E);
            //
            // Tool-bit Pascal PROCEDURE ABI:
            //   Caller pushes 8 bytes (4-byte Rect pointer at SP+0 +
            //   4-byte PixPatHandle at SP+4), trap pops 8 bytes, no
            //   FUNCTION result slot. A7 net-balanced across the call.
            //
            // Engines-agree subset: the documented Tool-bit Pascal
            // PROCEDURE pop-8 calling convention. Absolute pixel-fill
            // side effects are engines-divergent: BasiliskII System
            // 7.5.3 ROM Color QuickDraw expands the full PixPat record
            // per IM:V V-46 (patType / patMap / patData / patXData /
            // patXMap deep handle chain) and tiles the rectangle with
            // the expanded color pattern. Systemless HLE reads the
            // pat1Data field at offset +20 from the PixPat record and
            // uses that 8-byte monochrome fallback pattern to fill via
            // draw_rect(ShapeOp::Fill(pat)) — the host runtime renders
            // only to 1bpp canvases and has no Color QuickDraw
            // expansion pipeline.
            //
            // Catalogue proof: aa0e_fillcrect_strict
            // (BasiliskII System 7.5.3 ROM Color QuickDraw bake; uses
            // NewPixPat()-allocated PixPatHandles because NIL handles
            // are not safe on this ROM path) + contract test
            // fillcrect_pascal_procedure_preserves_stack_across_five_calls.
            (true, 0x20E) => {
                let sp = cpu.read_reg(Register::A7);
                let pp_handle = bus.read_long(sp);
                let rect_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                let r = read_rect(bus, rect_ptr);
                let raw_filled = self.fill_rect_with_raw_pixpat(bus, &r, pp_handle);
                if trace_qd_colors_enabled() {
                    let pp_ptr = if pp_handle != 0 {
                        bus.read_long(pp_handle)
                    } else {
                        0
                    };
                    let pat_type = if pp_ptr != 0 {
                        bus.read_word(pp_ptr)
                    } else {
                        0
                    };
                    let pat_map = if pp_ptr != 0 {
                        bus.read_long(pp_ptr + 2)
                    } else {
                        0
                    };
                    let pat_data = if pp_ptr != 0 {
                        bus.read_long(pp_ptr + 6)
                    } else {
                        0
                    };
                    let mut pat1 = [0u8; 8];
                    if pp_ptr != 0 {
                        for (i, byte) in pat1.iter_mut().enumerate() {
                            *byte = bus.read_byte(pp_ptr + 20 + i as u32);
                        }
                    }
                    eprintln!(
                        "[QD-COLOR] FillCRect port=${:08X} rect=({},{},{},{}) pp=${:08X}->${:08X} patType={} patMap=${:08X} patData=${:08X} pat1={:02X?} raw={}",
                        self.current_port,
                        r.top,
                        r.left,
                        r.bottom,
                        r.right,
                        pp_handle,
                        pp_ptr,
                        pat_type,
                        pat_map,
                        pat_data,
                        pat1,
                        raw_filled,
                    );
                }
                if !raw_filled {
                    if let Some(pat) = Self::read_pixpat_pat1data(bus, pp_handle) {
                        self.draw_rect(cpu, bus, &r, ShapeOp::Fill(pat));
                    }
                }
                Ok(())
            }

            // FillCOval ($AA0F)
            //
            // PROCEDURE FillCOval(r: Rect; pp: PixPatHandle);
            //
            // Inside Macintosh Volume V (1986), pp. V-67..V-69 (Color
            // QuickDraw — Color Drawing Operations — FillCOval). See
            // also Imaging With QuickDraw (1994) p. 4-75.
            //
            // MPW Universal Headers Quickdraw.h declares:
            //   EXTERN_API(void) FillCOval(const Rect *r, PixPatHandle pp)
            //       ONEWORDINLINE(0xAA0F);
            //
            // Tool-bit Pascal PROCEDURE ABI:
            //   Caller pushes 8 bytes (4-byte Rect pointer at SP+0 +
            //   4-byte PixPatHandle at SP+4), trap pops 8 bytes, no
            //   FUNCTION result slot. A7 net-balanced across the call.
            //
            // Engines-agree subset: the documented Tool-bit Pascal
            // PROCEDURE pop-8 calling convention. Absolute pixel-fill
            // side effects are engines-divergent: BasiliskII System
            // 7.5.3 ROM Color QuickDraw expands the full PixPat record
            // per IM:V V-46 and tiles the oval with the expanded color
            // pattern. Systemless HLE reads pat1Data at offset +20 and
            // uses that 8-byte monochrome fallback to fill via
            // draw_oval(ShapeOp::Fill(pat)).
            //
            // Catalogue proof:
            //   aa0f_aa12_fillcoval_fillcrgn_strict
            //   (BasiliskII System 7.5.3 ROM Color QuickDraw sibling-
            //   pair bake; uses NewPixPat()-allocated PixPatHandles
            //   because NIL handles are not safe on this ROM path) +
            //   contract test
            //   fillcoval_fillcrgn_pascal_procedure_preserves_stack_across_five_calls.
            (true, 0x20F) => {
                let sp = cpu.read_reg(Register::A7);
                let pp_handle = bus.read_long(sp);
                let rect_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                if let Some(pat) = Self::read_pixpat_pat1data(bus, pp_handle) {
                    let r = read_rect(bus, rect_ptr);
                    self.draw_oval(cpu, bus, &r, ShapeOp::Fill(pat));
                }
                Ok(())
            }

            // FillCRoundRect ($AA10)
            //
            // PROCEDURE FillCRoundRect(r: Rect; ovWid, ovHt: INTEGER;
            //                          pp: PixPatHandle);
            //
            // Inside Macintosh Volume V (1986), pp. V-67..V-69 (Color
            // QuickDraw — Color Drawing Operations — FillCRoundRect).
            //
            // MPW Universal Headers Quickdraw.h declares:
            //   EXTERN_API(void) FillCRoundRect(const Rect *r,
            //       short ovalWidth, short ovalHeight, PixPatHandle pp)
            //       ONEWORDINLINE(0xAA10);
            //
            // Tool-bit Pascal PROCEDURE ABI:
            //   Caller pushes 12 bytes (4-byte PixPatHandle at SP+0,
            //   2-byte ovHt at SP+4, 2-byte ovWid at SP+6, 4-byte Rect
            //   pointer at SP+8 — Pascal pushes args left-to-right so
            //   the Rect pointer is pushed first), trap pops 12 bytes,
            //   no FUNCTION result slot. A7 net-balanced across the call.
            //
            // Engines-agree subset: the documented Tool-bit Pascal
            // PROCEDURE pop-12 calling convention. Absolute pixel-fill
            // side effects are engines-divergent: BasiliskII System
            // 7.5.3 ROM Color QuickDraw expands the full PixPat record
            // per IM:V V-46 and tiles the rounded rectangle interior
            // with the expanded color pattern. Systemless HLE reads
            // pat1Data at offset +20 and uses that 8-byte monochrome
            // fallback to fill via draw_round_rect(ShapeOp::Fill(pat)).
            //
            // Catalogue proof:
            //   aa10_aa11_fillcroundrect_fillcarc_strict
            //   (BasiliskII sibling-pair bake; uses NewPixPat-allocated
            //   PixPatHandles and stack-allocated Rect records) + contract
            //   test fillcroundrect_fillcarc_pascal_procedure_preserves_stack_across_five_calls.
            (true, 0x210) => {
                let sp = cpu.read_reg(Register::A7);
                let pp_handle = bus.read_long(sp);
                let oval_height = bus.read_word(sp + 4) as i16;
                let oval_width = bus.read_word(sp + 6) as i16;
                let rect_ptr = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);
                if let Some(pat) = Self::read_pixpat_pat1data(bus, pp_handle) {
                    let r = read_rect(bus, rect_ptr);
                    self.draw_round_rect(cpu, bus, &r, oval_width, oval_height, ShapeOp::Fill(pat));
                }
                Ok(())
            }

            // FillCArc ($AA11)
            //
            // PROCEDURE FillCArc(r: Rect; startAngle, arcAngle: INTEGER;
            //                    pp: PixPatHandle);
            //
            // Inside Macintosh Volume V (1986), pp. V-67..V-69 (Color
            // QuickDraw — Color Drawing Operations — FillCArc). See
            // also Imaging With QuickDraw (1994) p. 4-76.
            //
            // MPW Universal Headers Quickdraw.h declares:
            //   EXTERN_API(void) FillCArc(const Rect *r,
            //       short startAngle, short arcAngle, PixPatHandle pp)
            //       ONEWORDINLINE(0xAA11);
            //
            // Tool-bit Pascal PROCEDURE ABI:
            //   Caller pushes 12 bytes (4-byte PixPatHandle at SP+0,
            //   2-byte arcAngle at SP+4, 2-byte startAngle at SP+6,
            //   4-byte Rect pointer at SP+8 — Pascal pushes args
            //   left-to-right so the Rect pointer is pushed first),
            //   trap pops 12 bytes, no FUNCTION result slot. A7
            //   net-balanced across the call.
            //
            // Engines-agree subset: the documented Tool-bit Pascal
            // PROCEDURE pop-12 calling convention. Absolute pixel-fill
            // side effects are engines-divergent: BasiliskII System
            // 7.5.3 ROM Color QuickDraw expands the full PixPat record
            // per IM:V V-46 and tiles the arc wedge with the expanded
            // color pattern. Systemless HLE reads pat1Data at offset +20
            // and uses that 8-byte monochrome fallback to fill via
            // draw_arc(ShapeOp::Fill(pat)).
            //
            // Catalogue proof:
            //   aa10_aa11_fillcroundrect_fillcarc_strict
            //   (BasiliskII sibling-pair bake; uses NewPixPat-allocated
            //   PixPatHandles and stack-allocated Rect records) + contract
            //   test fillcroundrect_fillcarc_pascal_procedure_preserves_stack_across_five_calls.
            (true, 0x211) => {
                let sp = cpu.read_reg(Register::A7);
                let pp_handle = bus.read_long(sp);
                let arc_angle = bus.read_word(sp + 4) as i16;
                let start_angle = bus.read_word(sp + 6) as i16;
                let rect_ptr = bus.read_long(sp + 8);
                cpu.write_reg(Register::A7, sp + 12);
                if let Some(pat) = Self::read_pixpat_pat1data(bus, pp_handle) {
                    let r = read_rect(bus, rect_ptr);
                    self.draw_arc(cpu, bus, &r, start_angle, arc_angle, ShapeOp::Fill(pat));
                }
                Ok(())
            }

            // FillCRgn ($AA12)
            //
            // PROCEDURE FillCRgn(rgn: RgnHandle; pp: PixPatHandle);
            //
            // Inside Macintosh Volume V (1986), pp. V-67..V-69 (Color
            // QuickDraw — Color Drawing Operations — FillCRgn). See
            // also Imaging With QuickDraw (1994) p. 4-77.
            //
            // MPW Universal Headers Quickdraw.h declares:
            //   EXTERN_API(void) FillCRgn(RgnHandle rgn, PixPatHandle pp)
            //       ONEWORDINLINE(0xAA12);
            //
            // Tool-bit Pascal PROCEDURE ABI:
            //   Caller pushes 8 bytes (4-byte RgnHandle at SP+0 +
            //   4-byte PixPatHandle at SP+4), trap pops 8 bytes, no
            //   FUNCTION result slot. A7 net-balanced across the call.
            //
            // Engines-agree subset: the documented Tool-bit Pascal
            // PROCEDURE pop-8 calling convention. Absolute pixel-fill
            // side effects are engines-divergent: BasiliskII System
            // 7.5.3 ROM Color QuickDraw expands the full PixPat record
            // per IM:V V-46 and tiles the region with the expanded
            // color pattern. Systemless HLE reads pat1Data at offset +20
            // and uses that 8-byte monochrome fallback to fill via
            // draw_rgn(ShapeOp::Fill(pat)).
            //
            // Catalogue proof:
            //   aa0f_aa12_fillcoval_fillcrgn_strict
            //   (BasiliskII System 7.5.3 ROM Color QuickDraw sibling-
            //   pair bake; uses NewPixPat()-allocated PixPatHandles +
            //   NewRgn/SetRectRgn-allocated RgnHandles) + contract test
            //   fillcoval_fillcrgn_pascal_procedure_preserves_stack_across_five_calls.
            (true, 0x212) => {
                let sp = cpu.read_reg(Register::A7);
                let pp_handle = bus.read_long(sp);
                let rgn_handle = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                if let Some(pat) = Self::read_pixpat_pat1data(bus, pp_handle) {
                    self.draw_rgn(cpu, bus, rgn_handle, ShapeOp::Fill(pat));
                }
                Ok(())
            }

            // FillCPoly ($AA13)
            //
            // Per IM:V 1986 p. V-69 (Color QuickDraw — Color Drawing
            // Operations — FillCPoly): "FillCPoly fills the polygon
            // with the pixel pattern referenced by ppat."
            //
            // PROCEDURE FillCPoly(poly: PolyHandle; pp: PixPatHandle);
            //
            // Inside Macintosh Volume V (1986), p. V-69 (Color
            // QuickDraw — Color Drawing Operations — FillCPoly). See
            // also Imaging With QuickDraw (1994) p. 4-76.
            //
            // MPW Universal Headers Quickdraw.h declares:
            //   EXTERN_API(void) FillCPoly(PolyHandle poly, PixPatHandle pp)
            //       ONEWORDINLINE(0xAA13);
            //
            // Tool-bit Pascal PROCEDURE ABI:
            //   Caller pushes 8 bytes (4-byte PolyHandle at SP+0 +
            //   4-byte PixPatHandle at SP+4), trap pops 8 bytes, no
            //   FUNCTION result slot. A7 net-balanced across the call.
            //
            // Engines-agree subset: the documented Tool-bit Pascal
            // PROCEDURE pop-8 calling convention. Absolute pixel-fill
            // side effects are engines-divergent: BasiliskII System
            // 7.5.3 ROM Color QuickDraw expands the full PixPat record
            // per IM:V V-46 and rasterises the polygon interior tiled
            // with the expanded color pattern. Systemless HLE reads
            // pat1Data at offset +20 and uses that 8-byte monochrome
            // fallback to fill via draw_poly(ShapeOp::Fill(pat)).
            //
            // Catalogue proof:
            //   aa13_fillcpoly_strict
            //   (BasiliskII System 7.5.3 ROM Color QuickDraw solo bake;
            //   uses NewPixPat()-allocated PixPatHandles +
            //   OpenPoly/MoveTo/LineTo/ClosePoly-recorded PolyHandles)
            //   + contract test
            //   fillcpoly_pascal_procedure_preserves_stack_across_five_calls.
            (true, 0x213) => {
                let sp = cpu.read_reg(Register::A7);
                let pp_handle = bus.read_long(sp);
                let poly_handle = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                if let Some(pat) = Self::read_pixpat_pat1data(bus, pp_handle) {
                    self.draw_poly(cpu, bus, poly_handle, ShapeOp::Fill(pat));
                }
                Ok(())
            }

            // ========== Color Pixel ==========

            // SetCPixel ($AA16)
            //
            // Per IM:V 1986 p. V-70 (Color QuickDraw Routines —
            // SetCPixel): "The SetCPixel function sets the pixel at
            // the specified position to the pixel value that most
            // closely matches the specified RGB."
            //
            // PROCEDURE SetCPixel (h,v: INTEGER; cPix: RGBColor);
            //
            // MPW Universal Headers Quickdraw.h declares the C-level
            // glue (Pascal records larger than 4 bytes are passed by
            // pointer to the trap; the Pascal by-value semantics are
            // honoured by the compiler-emitted temporary copy):
            //   EXTERN_API(void) SetCPixel(short h, short v, const RGBColor *cPix) ONEWORDINLINE(0xAA16);
            //
            // Tool-bit Pascal PROCEDURE ABI: caller pushes an 8-byte
            // arg frame consisting of 4-byte cPix pointer at SP+0,
            // 2-byte v INTEGER at SP+4, 2-byte h INTEGER at SP+6;
            // trap pops 8 bytes, no FUNCTION result slot. The
            // engines-agree subset (paired catalogue proof in
            // aa16_aa17_setcpixel_getcpixel_strict)
            // is the pop-8 calling convention itself: A7 unchanged
            // across the call. Absolute pixel-set side effect
            // diverges between engines — BII writes the best-match
            // CLUT index into the screen pixMap byte via the device
            // CLUT, while Systemless HLE walks its own port pixMap +
            // device_clut (different default CLUT initialisation).
            // NIL cPix pointer exits early after the 8-byte pop;
            // out-of-bounds (h, v) coords clip silently.
            //
            // Regression coverage:
            //   setcpixel_pops_eight_bytes
            //   setcpixel_writes_clut_index_255_for_black
            //   setcpixel_writes_clut_index_0_for_white
            //   setcpixel_touches_only_the_requested_pixel
            //   setcpixel_uses_current_device_clut_not_a_fixed_palette
            //   setcpixel_getcpixel_roundtrip_preserves_exact_clut_color
            //   src/trap/quickdraw.rs mod tests
            //     setcpixel_getcpixel_pascal_procedure_preserves_stack_across_five_calls
            //   aa16_aa17_setcpixel_getcpixel_strict (B1, B2)
            (true, 0x216) => {
                let sp = cpu.read_reg(Register::A7);
                let cpix_ptr = bus.read_long(sp);
                let v = bus.read_word(sp + 4) as i16;
                let h = bus.read_word(sp + 6) as i16;
                cpu.write_reg(Register::A7, sp + 8);

                if cpix_ptr == 0 {
                    return Some(Ok(()));
                }
                let rgb = [
                    bus.read_word(cpix_ptr),
                    bus.read_word(cpix_ptr + 2),
                    bus.read_word(cpix_ptr + 4),
                ];

                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port = bus.read_long(global_ptr);
                if port == 0 {
                    return Some(Ok(()));
                }

                if let Some(info) = self.resolve_pixel_target(bus, port, h, v) {
                    if info.base == self.screen_mode.0 {
                        self.ensure_dialog_background_saved_for_screen_port(bus, port);
                    }
                    self.write_cpixel(bus, &info, rgb);
                }
                Ok(())
            }

            // GetCPixel ($AA17)
            //
            // Per IM:V 1986 p. V-69 (Color QuickDraw Routines —
            // GetCPixel): "The GetCPixel function returns the RGB of
            // the pixel at the specified position in the current
            // port."
            //
            // PROCEDURE GetCPixel (h,v: INTEGER; VAR cPix: RGBColor);
            //
            // MPW Universal Headers Quickdraw.h declares the C-level
            // glue:
            //   EXTERN_API(void) GetCPixel(short h, short v, RGBColor *cPix) ONEWORDINLINE(0xAA17);
            //
            // Tool-bit Pascal PROCEDURE ABI: caller pushes an 8-byte
            // arg frame consisting of 4-byte VAR cPix pointer at
            // SP+0, 2-byte v INTEGER at SP+4, 2-byte h INTEGER at
            // SP+6; trap pops 8 bytes, no FUNCTION result slot. The
            // result RGBColor is written via the VAR pointer
            // parameter. The engines-agree subset (paired catalogue
            // proof in
            // aa16_aa17_setcpixel_getcpixel_strict)
            // is the pop-8 calling convention itself: A7 unchanged
            // across the call. Absolute RGB-read return value
            // diverges between engines — BII reads the screen
            // pixMap byte through the device CLUT, while Systemless HLE
            // reads its own port pixMap byte through device_clut
            // (different default CLUT initialisation). NIL VAR
            // pointer exits early after the 8-byte pop; out-of-bounds
            // (h, v) returns black ([0, 0, 0]).
            //
            // Regression coverage:
            //   getcpixel_pops_eight_bytes
            //   getcpixel_returns_canonical_rgb_for_index_zero
            //   getcpixel_returns_canonical_rgb_for_index_255
            //   getcpixel_uses_current_device_clut_not_a_fixed_palette
            //   setcpixel_getcpixel_roundtrip_preserves_exact_clut_color
            //   src/trap/quickdraw.rs mod tests
            //     setcpixel_getcpixel_pascal_procedure_preserves_stack_across_five_calls
            //   aa16_aa17_setcpixel_getcpixel_strict (B3, B4)
            (true, 0x217) => {
                let sp = cpu.read_reg(Register::A7);
                let cpix_ptr = bus.read_long(sp);
                let v = bus.read_word(sp + 4) as i16;
                let h = bus.read_word(sp + 6) as i16;
                cpu.write_reg(Register::A7, sp + 8);

                if cpix_ptr == 0 {
                    return Some(Ok(()));
                }

                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port = bus.read_long(global_ptr);

                let rgb = if port != 0 {
                    self.resolve_pixel_target(bus, port, h, v)
                        .map(|info| self.read_cpixel(bus, &info))
                        .unwrap_or([0, 0, 0])
                } else {
                    [0, 0, 0]
                };
                bus.write_word(cpix_ptr, rgb[0]);
                bus.write_word(cpix_ptr + 2, rgb[1]);
                bus.write_word(cpix_ptr + 4, rgb[2]);
                Ok(())
            }

            // ========== Stubs — Color Cursors ==========

            // GetCCursor ($AA1B)
            //
            // FUNCTION GetCCursor(crsrID: INTEGER): CCrsrHandle;
            //
            // Inside Macintosh Volume V (1986), p. V-74 (Color
            // QuickDraw — Operations on Color Cursors — GetCCursor).
            // Cross-referenced as Inside Macintosh: Imaging With
            // QuickDraw (1994), p. 8-26.
            //
            // Per IM:V V-74 verbatim: "The GetCCursor call creates a
            // new CCrsr data structure, then initializes it using the
            // information in the resource of type 'crsr' with the
            // specified ID. ... If the resource with the specified ID
            // isn't found, then this routine returns a NIL handle."
            //
            // MPW Universal Headers Quickdraw.h declares:
            //   EXTERN_API(CCrsrHandle) GetCCursor(short crsrID)
            //       ONEWORDINLINE(0xAA1B);
            //
            // ABI (Tool-bit Pascal FUNCTION): caller pre-pushes a
            // 4-byte CCrsrHandle result slot, then pushes a 2-byte
            // crsrID INTEGER. Trap pops the 2-byte argument and
            // writes the handle to the 4-byte result slot at the
            // post-pop SP (= caller's pre-call SP+2). Caller pops the
            // 4-byte slot after the trap returns. Net A7 effect
            // across the C-level call is zero.
            //
            // Status: Partial. Valid compiled 'crsr' resources are
            // promoted to live CCrsr records with PixMapHandle,
            // pixel-data Handle, and ColorTable Handle fields. Malformed
            // synthetic resources fall back to a Resource Manager handle so
            // legacy tests and non-rendering callers still receive a non-NIL
            // present-resource result.
            //
            // Apple-vs-BasiliskII engines-agree subset:
            //   (1) Pascal FUNCTION calling convention — A7 unchanged
            //       across the C-level call sequence.
            //   (2) Miss-returns-NIL contract per IM:V V-74.
            //
            // Engines-divergent (not witnessed by paired bake): the
            // byte contents of the dereferenced CCrsr record on the
            // present-resource path.
            //
            // Regression coverage:
            //   src/trap/quickdraw.rs mod tests
            //     getccursor_returns_handle_for_present_crsr_resource
            //     getccursor_missing_resource_returns_nil
            //     getccursor_pascal_function_preserves_stack_across_five_missing_calls
            //   aa1b_getccursor_strict (B1, B2)
            (true, 0x21B) => {
                let sp = cpu.read_reg(Register::A7);
                let crsr_id = bus.read_word(sp) as i16;
                let handle = match self.find_resource_any(*b"crsr", crsr_id) {
                    Some((_, data_ptr)) => self
                        .promote_compiled_crsr_resource(bus, data_ptr)
                        .unwrap_or_else(|| {
                            self.get_or_create_resource_handle(bus, *b"crsr", crsr_id, data_ptr)
                        }),
                    None => 0,
                };
                bus.write_long(sp + 2, handle);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // SetCCursor ($AA1C)
            // Changes the current cursor to the given color cursor.
            // PROCEDURE SetCCursor(cCrsr: CCrsrHandle);
            // Inside Macintosh Volume V, V-80
            // Stack: SP+0: cCrsr(4). Pop 4.
            // SetCCursor ($AA1C): Pops 4 bytes (cCrsr) and installs the
            // cursor image into dispatcher state; host renders the cursor
            // itself per IM:V V-80.
            (true, 0x21C) => {
                let sp = cpu.read_reg(Register::A7);
                let crsr_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);

                if crsr_handle != 0 {
                    let crsr_ptr = bus.read_long(crsr_handle);
                    if crsr_ptr != 0 {
                        if let Some(cursor) = self.cursor_image_from_ccrsr(bus, crsr_ptr) {
                            self.cursor_data = Some(cursor);
                        }
                        self.cursor_visible = self.cursor_level == 0;
                    }
                }
                Ok(())
            }

            // DisposeCCursor ($AA26)
            // Releases a color cursor allocated by GetCCursor. No-op stub.
            // PROCEDURE DisposeCCursor(cCrsr: CCrsrHandle);
            // Inside Macintosh Volume V, V-80
            // Stack: SP+0: cCrsr(4). Pop 4.
            // DisposeCCursor ($AA26): Pops 4 bytes (cCrsr); Systemless keeps
            // guest cursor allocations alive for the process lifetime, so
            // disposal is a no-op per the current HLE memory model. Return
            // noErr in D0 so callers can treat the procedure as a clean
            // do-nothing path.
            (true, 0x226) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 4);
                cpu.write_reg(Register::D0, 0);
                Ok(())
            }

            // ========== Stubs — Mask Operations ==========

            // CopyMask ($A817)
            // PROCEDURE CopyMask(srcBits, maskBits, dstBits: BitMap;
            //                    srcRect, maskRect, dstRect: Rect);
            // Inside Macintosh Volume IV, IV-33
            // Where mask bits are 1, the source bit is copied to the
            // destination; where mask bits are 0, the destination is
            // left unchanged. Source and mask must be the same size;
            // destination may have a different size; CopyMask scales the
            // source image to fit the destination.
            // Stack: SP+0: dstRect(4), SP+4: maskRect(4), SP+8: srcRect(4),
            //        SP+12: dstBits(4), SP+16: maskBits(4), SP+20: srcBits(4). Pop 24.
            // CopyMask ($A817): Mask-gated scaled blit srcBits → dstBits via maskBits (1 = copy, 0 = preserve dst); supports 1bpp + 8bpp dst per Imaging With QuickDraw 3-119..3-120
            (true, 0x017) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_rect_ptr = bus.read_long(sp);
                let mask_rect_ptr = bus.read_long(sp + 4);
                let src_rect_ptr = bus.read_long(sp + 8);
                let dst_bits_ptr = bus.read_long(sp + 12);
                let mask_bits_ptr = bus.read_long(sp + 16);
                let src_bits_ptr = bus.read_long(sp + 20);
                cpu.write_reg(Register::A7, sp + 24);

                let src_info = self.resolve_copy_bitmap(bus, src_bits_ptr);
                let mask_info = self.resolve_copy_bitmap(bus, mask_bits_ptr);
                let dst_info = self.resolve_copy_bitmap(bus, dst_bits_ptr);

                let src_top = bus.read_word(src_rect_ptr) as i16;
                let src_left = bus.read_word(src_rect_ptr + 2) as i16;
                let src_bottom = bus.read_word(src_rect_ptr + 4) as i16;
                let src_right = bus.read_word(src_rect_ptr + 6) as i16;
                let mask_top = bus.read_word(mask_rect_ptr) as i16;
                let mask_left = bus.read_word(mask_rect_ptr + 2) as i16;
                let mask_bottom = bus.read_word(mask_rect_ptr + 4) as i16;
                let mask_right = bus.read_word(mask_rect_ptr + 6) as i16;
                let dst_top = bus.read_word(dst_rect_ptr) as i16;
                let dst_left = bus.read_word(dst_rect_ptr + 2) as i16;
                let dst_bottom = bus.read_word(dst_rect_ptr + 4) as i16;
                let dst_right = bus.read_word(dst_rect_ptr + 6) as i16;

                if src_bottom <= src_top
                    || src_right <= src_left
                    || mask_bottom <= mask_top
                    || mask_right <= mask_left
                    || dst_bottom <= dst_top
                    || dst_right <= dst_left
                {
                    return Some(Ok(()));
                }

                let clip_t = dst_top.max(dst_info.bounds_top);
                let clip_l = dst_left.max(dst_info.bounds_left);
                let clip_b = dst_bottom.min(dst_info.bounds_bottom);
                let clip_r = dst_right.min(dst_info.bounds_right);
                if clip_b <= clip_t || clip_r <= clip_l {
                    return Some(Ok(()));
                }

                let src_w = i32::from(src_right) - i32::from(src_left);
                let src_h = i32::from(src_bottom) - i32::from(src_top);
                let mask_w = i32::from(mask_right) - i32::from(mask_left);
                let mask_h = i32::from(mask_bottom) - i32::from(mask_top);
                let dst_w = i32::from(dst_right) - i32::from(dst_left);
                let dst_h = i32::from(dst_bottom) - i32::from(dst_top);
                let no_src_scaling = src_w == dst_w && src_h == dst_h && src_w > 0 && src_h > 0;
                let no_mask_scaling =
                    mask_w == dst_w && mask_h == dst_h && mask_w > 0 && mask_h > 0;

                for dy in clip_t..clip_b {
                    let sy = if no_src_scaling {
                        i32::from(src_top) + (i32::from(dy) - i32::from(dst_top))
                    } else {
                        let Some(sy) =
                            Self::scale_coord(src_top, src_bottom, dst_top, dst_bottom, dy)
                        else {
                            continue;
                        };
                        sy
                    } as i16;
                    let my = if no_mask_scaling {
                        i32::from(mask_top) + (i32::from(dy) - i32::from(dst_top))
                    } else {
                        let Some(my) =
                            Self::scale_coord(mask_top, mask_bottom, dst_top, dst_bottom, dy)
                        else {
                            continue;
                        };
                        my
                    } as i16;
                    for dx in clip_l..clip_r {
                        let sx = if no_src_scaling {
                            i32::from(src_left) + (i32::from(dx) - i32::from(dst_left))
                        } else {
                            let Some(sx) =
                                Self::scale_coord(src_left, src_right, dst_left, dst_right, dx)
                            else {
                                continue;
                            };
                            sx
                        } as i16;
                        let mx = if no_mask_scaling {
                            i32::from(mask_left) + (i32::from(dx) - i32::from(dst_left))
                        } else {
                            let Some(mx) =
                                Self::scale_coord(mask_left, mask_right, dst_left, dst_right, dx)
                            else {
                                continue;
                            };
                            mx
                        } as i16;
                        let Some(mask_pixel) = Self::read_bitmap_pixel(bus, &mask_info, my, mx)
                        else {
                            continue;
                        };
                        if mask_pixel == 0 {
                            continue; // transparent — leave dst alone
                        }
                        let Some(src_pixel) = Self::read_bitmap_pixel(bus, &src_info, sy, sx)
                        else {
                            continue;
                        };
                        let row = (dy - dst_info.bounds_top) as u32;
                        let col = (dx - dst_info.bounds_left) as u32;
                        match dst_info.pixel_size {
                            1 => {
                                let addr = dst_info.base + row * dst_info.row_bytes + (col / 8);
                                let bit = 7 - (col % 8);
                                let byte = bus.read_byte(addr);
                                // src_pixel is 0 or 255 from
                                // read_bitmap_pixel's 1bpp branch (or
                                // an 8bpp value if dst is 8bpp). For
                                // 1bpp dst, a non-zero src bit sets,
                                // a zero src bit clears.
                                let new_byte = if src_pixel != 0 {
                                    byte | (1 << bit)
                                } else {
                                    byte & !(1 << bit)
                                };
                                bus.write_byte(addr, new_byte);
                            }
                            8 => {
                                bus.write_byte(
                                    dst_info.base + row * dst_info.row_bytes + col,
                                    src_pixel,
                                );
                            }
                            _ => {} // 16/32 bpp not yet supported
                        }
                    }
                }
                Ok(())
            }

            // CopyDeepMask ($AA51)
            // PROCEDURE CopyDeepMask(srcBits, maskBits, dstBits: BitMap;
            //                        srcRect, maskRect, dstRect: Rect;
            //                        mode: INTEGER; maskRgn: RgnHandle);
            // Imaging With QuickDraw, 3-36
            // Combines CopyBits with CopyMask using a deep mask:
            // where mask bits are set, the source bit is copied to the
            // destination via `mode`; where mask bits are clear, the
            // destination is left unchanged. maskRgn optionally
            // further clips the destination (NIL = no clip).
            // Stack: SP+0: maskRgn(4), SP+4: mode(2), SP+6: dstRect(4),
            //        SP+10: maskRect(4), SP+14: srcRect(4), SP+18: dstBits(4),
            //        SP+22: maskBits(4), SP+26: srcBits(4). Pop 30.
            // Reuses the same mask-gated pixel loop as CopyMask ($A817),
            // but maps destination pixels back through srcRect and
            // maskRect so differing dstRect dimensions scale like CopyBits.
            // The `mode` parameter is honored for 1bpp dst (srcCopy vs
            // Or/Xor/Bic + their notSrc variants); 8bpp dst follows srcCopy
            // semantics for now.
            // CopyDeepMask ($AA51): Scaled mask-gated CopyBits that clips to maskRgn, honors boolean transfer modes for 1bpp dst, and treats non-zero mask pixels as opaque for 1/8bpp masks per Inside Macintosh VI 17-25 / Imaging With QuickDraw 3-120..3-122
            (true, 0x251) => {
                let sp = cpu.read_reg(Register::A7);
                let mask_rgn = bus.read_long(sp);
                let mode = bus.read_word(sp + 4) as i16;
                let dst_rect_ptr = bus.read_long(sp + 6);
                let mask_rect_ptr = bus.read_long(sp + 10);
                let src_rect_ptr = bus.read_long(sp + 14);
                let dst_bits_ptr = bus.read_long(sp + 18);
                let mask_bits_ptr = bus.read_long(sp + 22);
                let src_bits_ptr = bus.read_long(sp + 26);
                cpu.write_reg(Register::A7, sp + 30);

                let src_info = self.resolve_copy_bitmap(bus, src_bits_ptr);
                let mask_info = self.resolve_copy_bitmap(bus, mask_bits_ptr);
                let dst_info = self.resolve_copy_bitmap(bus, dst_bits_ptr);

                let src_top = bus.read_word(src_rect_ptr) as i16;
                let src_left = bus.read_word(src_rect_ptr + 2) as i16;
                let src_bottom = bus.read_word(src_rect_ptr + 4) as i16;
                let src_right = bus.read_word(src_rect_ptr + 6) as i16;
                let mask_top = bus.read_word(mask_rect_ptr) as i16;
                let mask_left = bus.read_word(mask_rect_ptr + 2) as i16;
                let mask_bottom = bus.read_word(mask_rect_ptr + 4) as i16;
                let mask_right = bus.read_word(mask_rect_ptr + 6) as i16;
                let dst_top = bus.read_word(dst_rect_ptr) as i16;
                let dst_left = bus.read_word(dst_rect_ptr + 2) as i16;
                let dst_bottom = bus.read_word(dst_rect_ptr + 4) as i16;
                let dst_right = bus.read_word(dst_rect_ptr + 6) as i16;
                let mode_base = (mode & 0x3F) as u16;

                if src_bottom <= src_top
                    || src_right <= src_left
                    || mask_bottom <= mask_top
                    || mask_right <= mask_left
                    || dst_bottom <= dst_top
                    || dst_right <= dst_left
                {
                    return Some(Ok(()));
                }

                let mut clip_t = dst_top.max(dst_info.bounds_top);
                let mut clip_l = dst_left.max(dst_info.bounds_left);
                let mut clip_b = dst_bottom.min(dst_info.bounds_bottom);
                let mut clip_r = dst_right.min(dst_info.bounds_right);

                if !Self::clip_rect_to_region_bbox(
                    bus,
                    mask_rgn,
                    &mut clip_t,
                    &mut clip_l,
                    &mut clip_b,
                    &mut clip_r,
                ) {
                    return Some(Ok(()));
                }

                if clip_b <= clip_t || clip_r <= clip_l {
                    return Some(Ok(()));
                }

                let mask_membership =
                    Self::build_region_membership_cache(bus, mask_rgn, clip_t, clip_b);

                let src_w = i32::from(src_right) - i32::from(src_left);
                let src_h = i32::from(src_bottom) - i32::from(src_top);
                let mask_w = i32::from(mask_right) - i32::from(mask_left);
                let mask_h = i32::from(mask_bottom) - i32::from(mask_top);
                let dst_w = i32::from(dst_right) - i32::from(dst_left);
                let dst_h = i32::from(dst_bottom) - i32::from(dst_top);
                let no_src_scaling = src_w == dst_w && src_h == dst_h && src_w > 0 && src_h > 0;
                let no_mask_scaling =
                    mask_w == dst_w && mask_h == dst_h && mask_w > 0 && mask_h > 0;

                for dy in clip_t..clip_b {
                    let sy = if no_src_scaling {
                        i32::from(src_top) + (i32::from(dy) - i32::from(dst_top))
                    } else {
                        let Some(sy) =
                            Self::scale_coord(src_top, src_bottom, dst_top, dst_bottom, dy)
                        else {
                            continue;
                        };
                        sy
                    } as i16;
                    let my = if no_mask_scaling {
                        i32::from(mask_top) + (i32::from(dy) - i32::from(dst_top))
                    } else {
                        let Some(my) =
                            Self::scale_coord(mask_top, mask_bottom, dst_top, dst_bottom, dy)
                        else {
                            continue;
                        };
                        my
                    } as i16;
                    for dx in clip_l..clip_r {
                        if !Self::region_contains_point_cached(
                            bus,
                            mask_rgn,
                            mask_membership.as_ref(),
                            dy,
                            dx,
                        ) {
                            continue;
                        }
                        let sx = if no_src_scaling {
                            i32::from(src_left) + (i32::from(dx) - i32::from(dst_left))
                        } else {
                            let Some(sx) =
                                Self::scale_coord(src_left, src_right, dst_left, dst_right, dx)
                            else {
                                continue;
                            };
                            sx
                        } as i16;
                        let mx = if no_mask_scaling {
                            i32::from(mask_left) + (i32::from(dx) - i32::from(dst_left))
                        } else {
                            let Some(mx) =
                                Self::scale_coord(mask_left, mask_right, dst_left, dst_right, dx)
                            else {
                                continue;
                            };
                            mx
                        } as i16;
                        let Some(mask_pixel) = Self::read_bitmap_pixel(bus, &mask_info, my, mx)
                        else {
                            continue;
                        };
                        if mask_pixel == 0 {
                            continue;
                        }
                        let Some(src_pixel) = Self::read_bitmap_pixel(bus, &src_info, sy, sx)
                        else {
                            continue;
                        };
                        let row = (dy - dst_info.bounds_top) as u32;
                        let col = (dx - dst_info.bounds_left) as u32;
                        match dst_info.pixel_size {
                            1 => {
                                let addr = dst_info.base + row * dst_info.row_bytes + (col / 8);
                                let bit = 7 - (col % 8);
                                let byte = bus.read_byte(addr);
                                let dst_bit = (byte >> bit) & 1;
                                let src_bit = if src_pixel != 0 { 1 } else { 0 };
                                let result_bit: u8 = match mode_base {
                                    0 => src_bit,                  // srcCopy
                                    1 => src_bit | dst_bit,        // srcOr
                                    2 => src_bit ^ dst_bit,        // srcXor
                                    3 => (!src_bit) & dst_bit & 1, // srcBic
                                    4 => 1 - src_bit,              // notSrcCopy
                                    5 => (1 - src_bit) | dst_bit,  // notSrcOr
                                    6 => (1 - src_bit) ^ dst_bit,  // notSrcXor
                                    7 => src_bit & dst_bit,        // notSrcBic
                                    _ => src_bit,
                                };
                                let new_byte = if result_bit != 0 {
                                    byte | (1 << bit)
                                } else {
                                    byte & !(1 << bit)
                                };
                                bus.write_byte(addr, new_byte);
                            }
                            8 => {
                                // 8bpp: boolean transfer modes perform
                                // bitwise ops on the source + destination
                                // color indices per IM:V V-11. The
                                // arithmetic modes (addOver, addPin, blend,
                                // hilite) are not yet implemented.
                                let addr = dst_info.base + row * dst_info.row_bytes + col;
                                let dst_byte = bus.read_byte(addr);
                                let new_byte: u8 = match mode_base {
                                    0 => src_pixel,             // srcCopy
                                    1 => src_pixel | dst_byte,  // srcOr
                                    2 => src_pixel ^ dst_byte,  // srcXor
                                    3 => !src_pixel & dst_byte, // srcBic
                                    4 => !src_pixel,            // notSrcCopy
                                    5 => !src_pixel | dst_byte, // notSrcOr
                                    6 => !src_pixel ^ dst_byte, // notSrcXor
                                    7 => src_pixel & dst_byte,  // notSrcBic
                                    _ => src_pixel,
                                };
                                bus.write_byte(addr, new_byte);
                            }
                            _ => {}
                        }
                    }
                }
                Ok(())
            }

            // ========== Text / Font ==========

            // MeasureText ($A837)
            // PROCEDURE MeasureText(count: INTEGER; textAddr, charLocs: Ptr);
            // Inside Macintosh Volume IV (1986), p. IV-32
            //
            // "MeasureText takes a `count`-byte string of text starting
            //  at `textAddr` and fills `charLocs` with `count + 1`
            //  INTEGERs giving the horizontal pixel offsets of each
            //  character from the starting point. The first integer is
            //  always 0, and the last is the total width of the text."
            //
            // Stack: SP+0=charLocs(4), SP+4=textAddr(4), SP+8=count(2).
            // Pops 10.
            //
            // Regression coverage:
            //   measuretext_writes_count_plus_one_offsets_to_charlocs
            //   measuretext_zero_count_writes_just_one_zero
            //   measuretext_offsets_are_strictly_increasing_for_normal_text
            // MeasureText ($A837): Writes count+1 horizontal pixel offsets into charLocs (cumulative glyph advances scaled to current tx_size); first offset is 0 per IM:IV IV-32
            (true, 0x037) => {
                let sp = cpu.read_reg(Register::A7);
                let char_locs = bus.read_long(sp);
                let text_addr = bus.read_long(sp + 4);
                let count = bus.read_word(sp + 8) as i16;
                cpu.write_reg(Register::A7, sp + 10);

                if char_locs != 0 {
                    let (_face, scale) = get_font_face_scaled(self.tx_font, self.tx_size);
                    let mut offset: i16 = 0;
                    bus.write_word(char_locs, 0);
                    if text_addr != 0 && count > 0 {
                        for i in 0..(count as u32) {
                            let ch = bus.read_byte(text_addr + i) as char;
                            let advance =
                                if let Some((g, _)) = get_glyph(self.tx_font, self.tx_size, ch) {
                                    self.glyph_advance(g) * scale
                                } else {
                                    self.missing_glyph_advance() * scale
                                };
                            offset = offset.saturating_add(advance);
                            bus.write_word(char_locs + (i + 1) * 2, offset as u16);
                        }
                    }
                }
                Ok(())
            }

            // CharExtra ($AA23)
            // PROCEDURE CharExtra(extra: Fixed);
            // Inside Macintosh Volume V (1986), p. V-77 (Color QuickDraw —
            // Color Drawing Routines — CharExtra). Per the verbatim
            // contract: "The CharExtra procedure sets the cGrafPort's
            // charExtra field, which specifies the number of pixels by
            // which to widen every character excluding the space character
            // in a line of text. The charExtra field is stored in a
            // compressed format based on the txSize field, so you must set
            // txSize before calling CharExtra. The initial charExtra
            // setting is 0. CharExtra will accept a negative number.
            // CharExtra has no effect on grafPorts."
            //
            // MPW Universal Headers QuickdrawText.h:
            //   EXTERN_API(void) CharExtra(Fixed extra) ONEWORDINLINE(0xAA23);
            //
            // Tool-bit Pascal PROCEDURE pop-4 ABI: caller pushes the 4-byte
            // Fixed argument, trap pops it (no FUNCTION result slot is
            // written, A7 unchanged across the call). The shape is
            // identical to $AA47 SetDeskCPat / $AA0A PenPixPat /
            // $AA0B BackPixPat / $AA21 OpColor / $AA22 HiliteColor.
            //
            // Engines-divergent absolute side effect: BasiliskII System
            // 7.5.3 ROM Color QuickDraw honours the documented contract on
            // a CGrafPort (writes the Fixed value into the charExtra field
            // in compressed format based on txSize per IM:V V-77) and is a
            // documented pure no-op on a regular GrafPort. Systemless's HLE
            // stores the value in dispatcher-internal `char_extra` state
            // regardless of port type — engines-divergent from BII as a
            // side effect but engines-agree on the Pascal PROCEDURE pop-4
            // calling convention itself.
            //
            // Catalogue proof: aa23_charextra_strict — witnesses 2 of 2
            // engines-agree calling-convention assertions on the BII
            // System 7.5.3 ROM strict bake (single CharExtra call + 5-call
            // composition with distinct Fixed values).
            //
            // Stack: SP+0=extra(Fixed, 4). Pops 4.
            //
            // Future work: respect char_extra when computing glyph
            // advances in draw_string / draw_char (Systemless currently
            // stores the value without applying it during text rendering).
            (true, 0x223) => {
                let sp = cpu.read_reg(Register::A7);
                let extra_fixed = bus.read_long(sp) as i32;
                cpu.write_reg(Register::A7, sp + 4);
                self.char_extra = extra_fixed;
                Ok(())
            }

            // FontMetrics ($A835)
            // PROCEDURE FontMetrics(VAR theMetrics: FMetricRec);
            // Inside Macintosh Volume IV (1986), p. IV-32
            //
            // Per IM:IV IV-32: "FontMetrics is similar to the QuickDraw
            // procedure GetFontInfo except that it returns fixed-point
            // values for greater accuracy in high-resolution printing."
            // The procedure populates the caller's FMetricRec with the
            // ascent/descent/leading/widMax for the current font as
            // Fixed16.16 values, plus a handle to the font's global
            // width table.
            //
            // FMetricRec record layout (20 bytes total — IM:IV IV-32):
            //   +0   ascent      Fixed    base line to top
            //   +4   descent     Fixed    base line to bottom
            //   +8   leading     Fixed    leading between lines
            //  +12   widMax      Fixed    maximum character width
            //  +16   wTabHandle  Handle   handle to global width table
            //
            // MPW Universal Headers Fonts.h declaration:
            //   EXTERN_API(void) FontMetrics(FMetricRecPtr theMetrics)
            //                                ONEWORDINLINE(0xA835)
            //
            // Pascal PROCEDURE stack frame (caller perspective):
            //   sp+0..3   theMetrics      FMetricRecPtr (last/only push)
            //   4-byte pop, no function-result slot reserved
            //
            // BII-vs-Systemless divergence on wTabHandle: per IM:IV IV-32
            // "A handle to the global width table is returned by the
            // FontMetrics procedure." BasiliskII System 7.5.3 ROM
            // populates wTabHandle with a real handle to the system's
            // global width table (allocated in the system heap per
            // IM:IV IV-33). Systemless does not model the global width
            // table and writes NIL (0) verbatim. The engines-agree
            // contract is "wTabHandle field is overwritten" (sentinel
            // does not survive); the exact handle value is engine-
            // specific and not part of the strict bake witness.
            //
            // Regression coverage:
            //   tests::fontmetrics_writes_fixed_ascent_descent_leading_widmax
            //   tests::fontmetrics_writes_wtabhandle_field_in_fmetricrec
            //   tests::fontmetrics_consumes_fmetricrecptr_argument_and_pops_four_bytes
            //   a835_fontmetrics_strict/
            (true, 0x035) => {
                let sp = cpu.read_reg(Register::A7);
                let rec_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if rec_ptr != 0 {
                    let (face, scale) = get_font_face_scaled(self.tx_font, self.tx_size);
                    let metrics = face.metrics;
                    // Convert i16 pixel values to Fixed16.16 by shifting
                    // the integer into the high word.
                    let to_fixed = |v: i16| -> u32 { (v as i32 as u32) << 16 };
                    bus.write_long(rec_ptr, to_fixed(metrics.ascent * scale));
                    bus.write_long(rec_ptr + 4, to_fixed(metrics.descent * scale));
                    bus.write_long(rec_ptr + 8, to_fixed(metrics.leading * scale));
                    bus.write_long(rec_ptr + 12, to_fixed(metrics.wid_max * scale));
                    // wTabHandle: not modeled — leave NIL.
                    bus.write_long(rec_ptr + 16, 0);
                }
                Ok(())
            }

            // SetFScaleDisable ($A834)
            // Enables or disables font scaling in the Font Manager's
            // bitmapped-font selection path.
            // PROCEDURE SetFScaleDisable(fontScaleDisable: BOOLEAN);
            // Inside Macintosh Volume IV (1986), p. IV-32; Inside Macintosh:
            // Text (1993), pp. 4-38 and 4-59.
            //
            // Per IM:IV IV-32: "SetFScaleDisable lets you disable or enable
            // font scaling. If fontScaleDisable is TRUE, font scaling is
            // disabled and the Font Manager returns an unscaled font with
            // more space around the characters; if it's FALSE, the Font
            // Manager scales fonts. To ensure compatibility with existing
            // applications, the Font Manager defaults to scaling fonts."
            // The assembly-language note on the same page states the trap
            // mutates the FScaleDisable low-memory global directly; IM:III
            // (1985) p. III-227 records the global at byte address $0A63
            // ("nonzero to disable font scaling, byte"). MPW exposes the
            // global via TWOWORDINLINE accessors:
            //   LMGetFScaleDisable() = MOVE.B $0A63, D0  (0x1EB8 0x0A63)
            //   LMSetFScaleDisable(v) = MOVE.B v, $0A63  (0x11DF 0x0A63)
            //
            // Pascal PROCEDURE protocol (caller perspective):
            //   Stack on entry: SP+0: fontScaleDisable(2) — the BOOLEAN
            //   argument encoded as a 2-byte word with the value byte in
            //   the HIGH byte (MPW Pascal BOOLEAN convention). The trap
            //   pops 2 bytes; no function-result slot is reserved.
            //
            // Byte-write semantic: the real System 7.5 ROM writes the raw
            // Pascal BOOLEAN high byte verbatim to $0A63 — TRUE becomes
            // the byte value 0x01 (not a normalised 0xFF) and FALSE becomes
            // 0x00. Systemless mirrors this exact byte by reading SP+0
            // directly (no normalisation). BasiliskII System 7.5.3 ROM
            // follows the same convention, witnessed by
            // `a834_setfscaledisable_strict`.
            //
            // Note: per IM:IV IV-32 "setting the global variable
            // FScaleDisable is insufficient" — the real Font Manager also
            // flushes width caches and resets internal scaling state. The
            // Systemless HLE does not model those secondary effects because
            // the visible-state contract surfaces (LMGetFScaleDisable, the
            // byte at $0A63) match BII exactly.
            //
            // Regression coverage:
            //   tests::setfscaledisable_true_writes_one_byte_verbatim_to_fscale_disable_global
            //   tests::setfscaledisable_false_writes_zero_byte_to_fscale_disable_global
            //   tests::setfscaledisable_consumes_two_byte_boolean_argument_and_balances_stack
            // SetFScaleDisable ($A834): Writes FScaleDisable low-mem global ($0A63); per IM:IV IV-32
            (true, 0x034) => {
                let sp = cpu.read_reg(Register::A7);
                // Pascal BOOLEAN at SP+0 (MPW convention: value byte in
                // the high byte of the 2-byte stack slot).
                let fscale_disable_byte = bus.read_byte(sp);
                cpu.write_reg(Register::A7, sp + 2);
                bus.write_byte(0x0A63, fscale_disable_byte);
                Ok(())
            }

            // GetMaskTable ($A836)
            // Returns in A0 a pointer to QuickDraw's internal mask table.
            // Inside Macintosh Volume IV (1986), pp. IV-25..IV-26:
            //   "The function GetMaskTable, accessible only from
            //   assembly language, returns in register A0 a pointer
            //   to a ROM table containing the following useful masks:
            //   16 right masks, 16 left masks, 16 bit masks."
            // Per IM:IV trap-macro summary (p. IV-26): on entry — none;
            // on exit — A0: ptr to mask table in ROM. The trap consumes
            // no stack arguments and reserves no result slot — the
            // Pascal FUNCTION protocol is NOT used.
            //
            // MPW Universal Headers Quickdraw.h declares it as:
            //   #pragma parameter __A0 GetMaskTable
            //   EXTERN_API( Ptr ) GetMaskTable(void) ONEWORDINLINE(0xA836);
            // matching the register-return convention from IM:IV.
            //
            // Systemless lazily allocates a 96-byte copy of the documented
            // table on first call and caches the address in
            // `mask_table_addr`. The bytes match IM:IV's exact ROM
            // layout, so any caller that walks the three 16-word
            // sub-tables (right masks, left masks, bit masks) sees the
            // same values it would on real Mac ROM.
            //
            // Contract coverage:
            //   src/trap/quickdraw.rs::tests::getmasktable_writes_non_nil_pointer_to_a0_and_preserves_stack
            //   src/trap/quickdraw.rs::tests::getmasktable_table_contents_match_inside_macintosh_iv_layout
            //
            // Strict bake: a836_getmasktable_strict/
            (true, 0x036) => {
                let addr = match self.mask_table_addr {
                    Some(a) => a,
                    None => {
                        let a = bus.alloc(96);
                        // Right masks (IM:IV IV-25)
                        const RIGHT: [u16; 16] = [
                            0x0000, 0x8000, 0xC000, 0xE000, 0xF000, 0xF800, 0xFC00, 0xFE00, 0xFF00,
                            0xFF80, 0xFFC0, 0xFFE0, 0xFFF0, 0xFFF8, 0xFFFC, 0xFFFE,
                        ];
                        // Left masks (IM:IV IV-25)
                        const LEFT: [u16; 16] = [
                            0xFFFF, 0x7FFF, 0x3FFF, 0x1FFF, 0x0FFF, 0x07FF, 0x03FF, 0x01FF, 0x00FF,
                            0x007F, 0x003F, 0x001F, 0x000F, 0x0007, 0x0003, 0x0001,
                        ];
                        // Bit masks (IM:IV IV-25)
                        const BIT: [u16; 16] = [
                            0x8000, 0x4000, 0x2000, 0x1000, 0x0800, 0x0400, 0x0200, 0x0100, 0x0080,
                            0x0040, 0x0020, 0x0010, 0x0008, 0x0004, 0x0002, 0x0001,
                        ];
                        for (i, w) in RIGHT
                            .iter()
                            .chain(LEFT.iter())
                            .chain(BIT.iter())
                            .enumerate()
                        {
                            bus.write_word(a + (i as u32) * 2, *w);
                        }
                        self.mask_table_addr = Some(a);
                        a
                    }
                };
                cpu.write_reg(Register::A0, addr);
                Ok(())
            }

            // FontDispatch ($A854)
            // Per IM:VI Table C-1 line 57508 + Table C-3 lines
            // 57686..57692 master dispatch tables: $A854 is
            // _FontDispatch — a selector-based dispatcher for
            // System 7+ TrueType / outline font Manager extensions
            // with 7 documented selectors:
            //   $0000 IsOutline           (numer + denom Point → Boolean)
            //   $0001 SetOutlinePreferred (Boolean)
            //   $0008 OutlineMetrics      (8 args → OSErr)
            //   $0009 GetOutlinePreferred (→ Boolean)
            //   $000A SetPreserveGlyph    (Boolean)
            //   $000B GetPreserveGlyph    (→ Boolean)
            //   $000C FlushFonts          (→ OSErr)
            // Inside Macintosh Volume VI, 12-18..12-22
            // Inside Macintosh Volume VI, Table C-3 lines 57686..57692
            //
            // Selector encoding (System 7+ register-based dispatch
            // convention per IM:VI Pack8 / Pack14 / QDExtensions
            // family): D0 holds `(arg_words << 8) | routine`. The
            // selector lives in D0, NOT on the stack — so the
            // trap pops only the documented per-selector args
            // (no selector-word pop).
            //
            // ## HLE compromise
            //
            // Systemless's Font Manager state is statically baked
            // into trap/font_table.rs Rust glyph tables (Chicago
            // / Geneva / Monaco / system-font 9pt-12pt-9pt set
            // baked at build time). No FOND / FONT / NFNT / sfnt
            // resource loading; no TrueType outline font support.
            // All selectors collapse to "no outline fonts" sentinels:
            //   - IsOutline → FALSE (no outline fonts available)
            //   - GetOutlinePreferred → current session preference
            //     (default FALSE per IM:VI 12-18: "By default, the
            //     Font Manager prefers to use bitmapped fonts, for
            //     which the GetOutlinePreferred function returns
            //     FALSE")
            //   - GetPreserveGlyph → current session preference
            //     (default FALSE per IM:VI 12-21)
            //   - SetOutlinePreferred / SetPreserveGlyph → session
            //     preference setters
            //   - OutlineMetrics → resNotFound (-192) — no outline
            //     fonts exist to measure; matches IM:VI 12-19
            //     "OutlineMetrics works on TrueType fonts only"
            //   - FlushFonts → noErr (no font cache to flush)
            //
            // Status promoted Stub (no-op) -> Partial during the
            // stub-with-substantive-body audit.
            // Each selector now writes the documented "no outline
            // fonts" sentinel rather than collapsing all 7 to a
            // single 2-byte-pop no-op. Selector enumeration mirrors
            // the prior Pack14 ($A830 Help Manager) / Pack15 ($A831
            // Picture Utilities) / QDExtensions ($AB1D) selector
            // dispatcher patterns.
            // FontDispatch ($A854): Selector-based dispatcher per IM:VI Table C-3 lines 57686..57692 with 7 selectors ($0000 IsOutline → FALSE, $0001 SetOutlinePreferred updates current preference, $0008 OutlineMetrics → resNotFound -192, $0009 GetOutlinePreferred → current preference, $000A SetPreserveGlyph updates current preference, $000B GetPreserveGlyph → current preference, $000C FlushFonts → noErr). Selector in D0 per System 7+ register-based dispatch convention. HLE has no TrueType / outline font support (statically-baked Rust glyph tables in trap/font_table.rs) so the outline-geometry selectors still return the documented "no outline fonts available" sentinels per IM:VI 12-18..12-22.
            (true, 0x054) => {
                let sp = cpu.read_reg(Register::A7);
                let selector = cpu.read_reg(Register::D0);
                let routine = selector & 0xFFFF;
                match routine {
                    // $0000 IsOutline (numer Point + denom Point → Boolean)
                    // Stack: SP+0 denom Point (4) + SP+4 numer Point (4)
                    //        + SP+8 result BOOLEAN (2). Pop 8; result at sp+8.
                    // Returns FALSE — no outline fonts in HLE.
                    0x0000 => {
                        bus.write_word(sp + 8, 0); // FALSE
                        cpu.write_reg(Register::A7, sp + 8);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    // $0001 SetOutlinePreferred (Boolean) — pop the 2-byte Boolean slot and persist the preference
                    0x0001 => {
                        self.outline_preferred = bus.read_byte(sp) != 0;
                        cpu.write_reg(Register::A7, sp + 2);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    // $0008 OutlineMetrics (8 args → OSErr)
                    // Pascal frame per IM:VI 12-19:
                    //   byteCount INTEGER + textPtr Ptr + numer Point +
                    //   denom Point + VAR yMax INTEGER + VAR yMin INTEGER +
                    //   awArray FixedPtr + lsbArray FixedPtr +
                    //   boundsArray RectPtr → OSErr
                    // Total args: 2+4+4+4+4+4+4+4+4 = 34 bytes; pop 34 + 2 result.
                    0x0008 => {
                        // Returns resNotFound (-192) — no outline fonts
                        // exist to measure, per IM:VI 12-19 OutlineMetrics
                        // "works on TrueType fonts only".
                        bus.write_word(sp + 34, (-192i16) as u16);
                        cpu.write_reg(Register::A7, sp + 34);
                        cpu.write_reg(Register::D0, (-192i32) as u32);
                        Ok(())
                    }
                    // $0009 GetOutlinePreferred (→ Boolean) — write the 1-byte Boolean result into the return slot
                    0x0009 => {
                        let value = if self.outline_preferred { 1 } else { 0 };
                        bus.write_byte(sp, value as u8);
                        cpu.write_reg(Register::D0, value as u32);
                        Ok(())
                    }
                    // $000A SetPreserveGlyph (Boolean) — pop the 2-byte Boolean slot and persist the preference
                    0x000A => {
                        self.preserve_glyph = bus.read_byte(sp) != 0;
                        cpu.write_reg(Register::A7, sp + 2);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    // $000B GetPreserveGlyph (→ Boolean) — write the 1-byte Boolean result into the return slot
                    0x000B => {
                        let value = if self.preserve_glyph { 1 } else { 0 };
                        bus.write_byte(sp, value as u8);
                        cpu.write_reg(Register::D0, value as u32);
                        Ok(())
                    }
                    // $000C FlushFonts (→ OSErr) — no args
                    0x000C => {
                        bus.write_word(sp, 0); // noErr
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    // Unknown selector — defensive no-op + noErr
                    _ => {
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                }
            }

            // ========== Stubs — Selector-Based Dispatchers ==========

            // NQDMisc ($ABC3)
            // Selector-based dispatcher for QuickDraw picture-comment misc
            // codes. Selector 6 is the SetGrayLevel path.
            // Imaging With QuickDraw 1994, p. B-40.
            // Register calling convention: selector in D0, data pointer in A0.
            // No Pascal stack frame is consumed on the selector-6 path.
            (true, 0x3C3) => {
                cpu.write_reg(Register::D0, 0);
                Ok(())
            }

            // DisplayDispatch ($ABEB)
            // Selector-based dispatcher for the Display Manager. Abuse 1.01
            // enumerates active screen devices here. Systemless models one active
            // screen GDevice, so return that handle for first-screen and NIL for
            // next-screen. The OSErr-returning selectors below write the return
            // code to both D0 and the documented stack result slot.
            (true, 0x3EB) => {
                let sp = cpu.read_reg(Register::A7);
                match cpu.read_reg(Register::D0) as u16 {
                    // DMGetFirstScreenDevice(Boolean flags) -> GDHandle.
                    0 => {
                        let gdh = self.ensure_main_gdevice(bus);
                        bus.write_long(sp + 2, gdh);
                        cpu.write_reg(Register::A7, sp + 2);
                    }
                    // DMGetNextScreenDevice(GDHandle, Boolean flags) -> GDHandle.
                    1 => {
                        bus.write_long(sp + 6, 0);
                        cpu.write_reg(Register::A7, sp + 6);
                    }
                    // DMBeginConfigureDisplays(Handle *displayState) -> OSErr.
                    // BasiliskII returns the main GDevice handle as the begin
                    // token and stores the same handle through *displayState.
                    0x0206 => {
                        let display_state_ptr = bus.read_long(sp);
                        let display_state = self.ensure_main_gdevice(bus);
                        if display_state_ptr != 0 {
                            bus.write_long(display_state_ptr, display_state);
                        }
                        bus.write_word(sp + 4, display_state as u16);
                        cpu.write_reg(Register::A7, sp + 4);
                        cpu.write_reg(Register::D0, display_state as i16 as i32 as u32);
                    }
                    // DMEndConfigureDisplays(Handle displayState) -> OSErr.
                    0x0207 => {
                        bus.write_word(sp + 4, 0);
                        cpu.write_reg(Register::A7, sp + 4);
                        cpu.write_reg(Register::D0, 0);
                    }
                    // DMSetMainDisplay(GDHandle newMainDevice, Handle displayState) -> OSErr.
                    0x0410 => {
                        let new_main = bus.read_long(sp + 4);
                        if new_main != 0 {
                            self.main_gdevice_handle = new_main;
                            self.current_gdevice = new_main;
                            bus.write_long(0x0CC8, new_main);
                        }
                        bus.write_word(sp + 8, 0);
                        cpu.write_reg(Register::A7, sp + 8);
                        cpu.write_reg(Register::D0, 0);
                    }
                    // DMSetDisplayMode(GDHandle, UInt32 mode, UInt32 *depthMode,
                    //                  UInt32 reserved, Handle displayState) -> OSErr.
                    // Abuse uses this after accepting the 640x480/256-colour
                    // switch dialog. The single-screen HLE keeps the active
                    // framebuffer, writes the current device mode to depthMode
                    // for nonzero requests, and routes mode 0 through the
                    // BasiliskII rejection path that leaves depthMode alone,
                    // keeps the main GDevice gdMode unchanged, and does not
                    // run the framebuffer-reset helper.
                    0x0A11 => {
                        let depth_mode_ptr = bus.read_long(sp + 8);
                        let mode = bus.read_long(sp + 12);
                        let gdh = bus.read_long(sp + 16);
                        let gd = if gdh != 0 { bus.read_long(gdh) } else { 0 };
                        let current_mode = if gd != 0 { bus.read_long(gd + 42) } else { 0 };
                        let result = if mode == 0 {
                            DM_SET_DISPLAY_MODE_REJECT_ERR
                        } else {
                            if depth_mode_ptr != 0 {
                                bus.write_long(depth_mode_ptr, current_mode);
                            }
                            if gd != 0 {
                                bus.write_long(gd + 42, mode);
                            }
                            let (width, height) = Self::display_mode_geometry(mode)
                                .unwrap_or_else(|| self.current_screen_geometry());
                            self.do_setdepth_with_geometry(cpu, bus, 8, width, height);
                            0
                        };
                        bus.write_word(sp + 20, result as u16);
                        cpu.write_reg(Register::A7, sp + 20);
                        cpu.write_reg(Register::D0, result as i32 as u32);
                    }
                    // DMCheckDisplayMode(..., UInt32 *switchFlags, ..., Boolean *modeOk)
                    // -> OSErr. Single-screen model: every 8bpp-style request
                    // we expose is switchable now.
                    0x0C12 => {
                        let mode_ok_ptr = bus.read_long(sp);
                        let switch_flags_ptr = bus.read_long(sp + 8);
                        if switch_flags_ptr != 0 {
                            bus.write_long(switch_flags_ptr, 0);
                        }
                        if mode_ok_ptr != 0 {
                            bus.write_byte(mode_ok_ptr, 1);
                        }
                        bus.write_word(sp + 24, 0);
                        cpu.write_reg(Register::A7, sp + 24);
                        cpu.write_reg(Register::D0, 0);
                    }
                    // DMGetDisplayMode(GDHandle theDevice, VDSwitchInfoPtr switchInfo)
                    // -> OSErr. VDSwitchInfoRec layout from Video.h:
                    //   csMode.w, csData.l, csPage.w, csBaseAddr.l, csReserved.l.
                    // BasiliskII/System 7.5.3 reports csMode=$0085 and
                    // csData=$00000080 for the main 8bpp display. Systemless
                    // returns its guest framebuffer base instead of Basilisk's
                    // physical `$A0000000` so callers that use the returned
                    // pointer stay inside emulated RAM.
                    0x043E => {
                        let switch_info = bus.read_long(sp);
                        let device_gdh = bus.read_long(sp + 4);
                        let gdh = if device_gdh != 0 {
                            device_gdh
                        } else {
                            self.ensure_main_gdevice(bus)
                        };
                        let gd = if gdh != 0 { bus.read_long(gdh) } else { 0 };
                        let current_mode = if gd != 0 {
                            bus.read_long(gd + 42)
                        } else {
                            0x0000_0085
                        };
                        if switch_info != 0 {
                            let mut screen_base = self.screen_mode.0;
                            if screen_base == 0 {
                                let gd = bus.read_long(gdh);
                                let pm_handle = bus.read_long(gd + 22);
                                let pm = bus.read_long(pm_handle);
                                screen_base = bus.read_long(pm);
                                self.screen_mode = (
                                    screen_base,
                                    ORACLE_MACHINE_PROFILE.screen_row_bytes(),
                                    ORACLE_MACHINE_PROFILE.screen_width,
                                    ORACLE_MACHINE_PROFILE.screen_height,
                                    ORACLE_MACHINE_PROFILE.screen_depth,
                                );
                            }
                            bus.write_word(switch_info, current_mode as u16);
                            bus.write_long(switch_info + 2, 0x0000_0080);
                            bus.write_word(switch_info + 6, 0);
                            bus.write_long(switch_info + 8, screen_base);
                            bus.write_long(switch_info + 12, 0);
                        }
                        bus.write_word(sp + 8, 0);
                        cpu.write_reg(Register::A7, sp + 8);
                        cpu.write_reg(Register::D0, 0);
                    }
                    _ => {
                        let arg_bytes = u32::from((cpu.read_reg(Register::D0) as u16) >> 8) * 2;
                        if arg_bytes != 0 {
                            bus.write_word(sp + arg_bytes, (-50i16) as u16); // paramErr
                            cpu.write_reg(Register::A7, sp + arg_bytes);
                        } else {
                            cpu.write_reg(Register::A7, sp + 2);
                        }
                    }
                }
                Ok(())
            }

            // StdOpcodeProc ($ABF8)
            // Default picture-opcode handler invoked during picture playback.
            // PROCEDURE StdOpcodeProc(dataPtr: Ptr; opcode: INTEGER);
            // Imaging With QuickDraw (1994), p. 7-82.
            // BasiliskII-backed probe: payload unchanged, and Systemless's
            // public call surface needs a 10-byte A7 unwind here to match the
            // observed +4 StackSpace sandwich.
            (true, 0x3F8) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 10);
                Ok(())
            }

            // ========== Stubs — Miscellaneous ==========

            // GetCWMgrPort ($AA48)
            // Returns the Window Manager's CGrafPort in the VAR parameter.
            // On Color QuickDraw systems the Window Manager port is the
            // CGrafPort-backed WMgrCPort; this HLE returns a separate
            // color-port clone so callers do not alias the low-memory
            // WMgrPort record.
            // PROCEDURE GetCWMgrPort(VAR wMgrCPort: CGrafPtr);
            // Macintosh Toolbox Essentials (1992), pp. 4-113 to 4-114.
            // Stack: SP+0: wMgrCPort_ptr(4). Pop 4.
            //
            // Contract coverage:
            //   quickdraw::tests::getcwmgrport_writes_wmgrcport_pointer_and_consumes_argument
            //   quickdraw::tests::getcwmgrport_nil_var_pointer_is_safe_and_still_pops_argument
            // GetCWMgrPort ($AA48): Writes the color Window Manager port clone via VAR ptr.
            (true, 0x248) => {
                let sp = cpu.read_reg(Register::A7);
                let var_ptr = bus.read_long(sp);
                if var_ptr != 0 {
                    let wmgr_port = self.ensure_color_window_manager_port(bus);
                    bus.write_long(var_ptr, wmgr_port);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // UpdatePixMap ($AA38)
            // Updates a PixMap in place after the underlying ColorTable changes.
            // The legacy ROM patch is effectively a no-op RTS; the fixture
            // witnesses only the caller-observable wrapper behaviour.
            // PROCEDURE UpdatePixMap(pmHandle: PixMapHandle);
            // Inside Macintosh Volume V, V-58
            // Stack: SP+0: pmHandle(4). Leave A7 unchanged.
            // UpdatePixMap ($AA38): no PixMap contents are mutated in HLE.
            (true, 0x238) => Ok(()),

            // Unimplemented ($A89F)
            // Vector reached when an unimplemented trap is called. No-op stub —
            // callers expect no stack adjustment.
            // Inside Macintosh Volume I, I-89
            // Unimplemented ($A89F): Vector for unimplemented traps; returns
            // noErr with no stack adjustment so probes that compare against
            // this address don't crash per IM:I I-89
            (true, 0x09F) => {
                cpu.write_reg(Register::D0, 0);
                Ok(())
            }

            // Layout ($A8F7)
            // The January 1997 Mac OS SDK's Traps.h names this slot `_Layout`
            // at line 216. Older trap tables list A8F7 as RESERVED, but
            // BasiliskII returns D0 = -64 here, so Systemless mirrors that
            // behavior.
            (true, 0x0F7) => {
                cpu.write_reg(Register::D0, (-64i32) as u32);
                Ok(())
            }

            // ========== More Stubs ==========

            // ScrnBitMap ($A833)
            // Return the current screen BitMap record.
            //
            // FUNCTION ScrnBitMap: BitMap;                              $A833
            //
            // Per IM:IV IV-21 "ScrnBitMap is a low-level routine that
            // returns the current screen BitMap record."  Per IM:I I-163
            // QuickDraw's `screenBits` global describes the entire
            // active screen with baseAddr pointing at the screen's
            // frame buffer, rowBytes giving the byte stride per row,
            // and bounds = (0, 0, screen_height, screen_width).
            //
            // MPW Universal Headers do NOT expose ScrnBitMap as an
            // ordinary FUNCTION declaration because normal Pascal/C
            // code reads `screenBits` (a QuickDraw global) directly
            // rather than invoking the trap.  Callers that DO use the
            // trap declare an inline-A-line thunk:
            //
            //   pascal void kx_ScrnBitMap(BitMap *bm) = { 0xA833 };
            //
            // Pascal FUNCTION-result-by-structure stack layout:
            //   sp+0..3   result_ptr  (Ptr to caller-allocated 14-byte
            //                          BitMap, shallowest = pushed last)
            // 4-byte arg pop, no separate result slot (the trap writes
            // through result_ptr).  No registers consumed.
            //
            // BitMap record (14 bytes) written by ScrnBitMap:
            //   offset 0..3   baseAddr  Ptr to screen frame buffer
            //   offset 4..5   rowBytes  bytes per row (high bit clear
            //                           for 1bpp BitMap, set for PixMap)
            //   offset 6..7   bounds.top    (0)
            //   offset 8..9   bounds.left   (0)
            //   offset 10..11 bounds.bottom (screen height)
            //   offset 12..13 bounds.right  (screen width)
            //
            // Systemless HLE reads dispatcher `screen_mode = (base, row,
            // width, height, depth)` and writes those values into the
            // caller-provided buffer.  BII reads the System 7.5
            // QuickDraw globals and writes its own baseAddr/rowBytes
            // pair.  Both engines agree on bounds = (0, 0, 600, 800)
            // for the FixtureRunner/InitFixtureScreen 800x600 screen.
            //
            // Engines-agree subset witnessed by the strict bake at
            // a833_scrnbitmap_strict (8 bands; 2 of
            // 2 golden assertions: bounds equality + non-sentinel
            // overwrite of baseAddr/rowBytes; 4-byte stack pop).
            //
            // Regression coverage:
            //   quickdraw::tests::scrnbitmap_writes_current_screen_bitmap_record_to_result_pointer
            //   quickdraw::tests::scrnbitmap_overwrites_pre_poisoned_sentinel_fields_with_screen_bitmap_record
            //   quickdraw::tests::scrnbitmap_consumes_result_pointer_argument_and_pops_4_bytes
            // ScrnBitMap ($A833): Writes the current screen `BitMap` record from dispatcher `screen_mode` into the caller-provided result buffer; 14-byte BitMap layout per IM:IV IV-21 + IM:I I-163
            (true, 0x033) => {
                let sp = cpu.read_reg(Register::A7);
                let result_ptr = bus.read_long(sp);
                let (base_addr, row_bytes, width, height, _) = self.screen_mode;
                if result_ptr != 0 {
                    bus.write_long(result_ptr, base_addr);
                    bus.write_word(result_ptr + 4, row_bytes as u16);
                    bus.write_word(result_ptr + 6, 0);
                    bus.write_word(result_ptr + 8, 0);
                    bus.write_word(result_ptr + 10, height);
                    bus.write_word(result_ptr + 12, width);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // CalcMask ($A838)
            // PROCEDURE CalcMask (srcPtr, dstPtr: Ptr;
            //                     srcRow, dstRow, height, words: INTEGER);
            // Inside Macintosh Volume IV (1986), p. IV-22:
            //   "Given a source bit image, CalcMask computes a destination
            //    bit image with 1's only in the pixels where paint could not
            //    leak from any of the outer edges, like the MacPaint lasso
            //    tool.  Calls to CalcMask are not clipped to the current
            //    port and are not stored into QuickDraw pictures."
            //
            // MPW Universal Headers (Quickdraw.h):
            //   EXTERN_API(void) CalcMask(const void *srcPtr, void *dstPtr,
            //                             short srcRow, short dstRow,
            //                             short height, short words)
            //                                              ONEWORDINLINE(0xA838);
            //
            // Pascal LR push order (first arg deepest):
            //   sp+0..1   words      (last pushed → shallowest)
            //   sp+2..3   height
            //   sp+4..5   dstRow
            //   sp+6..7   srcRow
            //   sp+8..11  dstPtr
            //   sp+12..15 srcPtr     (first pushed → deepest)
            // 16-byte total pop, no result slot (PROCEDURE).
            //
            // BII-vs-Systemless divergence: BII System 7.5.3 ROM walks the src
            // bit image and writes the lasso-style mask into dst per IM:IV
            // IV-22.  Systemless HLE is a no-op stub because masks are not
            // modeled at the QuickDraw bottleneck-proc layer (high-level
            // CopyMask reads/writes the bitmap directly via Rust code).
            // The strict bake at
            // a838_a839_calcmask_seedfill_strict witnesses only the
            // engines-agree subset (16-byte pop discipline) via empty
            // StackSpace sandwich since IM:IV IV-22 explicitly disclaims
            // clipping (the empty-clipRgn trick used by other bottleneck-
            // proc bakes does not apply).
            //
            // Runtime proof:
            //   a838_a839_calcmask_seedfill_strict
            //
            // Contract coverage:
            //   src/trap/quickdraw.rs::tests::calcmask_consumes_16_byte_argument_frame
            //   src/trap/quickdraw.rs::tests::calcmask_five_call_composition_pops_eighty_bytes_total
            (true, 0x038) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 16);
                Ok(())
            }

            // SeedFill ($A839)
            // PROCEDURE SeedFill (srcPtr, dstPtr: Ptr;
            //                     srcRow, dstRow, height, words,
            //                     seedH, seedV: INTEGER);
            // Inside Macintosh Volume IV (1986), p. IV-22:
            //   "Given a source bit image, SeedFill computes a destination
            //    bit image with 1's only in the pixels where paint can leak
            //    from the starting seed point, like the MacPaint paint-
            //    bucket tool.  SeedH and seedV specify horizontal and
            //    vertical offsets, in pixels, from the beginning of the
            //    data pointed to by dstPtr, determining how far into the
            //    destination bit image filling should begin.  Calls to
            //    SeedFill are not clipped to the current port and are not
            //    stored into QuickDraw pictures."
            //
            // MPW Universal Headers (Quickdraw.h):
            //   EXTERN_API(void) SeedFill(const void *srcPtr, void *dstPtr,
            //                             short srcRow, short dstRow,
            //                             short height, short words,
            //                             short seedH, short seedV)
            //                                              ONEWORDINLINE(0xA839);
            //
            // Pascal LR push order (first arg deepest):
            //   sp+0..1   seedV      (last pushed → shallowest)
            //   sp+2..3   seedH
            //   sp+4..5   words
            //   sp+6..7   height
            //   sp+8..9   dstRow
            //   sp+10..11 srcRow
            //   sp+12..15 dstPtr
            //   sp+16..19 srcPtr     (first pushed → deepest)
            // 20-byte total pop, no result slot (PROCEDURE).
            //
            // Systemless implements the default 1-bit paint-bucket mask:
            // contiguous pixels equal to the source seed bit are written as
            // 1s in dst, with all other bits in the requested rectangle
            // cleared. The call is raw-bitmap based, not port clipped.
            //
            // Runtime proof:
            //   a838_a839_calcmask_seedfill_strict
            //
            // Contract coverage:
            //   src/trap/quickdraw.rs::tests::seedfill_consumes_20_byte_argument_frame
            //   src/trap/quickdraw.rs::tests::seedfill_five_call_composition_pops_one_hundred_bytes_total
            //   src/trap/quickdraw.rs::tests::seedfill_writes_contiguous_matching_region_mask
            //   src/trap/quickdraw.rs::tests::seedfill_seed_outside_rect_clears_destination_mask
            (true, 0x039) => {
                let sp = cpu.read_reg(Register::A7);
                let seed_v = bus.read_word(sp) as i16;
                let seed_h = bus.read_word(sp + 2) as i16;
                let words = bus.read_word(sp + 4) as i16;
                let height = bus.read_word(sp + 6) as i16;
                let dst_row = bus.read_word(sp + 8) as i16;
                let src_row = bus.read_word(sp + 10) as i16;
                let dst_ptr = bus.read_long(sp + 12);
                let src_ptr = bus.read_long(sp + 16);
                Self::seed_fill_1bpp_mask(
                    bus, src_ptr, dst_ptr, src_row, dst_row, height, words, seed_h, seed_v,
                );
                cpu.write_reg(Register::A7, sp + 20);
                Ok(())
            }

            // AnimateEntry ($AA99)
            // Changes the RGB value of dstEntry in the palette associated
            // with dstWindow to srcRGB. Any device indexes reserved for
            // the entry are updated immediately (via activate_palette_for_
            // window). Single-entry variant of AnimatePalette.
            // PROCEDURE AnimateEntry(dstWindow: WindowPtr; dstEntry: INTEGER;
            //                        srcRGB: RGBColor);
            // Inside Macintosh Volume V, V-159
            // Stack: SP+0: srcRGB_ptr(4), SP+4: dstEntry(2), SP+6: dstWindow(4). Pops 10.
            //
            // Regression coverage:
            //   animateentry_pops_ten_bytes
            //   animateentry_updates_palette_entry_rgb
            //   animateentry_preserves_usage_and_tolerance
            //   animateentry_out_of_range_entry_is_noop
            //   animateentry_with_no_palette_is_noop
            // AnimateEntry ($AA99): Writes the supplied RGB to the window's palette ColorInfo (preserving usage/tolerance) then calls activate_palette_for_window; out-of-range or missing-palette is a no-op, IM:V V-159
            (true, 0x299) => {
                let sp = cpu.read_reg(Register::A7);
                let src_rgb_ptr = bus.read_long(sp);
                let dst_entry = bus.read_word(sp + 4) as i16;
                let window = bus.read_long(sp + 6);
                cpu.write_reg(Register::A7, sp + 10);

                if src_rgb_ptr == 0 || dst_entry < 0 {
                    return Some(Ok(()));
                }
                let palette = self.window_palette_handle(window);
                let palette_ptr = Self::palette_ptr(bus, palette);
                if palette_ptr == 0 {
                    return Some(Ok(()));
                }
                let entries = Self::palette_entry_count(bus, palette);
                if (dst_entry as u16) >= entries {
                    return Some(Ok(()));
                }

                let rgb = [
                    bus.read_word(src_rgb_ptr),
                    bus.read_word(src_rgb_ptr + 2),
                    bus.read_word(src_rgb_ptr + 4),
                ];
                let (_, usage, tolerance) = Self::read_palette_color_info(
                    bus,
                    palette,
                    dst_entry as u16,
                )
                .unwrap_or(([0, 0, 0], PM_TOLERANT, 0));
                Self::write_palette_color_info(
                    bus,
                    palette_ptr,
                    dst_entry as u32,
                    rgb,
                    usage,
                    tolerance,
                );
                self.activate_associated_palette_for_window(bus, window);
                Ok(())
            }

            _ => return None,
        })
    }

    /// OpenCPort-specific allocation for CGrafPort-owned storage.
    /// Imaging With QuickDraw 1994 pp. 4-64 to 4-65:
    /// OpenCPort allocates storage, then calls InitCPort.
    fn allocate_cport_storage(&mut self, port: u32, bus: &mut MacMemoryBus) {
        let gd_handle = self.active_gdevice_or_main(bus);
        let gd_ptr = bus.read_long(gd_handle);

        // Read GDevice rect for region initialization.
        let gd_top = bus.read_word(gd_ptr + 34) as i16;
        let gd_left = bus.read_word(gd_ptr + 36) as i16;
        let gd_bottom = bus.read_word(gd_ptr + 38) as i16;
        let gd_right = bus.read_word(gd_ptr + 40) as i16;

        // Read GDevice's PixMap to seed the newly allocated port PixMap.
        let gd_pmap_handle = bus.read_long(gd_ptr + 22);
        let gd_pmap = bus.read_long(gd_pmap_handle);

        // Allocate a new PixMap, copying fields from the GDevice's PixMap.
        let pixmap = bus.alloc(50);
        for i in 0..50u32 {
            bus.write_byte(pixmap + i, bus.read_byte(gd_pmap + i));
        }
        let pixmap_handle = bus.alloc(4);
        bus.write_long(pixmap_handle, pixmap);
        self.cache_portbits_pixmap_handle(bus, port + 2, pixmap_handle);

        // Allocate visRgn = GDevice rect
        let vis_rgn_ptr = bus.alloc(10);
        bus.write_word(vis_rgn_ptr, 10); // rgnSize
        bus.write_word(vis_rgn_ptr + 2, gd_top as u16);
        bus.write_word(vis_rgn_ptr + 4, gd_left as u16);
        bus.write_word(vis_rgn_ptr + 6, gd_bottom as u16);
        bus.write_word(vis_rgn_ptr + 8, gd_right as u16);
        let vis_rgn = bus.alloc(4);
        bus.write_long(vis_rgn, vis_rgn_ptr);

        // Allocate clipRgn = infinite rect (-32767..32767)
        let clip_rgn_ptr = bus.alloc(10);
        bus.write_word(clip_rgn_ptr, 10);
        bus.write_word(clip_rgn_ptr + 2, (-32767_i16) as u16);
        bus.write_word(clip_rgn_ptr + 4, (-32767_i16) as u16);
        bus.write_word(clip_rgn_ptr + 6, 32767_u16);
        bus.write_word(clip_rgn_ptr + 8, 32767_u16);
        let clip_rgn = bus.alloc(4);
        bus.write_long(clip_rgn, clip_rgn_ptr);

        // Mark as CGrafPort and install allocated handles.
        bus.write_word(port + 6, 0xC000); // +6  portVersion high bits set
        bus.write_long(port + 2, pixmap_handle); // +2  portPixMap
        bus.write_long(port + 24, vis_rgn); // +24 visRgn
        bus.write_long(port + 28, clip_rgn); // +28 clipRgn
    }

    /// Initialize an existing CGrafPort with default values.
    /// Imaging With QuickDraw 1994 p. 4-66:
    /// InitCPort does not allocate storage and no-ops on non-CGrafPorts.
    fn init_cport(
        &mut self,
        port: u32,
        bus: &mut MacMemoryBus,
        allow_seeded_clut_clone: bool,
    ) -> bool {
        if (bus.read_word(port + 6) & 0xC000) != 0xC000 {
            return false;
        }

        let pixmap_handle = bus.read_long(port + 2);
        let vis_rgn = bus.read_long(port + 24);
        let clip_rgn = bus.read_long(port + 28);
        if pixmap_handle == 0 || vis_rgn == 0 || clip_rgn == 0 {
            return false;
        }

        let pixmap = bus.read_long(pixmap_handle);
        let vis_rgn_ptr = bus.read_long(vis_rgn);
        let clip_rgn_ptr = bus.read_long(clip_rgn);
        if pixmap == 0 || vis_rgn_ptr == 0 || clip_rgn_ptr == 0 {
            return false;
        }

        let gd_handle = self.active_gdevice_or_main(bus);
        let gd_ptr = bus.read_long(gd_handle);

        // Read GDevice rect for portRect and visRgn.
        let gd_top = bus.read_word(gd_ptr + 34) as i16;
        let gd_left = bus.read_word(gd_ptr + 36) as i16;
        let gd_bottom = bus.read_word(gd_ptr + 38) as i16;
        let gd_right = bus.read_word(gd_ptr + 40) as i16;

        // Copy the current device PixMap into the existing port PixMap.
        let gd_pmap_handle = bus.read_long(gd_ptr + 22);
        let gd_pmap = bus.read_long(gd_pmap_handle);
        for i in 0..50u32 {
            bus.write_byte(pixmap + i, bus.read_byte(gd_pmap + i));
        }
        if allow_seeded_clut_clone {
            if let Some(clut) = self.active_seeded_screen_clut_for_offscreen_clone(gd_handle, 0) {
                let depth = bus.read_word(pixmap + 32) as u32;
                let ctab_handle =
                    self.allocate_color_table_handle_with_clut(bus, depth, &clut, 0x8000);
                bus.write_long(pixmap + 42, ctab_handle);
            }
        }

        // Reinitialize visRgn to the current-device bounds.
        bus.write_word(vis_rgn_ptr, 10);
        bus.write_word(vis_rgn_ptr + 2, gd_top as u16);
        bus.write_word(vis_rgn_ptr + 4, gd_left as u16);
        bus.write_word(vis_rgn_ptr + 6, gd_bottom as u16);
        bus.write_word(vis_rgn_ptr + 8, gd_right as u16);

        // Reinitialize clipRgn to QuickDraw's infinite rectangle.
        bus.write_word(clip_rgn_ptr, 10);
        bus.write_word(clip_rgn_ptr + 2, (-32767_i16) as u16);
        bus.write_word(clip_rgn_ptr + 4, (-32767_i16) as u16);
        bus.write_word(clip_rgn_ptr + 6, 32767_u16);
        bus.write_word(clip_rgn_ptr + 8, 32767_u16);

        // CGrafPort field layout per Imaging With QuickDraw 1994, 4-125.
        bus.write_word(port, 0); // +0  device
        bus.write_long(port + 2, pixmap_handle); // +2  portPixMap (handle)
        bus.write_word(port + 6, 0xC000); // +6  portVersion
        bus.write_long(port + 8, 0); // +8  grafVars (handle) - not modeled yet
        bus.write_word(port + 12, 0); // +12 chExtra
        bus.write_word(port + 14, 0x8000); // +14 pnLocHFrac (0.5 fixed)
                                           // portRect = GDevice rect
        bus.write_word(port + 16, gd_top as u16); // +16 portRect.top
        bus.write_word(port + 18, gd_left as u16); // +18 portRect.left
        bus.write_word(port + 20, gd_bottom as u16); // +20 portRect.bottom
        bus.write_word(port + 22, gd_right as u16); // +22 portRect.right
        self.cache_portbits_pixmap_handle(bus, port + 2, pixmap_handle);
        bus.write_long(port + 24, vis_rgn); // +24 visRgn
        bus.write_long(port + 28, clip_rgn); // +28 clipRgn
        self.init_cgraf_port_defaults(port, bus);
        true
    }

    fn active_gdevice_or_main(&mut self, bus: &mut MacMemoryBus) -> u32 {
        if Self::gdevice_pixmap_handle(bus, self.current_gdevice) != 0 {
            self.current_gdevice
        } else {
            self.ensure_main_gdevice(bus)
        }
    }

    fn normalized_itable_resolution(bus: &MacMemoryBus, gdh: u32, requested_res: u16) -> u16 {
        let device_res = if requested_res == 0 && gdh != 0 {
            let gd = bus.read_long(gdh);
            if gd != 0 {
                bus.read_word(gd + 10)
            } else {
                0
            }
        } else {
            requested_res
        };

        if (3..=5).contains(&device_res) {
            device_res
        } else {
            4
        }
    }

    fn inverse_table_byte_count(res: u16) -> usize {
        1usize << (usize::from(res) * 3)
    }

    fn build_inverse_table_bytes(clut: &[[u16; 3]; 256], res: u16) -> Vec<u8> {
        if res == 4 && Self::uses_canonical_system_8bpp_clut(clut) {
            return Self::standard_mac_8bpp_itable().to_vec();
        }

        let entries = Self::inverse_table_byte_count(res);
        let mut table = vec![0u8; entries];
        let res_bits = u32::from(res);
        let mask = (1u32 << res_bits) - 1;
        let shift = 16u32 - res_bits;
        let centre = 1u32 << (15u32 - res_bits);

        for cell in 0u32..entries as u32 {
            let qb = cell & mask;
            let qg = (cell >> res_bits) & mask;
            let qr = (cell >> (res_bits * 2)) & mask;
            let cr = ((qr << shift) | centre) as i64;
            let cg = ((qg << shift) | centre) as i64;
            let cb = ((qb << shift) | centre) as i64;
            let mut best_idx = 0u8;
            let mut best_dist = i64::MAX;
            for (idx, entry) in clut.iter().enumerate() {
                let dr = cr - i64::from(entry[0]);
                let dg = cg - i64::from(entry[1]);
                let db = cb - i64::from(entry[2]);
                let dist = dr * dr + dg * dg + db * db;
                if dist < best_dist {
                    best_dist = dist;
                    best_idx = idx as u8;
                }
            }
            table[cell as usize] = best_idx;
        }

        table
    }

    fn cached_inverse_table_bytes(&mut self, clut: &[[u16; 3]; 256], res: u16) -> Vec<u8> {
        if let Some(pos) = self
            .inverse_table_cache
            .iter()
            .position(|entry| entry.res == res && entry.clut == *clut)
        {
            let entry = self.inverse_table_cache.remove(pos);
            let bytes = entry.bytes.clone();
            self.inverse_table_cache.push(entry);
            return bytes;
        }

        let bytes = Self::build_inverse_table_bytes(clut, res);
        self.inverse_table_cache.push(InverseTableCacheEntry {
            res,
            clut: *clut,
            bytes: bytes.clone(),
        });
        if self.inverse_table_cache.len() > INVERSE_TABLE_CACHE_LIMIT {
            self.inverse_table_cache.remove(0);
        }
        bytes
    }

    fn usable_itable_handle(bus: &mut MacMemoryBus, handle: u32) -> u32 {
        if handle == 0 || bus.get_alloc_size(handle).is_none() {
            return 0;
        }

        let ptr = bus.read_long(handle);
        if ptr != 0 {
            if bus.get_alloc_size(ptr).is_some() {
                return handle;
            }
            return 0;
        }

        let ptr = bus.alloc(0);
        bus.write_long(handle, ptr);
        handle
    }

    fn ensure_gdevice_itable_handle(bus: &mut MacMemoryBus, gdh: u32) -> u32 {
        if gdh == 0 {
            return 0;
        }
        let gd = bus.read_long(gdh);
        if gd == 0 {
            return 0;
        }

        let existing = bus.read_long(gd + 6);
        if Self::usable_itable_handle(bus, existing) != 0 {
            return existing;
        }

        let ptr = bus.alloc(0);
        let handle = bus.alloc(4);
        bus.write_long(handle, ptr);
        bus.write_long(gd + 6, handle);
        handle
    }

    fn make_inverse_table(
        &mut self,
        bus: &mut MacMemoryBus,
        ctab_handle: u32,
        itab_handle: u32,
        requested_res: u16,
    ) {
        let mut current_gdh = 0;
        let target_handle = if itab_handle != 0 {
            Self::usable_itable_handle(bus, itab_handle)
        } else {
            current_gdh = self.active_gdevice_or_main(bus);
            Self::ensure_gdevice_itable_handle(bus, current_gdh)
        };
        if target_handle == 0 {
            return;
        }

        let explicit_ctab = Self::color_table_ptr(bus, ctab_handle) != 0;
        if !explicit_ctab || requested_res == 0 {
            current_gdh = self.active_gdevice_or_main(bus);
        }

        let effective_ctab_handle = if explicit_ctab {
            ctab_handle
        } else {
            Self::gdevice_ctab_handle(bus, current_gdh)
        };
        let clut = self.read_ctab_handle_clut(bus, effective_ctab_handle);
        let seed = Self::ctab_seed(bus, effective_ctab_handle).unwrap_or(0);
        let res = Self::normalized_itable_resolution(bus, current_gdh, requested_res);
        let table = self.cached_inverse_table_bytes(&clut, res);
        let record_size = 6 + table.len() as u32;

        let mut ptr = bus.read_long(target_handle);
        if ptr == 0 {
            ptr = bus.alloc(record_size);
            bus.write_long(target_handle, ptr);
        } else {
            ptr = self.resize_handle_allocation(bus, target_handle, record_size);
        }
        if ptr == 0 {
            return;
        }

        bus.write_long(ptr, seed);
        bus.write_word(ptr + 4, res);
        bus.write_bytes(ptr + 6, &table);
    }

    fn extend_recording_bbox(
        recording: &mut super::dispatch::RegionRecording,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) {
        recording.bbox = Some(match recording.bbox {
            Some((current_top, current_left, current_bottom, current_right)) => (
                current_top.min(top),
                current_left.min(left),
                current_bottom.max(bottom),
                current_right.max(right),
            ),
            None => (top, left, bottom, right),
        });
    }

    fn add_recording_rows(
        recording: &mut super::dispatch::RegionRecording,
        rows_top: i16,
        rows: &[Vec<i16>],
    ) {
        for (row_index, row) in rows.iter().enumerate() {
            if row.is_empty() {
                continue;
            }
            let y = rows_top + row_index as i16;
            let left = row[0];
            let right = *row.last().unwrap();
            let current = recording.filled_rows.entry(y).or_default();
            *current = Self::union_region_rows(current, row);
            Self::extend_recording_bbox(recording, y, left, y + 1, right);
        }
    }

    fn record_region_rect_boundary(
        recording: &mut super::dispatch::RegionRecording,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) {
        if bottom <= top || right <= left {
            return;
        }
        recording.outline_segments.extend([
            ((top, left), (top, right)),
            ((top, right), (bottom, right)),
            ((bottom, right), (bottom, left)),
            ((bottom, left), (top, left)),
        ]);
        Self::extend_recording_bbox(recording, top, left, bottom, right);
    }

    /// While OpenRgn..CloseRgn is in progress, rectangular drawing
    /// primitives contribute their mathematical boundary and skip the actual
    /// framebuffer write. Returns true if recording is active.
    pub(crate) fn extend_recording_region(
        &mut self,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) -> bool {
        if let Some(recording) = self.recording_region.as_mut() {
            Self::record_region_rect_boundary(recording, top, left, bottom, right);
            true
        } else {
            false
        }
    }

    pub(crate) fn record_region_line(&mut self, v0: i16, h0: i16, v1: i16, h1: i16) -> bool {
        if let Some(recording) = self.recording_region.as_mut() {
            recording.outline_segments.push(((v0, h0), (v1, h1)));
            Self::extend_recording_bbox(recording, v0.min(v1), h0.min(h1), v0.max(v1), h0.max(h1));
            true
        } else {
            false
        }
    }

    /// Dereference a PolyHandle and extend the recording region by the
    /// polygon's stored polyBBox (4 words at poly_ptr+2). NIL handles and
    /// empty bboxes just report the recording state without mutating the
    /// bbox. Returns true if recording is active (caller should skip the
    /// draw).
    pub(crate) fn extend_recording_region_from_poly(
        &mut self,
        bus: &mut MacMemoryBus,
        poly_handle: u32,
    ) -> bool {
        if self.recording_region.is_none() {
            return false;
        }
        if poly_handle == 0 {
            return true;
        }
        let poly_ptr = bus.read_long(poly_handle);
        if poly_ptr == 0 {
            return true;
        }
        let top = bus.read_word(poly_ptr + 2) as i16;
        let left = bus.read_word(poly_ptr + 4) as i16;
        let bottom = bus.read_word(poly_ptr + 6) as i16;
        let right = bus.read_word(poly_ptr + 8) as i16;
        if bottom > top && right > left {
            self.extend_recording_region(top, left, bottom, right);
        }
        true
    }

    /// Dereference a RgnHandle and extend the recording region by the
    /// region's stored bbox (4 words at rgn_ptr+2). NIL handles and empty
    /// regions just report the recording state without mutating the bbox.
    /// Returns true if recording is active (caller should skip the draw).
    pub(crate) fn extend_recording_region_from_rgn(
        &mut self,
        bus: &mut MacMemoryBus,
        rgn_handle: u32,
    ) -> bool {
        if self.recording_region.is_none() {
            return false;
        }
        let Some((top, left, bottom, right)) = Self::region_bbox(bus, rgn_handle) else {
            return true;
        };
        if bottom <= top || right <= left {
            return true;
        }
        let rows = Self::region_rows_for_band(bus, rgn_handle, top, bottom);
        if let Some(recording) = self.recording_region.as_mut() {
            Self::add_recording_rows(recording, top, &rows);
        }
        true
    }

    pub(crate) fn init_cgraf_port_defaults(&mut self, port: u32, bus: &mut MacMemoryBus) {
        bus.write_long(port + 32, 0); // +32 bkPixPat
                                      // rgbFgColor = black (0,0,0)
        bus.write_word(port + 36, 0); // +36 rgbFgColor.red
        bus.write_word(port + 38, 0); // +38 rgbFgColor.green
        bus.write_word(port + 40, 0); // +40 rgbFgColor.blue
                                      // rgbBkColor = white (0xFFFF, 0xFFFF, 0xFFFF)
        bus.write_word(port + 42, 0xFFFF); // +42 rgbBkColor.red
        bus.write_word(port + 44, 0xFFFF); // +44 rgbBkColor.green
        bus.write_word(port + 46, 0xFFFF); // +46 rgbBkColor.blue
                                           // pnLoc = (0,0)
        bus.write_word(port + 48, 0); // +48 pnLoc.v
        bus.write_word(port + 50, 0); // +50 pnLoc.h
                                      // pnSize = (1,1)
        bus.write_word(port + 52, 1); // +52 pnSize.v
        bus.write_word(port + 54, 1); // +54 pnSize.h
        bus.write_word(port + 56, 8); // +56 pnMode (patCopy)
        bus.write_long(port + 58, 0); // +58 pnPixPat
        bus.write_long(port + 62, 0); // +62 fillPixPat
        bus.write_word(port + 66, 0); // +66 pnVis
        bus.write_word(port + 68, 0); // +68 txFont
        bus.write_word(port + 70, 0); // +70 txFace
        bus.write_word(port + 72, 1); // +72 txMode (srcOr)
        bus.write_word(port + 74, 0); // +74 txSize
        bus.write_long(port + 76, 0); // +76 spExtra
        bus.write_long(port + 80, 0x00000021); // +80 fgColor (blackColor=33)
        bus.write_long(port + 84, 0x0000001E); // +84 bkColor (whiteColor=30)
        bus.write_word(port + 88, 0); // +88 colrBit
        bus.write_word(port + 90, 0); // +90 patStretch
        bus.write_long(port + 92, 0); // +92 picSave
        bus.write_long(port + 96, 0); // +96 rgnSave
        bus.write_long(port + 100, 0); // +100 polySave
        bus.write_long(port + 104, 0); // +104 grafProcs
        self.port_draw_states.insert(port, PortDrawState::default());
    }

    /// Allocate the main GDevice in guest memory if not yet done.
    pub fn ensure_main_gdevice(&mut self, bus: &mut MacMemoryBus) -> u32 {
        if self.main_gdevice_handle != 0 {
            return self.main_gdevice_handle;
        }

        // Allocate ColorTable for 8-bit (256 entries)
        // ColorTable record: ctSeed(4) + ctFlags(2) + ctSize(2) + ctTable(256*8)
        // Inside Macintosh Volume V, V-48, V-135
        let ct_size: u32 = 8 + 256 * 8; // 2056 bytes
        let ctab = bus.alloc(ct_size);
        // Executor's C_NewGWorld (qGWorld.cpp:217) initialises the
        // GDevice-backing CTab with `CTAB_SEED(ctab) = (int32_t)depth`
        // — a deterministic seed that matches the authoring convention
        // games use for PICTs targeting the standard N-bpp palette.
        // EV's landing PICT (and most other canonical-palette PICTs)
        // ship with ctSeed=8 (depth). Initialising here with 8 lets the
        // PICT seed-match identity gate fire without remap on
        // those authored-for-standard PICTs.
        bus.write_long(ctab, 8); // ctSeed (+0, 4 bytes): depth convention
        bus.write_word(ctab + 4, 0x8000); // ctFlags (+4, 2 bytes): bit 15 = device color table
        bus.write_word(ctab + 6, 255); // ctSize (+6, 2 bytes): number of entries - 1
                                       // Fill with the standard Mac 8bpp color table
                                       // (matches device_clut initialization).
        {
            let std_clut = Self::standard_mac_8bpp_clut();
            for i in 0u32..256 {
                let entry = ctab + 8 + i * 8;
                bus.write_word(entry, i as u16); // value (index)
                bus.write_word(entry + 2, std_clut[i as usize][0]); // red
                bus.write_word(entry + 4, std_clut[i as usize][1]); // green
                bus.write_word(entry + 6, std_clut[i as usize][2]); // blue
            }
        }
        let ctab_handle = bus.alloc(4);
        bus.write_long(ctab_handle, ctab);

        // Allocate PixMap for the main screen (800x600 @ 8bpp)
        // PixMap record: Imaging With QuickDraw 1994, p.4-5
        let pixmap = bus.alloc(50);
        let screen_base = bus.ram_size() - 0x80000; // 512KB for 800x600x8bpp
        bus.write_long(pixmap, screen_base); // baseAddr (+0)
        bus.write_word(
            pixmap + 4,
            (ORACLE_MACHINE_PROFILE.screen_row_bytes() as u16) | 0x8000,
        ); // rowBytes (+4) with pixmap flag
        bus.write_word(pixmap + 6, 0); // bounds.top (+6)
        bus.write_word(pixmap + 8, 0); // bounds.left (+8)
        bus.write_word(pixmap + 10, ORACLE_MACHINE_PROFILE.screen_height); // bounds.bottom (+10)
        bus.write_word(pixmap + 12, ORACLE_MACHINE_PROFILE.screen_width); // bounds.right (+12)
        bus.write_word(pixmap + 14, 0); // pmVersion (+14)
        bus.write_word(pixmap + 16, 0); // packType (+16)
        bus.write_long(pixmap + 18, 0); // packSize (+18)
        bus.write_long(pixmap + 22, 0x00480000); // hRes (+22) = 72.0 fixed
        bus.write_long(pixmap + 26, 0x00480000); // vRes (+26) = 72.0 fixed
        bus.write_word(pixmap + 30, 0); // pixelType (+30) (chunky)
        bus.write_word(pixmap + 32, 8); // pixelSize (+32) (8bpp)
        bus.write_word(pixmap + 34, 1); // cmpCount (+34)
        bus.write_word(pixmap + 36, 8); // cmpSize (+36)
        bus.write_long(pixmap + 38, 0); // planeBytes (+38)
        bus.write_long(pixmap + 42, ctab_handle); // pmTable (+42) = CTabHandle
        bus.write_long(pixmap + 46, 0); // pmReserved (+46)

        let pixmap_handle = bus.alloc(4);
        bus.write_long(pixmap_handle, pixmap);

        // Allocate GDevice record (50 bytes)
        let gd = bus.alloc(50);
        bus.write_word(gd, 0); // gdRefNum
        bus.write_word(gd + 2, 0); // gdID
        bus.write_word(gd + 4, 0); // gdType (CLUT)
        bus.write_long(gd + 6, 0); // gdITable
        bus.write_word(gd + 10, 4); // gdResPref
        bus.write_long(gd + 12, 0); // gdSearchProc
        bus.write_long(gd + 16, 0); // gdCompProc
                                    // gdFlags per Inside Macintosh: Imaging with QuickDraw
                                    // bit 0 = gdDevType (1=color), bit 10 = ramInit, bit 11 = mainScreen,
                                    // bit 12 = allInit, bit 13 = screenDevice, bit 15 = screenActive
        let flags: u16 = (1 << 0) | (1 << 10) | (1 << 11) | (1 << 12) | (1 << 13) | (1 << 15);
        bus.write_word(gd + 20, flags); // gdFlags
        bus.write_long(gd + 22, pixmap_handle); // gdPMap
        bus.write_long(gd + 26, 0); // gdRefCon
        bus.write_long(gd + 30, 0); // gdNextGD = NULL (end of chain)
        bus.write_word(gd + 34, 0); // gdRect.top
        bus.write_word(gd + 36, 0); // gdRect.left
        bus.write_word(gd + 38, ORACLE_MACHINE_PROFILE.screen_height); // gdRect.bottom
        bus.write_word(gd + 40, ORACLE_MACHINE_PROFILE.screen_width); // gdRect.right
        bus.write_long(gd + 42, 0x0000_0085); // gdMode (current display mode token)

        let gd_handle = bus.alloc(4);
        bus.write_long(gd_handle, gd);

        self.main_gdevice_handle = gd_handle;
        self.current_gdevice = gd_handle;

        gd_handle
    }

    fn allocate_detached_gdevice(&mut self, bus: &mut MacMemoryBus) -> u32 {
        let main_gdh = self.ensure_main_gdevice(bus);
        let main_gd = bus.read_long(main_gdh);
        let main_pmh = bus.read_long(main_gd + 22);
        let main_pm = bus.read_long(main_pmh);
        let main_ctab_handle = bus.read_long(main_pm + 42);
        let main_ctab = bus.read_long(main_ctab_handle);
        let ctab_entries = u32::from(bus.read_word(main_ctab + 6)) + 1;
        let ctab_size = 8 + ctab_entries * 8;

        let new_ctab = bus.alloc(ctab_size);
        for off in 0..ctab_size {
            bus.write_byte(new_ctab + off, bus.read_byte(main_ctab + off));
        }
        let new_ctab_handle = bus.alloc(4);
        bus.write_long(new_ctab_handle, new_ctab);

        let new_pm = bus.alloc(50);
        for off in 0..50 {
            bus.write_byte(new_pm + off, bus.read_byte(main_pm + off));
        }
        bus.write_long(new_pm + 42, new_ctab_handle);
        let new_pmh = bus.alloc(4);
        bus.write_long(new_pmh, new_pm);

        let new_gd = bus.alloc(50);
        bus.write_word(new_gd, 0); // gdRefNum
        bus.write_word(new_gd + 2, 0); // gdID
        bus.write_word(new_gd + 4, 0); // gdType
        bus.write_long(new_gd + 6, 0); // gdITable
        bus.write_word(new_gd + 10, 4); // gdResPref
        bus.write_long(new_gd + 12, 0); // gdSearchProc
        bus.write_long(new_gd + 16, 0); // gdCompProc
        bus.write_word(new_gd + 20, 0); // gdFlags
        bus.write_long(new_gd + 22, new_pmh); // gdPMap
        bus.write_long(new_gd + 26, 0); // gdRefCon
        bus.write_long(new_gd + 30, 0); // gdNextGD
        for off in [34, 36, 38, 40] {
            bus.write_word(new_gd + off, bus.read_word(main_gd + off));
        }
        bus.write_long(new_gd + 42, 128); // default B/W mode per IWQD 5-25

        let new_gdh = bus.alloc(4);
        bus.write_long(new_gdh, new_gd);
        new_gdh
    }

    fn init_gdevice_minimal(&self, bus: &mut MacMemoryBus, gdh: u32, gd_ref_num: i16, mode: u32) {
        if gdh == 0 {
            return;
        }
        let gd = bus.read_long(gdh);
        if gd == 0 {
            return;
        }

        bus.write_word(gd, gd_ref_num as u16);
        bus.write_long(gd + 42, mode);

        // IWQD 5-25: mode 128 is the default black-and-white mode; other
        // non-driver modes expose color via gdDevType. Preserve other flags.
        let mut flags = bus.read_word(gd + 20);
        flags &= !1;
        if mode != 128 && mode != 0xFFFF_FFFF {
            flags |= 1;
        }
        bus.write_word(gd + 20, flags);
    }

    fn has_depth_mode_id(depth: u16, which_flags: u16, flags: u16) -> u16 {
        // HasDepth's flags bit 0 is the color-device constraint. Classic
        // color hardware can also advertise indexed grayscale monitor modes
        // such as 4-bit "16 grays" and 8-bit "256 grays"; Glider PRO checks
        // those modes before showing its color-depth dialog.
        if which_flags & 1 != 0 && flags & 1 == 0 {
            return match depth {
                1 | 2 | 4 | 8 => depth,
                _ => 0,
            };
        }

        if Self::supported_depth_from_mode(depth).is_some() {
            depth
        } else {
            0
        }
    }

    fn supported_depth_from_mode(depth_or_mode: u16) -> Option<u16> {
        match depth_or_mode {
            1 | 2 | 4 | 8 | 16 | 32 => Some(depth_or_mode),
            // Display Manager mode tokens already used by the HLE paths.
            0x0080 | 0x0085 => Some(8),
            _ => None,
        }
    }

    fn palette_dispatch_selector(d0: u32) -> u16 {
        let low_word = (d0 & 0xFFFF) as u16;

        // MPW's THREEWORDINLINE emits MOVE.W #selector,D0, which preserves
        // D0's high word. Prefer the documented public selector in the low
        // word before trying the alternate packed-long form.
        if Self::is_public_palette_dispatch_selector(low_word) || d0 <= 0xFFFF {
            return low_word;
        }

        // IM:VI Table C-3 lists _PaletteDispatch selectors as packed words:
        // the high byte is the argument-frame byte count and the low byte is
        // the routine selector (for example, SetDepth is $0A13). Some 68k
        // callers place the same two fields in a longword D0 value, such as
        // $000A0013. Normalize both encodings to the documented table value.
        (((d0 >> 8) & 0xFF00) | (d0 & 0x00FF)) as u16
    }

    fn is_public_palette_dispatch_selector(selector: u16) -> bool {
        matches!(
            selector,
            0x0002 | 0x0003 | 0x0015 | 0x0417 | 0x0616 | 0x0A13 | 0x0A14 | 0x1219
        )
    }

    fn record_gdevice_depth_mode(
        &mut self,
        bus: &mut MacMemoryBus,
        gdh: u32,
        mode: u16,
        depth: u16,
    ) {
        let gdh = if gdh != 0 {
            gdh
        } else {
            self.ensure_main_gdevice(bus)
        };
        let gd = bus.read_long(gdh);
        if gd == 0 {
            return;
        }

        bus.write_long(gd + 42, u32::from(mode));
        let mut flags = bus.read_word(gd + 20);
        flags &= !1;
        if depth > 1 {
            flags |= 1;
        }
        bus.write_word(gd + 20, flags);
    }

    fn set_depth_or_mode<C: CpuOps>(
        &mut self,
        cpu: &mut C,
        bus: &mut MacMemoryBus,
        gdh: u32,
        depth_or_mode: u16,
    ) -> u16 {
        let Some(depth) = Self::supported_depth_from_mode(depth_or_mode) else {
            return (-50i16) as u16; // paramErr
        };

        if matches!(depth, 1 | 8) {
            if depth == 8 {
                if let Some((width, height)) = Self::display_mode_geometry(u32::from(depth_or_mode))
                {
                    self.do_setdepth_with_geometry(cpu, bus, depth, width, height);
                } else {
                    self.do_setdepth(cpu, bus, depth);
                }
            } else {
                self.do_setdepth(cpu, bus, depth);
            }
        }

        self.record_gdevice_depth_mode(bus, gdh, depth_or_mode, depth);
        0
    }

    fn palette_ptr(bus: &MacMemoryBus, palette_handle: u32) -> u32 {
        if palette_handle == 0 {
            return 0;
        }
        bus.read_long(palette_handle)
    }

    fn palette_entry_count(bus: &MacMemoryBus, palette_handle: u32) -> u16 {
        let palette_ptr = Self::palette_ptr(bus, palette_handle);
        if palette_ptr == 0 {
            return 0;
        }
        bus.read_word(palette_ptr)
    }

    fn palette_color_info_ptr(palette_ptr: u32, entry: u32) -> u32 {
        palette_ptr + PALETTE_HEADER_SIZE + entry * PALETTE_COLOR_INFO_SIZE
    }

    fn read_palette_color_info(
        bus: &MacMemoryBus,
        palette_handle: u32,
        entry: u16,
    ) -> Option<([u16; 3], i16, i16)> {
        let palette_ptr = Self::palette_ptr(bus, palette_handle);
        let entries = Self::palette_entry_count(bus, palette_handle);
        if palette_ptr == 0 || entry >= entries {
            return None;
        }

        let color_info = Self::palette_color_info_ptr(palette_ptr, u32::from(entry));
        Some((
            [
                bus.read_word(color_info),
                bus.read_word(color_info + 2),
                bus.read_word(color_info + 4),
            ],
            bus.read_word(color_info + 6) as i16,
            bus.read_word(color_info + 8) as i16,
        ))
    }

    fn write_palette_color_info(
        bus: &mut MacMemoryBus,
        palette_ptr: u32,
        entry: u32,
        rgb: [u16; 3],
        usage: i16,
        tolerance: i16,
    ) {
        let color_info = Self::palette_color_info_ptr(palette_ptr, entry);
        bus.write_word(color_info, rgb[0]);
        bus.write_word(color_info + 2, rgb[1]);
        bus.write_word(color_info + 4, rgb[2]);
        bus.write_word(color_info + 6, usage as u16);
        bus.write_word(color_info + 8, tolerance as u16);
        bus.write_word(color_info + 10, 0);
        bus.write_word(color_info + 12, 0);
        bus.write_word(color_info + 14, 0);
    }

    fn create_palette_from_ctab(
        &mut self,
        bus: &mut MacMemoryBus,
        entries: u16,
        src_colors: u32,
        src_usage: i16,
        src_tolerance: i16,
    ) -> u32 {
        let palette_ptr =
            bus.alloc(PALETTE_HEADER_SIZE + u32::from(entries) * PALETTE_COLOR_INFO_SIZE);
        bus.write_word(palette_ptr, entries);
        for offset in (2..PALETTE_HEADER_SIZE).step_by(2) {
            bus.write_word(palette_ptr + offset, 0);
        }

        let src_ctab_ptr = if src_colors != 0 {
            bus.read_long(src_colors)
        } else {
            0
        };
        let src_ctab_entries = if src_ctab_ptr != 0 {
            u32::from(bus.read_word(src_ctab_ptr + 6)) + 1
        } else {
            0
        };

        for index in 0..u32::from(entries) {
            let rgb = if index < src_ctab_entries {
                let ctab_entry = src_ctab_ptr + 8 + index * 8;
                [
                    bus.read_word(ctab_entry + 2),
                    bus.read_word(ctab_entry + 4),
                    bus.read_word(ctab_entry + 6),
                ]
            } else {
                [0, 0, 0]
            };
            Self::write_palette_color_info(bus, palette_ptr, index, rgb, src_usage, src_tolerance);
        }

        let palette_handle = bus.alloc(4);
        bus.write_long(palette_handle, palette_ptr);
        self.palette_updates.insert(palette_handle, PM_ALL_UPDATES);
        palette_handle
    }

    fn resize_handle_allocation(
        &mut self,
        bus: &mut MacMemoryBus,
        handle: u32,
        new_size: u32,
    ) -> u32 {
        if handle == 0 {
            return 0;
        }

        let old_ptr = bus.read_long(handle);
        if old_ptr == 0 {
            return 0;
        }

        let old_size = bus.get_alloc_size(old_ptr).unwrap_or(0);
        let aligned_old = (old_size + 3) & !3;
        let aligned_new = (new_size + 3) & !3;
        if old_size == new_size || aligned_new <= aligned_old {
            bus.set_alloc_size(old_ptr, new_size);
            return old_ptr;
        }

        let new_ptr = bus.alloc(new_size);
        if new_ptr == 0 && new_size != 0 {
            return 0;
        }

        let copy_len = old_size.min(new_size) as usize;
        if copy_len > 0 {
            let bytes = bus.read_bytes(old_ptr, copy_len);
            bus.write_bytes(new_ptr, &bytes);
        }
        bus.free(old_ptr);
        bus.write_long(handle, new_ptr);

        let tracked_handle = self.ptr_to_handle.remove(&old_ptr).is_some();
        if tracked_handle {
            self.ptr_to_handle.insert(new_ptr, handle);
        }
        if let Some(entry) = self.loaded_handles.get_mut(&handle) {
            entry.0 = new_ptr;
        }
        if let Some(resources) = self.resources.as_mut() {
            for file in resources.files.values_mut() {
                for ptr in file.loaded.values_mut() {
                    if *ptr == old_ptr {
                        *ptr = new_ptr;
                    }
                }
                for (_id, ptr) in file.named.values_mut() {
                    if *ptr == old_ptr {
                        *ptr = new_ptr;
                    }
                }
            }
        }

        new_ptr
    }

    fn resize_palette(&mut self, bus: &mut MacMemoryBus, palette_handle: u32, new_size: i16) {
        if palette_handle == 0 || new_size < 0 {
            return;
        }

        let old_palette_ptr = Self::palette_ptr(bus, palette_handle);
        if old_palette_ptr == 0 {
            return;
        }

        let old_entries = u32::from(Self::palette_entry_count(bus, palette_handle));
        let new_entries = new_size as u32;
        if old_entries == new_entries {
            return;
        }

        let new_palette_ptr =
            bus.alloc(PALETTE_HEADER_SIZE + new_entries * PALETTE_COLOR_INFO_SIZE);
        bus.write_word(new_palette_ptr, new_entries as u16);
        for offset in (2..PALETTE_HEADER_SIZE).step_by(2) {
            bus.write_word(new_palette_ptr + offset, 0);
        }

        let preserved = old_entries.min(new_entries);
        for index in 0..preserved {
            if let Some((rgb, usage, tolerance)) =
                Self::read_palette_color_info(bus, palette_handle, index as u16)
            {
                Self::write_palette_color_info(bus, new_palette_ptr, index, rgb, usage, tolerance);
            }
        }
        for index in preserved..new_entries {
            Self::write_palette_color_info(bus, new_palette_ptr, index, [0, 0, 0], PM_COURTEOUS, 0);
        }

        bus.write_long(palette_handle, new_palette_ptr);
        bus.free(old_palette_ptr);
    }

    fn color_table_ptr(bus: &MacMemoryBus, ctab_handle: u32) -> u32 {
        if ctab_handle == 0 {
            return 0;
        }
        bus.read_long(ctab_handle)
    }

    fn color_table_entry_count(bus: &MacMemoryBus, ctab_handle: u32) -> u32 {
        let ctab_ptr = Self::color_table_ptr(bus, ctab_handle);
        if ctab_ptr == 0 {
            return 0;
        }
        u32::from(bus.read_word(ctab_ptr + 6)) + 1
    }

    fn resize_color_table(
        &mut self,
        bus: &mut MacMemoryBus,
        ctab_handle: u32,
        entries: u32,
    ) -> u32 {
        self.resize_handle_allocation(bus, ctab_handle, 8 + entries.saturating_mul(8))
    }

    fn copy_ctab_to_palette(
        &mut self,
        bus: &mut MacMemoryBus,
        src_ctab: u32,
        palette: u32,
        src_usage: i16,
        src_tolerance: i16,
    ) {
        let src_ctab_ptr = Self::color_table_ptr(bus, src_ctab);
        if src_ctab_ptr == 0 || palette == 0 {
            return;
        }

        let entries = Self::color_table_entry_count(bus, src_ctab).min(256);
        if u32::from(Self::palette_entry_count(bus, palette)) != entries {
            self.resize_palette(bus, palette, entries as i16);
        }

        let palette_ptr = Self::palette_ptr(bus, palette);
        if palette_ptr == 0 {
            return;
        }

        for offset in 0..entries {
            let ctab_entry = src_ctab_ptr + 8 + offset * 8;
            let rgb = [
                bus.read_word(ctab_entry + 2),
                bus.read_word(ctab_entry + 4),
                bus.read_word(ctab_entry + 6),
            ];
            Self::write_palette_color_info(bus, palette_ptr, offset, rgb, src_usage, src_tolerance);
        }
    }

    fn copy_palette_to_ctab(
        &mut self,
        bus: &mut MacMemoryBus,
        palette: u32,
        dst_ctab: u32,
        trace_label: &str,
    ) {
        let entries = u32::from(Self::palette_entry_count(bus, palette));
        if entries == 0 || dst_ctab == 0 {
            return;
        }

        if Self::color_table_entry_count(bus, dst_ctab) != entries {
            self.resize_color_table(bus, dst_ctab, entries);
        }

        let dst_ctab_ptr = Self::color_table_ptr(bus, dst_ctab);
        if dst_ctab_ptr == 0 {
            return;
        }

        let palette_seed = self.next_color_table_seed();
        bus.write_long(dst_ctab_ptr, palette_seed);
        self.trace_ctab_seed_write(dst_ctab_ptr, palette_seed, trace_label);
        bus.write_word(dst_ctab_ptr + 4, 0);
        bus.write_word(dst_ctab_ptr + 6, entries.saturating_sub(1) as u16);
        for offset in 0..entries {
            if let Some((rgb, _, _)) = Self::read_palette_color_info(bus, palette, offset as u16) {
                let ctab_entry = dst_ctab_ptr + 8 + offset * 8;
                bus.write_word(ctab_entry, offset as u16);
                bus.write_word(ctab_entry + 2, rgb[0]);
                bus.write_word(ctab_entry + 4, rgb[1]);
                bus.write_word(ctab_entry + 6, rgb[2]);
            }
        }
    }

    fn copy_palette_resource_from_ptr(
        &mut self,
        bus: &mut MacMemoryBus,
        palette_id: i16,
        resource_ptr: u32,
    ) -> u32 {
        if resource_ptr == 0 {
            return 0;
        }

        let entries = u32::from(bus.read_word(resource_ptr));
        let byte_size = PALETTE_HEADER_SIZE + entries * PALETTE_COLOR_INFO_SIZE;
        let palette_ptr = bus.alloc(byte_size);
        for offset in 0..byte_size {
            bus.write_byte(palette_ptr + offset, bus.read_byte(resource_ptr + offset));
        }
        for offset in (2..PALETTE_HEADER_SIZE).step_by(2) {
            bus.write_word(palette_ptr + offset, 0);
        }

        let palette_handle = bus.alloc(4);
        bus.write_long(palette_handle, palette_ptr);
        self.palette_updates.insert(palette_handle, PM_ALL_UPDATES);
        if trace_palette_enabled() {
            let resource_size = bus.get_alloc_size(resource_ptr).unwrap_or(0);
            eprintln!(
                "[PALETTE] copy_resource id={} resource=${:08X} size={} entries={} handle=${:08X}",
                palette_id, resource_ptr, resource_size, entries, palette_handle
            );
            let preview = entries.min(8);
            for index in 0..preview {
                let color_info = Self::palette_color_info_ptr(palette_ptr, index);
                eprintln!(
                    "[PALETTE]   entry[{}] rgb=({:04X},{:04X},{:04X}) usage=${:04X} tol={}",
                    index,
                    bus.read_word(color_info),
                    bus.read_word(color_info + 2),
                    bus.read_word(color_info + 4),
                    bus.read_word(color_info + 6),
                    bus.read_word(color_info + 8) as i16,
                );
            }
        }
        palette_handle
    }

    pub(crate) fn copy_palette_resource(&mut self, bus: &mut MacMemoryBus, palette_id: i16) -> u32 {
        let resource_ptr = self
            .find_resource_any(*b"pltt", palette_id)
            .or_else(|| {
                (palette_id != 0)
                    .then(|| self.find_resource_any(*b"pltt", 0))
                    .flatten()
            })
            .map(|(_, ptr)| ptr)
            .unwrap_or(0);
        self.copy_palette_resource_from_ptr(bus, palette_id, resource_ptr)
    }

    pub(crate) fn copy_palette_resource_exact(
        &mut self,
        bus: &mut MacMemoryBus,
        palette_id: i16,
    ) -> u32 {
        let resource_ptr = self
            .find_resource_any(*b"pltt", palette_id)
            .map(|(_, ptr)| ptr)
            .unwrap_or(0);
        self.copy_palette_resource_from_ptr(bus, palette_id, resource_ptr)
    }

    fn window_palette_handle(&self, window: u32) -> u32 {
        self.window_palettes
            .get(&window)
            .or_else(|| self.window_palettes.get(&PALETTE_DEFAULT_WINDOW))
            .map(|(palette, _)| *palette)
            .unwrap_or(0)
    }

    fn window_palette_handle_exact(&self, window: u32) -> u32 {
        self.window_palettes
            .get(&window)
            .map(|(palette, _)| *palette)
            .unwrap_or(0)
    }

    fn apply_palette_to_active_device(
        &mut self,
        bus: &mut MacMemoryBus,
        window: u32,
        palette_handle: u32,
        trace_label: &str,
        trace_seed_label: &str,
    ) {
        let palette_ptr = Self::palette_ptr(bus, palette_handle);
        if palette_ptr == 0 {
            return;
        }

        let palette_entries = usize::from(Self::palette_entry_count(bus, palette_handle)).min(256);
        let mut updated_clut = Self::standard_mac_8bpp_clut();
        for (index, rgb) in updated_clut.iter_mut().enumerate().take(palette_entries) {
            let color_info = Self::palette_color_info_ptr(palette_ptr, index as u32);
            *rgb = [
                bus.read_word(color_info),
                bus.read_word(color_info + 2),
                bus.read_word(color_info + 4),
            ];
        }
        self.device_clut = updated_clut;
        self.color_manager_clut = updated_clut;
        if trace_palette_enabled() {
            eprintln!(
                "[PALETTE] ActivatePalette{} window=${:08X} palette=${:08X} entries={}",
                trace_label, window, palette_handle, palette_entries
            );
            for index in 0..palette_entries.min(8) {
                let rgb = self.device_clut[index];
                eprintln!(
                    "[PALETTE]   device[{}]=({:04X},{:04X},{:04X})",
                    index, rgb[0], rgb[1], rgb[2]
                );
            }
            for index in [15usize, 31, 63, 127, 255] {
                let rgb = self.device_clut[index];
                eprintln!(
                    "[PALETTE]   device[{}]=({:04X},{:04X},{:04X})",
                    index, rgb[0], rgb[1], rgb[2]
                );
            }
        }

        let gdh = if self.current_gdevice != 0 {
            self.current_gdevice
        } else {
            self.ensure_main_gdevice(bus)
        };
        let ctab_handle = Self::gdevice_ctab_handle(bus, gdh);
        let ctab_ptr = if ctab_handle != 0 {
            bus.read_long(ctab_handle)
        } else {
            0
        };
        if ctab_ptr == 0 {
            return;
        }

        let seed = self.next_color_table_seed();
        bus.write_long(ctab_ptr, seed);
        self.trace_ctab_seed_write(ctab_ptr, seed, trace_seed_label);
        bus.write_word(ctab_ptr + 4, 0x8000);
        bus.write_word(ctab_ptr + 6, 255);
        for (index, rgb) in self.device_clut.iter().enumerate() {
            let entry = ctab_ptr + 8 + (index as u32) * 8;
            bus.write_word(entry, index as u16);
            bus.write_word(entry + 2, rgb[0]);
            bus.write_word(entry + 4, rgb[1]);
            bus.write_word(entry + 6, rgb[2]);
        }
    }

    pub(crate) fn set_window_palette_association(
        &mut self,
        window: u32,
        palette: u32,
        updates: i16,
    ) {
        if palette == 0 {
            self.window_palettes.remove(&window);
        } else {
            self.window_palettes.insert(window, (palette, updates));
            self.record_palette_updates(palette, updates);
        }
    }

    fn clear_palette_tracking(&mut self, palette: u32) {
        if palette == 0 {
            return;
        }
        self.palette_updates.remove(&palette);
        self.window_palettes
            .retain(|_, (handle, _)| *handle != palette);
    }

    fn record_palette_updates(&mut self, palette: u32, updates: i16) {
        if palette != 0 {
            self.palette_updates.insert(palette, updates);
        }
    }

    fn palette_update_mode(&self, palette: u32) -> i16 {
        self.palette_updates.get(&palette).copied().unwrap_or(0)
    }

    pub(crate) fn activate_palette_for_window(&mut self, bus: &mut MacMemoryBus, window: u32) {
        if window == 0 || window != self.front_window {
            return;
        }

        let palette_handle = self.window_palette_handle(window);
        self.apply_palette_to_active_device(
            bus,
            window,
            palette_handle,
            "",
            "activate_palette_for_window",
        );
    }

    /// Trap-facing activation path that requires an exact window-to-palette
    /// association instead of falling back to the default-window palette.
    pub(crate) fn activate_associated_palette_for_window(
        &mut self,
        bus: &mut MacMemoryBus,
        window: u32,
    ) {
        if window == 0 || window != self.front_window {
            return;
        }

        let palette_handle = self.window_palette_handle_exact(window);
        self.apply_palette_to_active_device(
            bus,
            window,
            palette_handle,
            " exact",
            "activate_associated_palette_for_window",
        );
    }

    fn palette_entry_rgb_for_current_window(
        &self,
        bus: &MacMemoryBus,
        entry: i16,
    ) -> Option<([u16; 3], i16)> {
        if entry < 0 {
            return None;
        }
        let palette_handle = self.window_palette_handle(self.current_port);
        if palette_handle == 0 {
            return None;
        }
        let (rgb, usage, _tolerance) =
            Self::read_palette_color_info(bus, palette_handle, entry as u16)?;
        Some((rgb, usage))
    }

    /// Read a 256-entry CLUT from a CTabHandle in guest memory.
    /// Falls back to the device CLUT if the handle is NULL.
    /// Imaging With QuickDraw 1994, p. 4-82
    pub(crate) fn read_port_clut(&self, bus: &MacMemoryBus, ctab_handle: u32) -> [[u16; 3]; 256] {
        if ctab_handle == 0 {
            return self.color_manager_clut;
        }
        let screen_ctab_handle = if self.main_gdevice_handle != 0 {
            Self::gdevice_ctab_handle(bus, self.main_gdevice_handle)
        } else {
            0
        };
        if ctab_handle == screen_ctab_handle {
            return self.color_manager_clut;
        }
        let ctab_ptr = bus.read_long(ctab_handle);
        if ctab_ptr == 0 {
            return self.color_manager_clut;
        }
        let ct_size = bus.read_word(ctab_ptr + 6) as u32;
        // Treat guest ColorTables as overrides on top of the Color Manager CLUT.
        // Marathon builds scratch/offscreen tables that do not always populate
        // every entry; zero-filling the rest collapses unrelated indices to black
        // during 8bpp CopyBits palette remapping.
        let mut clut = self.color_manager_clut;
        for i in 0..=ct_size.min(255) {
            let entry = ctab_ptr + 8 + i * 8;
            let value = bus.read_word(entry) as usize;
            let r = bus.read_word(entry + 2);
            let g = bus.read_word(entry + 4);
            let b = bus.read_word(entry + 6);
            let idx = value.min(255);
            clut[idx] = [r, g, b];
        }
        clut
    }

    pub(super) fn read_ctab_handle_clut(
        &self,
        bus: &MacMemoryBus,
        ctab_handle: u32,
    ) -> [[u16; 3]; 256] {
        if ctab_handle == 0 {
            return self.color_manager_clut;
        }
        let ctab_ptr = bus.read_long(ctab_handle);
        if ctab_ptr == 0 {
            return self.color_manager_clut;
        }
        let ct_size = bus.read_word(ctab_ptr + 6) as u32;
        let mut clut = self.color_manager_clut;
        for i in 0..=ct_size.min(255) {
            let entry = ctab_ptr + 8 + i * 8;
            let value = bus.read_word(entry) as usize;
            let r = bus.read_word(entry + 2);
            let g = bus.read_word(entry + 4);
            let b = bus.read_word(entry + 6);
            let idx = value.min(255);
            clut[idx] = [r, g, b];
        }
        clut
    }

    pub(super) fn uses_canonical_system_8bpp_clut(clut: &[[u16; 3]; 256]) -> bool {
        clut[0] == [0xFFFF, 0xFFFF, 0xFFFF]
            && clut[1] == [0xFFFF, 0xFFFF, 0xCCCC]
            && clut[16] == [0xFFFF, 0x9999, 0x3333]
            && clut[42] == [0xCCCC, 0xCCCC, 0xFFFF]
            && clut[128] == [0x6666, 0x6666, 0x9999]
            && clut[255] == [0, 0, 0]
    }

    fn uses_scaled_canonical_system_8bpp_clut(clut: &[[u16; 3]; 256]) -> bool {
        let sample0 = clut[0];
        if sample0[0] != sample0[1] || sample0[1] != sample0[2] {
            return false;
        }
        let scale = sample0[0] as f64 / 65535.0;
        if !(0.0..=1.0).contains(&scale) {
            return false;
        }

        let expected = Self::standard_mac_8bpp_clut();
        for index in [0usize, 1, 16, 42, 128, 255] {
            for channel in 0..3 {
                let want = (f64::from(expected[index][channel]) * scale).round() as i32;
                let got = i32::from(clut[index][channel]);
                if (got - want).abs() > 0x0200 {
                    return false;
                }
            }
        }
        true
    }

    fn pict_clut_is_dense_grayscale(clut: &[[u16; 3]; 256]) -> bool {
        let grayscale_entries = clut
            .iter()
            .take(192)
            .filter(|rgb| rgb[0] == rgb[1] && rgb[1] == rgb[2] && rgb[0] != 0)
            .count();
        grayscale_entries >= 96
    }

    /// Counts CTab entries whose RGB is non-zero. A sparse PICT CTab
    /// (only a handful of populated slots, the rest zeros from handle
    /// allocation) is not a full-scene palette and shouldn't be
    /// installed wholesale as the screen palette — the zero slots
    /// would paint black where the game expected its current colours.
    fn pict_clut_populated_count(clut: &[[u16; 3]; 256]) -> usize {
        clut.iter()
            .filter(|rgb| !(rgb[0] == 0 && rgb[1] == 0 && rgb[2] == 0))
            .count()
    }

    fn picture_clut_arrays(
        base_clut: &[[u16; 3]; 256],
        pict_clut: &[[u16; 3]],
    ) -> Option<([[u16; 3]; 256], [[u16; 3]; 256])> {
        // Palette Manager merge semantics: start from the active logical
        // table and overlay only entries the PICT actually specifies. The
        // raw zero-padded table remains available for the dense-grayscale
        // gate so sparse PICT CTabs do not look like full scene palettes.
        let mut raw_pict_array = [[0u16; 3]; 256];
        let mut merged = *base_clut;
        let mut had_nonzero = false;
        for (index, rgb) in pict_clut.iter().take(256).enumerate() {
            raw_pict_array[index] = *rgb;
            if !(rgb[0] == 0 && rgb[1] == 0 && rgb[2] == 0) {
                merged[index] = *rgb;
                had_nonzero = true;
            }
        }
        had_nonzero.then_some((raw_pict_array, merged))
    }

    fn should_seed_screen_palette_from_pict(
        pict_clut: &[[u16; 3]; 256],
        _current_clut: &[[u16; 3]; 256],
    ) -> bool {
        // Seed the screen palette from a PICT's embedded CTab ONLY when:
        //   (a) the PICT's CTab is NOT dense grayscale. Grayscale PICTs
        //       (e.g. EV title resource 8100) rely on later SetEntries
        //       fades to become their intended coloured presentation on
        //       Basilisk. Seeding from the grayscale would lock the
        //       title into gray and regress earlier frames. The palette
        //       investigation established this as the hard constraint.
        //   (b) the PICT CTab has at least 16 populated entries. The
        //       lower threshold works because the DrawPicture
        //       seeding call-site now MERGES non-zero entries into the
        //       current CLUT instead of wholesale-replacing it — so a
        //       PICT with 32 populated entries only overwrites those
        //       32 indices, preserving the rest. That admits
        //       small panel-style PICTs (EV's landing-scene
        //       composition draws the panel as multiple small PICTs)
        //       while still rejecting near-empty CTabs (3-entry
        //       sparse-grayscale test fixtures).
        //
        // Imaging With QuickDraw 1994, 7-14 (picture-with-palette
        // semantics); Inside Macintosh Volume VI 20-12 (Palette
        // Manager merge semantics).
        !Self::pict_clut_is_dense_grayscale(pict_clut)
            && Self::pict_clut_populated_count(pict_clut) >= 16
    }

    fn should_preserve_offscreen_picture_indices_from_pict(
        pict_clut: &[[u16; 3]; 256],
        current_clut: &[[u16; 3]; 256],
        logical_screen_clut: &[[u16; 3]; 256],
    ) -> bool {
        // Some games draw a full-scene PICT into an offscreen GWorld before
        // installing the palette that belongs to that picture, then blit the
        // raw 8-bit indices after SetEntries. Matching the PICT's colors
        // against the currently-active stale port CTab bakes the wrong index
        // values into the offscreen buffer. Preserve authored indices only for
        // substantial, non-grayscale PICT CTabs; sparse helper pictures and
        // dense grayscale fades still use normal color matching.
        //
        // Do not preserve when the destination is still the system palette
        // (possibly mid-fade). In that case QuickDraw should translate the
        // PICT colors into system-palette grays instead of storing raw PICT
        // indices that would display as unrelated color-cube entries.
        if Self::uses_canonical_system_8bpp_clut(current_clut)
            || Self::uses_scaled_canonical_system_8bpp_clut(current_clut)
            || Self::uses_canonical_system_8bpp_clut(logical_screen_clut)
            || Self::uses_scaled_canonical_system_8bpp_clut(logical_screen_clut)
        {
            return false;
        }
        // If the offscreen port has already inherited the current screen
        // palette, normal Color Manager translation is the real-Mac path.
        // Raw-index preservation is only for stale offscreen CTabs that will
        // be blitted after a different screen palette is installed.
        if current_clut == logical_screen_clut {
            return false;
        }
        !Self::pict_clut_is_dense_grayscale(pict_clut)
            && Self::pict_clut_populated_count(pict_clut) >= 64
    }

    fn scale_clut(clut: &[[u16; 3]; 256], scale: f64) -> [[u16; 3]; 256] {
        let clamped_scale = scale.clamp(0.0, 1.0);
        let mut scaled = [[0u16; 3]; 256];
        for (index, rgb) in clut.iter().enumerate() {
            scaled[index] = [
                (f64::from(rgb[0]) * clamped_scale)
                    .round()
                    .clamp(0.0, 65535.0) as u16,
                (f64::from(rgb[1]) * clamped_scale)
                    .round()
                    .clamp(0.0, 65535.0) as u16,
                (f64::from(rgb[2]) * clamped_scale)
                    .round()
                    .clamp(0.0, 65535.0) as u16,
            ];
        }
        scaled
    }

    fn screen_palette_brightness_scale(clut: &[[u16; 3]; 256]) -> f64 {
        let [r, g, b] = clut[0];
        if r != g || g != b {
            return 1.0;
        }
        (f64::from(r) / 65535.0).clamp(0.0, 1.0)
    }

    fn seeded_picture_palette_until_tick_for_seed(
        &self,
        extend_by_ticks: u32,
        preserve_active_window: bool,
    ) -> u32 {
        if preserve_active_window
            && self.tick_count < self.seeded_picture_palette_until_tick
            && !Self::uses_canonical_system_8bpp_clut(&self.seeded_picture_palette)
        {
            self.seeded_picture_palette_until_tick
        } else {
            self.tick_count.saturating_add(extend_by_ticks)
        }
    }

    fn remember_recent_resource_ctable_fetch(&mut self, ct_id: i16, ctab_handle: u32) {
        self.recent_resource_ctable_fetch = Some(RecentColorTableFetch {
            ct_id,
            ctab_handle,
            port: self.current_port,
            tick: self.tick_count,
        });
        if trace_palette_enabled() {
            eprintln!(
                "[PALETTE] RememberGetCTable tick={} trap={} port=${:08X} ct_id={} handle=${:08X}",
                self.tick_count, self.trap_count, self.current_port, ct_id, ctab_handle,
            );
        }
    }

    fn take_recent_drawpicture_resource_ctable_fetch(
        &mut self,
        port: u32,
        _port_base: u32,
        _port_rb: u32,
        port_ps: u16,
    ) -> Option<RecentColorTableFetch> {
        let recent = self.recent_resource_ctable_fetch?;
        let stale = self.tick_count > recent.tick.saturating_add(1);
        if stale {
            self.recent_resource_ctable_fetch = None;
            return None;
        }
        let eligible = port_ps == 8 && port == recent.port;
        if eligible {
            return self.recent_resource_ctable_fetch.take();
        }
        None
    }

    fn apply_fetched_ctable_to_offscreen_drawpicture_port(
        &mut self,
        bus: &mut MacMemoryBus,
        port: u32,
        port_ctab_handle: u32,
        fetch: &RecentColorTableFetch,
        clut: &[[u16; 3]; 256],
    ) {
        if port_ctab_handle != 0 {
            let ctab_ptr = bus.read_long(port_ctab_handle);
            let ct_flags = if ctab_ptr != 0 {
                bus.read_word(ctab_ptr + 4)
            } else {
                0x8000
            };
            let _ =
                self.overwrite_color_table_handle_with_clut(bus, port_ctab_handle, clut, ct_flags);
        }

        let entries = Self::color_table_entry_count(bus, fetch.ctab_handle);
        let current_screen_is_application_palette =
            !Self::uses_canonical_system_8bpp_clut(&self.color_manager_clut)
                && !Self::uses_scaled_canonical_system_8bpp_clut(&self.color_manager_clut);
        if entries < 16 || !current_screen_is_application_palette {
            return;
        }

        self.install_application_clut(bus, *clut);
        self.seeded_picture_palette = *clut;
        self.seeded_picture_palette_until_tick =
            self.seeded_picture_palette_until_tick_for_seed(64, true);
        if trace_palette_enabled() {
            eprintln!(
                "[PALETTE] PublishFetchedCTableForOffscreenDrawPicture tick={} port=${:08X} ct_id={} entries={} ctab=${:08X} device[8]=({:04X},{:04X},{:04X}) device[9]=({:04X},{:04X},{:04X})",
                self.tick_count,
                port,
                fetch.ct_id,
                entries,
                port_ctab_handle,
                self.device_clut[8][0],
                self.device_clut[8][1],
                self.device_clut[8][2],
                self.device_clut[9][0],
                self.device_clut[9][1],
                self.device_clut[9][2],
            );
        }
    }

    fn seed_screen_palette_from_picture_clut(
        &mut self,
        bus: &mut MacMemoryBus,
        pic_handle: u32,
        pic_ptr: u32,
        clut: &[[u16; 3]; 256],
        fetched_ct_id: Option<i16>,
    ) {
        // Diagnostic gate; when SYSTEMLESS_PICT_SEED_CLUT_ENABLE is unset,
        // skip the seed from a PICT CTab / fetched-CTable.
        if pict_seed_clut_disabled() {
            return;
        }
        let scale = Self::screen_palette_brightness_scale(&self.device_clut);
        self.device_clut = if palette_as_game_wrote_enabled() {
            *clut
        } else {
            Self::scale_clut(clut, scale)
        };
        self.color_manager_clut = *clut;
        self.seeded_picture_palette = *clut;
        self.seeded_picture_palette_until_tick =
            self.seeded_picture_palette_until_tick_for_seed(48, true);
        self.sync_canonical_offscreen_ctabs_to_clut(bus, clut);
        if trace_palette_enabled() {
            let event = match fetched_ct_id {
                Some(ct_id) => format!("SeedFromFetchedCTable({ct_id})"),
                None => "SeedFromTitleClut".to_string(),
            };
            eprintln!(
                "[PALETTE] {} tick={} picHandle=${:08X} picPtr=${:08X} scale={:.3} cm[0]=({:04X},{:04X},{:04X}) cm[1]=({:04X},{:04X},{:04X}) cm[16]=({:04X},{:04X},{:04X}) cm[42]=({:04X},{:04X},{:04X}) cm[128]=({:04X},{:04X},{:04X}) cm[255]=({:04X},{:04X},{:04X})",
                event,
                self.tick_count,
                pic_handle,
                pic_ptr,
                scale,
                self.color_manager_clut[0][0],
                self.color_manager_clut[0][1],
                self.color_manager_clut[0][2],
                self.color_manager_clut[1][0],
                self.color_manager_clut[1][1],
                self.color_manager_clut[1][2],
                self.color_manager_clut[16][0],
                self.color_manager_clut[16][1],
                self.color_manager_clut[16][2],
                self.color_manager_clut[42][0],
                self.color_manager_clut[42][1],
                self.color_manager_clut[42][2],
                self.color_manager_clut[128][0],
                self.color_manager_clut[128][1],
                self.color_manager_clut[128][2],
                self.color_manager_clut[255][0],
                self.color_manager_clut[255][1],
                self.color_manager_clut[255][2]
            );
        }
    }

    fn canonical_table_brightness_scale(bus: &MacMemoryBus, table_ptr: u32) -> Option<f64> {
        let white_entry = table_ptr + 2;
        let white = bus.read_word(white_entry);
        let green = bus.read_word(white_entry + 2);
        let blue = bus.read_word(white_entry + 4);
        if white != green || green != blue {
            return None;
        }
        Some(f64::from(white) / 65535.0)
    }

    pub(super) fn ctab_seed(bus: &MacMemoryBus, ctab_handle: u32) -> Option<u32> {
        if ctab_handle == 0 {
            return None;
        }
        let ctab_ptr = bus.read_long(ctab_handle);
        (ctab_ptr != 0).then(|| bus.read_long(ctab_ptr))
    }

    fn next_color_table_seed(&mut self) -> u32 {
        let seed = self.next_ct_seed;
        self.next_ct_seed = self.next_ct_seed.wrapping_add(1);
        if self.next_ct_seed == 0 {
            self.next_ct_seed = 1;
        }
        if trace_ctab_seed_enabled() {
            eprintln!(
                "[CTAB-SEED] tick={} next_ct_seed -> {}",
                self.tick_count, seed,
            );
        }
        seed
    }

    /// When `SYSTEMLESS_TRACE_CTAB_SEED=1`, logs a seed write to a CTab at
    /// `ctab_ptr` from a named code path. Paired with the counter-only
    /// trace in `next_color_table_seed`. Together they let a post-process
    /// correlate "seed S was allocated at tick T" with "seed S was
    /// written to CTab at ptr P at tick T+k by code path L", resolving
    /// ambiguity when the same seed value appears at multiple CTabs
    /// (notably when seed inheritance gives the same seed to the
    /// GDevice and an offscreen GWorld).
    fn trace_ctab_seed_write(&self, ctab_ptr: u32, seed: u32, label: &str) {
        if trace_ctab_seed_enabled() {
            eprintln!(
                "[CTAB-SEED-WRITE] tick={} label={} ctab_ptr=${:08X} seed={}",
                self.tick_count, label, ctab_ptr, seed,
            );
        }
    }

    fn reseed_color_table_handle(
        &mut self,
        bus: &mut MacMemoryBus,
        ctab_handle: u32,
    ) -> Option<u32> {
        if ctab_handle == 0 {
            return None;
        }
        let ctab_ptr = bus.read_long(ctab_handle);
        if ctab_ptr == 0 {
            return None;
        }
        // Executor's depth-convention preservation (qGWorld.cpp:217):
        // when the post-update CTab content is byte-identical to the
        // standard N-bpp palette, keep `seed = depth` so PICTs
        // authored against that canonical palette (also stamped with
        // seed=depth) hit the identity-copy gate without
        // closest-match remap. Only 8bpp is handled here — that's
        // the depth EV and Marathon both run in, and the only depth
        // whose canonical palette matters for PICT identity-copy.
        // Without this, every SetEntries/CTabChanged bumps the seed
        // to a fresh counter value even after a fade has returned the
        // palette to canonical, and PICTs authored for the standard
        // palette remap unnecessarily.
        let seed = if Self::ctab_is_canonical_8bpp(bus, ctab_ptr) {
            8
        } else {
            self.next_color_table_seed()
        };
        bus.write_long(ctab_ptr, seed);
        self.trace_ctab_seed_write(ctab_ptr, seed, "reseed_color_table_handle");
        Some(seed)
    }

    /// Returns true when the 256-entry CTab at `ctab_ptr` matches the
    /// standard Mac 8bpp system palette byte-for-byte. Read-only; used
    /// by `reseed_color_table_handle` to preserve the depth-convention
    /// seed for canonical CTabs. A 256-entry read is cheap compared to
    /// the CopyBits/DrawPicture hot paths and is only called on
    /// CTabChanged/SetEntries cleanup (bounded by the number of
    /// palette events, not per-pixel).
    fn ctab_is_canonical_8bpp(bus: &MacMemoryBus, ctab_ptr: u32) -> bool {
        // ctSize field (entries - 1). Must be 255 for an 8bpp CTab.
        if bus.read_word(ctab_ptr + 6) != 255 {
            return false;
        }
        let std = Self::standard_mac_8bpp_clut();
        for i in 0..256u32 {
            let entry = ctab_ptr + 8 + i * 8;
            let r = bus.read_word(entry + 2);
            let g = bus.read_word(entry + 4);
            let b = bus.read_word(entry + 6);
            if [r, g, b] != std[i as usize] {
                return false;
            }
        }
        true
    }

    pub(super) fn gdevice_ctab_handle(bus: &MacMemoryBus, gdh: u32) -> u32 {
        let pm_handle = Self::gdevice_pixmap_handle(bus, gdh);
        if pm_handle == 0 {
            return 0;
        }
        let pm_ptr = bus.read_long(pm_handle);
        if pm_ptr == 0 {
            return 0;
        }
        let Some(ctab_handle_addr) = pm_ptr.checked_add(42) else {
            return 0;
        };
        bus.read_long(ctab_handle_addr)
    }

    fn gdevice_pixmap_handle(bus: &MacMemoryBus, gdh: u32) -> u32 {
        if gdh == 0 {
            return 0;
        }
        let gd = bus.read_long(gdh);
        if gd == 0 {
            return 0;
        }
        let Some(pm_handle_addr) = gd.checked_add(22) else {
            return 0;
        };
        let pm_handle = bus.read_long(pm_handle_addr);
        if pm_handle == 0 {
            return 0;
        }
        let pm_ptr = bus.read_long(pm_handle);
        if pm_ptr == 0 {
            return 0;
        }
        pm_handle
    }

    fn gdevice_pixel_size(bus: &MacMemoryBus, gdh: u32) -> Option<u32> {
        if gdh == 0 {
            return None;
        }
        let gd = bus.read_long(gdh);
        if gd == 0 {
            return None;
        }
        let pm_handle = bus.read_long(gd.checked_add(22)?);
        if pm_handle == 0 {
            return None;
        }
        let pm_ptr = bus.read_long(pm_handle);
        if pm_ptr == 0 {
            return None;
        }
        Some(bus.read_word(pm_ptr.checked_add(32)?) as u32)
    }

    fn get_or_create_device_loop_trampoline(&mut self, bus: &mut MacMemoryBus) -> u32 {
        if self.device_loop_trampoline != 0 {
            return self.device_loop_trampoline;
        }

        let tramp = bus.alloc(42);
        bus.write_word(tramp, 0x48E7); // MOVEM.L regs,-(SP)
        bus.write_word(tramp + 2, 0xF0F0); // D0-D3/A0-A3
        bus.write_word(tramp + 4, 0x3F3C); // MOVE.W #imm,-(SP)
        bus.write_word(tramp + 8, 0x3F3C); // MOVE.W #imm,-(SP)
        bus.write_word(tramp + 12, 0x2F3C); // MOVE.L #imm,-(SP)
        bus.write_word(tramp + 18, 0x2F3C); // MOVE.L #imm,-(SP)
        bus.write_word(tramp + 24, 0x4EB9); // JSR abs.L
        bus.write_word(tramp + 30, 0x2E7C); // MOVEA.L #imm,A7
        bus.write_word(tramp + 36, 0x4CDF); // MOVEM.L (SP)+,regs
        bus.write_word(tramp + 38, 0x0F0F); // D0-D3/A0-A3
        bus.write_word(tramp + 40, 0x4E75); // RTS
        self.device_loop_trampoline = tramp;
        tramp
    }

    fn write_device_loop_trampoline(
        bus: &mut MacMemoryBus,
        tramp: u32,
        depth: u16,
        device_flags: u16,
        target_gdevice: u32,
        user_data: u32,
        drawing_proc: u32,
        return_slot: u32,
        next_trampoline: Option<u32>,
    ) {
        bus.write_word(tramp, 0x48E7); // MOVEM.L regs,-(SP)
        bus.write_word(tramp + 2, 0xF0F0); // D0-D3/A0-A3
        bus.write_word(tramp + 4, 0x3F3C); // MOVE.W #imm,-(SP)
        bus.write_word(tramp + 6, depth); // depth
        bus.write_word(tramp + 8, 0x3F3C); // MOVE.W #imm,-(SP)
        bus.write_word(tramp + 10, device_flags); // deviceFlags
        bus.write_word(tramp + 12, 0x2F3C); // MOVE.L #imm,-(SP)
        bus.write_long(tramp + 14, target_gdevice); // targetDevice
        bus.write_word(tramp + 18, 0x2F3C); // MOVE.L #imm,-(SP)
        bus.write_long(tramp + 20, user_data); // userData
        bus.write_word(tramp + 24, 0x4EB9); // JSR abs.L
        bus.write_long(tramp + 26, drawing_proc); // drawingProc
        bus.write_word(tramp + 30, 0x2E7C); // MOVEA.L #imm,A7
        bus.write_long(tramp + 32, return_slot.wrapping_sub(32));
        bus.write_word(tramp + 36, 0x4CDF); // MOVEM.L (SP)+,regs
        bus.write_word(tramp + 38, 0x0F0F); // D0-D3/A0-A3
        match next_trampoline {
            Some(next) => {
                bus.write_word(tramp + 40, 0x4EF9); // JMP abs.L
                bus.write_long(tramp + 42, next);
            }
            None => {
                bus.write_word(tramp + 40, 0x4E75); // RTS
            }
        }
    }

    fn pixmap_ptr_from_handle(bus: &MacMemoryBus, pm_handle: u32) -> Option<u32> {
        if pm_handle == 0 || pm_handle == u32::MAX || bus.get_alloc_size(pm_handle) != Some(4) {
            return None;
        }
        let pm_ptr = bus.read_long(pm_handle);
        (pm_ptr != 0 && bus.get_alloc_size(pm_ptr).is_some()).then_some(pm_ptr)
    }

    fn cached_copy_bitmap_from_pixmap(
        bus: &MacMemoryBus,
        pm_ptr: u32,
    ) -> Option<CachedCopyBitmapInfo> {
        if pm_ptr == 0 || bus.get_alloc_size(pm_ptr).is_none() {
            return None;
        }
        Some(CachedCopyBitmapInfo {
            base: Self::offscreen_pixmap_base_ptr(bus, pm_ptr),
            row_bytes: (bus.read_word(pm_ptr + 4) & 0x3FFF) as u32,
            bounds_top: bus.read_word(pm_ptr + 6) as i16,
            bounds_left: bus.read_word(pm_ptr + 8) as i16,
            bounds_bottom: bus.read_word(pm_ptr + 10) as i16,
            bounds_right: bus.read_word(pm_ptr + 12) as i16,
            pixel_size: bus.read_word(pm_ptr + 32) as u32,
            ctab_handle: bus.read_long(pm_ptr + 42),
        })
    }

    fn cache_portbits_pixmap_handle(&mut self, bus: &MacMemoryBus, bits_ptr: u32, pm_handle: u32) {
        if bits_ptr == 0 {
            return;
        }
        if let Some(pm_ptr) = Self::pixmap_ptr_from_handle(bus, pm_handle) {
            if let Some(info) = Self::cached_copy_bitmap_from_pixmap(bus, pm_ptr) {
                self.disposed_gworld_portbits.insert(bits_ptr, info);
            }
        }
    }

    fn offscreen_pixmap_base_handle(bus: &MacMemoryBus, pm_ptr: u32) -> u32 {
        if pm_ptr == 0 {
            return 0;
        }
        let base_field = bus.read_long(pm_ptr);
        if base_field == 0 {
            return 0;
        }
        // QuickDraw PixMap baseAddr is normally a direct pixel pointer.
        // Older Systemless builds stored an offscreen pixel handle here instead,
        // so keep recognizing 4-byte master pointer cells for compatibility
        // with stale/disposed state and hand-built tests.
        if bus.get_alloc_size(base_field) == Some(4) {
            base_field
        } else {
            0
        }
    }

    fn offscreen_pixmap_base_ptr(bus: &MacMemoryBus, pm_ptr: u32) -> u32 {
        if pm_ptr == 0 {
            return 0;
        }
        let base_field = bus.read_long(pm_ptr);
        if base_field == 0 {
            return 0;
        }
        if bus.get_alloc_size(base_field) == Some(4) {
            bus.read_long(base_field)
        } else {
            base_field
        }
    }

    fn indexed_ctab_entry_count(depth: u32) -> u32 {
        match depth {
            0 => 1,
            1..=7 => 1u32 << depth,
            _ => 256,
        }
    }

    fn allocate_color_table_handle(
        &mut self,
        bus: &mut MacMemoryBus,
        depth: u32,
        source_ctab_handle: u32,
        ct_flags: u16,
    ) -> u32 {
        let entry_count = Self::indexed_ctab_entry_count(depth);
        let ctab_ptr = bus.alloc(8 + entry_count * 8);
        let ctab_handle = bus.alloc(4);
        bus.write_long(ctab_handle, ctab_ptr);
        self.overwrite_color_table_handle(bus, ctab_handle, depth, source_ctab_handle, ct_flags);
        ctab_handle
    }

    fn allocate_color_table_handle_with_clut(
        &mut self,
        bus: &mut MacMemoryBus,
        depth: u32,
        clut: &[[u16; 3]; 256],
        ct_flags: u16,
    ) -> u32 {
        let entry_count = Self::indexed_ctab_entry_count(depth);
        let ctab_ptr = bus.alloc(8 + entry_count * 8);
        let ctab_handle = bus.alloc(4);
        bus.write_long(ctab_handle, ctab_ptr);
        let _ = self.overwrite_color_table_handle_with_clut(bus, ctab_handle, clut, ct_flags);
        ctab_handle
    }

    pub(crate) fn install_application_clut(
        &mut self,
        bus: &mut MacMemoryBus,
        clut: [[u16; 3]; 256],
    ) {
        self.device_clut = clut;
        self.color_manager_clut = clut;

        let gdh = self.ensure_main_gdevice(bus);
        let ctab_handle = Self::gdevice_ctab_handle(bus, gdh);
        let ctab_ptr = if ctab_handle != 0 {
            bus.read_long(ctab_handle)
        } else {
            0
        };
        let ct_flags = if ctab_ptr != 0 {
            bus.read_word(ctab_ptr + 4)
        } else {
            0x8000
        };
        let _ = self.overwrite_color_table_handle_with_clut(bus, ctab_handle, &clut, ct_flags);
        self.sync_canonical_offscreen_ctabs_to_clut(bus, &clut);
    }

    fn overwrite_color_table_handle(
        &mut self,
        bus: &mut MacMemoryBus,
        ctab_handle: u32,
        depth: u32,
        source_ctab_handle: u32,
        ct_flags: u16,
    ) -> Option<u32> {
        if ctab_handle == 0 {
            return None;
        }
        let ctab_ptr = bus.read_long(ctab_handle);
        if ctab_ptr == 0 {
            return None;
        }

        let entry_capacity = bus.get_alloc_size(ctab_ptr).unwrap_or(0).saturating_sub(8) / 8;
        let entry_count = Self::indexed_ctab_entry_count(depth).min(entry_capacity.max(1));
        let clut = self.read_ctab_handle_clut(bus, source_ctab_handle);

        // Mirror canonical-CLUT -> seed=depth precedence in
        // `overwrite_color_table_handle_with_clut` for the variant
        // that takes a source CTab handle. When the resulting content
        // equals the standard N-bpp palette, Executor's depth
        // convention (qGWorld.cpp:217) requires ctSeed = depth.
        // Only active for 8bpp; other depths retain counter semantics.
        let new_is_canonical_8 = depth == 8 && clut == Self::standard_mac_8bpp_clut();
        let (seed, label) = if new_is_canonical_8 {
            (8u32, "overwrite_color_table_handle/canonical=8")
        } else {
            (self.next_color_table_seed(), "overwrite_color_table_handle")
        };
        bus.write_long(ctab_ptr, seed);
        self.trace_ctab_seed_write(ctab_ptr, seed, label);
        bus.write_word(ctab_ptr + 4, ct_flags);
        bus.write_word(ctab_ptr + 6, (entry_count - 1) as u16);
        for index in 0..entry_count {
            let entry = ctab_ptr + 8 + index * 8;
            let rgb = clut[index as usize];
            bus.write_word(entry, index as u16);
            bus.write_word(entry + 2, rgb[0]);
            bus.write_word(entry + 4, rgb[1]);
            bus.write_word(entry + 6, rgb[2]);
        }
        Some(seed)
    }

    fn overwrite_color_table_handle_with_clut(
        &mut self,
        bus: &mut MacMemoryBus,
        ctab_handle: u32,
        clut: &[[u16; 3]; 256],
        ct_flags: u16,
    ) -> Option<u32> {
        if ctab_handle == 0 {
            return None;
        }
        let ctab_ptr = bus.read_long(ctab_handle);
        if ctab_ptr == 0 {
            return None;
        }

        let entry_capacity = bus.get_alloc_size(ctab_ptr).unwrap_or(0).saturating_sub(8) / 8;
        let entry_count = 256u32.min(entry_capacity.max(1));
        // Executor's ROMlib_copy_ctab (qColorutil.cpp:139-149) moves the
        // whole source CTab buffer, ctSeed included, so an offscreen
        // GWorld whose CTab content comes from the current GDevice ends
        // up sharing the GDevice's seed. Systemless used to unconditionally
        // allocate a fresh seed here, which broke the CopyBits 8→8 gate
        // (quickdraw.rs:1971-1988) even when the offscreen CLUT was
        // byte-identical to the GDevice CLUT. Inherit the GDevice CTab
        // seed when the supplied `clut` matches the current GDevice's
        // CTab CLUT exactly; fall back to a fresh seed otherwise.
        let gdh_ctab_handle = self.current_gdevice_ctab_handle(bus);
        let gdh_seed = Self::ctab_seed(bus, gdh_ctab_handle).unwrap_or(0);
        let new_is_canonical = *clut == Self::standard_mac_8bpp_clut();
        let seed = if new_is_canonical {
            // Executor qGWorld.cpp:217 depth convention: a CTab
            // initialised with the canonical N-bpp palette carries
            // ctSeed=N. Applies regardless of what was at this handle
            // before.
            8
        } else if gdh_seed != 0 && self.read_ctab_handle_clut(bus, gdh_ctab_handle) == *clut {
            // Executor ROMlib_copy_ctab (qColorutil.cpp:139-149)
            // semantics: an offscreen CTab whose content matches
            // the GDevice's inherits the GDevice seed.
            gdh_seed
        } else {
            self.next_color_table_seed()
        };
        bus.write_long(ctab_ptr, seed);
        self.trace_ctab_seed_write(
            ctab_ptr,
            seed,
            if new_is_canonical {
                "overwrite_with_clut/canonical=8"
            } else if seed == gdh_seed {
                "overwrite_with_clut/inherited"
            } else {
                "overwrite_with_clut/fresh"
            },
        );
        bus.write_word(ctab_ptr + 4, ct_flags);
        bus.write_word(ctab_ptr + 6, (entry_count - 1) as u16);
        for index in 0..entry_count {
            let entry = ctab_ptr + 8 + index * 8;
            let rgb = clut[index as usize];
            bus.write_word(entry, index as u16);
            bus.write_word(entry + 2, rgb[0]);
            bus.write_word(entry + 4, rgb[1]);
            bus.write_word(entry + 6, rgb[2]);
        }
        Some(seed)
    }

    fn sync_canonical_offscreen_ctabs_to_clut(
        &mut self,
        bus: &mut MacMemoryBus,
        clut: &[[u16; 3]; 256],
    ) {
        let screen_ctab_handle = if self.main_gdevice_handle != 0 {
            Self::gdevice_ctab_handle(bus, self.main_gdevice_handle)
        } else {
            0
        };
        // Sort port iteration so iteration order is deterministic across
        // runs. HashMap.keys()/HashSet.iter() return in randomized order;
        // the loop below writes CLUTs into guest memory, so iteration
        // order can subtly affect what the game reads later.
        let mut all_ports: Vec<u32> = self
            .gworld_devices
            .keys()
            .copied()
            .chain(self.cport_ports.iter().copied())
            .collect();
        all_ports.sort_unstable();
        for port in all_ports {
            let pixmap_handle = bus.read_long(port + 2);
            if pixmap_handle == 0 {
                continue;
            }
            let pixmap_ptr = bus.read_long(pixmap_handle);
            if pixmap_ptr == 0 || bus.read_word(pixmap_ptr + 32) != 8 {
                continue;
            }
            let ctab_handle = bus.read_long(pixmap_ptr + 42);
            if ctab_handle == 0 || ctab_handle == screen_ctab_handle {
                continue;
            }

            let current_clut = self.read_ctab_handle_clut(bus, ctab_handle);
            if current_clut == *clut
                || (!Self::uses_canonical_system_8bpp_clut(&current_clut)
                    && !Self::uses_scaled_canonical_system_8bpp_clut(&current_clut))
            {
                continue;
            }

            let ctab_ptr = bus.read_long(ctab_handle);
            let ct_flags = if ctab_ptr != 0 {
                bus.read_word(ctab_ptr + 4)
            } else {
                0x8000
            };
            let _ = self.overwrite_color_table_handle_with_clut(bus, ctab_handle, clut, ct_flags);
            if trace_palette_enabled() {
                eprintln!(
                    "[PALETTE] SyncOffscreenCtab tick={} port=${:08X} ctab=${:08X} rgb16=({:04X},{:04X},{:04X}) rgb42=({:04X},{:04X},{:04X}) rgb128=({:04X},{:04X},{:04X})",
                    self.tick_count,
                    port,
                    ctab_handle,
                    clut[16][0],
                    clut[16][1],
                    clut[16][2],
                    clut[42][0],
                    clut[42][1],
                    clut[42][2],
                    clut[128][0],
                    clut[128][1],
                    clut[128][2],
                );
            }
            // Record per-port sync events so the trap-sequence diff sees the
            // same sync_offscreen_ctab events the reference recorder emits.
            let _ = self.record_oracle_event(
                bus,
                0,
                "sync_offscreen_ctab",
                Self::oracle_field_map(&[
                    ("port", format!("{:08X}", port)),
                    ("ctab", format!("{:08X}", ctab_handle)),
                    (
                        "rgb16",
                        format!(
                            "{:04X},{:04X},{:04X}",
                            clut[16][0], clut[16][1], clut[16][2]
                        ),
                    ),
                    (
                        "rgb42",
                        format!(
                            "{:04X},{:04X},{:04X}",
                            clut[42][0], clut[42][1], clut[42][2]
                        ),
                    ),
                    (
                        "rgb128",
                        format!(
                            "{:04X},{:04X},{:04X}",
                            clut[128][0], clut[128][1], clut[128][2]
                        ),
                    ),
                ]),
                false,
            );
        }
    }

    fn create_offscreen_gdevice(
        &mut self,
        bus: &mut MacMemoryBus,
        pixmap_handle: u32,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) -> u32 {
        let gd = bus.alloc(50);
        bus.write_word(gd, 0);
        bus.write_word(gd + 2, 0);
        bus.write_word(gd + 4, 0);
        bus.write_long(gd + 6, 0);
        bus.write_word(gd + 10, 4);
        bus.write_long(gd + 12, 0);
        bus.write_long(gd + 16, 0);
        // Color offscreen device, initialized in RAM, but not a screen device.
        // Inside Macintosh Volume VI, 21-14 to 21-15
        bus.write_word(gd + 20, (1 << 0) | (1 << 10) | (1 << 12));
        bus.write_long(gd + 22, pixmap_handle);
        bus.write_long(gd + 26, 0);
        bus.write_long(gd + 30, 0);
        bus.write_word(gd + 34, top as u16);
        bus.write_word(gd + 36, left as u16);
        bus.write_word(gd + 38, bottom as u16);
        bus.write_word(gd + 40, right as u16);
        bus.write_long(gd + 42, 0);

        let gd_handle = bus.alloc(4);
        bus.write_long(gd_handle, gd);
        gd_handle
    }

    fn new_screen_buffer_common(
        &mut self,
        bus: &mut MacMemoryBus,
        global_rect_ptr: u32,
        purgeable: bool,
        gdh_out_ptr: u32,
        offscreen_pixmap_out_ptr: u32,
        use_temp_mem: bool,
    ) -> u32 {
        let _ = use_temp_mem;

        if global_rect_ptr == 0 || gdh_out_ptr == 0 || offscreen_pixmap_out_ptr == 0 {
            return (-50i32) as u32;
        }

        let top = bus.read_word(global_rect_ptr) as i16;
        let left = bus.read_word(global_rect_ptr + 2) as i16;
        let bottom = bus.read_word(global_rect_ptr + 4) as i16;
        let right = bus.read_word(global_rect_ptr + 6) as i16;
        let width = ((right as i32) - (left as i32)).max(1) as u32;
        let height = ((bottom as i32) - (top as i32)).max(1) as u32;

        let gdh = self.ensure_main_gdevice(bus);
        let gd = bus.read_long(gdh);
        let gd_pm_handle = if gd != 0 { bus.read_long(gd + 22) } else { 0 };
        let gd_pm_ptr = if gd_pm_handle != 0 {
            bus.read_long(gd_pm_handle)
        } else {
            0
        };
        if gd_pm_ptr == 0 {
            return C_NO_MEM_ERR;
        }

        let depth = bus.read_word(gd_pm_ptr + 32) as u32;
        let ctab_handle = bus.read_long(gd_pm_ptr + 42);
        let row_bytes = (width * depth).div_ceil(32) * 4;
        let pixel_bytes = row_bytes.saturating_mul(height);

        let pixel_buf = bus.alloc(pixel_bytes);
        if pixel_buf == 0 && pixel_bytes > 0 {
            return C_NO_MEM_ERR;
        }
        let pixmap = bus.alloc(50);
        if pixmap == 0 {
            return C_NO_MEM_ERR;
        }
        bus.write_long(pixmap, pixel_buf);
        bus.write_word(pixmap + 4, (row_bytes as u16) | 0x8000);
        bus.write_word(pixmap + 6, top as u16);
        bus.write_word(pixmap + 8, left as u16);
        bus.write_word(pixmap + 10, bottom as u16);
        bus.write_word(pixmap + 12, right as u16);
        bus.write_word(pixmap + 14, 0);
        bus.write_word(pixmap + 16, 0);
        bus.write_long(pixmap + 18, 0);
        bus.write_long(pixmap + 22, 0x00480000);
        bus.write_long(pixmap + 26, 0x00480000);
        bus.write_word(pixmap + 30, 0);
        bus.write_word(pixmap + 32, depth as u16);
        bus.write_word(pixmap + 34, 1);
        bus.write_word(pixmap + 36, depth as u16);
        bus.write_long(pixmap + 38, 0);
        bus.write_long(pixmap + 42, ctab_handle);
        bus.write_long(pixmap + 46, 0);

        let pixmap_handle = bus.alloc(4);
        if pixmap_handle == 0 {
            return C_NO_MEM_ERR;
        }
        bus.write_long(pixmap_handle, pixmap);
        self.gworld_pixel_states
            .insert(pixmap_handle, if purgeable { 1 << 6 } else { 0 });

        bus.write_long(gdh_out_ptr, gdh);
        bus.write_long(offscreen_pixmap_out_ptr, pixmap_handle);
        0
    }

    fn dispose_screen_buffer(&mut self, bus: &mut MacMemoryBus, offscreen_pixmap: u32) {
        if offscreen_pixmap == 0 {
            return;
        }

        let pixmap_ptr = bus.read_long(offscreen_pixmap);
        if pixmap_ptr != 0 {
            let pixel_buf_handle = Self::offscreen_pixmap_base_handle(bus, pixmap_ptr);
            if pixel_buf_handle != 0 {
                Self::free_handle_and_target(bus, pixel_buf_handle);
            } else {
                let pixel_buf = bus.read_long(pixmap_ptr);
                if pixel_buf != 0 {
                    bus.free(pixel_buf);
                }
            }
            bus.write_long(offscreen_pixmap, 0);
            bus.free(pixmap_ptr);
        }

        self.gworld_pixel_states.remove(&offscreen_pixmap);
        bus.free(offscreen_pixmap);
    }

    pub(super) fn current_gdevice_ctab_handle(&self, bus: &MacMemoryBus) -> u32 {
        let gdh = if self.current_gdevice != 0 {
            self.current_gdevice
        } else {
            self.main_gdevice_handle
        };
        let ctab_handle = Self::gdevice_ctab_handle(bus, gdh);
        if ctab_handle != 0 || gdh == self.main_gdevice_handle || self.main_gdevice_handle == 0 {
            ctab_handle
        } else {
            Self::gdevice_ctab_handle(bus, self.main_gdevice_handle)
        }
    }

    fn active_seeded_screen_clut_for_offscreen_clone(
        &self,
        source_gdevice: u32,
        explicit_ctab_handle: u32,
    ) -> Option<[[u16; 3]; 256]> {
        if explicit_ctab_handle != 0
            || source_gdevice != self.main_gdevice_handle
            || self.tick_count >= self.seeded_picture_palette_until_tick
            || Self::uses_canonical_system_8bpp_clut(&self.seeded_picture_palette)
        {
            None
        } else {
            Some(self.seeded_picture_palette)
        }
    }

    fn free_handle_and_target(bus: &mut MacMemoryBus, handle: u32) {
        if handle == 0 {
            return;
        }
        let ptr = bus.read_long(handle);
        if ptr != 0 {
            bus.free(ptr);
        }
        // Clear the master pointer cell before releasing it so a later
        // RecoverHandle scan cannot rediscover a stale owner for the same
        // payload address.
        bus.write_long(handle, 0);
        bus.free(handle);
    }

    fn seed_gworld_pixels_state(&mut self, pmh: u32, newgworld_flags: u32) {
        if pmh == 0 {
            return;
        }

        const PIX_PURGE: u32 = 1 << 0;
        const KEEP_LOCAL: u32 = 1 << 3;
        const PIXELS_PURGEABLE: u32 = 1 << 6;

        let mut state = 0;
        if (newgworld_flags & PIX_PURGE) != 0 {
            state |= PIXELS_PURGEABLE;
        }
        if (newgworld_flags & KEEP_LOCAL) != 0 {
            state |= KEEP_LOCAL;
        }

        self.gworld_pixel_states.insert(pmh, state);
    }

    fn gworld_pixels_state(&self, pmh: u32) -> u32 {
        self.gworld_pixel_states.get(&pmh).copied().unwrap_or(0)
    }

    fn set_gworld_pixels_state(&mut self, pmh: u32, state: u32) {
        if pmh == 0 {
            return;
        }

        const KEEP_LOCAL: u32 = 1 << 3;
        const PIXELS_PURGEABLE: u32 = 1 << 6;
        const PIXELS_LOCKED: u32 = 1 << 7;
        let existing = self.gworld_pixels_state(pmh);
        let masked = state & (KEEP_LOCAL | PIXELS_PURGEABLE | PIXELS_LOCKED);
        let preserved = existing & !(PIXELS_PURGEABLE | PIXELS_LOCKED);
        self.gworld_pixel_states.insert(pmh, preserved | masked);
    }

    fn set_gworld_pixels_purgeable(&mut self, pmh: u32, purgeable: bool) {
        if pmh == 0 {
            return;
        }

        const PIXELS_PURGEABLE: u32 = 1 << 6;
        let mut state = self.gworld_pixels_state(pmh);
        if purgeable {
            state |= PIXELS_PURGEABLE;
        } else {
            state &= !PIXELS_PURGEABLE;
        }
        self.set_gworld_pixels_state(pmh, state);
    }

    fn lock_gworld_pixels(&mut self, pmh: u32) -> bool {
        if pmh != 0 {
            const PIXELS_LOCKED: u32 = 1 << 7;
            let state = self.gworld_pixels_state(pmh) | PIXELS_LOCKED;
            self.set_gworld_pixels_state(pmh, state);
        }
        true
    }

    fn unlock_gworld_pixels(&mut self, pmh: u32) {
        if pmh == 0 {
            return;
        }

        const PIXELS_LOCKED: u32 = 1 << 7;
        let state = self.gworld_pixels_state(pmh) & !PIXELS_LOCKED;
        self.set_gworld_pixels_state(pmh, state);
    }

    fn dispose_gworld_port<C: CpuOps>(&mut self, cpu: &mut C, bus: &mut MacMemoryBus, port: u32) {
        if port == 0 {
            return;
        }

        let attached_gdevice = self.gworld_devices.remove(&port).unwrap_or(0);
        let pixmap_handle = bus.read_long(port + 2);
        self.gworld_pixel_states.remove(&pixmap_handle);
        let owns_attached_gdevice = if attached_gdevice != 0 {
            let gd_ptr = bus.read_long(attached_gdevice);
            gd_ptr
                .checked_add(22)
                .map(|addr| bus.read_long(addr) == pixmap_handle)
                .unwrap_or(false)
        } else {
            false
        };

        if self.current_port == port {
            let main_gdevice = self.ensure_main_gdevice(bus);
            let fallback_port = self.ensure_color_window_manager_port(bus);
            self.set_current_port_state(bus, cpu, fallback_port, Some(main_gdevice));
        } else if owns_attached_gdevice && attached_gdevice == self.current_gdevice {
            let main_gdevice = self.ensure_main_gdevice(bus);
            if self.current_port != 0 {
                self.set_current_port_state(bus, cpu, self.current_port, Some(main_gdevice));
            } else {
                self.current_gdevice = main_gdevice;
            }
        }

        // IM:VI 1991 p. 21-19 / IWQD 1994 p. 6-25: DisposeGWorld tears down
        // the offscreen port structure and any GDevice created for it. Keep
        // the PixMap / color table / pixel image allocations alive as the HLE
        // compatibility compromise: some games retain those raw guest pointers
        // after disposal, and eagerly recycling them aliases later scratch
        // buffers into stale restore paths.
        if pixmap_handle != 0 {
            // Compatibility path for stale `&gworld->portBits` pointers:
            // preserve a mapping from the disposed `port+2` address to the
            // still-live PixMap pointer so resolve_copy_bitmap can recover.
            self.cache_portbits_pixmap_handle(bus, port + 2, pixmap_handle);
        }
        Self::free_handle_and_target(bus, bus.read_long(port + 24));
        Self::free_handle_and_target(bus, bus.read_long(port + 28));
        if owns_attached_gdevice {
            let gd_ptr = bus.read_long(attached_gdevice);
            if gd_ptr != 0 {
                bus.free(gd_ptr);
            }
            bus.free(attached_gdevice);
        }
        bus.free(port);
        self.cport_ports.remove(&port);
        if self.manual_cport_presented_port == port {
            self.manual_cport_presented_port = 0;
        }
        self.port_draw_states.remove(&port);
    }

    pub(super) fn effective_destination_ctab_handle(
        &self,
        base: u32,
        row_bytes: u32,
        pixel_size: u32,
        ctab_handle: u32,
    ) -> u32 {
        if base == self.screen_mode.0
            && row_bytes == self.screen_mode.1
            && pixel_size == u32::from(self.screen_mode.4)
        {
            0
        } else {
            ctab_handle
        }
    }

    fn copy_bits_destination_ctab_handle(
        &self,
        bus: &MacMemoryBus,
        current_port: u32,
        dst_info: &CopyBitmapInfo,
    ) -> u32 {
        let current_device_ctab_handle = self.current_gdevice_ctab_handle(bus);
        if current_device_ctab_handle == 0 {
            return dst_info.ctab_handle;
        }

        // Screen-targeted blits should use the live device CLUT view
        // (color_manager_clut, returned via ctab_handle 0). The GDevice's
        // in-memory ColorTable may be stale — during seeded palette windows,
        // PreserveSeededPicture updates device_clut and color_manager_clut
        // but skips the GDevice ctab write. Check the screen case FIRST
        // so we never return the GDevice ctab handle for screen blits.
        if dst_info.base == self.screen_mode.0
            && dst_info.row_bytes == self.screen_mode.1
            && dst_info.pixel_size == u32::from(self.screen_mode.4)
        {
            return 0;
        }

        // For non-screen destinations: QuickDraw uses the current GDevice
        // for palette mapping when the destination is the current port.
        // If we're blitting into some other PixMap (for example, copying
        // an offscreen world while the offscreen port is current), use
        // that destination bitmap's table instead.
        let current_bits_ptr = self.bitmap_ptr_for_port(bus, current_port);
        if current_bits_ptr != 0 {
            let current_info = self.resolve_copy_bitmap(bus, current_bits_ptr);
            let dst_is_current_port = current_info.base == dst_info.base
                && current_info.row_bytes == dst_info.row_bytes
                && current_info.bounds_top == dst_info.bounds_top
                && current_info.bounds_left == dst_info.bounds_left
                && current_info.bounds_bottom == dst_info.bounds_bottom
                && current_info.bounds_right == dst_info.bounds_right
                && current_info.pixel_size == dst_info.pixel_size;
            if dst_is_current_port {
                return current_device_ctab_handle;
            }
        }

        {
            self.effective_destination_ctab_handle(
                dst_info.base,
                dst_info.row_bytes,
                dst_info.pixel_size,
                dst_info.ctab_handle,
            )
        }
    }

    pub(crate) fn set_current_port_state<C: CpuOps>(
        &mut self,
        bus: &mut MacMemoryBus,
        cpu: &mut C,
        port: u32,
        gdh: Option<u32>,
    ) {
        self.save_current_port_draw_state();
        let resolved_gdh = gdh.unwrap_or_else(|| self.gdevice_for_port(bus, port));
        self.current_port = port;
        self.current_gdevice = resolved_gdh;
        // Re-selecting a port re-arms QDDone for that port.
        self.qddone_seen_ports.remove(&port);
        bus.write_long(crate::memory::globals::addr::THE_PORT, port);

        let a5 = cpu.read_reg(Register::A5);
        let global_ptr = bus.read_long(a5);
        if global_ptr != 0 {
            bus.write_long(global_ptr, port);
        }

        if port != 0 {
            self.restore_port_draw_state(bus, port);
        }
    }

    fn gdevice_for_port(&mut self, bus: &mut MacMemoryBus, port: u32) -> u32 {
        if port == 0 {
            return 0;
        }

        self.gworld_devices
            .get(&port)
            .copied()
            .unwrap_or_else(|| self.ensure_main_gdevice(bus))
    }

    fn save_current_port_draw_state(&mut self) {
        if self.current_port == 0 {
            return;
        }

        self.port_draw_states
            .insert(self.current_port, self.current_draw_state());
    }

    fn current_draw_state(&self) -> PortDrawState {
        PortDrawState {
            fg_color: self.fg_color,
            bg_color: self.bg_color,
            bk_pat: self.bk_pat,
            pn_loc: self.pn_loc,
            pn_size: self.pn_size,
            pn_mode: self.pn_mode,
            pn_pat: self.pn_pat,
            tx_font: self.tx_font,
            tx_face: self.tx_face,
            tx_mode: self.tx_mode,
            tx_size: self.tx_size,
        }
    }

    fn restore_port_draw_state(&mut self, bus: &mut MacMemoryBus, port: u32) {
        if self.load_port_draw_state(bus, port) {
            self.port_draw_states
                .insert(port, self.current_draw_state());
            return;
        }

        if !self.port_allows_draw_state_memory_sync(bus, port) {
            return;
        }

        let state = self
            .port_draw_states
            .get(&port)
            .copied()
            .unwrap_or_default();
        self.fg_color = state.fg_color;
        self.bg_color = state.bg_color;
        self.bk_pat = state.bk_pat;
        self.pn_loc = state.pn_loc;
        self.pn_size = state.pn_size;
        self.pn_mode = state.pn_mode;
        self.pn_pat = state.pn_pat;
        self.tx_font = state.tx_font;
        self.tx_face = state.tx_face;
        self.tx_mode = state.tx_mode;
        self.tx_size = state.tx_size;
        self.sync_port_draw_state(bus, port);
    }

    fn load_port_draw_state(&mut self, bus: &MacMemoryBus, port: u32) -> bool {
        if port == 0 {
            return false;
        }

        let port_version = bus.read_word(port + 6);
        if (port_version & 0xC000) != 0xC000 {
            return false;
        }
        if !self.known_port(port) && !Self::looks_like_cgraf_port(bus, port) {
            return false;
        }

        let cached = self
            .port_draw_states
            .get(&port)
            .copied()
            .unwrap_or_default();
        self.fg_color = (
            bus.read_word(port + 36),
            bus.read_word(port + 38),
            bus.read_word(port + 40),
        );
        self.bg_color = (
            bus.read_word(port + 42),
            bus.read_word(port + 44),
            bus.read_word(port + 46),
        );
        self.bk_pat = cached.bk_pat;
        self.pn_loc = (
            bus.read_word(port + 48) as i16,
            bus.read_word(port + 50) as i16,
        );
        self.pn_size = (
            bus.read_word(port + 52) as i16,
            bus.read_word(port + 54) as i16,
        );
        self.pn_mode = bus.read_word(port + 56) as i16;
        self.pn_pat = cached.pn_pat;
        self.pn_vis = bus.read_word(port + 66) as i16;
        self.tx_font = bus.read_word(port + 68) as i16;
        self.tx_face = decode_text_face_style(bus.read_word(port + 70));
        self.tx_mode = bus.read_word(port + 72) as i16;
        self.tx_size = bus.read_word(port + 74) as i16;
        true
    }

    fn copy_bits_port_draw_colors(&self, bus: &MacMemoryBus, port: u32) -> ([u16; 3], [u16; 3]) {
        if port != 0 {
            let port_version = bus.read_word(port + 6);
            if (port_version & 0xC000) == 0xC000 {
                return (
                    [
                        bus.read_word(port + 36),
                        bus.read_word(port + 38),
                        bus.read_word(port + 40),
                    ],
                    [
                        bus.read_word(port + 42),
                        bus.read_word(port + 44),
                        bus.read_word(port + 46),
                    ],
                );
            }
            if let Some(state) = self.port_draw_states.get(&port) {
                return (
                    [state.fg_color.0, state.fg_color.1, state.fg_color.2],
                    [state.bg_color.0, state.bg_color.1, state.bg_color.2],
                );
            }
        }

        (
            [self.fg_color.0, self.fg_color.1, self.fg_color.2],
            [self.bg_color.0, self.bg_color.1, self.bg_color.2],
        )
    }

    /// Apply the side effects of `SetDepth(depth)` from the Graphics
    /// Devices Manager. Updates ScrnBase, screenBits, the QD globals
    /// copy of screenBits, the main GDevice PixMap, and
    /// `self.screen_mode`, then clears the active framebuffer.
    ///
    /// Imaging With QuickDraw 1994 pp. 5-34..5-35: SetDepth changes
    /// the pixel depth and color/B&W mode of the requested device.
    /// Systemless currently wires this for the two depths its screen
    /// renderer supports directly: 1bpp B&W and 8bpp indexed color.
    pub(crate) fn do_setdepth<C: CpuOps>(
        &mut self,
        cpu: &mut C,
        bus: &mut MacMemoryBus,
        depth: u16,
    ) {
        self.do_setdepth_with_geometry(
            cpu,
            bus,
            depth,
            ORACLE_MACHINE_PROFILE.screen_width,
            ORACLE_MACHINE_PROFILE.screen_height,
        );
    }

    fn row_bytes_for_depth(width: u16, depth: u16) -> Option<u32> {
        match depth {
            1 => Some(u32::from(width).div_ceil(8)),
            8 => Some(u32::from(width)),
            _ => None,
        }
    }

    fn display_mode_geometry(mode: u32) -> Option<(u16, u16)> {
        match mode {
            // Video.h / Display Manager mode token observed from BasiliskII
            // when games request the 640x480 8bpp switch.
            0x0000_0080 => Some((640, 480)),
            // Default 800x600 8bpp mode token used by the single-screen HLE.
            0x0000_0085 => Some((
                ORACLE_MACHINE_PROFILE.screen_width,
                ORACLE_MACHINE_PROFILE.screen_height,
            )),
            _ => None,
        }
    }

    fn current_screen_geometry(&self) -> (u16, u16) {
        let (_, _, width, height, _) = self.screen_mode;
        if width != 0 && height != 0 {
            (width, height)
        } else {
            (
                ORACLE_MACHINE_PROFILE.screen_width,
                ORACLE_MACHINE_PROFILE.screen_height,
            )
        }
    }

    fn sync_main_gdevice_geometry(
        &mut self,
        bus: &mut MacMemoryBus,
        width: u16,
        height: u16,
        row_bytes: u32,
        depth: u16,
    ) {
        let gdh = self.ensure_main_gdevice(bus);
        let gd = bus.read_long(gdh);
        if gd == 0 {
            return;
        }
        let pm_handle = bus.read_long(gd + 22);
        let pm = if pm_handle != 0 {
            bus.read_long(pm_handle)
        } else {
            0
        };
        if pm != 0 {
            bus.write_word(pm + 4, 0x8000 | row_bytes as u16);
            bus.write_word(pm + 6, 0);
            bus.write_word(pm + 8, 0);
            bus.write_word(pm + 10, height);
            bus.write_word(pm + 12, width);
            bus.write_word(pm + 32, depth);
            bus.write_word(pm + 34, 1);
            bus.write_word(pm + 36, depth);
            let ctab_handle = bus.read_long(pm + 42);
            let ctab = if ctab_handle != 0 {
                bus.read_long(ctab_handle)
            } else {
                0
            };
            if ctab != 0 {
                bus.write_long(ctab, u32::from(depth));
            }
        }
        let mut flags = bus.read_word(gd + 20);
        if depth == 1 {
            flags &= !0x0001;
        } else {
            flags |= 0x0001;
        }
        bus.write_word(gd + 20, flags);
        bus.write_word(gd + 34, 0);
        bus.write_word(gd + 36, 0);
        bus.write_word(gd + 38, height);
        bus.write_word(gd + 40, width);
    }

    fn do_setdepth_with_geometry<C: CpuOps>(
        &mut self,
        cpu: &mut C,
        bus: &mut MacMemoryBus,
        depth: u16,
        width: u16,
        height: u16,
    ) {
        let Some(row_bytes) = Self::row_bytes_for_depth(width, depth) else {
            return;
        };
        let ram_size = bus.ram_size();
        let color_screen_base = ram_size - 0x80000;
        bus.write_long(0x0824, color_screen_base); // ScrnBase

        let sb = crate::memory::globals::addr::SCREEN_BITS;
        bus.write_long(sb, color_screen_base);
        bus.write_word(sb + 4, row_bytes as u16);
        bus.write_word(sb + 6, 0);
        bus.write_word(sb + 8, 0);
        bus.write_word(sb + 10, height);
        bus.write_word(sb + 12, width);

        // Also update the QD globals copy of screenBits.
        let a5 = cpu.read_reg(Register::A5);
        let global_ptr = bus.read_long(a5);
        if global_ptr != 0 {
            let sb = global_ptr.wrapping_sub(122);
            bus.write_long(sb, color_screen_base);
            bus.write_word(sb + 4, row_bytes as u16);
            bus.write_word(sb + 6, 0);
            bus.write_word(sb + 8, 0);
            bus.write_word(sb + 10, height);
            bus.write_word(sb + 12, width);
        }

        self.sync_main_gdevice_geometry(bus, width, height, row_bytes, depth);

        let clear_byte = if depth == 1 { 0x00 } else { 0xFF };
        for i in 0..(row_bytes * u32::from(height)) {
            bus.write_byte(color_screen_base + i, clear_byte);
        }
        self.screen_mode = (color_screen_base, row_bytes, width, height, depth);
    }

    pub(super) fn sync_current_port_draw_state(&mut self, bus: &mut MacMemoryBus) {
        if self.current_port == 0 {
            return;
        }
        if !self.port_allows_draw_state_memory_sync(bus, self.current_port) {
            return;
        }
        let state = self.current_draw_state();
        self.port_draw_states.insert(self.current_port, state);
        self.sync_port_draw_state(bus, self.current_port);
    }

    fn sync_port_draw_state(&self, bus: &mut MacMemoryBus, port: u32) {
        if port == 0 {
            return;
        }
        if !self.port_allows_draw_state_memory_sync(bus, port) {
            return;
        }
        let port_version = bus.read_word(port + 6);
        let is_cgrafport = (port_version & 0xC000) == 0xC000;

        // Offsets 36-46: rgbFgColor/rgbBkColor in CGrafPort only.
        // In a basic GrafPort these bytes are bkPat/fillPat, so only
        // write RGB values when the port really is a CGrafPort.
        // Inside Macintosh Volume V, V-48 (CGrafPort record).
        if is_cgrafport {
            bus.write_word(port + 36, self.fg_color.0);
            bus.write_word(port + 38, self.fg_color.1);
            bus.write_word(port + 40, self.fg_color.2);
            bus.write_word(port + 42, self.bg_color.0);
            bus.write_word(port + 44, self.bg_color.1);
            bus.write_word(port + 46, self.bg_color.2);
        } else {
            // Classic GrafPort layout keeps bkPat at +32 and pnPat at +58
            // (IM:I I-148). Keep those fields synchronized so callers that
            // inspect the port record observe BackPat/PenPat updates.
            for (i, byte) in self.bk_pat.iter().enumerate() {
                bus.write_byte(port + 32 + i as u32, *byte);
            }
            for (i, byte) in self.pn_pat.iter().enumerate() {
                bus.write_byte(port + 58 + i as u32, *byte);
            }
        }

        // Offsets 48+: pnLoc, pnSize, pnMode, pnVis, txFont, txFace,
        // txMode, txSize are at the same offsets in both GrafPort and
        // CGrafPort.  Inside Macintosh Volume I, I-148.
        bus.write_word(port + 48, self.pn_loc.0 as u16);
        bus.write_word(port + 50, self.pn_loc.1 as u16);
        bus.write_word(port + 52, self.pn_size.0 as u16);
        bus.write_word(port + 54, self.pn_size.1 as u16);
        bus.write_word(port + 56, self.pn_mode as u16);
        bus.write_word(port + 66, self.pn_vis as u16);
        bus.write_word(port + 68, self.tx_font as u16);
        bus.write_word(port + 70, encode_text_face_style(self.tx_face));
        bus.write_word(port + 72, self.tx_mode as u16);
        bus.write_word(port + 74, self.tx_size as u16);
        let fg_legacy = if self.fg_color == (0, 0, 0) {
            0x00000021
        } else if self.fg_color == (0xFFFF, 0xFFFF, 0xFFFF) {
            0x0000001E
        } else {
            0
        };
        let bg_legacy = if self.bg_color == (0, 0, 0) {
            0x00000021
        } else if self.bg_color == (0xFFFF, 0xFFFF, 0xFFFF) {
            0x0000001E
        } else {
            0
        };
        bus.write_long(port + 80, fg_legacy);
        bus.write_long(port + 84, bg_legacy);
    }

    fn known_port(&self, port: u32) -> bool {
        self.port_draw_states.contains_key(&port)
            || self.cport_ports.contains(&port)
            || self.gworld_devices.contains_key(&port)
            || self.window_list.contains(&port)
            || self.front_window == port
            || self
                .window_stack
                .iter()
                .any(|(window, _, _, _)| *window == port)
    }

    fn port_allows_draw_state_memory_sync(&self, bus: &MacMemoryBus, port: u32) -> bool {
        if !Self::guest_range_in_ram(bus, port, 88) {
            return false;
        }
        if self.known_port(port) {
            return true;
        }
        !Self::looks_like_68k_entry_point(bus, port)
    }

    fn guest_range_in_ram(bus: &MacMemoryBus, addr: u32, len: u32) -> bool {
        addr.checked_add(len)
            .is_some_and(|end| end <= bus.ram_size())
    }

    fn looks_like_68k_entry_point(bus: &MacMemoryBus, addr: u32) -> bool {
        if !Self::guest_range_in_ram(bus, addr, 2) {
            return false;
        }
        match bus.read_word(addr) {
            // Common procedure starts and jump-table stubs. A GrafPort's
            // first word is the device field, so accepting one of these as
            // a port would let QuickDraw state writes corrupt executable
            // memory when an app accidentally passes a proc pointer.
            0x4EF9 | // JMP absolute long
            0x4EFA | // JMP PC-relative
            0x4EB9 | // JSR absolute long
            0x4E56 | // LINK
            0x48E7 | // MOVEM predecrement
            0xA9F0 | // LoadSeg
            0x3F3C => true, // MOVE.W #imm,-(SP) jump-table glue
            op if (op & 0xF000) == 0x6000 => true, // BRA/BSR/Bcc
            _ => false,
        }
    }

    fn handle_points_to_guest_range(bus: &MacMemoryBus, handle: u32, min_len: u32) -> bool {
        if !Self::guest_range_in_ram(bus, handle, 4) {
            return false;
        }
        let ptr = bus.read_long(handle);
        ptr != 0 && Self::guest_range_in_ram(bus, ptr, min_len)
    }

    fn sane_bitmap_geometry(row_bytes: u16, top: i16, left: i16, bottom: i16, right: i16) -> bool {
        let row_bytes = row_bytes & 0x3FFF;
        row_bytes > 0 && row_bytes <= 8192 && bottom >= top && right >= left
    }

    fn looks_like_cgraf_port(bus: &MacMemoryBus, port: u32) -> bool {
        if !Self::guest_range_in_ram(bus, port, 92) {
            return false;
        }
        if (bus.read_word(port + 6) & 0xC000) != 0xC000 {
            return false;
        }
        let pixmap_handle = bus.read_long(port + 2);
        if !Self::handle_points_to_guest_range(bus, pixmap_handle, 50) {
            return false;
        }
        let pixmap = bus.read_long(pixmap_handle);
        Self::sane_bitmap_geometry(
            bus.read_word(pixmap + 4),
            bus.read_word(pixmap + 6) as i16,
            bus.read_word(pixmap + 8) as i16,
            bus.read_word(pixmap + 10) as i16,
            bus.read_word(pixmap + 12) as i16,
        )
    }

    fn bitmap_ptr_for_port(&self, bus: &MacMemoryBus, port: u32) -> u32 {
        if port == 0 {
            return 0;
        }

        let port_version = bus.read_word(port + 6);
        if (port_version & 0xC000) == 0xC000 {
            let pixmap_handle = bus.read_long(port + 2);
            if pixmap_handle != 0 {
                return bus.read_long(pixmap_handle);
            }
            return 0;
        }

        port + 2
    }

    fn materialize_recent_spooled_picture(
        &mut self,
        bus: &mut MacMemoryBus,
        pic_ptr: u32,
    ) -> Option<u32> {
        let pic_size = usize::from(bus.read_word(pic_ptr));
        if pic_size <= 10 {
            return None;
        }

        let frame_top = bus.read_word(pic_ptr + 2) as i16;
        let frame_left = bus.read_word(pic_ptr + 4) as i16;
        let frame_bottom = bus.read_word(pic_ptr + 6) as i16;
        let frame_right = bus.read_word(pic_ptr + 8) as i16;
        let frame_height = i32::from(frame_bottom) - i32::from(frame_top);
        let frame_width = i32::from(frame_right) - i32::from(frame_left);
        if frame_bottom <= frame_top
            || frame_right <= frame_left
            || frame_height > 8192
            || frame_width > 8192
        {
            return None;
        }

        let recent = self.recent_file_read.clone()?;
        if recent.buffer != pic_ptr || recent.bytes_read != 10 {
            return None;
        }

        let file = self.vfs.get(&recent.filename)?;
        // Large spooled PICT files are delimited by EndOfPicture, not by the
        // 16-bit picSize field. QuickDraw intentionally reads until that
        // opcode. Inside Macintosh Volume V, V-92.
        let materialized_size =
            super::pict::picture_stream_len(file.get(recent.start..)?).unwrap_or(pic_size);
        let file_end = recent.start.checked_add(materialized_size)?;
        if file_end > file.len() {
            return None;
        }
        let loaded_header = bus.read_bytes(pic_ptr, 10);
        if file.get(recent.start..recent.start + 10)? != loaded_header.as_slice() {
            return None;
        }

        let materialized = bus.alloc(materialized_size as u32);
        if materialized == 0 {
            return None;
        }
        bus.write_bytes(materialized, &file[recent.start..file_end]);
        self.file_positions.insert(recent.ref_num, file_end);
        self.recent_file_read = Some(super::dispatch::RecentFileRead {
            ref_num: recent.ref_num,
            filename: recent.filename.clone(),
            buffer: materialized,
            start: recent.start,
            bytes_read: materialized_size,
        });
        if trace_drawpicture_enabled() {
            eprintln!(
                "[DRAWPICTURE] materialized spooled PICT ref={} file=\"{}\" offset={} size={} temp=${:08X}",
                recent.ref_num, recent.filename, recent.start, materialized_size, materialized
            );
        }
        Some(materialized)
    }

    fn sanitize_copy_bitmap_bounds(
        info: &mut CopyBitmapInfo,
        rect_top: i16,
        rect_left: i16,
        rect_bottom: i16,
        rect_right: i16,
    ) {
        if info.bounds_bottom > info.bounds_top && info.bounds_right > info.bounds_left {
            return;
        }
        if rect_bottom <= rect_top || rect_right <= rect_left {
            return;
        }
        info.bounds_top = rect_top;
        info.bounds_left = rect_left;
        info.bounds_bottom = rect_bottom;
        info.bounds_right = rect_right;
    }

    fn inert_copy_bitmap_info() -> CopyBitmapInfo {
        CopyBitmapInfo {
            base: u32::MAX,
            row_bytes: 0,
            bounds_top: 0,
            bounds_left: 0,
            bounds_bottom: 0,
            bounds_right: 0,
            pixel_size: 0,
            ctab_handle: 0,
        }
    }

    fn copy_bits_common<C: CpuOps>(
        &mut self,
        cpu: &mut C,
        bus: &mut MacMemoryBus,
        src_bits_ptr: u32,
        dst_bits_ptr: u32,
        src_rect_ptr: u32,
        dst_rect_ptr: u32,
        mode: i16,
        mask_rgn: u32,
    ) -> Result<()> {
        let mut src_info = self.resolve_copy_bitmap(bus, src_bits_ptr);
        let mut dst_info = self.resolve_copy_bitmap(bus, dst_bits_ptr);

        if let Some(path) = dump_copybits_src_path() {
            if src_info.pixel_size == 8
                && src_info.bounds_bottom > src_info.bounds_top
                && src_info.bounds_right > src_info.bounds_left
            {
                Self::dump_bitmap_as_png(
                    bus,
                    &src_info,
                    &self.read_port_clut(bus, src_info.ctab_handle),
                    path,
                );
            }
        }

        let src_top = bus.read_word(src_rect_ptr) as i16;
        let src_left = bus.read_word(src_rect_ptr + 2) as i16;
        let src_bottom = bus.read_word(src_rect_ptr + 4) as i16;
        let src_right = bus.read_word(src_rect_ptr + 6) as i16;
        let dst_top = bus.read_word(dst_rect_ptr) as i16;
        let dst_left = bus.read_word(dst_rect_ptr + 2) as i16;
        let dst_bottom = bus.read_word(dst_rect_ptr + 4) as i16;
        let dst_right = bus.read_word(dst_rect_ptr + 6) as i16;
        let mode_base = (mode & 0x3F) as u16;
        Self::sanitize_copy_bitmap_bounds(&mut src_info, src_top, src_left, src_bottom, src_right);
        Self::sanitize_copy_bitmap_bounds(&mut dst_info, dst_top, dst_left, dst_bottom, dst_right);

        if trace_menu_redraw_enabled()
            && trace_menu_redraw_rect_intersects(dst_top, dst_left, dst_bottom, dst_right)
        {
            eprintln!(
                "[MENU-REDRAW] CopyBits srcBase=${:08X} dstBase=${:08X} mode={} src=({},{}..{},{} ) dst=({},{}..{},{} ) mask=${:08X}",
                src_info.base,
                dst_info.base,
                mode_base,
                src_top,
                src_left,
                src_bottom,
                src_right,
                dst_top,
                dst_left,
                dst_bottom,
                dst_right,
                mask_rgn,
            );
        }
        if trace_dialog_draw_enabled() && self.dialog_tracking.is_some() {
            eprintln!(
                "[DIALOG-DRAW] CopyBits current_port=${:08X} srcBase=${:08X} dstBase=${:08X} mode={} src=({},{}..{},{} ) dst=({},{}..{},{} ) mask=${:08X}",
                self.current_port,
                src_info.base,
                dst_info.base,
                mode_base,
                src_top,
                src_left,
                src_bottom,
                src_right,
                dst_top,
                dst_left,
                dst_bottom,
                dst_right,
                mask_rgn,
            );
        }
        if trace_dialog_port_dump_enabled() && self.dialog_tracking.is_some() {
            Self::trace_port_snapshot(bus, "CopyBits-current", self.current_port);
        }
        if trace_dialog_port_dump_enabled() && self.dialog_tracking.is_some() {
            Self::trace_port_snapshot(bus, "CopyBits-current", self.current_port);
        }

        if src_bottom <= src_top
            || src_right <= src_left
            || dst_bottom <= dst_top
            || dst_right <= dst_left
        {
            return Ok(());
        }

        // Clip destination to the current port's visRgn and clipRgn,
        // but only when the destination is the current port's bitmap.
        // Per Inside Macintosh Volume I, I-158.
        let a5 = cpu.read_reg(Register::A5);
        let global_ptr = bus.read_long(a5);
        let port = bus.read_long(global_ptr);

        let (mut clip_t, mut clip_l, mut clip_b, mut clip_r) =
            (dst_top, dst_left, dst_bottom, dst_right);

        let mut vis_rgn_handle = 0;
        let mut clip_rgn_handle = 0;
        let dst_is_port = if port != 0 {
            let port_version = bus.read_word(port + 6);
            let port_base = if (port_version & 0xC000) == 0xC000 {
                let pm_handle = bus.read_long(port + 2);
                if pm_handle != 0 {
                    let pm_ptr = bus.read_long(pm_handle);
                    Self::offscreen_pixmap_base_ptr(bus, pm_ptr)
                } else {
                    0
                }
            } else {
                bus.read_long(port + 2)
            };
            port_base == dst_info.base
        } else {
            false
        };
        if dst_is_port && port != 0 {
            vis_rgn_handle = bus.read_long(port + 24);
            if !Self::clip_rect_to_region_bbox(
                bus,
                vis_rgn_handle,
                &mut clip_t,
                &mut clip_l,
                &mut clip_b,
                &mut clip_r,
            ) {
                return Ok(());
            }
            clip_rgn_handle = bus.read_long(port + 28);
            if !Self::clip_rect_to_region_bbox(
                bus,
                clip_rgn_handle,
                &mut clip_t,
                &mut clip_l,
                &mut clip_b,
                &mut clip_r,
            ) {
                return Ok(());
            }
        }

        if !Self::clip_rect_to_region_bbox(
            bus,
            mask_rgn,
            &mut clip_t,
            &mut clip_l,
            &mut clip_b,
            &mut clip_r,
        ) {
            return Ok(());
        }

        clip_t = clip_t.max(dst_info.bounds_top);
        clip_l = clip_l.max(dst_info.bounds_left);
        clip_b = clip_b.min(dst_info.bounds_bottom);
        clip_r = clip_r.min(dst_info.bounds_right);

        let eff_width = clip_r - clip_l;
        let eff_height = clip_b - clip_t;
        if eff_width <= 0 || eff_height <= 0 {
            return Ok(());
        }
        if dst_info.base == self.screen_mode.0 {
            self.ensure_dialog_background_saved_for_screen_port(bus, port);
        }

        let mask_membership = Self::build_region_membership_cache(bus, mask_rgn, clip_t, clip_b);
        let vis_membership =
            Self::build_region_membership_cache(bus, vis_rgn_handle, clip_t, clip_b);
        let clip_membership =
            Self::build_region_membership_cache(bus, clip_rgn_handle, clip_t, clip_b);

        let trace_probes = if trace_menu_redraw_enabled() {
            trace_menu_probe_points()
                .into_iter()
                .filter_map(|(label, probe_y, probe_x)| {
                    if !trace_menu_rect_contains_point(
                        clip_t, clip_l, clip_b, clip_r, probe_y, probe_x,
                    ) {
                        return None;
                    }
                    let src_y =
                        Self::scale_coord(src_top, src_bottom, dst_top, dst_bottom, probe_y)?
                            as i16;
                    let src_x =
                        Self::scale_coord(src_left, src_right, dst_left, dst_right, probe_x)?
                            as i16;
                    let src_pixel = Self::read_bitmap_pixel(bus, &src_info, src_y, src_x);
                    let dst_before = Self::read_bitmap_pixel(bus, &dst_info, probe_y, probe_x);
                    Some((label, probe_y, probe_x, src_y, src_x, src_pixel, dst_before))
                })
                .collect::<Vec<_>>()
        } else {
            Vec::new()
        };

        let dst_ctab_handle = self.copy_bits_destination_ctab_handle(bus, port, &dst_info);
        let src_clut =
            (src_info.pixel_size == 8).then(|| self.read_port_clut(bus, src_info.ctab_handle));
        let dst_clut =
            (dst_info.pixel_size == 8).then(|| self.read_port_clut(bus, dst_ctab_handle));
        let src_ctab_seed = Self::ctab_seed(bus, src_info.ctab_handle);
        let dst_ctab_seed = Self::ctab_seed(bus, dst_ctab_handle);
        let hardware_palette_active =
            dst_ctab_handle == 0 && self.device_clut != self.color_manager_clut;
        let palette_translation = if src_info.pixel_size == 8
            && dst_info.pixel_size == 8
            && src_info.ctab_handle != dst_info.ctab_handle
            && matches!(src_ctab_seed, Some(src_seed) if src_seed != 0)
            && src_ctab_seed != dst_ctab_seed
            && !hardware_palette_active
        {
            match (src_clut.as_ref(), dst_clut.as_ref()) {
                (Some(src_clut), Some(dst_clut)) => {
                    Some(self.build_palette_translation(bus, src_clut, dst_clut, dst_ctab_handle))
                }
                _ => None,
            }
        } else {
            None
        };
        let src_clut = src_clut.as_ref();
        let dst_clut = dst_clut.as_ref();
        let palette_translation = palette_translation.as_ref();
        let (copy_fg_rgb, copy_bg_rgb) = self.copy_bits_port_draw_colors(bus, port);
        if trace_copybits_enabled()
            && (mask_rgn != 0
                || src_info.pixel_size != dst_info.pixel_size
                || src_right - src_left != dst_right - dst_left
                || src_bottom - src_top != dst_bottom - dst_top
                || palette_translation.is_some())
        {
            eprintln!(
                "[COPYBITS] src={}bpp dst={}bpp mode={} src=({},{}..{},{} ) dst=({},{}..{},{} ) mask=${:08X} translate={} srcSeed={:?} dstSeed={:?}",
                src_info.pixel_size,
                dst_info.pixel_size,
                mode_base,
                src_top,
                src_left,
                src_bottom,
                src_right,
                dst_top,
                dst_left,
                dst_bottom,
                dst_right,
                mask_rgn,
                palette_translation.is_some(),
                src_ctab_seed,
                dst_ctab_seed,
            );
        }
        let fg_index = dst_clut
            .as_ref()
            .map(|clut| self.palette_index_for_rgb(bus, dst_ctab_handle, clut, copy_fg_rgb));
        let bg_index = dst_clut
            .as_ref()
            .map(|clut| self.palette_index_for_rgb(bus, dst_ctab_handle, clut, copy_bg_rgb));
        let transparent_src_indices = if mode_base == 36 {
            self.copy_bits_transparent_source_indices(
                bus,
                src_info.ctab_handle,
                src_clut,
                copy_bg_rgb,
            )
        } else {
            None
        };
        // no_scaling precompute, same pattern as the primary CopyBits path.
        // Skips mul/div in scale_coord when src/dst dimensions match.
        let src_w = i32::from(src_right) - i32::from(src_left);
        let src_h = i32::from(src_bottom) - i32::from(src_top);
        let dst_w = i32::from(dst_right) - i32::from(dst_left);
        let dst_h = i32::from(dst_bottom) - i32::from(dst_top);
        let no_scaling = src_w == dst_w && src_h == dst_h && src_w > 0 && src_h > 0;
        let transformed_blit = !no_scaling
            || palette_translation.is_some()
            || src_info.pixel_size != dst_info.pixel_size
            || mode_base != 0
            || vis_rgn_handle != 0
            || clip_rgn_handle != 0
            || mask_rgn != 0;
        self.add_hle_tick_cost(Self::quickdraw_blit_tick_cost(
            eff_width as u32,
            eff_height as u32,
            src_info.pixel_size,
            dst_info.pixel_size,
            transformed_blit,
        ));

        let source_snapshot = if src_info.base == dst_info.base {
            let mut row_start = u32::MAX;
            let mut row_end = 0u32;
            for dy in clip_t..clip_b {
                let src_y = if no_scaling {
                    let rel = i32::from(dy) - i32::from(dst_top);
                    if rel < 0 || rel >= dst_h {
                        continue;
                    }
                    i32::from(src_top) + rel
                } else {
                    let Some(s) = Self::scale_coord(src_top, src_bottom, dst_top, dst_bottom, dy)
                    else {
                        continue;
                    };
                    s
                };
                if src_y < i32::from(src_info.bounds_top)
                    || src_y >= i32::from(src_info.bounds_bottom)
                {
                    continue;
                }
                let row = (src_y - i32::from(src_info.bounds_top)) as u32;
                row_start = row_start.min(row);
                row_end = row_end.max(row);
            }

            if row_start != u32::MAX {
                let row_count = row_end - row_start + 1;
                let mut snapshot = vec![0u8; (row_count * src_info.row_bytes) as usize];
                // Bulk per-row snapshot.
                for row in 0..row_count {
                    let row_base = src_info.base + (row_start + row) * src_info.row_bytes;
                    let snapshot_offset = (row * src_info.row_bytes) as usize;
                    let row_data = bus.read_bytes(row_base, src_info.row_bytes as usize);
                    snapshot[snapshot_offset..snapshot_offset + src_info.row_bytes as usize]
                        .copy_from_slice(&row_data);
                }
                Some((row_start, row_count, snapshot))
            } else {
                None
            }
        } else {
            None
        };
        let read_src_byte = |bus: &MacMemoryBus, addr: u32| -> u8 {
            if let Some((row_start, row_count, snapshot)) = source_snapshot.as_ref() {
                let offset = addr.saturating_sub(src_info.base);
                let row = offset / src_info.row_bytes;
                if row >= *row_start && row < *row_start + *row_count {
                    let row_offset = row - *row_start;
                    let col = offset % src_info.row_bytes;
                    let snapshot_index = (row_offset * src_info.row_bytes + col) as usize;
                    if snapshot_index < snapshot.len() {
                        return snapshot[snapshot_index];
                    }
                }
            }
            bus.read_byte(addr)
        };

        for dy in clip_t..clip_b {
            let src_y = if no_scaling {
                let rel = i32::from(dy) - i32::from(dst_top);
                if rel < 0 || rel >= dst_h {
                    continue;
                }
                i32::from(src_top) + rel
            } else {
                let Some(s) = Self::scale_coord(src_top, src_bottom, dst_top, dst_bottom, dy)
                else {
                    continue;
                };
                s
            };
            if src_y < i32::from(src_info.bounds_top) || src_y >= i32::from(src_info.bounds_bottom)
            {
                continue;
            }
            let dst_y = (i32::from(dy) - i32::from(dst_info.bounds_top)) as u32;

            for dx in clip_l..clip_r {
                let src_x = if no_scaling {
                    let rel = i32::from(dx) - i32::from(dst_left);
                    if rel < 0 || rel >= dst_w {
                        continue;
                    }
                    i32::from(src_left) + rel
                } else {
                    let Some(s) = Self::scale_coord(src_left, src_right, dst_left, dst_right, dx)
                    else {
                        continue;
                    };
                    s
                };
                if src_x < i32::from(src_info.bounds_left)
                    || src_x >= i32::from(src_info.bounds_right)
                {
                    continue;
                }
                if !Self::region_contains_point_cached(
                    bus,
                    vis_rgn_handle,
                    vis_membership.as_ref(),
                    dy,
                    dx,
                ) {
                    continue;
                }
                if !Self::region_contains_point_cached(
                    bus,
                    clip_rgn_handle,
                    clip_membership.as_ref(),
                    dy,
                    dx,
                ) {
                    continue;
                }
                if !Self::region_contains_point_cached(
                    bus,
                    mask_rgn,
                    mask_membership.as_ref(),
                    dy,
                    dx,
                ) {
                    continue;
                }

                let dst_x = (i32::from(dx) - i32::from(dst_info.bounds_left)) as u32;
                let src_x_off = (src_x - i32::from(src_info.bounds_left)) as u32;
                let src_y_off = (src_y - i32::from(src_info.bounds_top)) as u32;

                match (src_info.pixel_size, dst_info.pixel_size) {
                    (8, 8) => {
                        let src_addr = src_info.base + src_y_off * src_info.row_bytes + src_x_off;
                        let dst_addr = dst_info.base + dst_y * dst_info.row_bytes + dst_x;
                        let src_pixel = read_src_byte(bus, src_addr);
                        if mode_base == 36
                            && transparent_src_indices
                                .as_ref()
                                .map_or(false, |indices| indices[src_pixel as usize])
                        {
                            continue;
                        }
                        let Some(dst_pixel) = self.copy_bits_color_source_mode_pixel(
                            bus,
                            dst_ctab_handle,
                            src_clut,
                            dst_clut,
                            palette_translation,
                            mode_base,
                            src_pixel,
                            bus.read_byte(dst_addr),
                            copy_fg_rgb,
                            copy_bg_rgb,
                        ) else {
                            continue;
                        };
                        bus.write_byte(dst_addr, dst_pixel);
                    }
                    (src_bits, dst_bits)
                        if src_bits >= 8 && dst_bits >= 8 && src_bits == dst_bits =>
                    {
                        let bytes_per_pixel = src_bits / 8;
                        let src_addr = src_info.base
                            + src_y_off * src_info.row_bytes
                            + src_x_off * bytes_per_pixel;
                        let dst_addr =
                            dst_info.base + dst_y * dst_info.row_bytes + dst_x * bytes_per_pixel;
                        for byte_index in 0..bytes_per_pixel {
                            let pixel = read_src_byte(bus, src_addr + byte_index);
                            bus.write_byte(dst_addr + byte_index, pixel);
                        }
                    }
                    (1, 8) => {
                        let src_byte_offset = src_y_off * src_info.row_bytes + (src_x_off / 8);
                        let src_bit = 7 - (src_x_off % 8);
                        let src_byte = read_src_byte(bus, src_info.base + src_byte_offset);
                        let src_pixel = (src_byte & (1 << src_bit)) != 0;
                        let dst_addr = dst_info.base + dst_y * dst_info.row_bytes + dst_x;
                        let dst_pixel = bus.read_byte(dst_addr);
                        let dst_clut = dst_clut.unwrap();
                        let fg_index = fg_index.unwrap();
                        let bg_index = bg_index.unwrap();
                        let new_pixel = match mode_base {
                            0 => Some(if src_pixel { fg_index } else { bg_index }),
                            1 => src_pixel.then_some(fg_index),
                            2 => {
                                src_pixel.then(|| Self::inverted_palette_index(dst_clut, dst_pixel))
                            }
                            3 => src_pixel.then_some(bg_index),
                            4 => Some(if src_pixel { bg_index } else { fg_index }),
                            5 => (!src_pixel).then_some(fg_index),
                            6 => (!src_pixel)
                                .then(|| Self::inverted_palette_index(dst_clut, dst_pixel)),
                            7 => (!src_pixel).then_some(bg_index),
                            36 => src_pixel.then_some(fg_index),
                            _ => Some(if src_pixel { fg_index } else { bg_index }),
                        };
                        if let Some(pixel) = new_pixel {
                            bus.write_byte(dst_addr, pixel);
                        }
                    }
                    (1, 1) => {
                        let src_byte_offset = src_y_off * src_info.row_bytes + (src_x_off / 8);
                        let src_bit = 7 - (src_x_off % 8);
                        let src_byte = read_src_byte(bus, src_info.base + src_byte_offset);
                        let src_pixel = (src_byte & (1 << src_bit)) != 0;
                        let dst_byte_offset = dst_y * dst_info.row_bytes + (dst_x / 8);
                        let dst_bit = 7 - (dst_x % 8);
                        let dst_addr = dst_info.base + dst_byte_offset;
                        let dst_byte = bus.read_byte(dst_addr);
                        let dst_pixel = (dst_byte & (1 << dst_bit)) != 0;
                        let new_bit = match mode_base {
                            0 => src_pixel,
                            1 => src_pixel || dst_pixel,
                            2 => src_pixel ^ dst_pixel,
                            3 => !src_pixel && dst_pixel,
                            4 => !src_pixel,
                            5 => !src_pixel || dst_pixel,
                            6 => !src_pixel ^ dst_pixel,
                            7 => src_pixel && dst_pixel,
                            36 => src_pixel || dst_pixel,
                            _ => src_pixel,
                        };
                        if new_bit {
                            bus.write_byte(dst_addr, dst_byte | (1 << dst_bit));
                        } else {
                            bus.write_byte(dst_addr, dst_byte & !(1 << dst_bit));
                        }
                    }
                    _ => {}
                }
            }
        }

        if trace_menu_redraw_enabled() {
            for (label, probe_y, probe_x, src_y, src_x, src_pixel, dst_before) in trace_probes {
                let dst_after = Self::read_bitmap_pixel(bus, &dst_info, probe_y, probe_x);
                eprintln!(
                    "[MENU-REDRAW] CopyBits probe={} srcPt=({}, {}) srcPixel={:?} dstBefore={:?} dstAfter={:?}",
                    label,
                    src_y,
                    src_x,
                    src_pixel,
                    dst_before,
                    dst_after,
                );
            }
        }

        if dst_info.base == self.screen_mode.0 {
            self.refresh_visible_dialog_snapshot_after_bulk_port_draw(bus, self.current_port);
        }

        Ok(())
    }

    fn note_screen_copybits_rect(
        &mut self,
        src_top: i16,
        src_left: i16,
        src_bottom: i16,
        src_right: i16,
        dst_top: i16,
        dst_left: i16,
        dst_bottom: i16,
        dst_right: i16,
    ) {
        let src_width = src_right.saturating_sub(src_left);
        let src_height = src_bottom.saturating_sub(src_top);
        let dst_width = dst_right.saturating_sub(dst_left);
        let dst_height = dst_bottom.saturating_sub(dst_top);
        if src_width <= 0 || src_height <= 0 || dst_width <= 0 || dst_height <= 0 {
            return;
        }

        let (_, _, screen_width, screen_height, _) = self.screen_mode;
        let min_width = (screen_width as i16 / 2).max(1);
        let min_height = (screen_height as i16 / 2).max(1);
        if dst_width < min_width || dst_height < min_height {
            return;
        }

        self.last_screen_copybits_rect = Some(ScreenCopyBitsRect {
            src_top,
            src_left,
            src_bottom,
            src_right,
            dst_top,
            dst_left,
            dst_bottom,
            dst_right,
        });
    }

    pub(super) fn resolve_copy_bitmap(&self, bus: &MacMemoryBus, bits_ptr: u32) -> CopyBitmapInfo {
        if bits_ptr == 0 || !Self::guest_range_in_ram(bus, bits_ptr, 14) {
            return Self::inert_copy_bitmap_info();
        }

        let normalize_base = |base: u32| -> u32 {
            if base == 0 || base == u32::MAX {
                return base;
            }
            if base < bus.ram_size() {
                return base;
            }
            // Classic QuickDraw callers sometimes hand CopyBits a baseAddr
            // with high-byte tag bits while the actual pixel buffer lives in
            // low 24-bit RAM. If the full 32-bit address is unmapped but the
            // low 24-bit address is valid RAM, prefer the stripped address.
            // This avoids sampling unmapped zeroes for legacy/non-clean blits.
            let stripped = base & 0x00FF_FFFF;
            if stripped < bus.ram_size() {
                return stripped;
            }
            base
        };

        let raw_word = bus.read_word(bits_ptr + 4);
        // CopyBits identifies parameter "shape" using the high bits of the
        // word at offset +4 from a BitMap-style argument:
        //   - bit15 set => PixMap
        //   - bit14 set => treat baseAddr as handle (cGrafPort portBits path)
        // See "RowBytes Revealed II" (Apple DTS, 1993) and IM QuickDraw docs.
        //
        // Systemless previously only recognized the 0xC000 cGrafPort case. Some
        // real-world callers (including game blitters) hand arguments whose
        // bit14 is set without the full 0xC000 signature; QuickDraw still
        // applies the handle-dereference rule there, so mirror that behavior.
        if (raw_word & 0x4000) != 0 && (raw_word & 0xC000) != 0xC000 {
            let handle_like = normalize_base(bus.read_long(bits_ptr));
            if let Some(pm_ptr) = Self::pixmap_ptr_from_handle(bus, handle_like) {
                if let Some(info) = Self::cached_copy_bitmap_from_pixmap(bus, pm_ptr) {
                    return CopyBitmapInfo {
                        base: normalize_base(info.base),
                        row_bytes: info.row_bytes,
                        bounds_top: info.bounds_top,
                        bounds_left: info.bounds_left,
                        bounds_bottom: info.bounds_bottom,
                        bounds_right: info.bounds_right,
                        pixel_size: info.pixel_size,
                        ctab_handle: info.ctab_handle,
                    };
                }
            }
        }
        if (raw_word & 0xC000) == 0xC000 {
            let pm_handle = normalize_base(bus.read_long(bits_ptr));
            let direct_pm_ptr = Self::pixmap_ptr_from_handle(bus, pm_handle);
            let direct_info =
                direct_pm_ptr.and_then(|pm_ptr| Self::cached_copy_bitmap_from_pixmap(bus, pm_ptr));
            let cached_info = self.disposed_gworld_portbits.get(&bits_ptr).copied();
            let resolved_info = direct_info.or(cached_info);
            let Some(info) = resolved_info else {
                return CopyBitmapInfo {
                    base: u32::MAX,
                    row_bytes: 0,
                    bounds_top: 0,
                    bounds_left: 0,
                    bounds_bottom: 0,
                    bounds_right: 0,
                    pixel_size: 0,
                    ctab_handle: 0,
                };
            };
            return CopyBitmapInfo {
                base: normalize_base(info.base),
                row_bytes: info.row_bytes,
                bounds_top: info.bounds_top,
                bounds_left: info.bounds_left,
                bounds_bottom: info.bounds_bottom,
                bounds_right: info.bounds_right,
                pixel_size: info.pixel_size,
                ctab_handle: info.ctab_handle,
            };
        }
        if (raw_word & 0x8000) != 0 {
            if !Self::guest_range_in_ram(bus, bits_ptr, 46) {
                return Self::inert_copy_bitmap_info();
            }
            return CopyBitmapInfo {
                base: normalize_base(Self::offscreen_pixmap_base_ptr(bus, bits_ptr)),
                row_bytes: (raw_word & 0x3FFF) as u32,
                bounds_top: bus.read_word(bits_ptr + 6) as i16,
                bounds_left: bus.read_word(bits_ptr + 8) as i16,
                bounds_bottom: bus.read_word(bits_ptr + 10) as i16,
                bounds_right: bus.read_word(bits_ptr + 12) as i16,
                pixel_size: bus.read_word(bits_ptr + 32) as u32,
                ctab_handle: bus.read_long(bits_ptr + 42),
            };
        }

        let base = normalize_base(bus.read_long(bits_ptr));
        let (screen_base, _, _, _, screen_ps) = self.screen_mode;
        // If this plain BitMap's baseAddr matches the screen framebuffer and
        // the screen is >1bpp, the caller passed qd.screenBits for a color
        // screen.  Treat it as the actual screen pixel depth so CopyBits
        // reads the pixel data correctly instead of interpreting 8bpp bytes
        // as 1bpp packed bits.
        let pixel_size = if base == screen_base && screen_ps > 1 {
            screen_ps as u32
        } else {
            1
        };
        CopyBitmapInfo {
            base,
            row_bytes: (raw_word & 0x3FFF) as u32,
            bounds_top: bus.read_word(bits_ptr + 6) as i16,
            bounds_left: bus.read_word(bits_ptr + 8) as i16,
            bounds_bottom: bus.read_word(bits_ptr + 10) as i16,
            bounds_right: bus.read_word(bits_ptr + 12) as i16,
            pixel_size,
            ctab_handle: 0,
        }
    }

    fn read_bitmap_pixel(bus: &MacMemoryBus, info: &CopyBitmapInfo, y: i16, x: i16) -> Option<u8> {
        if y < info.bounds_top
            || y >= info.bounds_bottom
            || x < info.bounds_left
            || x >= info.bounds_right
        {
            return None;
        }
        let row = (y - info.bounds_top) as u32;
        let col = (x - info.bounds_left) as u32;
        match info.pixel_size {
            8 => Some(bus.read_byte(info.base + row * info.row_bytes + col)),
            1 => {
                let addr = info.base + row * info.row_bytes + (col / 8);
                let bit = 7 - (col % 8);
                let byte = bus.read_byte(addr);
                Some(if (byte & (1 << bit)) != 0 { 255 } else { 0 })
            }
            _ => None,
        }
    }

    fn read_packed_bitmap_index(
        bus: &MacMemoryBus,
        base: u32,
        row_bytes: u32,
        pixel_size: u32,
        bounds_top: i16,
        bounds_left: i16,
        bounds_bottom: i16,
        bounds_right: i16,
        y: i16,
        x: i16,
    ) -> Option<u8> {
        if base == 0
            || row_bytes == 0
            || y < bounds_top
            || y >= bounds_bottom
            || x < bounds_left
            || x >= bounds_right
        {
            return None;
        }
        let row = (y - bounds_top) as u32;
        let col = (x - bounds_left) as u32;
        match pixel_size {
            1 => {
                let byte = bus.read_byte(base + row * row_bytes + col / 8);
                let shift = 7 - (col % 8);
                Some((byte >> shift) & 0x01)
            }
            2 => {
                let byte = bus.read_byte(base + row * row_bytes + col / 4);
                let shift = 6 - (col % 4) * 2;
                Some((byte >> shift) & 0x03)
            }
            4 => {
                let byte = bus.read_byte(base + row * row_bytes + col / 2);
                let shift = 4 - (col % 2) * 4;
                Some((byte >> shift) & 0x0F)
            }
            8 => Some(bus.read_byte(base + row * row_bytes + col)),
            _ => None,
        }
    }

    fn seed_fill_1bpp_mask(
        bus: &mut MacMemoryBus,
        src_ptr: u32,
        dst_ptr: u32,
        src_row: i16,
        dst_row: i16,
        height: i16,
        words: i16,
        seed_h: i16,
        seed_v: i16,
    ) -> bool {
        if src_ptr == 0 || dst_ptr == 0 || src_row <= 0 || dst_row <= 0 || height <= 0 || words <= 0
        {
            return false;
        }

        let width = (words as usize).saturating_mul(16);
        let width_bytes = (words as usize).saturating_mul(2);
        let height = height as usize;
        let src_row = src_row as usize;
        let dst_row = dst_row as usize;
        if width == 0 || width_bytes == 0 || src_row < width_bytes || dst_row < width_bytes {
            return false;
        }
        let Some(pixel_count) = width.checked_mul(height) else {
            return false;
        };
        if pixel_count > SEEDFILL_MAX_PIXELS {
            return false;
        }
        let Some(src_len) = Self::raw_1bpp_image_len(src_row, height, width_bytes) else {
            return false;
        };
        let Some(dst_len) = Self::raw_1bpp_image_len(dst_row, height, width_bytes) else {
            return false;
        };
        if !Self::guest_range_in_ram(bus, src_ptr, src_len)
            || !Self::guest_range_in_ram(bus, dst_ptr, dst_len)
        {
            return false;
        }

        let mut source = Vec::with_capacity(pixel_count);
        for y in 0..height {
            for x in 0..width {
                source.push(Self::raw_1bpp_bit(bus, src_ptr, src_row, y, x));
            }
        }

        for y in 0..height {
            let row_addr = dst_ptr + (y * dst_row) as u32;
            for byte in 0..width_bytes {
                bus.write_byte(row_addr + byte as u32, 0);
            }
        }

        if seed_h < 0 || seed_v < 0 || seed_h as usize >= width || seed_v as usize >= height {
            return true;
        }

        let seed_x = seed_h as usize;
        let seed_y = seed_v as usize;
        let seed_value = source[seed_y * width + seed_x];
        let mut seen = vec![false; pixel_count];
        let mut stack = vec![(seed_x, seed_y)];

        while let Some((x, y)) = stack.pop() {
            let index = y * width + x;
            if seen[index] {
                continue;
            }
            seen[index] = true;
            if source[index] != seed_value {
                continue;
            }
            Self::set_raw_1bpp_bit(bus, dst_ptr, dst_row, y, x, true);

            if x > 0 {
                stack.push((x - 1, y));
            }
            if x + 1 < width {
                stack.push((x + 1, y));
            }
            if y > 0 {
                stack.push((x, y - 1));
            }
            if y + 1 < height {
                stack.push((x, y + 1));
            }
        }

        true
    }

    fn raw_1bpp_image_len(row_bytes: usize, height: usize, width_bytes: usize) -> Option<u32> {
        if height == 0 {
            return Some(0);
        }
        let len = height
            .checked_sub(1)?
            .checked_mul(row_bytes)?
            .checked_add(width_bytes)?;
        u32::try_from(len).ok()
    }

    fn raw_1bpp_bit(bus: &MacMemoryBus, base: u32, row_bytes: usize, y: usize, x: usize) -> bool {
        let addr = base + (y * row_bytes + x / 8) as u32;
        let bit = 7 - (x % 8);
        (bus.read_byte(addr) & (1u8 << bit)) != 0
    }

    fn set_raw_1bpp_bit(
        bus: &mut MacMemoryBus,
        base: u32,
        row_bytes: usize,
        y: usize,
        x: usize,
        value: bool,
    ) {
        let addr = base + (y * row_bytes + x / 8) as u32;
        let mask = 1u8 << (7 - (x % 8));
        let byte = bus.read_byte(addr);
        bus.write_byte(addr, if value { byte | mask } else { byte & !mask });
    }

    fn region_ptr_and_size(bus: &MacMemoryBus, rgn_handle: u32) -> Option<(u32, u32)> {
        if rgn_handle == 0 {
            return None;
        }
        let rgn_ptr = bus.read_long(rgn_handle);
        if rgn_ptr == 0 {
            return None;
        }
        Some((rgn_ptr, u32::from(bus.read_word(rgn_ptr))))
    }

    pub(crate) fn region_bbox(bus: &MacMemoryBus, rgn_handle: u32) -> Option<(i16, i16, i16, i16)> {
        let (rgn_ptr, _) = Self::region_ptr_and_size(bus, rgn_handle)?;
        let top = bus.read_word(rgn_ptr + 2) as i16;
        let left = bus.read_word(rgn_ptr + 4) as i16;
        let bottom = bus.read_word(rgn_ptr + 6) as i16;
        let right = bus.read_word(rgn_ptr + 8) as i16;
        (bottom > top && right > left).then_some((top, left, bottom, right))
    }

    fn clip_rect_to_region_bbox(
        bus: &MacMemoryBus,
        rgn_handle: u32,
        clip_t: &mut i16,
        clip_l: &mut i16,
        clip_b: &mut i16,
        clip_r: &mut i16,
    ) -> bool {
        if rgn_handle == 0 {
            return true;
        }
        let Some((top, left, bottom, right)) = Self::region_bbox(bus, rgn_handle) else {
            return false;
        };
        *clip_t = (*clip_t).max(top);
        *clip_l = (*clip_l).max(left);
        *clip_b = (*clip_b).min(bottom);
        *clip_r = (*clip_r).min(right);
        *clip_b > *clip_t && *clip_r > *clip_l
    }

    pub(super) fn region_is_complex(bus: &MacMemoryBus, rgn_handle: u32) -> bool {
        let Some((_, rgn_size)) = Self::region_ptr_and_size(bus, rgn_handle) else {
            return false;
        };
        rgn_size > REGION_HEADER_SIZE
    }

    fn drawpicture_rect_intersects_current_port_clip(
        bus: &MacMemoryBus,
        port: u32,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) -> bool {
        if port == 0 || bottom <= top || right <= left {
            return false;
        }

        for region_offset in [24u32, 28u32] {
            let rgn_handle = bus.read_long(port.wrapping_add(region_offset));
            if rgn_handle == 0 {
                continue;
            }
            let Some((rgn_top, rgn_left, rgn_bottom, rgn_right)) =
                Self::region_bbox(bus, rgn_handle)
            else {
                return false;
            };
            if top >= rgn_bottom || bottom <= rgn_top || left >= rgn_right || right <= rgn_left {
                return false;
            }
        }

        true
    }

    fn drawpicture_current_port_pixel_clip(
        bus: &MacMemoryBus,
        port: u32,
        bounds_top: i16,
        bounds_left: i16,
        port_w: u16,
        port_h: u16,
    ) -> Option<super::pict::DstClip> {
        if port == 0 {
            return None;
        }

        let mut clip_top = 0i32;
        let mut clip_left = 0i32;
        let mut clip_bottom = i32::from(port_h);
        let mut clip_right = i32::from(port_w);
        let mut regions = Vec::new();
        let bounds_bottom = bounds_top.saturating_add(port_h as i16);
        let bounds_right = bounds_left.saturating_add(port_w as i16);

        for region_offset in [24u32, 28u32] {
            let rgn_handle = bus.read_long(port.wrapping_add(region_offset));
            if rgn_handle == 0 {
                continue;
            }
            let Some((rgn_top, rgn_left, rgn_bottom, rgn_right)) =
                Self::region_bbox(bus, rgn_handle)
            else {
                return Some(super::pict::DstClip::new((0, 0, 0, 0), Vec::new()));
            };
            let region_top = i32::from(rgn_top) - i32::from(bounds_top);
            let region_left = i32::from(rgn_left) - i32::from(bounds_left);
            let region_bottom = i32::from(rgn_bottom) - i32::from(bounds_top);
            let region_right = i32::from(rgn_right) - i32::from(bounds_left);
            clip_top = clip_top.max(region_top);
            clip_left = clip_left.max(region_left);
            clip_bottom = clip_bottom.min(region_bottom);
            clip_right = clip_right.min(region_right);

            let clipped_top = rgn_top.max(bounds_top);
            let clipped_left = rgn_left.max(bounds_left);
            let clipped_bottom = rgn_bottom.min(bounds_bottom);
            let clipped_right = rgn_right.min(bounds_right);
            if clipped_bottom <= clipped_top || clipped_right <= clipped_left {
                return Some(super::pict::DstClip::new((0, 0, 0, 0), Vec::new()));
            }

            let Some((_, rgn_size)) = Self::region_ptr_and_size(bus, rgn_handle) else {
                return Some(super::pict::DstClip::new((0, 0, 0, 0), Vec::new()));
            };
            if rgn_size <= REGION_HEADER_SIZE {
                regions.push(super::pict::DstClipRegion::rectangular(
                    i32::from(clipped_top) - i32::from(bounds_top),
                    i32::from(clipped_left) - i32::from(bounds_left),
                    i32::from(clipped_bottom) - i32::from(bounds_top),
                    i32::from(clipped_right) - i32::from(bounds_left),
                ));
                continue;
            }

            let Some(cache) =
                Self::build_region_membership_cache(bus, rgn_handle, clipped_top, clipped_bottom)
            else {
                return Some(super::pict::DstClip::new((0, 0, 0, 0), Vec::new()));
            };
            let rows = cache
                .rows
                .into_iter()
                .map(|row| {
                    row.into_iter()
                        .map(|edge| i32::from(edge) - i32::from(bounds_left))
                        .collect()
                })
                .collect();
            regions.push(super::pict::DstClipRegion::complex(
                i32::from(clipped_top) - i32::from(bounds_top),
                i32::from(clipped_left) - i32::from(bounds_left),
                i32::from(clipped_bottom) - i32::from(bounds_top),
                i32::from(clipped_right) - i32::from(bounds_left),
                rows,
            ));
        }

        Some(super::pict::DstClip::new(
            (clip_top, clip_left, clip_bottom, clip_right),
            regions,
        ))
    }

    fn ensure_region_capacity(bus: &mut MacMemoryBus, rgn_handle: u32, size: u32) -> Option<u32> {
        if rgn_handle == 0 {
            return None;
        }

        let current_ptr = bus.read_long(rgn_handle);
        if current_ptr != 0 {
            match bus.get_alloc_size(current_ptr) {
                Some(current_size) if current_size >= size => return Some(current_ptr),
                None => return Some(current_ptr),
                _ => {}
            }
        }

        let new_ptr = bus.alloc(size);
        if new_ptr == 0 {
            return None;
        }
        bus.write_long(rgn_handle, new_ptr);
        Some(new_ptr)
    }

    fn write_region(
        bus: &mut MacMemoryBus,
        rgn_handle: u32,
        bbox: Option<(i16, i16, i16, i16)>,
        data_words: &[i16],
    ) -> bool {
        if rgn_handle == 0 {
            return false;
        }

        let is_rectangular = data_words.is_empty();
        let size = if is_rectangular {
            REGION_HEADER_SIZE
        } else {
            REGION_HEADER_SIZE + (data_words.len() as u32 * 2)
        };
        let Some(rgn_ptr) = Self::ensure_region_capacity(bus, rgn_handle, size) else {
            return false;
        };

        bus.write_word(rgn_ptr, size as u16);
        if let Some((top, left, bottom, right)) = bbox {
            bus.write_word(rgn_ptr + 2, top as u16);
            bus.write_word(rgn_ptr + 4, left as u16);
            bus.write_word(rgn_ptr + 6, bottom as u16);
            bus.write_word(rgn_ptr + 8, right as u16);
        } else {
            bus.write_long(rgn_ptr + 2, 0);
            bus.write_long(rgn_ptr + 6, 0);
        }

        for (index, word) in data_words.iter().enumerate() {
            bus.write_word(
                rgn_ptr + REGION_HEADER_SIZE + (index as u32 * 2),
                *word as u16,
            );
        }
        true
    }

    fn region_band_height(top: i16, bottom: i16) -> usize {
        (i32::from(bottom) - i32::from(top)).max(0) as usize
    }

    fn region_row_index(y: i16, top: i16) -> usize {
        (i32::from(y) - i32::from(top)) as usize
    }

    fn region_rows_for_band(
        bus: &MacMemoryBus,
        rgn_handle: u32,
        top: i16,
        bottom: i16,
    ) -> Vec<Vec<i16>> {
        let height = Self::region_band_height(top, bottom);
        let mut rows = vec![Vec::new(); height];
        if height == 0 {
            return rows;
        }

        let Some((rgn_ptr, rgn_size)) = Self::region_ptr_and_size(bus, rgn_handle) else {
            return rows;
        };
        let rgn_top = bus.read_word(rgn_ptr + 2) as i16;
        let rgn_left = bus.read_word(rgn_ptr + 4) as i16;
        let rgn_bottom = bus.read_word(rgn_ptr + 6) as i16;
        let rgn_right = bus.read_word(rgn_ptr + 8) as i16;
        if rgn_bottom <= rgn_top || rgn_right <= rgn_left {
            return rows;
        }

        let overlap_top = top.max(rgn_top);
        let overlap_bottom = bottom.min(rgn_bottom);
        if overlap_bottom <= overlap_top {
            return rows;
        }

        if rgn_size <= REGION_HEADER_SIZE {
            for y in overlap_top..overlap_bottom {
                rows[Self::region_row_index(y, top)] = vec![rgn_left, rgn_right];
            }
            return rows;
        }

        if let Some(cache) =
            Self::build_region_membership_cache(bus, rgn_handle, overlap_top, overlap_bottom)
        {
            let start = Self::region_row_index(overlap_top, top);
            for (offset, row) in cache.rows.into_iter().enumerate() {
                rows[start + offset] = row;
            }
        }
        rows
    }

    fn endpoints_to_intervals(endpoints: &[i16]) -> Vec<(i16, i16)> {
        endpoints
            .chunks_exact(2)
            .filter_map(|pair| (pair[0] < pair[1]).then_some((pair[0], pair[1])))
            .collect()
    }

    fn intervals_to_endpoints(mut intervals: Vec<(i16, i16)>) -> Vec<i16> {
        if intervals.is_empty() {
            return Vec::new();
        }

        intervals.sort_unstable();
        let mut merged: Vec<(i16, i16)> = Vec::with_capacity(intervals.len());
        for (start, end) in intervals {
            if start >= end {
                continue;
            }
            if let Some((_, last_end)) = merged.last_mut() {
                if start <= *last_end {
                    *last_end = (*last_end).max(end);
                    continue;
                }
            }
            merged.push((start, end));
        }

        let mut endpoints = Vec::with_capacity(merged.len() * 2);
        for (start, end) in merged {
            endpoints.push(start);
            endpoints.push(end);
        }
        endpoints
    }

    fn intersect_region_rows(lhs: &[i16], rhs: &[i16]) -> Vec<i16> {
        let lhs = Self::endpoints_to_intervals(lhs);
        let rhs = Self::endpoints_to_intervals(rhs);
        let mut out = Vec::new();
        let mut lhs_index = 0usize;
        let mut rhs_index = 0usize;

        while let (Some(&(lhs_start, lhs_end)), Some(&(rhs_start, rhs_end))) =
            (lhs.get(lhs_index), rhs.get(rhs_index))
        {
            let start = lhs_start.max(rhs_start);
            let end = lhs_end.min(rhs_end);
            if start < end {
                out.push((start, end));
            }
            if lhs_end < rhs_end {
                lhs_index += 1;
            } else {
                rhs_index += 1;
            }
        }

        Self::intervals_to_endpoints(out)
    }

    fn union_region_rows(lhs: &[i16], rhs: &[i16]) -> Vec<i16> {
        let mut intervals = Self::endpoints_to_intervals(lhs);
        intervals.extend(Self::endpoints_to_intervals(rhs));
        Self::intervals_to_endpoints(intervals)
    }

    fn difference_region_rows(lhs: &[i16], rhs: &[i16]) -> Vec<i16> {
        let lhs = Self::endpoints_to_intervals(lhs);
        let rhs = Self::endpoints_to_intervals(rhs);
        let mut out = Vec::new();

        for (lhs_start, lhs_end) in lhs {
            let mut start = lhs_start;
            for &(rhs_start, rhs_end) in rhs.iter() {
                if rhs_end <= start {
                    continue;
                }
                if rhs_start >= lhs_end {
                    break;
                }
                if rhs_start > start {
                    out.push((start, rhs_start.min(lhs_end)));
                }
                start = start.max(rhs_end);
                if start >= lhs_end {
                    break;
                }
            }
            if start < lhs_end {
                out.push((start, lhs_end));
            }
        }

        Self::intervals_to_endpoints(out)
    }

    fn xor_region_rows(lhs: &[i16], rhs: &[i16]) -> Vec<i16> {
        let mut intervals = Self::endpoints_to_intervals(&Self::difference_region_rows(lhs, rhs));
        intervals.extend(Self::endpoints_to_intervals(&Self::difference_region_rows(
            rhs, lhs,
        )));
        Self::intervals_to_endpoints(intervals)
    }

    fn combine_region_rows(lhs: &[i16], rhs: &[i16], op: RegionBooleanOp) -> Vec<i16> {
        match op {
            RegionBooleanOp::Intersection => Self::intersect_region_rows(lhs, rhs),
            RegionBooleanOp::Union => Self::union_region_rows(lhs, rhs),
            RegionBooleanOp::Difference => Self::difference_region_rows(lhs, rhs),
            RegionBooleanOp::Xor => Self::xor_region_rows(lhs, rhs),
        }
    }

    fn quantize_region_edge(edge: f64) -> i16 {
        Self::clamp_region_coord((edge - 0.5).ceil() as i32)
    }

    fn recorded_outline_rows(
        segments: &[((i16, i16), (i16, i16))],
        top: i16,
        bottom: i16,
    ) -> Vec<Vec<i16>> {
        let height = Self::region_band_height(top, bottom);
        let mut rows = vec![Vec::new(); height];
        if height == 0 || segments.is_empty() {
            return rows;
        }

        for y32 in i32::from(top)..i32::from(bottom) {
            let y = y32 as i16;
            let scan_y = f64::from(y) + 0.5;
            let mut crossings = Vec::new();
            for &((v0, h0), (v1, h1)) in segments {
                if v0 == v1 {
                    continue;
                }

                let y0 = f64::from(v0);
                let y1 = f64::from(v1);
                let min_y = y0.min(y1);
                let max_y = y0.max(y1);
                if scan_y < min_y || scan_y >= max_y {
                    continue;
                }

                let x0 = f64::from(h0);
                let x1 = f64::from(h1);
                let t = (scan_y - y0) / (y1 - y0);
                crossings.push(x0 + t * (x1 - x0));
            }

            crossings.sort_by(|lhs, rhs| lhs.partial_cmp(rhs).unwrap());
            let intervals = crossings
                .chunks_exact(2)
                .filter_map(|pair| {
                    let left = Self::quantize_region_edge(pair[0].min(pair[1]));
                    let right = Self::quantize_region_edge(pair[0].max(pair[1]));
                    (left < right).then_some((left, right))
                })
                .collect::<Vec<_>>();
            rows[Self::region_row_index(y, top)] = Self::intervals_to_endpoints(intervals);
        }

        rows
    }

    fn recorded_region_rows(
        recording: &super::dispatch::RegionRecording,
    ) -> Option<(i16, Vec<Vec<i16>>)> {
        let (top, _, bottom, _) = recording.bbox?;
        if bottom <= top {
            return None;
        }

        let mut rows = vec![Vec::new(); Self::region_band_height(top, bottom)];
        for (&y, row) in recording.filled_rows.iter() {
            if y < top || y >= bottom {
                continue;
            }
            let index = Self::region_row_index(y, top);
            rows[index] = Self::union_region_rows(&rows[index], row);
        }

        let outline_rows = Self::recorded_outline_rows(&recording.outline_segments, top, bottom);
        for (row, outline_row) in rows.iter_mut().zip(outline_rows.iter()) {
            *row = Self::union_region_rows(row, outline_row);
        }

        Some((top, rows))
    }

    fn write_region_from_rows(
        bus: &mut MacMemoryBus,
        rgn_handle: u32,
        rows_top: i16,
        rows: &[Vec<i16>],
    ) -> bool {
        let mut bbox: Option<(i16, i16, i16, i16)> = None;
        for (row_index, row) in rows.iter().enumerate() {
            if row.is_empty() {
                continue;
            }
            let y = Self::clamp_region_coord(i32::from(rows_top) + row_index as i32);
            let next_y = Self::clamp_region_coord(i32::from(y) + 1);
            let left = row[0];
            let right = *row.last().unwrap();
            bbox = Some(match bbox {
                Some((top, current_left, bottom, current_right)) => (
                    top,
                    current_left.min(left),
                    bottom.max(next_y),
                    current_right.max(right),
                ),
                None => (y, left, next_y, right),
            });
        }

        let Some((top, left, bottom, right)) = bbox else {
            return Self::write_region(bus, rgn_handle, None, &[]);
        };

        let first_row = &rows[Self::region_row_index(top, rows_top)];
        let rectangular = first_row.len() == 2
            && first_row[0] == left
            && first_row[1] == right
            && (i32::from(top)..i32::from(bottom)).all(|y32| {
                let y = y32 as i16;
                rows[Self::region_row_index(y, rows_top)] == *first_row
            });
        if rectangular {
            return Self::write_region(bus, rgn_handle, Some((top, left, bottom, right)), &[]);
        }

        let mut data_words = Vec::new();
        let mut previous_row = Vec::new();
        for y32 in i32::from(top)..=i32::from(bottom) {
            let y = y32 as i16;
            let current_row = if y < bottom {
                rows[Self::region_row_index(y, rows_top)].clone()
            } else {
                Vec::new()
            };
            let delta = Self::merge_region_endpoints(&previous_row, &current_row);
            if !delta.is_empty() {
                data_words.push(y);
                data_words.extend(delta);
                data_words.push(REGION_STOP);
            }
            previous_row = current_row;
        }
        data_words.push(REGION_STOP);
        Self::write_region(
            bus,
            rgn_handle,
            Some((top, left, bottom, right)),
            &data_words,
        )
    }

    fn write_region_boolean_op(
        bus: &mut MacMemoryBus,
        dst_handle: u32,
        src_a: u32,
        src_b: u32,
        op: RegionBooleanOp,
    ) -> bool {
        let bbox_a = Self::region_bbox(bus, src_a);
        let bbox_b = Self::region_bbox(bus, src_b);

        let band = match op {
            RegionBooleanOp::Intersection => {
                let (Some(a), Some(b)) = (bbox_a, bbox_b) else {
                    return Self::write_region(bus, dst_handle, None, &[]);
                };
                let top = a.0.max(b.0);
                let bottom = a.2.min(b.2);
                if bottom <= top {
                    return Self::write_region(bus, dst_handle, None, &[]);
                }
                (top, bottom)
            }
            RegionBooleanOp::Difference => {
                let Some(a) = bbox_a else {
                    return Self::write_region(bus, dst_handle, None, &[]);
                };
                (a.0, a.2)
            }
            RegionBooleanOp::Union | RegionBooleanOp::Xor => match (bbox_a, bbox_b) {
                (Some(a), Some(b)) => (a.0.min(b.0), a.2.max(b.2)),
                (Some(a), None) => (a.0, a.2),
                (None, Some(b)) => (b.0, b.2),
                (None, None) => return Self::write_region(bus, dst_handle, None, &[]),
            },
        };

        let (top, bottom) = band;
        if bottom <= top {
            return Self::write_region(bus, dst_handle, None, &[]);
        }
        let rows_a = Self::region_rows_for_band(bus, src_a, top, bottom);
        let rows_b = Self::region_rows_for_band(bus, src_b, top, bottom);
        let rows = rows_a
            .iter()
            .zip(rows_b.iter())
            .map(|(lhs, rhs)| Self::combine_region_rows(lhs, rhs, op))
            .collect::<Vec<_>>();
        Self::write_region_from_rows(bus, dst_handle, top, &rows)
    }

    fn offset_region(bus: &mut MacMemoryBus, rgn_handle: u32, dh: i16, dv: i16) {
        let Some((rgn_ptr, rgn_size)) = Self::region_ptr_and_size(bus, rgn_handle) else {
            return;
        };
        if rgn_size < REGION_HEADER_SIZE {
            return;
        }

        for offset in [2u32, 6u32] {
            let value = bus.read_word(rgn_ptr + offset) as i16;
            bus.write_word(rgn_ptr + offset, value.wrapping_add(dv) as u16);
        }
        for offset in [4u32, 8u32] {
            let value = bus.read_word(rgn_ptr + offset) as i16;
            bus.write_word(rgn_ptr + offset, value.wrapping_add(dh) as u16);
        }

        if rgn_size <= REGION_HEADER_SIZE {
            return;
        }

        let region_end = rgn_ptr + rgn_size;
        let mut cursor = rgn_ptr + REGION_HEADER_SIZE;
        while cursor + 2 <= region_end {
            let y = bus.read_word(cursor) as i16;
            if y == REGION_STOP {
                break;
            }
            bus.write_word(cursor, y.wrapping_add(dv) as u16);
            cursor += 2;

            while cursor + 2 <= region_end {
                let edge = bus.read_word(cursor) as i16;
                if edge == REGION_STOP {
                    cursor += 2;
                    break;
                }
                bus.write_word(cursor, edge.wrapping_add(dh) as u16);
                cursor += 2;
            }
        }
    }

    fn clamp_region_coord(value: i32) -> i16 {
        value.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16
    }

    fn inset_region_row(row: &[i16], dh: i16) -> Vec<i16> {
        if row.is_empty() {
            return Vec::new();
        }

        let dh = i32::from(dh);
        let intervals = Self::endpoints_to_intervals(row)
            .into_iter()
            .filter_map(|(left, right)| {
                let new_left = i32::from(left) + dh;
                let new_right = i32::from(right) - dh;
                (new_left < new_right).then_some((
                    Self::clamp_region_coord(new_left),
                    Self::clamp_region_coord(new_right),
                ))
            })
            .collect::<Vec<_>>();
        Self::intervals_to_endpoints(intervals)
    }

    fn inset_region(bus: &mut MacMemoryBus, rgn_handle: u32, dh: i16, dv: i16) {
        let Some((rgn_ptr, rgn_size)) = Self::region_ptr_and_size(bus, rgn_handle) else {
            return;
        };
        if rgn_size < REGION_HEADER_SIZE {
            return;
        }

        let top = bus.read_word(rgn_ptr + 2) as i16;
        let left = bus.read_word(rgn_ptr + 4) as i16;
        let bottom = bus.read_word(rgn_ptr + 6) as i16;
        let right = bus.read_word(rgn_ptr + 8) as i16;
        if bottom <= top || right <= left {
            let _ = Self::write_region(bus, rgn_handle, None, &[]);
            return;
        }

        let new_top = Self::clamp_region_coord(i32::from(top) + i32::from(dv));
        let new_left = Self::clamp_region_coord(i32::from(left) + i32::from(dh));
        let new_bottom = Self::clamp_region_coord(i32::from(bottom) - i32::from(dv));
        let new_right = Self::clamp_region_coord(i32::from(right) - i32::from(dh));
        if new_bottom <= new_top || new_right <= new_left {
            let _ = Self::write_region(bus, rgn_handle, None, &[]);
            return;
        }

        if rgn_size <= REGION_HEADER_SIZE {
            let _ = Self::write_region(
                bus,
                rgn_handle,
                Some((new_top, new_left, new_bottom, new_right)),
                &[],
            );
            return;
        }

        let src_rows = Self::region_rows_for_band(bus, rgn_handle, top, bottom);
        let horizontal_rows = src_rows
            .iter()
            .map(|row| Self::inset_region_row(row, dh))
            .collect::<Vec<_>>();
        let source_top = i32::from(top);
        let source_bottom = i32::from(bottom);
        let radius = i32::from(dv).abs();
        let mut rows = Vec::with_capacity(Self::region_band_height(new_top, new_bottom));

        for y in i32::from(new_top)..i32::from(new_bottom) {
            if dv >= 0 {
                let first_source_y = y - radius;
                let last_source_y = y + radius;
                if first_source_y < source_top || last_source_y >= source_bottom {
                    rows.push(Vec::new());
                    continue;
                }

                let mut combined: Option<Vec<i16>> = None;
                for source_y in first_source_y..=last_source_y {
                    let source_row = &horizontal_rows[(source_y - source_top) as usize];
                    combined = Some(match combined {
                        Some(current) => Self::intersect_region_rows(&current, source_row),
                        None => source_row.clone(),
                    });
                    if combined.as_ref().is_some_and(Vec::is_empty) {
                        break;
                    }
                }
                rows.push(combined.unwrap_or_default());
            } else {
                let first_source_y = (y - radius).max(source_top);
                let last_source_y = (y + radius).min(source_bottom - 1);
                if first_source_y > last_source_y {
                    rows.push(Vec::new());
                    continue;
                }

                let mut combined = Vec::new();
                for source_y in first_source_y..=last_source_y {
                    let source_row = &horizontal_rows[(source_y - source_top) as usize];
                    combined = Self::union_region_rows(&combined, source_row);
                }
                rows.push(combined);
            }
        }

        let _ = Self::write_region_from_rows(bus, rgn_handle, new_top, &rows);
    }

    fn merge_region_endpoints(lhs: &[i16], rhs: &[i16]) -> Vec<i16> {
        let mut merged = Vec::with_capacity(lhs.len() + rhs.len());
        let mut lhs_index = 0usize;
        let mut rhs_index = 0usize;

        while lhs_index < lhs.len() || rhs_index < rhs.len() {
            match (lhs.get(lhs_index), rhs.get(rhs_index)) {
                (Some(&lhs_value), Some(&rhs_value)) if lhs_value < rhs_value => {
                    merged.push(lhs_value);
                    lhs_index += 1;
                }
                (Some(&lhs_value), Some(&rhs_value)) if rhs_value < lhs_value => {
                    merged.push(rhs_value);
                    rhs_index += 1;
                }
                (Some(_), Some(_)) => {
                    lhs_index += 1;
                    rhs_index += 1;
                }
                (Some(&lhs_value), None) => {
                    merged.push(lhs_value);
                    lhs_index += 1;
                }
                (None, Some(&rhs_value)) => {
                    merged.push(rhs_value);
                    rhs_index += 1;
                }
                (None, None) => break,
            }
        }

        merged
    }

    pub(super) fn endpoints_contain_point(endpoints: &[i16], x: i16) -> bool {
        let mut in_region = false;
        for &edge in endpoints {
            if edge > x {
                break;
            }
            in_region = !in_region;
        }
        in_region
    }

    fn scan_bitmap_row_endpoints(bus: &MacMemoryBus, info: &CopyBitmapInfo, y: i16) -> Vec<i16> {
        let mut endpoints = Vec::new();
        let row = (y - info.bounds_top) as u32;
        let row_base = info.base + row * info.row_bytes;
        let mut in_run = false;

        for x in info.bounds_left..info.bounds_right {
            let col = (x - info.bounds_left) as u32;
            let byte = bus.read_byte(row_base + (col / 8));
            let bit = 7 - (col % 8);
            let pixel_is_set = (byte & (1 << bit)) != 0;

            if pixel_is_set && !in_run {
                endpoints.push(x);
                in_run = true;
            } else if !pixel_is_set && in_run {
                endpoints.push(x);
                in_run = false;
            }
        }

        if in_run {
            endpoints.push(info.bounds_right);
        }

        endpoints
    }

    pub(super) fn build_region_membership_cache(
        bus: &MacMemoryBus,
        rgn_handle: u32,
        top: i16,
        bottom: i16,
    ) -> Option<RegionMembershipCache> {
        let (rgn_ptr, rgn_size) = Self::region_ptr_and_size(bus, rgn_handle)?;
        if rgn_size <= REGION_HEADER_SIZE || bottom <= top {
            return None;
        }

        let region_end = rgn_ptr + rgn_size;
        let mut cursor = rgn_ptr + REGION_HEADER_SIZE;
        if cursor + 2 > region_end {
            return None;
        }

        let mut next_change_y = bus.read_word(cursor) as i16;
        cursor += 2;
        let mut active = Vec::new();
        let mut rows = Vec::with_capacity(Self::region_band_height(top, bottom));

        for y32 in i32::from(top)..i32::from(bottom) {
            let y = y32 as i16;
            while next_change_y != REGION_STOP && next_change_y <= y {
                let mut delta = Vec::new();
                loop {
                    if cursor + 2 > region_end {
                        return None;
                    }
                    let value = bus.read_word(cursor) as i16;
                    cursor += 2;
                    if value == REGION_STOP {
                        break;
                    }
                    delta.push(value);
                }
                active = Self::merge_region_endpoints(&active, &delta);
                if cursor + 2 > region_end {
                    return None;
                }
                next_change_y = bus.read_word(cursor) as i16;
                cursor += 2;
            }
            rows.push(active.clone());
        }

        Some(RegionMembershipCache { top, rows })
    }

    pub(crate) fn region_contains_point(
        bus: &MacMemoryBus,
        rgn_handle: u32,
        y: i16,
        x: i16,
    ) -> bool {
        let Some((top, left, bottom, right)) = Self::region_bbox(bus, rgn_handle) else {
            return false;
        };
        if y < top || y >= bottom || x < left || x >= right {
            return false;
        }

        let Some((_, rgn_size)) = Self::region_ptr_and_size(bus, rgn_handle) else {
            return false;
        };
        if rgn_size <= REGION_HEADER_SIZE {
            return true;
        }

        let Some(cache) = Self::build_region_membership_cache(bus, rgn_handle, y, y + 1) else {
            return false;
        };
        cache
            .rows
            .first()
            .map(|row| Self::endpoints_contain_point(row, x))
            .unwrap_or(false)
    }

    /// RectInRgn helper — tests whether any pixel in `rect_ptr` lies inside
    /// `rgn_handle`.  For rectangular regions a bounding-box intersection is
    /// sufficient.  For complex regions we build a per-row membership cache and
    /// scan each row in the overlap band for a span that overlaps the rect's
    /// horizontal extent.
    ///
    /// IM:I I-185 — "Returns TRUE if any pixel enclosed by r lies within rgn."
    fn rect_in_rgn(bus: &MacMemoryBus, rgn_handle: u32, rect_ptr: u32) -> bool {
        if rgn_handle == 0 || rect_ptr == 0 {
            return false;
        }
        let Some((rgn_ptr, rgn_size)) = Self::region_ptr_and_size(bus, rgn_handle) else {
            return false;
        };

        let rt = bus.read_word(rect_ptr) as i16;
        let rl = bus.read_word(rect_ptr + 2) as i16;
        let rb = bus.read_word(rect_ptr + 4) as i16;
        let rr = bus.read_word(rect_ptr + 6) as i16;

        // Empty rect → no pixels to test
        if rb <= rt || rr <= rl {
            return false;
        }

        let gt = bus.read_word(rgn_ptr + 2) as i16;
        let gl = bus.read_word(rgn_ptr + 4) as i16;
        let gb = bus.read_word(rgn_ptr + 6) as i16;
        let gr = bus.read_word(rgn_ptr + 8) as i16;

        // Bounding-box intersection fast-out
        if rt >= gb || rb <= gt || rl >= gr || rr <= gl {
            return false;
        }

        // Rectangular region: bbox test is exact
        if rgn_size <= REGION_HEADER_SIZE {
            return true;
        }

        // Complex region: check each row in the overlap band for a span that
        // covers at least one x in [rl, rr).
        let top = rt.max(gt);
        let bottom = rb.min(gb);
        let Some(cache) = Self::build_region_membership_cache(bus, rgn_handle, top, bottom) else {
            // Cache build failed — fall back to bbox result
            return true;
        };
        let left = rl.max(gl);
        let right = rr.min(gr);
        for row in &cache.rows {
            // A row has an active span overlapping [left, right) iff the
            // toggle count at `left` is odd (point is inside) OR there is a
            // toggle before `right` (a span starts before right and inside
            // the rect starts at or before right).
            //
            // The endpoints list is sorted.  We scan it with the even/odd
            // rule.  If we are inside at `left`, the row qualifies.  If we
            // are outside at `left` but there is a toggle in (left, right),
            // that toggle starts a new span inside the rect → qualifies.
            let mut inside = false;
            for &edge in row.iter() {
                if edge >= right {
                    break;
                }
                if edge >= left {
                    // A toggle edge that lies inside the rect: if we are
                    // currently outside we are entering a span → qualifies.
                    if !inside {
                        return true;
                    }
                    inside = !inside;
                } else {
                    inside = !inside;
                }
            }
            if inside {
                // Still inside at `left` (or re-entered before `right`)
                return true;
            }
        }
        false
    }

    /// `#[inline]` — called per pixel × 3 in the CopyBits inner loop
    /// (vis_rgn, clip_rgn, mask_rgn checks). The hot-path early-out
    /// (`rgn_handle == 0 → true`) is what we want LLVM to fold across the
    /// whole inner loop.
    #[inline]
    pub(super) fn region_contains_point_cached(
        bus: &MacMemoryBus,
        rgn_handle: u32,
        cache: Option<&RegionMembershipCache>,
        y: i16,
        x: i16,
    ) -> bool {
        if rgn_handle == 0 {
            return true;
        }

        if let Some(cache) = cache {
            let row = y - cache.top;
            if row >= 0 && (row as usize) < cache.rows.len() {
                return Self::endpoints_contain_point(&cache.rows[row as usize], x);
            }
        }

        Self::region_contains_point(bus, rgn_handle, y, x)
    }

    fn trace_port_snapshot(bus: &MacMemoryBus, label: &str, port: u32) {
        if port == 0 {
            eprintln!("[DIALOG-PORT-DUMP] {} port=$00000000", label);
            return;
        }

        let port_version = bus.read_word(port + 6);
        let port_rect = (
            bus.read_word(port + 16) as i16,
            bus.read_word(port + 18) as i16,
            bus.read_word(port + 20) as i16,
            bus.read_word(port + 22) as i16,
        );
        let vis_rgn = Self::region_bbox(bus, bus.read_long(port + 24));
        let clip_rgn = Self::region_bbox(bus, bus.read_long(port + 28));
        let pm_handle = bus.read_long(port + 2);
        let pm_ptr = if pm_handle != 0 {
            bus.read_long(pm_handle)
        } else {
            0
        };

        eprintln!(
            "[DIALOG-PORT-DUMP] {} port=${:08X} version=${:04X} portRect=({},{},{},{}) vis={:?} clip={:?} pnLoc=({}, {}) txFont={} txFace=${:04X} txMode={} txSize={} fgRGB=({:04X},{:04X},{:04X}) bgRGB=({:04X},{:04X},{:04X}) fgLegacy=${:08X} bgLegacy=${:08X}",
            label,
            port,
            port_version,
            port_rect.0,
            port_rect.1,
            port_rect.2,
            port_rect.3,
            vis_rgn,
            clip_rgn,
            bus.read_word(port + 48) as i16,
            bus.read_word(port + 50) as i16,
            bus.read_word(port + 68),
            bus.read_word(port + 70),
            bus.read_word(port + 72),
            bus.read_word(port + 74),
            bus.read_word(port + 36),
            bus.read_word(port + 38),
            bus.read_word(port + 40),
            bus.read_word(port + 42),
            bus.read_word(port + 44),
            bus.read_word(port + 46),
            bus.read_long(port + 80),
            bus.read_long(port + 84),
        );

        if pm_ptr != 0 {
            eprintln!(
                "[DIALOG-PORT-DUMP] {} pixMapHandle=${:08X} pixMap=${:08X} base=${:08X} rowBytes=${:04X} bounds=({},{},{},{}) pixelSize={} ctab=${:08X}",
                label,
                pm_handle,
                pm_ptr,
                bus.read_long(pm_ptr),
                bus.read_word(pm_ptr + 4),
                bus.read_word(pm_ptr + 6) as i16,
                bus.read_word(pm_ptr + 8) as i16,
                bus.read_word(pm_ptr + 10) as i16,
                bus.read_word(pm_ptr + 12) as i16,
                bus.read_word(pm_ptr + 32),
                bus.read_long(pm_ptr + 42),
            );
        } else {
            eprintln!(
                "[DIALOG-PORT-DUMP] {} pixMapHandle=${:08X} pixMap=$00000000",
                label, pm_handle,
            );
        }
    }

    fn dump_bitmap_as_png(
        bus: &MacMemoryBus,
        info: &CopyBitmapInfo,
        clut: &[[u16; 3]; 256],
        path: &str,
    ) {
        let width = (info.bounds_right - info.bounds_left) as u32;
        let height = (info.bounds_bottom - info.bounds_top) as u32;
        if width == 0 || height == 0 {
            return;
        }

        let img = image::RgbImage::from_fn(width, height, |x, y| {
            let addr = info.base + y * info.row_bytes + x;
            let pixel = bus.read_byte(addr);
            let rgb = clut[pixel as usize];
            image::Rgb([
                (rgb[0] >> 8) as u8,
                (rgb[1] >> 8) as u8,
                (rgb[2] >> 8) as u8,
            ])
        });
        let _ = img.save(path);
    }

    fn scale_coord(
        src_start: i16,
        src_end: i16,
        dst_start: i16,
        dst_end: i16,
        dst_coord: i16,
    ) -> Option<i32> {
        let src_span = i32::from(src_end) - i32::from(src_start);
        let dst_span = i32::from(dst_end) - i32::from(dst_start);
        if src_span <= 0 || dst_span <= 0 {
            return None;
        }
        let rel = i32::from(dst_coord) - i32::from(dst_start);
        if rel < 0 || rel >= dst_span {
            return None;
        }
        Some(i32::from(src_start) + (rel * src_span) / dst_span)
    }

    fn nearest_palette_index(clut: &[[u16; 3]; 256], rgb: [u16; 3]) -> u8 {
        // QuickDraw's inverse tables reserve exact white/black for the first
        // and last CLUT entries when the endpoints are white/black, rather
        // than returning an arbitrary duplicate color-cube entry. During
        // fade-to-black steps entry 0 can temporarily equal black too; keep
        // canonical black drawing pinned to entry 255 in that collapsed case.
        // Imaging With QuickDraw 1994, p. 4-82
        // references/executor/src/quickdraw/qColorMgr.cpp (MakeITable)
        if rgb == [0, 0, 0] && clut[255] == [0, 0, 0] {
            return 255;
        }
        if rgb == [0xFFFF, 0xFFFF, 0xFFFF] && clut[0] == [0xFFFF, 0xFFFF, 0xFFFF] {
            return 0;
        }
        if rgb == clut[255] {
            return 255;
        }
        if rgb == clut[0] {
            return 0;
        }

        if let Some((index, _)) = clut.iter().enumerate().find(|(_, entry)| **entry == rgb) {
            return index as u8;
        }

        let mut best_index = 0u8;
        let mut best_distance = u64::MAX;
        for (index, entry) in clut.iter().enumerate() {
            let dr = i64::from(entry[0]) - i64::from(rgb[0]);
            let dg = i64::from(entry[1]) - i64::from(rgb[1]);
            let db = i64::from(entry[2]) - i64::from(rgb[2]);
            let distance = (dr * dr + dg * dg + db * db) as u64;
            if distance < best_distance {
                best_distance = distance;
                best_index = index as u8;
            }
        }
        best_index
    }

    pub(super) fn build_palette_translation(
        &self,
        bus: &MacMemoryBus,
        src_clut: &[[u16; 3]; 256],
        dst_clut: &[[u16; 3]; 256],
        dst_ctab_handle: u32,
    ) -> [u8; 256] {
        let mut translation = [0u8; 256];
        for (index, rgb) in src_clut.iter().enumerate() {
            translation[index] = if dst_clut[index] == *rgb {
                index as u8
            } else {
                self.palette_index_for_rgb(bus, dst_ctab_handle, dst_clut, *rgb)
            };
        }
        translation
    }

    fn colorize_src_copy_rgb(src_rgb: [u16; 3], fg_rgb: [u16; 3], bg_rgb: [u16; 3]) -> [u16; 3] {
        let mut out = [0u16; 3];
        for component in 0..3 {
            let src = u32::from(src_rgb[component]);
            let fg = u32::from(fg_rgb[component]);
            let bg = u32::from(bg_rgb[component]);
            out[component] = ((((0xFFFF - src) * fg) + (src * bg) + 0x7FFF) / 0xFFFF) as u16;
        }
        out
    }

    fn colorize_src_or_rgb(src_rgb: [u16; 3], fg_rgb: [u16; 3]) -> [u16; 3] {
        let mut out = [0u16; 3];
        for component in 0..3 {
            let src = u32::from(src_rgb[component]);
            let fg = u32::from(fg_rgb[component]);
            out[component] = (((0xFFFF - src) * fg + 0x7FFF) / 0xFFFF) as u16;
        }
        out
    }

    fn colorize_not_src_or_rgb(src_rgb: [u16; 3], fg_rgb: [u16; 3]) -> [u16; 3] {
        let mut out = [0u16; 3];
        for component in 0..3 {
            let src = u32::from(src_rgb[component]);
            let fg = u32::from(fg_rgb[component]);
            out[component] = ((src * fg + 0x7FFF) / 0xFFFF) as u16;
        }
        out
    }

    fn copy_bits_color_source_mode_pixel(
        &self,
        bus: &MacMemoryBus,
        dst_ctab_handle: u32,
        src_clut: Option<&[[u16; 3]; 256]>,
        dst_clut: Option<&[[u16; 3]; 256]>,
        palette_translation: Option<&[u8; 256]>,
        mode_base: u16,
        src_pixel: u8,
        dst_pixel: u8,
        fg_rgb: [u16; 3],
        bg_rgb: [u16; 3],
    ) -> Option<u8> {
        // Color QuickDraw applies current foreground/background colors for
        // Boolean source modes on colored pixels, and leaves destination
        // pixels untouched for the table's white/black preserve cases.
        // Imaging With QuickDraw 1994, p. 4-33.
        let raw_source = || {
            palette_translation
                .map(|translation| translation[src_pixel as usize])
                .unwrap_or(src_pixel)
        };
        let (Some(src_clut), Some(dst_clut)) = (src_clut, dst_clut) else {
            return Some(raw_source());
        };
        let src_rgb = src_clut[src_pixel as usize];
        let is_black = src_rgb == [0, 0, 0];
        let is_white = src_rgb == [0xFFFF, 0xFFFF, 0xFFFF];
        let map_rgb = |rgb| self.palette_index_for_rgb(bus, dst_ctab_handle, dst_clut, rgb);

        match mode_base {
            0 => Some(raw_source()),
            1 => (!is_white).then(|| map_rgb(Self::colorize_src_or_rgb(src_rgb, fg_rgb))),
            2 => is_black.then(|| Self::inverted_palette_index(dst_clut, dst_pixel)),
            3 => (!is_white).then(|| map_rgb(Self::colorize_src_or_rgb(src_rgb, bg_rgb))),
            4 => Some(map_rgb(Self::colorize_src_copy_rgb(
                src_rgb, bg_rgb, fg_rgb,
            ))),
            5 => (!is_black).then(|| map_rgb(Self::colorize_not_src_or_rgb(src_rgb, fg_rgb))),
            6 => is_white.then(|| Self::inverted_palette_index(dst_clut, dst_pixel)),
            7 => (!is_black).then(|| map_rgb(Self::colorize_not_src_or_rgb(src_rgb, bg_rgb))),
            _ => Some(raw_source()),
        }
    }

    fn inverted_palette_index(dst_clut: &[[u16; 3]; 256], pixel: u8) -> u8 {
        let rgb = dst_clut[pixel as usize];
        Self::nearest_palette_index(
            dst_clut,
            [0xFFFF - rgb[0], 0xFFFF - rgb[1], 0xFFFF - rgb[2]],
        )
    }

    /// Convert classic ForeColor/BackColor constants to RGB.
    /// Inside Macintosh Volume I (1985), pp. I-173 to I-174.
    fn legacy_qd_color_to_rgb(color: u32) -> (u16, u16, u16) {
        match color {
            30 => (0xFFFF, 0xFFFF, 0xFFFF), // whiteColor
            33 => (0, 0, 0),                // blackColor
            // Color QuickDraw uses canonical RGB values for the classic
            // ForeColor/BackColor constants rather than pure primaries.
            // greenColor (341) uses 0x8000 for green to match System 7.5.3
            // ROM rather than Executor's diverging 0x64AF.
            69 => (0xFC00, 0xF37D, 0x052F),  // yellowColor
            137 => (0xF2D7, 0x0856, 0x84EC), // magentaColor
            205 => (0xDD6B, 0x08C2, 0x06A2), // redColor
            273 => (0x0241, 0xAB54, 0xEAFF), // cyanColor
            341 => (0x0000, 0x8000, 0x11B0), // greenColor
            409 => (0x0000, 0x0000, 0xD400), // blueColor
            _ => {
                let idx = (color as usize) & 0xFF;
                let [r, g, b] = Self::standard_mac_8bpp_clut()[idx];
                (r, g, b)
            }
        }
    }

    fn set_entries_request_in_range(
        bus: &MacMemoryBus,
        table_ptr: u32,
        start: i16,
        count: i16,
    ) -> bool {
        // Systemless currently models an 8-bit indexed screen CLUT. Per
        // Inside Macintosh Volume V, V-143, any out-of-range SetEntries
        // request fails atomically: nothing in the device table changes.
        if count < 0 || count > 255 {
            return false;
        }

        if start >= 0 {
            let end = start as i32 + count as i32;
            return end <= 255;
        }

        if start != -1 {
            return false;
        }

        for i in 0..=(count as u32) {
            let value = bus.read_word(table_ptr + i * 8) as i16;
            if !(0..=255).contains(&value) {
                return false;
            }
        }

        true
    }

    /// Apply CLUT updates from a ColorSpec array (hardware only).
    /// Used by low-level video CLUT APIs (e.g. _Control cscSetEntries),
    /// which bypass the Color Manager and write directly to the video
    /// hardware CLUT without touching the GDevice's in-memory ColorTable.
    /// Each ColorSpec is 8 bytes: value(2) + red(2) + green(2) + blue(2).
    /// Inside Macintosh Volume V, V-143
    /// Designing Cards and Drivers 2nd Ed 1990, p. 216
    fn apply_color_table_updates(
        &mut self,
        bus: &mut MacMemoryBus,
        ctab_handle: u32,
        table_ptr: u32,
        start: i16,
        count: i16,
    ) -> Option<u32> {
        if !Self::set_entries_request_in_range(bus, table_ptr, start, count) {
            return None;
        }
        if ctab_handle == 0 {
            return None;
        }
        let ctab_ptr = bus.read_long(ctab_handle);
        if ctab_ptr == 0 {
            return None;
        }

        let num_entries = (count + 1) as u32;
        for i in 0..num_entries {
            let entry_addr = table_ptr + i * 8;
            let value = bus.read_word(entry_addr) as i16;
            let r = bus.read_word(entry_addr + 2);
            let g = bus.read_word(entry_addr + 4);
            let b = bus.read_word(entry_addr + 6);
            let idx = if start < 0 {
                value as usize
            } else {
                (start as usize) + i as usize
            };
            if idx < 256 {
                let ct_entry = ctab_ptr + 8 + (idx as u32) * 8;
                bus.write_word(ct_entry, idx as u16);
                bus.write_word(ct_entry + 2, r);
                bus.write_word(ct_entry + 4, g);
                bus.write_word(ct_entry + 6, b);
            }
        }

        self.reseed_color_table_handle(bus, ctab_handle)
    }

    pub(crate) fn apply_set_entries(
        &mut self,
        bus: &mut MacMemoryBus,
        table_ptr: u32,
        start: i16,
        count: i16,
    ) {
        if !Self::set_entries_request_in_range(bus, table_ptr, start, count) {
            return;
        }
        let num_entries = (count + 1) as u32;
        // Low-level SetEntries always targets the screen's hardware CLUT
        // and GDevice ColorTable, regardless of the current GrafPort.
        // On real Mac OS, the video driver SetEntries operates on the
        // screen device even when an offscreen GWorld is current.
        // Inside Macintosh Volume V, V-143
        let target_gdh = if self.main_gdevice_handle != 0 {
            self.main_gdevice_handle
        } else {
            self.palette_target_gdevice_handle(bus)
        };
        let target_is_screen = target_gdh == 0 || target_gdh == self.main_gdevice_handle;
        let trace_palette = trace_palette_enabled();
        let mut first_spec = None;
        let mut last_spec = None;
        for i in 0..num_entries {
            let entry_addr = table_ptr + i * 8;
            let value = bus.read_word(entry_addr) as i16;
            let r = bus.read_word(entry_addr + 2);
            let g = bus.read_word(entry_addr + 4);
            let b = bus.read_word(entry_addr + 6);
            if trace_palette {
                let spec = (value, r, g, b);
                if first_spec.is_none() {
                    first_spec = Some(spec);
                }
                last_spec = Some(spec);
            }
            let idx = if start < 0 {
                value as usize
            } else {
                (start as usize) + i as usize
            };
            if idx < 256 && target_is_screen {
                // Per Inside Macintosh Volume V, V-141: "If any of the
                // requested entries are protected ... a protection error
                // is returned, and nothing happens." Reserved entries
                // can only be changed by the owning client (gdID match);
                // for Systemless's single-client model, treat them as
                // protected to preserve palette-animation slots.
                if !self.clut_protected[idx] && !self.clut_reserved[idx] {
                    self.device_clut[idx] = [r, g, b];
                }
            }
        }
        let ctab_handle = Self::gdevice_ctab_handle(bus, target_gdh);
        let _ = self.apply_color_table_updates(bus, ctab_handle, table_ptr, start, count);
        // Note: color_manager_clut is NOT updated here. Low-level video
        // driver SetEntries only changes the hardware CLUT for palette
        // animation. QuickDraw's index mapping (ITable) stays stable.
        if trace_palette && target_is_screen {
            let [r0, g0, b0] = self.device_clut[0];
            let [r1, g1, b1] = self.device_clut[1];
            let [r16, g16, b16] = self.device_clut[16];
            let [r42, g42, b42] = self.device_clut[42];
            let [r128, g128, b128] = self.device_clut[128];
            let [r255, g255, b255] = self.device_clut[255];
            if let (Some((first_value, fr, fg, fb)), Some((last_value, lr, lg, lb))) =
                (first_spec, last_spec)
            {
                eprintln!(
                    "[PALETTE] SetEntries tick={} start={} count={} table=${:08X} first=value:{} rgb=({:04X},{:04X},{:04X}) last=value:{} rgb=({:04X},{:04X},{:04X}) device[0]=({:04X},{:04X},{:04X}) device[1]=({:04X},{:04X},{:04X}) device[16]=({:04X},{:04X},{:04X}) device[42]=({:04X},{:04X},{:04X}) device[128]=({:04X},{:04X},{:04X}) device[255]=({:04X},{:04X},{:04X}) cm[0]=({:04X},{:04X},{:04X}) cm[1]=({:04X},{:04X},{:04X}) cm[16]=({:04X},{:04X},{:04X}) cm[42]=({:04X},{:04X},{:04X}) cm[128]=({:04X},{:04X},{:04X}) cm[255]=({:04X},{:04X},{:04X})",
                    self.tick_count,
                    start,
                    count,
                    table_ptr,
                    first_value,
                    fr,
                    fg,
                    fb,
                    last_value,
                    lr,
                    lg,
                    lb,
                    r0,
                    g0,
                    b0,
                    r1,
                    g1,
                    b1,
                    r16,
                    g16,
                    b16,
                    r42,
                    g42,
                    b42,
                    r128,
                    g128,
                    b128,
                    r255,
                    g255,
                    b255,
                    self.color_manager_clut[0][0],
                    self.color_manager_clut[0][1],
                    self.color_manager_clut[0][2],
                    self.color_manager_clut[1][0],
                    self.color_manager_clut[1][1],
                    self.color_manager_clut[1][2],
                    self.color_manager_clut[16][0],
                    self.color_manager_clut[16][1],
                    self.color_manager_clut[16][2],
                    self.color_manager_clut[42][0],
                    self.color_manager_clut[42][1],
                    self.color_manager_clut[42][2],
                    self.color_manager_clut[128][0],
                    self.color_manager_clut[128][1],
                    self.color_manager_clut[128][2],
                    self.color_manager_clut[255][0],
                    self.color_manager_clut[255][1],
                    self.color_manager_clut[255][2]
                );
            }
        }
    }

    /// Apply CLUT updates via the Color Manager (high-level).
    /// Used by SetEntries ($AA3F), which updates both the hardware
    /// CLUT (device_clut) AND the GDevice's in-memory ColorTable.
    /// This ensures that QuickDraw operations (DrawPicture, CopyBits)
    /// that remap colors against the GDevice palette see the correct
    /// palette, while the screen also displays using that same palette.
    /// Inside Macintosh Volume V, V-143
    pub(crate) fn apply_set_entries_with_gdevice(
        &mut self,
        bus: &mut MacMemoryBus,
        table_ptr: u32,
        start: i16,
        count: i16,
    ) {
        self.apply_set_entries_with_gdevice_mode(
            bus,
            table_ptr,
            start,
            count,
            palette_strict_enabled(),
        );
    }

    fn apply_set_entries_with_gdevice_mode(
        &mut self,
        bus: &mut MacMemoryBus,
        table_ptr: u32,
        start: i16,
        count: i16,
        strict_palette: bool,
    ) {
        if !Self::set_entries_request_in_range(bus, table_ptr, start, count) {
            return;
        }
        let incoming_default_palette = start == 0
            && count == 255
            && Self::table_looks_like_scaled_canonical_system_8bpp(bus, table_ptr);
        // Extend the seeded_picture_palette window whenever a scaled-
        // canonical SetEntries fires against a non-canonical cm[]. An
        // active fade keeps device_clut tracking cm[] regardless of fade
        // duration; once the game stops fading (no scaled-canonical
        // SetEntries for 48+ ticks) the window expires naturally and the
        // next scene's SetEntries / SeedFromPicture can publish a fresh
        // cm[] without being blocked by the prior scene's palette.
        // Inside Macintosh Volume V, V-143.
        let preserve_seeded_picture_palette = self.set_entries_target_is_screen(bus)
            && incoming_default_palette
            && self.tick_count < self.seeded_picture_palette_until_tick
            && !Self::uses_canonical_system_8bpp_clut(&self.seeded_picture_palette);
        if preserve_seeded_picture_palette && !strict_palette && !palette_as_game_wrote_enabled() {
            if let Some(scale) = Self::canonical_table_brightness_scale(bus, table_ptr) {
                self.device_clut = Self::scale_clut(&self.seeded_picture_palette, scale);
            }
            // Extend the window so consecutive fade-up/-down SetEntries
            // (which may run 60+ ticks for a single scene transition)
            // all get the scene-palette preservation. Window resets to
            // tick + 64 on each fire; once fades stop, the window
            // expires within 64 ticks and the gate re-opens for the
            // next scene.
            //
            // 64 chosen empirically: +48 (the previous fixed
            // window) expires during EV's landing fade-up and lets
            // the final SetEntries publish canonical over cm[].
            // +48 left frame 13 at 69.26%; +64 also leaves it at
            // 69.26% but lifts 11/12 by 3 points each and 14/15 by
            // 2 points each (more consistent within-scene
            // preservation). +96 and +128 both regressed 11/12/13
            // by ~4 points (old scene palette leaks into new scene's
            // canonical fade-ups). +64 is the safe maximum that
            // avoids cross-scene leakage.
            self.seeded_picture_palette_until_tick = self.tick_count.saturating_add(64);
            if trace_palette_enabled() {
                eprintln!(
                    "[PALETTE] PreserveSeededPicture tick={} table=${:08X} until_tick={} device[0]=({:04X},{:04X},{:04X}) device[1]=({:04X},{:04X},{:04X}) device[42]=({:04X},{:04X},{:04X}) device[128]=({:04X},{:04X},{:04X}) device[255]=({:04X},{:04X},{:04X})",
                    self.tick_count,
                    table_ptr,
                    self.seeded_picture_palette_until_tick,
                    self.device_clut[0][0],
                    self.device_clut[0][1],
                    self.device_clut[0][2],
                    self.device_clut[1][0],
                    self.device_clut[1][1],
                    self.device_clut[1][2],
                    self.device_clut[42][0],
                    self.device_clut[42][1],
                    self.device_clut[42][2],
                    self.device_clut[128][0],
                    self.device_clut[128][1],
                    self.device_clut[128][2],
                    self.device_clut[255][0],
                    self.device_clut[255][1],
                    self.device_clut[255][2]
                );
            }
            if std::env::var_os("SYSTEMLESS_TRACE_CM_WRITE").is_some() {
                let cm_before = self.color_manager_clut[0];
                let seed = self.seeded_picture_palette[0];
                eprintln!(
                    "[CM-WRITE] PreserveSeeded@13177 tick={} cm[0]=({:04X},{:04X},{:04X}) <- seeded[0]=({:04X},{:04X},{:04X})",
                    self.tick_count, cm_before[0], cm_before[1], cm_before[2], seed[0], seed[1], seed[2]
                );
            }
            self.color_manager_clut = self.seeded_picture_palette;
            return;
        }

        let target_is_screen = self.set_entries_target_is_screen(bus);
        let is_full_replace = start == 0 && count == 255;

        // Normal path: install the supplied RGB values into device_clut
        // unconditionally. Per Inside Macintosh Volume V, V-143 the caller's
        // table values MUST land in the hardware CLUT — Systemless must not
        // second-guess the caller with a "looks like a fade" bypass that
        // substitutes a scaled `color_manager_clut`.
        //
        // Regression coverage:
        //   setentries_replaces_custom_palette_with_canonical_table
        self.apply_set_entries(bus, table_ptr, start, count);

        // Publish `device_clut → color_manager_clut` only on a full-replace
        // SetEntries (start=0, count=255) that represents a fresh palette
        // install. Static dark scene palettes still need to publish so
        // DrawPicture / CopyBits color matching uses the new palette, but
        // true brightness fades of the already-published palette must not.
        //
        // Partial updates (palette animation cycling a few slots) MUST NOT
        // publish — publishing them would lock the inverse-table baseline
        // to transient animation values and corrupt sprite color matching.
        //
        // Mid-fade frames (full-replace that are just a uniform scale of the
        // current logical palette) MUST NOT publish — publishing them would
        // lock cm to a dimmed transient and produce washed-out color
        // matching.
        //
        // Regression coverage:
        //   setentries_partial_update_does_not_corrupt_color_manager_clut
        //   setentries_many_partial_updates_do_not_publish_cm
        //   setentries_full_replace_at_full_brightness_publishes_cm
        //   setentries_dimmed_full_replace_does_not_publish_cm
        //   palette_strict_mode_publishes_dimmed_full_replace
        let dimmed_scale_of_current = !strict_palette
            && is_full_replace
            && Self::table_uniform_scale_of_clut(bus, table_ptr, &self.color_manager_clut)
                .is_some_and(|scale| scale < 0.98);

        if target_is_screen && is_full_replace && !dimmed_scale_of_current {
            if std::env::var_os("SYSTEMLESS_TRACE_CM_WRITE").is_some() {
                let cm_before = self.color_manager_clut[0];
                let dev0 = self.device_clut[0];
                eprintln!(
                    "[CM-WRITE] PublishCm@13220 tick={} cm[0]=({:04X},{:04X},{:04X}) <- device[0]=({:04X},{:04X},{:04X})",
                    self.tick_count, cm_before[0], cm_before[1], cm_before[2], dev0[0], dev0[1], dev0[2]
                );
            }
            self.color_manager_clut = self.device_clut;
            if trace_palette_enabled() {
                eprintln!(
                    "[PALETTE] PublishCm tick={} — fresh full palette install cm[0]=({:04X},{:04X},{:04X}) cm[255]=({:04X},{:04X},{:04X})",
                    self.tick_count,
                    self.color_manager_clut[0][0],
                    self.color_manager_clut[0][1],
                    self.color_manager_clut[0][2],
                    self.color_manager_clut[255][0],
                    self.color_manager_clut[255][1],
                    self.color_manager_clut[255][2]
                );
            }
        }

        // Also update the GDevice's in-memory ColorTable so QuickDraw
        // color remapping (e.g. DrawPicture) uses the same palette.
        let gdh = self.set_entries_target_gdevice_handle(bus);
        if gdh == 0 {
            return;
        }
        let ctab_handle = Self::gdevice_ctab_handle(bus, gdh);
        let _ = self.apply_color_table_updates(bus, ctab_handle, table_ptr, start, count);
    }

    fn set_entries_target_gdevice_handle(&self, bus: &MacMemoryBus) -> u32 {
        if self.main_gdevice_handle != 0 {
            self.main_gdevice_handle
        } else {
            self.palette_target_gdevice_handle(bus)
        }
    }

    fn set_entries_target_is_screen(&self, bus: &MacMemoryBus) -> bool {
        let target_gdh = self.set_entries_target_gdevice_handle(bus);
        target_gdh == 0 || target_gdh == self.main_gdevice_handle
    }

    fn palette_target_gdevice_handle(&self, bus: &MacMemoryBus) -> u32 {
        if self.current_gdevice != 0 {
            self.current_gdevice
        } else if self.main_gdevice_handle != 0 {
            self.main_gdevice_handle
        } else {
            bus.read_long(0x0CC8) // TheGDevice
        }
    }

    fn table_looks_like_scaled_canonical_system_8bpp(bus: &MacMemoryBus, table_ptr: u32) -> bool {
        let samples = [
            (0usize, [0xFFFF, 0xFFFF, 0xFFFF]),
            (1usize, [0xFFFF, 0xFFFF, 0xCCCC]),
            (16usize, [0xFFFF, 0x9999, 0x3333]),
            (42usize, [0xCCCC, 0xCCCC, 0xFFFF]),
            (128usize, [0x6666, 0x6666, 0x9999]),
            (255usize, [0, 0, 0]),
        ];
        let entry0 = table_ptr;
        let sample0 = [
            bus.read_word(entry0 + 2),
            bus.read_word(entry0 + 4),
            bus.read_word(entry0 + 6),
        ];
        if sample0[0] != sample0[1] || sample0[1] != sample0[2] {
            return false;
        }
        let scale = sample0[0] as f64 / 65535.0;
        if !(0.0..=1.0).contains(&scale) {
            return false;
        }
        samples.into_iter().all(|(index, expected)| {
            let entry = table_ptr + (index as u32) * 8;
            let actual = [
                bus.read_word(entry + 2),
                bus.read_word(entry + 4),
                bus.read_word(entry + 6),
            ];
            actual
                .into_iter()
                .zip(expected)
                .all(|(a, e)| (f64::from(a) - f64::from(e) * scale).abs() <= 0x0400 as f64)
        })
    }

    fn table_uniform_scale_of_clut(
        bus: &MacMemoryBus,
        table_ptr: u32,
        clut: &[[u16; 3]; 256],
    ) -> Option<f64> {
        let sample_indices = [0usize, 1, 16, 42, 128, 255];
        let mut scale: Option<f64> = None;

        for index in sample_indices {
            let entry = table_ptr + (index as u32) * 8;
            let actual = [
                bus.read_word(entry + 2),
                bus.read_word(entry + 4),
                bus.read_word(entry + 6),
            ];
            let expected = clut[index];

            for channel in 0..3 {
                let base = expected[channel];
                let got = actual[channel];
                if base == 0 {
                    if got > 0x0200 {
                        return None;
                    }
                    continue;
                }

                let candidate = f64::from(got) / f64::from(base);
                if !(0.0..=1.05).contains(&candidate) {
                    return None;
                }
                if let Some(existing) = scale {
                    let want = (f64::from(base) * existing).round() as i32;
                    if (i32::from(got) - want).abs() > 0x0300 {
                        return None;
                    }
                } else {
                    scale = Some(candidate);
                }
            }
        }

        scale
    }

    fn palette_exact_match_index(
        bus: &MacMemoryBus,
        ctab_handle: u32,
        rgb: [u16; 3],
    ) -> Option<u8> {
        if ctab_handle == 0 {
            return None;
        }
        let ctab_ptr = bus.read_long(ctab_handle);
        if ctab_ptr == 0 {
            return None;
        }

        let ct_size = bus.read_word(ctab_ptr + 6) as u32;
        for i in 0..=ct_size.min(255) {
            let entry = ctab_ptr + 8 + i * 8;
            let value = bus.read_word(entry) as u8;
            let entry_rgb = [
                bus.read_word(entry + 2),
                bus.read_word(entry + 4),
                bus.read_word(entry + 6),
            ];
            if entry_rgb == rgb {
                return Some(value);
            }
        }
        None
    }

    fn palette_index_for_rgb(
        &self,
        bus: &MacMemoryBus,
        ctab_handle: u32,
        clut: &[[u16; 3]; 256],
        rgb: [u16; 3],
    ) -> u8 {
        let screen_ctab_handle = if self.main_gdevice_handle != 0 {
            Self::gdevice_ctab_handle(bus, self.main_gdevice_handle)
        } else {
            0
        };
        if ctab_handle != 0 && ctab_handle != screen_ctab_handle {
            Self::palette_exact_match_index(bus, ctab_handle, rgb)
                .unwrap_or_else(|| Self::nearest_palette_index(clut, rgb))
        } else {
            Self::nearest_palette_index(clut, rgb)
        }
    }

    fn copy_bits_transparent_source_indices(
        &self,
        bus: &MacMemoryBus,
        ctab_handle: u32,
        src_clut: Option<&[[u16; 3]; 256]>,
        bg_rgb: [u16; 3],
    ) -> Option<[bool; 256]> {
        let clut = src_clut?;
        let mut indices = [false; 256];
        // IM: Imaging With QuickDraw 1994, p. 4-39: transparent mode
        // preserves the destination when the source pixel equals the current
        // background color.
        let bg_index = self.palette_index_for_rgb(bus, ctab_handle, clut, bg_rgb);
        indices[usize::from(bg_index)] = true;
        // BasiliskII/System 7.5.3 evidence from Blade's indexed GWorld
        // sprites shows source index 0 also acting as the matte hole even
        // when its CLUT entry is black and the current background is white.
        // Keep the documented background-color rule above and add the indexed
        // PixMap matte convention here for 8bpp transparent CopyBits.
        indices[0] = true;
        Some(indices)
    }

    /// Resolve the physical pixel target for SetCPixel/GetCPixel at (h,v).
    /// Returns the byte offset of the pixel together with the underlying
    /// pixmap layout. Returns None when the coordinate is outside the
    /// pixmap bounds (so writes are clipped safely) or when the port has
    /// no backing bitmap/pixmap.
    ///
    /// Inside Macintosh Volume V, V-37 (PixMap layout)
    /// Inside Macintosh Volume I, I-148 (GrafPort / BitMap layout)
    fn resolve_pixel_target(
        &self,
        bus: &MacMemoryBus,
        port: u32,
        h: i16,
        v: i16,
    ) -> Option<PixelTarget> {
        let port_version = bus.read_word(port + 6);
        let is_color = (port_version & 0xC000) != 0;

        let (base, row_bytes, pixel_size, top, left, bottom, right, ctab_handle) = if is_color {
            let pm_handle = bus.read_long(port + 2);
            if pm_handle == 0 {
                return None;
            }
            let pm_ptr = bus.read_long(pm_handle);
            if pm_ptr == 0 {
                return None;
            }
            (
                bus.read_long(pm_ptr),
                (bus.read_word(pm_ptr + 4) & 0x3FFF) as u32,
                bus.read_word(pm_ptr + 32),
                bus.read_word(pm_ptr + 6) as i16,
                bus.read_word(pm_ptr + 8) as i16,
                bus.read_word(pm_ptr + 10) as i16,
                bus.read_word(pm_ptr + 12) as i16,
                bus.read_long(pm_ptr + 42),
            )
        } else {
            (
                bus.read_long(port + 2),
                (bus.read_word(port + 6) & 0x3FFF) as u32,
                1u16,
                bus.read_word(port + 8) as i16,
                bus.read_word(port + 10) as i16,
                bus.read_word(port + 12) as i16,
                bus.read_word(port + 14) as i16,
                0u32,
            )
        };

        if base == 0 {
            return None;
        }
        if h < left || h >= right || v < top || v >= bottom {
            return None;
        }
        for offset in [24u32, 28u32] {
            let rgn_handle = bus.read_long(port.wrapping_add(offset));
            if rgn_handle == 0 {
                continue;
            }
            let rgn_ptr = bus.read_long(rgn_handle);
            if rgn_ptr == 0 {
                continue;
            }
            let rt = bus.read_word(rgn_ptr + 2) as i16;
            let rl = bus.read_word(rgn_ptr + 4) as i16;
            let rb = bus.read_word(rgn_ptr + 6) as i16;
            let rr = bus.read_word(rgn_ptr + 8) as i16;
            if v < rt || v >= rb || h < rl || h >= rr {
                return None;
            }
        }

        let dy = (v - top) as u32;
        let dx = (h - left) as u32;
        Some(PixelTarget {
            base,
            row_bytes,
            pixel_size,
            dx,
            dy,
            ctab_handle,
        })
    }

    /// Read the 8-byte monochrome fallback pattern (pat1Data at +20)
    /// from a PixPatHandle. Returns None when the handle or its master
    /// pointer is NIL so callers can skip the draw safely.
    ///
    /// The PixPat record layout is documented at NewPixPat ($AA07) and
    /// in Inside Macintosh Volume V, V-51; pat1Data lives at the record
    /// offset 20 as a raw Pattern (8 bytes) used by QuickDraw whenever
    /// the destination is 1bpp or the PixMap field is NIL.
    /// Imaging With QuickDraw 1994, p. 4-81
    fn read_pixpat_pat1data(bus: &MacMemoryBus, pp_handle: u32) -> Option<[u8; 8]> {
        if pp_handle == 0 {
            return None;
        }
        let pp_ptr = bus.read_long(pp_handle);
        if pp_ptr == 0 {
            return None;
        }
        let mut pat = [0u8; 8];
        for (i, slot) in pat.iter_mut().enumerate() {
            *slot = bus.read_byte(pp_ptr + 20 + i as u32);
        }
        Some(pat)
    }

    fn read_raw_pixpat_color_table(
        &self,
        bus: &MacMemoryBus,
        table_ptr: u32,
    ) -> Option<[[u16; 3]; 256]> {
        if table_ptr == 0 {
            return None;
        }
        let count = usize::from(bus.read_word(table_ptr + 6)).saturating_add(1);
        if count == 0 || count > 256 {
            return None;
        }
        let mut clut = self.device_clut;
        for ordinal in 0..count {
            let entry = table_ptr + 8 + ordinal as u32 * 8;
            let value = usize::from(bus.read_word(entry));
            if value < 256 {
                clut[value] = [
                    bus.read_word(entry + 2),
                    bus.read_word(entry + 4),
                    bus.read_word(entry + 6),
                ];
            }
        }
        Some(clut)
    }

    fn raw_pixpat_pixel_value(
        bus: &MacMemoryBus,
        data_ptr: u32,
        row_bytes: u32,
        pixel_size: u16,
        x: u32,
        y: u32,
    ) -> Option<u8> {
        let row = data_ptr + y.checked_mul(row_bytes)?;
        match pixel_size {
            1 => {
                let byte = bus.read_byte(row + x / 8);
                Some((byte >> (7 - (x & 7))) & 1)
            }
            2 => {
                let byte = bus.read_byte(row + x / 4);
                Some((byte >> (6 - 2 * (x & 3))) & 0x03)
            }
            4 => {
                let byte = bus.read_byte(row + x / 2);
                if x & 1 == 0 {
                    Some(byte >> 4)
                } else {
                    Some(byte & 0x0F)
                }
            }
            8 => Some(bus.read_byte(row + x)),
            _ => None,
        }
    }

    fn fill_rect_with_raw_pixpat(
        &mut self,
        bus: &mut MacMemoryBus,
        r: &Rect,
        pp_handle: u32,
    ) -> bool {
        macro_rules! reject {
            ($($arg:tt)*) => {{
                if trace_qd_colors_enabled() {
                    eprintln!(
                        "[QD-COLOR] FillCRect raw PixPat reject pp=${:08X}: {}",
                        pp_handle,
                        format_args!($($arg)*),
                    );
                }
                return false;
            }};
        }

        if self.pn_vis < 0 || pp_handle == 0 {
            return self.pn_vis < 0;
        }
        let pp_ptr = bus.read_long(pp_handle);
        if pp_ptr == 0 {
            reject!("nil master pointer");
        }

        // 'ppat' resources store offsets from the start of the PixPat
        // resource instead of live Handles. NewPixPat-created records store
        // actual handles here; those continue down the pat1Data fallback path.
        let pat_map_offset = bus.read_long(pp_ptr + 2);
        let pat_data_offset = bus.read_long(pp_ptr + 6);
        if !(28..=0x10000).contains(&pat_map_offset)
            || !(pat_map_offset + 50..=0x200000).contains(&pat_data_offset)
        {
            reject!(
                "unsupported offsets patMap=${:08X} patData=${:08X}",
                pat_map_offset,
                pat_data_offset
            );
        }

        let pat_map_ptr = pp_ptr + pat_map_offset;
        let pat_data_ptr = pp_ptr + pat_data_offset;
        let read_map = |base_offset: u32| {
            let row_bytes = u32::from(bus.read_word(pat_map_ptr + base_offset) & 0x3FFF);
            let top = bus.read_word(pat_map_ptr + base_offset + 2) as i16;
            let left = bus.read_word(pat_map_ptr + base_offset + 4) as i16;
            let bottom = bus.read_word(pat_map_ptr + base_offset + 6) as i16;
            let right = bus.read_word(pat_map_ptr + base_offset + 8) as i16;
            let pixel_size = bus.read_word(pat_map_ptr + base_offset + 28);
            let table_offset = bus.read_long(pat_map_ptr + base_offset + 38);
            (
                row_bytes,
                top,
                left,
                bottom,
                right,
                pixel_size,
                table_offset,
            )
        };
        let map_valid = |candidate: &(u32, i16, i16, i16, i16, u16, u32)| {
            let (row_bytes, top, left, bottom, right, pixel_size, table_offset) = *candidate;
            row_bytes != 0
                && i32::from(bottom) > i32::from(top)
                && i32::from(right) > i32::from(left)
                && matches!(pixel_size, 1 | 2 | 4 | 8)
                && table_offset != 0
                && table_offset <= 0x200000
        };

        // Raw 'ppat' resources store the pattern map without baseAddr
        // (rowBytes at +0). GetPixPat-expanded records can hold a full
        // live PixMap with baseAddr inserted (rowBytes at +4). Accept both.
        let raw_map = read_map(0);
        let live_map = read_map(4);
        let (src_row_bytes, src_top, src_left, src_bottom, src_right, src_pixel_size, table_offset) =
            if map_valid(&raw_map) {
                raw_map
            } else if map_valid(&live_map) {
                live_map
            } else {
                let (row_bytes, top, left, bottom, right, pixel_size, table_offset) = raw_map;
                reject!(
                    "invalid source map raw(rowBytes={} bounds=({},{},{},{}) pixelSize={} table=${:08X}) live(rowBytes={} bounds=({},{},{},{}) pixelSize={} table=${:08X})",
                    row_bytes,
                    top,
                    left,
                    bottom,
                    right,
                    pixel_size,
                    table_offset,
                    live_map.0,
                    live_map.1,
                    live_map.2,
                    live_map.3,
                    live_map.4,
                    live_map.5,
                    live_map.6,
                );
            };
        let src_height = i32::from(src_bottom) - i32::from(src_top);
        let src_width = i32::from(src_right) - i32::from(src_left);

        if table_offset == 0 || table_offset > 0x200000 {
            reject!("invalid table offset ${:08X}", table_offset);
        }
        let Some(src_clut) = self.read_raw_pixpat_color_table(bus, pp_ptr + table_offset) else {
            reject!("invalid color table at offset ${:08X}", table_offset);
        };

        let port = self.current_port;
        if port == 0 {
            reject!("no current port");
        }
        let port_version = bus.read_word(port.wrapping_add(6));
        if (port_version & 0xC000) == 0 {
            reject!(
                "current port ${:08X} is not a CGrafPort version=${:04X}",
                port,
                port_version
            );
        }
        let pix_map_handle = bus.read_long(port.wrapping_add(2));
        if pix_map_handle == 0 {
            reject!("current port has nil PixMap handle");
        }
        let pix_map_ptr = bus.read_long(pix_map_handle);
        if pix_map_ptr == 0 {
            reject!("current port PixMap handle has nil master pointer");
        }
        let dst_base = bus.read_long(pix_map_ptr);
        let dst_row_bytes = u32::from(bus.read_word(pix_map_ptr + 4) & 0x3FFF);
        let bounds_top = bus.read_word(pix_map_ptr + 6) as i16;
        let bounds_left = bus.read_word(pix_map_ptr + 8) as i16;
        let bounds_bottom = bus.read_word(pix_map_ptr + 10) as i16;
        let bounds_right = bus.read_word(pix_map_ptr + 12) as i16;
        let dst_pixel_size = bus.read_word(pix_map_ptr + 32);
        if dst_pixel_size != 8 || dst_base == 0 || dst_row_bytes == 0 {
            reject!(
                "unsupported destination base=${:08X} rowBytes={} pixelSize={}",
                dst_base,
                dst_row_bytes,
                dst_pixel_size
            );
        }

        let is_screen_port = dst_base == self.screen_mode.0
            && dst_row_bytes == self.screen_mode.1
            && dst_pixel_size == self.screen_mode.4;
        let dst_height = i32::from(bounds_bottom) - i32::from(bounds_top);
        let dst_width = i32::from(bounds_right) - i32::from(bounds_left);
        let rect_height = i32::from(r.bottom) - i32::from(r.top);
        let rect_width = i32::from(r.right) - i32::from(r.left);
        let fills_destination_bounds = r.top <= bounds_top
            && r.left <= bounds_left
            && r.bottom >= bounds_bottom
            && r.right >= bounds_right;
        let fills_hidden_menu_screen_bounds = self.menu_bar_hidden
            && is_screen_port
            && port == self.front_window
            && dst_height == self.screen_mode.3 as i32
            && dst_width == self.screen_mode.2 as i32
            && rect_height >= dst_height.saturating_sub(1)
            && rect_width >= dst_width.saturating_sub(2)
            && r.top <= bounds_top
            && r.left <= bounds_left
            && r.bottom <= bounds_bottom
            && r.right <= bounds_right;
        let skips_region_clip = fills_destination_bounds || fills_hidden_menu_screen_bounds;
        let (fill_top, fill_left, fill_bottom, fill_right) = if fills_hidden_menu_screen_bounds {
            (bounds_top, bounds_left, bounds_bottom, bounds_right)
        } else {
            (r.top, r.left, r.bottom, r.right)
        };

        let mut clip_top = bounds_top;
        let mut clip_left = bounds_left;
        let mut clip_bottom = bounds_bottom;
        let mut clip_right = bounds_right;
        let mut trace_vis = None;
        let mut trace_clip = None;
        if !skips_region_clip {
            let vis_rgn_handle = bus.read_long(port.wrapping_add(24));
            if vis_rgn_handle != 0 {
                let vis_rgn_ptr = bus.read_long(vis_rgn_handle);
                if vis_rgn_ptr != 0 {
                    let vt = bus.read_word(vis_rgn_ptr + 2) as i16;
                    let vl = bus.read_word(vis_rgn_ptr + 4) as i16;
                    let vb = bus.read_word(vis_rgn_ptr + 6) as i16;
                    let vr = bus.read_word(vis_rgn_ptr + 8) as i16;
                    trace_vis = Some((vt, vl, vb, vr));
                    clip_top = clip_top.max(vt);
                    clip_left = clip_left.max(vl);
                    clip_bottom = clip_bottom.min(vb);
                    clip_right = clip_right.min(vr);
                }
            }
            let clip_rgn_handle = bus.read_long(port.wrapping_add(28));
            if clip_rgn_handle != 0 {
                let clip_rgn_ptr = bus.read_long(clip_rgn_handle);
                if clip_rgn_ptr != 0 {
                    let ct = bus.read_word(clip_rgn_ptr + 2) as i16;
                    let cl = bus.read_word(clip_rgn_ptr + 4) as i16;
                    let cb = bus.read_word(clip_rgn_ptr + 6) as i16;
                    let cr = bus.read_word(clip_rgn_ptr + 8) as i16;
                    trace_clip = Some((ct, cl, cb, cr));
                    clip_top = clip_top.max(ct);
                    clip_left = clip_left.max(cl);
                    clip_bottom = clip_bottom.min(cb);
                    clip_right = clip_right.min(cr);
                }
            }
        }

        let dst_clut = if is_screen_port {
            self.device_clut
        } else {
            self.read_port_clut(bus, bus.read_long(pix_map_ptr + 42))
        };
        let mut src_to_dst = [0u8; 256];
        for (value, dst) in src_to_dst.iter_mut().enumerate() {
            let rgb = src_clut[value];
            *dst = super::pict::closest_clut_index(rgb[0], rgb[1], rgb[2], &dst_clut);
        }

        let top = fill_top.max(clip_top);
        let left = fill_left.max(clip_left);
        let bottom = fill_bottom.min(clip_bottom);
        let right = fill_right.min(clip_right);
        if trace_qd_colors_enabled() {
            eprintln!(
                "[QD-COLOR] FillCRect raw PixPat geometry dst_bounds=({},{},{},{}) vis={:?} clip_rgn={:?} combined=({},{},{},{}) final=({},{},{},{}) full_bounds={} base=${:08X} rowBytes={}",
                bounds_top,
                bounds_left,
                bounds_bottom,
                bounds_right,
                trace_vis,
                trace_clip,
                clip_top,
                clip_left,
                clip_bottom,
                clip_right,
                top,
                left,
                bottom,
                right,
                fills_destination_bounds,
                dst_base,
                dst_row_bytes,
            );
        }
        if bottom <= top || right <= left {
            return true;
        }
        if is_screen_port {
            self.ensure_dialog_background_saved_for_screen_port(bus, port);
        }

        for y in top..bottom {
            let dy = (y - bounds_top) as u32;
            let tile_y = i32::from(y).rem_euclid(src_height) as u32;
            for x in left..right {
                let dx = (x - bounds_left) as u32;
                if dx >= dst_row_bytes {
                    continue;
                }
                let tile_x = i32::from(x).rem_euclid(src_width) as u32;
                let Some(src_value) = Self::raw_pixpat_pixel_value(
                    bus,
                    pat_data_ptr,
                    src_row_bytes,
                    src_pixel_size,
                    tile_x,
                    tile_y,
                ) else {
                    continue;
                };
                bus.write_byte(
                    dst_base + dy * dst_row_bytes + dx,
                    src_to_dst[usize::from(src_value)],
                );
            }
        }
        true
    }

    /// CLUT used for SetCPixel/GetCPixel pixel matching. Per Inside
    /// Macintosh Volume V, V-70, both traps must consult the "current
    /// device's CLUT", which maps to `device_clut` — the live hardware
    /// CLUT that SetEntries/AnimateEntry mutate. The port's own ctab
    /// (when present and distinct from the main device) overrides that.
    fn cpixel_clut(&self, bus: &MacMemoryBus, ctab_handle: u32) -> [[u16; 3]; 256] {
        if ctab_handle == 0 {
            return self.device_clut;
        }
        let screen_ctab_handle = if self.main_gdevice_handle != 0 {
            Self::gdevice_ctab_handle(bus, self.main_gdevice_handle)
        } else {
            0
        };
        if ctab_handle == screen_ctab_handle {
            return self.device_clut;
        }
        self.read_ctab_handle_clut(bus, ctab_handle)
    }

    /// Write an RGBColor to the pixel described by `target`. Indexed
    /// pixmaps go through the current device CLUT (Color Manager best
    /// match); direct pixmaps pack the RGB components into a 16- or
    /// 32-bit pixel value.
    /// Imaging With QuickDraw 1994, p. 4-73
    fn write_cpixel(&self, bus: &mut MacMemoryBus, target: &PixelTarget, rgb: [u16; 3]) {
        let byte_offset = match target.pixel_size {
            1 => target.dy * target.row_bytes + target.dx / 8,
            8 => target.dy * target.row_bytes + target.dx,
            16 => target.dy * target.row_bytes + target.dx * 2,
            32 => target.dy * target.row_bytes + target.dx * 4,
            _ => return,
        };
        let addr = target.base + byte_offset;
        match target.pixel_size {
            1 => {
                // Mac 1bpp convention: bit 0 = white, bit 1 = black.
                let bit = 7 - (target.dx % 8);
                let mask = 1u8 << bit;
                let luminance = (u32::from(rgb[0]) + u32::from(rgb[1]) + u32::from(rgb[2])) / 3;
                let current = bus.read_byte(addr);
                let new = if luminance < 0x8000 {
                    current | mask
                } else {
                    current & !mask
                };
                bus.write_byte(addr, new);
            }
            8 => {
                let clut = self.cpixel_clut(bus, target.ctab_handle);
                let index = self.palette_index_for_rgb(bus, target.ctab_handle, &clut, rgb);
                bus.write_byte(addr, index);
            }
            16 => {
                // Direct 16bpp: 1 alpha bit + 5-5-5 RGB.
                let r5 = u32::from(rgb[0] >> 11) & 0x1F;
                let g5 = u32::from(rgb[1] >> 11) & 0x1F;
                let b5 = u32::from(rgb[2] >> 11) & 0x1F;
                let packed = ((r5 << 10) | (g5 << 5) | b5) as u16;
                bus.write_word(addr, packed);
            }
            32 => {
                // Direct 32bpp: 8 bits alpha + 8-8-8 RGB.
                let r8 = u32::from(rgb[0] >> 8);
                let g8 = u32::from(rgb[1] >> 8);
                let b8 = u32::from(rgb[2] >> 8);
                bus.write_long(addr, (r8 << 16) | (g8 << 8) | b8);
            }
            _ => {}
        }
    }

    /// Read an RGBColor from the pixel described by `target`. Indexed
    /// pixmaps look the stored pixel value up in the current device
    /// CLUT; direct pixmaps unpack the packed pixel into 48-bit RGB.
    /// Imaging With QuickDraw 1994, p. 4-80
    fn read_cpixel(&self, bus: &MacMemoryBus, target: &PixelTarget) -> [u16; 3] {
        let byte_offset = match target.pixel_size {
            1 => target.dy * target.row_bytes + target.dx / 8,
            8 => target.dy * target.row_bytes + target.dx,
            16 => target.dy * target.row_bytes + target.dx * 2,
            32 => target.dy * target.row_bytes + target.dx * 4,
            _ => return [0, 0, 0],
        };
        let addr = target.base + byte_offset;
        match target.pixel_size {
            1 => {
                let bit = 7 - (target.dx % 8);
                let byte = bus.read_byte(addr);
                if (byte & (1 << bit)) != 0 {
                    [0, 0, 0]
                } else {
                    [0xFFFF, 0xFFFF, 0xFFFF]
                }
            }
            8 => {
                let index = bus.read_byte(addr) as usize;
                let clut = self.cpixel_clut(bus, target.ctab_handle);
                clut[index]
            }
            16 => {
                let packed = bus.read_word(addr);
                // Expand 5-5-5 to full 16-bit channels by bit-replication.
                let r5 = (packed >> 10) & 0x1F;
                let g5 = (packed >> 5) & 0x1F;
                let b5 = packed & 0x1F;
                let expand5 = |c: u16| (c << 11) | (c << 6) | (c << 1) | (c >> 4);
                [expand5(r5), expand5(g5), expand5(b5)]
            }
            32 => {
                let packed = bus.read_long(addr);
                let r8 = ((packed >> 16) & 0xFF) as u16;
                let g8 = ((packed >> 8) & 0xFF) as u16;
                let b8 = (packed & 0xFF) as u16;
                [(r8 << 8) | r8, (g8 << 8) | g8, (b8 << 8) | b8]
            }
            _ => [0, 0, 0],
        }
    }

    /// Read portBits.bounds.topLeft from either a GrafPort or CGrafPort.
    /// GrafPort: portBits (BitMap) is inline at +2, bounds at +8..+14.
    /// CGrafPort: portPixMap (handle) at +2; bounds are in the PixMap record.
    /// Imaging With QuickDraw 1994, p. 4-125 (CGrafPort layout)
    /// Inside Macintosh Volume I, I-148 (GrafPort layout)
    pub(crate) fn port_bounds_top_left(&self, bus: &MacMemoryBus, port: u32) -> (i16, i16) {
        if port == 0 {
            return (0, 0);
        }
        let port_version = bus.read_word(port + 6);
        let is_color = (port_version & 0xC000) != 0;
        if is_color {
            let pm_handle = bus.read_long(port + 2);
            if pm_handle != 0 {
                let pm_ptr = bus.read_long(pm_handle);
                if pm_ptr != 0 {
                    return (
                        bus.read_word(pm_ptr + 6) as i16,
                        bus.read_word(pm_ptr + 8) as i16,
                    );
                }
            }
            (0, 0)
        } else {
            (
                bus.read_word(port + 8) as i16,
                bus.read_word(port + 10) as i16,
            )
        }
    }

    pub(crate) fn port_rect_top_left(&self, bus: &MacMemoryBus, port: u32) -> (i16, i16) {
        if port == 0 {
            return (0, 0);
        }
        (
            bus.read_word(port + 16) as i16,
            bus.read_word(port + 18) as i16,
        )
    }

    fn promote_compiled_crsr_resource(
        &mut self,
        bus: &mut MacMemoryBus,
        resource_ptr: u32,
    ) -> Option<u32> {
        let crsr_type = bus.read_word(resource_ptr);
        if crsr_type != 0x8001 && crsr_type != 0x8000 {
            return None;
        }

        let map_offset = bus.read_long(resource_ptr + 2);
        let data_offset = bus.read_long(resource_ptr + 6);
        if map_offset < 96 || data_offset <= map_offset {
            return None;
        }

        let raw_pixmap = resource_ptr + map_offset;
        let raw_row_bytes = bus.read_word(raw_pixmap);
        let row_bytes = u32::from(raw_row_bytes & 0x3FFF);
        let top = bus.read_word(raw_pixmap + 2) as i16;
        let left = bus.read_word(raw_pixmap + 4) as i16;
        let bottom = bus.read_word(raw_pixmap + 6) as i16;
        let right = bus.read_word(raw_pixmap + 8) as i16;
        let height = bottom.saturating_sub(top).max(0) as u32;
        if row_bytes == 0 || height == 0 {
            return None;
        }

        let table_offset = bus.read_long(raw_pixmap + 38);
        let pixel_data_len = if table_offset > data_offset {
            table_offset - data_offset
        } else {
            row_bytes.saturating_mul(height)
        }
        .max(1);

        let pixel_data_ptr = bus.alloc(pixel_data_len);
        for i in 0..pixel_data_len {
            bus.write_byte(
                pixel_data_ptr + i,
                bus.read_byte(resource_ptr + data_offset + i),
            );
        }
        let pixel_data_handle = bus.alloc(4);
        bus.write_long(pixel_data_handle, pixel_data_ptr);

        let ctab_handle = if table_offset != 0 && table_offset > data_offset {
            let raw_ctab = resource_ptr + table_offset;
            let ct_size = u32::from(bus.read_word(raw_ctab + 6)).min(255);
            let ctab_len = 8 + (ct_size + 1) * 8;
            let ctab_ptr = bus.alloc(ctab_len);
            for i in 0..ctab_len {
                bus.write_byte(ctab_ptr + i, bus.read_byte(raw_ctab + i));
            }
            let handle = bus.alloc(4);
            bus.write_long(handle, ctab_ptr);
            handle
        } else {
            0
        };

        let pixmap_ptr = bus.alloc(50);
        let pixmap_handle = bus.alloc(4);
        bus.write_long(pixmap_handle, pixmap_ptr);
        bus.write_long(pixmap_ptr, pixel_data_ptr);
        bus.write_word(pixmap_ptr + 4, raw_row_bytes);
        bus.write_word(pixmap_ptr + 6, top as u16);
        bus.write_word(pixmap_ptr + 8, left as u16);
        bus.write_word(pixmap_ptr + 10, bottom as u16);
        bus.write_word(pixmap_ptr + 12, right as u16);
        for (raw_off, live_off, len) in [
            (10u32, 14u32, 2u32),
            (12, 16, 2),
            (14, 18, 4),
            (18, 22, 4),
            (22, 26, 4),
            (26, 30, 2),
            (28, 32, 2),
            (30, 34, 2),
            (32, 36, 2),
            (34, 38, 4),
            (42, 46, 4),
        ] {
            for i in 0..len {
                bus.write_byte(
                    pixmap_ptr + live_off + i,
                    bus.read_byte(raw_pixmap + raw_off + i),
                );
            }
        }
        bus.write_long(pixmap_ptr + 42, ctab_handle);

        let crsr_ptr = bus.alloc(96);
        let crsr_handle = bus.alloc(4);
        bus.write_long(crsr_handle, crsr_ptr);
        bus.write_word(crsr_ptr, crsr_type);
        bus.write_long(crsr_ptr + 2, pixmap_handle);
        bus.write_long(crsr_ptr + 6, pixel_data_handle);
        bus.write_long(crsr_ptr + 10, 0);
        bus.write_word(crsr_ptr + 14, 0);
        bus.write_long(crsr_ptr + 16, 0);
        for i in 20..96u32 {
            bus.write_byte(crsr_ptr + i, bus.read_byte(resource_ptr + i));
        }

        Some(crsr_handle)
    }

    fn cursor_image_from_ccrsr(&self, bus: &MacMemoryBus, crsr_ptr: u32) -> Option<CursorImage> {
        if crsr_ptr == 0 {
            return None;
        }

        let mut mono_data = [0u8; 32];
        for (i, byte) in mono_data.iter_mut().enumerate() {
            *byte = bus.read_byte(crsr_ptr + 20 + i as u32);
        }
        let mut mono_mask = [0u8; 32];
        for (i, byte) in mono_mask.iter_mut().enumerate() {
            *byte = bus.read_byte(crsr_ptr + 52 + i as u32);
        }
        let hot_v = bus.read_word(crsr_ptr + 84) as i16;
        let hot_h = bus.read_word(crsr_ptr + 86) as i16;

        let crsr_type = bus.read_word(crsr_ptr);
        if crsr_type != 0x8001 {
            return Some(CursorImage::mono(mono_data, mono_mask, hot_v, hot_h));
        }

        let pixmap_handle = bus.read_long(crsr_ptr + 2);
        let pixel_handle = bus.read_long(crsr_ptr + 6);
        if pixmap_handle == 0 || pixel_handle == 0 {
            return Some(CursorImage::mono(mono_data, mono_mask, hot_v, hot_h));
        }
        let pixmap_ptr = bus.read_long(pixmap_handle);
        let pixel_data_ptr = bus.read_long(pixel_handle);
        if pixmap_ptr == 0 || pixel_data_ptr == 0 {
            return Some(CursorImage::mono(mono_data, mono_mask, hot_v, hot_h));
        }

        let row_bytes = u32::from(bus.read_word(pixmap_ptr + 4) & 0x3FFF);
        let top = bus.read_word(pixmap_ptr + 6) as i16;
        let left = bus.read_word(pixmap_ptr + 8) as i16;
        let bottom = bus.read_word(pixmap_ptr + 10) as i16;
        let right = bus.read_word(pixmap_ptr + 12) as i16;
        let width = right.saturating_sub(left).max(0) as u16;
        let height = bottom.saturating_sub(top).max(0) as u16;
        let pixel_size = bus.read_word(pixmap_ptr + 32);
        if row_bytes == 0 || width == 0 || height == 0 || width > 128 || height > 128 {
            return Some(CursorImage::mono(mono_data, mono_mask, hot_v, hot_h));
        }

        let pixels_argb = self.decode_cursor_pixels_argb(
            bus,
            pixel_data_ptr,
            row_bytes,
            width,
            height,
            pixel_size,
            bus.read_long(pixmap_ptr + 42),
        )?;

        Some(CursorImage::Color {
            width,
            height,
            pixels_argb,
            mask: mono_mask,
            hot_v,
            hot_h,
            mono_data,
            mono_mask,
        })
    }

    fn decode_cursor_pixels_argb(
        &self,
        bus: &MacMemoryBus,
        base: u32,
        row_bytes: u32,
        width: u16,
        height: u16,
        pixel_size: u16,
        ctab_handle: u32,
    ) -> Option<Vec<u32>> {
        let mut pixels = Vec::with_capacity(width as usize * height as usize);
        let clut = self.read_ctab_handle_clut(bus, ctab_handle);

        for row in 0..height as u32 {
            for col in 0..width as u32 {
                let argb = match pixel_size {
                    1 => {
                        let byte = bus.read_byte(base + row * row_bytes + col / 8);
                        let index = (byte >> (7 - (col % 8))) & 1;
                        display::clut_to_argb(&clut, index)
                    }
                    2 => {
                        let byte = bus.read_byte(base + row * row_bytes + col / 4);
                        let index = (byte >> (6 - (col % 4) * 2)) & 0x03;
                        display::clut_to_argb(&clut, index)
                    }
                    4 => {
                        let byte = bus.read_byte(base + row * row_bytes + col / 2);
                        let index = if (col & 1) == 0 {
                            (byte >> 4) & 0x0F
                        } else {
                            byte & 0x0F
                        };
                        display::clut_to_argb(&clut, index)
                    }
                    8 => {
                        let index = bus.read_byte(base + row * row_bytes + col);
                        display::clut_to_argb(&clut, index)
                    }
                    16 => {
                        let packed = bus.read_word(base + row * row_bytes + col * 2);
                        let r5 = (packed >> 10) & 0x1F;
                        let g5 = (packed >> 5) & 0x1F;
                        let b5 = packed & 0x1F;
                        let expand5 = |c: u16| ((c << 3) | (c >> 2)) as u32;
                        0xFF00_0000 | (expand5(r5) << 16) | (expand5(g5) << 8) | expand5(b5)
                    }
                    32 => bus.read_long(base + row * row_bytes + col * 4) | 0xFF00_0000,
                    _ => return None,
                };
                pixels.push(argb);
            }
        }

        Some(pixels)
    }
}

#[cfg(test)]
mod tests {
    use super::super::test_helpers::{setup, setup_with_port, TEST_SP};
    use crate::cpu::{CpuOps, Register};
    use crate::display::CursorImage;
    use crate::memory::{MacMemoryBus, MemoryBus};
    use crate::trap::dispatch::{
        DialogItem, LoadedResources, RecentColorTableFetch, ResourceFileMap,
    };
    use crate::trap::quickdraw::CopyBitmapInfo;
    use crate::trap::types::{Rect, ShapeOp};
    use crate::trap::TrapDispatcher;
    use crate::ui_theme::UiThemeId;
    use std::collections::HashMap;

    /// Helper: write a rect (top, left, bottom, right) at addr.
    fn write_rect(
        bus: &mut impl MemoryBus,
        addr: u32,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) {
        bus.write_word(addr, top as u16);
        bus.write_word(addr + 2, left as u16);
        bus.write_word(addr + 4, bottom as u16);
        bus.write_word(addr + 6, right as u16);
    }

    /// Helper: read a rect from addr, returning (top, left, bottom, right).
    fn read_rect(bus: &impl MemoryBus, addr: u32) -> (i16, i16, i16, i16) {
        (
            bus.read_word(addr) as i16,
            bus.read_word(addr + 2) as i16,
            bus.read_word(addr + 4) as i16,
            bus.read_word(addr + 6) as i16,
        )
    }

    /// Helper: allocate a simple 10-byte region and its handle at given addresses.
    fn make_rgn(
        bus: &mut impl MemoryBus,
        rgn_addr: u32,
        handle_addr: u32,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) {
        bus.write_word(rgn_addr, 10);
        bus.write_word(rgn_addr + 2, top as u16);
        bus.write_word(rgn_addr + 4, left as u16);
        bus.write_word(rgn_addr + 6, bottom as u16);
        bus.write_word(rgn_addr + 8, right as u16);
        bus.write_long(handle_addr, rgn_addr);
    }

    fn make_complex_rgn(
        bus: &mut MacMemoryBus,
        bbox: (i16, i16, i16, i16),
        data_words: &[i16],
    ) -> u32 {
        let handle = bus.alloc(4);
        assert!(TrapDispatcher::write_region(
            bus,
            handle,
            Some(bbox),
            data_words
        ));
        handle
    }

    /// Helper: write a BitMap record.
    fn write_bitmap_record(
        bus: &mut impl MemoryBus,
        bitmap_ptr: u32,
        base_addr: u32,
        row_bytes: u16,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) {
        bus.write_long(bitmap_ptr, base_addr);
        bus.write_word(bitmap_ptr + 4, row_bytes);
        bus.write_word(bitmap_ptr + 6, top as u16);
        bus.write_word(bitmap_ptr + 8, left as u16);
        bus.write_word(bitmap_ptr + 10, bottom as u16);
        bus.write_word(bitmap_ptr + 12, right as u16);
    }

    /// Helper: write a polygon record and its handle.
    /// PolyRec layout: polySize(2) + polyBBox(8) + polyPoints(4*N).
    fn make_poly(
        bus: &mut impl MemoryBus,
        poly_addr: u32,
        handle_addr: u32,
        bbox: (i16, i16, i16, i16),
        points: &[(i16, i16)],
    ) {
        let poly_size = 10 + points.len() as u32 * 4;
        bus.write_word(poly_addr, poly_size as u16);
        bus.write_word(poly_addr + 2, bbox.0 as u16);
        bus.write_word(poly_addr + 4, bbox.1 as u16);
        bus.write_word(poly_addr + 6, bbox.2 as u16);
        bus.write_word(poly_addr + 8, bbox.3 as u16);
        for (idx, (v, h)) in points.iter().enumerate() {
            let off = poly_addr + 10 + idx as u32 * 4;
            bus.write_word(off, *v as u16);
            bus.write_word(off + 2, *h as u16);
        }
        bus.write_long(handle_addr, poly_addr);
    }

    fn setup_polygon_surface(
        d: &mut TrapDispatcher,
        bus: &mut crate::memory::MacMemoryBus,
    ) -> (u32, u32) {
        let row_bytes = 64u32;
        let width = 64u16;
        let height = 64u16;
        let screen_base = bus.alloc(row_bytes * height as u32);
        bus.fill_zeros(screen_base, row_bytes * height as u32);
        bus.write_long(0x0824, screen_base);
        d.screen_mode = (screen_base, row_bytes, width, height, 8);

        const PORT_PTR: u32 = 0x181000;
        bus.write_long(PORT_PTR + 2, screen_base);
        bus.write_word(PORT_PTR + 6, row_bytes as u16);

        (screen_base, row_bytes)
    }

    /// Allocate a minimal PixPat handle+record and seed pat1Data bytes.
    fn make_pixpat_handle(bus: &mut crate::memory::MacMemoryBus, pat1_data: [u8; 8]) -> u32 {
        let pp_handle = bus.alloc(4);
        let pp_ptr = bus.alloc(28);
        bus.write_long(pp_handle, pp_ptr);
        bus.fill_zeros(pp_ptr, 28);
        bus.write_word(pp_ptr, 1); // patType=color
        bus.write_bytes(pp_ptr + 20, &pat1_data); // pat1Data fallback pattern
        pp_handle
    }

    fn make_raw_color_pixpat_handle(bus: &mut crate::memory::MacMemoryBus) -> u32 {
        let pp_handle = bus.alloc(4);
        let pp_ptr = bus.alloc(128);
        bus.write_long(pp_handle, pp_ptr);
        bus.fill_zeros(pp_ptr, 128);

        const PAT_MAP_OFFSET: u32 = 28;
        const PAT_DATA_OFFSET: u32 = 78;
        const CTAB_OFFSET: u32 = 80;

        bus.write_word(pp_ptr, 1); // patType = color
        bus.write_long(pp_ptr + 2, PAT_MAP_OFFSET);
        bus.write_long(pp_ptr + 6, PAT_DATA_OFFSET);
        bus.write_word(pp_ptr + 14, 0xFFFF); // patXValid
        bus.write_bytes(pp_ptr + 20, &[0xAA; 8]); // fallback unused on color path

        let pixmap = pp_ptr + PAT_MAP_OFFSET;
        bus.write_word(pixmap, 0x8001); // 1 byte/row, color PixMap rowBytes flag set
        bus.write_word(pixmap + 2, 0);
        bus.write_word(pixmap + 4, 0);
        bus.write_word(pixmap + 6, 2);
        bus.write_word(pixmap + 8, 4);
        bus.write_long(pixmap + 14, 0x0048_0000); // hRes
        bus.write_long(pixmap + 18, 0x0048_0000); // vRes
        bus.write_word(pixmap + 28, 2); // pixelSize
        bus.write_word(pixmap + 30, 1); // cmpCount
        bus.write_word(pixmap + 32, 2); // cmpSize
        bus.write_long(pixmap + 38, CTAB_OFFSET);

        // Two rows of 2bpp source pixels:
        // row 0 = values 0,1,2,1; row 1 = 2,1,0,1.
        bus.write_byte(pp_ptr + PAT_DATA_OFFSET, 0x19);
        bus.write_byte(pp_ptr + PAT_DATA_OFFSET + 1, 0x91);

        let ctab = pp_ptr + CTAB_OFFSET;
        bus.write_word(ctab + 6, 2); // 3 entries
        let entries = [
            (0u16, [0x1111, 0x0000, 0x0000]),
            (1u16, [0x0000, 0x2222, 0x0000]),
            (2u16, [0x0000, 0x0000, 0x3333]),
        ];
        for (idx, (value, rgb)) in entries.iter().enumerate() {
            let entry = ctab + 8 + idx as u32 * 8;
            bus.write_word(entry, *value);
            bus.write_word(entry + 2, rgb[0]);
            bus.write_word(entry + 4, rgb[1]);
            bus.write_word(entry + 6, rgb[2]);
        }

        pp_handle
    }

    fn make_test_ctab_handle(
        bus: &mut crate::memory::MacMemoryBus,
        entries: &[[u16; 3]],
        seed: u32,
        flags: u16,
    ) -> u32 {
        let handle = bus.alloc(4);
        let ptr = bus.alloc(8 + entries.len() as u32 * 8);
        bus.write_long(handle, ptr);
        bus.write_long(ptr, seed);
        bus.write_word(ptr + 4, flags);
        bus.write_word(ptr + 6, entries.len().saturating_sub(1) as u16);
        for (index, rgb) in entries.iter().enumerate() {
            let entry = ptr + 8 + index as u32 * 8;
            bus.write_word(entry, index as u16);
            bus.write_word(entry + 2, rgb[0]);
            bus.write_word(entry + 4, rgb[1]);
            bus.write_word(entry + 6, rgb[2]);
        }
        handle
    }

    fn read_test_ctab_rgb(
        bus: &crate::memory::MacMemoryBus,
        ctab_handle: u32,
        index: u32,
    ) -> [u16; 3] {
        let ptr = bus.read_long(ctab_handle);
        let entry = ptr + 8 + index * 8;
        [
            bus.read_word(entry + 2),
            bus.read_word(entry + 4),
            bus.read_word(entry + 6),
        ]
    }

    fn read_surface_pixel(
        bus: &impl MemoryBus,
        screen_base: u32,
        row_bytes: u32,
        x: u32,
        y: u32,
    ) -> u8 {
        bus.read_byte(screen_base + y * row_bytes + x)
    }

    /// Helper: read bbox rect (top,left,bottom,right) from a region handle.
    fn read_rgn_bbox(bus: &impl MemoryBus, rgn_handle: u32) -> (i16, i16, i16, i16) {
        let rgn_ptr = bus.read_long(rgn_handle);
        (
            bus.read_word(rgn_ptr + 2) as i16,
            bus.read_word(rgn_ptr + 4) as i16,
            bus.read_word(rgn_ptr + 6) as i16,
            bus.read_word(rgn_ptr + 8) as i16,
        )
    }

    fn assert_rgn_bytes_equal(bus: &impl MemoryBus, actual_handle: u32, expected_handle: u32) {
        let actual_ptr = bus.read_long(actual_handle);
        let expected_ptr = bus.read_long(expected_handle);
        let expected_size = bus.read_word(expected_ptr) as u32;
        assert_eq!(bus.read_word(actual_ptr) as u32, expected_size);
        for offset in 0..expected_size {
            assert_eq!(
                bus.read_byte(actual_ptr + offset),
                bus.read_byte(expected_ptr + offset),
                "region byte {} should match",
                offset
            );
        }
    }

    /// Build a minimal version-1 PICT with a single paintRect opcode.
    fn write_v1_paintrect_picture(
        bus: &mut impl MemoryBus,
        pic: u32,
        pic_frame: (i16, i16, i16, i16),
        paint_rect: (i16, i16, i16, i16),
    ) {
        let mut p = pic + 10;
        bus.write_byte(p, 0x11); // versionOp
        p += 1;
        bus.write_byte(p, 0x01); // version 1
        p += 1;
        bus.write_byte(p, 0x31); // paintRect
        p += 1;
        bus.write_word(p, paint_rect.0 as u16);
        p += 2;
        bus.write_word(p, paint_rect.1 as u16);
        p += 2;
        bus.write_word(p, paint_rect.2 as u16);
        p += 2;
        bus.write_word(p, paint_rect.3 as u16);
        p += 2;
        bus.write_byte(p, 0xFF); // EndOfPicture
        p += 1;

        bus.write_word(pic, (p - pic) as u16);
        bus.write_word(pic + 2, pic_frame.0 as u16);
        bus.write_word(pic + 4, pic_frame.1 as u16);
        bus.write_word(pic + 6, pic_frame.2 as u16);
        bus.write_word(pic + 8, pic_frame.3 as u16);
    }

    // ==================== CopyBits & Mask Operations ====================

    #[test]
    fn scrnbitmap_writes_current_screen_bitmap_record_to_result_pointer() {
        // Inside Macintosh Volume IV (1986), p. IV-21: ScrnBitMap returns
        // the current screen BitMap record. Inside Macintosh Volume I (1985),
        // p. I-163: screenBits describes the entire active screen.
        let (mut d, mut cpu, mut bus) = setup();
        let screen_base = 0x00AB_C000u32;
        let row_bytes = 160u32;
        let width = 640u16;
        let height = 400u16;
        d.screen_mode = (screen_base, row_bytes, width, height, 1);

        let result_ptr = 0x300000u32;
        bus.fill_zeros(result_ptr, 14);
        bus.write_long(TEST_SP, result_ptr);

        let result = d.dispatch_quickdraw(true, 0x033, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(result_ptr), screen_base);
        assert_eq!(bus.read_word(result_ptr + 4), row_bytes as u16);
        assert_eq!(
            read_rect(&bus, result_ptr + 6),
            (0, 0, height as i16, width as i16)
        );
    }

    #[test]
    fn scrnbitmap_consumes_result_pointer_argument_and_pops_4_bytes() {
        // Inside Macintosh Volume IV (1986), p. IV-21: ScrnBitMap uses
        // a caller-supplied result pointer argument.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0);

        let result = d.dispatch_quickdraw(true, 0x033, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn scrnbitmap_overwrites_pre_poisoned_sentinel_fields_with_screen_bitmap_record() {
        // Mirrors B1 of the a833_scrnbitmap_strict bake: pre-poison
        // every BitMap field with a recognisable sentinel before
        // dispatch, then assert each field is overwritten with a
        // sensible screen-mode value (baseAddr non-NIL and non-
        // sentinel, rowBytes positive and not the sentinel, bounds
        // resolving to (0, 0, height, width)).  This is the cross-
        // engine-agree witness pattern from the strict bake: BII and
        // Systemless disagree on the exact baseAddr/rowBytes values
        // because their screen bases differ, but BOTH overwrite the
        // sentinels and BOTH produce bounds = (0, 0, 600, 800) on the
        // FixtureRunner/InitFixtureScreen 800x600 screen.
        // Inside Macintosh Volume IV (1986), p. IV-21.
        let (mut d, mut cpu, mut bus) = setup();
        let screen_base = 0x00AB_C000u32;
        let row_bytes = 100u32;
        let width = 800u16;
        let height = 600u16;
        d.screen_mode = (screen_base, row_bytes, width, height, 1);

        let result_ptr = 0x300000u32;
        // Pre-poison every field with the same sentinels the strict
        // bake uses, so the test catches any stub that fails to
        // write a particular slot.
        bus.write_long(result_ptr, 0xDEAD_BEEF);
        bus.write_word(result_ptr + 4, 0x3FFF);
        bus.write_word(result_ptr + 6, 0x7FFF);
        bus.write_word(result_ptr + 8, 0x7FFF);
        bus.write_word(result_ptr + 10, 0x7FFE);
        bus.write_word(result_ptr + 12, 0x7FFE);
        bus.write_long(TEST_SP, result_ptr);

        let result = d.dispatch_quickdraw(true, 0x033, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        let base_after = bus.read_long(result_ptr);
        let rb_after = bus.read_word(result_ptr + 4);
        let (top_after, left_after, bottom_after, right_after) = read_rect(&bus, result_ptr + 6);

        assert_ne!(base_after, 0xDEAD_BEEF, "baseAddr sentinel not overwritten");
        assert_ne!(base_after, 0, "baseAddr must be non-NIL after ScrnBitMap");
        assert_ne!(rb_after, 0x3FFF, "rowBytes sentinel not overwritten");
        assert!(rb_after > 0, "rowBytes must be positive");
        assert!(
            rb_after < 4096,
            "rowBytes must be sub-4096 (covers 1..32 bpp at 800 px)"
        );
        assert_eq!(top_after, 0, "bounds.top must be 0");
        assert_eq!(left_after, 0, "bounds.left must be 0");
        assert_eq!(
            bottom_after, height as i16,
            "bounds.bottom must equal height"
        );
        assert_eq!(right_after, width as i16, "bounds.right must equal width");
    }

    #[test]
    fn calcmask_consumes_16_byte_argument_frame() {
        // Inside Macintosh Volume IV (1986), p. IV-22:
        // CalcMask(srcPtr, dstPtr, srcRow, dstRow, height, words).
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 4); // words
        bus.write_word(TEST_SP + 2, 8); // height
        bus.write_word(TEST_SP + 4, 16); // dstRow
        bus.write_word(TEST_SP + 6, 16); // srcRow
        bus.write_long(TEST_SP + 8, 0x300000); // dstPtr
        bus.write_long(TEST_SP + 12, 0x310000); // srcPtr

        let result = d.dispatch_quickdraw(true, 0x038, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);
    }

    #[test]
    fn calcmask_five_call_composition_pops_eighty_bytes_total() {
        // Mirrors B2 of the a838_a839_calcmask_seedfill_strict bake:
        // 5 successive CalcMask dispatches with varying metric args
        // (height, words) each advance A7 by 16 bytes, total 80 bytes.
        // Per IM:IV IV-22 the trap pops a fixed 16 bytes regardless
        // of arg values.
        let (mut d, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);

        let metrics: [(u16, u16, u16, u16); 5] = [
            (4, 4, 8, 2),
            (4, 4, 4, 2),
            (4, 4, 8, 1),
            (4, 4, 2, 2),
            (4, 4, 1, 1),
        ];
        for (i, (src_row, dst_row, height, words)) in metrics.iter().enumerate() {
            let sp = cpu.read_reg(Register::A7);
            bus.write_word(sp, *words);
            bus.write_word(sp + 2, *height);
            bus.write_word(sp + 4, *dst_row);
            bus.write_word(sp + 6, *src_row);
            bus.write_long(sp + 8, 0x300000 + (i as u32) * 0x1000);
            bus.write_long(sp + 12, 0x310000 + (i as u32) * 0x1000);

            let result = d.dispatch_quickdraw(true, 0x038, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(
                cpu.read_reg(Register::A7),
                sp + 16,
                "CalcMask iteration {} must pop exactly 16 bytes",
                i
            );
            cpu.write_reg(Register::A7, sp);
        }

        cpu.write_reg(Register::A7, sp_before + 80);
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_before + 80,
            "5 CalcMask calls aggregate exactly 80 bytes of stack pop"
        );
    }

    #[test]
    fn seedfill_consumes_20_byte_argument_frame() {
        // Inside Macintosh Volume IV (1986), p. IV-22:
        // SeedFill(srcPtr, dstPtr, srcRow, dstRow, height, words, seedH, seedV).
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 5); // seedV
        bus.write_word(TEST_SP + 2, 6); // seedH
        bus.write_word(TEST_SP + 4, 7); // words
        bus.write_word(TEST_SP + 6, 8); // height
        bus.write_word(TEST_SP + 8, 16); // dstRow
        bus.write_word(TEST_SP + 10, 16); // srcRow
        bus.write_long(TEST_SP + 12, 0x320000); // dstPtr
        bus.write_long(TEST_SP + 16, 0x330000); // srcPtr

        let result = d.dispatch_quickdraw(true, 0x039, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 20);
    }

    #[test]
    fn seedfill_writes_contiguous_matching_region_mask() {
        // Imaging With QuickDraw (1994), pp. 3-109..3-110:
        // SeedFill writes a 1-bit mask for the contiguous pixels reachable
        // from the seed point within the requested raw bit-image rectangle.
        let (mut d, mut cpu, mut bus) = setup();
        let src = bus.alloc(10);
        let dst = bus.alloc(10);
        let source_rows = [
            [0xFF, 0xFF],
            [0x87, 0xFF],
            [0x87, 0xFF],
            [0x87, 0xFF],
            [0xFF, 0xFF],
        ];
        for (row, bytes) in source_rows.iter().enumerate() {
            bus.write_byte(src + row as u32 * 2, bytes[0]);
            bus.write_byte(src + row as u32 * 2 + 1, bytes[1]);
            bus.write_byte(dst + row as u32 * 2, 0xAA);
            bus.write_byte(dst + row as u32 * 2 + 1, 0x55);
        }

        bus.write_word(TEST_SP, 2); // seedV
        bus.write_word(TEST_SP + 2, 2); // seedH
        bus.write_word(TEST_SP + 4, 1); // words = 16 pixels
        bus.write_word(TEST_SP + 6, 5); // height
        bus.write_word(TEST_SP + 8, 2); // dstRow
        bus.write_word(TEST_SP + 10, 2); // srcRow
        bus.write_long(TEST_SP + 12, dst);
        bus.write_long(TEST_SP + 16, src);

        let result = d.dispatch_quickdraw(true, 0x039, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 20);

        let expected_rows = [
            [0x00, 0x00],
            [0x78, 0x00],
            [0x78, 0x00],
            [0x78, 0x00],
            [0x00, 0x00],
        ];
        for (row, expected) in expected_rows.iter().enumerate() {
            assert_eq!(
                [
                    bus.read_byte(dst + row as u32 * 2),
                    bus.read_byte(dst + row as u32 * 2 + 1)
                ],
                *expected,
                "destination mask row {row}"
            );
        }
    }

    #[test]
    fn seedfill_seed_outside_rect_clears_destination_mask() {
        let (mut d, mut cpu, mut bus) = setup();
        let src = bus.alloc(4);
        let dst = bus.alloc(4);
        for i in 0..4u32 {
            bus.write_byte(src + i, 0);
            bus.write_byte(dst + i, 0xFF);
        }

        bus.write_word(TEST_SP, 0); // seedV
        bus.write_word(TEST_SP + 2, 99); // seedH outside the 16-pixel rectangle
        bus.write_word(TEST_SP + 4, 1); // words
        bus.write_word(TEST_SP + 6, 2); // height
        bus.write_word(TEST_SP + 8, 2); // dstRow
        bus.write_word(TEST_SP + 10, 2); // srcRow
        bus.write_long(TEST_SP + 12, dst);
        bus.write_long(TEST_SP + 16, src);

        let result = d.dispatch_quickdraw(true, 0x039, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 20);
        assert_eq!(bus.read_bytes(dst, 4), vec![0, 0, 0, 0]);
    }

    #[test]
    fn seedfill_five_call_composition_pops_one_hundred_bytes_total() {
        // Mirrors B4 of the a838_a839_calcmask_seedfill_strict bake:
        // 5 successive SeedFill dispatches with varying seedH/seedV
        // pairs (0,0)/(4,2)/(8,4)/(16,6)/(30,7) each advance A7 by 20
        // bytes, total 100 bytes. Per IM:IV IV-22 the trap pops a
        // fixed 20 bytes regardless of seed coordinate values.
        let (mut d, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);

        let seeds: [(u16, u16); 5] = [(0, 0), (4, 2), (8, 4), (16, 6), (30, 7)];
        for (i, (seed_h, seed_v)) in seeds.iter().enumerate() {
            let sp = cpu.read_reg(Register::A7);
            bus.write_word(sp, *seed_v);
            bus.write_word(sp + 2, *seed_h);
            bus.write_word(sp + 4, 2); // words
            bus.write_word(sp + 6, 8); // height
            bus.write_word(sp + 8, 4); // dstRow
            bus.write_word(sp + 10, 4); // srcRow
            bus.write_long(sp + 12, 0x320000 + (i as u32) * 0x1000);
            bus.write_long(sp + 16, 0x330000 + (i as u32) * 0x1000);

            let result = d.dispatch_quickdraw(true, 0x039, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(
                cpu.read_reg(Register::A7),
                sp + 20,
                "SeedFill iteration {} must pop exactly 20 bytes",
                i
            );
            cpu.write_reg(Register::A7, sp);
        }

        cpu.write_reg(Register::A7, sp_before + 100);
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_before + 100,
            "5 SeedFill calls aggregate exactly 100 bytes of stack pop"
        );
    }

    // ==================== Color Cursor Traps ====================

    #[test]
    fn getccursor_returns_handle_for_present_crsr_resource() {
        // IM:V 1986 p. V-79: GetCCursor returns a CCrsrHandle for a
        // present 'crsr' resource ID and advances A7 by the 2-byte
        // crsrID argument.
        let (mut d, mut cpu, mut bus) = setup();
        let crsr_data: Vec<u8> = (0..96).map(|i| (i as u8).wrapping_mul(7)).collect();
        d.install_test_resource(&mut bus, *b"crsr", 128, &crsr_data);
        bus.write_word(TEST_SP, 128);

        let result = d.dispatch_quickdraw(true, 0x21B, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_ne!(bus.read_long(TEST_SP + 2), 0);
    }

    #[test]
    fn getccursor_promotes_compiled_crsr_offsets_to_live_handles() {
        // Imaging With QuickDraw 1994 p. 8-35: compiled 'crsr'
        // resources store offsets to the PixMap, pixel data, and color
        // table. GetCCursor returns a live CCrsrHandle whose crsrMap and
        // crsrData fields are handles.
        let (mut d, mut cpu, mut bus) = setup();
        let crsr_data = compiled_crsr_resource_2bpp();
        d.install_test_resource(&mut bus, *b"crsr", 1026, &crsr_data);
        bus.write_word(TEST_SP, 1026);

        let result = d.dispatch_quickdraw(true, 0x21B, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        let crsr_handle = bus.read_long(TEST_SP + 2);
        assert_ne!(crsr_handle, 0);
        let crsr_ptr = bus.read_long(crsr_handle);
        assert_ne!(crsr_ptr, 0);

        let crsr_map_handle = bus.read_long(crsr_ptr + 2);
        let crsr_data_handle = bus.read_long(crsr_ptr + 6);
        assert_ne!(
            crsr_map_handle, 0x60,
            "compiled crsrMap offset must be promoted to a live PixMapHandle"
        );
        assert_ne!(
            crsr_data_handle, 0x92,
            "compiled crsrData offset must be promoted to a live pixel-data Handle"
        );
        let pixmap_ptr = bus.read_long(crsr_map_handle);
        let data_ptr = bus.read_long(crsr_data_handle);
        assert_ne!(pixmap_ptr, 0);
        assert_ne!(data_ptr, 0);
        assert_eq!(bus.read_long(pixmap_ptr), data_ptr);
        assert_ne!(
            bus.read_long(pixmap_ptr + 42),
            0xD2,
            "compiled pmTable offset must be promoted to a ColorTable handle"
        );
    }

    #[test]
    fn getccursor_missing_resource_returns_nil() {
        // IM:V 1986 p. V-79: if the specified 'crsr' resource is not
        // found, GetCCursor returns NIL.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 777);

        let result = d.dispatch_quickdraw(true, 0x21B, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(bus.read_long(TEST_SP + 2), 0);
    }

    #[test]
    fn getccursor_pascal_function_preserves_stack_across_five_missing_calls() {
        // Mirrors band B2 of aa1b_getccursor_strict.
        // Per IM:V 1986 p. V-74, each GetCCursor call obeys the Tool-
        // bit Pascal FUNCTION calling convention independently (caller
        // pre-pushes a 4-byte CCrsrHandle result slot + 2-byte crsrID
        // INTEGER, trap pops the 2-byte arg and writes the handle to
        // the result slot at the post-pop SP, caller pops the slot).
        // Across a 5-call composition with five distinct fictional
        // crsrIDs all five returned handles are NIL per the documented
        // miss-returns-NIL contract AND A7 returns to its pre-
        // composition value (5 missed 2-byte pops would cumulate to
        // 10 bytes A7 drift).
        let (mut d, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        let crsr_ids: [i16; 5] = [0x6FF0, 0x6FF1, 0x6FF2, 0x6FF3, 0x6FF4];
        for crsr_id in crsr_ids.iter().copied() {
            let sp = cpu.read_reg(Register::A7);
            let slot_addr = sp.wrapping_sub(6);
            bus.write_long(slot_addr, 0xDEAD_BEEF);
            bus.write_word(slot_addr + 4, crsr_id as u16);
            cpu.write_reg(Register::A7, slot_addr);

            let result = d.dispatch_quickdraw(true, 0x21B, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());

            assert_eq!(cpu.read_reg(Register::A7), slot_addr + 2);
            assert_eq!(bus.read_long(slot_addr + 2), 0);
            cpu.write_reg(Register::A7, slot_addr + 6);
        }
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_before,
            "5-call GetCCursor composition with distinct missing crsrIDs is net A7-balanced"
        );
    }

    #[test]
    fn setccursor_consumes_ccrsrhandle_pointer_argument() {
        // IM:V 1986 p. V-80: SetCCursor is a procedure with one
        // CCrsrHandle argument and no function result.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x1234_5678);

        let result = d.dispatch_quickdraw(true, 0x21C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn setccursor_updates_cursor_state_from_ccrsr_handle() {
        // IM:V 1986 p. V-80: SetCCursor installs the supplied color cursor
        // as the current cursor. Systemless HLE keeps the current cursor image
        // in dispatcher state so the host can render it.
        let (mut d, mut cpu, mut bus) = setup();
        let crsr_ptr = bus.alloc(96);
        let crsr_handle = bus.alloc(4);

        bus.write_long(crsr_handle, crsr_ptr);
        bus.write_word(crsr_ptr, 0x8001);
        bus.write_word(crsr_ptr + 14, 7);
        for i in 0..16u32 {
            bus.write_word(crsr_ptr + 20 + i * 2, 0x1100 | (i as u16));
            bus.write_word(crsr_ptr + 52 + i * 2, 0x2200 | (i as u16));
        }
        bus.write_word(crsr_ptr + 84, 13);
        bus.write_word(crsr_ptr + 86, -9i16 as u16);

        d.cursor_level = -1;
        d.cursor_visible = false;
        bus.write_long(TEST_SP, crsr_handle);

        let result = d.dispatch_quickdraw(true, 0x21C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(
            d.cursor_data(),
            Some((
                {
                    let mut data = [0u8; 32];
                    for i in 0..16u32 {
                        let v = 0x1100 | (i as u16);
                        data[(i as usize) * 2] = (v >> 8) as u8;
                        data[(i as usize) * 2 + 1] = (v & 0xFF) as u8;
                    }
                    data
                },
                {
                    let mut mask = [0u8; 32];
                    for i in 0..16u32 {
                        let v = 0x2200 | (i as u16);
                        mask[(i as usize) * 2] = (v >> 8) as u8;
                        mask[(i as usize) * 2 + 1] = (v & 0xFF) as u8;
                    }
                    mask
                },
                13,
                -9,
            ))
        );
        assert!(!d.cursor_visible());
    }

    #[test]
    fn setccursor_installs_color_pixels_from_promoted_ccrsr() {
        // Imaging With QuickDraw 1994 p. 8-5: on screens deeper than
        // 2bpp, a color cursor's image comes from crsrMap + crsrData,
        // with the same mask used by the one-bit fallback.
        let (mut d, mut cpu, mut bus) = setup();
        let crsr_data = compiled_crsr_resource_2bpp();
        d.install_test_resource(&mut bus, *b"crsr", 1026, &crsr_data);
        bus.write_word(TEST_SP, 1026);
        assert!(d
            .dispatch_quickdraw(true, 0x21B, &mut cpu, &mut bus)
            .unwrap()
            .is_ok());
        let crsr_handle = bus.read_long(TEST_SP + 2);

        let sp = cpu.read_reg(Register::A7);
        bus.write_long(sp, crsr_handle);
        assert!(d
            .dispatch_quickdraw(true, 0x21C, &mut cpu, &mut bus)
            .unwrap()
            .is_ok());

        let Some(CursorImage::Color {
            width,
            height,
            pixels_argb,
            hot_v,
            hot_h,
            ..
        }) = d.cursor()
        else {
            panic!("SetCCursor should install decoded color cursor pixels");
        };
        assert_eq!((*width, *height), (16, 16));
        assert_eq!((*hot_v, *hot_h), (15, 7));
        assert_eq!(pixels_argb[0], 0xFFFF_FFFF);
        assert_eq!(pixels_argb[1], 0xFFFF_0000);
        assert_eq!(pixels_argb[2], 0xFF00_FF00);
        assert_eq!(pixels_argb[3], 0xFF00_0000);
    }

    #[test]
    fn disposeccursor_consumes_ccrsrhandle_pointer_argument() {
        // IM:V 1986 p. V-80: DisposCCursor is a procedure with one
        // CCrsrHandle argument and no function result.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x90AB_CDEF);

        let result = d.dispatch_quickdraw(true, 0x226, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn disposeccursor_returns_noerr_and_consumes_ccrsrhandle_pointer_argument() {
        // IM:V 1986 p. V-80: DisposCCursor is a procedure with one
        // CCrsrHandle argument and no function result; Systemless pins
        // the no-op path by clearing D0 to noErr.
        let (mut d, mut cpu, mut bus) = setup();
        cpu.write_reg(Register::D0, 0x1234_5678);
        bus.write_long(TEST_SP, 0);

        let result = d.dispatch_quickdraw(true, 0x226, &mut cpu, &mut bus);
        assert!(result.is_some(), "DisposeCCursor should be handled");
        assert!(result.unwrap().is_ok(), "DisposeCCursor should return");
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP + 4,
            "DisposeCCursor should pop one CCrsrHandle argument"
        );
        assert_eq!(
            cpu.read_reg(Register::D0),
            0,
            "DisposeCCursor should return noErr in D0"
        );
    }

    // ==================== QuickDraw Initialization ====================

    #[test]
    fn initgraf_stores_globalptr_in_a5_base_slot() {
        // Inside Macintosh Volume I (1985), p. I-162: assembly callers pass
        // a pointer to thePort in globalPtr, and InitGraf stores it at (A5).
        let (mut d, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc(128 * 64);
        d.screen_mode = (screen_base, 128, 64, 32, 1);
        let global_ptr = 0x190000u32;
        bus.write_long(TEST_SP, global_ptr);

        let result = d.dispatch_quickdraw(true, 0x06E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        let a5 = cpu.read_reg(Register::A5);
        assert_eq!(bus.read_long(a5), global_ptr);
    }

    #[test]
    fn initgraf_initializes_standard_pattern_globals() {
        // Inside Macintosh Volume I (1985), p. I-162: InitGraf initializes
        // the standard white, black, and gray QuickDraw patterns.
        let (mut d, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc(128 * 64);
        d.screen_mode = (screen_base, 128, 64, 32, 1);
        let global_ptr = 0x191000u32;
        bus.write_long(TEST_SP, global_ptr);

        let result = d.dispatch_quickdraw(true, 0x06E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        for i in 0..8u32 {
            assert_eq!(bus.read_byte(global_ptr.wrapping_sub(8) + i), 0x00);
            assert_eq!(bus.read_byte(global_ptr.wrapping_sub(16) + i), 0xFF);
        }
        assert_eq!(
            bus.read_bytes(global_ptr.wrapping_sub(24), 8),
            vec![0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55]
        );
    }

    #[test]
    fn initgraf_sets_randseed_to_one() {
        // Inside Macintosh Volume I (1985), p. I-162: InitGraf sets randSeed
        // to an initial value of 1.
        let (mut d, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc(160 * 80);
        d.screen_mode = (screen_base, 160, 80, 40, 1);
        let global_ptr = 0x192000u32;
        bus.write_long(TEST_SP, global_ptr);

        let result = d.dispatch_quickdraw(true, 0x06E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_long(global_ptr.wrapping_sub(126)), 1);
    }

    #[test]
    fn initgraf_initializes_screenbits_from_active_screen() {
        // Inside Macintosh Volume I (1985), p. I-162: screenBits is initialized
        // to describe the entire active startup screen.
        let (mut d, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc(200 * 120);
        d.screen_mode = (screen_base, 200, 101, 77, 1);
        let global_ptr = 0x193000u32;
        bus.write_long(TEST_SP, global_ptr);

        let result = d.dispatch_quickdraw(true, 0x06E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        let screen_bits = global_ptr.wrapping_sub(122);
        assert_eq!(bus.read_long(screen_bits), screen_base);
        assert_eq!(bus.read_word(screen_bits + 4), 200);
        assert_eq!(read_rect(&bus, screen_bits + 6), (0, 0, 77, 101));
    }

    #[test]
    fn test_open_port() {
        let (mut d, mut cpu, mut bus) = setup();
        let a5 = cpu.read_reg(Register::A5);
        let global_ptr = bus.read_long(a5);
        let screen_bits_addr = global_ptr.wrapping_sub(122);
        // IM:I I-163: visRgn starts coincident with screenBits.bounds.
        bus.write_word(screen_bits_addr + 6, 11); // top
        bus.write_word(screen_bits_addr + 8, 17); // left
        bus.write_word(screen_bits_addr + 10, 211); // bottom
        bus.write_word(screen_bits_addr + 12, 317); // right

        let port_ptr = 0x300000u32;
        bus.write_long(TEST_SP, port_ptr);
        let result = d.dispatch_quickdraw(true, 0x06F, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        // visRgn and clipRgn handles should be non-zero
        let vis_rgn = bus.read_long(port_ptr + 24);
        let clip_rgn = bus.read_long(port_ptr + 28);
        assert_ne!(vis_rgn, 0);
        assert_ne!(clip_rgn, 0);
        assert_eq!(read_rgn_bbox(&bus, vis_rgn), (11, 17, 211, 317));
        // IM:I I-163: default clipRgn is infinite.
        assert_eq!(
            read_rgn_bbox(&bus, clip_rgn),
            (-32767, -32767, 32767, 32767)
        );
        // Pen defaults
        assert_eq!(d.pn_size, (1, 1));
        assert_eq!(d.pn_mode, 8);
        assert_eq!(d.pn_pat, [0xFF; 8]);
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::THE_PORT),
            port_ptr
        );
        assert_eq!(d.current_port, port_ptr);
        assert_ne!(d.current_gdevice, 0);
    }

    #[test]
    fn test_init_port() {
        let (mut d, mut cpu, mut bus) = setup();
        let a5 = cpu.read_reg(Register::A5);
        let global_ptr = bus.read_long(a5);
        let screen_bits_addr = global_ptr.wrapping_sub(122);
        bus.write_word(screen_bits_addr + 6, 4); // top
        bus.write_word(screen_bits_addr + 8, 9); // left
        bus.write_word(screen_bits_addr + 10, 104); // bottom
        bus.write_word(screen_bits_addr + 12, 209); // right

        let port_ptr = 0x300000u32;
        let vis_ptr = 0x310000u32;
        let vis_handle = 0x310100u32;
        make_rgn(&mut bus, vis_ptr, vis_handle, 1, 2, 3, 4);
        let clip_ptr = 0x310200u32;
        let clip_handle = 0x310300u32;
        make_rgn(&mut bus, clip_ptr, clip_handle, 5, 6, 7, 8);
        bus.write_long(port_ptr + 24, vis_handle);
        bus.write_long(port_ptr + 28, clip_handle);
        // Dirty fields that InitPort should reset.
        bus.write_word(port_ptr + 68, 9);
        bus.write_word(port_ptr + 70, 9);
        bus.write_word(port_ptr + 72, 9);
        bus.write_word(port_ptr + 74, 9);
        bus.write_long(port_ptr + 76, 0x00ABCDEF);

        bus.write_long(TEST_SP, port_ptr);
        let result = d.dispatch_quickdraw(true, 0x06D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        // IM:I I-164: InitPort reuses visRgn/clipRgn handles.
        assert_eq!(bus.read_long(port_ptr + 24), vis_handle);
        assert_eq!(bus.read_long(port_ptr + 28), clip_handle);
        assert_eq!(read_rgn_bbox(&bus, vis_handle), (4, 9, 104, 209));
        assert_eq!(
            read_rgn_bbox(&bus, clip_handle),
            (-32767, -32767, 32767, 32767)
        );
        assert_eq!(bus.read_word(port_ptr + 68), 0);
        assert_eq!(bus.read_word(port_ptr + 70), 0);
        assert_eq!(bus.read_word(port_ptr + 72), 1);
        assert_eq!(bus.read_word(port_ptr + 74), 0);
        assert_eq!(bus.read_long(port_ptr + 76), 0);
        // Port should be set as current port
        assert_eq!(bus.read_long(global_ptr), port_ptr);
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::THE_PORT),
            port_ptr
        );
        assert_eq!(d.current_port, port_ptr);
        assert_ne!(d.current_gdevice, 0);
    }

    #[test]
    fn grafdevice_sets_current_port_device_field() {
        // Inside Macintosh Volume I (1985), p. I-165:
        // GrafDevice writes the device field of the current grafPort.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        d.current_port = port_ptr;
        bus.write_word(port_ptr, 0x0000);
        bus.write_word(TEST_SP, 0x1234);

        let result = d.dispatch_quickdraw(true, 0x072, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(port_ptr), 0x1234);
    }

    #[test]
    fn grafdevice_consumes_device_argument() {
        // Inside Macintosh Volume I (1985), p. I-165:
        // GrafDevice(device: INTEGER) consumes one INTEGER argument.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        d.current_port = 0x181000;
        bus.write_word(TEST_SP, 0xABCD);

        let result = d.dispatch_quickdraw(true, 0x072, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
    }

    #[test]
    fn setportbits_copies_bitmap_record_into_current_portbits() {
        // Inside Macintosh Volume I (1985), p. I-165:
        // SetPortBits sets the current grafPort portBits field to bm.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        let bits_ptr = 0x260000u32;
        write_bitmap_record(&mut bus, bits_ptr, 0x00AB_C000, 160, 11, 22, 111, 222);
        bus.write_long(TEST_SP, bits_ptr);

        let result = d.dispatch_quickdraw(true, 0x075, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        for i in 0..14u32 {
            assert_eq!(
                bus.read_byte(port_ptr + 2 + i),
                bus.read_byte(bits_ptr + i),
                "portBits byte {} should match source BitMap record",
                i
            );
        }
    }

    #[test]
    fn setport_ignores_invalid_proc_pointer_without_syncing_draw_state() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let original_port = d.current_port;
        let invalid_port = 0x0001_6178u32;
        let watched = invalid_port + 56;

        // Shape this like a loaded jump-table/proc pointer, not a GrafPort.
        bus.write_word(invalid_port, 0x4EF9);
        bus.write_long(invalid_port + 2, 0x0026_B0B0);
        bus.write_long(watched, 0xDEAD_BEEF);

        bus.write_long(TEST_SP, invalid_port);
        let result = d.dispatch_quickdraw(true, 0x073, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(d.current_port, original_port);
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::THE_PORT),
            original_port
        );
        assert_eq!(
            bus.read_long(watched),
            0xDEAD_BEEF,
            "SetPort must not synthesize pen state into non-port memory"
        );
    }

    #[test]
    fn setportbits_consumes_bitmap_argument_and_preserves_portrect() {
        // Inside Macintosh Volume I (1985), p. I-165:
        // SetPortBits changes portBits, not portRect.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        let bits_ptr = 0x260100u32;
        write_bitmap_record(&mut bus, bits_ptr, 0x0012_3400, 64, 3, 4, 53, 84);
        let before = read_rect(&bus, port_ptr + 16);
        bus.write_long(TEST_SP, bits_ptr);

        let result = d.dispatch_quickdraw(true, 0x075, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(read_rect(&bus, port_ptr + 16), before);
    }

    #[test]
    fn moveportto_shifts_portbits_bounds_so_portrect_topleft_minus_bits_topleft_equals_args() {
        // Inside Macintosh Volume I (1985), p. I-166:
        // "leftGlobal and topGlobal parameters set the distance between
        //  the top left corner of portBits.bounds and the top left corner
        //  of the new portRect."
        // BasiliskII-witnessed semantic: portRect stays in place, and
        // portBits.bounds.topLeft becomes portRect.topLeft - (leftGlobal,
        // topGlobal). The bitmap's local coords shift so the same screen
        // pixels are now described by a different bits.bounds.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;

        bus.write_word(port_ptr + 8, 100); // portBits.bounds.top
        bus.write_word(port_ptr + 10, 200); // portBits.bounds.left
        bus.write_word(port_ptr + 12, 220); // portBits.bounds.bottom (120 tall)
        bus.write_word(port_ptr + 14, 360); // portBits.bounds.right (160 wide)
        write_rect(&mut bus, port_ptr + 16, 100, 200, 220, 360);

        bus.write_word(TEST_SP, 30); // topGlobal
        bus.write_word(TEST_SP + 2, 50); // leftGlobal
        let result = d.dispatch_quickdraw(true, 0x077, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        // portBits.bounds.topLeft = portRect.topLeft - (leftGlobal, topGlobal).
        let (b_top, b_left, b_bottom, b_right) = read_rect(&bus, port_ptr + 8);
        assert_eq!(b_top, 100 - 30); // 70
        assert_eq!(b_left, 200 - 50); // 150
                                      // Bitmap dimensions preserved.
        assert_eq!(b_bottom - b_top, 120);
        assert_eq!(b_right - b_left, 160);
    }

    #[test]
    fn moveportto_preserves_portrect_clip_and_vis_regions() {
        // Inside Macintosh Volume I (1985), p. I-166:
        // "MovePortTo doesn't change the clipRgn or the visRgn, nor does
        //  it affect the local coordinate system of the grafPort."
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;

        bus.write_word(port_ptr + 8, 10); // portBits.bounds.top
        bus.write_word(port_ptr + 10, 20); // portBits.bounds.left
        bus.write_word(port_ptr + 12, 110); // portBits.bounds.bottom
        bus.write_word(port_ptr + 14, 170); // portBits.bounds.right
        write_rect(&mut bus, port_ptr + 16, 70, 90, 170, 240); // 100x150

        let vis_handle = bus.read_long(port_ptr + 24);
        let clip_handle = bus.read_long(port_ptr + 28);
        let vis_ptr = bus.read_long(vis_handle);
        let clip_ptr = bus.read_long(clip_handle);
        write_rect(&mut bus, vis_ptr + 2, 1, 2, 101, 202);
        write_rect(&mut bus, clip_ptr + 2, 3, 4, 103, 204);

        let port_rect_before = read_rect(&bus, port_ptr + 16);
        let vis_before = read_rgn_bbox(&bus, vis_handle);
        let clip_before = read_rgn_bbox(&bus, clip_handle);
        bus.write_word(TEST_SP, 8); // topGlobal
        bus.write_word(TEST_SP + 2, 6); // leftGlobal

        let result = d.dispatch_quickdraw(true, 0x077, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        // portRect is byte-identical (local coord system unchanged).
        assert_eq!(read_rect(&bus, port_ptr + 16), port_rect_before);
        // clipRgn and visRgn bboxes unchanged.
        assert_eq!(read_rgn_bbox(&bus, vis_handle), vis_before);
        assert_eq!(read_rgn_bbox(&bus, clip_handle), clip_before);
    }

    #[test]
    fn moveportto_preserves_portbits_bounds_width_and_height() {
        // Bitmap dimensions are preserved across the bits.bounds shift.
        // Only the topLeft moves; bottomRight tracks by the same delta.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;

        bus.write_word(port_ptr + 8, 50); // portBits.bounds.top
        bus.write_word(port_ptr + 10, 80); // portBits.bounds.left
        bus.write_word(port_ptr + 12, 170); // portBits.bounds.bottom (120 tall)
        bus.write_word(port_ptr + 14, 240); // portBits.bounds.right (160 wide)
        write_rect(&mut bus, port_ptr + 16, 50, 80, 170, 240);

        bus.write_word(TEST_SP, 25); // topGlobal
        bus.write_word(TEST_SP + 2, 15); // leftGlobal

        let result = d.dispatch_quickdraw(true, 0x077, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        let (b_top, b_left, b_bottom, b_right) = read_rect(&bus, port_ptr + 8);
        assert_eq!(b_bottom - b_top, 120);
        assert_eq!(b_right - b_left, 160);
        // bits.bounds.topLeft = portRect.topLeft - (left, top) = (50-25, 80-15) = (25, 65).
        assert_eq!(b_top, 25);
        assert_eq!(b_left, 65);
    }

    #[test]
    fn moveportto_with_negative_offsets_shifts_bits_bounds_oppositely() {
        // IM:I I-166: leftGlobal/topGlobal are signed INTEGER offsets.
        // For negative args, bits.bounds.topLeft shifts up/right of
        // portRect.topLeft (since topLeft - (-N) = topLeft + N).
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;

        bus.write_word(port_ptr + 8, 100); // portBits.bounds.top
        bus.write_word(port_ptr + 10, 200); // portBits.bounds.left
        bus.write_word(port_ptr + 12, 180); // bottom (80 tall)
        bus.write_word(port_ptr + 14, 350); // right (150 wide)
        write_rect(&mut bus, port_ptr + 16, 100, 200, 180, 350);

        bus.write_word(TEST_SP, (-25i16) as u16); // topGlobal = -25
        bus.write_word(TEST_SP + 2, (-50i16) as u16); // leftGlobal = -50

        let result = d.dispatch_quickdraw(true, 0x077, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        // bits.bounds.top = 100 - (-25) = 125; .left = 200 - (-50) = 250.
        let (b_top, b_left, b_bottom, b_right) = read_rect(&bus, port_ptr + 8);
        assert_eq!(b_top, 125);
        assert_eq!(b_left, 250);
        assert_eq!(b_bottom - b_top, 80);
        assert_eq!(b_right - b_left, 150);
    }

    #[test]
    fn moveportto_consumes_leftglobal_topglobal_arguments() {
        // Inside Macintosh Volume I (1985), p. I-166:
        // MovePortTo(leftGlobal, topGlobal: INTEGER) consumes 4 stack bytes.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        bus.write_word(TEST_SP, 9);
        bus.write_word(TEST_SP + 2, 7);

        let result = d.dispatch_quickdraw(true, 0x077, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn shield_cursor_pops_eight_bytes_and_preserves_cursor_state_in_hle() {
        // IM:I I-474 signature: ShieldCursor(shieldRect: Rect; offsetPt: Point).
        // HLE contract: consume arguments but leave cursor level/visibility
        // unchanged (host-rendered cursor, no guest-side intersection model).
        let (mut d, mut cpu, mut bus) = setup();
        let shield_rect_ptr = 0x340000u32;
        write_rect(&mut bus, shield_rect_ptr, 100, 120, 200, 220);
        bus.write_word(TEST_SP, 12); // offsetPt.v
        bus.write_word(TEST_SP + 2, 34); // offsetPt.h
        bus.write_long(TEST_SP + 4, shield_rect_ptr);

        d.cursor_level = -2;
        d.cursor_visible = false;

        let result = d.dispatch_quickdraw(true, 0x055, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(d.cursor_level, -2);
        assert!(!d.cursor_visible);
    }

    #[test]
    fn shield_cursor_five_call_composition_pops_forty_bytes_total() {
        // Mirrors B4 of the a855_a856_shieldcursor_obscurecursor_strict
        // bake: 5 successive ShieldCursor dispatches with varying Rect
        // pointers and Point coords each advance A7 by 8 bytes, total
        // 40 bytes. Per IM:I I-474 the MPW Universal Headers C form
        // `ShieldCursor(const Rect *shieldRect, Point offsetPt)
        // ONEWORDINLINE(0xA855)` pushes 4-byte Rect ptr + 4-byte Point
        // by value = 8-byte frame per call regardless of arg values.
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptrs = [0x340000u32, 0x340010, 0x340020, 0x340030, 0x340040];
        let coords: [(i16, i16); 5] = [(10, 20), (-5, -7), (0, 0), (100, 200), (-100, -200)];

        for (i, &rect_ptr) in rect_ptrs.iter().enumerate() {
            write_rect(&mut bus, rect_ptr, 0, 0, 50, 50);
            let (v, h) = coords[i];
            cpu.write_reg(Register::A7, TEST_SP);
            bus.write_word(TEST_SP, v as u16);
            bus.write_word(TEST_SP + 2, h as u16);
            bus.write_long(TEST_SP + 4, rect_ptr);

            let result = d.dispatch_quickdraw(true, 0x055, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(
                cpu.read_reg(Register::A7),
                TEST_SP + 8,
                "iteration {i}: ShieldCursor must pop exactly 8 bytes regardless of arg values",
            );
        }
    }

    #[test]
    fn test_get_ct_seed_returns_unique_values() {
        let (mut d, mut cpu, mut bus) = setup();

        let result = d.dispatch_quickdraw(true, 0x228, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let first = bus.read_long(TEST_SP);

        let result = d.dispatch_quickdraw(true, 0x228, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let second = bus.read_long(TEST_SP);

        assert_ne!(first, 0);
        assert_eq!(second, first + 1);
    }

    #[test]
    fn test_set_entries_reseeds_gdevice_color_table() {
        let (mut d, _cpu, mut bus) = setup();
        d.ensure_main_gdevice(&mut bus);
        let ctab_handle = d.current_gdevice_ctab_handle(&bus);
        let ctab_ptr = bus.read_long(ctab_handle);
        let initial_seed = bus.read_long(ctab_ptr);

        let table_ptr = bus.alloc(8);
        bus.write_word(table_ptr, 1); // value
        bus.write_word(table_ptr + 2, 0x1111);
        bus.write_word(table_ptr + 4, 0x2222);
        bus.write_word(table_ptr + 6, 0x3333);

        d.apply_set_entries(&mut bus, table_ptr, -1, 0);

        let reseeded = bus.read_long(ctab_ptr);
        let entry = ctab_ptr + 8 + 8; // index 1
        assert_eq!(bus.read_word(entry + 2), 0x1111);
        assert_eq!(bus.read_word(entry + 4), 0x2222);
        assert_eq!(bus.read_word(entry + 6), 0x3333);
        assert_ne!(reseeded, initial_seed);
    }

    #[test]
    fn test_read_port_clut_uses_guest_table_for_non_screen_ctab() {
        let (d, _cpu, mut bus) = setup();
        let ctab_ptr = bus.alloc(8 + 256 * 8);
        bus.write_long(ctab_ptr, 0x12345678);
        bus.write_word(ctab_ptr + 4, 0);
        bus.write_word(ctab_ptr + 6, 255);
        let entry = ctab_ptr + 8 + 5 * 8;
        bus.write_word(entry, 5);
        bus.write_word(entry + 2, 0x1111);
        bus.write_word(entry + 4, 0x2222);
        bus.write_word(entry + 6, 0x3333);
        let ctab_handle = bus.alloc(4);
        bus.write_long(ctab_handle, ctab_ptr);

        let clut = d.read_port_clut(&bus, ctab_handle);

        assert_eq!(clut[5], [0x1111, 0x2222, 0x3333]);
    }

    #[test]
    fn test_copy_bits_destination_ctab_uses_live_device_clut_for_screen_blit() {
        let (mut d, _cpu, mut bus) = setup();
        d.ensure_main_gdevice(&mut bus);
        d.screen_mode = (0x00ABC000, 800, 800, 600, 8);

        let dst_info = CopyBitmapInfo {
            base: 0x00ABC000,
            row_bytes: 800,
            bounds_top: 0,
            bounds_left: 0,
            bounds_bottom: 600,
            bounds_right: 800,
            pixel_size: 8,
            ctab_handle: 0x12345678,
        };

        let ctab_handle = d.copy_bits_destination_ctab_handle(&bus, 0, &dst_info);

        assert_eq!(ctab_handle, 0);
    }

    #[test]
    fn test_black_and_white_map_to_clut_endpoints() {
        let clut = TrapDispatcher::standard_mac_8bpp_clut();

        assert_eq!(TrapDispatcher::nearest_palette_index(&clut, [0, 0, 0]), 255);
        assert_eq!(
            TrapDispatcher::nearest_palette_index(&clut, [0xFFFF, 0xFFFF, 0xFFFF]),
            0
        );
        assert_eq!(crate::trap::pict::closest_clut_index(0, 0, 0, &clut), 255);
        assert_eq!(
            crate::trap::pict::closest_clut_index(0xFFFF, 0xFFFF, 0xFFFF, &clut),
            0
        );
    }

    #[test]
    fn test_black_prefers_index_255_when_fade_collapses_entry_0_to_black() {
        let mut clut = TrapDispatcher::standard_mac_8bpp_clut();
        clut[0] = [0, 0, 0];

        assert_eq!(TrapDispatcher::nearest_palette_index(&clut, [0, 0, 0]), 255);
        assert_eq!(crate::trap::pict::closest_clut_index(0, 0, 0, &clut), 255);
    }

    #[test]
    fn test_grayscale_pict_colors_prefer_grayscale_dest_entries() {
        let clut = TrapDispatcher::standard_mac_8bpp_clut();
        let idx = crate::trap::pict::closest_clut_index(0x7777, 0x7777, 0x7777, &clut);
        let [r, g, b] = clut[idx as usize];
        assert_eq!(r, g);
        assert_eq!(g, b);
    }

    #[test]
    fn test_screen_backed_shape_drawing_uses_live_device_clut() {
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd_ptr = bus.read_long(gdh);
        let pixmap_handle = bus.read_long(gd_ptr + 22);
        let pixmap_ptr = bus.read_long(pixmap_handle);
        let screen_base = bus.read_long(pixmap_ptr);
        let screen_row_bytes = (bus.read_word(pixmap_ptr + 4) & 0x3FFF) as u32;
        d.screen_mode = (screen_base, screen_row_bytes, 800, 600, 8);

        let stale_ctab_handle = bus.read_long(pixmap_ptr + 42);
        let stale_ctab_ptr = bus.read_long(stale_ctab_handle);
        let stale_index_7 = stale_ctab_ptr + 8 + 7 * 8;
        let stale_index_9 = stale_ctab_ptr + 8 + 9 * 8;

        d.device_clut[7] = [0x1234, 0x5678, 0x9ABC];
        d.device_clut[9] = [0xEEEE, 0xDDDD, 0xCCCC];
        bus.write_word(stale_index_7 + 2, 0xEEEE);
        bus.write_word(stale_index_7 + 4, 0xDDDD);
        bus.write_word(stale_index_7 + 6, 0xCCCC);
        bus.write_word(stale_index_9 + 2, 0x1234);
        bus.write_word(stale_index_9 + 4, 0x5678);
        bus.write_word(stale_index_9 + 6, 0x9ABC);

        let port = bus.alloc(64);
        bus.write_long(port + 2, pixmap_handle);
        bus.write_word(port + 6, 0xC000);
        bus.write_word(port + 16, 0);
        bus.write_word(port + 18, 0);
        bus.write_word(port + 20, 600);
        bus.write_word(port + 22, 800);
        make_rgn(&mut bus, 0x310000, 0x310100, 0, 0, 600, 800);
        make_rgn(&mut bus, 0x310200, 0x310300, 0, 0, 600, 800);
        bus.write_long(port + 24, 0x310100);
        bus.write_long(port + 28, 0x310300);

        d.set_current_port_state(&mut bus, &mut cpu, port, Some(gdh));
        d.fg_color = (0x1234, 0x5678, 0x9ABC);
        d.bg_color = (0, 0, 0);
        d.pn_mode = 0;
        d.pn_pat = [0xFF; 8];

        let rect = Rect {
            top: 0,
            left: 0,
            bottom: 1,
            right: 1,
        };
        d.draw_rect(&mut cpu, &mut bus, &rect, ShapeOp::Paint);

        assert_eq!(bus.read_byte(screen_base), 7);
    }

    #[test]
    fn screen_backed_erase_uses_live_clut_white_entry_when_index_zero_is_black() {
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd_ptr = bus.read_long(gdh);
        let pixmap_handle = bus.read_long(gd_ptr + 22);
        let pixmap_ptr = bus.read_long(pixmap_handle);
        let screen_base = bus.read_long(pixmap_ptr);
        let screen_row_bytes = (bus.read_word(pixmap_ptr + 4) & 0x3FFF) as u32;
        d.screen_mode = (screen_base, screen_row_bytes, 800, 600, 8);

        d.device_clut = [[0, 0, 0]; 256];
        d.device_clut[1] = [0xFFFF, 0xFFFF, 0xFFFF];
        d.device_clut[255] = [0, 0, 0];

        let port = bus.alloc(64);
        bus.write_long(port + 2, pixmap_handle);
        bus.write_word(port + 6, 0xC000);
        bus.write_word(port + 16, 0);
        bus.write_word(port + 18, 0);
        bus.write_word(port + 20, 600);
        bus.write_word(port + 22, 800);
        make_rgn(&mut bus, 0x330000, 0x330100, 0, 0, 600, 800);
        make_rgn(&mut bus, 0x330200, 0x330300, 0, 0, 600, 800);
        bus.write_long(port + 24, 0x330100);
        bus.write_long(port + 28, 0x330300);

        d.set_current_port_state(&mut bus, &mut cpu, port, Some(gdh));
        d.bg_color = (0xFFFF, 0xFFFF, 0xFFFF);
        d.bk_pat = [0x00; 8];
        bus.write_byte(screen_base, 255);

        let rect = Rect {
            top: 0,
            left: 0,
            bottom: 1,
            right: 1,
        };
        d.draw_rect(&mut cpu, &mut bus, &rect, ShapeOp::Erase);

        assert_eq!(
            bus.read_byte(screen_base),
            1,
            "white erase must follow the live CLUT instead of hard-coding index 0"
        );
    }

    #[test]
    fn screen_backed_black_shape_drawing_prefers_255_when_entry_255_is_black() {
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd_ptr = bus.read_long(gdh);
        let pixmap_handle = bus.read_long(gd_ptr + 22);
        let pixmap_ptr = bus.read_long(pixmap_handle);
        let screen_base = bus.read_long(pixmap_ptr);
        let screen_row_bytes = (bus.read_word(pixmap_ptr + 4) & 0x3FFF) as u32;
        d.screen_mode = (screen_base, screen_row_bytes, 800, 600, 8);

        d.device_clut = [[0xFFFF, 0xFFFF, 0xFFFF]; 256];
        d.device_clut[71] = [0, 0, 0];
        d.device_clut[255] = [0, 0, 0];

        let port = bus.alloc(64);
        bus.write_long(port + 2, pixmap_handle);
        bus.write_word(port + 6, 0xC000);
        bus.write_word(port + 16, 0);
        bus.write_word(port + 18, 0);
        bus.write_word(port + 20, 600);
        bus.write_word(port + 22, 800);
        make_rgn(&mut bus, 0x320000, 0x320100, 0, 0, 600, 800);
        make_rgn(&mut bus, 0x320200, 0x320300, 0, 0, 600, 800);
        bus.write_long(port + 24, 0x320100);
        bus.write_long(port + 28, 0x320300);

        d.set_current_port_state(&mut bus, &mut cpu, port, Some(gdh));
        d.fg_color = (0, 0, 0);
        d.pn_mode = 0;
        d.pn_pat = [0xFF; 8];

        let rect = Rect {
            top: 0,
            left: 0,
            bottom: 1,
            right: 1,
        };
        d.draw_rect(&mut cpu, &mut bus, &rect, ShapeOp::Paint);

        assert_eq!(bus.read_byte(screen_base), 255);
    }

    #[test]
    fn screen_backed_black_shape_drawing_uses_live_clut_black_entry_when_255_is_not_black() {
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd_ptr = bus.read_long(gdh);
        let pixmap_handle = bus.read_long(gd_ptr + 22);
        let pixmap_ptr = bus.read_long(pixmap_handle);
        let screen_base = bus.read_long(pixmap_ptr);
        let screen_row_bytes = (bus.read_word(pixmap_ptr + 4) & 0x3FFF) as u32;
        d.screen_mode = (screen_base, screen_row_bytes, 800, 600, 8);

        d.device_clut = TrapDispatcher::standard_mac_8bpp_clut();
        d.device_clut[1] = [0, 0, 0];
        d.device_clut[255] = [0xFFFF, 0xFFFF, 0xCCCC];

        let port = bus.alloc(64);
        bus.write_long(port + 2, pixmap_handle);
        bus.write_word(port + 6, 0xC000);
        bus.write_word(port + 16, 0);
        bus.write_word(port + 18, 0);
        bus.write_word(port + 20, 600);
        bus.write_word(port + 22, 800);
        make_rgn(&mut bus, 0x350000, 0x350100, 0, 0, 600, 800);
        make_rgn(&mut bus, 0x350200, 0x350300, 0, 0, 600, 800);
        bus.write_long(port + 24, 0x350100);
        bus.write_long(port + 28, 0x350300);

        d.set_current_port_state(&mut bus, &mut cpu, port, Some(gdh));
        d.fg_color = (0, 0, 0);
        d.pn_mode = 0;
        d.pn_pat = [0xFF; 8];

        let rect = Rect {
            top: 0,
            left: 0,
            bottom: 1,
            right: 1,
        };
        d.draw_rect(&mut cpu, &mut bus, &rect, ShapeOp::Paint);

        assert_eq!(
            bus.read_byte(screen_base),
            1,
            "RGB black must follow the live CLUT when index 255 is not black"
        );
    }

    #[test]
    fn current_window_shape_drawing_uses_current_gdevice_ctab_not_stale_pixmap_ctab() {
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd_ptr = bus.read_long(gdh);
        let gd_pixmap_handle = bus.read_long(gd_ptr + 22);
        let gd_pixmap_ptr = bus.read_long(gd_pixmap_handle);
        let gd_ctab_handle = bus.read_long(gd_pixmap_ptr + 42);
        let gd_ctab_ptr = bus.read_long(gd_ctab_handle);

        let gd_black = gd_ctab_ptr + 8 + 8;
        bus.write_word(gd_black, 1);
        bus.write_word(gd_black + 2, 0);
        bus.write_word(gd_black + 4, 0);
        bus.write_word(gd_black + 6, 0);
        let gd_repurposed = gd_ctab_ptr + 8 + 255 * 8;
        bus.write_word(gd_repurposed, 255);
        bus.write_word(gd_repurposed + 2, 0xFFFF);
        bus.write_word(gd_repurposed + 4, 0xFFFF);
        bus.write_word(gd_repurposed + 6, 0xCCCC);

        let mut stale_entries = TrapDispatcher::standard_mac_8bpp_clut();
        stale_entries[1] = [0xFFFF, 0xFFFF, 0xCCCC];
        stale_entries[255] = [0, 0, 0];
        let stale_ctab = make_test_ctab_handle(&mut bus, &stale_entries, 0x1234, 0);

        let window_base = bus.alloc(16 * 16);
        let window_pixmap_handle = bus.alloc(4);
        let window_pixmap_ptr = bus.alloc(50);
        bus.write_long(window_pixmap_handle, window_pixmap_ptr);
        write_pixmap_8(&mut bus, window_pixmap_ptr, window_base, 16, 16, stale_ctab);

        let port = bus.alloc(96);
        bus.write_long(port + 2, window_pixmap_handle);
        bus.write_word(port + 6, 0xC000);
        bus.write_word(port + 16, 0);
        bus.write_word(port + 18, 0);
        bus.write_word(port + 20, 16);
        bus.write_word(port + 22, 16);
        make_rgn(&mut bus, 0x340000, 0x340100, 0, 0, 16, 16);
        make_rgn(&mut bus, 0x340200, 0x340300, 0, 0, 16, 16);
        bus.write_long(port + 24, 0x340100);
        bus.write_long(port + 28, 0x340300);

        d.set_current_port_state(&mut bus, &mut cpu, port, Some(gdh));
        d.fg_color = (0, 0, 0);
        d.pn_mode = 0;
        d.pn_pat = [0xFF; 8];

        let rect = Rect {
            top: 0,
            left: 0,
            bottom: 1,
            right: 1,
        };
        d.draw_rect(&mut cpu, &mut bus, &rect, ShapeOp::Paint);

        assert_eq!(
            bus.read_byte(window_base),
            1,
            "current window drawing must use the current GDevice CTable when its PixMap table is stale"
        );
    }

    #[test]
    fn drawrect_honors_complex_current_port_clip_region_on_8bpp_fast_fill() {
        let (mut d, mut cpu, mut bus) = setup();
        let base = bus.alloc(4 * 2);
        for offset in 0..8 {
            bus.write_byte(base + offset, 77);
        }

        let ctab = make_test_ctab_handle(
            &mut bus,
            &TrapDispatcher::standard_mac_8bpp_clut(),
            0x1234,
            0,
        );
        let pixmap_handle = bus.alloc(4);
        let pixmap_ptr = bus.alloc(50);
        bus.write_long(pixmap_handle, pixmap_ptr);
        write_pixmap_8(&mut bus, pixmap_ptr, base, 4, 2, ctab);

        let port = bus.alloc(96);
        bus.write_long(port + 2, pixmap_handle);
        bus.write_word(port + 6, 0xC000);
        write_rect(&mut bus, port + 16, 0, 0, 2, 4);

        let vis = make_complex_rgn(&mut bus, (0, 0, 2, 4), &[]);
        let clip = make_complex_rgn(
            &mut bus,
            (0, 0, 2, 4),
            &[
                0,
                0,
                1,
                3,
                4,
                super::REGION_STOP,
                2,
                super::REGION_STOP,
                super::REGION_STOP,
            ],
        );
        bus.write_long(port + 24, vis);
        bus.write_long(port + 28, clip);

        d.set_current_port_state(&mut bus, &mut cpu, port, None);
        d.fg_color = (0, 0, 0);
        d.pn_mode = 0;
        d.pn_pat = [0xFF; 8];

        let rect = Rect {
            top: 0,
            left: 0,
            bottom: 2,
            right: 4,
        };
        d.draw_rect(&mut cpu, &mut bus, &rect, ShapeOp::Paint);

        assert_eq!(bus.read_byte(base), 255);
        assert_eq!(bus.read_byte(base + 3), 255);
        assert_eq!(bus.read_byte(base + 4), 255);
        assert_eq!(bus.read_byte(base + 7), 255);
        assert_eq!(
            bus.read_byte(base + 1),
            77,
            "8bpp PaintRect must preserve holes in a complex clipRgn"
        );
        assert_eq!(bus.read_byte(base + 2), 77);
        assert_eq!(bus.read_byte(base + 5), 77);
        assert_eq!(bus.read_byte(base + 6), 77);
    }

    #[test]
    fn initfonts_procedure_call_preserves_stack_pointer() {
        let (mut d, mut cpu, mut bus) = setup();
        d.tx_font = 22;
        d.tx_size = 18;
        d.tx_mode = 3;
        d.tx_face = 1;
        let sp_before = cpu.read_reg(Register::A7);
        let result = d.dispatch_quickdraw(true, 0x0FE, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before);
        assert_eq!(d.tx_font, 22);
        assert_eq!(d.tx_size, 18);
        assert_eq!(d.tx_mode, 3);
        assert_eq!(d.tx_face, 1);
    }

    // ==================== Port Management ====================

    #[test]
    fn test_get_port() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let out_ptr = 0x300000u32;
        bus.write_long(TEST_SP, out_ptr);
        let result = d.dispatch_quickdraw(true, 0x074, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        let port = bus.read_long(out_ptr);
        assert_eq!(port, 0x181000);
    }

    #[test]
    fn test_set_port() {
        let (mut d, mut cpu, mut bus) = setup();
        let new_port = 0x300000u32;
        bus.write_long(TEST_SP, new_port);
        let result = d.dispatch_quickdraw(true, 0x073, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        let a5 = cpu.read_reg(Register::A5);
        let global_ptr = bus.read_long(a5);
        assert_eq!(bus.read_long(global_ptr), new_port);
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::THE_PORT),
            new_port
        );
        assert_eq!(d.current_port, new_port);
        assert_ne!(d.current_gdevice, 0);
    }

    #[test]
    fn local_global_transforms_use_port_global_offset() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port = 0x181000u32;
        let pt_ptr = 0x200000u32;

        // A window at global (top=80,left=120) with local portRect origin
        // still at (0,0) has portBits.bounds topLeft=(-80,-120).
        bus.write_word(port + 8, (-80i16) as u16);
        bus.write_word(port + 10, (-120i16) as u16);
        bus.write_word(port + 16, 0);
        bus.write_word(port + 18, 0);

        bus.write_word(pt_ptr, 10);
        bus.write_word(pt_ptr + 2, 20);
        bus.write_long(TEST_SP, pt_ptr);
        let result = d.dispatch_quickdraw(true, 0x070, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            (
                bus.read_word(pt_ptr) as i16,
                bus.read_word(pt_ptr + 2) as i16
            ),
            (90, 140),
            "LocalToGlobal should add the window's global top-left"
        );
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(pt_ptr, 90);
        bus.write_word(pt_ptr + 2, 140);
        bus.write_long(TEST_SP, pt_ptr);
        let result = d.dispatch_quickdraw(true, 0x071, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            (
                bus.read_word(pt_ptr) as i16,
                bus.read_word(pt_ptr + 2) as i16
            ),
            (10, 20),
            "GlobalToLocal should subtract the window's global top-left"
        );
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn test_set_port_bits() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let bits_ptr = 0x300000u32;
        // Write 14 bytes of test data to bits_ptr
        for i in 0..14u32 {
            bus.write_byte(bits_ptr + i, (i + 1) as u8);
        }
        bus.write_long(TEST_SP, bits_ptr);
        let result = d.dispatch_quickdraw(true, 0x075, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        // Verify port+2..port+16 got the 14 bytes
        let port_ptr = 0x181000u32;
        for i in 0..14u32 {
            assert_eq!(bus.read_byte(port_ptr + 2 + i), (i + 1) as u8);
        }
    }

    #[test]
    fn test_port_size() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        // portRect top=0, left=0
        bus.write_word(TEST_SP, 200u16); // height
        bus.write_word(TEST_SP + 2, 300u16); // width
        let result = d.dispatch_quickdraw(true, 0x076, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(port_ptr + 20), 200); // bottom = top + height
        assert_eq!(bus.read_word(port_ptr + 22), 300); // right = left + width
    }

    #[test]
    fn test_move_port_to() {
        // IM:I I-166 + BasiliskII-witnessed: MovePortTo shifts
        // portBits.bounds so portRect.topLeft - bits.bounds.topLeft ==
        // (leftGlobal, topGlobal). portRect itself is unchanged.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        // Default port: portRect = bits.bounds = (0,0,342,512).
        bus.write_word(TEST_SP, 50u16); // topGlobal
        bus.write_word(TEST_SP + 2, 60u16); // leftGlobal
        let result = d.dispatch_quickdraw(true, 0x077, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        // portRect unchanged.
        assert_eq!(bus.read_word(port_ptr + 16), 0);
        assert_eq!(bus.read_word(port_ptr + 18), 0);
        // bits.bounds.topLeft = (0,0) - (50, 60) = (-50, -60).
        assert_eq!(bus.read_word(port_ptr + 8) as i16, -50);
        assert_eq!(bus.read_word(port_ptr + 10) as i16, -60);
    }

    #[test]
    fn test_set_origin() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        bus.write_word(TEST_SP, 10u16); // v
        bus.write_word(TEST_SP + 2, 20u16); // h
        let result = d.dispatch_quickdraw(true, 0x078, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(port_ptr + 16) as i16, 10); // top
        assert_eq!(bus.read_word(port_ptr + 18) as i16, 20); // left
    }

    // ==================== Clipping ====================

    #[test]
    fn setclip_sets_current_port_cliprgn_to_source_region_bounds_and_preserves_handle() {
        // Inside Macintosh Volume I (1985), p. I-167: SetClip sets the
        // current port clipping region from the source region argument.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        let clip_ptr = bus.alloc(10);
        let clip_handle = bus.alloc(4);
        make_rgn(&mut bus, clip_ptr, clip_handle, 0, 0, 342, 512);
        bus.write_long(port_ptr + 28, clip_handle);
        let new_rgn_ptr = 0x300000u32;
        let new_rgn_handle = 0x300100u32;
        make_rgn(&mut bus, new_rgn_ptr, new_rgn_handle, 10, 20, 100, 200);
        bus.write_long(TEST_SP, new_rgn_handle);
        let result = d.dispatch_quickdraw(true, 0x079, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(
            bus.read_long(port_ptr + 28),
            clip_handle,
            "SetClip should preserve the existing port clipRgn handle"
        );
        assert_eq!(
            read_rect(&bus, clip_ptr + 2),
            (10, 20, 100, 200),
            "SetClip should copy source region bounds into port clipRgn"
        );
    }

    #[test]
    fn setclip_copies_region_data_by_value_without_aliasing_source() {
        // Inside Macintosh Volume I (1985), p. I-192: region copy operations
        // copy by value; mutating the source afterwards must not mutate the
        // destination region state held by the current port.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        let clip_ptr = bus.alloc(10);
        let clip_handle = bus.alloc(4);
        make_rgn(&mut bus, clip_ptr, clip_handle, 0, 0, 342, 512);
        bus.write_long(port_ptr + 28, clip_handle);
        let src_rgn_ptr = 0x300000u32;
        let src_rgn_handle = 0x300100u32;
        make_rgn(&mut bus, src_rgn_ptr, src_rgn_handle, 10, 20, 100, 200);

        bus.write_long(TEST_SP, src_rgn_handle);
        let result = d.dispatch_quickdraw(true, 0x079, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(read_rect(&bus, clip_ptr + 2), (10, 20, 100, 200));

        // Mutating the caller's scratch region must not affect the port clip.
        write_rect(&mut bus, src_rgn_ptr + 2, 2, 0, 172, 1);
        assert_eq!(read_rect(&bus, clip_ptr + 2), (10, 20, 100, 200));
    }

    #[test]
    fn setclip_copies_complex_region_data_and_resizes_port_clip_storage() {
        // IM:I I-167: SetClip copies the complete region into the current
        // port's clipRgn without replacing the clipRgn handle.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        let clip_ptr = bus.alloc(10);
        let clip_handle = bus.alloc(4);
        make_rgn(&mut bus, clip_ptr, clip_handle, 0, 0, 342, 512);
        bus.write_long(port_ptr + 28, clip_handle);

        let src = make_complex_rgn(
            &mut bus,
            (0, 0, 4, 6),
            &[
                0,
                0,
                6,
                super::REGION_STOP,
                1,
                2,
                4,
                super::REGION_STOP,
                3,
                2,
                4,
                super::REGION_STOP,
                4,
                0,
                6,
                super::REGION_STOP,
                super::REGION_STOP,
            ],
        );
        let src_size = bus.read_word(bus.read_long(src)) as u32;

        bus.write_long(TEST_SP, src);
        let result = d.dispatch_quickdraw(true, 0x079, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(port_ptr + 28), clip_handle);
        assert_ne!(
            bus.read_long(clip_handle),
            clip_ptr,
            "complex SetClip should resize the 10-byte destination storage"
        );
        assert_eq!(
            bus.get_alloc_size(bus.read_long(clip_handle)),
            Some(src_size)
        );
        assert_rgn_bytes_equal(&bus, clip_handle, src);
        assert!(TrapDispatcher::region_contains_point(
            &bus,
            clip_handle,
            1,
            1
        ));
        assert!(
            !TrapDispatcher::region_contains_point(&bus, clip_handle, 1, 3),
            "complex clipping hole should survive SetClip"
        );
        assert!(TrapDispatcher::region_contains_point(
            &bus,
            clip_handle,
            1,
            4
        ));
    }

    #[test]
    fn test_get_clip() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        // Create a destination region
        let dst_rgn = 0x300000u32;
        let dst_handle = 0x300100u32;
        bus.write_word(dst_rgn, 10);
        bus.write_long(dst_handle, dst_rgn);
        bus.write_long(TEST_SP, dst_handle);
        let result = d.dispatch_quickdraw(true, 0x07A, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        // Verify clip region was copied
        assert_eq!(bus.read_word(dst_rgn), 10); // rgnSize
    }

    #[test]
    fn getclip_copies_complex_current_clip_and_resizes_destination_storage() {
        // IM:I I-167: GetClip is the reverse of SetClip; it changes the
        // caller-supplied region to an equivalent copy of the port clip.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        let clip = make_complex_rgn(
            &mut bus,
            (0, 0, 4, 6),
            &[
                0,
                0,
                6,
                super::REGION_STOP,
                1,
                2,
                4,
                super::REGION_STOP,
                3,
                2,
                4,
                super::REGION_STOP,
                4,
                0,
                6,
                super::REGION_STOP,
                super::REGION_STOP,
            ],
        );
        bus.write_long(port_ptr + 28, clip);
        let clip_size = bus.read_word(bus.read_long(clip)) as u32;

        let dst_ptr = bus.alloc(10);
        let dst = bus.alloc(4);
        make_rgn(&mut bus, dst_ptr, dst, 9, 8, 7, 6);

        bus.write_long(TEST_SP, dst);
        let result = d.dispatch_quickdraw(true, 0x07A, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_ne!(
            bus.read_long(dst),
            dst_ptr,
            "complex GetClip should resize the 10-byte destination storage"
        );
        assert_eq!(bus.get_alloc_size(bus.read_long(dst)), Some(clip_size));
        assert_rgn_bytes_equal(&bus, dst, clip);
        assert!(TrapDispatcher::region_contains_point(&bus, dst, 1, 1));
        assert!(
            !TrapDispatcher::region_contains_point(&bus, dst, 1, 3),
            "complex clipping hole should survive GetClip"
        );
        assert!(TrapDispatcher::region_contains_point(&bus, dst, 1, 4));
    }

    #[test]
    fn copypixmap_copies_50_byte_pixmap_record_into_dst() {
        let (mut d, mut cpu, mut bus) = setup();

        let src_handle = bus.alloc(4);
        let dst_handle = bus.alloc(4);
        let src_pixels = bus.alloc(8);
        let src_pm = bus.alloc(50);
        let dst_pixels = bus.alloc(8);
        let dst_pm = bus.alloc(50);
        bus.write_long(src_handle, src_pm);
        bus.write_long(dst_handle, dst_pm);

        for i in 0..8u32 {
            bus.write_byte(src_pixels + i, 0xA0 + i as u8);
            bus.write_byte(dst_pixels + i, 0xD0 + i as u8);
        }

        bus.write_long(src_pm, src_pixels);
        bus.write_word(src_pm + 4, 0x1234);
        write_rect(&mut bus, src_pm + 6, 1, 2, 3, 4);
        bus.write_word(src_pm + 14, 0x0102);
        bus.write_word(src_pm + 16, 0x0304);
        bus.write_long(src_pm + 18, 0x05060708);
        bus.write_long(src_pm + 22, 0x11121314);
        bus.write_long(src_pm + 26, 0x21222324);
        bus.write_word(src_pm + 30, 0x0506);
        bus.write_word(src_pm + 32, 0x0708);
        bus.write_word(src_pm + 34, 0x090A);
        bus.write_word(src_pm + 36, 0x0B0C);
        bus.write_long(src_pm + 38, 0x31323334);
        bus.write_long(src_pm + 42, 0x41424344);
        bus.write_long(src_pm + 46, 0x51525354);

        bus.write_word(dst_pm + 4, 0x4321);
        bus.write_long(TEST_SP, dst_handle);
        bus.write_long(TEST_SP + 4, src_handle);
        let result = d.dispatch_quickdraw(true, 0x205, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);

        let copied_dst_pm = bus.read_long(dst_handle);
        assert_eq!(bus.read_word(copied_dst_pm + 4), 0x1234);
        assert_eq!(
            bus.read_bytes(dst_pixels, 8),
            vec![0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7]
        );
    }

    #[test]
    fn test_clip_rect() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 20, 100, 200);
        bus.write_long(TEST_SP, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x07B, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        // Verify clip region was updated
        let port_ptr = 0x181000u32;
        let clip_handle = bus.read_long(port_ptr + 28);
        let clip_ptr = bus.read_long(clip_handle);
        assert_eq!(bus.read_word(clip_ptr), 10); // rgnSize
        assert_eq!(bus.read_word(clip_ptr + 2) as i16, 10); // top
        assert_eq!(bus.read_word(clip_ptr + 4) as i16, 20); // left
    }

    #[test]
    fn closeport_releases_visrgn_and_cliprgn_and_nils_port_slots() {
        // Inside Macintosh Volume I (1985), p. I-163: ClosePort releases
        // visRgn and clipRgn storage for the target GrafPort.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        let vis_ptr = bus.alloc(10);
        let vis_handle = bus.alloc(4);
        bus.write_long(vis_handle, vis_ptr);
        let clip_ptr = bus.alloc(10);
        let clip_handle = bus.alloc(4);
        bus.write_long(clip_handle, clip_ptr);
        bus.write_long(port_ptr + 24, vis_handle);
        bus.write_long(port_ptr + 28, clip_handle);
        assert!(bus.get_alloc_size(vis_handle).is_some());
        assert!(bus.get_alloc_size(clip_handle).is_some());
        assert!(bus.get_alloc_size(vis_ptr).is_some());
        assert!(bus.get_alloc_size(clip_ptr).is_some());

        bus.write_long(TEST_SP, port_ptr);
        let result = d.dispatch_quickdraw(true, 0x07D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(port_ptr + 24), 0);
        assert_eq!(bus.read_long(port_ptr + 28), 0);
        assert!(bus.get_alloc_size(vis_handle).is_none());
        assert!(bus.get_alloc_size(clip_handle).is_none());
        assert!(bus.get_alloc_size(vis_ptr).is_none());
        assert!(bus.get_alloc_size(clip_ptr).is_none());

        cpu.write_reg(Register::A0, vis_ptr);
        assert!(d.dispatch_memory(false, 0x28, &mut cpu, &mut bus).is_some());
        assert_eq!(cpu.read_reg(Register::A0), 0);

        cpu.write_reg(Register::A0, clip_ptr);
        assert!(d.dispatch_memory(false, 0x28, &mut cpu, &mut bus).is_some());
        assert_eq!(cpu.read_reg(Register::A0), 0);
    }

    #[test]
    fn closeport_consumes_port_argument_and_pops_four_bytes() {
        // ClosePort/CloseCPort stack shape:
        //   PROCEDURE ClosePort(port: GrafPtr);
        //   PROCEDURE CloseCPort(port: CGrafPtr);
        // Both consume one 4-byte pointer argument.
        let (mut d, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        bus.write_long(sp_before, 0);

        let result = d.dispatch_quickdraw(true, 0x07D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before + 4);
    }

    #[test]
    fn closecport_releases_documented_cgrafport_subhandles_and_preserves_portpixmap_colortable() {
        // Inside Macintosh Volume V (1986), p. V-72: CloseCPort disposes
        // visRgn/clipRgn, bkPixPat/pnPixPat/fillPixPat, grafVars, and
        // portPixMap, but does NOT dispose the portPixMap color table.
        let (mut d, mut cpu, mut bus) = setup();
        let port_ptr = 0x300000u32;
        bus.write_word(port_ptr + 6, 0xC000); // CGrafPort signature

        let make_region = |bus: &mut crate::memory::MacMemoryBus| -> (u32, u32) {
            let ptr = bus.alloc(10);
            bus.write_word(ptr, 10);
            let handle = bus.alloc(4);
            bus.write_long(handle, ptr);
            (handle, ptr)
        };
        let (vis_handle, vis_ptr) = make_region(&mut bus);
        let (clip_handle, clip_ptr) = make_region(&mut bus);
        bus.write_long(port_ptr + 24, vis_handle);
        bus.write_long(port_ptr + 28, clip_handle);

        let make_pixpat = |bus: &mut crate::memory::MacMemoryBus| -> (u32, [u32; 8]) {
            let pat_data_ptr = bus.alloc(16);
            let pat_data_handle = bus.alloc(4);
            bus.write_long(pat_data_handle, pat_data_ptr);

            let pat_xdata_ptr = bus.alloc(16);
            let pat_xdata_handle = bus.alloc(4);
            bus.write_long(pat_xdata_handle, pat_xdata_ptr);

            let ctab_ptr = bus.alloc(16);
            let ctab_handle = bus.alloc(4);
            bus.write_long(ctab_handle, ctab_ptr);

            let pat_map_ptr = bus.alloc(64);
            bus.write_long(pat_map_ptr + 42, ctab_handle);
            let pat_map_handle = bus.alloc(4);
            bus.write_long(pat_map_handle, pat_map_ptr);

            let pixpat_ptr = bus.alloc(28);
            bus.write_long(pixpat_ptr + 2, pat_map_handle);
            bus.write_long(pixpat_ptr + 6, pat_data_handle);
            bus.write_long(pixpat_ptr + 10, pat_xdata_handle);
            let pixpat_handle = bus.alloc(4);
            bus.write_long(pixpat_handle, pixpat_ptr);

            (
                pixpat_handle,
                [
                    pat_data_ptr,
                    pat_data_handle,
                    pat_xdata_ptr,
                    pat_xdata_handle,
                    ctab_ptr,
                    ctab_handle,
                    pat_map_ptr,
                    pat_map_handle,
                ],
            )
        };
        let (bk_pixpat_handle, bk_addrs) = make_pixpat(&mut bus);
        let (pn_pixpat_handle, pn_addrs) = make_pixpat(&mut bus);
        let (fill_pixpat_handle, fill_addrs) = make_pixpat(&mut bus);
        bus.write_long(port_ptr + 32, bk_pixpat_handle);
        bus.write_long(port_ptr + 58, pn_pixpat_handle);
        bus.write_long(port_ptr + 62, fill_pixpat_handle);

        let graf_vars_ptr = bus.alloc(32);
        let graf_vars_handle = bus.alloc(4);
        bus.write_long(graf_vars_handle, graf_vars_ptr);
        bus.write_long(port_ptr + 8, graf_vars_handle);

        let port_ctab_ptr = bus.alloc(16);
        let port_ctab_handle = bus.alloc(4);
        bus.write_long(port_ctab_handle, port_ctab_ptr);
        let port_pixmap_ptr = bus.alloc(64);
        bus.write_long(port_pixmap_ptr + 42, port_ctab_handle);
        let port_pixmap_handle = bus.alloc(4);
        bus.write_long(port_pixmap_handle, port_pixmap_ptr);
        bus.write_long(port_ptr + 2, port_pixmap_handle);

        for addr in [
            vis_handle,
            vis_ptr,
            clip_handle,
            clip_ptr,
            bk_pixpat_handle,
            pn_pixpat_handle,
            fill_pixpat_handle,
            graf_vars_handle,
            graf_vars_ptr,
            port_pixmap_handle,
            port_pixmap_ptr,
            port_ctab_handle,
            port_ctab_ptr,
        ]
        .into_iter()
        .chain(bk_addrs.into_iter())
        .chain(pn_addrs.into_iter())
        .chain(fill_addrs.into_iter())
        {
            assert!(
                bus.get_alloc_size(addr).is_some(),
                "missing pre-allocation at ${addr:08X}"
            );
        }

        bus.write_long(TEST_SP, port_ptr);
        let result = d.dispatch_quickdraw(true, 0x07D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        assert_eq!(bus.read_long(port_ptr + 24), 0);
        assert_eq!(bus.read_long(port_ptr + 28), 0);
        assert_eq!(bus.read_long(port_ptr + 32), 0);
        assert_eq!(bus.read_long(port_ptr + 58), 0);
        assert_eq!(bus.read_long(port_ptr + 62), 0);
        assert_eq!(bus.read_long(port_ptr + 8), 0);
        assert_eq!(bus.read_long(port_ptr + 2), port_pixmap_handle);

        for addr in [
            vis_handle,
            vis_ptr,
            clip_handle,
            clip_ptr,
            bk_pixpat_handle,
            pn_pixpat_handle,
            fill_pixpat_handle,
            graf_vars_handle,
            graf_vars_ptr,
            port_pixmap_handle,
            port_pixmap_ptr,
        ]
        .into_iter()
        .chain(bk_addrs.into_iter())
        .chain(pn_addrs.into_iter())
        .chain(fill_addrs.into_iter())
        {
            assert!(
                bus.get_alloc_size(addr).is_none(),
                "expected ${addr:08X} to be released by CloseCPort"
            );
        }

        assert!(
            bus.get_alloc_size(port_ctab_handle).is_some(),
            "CloseCPort must not free the portPixMap color table handle"
        );
        assert!(
            bus.get_alloc_size(port_ctab_ptr).is_some(),
            "CloseCPort must not free the portPixMap color table record"
        );
    }

    // ==================== Coordinate Transforms ====================

    #[test]
    fn test_local_to_global() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let pt_ptr = 0x300000u32;
        bus.write_word(pt_ptr, 50u16); // v
        bus.write_word(pt_ptr + 2, 100u16); // h
        bus.write_long(TEST_SP, pt_ptr);
        let result = d.dispatch_quickdraw(true, 0x070, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        // bounds_top=0, bounds_left=0, so result = v-0, h-0
        assert_eq!(bus.read_word(pt_ptr) as i16, 50);
        assert_eq!(bus.read_word(pt_ptr + 2) as i16, 100);
    }

    #[test]
    fn test_global_to_local() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let pt_ptr = 0x300000u32;
        bus.write_word(pt_ptr, 50u16); // v
        bus.write_word(pt_ptr + 2, 100u16); // h
        bus.write_long(TEST_SP, pt_ptr);
        let result = d.dispatch_quickdraw(true, 0x071, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        // bounds_top=0, bounds_left=0, so result = v+0, h+0
        assert_eq!(bus.read_word(pt_ptr) as i16, 50);
        assert_eq!(bus.read_word(pt_ptr + 2) as i16, 100);
    }

    #[test]
    fn test_graf_device() {
        let (mut d, mut cpu, mut bus) = setup();
        let result = d.dispatch_quickdraw(true, 0x072, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
    }

    #[test]
    fn scalept_scales_width_and_height_by_rect_ratio() {
        // Inside Macintosh Volume I (1985), p. I-195: ScalePt scales
        // width/height by destination-to-source rectangle proportions.
        let (mut d, mut cpu, mut bus) = setup();
        let pt_ptr = 0x300000u32;
        let src_rect_ptr = 0x300100u32;
        let dst_rect_ptr = 0x300200u32;

        bus.write_word(pt_ptr, 2); // v (height)
        bus.write_word(pt_ptr + 2, 4); // h (width)
        write_rect(&mut bus, src_rect_ptr, 0, 0, 10, 20);
        write_rect(&mut bus, dst_rect_ptr, 0, 0, 30, 60);
        bus.write_long(TEST_SP, dst_rect_ptr);
        bus.write_long(TEST_SP + 4, src_rect_ptr);
        bus.write_long(TEST_SP + 8, pt_ptr);

        let result = d.dispatch_quickdraw(true, 0x0F8, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(bus.read_word(pt_ptr) as i16, 6);
        assert_eq!(bus.read_word(pt_ptr + 2) as i16, 12);
    }

    #[test]
    fn scalept_minimum_result_is_one_one() {
        // Inside Macintosh Volume I (1985), p. I-195: minimum ScalePt
        // result is (1,1).
        let (mut d, mut cpu, mut bus) = setup();
        let pt_ptr = 0x300010u32;
        let src_rect_ptr = 0x300110u32;
        let dst_rect_ptr = 0x300210u32;

        bus.write_word(pt_ptr, 4);
        bus.write_word(pt_ptr + 2, 3);
        write_rect(&mut bus, src_rect_ptr, 0, 0, 100, 100);
        write_rect(&mut bus, dst_rect_ptr, 0, 0, 1, 1);
        bus.write_long(TEST_SP, dst_rect_ptr);
        bus.write_long(TEST_SP + 4, src_rect_ptr);
        bus.write_long(TEST_SP + 8, pt_ptr);

        let result = d.dispatch_quickdraw(true, 0x0F8, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(pt_ptr) as i16, 1);
        assert_eq!(bus.read_word(pt_ptr + 2) as i16, 1);
    }

    #[test]
    fn mappt_maps_point_proportionally_between_rectangles() {
        // Inside Macintosh Volume I (1985), p. I-196: MapPt maps a point
        // to a similarly located position in dstRect.
        let (mut d, mut cpu, mut bus) = setup();
        let pt_ptr = 0x300020u32;
        let src_rect_ptr = 0x300120u32;
        let dst_rect_ptr = 0x300220u32;

        bus.write_word(pt_ptr, 35); // v
        bus.write_word(pt_ptr + 2, 70); // h
        write_rect(&mut bus, src_rect_ptr, 10, 20, 110, 220);
        write_rect(&mut bus, dst_rect_ptr, 0, 0, 200, 400);
        bus.write_long(TEST_SP, dst_rect_ptr);
        bus.write_long(TEST_SP + 4, src_rect_ptr);
        bus.write_long(TEST_SP + 8, pt_ptr);

        let result = d.dispatch_quickdraw(true, 0x0F9, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(pt_ptr) as i16, 50);
        assert_eq!(bus.read_word(pt_ptr + 2) as i16, 100);
    }

    #[test]
    fn mappt_consumes_point_and_rect_arguments() {
        // Inside Macintosh Volume I (1985), p. I-196:
        // MapPt(pt, srcRect, dstRect) consumes 12 bytes of pointers.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x300300);
        bus.write_long(TEST_SP + 4, 0x300304);
        bus.write_long(TEST_SP + 8, 0x300308);

        let result = d.dispatch_quickdraw(true, 0x0F9, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
    }

    #[test]
    fn maprgn_maps_region_bbox_proportionally() {
        // Inside Macintosh Volume I (1985), p. I-196: MapRgn maps region
        // points through MapPt; rectangular-region HLE maps the bbox.
        let (mut d, mut cpu, mut bus) = setup();
        let rgn_ptr = 0x300400u32;
        let rgn_handle = 0x300500u32;
        let src_rect_ptr = 0x300420u32;
        let dst_rect_ptr = 0x300520u32;

        make_rgn(&mut bus, rgn_ptr, rgn_handle, 20, 40, 60, 140);
        write_rect(&mut bus, src_rect_ptr, 10, 20, 110, 220);
        write_rect(&mut bus, dst_rect_ptr, 0, 0, 200, 400);
        bus.write_long(TEST_SP, dst_rect_ptr);
        bus.write_long(TEST_SP + 4, src_rect_ptr);
        bus.write_long(TEST_SP + 8, rgn_handle);

        let result = d.dispatch_quickdraw(true, 0x0FB, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(read_rgn_bbox(&bus, rgn_handle), (20, 40, 100, 240));
    }

    #[test]
    fn maprgn_consumes_region_and_rect_arguments() {
        // Inside Macintosh Volume I (1985), p. I-196:
        // MapRgn(rgn, srcRect, dstRect) consumes one handle and two rects.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x300600);
        bus.write_long(TEST_SP + 4, 0x300604);
        bus.write_long(TEST_SP + 8, 0);

        let result = d.dispatch_quickdraw(true, 0x0FB, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
    }

    #[test]
    fn mappoly_maps_all_vertices_proportionally() {
        // Inside Macintosh Volume I (1985), p. I-197: MapPoly maps every
        // polygon vertex through MapPt semantics.
        let (mut d, mut cpu, mut bus) = setup();
        let poly_ptr = bus.alloc(22);
        let poly_handle = bus.alloc(4);
        let src_rect_ptr = 0x300720u32;
        let dst_rect_ptr = 0x300820u32;

        make_poly(
            &mut bus,
            poly_ptr,
            poly_handle,
            (20, 40, 60, 140),
            &[(20, 40), (40, 80), (60, 140)],
        );
        write_rect(&mut bus, src_rect_ptr, 10, 20, 110, 220);
        write_rect(&mut bus, dst_rect_ptr, 0, 0, 200, 400);
        bus.write_long(TEST_SP, dst_rect_ptr);
        bus.write_long(TEST_SP + 4, src_rect_ptr);
        bus.write_long(TEST_SP + 8, poly_handle);

        let result = d.dispatch_quickdraw(true, 0x0FC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(read_rect(&bus, poly_ptr + 2), (20, 40, 100, 240));
        assert_eq!(bus.read_word(poly_ptr + 10) as i16, 20);
        assert_eq!(bus.read_word(poly_ptr + 12) as i16, 40);
        assert_eq!(bus.read_word(poly_ptr + 14) as i16, 60);
        assert_eq!(bus.read_word(poly_ptr + 16) as i16, 120);
        assert_eq!(bus.read_word(poly_ptr + 18) as i16, 100);
        assert_eq!(bus.read_word(poly_ptr + 20) as i16, 240);
    }

    #[test]
    fn mappoly_consumes_polygon_and_rect_arguments() {
        // Inside Macintosh Volume I (1985), p. I-197:
        // MapPoly(poly, srcRect, dstRect) consumes 12 bytes.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x300900);
        bus.write_long(TEST_SP + 4, 0x300904);
        bus.write_long(TEST_SP + 8, 0);

        let result = d.dispatch_quickdraw(true, 0x0FC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
    }

    // ==================== Point Operations ====================

    #[test]
    fn test_add_pt() {
        let (mut d, mut cpu, mut bus) = setup();
        let dst_ptr = 0x300000u32;
        bus.write_word(dst_ptr, 10u16); // dst_v
        bus.write_word(dst_ptr + 2, 20u16); // dst_h
        bus.write_long(TEST_SP, dst_ptr); // dst_ptr
        bus.write_word(TEST_SP + 4, 5u16); // src_v
        bus.write_word(TEST_SP + 6, 3u16); // src_h
        let result = d.dispatch_quickdraw(true, 0x07E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(dst_ptr) as i16, 15); // 10 + 5
        assert_eq!(bus.read_word(dst_ptr + 2) as i16, 23); // 20 + 3
    }

    #[test]
    fn test_sub_pt() {
        let (mut d, mut cpu, mut bus) = setup();
        let dst_ptr = 0x300000u32;
        bus.write_word(dst_ptr, 10u16);
        bus.write_word(dst_ptr + 2, 20u16);
        bus.write_long(TEST_SP, dst_ptr);
        bus.write_word(TEST_SP + 4, 3u16); // src_v
        bus.write_word(TEST_SP + 6, 5u16); // src_h
        let result = d.dispatch_quickdraw(true, 0x07F, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(dst_ptr) as i16, 7); // 10 - 3
        assert_eq!(bus.read_word(dst_ptr + 2) as i16, 15); // 20 - 5
    }

    #[test]
    fn test_set_pt() {
        let (mut d, mut cpu, mut bus) = setup();
        let pt_ptr = 0x300000u32;
        bus.write_word(TEST_SP, 42u16); // v
        bus.write_word(TEST_SP + 2, 99u16); // h
        bus.write_long(TEST_SP + 4, pt_ptr); // pt_ptr
        let result = d.dispatch_quickdraw(true, 0x080, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(pt_ptr) as i16, 42);
        assert_eq!(bus.read_word(pt_ptr + 2) as i16, 99);
    }

    // ==================== Pen State ====================

    #[test]
    fn test_pen_size() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 3u16); // height
        bus.write_word(TEST_SP + 2, 5u16); // width
        let result = d.dispatch_quickdraw(true, 0x09B, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(d.pn_size, (3, 5));
    }

    #[test]
    fn test_pen_mode() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 2u16); // mode
        let result = d.dispatch_quickdraw(true, 0x09C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(d.pn_mode, 2);
    }

    #[test]
    fn test_pen_pat() {
        let (mut d, mut cpu, mut bus) = setup();
        let pat_ptr = 0x300000u32;
        let pattern = [0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55];
        bus.write_bytes(pat_ptr, &pattern);
        bus.write_long(TEST_SP, pat_ptr);
        let result = d.dispatch_quickdraw(true, 0x09D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(d.pn_pat, pattern);
    }

    #[test]
    fn test_pen_normal() {
        let (mut d, mut cpu, mut bus) = setup();
        d.pn_size = (5, 5);
        d.pn_mode = 3;
        d.pn_pat = [0x00; 8];
        let result = d.dispatch_quickdraw(true, 0x09E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(d.pn_size, (1, 1));
        assert_eq!(d.pn_mode, 8);
        assert_eq!(d.pn_pat, [0xFF; 8]);
    }

    #[test]
    fn test_get_pen_state() {
        let (mut d, mut cpu, mut bus) = setup();
        d.pn_loc = (10, 20);
        d.pn_size = (3, 4);
        d.pn_mode = 2;
        d.pn_pat = [0xAA; 8];
        let state_ptr = 0x300000u32;
        bus.write_long(TEST_SP, state_ptr);
        let result = d.dispatch_quickdraw(true, 0x098, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(state_ptr) as i16, 10); // pn_loc.v
        assert_eq!(bus.read_word(state_ptr + 2) as i16, 20); // pn_loc.h
        assert_eq!(bus.read_word(state_ptr + 4) as i16, 3); // pn_size.v
        assert_eq!(bus.read_word(state_ptr + 6) as i16, 4); // pn_size.h
        assert_eq!(bus.read_word(state_ptr + 8) as i16, 2); // pn_mode
        for i in 0..8u32 {
            assert_eq!(bus.read_byte(state_ptr + 10 + i), 0xAA);
        }
    }

    #[test]
    fn hidepen_decrements_current_port_pnvis_and_preserves_stack() {
        // IM:I 1985 p. I-168: HidePen decrements pnVis, and when pnVis is
        // negative the pen does not draw.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        d.set_current_port_state(&mut bus, &mut cpu, port_ptr, None);
        let sp_before = cpu.read_reg(Register::A7);

        let result = d.dispatch_quickdraw(true, 0x096, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before);
        assert_eq!(d.pn_vis, -1);
        assert_eq!(bus.read_word(port_ptr + 66) as i16, -1);
    }

    #[test]
    fn showpen_increments_pnvis_and_allows_positive_values() {
        // IM:I 1985 p. I-168: ShowPen increments pnVis; extra calls may
        // increment pnVis beyond 0.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        d.set_current_port_state(&mut bus, &mut cpu, port_ptr, None);
        let sp_before = cpu.read_reg(Register::A7);

        let result = d.dispatch_quickdraw(true, 0x097, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before);
        assert_eq!(d.pn_vis, 1);
        assert_eq!(bus.read_word(port_ptr + 66) as i16, 1);
    }

    #[test]
    fn showpen_balances_hidepen_back_to_zero() {
        // IM:I 1985 p. I-168: HidePen/ShowPen are balanced visibility calls.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        d.set_current_port_state(&mut bus, &mut cpu, port_ptr, None);

        let result = d.dispatch_quickdraw(true, 0x096, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(d.pn_vis, -1);
        assert_eq!(bus.read_word(port_ptr + 66) as i16, -1);

        let result = d.dispatch_quickdraw(true, 0x097, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(d.pn_vis, 0);
        assert_eq!(bus.read_word(port_ptr + 66) as i16, 0);
    }

    #[test]
    fn getpen_writes_current_pnloc_to_output_point_and_pops_arg() {
        // IM:I 1985 p. I-169: GetPen returns the current pen location.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        d.set_current_port_state(&mut bus, &mut cpu, port_ptr, None);
        d.pn_loc = (-123, 234);
        d.sync_current_port_draw_state(&mut bus);
        let out_pt = 0x300000u32;
        bus.write_word(out_pt, 0xDEAD);
        bus.write_word(out_pt + 2, 0xBEEF);
        bus.write_long(TEST_SP, out_pt);

        let result = d.dispatch_quickdraw(true, 0x09A, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(out_pt) as i16, -123);
        assert_eq!(bus.read_word(out_pt + 2) as i16, 234);
    }

    #[test]
    fn getpen_reports_local_coordinates_of_current_port() {
        // IM:I 1985 p. I-169: result is in the local coordinates of the
        // current grafPort (not translated to global coordinates).
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        d.set_current_port_state(&mut bus, &mut cpu, port_ptr, None);
        bus.write_word(port_ptr + 16, 200); // portRect.top
        bus.write_word(port_ptr + 18, 300); // portRect.left
        d.pn_loc = (7, 9);
        d.sync_current_port_draw_state(&mut bus);
        let out_pt = 0x300100u32;
        bus.write_long(TEST_SP, out_pt);

        let result = d.dispatch_quickdraw(true, 0x09A, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(out_pt) as i16, 7);
        assert_eq!(bus.read_word(out_pt + 2) as i16, 9);
    }

    #[test]
    fn test_back_pat() {
        let (mut d, mut cpu, mut bus) = setup();
        let pat_ptr = 0x300000u32;
        let pattern = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88];
        let port = bus.alloc(108);
        bus.write_word(port + 6, 0); // classic GrafPort rowBytes/version word
        d.current_port = port;
        bus.write_bytes(pat_ptr, &pattern);
        bus.write_long(TEST_SP, pat_ptr);
        let result = d.dispatch_quickdraw(true, 0x07C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(d.bk_pat, pattern);
        for (i, expected) in pattern.iter().enumerate() {
            assert_eq!(bus.read_byte(port + 32 + i as u32), *expected);
        }
    }

    // ==================== Pen Movement ====================

    #[test]
    fn test_move_to() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 100u16); // v
        bus.write_word(TEST_SP + 2, 200u16); // h
        let result = d.dispatch_quickdraw(true, 0x093, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(d.pn_loc, (100, 200));
    }

    #[test]
    fn test_move_relative() {
        let (mut d, mut cpu, mut bus) = setup();
        d.pn_loc = (50, 60);
        bus.write_word(TEST_SP, 10u16); // dv
        bus.write_word(TEST_SP + 2, 20u16); // dh
        let result = d.dispatch_quickdraw(true, 0x094, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(d.pn_loc, (60, 80));
    }

    #[test]
    fn test_line_to() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        d.pn_loc = (10, 10);
        bus.write_word(TEST_SP, 20u16); // v
        bus.write_word(TEST_SP + 2, 30u16); // h
        let result = d.dispatch_quickdraw(true, 0x091, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(d.pn_loc, (20, 30));
    }

    #[test]
    fn test_line_relative() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        d.pn_loc = (10, 10);
        bus.write_word(TEST_SP, 5u16); // dv
        bus.write_word(TEST_SP + 2, 10u16); // dh
        let result = d.dispatch_quickdraw(true, 0x092, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(d.pn_loc, (15, 20));
    }

    // ==================== Region Operations ====================

    #[test]
    fn closergn_preserves_multiple_recorded_line_loops_as_complex_region() {
        // Imaging With QuickDraw (1994), p. 3-87..3-89: OpenRgn collects
        // line outlines and CloseRgn organizes them into the resulting region.
        // Separate closed loops must remain separate areas, not collapse to
        // the union bbox.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let dst_ptr = bus.alloc(10);
        let dst = bus.alloc(4);
        make_rgn(&mut bus, dst_ptr, dst, 0, 0, 0, 0);

        let result = d.dispatch_quickdraw(true, 0x0DA, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        for left in [0i16, 8] {
            cpu.write_reg(Register::A7, TEST_SP);
            bus.write_word(TEST_SP, 0); // v
            bus.write_word(TEST_SP + 2, left as u16); // h
            let result = d.dispatch_quickdraw(true, 0x093, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());

            for (dv, dh) in [(0i16, 4i16), (4, 0), (0, -4), (-4, 0)] {
                cpu.write_reg(Register::A7, TEST_SP);
                bus.write_word(TEST_SP, dv as u16);
                bus.write_word(TEST_SP + 2, dh as u16);
                let result = d.dispatch_quickdraw(true, 0x092, &mut cpu, &mut bus);
                assert!(result.unwrap().is_ok());
            }
        }

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dst);
        let result = d.dispatch_quickdraw(true, 0x0DB, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        assert_eq!(read_rgn_bbox(&bus, dst), (0, 0, 4, 12));
        assert!(
            bus.read_word(bus.read_long(dst)) > super::REGION_HEADER_SIZE as u16,
            "separate recorded loops should produce a complex region"
        );
        assert!(TrapDispatcher::region_contains_point(&bus, dst, 1, 1));
        assert!(
            !TrapDispatcher::region_contains_point(&bus, dst, 1, 6),
            "gap between recorded loops must remain outside the region"
        );
        assert!(TrapDispatcher::region_contains_point(&bus, dst, 1, 9));
    }

    #[test]
    fn test_new_rgn() {
        let (mut d, mut cpu, mut bus) = setup();
        let result = d.dispatch_quickdraw(true, 0x0D8, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        let handle = bus.read_long(TEST_SP);
        assert_ne!(handle, 0);
        let ptr = bus.read_long(handle);
        assert_ne!(ptr, 0);
        assert_eq!(bus.read_word(ptr), 10); // rgnSize
    }

    #[test]
    fn test_rect_rgn() {
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 20, 100, 200);
        let rgn_data = 0x300100u32;
        let rgn_handle = 0x300200u32;
        make_rgn(&mut bus, rgn_data, rgn_handle, 0, 0, 0, 0);
        bus.write_long(TEST_SP, rect_ptr);
        bus.write_long(TEST_SP + 4, rgn_handle);
        let result = d.dispatch_quickdraw(true, 0x0DF, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(rgn_data), 10);
        assert_eq!(bus.read_word(rgn_data + 2) as i16, 10);
        assert_eq!(bus.read_word(rgn_data + 4) as i16, 20);
        assert_eq!(bus.read_word(rgn_data + 6) as i16, 100);
        assert_eq!(bus.read_word(rgn_data + 8) as i16, 200);
    }

    #[test]
    fn test_dispose_rgn() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x300000);
        let result = d.dispatch_quickdraw(true, 0x0D9, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn test_copy_rgn() {
        let (mut d, mut cpu, mut bus) = setup();
        let src_rgn = 0x300000u32;
        let src_handle = 0x300100u32;
        make_rgn(&mut bus, src_rgn, src_handle, 5, 10, 50, 100);
        let dst_rgn = 0x300200u32;
        let dst_handle = 0x300300u32;
        make_rgn(&mut bus, dst_rgn, dst_handle, 0, 0, 0, 0);
        bus.write_long(TEST_SP, dst_handle);
        bus.write_long(TEST_SP + 4, src_handle);
        let result = d.dispatch_quickdraw(true, 0x0DC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        // Verify dst has src data
        assert_eq!(bus.read_word(dst_rgn + 2) as i16, 5);
        assert_eq!(bus.read_word(dst_rgn + 4) as i16, 10);
    }

    #[test]
    fn copyrgn_copies_complex_region_structure_and_resizes_destination_storage() {
        // IM:I I-183: CopyRgn duplicates the mathematical structure of the
        // source region; complex scanline data is part of that structure.
        let (mut d, mut cpu, mut bus) = setup();
        let src = make_complex_rgn(
            &mut bus,
            (0, 0, 4, 6),
            &[
                0,
                0,
                6,
                super::REGION_STOP,
                1,
                2,
                4,
                super::REGION_STOP,
                3,
                2,
                4,
                super::REGION_STOP,
                4,
                0,
                6,
                super::REGION_STOP,
                super::REGION_STOP,
            ],
        );
        let src_size = bus.read_word(bus.read_long(src)) as u32;
        let dst_ptr = bus.alloc(10);
        let dst = bus.alloc(4);
        make_rgn(&mut bus, dst_ptr, dst, 9, 8, 7, 6);

        bus.write_long(TEST_SP, dst);
        bus.write_long(TEST_SP + 4, src);
        let result = d.dispatch_quickdraw(true, 0x0DC, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_ne!(
            bus.read_long(dst),
            dst_ptr,
            "complex CopyRgn should resize the 10-byte destination storage"
        );
        assert_eq!(bus.get_alloc_size(bus.read_long(dst)), Some(src_size));
        assert_rgn_bytes_equal(&bus, dst, src);
        assert!(TrapDispatcher::region_contains_point(&bus, dst, 1, 1));
        assert!(
            !TrapDispatcher::region_contains_point(&bus, dst, 1, 3),
            "complex region hole should survive CopyRgn"
        );
        assert!(TrapDispatcher::region_contains_point(&bus, dst, 1, 4));
    }

    #[test]
    fn bitmaptoregion_consumes_bitmap_and_region_arguments_and_writes_noerr_result() {
        // Imaging With QuickDraw (1994), p. 2-49: BitMapToRegion returns
        // OSErr and consumes one BitMap plus one RgnHandle argument.
        let (mut d, mut cpu, mut bus) = setup();

        let bitmap_ptr = 0x300000u32;
        let bitmap_data = 0x300100u32;
        bus.write_byte(bitmap_data, 0x00);
        write_bitmap_record(&mut bus, bitmap_ptr, bitmap_data, 1, 0, 0, 1, 8);

        let rgn_data = 0x300200u32;
        let rgn_handle = 0x300240u32;
        make_rgn(&mut bus, rgn_data, rgn_handle, 1, 2, 3, 4);

        bus.write_long(TEST_SP, bitmap_ptr);
        bus.write_long(TEST_SP + 4, rgn_handle);
        bus.write_word(TEST_SP + 8, 0xFFFF);

        let result = d.dispatch_quickdraw(true, 0x0D7, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8), 0);
    }

    #[test]
    fn bitmaptoregion_nonempty_1bpp_bitmap_sets_region_bbox_to_ink_bounds() {
        // Imaging With QuickDraw (1994), p. 2-49: BitMapToRegion derives
        // region coverage from source bitmap inked pixels.
        let (mut d, mut cpu, mut bus) = setup();

        let bitmap_ptr = 0x300000u32;
        let bitmap_data = 0x300100u32;
        bus.write_byte(bitmap_data, 0x00);
        bus.write_byte(bitmap_data + 1, 0x1C);
        bus.write_byte(bitmap_data + 2, 0x1C);
        bus.write_byte(bitmap_data + 3, 0x00);
        write_bitmap_record(&mut bus, bitmap_ptr, bitmap_data, 1, 10, 20, 14, 28);

        let rgn_data = 0x300200u32;
        let rgn_handle = 0x300240u32;
        make_rgn(&mut bus, rgn_data, rgn_handle, 0, 0, 1, 1);

        bus.write_long(TEST_SP, bitmap_ptr);
        bus.write_long(TEST_SP + 4, rgn_handle);
        bus.write_word(TEST_SP + 8, 0xFFFF);

        let result = d.dispatch_quickdraw(true, 0x0D7, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8), 0);
        assert_eq!(read_rgn_bbox(&bus, rgn_handle), (11, 23, 13, 26));
        assert_eq!(bus.read_word(rgn_data), 10);
    }

    #[test]
    fn bitmaptoregion_empty_source_overwrites_preseeded_nonempty_bbox() {
        // Imaging With QuickDraw (1994), p. 2-49: "The old region
        // contents are lost." Mirrors B3 of the
        // a8d7_bitmaptoregion_strict fixture: pre-seed the destination
        // region with a clearly non-empty bbox matching the bake setup
        // (SetRectRgn(rgn, 100, 200, 300, 400) → bbox (top=200,
        // left=100, bottom=400, right=300)), then invoke
        // BitMapToRegion with a 3x8 all-zero source bounded at
        // (30, 40, 33, 48). The trap must overwrite the pre-seeded
        // region with the empty region (bbox (0,0,0,0), rgnSize 10).
        let (mut d, mut cpu, mut bus) = setup();

        let bitmap_ptr = 0x300000u32;
        let bitmap_data = 0x300100u32;
        bus.write_byte(bitmap_data, 0x00);
        bus.write_byte(bitmap_data + 1, 0x00);
        bus.write_byte(bitmap_data + 2, 0x00);
        write_bitmap_record(&mut bus, bitmap_ptr, bitmap_data, 1, 30, 40, 33, 48);

        let rgn_data = 0x300200u32;
        let rgn_handle = 0x300240u32;
        make_rgn(&mut bus, rgn_data, rgn_handle, 200, 100, 400, 300);
        assert_eq!(read_rgn_bbox(&bus, rgn_handle), (200, 100, 400, 300));

        bus.write_long(TEST_SP, bitmap_ptr);
        bus.write_long(TEST_SP + 4, rgn_handle);
        bus.write_word(TEST_SP + 8, 0xFFFF);

        let result = d.dispatch_quickdraw(true, 0x0D7, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8), 0);
        assert_eq!(read_rgn_bbox(&bus, rgn_handle), (0, 0, 0, 0));
        assert_eq!(bus.read_word(rgn_data), 10);
    }

    #[test]
    fn bitmaptoregion_empty_bitmap_clears_region_to_empty() {
        // Imaging With QuickDraw (1994), p. 2-49: bitmap-derived regions
        // with no set source pixels are empty.
        let (mut d, mut cpu, mut bus) = setup();

        let bitmap_ptr = 0x300000u32;
        let bitmap_data = 0x300100u32;
        bus.write_byte(bitmap_data, 0x00);
        bus.write_byte(bitmap_data + 1, 0x00);
        bus.write_byte(bitmap_data + 2, 0x00);
        write_bitmap_record(&mut bus, bitmap_ptr, bitmap_data, 1, 30, 40, 33, 48);

        let rgn_data = 0x300200u32;
        let rgn_handle = 0x300240u32;
        make_rgn(&mut bus, rgn_data, rgn_handle, 7, 8, 9, 10);

        bus.write_long(TEST_SP, bitmap_ptr);
        bus.write_long(TEST_SP + 4, rgn_handle);
        bus.write_word(TEST_SP + 8, 0xFFFF);

        let result = d.dispatch_quickdraw(true, 0x0D7, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8), 0);
        assert_eq!(read_rgn_bbox(&bus, rgn_handle), (0, 0, 0, 0));
        assert_eq!(bus.read_word(rgn_data), 10);
    }

    #[test]
    fn sectrgn_preserves_complex_scanline_intersection() {
        // Inside Macintosh Volume I (1985), p. I-184: SectRgn computes
        // the geometric intersection of two regions, not only their bboxes.
        let (mut d, mut cpu, mut bus) = setup();

        let complex = make_complex_rgn(
            &mut bus,
            (0, 0, 4, 6),
            &[
                0,
                0,
                6,
                super::REGION_STOP,
                1,
                2,
                4,
                super::REGION_STOP,
                3,
                2,
                4,
                super::REGION_STOP,
                4,
                0,
                6,
                super::REGION_STOP,
                super::REGION_STOP,
            ],
        );
        let clip_data = bus.alloc(10);
        let clip = bus.alloc(4);
        make_rgn(&mut bus, clip_data, clip, 1, 1, 3, 5);
        let dst_data = bus.alloc(10);
        let dst = bus.alloc(4);
        make_rgn(&mut bus, dst_data, dst, 0, 0, 0, 0);

        bus.write_long(TEST_SP, dst);
        bus.write_long(TEST_SP + 4, clip);
        bus.write_long(TEST_SP + 8, complex);

        let result = d.dispatch_quickdraw(true, 0x0E4, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(read_rgn_bbox(&bus, dst), (1, 1, 3, 5));
        assert!(
            bus.read_word(bus.read_long(dst)) > super::REGION_HEADER_SIZE as u16,
            "intersection should stay complex, not collapse to its bbox"
        );
        assert!(TrapDispatcher::region_contains_point(&bus, dst, 1, 1));
        assert!(
            !TrapDispatcher::region_contains_point(&bus, dst, 1, 3),
            "hole inside the source region must remain clipped out"
        );
        assert!(TrapDispatcher::region_contains_point(&bus, dst, 1, 4));
    }

    #[test]
    fn offsetrgn_shifts_complex_region_scanlines() {
        // Inside Macintosh Volume I (1985), p. I-183: OffsetRgn moves the
        // region shape. Complex scanline coordinates must move with the bbox.
        let (mut d, mut cpu, mut bus) = setup();
        let rgn = make_complex_rgn(
            &mut bus,
            (0, 0, 3, 6),
            &[
                0,
                0,
                6,
                super::REGION_STOP,
                1,
                2,
                4,
                super::REGION_STOP,
                2,
                2,
                4,
                super::REGION_STOP,
                3,
                0,
                6,
                super::REGION_STOP,
                super::REGION_STOP,
            ],
        );

        bus.write_word(TEST_SP, 20u16); // dv
        bus.write_word(TEST_SP + 2, 10u16); // dh
        bus.write_long(TEST_SP + 4, rgn);

        let result = d.dispatch_quickdraw(true, 0x0E0, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(read_rgn_bbox(&bus, rgn), (20, 10, 23, 16));
        assert!(TrapDispatcher::region_contains_point(&bus, rgn, 21, 11));
        assert!(
            !TrapDispatcher::region_contains_point(&bus, rgn, 21, 13),
            "the shifted interior hole must remain a hole"
        );
        assert!(TrapDispatcher::region_contains_point(&bus, rgn, 21, 14));
        assert!(!TrapDispatcher::region_contains_point(&bus, rgn, 1, 1));
    }

    #[test]
    fn insetrgn_shrinks_complex_region_scanlines() {
        // Imaging With QuickDraw (1994), p. 3-93: InsetRgn moves the region
        // boundary inward. Complex scanline edge data must move with the bbox.
        let (mut d, mut cpu, mut bus) = setup();
        let rgn = bus.alloc(4);
        let rows = vec![
            vec![0, 10],
            vec![0, 10],
            vec![0, 10],
            vec![2, 8],
            vec![2, 8],
            vec![0, 10],
            vec![0, 10],
            vec![0, 10],
        ];
        assert!(TrapDispatcher::write_region_from_rows(
            &mut bus, rgn, 0, &rows
        ));
        assert!(
            bus.read_word(bus.read_long(rgn)) > super::REGION_HEADER_SIZE as u16,
            "test fixture should exercise the complex-region path"
        );

        bus.write_word(TEST_SP, 1u16); // dv
        bus.write_word(TEST_SP + 2, 1u16); // dh
        bus.write_long(TEST_SP + 4, rgn);

        let result = d.dispatch_quickdraw(true, 0x0E1, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(read_rgn_bbox(&bus, rgn), (1, 1, 7, 9));
        assert!(TrapDispatcher::region_contains_point(&bus, rgn, 1, 2));
        assert!(TrapDispatcher::region_contains_point(&bus, rgn, 2, 3));
        assert!(
            !TrapDispatcher::region_contains_point(&bus, rgn, 2, 2),
            "middle-band edge should move inward with the complex scanlines"
        );
        assert!(!TrapDispatcher::region_contains_point(&bus, rgn, 0, 5));
    }

    // EqualRgn ($A8E3) lives at slot 0x0E3, not 0x0E5. Prior to the
    // region-op slot audit these tests were pointed at the UnionRgn
    // slot and asserted 0xFFFF for TRUE; both were wrong. The strong
    // contract coverage for EqualRgn lives in the equalrgn_* family;
    // these two inline tests retain the original
    // happy-path / sad-path shape but dispatch to the correct slot
    // and assert the 0x0100 Pascal BOOL convention used elsewhere in
    // this file (EmptyRgn, PtInRgn, RectInRgn).
    #[test]
    fn test_equal_rgn_true() {
        let (mut d, mut cpu, mut bus) = setup();
        let rgn_a = 0x300000u32;
        let handle_a = 0x300100u32;
        make_rgn(&mut bus, rgn_a, handle_a, 10, 20, 100, 200);
        let rgn_b = 0x300200u32;
        let handle_b = 0x300300u32;
        make_rgn(&mut bus, rgn_b, handle_b, 10, 20, 100, 200);
        bus.write_long(TEST_SP, handle_b);
        bus.write_long(TEST_SP + 4, handle_a);
        bus.write_word(TEST_SP + 8, 0); // result slot
        let result = d.dispatch_quickdraw(true, 0x0E3, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8), 0x0100); // TRUE
    }

    #[test]
    fn test_equal_rgn_false() {
        let (mut d, mut cpu, mut bus) = setup();
        let rgn_a = 0x300000u32;
        let handle_a = 0x300100u32;
        make_rgn(&mut bus, rgn_a, handle_a, 10, 20, 100, 200);
        let rgn_b = 0x300200u32;
        let handle_b = 0x300300u32;
        make_rgn(&mut bus, rgn_b, handle_b, 0, 0, 50, 50);
        bus.write_long(TEST_SP, handle_b);
        bus.write_long(TEST_SP + 4, handle_a);
        bus.write_word(TEST_SP + 8, 0xFFFF);
        let result = d.dispatch_quickdraw(true, 0x0E3, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8), 0); // FALSE
    }

    // ==================== Text Attributes ====================

    #[test]
    fn test_text_font() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 4u16); // Geneva
        let result = d.dispatch_quickdraw(true, 0x087, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(d.tx_font, 4);
    }

    #[test]
    fn test_text_face() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 1u16); // bold
        let result = d.dispatch_quickdraw(true, 0x088, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(d.tx_face, 1);
    }

    #[test]
    fn test_text_face_high_byte_style() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 0x0100u16); // bold in high byte (MPW C style arg shape)
        let result = d.dispatch_quickdraw(true, 0x088, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(d.tx_face, 1);
    }

    #[test]
    fn test_text_mode() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 2u16); // srcOr
        let result = d.dispatch_quickdraw(true, 0x089, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(d.tx_mode, 2);
    }

    #[test]
    fn textmode_sets_theport_txmode_and_pops_mode_word() {
        // IM:I (1985), p. I-171: TextMode writes thePort^.txMode.
        let (mut d, mut cpu, mut bus) = setup();
        let port = bus.alloc(128);
        bus.write_word(port + 6, 0); // classic GrafPort shape
        d.set_current_port_state(&mut bus, &mut cpu, port, None);
        bus.write_word(TEST_SP, 3u16); // srcXor
        let result = d.dispatch_quickdraw(true, 0x089, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(d.tx_mode, 3);
        assert_eq!(bus.read_word(port + 72) as i16, 3);
    }

    #[test]
    fn test_text_size() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 24u16);
        let result = d.dispatch_quickdraw(true, 0x08A, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(d.tx_size, 24);
    }

    #[test]
    fn test_get_font_info() {
        let (mut d, mut cpu, mut bus) = setup();
        let info_ptr = 0x300000u32;
        bus.write_long(TEST_SP, info_ptr);
        let result = d.dispatch_quickdraw(true, 0x08B, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        // Verify 4 words were written (ascent, descent, widMax, leading)
        // Just check they are reasonable (non-negative)
        let ascent = bus.read_word(info_ptr) as i16;
        assert!(ascent >= 0);
    }

    #[test]
    fn getfontname_known_font_returns_pascal_name() {
        // IM:I (1985), p. I-223: known font IDs return the font family name.
        let (mut d, mut cpu, mut bus) = setup();
        let name_ptr = 0x300000u32;
        bus.write_long(TEST_SP, name_ptr);
        bus.write_word(TEST_SP + 4, 3u16); // Geneva

        let result = d.dispatch_quickdraw(true, 0x0FF, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);

        let len = bus.read_byte(name_ptr) as usize;
        let mut bytes = vec![0u8; len];
        for (i, byte) in bytes.iter_mut().enumerate() {
            *byte = bus.read_byte(name_ptr + 1 + i as u32);
        }
        assert_eq!(std::str::from_utf8(&bytes).unwrap(), "Geneva");
    }

    #[test]
    fn getfontname_unknown_font_returns_empty_string() {
        // IM:I (1985), p. I-223: missing font IDs return an empty string.
        let (mut d, mut cpu, mut bus) = setup();
        let name_ptr = 0x300000u32;
        bus.write_byte(name_ptr, 0xFF);
        bus.write_long(TEST_SP, name_ptr);
        bus.write_word(TEST_SP + 4, 0x7FFFu16);

        let result = d.dispatch_quickdraw(true, 0x0FF, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(bus.read_byte(name_ptr), 0);
    }

    #[test]
    fn getfontname_loaded_fond_returns_resource_name() {
        let (mut d, mut cpu, mut bus) = setup();
        let mut file = ResourceFileMap::default();
        file.named
            .insert((*b"FOND", "twiwindy".to_string()), (700, 0x1234));
        d.resources = Some(LoadedResources {
            files: HashMap::from([(0, file)]),
            names: HashMap::from([(0, "Application".to_string())]),
            search_order: vec![0],
            current_file: 0,
        });
        let name_ptr = 0x300000u32;
        bus.write_long(TEST_SP, name_ptr);
        bus.write_word(TEST_SP + 4, 700u16);

        let result = d.dispatch_quickdraw(true, 0x0FF, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);

        let len = bus.read_byte(name_ptr) as usize;
        let mut bytes = vec![0u8; len];
        for (i, byte) in bytes.iter_mut().enumerate() {
            *byte = bus.read_byte(name_ptr + 1 + i as u32);
        }
        assert_eq!(std::str::from_utf8(&bytes).unwrap(), "twiwindy");
    }

    #[test]
    fn getfontname_pops_six_bytes() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 0u16);

        let result = d.dispatch_quickdraw(true, 0x0FF, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
    }

    #[test]
    fn getfnum_known_font_returns_family_id() {
        // IM:I (1985), p. I-223: known font names return the font number.
        let (mut d, mut cpu, mut bus) = setup();
        let num_ptr = 0x300000u32;
        let name_ptr = 0x300100u32;
        bus.write_word(num_ptr, 0xFFFF);
        bus.write_byte(name_ptr, 6); // "Geneva"
        bus.write_byte(name_ptr + 1, b'G');
        bus.write_byte(name_ptr + 2, b'e');
        bus.write_byte(name_ptr + 3, b'n');
        bus.write_byte(name_ptr + 4, b'e');
        bus.write_byte(name_ptr + 5, b'v');
        bus.write_byte(name_ptr + 6, b'a');
        bus.write_long(TEST_SP, num_ptr);
        bus.write_long(TEST_SP + 4, name_ptr);

        let result = d.dispatch_quickdraw(true, 0x100, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(num_ptr) as i16, 3);
    }

    #[test]
    fn getfnum_unknown_font_returns_zero() {
        // IM:I (1985), p. I-223: unknown font names return 0.
        let (mut d, mut cpu, mut bus) = setup();
        let num_ptr = 0x300000u32;
        let name_ptr = 0x300100u32;
        let name = b"NotARealFont";
        bus.write_word(num_ptr, 0xFFFF);
        bus.write_byte(name_ptr, name.len() as u8);
        for (i, b) in name.iter().enumerate() {
            bus.write_byte(name_ptr + 1 + i as u32, *b);
        }
        bus.write_long(TEST_SP, num_ptr);
        bus.write_long(TEST_SP + 4, name_ptr);

        let result = d.dispatch_quickdraw(true, 0x100, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(num_ptr), 0);
    }

    #[test]
    fn getfnum_loaded_fond_returns_family_id() {
        let (mut d, mut cpu, mut bus) = setup();
        let mut file = ResourceFileMap::default();
        file.named
            .insert((*b"FOND", "twiapple".to_string()), (701, 0x1234));
        d.resources = Some(LoadedResources {
            files: HashMap::from([(0, file)]),
            names: HashMap::from([(0, "Application".to_string())]),
            search_order: vec![0],
            current_file: 0,
        });
        let num_ptr = 0x300000u32;
        let name_ptr = 0x300100u32;
        let name = b"  TwiApple ";
        bus.write_word(num_ptr, 0xFFFF);
        bus.write_byte(name_ptr, name.len() as u8);
        for (i, b) in name.iter().enumerate() {
            bus.write_byte(name_ptr + 1 + i as u32, *b);
        }
        bus.write_long(TEST_SP, num_ptr);
        bus.write_long(TEST_SP + 4, name_ptr);

        let result = d.dispatch_quickdraw(true, 0x100, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(num_ptr) as i16, 701);
    }

    #[test]
    fn getfnum_name_match_is_case_insensitive() {
        let (mut d, mut cpu, mut bus) = setup();
        let num_ptr = 0x300000u32;
        let name_ptr = 0x300100u32;
        let name = b"  geNeVa  ";
        bus.write_word(num_ptr, 0xFFFF);
        bus.write_byte(name_ptr, name.len() as u8);
        for (i, b) in name.iter().enumerate() {
            bus.write_byte(name_ptr + 1 + i as u32, *b);
        }
        bus.write_long(TEST_SP, num_ptr);
        bus.write_long(TEST_SP + 4, name_ptr);

        let result = d.dispatch_quickdraw(true, 0x100, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(num_ptr) as i16, 3);
    }

    #[test]
    fn getfnum_pops_eight_bytes() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0);
        bus.write_long(TEST_SP + 4, 0);

        let result = d.dispatch_quickdraw(true, 0x100, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
    }

    #[test]
    fn slopefromangle_function_signature_writes_fixed_result_slot_and_pops_two_bytes() {
        // Inside Macintosh Volume I (1985), p. I-192:
        // FUNCTION SlopeFromAngle(angle: INTEGER): Fixed.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 30u16);
        bus.write_long(TEST_SP + 2, 0xDEAD_BEEF);

        let result = d.dispatch_quickdraw(true, 0x0BC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_ne!(
            bus.read_long(TEST_SP + 2),
            0xDEAD_BEEF,
            "SlopeFromAngle should write a Fixed result at the post-pop slot"
        );
    }

    #[test]
    fn slopefromangle_zero_is_zero() {
        // Inside Macintosh Volume I (1985), p. I-192 note: SlopeFromAngle(0)=0.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 0);
        bus.write_long(TEST_SP + 2, 0xFFFF_FFFF);

        let result = d.dispatch_quickdraw(true, 0x0BC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(bus.read_long(TEST_SP + 2), 0);
    }

    #[test]
    fn slopefromangle_45_is_negative_one_fixed() {
        // Inside Macintosh Volume I (1985), p. I-192 Figure 5.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 45u16);

        let result = d.dispatch_quickdraw(true, 0x0BC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(bus.read_long(TEST_SP + 2), 0xFFFF_0000);
    }

    #[test]
    fn slopefromangle_135_is_positive_one_fixed() {
        // Inside Macintosh Volume I (1985), p. I-192 angle convention.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 135u16);

        let result = d.dispatch_quickdraw(true, 0x0BC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(bus.read_long(TEST_SP + 2), 0x0001_0000);
    }

    #[test]
    fn slopefromangle_treats_angle_mod_180() {
        // Inside Macintosh Volume I (1985), p. I-192: input angle is MOD 180.
        let (mut d, mut cpu, mut bus) = setup();

        bus.write_word(TEST_SP, 45u16);
        let result = d.dispatch_quickdraw(true, 0x0BC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let slope_45 = bus.read_long(TEST_SP + 2);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 225u16);
        let result = d.dispatch_quickdraw(true, 0x0BC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let slope_225 = bus.read_long(TEST_SP + 2);

        assert_eq!(slope_225, slope_45);
    }

    #[test]
    fn anglefromslope_function_signature_writes_integer_result_slot_and_pops_four_bytes() {
        // Inside Macintosh Volume I (1985), p. I-192:
        // FUNCTION AngleFromSlope(slope: Fixed): INTEGER.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0xFFFF_0000);
        bus.write_word(TEST_SP + 4, 0xABCD);

        let result = d.dispatch_quickdraw(true, 0x0C4, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_ne!(
            bus.read_word(TEST_SP + 4),
            0xABCD,
            "AngleFromSlope should write an INTEGER result at the post-pop slot"
        );
    }

    #[test]
    fn anglefromslope_zero_returns_180() {
        // Inside Macintosh Volume I (1985), p. I-192 note: AngleFromSlope(0)=180.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0);

        let result = d.dispatch_quickdraw(true, 0x0C4, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(TEST_SP + 4) as i16, 180);
    }

    #[test]
    fn anglefromslope_negative_one_returns_45() {
        // Inside Macintosh Volume I (1985), p. I-192 Figure 5.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0xFFFF_0000);

        let result = d.dispatch_quickdraw(true, 0x0C4, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(TEST_SP + 4) as i16, 45);
    }

    #[test]
    fn anglefromslope_positive_one_returns_135() {
        // Inside Macintosh Volume I (1985), p. I-192 angle convention.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x0001_0000);

        let result = d.dispatch_quickdraw(true, 0x0C4, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(TEST_SP + 4) as i16, 135);
    }

    #[test]
    fn slopefromangle_anglefromslope_round_trip() {
        // Inside Macintosh Volume I (1985), p. I-192: AngleFromSlope(SlopeFromAngle(x))
        // is guaranteed within one degree for non-zero x.
        let (mut d, mut cpu, mut bus) = setup();
        let sample_angles: [i16; 6] = [30, 45, 60, 120, 135, 150];

        for angle in sample_angles {
            cpu.write_reg(Register::A7, TEST_SP);
            bus.write_word(TEST_SP, angle as u16);
            let slope_result = d.dispatch_quickdraw(true, 0x0BC, &mut cpu, &mut bus);
            assert!(slope_result.unwrap().is_ok());
            let slope = bus.read_long(TEST_SP + 2);

            cpu.write_reg(Register::A7, TEST_SP);
            bus.write_long(TEST_SP, slope);
            let angle_result = d.dispatch_quickdraw(true, 0x0C4, &mut cpu, &mut bus);
            assert!(angle_result.unwrap().is_ok());
            let round_trip = bus.read_word(TEST_SP + 4) as i16;
            let delta = (round_trip - angle).abs();
            assert!(
                delta <= 1,
                "round-trip angle {} -> {} exceeded 1 degree tolerance",
                angle,
                round_trip
            );
        }
    }

    #[test]
    fn test_string_width() {
        let (mut d, mut cpu, mut bus) = setup();
        let str_ptr = 0x300000u32;
        // Pascal string "Hi"
        bus.write_byte(str_ptr, 2); // length
        bus.write_byte(str_ptr + 1, b'H');
        bus.write_byte(str_ptr + 2, b'i');
        bus.write_long(TEST_SP, str_ptr);
        let result = d.dispatch_quickdraw(true, 0x08C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        let width = bus.read_word(TEST_SP + 4) as i16;
        assert!(width >= 0);
    }

    #[test]
    fn test_char_width() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, b'A' as u16);
        let result = d.dispatch_quickdraw(true, 0x08D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        let width = bus.read_word(TEST_SP + 2) as i16;
        assert!(width >= 0);
    }

    #[test]
    fn charwidth_returns_pen_advance_for_drawchar_of_same_character() {
        // IM:I (1985), p. I-173: CharWidth reports the amount DrawChar adds to pen.h.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        d.pn_loc = (120, 40);

        bus.write_word(TEST_SP, b'W' as u16);
        let result = d.dispatch_quickdraw(true, 0x08D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        let expected_advance = bus.read_word(TEST_SP + 2) as i16;
        assert_eq!(d.pn_loc, (120, 40));

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, b'W' as u16);
        let start = d.pn_loc;
        let result = d.dispatch_quickdraw(true, 0x083, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(d.pn_loc.0, start.0);
        assert_eq!(d.pn_loc.1 - start.1, expected_advance);
    }

    #[test]
    fn test_text_width() {
        let (mut d, mut cpu, mut bus) = setup();
        let buf = 0x300000u32;
        bus.write_byte(buf, b'H');
        bus.write_byte(buf + 1, b'i');
        bus.write_word(TEST_SP, 2u16); // byte_count
        bus.write_word(TEST_SP + 2, 0u16); // first_byte
        bus.write_long(TEST_SP + 4, buf); // text_buf
        let result = d.dispatch_quickdraw(true, 0x086, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        let width = bus.read_word(TEST_SP + 8) as i16;
        assert!(width >= 0);
    }

    #[test]
    fn textwidth_returns_sum_of_charwidths_for_selected_text_slice() {
        // IM:I (1985), p. I-173: TextWidth sums CharWidth over firstByte..byteCount.
        let (mut d, mut cpu, mut bus) = setup();
        let buf = 0x300000u32;
        bus.write_byte(buf, b'A');
        bus.write_byte(buf + 1, b'B');
        bus.write_byte(buf + 2, b'C');

        bus.write_word(TEST_SP, b'B' as u16);
        let result = d.dispatch_quickdraw(true, 0x08D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let width_b = bus.read_word(TEST_SP + 2) as i16;

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, b'C' as u16);
        let result = d.dispatch_quickdraw(true, 0x08D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let width_c = bus.read_word(TEST_SP + 2) as i16;

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 2u16); // byteCount
        bus.write_word(TEST_SP + 2, 1u16); // firstByte
        bus.write_long(TEST_SP + 4, buf);
        let result = d.dispatch_quickdraw(true, 0x086, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8) as i16, width_b + width_c);
    }

    #[derive(Debug, PartialEq, Eq)]
    struct QuickDrawTextMetricResults {
        font_info: [u16; 4],
        get_font_info_stack: u32,
        get_font_info_tail: u32,
        string_width: u16,
        string_width_stack: u32,
        string_width_tail: u16,
        char_width: u16,
        char_width_stack: u32,
        char_width_tail: u16,
        text_width: u16,
        text_width_stack: u32,
        text_width_tail: u16,
        measure_locs: [u16; 5],
        measure_text_stack: u32,
        measure_text_tail: u16,
    }

    fn quickdraw_text_metric_results_for_theme(theme_id: UiThemeId) -> QuickDrawTextMetricResults {
        let (mut d, mut cpu, mut bus) = setup();
        d.set_ui_theme_id(theme_id);
        d.tx_font = 3; // Geneva
        d.tx_size = 14;
        const SAMPLE: &[u8] = b"WIDE";

        let info_ptr = 0x300100u32;
        for off in [0u32, 2, 4, 6] {
            bus.write_word(info_ptr + off, 0xDEAD);
        }
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, info_ptr);
        bus.write_long(TEST_SP + 4, 0xCAFE_BABE);
        d.dispatch_quickdraw(true, 0x08B, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let font_info = [
            bus.read_word(info_ptr),
            bus.read_word(info_ptr + 2),
            bus.read_word(info_ptr + 4),
            bus.read_word(info_ptr + 6),
        ];
        let get_font_info_stack = cpu.read_reg(Register::A7);
        let get_font_info_tail = bus.read_long(TEST_SP + 4);

        let string_ptr = 0x300200u32;
        bus.write_byte(string_ptr, SAMPLE.len() as u8);
        for (idx, byte) in SAMPLE.iter().enumerate() {
            bus.write_byte(string_ptr + 1 + idx as u32, *byte);
        }
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, string_ptr);
        bus.write_word(TEST_SP + 4, 0xBEEF);
        bus.write_word(TEST_SP + 6, 0xCAFE);
        d.dispatch_quickdraw(true, 0x08C, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let string_width = bus.read_word(TEST_SP + 4);
        let string_width_stack = cpu.read_reg(Register::A7);
        let string_width_tail = bus.read_word(TEST_SP + 6);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, b'W' as u16);
        bus.write_word(TEST_SP + 2, 0xBEEF);
        bus.write_word(TEST_SP + 4, 0xCAFE);
        d.dispatch_quickdraw(true, 0x08D, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let char_width = bus.read_word(TEST_SP + 2);
        let char_width_stack = cpu.read_reg(Register::A7);
        let char_width_tail = bus.read_word(TEST_SP + 4);

        let text_ptr = 0x300300u32;
        for (idx, byte) in SAMPLE.iter().enumerate() {
            bus.write_byte(text_ptr + idx as u32, *byte);
        }
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, SAMPLE.len() as u16);
        bus.write_word(TEST_SP + 2, 0);
        bus.write_long(TEST_SP + 4, text_ptr);
        bus.write_word(TEST_SP + 8, 0xBEEF);
        bus.write_word(TEST_SP + 10, 0xCAFE);
        d.dispatch_quickdraw(true, 0x086, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let text_width = bus.read_word(TEST_SP + 8);
        let text_width_stack = cpu.read_reg(Register::A7);
        let text_width_tail = bus.read_word(TEST_SP + 10);

        let measure_locs_ptr = 0x300500u32;
        for idx in 0..=SAMPLE.len() {
            bus.write_word(measure_locs_ptr + (idx as u32) * 2, 0x7777);
        }
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, measure_locs_ptr);
        bus.write_long(TEST_SP + 4, text_ptr);
        bus.write_word(TEST_SP + 8, SAMPLE.len() as u16);
        bus.write_word(TEST_SP + 10, 0xCAFE);
        d.dispatch_quickdraw(true, 0x037, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let mut measure_locs = [0u16; 5];
        for (idx, slot) in measure_locs.iter_mut().enumerate() {
            *slot = bus.read_word(measure_locs_ptr + (idx as u32) * 2);
        }
        let measure_text_stack = cpu.read_reg(Register::A7);
        let measure_text_tail = bus.read_word(TEST_SP + 10);

        QuickDrawTextMetricResults {
            font_info,
            get_font_info_stack,
            get_font_info_tail,
            string_width,
            string_width_stack,
            string_width_tail,
            char_width,
            char_width_stack,
            char_width_tail,
            text_width,
            text_width_stack,
            text_width_tail,
            measure_locs,
            measure_text_stack,
            measure_text_tail,
        }
    }

    #[test]
    fn systemless_theme_does_not_change_quickdraw_text_metrics() {
        // IM:I I-173: CharWidth, StringWidth, TextWidth, and GetFontInfo
        // report guest-visible font metrics for the current GrafPort text
        // state. IM:IV IV-25 / IV-32: MeasureText writes count+1 pixel
        // offsets with the first entry 0. Theme chrome must not alter these
        // QuickDraw text measurements or their trap stack protocols.
        let classic = quickdraw_text_metric_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = quickdraw_text_metric_results_for_theme(UiThemeId::SystemlessDefault);

        assert!(classic.font_info.iter().any(|value| *value != 0));
        assert_eq!(classic.get_font_info_stack, TEST_SP + 4);
        assert_eq!(classic.get_font_info_tail, 0xCAFE_BABE);
        assert_eq!(classic.string_width, classic.text_width);
        assert!(classic.char_width > 0);
        assert_eq!(classic.string_width_stack, TEST_SP + 4);
        assert_eq!(classic.string_width_tail, 0xCAFE);
        assert_eq!(classic.char_width_stack, TEST_SP + 2);
        assert_eq!(classic.char_width_tail, 0xCAFE);
        assert_eq!(classic.text_width_stack, TEST_SP + 8);
        assert_eq!(classic.text_width_tail, 0xCAFE);
        assert_eq!(classic.measure_locs[0], 0);
        assert_eq!(classic.measure_locs[1], classic.char_width);
        assert_eq!(classic.measure_locs[4], classic.text_width);
        assert_eq!(classic.measure_text_stack, TEST_SP + 10);
        assert_eq!(classic.measure_text_tail, 0xCAFE);
        assert_eq!(
            themed, classic,
            "systemless-default must not change QuickDraw text metrics or stack protocol"
        );
    }

    #[test]
    fn stdtxmeas_identity_scaling_matches_textwidth_and_getfontinfo() {
        // IM:I (1985), p. I-199 / Text 1993, pp. 3-99..3-100:
        // StdTxMeas with numer=(1,1), denom=(1,1) measures the same
        // unscaled width as TextWidth and returns current-font metrics
        // through the FontInfo VAR parameter.
        let (mut d, mut cpu, mut bus) = setup();
        let text_buf = 0x300000u32;
        let numer_ptr = 0x300100u32;
        let denom_ptr = 0x300104u32;
        let info_ptr = 0x300108u32;
        let expected_info_ptr = 0x300200u32;
        const SAMPLE: &[u8] = b"WIDE MIX";

        for (i, byte) in SAMPLE.iter().enumerate() {
            bus.write_byte(text_buf + i as u32, *byte);
        }
        bus.write_word(numer_ptr, 1); // v
        bus.write_word(numer_ptr + 2, 1); // h
        bus.write_word(denom_ptr, 1); // v
        bus.write_word(denom_ptr + 2, 1); // h
        for off in [0u32, 2, 4, 6] {
            bus.write_word(info_ptr + off, 0xDEAD);
            bus.write_word(expected_info_ptr + off, 0xBEEF);
        }

        bus.write_long(TEST_SP, info_ptr);
        bus.write_long(TEST_SP + 4, denom_ptr);
        bus.write_long(TEST_SP + 8, numer_ptr);
        bus.write_long(TEST_SP + 12, text_buf);
        bus.write_word(TEST_SP + 16, SAMPLE.len() as u16);
        let result = d.dispatch_quickdraw(true, 0x0ED, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 18);
        let measured_width = bus.read_word(TEST_SP + 18) as i16;

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, SAMPLE.len() as u16);
        bus.write_word(TEST_SP + 2, 0);
        bus.write_long(TEST_SP + 4, text_buf);
        let result = d.dispatch_quickdraw(true, 0x086, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let expected_width = bus.read_word(TEST_SP + 8) as i16;

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, expected_info_ptr);
        let result = d.dispatch_quickdraw(true, 0x08B, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_eq!(measured_width, expected_width);
        for off in [0u32, 2, 4, 6] {
            assert_eq!(
                bus.read_word(info_ptr + off),
                bus.read_word(expected_info_ptr + off),
                "FontInfo field at offset {} should match GetFontInfo",
                off
            );
        }
    }

    #[test]
    fn stdtxmeas_applies_horizontal_scaling_to_result_width() {
        // Systemless HLE applies numer.h / denom.h to the returned width
        // while leaving the FontInfo metrics at current-font scale.
        let (mut d, mut cpu, mut bus) = setup();
        let text_buf = 0x300000u32;
        let numer_ptr = 0x300100u32;
        let denom_ptr = 0x300104u32;
        let info_ptr = 0x300108u32;
        const SAMPLE: &[u8] = b"AB";

        for (i, byte) in SAMPLE.iter().enumerate() {
            bus.write_byte(text_buf + i as u32, *byte);
        }
        bus.write_word(numer_ptr, 1); // v
        bus.write_word(numer_ptr + 2, 2); // h
        bus.write_word(denom_ptr, 1); // v
        bus.write_word(denom_ptr + 2, 1); // h

        bus.write_word(TEST_SP, SAMPLE.len() as u16);
        bus.write_word(TEST_SP + 2, 0);
        bus.write_long(TEST_SP + 4, text_buf);
        let result = d.dispatch_quickdraw(true, 0x086, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let unscaled_width = bus.read_word(TEST_SP + 8) as i16;

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, info_ptr);
        bus.write_long(TEST_SP + 4, denom_ptr);
        bus.write_long(TEST_SP + 8, numer_ptr);
        bus.write_long(TEST_SP + 12, text_buf);
        bus.write_word(TEST_SP + 16, SAMPLE.len() as u16);
        let result = d.dispatch_quickdraw(true, 0x0ED, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 18);
        assert_eq!(bus.read_word(TEST_SP + 18) as i16, unscaled_width * 2);
    }

    #[test]
    fn setfscaledisable_true_writes_one_byte_verbatim_to_fscale_disable_global() {
        // Inside Macintosh Volume IV (1986), p. IV-32; Inside Macintosh
        // Volume III (1985), p. III-227 lists FScaleDisable at byte
        // address $0A63 ("nonzero to disable font scaling, byte").
        // MPW LMSetFScaleDisable(v) expands to MOVE.B v, $0A63 — the
        // canonical low-memory accessor. The Mac Pascal BOOLEAN
        // convention places the value byte in the HIGH byte of the
        // 2-byte stack slot, so a Pascal TRUE pushes as the word
        // 0x0100 and the trap reads byte 0x01 at SP+0. Both Systemless
        // and BII write the byte verbatim (not normalised to 0xFF).
        let (mut d, mut cpu, mut bus) = setup();
        // Pre-poison $0A63 with a non-{0,1} sentinel so the assertion
        // proves the write happened (a no-op stub would leave 0xA5).
        bus.write_byte(0x0A63, 0xA5);
        // Adjacent sentinels guard against a wider-than-byte write.
        bus.write_byte(0x0A62, 0x77);
        bus.write_byte(0x0A64, 0x88);
        // Pascal BOOLEAN TRUE: high byte = 0x01.
        bus.write_word(TEST_SP, 0x0100);

        let result = d.dispatch_quickdraw(true, 0x034, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(0x0A63), 0x01);
        assert_eq!(bus.read_byte(0x0A62), 0x77);
        assert_eq!(bus.read_byte(0x0A64), 0x88);
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
    }

    #[test]
    fn setfscaledisable_false_writes_zero_byte_to_fscale_disable_global() {
        // Inside Macintosh Volume IV (1986), p. IV-32:
        // SetFScaleDisable(FALSE) reenables font scaling. Pascal
        // BOOLEAN FALSE encodes as byte 0x00 in the high byte of the
        // 2-byte stack slot. Pre-poison $0A63 with 0xC3 so a no-op
        // stub fails (would leave 0xC3 instead of 0x00).
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_byte(0x0A63, 0xC3);
        // Pascal BOOLEAN FALSE: high byte = 0x00.
        bus.write_word(TEST_SP, 0x0000);

        let result = d.dispatch_quickdraw(true, 0x034, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(0x0A63), 0x00);
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
    }

    #[test]
    fn setfscaledisable_consumes_two_byte_boolean_argument_and_balances_stack() {
        // Pascal PROCEDURE protocol: caller pushes one 2-byte BOOLEAN
        // argument; trap pops exactly 2 bytes; no function-result
        // slot is reserved. Net A7 delta from caller perspective is
        // zero across the push-args + trap + (no pop) sequence. The
        // unit test verifies the trap pops 2 bytes regardless of the
        // BOOLEAN value.
        let (mut d, mut cpu, mut bus) = setup();
        d.tx_font = 3;
        d.tx_size = 14;
        d.tx_face = 1;
        d.tx_mode = 2;
        bus.write_word(TEST_SP, 1); // TRUE

        let result = d.dispatch_quickdraw(true, 0x034, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        // Mutating FScaleDisable must not perturb unrelated text-port
        // state — the Font Manager re-evaluates font selection at the
        // next StringWidth/DrawText, not synchronously.
        assert_eq!(d.tx_font, 3);
        assert_eq!(d.tx_size, 14);
        assert_eq!(d.tx_face, 1);
        assert_eq!(d.tx_mode, 2);
    }

    #[test]
    fn getmasktable_writes_non_nil_pointer_to_a0_and_preserves_stack() {
        // Inside Macintosh Volume IV (1986), pp. IV-25..IV-26:
        // _GetMaskTable returns in A0 a pointer to the ROM mask table.
        // The trap consumes no stack arguments and does NOT use the
        // Pascal FUNCTION result-slot protocol — the result lives only
        // in register A0 per the IM:IV trap-macro summary (p. IV-26).
        // MPW Universal Headers Quickdraw.h reflects this with
        // `#pragma parameter __A0 GetMaskTable`.
        //
        // The contract test pre-poisons A0 with a sentinel, pre-poisons
        // the caller's first stack slot, then dispatches $A836 and
        // asserts: (a) A0 is overwritten with a non-NIL address that
        // differs from the sentinel; (b) the caller stack slot is
        // untouched; (c) SP is unchanged (no Pascal frame consumed).
        let (mut d, mut cpu, mut bus) = setup();
        cpu.write_reg(Register::A0, 0xDEAD_BEEF);
        bus.write_long(TEST_SP, 0xCAFE_BABE);
        let sp_before = cpu.read_reg(Register::A7);

        let result = d.dispatch_quickdraw(true, 0x036, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        let a0 = cpu.read_reg(Register::A0);
        assert_ne!(a0, 0xDEAD_BEEF, "GetMaskTable must overwrite A0");
        assert_ne!(a0, 0, "GetMaskTable must return a non-NIL pointer in A0");
        assert_eq!(
            bus.read_long(TEST_SP),
            0xCAFE_BABE,
            "GetMaskTable must not write to the caller stack frame"
        );
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_before,
            "GetMaskTable consumes no stack arguments"
        );
    }

    #[test]
    fn getmasktable_table_contents_match_inside_macintosh_iv_layout() {
        // Inside Macintosh Volume IV (1986), pp. IV-25..IV-26 documents
        // three 16-word sub-tables at the returned pointer:
        //   right masks at offset  0 : 0x0000, 0x8000, 0xC000, ...
        //   left masks  at offset 32 : 0xFFFF, 0x7FFF, 0x3FFF, ...
        //   bit masks   at offset 64 : 0x8000, 0x4000, 0x2000, ...
        // The contract test asserts the first and last entry of each
        // sub-table matches the IM:IV constants.
        let (mut d, mut cpu, mut bus) = setup();
        let result = d.dispatch_quickdraw(true, 0x036, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let base = cpu.read_reg(Register::A0);
        // Right masks: first entry 0x0000, last entry 0xFFFE
        assert_eq!(bus.read_word(base), 0x0000);
        assert_eq!(bus.read_word(base + 30), 0xFFFE);
        // Left masks: first entry 0xFFFF, last entry 0x0001
        assert_eq!(bus.read_word(base + 32), 0xFFFF);
        assert_eq!(bus.read_word(base + 62), 0x0001);
        // Bit masks: first entry 0x8000, last entry 0x0001
        assert_eq!(bus.read_word(base + 64), 0x8000);
        assert_eq!(bus.read_word(base + 94), 0x0001);

        // Second dispatch must return the SAME cached address (no
        // re-allocation, no leak).
        cpu.write_reg(Register::A0, 0);
        let result2 = d.dispatch_quickdraw(true, 0x036, &mut cpu, &mut bus);
        assert!(result2.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A0),
            base,
            "GetMaskTable must reuse the cached mask table address"
        );
    }

    #[test]
    fn fontmetrics_writes_fixed_ascent_descent_leading_widmax() {
        // Inside Macintosh Volume IV (1986), p. IV-32:
        // FontMetrics writes Fixed metrics into FMetricRec.
        let (mut d, mut cpu, mut bus) = setup();
        d.tx_font = 3; // Geneva
        d.tx_size = 14;
        let rec_ptr = 0x300000u32;
        for off in [0u32, 4, 8, 12, 16] {
            bus.write_long(rec_ptr + off, 0xA5A5A5A5);
        }
        bus.write_long(TEST_SP, rec_ptr);

        let result = d.dispatch_quickdraw(true, 0x035, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        let (face, scale) = crate::quickdraw::fonts::get_font_face_scaled(d.tx_font, d.tx_size);
        let metrics = face.metrics;
        let to_fixed = |v: i16| -> u32 { (v as i32 as u32) << 16 };
        assert_eq!(bus.read_long(rec_ptr), to_fixed(metrics.ascent * scale));
        assert_eq!(
            bus.read_long(rec_ptr + 4),
            to_fixed(metrics.descent * scale)
        );
        assert_eq!(
            bus.read_long(rec_ptr + 8),
            to_fixed(metrics.leading * scale)
        );
        assert_eq!(
            bus.read_long(rec_ptr + 12),
            to_fixed(metrics.wid_max * scale)
        );
        assert_ne!(
            bus.read_long(rec_ptr + 16),
            0xA5A5A5A5,
            "FontMetrics should write the wTabHandle slot"
        );
    }

    #[test]
    fn fontmetrics_consumes_fmetricrecptr_argument_and_pops_four_bytes() {
        // Inside Macintosh Volume IV (1986), p. IV-32:
        // PROCEDURE FontMetrics(VAR theMetrics: FMetricRec); MPW C
        // canonical declaration `EXTERN_API(void) FontMetrics(
        // FMetricRecPtr theMetrics) ONEWORDINLINE(0xA835)`. The caller
        // pushes a single 4-byte FMetricRecPtr; the trap pops 4 bytes
        // and reserves no function-result slot. Mirrors B3 of the
        // a835_fontmetrics_strict bake (Pascal PROCEDURE protocol).
        //
        // Pre-poisons every FMetricRec field with the strict bake's
        // distinct sentinels (0xCAFE000n + 0xDEADBEEF) and the slot
        // immediately above the arg frame (TEST_SP+4) with 0xCAFEBABE
        // so a stub that pops the wrong number of bytes or writes
        // through the wrong offset can be caught.
        let (mut d, mut cpu, mut bus) = setup();
        d.tx_font = 0; // systemFont (Chicago)
        d.tx_size = 12;
        let rec_ptr = 0x300000u32;
        bus.write_long(rec_ptr, 0xCAFE_0001);
        bus.write_long(rec_ptr + 4, 0xCAFE_0002);
        bus.write_long(rec_ptr + 8, 0xCAFE_0003);
        bus.write_long(rec_ptr + 12, 0xCAFE_0004);
        bus.write_long(rec_ptr + 16, 0xDEAD_BEEF);
        bus.write_long(TEST_SP, rec_ptr);
        bus.write_long(TEST_SP + 4, 0xCAFE_BABE);

        let result = d.dispatch_quickdraw(true, 0x035, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(
            bus.read_long(TEST_SP + 4),
            0xCAFE_BABE,
            "FontMetrics must not write past the 4-byte arg frame"
        );
        // Each of the 5 fields is overwritten (sentinels do not survive).
        assert_ne!(bus.read_long(rec_ptr), 0xCAFE_0001);
        assert_ne!(bus.read_long(rec_ptr + 4), 0xCAFE_0002);
        assert_ne!(bus.read_long(rec_ptr + 8), 0xCAFE_0003);
        assert_ne!(bus.read_long(rec_ptr + 12), 0xCAFE_0004);
        assert_ne!(bus.read_long(rec_ptr + 16), 0xDEAD_BEEF);
    }

    #[test]
    fn measuretext_writes_count_plus_one_offsets_with_first_entry_zero() {
        // Inside Macintosh Volume IV (1986), p. IV-25:
        // charLocs[0] is 0; array has count+1 entries of cumulative offsets.
        let (mut d, mut cpu, mut bus) = setup();
        let text_ptr = 0x300200u32;
        bus.write_byte(text_ptr, b'A');
        bus.write_byte(text_ptr + 1, b'B');
        bus.write_byte(text_ptr + 2, b'C');
        let char_locs = 0x300300u32;

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, b'A' as u16);
        let result = d.dispatch_quickdraw(true, 0x08D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let width_a = bus.read_word(TEST_SP + 2) as i16;

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, b'B' as u16);
        let result = d.dispatch_quickdraw(true, 0x08D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let width_b = bus.read_word(TEST_SP + 2) as i16;

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, b'C' as u16);
        let result = d.dispatch_quickdraw(true, 0x08D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let width_c = bus.read_word(TEST_SP + 2) as i16;

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, char_locs);
        bus.write_long(TEST_SP + 4, text_ptr);
        bus.write_word(TEST_SP + 8, 3);
        let result = d.dispatch_quickdraw(true, 0x037, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(char_locs) as i16, 0);
        assert_eq!(bus.read_word(char_locs + 2) as i16, width_a);
        assert_eq!(bus.read_word(char_locs + 4) as i16, width_a + width_b);
        assert_eq!(
            bus.read_word(char_locs + 6) as i16,
            width_a + width_b + width_c
        );
    }

    #[test]
    fn measuretext_consumes_count_textaddr_charlocs_arguments() {
        // Inside Macintosh Volume IV (1986), p. IV-32:
        // MeasureText(count: INTEGER; textAddr, charLocs: Ptr).
        let (mut d, mut cpu, mut bus) = setup();
        let text_ptr = 0x300400u32;
        let char_locs = 0x300500u32;
        bus.write_word(char_locs, 0x7F7F);
        bus.write_word(char_locs + 2, 0x7F7F);
        bus.write_long(TEST_SP, char_locs);
        bus.write_long(TEST_SP + 4, text_ptr);
        bus.write_word(TEST_SP + 8, 0);

        let result = d.dispatch_quickdraw(true, 0x037, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(bus.read_word(char_locs), 0);
        assert_eq!(
            bus.read_word(char_locs + 2),
            0x7F7F,
            "zero-count path should write only charLocs[0]"
        );
    }

    #[test]
    fn measuretext_negative_count_is_treated_as_empty_run() {
        // Inside Macintosh Volume IV (1986), p. IV-32:
        // MeasureText(count: INTEGER; textAddr, charLocs: Ptr).
        let (mut d, mut cpu, mut bus) = setup();
        let text_ptr = 0x300400u32;
        let char_locs = 0x300500u32;
        bus.write_word(char_locs, 0x7F7F);
        bus.write_word(char_locs + 2, 0x7F7F);
        bus.write_long(TEST_SP, char_locs);
        bus.write_long(TEST_SP + 4, text_ptr);
        bus.write_word(TEST_SP + 8, 0xFFFF);

        let result = d.dispatch_quickdraw(true, 0x037, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(bus.read_word(char_locs), 0);
        assert_eq!(
            bus.read_word(char_locs + 2),
            0x7F7F,
            "negative counts should not advance beyond charLocs[0]"
        );
    }

    // ==================== Text Drawing ====================

    #[test]
    fn drawchar_consumes_char_argument_word() {
        // Inside Macintosh Volume I (1985), p. I-172:
        // DrawChar(ch: CHAR) is a procedure with one 2-byte CHAR argument.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        d.pn_loc = (100, 50);
        bus.write_word(TEST_SP, b'A' as u16);
        let result = d.dispatch_quickdraw(true, 0x083, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
    }

    #[test]
    fn test_draw_string() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        d.pn_loc = (100, 50);
        let str_ptr = 0x300000u32;
        bus.write_byte(str_ptr, 2);
        bus.write_byte(str_ptr + 1, b'O');
        bus.write_byte(str_ptr + 2, b'K');
        bus.write_long(TEST_SP, str_ptr);
        let result = d.dispatch_quickdraw(true, 0x084, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn drawtext_draws_requested_slice_and_advances_pen_rightward() {
        // IM:I (1985), p. I-172: DrawText draws textBuf[firstByte..] and leaves pen to the right.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let text_ptr = 0x300000u32;
        bus.write_byte(text_ptr, b'X');
        bus.write_byte(text_ptr + 1, b'B');
        bus.write_byte(text_ptr + 2, b'C');
        bus.write_byte(text_ptr + 3, b'Y');

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 2u16); // byteCount
        bus.write_word(TEST_SP + 2, 1u16); // firstByte
        bus.write_long(TEST_SP + 4, text_ptr);
        let result = d.dispatch_quickdraw(true, 0x086, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let expected_width = bus.read_word(TEST_SP + 8) as i16;

        d.pn_loc = (90, 30);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 2u16); // byteCount
        bus.write_word(TEST_SP + 2, 1u16); // firstByte
        bus.write_long(TEST_SP + 4, text_ptr);
        let result = d.dispatch_quickdraw(true, 0x085, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(d.pn_loc.0, 90);
        assert_eq!(d.pn_loc.1, 30 + expected_width);
    }

    #[test]
    fn test_std_text() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        d.pn_loc = (100, 50);
        // StdText stack: denom(4) + numer(4) + textBuf(4) + byteCount(2) = 14 bytes.
        let text_ptr = 0x300000u32;
        bus.write_byte(text_ptr, b'H');
        bus.write_byte(text_ptr + 1, b'i');
        let start = d.pn_loc;

        // Identity scaling should advance the pen horizontally and keep the
        // baseline unchanged.
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 1); // denom.v
        bus.write_word(TEST_SP + 2, 1); // denom.h
        bus.write_word(TEST_SP + 4, 1); // numer.v
        bus.write_word(TEST_SP + 6, 1); // numer.h
        bus.write_long(TEST_SP + 8, text_ptr);
        bus.write_word(TEST_SP + 12, 2);
        let result = d.dispatch_quickdraw(true, 0x082, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 14);
        assert!(d.pn_loc.1 > start.1);
        let identity_advance = d.pn_loc.1 - start.1;

        // Uniform 2x scaling should advance the pen farther than identity.
        d.pn_loc = start;
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 1); // denom.v
        bus.write_word(TEST_SP + 2, 1); // denom.h
        bus.write_word(TEST_SP + 4, 2); // numer.v
        bus.write_word(TEST_SP + 6, 2); // numer.h
        bus.write_long(TEST_SP + 8, text_ptr);
        bus.write_word(TEST_SP + 12, 2);
        let result = d.dispatch_quickdraw(true, 0x082, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 14);
        assert!(d.pn_loc.1 > start.1);
        assert!(
            d.pn_loc.1 - start.1 > identity_advance,
            "scaled StdText should advance farther than identity-scale StdText"
        );
    }

    #[test]
    fn setstdprocs_consumes_qdprocs_pointer_and_writes_13_standard_slots() {
        // IM:I I-197..I-198: SetStdProcs fills all 13 QDProcs fields with
        // the standard bottleneck routines in record order.
        // Systemless writes synthesised $00F0AXXX markers that correspond to
        // those standard bottlenecks.
        let (mut d, mut cpu, mut bus) = setup();
        let procs_ptr = 0x300000u32;
        bus.write_long(TEST_SP, procs_ptr);
        let result = d.dispatch_quickdraw(true, 0x0EA, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        // QDProcs field order per IM:I-196. Each field = 0x00F00000 | trap.
        const EXPECTED: [u32; 13] = [
            0x00F0A882, // textProc (StdText)
            0x00F0A890, // lineProc (StdLine)
            0x00F0A8A0, // rectProc (StdRect)
            0x00F0A8AF, // rRectProc (StdRRect)
            0x00F0A8B6, // ovalProc (StdOval)
            0x00F0A8BD, // arcProc (StdArc)
            0x00F0A8C5, // polyProc (StdPoly)
            0x00F0A8D1, // rgnProc (StdRgn)
            0x00F0A8EB, // bitsProc (StdBits)
            0x00F0A8F1, // commentProc (StdComment) — corrected from $A89F (Unimplemented) per IM:I I-198
            0x00F0A8ED, // txMeasProc (StdTxMeas)
            0x00F0A8EE, // getPicProc (StdGetPic)
            0x00F0A8F0, // putPicProc (StdPutPic)
        ];
        for (i, expected) in EXPECTED.iter().enumerate() {
            assert_eq!(
                bus.read_long(procs_ptr + (i as u32) * 4),
                *expected,
                "QDProcs field {} should be synthesised trap marker",
                i
            );
        }
    }

    #[test]
    fn setstdcprocs_consumes_cqdprocs_pointer_and_writes_standard_slots() {
        // IM:V V-77 + V-91 / IWQD 7-82..7-83 / QuickTime 1993 3-137..3-138:
        // SetStdCProcs fills the 13 inherited bottleneck slots plus
        // opcodeProc and newProc1 (StdPix) with the standard Color
        // QuickDraw routines in CQDProcs field order.
        let (mut d, mut cpu, mut bus) = setup();
        let cprocs_ptr = 0x300100u32;
        for i in 0..20u32 {
            bus.write_long(cprocs_ptr + i * 4, 0xDEADBEEF);
        }
        bus.write_long(TEST_SP, cprocs_ptr);

        let result = d.dispatch_quickdraw(true, 0x24E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        const EXPECTED: [u32; 15] = [
            0x00F0A882, // textProc (StdText)
            0x00F0A890, // lineProc (StdLine)
            0x00F0A8A0, // rectProc (StdRect)
            0x00F0A8AF, // rRectProc (StdRRect)
            0x00F0A8B6, // ovalProc (StdOval)
            0x00F0A8BD, // arcProc (StdArc)
            0x00F0A8C5, // polyProc (StdPoly)
            0x00F0A8D1, // rgnProc (StdRgn)
            0x00F0A8EB, // bitsProc (StdBits)
            0x00F0A8F1, // commentProc (StdComment)
            0x00F0A8ED, // txMeasProc (StdTxMeas)
            0x00F0A8EE, // getPicProc (StdGetPic)
            0x00F0A8F0, // putPicProc (StdPutPic)
            0x00F0ABF8, // opcodeProc (StdOpcodeProc)
            0x00F05058, // newProc1 (StdPix synthetic marker)
        ];
        for (i, expected) in EXPECTED.iter().enumerate() {
            assert_eq!(
                bus.read_long(cprocs_ptr + (i as u32) * 4),
                *expected,
                "CQDProcs field {} should be synthesised trap marker",
                i
            );
        }
    }

    #[test]
    fn setstdcprocs_leaves_reserved_newproc2_through_newproc6_slots_nil() {
        // IM:V V-91 / IWQD 4-60..4-61 mark newProc2..newProc6 reserved.
        // BasiliskII 7.5.3 fills newProc1 with StdPix but leaves the
        // remaining reserved extension fields NIL.
        let (mut d, mut cpu, mut bus) = setup();
        let cprocs_ptr = 0x300100u32;
        for i in 0..20u32 {
            bus.write_long(cprocs_ptr + i * 4, 0xDEADBEEF);
        }
        bus.write_long(TEST_SP, cprocs_ptr);

        let result = d.dispatch_quickdraw(true, 0x24E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        for i in 15..20u32 {
            assert_eq!(
                bus.read_long(cprocs_ptr + i * 4),
                0,
                "reserved CQDProcs slot {} should remain NIL",
                i
            );
        }
    }

    #[test]
    fn fontdispatch_outline_preference_round_trips_through_set_and_get_selectors() {
        // Inside Macintosh: Text 1993, pp. 4-60 to 4-61:
        // SetOutlinePreferred and GetOutlinePreferred share _FontDispatch.
        let (mut d, mut cpu, mut bus) = setup();

        cpu.write_reg(Register::D0, 0x0001);
        bus.write_byte(TEST_SP, 1);
        let result = d.dispatch_quickdraw(true, 0x054, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert!(d.outline_preferred);

        cpu.write_reg(Register::D0, 0x0009);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_byte(TEST_SP, 0xCA);
        bus.write_byte(TEST_SP + 1, 0xFE);
        let result = d.dispatch_quickdraw(true, 0x054, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_eq!(bus.read_byte(TEST_SP), 1);
        assert_eq!(cpu.read_reg(Register::D0), 1);

        cpu.write_reg(Register::D0, 0x0001);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_byte(TEST_SP, 0);
        let result = d.dispatch_quickdraw(true, 0x054, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert!(!d.outline_preferred);

        cpu.write_reg(Register::D0, 0x0009);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_byte(TEST_SP, 0xCA);
        bus.write_byte(TEST_SP + 1, 0xFE);
        let result = d.dispatch_quickdraw(true, 0x054, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_eq!(bus.read_byte(TEST_SP), 0);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    #[test]
    fn fontdispatch_preserve_glyph_round_trips_through_set_and_get_selectors() {
        // Inside Macintosh: Text 1993, pp. 4-62 to 4-63:
        // SetPreserveGlyph and GetPreserveGlyph share _FontDispatch.
        let (mut d, mut cpu, mut bus) = setup();

        cpu.write_reg(Register::D0, 0x000A);
        bus.write_byte(TEST_SP, 1);
        let result = d.dispatch_quickdraw(true, 0x054, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert!(d.preserve_glyph);

        cpu.write_reg(Register::D0, 0x000B);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_byte(TEST_SP, 0xCA);
        bus.write_byte(TEST_SP + 1, 0xFE);
        let result = d.dispatch_quickdraw(true, 0x054, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_eq!(bus.read_byte(TEST_SP), 1);
        assert_eq!(cpu.read_reg(Register::D0), 1);

        cpu.write_reg(Register::D0, 0x000A);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_byte(TEST_SP, 0);
        let result = d.dispatch_quickdraw(true, 0x054, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert!(!d.preserve_glyph);

        cpu.write_reg(Register::D0, 0x000B);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_byte(TEST_SP, 0xCA);
        bus.write_byte(TEST_SP + 1, 0xFE);
        let result = d.dispatch_quickdraw(true, 0x054, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_eq!(bus.read_byte(TEST_SP), 0);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    #[test]
    fn stdline_consumes_point_argument_by_value() {
        // Inside Macintosh Volume I (1985), p. I-197:
        // StdLine(newPt: Point) consumes one by-value Point argument.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 42u16); // newPt.v
        bus.write_word(TEST_SP + 2, 84u16); // newPt.h

        let result = d.dispatch_quickdraw(true, 0x090, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn stdline_stub_preserves_non_stack_registers_in_hle_compromise_path() {
        // Inside Macintosh Volume I (1985), pp. I-197..I-198:
        // StdLine is a QDProcs bottleneck; Systemless's compromise path
        // must preserve non-stack registers while consuming the frame.
        let (mut d, mut cpu, mut bus) = setup();
        cpu.write_reg(Register::A0, 0x1111_2222);
        cpu.write_reg(Register::A1, 0x3333_4444);
        cpu.write_reg(Register::D0, 0x5555_6666);
        cpu.write_reg(Register::D1, 0x7777_8888);
        bus.write_word(TEST_SP, 1u16);
        bus.write_word(TEST_SP + 2, 2u16);

        let result = d.dispatch_quickdraw(true, 0x090, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(cpu.read_reg(Register::A0), 0x1111_2222);
        assert_eq!(cpu.read_reg(Register::A1), 0x3333_4444);
        assert_eq!(cpu.read_reg(Register::D0), 0x5555_6666);
        assert_eq!(cpu.read_reg(Register::D1), 0x7777_8888);
    }

    #[test]
    fn stdline_five_call_composition_pops_twenty_bytes_total() {
        // Inside Macintosh Volume I (1985), p. I-197 + Imaging With QuickDraw
        // 1994 p. 3-132: StdLine(newPt: Point) pops a fixed 4-byte Point
        // by-value frame regardless of the argument coordinates.  Mirrors B2
        // of the strict bake a890_stdline_strict — five successive StdLine
        // dispatches with varying Point coords each advance A7 by 4 bytes,
        // total 20 bytes.  The varying values defeat any value-dependent pop.
        let (mut d, mut cpu, mut bus) = setup();
        let coords: [(u16, u16); 5] = [
            (10, 20),
            (40, 60),
            (100, 150),
            (200, 300),
            (0xFFCE, 0xFFB5), // -50, -75 as two's-complement INTEGER
        ];
        for (i, (v, h)) in coords.iter().enumerate() {
            let base = TEST_SP + (i as u32) * 4;
            bus.write_word(base, *v);
            bus.write_word(base + 2, *h);
        }
        cpu.write_reg(Register::A7, TEST_SP);
        for _ in 0..5 {
            let result = d.dispatch_quickdraw(true, 0x090, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
        }
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 20);
    }

    #[test]
    fn stdrrect_consumes_verb_rect_and_corner_diameter_arguments() {
        // Inside Macintosh Volume I (1985), p. I-197 + Imaging With QuickDraw
        // 1994 p. 3-133: PROCEDURE StdRRect(verb: GrafVerb; r: Rect;
        // ovalWidth, ovalHeight: INTEGER) consumes a word + pointer + 2-word
        // frame.  MPW Universal Headers Quickdraw.h declares the C form
        // `EXTERN_API(void) StdRRect(GrafVerb verb, const Rect *r,
        // short ovalWidth, short ovalHeight) ONEWORDINLINE(0xA8AF)` —
        // 10-byte pop, no result slot.
        //
        // Engines-agree contract pinned by the strict bake
        // `a8af_a8bd_stdrrect_stdarc_strict` via empty-clipRgn + StackSpace
        // sandwich.
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300180u32;
        write_rect(&mut bus, rect_ptr, 10, 20, 40, 80);
        bus.write_word(TEST_SP, 0u16); // frame verb
        bus.write_long(TEST_SP + 2, rect_ptr);
        bus.write_word(TEST_SP + 6, 12u16); // ovalWidth
        bus.write_word(TEST_SP + 8, 16u16); // ovalHeight

        let result = d.dispatch_quickdraw(true, 0x0AF, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
    }

    #[test]
    fn stdrrect_five_verb_composition_pops_fifty_bytes_total() {
        // Mirrors B2 of the a8af_a8bd_stdrrect_stdarc_strict bake: 5
        // successive StdRRect dispatches with frame/paint/erase/invert/fill
        // verbs each pop their 10-byte frame, advancing A7 by 50 bytes total.
        // Varying ovalWidth/ovalHeight pairs across the verbs stresses that
        // the trap pops a fixed 10 bytes regardless of corner-diameter values.
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300190u32;
        write_rect(&mut bus, rect_ptr, 0, 0, 50, 50);
        let mut sp = TEST_SP;
        for verb in 0u16..5 {
            bus.write_word(sp, verb);
            bus.write_long(sp + 2, rect_ptr);
            bus.write_word(sp + 6, 12 + verb * 2); // ovalWidth varies
            bus.write_word(sp + 8, 16 + verb * 2); // ovalHeight varies
            cpu.write_reg(Register::A7, sp);
            let result = d.dispatch_quickdraw(true, 0x0AF, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp + 10);
            sp += 10;
        }
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 50);
    }

    #[test]
    fn stdrrect_stub_preserves_non_stack_registers_in_hle_compromise_path() {
        // Inside Macintosh Volume I (1985), pp. I-197..I-198:
        // StdRRect is a QDProcs bottleneck; compromise path preserves
        // non-stack registers.
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x3001A0u32;
        write_rect(&mut bus, rect_ptr, 0, 0, 30, 30);
        cpu.write_reg(Register::A0, 0xAAA0_BBB0);
        cpu.write_reg(Register::A1, 0xCCC0_DDD0);
        cpu.write_reg(Register::D0, 0xEEE0_FFF0);
        cpu.write_reg(Register::D1, 0x1234_5678);
        bus.write_word(TEST_SP, 1u16); // paint verb
        bus.write_long(TEST_SP + 2, rect_ptr);
        bus.write_word(TEST_SP + 6, 8u16);
        bus.write_word(TEST_SP + 8, 8u16);

        let result = d.dispatch_quickdraw(true, 0x0AF, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(cpu.read_reg(Register::A0), 0xAAA0_BBB0);
        assert_eq!(cpu.read_reg(Register::A1), 0xCCC0_DDD0);
        assert_eq!(cpu.read_reg(Register::D0), 0xEEE0_FFF0);
        assert_eq!(cpu.read_reg(Register::D1), 0x1234_5678);
    }

    #[test]
    fn stdoval_consumes_verb_and_rect_pointer_arguments() {
        // Inside Macintosh Volume I (1985), p. I-197 + Imaging With QuickDraw
        // 1994 p. 3-133: PROCEDURE StdOval(verb: GrafVerb; r: Rect) consumes
        // a word + pointer frame.  MPW Universal Headers Quickdraw.h declare
        // the C form `EXTERN_API(void) StdOval(GrafVerb verb, const Rect *r)
        // ONEWORDINLINE(0xA8B6)` — same layout as StdRect (sp+0..3 rect_ptr,
        // sp+4..5 verb, 6-byte pop, no result).
        //
        // Engines-agree contract pinned by the strict bake
        // `a8a0_a8b6_stdrect_stdoval_strict` via StackSpace sandwich.
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x3001C0u32;
        write_rect(&mut bus, rect_ptr, 5, 6, 50, 60);
        bus.write_word(TEST_SP, 2u16); // erase verb
        bus.write_long(TEST_SP + 2, rect_ptr);

        let result = d.dispatch_quickdraw(true, 0x0B6, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
    }

    #[test]
    fn stdoval_five_verb_composition_pops_thirty_bytes_total() {
        // Mirrors B4 of the a8a0_a8b6_stdrect_stdoval_strict bake: 5
        // successive StdOval dispatches with frame/paint/erase/invert/fill
        // verbs each pop their 6-byte frame, advancing A7 by 30 bytes total.
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x3001D0u32;
        write_rect(&mut bus, rect_ptr, 0, 0, 50, 50);
        let mut sp = TEST_SP;
        for verb in 0u16..5 {
            bus.write_word(sp, verb);
            bus.write_long(sp + 2, rect_ptr);
            cpu.write_reg(Register::A7, sp);
            let result = d.dispatch_quickdraw(true, 0x0B6, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp + 6);
            sp += 6;
        }
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 30);
    }

    #[test]
    fn stdoval_stub_preserves_non_stack_registers_in_hle_compromise_path() {
        // Inside Macintosh Volume I (1985), pp. I-197..I-198:
        // StdOval compromise path preserves non-stack registers.
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x3001E0u32;
        write_rect(&mut bus, rect_ptr, 2, 4, 22, 44);
        cpu.write_reg(Register::A0, 0x0A0A_0A0A);
        cpu.write_reg(Register::A1, 0x1B1B_1B1B);
        cpu.write_reg(Register::D0, 0x2C2C_2C2C);
        cpu.write_reg(Register::D1, 0x3D3D_3D3D);
        bus.write_word(TEST_SP, 3u16); // invert verb
        bus.write_long(TEST_SP + 2, rect_ptr);

        let result = d.dispatch_quickdraw(true, 0x0B6, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(cpu.read_reg(Register::A0), 0x0A0A_0A0A);
        assert_eq!(cpu.read_reg(Register::A1), 0x1B1B_1B1B);
        assert_eq!(cpu.read_reg(Register::D0), 0x2C2C_2C2C);
        assert_eq!(cpu.read_reg(Register::D1), 0x3D3D_3D3D);
    }

    #[test]
    fn stdarc_consumes_verb_rect_and_angle_arguments() {
        // Inside Macintosh Volume I (1985), p. I-197 + Imaging With QuickDraw
        // 1994 p. 3-134: PROCEDURE StdArc(verb: GrafVerb; r: Rect;
        // startAngle, arcAngle: INTEGER) consumes a word + pointer + 2-word
        // frame.  MPW Universal Headers Quickdraw.h declares the C form
        // `EXTERN_API(void) StdArc(GrafVerb verb, const Rect *r,
        // short startAngle, short arcAngle) ONEWORDINLINE(0xA8BD)` —
        // 10-byte pop, no result slot.
        //
        // Engines-agree contract pinned by the strict bake
        // `a8af_a8bd_stdrrect_stdarc_strict` via empty-clipRgn + StackSpace
        // sandwich.
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300200u32;
        write_rect(&mut bus, rect_ptr, 0, 0, 100, 100);
        bus.write_word(TEST_SP, 1u16); // paint verb
        bus.write_long(TEST_SP + 2, rect_ptr);
        bus.write_word(TEST_SP + 6, 45u16); // startAngle
        bus.write_word(TEST_SP + 8, 90u16); // arcAngle

        let result = d.dispatch_quickdraw(true, 0x0BD, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
    }

    #[test]
    fn stdarc_five_verb_composition_pops_fifty_bytes_total() {
        // Mirrors B4 of the a8af_a8bd_stdrrect_stdarc_strict bake: 5
        // successive StdArc dispatches with frame/paint/erase/invert/fill
        // verbs each pop their 10-byte frame, advancing A7 by 50 bytes total.
        // Varying startAngle/arcAngle pairs across the verbs stresses that
        // the trap pops a fixed 10 bytes regardless of angle values.
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300210u32;
        write_rect(&mut bus, rect_ptr, 0, 0, 80, 80);
        let mut sp = TEST_SP;
        for verb in 0u16..5 {
            bus.write_word(sp, verb);
            bus.write_long(sp + 2, rect_ptr);
            bus.write_word(sp + 6, verb * 45); // startAngle varies
            bus.write_word(sp + 8, 90); // arcAngle
            cpu.write_reg(Register::A7, sp);
            let result = d.dispatch_quickdraw(true, 0x0BD, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp + 10);
            sp += 10;
        }
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 50);
    }

    #[test]
    fn stdarc_stub_preserves_non_stack_registers_in_hle_compromise_path() {
        // Inside Macintosh Volume I (1985), pp. I-197..I-198:
        // StdArc compromise path preserves non-stack registers.
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300220u32;
        write_rect(&mut bus, rect_ptr, 1, 2, 30, 40);
        cpu.write_reg(Register::A0, 0xABC0_0001);
        cpu.write_reg(Register::A1, 0xABC0_0002);
        cpu.write_reg(Register::D0, 0xABC0_0003);
        cpu.write_reg(Register::D1, 0xABC0_0004);
        bus.write_word(TEST_SP, 4u16); // fill verb
        bus.write_long(TEST_SP + 2, rect_ptr);
        bus.write_word(TEST_SP + 6, 0u16);
        bus.write_word(TEST_SP + 8, 180u16);

        let result = d.dispatch_quickdraw(true, 0x0BD, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(cpu.read_reg(Register::A0), 0xABC0_0001);
        assert_eq!(cpu.read_reg(Register::A1), 0xABC0_0002);
        assert_eq!(cpu.read_reg(Register::D0), 0xABC0_0003);
        assert_eq!(cpu.read_reg(Register::D1), 0xABC0_0004);
    }

    #[test]
    fn stdpoly_consumes_verb_and_polygon_handle_arguments() {
        // Inside Macintosh Volume I (1985), p. I-198 + Imaging With QuickDraw
        // 1994 p. 3-135: PROCEDURE StdPoly(verb: GrafVerb; poly: PolyHandle)
        // consumes a word + handle frame.  MPW Universal Headers Quickdraw.h
        // declare the C form `EXTERN_API(void) StdPoly(GrafVerb verb,
        // PolyHandle poly) ONEWORDINLINE(0xA8C5)` — Pascal LR push leaves
        // sp+0..3 poly handle, sp+4..5 verb, 6-byte pop, no result.
        //
        // Engines-agree contract pinned by the strict bake
        // `a8c5_a8d1_stdpoly_stdrgn_strict` via StackSpace sandwich.
        let (mut d, mut cpu, mut bus) = setup();
        let poly_ptr = 0x300240u32;
        let poly_handle = 0x300280u32;
        make_poly(
            &mut bus,
            poly_ptr,
            poly_handle,
            (10, 10, 40, 40),
            &[(10, 10), (10, 40), (40, 40), (40, 10)],
        );
        bus.write_word(TEST_SP, 1u16); // paint verb
        bus.write_long(TEST_SP + 2, poly_handle);

        let result = d.dispatch_quickdraw(true, 0x0C5, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
    }

    #[test]
    fn stdpoly_five_verb_composition_pops_thirty_bytes_total() {
        // Mirrors B2 of the a8c5_a8d1_stdpoly_stdrgn_strict bake: 5
        // successive StdPoly dispatches with frame/paint/erase/invert/fill
        // verbs each pop their 6-byte frame, advancing A7 by 30 bytes total
        // (5 × 6).  Defeats stubs that pop only on verb=0 (frame) or that
        // hardcode the pop amount.
        let (mut d, mut cpu, mut bus) = setup();
        let poly_ptr = 0x300240u32;
        let poly_handle = 0x300280u32;
        make_poly(
            &mut bus,
            poly_ptr,
            poly_handle,
            (10, 10, 40, 40),
            &[(10, 10), (10, 40), (40, 40), (40, 10)],
        );
        let mut sp = TEST_SP;
        for verb in 0u16..5 {
            bus.write_word(sp, verb);
            bus.write_long(sp + 2, poly_handle);
            cpu.write_reg(Register::A7, sp);
            let result = d.dispatch_quickdraw(true, 0x0C5, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp + 6);
            sp += 6;
        }
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 30);
    }

    #[test]
    fn stdpoly_stub_preserves_non_stack_registers_in_hle_compromise_path() {
        // Inside Macintosh Volume I (1985), pp. I-198..I-199:
        // StdPoly compromise path preserves non-stack registers.  This is the
        // Systemless-only contract — BasiliskII's real-ROM StdPoly mutates
        // A0/A1/D0/D1 during polygon rasterisation, so the assertion in the
        // catalog row carries witness_kind = "contract" and is not part of
        // the strict-bake golden coverage.
        let (mut d, mut cpu, mut bus) = setup();
        let poly_ptr = 0x3002A0u32;
        let poly_handle = 0x3002E0u32;
        make_poly(
            &mut bus,
            poly_ptr,
            poly_handle,
            (0, 0, 20, 20),
            &[(0, 0), (0, 20), (20, 20), (20, 0)],
        );
        cpu.write_reg(Register::A0, 0xC001_C001);
        cpu.write_reg(Register::A1, 0xC002_C002);
        cpu.write_reg(Register::D0, 0xC003_C003);
        cpu.write_reg(Register::D1, 0xC004_C004);
        bus.write_word(TEST_SP, 0u16); // frame verb
        bus.write_long(TEST_SP + 2, poly_handle);

        let result = d.dispatch_quickdraw(true, 0x0C5, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(cpu.read_reg(Register::A0), 0xC001_C001);
        assert_eq!(cpu.read_reg(Register::A1), 0xC002_C002);
        assert_eq!(cpu.read_reg(Register::D0), 0xC003_C003);
        assert_eq!(cpu.read_reg(Register::D1), 0xC004_C004);
    }

    #[test]
    fn stdbits_consumes_srcbits_srcrect_dstrect_mode_and_mask_arguments() {
        // Inside Macintosh Volume I (1985), p. I-199:
        // StdBits consumes maskRgn/mode/dstRect/srcRect/srcBits stack frame.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        d.current_port = 0x181000;
        let src_base = 0x300300u32;
        bus.write_byte(src_base, 0x80);
        let src_bits_ptr = 0x300320u32;
        write_bitmap_record(&mut bus, src_bits_ptr, src_base, 1, 0, 0, 1, 8);
        let src_rect_ptr = 0x300340u32;
        let dst_rect_ptr = 0x300360u32;
        write_rect(&mut bus, src_rect_ptr, 0, 0, 1, 1);
        write_rect(&mut bus, dst_rect_ptr, 0, 0, 1, 1);

        bus.write_long(TEST_SP, 0); // maskRgn
        bus.write_word(TEST_SP + 4, 0); // mode = srcCopy
        bus.write_long(TEST_SP + 6, dst_rect_ptr);
        bus.write_long(TEST_SP + 10, src_rect_ptr);
        bus.write_long(TEST_SP + 14, src_bits_ptr);

        let result = d.dispatch_quickdraw(true, 0x0EB, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 18);
    }

    #[test]
    fn stdbits_copies_source_bitmap_into_current_port_bitmap() {
        // Inside Macintosh Volume I (1985), pp. I-176 and I-199:
        // StdBits transfers bits as CopyBits to the current port bitmap.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        d.current_port = 0x181000;
        const PORT_PTR: u32 = 0x181000;

        let dst_base = 0x300380u32;
        bus.write_byte(dst_base, 0x00);
        bus.write_long(PORT_PTR + 2, dst_base);
        bus.write_word(PORT_PTR + 6, 1); // 1 byte per row
        bus.write_word(PORT_PTR + 8, 0);
        bus.write_word(PORT_PTR + 10, 0);
        bus.write_word(PORT_PTR + 12, 1);
        bus.write_word(PORT_PTR + 14, 8);

        let src_base = 0x3003A0u32;
        bus.write_byte(src_base, 0x80); // leftmost source pixel = black
        let src_bits_ptr = 0x3003C0u32;
        write_bitmap_record(&mut bus, src_bits_ptr, src_base, 1, 0, 0, 1, 8);

        let src_rect_ptr = 0x3003E0u32;
        let dst_rect_ptr = 0x300400u32;
        write_rect(&mut bus, src_rect_ptr, 0, 0, 1, 1);
        write_rect(&mut bus, dst_rect_ptr, 0, 0, 1, 1);

        bus.write_long(TEST_SP, 0); // maskRgn
        bus.write_word(TEST_SP + 4, 0); // mode = srcCopy
        bus.write_long(TEST_SP + 6, dst_rect_ptr);
        bus.write_long(TEST_SP + 10, src_rect_ptr);
        bus.write_long(TEST_SP + 14, src_bits_ptr);

        let result = d.dispatch_quickdraw(true, 0x0EB, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(dst_base), 0x80);
    }

    #[test]
    fn stdrect_consumes_verb_and_rect_pointer_arguments() {
        // Inside Macintosh Volume I (1985), p. I-197 + Imaging With QuickDraw
        // 1994 p. 3-132: PROCEDURE StdRect(verb: GrafVerb; r: Rect) consumes
        // a word + pointer frame.  MPW Universal Headers Quickdraw.h declare
        // the C form `EXTERN_API(void) StdRect(GrafVerb verb, const Rect *r)
        // ONEWORDINLINE(0xA8A0)` — sp+0..3 = rect_ptr (shallowest),
        // sp+4..5 = verb (deepest), 6-byte pop, no result slot.
        //
        // Engines-agree contract pinned by the strict bake
        // `a8a0_a8b6_stdrect_stdoval_strict` via StackSpace sandwich
        // (BasiliskII System 7.5 ROM advances A7 by exactly 6 bytes per
        // StdRect dispatch regardless of GrafVerb code).
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300220u32;
        write_rect(&mut bus, rect_ptr, 10, 20, 30, 40);
        bus.write_word(TEST_SP, 0u16); // frame verb
        bus.write_long(TEST_SP + 2, rect_ptr);

        let result = d.dispatch_quickdraw(true, 0x0A0, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
    }

    #[test]
    fn stdrect_stub_preserves_non_stack_registers_in_hle_compromise_path() {
        // Inside Macintosh Volume I (1985), pp. I-197..I-198: StdRect is
        // the bottleneck installed in QDProcs.rectProc.  Systemless's HLE
        // compromise (quickdraw.rs:8625..8641) leaves A0/A1/D0/D1
        // unchanged because high-level FrameRect/PaintRect/EraseRect/
        // InvertRect/FillRect draw directly via Rust framebuffer code;
        // StdRect itself is reachable only via SetStdProcs → JSR-through-
        // slot, a non-corpus pattern.  BasiliskII's real-ROM StdRect
        // mutates A0/A1/D0/D1 during rectangle rasterisation, so this
        // assertion is `witness_kind = "contract"` in the catalog row —
        // Systemless-HLE-only, pinned by this contract test.
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300220u32;
        write_rect(&mut bus, rect_ptr, 10, 20, 30, 40);
        cpu.write_reg(Register::A0, 0x1122_3344);
        cpu.write_reg(Register::A1, 0x5566_7788);
        cpu.write_reg(Register::D0, 0x99AA_BBCC);
        cpu.write_reg(Register::D1, 0xDDEE_F012);
        bus.write_word(TEST_SP, 0u16); // frame verb
        bus.write_long(TEST_SP + 2, rect_ptr);

        let result = d.dispatch_quickdraw(true, 0x0A0, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(cpu.read_reg(Register::A0), 0x1122_3344);
        assert_eq!(cpu.read_reg(Register::A1), 0x5566_7788);
        assert_eq!(cpu.read_reg(Register::D0), 0x99AA_BBCC);
        assert_eq!(cpu.read_reg(Register::D1), 0xDDEE_F012);
    }

    #[test]
    fn stdrect_five_verb_composition_pops_thirty_bytes_total() {
        // Mirrors B2 of the a8a0_a8b6_stdrect_stdoval_strict bake: 5
        // successive StdRect dispatches with frame/paint/erase/invert/fill
        // verbs each pop their 6-byte frame, advancing A7 by 30 bytes total
        // (5 × 6).  Defeats stubs that pop only on verb=0 (frame) or that
        // hardcode the pop amount.
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300240u32;
        write_rect(&mut bus, rect_ptr, 0, 0, 50, 50);
        let mut sp = TEST_SP;
        for verb in 0u16..5 {
            bus.write_word(sp, verb);
            bus.write_long(sp + 2, rect_ptr);
            cpu.write_reg(Register::A7, sp);
            let result = d.dispatch_quickdraw(true, 0x0A0, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp + 6);
            sp += 6;
        }
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 30);
    }

    #[test]
    fn stdrgn_consumes_verb_and_region_handle_arguments() {
        // Inside Macintosh Volume I (1985), p. I-198 + Imaging With QuickDraw
        // 1994 p. 3-135: PROCEDURE StdRgn(verb: GrafVerb; rgn: RgnHandle)
        // consumes a word + handle frame.  MPW Universal Headers Quickdraw.h
        // declare the C form `EXTERN_API(void) StdRgn(GrafVerb verb,
        // RgnHandle rgn) ONEWORDINLINE(0xA8D1)` — Pascal LR push leaves
        // sp+0..3 rgn handle, sp+4..5 verb, 6-byte pop, no result.
        //
        // Engines-agree contract pinned by the strict bake
        // `a8c5_a8d1_stdpoly_stdrgn_strict` via StackSpace sandwich.
        let (mut d, mut cpu, mut bus) = setup();
        let rgn_ptr = 0x300260u32;
        let rgn_handle = 0x300280u32;
        make_rgn(&mut bus, rgn_ptr, rgn_handle, 10, 20, 40, 60);

        bus.write_word(TEST_SP, 1u16); // paint verb
        bus.write_long(TEST_SP + 2, rgn_handle);

        let result = d.dispatch_quickdraw(true, 0x0D1, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
    }

    #[test]
    fn stdrgn_five_verb_composition_pops_thirty_bytes_total() {
        // Mirrors B4 of the a8c5_a8d1_stdpoly_stdrgn_strict bake: 5 successive
        // StdRgn dispatches with frame/paint/erase/invert/fill verbs each pop
        // their 6-byte frame, advancing A7 by 30 bytes total (5 × 6).  Defeats
        // stubs that pop only on verb=0 (frame) or that hardcode the pop
        // amount.
        let (mut d, mut cpu, mut bus) = setup();
        let rgn_ptr = 0x300260u32;
        let rgn_handle = 0x300280u32;
        make_rgn(&mut bus, rgn_ptr, rgn_handle, 10, 20, 40, 60);
        let mut sp = TEST_SP;
        for verb in 0u16..5 {
            bus.write_word(sp, verb);
            bus.write_long(sp + 2, rgn_handle);
            cpu.write_reg(Register::A7, sp);
            let result = d.dispatch_quickdraw(true, 0x0D1, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp + 6);
            sp += 6;
        }
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 30);
    }

    #[test]
    fn stdrgn_stub_preserves_non_stack_registers_in_hle_compromise_path() {
        // Inside Macintosh Volume I (1985), pp. I-197..I-198:
        // StdRgn compromise path preserves non-stack registers.  This is the
        // Systemless-only contract — BasiliskII's real-ROM StdRgn mutates
        // A0/A1/D0/D1 during region rasterisation, so the assertion in the
        // catalog row carries witness_kind = "contract" and is not part of
        // the strict-bake golden coverage.
        let (mut d, mut cpu, mut bus) = setup();
        let rgn_ptr = 0x3002A0u32;
        let rgn_handle = 0x3002E0u32;
        make_rgn(&mut bus, rgn_ptr, rgn_handle, 5, 10, 25, 40);

        cpu.write_reg(Register::A0, 0x1357_9BDF);
        cpu.write_reg(Register::A1, 0x2468_ACE0);
        cpu.write_reg(Register::D0, 0x1020_3040);
        cpu.write_reg(Register::D1, 0x5060_7080);
        bus.write_word(TEST_SP, 1u16); // paint verb
        bus.write_long(TEST_SP + 2, rgn_handle);

        let result = d.dispatch_quickdraw(true, 0x0D1, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(cpu.read_reg(Register::A0), 0x1357_9BDF);
        assert_eq!(cpu.read_reg(Register::A1), 0x2468_ACE0);
        assert_eq!(cpu.read_reg(Register::D0), 0x1020_3040);
        assert_eq!(cpu.read_reg(Register::D1), 0x5060_7080);
    }

    #[test]
    fn stdcomment_consumes_kind_datasize_and_handle_arguments() {
        // Inside Macintosh Volume I (1985), p. I-198:
        // StdComment(kind, dataSize: INTEGER; dataHandle: Handle) pops 8 bytes.
        let (mut d, mut cpu, mut bus) = setup();
        let data_handle = 0x3002A0u32;
        let data_ptr = 0x3002C0u32;
        bus.write_long(data_handle, data_ptr);
        bus.write_byte(data_ptr, 0xAA);

        bus.write_word(TEST_SP, 101u16); // kind
        bus.write_word(TEST_SP + 2, 1u16); // dataSize
        bus.write_long(TEST_SP + 4, data_handle);
        let result = d.dispatch_quickdraw(true, 0x0F1, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
    }

    #[test]
    fn stdcomment_default_handler_ignores_comment_payload() {
        // Inside Macintosh Volume I (1985), p. I-198 and Imaging With QuickDraw
        // (1994), p. 3-137: the default StdComment routine ignores comments.
        let (mut d, mut cpu, mut bus) = setup();
        let data_handle = 0x3002E0u32;
        let data_ptr = 0x300300u32;
        bus.write_long(data_handle, data_ptr);
        bus.write_byte(data_ptr, 0x11);
        bus.write_byte(data_ptr + 1, 0x22);
        bus.write_byte(data_ptr + 2, 0x33);

        cpu.write_reg(Register::A0, 0xCAFEBABE);
        cpu.write_reg(Register::A1, 0x0BADF00D);
        cpu.write_reg(Register::D0, 0x01020304);
        cpu.write_reg(Register::D1, 0xA1B2C3D4);
        bus.write_word(TEST_SP, 0x7FFF); // kind
        bus.write_word(TEST_SP + 2, 3u16); // dataSize
        bus.write_long(TEST_SP + 4, data_handle);

        let result = d.dispatch_quickdraw(true, 0x0F1, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(cpu.read_reg(Register::A0), 0xCAFEBABE);
        assert_eq!(cpu.read_reg(Register::A1), 0x0BADF00D);
        assert_eq!(cpu.read_reg(Register::D0), 0x01020304);
        assert_eq!(cpu.read_reg(Register::D1), 0xA1B2C3D4);
        assert_eq!(bus.read_byte(data_ptr), 0x11);
        assert_eq!(bus.read_byte(data_ptr + 1), 0x22);
        assert_eq!(bus.read_byte(data_ptr + 2), 0x33);
    }

    #[test]
    fn stdgetpic_consumes_data_ptr_and_byte_count_arguments_and_returns_noerr() {
        // Inside Macintosh Volume I (1985), p. I-200; Imaging With QuickDraw
        // (1994), pp. 3-138 to 3-139: StdGetPic is a picture bottleneck
        // procedure, so direct calls should consume the 6-byte frame and
        // normalize the return register to noErr in Systemless's HLE path.
        let (mut d, mut cpu, mut bus) = setup();
        let data_ptr = 0x300360u32;

        cpu.write_reg(Register::D0, 0xDEAD_BEEFu32);
        bus.write_long(TEST_SP, data_ptr);
        bus.write_word(TEST_SP + 4, 0u16);

        let result = d.dispatch_quickdraw(true, 0x0EE, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    #[test]
    fn stdputpic_consumes_data_ptr_and_byte_count_arguments_and_returns_noerr() {
        // Inside Macintosh Volume I (1985), p. I-200; Imaging With QuickDraw
        // (1994), pp. 3-138 to 3-139: StdPutPic is a picture bottleneck
        // procedure, so direct calls should consume the 6-byte frame and
        // normalize the return register to noErr in Systemless's HLE path.
        let (mut d, mut cpu, mut bus) = setup();
        let data_ptr = 0x300380u32;

        cpu.write_reg(Register::D0, 0xDEAD_BEEFu32);
        bus.write_long(TEST_SP, data_ptr);
        bus.write_word(TEST_SP + 4, 0u16);

        let result = d.dispatch_quickdraw(true, 0x0F0, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // ==================== Rect Operations ====================

    #[test]
    fn test_set_rect() {
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300000u32;
        bus.write_word(TEST_SP, 200u16); // bottom
        bus.write_word(TEST_SP + 2, 300u16); // right
        bus.write_word(TEST_SP + 4, 10u16); // top
        bus.write_word(TEST_SP + 6, 20u16); // left
        bus.write_long(TEST_SP + 8, rect_ptr); // rect_ptr
        let result = d.dispatch_quickdraw(true, 0x0A7, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        let r = read_rect(&bus, rect_ptr);
        assert_eq!(r, (10, 20, 200, 300));
    }

    #[test]
    fn test_inset_rect() {
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 20, 100, 200);
        bus.write_word(TEST_SP, 5u16); // dv
        bus.write_word(TEST_SP + 2, 10u16); // dh
        bus.write_long(TEST_SP + 4, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0A9, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        let r = read_rect(&bus, rect_ptr);
        assert_eq!(r, (15, 30, 95, 190));
    }

    #[test]
    fn test_offset_rect() {
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 20, 100, 200);
        bus.write_word(TEST_SP, 5u16); // dv
        bus.write_word(TEST_SP + 2, 10u16); // dh
        bus.write_long(TEST_SP + 4, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0A8, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        let r = read_rect(&bus, rect_ptr);
        assert_eq!(r, (15, 30, 105, 210));
    }

    #[test]
    fn test_empty_rect_true() {
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 20, 10, 200); // bottom == top -> empty
        bus.write_long(TEST_SP, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0AE, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(TEST_SP + 4), 0x0100); // TRUE
    }

    #[test]
    fn test_empty_rect_false() {
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 20, 100, 200); // non-empty
        bus.write_long(TEST_SP, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0AE, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(TEST_SP + 4), 0); // FALSE
    }

    #[test]
    fn test_pt_in_rect_inside() {
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 20, 100, 200);
        bus.write_long(TEST_SP, rect_ptr);
        bus.write_word(TEST_SP + 4, 50u16); // pt_v (inside)
        bus.write_word(TEST_SP + 6, 100u16); // pt_h (inside)
        let result = d.dispatch_quickdraw(true, 0x0AD, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8), 0x0100); // TRUE
    }

    #[test]
    fn test_pt_in_rect_outside() {
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 20, 100, 200);
        bus.write_long(TEST_SP, rect_ptr);
        bus.write_word(TEST_SP + 4, 5u16); // pt_v (above)
        bus.write_word(TEST_SP + 6, 100u16); // pt_h
        let result = d.dispatch_quickdraw(true, 0x0AD, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8), 0); // FALSE
    }

    #[test]
    fn test_sect_rect_intersecting() {
        let (mut d, mut cpu, mut bus) = setup();
        let r1 = 0x300000u32;
        let r2 = 0x300010u32;
        let dst = 0x300020u32;
        write_rect(&mut bus, r1, 10, 20, 100, 200);
        write_rect(&mut bus, r2, 50, 80, 150, 250);
        bus.write_long(TEST_SP, dst);
        bus.write_long(TEST_SP + 4, r2);
        bus.write_long(TEST_SP + 8, r1);
        let result = d.dispatch_quickdraw(true, 0x0AA, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(bus.read_word(TEST_SP + 12), 0x0100); // TRUE
        let r = read_rect(&bus, dst);
        assert_eq!(r, (50, 80, 100, 200));
    }

    #[test]
    fn test_sect_rect_non_intersecting() {
        let (mut d, mut cpu, mut bus) = setup();
        let r1 = 0x300000u32;
        let r2 = 0x300010u32;
        let dst = 0x300020u32;
        write_rect(&mut bus, r1, 10, 20, 30, 40);
        write_rect(&mut bus, r2, 50, 60, 70, 80);
        bus.write_long(TEST_SP, dst);
        bus.write_long(TEST_SP + 4, r2);
        bus.write_long(TEST_SP + 8, r1);
        let result = d.dispatch_quickdraw(true, 0x0AA, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(bus.read_word(TEST_SP + 12), 0); // FALSE
    }

    #[test]
    fn test_union_rect() {
        let (mut d, mut cpu, mut bus) = setup();
        let r1 = 0x300000u32;
        let r2 = 0x300010u32;
        let dst = 0x300020u32;
        write_rect(&mut bus, r1, 10, 20, 100, 200);
        write_rect(&mut bus, r2, 50, 80, 150, 250);
        bus.write_long(TEST_SP, dst);
        bus.write_long(TEST_SP + 4, r2);
        bus.write_long(TEST_SP + 8, r1);
        let result = d.dispatch_quickdraw(true, 0x0AB, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        let r = read_rect(&bus, dst);
        assert_eq!(r, (10, 20, 150, 250));
    }

    #[test]
    fn test_equal_rect_true() {
        let (mut d, mut cpu, mut bus) = setup();
        let r1 = 0x300000u32;
        let r2 = 0x300010u32;
        write_rect(&mut bus, r1, 10, 20, 100, 200);
        write_rect(&mut bus, r2, 10, 20, 100, 200);
        bus.write_long(TEST_SP, r2);
        bus.write_long(TEST_SP + 4, r1);
        let result = d.dispatch_quickdraw(true, 0x0A6, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8), 0x0100); // TRUE
    }

    #[test]
    fn test_equal_rect_false() {
        let (mut d, mut cpu, mut bus) = setup();
        let r1 = 0x300000u32;
        let r2 = 0x300010u32;
        write_rect(&mut bus, r1, 10, 20, 100, 200);
        write_rect(&mut bus, r2, 0, 0, 50, 50);
        bus.write_long(TEST_SP, r2);
        bus.write_long(TEST_SP + 4, r1);
        let result = d.dispatch_quickdraw(true, 0x0A6, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8), 0); // FALSE
    }

    #[test]
    fn test_pt2_rect() {
        let (mut d, mut cpu, mut bus) = setup();
        let dst = 0x300000u32;
        bus.write_long(TEST_SP, dst);
        bus.write_word(TEST_SP + 4, 100u16); // pt2_v
        bus.write_word(TEST_SP + 6, 200u16); // pt2_h
        bus.write_word(TEST_SP + 8, 10u16); // pt1_v
        bus.write_word(TEST_SP + 10, 20u16); // pt1_h
        let result = d.dispatch_quickdraw(true, 0x0AC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        let r = read_rect(&bus, dst);
        assert_eq!(r, (10, 20, 100, 200));
    }

    // ==================== Shape Drawing ====================

    #[test]
    fn test_frame_rect() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 10, 20, 20);
        bus.write_long(TEST_SP, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0A1, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn test_paint_rect() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 10, 20, 20);
        bus.write_long(TEST_SP, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0A2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn test_erase_rect() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 10, 20, 20);
        bus.write_long(TEST_SP, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0A3, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn test_invert_rect() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 10, 20, 20);
        bus.write_long(TEST_SP, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0A4, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn test_fill_rect() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let pat_ptr = 0x300000u32;
        bus.write_bytes(pat_ptr, &[0xAA; 8]);
        let rect_ptr = 0x300010u32;
        write_rect(&mut bus, rect_ptr, 10, 10, 20, 20);
        bus.write_long(TEST_SP, pat_ptr);
        bus.write_long(TEST_SP + 4, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0A5, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
    }

    /// When a basic GrafPort (port_version & 0xC000 == 0) has its
    /// portBits.baseAddr set to the 8bpp screen base AND its rowBytes
    /// matches screen.row_bytes, `draw_generic_shape` MUST treat it as
    /// 8bpp instead of the implicit-1bpp default — otherwise per-bit
    /// set/clear into 8bpp screen bytes corrupts pixels.
    #[test]
    fn test_fill_rect_basic_grafport_at_screen_writes_8bpp() {
        let (mut d, mut cpu, mut bus) = setup_with_port();

        // Stand up an 8bpp 800×600 screen + matching screen_mode.
        let screen_base = bus.alloc(800 * 600);
        bus.write_long(0x0824, screen_base);
        d.screen_mode = (screen_base, 800, 800, 600, 8);

        // setup_with_port gave PORT_PTR=0x181000 a basic GrafPort with
        // portBits.baseAddr=screen_base (whatever that was) and
        // rowBytes=64. Override portBits to point at our new 8bpp
        // screen with rowBytes matching the screen — exactly the
        // shape OpenPort would produce after copying QD-globals
        // screenBits on a color screen.
        const PORT_PTR: u32 = 0x181000;
        bus.write_long(PORT_PTR + 2, screen_base); // portBits.baseAddr
        bus.write_word(PORT_PTR + 6, 800); // portBits.rowBytes (no high-bit flag)

        // Pre-fill the test pixel with a sentinel.
        let pixel_addr = screen_base + 30 * 800 + 100; // y=30, x=100
        bus.write_byte(pixel_addr, 0xAB);

        // FillRect with an all-1s pattern. Per-pixel write should be 0xFF
        // (foreground = black = idx 255), not the bit-set fragments the
        // 1bpp branch would produce.
        let pat_ptr = 0x300000u32;
        bus.write_bytes(pat_ptr, &[0xFF; 8]);
        let rect_ptr = 0x300010u32;
        write_rect(&mut bus, rect_ptr, 30, 100, 31, 101); // single pixel
        bus.write_long(TEST_SP, pat_ptr);
        bus.write_long(TEST_SP + 4, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0A5, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_eq!(
            bus.read_byte(pixel_addr),
            0xFF,
            "basic GrafPort with screen_base baseAddr must write 8bpp pixels (0xFF for black), not 1bpp bit-set fragments"
        );
    }

    #[test]
    fn test_frame_oval() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 10, 50, 50);
        bus.write_long(TEST_SP, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0B7, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn test_paint_oval() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 10, 50, 50);
        bus.write_long(TEST_SP, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0B8, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn test_erase_oval() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 10, 50, 50);
        bus.write_long(TEST_SP, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0B9, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn test_invert_oval() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 10, 50, 50);
        bus.write_long(TEST_SP, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0BA, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn test_fill_oval() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let pat_ptr = 0x300000u32;
        bus.write_bytes(pat_ptr, &[0x55; 8]);
        let rect_ptr = 0x300010u32;
        write_rect(&mut bus, rect_ptr, 10, 10, 50, 50);
        bus.write_long(TEST_SP, pat_ptr);
        bus.write_long(TEST_SP + 4, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0BB, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
    }

    #[test]
    fn test_frame_round_rect() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 10, 50, 50);
        bus.write_word(TEST_SP, 8u16); // oval_height
        bus.write_word(TEST_SP + 2, 8u16); // oval_width
        bus.write_long(TEST_SP + 4, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0B0, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
    }

    #[test]
    fn test_paint_round_rect() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 10, 50, 50);
        bus.write_word(TEST_SP, 8u16);
        bus.write_word(TEST_SP + 2, 8u16);
        bus.write_long(TEST_SP + 4, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0B1, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
    }

    #[test]
    fn test_erase_round_rect() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 10, 50, 50);
        bus.write_word(TEST_SP, 8u16);
        bus.write_word(TEST_SP + 2, 8u16);
        bus.write_long(TEST_SP + 4, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0B2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
    }

    #[test]
    fn test_invert_round_rect() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 10, 50, 50);
        bus.write_word(TEST_SP, 8u16);
        bus.write_word(TEST_SP + 2, 8u16);
        bus.write_long(TEST_SP + 4, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0B3, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
    }

    #[test]
    fn test_fill_round_rect() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let pat_ptr = 0x300000u32;
        bus.write_bytes(pat_ptr, &[0xCC; 8]);
        let rect_ptr = 0x300010u32;
        write_rect(&mut bus, rect_ptr, 10, 10, 50, 50);
        bus.write_long(TEST_SP, pat_ptr);
        bus.write_word(TEST_SP + 4, 8u16); // oval_height
        bus.write_word(TEST_SP + 6, 8u16); // oval_width
        bus.write_long(TEST_SP + 8, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x0B4, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
    }

    #[test]
    fn fillcrect_consumes_pp_and_rect_arguments() {
        // IM:V 1986 p. V-73: FillCRect consumes PixPatHandle + Rect*
        // (8 bytes total) as a procedure call.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = bus.alloc(8);
        write_rect(&mut bus, rect_ptr, 10, 10, 30, 30);
        let pp_handle = make_pixpat_handle(&mut bus, [0xFF; 8]);

        bus.write_long(TEST_SP, pp_handle);
        bus.write_long(TEST_SP + 4, rect_ptr);

        let result = d.dispatch_quickdraw(true, 0x20E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
    }

    #[test]
    fn fillcrect_pascal_procedure_preserves_stack_across_five_calls() {
        // IM:V 1986 p. V-73: FillCRect(r: Rect; pp: PixPatHandle) is a
        // Pascal PROCEDURE. Each call pops 8 bytes (4-byte Rect pointer
        // + 4-byte PixPatHandle) and writes no FUNCTION result slot.
        // Five successive C-level calls (each pre-pushing 8 bytes and
        // letting the trap pop them) must net-balance A7 (mirrors B2
        // of the aa0e_fillcrect_strict catalog test bake).
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let sp_pre = cpu.read_reg(Register::A7);
        let rects = [
            bus.alloc(8),
            bus.alloc(8),
            bus.alloc(8),
            bus.alloc(8),
            bus.alloc(8),
        ];
        for r in &rects {
            write_rect(&mut bus, *r, 0, 0, 1, 1);
        }
        let pps = [
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
        ];

        for i in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 8);
            bus.write_long(sp - 8, pps[i]);
            bus.write_long(sp - 4, rects[i]);
            let result = d.dispatch_quickdraw(true, 0x20E, &mut cpu, &mut bus);
            assert!(
                result.unwrap().is_ok(),
                "FillCRect dispatch should succeed (call {})",
                i
            );
            assert_eq!(
                cpu.read_reg(Register::A7),
                sp,
                "FillCRect should pop 8 bytes per call (call {})",
                i
            );
        }

        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_pre,
            "5 successive FillCRect calls should net-balance A7"
        );
    }

    #[test]
    fn fillcoval_fillcrgn_pascal_procedure_preserves_stack_across_five_calls() {
        // IM:V 1986 pp. V-67..V-69 (Color QuickDraw — Color Drawing
        // Operations): FillCOval(r: Rect; pp: PixPatHandle) and
        // FillCRgn(rgn: RgnHandle; pp: PixPatHandle) are Pascal
        // PROCEDUREs. Each call pops 8 bytes (4-byte first arg pointer
        // + 4-byte PixPatHandle) and writes no FUNCTION result slot.
        // This test mirrors B2 + B4 of the
        // aa0f_aa12_fillcoval_fillcrgn_strict catalog test bake:
        // 5 successive FillCOval calls then 5 successive FillCRgn
        // calls each net-balance A7.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let sp_pre = cpu.read_reg(Register::A7);

        let rects = [
            bus.alloc(8),
            bus.alloc(8),
            bus.alloc(8),
            bus.alloc(8),
            bus.alloc(8),
        ];
        for r in &rects {
            write_rect(&mut bus, *r, 0, 0, 1, 1);
        }
        let oval_pps = [
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
        ];

        for i in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 8);
            bus.write_long(sp - 8, oval_pps[i]);
            bus.write_long(sp - 4, rects[i]);
            let result = d.dispatch_quickdraw(true, 0x20F, &mut cpu, &mut bus);
            assert!(
                result.unwrap().is_ok(),
                "FillCOval dispatch should succeed (call {})",
                i
            );
            assert_eq!(
                cpu.read_reg(Register::A7),
                sp,
                "FillCOval should pop 8 bytes per call (call {})",
                i
            );
        }

        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_pre,
            "5 successive FillCOval calls should net-balance A7"
        );

        // Build five minimal RgnHandles: each handle points at a 10-byte
        // region record (size=10, empty bbox) — the trap only needs to
        // see a non-NIL handle and a valid first-long pointer to satisfy
        // the calling convention witness.
        let rgns: [u32; 5] = std::array::from_fn(|_| {
            let h = bus.alloc(4);
            let rec = bus.alloc(10);
            bus.write_long(h, rec);
            bus.write_word(rec, 10); // rgnSize
                                     // bbox top/left/bottom/right = 0,0,0,0 (empty region)
            h
        });
        let rgn_pps = [
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
        ];

        for i in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 8);
            bus.write_long(sp - 8, rgn_pps[i]);
            bus.write_long(sp - 4, rgns[i]);
            let result = d.dispatch_quickdraw(true, 0x212, &mut cpu, &mut bus);
            assert!(
                result.unwrap().is_ok(),
                "FillCRgn dispatch should succeed (call {})",
                i
            );
            assert_eq!(
                cpu.read_reg(Register::A7),
                sp,
                "FillCRgn should pop 8 bytes per call (call {})",
                i
            );
        }

        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_pre,
            "5 successive FillCRgn calls (after 5 FillCOval calls) should net-balance A7"
        );
    }

    #[test]
    fn fillcroundrect_fillcarc_pascal_procedure_preserves_stack_across_five_calls() {
        // IM:V 1986 pp. V-67..V-69 (Color QuickDraw — Color Drawing
        // Operations): FillCRoundRect(r: Rect; ovWid, ovHt: INTEGER;
        // pp: PixPatHandle) and FillCArc(r: Rect; startAngle, arcAngle:
        // INTEGER; pp: PixPatHandle) are Pascal PROCEDUREs. Each call
        // pops 12 bytes (4-byte PixPatHandle at SP+0, 2-byte second
        // INTEGER at SP+4, 2-byte first INTEGER at SP+6, 4-byte Rect
        // pointer at SP+8) and writes no FUNCTION result slot. This
        // test mirrors B2 + B4 of the
        // aa10_aa11_fillcroundrect_fillcarc_strict catalog test bake.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let sp_pre = cpu.read_reg(Register::A7);

        let rr_rects = [
            bus.alloc(8),
            bus.alloc(8),
            bus.alloc(8),
            bus.alloc(8),
            bus.alloc(8),
        ];
        for r in &rr_rects {
            write_rect(&mut bus, *r, 0, 0, 1, 1);
        }
        let rr_pps = [
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
        ];
        let rr_int_pairs: [(i16, i16); 5] = [(1, 1), (1, 2), (2, 1), (2, 2), (3, 3)];

        for i in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 12);
            bus.write_long(sp - 12, rr_pps[i]);
            bus.write_word(sp - 8, rr_int_pairs[i].1 as u16); // ovHt at SP+4
            bus.write_word(sp - 6, rr_int_pairs[i].0 as u16); // ovWid at SP+6
            bus.write_long(sp - 4, rr_rects[i]);
            let result = d.dispatch_quickdraw(true, 0x210, &mut cpu, &mut bus);
            assert!(
                result.unwrap().is_ok(),
                "FillCRoundRect dispatch should succeed (call {})",
                i
            );
            assert_eq!(
                cpu.read_reg(Register::A7),
                sp,
                "FillCRoundRect should pop 12 bytes per call (call {})",
                i
            );
        }

        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_pre,
            "5 successive FillCRoundRect calls should net-balance A7"
        );

        let arc_rects = [
            bus.alloc(8),
            bus.alloc(8),
            bus.alloc(8),
            bus.alloc(8),
            bus.alloc(8),
        ];
        for r in &arc_rects {
            write_rect(&mut bus, *r, 0, 0, 1, 1);
        }
        let arc_pps = [
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
        ];
        let arc_int_pairs: [(i16, i16); 5] = [(0, 90), (45, 90), (90, 180), (180, 90), (270, 45)];

        for i in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 12);
            bus.write_long(sp - 12, arc_pps[i]);
            bus.write_word(sp - 8, arc_int_pairs[i].1 as u16); // arcAngle at SP+4
            bus.write_word(sp - 6, arc_int_pairs[i].0 as u16); // startAngle at SP+6
            bus.write_long(sp - 4, arc_rects[i]);
            let result = d.dispatch_quickdraw(true, 0x211, &mut cpu, &mut bus);
            assert!(
                result.unwrap().is_ok(),
                "FillCArc dispatch should succeed (call {})",
                i
            );
            assert_eq!(
                cpu.read_reg(Register::A7),
                sp,
                "FillCArc should pop 12 bytes per call (call {})",
                i
            );
        }

        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_pre,
            "5 successive FillCArc calls (after 5 FillCRoundRect calls) should net-balance A7"
        );
    }

    #[test]
    fn saveentries_restoreentries_pascal_procedure_preserves_stack_across_five_calls() {
        // IM:V 1986 p. V-144 (Color Manager — Color Manager Routines):
        // SaveEntries(srcTable: CTabHandle; resultTable: CTabHandle;
        //             VAR selection: ReqListRec) and
        // RestoreEntries(srcTable: CTabHandle; dstTable: CTabHandle;
        //                VAR selection: ReqListRec)
        // are Pascal PROCEDUREs. Each call pops 12 bytes (4-byte
        // ReqListRec pointer at SP+0, 4-byte resultTable/dstTable handle
        // at SP+4, 4-byte srcTable handle at SP+8) and writes no
        // FUNCTION result slot. This test mirrors B2 + B4 of the
        // aa49_aa4a_saveentries_restoreentries_strict catalog test bake.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let sp_pre = cpu.read_reg(Register::A7);

        // Helper: build a minimal 16-byte CTab record (8-byte header +
        // 1 ColorSpec) and return a handle pointing at it. ctSize=0
        // (1 entry: index 0).
        fn make_ctab(bus: &mut crate::memory::MacMemoryBus, seed: u32) -> u32 {
            let rec = bus.alloc(16);
            bus.fill_zeros(rec, 16);
            bus.write_long(rec, seed); // ctSeed
            bus.write_word(rec + 4, 0); // ctFlags
            bus.write_word(rec + 6, 0); // ctSize (1 entry)
            let handle = bus.alloc(4);
            bus.write_long(handle, rec);
            handle
        }

        // Helper: build a 4-byte ReqListRec (reqLSize=0, reqLData[0]=0).
        fn make_sel(bus: &mut crate::memory::MacMemoryBus) -> u32 {
            let sel = bus.alloc(4);
            bus.write_word(sel, 0); // reqLSize
            bus.write_word(sel + 2, 0); // reqLData[0]
            sel
        }

        // 5 distinct SaveEntries arg sets (src + result + selection).
        let save_args: Vec<(u32, u32, u32)> = (0..5)
            .map(|i| {
                let src = make_ctab(&mut bus, 0x2001 + i as u32);
                let result = make_ctab(&mut bus, 0x2101 + i as u32);
                let sel = make_sel(&mut bus);
                (src, result, sel)
            })
            .collect();

        for (i, (src, result, sel)) in save_args.iter().enumerate() {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 12);
            bus.write_long(sp - 12, *sel); // selection at SP+0
            bus.write_long(sp - 8, *result); // resultTable at SP+4
            bus.write_long(sp - 4, *src); // srcTable at SP+8
            let r = d.dispatch_quickdraw(true, 0x249, &mut cpu, &mut bus);
            assert!(
                r.unwrap().is_ok(),
                "SaveEntries dispatch should succeed (call {})",
                i
            );
            assert_eq!(
                cpu.read_reg(Register::A7),
                sp,
                "SaveEntries should pop 12 bytes per call (call {})",
                i
            );
        }

        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_pre,
            "5 successive SaveEntries calls should net-balance A7"
        );

        // 5 distinct RestoreEntries arg sets (src + dst + selection).
        let restore_args: Vec<(u32, u32, u32)> = (0..5)
            .map(|i| {
                let src = make_ctab(&mut bus, 0x4001 + i as u32);
                let dst = make_ctab(&mut bus, 0x4101 + i as u32);
                let sel = make_sel(&mut bus);
                (src, dst, sel)
            })
            .collect();

        for (i, (src, dst, sel)) in restore_args.iter().enumerate() {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 12);
            bus.write_long(sp - 12, *sel); // selection at SP+0
            bus.write_long(sp - 8, *dst); // dstTable at SP+4
            bus.write_long(sp - 4, *src); // srcTable at SP+8
            let r = d.dispatch_quickdraw(true, 0x24A, &mut cpu, &mut bus);
            assert!(
                r.unwrap().is_ok(),
                "RestoreEntries dispatch should succeed (call {})",
                i
            );
            assert_eq!(
                cpu.read_reg(Register::A7),
                sp,
                "RestoreEntries should pop 12 bytes per call (call {})",
                i
            );
        }

        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_pre,
            "5 successive RestoreEntries calls (after 5 SaveEntries calls) should net-balance A7"
        );
    }

    #[test]
    fn saveentries_nil_source_resizes_zero_length_result_handle_to_ctab() {
        // IM:V 1986 p. V-144: SaveEntries packs selected entries from
        // the current device color table when srcTable is NIL. Callers
        // commonly pass a freshly-created zero-length result handle;
        // the result must still become a structurally valid ColorTable.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        d.ensure_main_gdevice(&mut bus);

        let selection = bus.alloc(2 + 256 * 2);
        bus.write_word(selection, 255); // reqLSize: 256 requests
        for index in 0..256u32 {
            bus.write_word(selection + 2 + index * 2, index as u16);
        }

        let zero_len_ptr = bus.alloc(0);
        let result_handle = bus.alloc(4);
        bus.write_long(result_handle, zero_len_ptr);
        assert_eq!(bus.get_alloc_size(zero_len_ptr), Some(0));

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, selection);
        bus.write_long(TEST_SP + 4, result_handle);
        bus.write_long(TEST_SP + 8, 0); // NIL srcTable => current device CTab

        let result = d.dispatch_quickdraw(true, 0x249, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);

        let result_ptr = bus.read_long(result_handle);
        assert_ne!(result_ptr, 0);
        assert_eq!(
            bus.get_alloc_size(result_ptr),
            Some(8 + 256 * 8),
            "SaveEntries should resize resultTable for a packed 256-entry CTab"
        );
        assert_eq!(bus.read_word(result_ptr + 6), 255);

        let canonical = TrapDispatcher::standard_mac_8bpp_clut();
        let entry_42 = result_ptr + 8 + 42 * 8;
        assert_eq!(bus.read_word(entry_42), 42);
        assert_eq!(bus.read_word(entry_42 + 2), canonical[42][0]);
        assert_eq!(bus.read_word(entry_42 + 4), canonical[42][1]);
        assert_eq!(bus.read_word(entry_42 + 6), canonical[42][2]);
    }

    #[test]
    fn getsubtable_pascal_procedure_preserves_stack_across_five_calls() {
        // IM:V 1986 p. V-142 (Color Manager — Color Manager Routines):
        // GetSubTable(myColors: CTabHandle; iTabRes: INTEGER;
        //             targetTbl: CTabHandle)
        // is a Pascal PROCEDURE. Each call pops 10 bytes (4-byte
        // targetTbl handle at SP+0, 2-byte iTabRes INTEGER at SP+4,
        // 4-byte myColors handle at SP+6) and writes no FUNCTION
        // result slot. This test mirrors B2 of the aa37_getsubtable_strict
        // catalog test bake.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let sp_pre = cpu.read_reg(Register::A7);

        // Helper: build a minimal 16-byte CTab record (8-byte header +
        // 1 ColorSpec) and return a handle pointing at it. ctSize=0
        // (1 entry: index 0).
        fn make_ctab(bus: &mut crate::memory::MacMemoryBus, seed: u32) -> u32 {
            let rec = bus.alloc(16);
            bus.fill_zeros(rec, 16);
            bus.write_long(rec, seed); // ctSeed
            bus.write_word(rec + 4, 0); // ctFlags
            bus.write_word(rec + 6, 0); // ctSize (1 entry)
            let handle = bus.alloc(4);
            bus.write_long(handle, rec);
            handle
        }

        // 5 distinct GetSubTable arg sets (myColors + targetTbl).
        let args: Vec<(u32, u32)> = (0..5)
            .map(|i| {
                let my = make_ctab(&mut bus, 0x6001 + i as u32);
                let target = make_ctab(&mut bus, 0x6101 + i as u32);
                (my, target)
            })
            .collect();

        for (i, (my, target)) in args.iter().enumerate() {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 10);
            bus.write_long(sp - 10, *target); // targetTbl at SP+0
            bus.write_word(sp - 6, 4); // iTabRes at SP+4
            bus.write_long(sp - 4, *my); // myColors at SP+6
            let r = d.dispatch_quickdraw(true, 0x237, &mut cpu, &mut bus);
            assert!(
                r.unwrap().is_ok(),
                "GetSubTable dispatch should succeed (call {})",
                i
            );
            assert_eq!(
                cpu.read_reg(Register::A7),
                sp,
                "GetSubTable should pop 10 bytes per call (call {})",
                i
            );
        }

        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_pre,
            "5 successive GetSubTable calls should net-balance A7"
        );
    }

    #[test]
    fn makeitable_pascal_procedure_preserves_stack_across_five_calls() {
        // IM:V 1986 p. V-142 (Color Manager — Color Manager Routines):
        // MakeITable(cTabH: CTabHandle; iTabH: ITabHandle; res: INTEGER)
        // is a Pascal PROCEDURE. Each call pops 10 bytes (2-byte res
        // INTEGER at SP+0, 4-byte iTabH at SP+2, 4-byte cTabH at SP+6;
        // Pascal pushes left-to-right so cTabH is pushed first and ends
        // up at the deepest slot — note this differs from AA37
        // GetSubTable which has the INTEGER in the middle slot SP+4).
        // No FUNCTION result slot. This test mirrors B2 of the
        // aa39_makeitable_strict catalog test bake.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let sp_pre = cpu.read_reg(Register::A7);

        // Helper: build a minimal 16-byte CTab record (1 entry).
        fn make_ctab(bus: &mut crate::memory::MacMemoryBus, seed: u32) -> u32 {
            let rec = bus.alloc(16);
            bus.fill_zeros(rec, 16);
            bus.write_long(rec, seed); // ctSeed
            bus.write_word(rec + 4, 0); // ctFlags
            bus.write_word(rec + 6, 0); // ctSize (1 entry)
            let handle = bus.alloc(4);
            bus.write_long(handle, rec);
            handle
        }

        // Helper: build an ITab record sized for res=3 (4 + 2 + 512).
        fn make_itab(bus: &mut crate::memory::MacMemoryBus) -> u32 {
            let rec = bus.alloc(518);
            bus.fill_zeros(rec, 518);
            let handle = bus.alloc(4);
            bus.write_long(handle, rec);
            handle
        }

        // 5 distinct MakeITable arg sets (cTabH + iTabH).
        let args: Vec<(u32, u32)> = (0..5)
            .map(|i| {
                let ctab = make_ctab(&mut bus, 0x7101 + i as u32);
                let itab = make_itab(&mut bus);
                (ctab, itab)
            })
            .collect();

        for (i, (ctab, itab)) in args.iter().enumerate() {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 10);
            bus.write_word(sp - 10, 3); // res INTEGER at SP+0
            bus.write_long(sp - 8, *itab); // iTabH at SP+2
            bus.write_long(sp - 4, *ctab); // cTabH at SP+6
            let r = d.dispatch_quickdraw(true, 0x239, &mut cpu, &mut bus);
            assert!(
                r.unwrap().is_ok(),
                "MakeITable dispatch should succeed (call {})",
                i
            );
            assert_eq!(
                cpu.read_reg(Register::A7),
                sp,
                "MakeITable should pop 10 bytes per call (call {})",
                i
            );
        }

        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_pre,
            "5 successive MakeITable calls should net-balance A7"
        );
    }

    #[test]
    fn makeitable_nil_arguments_builds_current_gdevice_itable() {
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        bus.write_long(0x0CC8, gdh); // TheGDevice
        let gd = bus.read_long(gdh);
        assert_eq!(bus.read_long(gd + 6), 0);

        bus.write_word(TEST_SP, 0); // res=0 => gdResPref
        bus.write_long(TEST_SP + 2, 0); // NIL iTabH
        bus.write_long(TEST_SP + 6, 0); // NIL cTabH

        let result = d.dispatch_quickdraw(true, 0x239, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);

        let itab_handle = bus.read_long(gd + 6);
        assert_ne!(itab_handle, 0);
        let itab_ptr = bus.read_long(itab_handle);
        assert_ne!(itab_ptr, 0);
        assert_eq!(bus.get_alloc_size(itab_ptr), Some(6 + 4096));
        assert_eq!(bus.read_long(itab_ptr), 8);
        assert_eq!(bus.read_word(itab_ptr + 4), 4);

        let standard = TrapDispatcher::standard_mac_8bpp_itable();
        assert_eq!(
            bus.read_bytes(itab_ptr + 6, standard.len()),
            standard.to_vec()
        );
    }

    #[test]
    fn makeitable_fills_explicit_itable_handle_without_attaching_to_gdevice() {
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        bus.write_long(0x0CC8, gdh); // TheGDevice
        let gd = bus.read_long(gdh);

        let ctab_ptr = bus.alloc(8 + 2 * 8);
        bus.write_long(ctab_ptr, 0x1234_5678);
        bus.write_word(ctab_ptr + 4, 0);
        bus.write_word(ctab_ptr + 6, 1);
        bus.write_word(ctab_ptr + 8, 0);
        bus.write_word(ctab_ptr + 10, 0xFFFF);
        bus.write_word(ctab_ptr + 12, 0xFFFF);
        bus.write_word(ctab_ptr + 14, 0xFFFF);
        bus.write_word(ctab_ptr + 16, 1);
        bus.write_word(ctab_ptr + 18, 0);
        bus.write_word(ctab_ptr + 20, 0);
        bus.write_word(ctab_ptr + 22, 0);
        let ctab_handle = bus.alloc(4);
        bus.write_long(ctab_handle, ctab_ptr);

        let stale_itab_ptr = bus.alloc(1);
        let itab_handle = bus.alloc(4);
        bus.write_long(itab_handle, stale_itab_ptr);

        bus.write_word(TEST_SP, 3);
        bus.write_long(TEST_SP + 2, itab_handle);
        bus.write_long(TEST_SP + 6, ctab_handle);

        let result = d.dispatch_quickdraw(true, 0x239, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(bus.read_long(gd + 6), 0);

        let itab_ptr = bus.read_long(itab_handle);
        assert_ne!(itab_ptr, 0);
        assert_eq!(bus.get_alloc_size(itab_ptr), Some(6 + 512));
        assert_eq!(bus.read_long(itab_ptr), 0x1234_5678);
        assert_eq!(bus.read_word(itab_ptr + 4), 3);
        assert!(bus
            .read_bytes(itab_ptr + 6, 512)
            .iter()
            .any(|&byte| byte != 0));
    }

    #[test]
    fn makeitable_reuses_cached_inverse_bytes_but_writes_target_seed() {
        fn make_ctab(bus: &mut MacMemoryBus, seed: u32) -> u32 {
            let ctab_ptr = bus.alloc(8 + 2 * 8);
            bus.write_long(ctab_ptr, seed);
            bus.write_word(ctab_ptr + 4, 0);
            bus.write_word(ctab_ptr + 6, 1);
            bus.write_word(ctab_ptr + 8, 0);
            bus.write_word(ctab_ptr + 10, 0xFFFF);
            bus.write_word(ctab_ptr + 12, 0xFFFF);
            bus.write_word(ctab_ptr + 14, 0xFFFF);
            bus.write_word(ctab_ptr + 16, 1);
            bus.write_word(ctab_ptr + 18, 0x1111);
            bus.write_word(ctab_ptr + 20, 0x2222);
            bus.write_word(ctab_ptr + 22, 0x3333);
            let ctab_handle = bus.alloc(4);
            bus.write_long(ctab_handle, ctab_ptr);
            ctab_handle
        }

        fn make_itab(bus: &mut MacMemoryBus) -> u32 {
            let itab_ptr = bus.alloc(0);
            let itab_handle = bus.alloc(4);
            bus.write_long(itab_handle, itab_ptr);
            itab_handle
        }

        let (mut d, mut cpu, mut bus) = setup();
        let first_ctab = make_ctab(&mut bus, 0x1111_2222);
        let second_ctab = make_ctab(&mut bus, 0x3333_4444);
        let first_itab = make_itab(&mut bus);
        let second_itab = make_itab(&mut bus);

        bus.write_word(TEST_SP, 4);
        bus.write_long(TEST_SP + 2, first_itab);
        bus.write_long(TEST_SP + 6, first_ctab);
        let first = d.dispatch_quickdraw(true, 0x239, &mut cpu, &mut bus);
        assert!(first.unwrap().is_ok());
        assert_eq!(d.inverse_table_cache.len(), 1);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 4);
        bus.write_long(TEST_SP + 2, second_itab);
        bus.write_long(TEST_SP + 6, second_ctab);
        let second = d.dispatch_quickdraw(true, 0x239, &mut cpu, &mut bus);
        assert!(second.unwrap().is_ok());
        assert_eq!(
            d.inverse_table_cache.len(),
            1,
            "same CLUT bytes and resolution should reuse one cached inverse payload"
        );

        let first_ptr = bus.read_long(first_itab);
        let second_ptr = bus.read_long(second_itab);
        assert_eq!(bus.read_long(first_ptr), 0x1111_2222);
        assert_eq!(
            bus.read_long(second_ptr),
            0x3333_4444,
            "MakeITable must still copy the source CTab seed into each target ITab"
        );
        assert_eq!(
            bus.read_bytes(first_ptr + 6, 4096),
            bus.read_bytes(second_ptr + 6, 4096)
        );
    }

    #[test]
    fn makeitable_cache_key_uses_clut_contents_not_only_seed() {
        fn make_ctab(bus: &mut MacMemoryBus, red: u16, green: u16, blue: u16) -> u32 {
            let ctab_ptr = bus.alloc(8 + 2 * 8);
            bus.write_long(ctab_ptr, 0x5555_6666);
            bus.write_word(ctab_ptr + 4, 0);
            bus.write_word(ctab_ptr + 6, 1);
            bus.write_word(ctab_ptr + 8, 0);
            bus.write_word(ctab_ptr + 10, 0xFFFF);
            bus.write_word(ctab_ptr + 12, 0xFFFF);
            bus.write_word(ctab_ptr + 14, 0xFFFF);
            bus.write_word(ctab_ptr + 16, 1);
            bus.write_word(ctab_ptr + 18, red);
            bus.write_word(ctab_ptr + 20, green);
            bus.write_word(ctab_ptr + 22, blue);
            let ctab_handle = bus.alloc(4);
            bus.write_long(ctab_handle, ctab_ptr);
            ctab_handle
        }

        fn make_itab(bus: &mut MacMemoryBus) -> u32 {
            let itab_ptr = bus.alloc(0);
            let itab_handle = bus.alloc(4);
            bus.write_long(itab_handle, itab_ptr);
            itab_handle
        }

        let (mut d, mut cpu, mut bus) = setup();
        let red_ctab = make_ctab(&mut bus, 0xFFFF, 0x0000, 0x0000);
        let green_ctab = make_ctab(&mut bus, 0x0000, 0xFFFF, 0x0000);
        let red_itab = make_itab(&mut bus);
        let green_itab = make_itab(&mut bus);

        bus.write_word(TEST_SP, 4);
        bus.write_long(TEST_SP + 2, red_itab);
        bus.write_long(TEST_SP + 6, red_ctab);
        let red = d.dispatch_quickdraw(true, 0x239, &mut cpu, &mut bus);
        assert!(red.unwrap().is_ok());
        assert_eq!(d.inverse_table_cache.len(), 1);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 4);
        bus.write_long(TEST_SP + 2, green_itab);
        bus.write_long(TEST_SP + 6, green_ctab);
        let green = d.dispatch_quickdraw(true, 0x239, &mut cpu, &mut bus);
        assert!(green.unwrap().is_ok());
        assert_eq!(
            d.inverse_table_cache.len(),
            2,
            "same ctSeed with different RGB entries must build a distinct inverse payload"
        );
    }

    #[test]
    fn index2color_realcolor_pascal_call_preserves_stack_across_five_calls() {
        // IM:V 1986 p. V-141 (Color Manager — Color Manager Routines —
        // Color Conversion):
        //   PROCEDURE Index2Color(index: LONGINT; VAR rgb: RGBColor) — pop-8
        //   FUNCTION  RealColor(color: RGBColor): BOOLEAN — pop-4 + 2-byte
        //     result slot pre-pushed by caller; net A7 zero across the
        //     C-level call.
        // Mirrors B2 + B4 of the aa34_aa36_index2color_realcolor_strict
        // catalog test bake via 5 successive dispatch_quickdraw(true, 0x234)
        // Index2Color calls then 5 successive dispatch_quickdraw(true, 0x236)
        // RealColor calls.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let gdh = d.ensure_main_gdevice(&mut bus);
        bus.write_long(0x0CC8, gdh); // TheGDevice

        let sp_pre = cpu.read_reg(Register::A7);

        // ---- 5 Index2Color calls with distinct (index, rgb_ptr) ----
        let rgb_base = bus.alloc(5 * 8); // 5 RGBColor records (6 bytes + pad)
        let indices: [u32; 5] = [1, 17, 42, 128, 255];
        for (i, &idx) in indices.iter().enumerate() {
            let rgb_ptr = rgb_base + (i as u32) * 8;
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 8);
            bus.write_long(sp - 8, rgb_ptr); // rgb_ptr at SP+0
            bus.write_long(sp - 4, idx); // index at SP+4
            let r = d.dispatch_quickdraw(true, 0x234, &mut cpu, &mut bus);
            assert!(
                r.unwrap().is_ok(),
                "Index2Color dispatch should succeed (call {})",
                i
            );
            assert_eq!(
                cpu.read_reg(Register::A7),
                sp,
                "Index2Color should pop 8 bytes per call (call {})",
                i
            );
        }
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_pre,
            "5 successive Index2Color calls should net-balance A7"
        );

        // ---- 5 RealColor calls with distinct RGBColor pointers ----
        let rc_base = bus.alloc(5 * 8);
        let rgbs: [[u16; 3]; 5] = [
            [0x0001, 0x0002, 0x0003],
            [0x1111, 0x2222, 0x3333],
            [0x4444, 0x5555, 0x6666],
            [0x7777, 0x8888, 0x9999],
            [0xAAAA, 0xBBBB, 0xCCCC],
        ];
        for (i, rgb) in rgbs.iter().enumerate() {
            let rgb_ptr = rc_base + (i as u32) * 8;
            bus.write_word(rgb_ptr, rgb[0]);
            bus.write_word(rgb_ptr + 2, rgb[1]);
            bus.write_word(rgb_ptr + 4, rgb[2]);
            let sp = cpu.read_reg(Register::A7);
            // Caller pre-pushes 2-byte BOOLEAN result slot then 4-byte
            // RGBColor pointer.
            cpu.write_reg(Register::A7, sp - 6);
            bus.write_word(sp - 6, 0xDEAD); // sentinel for result slot
            bus.write_long(sp - 4, rgb_ptr); // rgb_ptr at SP+0
            let r = d.dispatch_quickdraw(true, 0x236, &mut cpu, &mut bus);
            assert!(
                r.unwrap().is_ok(),
                "RealColor dispatch should succeed (call {})",
                i
            );
            // Trap pops 4 bytes (the arg); A7 now points at the result slot.
            assert_eq!(
                cpu.read_reg(Register::A7),
                sp - 2,
                "RealColor should pop 4-byte arg leaving 2-byte result slot (call {})",
                i
            );
            // Caller pops the 2-byte result slot.
            cpu.write_reg(Register::A7, sp);
        }
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_pre,
            "5 successive RealColor calls should net-balance A7"
        );
    }

    #[test]
    fn fillcrect_uses_supplied_pixpat_pat1data_to_fill_rect_interior() {
        // IM:V 1986 p. V-73 + Imaging With QuickDraw 1994 p. 4-81:
        // FillCRect fills with the supplied PixPat; with pat1Data
        // fallback, interior pixels use that pattern.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let (screen_base, row_bytes) = setup_polygon_surface(&mut d, &mut bus);
        let rect_ptr = bus.alloc(8);
        write_rect(&mut bus, rect_ptr, 10, 10, 30, 30);
        let pp_handle = make_pixpat_handle(&mut bus, [0xFF; 8]);

        bus.write_long(TEST_SP, pp_handle);
        bus.write_long(TEST_SP + 4, rect_ptr);

        let result = d.dispatch_quickdraw(true, 0x20E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            read_surface_pixel(&bus, screen_base, row_bytes, 20, 20),
            0xFF,
            "FillCRect should fill pixels inside the rectangle with the supplied pat1Data pattern"
        );
        assert_eq!(
            read_surface_pixel(&bus, screen_base, row_bytes, 5, 5),
            0x00,
            "FillCRect should not alter pixels outside the requested rectangle"
        );
    }

    #[test]
    fn fillcrect_tiles_raw_color_pixpat_resource_on_8bpp_cgrafport() {
        let (mut d, mut cpu, mut bus) = setup();
        let row_bytes = 64u32;
        let screen_base = bus.alloc(row_bytes * 64);
        bus.fill_zeros(screen_base, row_bytes * 64);
        d.screen_mode = (screen_base, row_bytes, 64, 64, 8);
        d.device_clut = [[0, 0, 0]; 256];
        d.device_clut[7] = [0x1111, 0x0000, 0x0000];
        d.device_clut[8] = [0x0000, 0x2222, 0x0000];
        d.device_clut[9] = [0x0000, 0x0000, 0x3333];

        let port = bus.alloc(128);
        let pixmap_handle = bus.alloc(4);
        let pixmap = bus.alloc(50);
        bus.write_long(pixmap_handle, pixmap);
        bus.write_long(port + 2, pixmap_handle);
        bus.write_word(port + 6, 0xC000);
        bus.write_long(pixmap, screen_base);
        bus.write_word(pixmap + 4, 0x8000 | row_bytes as u16);
        bus.write_word(pixmap + 6, 0);
        bus.write_word(pixmap + 8, 0);
        bus.write_word(pixmap + 10, 64);
        bus.write_word(pixmap + 12, 64);
        bus.write_word(pixmap + 32, 8);

        let vis = bus.alloc(10);
        bus.write_word(vis, 10);
        bus.write_word(vis + 6, 64);
        bus.write_word(vis + 8, 64);
        let vis_handle = bus.alloc(4);
        bus.write_long(vis_handle, vis);
        bus.write_long(port + 24, vis_handle);

        let clip = bus.alloc(10);
        bus.write_word(clip, 10);
        bus.write_word(clip + 6, 64);
        bus.write_word(clip + 8, 64);
        let clip_handle = bus.alloc(4);
        bus.write_long(clip_handle, clip);
        bus.write_long(port + 28, clip_handle);

        d.current_port = port;
        let globals_ptr = bus.read_long(cpu.read_reg(Register::A5));
        bus.write_long(globals_ptr, port);

        let rect_ptr = bus.alloc(8);
        write_rect(&mut bus, rect_ptr, 0, 0, 2, 4);
        let pp_handle = make_raw_color_pixpat_handle(&mut bus);

        bus.write_long(TEST_SP, pp_handle);
        bus.write_long(TEST_SP + 4, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x20E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_eq!(read_surface_pixel(&bus, screen_base, row_bytes, 0, 0), 7);
        assert_eq!(read_surface_pixel(&bus, screen_base, row_bytes, 1, 0), 8);
        assert_eq!(read_surface_pixel(&bus, screen_base, row_bytes, 2, 0), 9);
        assert_eq!(read_surface_pixel(&bus, screen_base, row_bytes, 3, 0), 8);
        assert_eq!(read_surface_pixel(&bus, screen_base, row_bytes, 0, 1), 9);
        assert_eq!(read_surface_pixel(&bus, screen_base, row_bytes, 1, 1), 8);
        assert_eq!(read_surface_pixel(&bus, screen_base, row_bytes, 2, 1), 7);
        assert_eq!(read_surface_pixel(&bus, screen_base, row_bytes, 3, 1), 8);
    }

    #[test]
    fn fillcrect_full_bounds_raw_pixpat_repaints_offset_backing_store() {
        let (mut d, mut cpu, mut bus) = setup();
        let row_bytes = 64u32;
        let screen_base = bus.alloc(row_bytes * 64);
        bus.fill_zeros(screen_base, row_bytes * 64);
        d.screen_mode = (screen_base, row_bytes, 64, 64, 8);
        d.device_clut = [[0, 0, 0]; 256];
        d.device_clut[7] = [0x1111, 0x0000, 0x0000];
        d.device_clut[8] = [0x0000, 0x2222, 0x0000];
        d.device_clut[9] = [0x0000, 0x0000, 0x3333];

        let port = bus.alloc(128);
        let pixmap_handle = bus.alloc(4);
        let pixmap = bus.alloc(50);
        bus.write_long(pixmap_handle, pixmap);
        bus.write_long(port + 2, pixmap_handle);
        bus.write_word(port + 6, 0xC000);
        bus.write_long(pixmap, screen_base);
        bus.write_word(pixmap + 4, 0x8000 | row_bytes as u16);
        bus.write_word(pixmap + 6, (-2i16) as u16);
        bus.write_word(pixmap + 8, (-3i16) as u16);
        bus.write_word(pixmap + 10, 2);
        bus.write_word(pixmap + 12, 5);
        bus.write_word(pixmap + 32, 8);

        let vis = bus.alloc(10);
        bus.write_word(vis, 10);
        bus.write_word(vis + 2, 0);
        bus.write_word(vis + 4, 0);
        bus.write_word(vis + 6, 1);
        bus.write_word(vis + 8, 1);
        let vis_handle = bus.alloc(4);
        bus.write_long(vis_handle, vis);
        bus.write_long(port + 24, vis_handle);

        let clip = bus.alloc(10);
        bus.write_word(clip, 10);
        bus.write_word(clip + 2, 0);
        bus.write_word(clip + 4, 0);
        bus.write_word(clip + 6, 1);
        bus.write_word(clip + 8, 1);
        let clip_handle = bus.alloc(4);
        bus.write_long(clip_handle, clip);
        bus.write_long(port + 28, clip_handle);

        d.current_port = port;
        let globals_ptr = bus.read_long(cpu.read_reg(Register::A5));
        bus.write_long(globals_ptr, port);

        let pp_handle = make_raw_color_pixpat_handle(&mut bus);
        let rect_ptr = bus.alloc(8);
        write_rect(&mut bus, rect_ptr, -2, -3, 0, -1);
        bus.write_long(TEST_SP, pp_handle);
        bus.write_long(TEST_SP + 4, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x20E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            read_surface_pixel(&bus, screen_base, row_bytes, 0, 0),
            0,
            "non-full raw PixPat fills still honor the current clip region"
        );

        write_rect(&mut bus, rect_ptr, -2, -3, 2, 5);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, pp_handle);
        bus.write_long(TEST_SP + 4, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x20E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            read_surface_pixel(&bus, screen_base, row_bytes, 0, 0),
            8,
            "full-bounds raw PixPat fills repaint offset backing pixels outside stale clip bounds"
        );
    }

    #[test]
    fn fillcrect_hidden_menu_screen_sized_raw_pixpat_repaints_full_backing_store() {
        let (mut d, mut cpu, mut bus) = setup();
        let row_bytes = 64u32;
        let screen_base = bus.alloc(row_bytes * 64);
        bus.fill_zeros(screen_base, row_bytes * 64);
        d.screen_mode = (screen_base, row_bytes, 64, 64, 8);
        d.device_clut = [[0, 0, 0]; 256];
        d.device_clut[7] = [0x1111, 0x0000, 0x0000];
        d.device_clut[8] = [0x0000, 0x2222, 0x0000];
        d.device_clut[9] = [0x0000, 0x0000, 0x3333];

        let port = bus.alloc(128);
        let pixmap_handle = bus.alloc(4);
        let pixmap = bus.alloc(50);
        bus.write_long(pixmap_handle, pixmap);
        bus.write_long(port + 2, pixmap_handle);
        bus.write_word(port + 6, 0xC000);
        bus.write_long(pixmap, screen_base);
        bus.write_word(pixmap + 4, 0x8000 | row_bytes as u16);
        bus.write_word(pixmap + 6, (-20i16) as u16);
        bus.write_word(pixmap + 8, (-1i16) as u16);
        bus.write_word(pixmap + 10, 44);
        bus.write_word(pixmap + 12, 63);
        bus.write_word(pixmap + 32, 8);

        let vis = bus.alloc(10);
        bus.write_word(vis, 10);
        bus.write_word(vis + 2, (-20i16) as u16);
        bus.write_word(vis + 4, (-1i16) as u16);
        bus.write_word(vis + 6, 44);
        bus.write_word(vis + 8, 63);
        let vis_handle = bus.alloc(4);
        bus.write_long(vis_handle, vis);
        bus.write_long(port + 24, vis_handle);

        let clip = bus.alloc(10);
        bus.write_word(clip, 10);
        bus.write_word(clip + 2, 0);
        bus.write_word(clip + 4, 0);
        bus.write_word(clip + 6, 24);
        bus.write_word(clip + 8, 62);
        let clip_handle = bus.alloc(4);
        bus.write_long(clip_handle, clip);
        bus.write_long(port + 28, clip_handle);

        d.current_port = port;
        d.front_window = port;
        d.menu_bar_hidden = true;
        let globals_ptr = bus.read_long(cpu.read_reg(Register::A5));
        bus.write_long(globals_ptr, port);

        let pp_handle = make_raw_color_pixpat_handle(&mut bus);
        let rect_ptr = bus.alloc(8);
        write_rect(&mut bus, rect_ptr, -40, -2, 24, 62);
        bus.write_long(TEST_SP, pp_handle);
        bus.write_long(TEST_SP + 4, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x20E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_ne!(
            read_surface_pixel(&bus, screen_base, row_bytes, 0, 0),
            0,
            "hidden-menu screen-sized backdrop fills must repaint the exposed top row"
        );
        assert_ne!(
            read_surface_pixel(&bus, screen_base, row_bytes, 63, 63),
            0,
            "hidden-menu screen-sized backdrop fills must repaint the exposed bottom row"
        );
    }

    #[test]
    fn fillcpoly_consumes_pp_and_polyhandle_arguments() {
        // IM:V 1986 p. V-74: FillCPoly consumes PixPatHandle +
        // PolyHandle (8 bytes total) as a procedure call.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let poly_ptr = bus.alloc(26);
        let poly_handle = bus.alloc(4);
        make_poly(
            &mut bus,
            poly_ptr,
            poly_handle,
            (10, 10, 30, 30),
            &[(10, 10), (10, 30), (30, 30), (30, 10)],
        );
        let pp_handle = make_pixpat_handle(&mut bus, [0xFF; 8]);

        bus.write_long(TEST_SP, pp_handle);
        bus.write_long(TEST_SP + 4, poly_handle);

        let result = d.dispatch_quickdraw(true, 0x213, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
    }

    #[test]
    fn fillcpoly_uses_supplied_pixpat_pat1data_to_fill_polygon_interior() {
        // IM:V 1986 p. V-74 + Imaging With QuickDraw 1994 p. 4-81:
        // FillCPoly fills with the supplied PixPat; with pat1Data
        // fallback, polygon interior pixels use that pattern.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let (screen_base, row_bytes) = setup_polygon_surface(&mut d, &mut bus);
        let poly_ptr = bus.alloc(26);
        let poly_handle = bus.alloc(4);
        make_poly(
            &mut bus,
            poly_ptr,
            poly_handle,
            (10, 10, 30, 30),
            &[(10, 10), (10, 30), (30, 30), (30, 10)],
        );
        let pp_handle = make_pixpat_handle(&mut bus, [0xFF; 8]);

        bus.write_long(TEST_SP, pp_handle);
        bus.write_long(TEST_SP + 4, poly_handle);

        let result = d.dispatch_quickdraw(true, 0x213, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            read_surface_pixel(&bus, screen_base, row_bytes, 20, 20),
            0xFF,
            "FillCPoly should fill pixels inside the polygon with the supplied pat1Data pattern"
        );
        assert_eq!(
            read_surface_pixel(&bus, screen_base, row_bytes, 5, 5),
            0x00,
            "FillCPoly should not alter pixels outside the polygon bounds"
        );
    }

    #[test]
    fn fillcpoly_pascal_procedure_preserves_stack_across_five_calls() {
        // IM:V 1986 p. V-69 (Color QuickDraw — Color Drawing
        // Operations — FillCPoly): Pascal PROCEDURE
        // FillCPoly(poly: PolyHandle; pp: PixPatHandle). Each call
        // pops 8 bytes (4-byte PolyHandle + 4-byte PixPatHandle) and
        // writes no FUNCTION result slot. This test mirrors B2 of
        // the aa13_fillcpoly_strict catalog test bake: 5 successive
        // FillCPoly calls each net-balance A7.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let sp_pre = cpu.read_reg(Register::A7);

        let polys: [u32; 5] = std::array::from_fn(|i| {
            let poly_ptr = bus.alloc(26);
            let poly_handle = bus.alloc(4);
            let x0 = (i as i16) * 3;
            make_poly(
                &mut bus,
                poly_ptr,
                poly_handle,
                (0, x0, 2, x0 + 2),
                &[(x0, 0), (x0 + 2, 0), (x0 + 1, 2), (x0, 0)],
            );
            poly_handle
        });
        let pps = [
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
            make_pixpat_handle(&mut bus, [0; 8]),
        ];

        for i in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 8);
            bus.write_long(sp - 8, pps[i]);
            bus.write_long(sp - 4, polys[i]);
            let result = d.dispatch_quickdraw(true, 0x213, &mut cpu, &mut bus);
            assert!(
                result.unwrap().is_ok(),
                "FillCPoly dispatch should succeed (call {})",
                i
            );
            assert_eq!(
                cpu.read_reg(Register::A7),
                sp,
                "FillCPoly should pop 8 bytes per call (call {})",
                i
            );
        }

        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_pre,
            "5 successive FillCPoly calls should net-balance A7"
        );
    }

    #[test]
    fn fillcroundrect_consumes_pp_ovht_ovwid_rect_arguments() {
        // IM:V 1986 p. V-73: FillCRoundRect consumes PixPatHandle +
        // ovHt + ovWid + Rect* (12 bytes total) as a procedure call.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let (screen_base, row_bytes) = setup_polygon_surface(&mut d, &mut bus);

        let rect_ptr = bus.alloc(8);
        write_rect(&mut bus, rect_ptr, 10, 10, 30, 30);
        let pp_handle = make_pixpat_handle(&mut bus, [0xFF; 8]);

        bus.write_long(TEST_SP, pp_handle);
        bus.write_word(TEST_SP + 4, 10u16); // ovHt
        bus.write_word(TEST_SP + 6, 10u16); // ovWid
        bus.write_long(TEST_SP + 8, rect_ptr);

        let result = d.dispatch_quickdraw(true, 0x210, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(
            read_surface_pixel(&bus, screen_base, row_bytes, 20, 20),
            0xFF,
            "FillCRoundRect should rasterize interior pixels for a solid pat1Data pattern"
        );
    }

    #[test]
    fn fillcroundrect_uses_pixpat_pat1data_and_rounded_geometry() {
        // IM:V 1986 p. V-73 + Imaging With QuickDraw 1994 p. 4-81:
        // FillCRoundRect fills with the supplied PixPat; with pat1Data
        // fallback, corner pixels outside the rounded geometry remain clear.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let (screen_base, row_bytes) = setup_polygon_surface(&mut d, &mut bus);

        let rect_ptr = bus.alloc(8);
        write_rect(&mut bus, rect_ptr, 10, 10, 30, 30);
        let pp_handle = make_pixpat_handle(&mut bus, [0xFF; 8]);

        bus.write_long(TEST_SP, pp_handle);
        bus.write_word(TEST_SP + 4, 14u16); // ovHt
        bus.write_word(TEST_SP + 6, 14u16); // ovWid
        bus.write_long(TEST_SP + 8, rect_ptr);

        let result = d.dispatch_quickdraw(true, 0x210, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_eq!(
            read_surface_pixel(&bus, screen_base, row_bytes, 20, 20),
            0xFF,
            "FillCRoundRect should fill the rounded-rectangle interior"
        );
        assert_eq!(
            read_surface_pixel(&bus, screen_base, row_bytes, 10, 10),
            0x00,
            "FillCRoundRect should leave clipped corner pixels outside the rounded geometry unchanged"
        );
    }

    #[test]
    fn fillcarc_consumes_pp_arcangle_startangle_rect_arguments() {
        // IM:V 1986 p. V-73: FillCArc consumes PixPatHandle + arcAngle +
        // startAngle + Rect* (12 bytes total) as a procedure call.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = bus.alloc(8);
        write_rect(&mut bus, rect_ptr, 10, 10, 30, 30);
        let pp_handle = make_pixpat_handle(&mut bus, [0xFF; 8]);

        bus.write_long(TEST_SP, pp_handle);
        bus.write_word(TEST_SP + 4, 90u16); // arcAngle
        bus.write_word(TEST_SP + 6, 0u16); // startAngle
        bus.write_long(TEST_SP + 8, rect_ptr);

        let result = d.dispatch_quickdraw(true, 0x211, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
    }

    #[test]
    fn fillcarc_startangle_and_arcangle_bound_filled_wedge() {
        // IM:I 1985 p. I-184 arc convention (0° = 12 o'clock, clockwise)
        // applies to FillCArc's color-pattern fill path (IM:V V-73).
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let (screen_base, row_bytes) = setup_polygon_surface(&mut d, &mut bus);

        let rect_ptr = bus.alloc(8);
        write_rect(&mut bus, rect_ptr, 10, 10, 30, 30);
        let pp_handle = make_pixpat_handle(&mut bus, [0xFF; 8]);

        bus.write_long(TEST_SP, pp_handle);
        bus.write_word(TEST_SP + 4, 90u16); // arcAngle
        bus.write_word(TEST_SP + 6, 0u16); // startAngle
        bus.write_long(TEST_SP + 8, rect_ptr);

        let result = d.dispatch_quickdraw(true, 0x211, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_eq!(
            read_surface_pixel(&bus, screen_base, row_bytes, 24, 14),
            0xFF,
            "FillCArc should fill pixels inside the requested 0..90 degree wedge"
        );
        assert_eq!(
            read_surface_pixel(&bus, screen_base, row_bytes, 14, 24),
            0x00,
            "FillCArc should leave pixels outside the wedge unchanged"
        );
    }

    // IM:I I-190: when a region is open and being formed, FramePoly adds the
    // polygon's outside outline to the region boundary and pops one PolyHandle
    // argument.
    #[test]
    fn framepoly_open_region_updates_recorded_bounds_and_pops_arg() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let (screen_base, row_bytes) = setup_polygon_surface(&mut d, &mut bus);

        let poly_ptr = bus.alloc(22);
        let poly_handle = bus.alloc(4);
        make_poly(
            &mut bus,
            poly_ptr,
            poly_handle,
            (10, 10, 20, 20),
            &[(10, 10), (10, 20), (20, 20)],
        );

        d.recording_region = Some(super::super::dispatch::RegionRecording::default());
        bus.write_long(TEST_SP, poly_handle);
        let result = d.dispatch_quickdraw(true, 0x0C6, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(
            d.recording_region
                .as_ref()
                .and_then(|recording| recording.bbox),
            Some((10, 10, 20, 20)),
            "FramePoly should fold polygon bounds into the active OpenRgn recording extent"
        );
        assert_eq!(
            bus.read_byte(screen_base + 15 * row_bytes + 15),
            0x00,
            "FramePoly should not rasterize while OpenRgn recording is active"
        );
    }

    // IM:I I-190: PaintPoly fills polygon interior using pnPat/pnMode and pops
    // one PolyHandle argument.
    #[test]
    fn paintpoly_fills_polygon_with_pen_pattern_and_pops_arg() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let (screen_base, row_bytes) = setup_polygon_surface(&mut d, &mut bus);

        d.pn_pat = [0xFF; 8];

        let poly_ptr = bus.alloc(22);
        let poly_handle = bus.alloc(4);
        make_poly(
            &mut bus,
            poly_ptr,
            poly_handle,
            (10, 10, 20, 20),
            &[(10, 10), (10, 20), (20, 10)],
        );

        bus.write_long(TEST_SP, poly_handle);
        let result = d.dispatch_quickdraw(true, 0x0C7, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        let inside = screen_base + 13 * row_bytes + 13;
        let outside = screen_base + 30 * row_bytes + 30;
        assert_eq!(
            bus.read_byte(inside),
            0xFF,
            "PaintPoly should paint interior pixels with the pen pattern"
        );
        assert_eq!(
            bus.read_byte(outside),
            0x00,
            "PaintPoly should not paint pixels outside polygon bounds"
        );
    }

    // IM:I I-190: ErasePoly fills polygon interior with bkPat in patCopy mode
    // and pops one PolyHandle argument.
    #[test]
    fn erasepoly_uses_background_pattern_and_pops_arg() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let (screen_base, row_bytes) = setup_polygon_surface(&mut d, &mut bus);

        for offset in 0..(row_bytes * 64) {
            bus.write_byte(screen_base + offset, 0xFF);
        }
        d.bk_pat = [0x00; 8];
        d.pn_pat = [0xFF; 8];

        let poly_ptr = bus.alloc(22);
        let poly_handle = bus.alloc(4);
        make_poly(
            &mut bus,
            poly_ptr,
            poly_handle,
            (10, 10, 20, 20),
            &[(10, 10), (10, 20), (20, 10)],
        );

        bus.write_long(TEST_SP, poly_handle);
        let result = d.dispatch_quickdraw(true, 0x0C8, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        let inside = screen_base + 13 * row_bytes + 13;
        let outside = screen_base + 30 * row_bytes + 30;
        assert_eq!(
            bus.read_byte(inside),
            0x00,
            "ErasePoly should paint interior pixels with the background pattern"
        );
        assert_eq!(
            bus.read_byte(outside),
            0xFF,
            "ErasePoly should not touch pixels outside polygon bounds"
        );
    }

    // IM:I I-190: InvertPoly inverts enclosed pixels and pops one PolyHandle
    // argument.
    #[test]
    fn invertpoly_inverts_polygon_pixels_and_pops_arg() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let (screen_base, row_bytes) = setup_polygon_surface(&mut d, &mut bus);

        let poly_ptr = bus.alloc(22);
        let poly_handle = bus.alloc(4);
        make_poly(
            &mut bus,
            poly_ptr,
            poly_handle,
            (10, 10, 20, 20),
            &[(10, 10), (10, 20), (20, 10)],
        );

        bus.write_long(TEST_SP, poly_handle);
        let result = d.dispatch_quickdraw(true, 0x0C9, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        let inside = screen_base + 13 * row_bytes + 13;
        let outside = screen_base + 30 * row_bytes + 30;
        assert_ne!(
            bus.read_byte(inside),
            0x00,
            "InvertPoly should invert interior pixels from their prior value"
        );
        assert_eq!(
            bus.read_byte(outside),
            0x00,
            "InvertPoly should not touch pixels outside polygon bounds"
        );
    }

    // IM:I I-190: FillPoly fills polygon interior using the caller's Pattern
    // and pops the pattern pointer + polygon handle arguments.
    #[test]
    fn fillpoly_fills_polygon_area_with_supplied_pattern_and_pops_args() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let (screen_base, row_bytes) = setup_polygon_surface(&mut d, &mut bus);

        let poly_ptr = bus.alloc(22);
        let poly_handle = bus.alloc(4);
        make_poly(
            &mut bus,
            poly_ptr,
            poly_handle,
            (10, 10, 20, 20),
            &[(10, 10), (10, 20), (20, 10)],
        );

        let pat_ptr = 0x300000u32;
        bus.write_bytes(pat_ptr, &[0xFF; 8]);
        bus.write_long(TEST_SP, pat_ptr);
        bus.write_long(TEST_SP + 4, poly_handle);

        let result = d.dispatch_quickdraw(true, 0x0CA, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);

        let inside = screen_base + 13 * row_bytes + 13;
        let outside = screen_base + 30 * row_bytes + 30;
        assert_eq!(
            bus.read_byte(inside),
            0xFF,
            "FillPoly should paint interior pixels for an all-ones pattern"
        );
        assert_eq!(
            bus.read_byte(outside),
            0x00,
            "FillPoly should not paint pixels outside polygon bounds"
        );
    }

    // IM:I I-191: KillPoly releases polygon memory and consumes one PolyHandle
    // argument from the stack.
    #[test]
    fn killpoly_releases_polygon_handle_and_data_and_pops_arg() {
        let (mut d, mut cpu, mut bus) = setup_with_port();

        let poly_ptr = bus.alloc(22);
        let poly_handle = bus.alloc(4);
        make_poly(
            &mut bus,
            poly_ptr,
            poly_handle,
            (10, 10, 20, 20),
            &[(10, 10), (10, 20), (20, 10)],
        );
        assert!(bus.get_alloc_size(poly_ptr).is_some());
        assert!(bus.get_alloc_size(poly_handle).is_some());

        bus.write_long(TEST_SP, poly_handle);
        let result = d.dispatch_quickdraw(true, 0x0CD, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert!(
            bus.get_alloc_size(poly_ptr).is_none(),
            "KillPoly should free polygon data block"
        );
        assert!(
            bus.get_alloc_size(poly_handle).is_none(),
            "KillPoly should free polygon handle"
        );
    }

    fn write_bitmap_1bpp(
        bus: &mut impl MemoryBus,
        bits_ptr: u32,
        base_addr: u32,
        row_bytes: u16,
        bounds: (i16, i16, i16, i16),
    ) {
        bus.write_long(bits_ptr, base_addr);
        bus.write_word(bits_ptr + 4, row_bytes);
        write_rect(bus, bits_ptr + 6, bounds.0, bounds.1, bounds.2, bounds.3);
    }

    // ==================== CopyMask ====================

    #[test]
    fn copymask_consumes_srcbits_maskbits_dstbits_srcrect_maskrect_dstrect_arguments() {
        // Inside Macintosh Volume IV (1986), p. IV-33:
        // CopyMask takes three BitMap parameters and three Rect parameters.
        let (mut d, mut cpu, mut bus) = setup();

        let src_bits = bus.alloc(14);
        let mask_bits = bus.alloc(14);
        let dst_bits = bus.alloc(14);
        let src_base = bus.alloc(1);
        let mask_base = bus.alloc(1);
        let dst_base = bus.alloc(1);
        write_bitmap_1bpp(&mut bus, src_bits, src_base, 1, (0, 0, 1, 8));
        write_bitmap_1bpp(&mut bus, mask_bits, mask_base, 1, (0, 0, 1, 8));
        write_bitmap_1bpp(&mut bus, dst_bits, dst_base, 1, (0, 0, 1, 8));

        let src_rect = bus.alloc(8);
        let mask_rect = bus.alloc(8);
        let dst_rect = bus.alloc(8);
        write_rect(&mut bus, src_rect, 0, 0, 1, 8);
        write_rect(&mut bus, mask_rect, 0, 0, 1, 8);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 8);

        bus.write_long(TEST_SP, dst_rect);
        bus.write_long(TEST_SP + 4, mask_rect);
        bus.write_long(TEST_SP + 8, src_rect);
        bus.write_long(TEST_SP + 12, dst_bits);
        bus.write_long(TEST_SP + 16, mask_bits);
        bus.write_long(TEST_SP + 20, src_bits);

        let result = d.dispatch_quickdraw(true, 0x017, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 24);
    }

    #[test]
    fn copymask_copies_source_bits_where_mask_bits_are_one() {
        // Inside Macintosh Volume IV (1986), p. IV-33 / Imaging With QuickDraw
        // (1994), p. 3-119: CopyMask copies source bits to destination where
        // mask bits are 1.
        let (mut d, mut cpu, mut bus) = setup();

        let src_bits = bus.alloc(14);
        let mask_bits = bus.alloc(14);
        let dst_bits = bus.alloc(14);
        let src_base = bus.alloc(1);
        let mask_base = bus.alloc(1);
        let dst_base = bus.alloc(1);
        write_bitmap_1bpp(&mut bus, src_bits, src_base, 1, (0, 0, 1, 8));
        write_bitmap_1bpp(&mut bus, mask_bits, mask_base, 1, (0, 0, 1, 8));
        write_bitmap_1bpp(&mut bus, dst_bits, dst_base, 1, (0, 0, 1, 8));

        bus.write_byte(src_base, 0b1010_0101);
        bus.write_byte(mask_base, 0xFF);
        bus.write_byte(dst_base, 0);

        let src_rect = bus.alloc(8);
        let mask_rect = bus.alloc(8);
        let dst_rect = bus.alloc(8);
        write_rect(&mut bus, src_rect, 0, 0, 1, 8);
        write_rect(&mut bus, mask_rect, 0, 0, 1, 8);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 8);

        bus.write_long(TEST_SP, dst_rect);
        bus.write_long(TEST_SP + 4, mask_rect);
        bus.write_long(TEST_SP + 8, src_rect);
        bus.write_long(TEST_SP + 12, dst_bits);
        bus.write_long(TEST_SP + 16, mask_bits);
        bus.write_long(TEST_SP + 20, src_bits);

        let result = d.dispatch_quickdraw(true, 0x017, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            bus.read_byte(dst_base),
            0b1010_0101,
            "mask bits set to 1 should copy source bits into destination"
        );
    }

    #[test]
    fn copymask_preserves_destination_bits_where_mask_bits_are_zero() {
        // Inside Macintosh Volume IV (1986), p. IV-33 / Imaging With QuickDraw
        // (1994), p. 3-119: where mask bits are 0, destination bits are left
        // unchanged.
        let (mut d, mut cpu, mut bus) = setup();

        let src_bits = bus.alloc(14);
        let mask_bits = bus.alloc(14);
        let dst_bits = bus.alloc(14);
        let src_base = bus.alloc(1);
        let mask_base = bus.alloc(1);
        let dst_base = bus.alloc(1);
        write_bitmap_1bpp(&mut bus, src_bits, src_base, 1, (0, 0, 1, 8));
        write_bitmap_1bpp(&mut bus, mask_bits, mask_base, 1, (0, 0, 1, 8));
        write_bitmap_1bpp(&mut bus, dst_bits, dst_base, 1, (0, 0, 1, 8));

        bus.write_byte(src_base, 0xFF);
        bus.write_byte(mask_base, 0x00);
        bus.write_byte(dst_base, 0b0101_1010);

        let src_rect = bus.alloc(8);
        let mask_rect = bus.alloc(8);
        let dst_rect = bus.alloc(8);
        write_rect(&mut bus, src_rect, 0, 0, 1, 8);
        write_rect(&mut bus, mask_rect, 0, 0, 1, 8);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 8);

        bus.write_long(TEST_SP, dst_rect);
        bus.write_long(TEST_SP + 4, mask_rect);
        bus.write_long(TEST_SP + 8, src_rect);
        bus.write_long(TEST_SP + 12, dst_bits);
        bus.write_long(TEST_SP + 16, mask_bits);
        bus.write_long(TEST_SP + 20, src_bits);

        let result = d.dispatch_quickdraw(true, 0x017, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            bus.read_byte(dst_base),
            0b0101_1010,
            "mask bits cleared to 0 should preserve destination bits"
        );
    }

    #[test]
    fn copymask_scales_source_and_mask_into_destination_rect() {
        // Imaging With QuickDraw (1994), pp. 3-119..3-120: CopyMask scales the
        // source image to fit dstRect when srcRect and dstRect differ.
        let (mut d, mut cpu, mut bus) = setup();

        let src_bits = bus.alloc(14);
        let mask_bits = bus.alloc(14);
        let dst_bits = bus.alloc(14);
        let src_base = bus.alloc(1);
        let mask_base = bus.alloc(1);
        let dst_base = bus.alloc(1);
        write_bitmap_1bpp(&mut bus, src_bits, src_base, 1, (0, 0, 1, 4));
        write_bitmap_1bpp(&mut bus, mask_bits, mask_base, 1, (0, 0, 1, 4));
        write_bitmap_1bpp(&mut bus, dst_bits, dst_base, 1, (0, 0, 1, 8));

        bus.write_byte(src_base, 0b1010_0000);
        bus.write_byte(mask_base, 0b1111_0000);
        bus.write_byte(dst_base, 0);

        let src_rect = bus.alloc(8);
        let mask_rect = bus.alloc(8);
        let dst_rect = bus.alloc(8);
        write_rect(&mut bus, src_rect, 0, 0, 1, 4);
        write_rect(&mut bus, mask_rect, 0, 0, 1, 4);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 8);

        bus.write_long(TEST_SP, dst_rect);
        bus.write_long(TEST_SP + 4, mask_rect);
        bus.write_long(TEST_SP + 8, src_rect);
        bus.write_long(TEST_SP + 12, dst_bits);
        bus.write_long(TEST_SP + 16, mask_bits);
        bus.write_long(TEST_SP + 20, src_bits);

        let result = d.dispatch_quickdraw(true, 0x017, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 24);
        assert_eq!(
            bus.read_byte(dst_base),
            0b1100_1100,
            "4 source pixels should scale to 8 destination pixels through the mask"
        );
    }

    #[test]
    fn copydeepmask_consumes_src_mask_dst_rects_mode_and_maskrgn_arguments() {
        // Inside Macintosh Volume VI (1991), p. 17-25 / Imaging With QuickDraw
        // (1994), p. 3-120: CopyDeepMask takes three BitMap pointers, three
        // Rect pointers, one INTEGER mode, and one RgnHandle.
        let (mut d, mut cpu, mut bus) = setup();

        let src_bits = bus.alloc(14);
        let mask_bits = bus.alloc(14);
        let dst_bits = bus.alloc(14);
        let src_base = bus.alloc(1);
        let mask_base = bus.alloc(1);
        let dst_base = bus.alloc(2);
        write_bitmap_1bpp(&mut bus, src_bits, src_base, 1, (0, 0, 1, 8));
        write_bitmap_1bpp(&mut bus, mask_bits, mask_base, 1, (0, 0, 1, 8));
        write_bitmap_1bpp(&mut bus, dst_bits, dst_base, 2, (0, 0, 1, 16));

        let src_rect = bus.alloc(8);
        let mask_rect = bus.alloc(8);
        let dst_rect = bus.alloc(8);
        write_rect(&mut bus, src_rect, 0, 0, 1, 8);
        write_rect(&mut bus, mask_rect, 0, 0, 1, 8);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 16);

        let mask_rgn = bus.alloc(4);
        assert!(TrapDispatcher::write_region(
            &mut bus,
            mask_rgn,
            Some((0, 4, 1, 12)),
            &[],
        ));

        bus.write_long(TEST_SP, mask_rgn);
        bus.write_word(TEST_SP + 4, 0u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, mask_rect);
        bus.write_long(TEST_SP + 14, src_rect);
        bus.write_long(TEST_SP + 18, dst_bits);
        bus.write_long(TEST_SP + 22, mask_bits);
        bus.write_long(TEST_SP + 26, src_bits);

        let result = d.dispatch_quickdraw(true, 0x251, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 30);
    }

    #[test]
    fn copydeepmask_scales_source_and_clips_to_mask_region() {
        // Inside Macintosh Volume VI (1991), p. 17-25 / Imaging With QuickDraw
        // (1994), pp. 3-120..3-121: CopyDeepMask scales srcRect into dstRect,
        // but clips the result to maskRgn in destination coordinates.
        let (mut d, mut cpu, mut bus) = setup();

        let src_bits = bus.alloc(14);
        let mask_bits = bus.alloc(14);
        let dst_bits = bus.alloc(14);
        let src_base = bus.alloc(1);
        let mask_base = bus.alloc(1);
        let dst_base = bus.alloc(2);
        write_bitmap_1bpp(&mut bus, src_bits, src_base, 1, (0, 0, 1, 8));
        write_bitmap_1bpp(&mut bus, mask_bits, mask_base, 1, (0, 0, 1, 8));
        write_bitmap_1bpp(&mut bus, dst_bits, dst_base, 2, (0, 0, 1, 16));

        // Source: left half black, right half white. Scaling 8px → 16px
        // should produce 8 black destination pixels followed by 8 white ones.
        bus.write_byte(src_base, 0b1111_0000);
        bus.write_byte(mask_base, 0xFF);
        bus.write_byte(dst_base, 0x00);
        bus.write_byte(dst_base + 1, 0x00);

        let src_rect = bus.alloc(8);
        let mask_rect = bus.alloc(8);
        let dst_rect = bus.alloc(8);
        write_rect(&mut bus, src_rect, 0, 0, 1, 8);
        write_rect(&mut bus, mask_rect, 0, 0, 1, 8);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 16);

        let mask_rgn = bus.alloc(4);
        assert!(TrapDispatcher::write_region(
            &mut bus,
            mask_rgn,
            Some((0, 4, 1, 12)),
            &[],
        ));

        bus.write_long(TEST_SP, mask_rgn);
        bus.write_word(TEST_SP + 4, 0u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, mask_rect);
        bus.write_long(TEST_SP + 14, src_rect);
        bus.write_long(TEST_SP + 18, dst_bits);
        bus.write_long(TEST_SP + 22, mask_bits);
        bus.write_long(TEST_SP + 26, src_bits);

        let result = d.dispatch_quickdraw(true, 0x251, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 30);
        assert_eq!(
            bus.read_byte(dst_base),
            0b0000_1111,
            "maskRgn should clip the scaled copy to destination columns 4..7"
        );
        assert_eq!(
            bus.read_byte(dst_base + 1),
            0x00,
            "scaled white source half plus maskRgn clipping should leave the second byte white"
        );
    }

    // ==================== CopyBits ====================

    #[test]
    fn test_copy_bits() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        // Use port bitmap as both src and dst
        let src_rect = 0x300000u32;
        let dst_rect = 0x300010u32;
        write_rect(&mut bus, src_rect, 0, 0, 10, 10);
        write_rect(&mut bus, dst_rect, 0, 0, 10, 10);
        let mask_rgn = 0u32; // NULL
        bus.write_long(TEST_SP, mask_rgn); // mask_rgn
        bus.write_word(TEST_SP + 4, 0u16); // mode (srcCopy)
        bus.write_long(TEST_SP + 6, dst_rect); // dst_rect
        bus.write_long(TEST_SP + 10, src_rect); // src_rect
        bus.write_long(TEST_SP + 14, port_ptr); // dst_bits (port itself)
        bus.write_long(TEST_SP + 18, port_ptr); // src_bits (port itself)
        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 22);
    }

    #[test]
    fn copy_bits_identity_screen_blit_refreshes_visible_dialog_snapshot() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let dialog_ptr = 0x181000u32;
        let screen_base = bus.alloc(16 * 16);
        let src_base = bus.alloc(16 * 16);
        let dst_pixmap = bus.alloc(50);
        let dst_pixmap_handle = bus.alloc(4);
        let src_pixmap = bus.alloc(50);
        let src_rect = bus.alloc(8);
        let dst_rect = bus.alloc(8);

        d.screen_mode = (screen_base, 16, 16, 16, 8);
        bus.write_long(0x0824, screen_base);
        write_pixmap_8(&mut bus, dst_pixmap, screen_base, 16, 16, 0);
        bus.write_long(dst_pixmap_handle, dst_pixmap);
        write_pixmap_8(&mut bus, src_pixmap, src_base, 16, 16, 0);
        bus.write_long(dialog_ptr + 2, dst_pixmap_handle);
        bus.write_word(dialog_ptr + 6, 0xC000);
        write_rect(&mut bus, dialog_ptr + 16, 0, 0, 16, 16);
        bus.write_byte(dialog_ptr + 110, 0xFF);

        let vis_rgn = bus.alloc(10);
        let vis_rgn_handle = bus.alloc(4);
        make_rgn(&mut bus, vis_rgn, vis_rgn_handle, 0, 0, 16, 16);
        bus.write_long(dialog_ptr + 24, vis_rgn_handle);
        let clip_rgn = bus.alloc(10);
        let clip_rgn_handle = bus.alloc(4);
        make_rgn(&mut bus, clip_rgn, clip_rgn_handle, 0, 0, 16, 16);
        bus.write_long(dialog_ptr + 28, clip_rgn_handle);

        let global_ptr = bus.read_long(cpu.read_reg(Register::A5));
        bus.write_long(global_ptr, dialog_ptr);
        d.current_port = dialog_ptr;
        d.front_window = dialog_ptr;
        d.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 0x80,
                rect: (2, 2, 6, 6),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        for offset in 0..16u32 * 16 {
            bus.write_byte(screen_base + offset, 0);
            bus.write_byte(src_base + offset, 0x7A);
        }
        write_rect(&mut bus, src_rect, 0, 0, 4, 4);
        write_rect(&mut bus, dst_rect, 2, 2, 6, 6);
        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 0);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(screen_base + 2 * 16 + 2), 0x7A);

        bus.write_byte(screen_base + 2 * 16 + 2, 0);
        d.restore_visible_dialog_snapshots(&mut bus);

        assert_eq!(
            bus.read_byte(screen_base + 2 * 16 + 2),
            0x7A,
            "identity CopyBits screen writes into a visible dialog must update the retained snapshot"
        );
    }

    #[test]
    fn copy_bits_empty_mask_region_blocks_identity_blit() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let src_pixmap = 0x300100u32;
        let dst_pixmap = 0x300200u32;
        let src_base = 0x301000u32;
        let dst_base = 0x302000u32;
        let src_rect = 0x303000u32;
        let dst_rect = 0x303010u32;

        write_pixmap_8(&mut bus, src_pixmap, src_base, 4, 1, 0);
        write_pixmap_8(&mut bus, dst_pixmap, dst_base, 4, 1, 0);
        bus.write_bytes(src_base, &[1, 2, 3, 4]);
        bus.write_bytes(dst_base, &[9, 9, 9, 9]);
        write_rect(&mut bus, src_rect, 0, 0, 1, 4);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 4);

        let mask_rgn = bus.alloc(4);
        assert!(TrapDispatcher::write_region(&mut bus, mask_rgn, None, &[]));

        bus.write_long(TEST_SP, mask_rgn);
        bus.write_word(TEST_SP + 4, 0u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_bytes(dst_base, 4), vec![9, 9, 9, 9]);
    }

    #[test]
    fn copy_bits_nonzero_mask_without_region_record_blocks_identity_blit() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let src_pixmap = 0x300100u32;
        let dst_pixmap = 0x300200u32;
        let src_base = 0x301000u32;
        let dst_base = 0x302000u32;
        let src_rect = 0x303000u32;
        let dst_rect = 0x303010u32;

        write_pixmap_8(&mut bus, src_pixmap, src_base, 4, 1, 0);
        write_pixmap_8(&mut bus, dst_pixmap, dst_base, 4, 1, 0);
        bus.write_bytes(src_base, &[1, 2, 3, 4]);
        bus.write_bytes(dst_base, &[9, 9, 9, 9]);
        write_rect(&mut bus, src_rect, 0, 0, 1, 4);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 4);

        let mask_rgn = 0x0038_0000u32;
        assert_eq!(bus.read_long(mask_rgn), 0);

        bus.write_long(TEST_SP, mask_rgn);
        bus.write_word(TEST_SP + 4, 0u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_bytes(dst_base, 4), vec![9, 9, 9, 9]);
    }

    #[test]
    fn test_copy_bits_skips_sentinel_ffffffff_baseaddr() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let src_bits = 0x300100u32;
        let dst_bits = 0x300200u32;
        let src_base = 0x301000u32;
        let src_rect = 0x302000u32;
        let dst_rect = 0x302010u32;

        // 1bpp source bitmap: 16 white pixels in a single row.
        bus.write_long(src_bits, src_base);
        bus.write_word(src_bits + 4, 2u16); // rowBytes
        bus.write_word(src_bits + 6, 0); // top
        bus.write_word(src_bits + 8, 0); // left
        bus.write_word(src_bits + 10, 1); // bottom
        bus.write_word(src_bits + 12, 16); // right
        bus.write_byte(src_base, 0xFF);
        bus.write_byte(src_base + 1, 0xFF);

        // Destination bitmap uses baseAddr=$FFFFFFFF sentinel.
        // Without the guard this can wrap and write low RAM.
        bus.write_long(dst_bits, u32::MAX);
        bus.write_word(dst_bits + 4, 2u16); // rowBytes
        bus.write_word(dst_bits + 6, 0); // top
        bus.write_word(dst_bits + 8, 0); // left
        bus.write_word(dst_bits + 10, 1); // bottom
        bus.write_word(dst_bits + 12, 16); // right

        write_rect(&mut bus, src_rect, 0, 0, 1, 16);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 16);

        bus.write_byte(0, 0xAA);
        bus.write_byte(1, 0x55);

        bus.write_long(TEST_SP, 0); // maskRgn
        bus.write_word(TEST_SP + 4, 0u16); // mode=srcCopy
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_bits);
        bus.write_long(TEST_SP + 18, src_bits);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 22);
        assert_eq!(
            bus.read_byte(0),
            0xAA,
            "CopyBits must no-op when dst baseAddr is $FFFFFFFF"
        );
        assert_eq!(
            bus.read_byte(1),
            0x55,
            "sentinel-baseAddr CopyBits must not wrap writes into low RAM"
        );
    }

    #[test]
    fn test_copy_bits_uses_low24_baseaddr_when_tagged_high_byte_points_unmapped() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let src_bits = 0x300100u32;
        let dst_pixmap = 0x300200u32;
        let src_base = 0x0030_1000u32;
        let tagged_src_base = 0x3300_0000u32 | src_base;
        let dst_base = 0x302000u32;
        let src_rect = 0x303000u32;
        let dst_rect = 0x303010u32;

        // 1bpp source bitmap with all bits set (black pixels).
        write_bitmap_record(&mut bus, src_bits, tagged_src_base, 2, 0, 0, 1, 16);
        bus.write_byte(src_base, 0xFF);
        bus.write_byte(src_base + 1, 0xFF);

        // 8bpp destination pixmap.
        write_pixmap_8(&mut bus, dst_pixmap, dst_base, 16, 1, 0);
        bus.fill_zeros(dst_base, 16);

        write_rect(&mut bus, src_rect, 0, 0, 1, 16);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 16);

        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 0u16); // srcCopy
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_bits);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            bus.read_byte(dst_base),
            0xFF,
            "CopyBits should sample from low-24-bit source baseAddr when tagged high byte points unmapped"
        );
        assert_eq!(bus.read_byte(dst_base + 15), 0xFF);
    }

    #[test]
    fn copy_bits_8bpp_srcor_colorizes_black_source_and_preserves_white_source() {
        // Imaging With QuickDraw 1994, p. 4-33: for colored pixels, srcOr
        // applies the foreground color where the source is black and leaves
        // destination pixels alone where the source is white.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        d.fg_color = (0xFFFF, 0xFFFF, 0xFFFF);
        d.bg_color = (0, 0, 0);

        let src_pixmap = 0x300100u32;
        let dst_pixmap = 0x300200u32;
        let src_base = 0x301000u32;
        let dst_base = 0x302000u32;
        write_pixmap_8(&mut bus, src_pixmap, src_base, 4, 4, 0);
        write_pixmap_8(&mut bus, dst_pixmap, dst_base, 4, 4, 0);

        // Black source pixels form a top-left bracket on a white source
        // background. srcOr should draw only those bracket strokes.
        bus.write_bytes(
            src_base,
            &[
                255, 255, 255, 255, //
                255, 0, 0, 0, //
                255, 0, 0, 0, //
                255, 0, 0, 0,
            ],
        );
        bus.write_bytes(dst_base, &[255; 16]);

        let src_rect = bus.alloc(8);
        let dst_rect = bus.alloc(8);
        write_rect(&mut bus, src_rect, 0, 0, 4, 4);
        write_rect(&mut bus, dst_rect, 0, 0, 4, 4);

        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 1u16); // srcOr
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_eq!(
            bus.read_bytes(dst_base, 16),
            vec![
                0, 0, 0, 0, //
                0, 255, 255, 255, //
                0, 255, 255, 255, //
                0, 255, 255, 255,
            ],
            "8bpp srcOr should render bracket strokes through the foreground color without copying the source rectangle's white background"
        );
    }

    fn write_pixmap_8(
        bus: &mut crate::memory::MacMemoryBus,
        pixmap_ptr: u32,
        base_addr: u32,
        width: u16,
        height: u16,
        ctab_handle: u32,
    ) {
        bus.write_long(pixmap_ptr, base_addr);
        bus.write_word(pixmap_ptr + 4, 0x8000 | width);
        bus.write_word(pixmap_ptr + 6, 0);
        bus.write_word(pixmap_ptr + 8, 0);
        bus.write_word(pixmap_ptr + 10, height);
        bus.write_word(pixmap_ptr + 12, width);
        bus.write_word(pixmap_ptr + 30, 0);
        bus.write_word(pixmap_ptr + 32, 8);
        bus.write_word(pixmap_ptr + 34, 1);
        bus.write_word(pixmap_ptr + 36, 8);
        bus.write_long(pixmap_ptr + 42, ctab_handle);
    }

    fn write_color_table(
        bus: &mut crate::memory::MacMemoryBus,
        handle: u32,
        seed: u32,
        entries: &[(u16, u16, u16, u16)],
    ) {
        let ctab_ptr = handle + 0x100;
        bus.write_long(handle, ctab_ptr);
        bus.write_long(ctab_ptr, seed);
        bus.write_word(ctab_ptr + 4, 0);
        bus.write_word(ctab_ptr + 6, entries.len().saturating_sub(1) as u16);
        for (index, (value, red, green, blue)) in entries.iter().enumerate() {
            let entry_ptr = ctab_ptr + 8 + (index as u32) * 8;
            bus.write_word(entry_ptr, *value);
            bus.write_word(entry_ptr + 2, *red);
            bus.write_word(entry_ptr + 4, *green);
            bus.write_word(entry_ptr + 6, *blue);
        }
    }

    fn compiled_crsr_resource_2bpp() -> Vec<u8> {
        let mut data = vec![0u8; 0xD2 + 8 + 4 * 8];

        data[0..2].copy_from_slice(&0x8001u16.to_be_bytes());
        data[2..6].copy_from_slice(&0x60u32.to_be_bytes());
        data[6..10].copy_from_slice(&0x92u32.to_be_bytes());
        data[84..86].copy_from_slice(&15i16.to_be_bytes());
        data[86..88].copy_from_slice(&7i16.to_be_bytes());
        for row in 0..16usize {
            let mask = if row < 8 { 0xFFFFu16 } else { 0x0000 };
            data[52 + row * 2..54 + row * 2].copy_from_slice(&mask.to_be_bytes());
        }

        let pixmap = 0x60usize;
        data[pixmap..pixmap + 2].copy_from_slice(&0x8004u16.to_be_bytes());
        data[pixmap + 2..pixmap + 4].copy_from_slice(&0i16.to_be_bytes());
        data[pixmap + 4..pixmap + 6].copy_from_slice(&0i16.to_be_bytes());
        data[pixmap + 6..pixmap + 8].copy_from_slice(&16i16.to_be_bytes());
        data[pixmap + 8..pixmap + 10].copy_from_slice(&16i16.to_be_bytes());
        data[pixmap + 28..pixmap + 30].copy_from_slice(&2u16.to_be_bytes());
        data[pixmap + 30..pixmap + 32].copy_from_slice(&1u16.to_be_bytes());
        data[pixmap + 32..pixmap + 34].copy_from_slice(&2u16.to_be_bytes());
        data[pixmap + 38..pixmap + 42].copy_from_slice(&0xD2u32.to_be_bytes());

        let pixels = 0x92usize;
        for row in 0..16usize {
            for col_byte in 0..4usize {
                data[pixels + row * 4 + col_byte] = match col_byte {
                    0 => 0b00_01_10_11,
                    1 => 0b11_10_01_00,
                    2 => 0b00_00_01_01,
                    _ => 0b10_10_11_11,
                };
            }
        }

        let ctab = 0xD2usize;
        data[ctab..ctab + 4].copy_from_slice(&0u32.to_be_bytes());
        data[ctab + 4..ctab + 6].copy_from_slice(&0u16.to_be_bytes());
        data[ctab + 6..ctab + 8].copy_from_slice(&3u16.to_be_bytes());
        let entries = [
            (0u16, 0xFFFFu16, 0xFFFFu16, 0xFFFFu16),
            (1, 0xFFFF, 0x0000, 0x0000),
            (2, 0x0000, 0xFFFF, 0x0000),
            (3, 0x0000, 0x0000, 0x0000),
        ];
        for (index, (value, red, green, blue)) in entries.iter().copied().enumerate() {
            let entry = ctab + 8 + index * 8;
            data[entry..entry + 2].copy_from_slice(&value.to_be_bytes());
            data[entry + 2..entry + 4].copy_from_slice(&red.to_be_bytes());
            data[entry + 4..entry + 6].copy_from_slice(&green.to_be_bytes());
            data[entry + 6..entry + 8].copy_from_slice(&blue.to_be_bytes());
        }

        data
    }

    #[test]
    fn test_copy_bits_transparent_translates_palette() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let src_pixmap = 0x300100u32;
        let dst_pixmap = 0x300200u32;
        let src_base = 0x301000u32;
        let dst_base = 0x302000u32;
        let src_ctab_handle = 0x303000u32;
        let dst_ctab_handle = 0x304000u32;
        let src_rect = 0x305000u32;
        let dst_rect = 0x305010u32;

        write_color_table(
            &mut bus,
            src_ctab_handle,
            0x1111_1111,
            &[(0, 0xFFFF, 0xFFFF, 0xFFFF), (1, 0xFFFF, 0x0000, 0x0000)],
        );
        write_color_table(
            &mut bus,
            dst_ctab_handle,
            0x2222_2222,
            &[
                (7, 0x0000, 0x0000, 0x0000),
                (42, 0xFFFF, 0x0000, 0x0000),
                (99, 0xFFFF, 0xFFFF, 0xFFFF),
            ],
        );
        write_pixmap_8(&mut bus, src_pixmap, src_base, 2, 1, src_ctab_handle);
        write_pixmap_8(&mut bus, dst_pixmap, dst_base, 2, 1, dst_ctab_handle);
        bus.write_byte(src_base, 0);
        bus.write_byte(src_base + 1, 1);
        bus.write_byte(dst_base, 7);
        bus.write_byte(dst_base + 1, 7);
        write_rect(&mut bus, src_rect, 0, 0, 1, 2);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 2);
        d.bg_color = (0xFFFF, 0xFFFF, 0xFFFF);

        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 36u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(dst_base), 7);
        assert_eq!(bus.read_byte(dst_base + 1), 42);
    }

    #[test]
    fn test_copy_bits_transparent_uses_cgrafport_background_color() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let src_pixmap = 0x300100u32;
        let dst_pixmap = 0x300200u32;
        let dst_pixmap_handle = 0x300280u32;
        let src_base = 0x301000u32;
        let dst_base = 0x302000u32;
        let src_ctab_handle = 0x303000u32;
        let dst_ctab_handle = 0x304000u32;
        let src_rect = 0x305000u32;
        let dst_rect = 0x305010u32;
        let matte_src_rgb = (0x7FFF, 0x0000, 0xB332);
        let matte_bg_rgb = (0x7F7F, 0x0000, 0xB3B3);
        let opaque_rgb = (0x1111, 0xCCCC, 0x3333);

        write_color_table(
            &mut bus,
            src_ctab_handle,
            0x1111_1111,
            &[
                (5, matte_src_rgb.0, matte_src_rgb.1, matte_src_rgb.2),
                (9, opaque_rgb.0, opaque_rgb.1, opaque_rgb.2),
            ],
        );
        write_color_table(
            &mut bus,
            dst_ctab_handle,
            0x2222_2222,
            &[
                (7, 0x0000, 0x0000, 0x0000),
                (9, opaque_rgb.0, opaque_rgb.1, opaque_rgb.2),
                (55, matte_src_rgb.0, matte_src_rgb.1, matte_src_rgb.2),
            ],
        );
        write_pixmap_8(&mut bus, src_pixmap, src_base, 2, 1, src_ctab_handle);
        write_pixmap_8(&mut bus, dst_pixmap, dst_base, 2, 1, dst_ctab_handle);
        bus.write_byte(src_base, 5);
        bus.write_byte(src_base + 1, 9);
        bus.write_byte(dst_base, 7);
        bus.write_byte(dst_base + 1, 7);
        write_rect(&mut bus, src_rect, 0, 0, 1, 2);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 2);

        let globals_ptr = bus.read_long(cpu.read_reg(Register::A5));
        let port_ptr = bus.read_long(globals_ptr);
        bus.write_long(dst_pixmap_handle, dst_pixmap);
        bus.write_long(port_ptr + 2, dst_pixmap_handle);
        bus.write_word(port_ptr + 6, 0xC000);
        bus.write_word(port_ptr + 36, 0);
        bus.write_word(port_ptr + 38, 0);
        bus.write_word(port_ptr + 40, 0);
        bus.write_word(port_ptr + 42, matte_bg_rgb.0);
        bus.write_word(port_ptr + 44, matte_bg_rgb.1);
        bus.write_word(port_ptr + 46, matte_bg_rgb.2);
        d.bg_color = (0xFFFF, 0xFFFF, 0xFFFF);

        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 36u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            bus.read_byte(dst_base),
            7,
            "transparent source pixel should preserve the destination"
        );
        assert_eq!(bus.read_byte(dst_base + 1), 9);
    }

    #[test]
    fn test_copy_bits_transparent_preserves_black_index_zero_matte() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let src_pixmap = 0x300100u32;
        let dst_pixmap = 0x300200u32;
        let src_base = 0x301000u32;
        let dst_base = 0x302000u32;
        let src_ctab_handle = 0x303000u32;
        let dst_ctab_handle = 0x304000u32;
        let src_rect = 0x305000u32;
        let dst_rect = 0x305010u32;

        write_color_table(
            &mut bus,
            src_ctab_handle,
            0x1111_1111,
            &[(0, 0x0000, 0x0000, 0x0000), (9, 0xFFFF, 0x0000, 0x0000)],
        );
        write_color_table(
            &mut bus,
            dst_ctab_handle,
            0x2222_2222,
            &[
                (7, 0x2222, 0x3333, 0x4444),
                (42, 0xFFFF, 0x0000, 0x0000),
                (255, 0x0000, 0x0000, 0x0000),
            ],
        );
        write_pixmap_8(&mut bus, src_pixmap, src_base, 2, 1, src_ctab_handle);
        write_pixmap_8(&mut bus, dst_pixmap, dst_base, 2, 1, dst_ctab_handle);
        bus.write_byte(src_base, 0);
        bus.write_byte(src_base + 1, 9);
        bus.write_byte(dst_base, 7);
        bus.write_byte(dst_base + 1, 7);
        write_rect(&mut bus, src_rect, 0, 0, 1, 2);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 2);
        d.bg_color = (0xFFFF, 0xFFFF, 0xFFFF);

        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 36u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            bus.read_byte(dst_base),
            7,
            "black source index 0 should act as the transparent sprite matte"
        );
        assert_eq!(bus.read_byte(dst_base + 1), 42);
    }

    #[test]
    fn test_copy_bits_8bpp_overlap_uses_source_snapshot() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let pixmap = 0x300100u32;
        let base = 0x301000u32;
        let src_rect = 0x302000u32;
        let dst_rect = 0x302010u32;

        write_pixmap_8(&mut bus, pixmap, base, 4, 1, 0);
        bus.write_byte(base, 1);
        bus.write_byte(base + 1, 2);
        bus.write_byte(base + 2, 3);
        bus.write_byte(base + 3, 4);
        write_rect(&mut bus, src_rect, 0, 0, 1, 3);
        write_rect(&mut bus, dst_rect, 0, 1, 1, 4);

        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 0u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, pixmap);
        bus.write_long(TEST_SP + 18, pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(base), 1);
        assert_eq!(bus.read_byte(base + 1), 1);
        assert_eq!(bus.read_byte(base + 2), 2);
        assert_eq!(bus.read_byte(base + 3), 3);
    }

    #[test]
    fn test_copy_bits_src_copy_colorizes_8bpp() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let src_pixmap = 0x300300u32;
        let dst_pixmap = 0x300400u32;
        let src_base = 0x303000u32;
        let dst_base = 0x304000u32;
        let src_ctab_handle = 0x305000u32;
        let dst_ctab_handle = 0x306000u32;
        let src_rect = 0x307000u32;
        let dst_rect = 0x307010u32;
        let custom_rgb = (0xF123, 0x4567, 0x89AB);

        write_color_table(
            &mut bus,
            src_ctab_handle,
            0x1111_1111,
            &[(1, custom_rgb.0, custom_rgb.1, custom_rgb.2)],
        );
        write_color_table(
            &mut bus,
            dst_ctab_handle,
            0x2222_2222,
            &[
                (7, 0x0000, 0x0000, 0x0000),
                (42, custom_rgb.0, custom_rgb.1, custom_rgb.2),
                (99, 0xFFFF, 0xFFFF, 0xFFFF),
            ],
        );
        write_pixmap_8(&mut bus, src_pixmap, src_base, 1, 1, src_ctab_handle);
        write_pixmap_8(&mut bus, dst_pixmap, dst_base, 1, 1, dst_ctab_handle);
        bus.write_byte(src_base, 1);
        bus.write_byte(dst_base, 7);
        write_rect(&mut bus, src_rect, 0, 0, 1, 1);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 1);
        d.fg_color = (0, 0, 0);
        d.bg_color = (0xFFFF, 0xFFFF, 0xFFFF);

        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 0u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(dst_base), 42);
    }

    #[test]
    fn test_copy_bits_src_copy_8bpp_ignores_fg_bg_for_palette_translation() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let src_pixmap = 0x314000u32;
        let dst_pixmap = 0x314100u32;
        let src_base = 0x315000u32;
        let dst_base = 0x316000u32;
        let src_ctab_handle = 0x317000u32;
        let dst_ctab_handle = 0x318000u32;
        let src_rect = 0x319000u32;
        let dst_rect = 0x319010u32;

        write_color_table(
            &mut bus,
            src_ctab_handle,
            0x3333_3333,
            &[(5, 0x6666, 0x6666, 0x6666)],
        );
        write_color_table(
            &mut bus,
            dst_ctab_handle,
            0x4444_4444,
            &[
                (2, 0x0000, 0x0000, 0x0000),
                (42, 0x6666, 0x6666, 0x6666),
                (77, 0x9999, 0x9999, 0x9999),
                (99, 0xFFFF, 0xFFFF, 0xFFFF),
            ],
        );
        write_pixmap_8(&mut bus, src_pixmap, src_base, 1, 1, src_ctab_handle);
        write_pixmap_8(&mut bus, dst_pixmap, dst_base, 1, 1, dst_ctab_handle);
        bus.write_byte(src_base, 5);
        bus.write_byte(dst_base, 2);
        write_rect(&mut bus, src_rect, 0, 0, 1, 1);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 1);
        d.fg_color = (0xFFFF, 0xFFFF, 0xFFFF);
        d.bg_color = (0, 0, 0);

        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 0u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(dst_base), 42);
    }

    #[test]
    fn test_copy_bits_palette_translation_preserves_same_index_duplicate_match() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let src_pixmap = 0x314200u32;
        let dst_pixmap = 0x314300u32;
        let src_base = 0x315200u32;
        let dst_base = 0x316200u32;
        let src_ctab_handle = 0x317200u32;
        let dst_ctab_handle = 0x318200u32;
        let src_rect = 0x319200u32;
        let dst_rect = 0x319210u32;
        let rgb = (0x2E2E, 0x0000, 0x3333);

        write_color_table(
            &mut bus,
            src_ctab_handle,
            0x1111_1111,
            &[(71, rgb.0, rgb.1, rgb.2)],
        );
        write_color_table(
            &mut bus,
            dst_ctab_handle,
            0x2222_2222,
            &[(3, rgb.0, rgb.1, rgb.2), (71, rgb.0, rgb.1, rgb.2)],
        );
        write_pixmap_8(&mut bus, src_pixmap, src_base, 1, 1, src_ctab_handle);
        write_pixmap_8(&mut bus, dst_pixmap, dst_base, 1, 1, dst_ctab_handle);
        bus.write_byte(src_base, 71);
        bus.write_byte(dst_base, 0);
        write_rect(&mut bus, src_rect, 0, 0, 1, 1);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 1);

        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 0u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(dst_base), 71);
    }

    #[test]
    fn test_copy_bits_screen_blit_preserves_indices_when_hardware_palette_active() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd = bus.read_long(gdh);
        let screen_pm_handle = bus.read_long(gd + 22);
        let screen_pm = bus.read_long(screen_pm_handle);
        let screen_base = bus.read_long(screen_pm);
        let screen_row_bytes = (bus.read_word(screen_pm + 4) & 0x3FFF) as u32;
        d.screen_mode = (screen_base, screen_row_bytes, 800, 600, 8);

        let src_pixmap = 0x31A000u32;
        let src_base = 0x31B000u32;
        let src_ctab_handle = 0x31C000u32;
        let src_rect = 0x31D000u32;
        let dst_rect = 0x31D010u32;
        let cm = d.color_manager_clut;

        write_color_table(
            &mut bus,
            src_ctab_handle,
            0x1111_1111,
            &[(42, cm[7][0], cm[7][1], cm[7][2])],
        );
        write_pixmap_8(&mut bus, src_pixmap, src_base, 1, 1, src_ctab_handle);
        bus.write_byte(src_base, 42);
        bus.write_byte(screen_base, 0);
        write_rect(&mut bus, src_rect, 0, 0, 1, 1);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 1);

        d.device_clut[1] = [0x0100, 0x0200, 0x0300];

        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 0u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, screen_pm);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(screen_base), 42);
    }

    #[test]
    fn test_copy_bits_src_copy_with_null_source_ctab_preserves_indices() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let src_pixmap = 0x31A600u32;
        let dst_pixmap = 0x31A700u32;
        let src_base = 0x31A800u32;
        let dst_base = 0x31A900u32;
        let dst_ctab_handle = 0x31AA00u32;
        let src_rect = 0x31AB00u32;
        let dst_rect = 0x31AB10u32;
        let color = d.device_clut[3];

        write_color_table(
            &mut bus,
            dst_ctab_handle,
            0x5555_5555,
            &[(42, color[0], color[1], color[2]), (7, 0, 0, 0)],
        );
        write_pixmap_8(&mut bus, src_pixmap, src_base, 1, 1, 0);
        write_pixmap_8(&mut bus, dst_pixmap, dst_base, 1, 1, dst_ctab_handle);
        bus.write_byte(src_base, 3);
        bus.write_byte(dst_base, 7);
        write_rect(&mut bus, src_rect, 0, 0, 1, 1);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 1);

        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 0u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(dst_base), 3);
    }

    #[test]
    fn test_copy_bits_uses_destination_ctab_for_non_current_bitmap() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let src_pixmap = 0x31A000u32;
        let dst_pixmap = 0x31A100u32;
        let current_pixmap = 0x31A200u32;
        let src_base = 0x31B000u32;
        let dst_base = 0x31C000u32;
        let current_base = 0x31D000u32;
        let src_ctab_handle = 0x31E000u32;
        let dst_ctab_handle = 0x31E100u32;
        let current_ctab_handle = 0x31E200u32;
        let current_pm_handle = 0x31E300u32;
        let current_gd = 0x31E400u32;
        let current_gdh = 0x31E500u32;
        let src_rect = 0x31F000u32;
        let dst_rect = 0x31F010u32;
        let color = (0x1234, 0x5678, 0x9ABCu16);

        write_color_table(
            &mut bus,
            src_ctab_handle,
            0,
            &[(1, color.0, color.1, color.2)],
        );
        write_color_table(
            &mut bus,
            dst_ctab_handle,
            0,
            &[(7, color.0, color.1, color.2), (42, 0xFFFF, 0xFFFF, 0xFFFF)],
        );
        write_color_table(
            &mut bus,
            current_ctab_handle,
            0,
            &[(42, color.0, color.1, color.2), (7, 0xFFFF, 0xFFFF, 0xFFFF)],
        );
        write_pixmap_8(&mut bus, src_pixmap, src_base, 1, 1, src_ctab_handle);
        write_pixmap_8(&mut bus, dst_pixmap, dst_base, 1, 1, dst_ctab_handle);
        write_pixmap_8(
            &mut bus,
            current_pixmap,
            current_base,
            1,
            1,
            current_ctab_handle,
        );
        bus.write_long(current_pm_handle, current_pixmap);
        bus.write_long(current_gd + 22, current_pm_handle);
        bus.write_long(current_gdh, current_gd);
        d.current_gdevice = current_gdh;

        bus.write_byte(src_base, 1);
        bus.write_byte(dst_base, 0);
        write_rect(&mut bus, src_rect, 0, 0, 1, 1);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 1);

        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 0u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(dst_base), 7);
    }

    #[test]
    fn test_copy_bits_same_seed_preserves_indices() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd = bus.read_long(gdh);
        let screen_pm_handle = bus.read_long(gd + 22);
        let screen_pm = bus.read_long(screen_pm_handle);
        let screen_ctab_handle = bus.read_long(screen_pm + 42);
        let screen_ctab = bus.read_long(screen_ctab_handle);
        let shared_seed = 0x3333_3333;
        let color_7 = d.device_clut[7];
        let color_9 = d.device_clut[9];

        bus.write_long(screen_ctab, shared_seed);

        let src_pixmap = 0x30A000u32;
        let dst_pixmap = 0x30A100u32;
        let src_base = 0x30B000u32;
        let dst_base = 0x30C000u32;
        let src_ctab_handle = 0x30D000u32;
        let src_rect = 0x30E000u32;
        let dst_rect = 0x30E010u32;

        write_color_table(
            &mut bus,
            src_ctab_handle,
            shared_seed,
            &[
                (7, color_7[0], color_7[1], color_7[2]),
                (9, color_9[0], color_9[1], color_9[2]),
            ],
        );
        write_pixmap_8(&mut bus, src_pixmap, src_base, 2, 1, src_ctab_handle);
        write_pixmap_8(&mut bus, dst_pixmap, dst_base, 2, 1, 0);
        bus.write_byte(src_base, 7);
        bus.write_byte(src_base + 1, 9);
        write_rect(&mut bus, src_rect, 0, 0, 1, 2);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 2);

        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 0u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(dst_base), 7);
        assert_eq!(bus.read_byte(dst_base + 1), 9);
    }

    #[test]
    fn test_copy_bits_zero_seed_preserves_indices() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd = bus.read_long(gdh);
        let screen_pm_handle = bus.read_long(gd + 22);
        let screen_pm = bus.read_long(screen_pm_handle);
        let screen_ctab_handle = bus.read_long(screen_pm + 42);
        let screen_ctab = bus.read_long(screen_ctab_handle);
        let color_7 = d.device_clut[7];
        let color_9 = d.device_clut[9];

        bus.write_long(screen_ctab, 0x4444_4444);

        let src_pixmap = 0x30F000u32;
        let dst_pixmap = 0x30F100u32;
        let src_base = 0x310000u32;
        let dst_base = 0x311000u32;
        let src_ctab_handle = 0x312000u32;
        let src_rect = 0x313000u32;
        let dst_rect = 0x313010u32;

        write_color_table(
            &mut bus,
            src_ctab_handle,
            0,
            &[
                (7, color_7[0], color_7[1], color_7[2]),
                (9, color_9[0], color_9[1], color_9[2]),
            ],
        );
        write_pixmap_8(&mut bus, src_pixmap, src_base, 2, 1, src_ctab_handle);
        write_pixmap_8(&mut bus, dst_pixmap, dst_base, 2, 1, 0);
        bus.write_byte(src_base, 7);
        bus.write_byte(src_base + 1, 9);
        write_rect(&mut bus, src_rect, 0, 0, 1, 2);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 2);

        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 0u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(dst_base), 7);
        assert_eq!(bus.read_byte(dst_base + 1), 9);
    }

    #[test]
    fn test_copy_bits_sparse_source_ctab_preserves_device_entries() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let src_pixmap = 0x320000u32;
        let dst_pixmap = 0x320100u32;
        let src_base = 0x321000u32;
        let dst_base = 0x322000u32;
        let src_ctab_handle = 0x323000u32;
        let src_rect = 0x324000u32;
        let dst_rect = 0x324010u32;

        // Only override one unrelated entry; source pixel 3 should still resolve
        // through the device CLUT rather than an implicit black fallback.
        write_color_table(
            &mut bus,
            src_ctab_handle,
            0x5555_5555,
            &[(42, 0x1234, 0x5678, 0x9ABC)],
        );
        write_pixmap_8(&mut bus, src_pixmap, src_base, 1, 1, src_ctab_handle);
        write_pixmap_8(&mut bus, dst_pixmap, dst_base, 1, 1, 0);
        bus.write_byte(src_base, 3);
        bus.write_byte(dst_base, 0);
        write_rect(&mut bus, src_rect, 0, 0, 1, 1);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 1);

        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 0u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(dst_base), 3);
    }

    #[test]
    fn test_copy_bits_scales_8bpp_source() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let src_pixmap = 0x306000u32;
        let dst_pixmap = 0x306100u32;
        let src_base = 0x307000u32;
        let dst_base = 0x308000u32;
        let src_rect = 0x309000u32;
        let dst_rect = 0x309010u32;

        write_pixmap_8(&mut bus, src_pixmap, src_base, 2, 1, 0);
        write_pixmap_8(&mut bus, dst_pixmap, dst_base, 4, 1, 0);
        bus.write_byte(src_base, 7);
        bus.write_byte(src_base + 1, 9);
        write_rect(&mut bus, src_rect, 0, 0, 1, 2);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 4);

        bus.write_long(TEST_SP, 0);
        bus.write_word(TEST_SP + 4, 0u16);
        bus.write_long(TEST_SP + 6, dst_rect);
        bus.write_long(TEST_SP + 10, src_rect);
        bus.write_long(TEST_SP + 14, dst_pixmap);
        bus.write_long(TEST_SP + 18, src_pixmap);

        let result = d.dispatch_quickdraw(true, 0x0EC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(dst_base), 7);
        assert_eq!(bus.read_byte(dst_base + 1), 7);
        assert_eq!(bus.read_byte(dst_base + 2), 9);
        assert_eq!(bus.read_byte(dst_base + 3), 9);
    }

    // ==================== Pixel Operations ====================

    #[test]
    fn test_get_pixel() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        bus.write_word(TEST_SP, 0u16); // v
        bus.write_word(TEST_SP + 2, 0u16); // h
        let result = d.dispatch_quickdraw(true, 0x065, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        // Pixel at (0,0) should be 0 (blank screen)
        let pixel = bus.read_word(TEST_SP + 4);
        assert_eq!(pixel, 0);
    }

    #[test]
    fn getpixel_set_bit_returns_pascal_boolean_true_high_byte_encoding() {
        // Per IM:I I-195 GetPixel returns TRUE when the bit at the
        // requested local-coordinate pixel is set. The Mac Pascal
        // BOOLEAN result encoding is 0x0100 (high byte = 1) for TRUE.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let screen_base = bus.read_long(0x0824);

        // Set the bit corresponding to local-coordinate (0, 0) in the
        // 1bpp screen framebuffer (row 0, byte 0, MSB).
        bus.write_byte(screen_base, 0x80);

        bus.write_word(TEST_SP, 0u16); // v
        bus.write_word(TEST_SP + 2, 0u16); // h
        let result = d.dispatch_quickdraw(true, 0x065, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(TEST_SP + 4), 0x0100);
    }

    #[test]
    fn getpixel_function_protocol_preserves_caller_memory_around_result_slot() {
        // GetPixel must write exactly the 2-byte Pascal Boolean into
        // the result slot at SP+4 (the original SP+0 of the BOOLEAN
        // slot the caller pre-allocated). Stack words outside that
        // slot must be untouched. Pre-poison SP+2 (low arg word) and
        // SP+6 (memory past the result slot) with distinct sentinels
        // and assert they survive the call. Defeats any future "fix"
        // that writes 4 bytes to SP+4 or writes the result at SP+2.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        bus.write_word(TEST_SP, 0u16); // v
        bus.write_word(TEST_SP + 2, 0u16); // h
        bus.write_word(TEST_SP + 6, 0xBEEFu16); // sentinel past result
        bus.write_word(TEST_SP + 8, 0xCAFEu16); // sentinel further past
        let result = d.dispatch_quickdraw(true, 0x065, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(TEST_SP + 6), 0xBEEFu16);
        assert_eq!(bus.read_word(TEST_SP + 8), 0xCAFEu16);
    }

    // ==================== Color Manager ====================

    #[test]
    fn fore_color_sets_standard_red_and_preserves_existing_background() {
        let (mut d, mut cpu, mut bus) = setup();
        // IM:I I-173: ForeColor sets the current grafPort foreground color
        // to the requested standard color constant.
        let bg_ptr = 0x300000u32;
        bus.write_word(bg_ptr, 0x1357);
        bus.write_word(bg_ptr + 2, 0x2468);
        bus.write_word(bg_ptr + 4, 0x369C);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, bg_ptr);
        let set_bg = d.dispatch_quickdraw(true, 0x215, &mut cpu, &mut bus); // RGBBackColor
        assert!(set_bg.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, 205u32); // redColor
        let result = d.dispatch_quickdraw(true, 0x062, &mut cpu, &mut bus); // ForeColor
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        let fg_out = 0x300100u32;
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, fg_out);
        let get_fg = d.dispatch_quickdraw(true, 0x219, &mut cpu, &mut bus); // GetForeColor
        assert!(get_fg.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(fg_out), 0xDD6B);
        assert_eq!(bus.read_word(fg_out + 2), 0x08C2);
        assert_eq!(bus.read_word(fg_out + 4), 0x06A2);

        // ForeColor updates foreground only; existing background is preserved.
        let bg_out = 0x300200u32;
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, bg_out);
        let get_bg = d.dispatch_quickdraw(true, 0x21A, &mut cpu, &mut bus); // GetBackColor
        assert!(get_bg.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(bg_out), 0x1357);
        assert_eq!(bus.read_word(bg_out + 2), 0x2468);
        assert_eq!(bus.read_word(bg_out + 4), 0x369C);
    }

    #[test]
    fn back_color_sets_standard_blue_and_preserves_existing_foreground() {
        let (mut d, mut cpu, mut bus) = setup();
        // IM:I I-174: BackColor sets the current grafPort background color
        // to the requested standard color constant.
        let fg_ptr = 0x300000u32;
        bus.write_word(fg_ptr, 0x7777);
        bus.write_word(fg_ptr + 2, 0x5555);
        bus.write_word(fg_ptr + 4, 0x3333);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, fg_ptr);
        let set_fg = d.dispatch_quickdraw(true, 0x214, &mut cpu, &mut bus); // RGBForeColor
        assert!(set_fg.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, 409u32); // blueColor
        let result = d.dispatch_quickdraw(true, 0x063, &mut cpu, &mut bus); // BackColor
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        let bg_out = 0x300100u32;
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, bg_out);
        let get_bg = d.dispatch_quickdraw(true, 0x21A, &mut cpu, &mut bus); // GetBackColor
        assert!(get_bg.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(bg_out), 0x0000);
        assert_eq!(bus.read_word(bg_out + 2), 0x0000);
        assert_eq!(bus.read_word(bg_out + 4), 0xD400);

        // BackColor updates background only; existing foreground is preserved.
        let fg_out = 0x300200u32;
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, fg_out);
        let get_fg = d.dispatch_quickdraw(true, 0x219, &mut cpu, &mut bus); // GetForeColor
        assert!(get_fg.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(fg_out), 0x7777);
        assert_eq!(bus.read_word(fg_out + 2), 0x5555);
        assert_eq!(bus.read_word(fg_out + 4), 0x3333);
    }

    #[test]
    fn test_rgb_fore_color() {
        let (mut d, mut cpu, mut bus) = setup();
        let color_ptr = 0x300000u32;
        bus.write_word(color_ptr, 0x1111);
        bus.write_word(color_ptr + 2, 0x2222);
        bus.write_word(color_ptr + 4, 0x3333);
        bus.write_long(TEST_SP, color_ptr);
        let result = d.dispatch_quickdraw(true, 0x214, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(d.fg_color, (0x1111, 0x2222, 0x3333));
    }

    #[test]
    fn test_rgb_back_color() {
        let (mut d, mut cpu, mut bus) = setup();
        let color_ptr = 0x300000u32;
        bus.write_word(color_ptr, 0xAAAA);
        bus.write_word(color_ptr + 2, 0xBBBB);
        bus.write_word(color_ptr + 4, 0xCCCC);
        bus.write_long(TEST_SP, color_ptr);
        let result = d.dispatch_quickdraw(true, 0x215, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(d.bg_color, (0xAAAA, 0xBBBB, 0xCCCC));
    }

    #[test]
    fn getpixpat_missing_resource_returns_nil_and_pops_patid() {
        // Inside Macintosh Volume V (1986), p. V-72: GetPixPat returns NIL
        // when the requested 'ppat' resource is not found.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 7);

        let result = d.dispatch_quickdraw(true, 0x20C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(bus.read_long(TEST_SP + 2), 0);
    }

    #[test]
    fn getpixpat_present_resource_returns_non_nil_handle() {
        // Inside Macintosh Volume V (1986), p. V-72: GetPixPat creates a
        // pixel pattern from a present 'ppat' resource and returns a handle.
        let (mut d, mut cpu, mut bus) = setup();
        let ppat_data = [0x00u8, 0x00, 0x00, 0x14, 0xAA, 0x55, 0xAA, 0x55];
        let _ = d.install_test_resource(&mut bus, *b"ppat", 128, &ppat_data);
        bus.write_word(TEST_SP, 128);

        let result = d.dispatch_quickdraw(true, 0x20C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_ne!(bus.read_long(TEST_SP + 2), 0);
    }

    // ==================== Color Icon Traps ====================

    fn make_test_cicn_resource(mask_byte: u8, bmap_byte: u8, pixel_byte: u8) -> Vec<u8> {
        // Minimal 1-row, 8-pixel CIcon record payload:
        // CIcon header (82 bytes) + mask byte + bmap byte +
        // ColorTable (16 bytes for ctSize=0) + pixel byte.
        let mut data = vec![0u8; 101];

        // iconPMap (offset 0)
        data[4] = 0x00;
        data[5] = 0x01; // rowBytes
        data[10] = 0x00;
        data[11] = 0x01; // bounds.bottom = 1
        data[12] = 0x00;
        data[13] = 0x08; // bounds.right = 8
        data[32] = 0x00;
        data[33] = 0x01; // pixelSize = 1

        // iconMask BitMap (offset 50)
        data[54] = 0x00;
        data[55] = 0x01; // rowBytes
        data[60] = 0x00;
        data[61] = 0x01; // bounds.bottom = 1
        data[62] = 0x00;
        data[63] = 0x08; // bounds.right = 8

        // iconBMap BitMap (offset 64)
        data[68] = 0x00;
        data[69] = 0x01; // rowBytes
        data[74] = 0x00;
        data[75] = 0x01; // bounds.bottom = 1
        data[76] = 0x00;
        data[77] = 0x08; // bounds.right = 8

        // iconData handle (offset 78) intentionally zero.

        // iconMaskData + iconBMapData
        data[82] = mask_byte;
        data[83] = bmap_byte;

        // ColorTable header at offset 84:
        // ctSeed(4), ctFlags(2), ctSize(2). ctSize = 0 => 1 entry.
        data[90] = 0x00;
        data[91] = 0x00;

        // First ColorSpec entry (offset 92..99) left as zeroes.
        data[100] = pixel_byte; // inline pixel data
        data
    }

    fn make_test_cicn_resource_4bpp(pixel_bytes: [u8; 2], entries: &[[u16; 3]; 16]) -> Vec<u8> {
        // Minimal 1-row, 4-pixel, 4bpp CIcon record payload:
        // CIcon header (82 bytes) + 1-byte mask + 1-byte bmap +
        // ColorTable (16 entries) + 2 bytes of packed pixel data.
        let mut data = vec![0u8; 222];

        // iconPMap (offset 0)
        data[4] = 0x80;
        data[5] = 0x02; // rowBytes = 2, PixMap flag set
        data[10] = 0x00;
        data[11] = 0x01; // bounds.bottom = 1
        data[12] = 0x00;
        data[13] = 0x04; // bounds.right = 4
        data[32] = 0x00;
        data[33] = 0x04; // pixelSize = 4

        // iconMask BitMap (offset 50)
        data[54] = 0x00;
        data[55] = 0x01; // rowBytes
        data[60] = 0x00;
        data[61] = 0x01; // bounds.bottom = 1
        data[62] = 0x00;
        data[63] = 0x04; // bounds.right = 4

        // iconBMap BitMap (offset 64)
        data[68] = 0x00;
        data[69] = 0x01; // rowBytes
        data[74] = 0x00;
        data[75] = 0x01; // bounds.bottom = 1
        data[76] = 0x00;
        data[77] = 0x04; // bounds.right = 4

        data[82] = 0xF0; // mask: first four pixels opaque

        // ColorTable header at offset 84:
        // ctSeed(4), ctFlags(2), ctSize(2). ctSize = 15 => 16 entries.
        data[90] = 0x00;
        data[91] = 0x0F;
        for (i, rgb) in entries.iter().enumerate() {
            let entry = 92 + i * 8;
            data[entry] = 0;
            data[entry + 1] = i as u8;
            data[entry + 2..entry + 4].copy_from_slice(&rgb[0].to_be_bytes());
            data[entry + 4..entry + 6].copy_from_slice(&rgb[1].to_be_bytes());
            data[entry + 6..entry + 8].copy_from_slice(&rgb[2].to_be_bytes());
        }

        data[220] = pixel_bytes[0];
        data[221] = pixel_bytes[1];
        data
    }

    #[test]
    fn getcicon_missing_resource_returns_nil_and_pops_iconid_word() {
        // Inside Macintosh Volume V (1986), p. V-76: GetCIcon returns
        // NIL when the requested 'cicn' resource cannot be found.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 777);

        let result = d.dispatch_quickdraw(true, 0x21E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(bus.read_long(TEST_SP + 2), 0);
    }

    #[test]
    fn getcicon_pascal_function_preserves_stack_across_five_missing_calls() {
        // Mirrors band B2 of aa1e_getcicon_strict.
        // Per IM:V 1986 p. V-76, each GetCIcon call obeys the Tool-bit
        // Pascal FUNCTION calling convention independently (caller
        // pre-pushes a 4-byte CIconHandle result slot + 2-byte iconID
        // INTEGER, trap pops the 2-byte arg and writes the handle to
        // the result slot at the post-pop SP, caller pops the slot).
        // Across a 5-call composition with five distinct fictional
        // iconIDs all five returned handles are NIL per the documented
        // miss-returns-NIL contract AND A7 returns to its pre-
        // composition value (5 missed 2-byte pops would cumulate to
        // 10 bytes A7 drift).
        let (mut d, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        let icon_ids: [i16; 5] = [0x6FF0, 0x6FF1, 0x6FF2, 0x6FF3, 0x6FF4];
        for icon_id in icon_ids.iter().copied() {
            let sp = cpu.read_reg(Register::A7);
            let slot_addr = sp.wrapping_sub(6);
            bus.write_long(slot_addr, 0xDEAD_BEEF);
            bus.write_word(slot_addr + 4, icon_id as u16);
            cpu.write_reg(Register::A7, slot_addr);

            let result = d.dispatch_quickdraw(true, 0x21E, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());

            assert_eq!(cpu.read_reg(Register::A7), slot_addr + 2);
            assert_eq!(bus.read_long(slot_addr + 2), 0);
            cpu.write_reg(Register::A7, slot_addr + 6);
        }
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_before,
            "5-call GetCIcon composition with distinct missing iconIDs is net A7-balanced"
        );
    }

    #[test]
    fn getcicon_present_resource_returns_fresh_handle_each_call_and_initializes_icondata_handle() {
        // More Macintosh Toolbox (1993), p. 5-29: GetCIcon returns a
        // fresh CIconHandle for each present 'cicn' resource hit and
        // initializes a CIcon record from that data.
        // Inside Macintosh Volume V (1986), p. V-64 defines iconData at
        // offset 78 in the CIcon record layout.
        let (mut d, mut cpu, mut bus) = setup();
        let cicn_data = make_test_cicn_resource(0xF0, 0x00, 0x55);
        d.install_test_resource(&mut bus, *b"cicn", 128, &cicn_data);
        bus.write_word(TEST_SP, 128);

        let result = d.dispatch_quickdraw(true, 0x21E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);

        let icon_handle = bus.read_long(TEST_SP + 2);
        assert_ne!(icon_handle, 0);

        let icon_ptr = bus.read_long(icon_handle);
        assert_ne!(icon_ptr, 0);

        let icon_data_handle = bus.read_long(icon_ptr + 78);
        assert_ne!(icon_data_handle, 0);
        let icon_data_ptr = bus.read_long(icon_data_handle);
        assert_ne!(icon_data_ptr, 0);
        assert_ne!(icon_data_ptr, icon_ptr + 100);
        assert_eq!(bus.read_byte(icon_data_ptr), 0x55);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 128);
        let result2 = d.dispatch_quickdraw(true, 0x21E, &mut cpu, &mut bus);
        assert!(result2.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);

        let icon_handle2 = bus.read_long(TEST_SP + 2);
        assert_ne!(icon_handle2, 0);
        assert_ne!(icon_handle2, icon_handle);

        let icon_ptr2 = bus.read_long(icon_handle2);
        assert_ne!(icon_ptr2, 0);
        assert_ne!(icon_ptr2, icon_ptr);

        let icon_data_handle2 = bus.read_long(icon_ptr2 + 78);
        assert_ne!(icon_data_handle2, 0);
        let icon_data_ptr2 = bus.read_long(icon_data_handle2);
        assert_ne!(icon_data_ptr2, 0);
        assert_ne!(icon_data_ptr2, icon_ptr2 + 100);
        assert_eq!(bus.read_byte(icon_data_ptr2), 0x55);
    }

    #[test]
    fn getcicon_present_resource_initializes_live_bitmap_fields_for_copymask_callers() {
        // IM:V V-79..V-80 defines 'cicn' resources with zero placeholder
        // fields for PixMap.baseAddr, PixMap.pmTable, and iconData; IM:V
        // V-78 says Color QuickDraw copies these resources and alters the
        // copy before returning it. Apps may use the returned CIcon record's
        // embedded PixMap and mask directly with CopyMask rather than going
        // through PlotCIcon, so those live fields must address usable data.
        let (mut d, mut cpu, mut bus) = setup();
        let cicn_data = make_test_cicn_resource(0xF0, 0x00, 0xA5);
        d.install_test_resource(&mut bus, *b"cicn", 128, &cicn_data);
        bus.write_word(TEST_SP, 128);

        let get_icon = d.dispatch_quickdraw(true, 0x21E, &mut cpu, &mut bus);
        assert!(get_icon.unwrap().is_ok());

        let icon_handle = bus.read_long(TEST_SP + 2);
        assert_ne!(icon_handle, 0);
        let icon_ptr = bus.read_long(icon_handle);
        assert_ne!(icon_ptr, 0);

        let icon_data_handle = bus.read_long(icon_ptr + 78);
        assert_ne!(icon_data_handle, 0);
        let icon_data_ptr = bus.read_long(icon_data_handle);
        assert_ne!(icon_data_ptr, 0);
        assert_eq!(
            bus.read_long(icon_ptr),
            icon_data_ptr,
            "live CIcon.iconPMap.baseAddr must point at the copied pixel data"
        );

        let pm_table_handle = bus.read_long(icon_ptr + 42);
        assert_ne!(pm_table_handle, 0);
        let pm_table_ptr = bus.read_long(pm_table_handle);
        assert_ne!(pm_table_ptr, 0);
        assert_ne!(pm_table_ptr, icon_ptr + 84);
        assert_eq!(bus.read_word(pm_table_ptr + 6), 0);

        assert_eq!(
            bus.read_long(icon_ptr + 50),
            icon_ptr + 82,
            "live CIcon.iconMask.baseAddr must point at inline mask data"
        );
        assert_eq!(
            bus.read_long(icon_ptr + 64),
            icon_ptr + 83,
            "live CIcon.iconBMap.baseAddr must point at inline bitmap data"
        );

        let dst_bits = bus.alloc(14);
        let dst_base = bus.alloc(1);
        write_bitmap_1bpp(&mut bus, dst_bits, dst_base, 1, (0, 0, 1, 8));
        bus.write_byte(dst_base, 0x0F);

        let src_rect = bus.alloc(8);
        let mask_rect = bus.alloc(8);
        let dst_rect = bus.alloc(8);
        write_rect(&mut bus, src_rect, 0, 0, 1, 8);
        write_rect(&mut bus, mask_rect, 0, 0, 1, 8);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 8);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dst_rect);
        bus.write_long(TEST_SP + 4, mask_rect);
        bus.write_long(TEST_SP + 8, src_rect);
        bus.write_long(TEST_SP + 12, dst_bits);
        bus.write_long(TEST_SP + 16, icon_ptr + 50);
        bus.write_long(TEST_SP + 20, icon_ptr);

        let copy_mask = d.dispatch_quickdraw(true, 0x017, &mut cpu, &mut bus);
        assert!(copy_mask.unwrap().is_ok());
        assert_eq!(
            bus.read_byte(dst_base),
            0xAF,
            "CopyMask should be able to draw from GetCIcon's live PixMap and mask"
        );
    }

    #[test]
    fn disposecicon_clears_getcicon_icondata_handle_and_preserves_general_registers() {
        // More Macintosh Toolbox (1993), p. 5-30: DisposeCIcon disposes
        // the structures allocated by GetCIcon and takes one CIconHandle
        // argument.
        let (mut d, mut cpu, mut bus) = setup();
        let cicn_data = make_test_cicn_resource(0xF0, 0x00, 0x55);
        d.install_test_resource(&mut bus, *b"cicn", 128, &cicn_data);
        bus.write_word(TEST_SP, 128);

        let get_icon = d.dispatch_quickdraw(true, 0x21E, &mut cpu, &mut bus);
        assert!(get_icon.unwrap().is_ok());
        let icon_handle = bus.read_long(TEST_SP + 2);
        assert_ne!(icon_handle, 0);
        let icon_ptr = bus.read_long(icon_handle);
        assert_ne!(icon_ptr, 0);
        assert_ne!(bus.read_long(icon_ptr + 78), 0);

        cpu.write_reg(Register::A0, icon_ptr);
        let recover_live = d.dispatch_memory(false, 0x28, &mut cpu, &mut bus);
        assert!(recover_live.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A0), icon_handle);

        cpu.write_reg(Register::A0, 0x1111_2222);
        cpu.write_reg(Register::A1, 0x3333_4444);
        cpu.write_reg(Register::D0, 0x5555_6666);
        cpu.write_reg(Register::D1, 0x7777_8888);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, icon_handle);
        let dispose = d.dispatch_quickdraw(true, 0x225, &mut cpu, &mut bus);
        assert!(dispose.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(cpu.read_reg(Register::A0), 0x1111_2222);
        assert_eq!(cpu.read_reg(Register::A1), 0x3333_4444);
        assert_eq!(cpu.read_reg(Register::D0), 0x5555_6666);
        assert_eq!(cpu.read_reg(Register::D1), 0x7777_8888);
        assert_eq!(bus.read_long(icon_ptr + 78), 0);
    }

    #[test]
    fn plotcicon_consumes_theicon_and_therect_arguments() {
        // More Macintosh Toolbox (1993), p. 5-25: PlotCIcon is a
        // procedure with Rect and CIconHandle arguments.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = 0x300100u32;
        write_rect(&mut bus, rect_ptr, 0, 0, 1, 8);
        bus.write_long(TEST_SP, 0); // nil icon handle
        bus.write_long(TEST_SP + 4, rect_ptr);

        let result = d.dispatch_quickdraw(true, 0x21F, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
    }

    #[test]
    fn plotcicon_uses_iconmask_to_gate_destination_writes() {
        // More Macintosh Toolbox (1993), p. 5-25 and Inside Macintosh
        // Volume V (1986), p. V-76: PlotCIcon copies icon pixels only
        // where iconMask bits are 1; mask=0 bits leave destination unchanged.
        let (mut d, mut cpu, mut bus) = setup();
        let dst_base = bus.alloc(64);
        bus.fill_zeros(dst_base, 64);
        bus.write_byte(dst_base, 0x0F);

        // Build a minimal CGrafPort with a portPixMap handle so PlotCIcon
        // takes the Color QuickDraw destination path.
        let pm_handle = bus.alloc(4);
        let pm_ptr = bus.alloc(64);
        bus.fill_zeros(pm_ptr, 64);
        bus.write_long(pm_handle, pm_ptr);
        bus.write_long(pm_ptr, dst_base);
        bus.write_word(pm_ptr + 4, 1); // rowBytes
        write_rect(&mut bus, pm_ptr + 6, 0, 0, 1, 8);
        let port_ptr = bus.alloc(128);
        bus.fill_zeros(port_ptr, 128);
        bus.write_long(port_ptr, pm_handle);
        bus.write_word(port_ptr + 4, 0xC000); // CGrafPort signature

        let a5 = cpu.read_reg(Register::A5);
        let globals_ptr = bus.read_long(a5);
        bus.write_long(globals_ptr, port_ptr);

        let cicn_data = make_test_cicn_resource(0xF0, 0x00, 0x55);
        d.install_test_resource(&mut bus, *b"cicn", 128, &cicn_data);

        bus.write_word(TEST_SP, 128);
        let get_icon = d.dispatch_quickdraw(true, 0x21E, &mut cpu, &mut bus);
        assert!(get_icon.unwrap().is_ok());
        let icon_handle = bus.read_long(TEST_SP + 2);
        assert_ne!(icon_handle, 0);

        let rect_ptr = 0x300120u32;
        write_rect(&mut bus, rect_ptr, 0, 0, 1, 8);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, icon_handle);
        bus.write_long(TEST_SP + 4, rect_ptr);

        let plot = d.dispatch_quickdraw(true, 0x21F, &mut cpu, &mut bus);
        assert!(plot.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_byte(dst_base), 0x5F);
    }

    #[test]
    fn plotcicon_draws_into_real_cgrafport_portpixmap_layout() {
        // Systemless CGrafPorts store portPixMap at +2 and portVersion at
        // +6, matching Imaging With QuickDraw's CGrafPort layout. PlotCIcon
        // must resolve that real layout, not only the legacy synthetic test
        // layout with the PixMap handle at +0.
        let (mut d, mut cpu, mut bus) = setup();
        let dst_base = bus.alloc(64);
        bus.fill_zeros(dst_base, 64);
        bus.write_byte(dst_base, 0x0F);

        let pm_handle = bus.alloc(4);
        let pm_ptr = bus.alloc(64);
        bus.fill_zeros(pm_ptr, 64);
        bus.write_long(pm_handle, pm_ptr);
        bus.write_long(pm_ptr, dst_base);
        bus.write_word(pm_ptr + 4, 1);
        write_rect(&mut bus, pm_ptr + 6, 0, 0, 1, 8);

        let port_ptr = bus.alloc(128);
        bus.fill_zeros(port_ptr, 128);
        bus.write_long(port_ptr + 2, pm_handle);
        bus.write_word(port_ptr + 6, 0xC000);

        let a5 = cpu.read_reg(Register::A5);
        let globals_ptr = bus.read_long(a5);
        bus.write_long(globals_ptr, port_ptr);

        let cicn_data = make_test_cicn_resource(0xF0, 0x00, 0x55);
        d.install_test_resource(&mut bus, *b"cicn", 128, &cicn_data);

        bus.write_word(TEST_SP, 128);
        let get_icon = d.dispatch_quickdraw(true, 0x21E, &mut cpu, &mut bus);
        assert!(get_icon.unwrap().is_ok());
        let icon_handle = bus.read_long(TEST_SP + 2);
        assert_ne!(icon_handle, 0);

        let rect_ptr = 0x300140u32;
        write_rect(&mut bus, rect_ptr, 0, 0, 1, 8);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, icon_handle);
        bus.write_long(TEST_SP + 4, rect_ptr);

        let plot = d.dispatch_quickdraw(true, 0x21F, &mut cpu, &mut bus);
        assert!(plot.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_byte(dst_base), 0x5F);
    }

    #[test]
    fn plotcicon_decodes_packed_4bpp_icon_pixels_into_8bpp_destination() {
        // More Macintosh Toolbox (1993), p. 5-25 / Inside Macintosh V
        // (1986), p. V-76: PlotCIcon stretches the iconPMap and remaps its
        // pixels to the current depth and color table.
        let (mut d, mut cpu, mut bus) = setup();
        d.device_clut = [[0, 0, 0]; 256];
        d.device_clut[7] = [0x1111, 0x0000, 0x0000];
        d.device_clut[9] = [0x0000, 0x2222, 0x0000];
        d.device_clut[42] = [0x0000, 0x0000, 0x3333];
        assert_eq!(
            crate::trap::pict::closest_clut_index(0x1111, 0x0000, 0x0000, &d.device_clut),
            7
        );

        let dst_base = bus.alloc(8);
        bus.fill_zeros(dst_base, 8);

        let pm_handle = bus.alloc(4);
        let pm_ptr = bus.alloc(64);
        bus.fill_zeros(pm_ptr, 64);
        bus.write_long(pm_handle, pm_ptr);
        write_pixmap_8(&mut bus, pm_ptr, dst_base, 4, 1, 0);

        let port_ptr = bus.alloc(128);
        bus.fill_zeros(port_ptr, 128);
        bus.write_long(port_ptr + 2, pm_handle);
        bus.write_word(port_ptr + 6, 0xC000);

        let a5 = cpu.read_reg(Register::A5);
        let globals_ptr = bus.read_long(a5);
        bus.write_long(globals_ptr, port_ptr);

        let mut icon_entries = [[0, 0, 0]; 16];
        icon_entries[1] = d.device_clut[7];
        icon_entries[2] = d.device_clut[9];
        icon_entries[3] = d.device_clut[42];
        let cicn_data = make_test_cicn_resource_4bpp([0x12, 0x30], &icon_entries);
        d.install_test_resource(&mut bus, *b"cicn", 129, &cicn_data);

        bus.write_word(TEST_SP, 129);
        let get_icon = d.dispatch_quickdraw(true, 0x21E, &mut cpu, &mut bus);
        assert!(get_icon.unwrap().is_ok());
        let icon_handle = bus.read_long(TEST_SP + 2);
        assert_ne!(icon_handle, 0);
        let icon_ptr = bus.read_long(icon_handle);
        assert_eq!(bus.read_word(icon_ptr + 32), 4);
        let icon_data_handle = bus.read_long(icon_ptr + 78);
        let icon_data_ptr = bus.read_long(icon_data_handle);
        assert_eq!(bus.read_bytes(icon_data_ptr, 2), vec![0x12, 0x30]);
        assert_eq!(
            TrapDispatcher::read_packed_bitmap_index(&bus, icon_data_ptr, 2, 4, 0, 0, 1, 4, 0, 0),
            Some(1)
        );
        assert_eq!(
            TrapDispatcher::read_packed_bitmap_index(&bus, icon_data_ptr, 2, 4, 0, 0, 1, 4, 0, 2),
            Some(3)
        );

        let rect_ptr = 0x300160u32;
        write_rect(&mut bus, rect_ptr, 0, 0, 1, 4);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, icon_handle);
        bus.write_long(TEST_SP + 4, rect_ptr);

        let plot = d.dispatch_quickdraw(true, 0x21F, &mut cpu, &mut bus);
        assert!(plot.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(
            bus.read_bytes(dst_base, 4),
            vec![7, 9, 42, 255],
            "packed 4bpp source nibbles should decode and remap through the icon ColorTable"
        );
    }

    #[test]
    fn plotcicon_nil_rect_is_harmless_noop() {
        // More Macintosh Toolbox (1993), p. 5-25: PlotCIcon takes a
        // Rect pointer, but a NIL pointer should not trash unrelated
        // memory when the call is otherwise well-formed.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let dst_base = bus.alloc(64);
        bus.fill_zeros(dst_base, 64);
        bus.write_byte(dst_base, 0x0F);

        let pm_handle = bus.alloc(4);
        let pm_ptr = bus.alloc(64);
        bus.fill_zeros(pm_ptr, 64);
        bus.write_long(pm_handle, pm_ptr);
        bus.write_long(pm_ptr, dst_base);
        bus.write_word(pm_ptr + 4, 1);
        write_rect(&mut bus, pm_ptr + 6, 0, 0, 1, 8);
        let port_ptr = bus.alloc(128);
        bus.fill_zeros(port_ptr, 128);
        bus.write_long(port_ptr, pm_handle);
        bus.write_word(port_ptr + 4, 0xC000);

        let a5 = cpu.read_reg(Register::A5);
        let globals_ptr = bus.read_long(a5);
        bus.write_long(globals_ptr, port_ptr);

        let cicn_data = make_test_cicn_resource(0xF0, 0x00, 0x55);
        d.install_test_resource(&mut bus, *b"cicn", 128, &cicn_data);

        bus.write_word(TEST_SP, 128);
        let get_icon = d.dispatch_quickdraw(true, 0x21E, &mut cpu, &mut bus);
        assert!(get_icon.unwrap().is_ok());
        let icon_handle = bus.read_long(TEST_SP + 2);
        assert_ne!(icon_handle, 0);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, icon_handle);
        bus.write_long(TEST_SP + 4, 0);

        let plot = d.dispatch_quickdraw(true, 0x21F, &mut cpu, &mut bus);
        assert!(plot.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_byte(dst_base), 0x0F);
    }

    // ==================== Color Port Stubs ====================

    #[test]
    fn open_cport_allocates_portpixmap_handle_and_record() {
        // Imaging With QuickDraw 1994, p. 4-64 (Table 4-3):
        // OpenCPort allocates a portPixMap handle for the CGrafPort.
        let (mut d, mut cpu, mut bus) = setup();
        let port_ptr = 0x300000u32;
        bus.write_long(TEST_SP, port_ptr);
        let result = d.dispatch_quickdraw(true, 0x200, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        let pixmap_handle = bus.read_long(port_ptr + 2);
        assert_ne!(pixmap_handle, 0);
        assert_ne!(bus.read_long(pixmap_handle), 0);
    }

    #[test]
    fn open_cport_sets_portversion_and_region_defaults() {
        // Imaging With QuickDraw 1994, p. 4-64 (Table 4-3): portVersion is
        // $C000, visRgn is screen bounds, and clipRgn is initialized infinite.
        let (mut d, mut cpu, mut bus) = setup();
        let port_ptr = 0x300000u32;
        bus.write_long(TEST_SP, port_ptr);
        let result = d.dispatch_quickdraw(true, 0x200, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(port_ptr + 6), 0xC000);

        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd_ptr = bus.read_long(gdh);
        let expected_bounds = (
            bus.read_word(gd_ptr + 34) as i16,
            bus.read_word(gd_ptr + 36) as i16,
            bus.read_word(gd_ptr + 38) as i16,
            bus.read_word(gd_ptr + 40) as i16,
        );
        let vis_rgn = bus.read_long(port_ptr + 24);
        assert_ne!(vis_rgn, 0);
        assert_eq!(read_rgn_bbox(&bus, vis_rgn), expected_bounds);

        let clip_rgn = bus.read_long(port_ptr + 28);
        assert_ne!(clip_rgn, 0);
        assert_eq!(
            read_rgn_bbox(&bus, clip_rgn),
            (-32767, -32767, 32767, 32767)
        );
    }

    #[test]
    fn open_cport_initializes_pixmap_from_current_gdevice() {
        // Imaging With QuickDraw 1994, pp. 4-64 to 4-66: OpenCPort allocates
        // port-owned storage, then InitCPort initializes it from the current
        // device. Apps can SetGDevice to an offscreen GDevice before opening
        // a temporary CGrafPort.
        let (mut d, mut cpu, mut bus) = setup();
        let offscreen_gdh = d.allocate_detached_gdevice(&mut bus);
        let offscreen_gd = bus.read_long(offscreen_gdh);
        let offscreen_pm_handle = bus.read_long(offscreen_gd + 22);
        let offscreen_pm = bus.read_long(offscreen_pm_handle);
        let offscreen_base = bus.alloc(64 * 64);

        bus.write_long(offscreen_pm, offscreen_base);
        bus.write_word(offscreen_pm + 4, 0x8000 | 64);
        bus.write_word(offscreen_pm + 6, 7);
        bus.write_word(offscreen_pm + 8, 9);
        bus.write_word(offscreen_pm + 10, 47);
        bus.write_word(offscreen_pm + 12, 73);
        bus.write_word(offscreen_gd + 34, 7);
        bus.write_word(offscreen_gd + 36, 9);
        bus.write_word(offscreen_gd + 38, 47);
        bus.write_word(offscreen_gd + 40, 73);

        d.current_gdevice = offscreen_gdh;
        bus.write_long(0x0CC8, offscreen_gdh);

        let port_ptr = 0x300000u32;
        bus.write_long(TEST_SP, port_ptr);
        let result = d.dispatch_quickdraw(true, 0x200, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(d.current_gdevice, offscreen_gdh);

        let port_pm_handle = bus.read_long(port_ptr + 2);
        let port_pm = bus.read_long(port_pm_handle);
        assert_ne!(port_pm_handle, offscreen_pm_handle);
        assert_eq!(bus.read_long(port_pm), offscreen_base);
        assert_eq!(bus.read_word(port_pm + 4), 0x8000 | 64);
        assert_eq!(read_rect(&bus, port_pm + 6), (7, 9, 47, 73));
        assert_eq!(read_rect(&bus, port_ptr + 16), (7, 9, 47, 73));
        assert_eq!(
            read_rgn_bbox(&bus, bus.read_long(port_ptr + 24)),
            (7, 9, 47, 73)
        );
    }

    #[test]
    fn init_cport_copies_current_device_clut_handle_into_port_pixmap() {
        // Imaging With QuickDraw 1994, p. 4-66:
        // InitCPort replaces pmTable with a copy of the current device CLUT handle.
        let (mut d, mut cpu, mut bus) = setup();
        let port_ptr = 0x300000u32;
        bus.write_long(TEST_SP, port_ptr);
        let open = d.dispatch_quickdraw(true, 0x200, &mut cpu, &mut bus);
        assert!(open.unwrap().is_ok());

        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd_ptr = bus.read_long(gdh);
        let gd_pixmap_handle = bus.read_long(gd_ptr + 22);
        let gd_pixmap = bus.read_long(gd_pixmap_handle);
        let replacement_ctab_ptr = bus.alloc(16);
        let replacement_ctab_handle = bus.alloc(4);
        bus.write_long(replacement_ctab_handle, replacement_ctab_ptr);
        bus.write_long(gd_pixmap + 42, replacement_ctab_handle);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, port_ptr);
        let init = d.dispatch_quickdraw(true, 0x201, &mut cpu, &mut bus);
        assert!(init.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        let port_pixmap_handle = bus.read_long(port_ptr + 2);
        let port_pixmap = bus.read_long(port_pixmap_handle);
        assert_eq!(bus.read_long(port_pixmap + 42), replacement_ctab_handle);
    }

    #[test]
    fn init_cport_reinitializes_field_defaults_on_existing_cgraf_port() {
        // Imaging With QuickDraw 1994, p. 4-66:
        // InitCPort resets CGrafPort fields to the Table 4-3 defaults.
        let (mut d, mut cpu, mut bus) = setup();
        let port_ptr = 0x300000u32;
        bus.write_long(TEST_SP, port_ptr);
        let open = d.dispatch_quickdraw(true, 0x200, &mut cpu, &mut bus);
        assert!(open.unwrap().is_ok());

        // Dirty old/new fields and region bboxes; InitCPort should reset them.
        bus.write_word(port_ptr + 12, 0x1234); // chExtra
        bus.write_word(port_ptr + 14, 0x0001); // pnLocHFrac
        bus.write_word(port_ptr + 16, 777); // portRect.top
        bus.write_word(port_ptr + 18, 888); // portRect.left
        bus.write_word(port_ptr + 20, 999); // portRect.bottom
        bus.write_word(port_ptr + 22, 1111); // portRect.right
        bus.write_word(port_ptr + 48, 9); // pnLoc.v
        bus.write_word(port_ptr + 50, 9); // pnLoc.h
        bus.write_word(port_ptr + 52, 7); // pnSize.v
        bus.write_word(port_ptr + 54, 7); // pnSize.h
        let vis_rgn = bus.read_long(port_ptr + 24);
        let vis_rgn_ptr = bus.read_long(vis_rgn);
        bus.write_word(vis_rgn_ptr + 2, 1);
        bus.write_word(vis_rgn_ptr + 4, 2);
        bus.write_word(vis_rgn_ptr + 6, 3);
        bus.write_word(vis_rgn_ptr + 8, 4);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, port_ptr);
        let init = d.dispatch_quickdraw(true, 0x201, &mut cpu, &mut bus);
        assert!(init.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd_ptr = bus.read_long(gdh);
        let expected_bounds = (
            bus.read_word(gd_ptr + 34) as i16,
            bus.read_word(gd_ptr + 36) as i16,
            bus.read_word(gd_ptr + 38) as i16,
            bus.read_word(gd_ptr + 40) as i16,
        );

        assert_eq!(bus.read_word(port_ptr + 6), 0xC000);
        assert_eq!(bus.read_word(port_ptr + 12), 0);
        assert_eq!(bus.read_word(port_ptr + 14), 0x8000);
        assert_eq!(read_rect(&bus, port_ptr + 16), expected_bounds);
        assert_eq!(read_rgn_bbox(&bus, vis_rgn), expected_bounds);
        assert_eq!(bus.read_word(port_ptr + 48), 0);
        assert_eq!(bus.read_word(port_ptr + 50), 0);
        assert_eq!(bus.read_word(port_ptr + 52), 1);
        assert_eq!(bus.read_word(port_ptr + 54), 1);
    }

    #[test]
    fn init_cport_on_non_cgraf_port_is_noop() {
        // IM:V 1986 p. V-67 and Imaging With QuickDraw 1994 p. 4-66:
        // InitCPort simply returns if passed a GrafPort.
        let (mut d, mut cpu, mut bus) = setup();
        let port_ptr = 0x300000u32;
        bus.write_long(port_ptr + 2, 0x11112222);
        bus.write_word(port_ptr + 6, 0x0000); // not a CGrafPort marker
        bus.write_long(port_ptr + 24, 0x33334444);
        bus.write_long(port_ptr + 28, 0x55556666);
        bus.write_word(port_ptr + 12, 0xABCD);

        bus.write_long(TEST_SP, port_ptr);
        let result = d.dispatch_quickdraw(true, 0x201, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(port_ptr + 2), 0x11112222);
        assert_eq!(bus.read_word(port_ptr + 6), 0x0000);
        assert_eq!(bus.read_long(port_ptr + 24), 0x33334444);
        assert_eq!(bus.read_long(port_ptr + 28), 0x55556666);
        assert_eq!(bus.read_word(port_ptr + 12), 0xABCD);
    }

    #[test]
    fn set_cport_pix_updates_current_cgrafport_pixmap_handle_and_pops_argument() {
        // Inside Macintosh Volume V (1986), p. V-74:
        // SetCPortPix writes the PixMap handle into the current CGrafPort.
        let (mut d, mut cpu, mut bus) = setup();
        let port_ptr = 0x300000u32;
        bus.write_long(TEST_SP, port_ptr);

        let open = d.dispatch_quickdraw(true, 0x200, &mut cpu, &mut bus);
        assert!(open.unwrap().is_ok());

        let original_pm = bus.read_long(port_ptr + 2);
        let replacement_pm = d.dispatch_quickdraw(true, 0x203, &mut cpu, &mut bus);
        assert!(replacement_pm.unwrap().is_ok());
        let replacement_pm = bus.read_long(TEST_SP);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, replacement_pm);
        let set = d.dispatch_quickdraw(true, 0x206, &mut cpu, &mut bus);
        assert!(set.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_ne!(original_pm, replacement_pm);
        assert_eq!(bus.read_long(port_ptr + 2), replacement_pm);
    }

    #[test]
    fn newpixmap_returns_non_nil_pixmap_handle_and_record() {
        // Inside Macintosh Volume V 1986 p. V-57: NewPixMap returns
        // a PixMapHandle for a newly allocated PixMap record.
        let (mut d, mut cpu, mut bus) = setup();
        let result = d.dispatch_quickdraw(true, 0x203, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let handle = bus.read_long(TEST_SP);
        assert_ne!(handle, 0);
        assert_ne!(bus.read_long(handle), 0);
    }

    #[test]
    fn newpixmap_function_signature_writes_result_slot_without_popping_arguments() {
        // Inside Macintosh Volume V 1986 p. V-57: NewPixMap is a no-arg
        // function returning PixMapHandle, so A7 is unchanged and the
        // result is written at [SP].
        let (mut d, mut cpu, mut bus) = setup();
        let result = d.dispatch_quickdraw(true, 0x203, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_ne!(bus.read_long(TEST_SP), 0);
    }

    #[test]
    fn dispospixmap_releases_pixmap_and_pmtable_allocations() {
        // Inside Macintosh Volume V 1986 p. V-57: DisposPixMap releases
        // NewPixMap-owned storage, including the pmTable and the PixMap.
        let (mut d, mut cpu, mut bus) = setup();
        let ctab_ptr = bus.alloc(8 + 8);
        let ctab_handle = bus.alloc(4);
        bus.write_long(ctab_handle, ctab_ptr);

        let pixmap_ptr = bus.alloc(64);
        bus.write_long(pixmap_ptr + 42, ctab_handle); // pmTable
        let pixmap_handle = bus.alloc(4);
        bus.write_long(pixmap_handle, pixmap_ptr);

        assert!(bus.get_alloc_size(ctab_ptr).is_some());
        assert!(bus.get_alloc_size(ctab_handle).is_some());
        assert!(bus.get_alloc_size(pixmap_ptr).is_some());
        assert!(bus.get_alloc_size(pixmap_handle).is_some());

        bus.write_long(TEST_SP, pixmap_handle);
        let result = d.dispatch_quickdraw(true, 0x204, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert!(bus.get_alloc_size(ctab_ptr).is_none());
        assert!(bus.get_alloc_size(ctab_handle).is_none());
        assert!(bus.get_alloc_size(pixmap_ptr).is_none());
        assert!(bus.get_alloc_size(pixmap_handle).is_none());
    }

    #[test]
    fn dispospixmap_consumes_pixmaphandle_pointer_argument() {
        // Inside Macintosh Volume V 1986 p. V-57 signature:
        //   PROCEDURE DisposPixMap(pm: PixMapHandle);
        let (mut d, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        bus.write_long(sp_before, 0);

        let result = d.dispatch_quickdraw(true, 0x204, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before + 4);
    }

    #[test]
    fn dispospixmap_clears_caller_handle_after_disposal() {
        // The disposal path invalidates the caller's PixMapHandle cell so
        // a repeated dispose sees NIL instead of a stale freed pointer.
        let (mut d, mut cpu, mut bus) = setup();
        let ctab_ptr = bus.alloc(8 + 8);
        let ctab_handle = bus.alloc(4);
        bus.write_long(ctab_handle, ctab_ptr);

        let pixmap_ptr = bus.alloc(64);
        bus.write_long(pixmap_ptr + 42, ctab_handle);
        let pixmap_handle = bus.alloc(4);
        bus.write_long(pixmap_handle, pixmap_ptr);

        bus.write_long(TEST_SP, pixmap_handle);
        let result = d.dispatch_quickdraw(true, 0x204, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_long(pixmap_handle), 0);
    }

    #[test]
    fn dispospixpat_releases_owned_data_handles_and_embedded_pixmap() {
        // Inside Macintosh Volume V 1986 p. V-72: DisposPixPat releases
        // NewPixPat-owned data handle, expanded-data handle, and pixMap handle.
        let (mut d, mut cpu, mut bus) = setup();

        let pat_data_ptr = bus.alloc(32);
        let pat_data_handle = bus.alloc(4);
        bus.write_long(pat_data_handle, pat_data_ptr);

        let pat_xdata_ptr = bus.alloc(16);
        let pat_xdata_handle = bus.alloc(4);
        bus.write_long(pat_xdata_handle, pat_xdata_ptr);

        let ctab_ptr = bus.alloc(8 + 8);
        let ctab_handle = bus.alloc(4);
        bus.write_long(ctab_handle, ctab_ptr);

        let pat_map_ptr = bus.alloc(64);
        bus.write_long(pat_map_ptr + 42, ctab_handle); // pmTable
        let pat_map_handle = bus.alloc(4);
        bus.write_long(pat_map_handle, pat_map_ptr);

        let pixpat_ptr = bus.alloc(28);
        bus.write_long(pixpat_ptr + 2, pat_map_handle); // patMap
        bus.write_long(pixpat_ptr + 6, pat_data_handle); // patData
        bus.write_long(pixpat_ptr + 10, pat_xdata_handle); // patXData
        let pixpat_handle = bus.alloc(4);
        bus.write_long(pixpat_handle, pixpat_ptr);

        for addr in [
            pat_data_ptr,
            pat_data_handle,
            pat_xdata_ptr,
            pat_xdata_handle,
            ctab_ptr,
            ctab_handle,
            pat_map_ptr,
            pat_map_handle,
            pixpat_ptr,
            pixpat_handle,
        ] {
            assert!(
                bus.get_alloc_size(addr).is_some(),
                "expected allocation at ${addr:08X} before DisposPixPat"
            );
        }

        bus.write_long(TEST_SP, pixpat_handle);
        let result = d.dispatch_quickdraw(true, 0x208, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        for addr in [
            pat_data_ptr,
            pat_data_handle,
            pat_xdata_ptr,
            pat_xdata_handle,
            ctab_ptr,
            ctab_handle,
            pat_map_ptr,
            pat_map_handle,
            pixpat_ptr,
            pixpat_handle,
        ] {
            assert!(
                bus.get_alloc_size(addr).is_none(),
                "expected ${addr:08X} to be released by DisposPixPat"
            );
        }
    }

    #[test]
    fn dispospixpat_consumes_pixpathandle_pointer_argument() {
        // Inside Macintosh Volume V 1986 p. V-72 signature:
        //   PROCEDURE DisposPixPat(ppat: PixPatHandle);
        let (mut d, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        bus.write_long(sp_before, 0);

        let result = d.dispatch_quickdraw(true, 0x208, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before + 4);
    }

    #[test]
    fn copypixpat_copies_28_byte_pixpat_record_from_source_to_destination() {
        // Inside Macintosh Volume V 1986 p. V-72: CopyPixPat copies
        // the source PixPat structure to destination.
        let (mut d, mut cpu, mut bus) = setup();
        let src_ptr = bus.alloc(28);
        let dst_ptr = bus.alloc(28);
        for i in 0..28u32 {
            bus.write_byte(src_ptr + i, (i as u8).wrapping_mul(7).wrapping_add(3));
            bus.write_byte(dst_ptr + i, 0xEE);
        }
        let src_handle = bus.alloc(4);
        let dst_handle = bus.alloc(4);
        bus.write_long(src_handle, src_ptr);
        bus.write_long(dst_handle, dst_ptr);

        // Stack shape for CopyPixPat in Systemless dispatch:
        // SP+0: dstPP, SP+4: srcPP.
        bus.write_long(TEST_SP, dst_handle);
        bus.write_long(TEST_SP + 4, src_handle);
        let result = d.dispatch_quickdraw(true, 0x209, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);

        for i in 0..28u32 {
            assert_eq!(
                bus.read_byte(dst_ptr + i),
                bus.read_byte(src_ptr + i),
                "PixPat byte {} should be copied",
                i
            );
        }
    }

    #[test]
    fn copypixpat_consumes_destination_and_source_handle_arguments() {
        // Inside Macintosh Volume V 1986 p. V-72 signature:
        //   PROCEDURE CopyPixPat(srcPP, dstPP: PixPatHandle).
        let (mut d, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        bus.write_long(sp_before, 0);
        bus.write_long(sp_before + 4, 0);

        let result = d.dispatch_quickdraw(true, 0x209, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before + 8);
    }

    #[test]
    fn test_new_pix_map_during_seeded_picture_window_clones_seeded_palette() {
        let (mut d, mut cpu, mut bus) = setup();
        d.ensure_main_gdevice(&mut bus);
        let mut seeded = TrapDispatcher::standard_mac_8bpp_clut();
        seeded[42] = [0x4444, 0x5555, 0x6666];
        d.seeded_picture_palette = seeded;
        d.seeded_picture_palette_until_tick = 90;
        d.tick_count = 69;

        let result = d.dispatch_quickdraw(true, 0x203, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let handle = bus.read_long(TEST_SP);
        let pixmap = bus.read_long(handle);
        let ctab_handle = bus.read_long(pixmap + 42);
        let ctab_ptr = bus.read_long(ctab_handle);
        let entry = ctab_ptr + 8 + 42 * 8;
        assert_eq!(bus.read_word(entry + 2), 0x4444);
        assert_eq!(bus.read_word(entry + 4), 0x5555);
        assert_eq!(bus.read_word(entry + 6), 0x6666);
    }

    #[test]
    fn test_set_cport_pix() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let pm_handle = 0x300000u32;
        bus.write_long(TEST_SP, pm_handle);
        let result = d.dispatch_quickdraw(true, 0x206, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn test_make_rgb_pat() {
        // $AA0D is MakeRGBPat (PixPatHandle, RGBColor*) -> pops 8.
        // Previously this slot was wrongly bound to CloseCPort, which
        // popped 4. The polymorphic CloseCPort lives at $A87D
        // (test_close_port above) per IM:V's trap-table appendix.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x300000);
        bus.write_long(TEST_SP + 4, 0x300100);
        let result = d.dispatch_quickdraw(true, 0x20D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
    }

    #[test]
    fn test_init_palettes() {
        let (mut d, mut cpu, mut bus) = setup();
        let result = d.dispatch_quickdraw(true, 0x290, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
    }

    #[test]
    fn newpalette_zero_entry_count_returns_non_nil_palette_handle() {
        let (mut d, mut cpu, mut bus) = setup();
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 0);
        bus.write_word(TEST_SP + 2, 0x1234);
        bus.write_long(TEST_SP + 4, 0);
        bus.write_word(TEST_SP + 8, 0x0000);
        bus.write_long(TEST_SP + 10, 0xDEAD_BEEFu32);

        let result = d.dispatch_quickdraw(true, 0x291, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_ne!(bus.read_long(TEST_SP + 10), 0);
    }

    #[test]
    fn test_init_menus_color() {
        let (mut d, mut cpu, mut bus) = setup();
        let result = d.dispatch_quickdraw(true, 0x291, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
    }

    #[test]
    fn test_init_windows_color() {
        let (mut d, mut cpu, mut bus) = setup();
        let result = d.dispatch_quickdraw(true, 0x292, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
    }

    #[test]
    fn getpalette_returns_associated_palette_handle_for_window() {
        // Inside Macintosh Volume VI (1991), p. 20-20: GetPalette returns
        // the palette associated with srcWindow.
        let (mut d, mut cpu, mut bus) = setup();
        let window = 0x0020_1000u32;
        let palette_ptr = bus.alloc(16);
        let palette_handle = bus.alloc(4);
        bus.write_long(palette_handle, palette_ptr);
        d.set_window_palette_association(window, palette_handle, 0);

        bus.write_long(TEST_SP, window);
        let result = d.dispatch_quickdraw(true, 0x296, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_long(TEST_SP + 4), palette_handle);
    }

    #[test]
    fn getpalette_exact_lookup_ignores_default_palette_fallback() {
        // GetPalette must not synthesize a palette from the helper's
        // default-window fallback. The fallback remains available to
        // palette machinery, but GetPalette itself is exact.
        let (mut d, mut cpu, mut bus) = setup();
        let window = 0x0020_1800u32;
        let default_palette_ptr = bus.alloc(16);
        let default_palette_handle = bus.alloc(4);
        bus.write_long(default_palette_handle, default_palette_ptr);
        d.window_palettes
            .insert(u32::MAX, (default_palette_handle, 0));

        assert_eq!(
            d.window_palette_handle(window),
            default_palette_handle,
            "palette machinery should still see the default fallback"
        );

        bus.write_long(TEST_SP, window);
        let result = d.dispatch_quickdraw(true, 0x296, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(TEST_SP + 4), 0);
    }

    #[test]
    fn getpalette_returns_nil_when_window_has_no_associated_palette() {
        // Inside Macintosh Volume VI (1991), p. 20-20: if no palette is
        // associated with srcWindow, GetPalette returns NIL.
        let (mut d, mut cpu, mut bus) = setup();
        let window = 0x0020_2000u32;

        bus.write_long(TEST_SP, window);
        let result = d.dispatch_quickdraw(true, 0x296, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_long(TEST_SP + 4), 0);
    }

    #[test]
    fn pmforecolor_syncs_rgb_into_current_cgrafport() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port = 0x181000u32;
        bus.write_word(port + 6, 0xC000);
        bus.write_word(port + 36, 0);
        bus.write_word(port + 38, 0);
        bus.write_word(port + 40, 0);

        d.set_current_port_state(&mut bus, &mut cpu, port, None);
        let palette = d.create_palette_from_ctab(&mut bus, 2, 0, super::PM_TOLERANT, 0);
        let palette_ptr = TrapDispatcher::palette_ptr(&bus, palette);
        TrapDispatcher::write_palette_color_info(
            &mut bus,
            palette_ptr,
            1,
            [0x1234, 0x5678, 0x9ABC],
            super::PM_TOLERANT,
            0,
        );
        d.set_window_palette_association(port, palette, 0);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 1);
        let result = d.dispatch_quickdraw(true, 0x297, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(d.fg_color, (0x1234, 0x5678, 0x9ABC));
        assert_eq!(bus.read_word(port + 36), 0x1234);
        assert_eq!(bus.read_word(port + 38), 0x5678);
        assert_eq!(bus.read_word(port + 40), 0x9ABC);

        d.set_current_port_state(&mut bus, &mut cpu, 0, None);
        d.fg_color = (0, 0, 0);
        d.set_current_port_state(&mut bus, &mut cpu, port, None);

        assert_eq!(d.fg_color, (0x1234, 0x5678, 0x9ABC));
    }

    #[test]
    fn getpalette_consumes_windowptr_argument_and_writes_function_result_slot() {
        // Inside Macintosh Volume VI (1991), p. 20-20:
        // FUNCTION GetPalette(srcWindow: WindowPtr): PaletteHandle.
        let (mut d, mut cpu, mut bus) = setup();
        let window = 0x0020_3000u32;
        bus.write_long(TEST_SP, window);
        bus.write_long(TEST_SP + 4, 0xDEAD_BEEFu32);

        let result = d.dispatch_quickdraw(true, 0x296, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(TEST_SP + 4), 0);
    }

    #[test]
    fn nsetpalette_nil_window_is_a_noop_and_pops_ten_bytes() {
        // Inside Macintosh Volume VI (1991), p. 20-21:
        // NSetPalette(dstWindow: WindowPtr; srcPalette: PaletteHandle; nUpdates: Integer).
        let (mut d, mut cpu, mut bus) = setup();
        let palette_ptr = bus.alloc(16);
        let palette_handle = bus.alloc(4);
        bus.write_long(palette_handle, palette_ptr);
        bus.write_word(TEST_SP, 0);
        bus.write_long(TEST_SP + 2, palette_handle);
        bus.write_long(TEST_SP + 6, 0);

        let result = d.dispatch_quickdraw(true, 0x295, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(d.window_palette_handle_exact(0), 0);
    }

    #[test]
    fn activatepalette_exact_lookup_leaves_device_clut_unchanged_without_association() {
        let (mut d, mut cpu, mut bus) = setup();
        let window = 0x0020_4000u32;
        let palette_ptr = bus.alloc(16);
        let palette_handle = bus.alloc(4);

        bus.write_long(palette_handle, palette_ptr);
        bus.write_word(palette_ptr, 0x1234);
        bus.write_word(palette_ptr + 2, 0x5678);
        bus.write_word(palette_ptr + 4, 0x9ABC);
        d.window_palettes.insert(u32::MAX, (palette_handle, 0));
        d.front_window = window;
        d.current_port = window;

        let before = d.device_clut;
        bus.write_long(TEST_SP, window);

        let result = d.dispatch_quickdraw(true, 0x294, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(d.device_clut, before);
    }

    #[test]
    fn activatepalette_consumes_windowptr_argument_and_updates_device_clut() {
        let (mut d, mut cpu, mut bus) = setup();
        let window = 0x0020_4100u32;
        let palette_ptr = bus.alloc(32);
        let palette_handle = bus.alloc(4);

        bus.write_long(palette_handle, palette_ptr);
        bus.write_word(palette_ptr, 1);
        bus.write_word(palette_ptr + 2, 0);
        bus.write_word(palette_ptr + 4, 0);
        bus.write_word(palette_ptr + 6, 0);
        bus.write_word(palette_ptr + 8, 0);
        bus.write_word(palette_ptr + 10, 0);
        bus.write_word(palette_ptr + 12, 0);
        bus.write_word(palette_ptr + 14, 0);
        bus.write_word(palette_ptr + 16, 0x1234);
        bus.write_word(palette_ptr + 18, 0x5678);
        bus.write_word(palette_ptr + 20, 0x9ABC);

        d.set_window_palette_association(window, palette_handle, 0);
        d.front_window = window;
        d.current_port = window;

        let before = d.device_clut;
        bus.write_long(TEST_SP, window);

        let result = d.dispatch_quickdraw(true, 0x294, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_ne!(d.device_clut, before);
        assert_eq!(d.device_clut[0], [0x1234, 0x5678, 0x9ABC]);
        assert_eq!(d.color_manager_clut[0], [0x1234, 0x5678, 0x9ABC]);
    }

    #[test]
    fn disposepalette_consumes_palettehandle_and_clears_palette_associations() {
        let (mut d, mut cpu, mut bus) = setup();
        let window = 0x0020_4200u32;
        let palette_ptr = bus.alloc(16);
        let palette_handle = bus.alloc(4);

        bus.write_long(palette_handle, palette_ptr);
        d.window_palettes.insert(window, (palette_handle, 0));
        d.palette_updates.insert(palette_handle, 0x1234);
        bus.write_long(TEST_SP, palette_handle);

        let result = d.dispatch_quickdraw(true, 0x293, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(d.window_palette_handle_exact(window), 0);
        assert!(!d.palette_updates.contains_key(&palette_handle));
    }

    #[test]
    fn animateentry_exact_lookup_leaves_default_palette_unchanged_without_association() {
        let (mut d, mut cpu, mut bus) = setup();
        let window = 0x0020_5000u32;
        let palette_ptr = bus.alloc(16);
        let palette_handle = bus.alloc(4);
        let src_rgb = bus.alloc(6);

        bus.write_long(palette_handle, palette_ptr);
        bus.write_word(palette_ptr, 0x2468);
        bus.write_word(palette_ptr + 2, 0x1111);
        bus.write_word(palette_ptr + 4, 0x2222);
        bus.write_word(palette_ptr + 6, 0x3333);
        bus.write_word(src_rgb, 0x7777);
        bus.write_word(src_rgb + 2, 0x8888);
        bus.write_word(src_rgb + 4, 0x9999);
        d.window_palettes.insert(u32::MAX, (palette_handle, 0));
        d.front_window = window;
        d.current_port = window;

        let before = d.device_clut;
        bus.write_long(TEST_SP, src_rgb);
        bus.write_word(TEST_SP + 4, 0);
        bus.write_long(TEST_SP + 6, window);

        let result = d.dispatch_quickdraw(true, 0x299, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(d.device_clut, before);
    }

    // ==================== ScrollRect ====================

    #[test]
    fn test_scroll_rect() {
        let (mut d, mut cpu, mut bus) = setup();
        // Stack layout: updateRgn(4) + dv(2) + dh(2) + rect_ptr(4) = 12 bytes
        let result = d.dispatch_quickdraw(true, 0x0EF, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
    }

    #[test]
    fn test_scroll_rect_update_rgn_clamps_oversize_vertical_delta() {
        // IM:I I-178: updateRgn is the vacated area. For |dv| >= rect height,
        // the full rect is vacated, so updateRgn bbox should equal r.
        let (mut d, mut cpu, mut bus) = setup();
        let rect_ptr = 0x300200u32;
        let rgn_ptr = 0x300300u32;
        let rgn_handle = 0x300400u32;

        write_rect(&mut bus, rect_ptr, 125, 5, 235, 155);
        make_rgn(&mut bus, rgn_ptr, rgn_handle, 0, 0, 0, 0);

        bus.write_long(TEST_SP, rgn_handle); // updateRgn
        bus.write_word(TEST_SP + 4, 200u16); // dv
        bus.write_word(TEST_SP + 6, 0u16); // dh
        bus.write_long(TEST_SP + 8, rect_ptr); // r

        let result = d.dispatch_quickdraw(true, 0x0EF, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(read_rect(&bus, rgn_ptr + 2), (125, 5, 235, 155));
    }

    // ==================== GDevice ====================

    #[test]
    fn test_get_device_list() {
        let (mut d, mut cpu, mut bus) = setup();
        let result = d.dispatch_quickdraw(true, 0x229, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let handle = bus.read_long(TEST_SP);
        assert_ne!(handle, 0);
    }

    #[test]
    fn getmaxdevice_returns_handle_for_intersecting_globalrect() {
        let (mut d, mut cpu, mut bus) = setup();
        // Inside Macintosh Volume VI 1991 p.21-22: GetMaxDevice returns a
        // handle to the deepest device intersecting globalRect.
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 0, 0, 480, 640);
        bus.write_long(TEST_SP, rect_ptr);
        let result = d.dispatch_quickdraw(true, 0x227, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        let handle = bus.read_long(TEST_SP + 4);
        assert_ne!(handle, 0);
    }

    #[test]
    fn getmaxdevice_single_device_matches_getmaindevice() {
        let (mut d, mut cpu, mut bus) = setup();
        // Inside Macintosh Volume VI 1991 pp.21-21..21-22: in a one-device
        // setup, the deepest intersecting device is the main device.
        let get_main = d.dispatch_quickdraw(true, 0x22A, &mut cpu, &mut bus);
        assert!(get_main.unwrap().is_ok());
        let main_gdh = bus.read_long(TEST_SP);
        assert_ne!(main_gdh, 0);

        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, 10, 10, 110, 210);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, rect_ptr);
        let get_max = d.dispatch_quickdraw(true, 0x227, &mut cpu, &mut bus);
        assert!(get_max.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(TEST_SP + 4), main_gdh);
    }

    #[test]
    fn getmaxdevice_returns_nil_for_nonintersecting_globalrect() {
        let (mut d, mut cpu, mut bus) = setup();
        // Inside Macintosh Volume VI 1991 p.21-22: a rectangle that
        // intersects no screen device yields NIL in the single-screen HLE.
        let rect_ptr = 0x300000u32;
        write_rect(&mut bus, rect_ptr, -200, -200, -100, -100);
        bus.write_long(TEST_SP, rect_ptr);

        let result = d.dispatch_quickdraw(true, 0x227, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(TEST_SP + 4), 0);
    }

    #[test]
    fn test_get_main_device() {
        let (mut d, mut cpu, mut bus) = setup();
        let result = d.dispatch_quickdraw(true, 0x22A, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let handle = bus.read_long(TEST_SP);
        assert_ne!(handle, 0);
    }

    #[test]
    fn get_next_device_returns_gdnextgd_link_handle() {
        let (mut d, mut cpu, mut bus) = setup();
        // Imaging With QuickDraw 1994 pp.5-16 and 5-28: GetNextDevice returns
        // curDevice^^.gdNextGD (or NIL if end-of-list).
        let first_gdh = d.ensure_main_gdevice(&mut bus);
        let first_gd_ptr = bus.read_long(first_gdh);
        let second_gd_ptr = bus.alloc(64);
        let second_gdh = bus.alloc(4);
        bus.write_long(second_gdh, second_gd_ptr);
        bus.write_long(first_gd_ptr + 30, second_gdh); // gdNextGD

        bus.write_long(TEST_SP, first_gdh);
        let result = d.dispatch_quickdraw(true, 0x22B, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(TEST_SP + 4), second_gdh);
    }

    #[test]
    fn get_next_device_returns_nil_when_gdnextgd_is_zero() {
        let (mut d, mut cpu, mut bus) = setup();
        // Imaging With QuickDraw 1994 p.5-28: last-device calls return NIL.
        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd_ptr = bus.read_long(gdh);
        bus.write_long(gd_ptr + 30, 0); // gdNextGD = NIL

        bus.write_long(TEST_SP, gdh);
        let result = d.dispatch_quickdraw(true, 0x22B, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(TEST_SP + 4), 0);
    }

    #[test]
    fn displaydispatch_get_first_screen_device_returns_main_gdevice() {
        let (mut d, mut cpu, mut bus) = setup();
        let main_gdh = d.ensure_main_gdevice(&mut bus);

        cpu.write_reg(Register::D0, 0);
        bus.write_word(TEST_SP, 1); // dmOnlyActiveDisplays
        let result = d.dispatch_quickdraw(true, 0x3EB, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(bus.read_long(TEST_SP + 2), main_gdh);
    }

    #[test]
    fn displaydispatch_get_next_screen_device_returns_nil_for_single_display() {
        let (mut d, mut cpu, mut bus) = setup();
        let main_gdh = d.ensure_main_gdevice(&mut bus);

        cpu.write_reg(Register::D0, 1);
        bus.write_word(TEST_SP, 1); // dmOnlyActiveDisplays
        bus.write_long(TEST_SP + 2, main_gdh);
        let result = d.dispatch_quickdraw(true, 0x3EB, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(bus.read_long(TEST_SP + 6), 0);
    }

    #[test]
    fn displaydispatch_get_display_mode_writes_vdswitchinfo_and_pops_arguments() {
        let (mut d, mut cpu, mut bus) = setup();
        let main_gdh = d.ensure_main_gdevice(&mut bus);
        let switch_info = 0x0031_1000u32;

        cpu.write_reg(Register::D0, 0x043E);
        bus.write_long(TEST_SP, switch_info);
        bus.write_long(TEST_SP + 4, main_gdh);
        bus.write_word(TEST_SP + 8, 0xA55A);
        let result = d.dispatch_quickdraw(true, 0x3EB, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8), 0);
        assert_eq!(bus.read_word(switch_info), 0x0085);
        assert_eq!(bus.read_long(switch_info + 2), 0x0000_0080);
        assert_eq!(bus.read_word(switch_info + 6), 0);
        assert_eq!(bus.read_long(switch_info + 8), d.screen_mode.0);
        assert_eq!(bus.read_long(switch_info + 12), 0);
    }

    #[test]
    fn displaydispatch_set_display_mode_accepts_single_screen_request() {
        let (mut d, mut cpu, mut bus) = setup();
        let main_gdh = d.ensure_main_gdevice(&mut bus);
        let depth_mode = 0x0031_1100u32;
        let gd = bus.read_long(main_gdh);
        let pm_handle = bus.read_long(gd + 22);
        let pm = bus.read_long(pm_handle);
        let screen_bits_ptr = crate::memory::globals::addr::SCREEN_BITS;

        bus.write_long(depth_mode, 0xFFFF_FFFF);
        cpu.write_reg(Register::D0, 0x0A11);
        bus.write_long(TEST_SP, 0); // displayState
        bus.write_long(TEST_SP + 4, 0); // reserved
        bus.write_long(TEST_SP + 8, depth_mode);
        bus.write_long(TEST_SP + 12, 0x0000_0080); // mode
        bus.write_long(TEST_SP + 16, main_gdh);
        bus.write_word(TEST_SP + 20, 0xA55A);
        let result = d.dispatch_quickdraw(true, 0x3EB, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 20);
        assert_eq!(bus.read_word(TEST_SP + 20), 0);
        assert_eq!(bus.read_long(depth_mode), 0x0000_0085);
        assert_eq!(bus.read_long(gd + 42), 0x0000_0080);
        assert_eq!(d.screen_mode.2, 640);
        assert_eq!(d.screen_mode.3, 480);
        assert_eq!(d.screen_mode.4, 8);
        assert_eq!(bus.read_word(screen_bits_ptr + 4), 640);
        assert_eq!(bus.read_word(screen_bits_ptr + 10), 480);
        assert_eq!(bus.read_word(screen_bits_ptr + 12), 640);
        assert_eq!(bus.read_word(pm + 4), 0x8000 | 640);
        assert_eq!(bus.read_word(pm + 10), 480);
        assert_eq!(bus.read_word(pm + 12), 640);
        assert_eq!(bus.read_word(gd + 38), 480);
        assert_eq!(bus.read_word(gd + 40), 640);
    }

    #[test]
    fn displaydispatch_get_display_mode_reports_active_mode_token() {
        let (mut d, mut cpu, mut bus) = setup();
        let main_gdh = d.ensure_main_gdevice(&mut bus);
        let depth_mode = 0x0031_2200u32;
        let switch_info = 0x0031_2300u32;

        bus.write_long(depth_mode, 0xFFFF_FFFF);
        cpu.write_reg(Register::D0, 0x0A11);
        bus.write_long(TEST_SP, 0); // displayState
        bus.write_long(TEST_SP + 4, 0); // reserved
        bus.write_long(TEST_SP + 8, depth_mode);
        bus.write_long(TEST_SP + 12, 0x0000_0080); // mode
        bus.write_long(TEST_SP + 16, main_gdh);
        bus.write_word(TEST_SP + 20, 0xA55A);
        let set_result = d.dispatch_quickdraw(true, 0x3EB, &mut cpu, &mut bus);
        assert!(set_result.unwrap().is_ok());
        assert_eq!(bus.read_word(TEST_SP + 20), 0);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x043E);
        bus.write_long(TEST_SP, switch_info);
        bus.write_long(TEST_SP + 4, main_gdh);
        bus.write_word(TEST_SP + 8, 0xA55A);
        let get_result = d.dispatch_quickdraw(true, 0x3EB, &mut cpu, &mut bus);

        assert!(get_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8), 0);
        assert_eq!(bus.read_word(switch_info), 0x0080);
    }

    #[test]
    fn displaydispatch_mode_zero_rejects_and_preserves_depth_mode() {
        let (mut d, mut cpu, mut bus) = setup();
        let main_gdh = d.ensure_main_gdevice(&mut bus);
        let gd = bus.read_long(main_gdh);
        let display_state_ptr = 0x0031_1200u32;
        let depth_mode = 0x0031_1300u32;
        let screen_bits_ptr = crate::memory::globals::addr::SCREEN_BITS;
        let screen_base_before = bus.read_long(screen_bits_ptr);
        assert_ne!(
            screen_base_before, 0,
            "screenBits.baseAddr should be initialized before DMSetDisplayMode"
        );

        bus.write_long(display_state_ptr, 0xDEAD_BEEFu32);
        bus.write_long(depth_mode, 0xCAFEBABEu32);
        bus.write_byte(screen_base_before, 0x11);

        cpu.write_reg(Register::D0, 0x0206);
        bus.write_long(TEST_SP, display_state_ptr);
        let begin_result = d.dispatch_quickdraw(true, 0x3EB, &mut cpu, &mut bus);
        assert!(begin_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(
            bus.read_word(TEST_SP + 4),
            cpu.read_reg(Register::D0) as u16
        );
        assert_eq!(
            cpu.read_reg(Register::D0) as u16,
            bus.read_long(display_state_ptr) as u16
        );
        assert_eq!(bus.read_long(display_state_ptr), main_gdh);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0A11);
        bus.write_long(TEST_SP, bus.read_long(display_state_ptr));
        bus.write_long(TEST_SP + 4, 0);
        bus.write_long(TEST_SP + 8, depth_mode);
        bus.write_long(TEST_SP + 12, 0);
        bus.write_long(TEST_SP + 16, main_gdh);
        bus.write_word(TEST_SP + 20, 0xA55A);
        let set_result = d.dispatch_quickdraw(true, 0x3EB, &mut cpu, &mut bus);
        assert!(set_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 20);
        assert_eq!(bus.read_word(TEST_SP + 20), (-330i16) as u16);
        assert_eq!(cpu.read_reg(Register::D0), (-330i32) as u32);
        assert_eq!(bus.read_long(depth_mode), 0xCAFEBABEu32);
        assert_eq!(bus.read_long(gd + 42), 0x0000_0085);
        assert_eq!(
            bus.read_long(screen_bits_ptr),
            screen_base_before,
            "DMSetDisplayMode(mode=0) should not change the active screen base"
        );
        assert_eq!(
            bus.read_byte(screen_base_before),
            0x11,
            "DMSetDisplayMode(mode=0) should preserve the poisoned screen byte"
        );

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0207);
        bus.write_long(TEST_SP, bus.read_long(display_state_ptr));
        bus.write_word(TEST_SP + 4, 0xA55C);
        let end_result = d.dispatch_quickdraw(true, 0x3EB, &mut cpu, &mut bus);
        assert!(end_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(TEST_SP + 4), 0);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    #[test]
    fn test_test_device_attribute() {
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        bus.write_word(TEST_SP, 13u16); // attribute (screenDevice bit)
        bus.write_long(TEST_SP + 2, gdh); // gd_handle
        let result = d.dispatch_quickdraw(true, 0x22C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        // bit 13 (screenDevice) should be set
        assert_eq!(bus.read_word(TEST_SP + 6), 0xFFFF);
    }

    #[test]
    fn setdeviceattribute_sets_and_clears_requested_gdflags_bit() {
        // IM:V 1986 p. V-124 + Imaging With QuickDraw 1994 p. 5-22:
        // SetDeviceAttribute sets the requested gdFlags attribute bit
        // to the BOOLEAN value argument.
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd_ptr = bus.read_long(gdh);
        let bit = 1u16 << 14; // noDriver

        // Clear bit.
        bus.write_word(TEST_SP, 0x0000);
        bus.write_word(TEST_SP + 2, 14u16);
        bus.write_long(TEST_SP + 4, gdh);
        let clear_result = d.dispatch_quickdraw(true, 0x22D, &mut cpu, &mut bus);
        assert!(clear_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(gd_ptr + 20) & bit, 0);

        // Set bit.
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 0x0100);
        bus.write_word(TEST_SP + 2, 14u16);
        bus.write_long(TEST_SP + 4, gdh);
        let set_result = d.dispatch_quickdraw(true, 0x22D, &mut cpu, &mut bus);
        assert!(set_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_ne!(bus.read_word(gd_ptr + 20) & bit, 0);
    }

    #[test]
    fn setdeviceattribute_consumes_value_attribute_and_gdhandle_arguments() {
        // IM:V 1986 p. V-124 signature:
        //   PROCEDURE SetDeviceAttribute(gdh: GDHandle; attribute: INTEGER; value: BOOLEAN);
        // Pascal frame consumes 8 bytes total.
        let (mut d, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        bus.write_word(sp_before, 0x0100);
        bus.write_word(sp_before + 2, 13u16);
        bus.write_long(sp_before + 4, 0);

        let result = d.dispatch_quickdraw(true, 0x22D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before + 8);
    }

    #[test]
    fn setdeviceattribute_ignores_out_of_range_attribute_indices() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 128u32);
        bus.write_word(TEST_SP + 4, 7u16);
        let result = d.dispatch_quickdraw(true, 0x22F, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        let gdh = bus.read_long(TEST_SP + 6);
        assert_ne!(gdh, 0);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 0x0100);
        bus.write_word(TEST_SP + 2, 15u16);
        bus.write_long(TEST_SP + 4, gdh);
        let set_result = d.dispatch_quickdraw(true, 0x22D, &mut cpu, &mut bus);
        assert!(set_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 15u16);
        bus.write_long(TEST_SP + 2, gdh);
        let set_check_result = d.dispatch_quickdraw(true, 0x22C, &mut cpu, &mut bus);
        assert!(set_check_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(bus.read_word(TEST_SP + 6), 0xFFFF);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 31u16);
        bus.write_long(TEST_SP + 2, gdh);
        let test_result = d.dispatch_quickdraw(true, 0x22C, &mut cpu, &mut bus);
        assert!(test_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(bus.read_word(TEST_SP + 6), 0);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 0x0100);
        bus.write_word(TEST_SP + 2, 31u16);
        bus.write_long(TEST_SP + 4, gdh);
        let invalid_set_result = d.dispatch_quickdraw(true, 0x22D, &mut cpu, &mut bus);
        assert!(invalid_set_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 15u16);
        bus.write_long(TEST_SP + 2, gdh);
        let screen_active_result = d.dispatch_quickdraw(true, 0x22C, &mut cpu, &mut bus);
        assert!(screen_active_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(bus.read_word(TEST_SP + 6), 0xFFFF);
    }

    #[test]
    fn test_get_gdevice() {
        let (mut d, mut cpu, mut bus) = setup();
        let result = d.dispatch_quickdraw(true, 0x232, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let handle = bus.read_long(TEST_SP);
        assert_ne!(handle, 0);
    }

    #[test]
    fn set_gdevice_sets_current_device_and_updates_thegdevice_lowmem() {
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = 0x400000u32;
        // Imaging With QuickDraw 1994 pp.5-4 and 5-24: SetGDevice changes the
        // current device and TheGDevice keeps the active-device handle.
        bus.write_long(0x0CC8, 0x1111_2222);
        bus.write_long(TEST_SP, gdh);
        let result = d.dispatch_quickdraw(true, 0x231, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(d.current_gdevice, gdh);
        assert_eq!(bus.read_long(0x0CC8), gdh);
    }

    #[test]
    fn set_gdevice_is_observed_by_get_gdevice() {
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = 0x500000u32;
        bus.write_long(TEST_SP, gdh);
        let set_result = d.dispatch_quickdraw(true, 0x231, &mut cpu, &mut bus);
        assert!(set_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        cpu.write_reg(Register::A7, TEST_SP);
        let get_result = d.dispatch_quickdraw(true, 0x232, &mut cpu, &mut bus);
        assert!(get_result.unwrap().is_ok());
        assert_eq!(bus.read_long(TEST_SP), gdh);
    }

    #[test]
    fn disposegdevice_consumes_gdhandle_argument() {
        // Imaging With QuickDraw 1994 p. 5-25 signature:
        //   PROCEDURE DisposeGDevice(gdh: GDHandle);
        // One 4-byte GDHandle argument is consumed.
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        bus.write_long(0x0CC8, gdh);
        bus.write_long(TEST_SP, gdh);

        let result = d.dispatch_quickdraw(true, 0x230, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn newgdevice_returns_non_nil_gdhandle_for_nominal_call() {
        // Imaging With QuickDraw 1994 p. 5-25: NewGDevice returns
        // a GDHandle for a newly allocated graphics device.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0u32); // mode
        bus.write_word(TEST_SP + 4, 0u16); // refNum
        let result = d.dispatch_quickdraw(true, 0x22F, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let handle = bus.read_long(TEST_SP + 6);
        assert_ne!(handle, 0);
    }

    #[test]
    fn newgdevice_returns_handle_distinct_from_main_device_and_clears_gdflags() {
        // IWQD 1994 p. 5-25: NewGDevice allocates a fresh GDevice and, for
        // the default black-and-white mode, leaves gdFlags cleared.
        let (mut d, mut cpu, mut bus) = setup();
        let main_gdh = d.ensure_main_gdevice(&mut bus);

        bus.write_long(TEST_SP, 128u32); // default B/W mode
        bus.write_word(TEST_SP + 4, 7u16); // refNum
        let result = d.dispatch_quickdraw(true, 0x22F, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);

        let new_gdh = bus.read_long(TEST_SP + 6);
        let new_gd = bus.read_long(new_gdh);
        assert_ne!(new_gdh, 0);
        assert_ne!(new_gdh, main_gdh);
        assert_ne!(new_gd, 0);
        assert_eq!(bus.read_word(new_gd + 20), 0);
    }

    #[test]
    fn newgdevice_consumes_mode_and_refnum_arguments_and_writes_result_slot() {
        // Imaging With QuickDraw 1994 p. 5-25: FUNCTION
        // NewGDevice(refNum: INTEGER; mode: LONGINT): GDHandle.
        // Stack therefore consumes 6 bytes of arguments and writes
        // the GDHandle function result in the post-pop result slot.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x0001_0000); // mode
        bus.write_word(TEST_SP + 4, 3u16); // refNum
        let result = d.dispatch_quickdraw(true, 0x22F, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_ne!(bus.read_long(TEST_SP + 6), 0);
    }

    #[test]
    fn has_depth_supported_depth_returns_nonzero_mode_id() {
        // Imaging With QuickDraw 1994 pp.5-33..5-34: HasDepth returns
        // a nonzero mode ID when the requested depth is supported.
        // Stack: SP+0=selector, SP+2=flags, SP+4=whichFlags, SP+6=depth,
        //        SP+8=gd, SP+12=result. Pops 12 bytes.
        for depth in [1, 2, 4, 8] {
            let (mut d, mut cpu, mut bus) = setup();
            bus.write_word(TEST_SP, 0x0A14); // selector
            bus.write_word(TEST_SP + 2, 1); // flags (color)
            bus.write_word(TEST_SP + 4, 0); // whichFlags
            bus.write_word(TEST_SP + 6, depth);
            bus.write_long(TEST_SP + 8, 0); // gd
            let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
            assert_ne!(bus.read_word(TEST_SP + 12), 0);
        }
    }

    #[test]
    fn has_depth_d0_path_32bit_color_returns_nonzero_mode_id() {
        // Marathon 2's preferences path uses the MPW D0-selector form and
        // probes the main device for a 32bpp color mode.
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        cpu.write_reg(Register::D0, 0x0A14);
        bus.write_word(TEST_SP, 1); // flags (color)
        bus.write_word(TEST_SP + 2, 1); // whichFlags: color flag matters
        bus.write_word(TEST_SP + 4, 32); // depth
        bus.write_long(TEST_SP + 6, gdh); // gd
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(bus.read_word(TEST_SP + 10), 32);
    }

    #[test]
    fn has_depth_d0_path_indexed_grayscale_modes_return_nonzero() {
        // Glider PRO probes for indexed grayscale monitor modes before its
        // startup depth dialog. BasiliskII reports 16-gray and 256-gray modes
        // as available on the same color-capable main device.
        for depth in [4, 8] {
            let (mut d, mut cpu, mut bus) = setup();
            let gdh = d.ensure_main_gdevice(&mut bus);
            cpu.write_reg(Register::D0, 0x0A14);
            bus.write_word(TEST_SP, 0); // flags: gdDevType clear (grayscale)
            bus.write_word(TEST_SP + 2, 1); // whichFlags: gdDevType matters
            bus.write_word(TEST_SP + 4, depth);
            bus.write_long(TEST_SP + 6, gdh);
            let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
            assert_eq!(bus.read_word(TEST_SP + 10), depth);
        }
    }

    #[test]
    fn has_depth_stack_selector_indexed_grayscale_modes_return_nonzero() {
        for depth in [4, 8] {
            let (mut d, mut cpu, mut bus) = setup();
            let gdh = d.ensure_main_gdevice(&mut bus);
            bus.write_word(TEST_SP, 0x0A14); // selector
            bus.write_word(TEST_SP + 2, 0); // flags: gdDevType clear
            bus.write_word(TEST_SP + 4, 1); // whichFlags: gdDevType matters
            bus.write_word(TEST_SP + 6, depth);
            bus.write_long(TEST_SP + 8, gdh);
            let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
            assert_eq!(bus.read_word(TEST_SP + 12), depth);
        }
    }

    #[test]
    fn has_depth_accepts_packed_long_d0_selector() {
        // IM:VI Table C-3 lists HasDepth as selector $0A14. Some 68k
        // compilers place the selector byte and argument byte count in a
        // longword D0 value ($000A0014), which must route to the same public
        // D0-selector stack layout as the word selector.
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        cpu.write_reg(Register::D0, 0x000A_0014);
        bus.write_word(TEST_SP, 1); // flags (color)
        bus.write_word(TEST_SP + 2, 1); // whichFlags: color flag matters
        bus.write_word(TEST_SP + 4, 4); // depth
        bus.write_long(TEST_SP + 6, gdh); // gd
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(bus.read_word(TEST_SP + 10), 4);
    }

    #[test]
    fn has_depth_accepts_move_word_selector_with_stale_d0_high_word() {
        // MOVE.W #$0A14,D0 updates only D0's low word on 68k. The public
        // _PaletteDispatch selector must therefore be recognized even when
        // the high word still contains data from a previous computation.
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        cpu.write_reg(Register::D0, 0x0029_0A14);
        bus.write_word(TEST_SP, 1); // flags (color)
        bus.write_word(TEST_SP + 2, 1); // whichFlags: color flag matters
        bus.write_word(TEST_SP + 4, 8); // depth
        bus.write_long(TEST_SP + 6, gdh); // gd
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(bus.read_word(TEST_SP + 10), 8);
    }

    #[test]
    fn has_depth_unsupported_depth_returns_zero() {
        // Imaging With QuickDraw 1994 pp.5-33..5-34: HasDepth returns
        // 0 when the requested depth is unsupported.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 0x0A14); // selector
        bus.write_word(TEST_SP + 2, 1); // flags (color)
        bus.write_word(TEST_SP + 4, 0); // whichFlags
        bus.write_word(TEST_SP + 6, 99); // unsupported depth in HLE
        bus.write_long(TEST_SP + 8, 0); // gd
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(bus.read_word(TEST_SP + 12), 0);
    }

    #[test]
    fn test_set_depth() {
        // Per IM:VI VI-21-12, SetDepth is _PaletteDispatch ($AAA2)
        // selector $0A13. This exercises the selector-word fallback
        // path, which shares the same public stack layout as HasDepth.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 0x0A13); // selector
        bus.write_word(TEST_SP + 2, 1); // flags (color)
        bus.write_word(TEST_SP + 4, 0); // whichFlags
        bus.write_word(TEST_SP + 6, 8); // depth
        bus.write_long(TEST_SP + 8, 0); // gd
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(bus.read_word(TEST_SP + 12), 0); // noErr
    }

    #[test]
    fn set_depth_one_switches_screen_and_gdevice_to_monochrome() {
        // Imaging With QuickDraw 1994 pp. 5-34..5-35: SetDepth sets the
        // requested device depth and gdDevType flag. A B&W request uses
        // flags=0 and depth=1, and should leave screenBits, the active
        // screen_mode, and the main GDevice PixMap in agreement.
        let (mut d, mut cpu, mut bus) = setup();
        let main_gdh = d.ensure_main_gdevice(&mut bus);
        let main_gd = bus.read_long(main_gdh);
        let main_pm_handle = bus.read_long(main_gd + 22);
        let main_pm = bus.read_long(main_pm_handle);
        let screen_bits_ptr = crate::memory::globals::addr::SCREEN_BITS;

        cpu.write_reg(Register::D0, 0x0A13);
        bus.write_word(TEST_SP, 0); // flags: black-and-white
        bus.write_word(TEST_SP + 2, 1); // whichFlags: gdDevType
        bus.write_word(TEST_SP + 4, 1); // depth: 1bpp
        bus.write_long(TEST_SP + 6, main_gdh); // gd
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(bus.read_word(TEST_SP + 10), 0);
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(d.screen_mode.1, 100);
        assert_eq!(d.screen_mode.2, 800);
        assert_eq!(d.screen_mode.3, 600);
        assert_eq!(d.screen_mode.4, 1);
        assert_eq!(bus.read_long(screen_bits_ptr), d.screen_mode.0);
        assert_eq!(bus.read_word(screen_bits_ptr + 4), 100);
        assert_eq!(bus.read_word(screen_bits_ptr + 10), 600);
        assert_eq!(bus.read_word(screen_bits_ptr + 12), 800);
        assert_eq!(bus.read_byte(d.screen_mode.0), 0x00);
        assert_eq!(
            bus.read_byte(d.screen_mode.0 + d.screen_mode.1 * 600 - 1),
            0x00
        );
        assert_eq!(bus.read_word(main_pm + 4), 0x8000 | 100);
        assert_eq!(bus.read_word(main_pm + 32), 1);
        assert_eq!(bus.read_word(main_pm + 36), 1);
        assert_eq!(bus.read_word(main_gd + 20) & 0x0001, 0);
    }

    #[test]
    fn set_depth_accepts_packed_long_d0_selector() {
        // IM:VI Table C-3 lists SetDepth as selector $0A13. Normalize the
        // longword D0 form ($000A0013) before falling back to the legacy
        // stack-selector path, otherwise the flags word at SP would be
        // misread as a Palette Manager selector.
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let before_screen_mode = d.screen_mode;

        cpu.write_reg(Register::D0, 0x000A_0013);
        bus.write_word(TEST_SP, 1); // flags (color)
        bus.write_word(TEST_SP + 2, 1); // whichFlags: gdDevType
        bus.write_word(TEST_SP + 4, 4); // depth: 4bpp color mode
        bus.write_long(TEST_SP + 6, gdh); // gd
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(bus.read_word(TEST_SP + 10), 0);
        assert_eq!(d.screen_mode, before_screen_mode);

        let gd = bus.read_long(gdh);
        assert_eq!(bus.read_long(gd + 42), 4);
        assert_ne!(bus.read_word(gd + 20) & 1, 0);
    }

    #[test]
    fn set_depth_accepts_has_depth_mode_id_without_rewiring_non_8bpp_framebuffer() {
        // HasDepth's result can be passed back to SetDepth. Systemless records
        // the requested display mode on the GDevice but keeps the real
        // framebuffer in its wired shape for non-1/8bpp requests.
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let before_screen_mode = d.screen_mode;
        bus.write_word(TEST_SP, 0x0A13); // selector
        bus.write_word(TEST_SP + 2, 1); // flags (color)
        bus.write_word(TEST_SP + 4, 1); // whichFlags: color flag matters
        bus.write_word(TEST_SP + 6, 32); // HasDepth mode ID for 32bpp
        bus.write_long(TEST_SP + 8, gdh); // gd
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(bus.read_word(TEST_SP + 12), 0); // noErr
        assert_eq!(d.screen_mode, before_screen_mode);

        let gd = bus.read_long(gdh);
        assert_eq!(bus.read_long(gd + 42), 32);
        assert_ne!(bus.read_word(gd + 20) & 1, 0);
    }

    #[test]
    fn set_depth_unsupported_depth_returns_nonzero() {
        // Imaging With QuickDraw 1994 pp. 5-34..5-35: SetDepth returns
        // a nonzero OSErr when it cannot impose the requested depth.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 0x0A13); // selector
        bus.write_word(TEST_SP + 2, 1); // flags (color)
        bus.write_word(TEST_SP + 4, 0); // whichFlags
        bus.write_word(TEST_SP + 6, 99); // unsupported depth in HLE
        bus.write_long(TEST_SP + 8, 0); // gd
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_ne!(bus.read_word(TEST_SP + 12), 0);
    }

    // ==================== Palette ====================

    #[test]
    fn test_palette_dispatch_init() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 0x0000u16); // selector: InitPalettes
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
    }

    #[test]
    fn test_palette_dispatch_init_preserves_current_port() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let saved_port = d.current_port;
        bus.write_word(TEST_SP, 0x0000u16); // selector: InitPalettes
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(d.current_port, saved_port);
    }

    #[test]
    fn palettedispatch_d0_restoredeviceclut_nil_resets_canonical_screen_clut_and_pops_gdhandle() {
        // IM:VI table C-3 routes _PaletteDispatch selector $0002 to
        // RestoreDeviceClut(gdh). NIL means "restore all screens".
        let (mut d, mut cpu, mut bus) = setup();
        d.device_clut[42] = [0x1234, 0x5678, 0x9ABC];
        d.color_manager_clut[42] = [0x1234, 0x5678, 0x9ABC];

        cpu.write_reg(Register::D0, 0x0002);
        bus.write_long(TEST_SP, 0);

        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        let canonical = TrapDispatcher::standard_mac_8bpp_clut();
        assert_eq!(d.device_clut[42], canonical[42]);
        assert_eq!(d.color_manager_clut[42], canonical[42]);
    }

    #[test]
    fn palettedispatch_d0_restoredeviceclut_invalid_gdevice_still_balances_stack() {
        // RestoreDeviceClut must still consume one GDHandle argument
        // even when the handle does not resolve to our modeled device.
        let (mut d, mut cpu, mut bus) = setup();
        d.device_clut[42] = [0xAAAA, 0xBBBB, 0xCCCC];
        d.color_manager_clut[42] = [0xAAAA, 0xBBBB, 0xCCCC];

        cpu.write_reg(Register::D0, 0x0002);
        bus.write_long(TEST_SP, 0x00DE_ADBE);

        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(d.device_clut[42], [0xAAAA, 0xBBBB, 0xCCCC]);
        assert_eq!(d.color_manager_clut[42], [0xAAAA, 0xBBBB, 0xCCCC]);
    }

    fn prepare_animatepalette_negative_span_case(
        d: &mut TrapDispatcher,
        bus: &mut MacMemoryBus,
    ) -> (u32, u32, u32, Option<([u16; 3], i16, i16)>) {
        let window = 0x0020_6000u32;
        let dst_ctab = make_test_ctab_handle(bus, &[[0x2468, 0x1357, 0xBEEF]], 0x1111_2222, 0);
        let palette = d.create_palette_from_ctab(bus, 1, dst_ctab, super::PM_TOLERANT, 0);
        let src_ctab = make_test_ctab_handle(bus, &[[0x7777, 0x1111, 0x4444]], 0x3333_4444, 0);
        d.set_window_palette_association(window, palette, 0);
        d.front_window = window;
        d.current_port = window;

        let before = TrapDispatcher::read_palette_color_info(bus, palette, 0);
        (window, palette, src_ctab, before)
    }

    #[test]
    fn animatepalette_negative_length_leaves_palette_entry_unchanged() {
        let (mut d, mut cpu, mut bus) = setup();
        let (window, palette, src_ctab, before) =
            prepare_animatepalette_negative_span_case(&mut d, &mut bus);

        bus.write_word(TEST_SP, 0xFFFF);
        bus.write_word(TEST_SP + 2, 0);
        bus.write_word(TEST_SP + 4, 0);
        bus.write_long(TEST_SP + 6, src_ctab);
        bus.write_long(TEST_SP + 10, window);
        let result = d.dispatch_quickdraw(true, 0x29A, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 14);
        assert_eq!(
            TrapDispatcher::read_palette_color_info(&bus, palette, 0),
            before
        );
    }

    #[test]
    fn palettedispatch_negative_length_leaves_palette_entry_unchanged() {
        let (mut d, mut cpu, mut bus) = setup();
        let (window, palette, src_ctab, before) =
            prepare_animatepalette_negative_span_case(&mut d, &mut bus);

        bus.write_word(TEST_SP, 0x000A);
        bus.write_word(TEST_SP + 2, 0xFFFF);
        bus.write_word(TEST_SP + 4, 0);
        bus.write_word(TEST_SP + 6, 0);
        bus.write_long(TEST_SP + 8, src_ctab);
        bus.write_long(TEST_SP + 12, window);
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);
        assert_eq!(
            TrapDispatcher::read_palette_color_info(&bus, palette, 0),
            before
        );
    }

    #[test]
    fn palettedispatch_d0_resizepalette_grows_palette_and_zero_fills_new_entries() {
        let (mut d, mut cpu, mut bus) = setup();
        let palette = d.create_palette_from_ctab(&mut bus, 2, 0, super::PM_TOLERANT, 0);

        cpu.write_reg(Register::D0, 0x0003);
        bus.write_word(TEST_SP, 4);
        bus.write_long(TEST_SP + 2, palette);
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(TrapDispatcher::palette_entry_count(&bus, palette), 4);
        assert_eq!(
            TrapDispatcher::read_palette_color_info(&bus, palette, 2),
            Some(([0, 0, 0], super::PM_COURTEOUS, 0))
        );
        assert_eq!(
            TrapDispatcher::read_palette_color_info(&bus, palette, 3),
            Some(([0, 0, 0], super::PM_COURTEOUS, 0))
        );
    }

    #[test]
    fn palettedispatch_d0_setpaletteupdates_consumes_palettehandle_and_updates_arguments() {
        // MPW Universal Headers Palettes.h declares:
        //   SetPaletteUpdates(PaletteHandle p, short updates)
        //   THREEWORDINLINE(0x303C, 0x0616, 0xAAA2).
        let (mut d, mut cpu, mut bus) = setup();
        let palette = d.create_palette_from_ctab(&mut bus, 1, 0, super::PM_TOLERANT, 0);
        let pm_fg_updates = 0xC000u16 as i16;

        cpu.write_reg(Register::D0, 0x0616);
        bus.write_word(TEST_SP, pm_fg_updates as u16);
        bus.write_long(TEST_SP + 2, palette);

        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(
            d.palette_updates.get(&palette).copied(),
            Some(pm_fg_updates)
        );
    }

    #[test]
    fn palettedispatch_d0_getpaletteupdates_returns_recorded_updates() {
        // MPW Universal Headers Palettes.h declares:
        //   GetPaletteUpdates(PaletteHandle p)
        //   THREEWORDINLINE(0x303C, 0x0417, 0xAAA2).
        let (mut d, mut cpu, mut bus) = setup();
        let palette = d.create_palette_from_ctab(&mut bus, 1, 0, super::PM_TOLERANT, 0);
        let pm_fg_updates = 0xC000u16 as i16;

        cpu.write_reg(Register::D0, 0x0616);
        bus.write_word(TEST_SP, pm_fg_updates as u16);
        bus.write_long(TEST_SP + 2, palette);

        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(
            d.palette_updates.get(&palette).copied(),
            Some(pm_fg_updates)
        );

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0417);
        bus.write_long(TEST_SP, palette);
        bus.write_word(TEST_SP + 4, 0xBEEF);

        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(TEST_SP + 4), pm_fg_updates as u16);
    }

    #[test]
    fn palettedispatch_legacy_setpaletteupdates_records_requested_updates_in_internal_map() {
        // The legacy stack-selector path should mirror the D0 fast-path
        // bookkeeping and the public getter should return the recorded mode.
        let (mut d, mut cpu, mut bus) = setup();
        let palette = d.create_palette_from_ctab(&mut bus, 1, 0, super::PM_TOLERANT, 0);
        let pm_no_updates = 0x8000u16 as i16;

        cpu.write_reg(Register::D0, 0xDEAD);
        bus.write_word(TEST_SP, 0x0616);
        bus.write_word(TEST_SP + 2, pm_no_updates as u16);
        bus.write_long(TEST_SP + 4, palette);

        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(
            d.palette_updates.get(&palette).copied(),
            Some(pm_no_updates)
        );

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0xDEAD);
        bus.write_word(TEST_SP, 0x0417);
        bus.write_long(TEST_SP + 2, palette);
        bus.write_word(TEST_SP + 6, 0xBEEF);

        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(bus.read_word(TEST_SP + 6), pm_no_updates as u16);
    }

    #[test]
    fn palettedispatch_legacy_getpaletteupdates_consumes_palettehandle_and_writes_result_slot() {
        // MPW Universal Headers Palettes.h declares:
        //   GetPaletteUpdates(PaletteHandle p)
        //   THREEWORDINLINE(0x303C, 0x0417, 0xAAA2).
        let (mut d, mut cpu, mut bus) = setup();
        let palette = d.create_palette_from_ctab(&mut bus, 1, 0, super::PM_TOLERANT, 0);
        let pm_fg_updates = 0xC000u16 as i16;

        d.record_palette_updates(palette, pm_fg_updates);

        cpu.write_reg(Register::D0, 0xDEAD);
        bus.write_word(TEST_SP, 0x0417);
        bus.write_long(TEST_SP + 2, palette);
        bus.write_word(TEST_SP + 6, 0xDEAD);

        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(bus.read_word(TEST_SP + 6), pm_fg_updates as u16);
    }

    #[test]
    fn palettedispatch_d0_setpaletteupdates_then_getpaletteupdates_returns_recorded_updates() {
        // Public selector pair should preserve the requested update mode
        // across a SetPaletteUpdates / GetPaletteUpdates round-trip.
        let (mut d, mut cpu, mut bus) = setup();
        let palette = d.create_palette_from_ctab(&mut bus, 1, 0, super::PM_TOLERANT, 0);
        let pm_fg_updates = 0xC000u16 as i16;
        let pm_no_updates = 0x8000u16 as i16;

        cpu.write_reg(Register::D0, 0x0616);
        bus.write_word(TEST_SP, pm_fg_updates as u16);
        bus.write_long(TEST_SP + 2, palette);
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0616);
        bus.write_word(TEST_SP, pm_no_updates as u16);
        bus.write_long(TEST_SP + 2, palette);
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0417);
        bus.write_long(TEST_SP, palette);
        bus.write_word(TEST_SP + 4, 0xBEEF);
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(TEST_SP + 4), pm_no_updates as u16);
    }

    #[test]
    fn getentryusage_reads_usage_and_tolerance_fields() {
        let (mut d, mut cpu, mut bus) = setup();
        let palette = d.create_palette_from_ctab(&mut bus, 3, 0, super::PM_TOLERANT, 0);
        let palette_ptr = TrapDispatcher::palette_ptr(&bus, palette);
        let usage_ptr = 0x300000u32;
        let tolerance_ptr = 0x300002u32;

        TrapDispatcher::write_palette_color_info(
            &mut bus,
            palette_ptr,
            1,
            [0x1111, 0x2222, 0x3333],
            super::PM_EXPLICIT,
            77,
        );
        bus.write_word(usage_ptr, 0x7F7F);
        bus.write_word(tolerance_ptr, 0x6E6E);
        bus.write_long(TEST_SP, tolerance_ptr);
        bus.write_long(TEST_SP + 4, usage_ptr);
        bus.write_word(TEST_SP + 8, 1);
        bus.write_long(TEST_SP + 10, palette);

        let result = d.dispatch_quickdraw(true, 0x29D, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 14);
        assert_eq!(bus.read_word(usage_ptr), super::PM_EXPLICIT as u16);
        assert_eq!(bus.read_word(tolerance_ptr), 77);
    }

    #[test]
    fn getentrycolor_nil_palette_leaves_destination_unchanged_and_pops_ten_bytes() {
        let (mut d, mut cpu, mut bus) = setup();
        let rgb_ptr = 0x300100u32;

        bus.write_word(rgb_ptr, 0xDEAD);
        bus.write_word(rgb_ptr + 2, 0xBEEF);
        bus.write_word(rgb_ptr + 4, 0xCAFE);
        bus.write_long(TEST_SP, rgb_ptr);
        bus.write_word(TEST_SP + 4, 0);
        bus.write_long(TEST_SP + 6, 0);

        let result = d.dispatch_quickdraw(true, 0x29B, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(bus.read_word(rgb_ptr), 0xDEAD);
        assert_eq!(bus.read_word(rgb_ptr + 2), 0xBEEF);
        assert_eq!(bus.read_word(rgb_ptr + 4), 0xCAFE);
    }

    #[test]
    fn setentryusage_updates_usage_and_tolerance_and_preserves_rgb() {
        let (mut d, mut cpu, mut bus) = setup();
        let palette = d.create_palette_from_ctab(&mut bus, 3, 0, super::PM_TOLERANT, 0);
        let palette_ptr = TrapDispatcher::palette_ptr(&bus, palette);

        TrapDispatcher::write_palette_color_info(
            &mut bus,
            palette_ptr,
            1,
            [0x1111, 0x2222, 0x3333],
            super::PM_COURTEOUS,
            11,
        );
        bus.write_word(TEST_SP, 222);
        bus.write_word(TEST_SP + 2, super::PM_TOLERANT as u16);
        bus.write_word(TEST_SP + 4, 1);
        bus.write_long(TEST_SP + 6, palette);

        let result = d.dispatch_quickdraw(true, 0x29E, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(
            TrapDispatcher::read_palette_color_info(&bus, palette, 1),
            Some(([0x1111, 0x2222, 0x3333], super::PM_TOLERANT, 222))
        );
    }

    #[test]
    fn setentryusage_minus_one_usage_or_tolerance_writes_both_fields() {
        let (mut d, mut cpu, mut bus) = setup();
        let palette = d.create_palette_from_ctab(&mut bus, 3, 0, super::PM_TOLERANT, 0);
        let palette_ptr = TrapDispatcher::palette_ptr(&bus, palette);

        TrapDispatcher::write_palette_color_info(
            &mut bus,
            palette_ptr,
            1,
            [0xABCD, 0x1357, 0x2468],
            super::PM_EXPLICIT,
            55,
        );

        bus.write_word(TEST_SP, 222);
        bus.write_word(TEST_SP + 2, 0xFFFF);
        bus.write_word(TEST_SP + 4, 1);
        bus.write_long(TEST_SP + 6, palette);
        let result = d.dispatch_quickdraw(true, 0x29E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            TrapDispatcher::read_palette_color_info(&bus, palette, 1),
            Some(([0xABCD, 0x1357, 0x2468], -1, 222))
        );

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 0xFFFF);
        bus.write_word(TEST_SP + 2, super::PM_TOLERANT as u16);
        bus.write_word(TEST_SP + 4, 1);
        bus.write_long(TEST_SP + 6, palette);
        let result = d.dispatch_quickdraw(true, 0x29E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(
            TrapDispatcher::read_palette_color_info(&bus, palette, 1),
            Some(([0xABCD, 0x1357, 0x2468], super::PM_TOLERANT, -1))
        );
    }

    #[test]
    fn setentrycolor_updates_rgb_and_preserves_usage_and_tolerance() {
        let (mut d, mut cpu, mut bus) = setup();
        let palette = d.create_palette_from_ctab(&mut bus, 3, 0, super::PM_TOLERANT, 0);
        let palette_ptr = TrapDispatcher::palette_ptr(&bus, palette);

        TrapDispatcher::write_palette_color_info(
            &mut bus,
            palette_ptr,
            1,
            [0x1111, 0x2222, 0x3333],
            super::PM_EXPLICIT,
            77,
        );

        let rgb_ptr = 0x300500u32;
        bus.write_word(rgb_ptr, 0xAAAA);
        bus.write_word(rgb_ptr + 2, 0xBBBB);
        bus.write_word(rgb_ptr + 4, 0xCCCC);
        bus.write_long(TEST_SP, rgb_ptr);
        bus.write_word(TEST_SP + 4, 1);
        bus.write_long(TEST_SP + 6, palette);

        let result = d.dispatch_quickdraw(true, 0x29C, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(
            TrapDispatcher::read_palette_color_info(&bus, palette, 1),
            Some(([0xAAAA, 0xBBBB, 0xCCCC], super::PM_EXPLICIT, 77))
        );
    }

    #[test]
    fn palettedispatch_d0_resizepalette_shrinks_palette_and_regrowth_reinitializes_tail() {
        let (mut d, mut cpu, mut bus) = setup();
        let palette = d.create_palette_from_ctab(&mut bus, 4, 0, super::PM_TOLERANT, 0);
        let palette_ptr = TrapDispatcher::palette_ptr(&bus, palette);
        TrapDispatcher::write_palette_color_info(
            &mut bus,
            palette_ptr,
            3,
            [0xFFFF, 0x0000, 0x8888],
            super::PM_TOLERANT,
            77,
        );

        cpu.write_reg(Register::D0, 0x0003);
        bus.write_word(TEST_SP, 2);
        bus.write_long(TEST_SP + 2, palette);
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(TrapDispatcher::palette_entry_count(&bus, palette), 2);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0003);
        bus.write_word(TEST_SP, 4);
        bus.write_long(TEST_SP + 2, palette);
        let result = d.dispatch_quickdraw(true, 0x2A2, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(TrapDispatcher::palette_entry_count(&bus, palette), 4);
        assert_eq!(
            TrapDispatcher::read_palette_color_info(&bus, palette, 3),
            Some(([0, 0, 0], super::PM_COURTEOUS, 0))
        );
    }

    #[test]
    fn copypalette_pops_fourteen_bytes() {
        let (mut d, mut cpu, mut bus) = setup();
        let src = d.create_palette_from_ctab(&mut bus, 2, 0, super::PM_TOLERANT, 0);
        let dst = d.create_palette_from_ctab(&mut bus, 2, 0, super::PM_TOLERANT, 0);

        bus.write_word(TEST_SP, 1);
        bus.write_word(TEST_SP + 2, 0);
        bus.write_word(TEST_SP + 4, 0);
        bus.write_long(TEST_SP + 6, dst);
        bus.write_long(TEST_SP + 10, src);
        let result = d.dispatch_quickdraw(true, 0x2A1, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 14);
    }

    #[test]
    fn copypalette_resizes_destination_when_copy_extends_past_tail() {
        let (mut d, mut cpu, mut bus) = setup();
        let src = d.create_palette_from_ctab(&mut bus, 5, 0, super::PM_TOLERANT, 0);
        let dst = d.create_palette_from_ctab(&mut bus, 3, 0, super::PM_TOLERANT, 0);

        bus.write_word(TEST_SP, 2);
        bus.write_word(TEST_SP + 2, 3);
        bus.write_word(TEST_SP + 4, 2);
        bus.write_long(TEST_SP + 6, dst);
        bus.write_long(TEST_SP + 10, src);
        let result = d.dispatch_quickdraw(true, 0x2A1, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(TrapDispatcher::palette_entry_count(&bus, dst), 5);
    }

    #[test]
    fn copypalette_copies_requested_range_into_resized_tail() {
        let (mut d, mut cpu, mut bus) = setup();
        let src = d.create_palette_from_ctab(&mut bus, 5, 0, super::PM_TOLERANT, 0);
        let dst = d.create_palette_from_ctab(&mut bus, 3, 0, super::PM_TOLERANT, 0);
        let src_ptr = TrapDispatcher::palette_ptr(&bus, src);

        TrapDispatcher::write_palette_color_info(
            &mut bus,
            src_ptr,
            2,
            [0x1234, 0xABCD, 0x0F0F],
            super::PM_EXPLICIT,
            77,
        );
        TrapDispatcher::write_palette_color_info(
            &mut bus,
            src_ptr,
            3,
            [0xFEDC, 0x2222, 0x9999],
            super::PM_EXPLICIT,
            88,
        );

        bus.write_word(TEST_SP, 2);
        bus.write_word(TEST_SP + 2, 3);
        bus.write_word(TEST_SP + 4, 2);
        bus.write_long(TEST_SP + 6, dst);
        bus.write_long(TEST_SP + 10, src);
        let result = d.dispatch_quickdraw(true, 0x2A1, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(
            TrapDispatcher::read_palette_color_info(&bus, dst, 3),
            Some(([0x1234, 0xABCD, 0x0F0F], super::PM_EXPLICIT, 77))
        );
        assert_eq!(
            TrapDispatcher::read_palette_color_info(&bus, dst, 4),
            Some(([0xFEDC, 0x2222, 0x9999], super::PM_EXPLICIT, 88))
        );
    }

    #[test]
    fn copypalette_nil_source_does_not_mutate_destination() {
        let (mut d, mut cpu, mut bus) = setup();
        let dst = d.create_palette_from_ctab(&mut bus, 3, 0, super::PM_TOLERANT, 0);
        let dst_ptr = TrapDispatcher::palette_ptr(&bus, dst);

        TrapDispatcher::write_palette_color_info(
            &mut bus,
            dst_ptr,
            1,
            [0x1111, 0x2222, 0x3333],
            super::PM_EXPLICIT,
            55,
        );

        bus.write_word(TEST_SP, 2);
        bus.write_word(TEST_SP + 2, 1);
        bus.write_word(TEST_SP + 4, 0);
        bus.write_long(TEST_SP + 6, dst);
        bus.write_long(TEST_SP + 10, 0);
        let result = d.dispatch_quickdraw(true, 0x2A1, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(
            TrapDispatcher::read_palette_color_info(&bus, dst, 1),
            Some(([0x1111, 0x2222, 0x3333], super::PM_EXPLICIT, 55))
        );
    }

    #[test]
    fn ctab2palette_resizes_destination_palette_to_match_source_color_table() {
        let (mut d, mut cpu, mut bus) = setup();
        let src_ctab = make_test_ctab_handle(
            &mut bus,
            &[
                [0x1111, 0x0000, 0x0000],
                [0x2222, 0x0000, 0x0000],
                [0x3333, 0x0000, 0x0000],
                [0x4444, 0x0000, 0x0000],
                [0xABCD, 0x1357, 0x2468],
            ],
            0x1020_3040,
            0,
        );
        let dst_palette = d.create_palette_from_ctab(&mut bus, 2, 0, super::PM_TOLERANT, 0);
        let old_size = bus
            .get_alloc_size(bus.read_long(dst_palette))
            .expect("palette allocation should be tracked");

        bus.write_word(TEST_SP, 77u16);
        bus.write_word(TEST_SP + 2, super::PM_EXPLICIT as u16);
        bus.write_long(TEST_SP + 4, dst_palette);
        bus.write_long(TEST_SP + 8, src_ctab);
        let result = d.dispatch_quickdraw(true, 0x29F, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(TrapDispatcher::palette_entry_count(&bus, dst_palette), 5);
        assert_eq!(
            bus.get_alloc_size(bus.read_long(dst_palette)),
            Some(super::PALETTE_HEADER_SIZE + 5 * super::PALETTE_COLOR_INFO_SIZE)
        );
        assert!(
            bus.get_alloc_size(bus.read_long(dst_palette))
                .expect("palette allocation should stay tracked")
                > old_size
        );
        assert_eq!(
            TrapDispatcher::read_palette_color_info(&bus, dst_palette, 4),
            Some(([0xABCD, 0x1357, 0x2468], super::PM_EXPLICIT, 77))
        );
    }

    #[test]
    fn ctab2palette_shrinks_destination_palette_to_match_source_color_table() {
        let (mut d, mut cpu, mut bus) = setup();
        let src_ctab = make_test_ctab_handle(
            &mut bus,
            &[[0x1234, 0x5678, 0x9ABC], [0xAAAA, 0xBBBB, 0xCCCC]],
            1,
            0,
        );
        let dst_palette = d.create_palette_from_ctab(&mut bus, 5, 0, super::PM_TOLERANT, 0);

        bus.write_word(TEST_SP, 9u16);
        bus.write_word(TEST_SP + 2, super::PM_COURTEOUS as u16);
        bus.write_long(TEST_SP + 4, dst_palette);
        bus.write_long(TEST_SP + 8, src_ctab);
        let result = d.dispatch_quickdraw(true, 0x29F, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(TrapDispatcher::palette_entry_count(&bus, dst_palette), 2);
        assert_eq!(
            bus.get_alloc_size(bus.read_long(dst_palette)),
            Some(super::PALETTE_HEADER_SIZE + 2 * super::PALETTE_COLOR_INFO_SIZE)
        );
        assert_eq!(
            TrapDispatcher::read_palette_color_info(&bus, dst_palette, 1),
            Some(([0xAAAA, 0xBBBB, 0xCCCC], super::PM_COURTEOUS, 9))
        );
    }

    #[test]
    fn palette2ctab_resizes_destination_color_table_to_match_source_palette() {
        let (mut d, mut cpu, mut bus) = setup();
        let src_palette = d.create_palette_from_ctab(&mut bus, 5, 0, super::PM_TOLERANT, 0);
        let src_palette_ptr = TrapDispatcher::palette_ptr(&bus, src_palette);
        TrapDispatcher::write_palette_color_info(
            &mut bus,
            src_palette_ptr,
            4,
            [0xBEEF, 0x1357, 0x2468],
            super::PM_EXPLICIT,
            44,
        );
        let dst_ctab = make_test_ctab_handle(
            &mut bus,
            &[[0x0000, 0x0000, 0x0000], [0x0101, 0x0202, 0x0303]],
            7,
            0x8000,
        );
        let old_size = bus
            .get_alloc_size(bus.read_long(dst_ctab))
            .expect("ctab allocation should be tracked");

        bus.write_long(TEST_SP, dst_ctab);
        bus.write_long(TEST_SP + 4, src_palette);
        let result = d.dispatch_quickdraw(true, 0x2A0, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(TrapDispatcher::color_table_entry_count(&bus, dst_ctab), 5);
        assert_eq!(bus.get_alloc_size(bus.read_long(dst_ctab)), Some(8 + 5 * 8));
        assert!(
            bus.get_alloc_size(bus.read_long(dst_ctab))
                .expect("ctab allocation should stay tracked")
                > old_size
        );
        assert_eq!(
            read_test_ctab_rgb(&bus, dst_ctab, 4),
            [0xBEEF, 0x1357, 0x2468]
        );
    }

    #[test]
    fn palette2ctab_shrinks_destination_color_table_to_match_source_palette() {
        let (mut d, mut cpu, mut bus) = setup();
        let src_palette = d.create_palette_from_ctab(&mut bus, 2, 0, super::PM_TOLERANT, 0);
        let src_palette_ptr = TrapDispatcher::palette_ptr(&bus, src_palette);
        TrapDispatcher::write_palette_color_info(
            &mut bus,
            src_palette_ptr,
            1,
            [0xAAAA, 0xBBBB, 0xCCCC],
            super::PM_EXPLICIT,
            11,
        );
        let dst_ctab = make_test_ctab_handle(
            &mut bus,
            &[
                [0x0000, 0x0000, 0x0000],
                [0x1111, 0x1111, 0x1111],
                [0x2222, 0x2222, 0x2222],
                [0x3333, 0x3333, 0x3333],
                [0x4444, 0x4444, 0x4444],
            ],
            7,
            0,
        );

        bus.write_long(TEST_SP, dst_ctab);
        bus.write_long(TEST_SP + 4, src_palette);
        let result = d.dispatch_quickdraw(true, 0x2A0, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(TrapDispatcher::color_table_entry_count(&bus, dst_ctab), 2);
        assert_eq!(bus.get_alloc_size(bus.read_long(dst_ctab)), Some(8 + 2 * 8));
        assert_eq!(
            read_test_ctab_rgb(&bus, dst_ctab, 1),
            [0xAAAA, 0xBBBB, 0xCCCC]
        );
    }

    // ==================== Color Queries ====================

    #[test]
    fn test_get_fore_color() {
        let (mut d, mut cpu, mut bus) = setup();
        d.fg_color = (0x1234, 0x5678, 0x9ABC);
        let color_ptr = 0x300000u32;
        bus.write_long(TEST_SP, color_ptr);
        let result = d.dispatch_quickdraw(true, 0x219, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(color_ptr), 0x1234);
        assert_eq!(bus.read_word(color_ptr + 2), 0x5678);
        assert_eq!(bus.read_word(color_ptr + 4), 0x9ABC);
    }

    #[test]
    fn test_get_back_color() {
        let (mut d, mut cpu, mut bus) = setup();
        d.bg_color = (0xDEAD, 0xBEEF, 0xCAFE);
        let color_ptr = 0x300000u32;
        bus.write_long(TEST_SP, color_ptr);
        let result = d.dispatch_quickdraw(true, 0x21A, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(color_ptr), 0xDEAD);
        assert_eq!(bus.read_word(color_ptr + 2), 0xBEEF);
        assert_eq!(bus.read_word(color_ptr + 4), 0xCAFE);
    }

    #[test]
    fn test_get_ctable() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 0u16); // ct_id
        let result = d.dispatch_quickdraw(true, 0x218, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        let handle = bus.read_long(TEST_SP + 2);
        assert_ne!(handle, 0);
    }

    #[test]
    fn test_dispose_ctable() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x300000);
        let result = d.dispatch_quickdraw(true, 0x224, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn makeitable_consumes_colortab_inversetab_and_res_arguments() {
        // Inside Macintosh Volume V (1986), p. V-142 signature:
        //   PROCEDURE MakeITable(colorTab: CTabHandle;
        //                        inverseTab: ITabHandle;
        //                        res: INTEGER);
        // Two 4-byte handles plus one 2-byte INTEGER => 10-byte pop.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x0030_0000); // colorTab
        bus.write_long(TEST_SP + 4, 0x0040_0000); // inverseTab
        bus.write_word(TEST_SP + 8, 4u16); // res
        let result = d.dispatch_quickdraw(true, 0x239, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
    }

    #[test]
    fn index2color_returns_rgb_for_requested_index_from_current_gdevice_clut() {
        let (mut d, mut cpu, mut bus) = setup();
        // Inside Macintosh Volume V 1986 p.V-140: Index2Color returns the RGB
        // for an index from the current gDevice color table.
        let gdh = d.ensure_main_gdevice(&mut bus);
        bus.write_long(0x0CC8, gdh); // TheGDevice

        let table_ptr = bus.alloc(8);
        bus.write_word(table_ptr, 17);
        bus.write_word(table_ptr + 2, 0x1234);
        bus.write_word(table_ptr + 4, 0x5678);
        bus.write_word(table_ptr + 6, 0x9ABC);
        d.apply_set_entries_with_gdevice(&mut bus, table_ptr, -1, 0);

        let rgb_ptr = 0x301000u32;
        bus.write_word(rgb_ptr, 0xFFFF);
        bus.write_word(rgb_ptr + 2, 0xFFFF);
        bus.write_word(rgb_ptr + 4, 0xFFFF);
        bus.write_long(TEST_SP, rgb_ptr);
        bus.write_long(TEST_SP + 4, 17);

        let result = d.dispatch_quickdraw(true, 0x234, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(rgb_ptr), 0x1234);
        assert_eq!(bus.read_word(rgb_ptr + 2), 0x5678);
        assert_eq!(bus.read_word(rgb_ptr + 4), 0x9ABC);
    }

    #[test]
    fn index2color_consumes_index_and_rgb_arguments() {
        let (mut d, mut cpu, mut bus) = setup();
        // Inside Macintosh Volume V 1986 p.V-140 signature:
        // Index2Color(index: LONGINT; VAR rgb: RGBColor).
        let gdh = d.ensure_main_gdevice(&mut bus);
        bus.write_long(0x0CC8, gdh); // TheGDevice
        let rgb_ptr = 0x302000u32;
        bus.write_long(TEST_SP, rgb_ptr);
        bus.write_long(TEST_SP + 4, 0);

        let result = d.dispatch_quickdraw(true, 0x234, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
    }

    // ==================== GWorld ====================

    #[test]
    fn test_new_gworld() {
        let (mut d, mut cpu, mut bus) = setup();
        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 100, 100);
        bus.write_long(TEST_SP, 0u32); // flags
        bus.write_long(TEST_SP + 4, 0u32); // gdevice
        bus.write_long(TEST_SP + 8, 0u32); // ctab
        bus.write_long(TEST_SP + 12, bounds_ptr); // bounds
        bus.write_word(TEST_SP + 16, 8u16); // depth
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr); // gworldPtr
        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 22);
        assert_eq!(bus.read_word(TEST_SP + 22), 0); // noErr
        let gworld = bus.read_long(gworld_ptr_ptr);
        assert_ne!(gworld, 0);
    }

    #[test]
    fn test_new_gworld_depth_zero_creates_local_offscreen_world() {
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd = bus.read_long(gdh);
        let gd_pmh = bus.read_long(gd + 22);
        let gd_pm = bus.read_long(gd_pmh);
        let gd_ctab = bus.read_long(gd_pm + 42);

        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 25, 40, 125, 140);
        bus.write_long(TEST_SP, 0u32); // flags
        bus.write_long(TEST_SP + 4, 0u32); // gdevice
        bus.write_long(TEST_SP + 8, 0u32); // ctab
        bus.write_long(TEST_SP + 12, bounds_ptr); // bounds
        bus.write_word(TEST_SP + 16, 0u16); // depth=0 uses intersecting device
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr); // gworldPtr

        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let gw_pmh = bus.read_long(gworld + 2);
        let gw_pm = bus.read_long(gw_pmh);
        let gw_ctab = bus.read_long(gw_pm + 42);
        let gw_gdh = d.gworld_devices.get(&gworld).copied().unwrap_or(0);
        let gw_gd = bus.read_long(gw_gdh);

        assert_ne!(gw_ctab, 0);
        assert_ne!(gw_ctab, gd_ctab);
        assert_ne!(gw_gdh, 0);
        assert_ne!(gw_gdh, gdh);
        assert_eq!(bus.read_word(gw_pm + 6), 0);
        assert_eq!(bus.read_word(gw_pm + 8), 0);
        assert_eq!(bus.read_word(gw_pm + 10), 100);
        assert_eq!(bus.read_word(gw_pm + 12), 100);
        assert_eq!(bus.read_word(gw_gd + 34), 0);
        assert_eq!(bus.read_word(gw_gd + 36), 0);
        assert_eq!(bus.read_word(gw_gd + 38), 100);
        assert_eq!(bus.read_word(gw_gd + 40), 100);
    }

    #[test]
    fn test_new_gworld_depth_zero_clones_intersecting_device_palette() {
        let (mut d, mut cpu, mut bus) = setup();
        d.ensure_main_gdevice(&mut bus);

        let table_ptr = bus.alloc(8);
        bus.write_word(table_ptr, 17);
        bus.write_word(table_ptr + 2, 0x1111);
        bus.write_word(table_ptr + 4, 0x2222);
        bus.write_word(table_ptr + 6, 0x3333);
        d.apply_set_entries_with_gdevice(&mut bus, table_ptr, -1, 0);

        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 25, 40, 125, 140);
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 0u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);

        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let gw_pmh = bus.read_long(gworld + 2);
        let gw_pm = bus.read_long(gw_pmh);
        let gw_ctab_handle = bus.read_long(gw_pm + 42);
        let gw_ctab_ptr = bus.read_long(gw_ctab_handle);
        let entry = gw_ctab_ptr + 8 + 17 * 8;
        assert_eq!(bus.read_word(entry + 2), 0x1111);
        assert_eq!(bus.read_word(entry + 4), 0x2222);
        assert_eq!(bus.read_word(entry + 6), 0x3333);
    }

    #[test]
    fn test_new_screen_buffer_returns_live_pixmap_and_state_helpers() {
        let (mut d, mut cpu, mut bus) = setup();
        let main_gdh = d.ensure_main_gdevice(&mut bus);

        let rect_ptr_1 = 0x330000u32;
        let gdh_out_1 = 0x330100u32;
        let pm_out_1 = 0x330104u32;
        write_rect(&mut bus, rect_ptr_1, 10, 20, 110, 220);
        bus.write_long(TEST_SP, pm_out_1);
        bus.write_long(TEST_SP + 4, gdh_out_1);
        bus.write_word(TEST_SP + 8, 0);
        bus.write_long(TEST_SP + 10, rect_ptr_1);
        cpu.write_reg(Register::D0, 0x000E_0010);

        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 14);
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(bus.read_long(gdh_out_1), main_gdh);
        let pmh_1 = bus.read_long(pm_out_1);
        assert_ne!(pmh_1, 0);

        cpu.write_reg(Register::D0, 0x000F);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, pmh_1);
        bus.write_long(TEST_SP + 4, 0xA5A5_A5A5);
        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        let base_1 = bus.read_long(TEST_SP + 4);
        assert_ne!(base_1, 0);

        cpu.write_reg(Register::D0, 0x000D);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, pmh_1);
        bus.write_word(TEST_SP + 4, 0xFFFF);
        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(TEST_SP + 4), 0);
    }

    #[test]
    fn test_new_gworld_depth_zero_during_seeded_picture_window_clones_seeded_palette() {
        let (mut d, mut cpu, mut bus) = setup();
        d.ensure_main_gdevice(&mut bus);

        let mut seeded = TrapDispatcher::standard_mac_8bpp_clut();
        seeded[16] = [0x1111, 0x2222, 0x3333];
        seeded[42] = [0x4444, 0x5555, 0x6666];
        seeded[128] = [0x7777, 0x8888, 0x9999];
        d.seeded_picture_palette = seeded;
        d.seeded_picture_palette_until_tick = 90;
        d.tick_count = 69;

        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 25, 40, 125, 140);
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 0u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);

        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let gw_pmh = bus.read_long(gworld + 2);
        let gw_pm = bus.read_long(gw_pmh);
        let gw_ctab_handle = bus.read_long(gw_pm + 42);
        let gw_ctab_ptr = bus.read_long(gw_ctab_handle);

        let entry16 = gw_ctab_ptr + 8 + 16 * 8;
        let entry42 = gw_ctab_ptr + 8 + 42 * 8;
        let entry128 = gw_ctab_ptr + 8 + 128 * 8;
        assert_eq!(bus.read_word(entry16 + 2), 0x1111);
        assert_eq!(bus.read_word(entry16 + 4), 0x2222);
        assert_eq!(bus.read_word(entry16 + 6), 0x3333);
        assert_eq!(bus.read_word(entry42 + 2), 0x4444);
        assert_eq!(bus.read_word(entry42 + 4), 0x5555);
        assert_eq!(bus.read_word(entry42 + 6), 0x6666);
        assert_eq!(bus.read_word(entry128 + 2), 0x7777);
        assert_eq!(bus.read_word(entry128 + 4), 0x8888);
        assert_eq!(bus.read_word(entry128 + 6), 0x9999);
    }

    #[test]
    fn test_update_gworld_depth_zero_syncs_offscreen_ctab_from_device() {
        let (mut d, mut cpu, mut bus) = setup();
        d.ensure_main_gdevice(&mut bus);

        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 10, 20, 110, 120);
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 0u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let gw_pmh = bus.read_long(gworld + 2);
        let gw_pm = bus.read_long(gw_pmh);
        let gw_ctab_handle = bus.read_long(gw_pm + 42);
        let gw_ctab_ptr = bus.read_long(gw_ctab_handle);
        let old_seed = bus.read_long(gw_ctab_ptr);

        let table_ptr = bus.alloc(8);
        bus.write_word(table_ptr, 17);
        bus.write_word(table_ptr + 2, 0x1111);
        bus.write_word(table_ptr + 4, 0x2222);
        bus.write_word(table_ptr + 6, 0x3333);
        d.apply_set_entries_with_gdevice(&mut bus, table_ptr, -1, 0);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0003);
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 0u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        let new_seed = bus.read_long(gw_ctab_ptr);
        let entry = gw_ctab_ptr + 8 + 17 * 8;
        assert_ne!(new_seed, old_seed);
        assert_eq!(bus.read_word(entry + 2), 0x1111);
        assert_eq!(bus.read_word(entry + 4), 0x2222);
        assert_eq!(bus.read_word(entry + 6), 0x3333);
    }

    #[test]
    fn test_allocate_color_table_handle_clones_live_screen_ctab() {
        let (mut d, _cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let screen_ctab = TrapDispatcher::gdevice_ctab_handle(&bus, gdh);
        let screen_ctab_ptr = bus.read_long(screen_ctab);
        let screen_entry = screen_ctab_ptr + 8 + 17 * 8;

        d.color_manager_clut[17] = [0xAAAA, 0xBBBB, 0xCCCC];
        bus.write_word(screen_entry + 2, 0x1111);
        bus.write_word(screen_entry + 4, 0x2222);
        bus.write_word(screen_entry + 6, 0x3333);

        let cloned = d.allocate_color_table_handle(&mut bus, 8, screen_ctab, 0x8000);
        let cloned_ptr = bus.read_long(cloned);
        let cloned_entry = cloned_ptr + 8 + 17 * 8;
        assert_eq!(bus.read_word(cloned_entry + 2), 0x1111);
        assert_eq!(bus.read_word(cloned_entry + 4), 0x2222);
        assert_eq!(bus.read_word(cloned_entry + 6), 0x3333);
    }

    #[test]
    fn test_sync_canonical_offscreen_ctabs_to_clut_updates_depth_zero_gworlds() {
        let (mut d, mut cpu, mut bus) = setup();
        d.ensure_main_gdevice(&mut bus);

        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 64, 64);
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 0u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let gw_pmh = bus.read_long(gworld + 2);
        let gw_pm = bus.read_long(gw_pmh);
        let gw_ctab_handle = bus.read_long(gw_pm + 42);
        let gw_ctab_ptr = bus.read_long(gw_ctab_handle);

        let mut target_clut = TrapDispatcher::standard_mac_8bpp_clut();
        target_clut[16] = [0x1111, 0x2222, 0x3333];
        target_clut[42] = [0x4444, 0x5555, 0x6666];
        target_clut[128] = [0x7777, 0x8888, 0x9999];

        d.sync_canonical_offscreen_ctabs_to_clut(&mut bus, &target_clut);

        let entry16 = gw_ctab_ptr + 8 + 16 * 8;
        let entry42 = gw_ctab_ptr + 8 + 42 * 8;
        let entry128 = gw_ctab_ptr + 8 + 128 * 8;
        assert_eq!(bus.read_word(entry16 + 2), 0x1111);
        assert_eq!(bus.read_word(entry16 + 4), 0x2222);
        assert_eq!(bus.read_word(entry16 + 6), 0x3333);
        assert_eq!(bus.read_word(entry42 + 2), 0x4444);
        assert_eq!(bus.read_word(entry42 + 4), 0x5555);
        assert_eq!(bus.read_word(entry42 + 6), 0x6666);
        assert_eq!(bus.read_word(entry128 + 2), 0x7777);
        assert_eq!(bus.read_word(entry128 + 4), 0x8888);
        assert_eq!(bus.read_word(entry128 + 6), 0x9999);
    }

    #[test]
    fn test_sync_canonical_offscreen_ctabs_to_clut_preserves_custom_gworlds() {
        let (mut d, mut cpu, mut bus) = setup();
        d.ensure_main_gdevice(&mut bus);

        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 64, 64);
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 0u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let gw_pmh = bus.read_long(gworld + 2);
        let gw_pm = bus.read_long(gw_pmh);
        let gw_ctab_handle = bus.read_long(gw_pm + 42);
        let gw_ctab_ptr = bus.read_long(gw_ctab_handle);
        let custom_entry = gw_ctab_ptr + 8 + 42 * 8;
        bus.write_word(custom_entry + 2, 0xAAAA);
        bus.write_word(custom_entry + 4, 0xBBBB);
        bus.write_word(custom_entry + 6, 0xCCCC);

        let mut target_clut = TrapDispatcher::standard_mac_8bpp_clut();
        target_clut[42] = [0x1111, 0x2222, 0x3333];

        d.sync_canonical_offscreen_ctabs_to_clut(&mut bus, &target_clut);

        assert_eq!(bus.read_word(custom_entry + 2), 0xAAAA);
        assert_eq!(bus.read_word(custom_entry + 4), 0xBBBB);
        assert_eq!(bus.read_word(custom_entry + 6), 0xCCCC);
    }

    #[test]
    fn test_install_application_clut_updates_main_gdevice_ctab() {
        let (mut d, _cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let ctab_handle = TrapDispatcher::gdevice_ctab_handle(&bus, gdh);
        let ctab_ptr = bus.read_long(ctab_handle);

        let mut target_clut = TrapDispatcher::standard_mac_8bpp_clut();
        target_clut[16] = [0x1111, 0x2222, 0x3333];
        target_clut[42] = [0x4444, 0x5555, 0x6666];
        target_clut[128] = [0x7777, 0x8888, 0x9999];

        d.install_application_clut(&mut bus, target_clut);

        assert_eq!(d.device_clut[16], [0x1111, 0x2222, 0x3333]);
        assert_eq!(d.color_manager_clut[42], [0x4444, 0x5555, 0x6666]);

        let entry16 = ctab_ptr + 8 + 16 * 8;
        let entry42 = ctab_ptr + 8 + 42 * 8;
        let entry128 = ctab_ptr + 8 + 128 * 8;
        assert_eq!(bus.read_word(entry16 + 2), 0x1111);
        assert_eq!(bus.read_word(entry16 + 4), 0x2222);
        assert_eq!(bus.read_word(entry16 + 6), 0x3333);
        assert_eq!(bus.read_word(entry42 + 2), 0x4444);
        assert_eq!(bus.read_word(entry42 + 4), 0x5555);
        assert_eq!(bus.read_word(entry42 + 6), 0x6666);
        assert_eq!(bus.read_word(entry128 + 2), 0x7777);
        assert_eq!(bus.read_word(entry128 + 4), 0x8888);
        assert_eq!(bus.read_word(entry128 + 6), 0x9999);
    }

    #[test]
    fn test_install_application_clut_syncs_existing_canonical_offscreen_ctabs() {
        let (mut d, mut cpu, mut bus) = setup();
        d.ensure_main_gdevice(&mut bus);

        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 64, 64);
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 0u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let gw_pmh = bus.read_long(gworld + 2);
        let gw_pm = bus.read_long(gw_pmh);
        let gw_ctab_handle = bus.read_long(gw_pm + 42);
        let gw_ctab_ptr = bus.read_long(gw_ctab_handle);

        let mut target_clut = TrapDispatcher::standard_mac_8bpp_clut();
        target_clut[16] = [0xAAAA, 0x1111, 0x2222];
        target_clut[42] = [0xBBBB, 0x3333, 0x4444];
        target_clut[128] = [0xCCCC, 0x5555, 0x6666];

        d.install_application_clut(&mut bus, target_clut);

        let entry16 = gw_ctab_ptr + 8 + 16 * 8;
        let entry42 = gw_ctab_ptr + 8 + 42 * 8;
        let entry128 = gw_ctab_ptr + 8 + 128 * 8;
        assert_eq!(bus.read_word(entry16 + 2), 0xAAAA);
        assert_eq!(bus.read_word(entry16 + 4), 0x1111);
        assert_eq!(bus.read_word(entry16 + 6), 0x2222);
        assert_eq!(bus.read_word(entry42 + 2), 0xBBBB);
        assert_eq!(bus.read_word(entry42 + 4), 0x3333);
        assert_eq!(bus.read_word(entry42 + 6), 0x4444);
        assert_eq!(bus.read_word(entry128 + 2), 0xCCCC);
        assert_eq!(bus.read_word(entry128 + 4), 0x5555);
        assert_eq!(bus.read_word(entry128 + 6), 0x6666);
    }

    #[test]
    fn test_low_level_set_entries_updates_screen_ctab_not_current_offscreen_gworld() {
        let (mut d, mut cpu, mut bus) = setup();
        let main_gdh = d.ensure_main_gdevice(&mut bus);
        let main_ctab = TrapDispatcher::gdevice_ctab_handle(&bus, main_gdh);

        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 64, 64);
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 8u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let gw_pmh = bus.read_long(gworld + 2);
        let gw_pm = bus.read_long(gw_pmh);
        let gw_ctab = bus.read_long(gw_pm + 42);
        let gw_ctab_ptr = bus.read_long(gw_ctab);
        let gw_entry = gw_ctab_ptr + 8 + 17 * 8;
        let before_offscreen = [
            bus.read_word(gw_entry + 2),
            bus.read_word(gw_entry + 4),
            bus.read_word(gw_entry + 6),
        ];

        d.current_port = gworld;
        d.current_gdevice = d.gdevice_for_port(&mut bus, gworld);

        let table_ptr = bus.alloc(8);
        bus.write_word(table_ptr, 17);
        bus.write_word(table_ptr + 2, 0x1111);
        bus.write_word(table_ptr + 4, 0x2222);
        bus.write_word(table_ptr + 6, 0x3333);
        d.apply_set_entries(&mut bus, table_ptr, -1, 0);

        let main_ctab_ptr = bus.read_long(main_ctab);
        let main_entry = main_ctab_ptr + 8 + 17 * 8;
        assert_eq!(bus.read_word(main_entry + 2), 0x1111);
        assert_eq!(bus.read_word(main_entry + 4), 0x2222);
        assert_eq!(bus.read_word(main_entry + 6), 0x3333);
        assert_eq!(
            [
                bus.read_word(gw_entry + 2),
                bus.read_word(gw_entry + 4),
                bus.read_word(gw_entry + 6),
            ],
            before_offscreen
        );
    }

    // $AB1C is not documented as SetGWorld; Systemless treats it as a no-op.
    // Verify that $AB1C (0x31C) pops 8 bytes and does NOT update the port,
    // even when called after a valid NewGWorld that registered a device.
    #[test]
    fn test_ab1c_noop_does_not_change_port_after_newgworld() {
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 100, 100);
        bus.write_long(TEST_SP, 1u32 << 1); // noNewDevice
        bus.write_long(TEST_SP + 4, gdh); // gdevice
        bus.write_long(TEST_SP + 8, 0u32); // ctab
        bus.write_long(TEST_SP + 12, bounds_ptr); // bounds
        bus.write_word(TEST_SP + 16, 8u16); // depth
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr); // gworldPtr

        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        assert_eq!(d.gworld_devices.get(&gworld).copied(), Some(gdh));

        let prior_port = d.current_port;
        let prior_gdevice = d.current_gdevice;
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, 0u32); // would-be gdevice=nil
        bus.write_long(TEST_SP + 4, gworld); // would-be port
        let result = d.dispatch_quickdraw(true, 0x31C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(d.current_port, prior_port);
        assert_eq!(d.current_gdevice, prior_gdevice);
    }

    #[test]
    fn disposegworld_consumes_offscreengworld_argument() {
        // IM:VI 1991 p. 21-19 / IWQD 1994 p. 6-25:
        // DisposeGWorld(offscreenGWorld) is a procedure taking one pointer.
        let (mut d, mut cpu, mut bus) = setup();
        cpu.write_reg(Register::D0, 0x0004_0004);
        bus.write_long(TEST_SP, 0x300000);
        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn disposegworld_releases_associated_offscreen_device_state() {
        // IM:VI 1991 p. 21-19 / IWQD 1994 p. 6-25: disposing an offscreen
        // world releases associated offscreen state.
        let (mut d, mut cpu, mut bus) = setup();
        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 64, 64);

        // NewGWorld via _QDExtensions selector 0 with default flags so this
        // world owns an offscreen GDevice that DisposeGWorld should tear down.
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 8u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let create = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(create.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let vis_rgn = bus.read_long(gworld + 24);
        let vis_rgn_ptr = bus.read_long(vis_rgn);
        let clip_rgn = bus.read_long(gworld + 28);
        let clip_rgn_ptr = bus.read_long(clip_rgn);
        let pixmap_handle = bus.read_long(gworld + 2);
        let pixmap_ptr = bus.read_long(pixmap_handle);
        let attached_gdevice = d.gworld_devices.get(&gworld).copied().unwrap_or(0);
        let attached_gd_ptr = bus.read_long(attached_gdevice);

        assert_ne!(attached_gdevice, 0);
        assert_eq!(bus.read_long(attached_gd_ptr + 22), pixmap_handle);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0004_0004);
        bus.write_long(TEST_SP, gworld);
        let dispose = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(dispose.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert!(!d.gworld_devices.contains_key(&gworld));
        assert_eq!(bus.get_alloc_size(gworld), None);
        assert_eq!(bus.get_alloc_size(vis_rgn), None);
        assert_eq!(bus.get_alloc_size(vis_rgn_ptr), None);
        assert_eq!(bus.get_alloc_size(clip_rgn), None);
        assert_eq!(bus.get_alloc_size(clip_rgn_ptr), None);
        assert_eq!(bus.get_alloc_size(attached_gdevice), None);
        assert_eq!(bus.get_alloc_size(attached_gd_ptr), None);
        assert_ne!(bus.get_alloc_size(pixmap_handle), None);
        assert_ne!(bus.get_alloc_size(pixmap_ptr), None);
    }

    #[test]
    fn disposegworld_stale_portbits_pointer_still_resolves_pixmap() {
        let (mut d, mut cpu, mut bus) = setup();
        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 64, 64);

        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 8u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let create = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(create.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let bits_ptr = gworld + 2;
        let pm_handle = bus.read_long(bits_ptr);
        let pm_ptr = bus.read_long(pm_handle);
        let expected_base = TrapDispatcher::offscreen_pixmap_base_ptr(&bus, pm_ptr);
        let expected_row_bytes = (bus.read_word(pm_ptr + 4) & 0x3FFF) as u32;
        let expected_pixel_size = bus.read_word(pm_ptr + 32) as u32;

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0004_0004);
        bus.write_long(TEST_SP, gworld);
        let dispose = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(dispose.unwrap().is_ok());

        // Reuse the just-freed port block and poison `portBits` bytes so
        // resolve_copy_bitmap must fall back to the disposed-port mapping.
        let reused = bus.alloc(170);
        assert_eq!(reused, gworld);
        bus.write_long(bits_ptr, u32::MAX);
        bus.write_word(bits_ptr + 4, 0xFFFF);

        let info = d.resolve_copy_bitmap(&bus, bits_ptr);
        assert_eq!(info.base, expected_base);
        assert_eq!(info.row_bytes, expected_row_bytes);
        assert_eq!(info.pixel_size, expected_pixel_size);
    }

    #[test]
    fn resolve_copy_bitmap_recovers_clobbered_live_gworld_portbits_handle() {
        let (mut d, mut cpu, mut bus) = setup();
        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 64, 64);

        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 8u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let create = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(create.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let bits_ptr = gworld + 2;
        let pm_handle = bus.read_long(bits_ptr);
        let pm_ptr = bus.read_long(pm_handle);
        let expected_base = TrapDispatcher::offscreen_pixmap_base_ptr(&bus, pm_ptr);
        let expected_row_bytes = (bus.read_word(pm_ptr + 4) & 0x3FFF) as u32;
        let expected_pixel_size = bus.read_word(pm_ptr + 32) as u32;

        // Mimic apps that keep `&portBits` while the live port storage gets
        // clobbered before DisposeGWorld runs.
        bus.write_long(bits_ptr, u32::MAX);
        bus.write_word(bits_ptr + 4, 0xFFFF);

        let info = d.resolve_copy_bitmap(&bus, bits_ptr);
        assert_eq!(info.base, expected_base);
        assert_eq!(info.row_bytes, expected_row_bytes);
        assert_eq!(info.pixel_size, expected_pixel_size);
    }

    #[test]
    fn resolve_copy_bitmap_nil_or_sentinel_bits_are_inert() {
        let (d, _cpu, bus) = setup();

        for bits_ptr in [0, u32::MAX] {
            let info = d.resolve_copy_bitmap(&bus, bits_ptr);
            assert_eq!(info.base, u32::MAX);
            assert_eq!(info.row_bytes, 0);
            assert_eq!(info.pixel_size, 0);
        }
    }

    #[test]
    fn resolve_copy_bitmap_bit14_compat_dereferences_base_handle() {
        let (d, _cpu, mut bus) = setup();
        let bits_ptr = bus.alloc(32);
        let pm_handle = bus.alloc(4);
        let pm_ptr = bus.alloc(64);
        let pix_base = bus.alloc(640 * 480);
        let ctab_handle = bus.alloc(4);

        bus.write_long(pm_handle, pm_ptr);
        write_pixmap_8(&mut bus, pm_ptr, pix_base, 640, 480, ctab_handle);

        // CopyBits compatibility path: bit14 set in the word at +4 means
        // baseAddr should be treated as a handle and dereferenced once.
        // We also tag the high byte to exercise low-24 normalization.
        let tagged_pm_handle = 0x3300_0000 | (pm_handle & 0x00FF_FFFF);
        bus.write_long(bits_ptr, tagged_pm_handle);
        bus.write_word(bits_ptr + 4, 0x4000 | 0x1A0B);
        bus.write_long(bits_ptr + 6, 0xFFFF_0B07); // garbage bounds payload

        let info = d.resolve_copy_bitmap(&bus, bits_ptr);
        assert_eq!(info.base, pix_base);
        assert_eq!(info.row_bytes, 640);
        assert_eq!(info.bounds_top, 0);
        assert_eq!(info.bounds_left, 0);
        assert_eq!(info.bounds_bottom, 480);
        assert_eq!(info.bounds_right, 640);
        assert_eq!(info.pixel_size, 8);
        assert_eq!(info.ctab_handle, ctab_handle);
    }

    #[test]
    fn resolve_copy_bitmap_direct_pixmap_dereferences_offscreen_base_handle() {
        let (d, _cpu, mut bus) = setup();
        let pm_ptr = bus.alloc(64);
        let pix_base_handle = bus.alloc(4);
        let pix_base = bus.alloc(640 * 320);
        let ctab_handle = bus.alloc(4);

        bus.write_long(pix_base_handle, pix_base);
        write_pixmap_8(&mut bus, pm_ptr, pix_base_handle, 640, 320, ctab_handle);

        let info = d.resolve_copy_bitmap(&bus, pm_ptr);
        assert_eq!(info.base, pix_base);
        assert_eq!(info.row_bytes, 640);
        assert_eq!(info.bounds_top, 0);
        assert_eq!(info.bounds_left, 0);
        assert_eq!(info.bounds_bottom, 320);
        assert_eq!(info.bounds_right, 640);
        assert_eq!(info.pixel_size, 8);
        assert_eq!(info.ctab_handle, ctab_handle);
    }

    #[test]
    fn disposegworld_alias_preserves_stack_pointer_and_offscreen_state() {
        // BasiliskII System 7.5.3 leaves the Pascal argument frame on the
        // stack and does not tear down tracked offscreen state for direct
        // $AB1F calls.
        let (mut d, mut cpu, mut bus) = setup();
        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 64, 64);

        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 8u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let create = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(create.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let vis_rgn = bus.read_long(gworld + 24);
        let vis_rgn_ptr = bus.read_long(vis_rgn);
        let clip_rgn = bus.read_long(gworld + 28);
        let clip_rgn_ptr = bus.read_long(clip_rgn);
        let pixmap_handle = bus.read_long(gworld + 2);
        let pixmap_ptr = bus.read_long(pixmap_handle);
        let attached_gdevice = d.gworld_devices.get(&gworld).copied().unwrap_or(0);
        let attached_gd_ptr = bus.read_long(attached_gdevice);

        assert_ne!(attached_gdevice, 0);
        assert_eq!(bus.read_long(attached_gd_ptr + 22), pixmap_handle);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, gworld);
        let dispose = d.dispatch_quickdraw(true, 0x31F, &mut cpu, &mut bus);
        assert!(dispose.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_eq!(
            d.gworld_devices.get(&gworld).copied(),
            Some(attached_gdevice)
        );
        assert_ne!(bus.get_alloc_size(gworld), None);
        assert_ne!(bus.get_alloc_size(vis_rgn), None);
        assert_ne!(bus.get_alloc_size(vis_rgn_ptr), None);
        assert_ne!(bus.get_alloc_size(clip_rgn), None);
        assert_ne!(bus.get_alloc_size(clip_rgn_ptr), None);
        assert_ne!(bus.get_alloc_size(attached_gdevice), None);
        assert_ne!(bus.get_alloc_size(attached_gd_ptr), None);
        assert_ne!(bus.get_alloc_size(pixmap_handle), None);
        assert_ne!(bus.get_alloc_size(pixmap_ptr), None);
    }

    #[test]
    fn disposegworld_current_offscreen_device_resets_to_main_device() {
        // IM:VI 1991 p. 21-19 / IWQD 1994 p. 6-25: if the disposed world's
        // offscreen device is current, DisposeGWorld resets current device to
        // MainDevice.
        let (mut d, mut cpu, mut bus) = setup();
        let main_gdevice = d.ensure_main_gdevice(&mut bus);
        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 64, 64);

        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 8u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let create = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(create.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let attached_gdevice = d.gworld_devices.get(&gworld).copied().unwrap_or(0);
        assert_ne!(attached_gdevice, 0);
        assert_ne!(attached_gdevice, main_gdevice);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0008_0006);
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, gworld);
        let set_current = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(set_current.unwrap().is_ok());
        assert_eq!(d.current_port, gworld);
        assert_eq!(d.current_gdevice, attached_gdevice);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0004_0004);
        bus.write_long(TEST_SP, gworld);
        let dispose = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(dispose.unwrap().is_ok());
        assert_eq!(d.current_gdevice, main_gdevice);
        assert_ne!(d.current_port, gworld);
    }

    #[test]
    fn disposegworld_alias_leaves_current_offscreen_device_unchanged() {
        // BasiliskII System 7.5.3 direct $AB1F is a no-op with respect to
        // the current offscreen port/device selection.
        let (mut d, mut cpu, mut bus) = setup();
        let main_gdevice = d.ensure_main_gdevice(&mut bus);
        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 64, 64);

        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 8u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let create = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(create.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let attached_gdevice = d.gworld_devices.get(&gworld).copied().unwrap_or(0);
        assert_ne!(attached_gdevice, 0);
        assert_ne!(attached_gdevice, main_gdevice);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0008_0006);
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, gworld);
        let set_current = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(set_current.unwrap().is_ok());
        assert_eq!(d.current_port, gworld);
        assert_eq!(d.current_gdevice, attached_gdevice);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, gworld);
        let dispose = d.dispatch_quickdraw(true, 0x31F, &mut cpu, &mut bus);
        assert!(dispose.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_eq!(d.current_port, gworld);
        assert_eq!(d.current_gdevice, attached_gdevice);
    }

    #[test]
    fn test_get_gworld() {
        let (mut d, mut cpu, mut bus) = setup();
        d.current_port = 0x400000;
        d.current_gdevice = 0x500000;
        let gd_ptr = 0x300000u32;
        let port_ptr = 0x300100u32;
        bus.write_long(TEST_SP, gd_ptr);
        bus.write_long(TEST_SP + 4, port_ptr);
        let result = d.dispatch_quickdraw(true, 0x31E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_long(port_ptr), 0x400000);
        assert_eq!(bus.read_long(gd_ptr), 0x500000);
    }

    #[test]
    fn getpixbaseaddr_returns_direct_pointer_for_onscreen_pixmap() {
        // Onscreen PixMaps (GDevice-backed) store baseAddr as a direct pointer.
        // GetPixBaseAddr must return that pointer, not dereference it as if it
        // were an offscreen Handle.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd_ptr = bus.read_long(gdh);
        let pm_handle = bus.read_long(gd_ptr + 22);
        let pm_ptr = bus.read_long(pm_handle);
        let onscreen_base = bus.read_long(pm_ptr);

        assert_ne!(onscreen_base, 0);
        // If the implementation incorrectly dereferences screen_base, it would
        // return this poison value instead of the framebuffer pointer.
        bus.write_long(onscreen_base, 0x1122_3344);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0004_000F);
        bus.write_long(TEST_SP, pm_handle);
        bus.write_long(TEST_SP + 4, 0xDEAD_BEEFu32);
        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(TEST_SP + 4), onscreen_base);
        assert_eq!(
            TrapDispatcher::offscreen_pixmap_base_handle(&bus, pm_ptr),
            0
        );
        assert_eq!(
            TrapDispatcher::offscreen_pixmap_base_ptr(&bus, pm_ptr),
            onscreen_base
        );
    }

    #[test]
    fn getpixbaseaddr_returns_direct_pointer_for_offscreen_pixmap() {
        // Offscreen GWorld PixMaps expose baseAddr as a direct pixel pointer.
        // Games may read (**pmh).baseAddr directly instead of calling
        // GetPixBaseAddr, so the visible PixMap field must not be Systemless's
        // private allocation handle.
        let (mut d, mut cpu, mut bus) = setup();
        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 64, 64);
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 8u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let create = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(create.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let pm_handle = bus.read_long(gworld + 2);
        let pm_ptr = bus.read_long(pm_handle);
        let expected_base = bus.read_long(pm_ptr);
        assert_ne!(expected_base, 0);
        assert_ne!(bus.get_alloc_size(expected_base), Some(4));
        assert_ne!(bus.get_alloc_size(expected_base), None);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0004_000F);
        bus.write_long(TEST_SP, pm_handle);
        bus.write_long(TEST_SP + 4, 0xDEAD_BEEFu32);
        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(TEST_SP + 4), expected_base);
        assert_eq!(
            TrapDispatcher::offscreen_pixmap_base_handle(&bus, pm_ptr),
            0
        );
        assert_eq!(
            TrapDispatcher::offscreen_pixmap_base_ptr(&bus, pm_ptr),
            expected_base
        );
    }

    #[test]
    fn getpixbaseaddr_dereferences_legacy_offscreen_base_handle() {
        // Systemless used to store a 4-byte pixel master pointer cell in PixMap
        // baseAddr. Keep the resolver tolerant so disposed-port caches and
        // hand-built PixMaps continue to work.
        let (mut d, mut cpu, mut bus) = setup();
        let pm_handle = bus.alloc(4);
        let pm_ptr = bus.alloc(64);
        let base_handle = bus.alloc(4);
        let expected_base = bus.alloc(256);
        let ctab_handle = bus.alloc(4);

        bus.write_long(pm_handle, pm_ptr);
        bus.write_long(base_handle, expected_base);
        write_pixmap_8(&mut bus, pm_ptr, base_handle, 16, 16, ctab_handle);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0004_000F);
        bus.write_long(TEST_SP, pm_handle);
        bus.write_long(TEST_SP + 4, 0xDEAD_BEEFu32);
        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(TEST_SP + 4), expected_base);
        assert_eq!(
            TrapDispatcher::offscreen_pixmap_base_handle(&bus, pm_ptr),
            base_handle
        );
        assert_eq!(
            TrapDispatcher::offscreen_pixmap_base_ptr(&bus, pm_ptr),
            expected_base
        );
    }

    #[test]
    fn getgworlddevice_regular_port_returns_current_device_after_setgworld() {
        // IM:VI 1991 p. 21-18 / IWQD 1994 p. 6-33:
        // If the argument points to a regular GrafPort/CGrafPort,
        // GetGWorldDevice returns the current device.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let regular_port = d.current_port;
        let main_gdh = d.ensure_main_gdevice(&mut bus);
        d.current_gdevice = main_gdh;

        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 64, 64);
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 8u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let create = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(create.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let offscreen_gdh = d.gworld_devices.get(&gworld).copied().unwrap_or(0);
        assert_ne!(gworld, 0);
        assert_ne!(offscreen_gdh, 0);
        assert_ne!(offscreen_gdh, main_gdh);

        d.current_port = gworld;
        d.current_gdevice = offscreen_gdh;

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0004_0012);
        bus.write_long(TEST_SP, regular_port);
        bus.write_long(TEST_SP + 4, 0xDEAD_BEEF);
        let get = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(get.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(TEST_SP + 4), offscreen_gdh);
    }

    #[test]
    fn deviceloop_all_devices_arms_a_trampoline_chain() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let main_gdh = d.ensure_main_gdevice(&mut bus);
        let main_gd = bus.read_long(main_gdh);
        let main_pm_handle = bus.read_long(main_gd + 22);
        let main_pm_ptr = bus.read_long(main_pm_handle);

        let linked_gd_ptr = bus.alloc(128);
        let linked_pm_ptr = bus.alloc(64);
        let linked_gdh = bus.alloc(4);
        let linked_pm = bus.alloc(4);
        bus.write_long(linked_gdh, linked_gd_ptr);
        bus.write_long(linked_pm, linked_pm_ptr);
        bus.write_long(linked_gd_ptr + 22, linked_pm);
        bus.write_word(linked_pm_ptr + 32, 4);
        bus.write_word(linked_gd_ptr + 20, 0x2468);
        write_rect(&mut bus, linked_gd_ptr + 34, 900, 20, 940, 52);
        bus.write_long(main_gd + 30, linked_gdh);
        d.current_gdevice = main_gdh;

        let rgn_addr = bus.alloc(10);
        let rgn_handle = bus.alloc(4);
        make_rgn(&mut bus, rgn_addr, rgn_handle, 2000, 2000, 2100, 2100);

        let drawing_proc = bus.alloc(2);
        bus.write_word(drawing_proc, 0x4E56);

        let return_pc = 0x1234_5678;
        cpu.write_reg(Register::PC, return_pc);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, 4);
        bus.write_long(TEST_SP + 4, 0xCAFEBABE);
        bus.write_long(TEST_SP + 8, drawing_proc);
        bus.write_long(TEST_SP + 12, rgn_handle);

        let result = d.dispatch_quickdraw(true, 0x3CA, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        let first_trampoline = cpu.read_reg(Register::PC);
        let second_trampoline = bus.read_long(first_trampoline + 42);

        assert_ne!(first_trampoline, 0);
        assert_ne!(second_trampoline, 0);
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(bus.read_long(TEST_SP + 12), return_pc);
        assert_eq!(bus.read_word(first_trampoline + 40), 0x4EF9);
        assert_eq!(bus.read_word(second_trampoline + 40), 0x4E75);
        assert_eq!(bus.read_long(first_trampoline + 14), main_gdh);
        assert_eq!(bus.read_long(second_trampoline + 14), linked_gdh);
        assert_eq!(
            bus.read_word(first_trampoline + 6),
            bus.read_word(main_pm_ptr + 32)
        );
        assert_eq!(bus.read_word(second_trampoline + 6), 4);
    }

    #[test]
    fn qddone_returns_true_on_repeated_calls_and_writes_boolean_result_slot() {
        // Inside Macintosh: Imaging With QuickDraw 1994, pp. 3-125 to 3-126:
        // QDDone(port: GrafPtr): Boolean uses the QDExtensions selector
        // $00040013 and returns a Boolean result in the caller's slot.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port = 0x181000u32;

        cpu.write_reg(Register::D0, 0x0004_0013);
        bus.write_word(TEST_SP + 4, 0xFFFF); // pre-poison result slot
        bus.write_long(TEST_SP, port);

        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(cpu.read_reg(Register::D0), 1);
        assert_eq!(bus.read_byte(TEST_SP + 4), 1);
        assert_eq!(bus.read_word(TEST_SP + 4), 0x0100);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0004_0013);
        bus.write_word(TEST_SP + 4, 0xFFFF);
        bus.write_long(TEST_SP, port);

        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(TEST_SP + 4), 0x0100);
    }

    #[test]
    fn qddone_stays_true_when_the_port_is_reselected() {
        // IM:VI 1994, pp. 3-125 to 3-126:
        // QDDone(port: GrafPtr): Boolean reports completion for the
        // active port, and re-selecting that port should re-arm it.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port = 0x181000u32;

        d.set_current_port_state(&mut bus, &mut cpu, port, None);
        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0004_0013);
        bus.write_word(TEST_SP + 4, 0xFFFF);
        bus.write_long(TEST_SP, port);
        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(TEST_SP + 4), 0x0100);
        assert_eq!(cpu.read_reg(Register::D0), 1);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0004_0013);
        bus.write_long(TEST_SP, port);
        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(TEST_SP + 4), 0x0100);
        assert_eq!(cpu.read_reg(Register::D0), 1);

        d.set_current_port_state(&mut bus, &mut cpu, port, None);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0004_0013);
        bus.write_word(TEST_SP + 4, 0xFFFF);
        bus.write_long(TEST_SP, port);
        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(TEST_SP + 4), 0x0100);
    }

    #[test]
    fn qddone_returns_true_for_a_live_but_inactive_port() {
        // IM:VI 1994, pp. 3-125 to 3-126:
        // QDDone reports completion for a live port even when another
        // port is currently selected.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let saved_port = 0x181000u32;
        let inactive_port = 0x182000u32;

        d.set_current_port_state(&mut bus, &mut cpu, saved_port, None);
        d.set_current_port_state(&mut bus, &mut cpu, inactive_port, None);
        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0004_0013);
        bus.write_word(TEST_SP + 4, 0xFFFF);
        bus.write_long(TEST_SP, saved_port);

        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(cpu.read_reg(Register::D0), 1);
        assert_eq!(bus.read_word(TEST_SP + 4), 0x0100);
    }

    #[test]
    fn offscreenversion_returns_nonzero_and_preserves_stack() {
        // QuickDraw Reference p. 307 / QDOffscreen.h:
        // OffscreenVersion is a no-arg Pascal FUNCTION, so the caller
        // pre-pushes a 4-byte result slot and the trap leaves A7
        // unchanged while writing a nonzero version word.
        let (mut d, mut cpu, mut bus) = setup();
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, 0xDEAD_BEEFu32);
        cpu.write_reg(Register::D0, 0x0004_0014);

        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_ne!(bus.read_long(TEST_SP), 0);
        assert_ne!(bus.read_long(TEST_SP), 0xDEAD_BEEFu32);
    }

    // $AB1C is not documented as SetGWorld (SetGWorld uses _QDExtensions
    // $AB1D sel 6). In BasiliskII System 7.5.3, $AB1C does not update
    // thePort. Systemless pops 8 bytes and returns without changing port.
    #[test]
    fn test_ab1c_is_noop() {
        let (mut d, mut cpu, mut bus) = setup();
        let prior_port = d.current_port;
        let prior_gdevice = d.current_gdevice;
        bus.write_long(TEST_SP, 0x600000); // would-be gdevice
        bus.write_long(TEST_SP + 4, 0x700000); // would-be port
        let result = d.dispatch_quickdraw(true, 0x31C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(d.current_port, prior_port);
        assert_eq!(d.current_gdevice, prior_gdevice);
    }

    #[test]
    fn lockpixels_returns_true_when_offscreen_buffer_is_not_purged() {
        // IM:VI 1991 p. 17-13 / IWQD 1994 p. 6-44:
        // LockPixels returns TRUE when the buffer is not purged.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x300000); // pm_handle
        let result = d.dispatch_quickdraw(true, 0x304, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(TEST_SP + 4), 1); // TRUE
        assert_eq!(bus.read_word(TEST_SP + 4), 0x0100);
    }

    #[test]
    fn lockpixels_consumes_pixmaphandle_and_writes_boolean_result_slot() {
        // IM:VI 1991 p. 17-13 / IWQD 1994 p. 6-44:
        // FUNCTION LockPixels(pm: PixMapHandle): Boolean
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP + 4, 0); // seed result slot to FALSE
        bus.write_long(TEST_SP, 0x300000);
        let result = d.dispatch_quickdraw(true, 0x304, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_byte(TEST_SP + 4), 1);
        assert_eq!(bus.read_word(TEST_SP + 4), 0x0100);
    }

    #[test]
    fn qde_lockpixels_writes_pascal_boolean_byte_result_slot() {
        // MPW callers reserve the LockPixels Boolean result with CLR.B
        // -(SP), then test the byte at post-call A7. TRUE must therefore
        // occupy the first byte of the word-aligned result slot.
        let (mut d, mut cpu, mut bus) = setup();
        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0004_0001);
        bus.write_word(TEST_SP + 4, 0xFFFF);
        bus.write_long(TEST_SP, 0x300000);

        let result = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_byte(TEST_SP + 4), 1);
        assert_eq!(bus.read_word(TEST_SP + 4), 0x0100);
    }

    #[test]
    fn unlockpixels_consumes_pixmaphandle_argument() {
        // IM:VI 1991 p. 17-13 / IWQD 1994 p. 6-45:
        // PROCEDURE UnlockPixels(pm: PixMapHandle)
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x300000);
        let result = d.dispatch_quickdraw(true, 0x305, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn unlockpixels_purged_pixels_call_is_harmless_noop() {
        // IM:VI 1991 p. 17-13 / IWQD 1994 p. 6-45:
        // calling UnlockPixels on purged pixels does no harm.
        let (mut d, mut cpu, mut bus) = setup();
        let prior_port = d.current_port;
        let prior_gdevice = d.current_gdevice;
        bus.write_long(TEST_SP, 0); // stand-in for purged/invalid handle path
        let result = d.dispatch_quickdraw(true, 0x305, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(d.current_port, prior_port);
        assert_eq!(d.current_gdevice, prior_gdevice);
    }

    #[test]
    fn lockpixels_direct_alias_sets_locked_bit_visible_via_getpixelsstate() {
        let (mut d, mut cpu, mut bus) = setup();
        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 32, 32);
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 1u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let create = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(create.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let pmh = bus.read_long(gworld + 2);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, pmh);
        let lock = d.dispatch_quickdraw(true, 0x304, &mut cpu, &mut bus);
        assert!(lock.unwrap().is_ok());

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0004_000D);
        bus.write_long(TEST_SP, pmh);
        bus.write_long(TEST_SP + 4, 0);
        let get_state = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(get_state.unwrap().is_ok());
        assert_eq!(bus.read_long(TEST_SP + 4) & (1 << 7), 1 << 7);
    }

    #[test]
    fn unlockpixels_direct_alias_clears_locked_bit_visible_via_getpixelsstate() {
        let (mut d, mut cpu, mut bus) = setup();
        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 32, 32);
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 1u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let create = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(create.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let pmh = bus.read_long(gworld + 2);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, pmh);
        let lock = d.dispatch_quickdraw(true, 0x304, &mut cpu, &mut bus);
        assert!(lock.unwrap().is_ok());

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, pmh);
        let unlock = d.dispatch_quickdraw(true, 0x305, &mut cpu, &mut bus);
        assert!(unlock.unwrap().is_ok());

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0004_000D);
        bus.write_long(TEST_SP, pmh);
        bus.write_long(TEST_SP + 4, 0xFFFF_FFFF);
        let get_state = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(get_state.unwrap().is_ok());
        assert_eq!(bus.read_long(TEST_SP + 4) & (1 << 7), 0);
    }

    #[test]
    fn setpixelsstate_preserves_unrelated_state_bits() {
        let (mut d, _, _) = setup();
        let pmh = 0x300000u32;
        let keep_local = 1u32 << 3;
        let purgeable = 1u32 << 6;
        let locked = 1u32 << 7;

        d.gworld_pixel_states.insert(pmh, keep_local);
        assert_eq!(d.gworld_pixels_state(pmh), keep_local);

        d.set_gworld_pixels_state(pmh, purgeable);
        assert_eq!(
            d.gworld_pixels_state(pmh),
            keep_local | purgeable,
            "SetPixelsState should keep unrelated GWorldFlags bits while toggling purgeability",
        );

        d.set_gworld_pixels_state(pmh, 0);
        assert_eq!(
            d.gworld_pixels_state(pmh),
            keep_local,
            "SetPixelsState should clear only the documented pixel-state bits",
        );

        d.set_gworld_pixels_state(pmh, purgeable | locked);
        assert_eq!(d.gworld_pixels_state(pmh), keep_local | purgeable | locked,);
    }

    #[test]
    fn updategworld_sets_mappix_when_color_table_handle_changes_even_if_seed_matches() {
        // IM:VI 1991 pp. 6-23 to 6-26 / IWQD 1994 pp. 6-23 to 6-26:
        // UpdateGWorld remaps colors when the supplied color table changes.
        let (mut d, mut cpu, mut bus) = setup();
        let bounds_ptr = 0x300000u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 64, 64);

        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 8u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let create = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(create.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let pmh = bus.read_long(gworld + 2);
        let pm = bus.read_long(pmh);
        let old_ctab = bus.read_long(pm + 42);
        let old_ctab_ptr = bus.read_long(old_ctab);
        let ctab_bytes = bus.get_alloc_size(old_ctab).unwrap();
        let copy_ptr = bus.alloc(ctab_bytes);
        let copy_handle = bus.alloc(4);
        bus.write_long(copy_handle, copy_ptr);

        assert_ne!(old_ctab, 0);
        assert_ne!(copy_handle, 0);
        assert_ne!(copy_handle, old_ctab);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0008_0006);
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, gworld);
        let set_current = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(set_current.unwrap().is_ok());

        for off in 0..ctab_bytes {
            let byte = bus.read_byte(old_ctab_ptr + off);
            bus.write_byte(copy_ptr + off, byte);
        }
        bus.write_long(copy_ptr, bus.read_long(old_ctab_ptr));
        bus.write_word(copy_ptr + 4, bus.read_word(old_ctab_ptr + 4));
        bus.write_word(copy_ptr + 6, bus.read_word(old_ctab_ptr + 6));
        bus.write_word(copy_ptr + 8 + 17 * 8, 17);
        bus.write_word(copy_ptr + 8 + 17 * 8 + 2, 0x1234);
        bus.write_word(copy_ptr + 8 + 17 * 8 + 4, 0x5678);
        bus.write_word(copy_ptr + 8 + 17 * 8 + 6, 0x9ABC);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0016_0003);
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, copy_handle);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 8u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let update = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(update.unwrap().is_ok());

        let flags = cpu.read_reg(Register::D0);
        let updated_ctab = bus.read_long(pm + 42);
        let stack_flags = bus.read_long(TEST_SP + 22);
        let stack_ok = cpu.read_reg(Register::A7) == TEST_SP + 22;

        assert_ne!(updated_ctab, 0);
        assert_ne!(copy_handle, 0);
        assert_eq!(flags & (1 << 16), 1 << 16);
        assert_eq!(stack_flags, flags);
        assert_eq!(d.current_port, gworld);
        assert!(stack_ok);
    }

    #[test]
    fn updategworld_realloc_without_clip_or_stretch_discards_old_pixels() {
        // IWQD 1994 pp. 6-23..6-26: with no UpdateGWorld pixel-update
        // flags, pixels are not updated; if the base is reallocated,
        // reallocPix tells the caller to reconstruct the image.
        let (mut d, mut cpu, mut bus) = setup();
        let bounds_ptr = 0x300000u32;
        let resized_bounds_ptr = 0x300020u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 4, 4);
        write_rect(&mut bus, resized_bounds_ptr, 0, 0, 4, 8);

        bus.write_long(TEST_SP, 0);
        bus.write_long(TEST_SP + 4, 0);
        bus.write_long(TEST_SP + 8, 0);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 8);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let create = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(create.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let pmh = bus.read_long(gworld + 2);
        let pm = bus.read_long(pmh);
        let old_base = bus.read_long(pm);
        for i in 0..16 {
            bus.write_byte(old_base + i, 0xA5);
        }

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0016_0003);
        bus.write_long(TEST_SP, 0);
        bus.write_long(TEST_SP + 4, 0);
        bus.write_long(TEST_SP + 8, 0);
        bus.write_long(TEST_SP + 12, resized_bounds_ptr);
        bus.write_word(TEST_SP + 16, 8);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let update = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(update.unwrap().is_ok());

        let new_base = bus.read_long(pm);
        let flags = cpu.read_reg(Register::D0);
        assert_ne!(new_base, old_base);
        assert_eq!(flags & (1 << 20), 1 << 20);
        assert_eq!(flags & (1 << 19), 1 << 19);
        for i in 0..16 {
            assert_eq!(
                bus.read_byte(new_base + i),
                0,
                "byte {i} should not be copied without clipPix/stretchPix"
            );
        }
    }

    #[test]
    fn updategworld_realloc_with_clippix_preserves_overlapping_pixels() {
        // IWQD 1994 pp. 6-23..6-26: clipPix requests that pixels be
        // updated to the new bounds; Systemless preserves the overlapping
        // rows when it reallocates the offscreen buffer.
        let (mut d, mut cpu, mut bus) = setup();
        let bounds_ptr = 0x300000u32;
        let resized_bounds_ptr = 0x300020u32;
        let gworld_ptr_ptr = 0x300100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 4, 4);
        write_rect(&mut bus, resized_bounds_ptr, 0, 0, 4, 8);

        bus.write_long(TEST_SP, 0);
        bus.write_long(TEST_SP + 4, 0);
        bus.write_long(TEST_SP + 8, 0);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 8);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let create = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(create.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let pmh = bus.read_long(gworld + 2);
        let pm = bus.read_long(pmh);
        let old_base = bus.read_long(pm);
        for i in 0..16 {
            bus.write_byte(old_base + i, (0x40 + i) as u8);
        }

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0016_0003);
        bus.write_long(TEST_SP, 1 << 28);
        bus.write_long(TEST_SP + 4, 0);
        bus.write_long(TEST_SP + 8, 0);
        bus.write_long(TEST_SP + 12, resized_bounds_ptr);
        bus.write_word(TEST_SP + 16, 8);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let update = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(update.unwrap().is_ok());

        let new_base = bus.read_long(pm);
        assert_ne!(new_base, old_base);
        for row in 0..4u32 {
            for col in 0..4u32 {
                assert_eq!(
                    bus.read_byte(new_base + row * 8 + col),
                    (0x40 + row * 4 + col) as u8
                );
            }
        }
    }

    #[test]
    fn getpixdepth_regular_pixmap_reads_pixelsize_field() {
        // IM:V 1986 p. V-108: PixMap.pixelSize is the physical bits per pixel.
        let (mut d, mut cpu, mut bus) = setup();
        let pm_ptr = 0x300000u32;
        let pm_handle = 0x300100u32;
        bus.write_word(pm_ptr + 32, 1); // pixelSize
        bus.write_long(pm_handle, pm_ptr);
        bus.write_long(TEST_SP, pm_handle);
        let result = d.dispatch_quickdraw(true, 0x308, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(TEST_SP + 4), 1);
    }

    #[test]
    fn getpixdepth_main_device_gdpmap_returns_basiliskii_zero() {
        // BasiliskII ROM quirk: GetPixDepth((**GetMainDevice()).gdPMap) returns 0
        // even though the backing screen PixMap advertises pixelSize=8.
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd_ptr = bus.read_long(gdh);
        let pm_handle = bus.read_long(gd_ptr + 22);
        let pm_ptr = bus.read_long(pm_handle);
        assert_eq!(bus.read_word(pm_ptr + 32), 8);

        bus.write_long(TEST_SP, pm_handle);
        let result = d.dispatch_quickdraw(true, 0x308, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(TEST_SP + 4), 0);
    }

    #[test]
    fn getpixdepth_pascal_function_preserves_stack_across_five_calls() {
        // MPW Quickdraw.p declares FUNCTION GetPixDepth(pixMap: PixMapHandle): INTEGER;
        // Caller pre-pushes a 2-byte result slot, then the 4-byte PixMapHandle arg.
        let (mut d, mut cpu, mut bus) = setup();
        let pm_ptr = 0x300000u32;
        let pm_handle = 0x300100u32;
        bus.write_word(pm_ptr + 32, 1);
        bus.write_long(pm_handle, pm_ptr);

        let sp_pre = cpu.read_reg(Register::A7);
        for i in 0..5u32 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 6);
            bus.write_long(sp - 6, pm_handle);
            bus.write_word(sp - 2, 0xD00Du16.wrapping_add(i as u16));

            let result = d.dispatch_quickdraw(true, 0x308, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp - 2);
            assert_eq!(bus.read_word(sp - 2), 1);

            cpu.write_reg(Register::A7, sp);
        }
        assert_eq!(cpu.read_reg(Register::A7), sp_pre);
    }

    // ==================== Arc Contracts ====================

    #[test]
    fn framearc_consumes_rect_startangle_arcangle_arguments_and_preserves_pen_location() {
        // IM:I 1985 p. I-184: FrameArc(r,startAngle,arcAngle) is a procedure
        // call that does not change the pen location.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let rect_ptr = bus.alloc(8);
        write_rect(&mut bus, rect_ptr, 10, 10, 30, 30);
        d.pn_loc = (37, 91);

        bus.write_word(TEST_SP, 90u16); // arcAngle
        bus.write_word(TEST_SP + 2, 0u16); // startAngle
        bus.write_long(TEST_SP + 4, rect_ptr); // rect

        let result = d.dispatch_quickdraw(true, 0x0BE, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(d.pn_loc, (37, 91));
    }

    #[test]
    fn paintarc_fills_wedge_using_pen_pattern_without_moving_pen() {
        // IM:I 1985 p. I-184: PaintArc paints the requested wedge using
        // pnPat/pnMode and leaves pen location unchanged.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let (screen_base, row_bytes) = setup_polygon_surface(&mut d, &mut bus);
        d.pn_pat = [0xFF; 8];
        d.pn_mode = 0; // srcCopy
        d.pn_loc = (12, 34);

        let rect_ptr = bus.alloc(8);
        write_rect(&mut bus, rect_ptr, 10, 10, 30, 30);
        bus.write_word(TEST_SP, 90u16); // arcAngle
        bus.write_word(TEST_SP + 2, 0u16); // startAngle
        bus.write_long(TEST_SP + 4, rect_ptr); // rect

        let result = d.dispatch_quickdraw(true, 0x0BF, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(d.pn_loc, (12, 34));
        assert_eq!(
            read_surface_pixel(&bus, screen_base, row_bytes, 24, 12),
            255
        );
        assert_eq!(read_surface_pixel(&bus, screen_base, row_bytes, 12, 24), 0);
    }

    #[test]
    fn erasearc_uses_background_pattern_patcopy_ignoring_pen_pattern_and_mode() {
        // IM:I 1985 p. I-184: EraseArc uses bkPat in patCopy mode and ignores
        // pnPat/pnMode.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let (screen_base, row_bytes) = setup_polygon_surface(&mut d, &mut bus);
        d.pn_pat = [0xFF; 8];
        d.pn_mode = 1; // srcOr (should be ignored by EraseArc)
        d.bk_pat = [0x00; 8];

        let rect_ptr = bus.alloc(8);
        write_rect(&mut bus, rect_ptr, 10, 10, 30, 30);
        bus.write_byte(screen_base + 12 * row_bytes + 24, 200);
        bus.write_byte(screen_base + 24 * row_bytes + 12, 77);
        bus.write_word(TEST_SP, 90u16); // arcAngle
        bus.write_word(TEST_SP + 2, 0u16); // startAngle
        bus.write_long(TEST_SP + 4, rect_ptr); // rect

        let result = d.dispatch_quickdraw(true, 0x0C0, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(read_surface_pixel(&bus, screen_base, row_bytes, 24, 12), 0);
        assert_eq!(read_surface_pixel(&bus, screen_base, row_bytes, 12, 24), 77);
    }

    #[test]
    fn invertarc_inverts_enclosed_wedge_pixels_without_moving_pen() {
        // IM:I 1985 p. I-184: InvertArc inverts enclosed wedge pixels and
        // does not change pen location.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let (screen_base, row_bytes) = setup_polygon_surface(&mut d, &mut bus);
        d.pn_loc = (8, 16);

        let rect_ptr = bus.alloc(8);
        write_rect(&mut bus, rect_ptr, 10, 10, 30, 30);
        bus.write_byte(screen_base + 12 * row_bytes + 24, 40);
        bus.write_byte(screen_base + 24 * row_bytes + 12, 99);
        bus.write_word(TEST_SP, 90u16); // arcAngle
        bus.write_word(TEST_SP + 2, 0u16); // startAngle
        bus.write_long(TEST_SP + 4, rect_ptr); // rect

        let result = d.dispatch_quickdraw(true, 0x0C1, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(d.pn_loc, (8, 16));
        assert_eq!(
            read_surface_pixel(&bus, screen_base, row_bytes, 24, 12),
            215
        );
        assert_eq!(read_surface_pixel(&bus, screen_base, row_bytes, 12, 24), 99);
    }

    #[test]
    fn fillarc_uses_supplied_pattern_patcopy_ignoring_port_patterns_and_pops_12_bytes() {
        // IM:I 1985 p. I-184: FillArc uses the passed-in Pattern in patCopy
        // mode, ignores pnPat/pnMode/bkPat, and consumes 12 stack bytes.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let (screen_base, row_bytes) = setup_polygon_surface(&mut d, &mut bus);
        d.pn_pat = [0xFF; 8];
        d.pn_mode = 1; // srcOr (should be ignored by FillArc)
        d.bk_pat = [0xFF; 8];

        let pat_ptr = bus.alloc(8);
        bus.write_bytes(pat_ptr, &[0x00; 8]);
        let rect_ptr = bus.alloc(8);
        write_rect(&mut bus, rect_ptr, 10, 10, 30, 30);
        bus.write_byte(screen_base + 12 * row_bytes + 24, 200);
        bus.write_byte(screen_base + 24 * row_bytes + 12, 77);
        bus.write_long(TEST_SP, pat_ptr); // pat
        bus.write_word(TEST_SP + 4, 90u16); // arcAngle
        bus.write_word(TEST_SP + 6, 0u16); // startAngle
        bus.write_long(TEST_SP + 8, rect_ptr); // rect

        let result = d.dispatch_quickdraw(true, 0x0C2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(read_surface_pixel(&bus, screen_base, row_bytes, 24, 12), 0);
        assert_eq!(read_surface_pixel(&bus, screen_base, row_bytes, 12, 24), 77);
    }

    // ==================== Picture ====================

    // DrawPicture semantics per Imaging With QuickDraw 1994, pp. 7-44 to 7-45.
    #[test]
    fn drawpicture_scales_picframe_to_destination_rect() {
        let (_d, _cpu, mut bus) = setup();
        let screen_base = 0x340000u32;
        let screen_w: u16 = 16;
        let screen_h: u16 = 16;
        let row_bytes = screen_w as u32;
        bus.fill_zeros(screen_base, row_bytes * screen_h as u32);

        let pic = 0x341000u32;
        write_v1_paintrect_picture(&mut bus, pic, (0, 0, 1, 1), (0, 0, 1, 1));

        let clut = TrapDispatcher::standard_mac_8bpp_clut();
        let (rendered, _) = super::super::pict::draw_picture(
            &mut bus,
            pic,
            0,
            0,
            2,
            2,
            (screen_base, row_bytes, screen_w, screen_h, 8),
            &clut,
            0,
            None,
        );
        assert!(rendered);

        // Inside Macintosh: Imaging 1994 p. 7-44: picture frame scales to dstRect.
        assert_eq!(bus.read_byte(screen_base), 255);
        assert_eq!(bus.read_byte(screen_base + 1), 255);
        assert_eq!(bus.read_byte(screen_base + row_bytes), 255);
        assert_eq!(bus.read_byte(screen_base + row_bytes + 1), 255);
        assert_eq!(bus.read_byte(screen_base + 2), 0);
        assert_eq!(bus.read_byte(screen_base + row_bytes * 2), 0);
    }

    #[test]
    fn drawpicture_uses_dstrect_in_current_port_local_coordinates() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let a5 = cpu.read_reg(Register::A5);
        let qd_globals = bus.read_long(a5);
        let port_ptr = bus.read_long(qd_globals);

        let screen_base = 0x350000u32;
        bus.fill_zeros(screen_base, 8);
        bus.write_long(port_ptr + 2, screen_base);
        bus.write_word(port_ptr + 6, 1); // rowBytes for 8 one-bit pixels
        bus.write_word(port_ptr + 8, 10); // bounds.top
        bus.write_word(port_ptr + 10, 20); // bounds.left
        bus.write_word(port_ptr + 12, 18); // bounds.bottom
        bus.write_word(port_ptr + 14, 28); // bounds.right
        bus.write_word(port_ptr + 16, 10); // portRect.top
        bus.write_word(port_ptr + 18, 20); // portRect.left
        bus.write_word(port_ptr + 20, 18); // portRect.bottom
        bus.write_word(port_ptr + 22, 28); // portRect.right

        let pic = 0x351000u32;
        write_v1_paintrect_picture(&mut bus, pic, (0, 0, 1, 1), (0, 0, 1, 1));
        let pic_handle = 0x351100u32;
        bus.write_long(pic_handle, pic);

        let dst_rect = 0x351200u32;
        write_rect(&mut bus, dst_rect, 10, 20, 11, 21);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dst_rect);
        bus.write_long(TEST_SP + 4, pic_handle);

        let result = d.dispatch_quickdraw(true, 0x0F6, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);

        // Imaging With QuickDraw 1994 p. 7-44: dstRect is local to current port.
        // The top-left destination pixel should land at framebuffer coordinate (0,0).
        assert_eq!(bus.read_byte(screen_base), 0x80);
    }

    #[test]
    fn drawpicture_skips_rect_outside_current_port_visible_region() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let a5 = cpu.read_reg(Register::A5);
        let qd_globals = bus.read_long(a5);
        let port_ptr = bus.read_long(qd_globals);

        let row_bytes = 25u32; // 200 one-bit pixels per row.
        let screen_base = bus.alloc(row_bytes * 200);
        bus.fill_zeros(screen_base, row_bytes * 200);
        bus.write_long(port_ptr + 2, screen_base);
        bus.write_word(port_ptr + 6, row_bytes as u16);
        bus.write_word(port_ptr + 8, (-100i16) as u16); // bounds.top
        bus.write_word(port_ptr + 10, 0); // bounds.left
        bus.write_word(port_ptr + 12, 100); // bounds.bottom
        bus.write_word(port_ptr + 14, 200); // bounds.right
        write_rect(&mut bus, port_ptr + 16, 0, 0, 8, 8);

        let vis_rgn = bus.read_long(port_ptr + 24);
        let clip_rgn = bus.read_long(port_ptr + 28);
        let vis_rgn_ptr = bus.read_long(vis_rgn);
        let clip_rgn_ptr = bus.read_long(clip_rgn);
        write_rect(&mut bus, vis_rgn_ptr + 2, 0, 0, 8, 8);
        write_rect(&mut bus, clip_rgn_ptr + 2, -32767, -32767, 32767, 32767);

        let pic = bus.alloc(32);
        write_v1_paintrect_picture(&mut bus, pic, (0, 0, 1, 1), (0, 0, 1, 1));
        let pic_handle = bus.alloc(4);
        bus.write_long(pic_handle, pic);

        let dst_rect = bus.alloc(8);
        write_rect(&mut bus, dst_rect, 12, 0, 13, 1);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dst_rect);
        bus.write_long(TEST_SP + 4, pic_handle);

        let result = d.dispatch_quickdraw(true, 0x0F6, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(
            bus.read_byte(screen_base + 112 * row_bytes),
            0,
            "DrawPicture outside visRgn must not write into the backing bitmap"
        );

        write_rect(&mut bus, dst_rect, 2, 0, 3, 1);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dst_rect);
        bus.write_long(TEST_SP + 4, pic_handle);

        let result = d.dispatch_quickdraw(true, 0x0F6, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            bus.read_byte(screen_base + 102 * row_bytes),
            0x80,
            "DrawPicture inside visRgn should still render"
        );
    }

    #[test]
    fn drawpicture_clips_partially_intersecting_rect_to_current_port_clip_region() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let a5 = cpu.read_reg(Register::A5);
        let qd_globals = bus.read_long(a5);
        let port_ptr = bus.read_long(qd_globals);

        let row_bytes = 1u32; // 8 one-bit pixels per row.
        let screen_base = bus.alloc(row_bytes * 8);
        bus.fill_zeros(screen_base, row_bytes * 8);
        bus.write_long(port_ptr + 2, screen_base);
        bus.write_word(port_ptr + 6, row_bytes as u16);
        write_rect(&mut bus, port_ptr + 8, 0, 0, 8, 8);
        write_rect(&mut bus, port_ptr + 16, 0, 0, 8, 8);

        let vis_rgn = bus.read_long(port_ptr + 24);
        let clip_rgn = bus.read_long(port_ptr + 28);
        let vis_rgn_ptr = bus.read_long(vis_rgn);
        let clip_rgn_ptr = bus.read_long(clip_rgn);
        write_rect(&mut bus, vis_rgn_ptr + 2, 0, 0, 8, 8);
        write_rect(&mut bus, clip_rgn_ptr + 2, 1, 1, 3, 3);

        let pic = bus.alloc(32);
        write_v1_paintrect_picture(&mut bus, pic, (0, 0, 4, 4), (0, 0, 4, 4));
        let pic_handle = bus.alloc(4);
        bus.write_long(pic_handle, pic);

        let dst_rect = bus.alloc(8);
        write_rect(&mut bus, dst_rect, 0, 0, 4, 4);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dst_rect);
        bus.write_long(TEST_SP + 4, pic_handle);

        let result = d.dispatch_quickdraw(true, 0x0F6, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_eq!(
            bus.read_byte(screen_base),
            0,
            "DrawPicture must leave pixels above/left of clipRgn untouched"
        );
        assert_eq!(
            bus.read_byte(screen_base + 2 * row_bytes) & 0b0010_0000,
            0b0010_0000,
            "DrawPicture must still render pixels inside the clipRgn"
        );
        assert_eq!(
            bus.read_byte(screen_base + 4 * row_bytes),
            0,
            "DrawPicture must leave pixels below the clipRgn untouched"
        );
    }

    #[test]
    fn drawpicture_honors_complex_current_port_clip_region() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let a5 = cpu.read_reg(Register::A5);
        let qd_globals = bus.read_long(a5);
        let port_ptr = bus.read_long(qd_globals);

        let row_bytes = 1u32; // 8 one-bit pixels per row.
        let screen_base = bus.alloc(row_bytes * 4);
        bus.fill_zeros(screen_base, row_bytes * 4);
        bus.write_long(port_ptr + 2, screen_base);
        bus.write_word(port_ptr + 6, row_bytes as u16);
        write_rect(&mut bus, port_ptr + 8, 0, 0, 4, 8);
        write_rect(&mut bus, port_ptr + 16, 0, 0, 4, 8);

        let vis_rgn = bus.read_long(port_ptr + 24);
        let clip_rgn = bus.read_long(port_ptr + 28);
        let vis_rgn_ptr = bus.read_long(vis_rgn);
        write_rect(&mut bus, vis_rgn_ptr + 2, 0, 0, 4, 8);

        let data_words = [
            0,
            0,
            1,
            3,
            4,
            super::REGION_STOP,
            2,
            0,
            1,
            3,
            4,
            super::REGION_STOP,
            super::REGION_STOP,
        ];
        assert!(TrapDispatcher::write_region(
            &mut bus,
            clip_rgn,
            Some((0, 0, 2, 4)),
            &data_words,
        ));

        let pic = bus.alloc(32);
        write_v1_paintrect_picture(&mut bus, pic, (0, 0, 2, 4), (0, 0, 2, 4));
        let pic_handle = bus.alloc(4);
        bus.write_long(pic_handle, pic);

        let dst_rect = bus.alloc(8);
        write_rect(&mut bus, dst_rect, 0, 0, 2, 4);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dst_rect);
        bus.write_long(TEST_SP + 4, pic_handle);

        let result = d.dispatch_quickdraw(true, 0x0F6, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_eq!(
            bus.read_byte(screen_base),
            0b1001_0000,
            "DrawPicture must preserve holes in the current port clipRgn"
        );
        assert_eq!(
            bus.read_byte(screen_base + row_bytes),
            0b1001_0000,
            "complex clipRgn should apply to every row in the region"
        );
        assert_eq!(bus.read_byte(screen_base + row_bytes * 2), 0);
    }

    #[test]
    fn drawpicture_writes_to_offscreen_gworld_base_pointer_not_base_handle_cell() {
        let (mut d, mut cpu, mut bus) = setup();
        let bounds_ptr = 0x360000u32;
        let gworld_ptr_ptr = 0x360100u32;
        write_rect(&mut bus, bounds_ptr, 0, 0, 64, 64);

        // QDExtensions selector 1 (NewGWorld).
        bus.write_long(TEST_SP, 0u32);
        bus.write_long(TEST_SP + 4, 0u32);
        bus.write_long(TEST_SP + 8, 0u32);
        bus.write_long(TEST_SP + 12, bounds_ptr);
        bus.write_word(TEST_SP + 16, 8u16);
        bus.write_long(TEST_SP + 18, gworld_ptr_ptr);
        let create = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(create.unwrap().is_ok());

        let gworld = bus.read_long(gworld_ptr_ptr);
        let gdh = d.gworld_devices.get(&gworld).copied().unwrap_or(0);
        assert_ne!(gdh, 0);

        // QDExtensions selector 6 (SetGWorld): new_gd at SP, new_port at SP+4.
        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 0x0008_0006);
        bus.write_long(TEST_SP, gdh);
        bus.write_long(TEST_SP + 4, gworld);
        let set_current = d.dispatch_quickdraw(true, 0x31D, &mut cpu, &mut bus);
        assert!(set_current.unwrap().is_ok());
        assert_eq!(d.current_port, gworld);

        let pm_handle = bus.read_long(gworld + 2);
        let pm_ptr = bus.read_long(pm_handle);
        let base_field = bus.read_long(pm_ptr);
        let base_handle = TrapDispatcher::offscreen_pixmap_base_handle(&bus, pm_ptr);
        let base_ptr = TrapDispatcher::offscreen_pixmap_base_ptr(&bus, pm_ptr);
        assert_eq!(base_handle, 0);
        assert_eq!(base_field, base_ptr);
        assert_ne!(base_ptr, 0);

        bus.fill_zeros(base_ptr, 64u32 * 64u32);
        let expected_base_ptr = base_ptr;

        let pic_ptr = 0x361000u32;
        let pic_handle = 0x361100u32;
        let dst_rect = 0x361200u32;
        write_v1_paintrect_picture(&mut bus, pic_ptr, (0, 0, 1, 1), (0, 0, 1, 1));
        bus.write_long(pic_handle, pic_ptr);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 1);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dst_rect);
        bus.write_long(TEST_SP + 4, pic_handle);
        let draw = d.dispatch_quickdraw(true, 0x0F6, &mut cpu, &mut bus);
        assert!(draw.unwrap().is_ok());

        assert_eq!(
            bus.read_long(pm_ptr),
            expected_base_ptr,
            "DrawPicture must not rewrite the offscreen PixMap baseAddr"
        );
        assert_ne!(
            bus.read_byte(base_ptr),
            0,
            "DrawPicture should modify pixels at the offscreen base pointer"
        );
    }

    #[test]
    fn drawpicture_draws_within_specified_destination_rect() {
        // Imaging With QuickDraw 1994 p. 7-44: DrawPicture renders within dstRect.
        let (_d, _cpu, mut bus) = setup();
        let screen_base = 0x352000u32;
        let screen_w: u16 = 8;
        let screen_h: u16 = 8;
        let row_bytes = screen_w as u32;
        for y in 0..screen_h as u32 {
            for x in 0..row_bytes {
                bus.write_byte(screen_base + y * row_bytes + x, 77);
            }
        }

        let pic = 0x353000u32;
        write_v1_paintrect_picture(&mut bus, pic, (0, 0, 1, 1), (0, 0, 1, 1));

        let clut = TrapDispatcher::standard_mac_8bpp_clut();
        let (rendered, _) = super::super::pict::draw_picture(
            &mut bus,
            pic,
            2,
            3,
            4,
            5,
            (screen_base, row_bytes, screen_w, screen_h, 8),
            &clut,
            0,
            None,
        );
        assert!(rendered);

        // Pixels inside dstRect changed by DrawPicture.
        assert_eq!(bus.read_byte(screen_base + 2 * row_bytes + 3), 255);
        assert_eq!(bus.read_byte(screen_base + 2 * row_bytes + 4), 255);
        assert_eq!(bus.read_byte(screen_base + 3 * row_bytes + 3), 255);
        assert_eq!(bus.read_byte(screen_base + 3 * row_bytes + 4), 255);

        // Pixels immediately outside dstRect remain unchanged.
        assert_eq!(bus.read_byte(screen_base + 2 * row_bytes + 2), 77);
        assert_eq!(bus.read_byte(screen_base + 2 * row_bytes + 5), 77);
        assert_eq!(bus.read_byte(screen_base + row_bytes + 3), 77);
        assert_eq!(bus.read_byte(screen_base + 4 * row_bytes + 3), 77);
    }

    #[test]
    fn drawpicture_spools_remaining_picture_bytes_from_recent_file_header_read() {
        // IM:V 1986 pp. V-93..V-95: large pictures may live in a data fork
        // and be spooled by replacing getPicProc; DrawPicture requests the
        // picture definition bytes after the 10-byte Picture header.
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        let screen_base = bus.read_long(port_ptr + 2);
        bus.fill_zeros(screen_base, 64);

        let file_picture = 0x362000u32;
        write_v1_paintrect_picture(&mut bus, file_picture, (0, 0, 1, 1), (0, 0, 1, 1));
        let pic_size = usize::from(bus.read_word(file_picture));
        let mut picture_bytes = bus.read_bytes(file_picture, pic_size);
        picture_bytes[1] = 12;
        d.vfs.insert("StreamedPICT".to_string(), picture_bytes);
        d.open_files.insert(100, "StreamedPICT".to_string());
        d.file_positions.insert(100, 0);

        let pb = 0x363000u32;
        let pic_ptr = 0x364000u32;
        cpu.write_reg(Register::A0, pb);
        bus.write_word(pb + 24, 100);
        bus.write_long(pb + 32, pic_ptr);
        bus.write_long(pb + 36, 10);
        bus.write_word(pb + 44, 0);
        bus.write_long(pb + 46, 0);
        let read = d.dispatch_resource(false, 0x02, &mut cpu, &mut bus);
        assert!(read.unwrap().is_ok());
        assert_eq!(bus.read_long(pb + 40), 10);
        assert_eq!(*d.file_positions.get(&100).unwrap(), 10);

        let pic_handle = 0x364010u32;
        let dst_rect = 0x364100u32;
        bus.write_long(pic_handle, pic_ptr);
        write_rect(&mut bus, dst_rect, 0, 0, 1, 1);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dst_rect);
        bus.write_long(TEST_SP + 4, pic_handle);

        let draw = d.dispatch_quickdraw(true, 0x0F6, &mut cpu, &mut bus);
        assert!(draw.unwrap().is_ok());

        assert_eq!(
            bus.read_byte(screen_base),
            0x80,
            "DrawPicture should render opcodes that were still only in the backing file"
        );
        assert_eq!(
            *d.file_positions.get(&100).unwrap(),
            pic_size,
            "spooled DrawPicture should ignore a short picSize and advance through EndOfPicture"
        );
    }

    #[test]
    fn test_draw_picture() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let dst_rect = 0x300000u32;
        write_rect(&mut bus, dst_rect, 0, 0, 100, 100);
        // Create a minimal picture (just size + picFrame, no opcodes)
        let pic_data = 0x300100u32;
        bus.write_word(pic_data, 10); // picSize (header only)
        write_rect(&mut bus, pic_data + 2, 0, 0, 100, 100); // picFrame
        let pic_handle = 0x300200u32;
        bus.write_long(pic_handle, pic_data);
        bus.write_long(TEST_SP, dst_rect);
        bus.write_long(TEST_SP + 4, pic_handle);
        let result = d.dispatch_quickdraw(true, 0x0F6, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
    }

    #[test]
    fn test_draw_picture_packbits_rect_respects_src_rect() {
        let (_d, _cpu, mut bus) = setup();
        let screen_base = 0x300000u32;
        let screen_row_bytes = 32u32;
        for i in 0..screen_row_bytes {
            bus.write_byte(screen_base + i, 0);
        }

        let mut device_clut = [[0u16; 3]; 256];
        device_clut[1] = [0xFFFF, 0, 0];

        let pic_data = 0x301000u32;
        let mut pos = pic_data;
        bus.write_word(pos, 0);
        pos += 2;
        write_rect(&mut bus, pos, 0, 0, 1, 3);
        pos += 8;
        bus.write_byte(pos, 0x98); // PackBitsRect
        pos += 1;

        bus.write_word(pos, 0x800A); // PixMap rowBytes = 10
        pos += 2;
        write_rect(&mut bus, pos, 0, 0, 1, 10);
        pos += 8;
        bus.write_word(pos, 0); // version
        pos += 2;
        bus.write_word(pos, 0); // packType
        pos += 2;
        bus.write_long(pos, 0); // packSize
        pos += 4;
        bus.write_long(pos, 0); // hRes
        pos += 4;
        bus.write_long(pos, 0); // vRes
        pos += 4;
        bus.write_word(pos, 0); // pixelType
        pos += 2;
        bus.write_word(pos, 8); // pixelSize
        pos += 2;
        bus.write_word(pos, 1); // cmpCount
        pos += 2;
        bus.write_word(pos, 8); // cmpSize
        pos += 2;
        bus.write_long(pos, 0); // planeBytes
        pos += 4;
        bus.write_long(pos, 0); // pmTable
        pos += 4;
        bus.write_long(pos, 0); // pmReserved
        pos += 4;

        bus.write_long(pos, 0); // ctSeed
        pos += 4;
        bus.write_word(pos, 0x8000); // indexed by entry order
        pos += 2;
        bus.write_word(pos, 1); // two ColorSpec entries
        pos += 2;

        bus.write_word(pos, 0);
        pos += 2;
        bus.write_word(pos, 0);
        pos += 2;
        bus.write_word(pos, 0);
        pos += 2;
        bus.write_word(pos, 0);
        pos += 2;

        bus.write_word(pos, 1);
        pos += 2;
        bus.write_word(pos, 0xFFFF);
        pos += 2;
        bus.write_word(pos, 0);
        pos += 2;
        bus.write_word(pos, 0);
        pos += 2;

        write_rect(&mut bus, pos, 0, 0, 1, 3);
        pos += 8;
        write_rect(&mut bus, pos, 0, 0, 1, 3);
        pos += 8;
        bus.write_word(pos, 0); // srcCopy
        pos += 2;

        bus.write_byte(pos, 11); // PackBits byte count
        pos += 1;
        bus.write_byte(pos, 9); // ten literal bytes follow
        pos += 1;
        for i in 0..10u32 {
            bus.write_byte(pos + i, 1);
        }
        pos += 10;
        bus.write_byte(pos, 0xFF); // EndOfPicture
        pos += 1;
        bus.write_word(pic_data, (pos - pic_data) as u16);

        let (rendered, _) = super::super::pict::draw_picture(
            &mut bus,
            pic_data,
            0,
            0,
            1,
            3,
            (screen_base, screen_row_bytes, 32, 1, 8),
            &device_clut,
            0,
            None,
        );

        assert!(rendered);
        assert_eq!(bus.read_byte(screen_base), 1);
        assert_eq!(bus.read_byte(screen_base + 1), 1);
        assert_eq!(bus.read_byte(screen_base + 2), 1);
        assert_eq!(bus.read_byte(screen_base + 3), 0);
        assert_eq!(bus.read_byte(screen_base + 9), 0);
    }

    #[test]
    fn test_draw_picture_packbits_rect_transparent_preserves_destination() {
        // Transparent mode (36): src pixels that map to index 0 (white)
        // should NOT overwrite the destination.
        let (_d, _cpu, mut bus) = setup();
        let screen_base = 0x300000u32;
        let screen_row_bytes = 32u32;
        // Pre-fill destination: both pixels = 7
        bus.write_byte(screen_base, 7);
        bus.write_byte(screen_base + 1, 7);

        let mut device_clut = [[0u16; 3]; 256];
        device_clut[7] = [0, 0, 0]; // black
        device_clut[42] = [0xFFFF, 0, 0]; // red

        let pic_data = 0x301000u32;
        let mut pos = pic_data;
        bus.write_word(pos, 0); // picSize (updated later)
        pos += 2;
        write_rect(&mut bus, pos, 0, 0, 1, 2); // picFrame
        pos += 8;
        bus.write_byte(pos, 0x98); // PackBitsRect opcode
        pos += 1;

        // PixMap header
        bus.write_word(pos, 0x8002); // rowBytes = 2 | 0x8000
        pos += 2;
        write_rect(&mut bus, pos, 0, 0, 1, 2); // bounds
        pos += 8;
        bus.write_word(pos, 0); // version
        pos += 2;
        bus.write_word(pos, 0); // packType
        pos += 2;
        bus.write_long(pos, 0); // packSize
        pos += 4;
        bus.write_long(pos, 0x00480000); // hRes = 72 dpi
        pos += 4;
        bus.write_long(pos, 0x00480000); // vRes = 72 dpi
        pos += 4;
        bus.write_word(pos, 0); // pixelType = chunky
        pos += 2;
        bus.write_word(pos, 8); // pixelSize = 8bpp
        pos += 2;
        bus.write_word(pos, 1); // cmpCount
        pos += 2;
        bus.write_word(pos, 8); // cmpSize
        pos += 2;
        bus.write_long(pos, 0); // planeBytes
        pos += 4;
        bus.write_long(pos, 0); // pmTable (offset, will use inline CT)
        pos += 4;
        bus.write_long(pos, 0); // pmReserved
        pos += 4;

        // Inline ColorTable: 2 entries
        bus.write_long(pos, 0); // ctSeed
        pos += 4;
        bus.write_word(pos, 0x8000); // ctFlags
        pos += 2;
        bus.write_word(pos, 1); // ctSize = 1 (means 2 entries: 0..1)
        pos += 2;
        // Entry 0: white (this is the "transparent" color)
        bus.write_word(pos, 0); // value
        pos += 2;
        bus.write_word(pos, 0xFFFF);
        pos += 2; // R
        bus.write_word(pos, 0xFFFF);
        pos += 2; // G
        bus.write_word(pos, 0xFFFF);
        pos += 2; // B
                  // Entry 1: red
        bus.write_word(pos, 1);
        pos += 2;
        bus.write_word(pos, 0xFFFF);
        pos += 2; // R
        bus.write_word(pos, 0);
        pos += 2; // G
        bus.write_word(pos, 0);
        pos += 2; // B

        // srcRect, dstRect, mode
        write_rect(&mut bus, pos, 0, 0, 1, 2); // srcRect
        pos += 8;
        write_rect(&mut bus, pos, 0, 0, 1, 2); // dstRect
        pos += 8;
        bus.write_word(pos, 36); // transfer mode = transparent
        pos += 2;

        // Row data: rowBytes=2 < 8, so stored RAW (no PackBits, no byte count).
        // 1 row of 2 pixels: [0, 1]
        bus.write_byte(pos, 0); // pixel[0] = 0 (white = transparent)
        pos += 1;
        bus.write_byte(pos, 1); // pixel[1] = 1 (red = should map to device 42)
        pos += 1;

        // End-of-picture opcode
        bus.write_byte(pos, 0xFF);
        pos += 1;

        bus.write_word(pic_data, (pos - pic_data) as u16); // update picSize

        let (rendered, _) = super::super::pict::draw_picture(
            &mut bus,
            pic_data,
            0,
            0,
            1,
            2,
            (screen_base, screen_row_bytes, 32, 1, 8),
            &device_clut,
            0,
            None,
        );

        assert!(rendered);
        // pixel[0]: src was index 0 (white) → transparent mode should PRESERVE dst
        assert_eq!(
            bus.read_byte(screen_base),
            7,
            "transparent pixel should preserve destination"
        );
        // pixel[1]: src was index 1 (red) → maps to device index 42
        assert_eq!(
            bus.read_byte(screen_base + 1),
            42,
            "non-transparent pixel should be written"
        );
    }

    #[test]
    fn test_draw_picture_screen_backed_port_uses_stable_screen_ctab() {
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let gd_ptr = bus.read_long(gdh);
        let pixmap_handle = bus.read_long(gd_ptr + 22);
        let pixmap_ptr = bus.read_long(pixmap_handle);
        let screen_base = bus.read_long(pixmap_ptr);
        let screen_row_bytes = (bus.read_word(pixmap_ptr + 4) & 0x3FFF) as u32;
        d.screen_mode = (screen_base, screen_row_bytes, 800, 600, 8);

        let target_rgb = [0x1357, 0x2468, 0x369C];
        d.device_clut[7] = [0, 0, 0];
        d.device_clut[42] = target_rgb;
        d.color_manager_clut[7] = [0, 0, 0];
        d.color_manager_clut[42] = target_rgb;

        let port = bus.alloc(64);
        bus.write_long(port + 2, pixmap_handle);
        bus.write_word(port + 6, 0xC000);
        bus.write_word(port + 16, 0);
        bus.write_word(port + 18, 0);
        bus.write_word(port + 20, 600);
        bus.write_word(port + 22, 800);
        make_rgn(&mut bus, 0x320000, 0x320100, 0, 0, 600, 800);
        make_rgn(&mut bus, 0x320200, 0x320300, 0, 0, 600, 800);
        bus.write_long(port + 24, 0x320100);
        bus.write_long(port + 28, 0x320300);
        d.set_current_port_state(&mut bus, &mut cpu, port, Some(gdh));

        let dst_rect = 0x330000u32;
        write_rect(&mut bus, dst_rect, 0, 0, 1, 1);

        let pic_data = 0x331000u32;
        let mut pos = pic_data;
        bus.write_word(pos, 0);
        pos += 2;
        write_rect(&mut bus, pos, 0, 0, 1, 1);
        pos += 8;
        bus.write_byte(pos, 0x98);
        pos += 1;

        bus.write_word(pos, 0x8002);
        pos += 2;
        write_rect(&mut bus, pos, 0, 0, 1, 2);
        pos += 8;
        bus.write_word(pos, 0);
        pos += 2;
        bus.write_word(pos, 0);
        pos += 2;
        bus.write_long(pos, 0);
        pos += 4;
        bus.write_long(pos, 0);
        pos += 4;
        bus.write_long(pos, 0);
        pos += 4;
        bus.write_word(pos, 0);
        pos += 2;
        bus.write_word(pos, 8);
        pos += 2;
        bus.write_word(pos, 1);
        pos += 2;
        bus.write_word(pos, 8);
        pos += 2;
        bus.write_long(pos, 0);
        pos += 4;
        bus.write_long(pos, 0);
        pos += 4;
        bus.write_long(pos, 0);
        pos += 4;

        bus.write_long(pos, 0);
        pos += 4;
        bus.write_word(pos, 0x8000);
        pos += 2;
        bus.write_word(pos, 1);
        pos += 2;

        bus.write_word(pos, 0);
        pos += 2;
        bus.write_word(pos, 0);
        pos += 2;
        bus.write_word(pos, 0);
        pos += 2;
        bus.write_word(pos, 0);
        pos += 2;

        bus.write_word(pos, 1);
        pos += 2;
        bus.write_word(pos, target_rgb[0]);
        pos += 2;
        bus.write_word(pos, target_rgb[1]);
        pos += 2;
        bus.write_word(pos, target_rgb[2]);
        pos += 2;

        write_rect(&mut bus, pos, 0, 0, 1, 1);
        pos += 8;
        write_rect(&mut bus, pos, 0, 0, 1, 1);
        pos += 8;
        bus.write_word(pos, 0);
        pos += 2;

        bus.write_byte(pos, 1);
        pos += 1;
        bus.write_byte(pos, 0);
        pos += 1;
        bus.write_byte(pos, 0xFF);
        pos += 1;
        bus.write_word(pic_data, (pos - pic_data) as u16);

        let pic_handle = 0x332000u32;
        bus.write_long(pic_handle, pic_data);
        bus.write_long(TEST_SP, dst_rect);
        bus.write_long(TEST_SP + 4, pic_handle);

        let result = d.dispatch_quickdraw(true, 0x0F6, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(screen_base), 42);
    }

    #[test]
    fn test_dense_grayscale_pict_clut_does_not_seed_screen_palette() {
        let current = TrapDispatcher::standard_mac_8bpp_clut();
        let mut pict = [[0u16; 3]; 256];
        for (index, rgb) in pict.iter_mut().enumerate().take(192) {
            let value = 0xFFFFu16.saturating_sub((index as u16) * 0x0101);
            *rgb = [value, value, value];
        }
        assert!(!TrapDispatcher::should_seed_screen_palette_from_pict(
            &pict, &current
        ));
    }

    #[test]
    fn test_sparse_grayscale_pict_clut_does_not_seed_screen_palette() {
        let current = TrapDispatcher::standard_mac_8bpp_clut();
        let mut pict = [[0u16; 3]; 256];
        pict[0] = [0xFFFF, 0xFFFF, 0xFFFF];
        pict[1] = [0x8888, 0x8888, 0x8888];
        pict[2] = [0x5555, 0x5555, 0x5555];
        assert!(!TrapDispatcher::should_seed_screen_palette_from_pict(
            &pict, &current
        ));
    }

    #[test]
    fn test_custom_screen_palette_is_not_reseeded_from_pict() {
        let mut current = TrapDispatcher::standard_mac_8bpp_clut();
        current[42] = [0x2222, 0x3333, 0x4444];
        let mut pict = [[0u16; 3]; 256];
        for (index, rgb) in pict.iter_mut().enumerate().take(192) {
            let value = 0xFFFFu16.saturating_sub((index as u16) * 0x0101);
            *rgb = [value, value, value];
        }
        assert!(!TrapDispatcher::should_seed_screen_palette_from_pict(
            &pict, &current
        ));
    }

    #[test]
    fn test_scaled_canonical_screen_palette_is_not_reseeded_from_dense_grayscale_pict() {
        let mut current = TrapDispatcher::standard_mac_8bpp_clut();
        for rgb in &mut current {
            rgb[0] /= 2;
            rgb[1] /= 2;
            rgb[2] /= 2;
        }
        let mut pict = [[0u16; 3]; 256];
        for (index, rgb) in pict.iter_mut().enumerate().take(192) {
            let value = 0xFFFFu16.saturating_sub((index as u16) * 0x0101);
            *rgb = [value, value, value];
        }

        assert!(!TrapDispatcher::should_seed_screen_palette_from_pict(
            &pict, &current
        ));
    }

    #[test]
    fn test_offscreen_pict_indices_not_preserved_against_system_palette() {
        let current = TrapDispatcher::standard_mac_8bpp_clut();
        let mut pict = [[0u16; 3]; 256];
        for (index, rgb) in pict.iter_mut().enumerate().take(128) {
            *rgb = [
                ((index as u16) << 8) | index as u16,
                0x8000u16.saturating_sub((index as u16) << 6),
                0x1000u16.saturating_add((index as u16) << 5),
            ];
        }

        assert!(
            !TrapDispatcher::should_preserve_offscreen_picture_indices_from_pict(
                &pict, &current, &current
            )
        );
    }

    #[test]
    fn test_offscreen_pict_indices_preserved_against_custom_palette() {
        let mut current = TrapDispatcher::standard_mac_8bpp_clut();
        current[42] = [0x1234, 0x5678, 0x9ABC];
        let mut logical = current;
        logical[43] = [0x2222, 0x3333, 0x4444];
        let mut pict = [[0u16; 3]; 256];
        for (index, rgb) in pict.iter_mut().enumerate().take(128) {
            *rgb = [
                ((index as u16) << 8) | index as u16,
                0x8000u16.saturating_sub((index as u16) << 6),
                0x1000u16.saturating_add((index as u16) << 5),
            ];
        }

        assert!(
            TrapDispatcher::should_preserve_offscreen_picture_indices_from_pict(
                &pict, &current, &logical
            )
        );
    }

    #[test]
    fn test_offscreen_pict_indices_not_preserved_when_port_matches_screen_palette() {
        let mut current = TrapDispatcher::standard_mac_8bpp_clut();
        current[42] = [0x1234, 0x5678, 0x9ABC];
        let logical = current;
        let mut pict = [[0u16; 3]; 256];
        for (index, rgb) in pict.iter_mut().enumerate().take(128) {
            *rgb = [
                ((index as u16) << 8) | index as u16,
                0x8000u16.saturating_sub((index as u16) << 6),
                0x1000u16.saturating_add((index as u16) << 5),
            ];
        }

        assert!(
            !TrapDispatcher::should_preserve_offscreen_picture_indices_from_pict(
                &pict, &current, &logical
            )
        );
    }

    #[test]
    fn test_offscreen_pict_indices_not_preserved_when_logical_screen_palette_is_canonical() {
        let mut current = TrapDispatcher::standard_mac_8bpp_clut();
        current[42] = [0x1234, 0x5678, 0x9ABC];
        let logical = TrapDispatcher::standard_mac_8bpp_clut();
        let mut pict = [[0u16; 3]; 256];
        for (index, rgb) in pict.iter_mut().enumerate().take(128) {
            *rgb = [
                ((index as u16) << 8) | index as u16,
                0x8000u16.saturating_sub((index as u16) << 6),
                0x1000u16.saturating_add((index as u16) << 5),
            ];
        }

        assert!(
            !TrapDispatcher::should_preserve_offscreen_picture_indices_from_pict(
                &pict, &current, &logical
            )
        );
    }

    #[test]
    fn test_preserve_seeded_picture_palette_scales_seeded_device_clut() {
        let (mut d, _cpu, mut bus) = setup();
        let table_ptr = 0x333000u32;
        let canonical = TrapDispatcher::standard_mac_8bpp_clut();
        for (index, rgb) in canonical.iter().enumerate() {
            let entry = table_ptr + (index as u32) * 8;
            bus.write_word(entry, index as u16);
            bus.write_word(entry + 2, rgb[0] / 2);
            bus.write_word(entry + 4, rgb[1] / 2);
            bus.write_word(entry + 6, rgb[2] / 2);
        }

        let mut seeded = [[0u16; 3]; 256];
        for (index, rgb) in seeded.iter_mut().enumerate().take(192).skip(2) {
            let value = 0xFFFFu16.saturating_sub((index as u16) * 0x0080);
            *rgb = [value, value, value];
        }
        seeded[0] = [0xEEEE, 0xEEEE, 0xEEEE];
        seeded[1] = [0x4444, 0x1111, 0x1111];
        seeded[42] = [0x8888, 0x6666, 0x4444];
        seeded[128] = [0x2222, 0x1111, 0x7777];
        seeded[255] = [0, 0, 0];

        d.color_manager_clut = seeded;
        d.seeded_picture_palette = seeded;
        d.seeded_picture_palette_until_tick = 99;
        d.tick_count = 50;

        d.apply_set_entries_with_gdevice(&mut bus, table_ptr, 0, 255);

        assert_eq!(d.device_clut[0], [0x7777, 0x7777, 0x7777]);
        assert_eq!(d.device_clut[1], [0x2222, 0x0888, 0x0888]);
        assert_eq!(d.device_clut[42], [0x4444, 0x3333, 0x2222]);
        assert_eq!(d.device_clut[128], [0x1111, 0x0888, 0x3BBB]);
        assert_eq!(d.device_clut[255], [0, 0, 0]);
        assert_eq!(d.color_manager_clut[42], seeded[42]);
    }

    #[test]
    fn test_preserve_seeded_picture_palette_survives_stable_palette_drift() {
        let (mut d, _cpu, mut bus) = setup();
        let table_ptr = 0x334000u32;
        let canonical = TrapDispatcher::standard_mac_8bpp_clut();
        for (index, rgb) in canonical.iter().enumerate() {
            let entry = table_ptr + (index as u32) * 8;
            bus.write_word(entry, index as u16);
            bus.write_word(entry + 2, rgb[0] / 2);
            bus.write_word(entry + 4, rgb[1] / 2);
            bus.write_word(entry + 6, rgb[2] / 2);
        }

        let mut seeded = [[0u16; 3]; 256];
        seeded[0] = [0xE000, 0xE000, 0xE000];
        seeded[1] = [0x4444, 0x1111, 0x1111];
        seeded[42] = [0x8888, 0x6666, 0x4444];
        seeded[128] = [0x2222, 0x1111, 0x7777];
        seeded[255] = [0, 0, 0];

        d.color_manager_clut = TrapDispatcher::standard_mac_8bpp_clut();
        d.seeded_picture_palette = seeded;
        d.seeded_picture_palette_until_tick = 99;
        d.tick_count = 50;

        d.apply_set_entries_with_gdevice(&mut bus, table_ptr, 0, 255);

        assert_eq!(d.device_clut[0], [0x7000, 0x7000, 0x7000]);
        assert_eq!(d.device_clut[42], [0x4444, 0x3333, 0x2222]);
        assert_eq!(d.color_manager_clut[42], seeded[42]);
        assert_eq!(d.color_manager_clut[128], seeded[128]);
    }

    #[test]
    fn test_palette_strict_mode_bypasses_seeded_picture_preservation() {
        let (mut d, _cpu, mut bus) = setup();
        let table_ptr = 0x335000u32;
        let canonical = TrapDispatcher::standard_mac_8bpp_clut();
        for (index, rgb) in canonical.iter().enumerate() {
            let entry = table_ptr + (index as u32) * 8;
            bus.write_word(entry, index as u16);
            bus.write_word(entry + 2, rgb[0] / 2);
            bus.write_word(entry + 4, rgb[1] / 2);
            bus.write_word(entry + 6, rgb[2] / 2);
        }

        let mut seeded = [[0u16; 3]; 256];
        seeded[0] = [0xEEEE, 0xEEEE, 0xEEEE];
        seeded[1] = [0x4444, 0x1111, 0x1111];
        seeded[42] = [0x8888, 0x6666, 0x4444];
        seeded[128] = [0x2222, 0x1111, 0x7777];
        seeded[255] = [0, 0, 0];

        d.color_manager_clut = seeded;
        d.seeded_picture_palette = seeded;
        d.seeded_picture_palette_until_tick = 99;
        d.tick_count = 50;

        d.apply_set_entries_with_gdevice_mode(&mut bus, table_ptr, 0, 255, true);

        assert_eq!(d.device_clut[0], [canonical[0][0] / 2; 3]);
        assert_eq!(
            d.device_clut[42],
            [
                canonical[42][0] / 2,
                canonical[42][1] / 2,
                canonical[42][2] / 2
            ]
        );
        assert_eq!(
            d.color_manager_clut[42],
            [
                canonical[42][0] / 2,
                canonical[42][1] / 2,
                canonical[42][2] / 2
            ]
        );
        assert_ne!(d.color_manager_clut[42], seeded[42]);
    }

    #[test]
    fn test_palette_strict_mode_publishes_dimmed_full_replace() {
        let (mut d, _cpu, mut bus) = setup();
        let table_ptr = 0x336000u32;
        let canonical = TrapDispatcher::standard_mac_8bpp_clut();
        for (index, rgb) in canonical.iter().enumerate() {
            let entry = table_ptr + (index as u32) * 8;
            bus.write_word(entry, index as u16);
            bus.write_word(entry + 2, rgb[0] / 2);
            bus.write_word(entry + 4, rgb[1] / 2);
            bus.write_word(entry + 6, rgb[2] / 2);
        }

        d.color_manager_clut = TrapDispatcher::standard_mac_8bpp_clut();

        d.apply_set_entries_with_gdevice_mode(&mut bus, table_ptr, 0, 255, true);

        let expected7 = [
            canonical[7][0] / 2,
            canonical[7][1] / 2,
            canonical[7][2] / 2,
        ];
        let expected42 = [
            canonical[42][0] / 2,
            canonical[42][1] / 2,
            canonical[42][2] / 2,
        ];
        assert_eq!(d.device_clut[7], expected7);
        assert_eq!(d.device_clut[42], expected42);
        assert_eq!(d.color_manager_clut[7], expected7);
        assert_eq!(d.color_manager_clut[42], expected42);
    }

    #[test]
    fn test_setentries_publishes_screen_palette_when_offscreen_gdevice_current() {
        let (mut d, _cpu, mut bus) = setup_with_port();
        let main_gdh = d.ensure_main_gdevice(&mut bus);
        let main_gd = bus.read_long(main_gdh);
        let main_pm_handle = bus.read_long(main_gd + 22);
        let main_pm = bus.read_long(main_pm_handle);
        let main_ctab_handle = bus.read_long(main_pm + 42);
        let main_ctab = bus.read_long(main_ctab_handle);

        let offscreen_pixmap = 0x337000u32;
        let offscreen_ctab_handle = 0x337200u32;
        let offscreen_pm_handle = 0x337400u32;
        let offscreen_gd = 0x337500u32;
        let offscreen_gdh = 0x337600u32;
        write_color_table(
            &mut bus,
            offscreen_ctab_handle,
            0,
            &[(0, 0x1111, 0x2222, 0x3333)],
        );
        write_pixmap_8(
            &mut bus,
            offscreen_pixmap,
            0x338000,
            1,
            1,
            offscreen_ctab_handle,
        );
        bus.write_long(offscreen_pm_handle, offscreen_pixmap);
        bus.write_long(offscreen_gd + 22, offscreen_pm_handle);
        bus.write_long(offscreen_gdh, offscreen_gd);
        d.current_gdevice = offscreen_gdh;

        let table_ptr = 0x339000u32;
        for index in 0..256u32 {
            let entry = table_ptr + index * 8;
            bus.write_word(entry, index as u16);
            bus.write_word(entry + 2, (index as u16) << 8);
            bus.write_word(entry + 4, ((255 - index) as u16) << 8);
            bus.write_word(entry + 6, 0x5500);
        }

        d.apply_set_entries_with_gdevice(&mut bus, table_ptr, 0, 255);

        assert_eq!(d.device_clut[42], [0x2A00, 0xD500, 0x5500]);
        assert_eq!(d.color_manager_clut[42], [0x2A00, 0xD500, 0x5500]);
        let main_entry_42 = main_ctab + 8 + 42 * 8;
        assert_eq!(bus.read_word(main_entry_42 + 2), 0x2A00);
        assert_eq!(bus.read_word(main_entry_42 + 4), 0xD500);
        assert_eq!(bus.read_word(main_entry_42 + 6), 0x5500);
        let offscreen_ctab = bus.read_long(offscreen_ctab_handle);
        assert_eq!(bus.read_word(offscreen_ctab + 8 + 2), 0x1111);
    }

    #[test]
    fn test_title_seed_reuses_active_seeded_picture_window() {
        let (mut d, _cpu, _bus) = setup();
        let mut seeded = [[0u16; 3]; 256];
        seeded[0] = [0xEEEE, 0xEEEE, 0xEEEE];
        seeded[42] = [0x8888, 0x6666, 0x4444];
        seeded[255] = [0, 0, 0];

        d.seeded_picture_palette = seeded;
        d.seeded_picture_palette_until_tick = 90;
        d.tick_count = 69;

        assert_eq!(d.seeded_picture_palette_until_tick_for_seed(48, true), 90);
    }

    #[test]
    fn test_title_seed_starts_new_window_without_active_seeded_picture() {
        let (mut d, _cpu, _bus) = setup();
        d.seeded_picture_palette = TrapDispatcher::standard_mac_8bpp_clut();
        d.seeded_picture_palette_until_tick = 0;
        d.tick_count = 69;

        assert_eq!(d.seeded_picture_palette_until_tick_for_seed(48, true), 117);
    }

    #[test]
    fn test_recent_resource_ctable_fetch_consumed_for_immediate_screen_drawpicture() {
        let (mut d, _cpu, mut bus) = setup();
        d.screen_mode = (0x00ABC000, 800, 800, 600, 8);
        d.current_port = 0x00284CFC;
        d.tick_count = 33;
        d.trap_count = 400;

        let ctab_ptr = bus.alloc(8 + 256 * 8);
        bus.write_long(ctab_ptr, 1);
        bus.write_word(ctab_ptr + 4, 0);
        bus.write_word(ctab_ptr + 6, 255);
        let entry = ctab_ptr + 8 + 42 * 8;
        bus.write_word(entry, 42);
        bus.write_word(entry + 2, 0x1111);
        bus.write_word(entry + 4, 0x2222);
        bus.write_word(entry + 6, 0x3333);
        let ctab_handle = bus.alloc(4);
        bus.write_long(ctab_handle, ctab_ptr);

        d.remember_recent_resource_ctable_fetch(1001, ctab_handle);

        let fetch = d
            .take_recent_drawpicture_resource_ctable_fetch(
                d.current_port,
                d.screen_mode.0,
                d.screen_mode.1,
                8,
            )
            .expect("immediate screen DrawPicture should consume the fetched CLUT");

        assert_eq!(fetch.ct_id, 1001);
        assert_eq!(fetch.ctab_handle, ctab_handle);
        assert!(d.recent_resource_ctable_fetch.is_none());
    }

    #[test]
    fn test_recent_resource_ctable_fetch_consumed_for_immediate_offscreen_drawpicture() {
        let (mut d, _cpu, mut bus) = setup();
        d.screen_mode = (0x00ABC000, 800, 800, 600, 8);
        d.current_port = 0x00284CFC;
        d.tick_count = 33;
        d.trap_count = 400;

        let ctab_ptr = bus.alloc(8 + 256 * 8);
        bus.write_long(ctab_ptr, 1);
        bus.write_word(ctab_ptr + 4, 0);
        bus.write_word(ctab_ptr + 6, 255);
        let ctab_handle = bus.alloc(4);
        bus.write_long(ctab_handle, ctab_ptr);

        d.remember_recent_resource_ctable_fetch(1001, ctab_handle);

        let fetch =
            d.take_recent_drawpicture_resource_ctable_fetch(d.current_port, 0x00ABD000, 512, 8);

        assert_eq!(
            fetch
                .expect("immediate same-port offscreen DrawPicture should consume the fetched CLUT")
                .ct_id,
            1001
        );
        assert!(d.recent_resource_ctable_fetch.is_none());
    }

    #[test]
    fn test_recent_resource_ctable_fetch_ignored_for_different_drawpicture_port() {
        let (mut d, _cpu, mut bus) = setup();
        d.screen_mode = (0x00ABC000, 800, 800, 600, 8);
        d.current_port = 0x00284CFC;
        d.tick_count = 33;
        d.trap_count = 400;

        let ctab_ptr = bus.alloc(8 + 256 * 8);
        bus.write_long(ctab_ptr, 1);
        bus.write_word(ctab_ptr + 4, 0);
        bus.write_word(ctab_ptr + 6, 255);
        let ctab_handle = bus.alloc(4);
        bus.write_long(ctab_handle, ctab_ptr);

        d.remember_recent_resource_ctable_fetch(1001, ctab_handle);

        let fetch = d.take_recent_drawpicture_resource_ctable_fetch(0x00284D00, 0x00ABD000, 512, 8);

        assert!(fetch.is_none());
        assert!(d.recent_resource_ctable_fetch.is_some());
    }

    #[test]
    fn test_fetched_ctable_offscreen_drawpicture_publishes_application_palette() {
        let (mut d, _cpu, mut bus) = setup();
        let main_gdh = d.ensure_main_gdevice(&mut bus);

        let mut active_clut = TrapDispatcher::standard_mac_8bpp_clut();
        active_clut[8] = [0xFFFF, 0xCCCC, 0x0000];
        active_clut[9] = [0xB332, 0x0000, 0xFFFF];
        active_clut[42] = [0x1111, 0x2222, 0x3333];
        d.install_application_clut(&mut bus, active_clut);

        let mut fetched_entries = [[0u16; 3]; 16];
        fetched_entries[0] = [0xFFFF, 0xFFFF, 0xFFFF];
        fetched_entries[8] = [0x3333, 0xFFFF, 0x3333];
        fetched_entries[9] = [0x0000, 0x9999, 0x0000];
        fetched_entries[15] = [0x0000, 0x0000, 0x0000];
        let fetched_ctab = make_test_ctab_handle(&mut bus, &fetched_entries, 0x2222_2222, 0);
        let fetched_clut = d.read_ctab_handle_clut(&bus, fetched_ctab);

        let offscreen_ctab =
            d.allocate_color_table_handle_with_clut(&mut bus, 8, &active_clut, 0x8000);
        let pixmap = bus.alloc(50);
        write_pixmap_8(&mut bus, pixmap, 0x0030_0000, 64, 64, offscreen_ctab);
        let pixmap_handle = bus.alloc(4);
        bus.write_long(pixmap_handle, pixmap);
        let port = bus.alloc(170);
        bus.write_word(port + 6, 0xC000);
        bus.write_long(port + 2, pixmap_handle);
        let gdh = d.create_offscreen_gdevice(&mut bus, pixmap_handle, 0, 0, 64, 64);
        d.gworld_devices.insert(port, gdh);
        d.current_port = port;
        d.current_gdevice = gdh;

        let fetch = RecentColorTableFetch {
            ct_id: 131,
            ctab_handle: fetched_ctab,
            port,
            tick: d.tick_count,
        };
        d.apply_fetched_ctable_to_offscreen_drawpicture_port(
            &mut bus,
            port,
            offscreen_ctab,
            &fetch,
            &fetched_clut,
        );

        assert_eq!(d.device_clut[8], [0x3333, 0xFFFF, 0x3333]);
        assert_eq!(d.color_manager_clut[9], [0x0000, 0x9999, 0x0000]);

        let main_ctab = TrapDispatcher::gdevice_ctab_handle(&bus, main_gdh);
        let main_ctab_ptr = bus.read_long(main_ctab);
        let main_entry8 = main_ctab_ptr + 8 + 8 * 8;
        assert_eq!(bus.read_word(main_entry8 + 2), 0x3333);
        assert_eq!(bus.read_word(main_entry8 + 4), 0xFFFF);
        assert_eq!(bus.read_word(main_entry8 + 6), 0x3333);

        let offscreen_ctab_ptr = bus.read_long(offscreen_ctab);
        let offscreen_entry9 = offscreen_ctab_ptr + 8 + 9 * 8;
        assert_eq!(bus.read_word(offscreen_entry9 + 2), 0x0000);
        assert_eq!(bus.read_word(offscreen_entry9 + 4), 0x9999);
        assert_eq!(bus.read_word(offscreen_entry9 + 6), 0x0000);
    }

    #[test]
    fn test_recent_resource_ctable_fetch_survives_intervening_traps_in_same_tick() {
        let (mut d, _cpu, mut bus) = setup();
        d.screen_mode = (0x00ABC000, 800, 800, 600, 8);
        d.current_port = 0x00284CFC;
        d.tick_count = 33;
        d.trap_count = 400;

        let ctab_ptr = bus.alloc(8 + 256 * 8);
        bus.write_long(ctab_ptr, 1);
        bus.write_word(ctab_ptr + 4, 0);
        bus.write_word(ctab_ptr + 6, 255);
        let ctab_handle = bus.alloc(4);
        bus.write_long(ctab_handle, ctab_ptr);

        d.remember_recent_resource_ctable_fetch(1001, ctab_handle);
        d.trap_count = 404;

        let fetch = d.take_recent_drawpicture_resource_ctable_fetch(
            d.current_port,
            d.screen_mode.0,
            d.screen_mode.1,
            8,
        );

        assert_eq!(
            fetch
                .expect("same-tick screen DrawPicture should still consume the fetched CLUT")
                .ct_id,
            1001
        );
        assert!(d.recent_resource_ctable_fetch.is_none());
    }

    #[test]
    fn test_recent_resource_ctable_fetch_expires_after_tick_advances() {
        let (mut d, _cpu, mut bus) = setup();
        d.screen_mode = (0x00ABC000, 800, 800, 600, 8);
        d.current_port = 0x00284CFC;
        d.tick_count = 33;
        d.trap_count = 400;

        let ctab_ptr = bus.alloc(8 + 256 * 8);
        bus.write_long(ctab_ptr, 1);
        bus.write_word(ctab_ptr + 4, 0);
        bus.write_word(ctab_ptr + 6, 255);
        let ctab_handle = bus.alloc(4);
        bus.write_long(ctab_handle, ctab_ptr);

        d.remember_recent_resource_ctable_fetch(1001, ctab_handle);
        d.tick_count = 35;

        let fetch = d.take_recent_drawpicture_resource_ctable_fetch(
            d.current_port,
            d.screen_mode.0,
            d.screen_mode.1,
            8,
        );

        assert!(fetch.is_none());
        assert!(d.recent_resource_ctable_fetch.is_none());
    }

    #[test]
    fn openpicture_returns_non_nil_pichandle_for_requested_picframe() {
        // IM:I 1985 p. I-189: OpenPicture returns a handle to a new
        // picture with the requested picture-frame rectangle.
        let (mut d, mut cpu, mut bus) = setup();
        let pic_frame = 0x300000u32;
        write_rect(&mut bus, pic_frame, 7, 11, 57, 91);
        bus.write_long(TEST_SP, pic_frame);

        let result = d.dispatch_quickdraw(true, 0x0F3, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        let handle = bus.read_long(TEST_SP + 4);
        assert_ne!(handle, 0);
        let pic_ptr = bus.read_long(handle);
        assert_ne!(pic_ptr, 0);
        assert_eq!(read_rect(&bus, pic_ptr + 2), (7, 11, 57, 91));
    }

    #[test]
    fn openpicture_consumes_picframe_pointer_and_returns_result_on_stack() {
        // IM:I 1985 p. I-189 signature:
        //   FUNCTION OpenPicture(picFrame: Rect): PicHandle;
        // Trap ABI contract: one pointer argument consumed from A7.
        let (mut d, mut cpu, mut bus) = setup();
        let pic_frame = 0x300100u32;
        write_rect(&mut bus, pic_frame, 0, 0, 10, 10);

        let sp_before = cpu.read_reg(Register::A7);
        bus.write_long(sp_before, pic_frame);
        let result = d.dispatch_quickdraw(true, 0x0F3, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before + 4);
        assert_ne!(bus.read_long(sp_before + 4), 0);
    }

    #[test]
    fn opencpicture_returns_nil_handle_and_writes_nil_in_d0() {
        // IM:V p. V-89: OpenCPicture returns a NIL PicHandle when
        // color-PICT recording is unavailable.
        let (mut d, mut cpu, mut bus) = setup();
        let header_ptr = 0x300300u32;

        bus.fill_zeros(header_ptr, 24);
        write_rect(&mut bus, header_ptr, 0, 0, 16, 16);
        bus.write_long(header_ptr + 8, 0);
        bus.write_long(header_ptr + 12, 0);
        bus.write_word(header_ptr + 16, 1);
        bus.write_word(header_ptr + 18, 0);
        bus.write_long(header_ptr + 20, 0);
        bus.write_long(TEST_SP, header_ptr);

        let result = d.dispatch_quickdraw(true, 0x220, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(TEST_SP + 4), 0);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    #[test]
    fn closepicture_procedure_signature_consumes_no_stack_arguments() {
        // IM:I 1985 p. I-189 signature:
        //   PROCEDURE ClosePicture;
        let (mut d, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        bus.write_long(sp_before, 0x12345678);

        let result = d.dispatch_quickdraw(true, 0x0F4, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before);
        assert_eq!(bus.read_long(sp_before), 0x12345678);
    }

    #[test]
    fn closepicture_ends_current_openpicture_recording_session() {
        // IM:I 1985 p. I-189: ClosePicture stops saving drawing
        // operations/comments as the current picture definition.
        let (mut d, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc(64 * 32);
        bus.fill_zeros(screen_base, 64 * 32);
        d.screen_mode = (screen_base, 64, 64, 32, 1);

        let pic_frame = 0x300200u32;
        write_rect(&mut bus, pic_frame, 0, 0, 8, 16);
        bus.write_long(TEST_SP, pic_frame);
        let open_result = d.dispatch_quickdraw(true, 0x0F3, &mut cpu, &mut bus);
        assert!(open_result.unwrap().is_ok());

        let handle = bus.read_long(TEST_SP + 4);
        let old_pic_ptr = bus.read_long(handle);
        assert!(d.recording_picture.is_some());

        let sp_before_close = cpu.read_reg(Register::A7);
        let close_result = d.dispatch_quickdraw(true, 0x0F4, &mut cpu, &mut bus);
        assert!(close_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before_close);
        assert!(d.recording_picture.is_none());

        let finalized_pic_ptr = bus.read_long(handle);
        assert_ne!(finalized_pic_ptr, 0);
        assert_ne!(finalized_pic_ptr, old_pic_ptr);
        assert!(bus.read_word(finalized_pic_ptr) > 10);
        assert_eq!(read_rect(&bus, finalized_pic_ptr + 2), (0, 0, 8, 16));
    }

    #[test]
    fn killpicture_releases_picture_data_and_handle_memory() {
        // IM:I 1985 p. I-190: KillPicture releases memory occupied by the picture.
        let (mut d, mut cpu, mut bus) = setup();
        let pic_ptr = bus.alloc(64);
        let pic_handle = bus.alloc(4);
        bus.write_long(pic_handle, pic_ptr);
        assert!(bus.get_alloc_size(pic_ptr).is_some());
        assert!(bus.get_alloc_size(pic_handle).is_some());

        bus.write_long(TEST_SP, pic_handle);
        let result = d.dispatch_quickdraw(true, 0x0F5, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert!(bus.get_alloc_size(pic_ptr).is_none());
        assert!(bus.get_alloc_size(pic_handle).is_none());
    }

    #[test]
    fn killpicture_consumes_pichandle_argument() {
        // IM:I 1985 p. I-190 signature:
        //   PROCEDURE KillPicture(myPicture: PicHandle);
        let (mut d, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        bus.write_long(sp_before, 0);

        let result = d.dispatch_quickdraw(true, 0x0F5, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before + 4);
    }

    // ==================== Color QD Extended ====================

    #[test]
    fn test_get_ct_seed() {
        let (mut d, mut cpu, mut bus) = setup();
        let result = d.dispatch_quickdraw(true, 0x228, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_long(TEST_SP), 1);
    }

    #[test]
    fn getctseed_pascal_function_preserves_stack_across_five_calls() {
        // Mirrors band B2 of aa28_getctseed_strict catalogue proof.
        // Per IM:V V-143 each GetCTSeed() call is a Pascal FUNCTION
        // pop-0 + 4-byte result slot — caller pre-pushes 4 bytes, trap
        // writes the LONGINT at [A7], caller pops 4 bytes. Five
        // successive calls each preserve A7 across the trap-side
        // dispatch AND each call writes a distinct, non-zero seed.
        let (mut d, mut cpu, mut bus) = setup();
        let sp_pre_composition = cpu.read_reg(Register::A7);
        let mut seeds = [0u32; 5];
        for i in 0..5u32 {
            let sp_before_call = cpu.read_reg(Register::A7);
            // Caller pre-pushes 4-byte LONGINT result slot poisoned
            // with a per-call sentinel so the dispatch is forced to
            // overwrite it with the actual seed.
            let new_sp = sp_before_call - 4;
            cpu.write_reg(Register::A7, new_sp);
            bus.write_long(new_sp, 0xDEAD_BEEF_u32.wrapping_add(i));

            let result = d.dispatch_quickdraw(true, 0x228, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(
                cpu.read_reg(Register::A7),
                new_sp,
                "GetCTSeed call {i} should pop 0 argument bytes",
            );
            let seed = bus.read_long(new_sp);
            assert_ne!(
                seed, 0,
                "GetCTSeed call {i} should write a non-zero seed per IM:V V-143",
            );
            seeds[i as usize] = seed;

            // Caller pops the 4-byte result slot.
            cpu.write_reg(Register::A7, new_sp + 4);
        }
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_pre_composition,
            "5-call GetCTSeed composition should net A7-zero across the C-level sequence",
        );
        // Each call should produce a distinct seed per IM:V V-143 (the
        // seed is documented as unique so a fresh table is recognised
        // as distinct from any existing table).
        for i in 0..5 {
            for j in (i + 1)..5 {
                assert_ne!(
                    seeds[i], seeds[j],
                    "GetCTSeed seeds [{i}]={} and [{j}]={} should be distinct",
                    seeds[i], seeds[j]
                );
            }
        }
    }

    #[test]
    fn test_dispose_ctable_releases_colortable_handle_and_data() {
        let (mut d, mut cpu, mut bus) = setup();
        let ct_ptr = bus.alloc(8 + 8);
        let ct_handle = bus.alloc(4);
        bus.write_long(ct_handle, ct_ptr);
        assert!(bus.get_alloc_size(ct_ptr).is_some());
        assert!(bus.get_alloc_size(ct_handle).is_some());

        bus.write_long(TEST_SP, ct_handle);
        let result = d.dispatch_quickdraw(true, 0x224, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert!(
            bus.get_alloc_size(ct_ptr).is_none(),
            "DisposeCTable must release the ColorTable data allocation (Imaging With QuickDraw 1994, p. 4-93)"
        );
        assert!(
            bus.get_alloc_size(ct_handle).is_none(),
            "DisposeCTable must release the CTabHandle allocation (Imaging With QuickDraw 1994, p. 4-93)"
        );
    }

    #[test]
    fn realcolor_true_when_top_4_bits_match_clut_entry() {
        // IM:V 1986 p. V-141: with iTabRes=4, RealColor is TRUE when
        // some table entry matches the top 4 bits of each RGB component.
        let (mut d, mut cpu, mut bus) = setup();
        d.device_clut = [[0x0000, 0x0000, 0x0000]; 256];
        d.device_clut[42] = [0xA111, 0xB222, 0xC333];

        let rgb_ptr = 0x300300u32;
        bus.write_word(rgb_ptr, 0xAFFF);
        bus.write_word(rgb_ptr + 2, 0xB001);
        bus.write_word(rgb_ptr + 4, 0xCABC);
        bus.write_long(TEST_SP, rgb_ptr);

        let result = d.dispatch_quickdraw(true, 0x236, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(TEST_SP + 4), 0x0100);
    }

    #[test]
    fn realcolor_false_when_no_top_4_bit_match_exists() {
        // IM:V 1986 p. V-141: RealColor reports FALSE when the current
        // device table has no entry matching the requested RGB at the
        // active inverse-table resolution.
        let (mut d, mut cpu, mut bus) = setup();
        d.device_clut = [[0x1000, 0x2000, 0x3000]; 256];

        let rgb_ptr = 0x300340u32;
        bus.write_word(rgb_ptr, 0xA000);
        bus.write_word(rgb_ptr + 2, 0xB000);
        bus.write_word(rgb_ptr + 4, 0xC000);
        bus.write_long(TEST_SP, rgb_ptr);

        let result = d.dispatch_quickdraw(true, 0x236, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(TEST_SP + 4), 0x0000);
    }

    #[test]
    fn protectentry_true_marks_entry_and_blocks_setentries_overwrite() {
        // IM:V 1986 p. V-143: ProtectEntry(TRUE) protects a CLUT entry
        // so SetEntries cannot change that entry.
        let (mut d, mut cpu, mut bus) = setup();
        d.device_clut[7] = [0x1111, 0x2222, 0x3333];

        bus.write_word(TEST_SP, 0x0100); // protect=TRUE
        bus.write_word(TEST_SP + 2, 7u16); // index
        let protect_result = d.dispatch_quickdraw(true, 0x23D, &mut cpu, &mut bus);
        assert!(protect_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert!(d.clut_protected[7]);

        let table_ptr = 0x300380u32;
        bus.write_word(table_ptr, 7); // value (ignored in sequence mode)
        bus.write_word(table_ptr + 2, 0xAAAA);
        bus.write_word(table_ptr + 4, 0xBBBB);
        bus.write_word(table_ptr + 6, 0xCCCC);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, table_ptr); // aTable
        bus.write_word(TEST_SP + 4, 0u16); // count
        bus.write_word(TEST_SP + 6, 7u16); // start
        let set_result = d.dispatch_quickdraw(true, 0x23F, &mut cpu, &mut bus);
        assert!(set_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(d.device_clut[7], [0x1111, 0x2222, 0x3333]);
    }

    #[test]
    fn protectentry_false_clears_protection_and_allows_setentries_overwrite() {
        // IM:V 1986 p. V-143: ProtectEntry(FALSE) removes protection.
        let (mut d, mut cpu, mut bus) = setup();
        d.device_clut[7] = [0x1111, 0x2222, 0x3333];
        d.clut_protected[7] = true;

        bus.write_word(TEST_SP, 0x0000); // protect=FALSE
        bus.write_word(TEST_SP + 2, 7u16); // index
        let unprotect_result = d.dispatch_quickdraw(true, 0x23D, &mut cpu, &mut bus);
        assert!(unprotect_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert!(!d.clut_protected[7]);

        let table_ptr = 0x3003C0u32;
        bus.write_word(table_ptr, 7);
        bus.write_word(table_ptr + 2, 0xAAAA);
        bus.write_word(table_ptr + 4, 0xBBBB);
        bus.write_word(table_ptr + 6, 0xCCCC);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, table_ptr); // aTable
        bus.write_word(TEST_SP + 4, 0u16); // count
        bus.write_word(TEST_SP + 6, 7u16); // start
        let set_result = d.dispatch_quickdraw(true, 0x23F, &mut cpu, &mut bus);
        assert!(set_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(d.device_clut[7], [0xAAAA, 0xBBBB, 0xCCCC]);
    }

    #[test]
    fn reserveentry_true_marks_entry_and_color2index_skips_it() {
        // IM:V 1986 p. V-143: a reserved entry is not returned by
        // Color2Index or other search procedures.
        let (mut d, mut cpu, mut bus) = setup();
        d.device_clut = [[0x0000, 0x0000, 0x0000]; 256];
        d.device_clut[3] = [0x4444, 0x5555, 0x6666];
        d.device_clut[9] = [0x4444, 0x5555, 0x6666];

        bus.write_word(TEST_SP, 0x0100); // reserve=TRUE
        bus.write_word(TEST_SP + 2, 3u16); // index
        let reserve_result = d.dispatch_quickdraw(true, 0x23E, &mut cpu, &mut bus);
        assert!(reserve_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert!(d.clut_reserved[3]);

        let rgb_ptr = 0x300400u32;
        bus.write_word(rgb_ptr, 0x4444);
        bus.write_word(rgb_ptr + 2, 0x5555);
        bus.write_word(rgb_ptr + 4, 0x6666);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, rgb_ptr);
        let color2index_result = d.dispatch_quickdraw(true, 0x233, &mut cpu, &mut bus);
        assert!(color2index_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(TEST_SP + 4), 9);
    }

    #[test]
    fn reserveentry_false_clears_reservation_and_color2index_can_return_entry() {
        // IM:V 1986 p. V-143: ReserveEntry(FALSE) removes reservation so
        // the entry is again eligible for Color2Index matching.
        let (mut d, mut cpu, mut bus) = setup();
        d.device_clut = [[0x0000, 0x0000, 0x0000]; 256];
        d.device_clut[3] = [0x4444, 0x5555, 0x6666];
        d.device_clut[9] = [0x4444, 0x5555, 0x6650];
        d.clut_reserved[3] = true;

        bus.write_word(TEST_SP, 0x0000); // reserve=FALSE
        bus.write_word(TEST_SP + 2, 3u16); // index
        let dereserve_result = d.dispatch_quickdraw(true, 0x23E, &mut cpu, &mut bus);
        assert!(dereserve_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert!(!d.clut_reserved[3]);

        let rgb_ptr = 0x300440u32;
        bus.write_word(rgb_ptr, 0x4444);
        bus.write_word(rgb_ptr + 2, 0x5555);
        bus.write_word(rgb_ptr + 4, 0x6666);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, rgb_ptr);
        let color2index_result = d.dispatch_quickdraw(true, 0x233, &mut cpu, &mut bus);
        assert!(color2index_result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(TEST_SP + 4), 3);
    }

    #[test]
    fn addsearch_addcomp_pascal_procedure_preserves_stack_across_five_calls() {
        // Inside Macintosh Volume V (1986), p. V-147:
        //   PROCEDURE AddSearch(searchProc: ProcPtr);
        //   PROCEDURE AddComp  (compProc:   ProcPtr);
        // Both pop 4 bytes (4-byte ProcPtr). Mirrors B2 and B4 of
        // aa3a_aa3b_addsearch_addcomp_strict: five successive
        // distinct-arg calls preserve A7 net-balance.
        let (mut d, mut cpu, mut bus) = setup();

        // AA3A AddSearch composition (5 distinct fabricated ProcPtrs)
        let pre = cpu.read_reg(Register::A7);
        for i in 0..5u32 {
            let proc_ptr = 0x00115500u32 + i * 0x10;
            let sp = cpu.read_reg(Register::A7);
            bus.write_long(sp - 4, proc_ptr);
            cpu.write_reg(Register::A7, sp - 4);
            let r = d.dispatch_quickdraw(true, 0x23A, &mut cpu, &mut bus);
            assert!(r.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp);
        }
        assert_eq!(cpu.read_reg(Register::A7), pre);

        // AA3B AddComp composition (5 distinct fabricated ProcPtrs)
        let pre = cpu.read_reg(Register::A7);
        for i in 0..5u32 {
            let proc_ptr = 0x00116600u32 + i * 0x10;
            let sp = cpu.read_reg(Register::A7);
            bus.write_long(sp - 4, proc_ptr);
            cpu.write_reg(Register::A7, sp - 4);
            let r = d.dispatch_quickdraw(true, 0x23B, &mut cpu, &mut bus);
            assert!(r.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp);
        }
        assert_eq!(cpu.read_reg(Register::A7), pre);
    }

    #[test]
    fn delsearch_delcomp_pascal_procedure_preserves_stack_across_five_calls() {
        // Inside Macintosh Volume V (1986), p. V-147:
        //   PROCEDURE DelSearch(searchProc: ProcPtr);
        //   PROCEDURE DelComp  (compProc:   ProcPtr);
        // Both pop 4 bytes (4-byte ProcPtr). Mirrors B2 and B4 of
        // aa4c_aa4d_delsearch_delcomp_strict: five successive
        // distinct-arg calls preserve A7 net-balance.
        let (mut d, mut cpu, mut bus) = setup();

        // AA4C DelSearch composition (5 distinct fabricated ProcPtrs)
        let pre = cpu.read_reg(Register::A7);
        for i in 0..5u32 {
            let proc_ptr = 0x0054C100u32 + i * 0x10;
            let sp = cpu.read_reg(Register::A7);
            bus.write_long(sp - 4, proc_ptr);
            cpu.write_reg(Register::A7, sp - 4);
            let r = d.dispatch_quickdraw(true, 0x24C, &mut cpu, &mut bus);
            assert!(r.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp);
        }
        assert_eq!(cpu.read_reg(Register::A7), pre);

        // AA4D DelComp composition (5 distinct fabricated ProcPtrs)
        let pre = cpu.read_reg(Register::A7);
        for i in 0..5u32 {
            let proc_ptr = 0x0054D100u32 + i * 0x10;
            let sp = cpu.read_reg(Register::A7);
            bus.write_long(sp - 4, proc_ptr);
            cpu.write_reg(Register::A7, sp - 4);
            let r = d.dispatch_quickdraw(true, 0x24D, &mut cpu, &mut bus);
            assert!(r.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp);
        }
        assert_eq!(cpu.read_reg(Register::A7), pre);
    }

    fn assert_pop_28_bytes(
        d: &mut TrapDispatcher,
        cpu: &mut impl CpuOps,
        bus: &mut MacMemoryBus,
        trap_num: u16,
        salt: u32,
    ) {
        let pre = cpu.read_reg(Register::A7);

        for i in 0..5u32 {
            let sp = cpu.read_reg(Register::A7);
            let frame_sp = sp - 28;

            // The handlers under test ignore the frame payload; these
            // dummy values just make the stack layout explicit.
            for slot in 0..7u32 {
                bus.write_long(frame_sp + slot * 4, salt + i * 0x10 + slot);
            }

            cpu.write_reg(Register::A7, frame_sp);
            let result = d.dispatch_quickdraw(true, trap_num, cpu, bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp);
        }

        assert_eq!(cpu.read_reg(Register::A7), pre);
    }

    #[test]
    fn calcmask_seedcfill_pascal_procedure_preserves_stack_across_five_calls() {
        // Imaging With QuickDraw (1994), pp. 3-100 and 3-101:
        // CalcCMask / SeedCFill accept matchProc = NIL and
        // matchData = 0 for the default search behavior, so the
        // Systemless HLE stack-popping contract can be exercised with
        // inert frame payloads.
        let (mut d, mut cpu, mut bus) = setup();

        assert_pop_28_bytes(&mut d, &mut cpu, &mut bus, 0x24F, 0x024F_0000);
        assert_pop_28_bytes(&mut d, &mut cpu, &mut bus, 0x250, 0x0250_0000);
    }

    #[test]
    fn setclientid_pascal_procedure_preserves_stack_across_five_calls() {
        // Inside Macintosh Volume V (1986), p. V-147:
        //   PROCEDURE SetClientID(id: INTEGER);
        // Pops 2 bytes (single 2-byte INTEGER id). Mirrors B2 of
        // aa3c_setclientid_strict: five successive distinct-id calls
        // preserve A7 net-balance.
        let (mut d, mut cpu, mut bus) = setup();

        let ids: [u16; 5] = [0x1234, 0x5678, 0x4321, 0x7654, 0x0BAD];
        let pre = cpu.read_reg(Register::A7);
        for &id in &ids {
            let sp = cpu.read_reg(Register::A7);
            bus.write_word(sp - 2, id);
            cpu.write_reg(Register::A7, sp - 2);
            let r = d.dispatch_quickdraw(true, 0x23C, &mut cpu, &mut bus);
            assert!(r.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp);
        }
        assert_eq!(cpu.read_reg(Register::A7), pre);
    }

    #[test]
    fn protectentry_reserveentry_pascal_procedure_preserves_stack_across_five_calls() {
        // Inside Macintosh Volume V (1986), p. V-143:
        //   PROCEDURE ProtectEntry(index: INTEGER; protect: BOOLEAN);
        //   PROCEDURE ReserveEntry(index: INTEGER; reserve: BOOLEAN);
        // Both pop 4 bytes (2-byte INTEGER + 2-byte BOOLEAN). Mirrors B2
        // and B4 of aa3d_aa3e_protectentry_reserveentry_strict: five
        // successive distinct-arg calls preserve A7 net-balance.
        let (mut d, mut cpu, mut bus) = setup();

        // AA3D ProtectEntry composition (indices 64..68, alternating protect)
        let pre = cpu.read_reg(Register::A7);
        for i in 0..5u32 {
            let index = (64 + i) as u16;
            let protect: u16 = if i % 2 == 0 { 0x0100 } else { 0x0000 };
            let sp = cpu.read_reg(Register::A7);
            bus.write_word(sp - 4, protect);
            bus.write_word(sp - 2, index);
            cpu.write_reg(Register::A7, sp - 4);
            let r = d.dispatch_quickdraw(true, 0x23D, &mut cpu, &mut bus);
            assert!(r.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp);
        }
        assert_eq!(cpu.read_reg(Register::A7), pre);

        // AA3E ReserveEntry composition (indices 96..100, alternating reserve)
        let pre = cpu.read_reg(Register::A7);
        for i in 0..5u32 {
            let index = (96 + i) as u16;
            let reserve: u16 = if i % 2 == 0 { 0x0100 } else { 0x0000 };
            let sp = cpu.read_reg(Register::A7);
            bus.write_word(sp - 4, reserve);
            bus.write_word(sp - 2, index);
            cpu.write_reg(Register::A7, sp - 4);
            let r = d.dispatch_quickdraw(true, 0x23E, &mut cpu, &mut bus);
            assert!(r.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp);
        }
        assert_eq!(cpu.read_reg(Register::A7), pre);
    }

    #[test]
    fn setentries_pascal_procedure_preserves_stack_across_five_calls() {
        // Inside Macintosh Volume V (1986), p. V-143:
        //   PROCEDURE SetEntries(start, count: INTEGER; aTable: CSpecArray);
        // Pops 8 bytes (4-byte aTable pointer at SP+0, 2-byte count at SP+4,
        // 2-byte start at SP+6). Mirrors B2 of aa3f_setentries_strict: five
        // successive distinct-arg calls preserve A7 net-balance.
        let (mut d, mut cpu, mut bus) = setup();

        let pre = cpu.read_reg(Register::A7);
        for i in 0..5u32 {
            let table_ptr = 0x310000u32 + i * 0x100;
            // Two ColorSpec entries (16 bytes total) with deterministic
            // contents — sequence-mode SetEntries ignores ColorSpec.value
            // but still dereferences the table pointer.
            bus.write_word(table_ptr, (0x2000 + i) as u16);
            bus.write_word(table_ptr + 2, (0x5000 + i * 0x100) as u16);
            bus.write_word(table_ptr + 4, (0x5000 + i * 0x100) as u16);
            bus.write_word(table_ptr + 6, (0x5000 + i * 0x100) as u16);
            bus.write_word(table_ptr + 8, (0x2080 + i) as u16);
            bus.write_word(table_ptr + 10, (0x6000 + i * 0x100) as u16);
            bus.write_word(table_ptr + 12, (0x6000 + i * 0x100) as u16);
            bus.write_word(table_ptr + 14, (0x6000 + i * 0x100) as u16);

            let start: u16 = (150 + i * 10) as u16;
            let count: u16 = if i % 2 == 0 { 0 } else { 1 };
            let sp = cpu.read_reg(Register::A7);
            bus.write_long(sp - 8, table_ptr);
            bus.write_word(sp - 4, count);
            bus.write_word(sp - 2, start);
            cpu.write_reg(Register::A7, sp - 8);
            let r = d.dispatch_quickdraw(true, 0x23F, &mut cpu, &mut bus);
            assert!(r.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp);
        }
        assert_eq!(cpu.read_reg(Register::A7), pre);
    }

    #[test]
    fn setentries_oversized_sequence_does_not_corrupt_device_clut() {
        // Inside Macintosh Volume V (1986), p. V-143:
        // "If any of the requested entries are protected or out of range, a
        // protection error is returned, and nothing happens."
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let before_device = d.device_clut;
        let ctab_handle = d.current_gdevice_ctab_handle(&bus);
        let ctab_ptr = bus.read_long(ctab_handle);
        let before_ctab = bus.read_bytes(ctab_ptr, 8 + 256 * 8);

        let table_ptr = 0x310000u32;
        for i in 0..257u32 {
            let entry = table_ptr + i * 8;
            bus.write_word(entry, i as u16);
            bus.write_word(entry + 2, 0x1111);
            bus.write_word(entry + 4, 0x2222);
            bus.write_word(entry + 6, 0x3333);
        }

        let sp = cpu.read_reg(Register::A7);
        bus.write_long(sp - 8, table_ptr);
        bus.write_word(sp - 4, 256); // requests index 256 in sequence mode
        bus.write_word(sp - 2, 0);
        cpu.write_reg(Register::A7, sp - 8);

        let result = d.dispatch_quickdraw(true, 0x23F, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp);
        assert_eq!(
            d.device_clut, before_device,
            "out-of-range SetEntries must not change the hardware CLUT"
        );
        assert_eq!(
            bus.read_bytes(ctab_ptr, 8 + 256 * 8),
            before_ctab,
            "out-of-range SetEntries must not change the GDevice color table"
        );
    }

    #[test]
    fn getforecolor_getbackcolor_pascal_procedure_preserves_stack_across_five_calls() {
        // Inside Macintosh Volume V (1986), p. V-68:
        //   PROCEDURE GetForeColor (VAR color: RGBColor);
        //   PROCEDURE GetBackColor (VAR color: RGBColor);
        // Both pop 4 bytes (a single 4-byte VAR RGBColor pointer).
        // Mirrors B2 and B4 of aa19_aa1a_getforecolor_getbackcolor_strict:
        // five successive calls per trap with distinct RGBColor pointers
        // preserve A7 net-balance.
        let (mut d, mut cpu, mut bus) = setup();

        // AA19 GetForeColor composition (5 distinct RGBColor scratch slots)
        let pre = cpu.read_reg(Register::A7);
        for i in 0..5u32 {
            let color_ptr = 0x320000u32 + i * 0x10;
            // Zero the scratch RGBColor so the trap-side write is observable.
            bus.write_word(color_ptr, 0);
            bus.write_word(color_ptr + 2, 0);
            bus.write_word(color_ptr + 4, 0);

            let sp = cpu.read_reg(Register::A7);
            bus.write_long(sp - 4, color_ptr);
            cpu.write_reg(Register::A7, sp - 4);
            let r = d.dispatch_quickdraw(true, 0x219, &mut cpu, &mut bus);
            assert!(r.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp);
        }
        assert_eq!(cpu.read_reg(Register::A7), pre);

        // AA1A GetBackColor composition (5 distinct RGBColor scratch slots
        // disjoint from the AA19 range above)
        let pre = cpu.read_reg(Register::A7);
        for i in 0..5u32 {
            let color_ptr = 0x320100u32 + i * 0x10;
            bus.write_word(color_ptr, 0);
            bus.write_word(color_ptr + 2, 0);
            bus.write_word(color_ptr + 4, 0);

            let sp = cpu.read_reg(Register::A7);
            bus.write_long(sp - 4, color_ptr);
            cpu.write_reg(Register::A7, sp - 4);
            let r = d.dispatch_quickdraw(true, 0x21A, &mut cpu, &mut bus);
            assert!(r.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp);
        }
        assert_eq!(cpu.read_reg(Register::A7), pre);
    }

    #[test]
    fn getmaindevice_getgdevice_pascal_function_preserves_stack_across_five_calls() {
        // Inside Macintosh Volume V (1986), p. V-123 / V-124:
        //   FUNCTION GetMainDevice: GDHandle;
        //   FUNCTION GetGDevice:    GDHandle;
        // Pop-0 + 4-byte GDHandle result slot. Caller pre-pushes a
        // 4-byte result slot (A7 -= 4), the trap writes the GDHandle
        // at [A7] without popping any arg frame, and the caller pops
        // the 4-byte slot afterwards. Net A7 zero across the C-level
        // call.
        // Mirrors B2 and B4 of aa2a_aa32_getmaindevice_getgdevice_strict:
        // five successive calls per trap with five distinct result
        // slots preserve A7 net-balance AND all five returned
        // GDHandles are non-NIL.
        let (mut d, mut cpu, mut bus) = setup();

        // AA2A GetMainDevice composition (5 distinct result slots)
        let pre = cpu.read_reg(Register::A7);
        for i in 0..5u32 {
            let sp = cpu.read_reg(Register::A7);
            // Pre-push 4-byte result slot with poison sentinel so a
            // stub failing to write the slot is observable.
            bus.write_long(sp - 4, 0xDEAD_BEEFu32.wrapping_add(i));
            cpu.write_reg(Register::A7, sp - 4);
            let r = d.dispatch_quickdraw(true, 0x22A, &mut cpu, &mut bus);
            assert!(r.unwrap().is_ok());
            // Trap pops 0 args — A7 unchanged from the post-pre-push value.
            assert_eq!(cpu.read_reg(Register::A7), sp - 4);
            // Result slot was overwritten with a non-NIL GDHandle.
            let handle = bus.read_long(sp - 4);
            assert_ne!(handle, 0);
            assert_ne!(handle, 0xDEAD_BEEFu32.wrapping_add(i));
            // Caller pops 4-byte result slot.
            cpu.write_reg(Register::A7, sp);
        }
        assert_eq!(cpu.read_reg(Register::A7), pre);

        // AA32 GetGDevice composition (5 distinct result slots disjoint
        // from the AA2A range above is automatic since A7 returns to
        // pre after each call)
        let pre = cpu.read_reg(Register::A7);
        for i in 0..5u32 {
            let sp = cpu.read_reg(Register::A7);
            bus.write_long(sp - 4, 0xCAFE_BABEu32.wrapping_add(i));
            cpu.write_reg(Register::A7, sp - 4);
            let r = d.dispatch_quickdraw(true, 0x232, &mut cpu, &mut bus);
            assert!(r.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp - 4);
            let handle = bus.read_long(sp - 4);
            assert_ne!(handle, 0);
            assert_ne!(handle, 0xCAFE_BABEu32.wrapping_add(i));
            cpu.write_reg(Register::A7, sp);
        }
        assert_eq!(cpu.read_reg(Register::A7), pre);
    }

    #[test]
    fn test_set_entries() {
        let (mut d, mut cpu, mut bus) = setup();
        let table_ptr = 0x300000u32;
        // Write one ColorSpec entry: value=5, R=0x1111, G=0x2222, B=0x3333
        bus.write_word(table_ptr, 5); // value
        bus.write_word(table_ptr + 2, 0x1111); // red
        bus.write_word(table_ptr + 4, 0x2222); // green
        bus.write_word(table_ptr + 6, 0x3333); // blue
        bus.write_long(TEST_SP, table_ptr); // aTable
        bus.write_word(TEST_SP + 4, 0u16); // count (0 = 1 entry)
        bus.write_word(TEST_SP + 6, 5u16); // start
        let result = d.dispatch_quickdraw(true, 0x23F, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(d.device_clut[5], [0x1111, 0x2222, 0x3333]);
    }

    #[test]
    fn test_set_entries_sequence_mode_uses_start_range_and_ignores_value_fields() {
        let (mut d, mut cpu, mut bus) = setup();
        let table_ptr = 0x301000u32;

        // IM:V V-143 sequence mode ignores ColorSpec.value.
        bus.write_word(table_ptr, 200);
        bus.write_word(table_ptr + 2, 0x1111);
        bus.write_word(table_ptr + 4, 0x2222);
        bus.write_word(table_ptr + 6, 0x3333);
        bus.write_word(table_ptr + 8, 3);
        bus.write_word(table_ptr + 10, 0x4444);
        bus.write_word(table_ptr + 12, 0x5555);
        bus.write_word(table_ptr + 14, 0x6666);

        bus.write_long(TEST_SP, table_ptr);
        bus.write_word(TEST_SP + 4, 1u16); // count=1 => two entries
        bus.write_word(TEST_SP + 6, 5u16); // sequence mode start

        let result = d.dispatch_quickdraw(true, 0x23F, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(d.device_clut[5], [0x1111, 0x2222, 0x3333]);
        assert_eq!(d.device_clut[6], [0x4444, 0x5555, 0x6666]);
        assert_ne!(d.device_clut[200], [0x1111, 0x2222, 0x3333]);
        assert_ne!(d.device_clut[3], [0x4444, 0x5555, 0x6666]);
    }

    #[test]
    fn test_set_entries_index_mode_uses_colorspec_value_fields() {
        let (mut d, mut cpu, mut bus) = setup();
        let table_ptr = 0x302000u32;

        // IM:V V-143 index mode (start=-1) uses ColorSpec.value.
        bus.write_word(table_ptr, 9);
        bus.write_word(table_ptr + 2, 0xAAAA);
        bus.write_word(table_ptr + 4, 0xBBBB);
        bus.write_word(table_ptr + 6, 0xCCCC);
        bus.write_word(table_ptr + 8, 17);
        bus.write_word(table_ptr + 10, 0x1234);
        bus.write_word(table_ptr + 12, 0x2345);
        bus.write_word(table_ptr + 14, 0x3456);

        bus.write_long(TEST_SP, table_ptr);
        bus.write_word(TEST_SP + 4, 1u16); // count=1 => two entries
        bus.write_word(TEST_SP + 6, 0xFFFFu16); // start=-1 (index mode)

        let result = d.dispatch_quickdraw(true, 0x23F, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(d.device_clut[9], [0xAAAA, 0xBBBB, 0xCCCC]);
        assert_eq!(d.device_clut[17], [0x1234, 0x2345, 0x3456]);
    }

    #[test]
    fn test_set_entries_count_is_zero_based_last_index() {
        let (mut d, mut cpu, mut bus) = setup();
        let table_ptr = 0x303000u32;

        for i in 0..3u32 {
            let entry = table_ptr + i * 8;
            bus.write_word(entry, 0u16);
            bus.write_word(entry + 2, 0x1000 + i as u16);
            bus.write_word(entry + 4, 0x2000 + i as u16);
            bus.write_word(entry + 6, 0x3000 + i as u16);
        }

        // IM:V V-143: count is zero-based (count=2 writes 3 entries).
        bus.write_long(TEST_SP, table_ptr);
        bus.write_word(TEST_SP + 4, 2u16);
        bus.write_word(TEST_SP + 6, 20u16);

        let result = d.dispatch_quickdraw(true, 0x23F, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(d.device_clut[20], [0x1000, 0x2000, 0x3000]);
        assert_eq!(d.device_clut[21], [0x1001, 0x2001, 0x3001]);
        assert_eq!(d.device_clut[22], [0x1002, 0x2002, 0x3002]);
    }

    #[test]
    fn disposecicon_consumes_theicon_ciconhandle_argument() {
        // More Macintosh Toolbox (1993), p. 5-30:
        // PROCEDURE DisposeCIcon(theIcon: CIconHandle).
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x00BA_D000);

        let result = d.dispatch_quickdraw(true, 0x225, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP + 4,
            "DisposeCIcon should pop one CIconHandle argument (4 bytes)"
        );
    }

    #[test]
    fn disposecicon_preserves_nonstack_registers_while_releasing_icondata_handle() {
        // More Macintosh Toolbox (1993), p. 5-30: DisposeCIcon is a
        // PROCEDURE with one parameter and no function result slot.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x00BA_D000);
        cpu.write_reg(Register::A0, 0x0012_3400);
        cpu.write_reg(Register::A1, 0x0056_7800);
        cpu.write_reg(Register::D0, 0x89AB_CDEF);
        cpu.write_reg(Register::D1, 0x1357_9BDF);

        let result = d.dispatch_quickdraw(true, 0x225, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A0), 0x0012_3400);
        assert_eq!(cpu.read_reg(Register::A1), 0x0056_7800);
        assert_eq!(cpu.read_reg(Register::D0), 0x89AB_CDEF);
        assert_eq!(cpu.read_reg(Register::D1), 0x1357_9BDF);
    }

    #[test]
    fn disposecicon_detaches_live_getcicon_handle_from_recoverhandle() {
        // More Macintosh Toolbox (1993), p. 5-30: DisposeCIcon removes the
        // live GetCIcon allocation while leaving the resource file usable.
        let (mut d, mut cpu, mut bus) = setup();
        let cicn_data = make_test_cicn_resource(0xF0, 0x00, 0x55);
        d.install_test_resource(&mut bus, *b"cicn", 128, &cicn_data);
        bus.write_word(TEST_SP, 128);

        let get_icon = d.dispatch_quickdraw(true, 0x21E, &mut cpu, &mut bus);
        assert!(get_icon.unwrap().is_ok());
        let icon_handle = bus.read_long(TEST_SP + 2);
        assert_ne!(icon_handle, 0);
        let icon_ptr = bus.read_long(icon_handle);
        assert_ne!(icon_ptr, 0);
        assert_eq!(bus.get_alloc_size(icon_handle), Some(4));

        cpu.write_reg(Register::A0, icon_ptr);
        let recover_live = d.dispatch_memory(false, 0x28, &mut cpu, &mut bus);
        assert!(recover_live.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A0), icon_handle);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, icon_handle);
        let dispose = d.dispatch_quickdraw(true, 0x225, &mut cpu, &mut bus);
        assert!(dispose.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(icon_ptr + 78), 0);
        assert!(bus.get_alloc_size(icon_handle).is_none());

        cpu.write_reg(Register::A0, icon_ptr);
        let recover = d.dispatch_memory(false, 0x28, &mut cpu, &mut bus);
        assert!(recover.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A0),
            0,
            "DisposeCIcon should detach the live GetCIcon handle"
        );
    }

    #[test]
    fn initgdevice_consumes_gdrefnum_mode_and_gdhandle_arguments() {
        // Imaging With QuickDraw (1994), pp. 5-21 to 5-22:
        // PROCEDURE InitGDevice(gdRefNum: Integer; mode: LongInt;
        //                       gdh: GDHandle).
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x00CA_1000); // gdh
        bus.write_long(TEST_SP + 4, 0x0000_0080); // mode
        bus.write_word(TEST_SP + 8, 0x0001); // gdRefNum

        let result = d.dispatch_quickdraw(true, 0x22E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP + 10,
            "InitGDevice should pop gdh(4)+mode(4)+gdRefNum(2)"
        );
    }

    #[test]
    fn initgdevice_writes_gdrefnum_and_mode_fields_into_target_record() {
        // IWQD 1994 pp. 5-21 to 5-22: InitGDevice fills out the target
        // GDevice record for the requested refNum + mode.
        let (mut d, mut cpu, mut bus) = setup();
        let gdh = d.allocate_detached_gdevice(&mut bus);
        let gd = bus.read_long(gdh);
        assert_ne!(gd, 0);
        bus.write_word(gd, 0);
        bus.write_long(gd + 42, 128);
        bus.write_word(gd + 20, 0);

        bus.write_long(TEST_SP, gdh);
        bus.write_long(TEST_SP + 4, 0x0000_0081);
        bus.write_word(TEST_SP + 8, 0x0017);

        let result = d.dispatch_quickdraw(true, 0x22E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(bus.read_word(gd), 0x0017);
        assert_eq!(bus.read_long(gd + 42), 0x0000_0081);
        assert_eq!(bus.read_word(gd + 20) & 1, 1);
    }

    #[test]
    fn qderror_returns_noerr_in_function_result_slot_without_stack_pop() {
        // Imaging With QuickDraw (1994), pp. 4-94 to 4-95:
        // FUNCTION QDError: Integer.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 0x7F7F);

        let result = d.dispatch_quickdraw(true, 0x240, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP,
            "QDError has no stack arguments to pop"
        );
        assert_eq!(
            bus.read_word(TEST_SP),
            0,
            "QDError should write noErr to the function result slot"
        );
    }

    #[test]
    fn qderror_stub_preserves_general_registers_while_writing_result_slot() {
        // Imaging With QuickDraw (1994), pp. 4-94 to 4-95: QDError returns
        // result via function slot; no documented side effects on general registers.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 0xFFFF);
        cpu.write_reg(Register::A0, 0x00A0_A000);
        cpu.write_reg(Register::A1, 0x00A1_A100);
        cpu.write_reg(Register::D0, 0xABCD_0001);
        cpu.write_reg(Register::D1, 0x1234_F0F0);

        let result = d.dispatch_quickdraw(true, 0x240, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A0), 0x00A0_A000);
        assert_eq!(cpu.read_reg(Register::A1), 0x00A1_A100);
        assert_eq!(cpu.read_reg(Register::D0), 0xABCD_0001);
        assert_eq!(cpu.read_reg(Register::D1), 0x1234_F0F0);
        assert_eq!(bus.read_word(TEST_SP), 0);
    }

    #[test]
    fn qderror_pascal_function_preserves_stack_across_five_calls() {
        // Mirrors band B2 of aa40_qderror_strict catalogue proof.
        // Per IM:V V-145 / IWQD 4-94..4-95 each QDError() call is a
        // Pascal FUNCTION pop-0 + 2-byte result slot — caller pre-pushes
        // 2 bytes, trap writes the INTEGER at [A7], caller pops 2 bytes.
        // Five successive calls each preserve A7 across the trap-side
        // dispatch (A7 unchanged across the dispatch — caller-side
        // result-slot push/pop is simulated explicitly here).
        let (mut d, mut cpu, mut bus) = setup();
        let sp_pre_composition = cpu.read_reg(Register::A7);
        for i in 0..5u32 {
            let sp_before_call = cpu.read_reg(Register::A7);
            // Caller pre-pushes 2-byte result slot poisoned with a
            // per-call sentinel so the dispatch is forced to overwrite
            // it with noErr (0).
            let new_sp = sp_before_call - 2;
            cpu.write_reg(Register::A7, new_sp);
            bus.write_word(new_sp, 0xDEAD_u16.wrapping_add(i as u16));

            let result = d.dispatch_quickdraw(true, 0x240, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(
                cpu.read_reg(Register::A7),
                new_sp,
                "QDError call {i} should pop 0 argument bytes",
            );
            assert_eq!(
                bus.read_word(new_sp),
                0,
                "QDError call {i} should overwrite the result slot with noErr",
            );

            // Caller pops the 2-byte result slot.
            cpu.write_reg(Register::A7, new_sp + 2);
        }
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_pre_composition,
            "5-call QDError composition should net A7-zero across the C-level sequence",
        );
    }

    #[test]
    fn getcwmgrport_writes_wmgrcport_pointer_and_consumes_argument() {
        // Macintosh Toolbox Essentials (1992), pp. 4-113 to 4-114:
        // PROCEDURE GetCWMgrPort(VAR wMgrCPort: CGrafPtr).
        let (mut d, mut cpu, mut bus) = setup();
        let out_ptr = bus.alloc(4);
        bus.write_long(out_ptr, 0xDEAD_BEEF);
        bus.write_long(TEST_SP, out_ptr);

        let result = d.dispatch_quickdraw(true, 0x248, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP + 4,
            "GetCWMgrPort should pop one VAR pointer argument"
        );
        let wmgr_port = bus.read_long(out_ptr);
        assert_ne!(wmgr_port, 0, "GetCWMgrPort should write a non-NIL CGrafPtr");
        assert_ne!(
            wmgr_port,
            bus.read_long(0x09DE),
            "GetCWMgrPort should write a non-NIL color Window Manager port distinct from WMgrPort"
        );
        assert_eq!(
            bus.read_word(wmgr_port + 6) & 0xC000,
            0xC000,
            "GetCWMgrPort should return a CGrafPort-backed Window Manager port"
        );
    }

    #[test]
    fn getcwmgrport_nil_var_pointer_is_safe_and_still_pops_argument() {
        // Macintosh Toolbox Essentials (1992), pp. 4-113 to 4-114:
        // stack consumption is independent of pointer contents.
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0);

        let result = d.dispatch_quickdraw(true, 0x248, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP + 4,
            "GetCWMgrPort should pop VAR pointer even when pointer is NIL"
        );
    }

    // ==================== Unhandled ====================

    #[test]
    fn test_unhandled_trap_returns_none() {
        let (mut d, mut cpu, mut bus) = setup();
        let result = d.dispatch_quickdraw(true, 0xFFFF, &mut cpu, &mut bus);
        assert!(result.is_none());
    }

    /// Regression: GDevice CTab initial seed must be 8 (Executor
    /// depth convention from qGWorld.cpp:217). Used by the
    /// PICT identity-copy gate — EV's PICTs author with ctSeed=8 and
    /// need the GDevice to match for identity.
    #[test]
    fn test_ensure_main_gdevice_seeds_with_depth_convention() {
        let (mut d, _cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let ctab_handle = TrapDispatcher::gdevice_ctab_handle(&bus, gdh);
        let ctab_ptr = bus.read_long(ctab_handle);
        assert_eq!(
            bus.read_long(ctab_ptr),
            8,
            "GDevice CTab must initialise with ctSeed=8 (depth convention)"
        );
    }

    #[test]
    fn gdevice_helpers_treat_overflowing_master_pointer_as_missing() {
        let (_d, _cpu, mut bus) = setup();
        let bogus_gdh = bus.alloc(4);
        bus.write_long(bogus_gdh, 0xFFFF_FFF0);

        assert_eq!(TrapDispatcher::gdevice_ctab_handle(&bus, bogus_gdh), 0);
        assert_eq!(TrapDispatcher::gdevice_pixel_size(&bus, bogus_gdh), None);
    }

    #[test]
    fn current_gdevice_ctab_falls_back_to_main_when_current_device_is_stale() {
        let (mut d, _cpu, mut bus) = setup();
        let main_gdh = d.ensure_main_gdevice(&mut bus);
        let main_ctab = TrapDispatcher::gdevice_ctab_handle(&bus, main_gdh);
        let stale_gdh = bus.alloc(4);
        bus.write_long(stale_gdh, 0xFFFF_FFF0);
        d.current_gdevice = stale_gdh;

        assert_eq!(d.current_gdevice_ctab_handle(&bus), main_ctab);
    }

    #[test]
    fn dispose_gworld_ignores_attached_gdevice_with_overflowing_pointer() {
        let (mut d, mut cpu, mut bus) = setup();
        let port = bus.alloc(170);
        let pixmap_handle = bus.alloc(4);
        let stale_gdh = bus.alloc(4);
        bus.write_long(port + 2, pixmap_handle);
        bus.write_long(stale_gdh, 0xFFFF_FFF0);
        d.gworld_devices.insert(port, stale_gdh);

        d.dispose_gworld_port(&mut cpu, &mut bus, port);

        assert!(!d.gworld_devices.contains_key(&port));
    }

    /// Regression: reseed_color_table_handle must preserve
    /// ctSeed=8 when the CTab's CLUT is byte-identical to the
    /// standard 8bpp palette. Without this, every SetEntries /
    /// CTabChanged bumps the GDevice seed to a counter value and
    /// PICTs authored for the canonical palette remap unnecessarily.
    #[test]
    fn test_reseed_preserves_depth_seed_on_canonical_clut() {
        let (mut d, _cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let ctab_handle = TrapDispatcher::gdevice_ctab_handle(&bus, gdh);
        // Sanity: GDevice starts with the canonical 8bpp palette.
        let ctab_ptr = bus.read_long(ctab_handle);
        assert!(TrapDispatcher::ctab_is_canonical_8bpp(&bus, ctab_ptr));
        // Pre-bump the counter to a large value so we can distinguish
        // a "fresh counter seed" from the preserved depth value.
        for _ in 0..500 {
            let _ = d.next_color_table_seed();
        }
        // Reseed should notice the CLUT is canonical and keep seed=8.
        let seed = d.reseed_color_table_handle(&mut bus, ctab_handle);
        assert_eq!(
            seed,
            Some(8),
            "reseed must preserve seed=8 on canonical CLUT"
        );
        assert_eq!(bus.read_long(ctab_ptr), 8);
    }

    /// Regression: When the CTab's CLUT is non-canonical,
    /// reseed_color_table_handle must use the counter (mac semantics:
    /// SetEntries bumps the seed when content changes).
    #[test]
    fn test_reseed_uses_counter_on_non_canonical_clut() {
        let (mut d, _cpu, mut bus) = setup();
        let gdh = d.ensure_main_gdevice(&mut bus);
        let ctab_handle = TrapDispatcher::gdevice_ctab_handle(&bus, gdh);
        let ctab_ptr = bus.read_long(ctab_handle);
        // Corrupt the CLUT so it's no longer canonical.
        bus.write_word(ctab_ptr + 8 + 10 * 8 + 2, 0xABCD); // idx 10 red
        assert!(!TrapDispatcher::ctab_is_canonical_8bpp(&bus, ctab_ptr));
        let counter_before = d.next_ct_seed;
        let seed = d.reseed_color_table_handle(&mut bus, ctab_handle);
        assert_eq!(seed, Some(counter_before));
        assert_ne!(seed, Some(8));
    }

    /// overwrite_color_table_handle_with_clut must inherit the current
    /// GDevice CTab's seed when the supplied CLUT matches the GDevice's
    /// CLUT byte-for-byte (Executor ROMlib_copy_ctab semantics: BlockMoveData
    /// moves the whole CTab including ctSeed). Without inheritance the
    /// CopyBits 8→8 translation gate fires on every HUD blit even when the
    /// offscreen CTab was inherited from the GDevice.
    #[test]
    fn test_new_gworld_style_ctab_inherits_gdevice_seed_on_content_match() {
        let (mut d, _cpu, mut bus) = setup();
        // Establish the GDevice CTab with seed=8.
        let _gdh = d.ensure_main_gdevice(&mut bus);
        let std_clut = TrapDispatcher::standard_mac_8bpp_clut();
        // Pre-bump the counter so "fresh seed" ≠ 8.
        for _ in 0..100 {
            let _ = d.next_color_table_seed();
        }
        let inherited_handle =
            d.allocate_color_table_handle_with_clut(&mut bus, 8, &std_clut, 0x8000);
        let inherited_ptr = bus.read_long(inherited_handle);
        assert_eq!(
            bus.read_long(inherited_ptr),
            8,
            "new CTab with canonical CLUT must inherit GDevice seed=8"
        );
    }

    /// GetCTable($AA18) with a depth ID (1, 2, 4, 8) must return a CTab
    /// whose ctSeed equals the depth (Executor qGWorld.cpp:217 depth
    /// convention). This lets PICTs stamped with ctSeed=depth identity-
    /// copy via the seed-match gate when the destination is this CTab.
    #[test]
    fn test_get_ctable_depth_id_returns_depth_seed() {
        let (mut d, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 8u16); // ctID = 8
        let result = d.dispatch_quickdraw(true, 0x218, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        // FUNCTION returns CTabHandle at SP+2 (just-popped ctID position)
        // and leaves SP at TEST_SP + 2 (ctID popped).
        let new_sp = cpu.read_reg(Register::A7);
        assert_eq!(new_sp, TEST_SP + 2);
        let handle = bus.read_long(new_sp);
        assert_ne!(handle, 0, "GetCTable(8) must return a non-null handle");
        let ctab_ptr = bus.read_long(handle);
        assert_ne!(ctab_ptr, 0);
        assert_eq!(
            bus.read_long(ctab_ptr),
            8,
            "GetCTable(8) must return ctSeed=8 (depth convention)"
        );
    }

    /// When the supplied CLUT DIFFERS from the GDevice CTab's CLUT, a
    /// fresh counter seed must be assigned (not the GDevice seed) so that
    /// CopyBits correctly translates across the palette mismatch.
    #[test]
    fn test_new_gworld_style_ctab_uses_fresh_seed_on_content_mismatch() {
        let (mut d, _cpu, mut bus) = setup();
        let _gdh = d.ensure_main_gdevice(&mut bus);
        let mut altered = TrapDispatcher::standard_mac_8bpp_clut();
        altered[42] = [0xABCD, 0xABCD, 0xABCD]; // diverge on one entry
        let counter_before = d.next_ct_seed;
        let handle = d.allocate_color_table_handle_with_clut(&mut bus, 8, &altered, 0x8000);
        let ptr = bus.read_long(handle);
        let got_seed = bus.read_long(ptr);
        assert_eq!(got_seed, counter_before);
        assert_ne!(got_seed, 8, "mismatched CLUT must NOT inherit GDevice seed");
    }

    // UnionRect empty-source handling per IM:I I-176.

    fn union_rect(s1: (i16, i16, i16, i16), s2: (i16, i16, i16, i16)) -> (i16, i16, i16, i16) {
        let (mut d, mut cpu, mut bus) = setup();
        let s1_ptr = 0x300000u32;
        let s2_ptr = 0x300010u32;
        let dst_ptr = 0x300020u32;
        write_rect(&mut bus, s1_ptr, s1.0, s1.1, s1.2, s1.3);
        write_rect(&mut bus, s2_ptr, s2.0, s2.1, s2.2, s2.3);
        let sp = TEST_SP - 12;
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, dst_ptr);
        bus.write_long(sp + 4, s2_ptr);
        bus.write_long(sp + 8, s1_ptr);
        let result = d.dispatch_quickdraw(true, 0x0AB, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        (
            bus.read_word(dst_ptr) as i16,
            bus.read_word(dst_ptr + 2) as i16,
            bus.read_word(dst_ptr + 4) as i16,
            bus.read_word(dst_ptr + 6) as i16,
        )
    }

    #[test]
    fn unionrect_empty_src1_returns_src2() {
        let result = union_rect((0, 0, 0, 0), (10, 20, 30, 40));
        assert_eq!(result, (10, 20, 30, 40));
    }

    #[test]
    fn unionrect_empty_src2_returns_src1() {
        let result = union_rect((10, 20, 30, 40), (5, 5, 5, 5));
        assert_eq!(result, (10, 20, 30, 40));
    }

    #[test]
    fn unionrect_both_nonempty_bounding_box() {
        let result = union_rect((10, 20, 30, 40), (15, 50, 35, 80));
        assert_eq!(result, (10, 20, 35, 80));
    }

    #[test]
    fn unionrect_both_empty_returns_zero_rect() {
        let result = union_rect((0, 0, 0, 0), (5, 5, 5, 5));
        assert_eq!(result, (0, 0, 0, 0));
    }

    // CharExtra ($AA23) — Color QuickDraw Tool-bit Pascal PROCEDURE
    // per IM:V 1986 p. V-77. Mirrors B2 of the aa23_charextra_strict
    // bake: five successive CharExtra dispatches with distinct Fixed
    // values must each pop their 4-byte Fixed argument and
    // cumulatively leave A7 unchanged across the 5-call C-level
    // composition.
    #[test]
    fn charextra_pascal_procedure_preserves_stack_across_five_calls() {
        let (mut d, mut cpu, mut bus) = setup();
        let sp_pre = cpu.read_reg(Register::A7);
        // Distinct Fixed values: 0.0, 1.0, 2.0, -1.0, 0.5
        let values: [u32; 5] = [0x00000000, 0x00010000, 0x00020000, 0xFFFF0000, 0x00008000];
        for &value in &values {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 4);
            bus.write_long(sp - 4, value);
            let result = d.dispatch_quickdraw(true, 0x223, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
        }
        assert_eq!(cpu.read_reg(Register::A7), sp_pre);
    }

    // SetDeskCPat ($AA47) — Window Manager Tool-bit Pascal PROCEDURE
    // per IM:V 1986 p. V-210. Mirrors B2 of the aa47_setdeskcpat_strict
    // bake: five successive SetDeskCPat(NIL) dispatches must each pop
    // their 4-byte PixPatHandle argument and cumulatively leave A7
    // unchanged across the 5-call C-level composition.
    #[test]
    fn setdeskcpat_pascal_procedure_preserves_stack_across_five_calls() {
        let (mut d, mut cpu, mut bus) = setup();
        let sp_pre = cpu.read_reg(Register::A7);
        for _ in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 4);
            bus.write_long(sp - 4, 0); // NIL PixPatHandle
            let result = d.dispatch_quickdraw(true, 0x247, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
        }
        assert_eq!(cpu.read_reg(Register::A7), sp_pre);
    }

    // PenPixPat ($AA0A) + BackPixPat ($AA0B) — Color QuickDraw Tool-bit
    // Pascal PROCEDUREs per IM:V 1986 p. V-72. Mirrors B2 and B4 of the
    // aa0a_aa0b_pen_back_pixpat_strict bake: five successive PenPixPat(NIL)
    // calls followed by five successive BackPixPat(NIL) calls must each
    // pop their 4-byte PixPatHandle argument and cumulatively leave A7
    // unchanged across each 5-call C-level composition (B2/B4 of the bake).
    #[test]
    fn pen_back_pixpat_pascal_procedure_preserves_stack_across_five_calls() {
        let (mut d, mut cpu, mut bus) = setup();
        let sp_pre = cpu.read_reg(Register::A7);
        for _ in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 4);
            bus.write_long(sp - 4, 0); // NIL PixPatHandle
            let result = d.dispatch_quickdraw(true, 0x20A, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
        }
        assert_eq!(cpu.read_reg(Register::A7), sp_pre);
        for _ in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 4);
            bus.write_long(sp - 4, 0); // NIL PixPatHandle
            let result = d.dispatch_quickdraw(true, 0x20B, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
        }
        assert_eq!(cpu.read_reg(Register::A7), sp_pre);
    }

    // OpColor ($AA21) + HiliteColor ($AA22) — Color QuickDraw Tool-bit
    // Pascal PROCEDUREs per IM:V 1986 p. V-77. Mirrors B2 and B4 of the
    // aa21_aa22_opcolor_hilitecolor_strict bake: five successive
    // OpColor(NIL) calls followed by five successive HiliteColor(NIL)
    // calls must each pop their 4-byte RGBColor pointer argument and
    // cumulatively leave A7 unchanged across each 5-call C-level
    // composition (B2/B4 of the bake).
    #[test]
    fn opcolor_hilitecolor_pascal_procedure_preserves_stack_across_five_calls() {
        let (mut d, mut cpu, mut bus) = setup();
        let sp_pre = cpu.read_reg(Register::A7);
        for _ in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 4);
            bus.write_long(sp - 4, 0); // NIL RGBColor pointer
            let result = d.dispatch_quickdraw(true, 0x221, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
        }
        assert_eq!(cpu.read_reg(Register::A7), sp_pre);
        for _ in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 4);
            bus.write_long(sp - 4, 0); // NIL RGBColor pointer
            let result = d.dispatch_quickdraw(true, 0x222, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
        }
        assert_eq!(cpu.read_reg(Register::A7), sp_pre);
    }

    // MakeRGBPat ($AA0D) — Color QuickDraw Tool-bit Pascal PROCEDURE per
    // IM:V 1986 p. V-73. Mirrors B2 of the aa0d_makergbpat_strict bake:
    // five successive MakeRGBPat(pp, &color) calls must each pop their
    // 8-byte arg frame (4-byte PixPatHandle + 4-byte RGBColor pointer)
    // and cumulatively leave A7 unchanged across the 5-call C-level
    // composition (B2 of the bake). The PixPatHandle and RGBColor
    // pointer values are arbitrary 4-byte non-zero placeholders; the
    // Systemless HLE pops 8 bytes regardless of the argument values.
    #[test]
    fn makergbpat_pascal_procedure_preserves_stack_across_five_calls() {
        let (mut d, mut cpu, mut bus) = setup();
        let sp_pre = cpu.read_reg(Register::A7);
        for _ in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 8);
            bus.write_long(sp - 8, 0x0000_1000); // arbitrary RGBColor pointer
            bus.write_long(sp - 4, 0x0000_2000); // arbitrary PixPatHandle
            let result = d.dispatch_quickdraw(true, 0x20D, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
        }
        assert_eq!(cpu.read_reg(Register::A7), sp_pre);
    }

    // NewPixPat ($AA07) — Color QuickDraw Tool-bit Pascal FUNCTION per
    // IM:V 1986 p. V-72. Mirrors B2 of the aa07_newpixpat_strict bake:
    // five successive NewPixPat() calls must each leave A7 unchanged
    // across the C-level call sequence (caller pre-pushes a 4-byte
    // PixPatHandle result slot at SP-4, trap writes [SP+0] without
    // modifying A7, caller pops the slot afterwards) AND each call
    // must return a non-NIL handle per IM:V V-72 "returns a handle to
    // a new pixel pattern". The C-level idiom in the bake's main.c
    // is `h = NewPixPat();` repeated 5 times inside one StackSpace
    // sandwich; this contract test models that idiom at the trap-
    // dispatch level by pre-pushing the result slot, dispatching the
    // trap, reading the handle out, and popping the slot — for each
    // of 5 iterations.
    #[test]
    fn newpixpat_pascal_function_returns_nonnil_handle_and_preserves_stack_across_five_calls() {
        let (mut d, mut cpu, mut bus) = setup();
        let sp_pre = cpu.read_reg(Register::A7);
        for _ in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 4);
            bus.write_long(sp - 4, 0xDEAD_BEEF); // sentinel — trap must overwrite
            let result = d.dispatch_quickdraw(true, 0x207, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            // Trap must have left A7 alone (Pascal FUNCTION ABI: result
            // slot is written via [SP+0], A7 is unchanged across the
            // dispatch itself).
            assert_eq!(cpu.read_reg(Register::A7), sp - 4);
            // Trap must have written a non-NIL handle into the result
            // slot, overwriting the 0xDEADBEEF sentinel.
            let handle = bus.read_long(sp - 4);
            assert_ne!(handle, 0xDEAD_BEEF, "trap did not write result slot");
            assert_ne!(handle, 0, "trap returned NIL handle (IM:V V-72 violation)");
            // Caller pops the result slot.
            cpu.write_reg(Register::A7, sp);
        }
        assert_eq!(cpu.read_reg(Register::A7), sp_pre);
    }

    #[test]
    fn newpixpat_initializes_gray_pattern_and_penpixpat_copies_it() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let sp = cpu.read_reg(Register::A7);
        let gray = [0xAAu8, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55];

        cpu.write_reg(Register::A7, sp - 4);
        bus.write_long(sp - 4, 0xDEAD_BEEF);
        let result = d.dispatch_quickdraw(true, 0x207, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp - 4);

        let handle = bus.read_long(sp - 4);
        let pixpat = bus.read_long(handle);
        for (i, byte) in gray.iter().enumerate() {
            assert_eq!(
                bus.read_byte(pixpat + 20 + i as u32),
                *byte,
                "NewPixPat should seed pat1Data byte {}",
                i
            );
        }

        cpu.write_reg(Register::A7, sp - 4);
        bus.write_long(sp - 4, handle);
        let result = d.dispatch_quickdraw(true, 0x20A, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp);
        assert_eq!(d.pn_pat, gray);
    }

    // GetPixPat ($AA0C) — Color QuickDraw Tool-bit Pascal FUNCTION
    // taking 2-byte INTEGER patID, returning 4-byte PixPatHandle per
    // IM:V 1986 p. V-73. Mirrors B2 of the aa0c_getpixpat_strict bake:
    // five successive GetPixPat(distinct_missing_patIDs) calls must
    // leave A7 unchanged across the 5-call C-level composition. Each
    // call pre-pushes a 4-byte result slot + 2-byte arg, trap pops
    // the 2-byte arg and writes the handle into the 4-byte result
    // slot at the post-pop SP, caller pops the 4-byte result slot.
    // The fictional patIDs (0x7FF0..0x7FF4) miss the Systemless resource
    // map per IM:V V-73 documented "If the resource with the
    // specified ID is not found, then this routine returns a NIL
    // handle" path; all five handles must equal 0 and the result
    // slot's sentinel must be overwritten by the trap.
    #[test]
    fn getpixpat_pascal_function_preserves_stack_across_five_missing_calls() {
        let (mut d, mut cpu, mut bus) = setup();
        let sp_pre = cpu.read_reg(Register::A7);
        let pat_ids: [u16; 5] = [0x7FF0, 0x7FF1, 0x7FF2, 0x7FF3, 0x7FF4];
        for &pat_id in &pat_ids {
            let sp = cpu.read_reg(Register::A7);
            // Caller pre-pushes: 4-byte result slot then 2-byte arg.
            cpu.write_reg(Register::A7, sp - 6);
            bus.write_long(sp - 6, 0xDEAD_BEEF); // sentinel — trap must overwrite
            bus.write_word(sp - 2, pat_id);
            let result = d.dispatch_quickdraw(true, 0x20C, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            // Trap pops the 2-byte arg, leaving A7 at sp - 4 (= post-arg-pop SP).
            assert_eq!(cpu.read_reg(Register::A7), sp - 4);
            // Trap must have written a NIL handle into the result slot,
            // overwriting the 0xDEADBEEF sentinel.
            let handle = bus.read_long(sp - 4);
            assert_ne!(handle, 0xDEAD_BEEF, "trap did not write result slot");
            assert_eq!(
                handle, 0,
                "GetPixPat with missing patID should return NIL per IM:V V-73"
            );
            // Caller pops the result slot.
            cpu.write_reg(Register::A7, sp);
        }
        assert_eq!(cpu.read_reg(Register::A7), sp_pre);
    }

    // DisposPixPat ($AA08) + CopyPixPat ($AA09) — Color QuickDraw Tool-bit
    // Pascal PROCEDUREs per IM:V 1986 p. V-73. Mirrors B2 and B4 of the
    // aa08_aa09_dispose_copy_pixpat_strict bake: five successive
    // DisposPixPat(pp) calls (each popping a 4-byte PixPatHandle)
    // followed by five successive CopyPixPat(src, dst) calls (each
    // popping an 8-byte two-handle frame) must cumulatively leave A7
    // unchanged across each 5-call C-level composition (B2/B4 of the
    // bake). The handle values are arbitrary 4-byte non-zero placeholders;
    // the Systemless HLE pops the documented arg count regardless of the
    // argument values (nested dereferences are guarded by non-NIL checks
    // on each level).
    #[test]
    fn dispose_copy_pixpat_pascal_procedure_preserves_stack_across_five_calls() {
        let (mut d, mut cpu, mut bus) = setup();
        let sp_pre = cpu.read_reg(Register::A7);
        // DisposPixPat — pop-4 PROCEDURE.
        for _ in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 4);
            bus.write_long(sp - 4, 0); // NIL PixPatHandle (no-op path)
            let result = d.dispatch_quickdraw(true, 0x208, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
        }
        assert_eq!(cpu.read_reg(Register::A7), sp_pre);
        // CopyPixPat — pop-8 PROCEDURE.
        for _ in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 8);
            bus.write_long(sp - 8, 0); // NIL dstPP
            bus.write_long(sp - 4, 0); // NIL srcPP (no-op path)
            let result = d.dispatch_quickdraw(true, 0x209, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
        }
        assert_eq!(cpu.read_reg(Register::A7), sp_pre);
    }

    // SetCPixel ($AA16) + GetCPixel ($AA17) — Color QuickDraw Tool-bit
    // Pascal PROCEDUREs per IM:V 1986 p. V-70 and p. V-69. Mirrors B2
    // and B4 of the aa16_aa17_setcpixel_getcpixel_strict bake: five
    // successive SetCPixel calls (each popping an 8-byte arg frame:
    // 4-byte cPix pointer + 2-byte v + 2-byte h) followed by five
    // successive GetCPixel calls (same arg frame shape) must
    // cumulatively leave A7 unchanged across each 5-call C-level
    // composition (B2/B4 of the bake). NIL cPix pointers exercise
    // the Systemless HLE's documented NIL early-exit (per the cpix_ptr
    // == 0 guard at quickdraw.rs (true, 0x216) and (true, 0x217));
    // the trap still pops the full 8-byte arg frame before
    // returning.
    // Color2Index ($AA33) + InvertColor ($AA35) — Color Manager Tool-bit
    // Pascal FUNCTION + PROCEDURE per IM:V 1986 p. V-141. Mirrors B2
    // and B4 of the aa33_aa35_color2index_invertcolor_strict bake: five
    // successive Color2Index calls (each pre-pushing a 4-byte LONGINT
    // result slot + 4-byte RGBColor pointer arg frame) followed by
    // five successive InvertColor calls (each pre-pushing a 4-byte
    // RGBColor VAR pointer) must cumulatively leave A7 unchanged
    // across each 5-call C-level composition. InvertColor also writes
    // the 1's complement of each VAR RGBColor back through the
    // pointer per IM:V V-141 — the unit test asserts that contract
    // alongside the stack discipline.
    #[test]
    fn color2index_invertcolor_pascal_call_preserves_stack_across_five_calls() {
        let (mut d, mut cpu, mut bus) = setup();
        let sp_pre = cpu.read_reg(Register::A7);
        // Stage 5 RGBColor records in a scratch buffer for the trap to
        // dereference; one record per Color2Index call.
        let rgb_base: u32 = 0x0001_0000;
        let inputs: [[u16; 3]; 5] = [
            [0x1000, 0x2000, 0x3000],
            [0x2000, 0x4000, 0x6000],
            [0x4000, 0x8000, 0xC000],
            [0x8000, 0xC000, 0x2000],
            [0xC000, 0x2000, 0x8000],
        ];
        for (i, rgb) in inputs.iter().enumerate() {
            let p = rgb_base + (i as u32) * 8;
            bus.write_word(p, rgb[0]);
            bus.write_word(p + 2, rgb[1]);
            bus.write_word(p + 4, rgb[2]);
        }
        // Color2Index: caller pre-pushes 4-byte LONGINT result slot
        // then 4-byte RGBColor pointer; trap pops the 4-byte arg and
        // writes LONGINT to the post-pop result slot; caller pops the
        // 4-byte result slot. Net A7 across the C-level call is zero.
        for i in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 8);
            bus.write_long(sp - 8, 0xDEAD_BEEF); // sentinel result slot
            bus.write_long(sp - 4, rgb_base + (i as u32) * 8);
            let result = d.dispatch_quickdraw(true, 0x233, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            // Result slot is now at the new SP. Caller pops 4 bytes.
            let sp_after = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp_after + 4);
        }
        assert_eq!(cpu.read_reg(Register::A7), sp_pre);

        // InvertColor: caller pushes 4-byte RGBColor VAR pointer, trap
        // pops 4 bytes, mutates RGB to 1's complement, no result slot.
        let inv_base: u32 = 0x0002_0000;
        let inv_inputs: [[u16; 3]; 5] = [
            [0x0001, 0x0002, 0x0003],
            [0x1111, 0x2222, 0x3333],
            [0x4444, 0x5555, 0x6666],
            [0x7777, 0x8888, 0x9999],
            [0xAAAA, 0xBBBB, 0xCCCC],
        ];
        for i in 0..5 {
            let p = inv_base + (i as u32) * 8;
            bus.write_word(p, inv_inputs[i][0]);
            bus.write_word(p + 2, inv_inputs[i][1]);
            bus.write_word(p + 4, inv_inputs[i][2]);
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 4);
            bus.write_long(sp - 4, p);
            let result = d.dispatch_quickdraw(true, 0x235, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(bus.read_word(p), !inv_inputs[i][0]);
            assert_eq!(bus.read_word(p + 2), !inv_inputs[i][1]);
            assert_eq!(bus.read_word(p + 4), !inv_inputs[i][2]);
        }
        assert_eq!(cpu.read_reg(Register::A7), sp_pre);
    }

    #[test]
    fn setcpixel_getcpixel_pascal_procedure_preserves_stack_across_five_calls() {
        let (mut d, mut cpu, mut bus) = setup();
        let sp_pre = cpu.read_reg(Register::A7);
        // SetCPixel — pop-8 PROCEDURE.
        for i in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 8);
            bus.write_long(sp - 8, 0); // NIL cPix pointer (no-op path)
            bus.write_word(sp - 4, (i as u16) + 10); // v
            bus.write_word(sp - 2, (i as u16) + 100); // h
            let result = d.dispatch_quickdraw(true, 0x216, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
        }
        assert_eq!(cpu.read_reg(Register::A7), sp_pre);
        // GetCPixel — pop-8 PROCEDURE.
        for i in 0..5 {
            let sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, sp - 8);
            bus.write_long(sp - 8, 0); // NIL cPix pointer (no-op path)
            bus.write_word(sp - 4, (i as u16) + 10); // v
            bus.write_word(sp - 2, (i as u16) + 100); // h
            let result = d.dispatch_quickdraw(true, 0x217, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
        }
        assert_eq!(cpu.read_reg(Register::A7), sp_pre);
    }

    #[test]
    fn setcpixel_uses_pixmap_pmtable_not_pmreserved_for_cgrafport() {
        let (mut d, mut cpu, mut bus) = setup_with_port();
        let globals_ptr = bus.read_long(cpu.read_reg(Register::A5));
        let port_ptr = bus.read_long(globals_ptr);
        let pixmap = 0x300100u32;
        let pixmap_handle = 0x300200u32;
        let base = 0x301000u32;
        let ctab_handle = 0x302000u32;
        let reserved_ctab_handle = 0x303000u32;
        let color_ptr = 0x304000u32;
        let rgb = [0x1234u16, 0x5678, 0x9ABC];

        write_color_table(
            &mut bus,
            ctab_handle,
            0x1111_1111,
            &[(0, 0x0000, 0x0000, 0x0000), (77, rgb[0], rgb[1], rgb[2])],
        );
        write_color_table(
            &mut bus,
            reserved_ctab_handle,
            0x2222_2222,
            &[(0, 0x0000, 0x0000, 0x0000), (5, rgb[0], rgb[1], rgb[2])],
        );
        write_pixmap_8(&mut bus, pixmap, base, 2, 2, ctab_handle);
        bus.write_long(pixmap + 46, reserved_ctab_handle);
        bus.write_long(pixmap_handle, pixmap);
        bus.write_long(port_ptr + 2, pixmap_handle);
        bus.write_word(port_ptr + 6, 0xC000);
        bus.write_word(color_ptr, rgb[0]);
        bus.write_word(color_ptr + 2, rgb[1]);
        bus.write_word(color_ptr + 4, rgb[2]);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, color_ptr);
        bus.write_word(TEST_SP + 4, 1); // v
        bus.write_word(TEST_SP + 6, 1); // h

        let result = d.dispatch_quickdraw(true, 0x216, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            bus.read_byte(base + 3),
            77,
            "SetCPixel must use PixMap.pmTable at +42, not pmReserved at +46"
        );
    }

    #[test]
    fn alloccursor_pascal_procedure_preserves_stack_across_five_calls() {
        let (mut d, mut cpu, mut bus) = setup();
        let sp_pre = cpu.read_reg(Register::A7);

        for _ in 0..5 {
            let result = d.dispatch_quickdraw(true, 0x21D, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
        }

        assert_eq!(cpu.read_reg(Register::A7), sp_pre);
    }

    #[test]
    fn nqdmisc_selector_six_uses_register_args_and_leaves_gray_byte_unchanged() {
        let (mut d, mut cpu, mut bus) = setup();
        let sp = cpu.read_reg(Register::A7);
        let gray_byte = bus.alloc(2);
        let a0 = gray_byte;

        bus.write_byte(gray_byte, 0x11);
        bus.write_byte(gray_byte + 1, 0x22);

        cpu.write_reg(Register::D0, 6);
        cpu.write_reg(Register::A0, a0);

        let result = d.dispatch_quickdraw(true, 0x3C3, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok(), "NQDMisc should succeed");
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp,
            "NQDMisc should not pop a Pascal stack frame"
        );
        assert_eq!(cpu.read_reg(Register::D0), 0, "NQDMisc should return noErr");
        assert_eq!(cpu.read_reg(Register::A0), a0, "NQDMisc should preserve A0");
        assert_eq!(
            bus.read_byte(gray_byte),
            0x11,
            "NQDMisc should leave the gray-level byte unchanged on the no-op path"
        );
        assert_eq!(
            bus.read_byte(gray_byte + 1),
            0x22,
            "NQDMisc should leave the sentinel byte unchanged on the no-op path"
        );
    }
}
