//! QuickDraw trap handlers (initialization, port management, pen state,
//! coordinate transforms, region ops, drawing commands).
use super::types::{read_rect, ShapeOp};
use crate::cpu::{CpuOps, Register};
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, PortDrawState, RecentColorTableFetch};
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>>,
}
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;
// 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();
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())
}
// 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_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_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 {
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 port=${:08X}", port);
}
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) => {
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 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);
}
}
Ok(())
}
// ========== Clipping ==========
// SetClip ($A879)
// Changes the given region to be equivalent to the clipping region of the current grafPort.
// 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));
}
}
}
Ok(())
}
// GetClip ($A87A)
// Copies the current grafPort's clipping region into the given region.
// 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);
let clip_rgn_ptr = bus.read_long(clip_rgn);
let dest_ptr = bus.read_long(rgn_handle);
let rgn_size = bus.read_word(clip_rgn_ptr) as u32;
for i in 0..rgn_size {
bus.write_byte(dest_ptr + i, bus.read_byte(clip_rgn_ptr + i));
}
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): Subtracts 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 v = bus.read_word(pt_ptr) as i16;
let h = bus.read_word(pt_ptr + 2) as i16;
// wrapping_sub matches 68k Mac OS behaviour (signed
// 16-bit wrap-around) and avoids the debug-build
// overflow panic when a port's bounds.topLeft is
// outside the i16 range relative to the input point.
bus.write_word(pt_ptr, v.wrapping_sub(bounds_top) as u16);
bus.write_word(pt_ptr + 2, h.wrapping_sub(bounds_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): Adds 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 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, avoiding debug-build panics
// when a port's bounds.topLeft + the input point
// would overflow i16 range. (Real Mac OS just wraps.)
let new_v = v.wrapping_add(bounds_top);
let new_h = h.wrapping_add(bounds_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=({},{}) global=({},{}) -> local=({},{})",
trap_pc, port, bounds_top, bounds_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.recording_region.is_some() {
// During OpenRgn, line primitives extend the recorded
// bbox (both endpoints normalized) and are suppressed
// from the port per IM:I I-184.
let (top, bot) = if v0 <= v { (v0, v) } else { (v, v0) };
let (left, right) = if h0 <= h { (h0, h) } else { (h, h0) };
self.extend_recording_region(
top,
left,
bot.saturating_add(1),
right.saturating_add(1),
);
} 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.recording_region.is_some() {
// See LineTo above for OpenRgn-recording rationale.
let (top, bot) = if v0 <= v1 { (v0, v1) } else { (v1, v0) };
let (left, right) = if h0 <= h1 { (h0, h1) } else { (h1, h0) };
self.extend_recording_region(
top,
left,
bot.saturating_add(1),
right.saturating_add(1),
);
} 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 10 bytes of region data
(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);
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 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 current_port=${:08X} pnLoc=({}, {}) fg=({:04X},{:04X},{:04X}) bg=({:04X},{:04X},{:04X}) mode={} text=\"{}\"",
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);
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 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 current_port=${:08X} pnLoc=({}, {}) fg=({:04X},{:04X},{:04X}) bg=({:04X},{:04X},{:04X}) mode={} text=\"{}\"",
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);
}
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 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 current_port=${:08X} pnLoc=({}, {}) fg=({:04X},{:04X},{:04X}) bg=({:04X},{:04X},{:04X}) mode={} numer=({}, {}) denom=({}, {}) text=\"{}\"",
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;
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 + 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(())
}
// 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;
}
}
}
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) => {
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;
}
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 let Some((vt, vl, vb, vr)) = Self::region_bbox(bus, vis_rgn_handle) {
clip_t = clip_t.max(vt);
clip_l = clip_l.max(vl);
clip_b = clip_b.min(vb);
clip_r = clip_r.min(vr);
}
// Clip to clipRgn (GrafPort offset 28)
clip_rgn_handle = bus.read_long(port + 28);
if let Some((ct, cl, cb, cr)) = Self::region_bbox(bus, clip_rgn_handle) {
clip_t = clip_t.max(ct);
clip_l = clip_l.max(cl);
clip_b = clip_b.min(cb);
clip_r = clip_r.min(cr);
}
}
if let Some((mt, ml, mb, mr)) = Self::region_bbox(bus, mask_rgn) {
clip_t = clip_t.max(mt);
clip_l = clip_l.max(ml);
clip_b = clip_b.min(mb);
clip_r = clip_r.min(mr);
}
// 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(()));
}
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();
// 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
&& 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);
}
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 = trace_copybits_hud_probe().is_some()
&& src_info.pixel_size == 8
&& dst_info.pixel_size == 8
&& dst_info.base == self.screen_mode.0;
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,
[self.fg_color.0, self.fg_color.1, self.fg_color.2],
)
});
let bg_index = dst_clut.as_ref().map(|clut| {
self.palette_index_for_rgb(
bus,
dst_ctab_handle,
clut,
[self.bg_color.0, self.bg_color.1, self.bg_color.2],
)
});
// 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 src_rgb
== [self.bg_color.0, self.bg_color.1, self.bg_color.2]
{
continue;
}
}
let dst_pixel = match mode_base {
0 => palette_translation
.map(|translation| translation[src_pixel as usize])
.unwrap_or(src_pixel),
4 => {
if let (Some(src_clut), Some(dst_clut)) =
(src_clut, dst_clut)
{
let src_rgb = src_clut[src_pixel as usize];
let colorized = Self::colorize_src_copy_rgb(
src_rgb,
[self.bg_color.0, self.bg_color.1, self.bg_color.2],
[self.fg_color.0, self.fg_color.1, self.fg_color.2],
);
Self::nearest_palette_index(dst_clut, colorized)
} else {
palette_translation
.map(|translation| translation[src_pixel as usize])
.unwrap_or(src_pixel)
}
}
_ => palette_translation
.map(|translation| translation[src_pixel as usize])
.unwrap_or(src_pixel),
};
if copybits_hud_probe
&& dx == 720
&& matches!(dy, 100 | 185 | 320 | 500)
{
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={} srcBase=${: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(),
src_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));
}
}
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
let gd_handle = self.ensure_main_gdevice(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(¤t_clut)
|| Self::uses_scaled_canonical_system_8bpp_clut(¤t_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])
};
}
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);
}
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);
// 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);
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);
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) => {
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;
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;
let _bytes_per_pixel: u32 = if is_color { 1 } else { 0 }; // 0 means 1bpp
// 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;
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)
};
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);
}
}
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) as u16;
if d0 == 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 == 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 == 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 == 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 & 0xFF == 0x15 {
// 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 == 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 depth = bus.read_word(sp + 4);
let mode_id: u16 = if matches!(depth, 1 | 2 | 4 | 8) { 1 } else { 0 };
bus.write_word(sp + 10, mode_id);
cpu.write_reg(Register::A7, sp + 10);
return Some(Ok(()));
}
if d0 == 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 result = if depth == 8 {
self.do_setdepth(cpu, bus, depth);
0
} else {
(-50i16) as u16 // paramErr
};
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's framebuffer model supports CLUT depths
// 1/2/4/8 bpp; higher depths (16/32) return unsupported.
0x0A14 => {
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 mode_id = if matches!(depth, 1 | 2 | 4 | 8) { 1 } else { 0 };
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 to the requested depth (only 8bpp is
// actually supported), then returns noErr for depth 8
// or paramErr for unsupported depths.
//
// 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 = if depth == 8 {
self.do_setdepth(cpu, bus, depth);
0
} else {
(-50i16) as u16 // paramErr
};
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.
// Systemless HLE is a documented no-op stub; BII System 7.5.3
// ROM Color Manager actually builds the inverse table. The
// engines-agree subset is the pop-10 calling convention,
// proven by aa39_makeitable_strict.
(true, 0x239) => {
let sp = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, sp + 10);
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() {
eprintln!(
"[DIALOG-GWORLD] NewGWorld port=${:08X} pixmap=${:08X} base=${:08X} bounds=({},{},{},{}) depth={} rowBytes={} flags=${:08X} gdh=${:08X}",
port,
pixmap,
pixel_buf,
top,
left,
bottom,
right,
depth,
row_bytes,
flags,
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, u16::from(locked));
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
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
{
// Allocate new pixel buffer and copy old data
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);
}
// Copy overlapping rows
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, u16::from(done));
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, u16::from(requires_32bit));
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 (2 bytes pre-pushed by caller). 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 2-byte BOOLEAN result slot 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, u16::from(locked));
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 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 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 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,
pic_ptr,
adj_top,
adj_left,
adj_bottom,
adj_right,
port_mode,
draw_port_clut,
device_ct_seed,
);
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
&& port_base == self.screen_mode.0
&& port_rb == self.screen_mode.1
{
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,
pic_ptr,
fetch_clut,
Some(fetch.ct_id),
);
} else if let Some(pict_clut) = pict_clut.as_ref() {
// Hour-133 Palette Manager: merge the PICT's
// non-zero entries into a copy of the current
// color_manager_clut instead of using a
// zero-initialised buffer. A PICT CTab that
// declares only 64 entries (common for panel
// PICTs in multi-PICT dialogs like EV's
// landing scene) would previously zero the
// remaining 192 slots, wiping out the screen
// palette's existing colours. With a merge,
// only the indices the PICT claims are
// overwritten; the rest retain their current
// values. This is the Palette Manager's
// "cumulative palette install" semantics —
// Inside Macintosh Volume VI 20-12 notes
// that a window palette with fewer entries
// than the device CLUT only affects the
// entries it defines.
// Snapshot the PICT's raw CTab (zero-padded,
// as emitted by the PICT parser) for the
// dense-grayscale gate check — protects EV
// title from being reseeded from its
// grayscale art.
let mut raw_pict_array = [[0u16; 3]; 256];
for (index, rgb) in pict_clut.iter().take(256).enumerate() {
raw_pict_array[index] = *rgb;
}
// Merge semantics: start from the current
// color_manager_clut, overlay only the non-zero
// entries the PICT specifies.
let mut pict_clut_array = self.color_manager_clut;
let mut had_nonzero = false;
for (index, rgb) in pict_clut.iter().take(256).enumerate() {
if !(rgb[0] == 0 && rgb[1] == 0 && rgb[2] == 0) {
pict_clut_array[index] = *rgb;
had_nonzero = true;
}
}
if had_nonzero
&& 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,
pic_ptr,
adj_top,
adj_left,
adj_bottom,
adj_right,
port_mode,
&self.device_clut,
device_ct_seed,
);
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]
);
}
}
}
}
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),
);
}
}
}
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 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 ctab_ptr = icon_ptr + 82 + mask_size + bmap_size;
let ct_size = bus.read_word(ctab_ptr + 6) as u32;
let ctab_total_bytes = 8 + (ct_size + 1) * 8;
let inline_pixel_ptr = ctab_ptr + ctab_total_bytes;
let pixel_len =
resource_size.saturating_sub(inline_pixel_ptr - icon_ptr);
let icon_data_handle = if pixel_len > 0 {
let pixel_ptr = bus.alloc(pixel_len);
if pixel_ptr == 0 {
bus.free(icon_ptr);
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);
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);
0
} else {
bus.write_long(pixel_handle, 0);
pixel_handle
}
};
if icon_data_handle == 0 && resource_size != 0 {
0
} else {
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_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;
// 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 port_version = bus.read_word(port + 4);
let (dst_base, dst_row_bytes, dst_bounds_top, dst_bounds_left) =
if (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,
)
} else {
(
bus.read_long(port),
(bus.read_word(port + 4) & 0x3FFF) as u32,
bus.read_word(port + 6) as i16,
bus.read_word(port + 8) as i16,
)
};
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),
];
}
// Draw: for each destination pixel, map to source, check mask, copy pixel
let bytes_per_pixel = if pixel_size >= 8 { pixel_size / 8 } else { 1 };
for dy in 0..dst_h {
// Map destination row to source row (scale)
let sy = (dy * icon_h / dst_h) as u32;
let dst_y = (dst_top as i32 + dy - dst_bounds_top as i32) as u32;
for dx in 0..dst_w {
let sx = (dx * icon_w / dst_w) as u32;
let dst_x = (dst_left as i32 + dx - dst_bounds_left as i32) as u32;
// Check mask bit
let mask_byte_off = sy * mask_row_bytes + sx / 8;
let mask_bit = 7 - (sx % 8);
let mask_byte = bus.read_byte(mask_data_ptr + mask_byte_off);
if (mask_byte & (1 << mask_bit)) == 0 {
continue; // Masked out
}
// Read source pixel
if pixel_size >= 8 {
let src_off = sy * pm_row_bytes + sx * bytes_per_pixel;
let dst_off = dst_y * dst_row_bytes + dst_x * bytes_per_pixel;
for b in 0..bytes_per_pixel {
let pixel = bus.read_byte(icon_data_ptr + src_off + b);
bus.write_byte(dst_base + dst_off + b, pixel);
}
} else {
// 1bpp icon data path
let src_byte_off = sy * pm_row_bytes + sx / 8;
let src_bit = 7 - (sx % 8);
let src_byte = bus.read_byte(icon_data_ptr + src_byte_off);
let is_set = (src_byte & (1 << src_bit)) != 0;
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 is_set {
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_data_handle = bus.read_long(icon_ptr + 78);
bus.write_long(icon_ptr + 78, 0);
Self::free_handle_and_target(bus, icon_data_handle);
self.ptr_to_handle.remove(&icon_ptr);
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);
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(¤t_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, ¤t_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(()));
}
} else {
Self::write_region(bus, rgn_handle, None, &[]);
}
}
// 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).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 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) => font_id_for_name(name).unwrap_or(0),
Err(_) => 0,
}
} else {
0
};
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;
// Inside Macintosh Volume I, I-186
// Starts region recording. While recording, drawing primitives
// accumulate their bounds into self.recording_region and skip
// framebuffer writes.
// OpenRgn ($A8DA): Begins region recording into self.recording_region (bbox-only); subsequent drawing primitives skip framebuffer writes per IM:I I-186
(true, 0x0DA) => {
self.recording_region = Some((i16::MAX, i16::MAX, i16::MIN, i16::MIN));
Ok(())
}
// CloseRgn ($A8DB)
// PROCEDURE CloseRgn(dstRgn: RgnHandle);
// Inside Macintosh Volume I, I-186
// Ends recording. Writes the accumulated bbox into the dst
// region's data block. Region storage is bbox-only (matches
// SectRgn/UnionRgn/etc bbox-approx semantics).
// CloseRgn ($A8DB): Ends region recording, writes accumulated bbox into dstRgn (bbox-only storage) per IM:I I-186
(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((rt, rl, rb, rr)) = self.recording_region.take() {
if dst_handle != 0 {
let dst_ptr = bus.read_long(dst_handle);
if dst_ptr != 0 {
let (top, left, bottom, right) = if rt > rb || rl > rr {
// Empty recording.
(0, 0, 0, 0)
} else {
(rt, rl, rb, rr)
};
bus.write_word(dst_ptr, 10); // rgnSize
bus.write_word(dst_ptr + 2, top as u16);
bus.write_word(dst_ptr + 4, left as u16);
bus.write_word(dst_ptr + 6, bottom as u16);
bus.write_word(dst_ptr + 8, right as u16);
}
}
}
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);
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;
bus.write_word(rgn_ptr + 2, (t + dv) as u16);
bus.write_word(rgn_ptr + 4, (l + dh) as u16);
bus.write_word(rgn_ptr + 6, (b + dv) as u16);
bus.write_word(rgn_ptr + 8, (r + dh) as u16);
}
}
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);
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;
bus.write_word(rgn_ptr + 2, (t + dv) as u16);
bus.write_word(rgn_ptr + 4, (l + dh) as u16);
bus.write_word(rgn_ptr + 6, (b - dv) as u16);
bus.write_word(rgn_ptr + 8, (r - dh) as u16);
}
}
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 {
let pa = bus.read_long(src_a);
let pb = bus.read_long(src_b);
let pd = bus.read_long(dst_handle);
if pa != 0 && pb != 0 && pd != 0 {
let at = bus.read_word(pa + 2) as i16;
let al = bus.read_word(pa + 4) as i16;
let ab = bus.read_word(pa + 6) as i16;
let ar = bus.read_word(pa + 8) as i16;
let bt = bus.read_word(pb + 2) as i16;
let bl = bus.read_word(pb + 4) as i16;
let bb = bus.read_word(pb + 6) as i16;
let br = bus.read_word(pb + 8) as i16;
let dt = at.max(bt);
let dl = al.max(bl);
let db = ab.min(bb);
let dr = ar.min(br);
bus.write_word(pd, 10);
if dt < db && dl < dr {
bus.write_word(pd + 2, dt as u16);
bus.write_word(pd + 4, dl as u16);
bus.write_word(pd + 6, db as u16);
bus.write_word(pd + 8, dr as u16);
} else {
bus.write_long(pd + 2, 0);
bus.write_long(pd + 6, 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 {
let pa = bus.read_long(src_a);
let pb = bus.read_long(src_b);
let pd = bus.read_long(dst_handle);
if pa != 0 && pb != 0 && pd != 0 {
let at = bus.read_word(pa + 2) as i16;
let al = bus.read_word(pa + 4) as i16;
let ab = bus.read_word(pa + 6) as i16;
let ar = bus.read_word(pa + 8) as i16;
let bt = bus.read_word(pb + 2) as i16;
let bl = bus.read_word(pb + 4) as i16;
let bb = bus.read_word(pb + 6) as i16;
let br = bus.read_word(pb + 8) as i16;
let a_empty = ab <= at || ar <= al;
let b_empty = bb <= bt || br <= bl;
let (dt, dl, db, dr) = if a_empty && b_empty {
(0, 0, 0, 0)
} else if a_empty {
(bt, bl, bb, br)
} else if b_empty {
(at, al, ab, ar)
} else {
(at.min(bt), al.min(bl), ab.max(bb), ar.max(br))
};
bus.write_word(pd, 10);
bus.write_word(pd + 2, dt as u16);
bus.write_word(pd + 4, dl as u16);
bus.write_word(pd + 6, db as u16);
bus.write_word(pd + 8, dr as u16);
}
}
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 {
let pa = bus.read_long(src_a);
let pd = bus.read_long(dst_handle);
if pa != 0 && pd != 0 {
let at = bus.read_word(pa + 2) as i16;
let al = bus.read_word(pa + 4) as i16;
let ab = bus.read_word(pa + 6) as i16;
let ar = bus.read_word(pa + 8) as i16;
let pb = if src_b != 0 { bus.read_long(src_b) } else { 0 };
let (bt, bl, bb, br, b_empty) = if pb != 0 {
let t = bus.read_word(pb + 2) as i16;
let l = bus.read_word(pb + 4) as i16;
let b = bus.read_word(pb + 6) as i16;
let r = bus.read_word(pb + 8) as i16;
let empty = t >= b || l >= r;
(t, l, b, r, empty)
} else {
(0, 0, 0, 0, true)
};
bus.write_word(pd, 10);
let a_empty = at >= ab || al >= ar;
let equal =
!a_empty && !b_empty && at == bt && al == bl && ab == bb && ar == br;
let b_contains_a =
!a_empty && !b_empty && bt <= at && bl <= al && bb >= ab && br >= ar;
if a_empty || equal || b_contains_a {
bus.write_long(pd + 2, 0);
bus.write_long(pd + 6, 0);
} else {
// Over-approximation: return A unchanged.
bus.write_word(pd + 2, at as u16);
bus.write_word(pd + 4, al as u16);
bus.write_word(pd + 6, ab as u16);
bus.write_word(pd + 8, ar as u16);
}
}
}
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 {
let pa = bus.read_long(src_a);
let pb = bus.read_long(src_b);
let pd = bus.read_long(dst_handle);
if pa != 0 && pb != 0 && pd != 0 {
let at = bus.read_word(pa + 2) as i16;
let al = bus.read_word(pa + 4) as i16;
let ab = bus.read_word(pa + 6) as i16;
let ar = bus.read_word(pa + 8) as i16;
let bt = bus.read_word(pb + 2) as i16;
let bl = bus.read_word(pb + 4) as i16;
let bb = bus.read_word(pb + 6) as i16;
let br = bus.read_word(pb + 8) as i16;
let a_empty = at >= ab || al >= ar;
let b_empty = bt >= bb || bl >= br;
bus.write_word(pd, 10);
if a_empty && b_empty {
// Empty result: write zero rect.
bus.write_long(pd + 2, 0);
bus.write_long(pd + 6, 0);
} else if a_empty {
bus.write_word(pd + 2, bt as u16);
bus.write_word(pd + 4, bl as u16);
bus.write_word(pd + 6, bb as u16);
bus.write_word(pd + 8, br as u16);
} else if b_empty {
bus.write_word(pd + 2, at as u16);
bus.write_word(pd + 4, al as u16);
bus.write_word(pd + 6, ab as u16);
bus.write_word(pd + 8, ar as u16);
} else if at == bt && al == bl && ab == bb && ar == br {
// Equal regions → empty.
bus.write_long(pd + 2, 0);
bus.write_long(pd + 6, 0);
} else {
// Over-approximation: bbox of (A ∪ B).
bus.write_word(pd + 2, at.min(bt) as u16);
bus.write_word(pd + 4, al.min(bl) as u16);
bus.write_word(pd + 6, ab.max(bb) as u16);
bus.write_word(pd + 8, ar.max(br) as u16);
}
}
}
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);
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);
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 the PICT directly from the resource bytes without going
// through the bottleneck, so direct trap calls are a no-op that
// pop the frame and return 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;
// Source: NIL handle = current device CLUT, else read from
// the CTab pointed to by the handle.
let src_ptr = if src_handle != 0 {
bus.read_long(src_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
};
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);
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);
if let Some(pat) = Self::read_pixpat_pat1data(bus, pp_handle) {
let r = read_rect(bus, rect_ptr);
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) {
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: Stub → Partial. Matches the established GetXxx
// family pattern (A9B9 GetCursor / A9B8 GetPattern /
// A9BB GetIcon / A9BC GetPicture / AA1F GetCIcon /
// AA0C GetPixPat). All route through find_resource_any +
// get_or_create_resource_handle for handle stability and
// return NIL on miss per the IM-canonical "resource not
// found returns NIL" semantic.
//
// HLE compromise on the present-resource path: returns
// the master ptr to the raw 'crsr' resource bytes wrapped
// in a stable handle, NOT a freshly-allocated CCrsr
// record. BasiliskII System 7.5.3 ROM Color QuickDraw
// allocates a fresh CCrsr record per the IM:V V-74 layout
// (PixMap + colorTable + crsrXData expanded for current
// screen depth). The handle-stability gate matters
// because apps cache the GetCCursor result and reuse it
// across many SetCCursor calls per frame; without
// get_or_create_resource_handle each call would allocate
// a fresh handle and leak. Per IM:V V-74 explicit
// guidance: "Since GetCCursor creates a new CCrsr data
// structure each time it is called, your application
// shouldn't call GetCCursor before each call to
// SetCCursor."
//
// 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.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 {
let mut data = [0u8; 32];
for (i, byte) in data.iter_mut().enumerate() {
*byte = bus.read_byte(crsr_ptr + 20 + i as u32);
}
let mut mask = [0u8; 32];
for (i, byte) in 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;
self.cursor_data = Some((data, mask, hot_v, hot_h));
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); GetCCursor doesn't allocate so disposal is a no-op per IM:V V-80. 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 can be at any position with the same dimensions
// (CopyMask does not scale).
// 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 unscaled blit srcBits → dstBits via maskBits (1 = copy, 0 = preserve dst); supports 1bpp + 8bpp dst per IM:IV IV-33
(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 dst_top = bus.read_word(dst_rect_ptr) as i16;
let dst_left = bus.read_word(dst_rect_ptr + 2) as i16;
// Walk every (sy, sx) inside srcRect; map to mask + dst
// by relative offset. CopyMask is unscaled, so the loop
// iterates srcRect once and uses offsets into the other
// two rects.
for sy in src_top..src_bottom {
for sx in src_left..src_right {
let dy_off = sy - src_top;
let dx_off = sx - src_left;
let my = mask_top + dy_off;
let mx = mask_left + dx_off;
let dy = dst_top + dy_off;
let dx = dst_left + dx_off;
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;
};
// Write src pixel to dst at (dy, dx). Reuse the
// bounds-checked path: skip writes outside dst.
if dy < dst_info.bounds_top
|| dy >= dst_info.bounds_bottom
|| dx < dst_info.bounds_left
|| dx >= dst_info.bounds_right
{
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 let Some((mt, ml, mb, mr)) = Self::region_bbox(bus, mask_rgn) {
clip_t = clip_t.max(mt);
clip_l = clip_l.max(ml);
clip_b = clip_b.min(mb);
clip_r = clip_r.min(mr);
}
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).
//
// BII-vs-Systemless divergence: BII System 7.5.3 ROM walks the src
// bit image from the (seedH, seedV) origin and writes the
// paint-bucket-style fill into dst per IM:IV IV-22. Systemless
// HLE is a no-op stub for the same reason as CalcMask above.
// The strict bake witnesses only the engines-agree 20-byte
// pop discipline (single-call + 5-call composition with varying
// seedH/seedV pairs).
//
// 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
(true, 0x039) => {
let sp = cpu.read_reg(Register::A7);
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.ensure_main_gdevice(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.ensure_main_gdevice(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
}
/// While OpenRgn..CloseRgn is in progress, drawing primitives extend
/// the accumulated bbox here and skip the actual framebuffer write
/// per IM:I I-189. Returns true if recording is active (caller should
/// skip the draw); false if not.
pub(crate) fn extend_recording_region(
&mut self,
top: i16,
left: i16,
bottom: i16,
right: i16,
) -> bool {
if let Some(bbox) = self.recording_region.as_mut() {
if top < bbox.0 {
bbox.0 = top;
}
if left < bbox.1 {
bbox.1 = left;
}
if bottom > bbox.2 {
bbox.2 = bottom;
}
if right > bbox.3 {
bbox.3 = right;
}
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;
}
if let Some((top, left, bottom, right)) = Self::region_handle_rect(bus, rgn_handle) {
self.extend_recording_region(top, left, bottom, right);
}
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 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
}
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
}
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 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 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_base == self.screen_mode.0
&& port_rb == self.screen_mode.1
&& port == recent.port;
if eligible {
return self.recent_resource_ctable_fetch.take();
}
None
}
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
}
fn gdevice_ctab_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;
}
let Some(ctab_handle_addr) = pm_ptr.checked_add(42) else {
return 0;
};
bus.read_long(ctab_handle_addr)
}
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(¤t_clut)
&& !Self::uses_scaled_canonical_system_8bpp_clut(¤t_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);
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;
}
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;
}
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
}
/// Apply the side effects of `SetDepth(depth)` from the Graphics
/// Devices Manager. Updates ScrnBase, screenBits, the QD globals
/// copy of screenBits, and `self.screen_mode`, then clears the
/// 8bpp framebuffer to black (index 255).
///
/// Currently only 8bpp is fully wired up; other depths leave the
/// state alone (matching the original behavior of the squatting
/// SetDepth handler at $AA36).
///
/// Inside Macintosh Volume VI, VI-21-12
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 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) {
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 | width);
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(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,
) {
if depth != 8 {
return;
}
eprintln!("[TRAP] SetDepth: depth={}", depth);
let ram_size = bus.ram_size();
let color_screen_base = ram_size - 0x80000;
let row_bytes = u32::from(width);
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);
// Clear the 8bpp screen to black (index 255 = black in Mac palette).
for i in 0..(u32::from(width) * u32::from(height)) {
bus.write_byte(color_screen_base + i, 0xFF);
}
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;
}
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;
}
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 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 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 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 let Some((vt, vl, vb, vr)) = Self::region_bbox(bus, vis_rgn_handle) {
clip_t = clip_t.max(vt);
clip_l = clip_l.max(vl);
clip_b = clip_b.min(vb);
clip_r = clip_r.min(vr);
}
clip_rgn_handle = bus.read_long(port + 28);
if let Some((ct, cl, cb, cr)) = Self::region_bbox(bus, clip_rgn_handle) {
clip_t = clip_t.max(ct);
clip_l = clip_l.max(cl);
clip_b = clip_b.min(cb);
clip_r = clip_r.min(cr);
}
}
if let Some((mt, ml, mb, mr)) = Self::region_bbox(bus, mask_rgn) {
clip_t = clip_t.max(mt);
clip_l = clip_l.max(ml);
clip_b = clip_b.min(mb);
clip_r = clip_r.min(mr);
}
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(());
}
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();
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,
[self.fg_color.0, self.fg_color.1, self.fg_color.2],
)
});
let bg_index = dst_clut.as_ref().map(|clut| {
self.palette_index_for_rgb(
bus,
dst_ctab_handle,
clut,
[self.bg_color.0, self.bg_color.1, self.bg_color.2],
)
});
// 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 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 {
let src_rgb = src_clut.unwrap()[src_pixel as usize];
if src_rgb == [self.bg_color.0, self.bg_color.1, self.bg_color.2] {
continue;
}
}
let dst_pixel = match mode_base {
0 => palette_translation
.map(|translation| translation[src_pixel as usize])
.unwrap_or(src_pixel),
4 => {
if let (Some(src_clut), Some(dst_clut)) = (src_clut, dst_clut) {
let src_rgb = src_clut[src_pixel as usize];
let colorized = Self::colorize_src_copy_rgb(
src_rgb,
[self.bg_color.0, self.bg_color.1, self.bg_color.2],
[self.fg_color.0, self.fg_color.1, self.fg_color.2],
);
Self::nearest_palette_index(dst_clut, colorized)
} else {
palette_translation
.map(|translation| translation[src_pixel as usize])
.unwrap_or(src_pixel)
}
}
_ => palette_translation
.map(|translation| translation[src_pixel as usize])
.unwrap_or(src_pixel),
};
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,
);
}
}
Ok(())
}
pub(super) fn resolve_copy_bitmap(&self, bus: &MacMemoryBus, bits_ptr: u32) -> CopyBitmapInfo {
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 {
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 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))))
}
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 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 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((bottom - top) as usize);
for y in top..bottom {
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 })
}
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]
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
}
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] = 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 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)
}
}
}
/// 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 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,
) {
// POD MARS Master ships its 'ctab' resource ID 128 in a custom
// 'ajcp'-prefixed format (1190 bytes, neither standard 2056-byte
// ColorTable nor Apple compressed-resource layout). The app
// installs that handle's data at an A5-relative global, then
// calls SetEntries(0, 255, *handle + 8) — a full-CLUT replace.
// Without POD's own decompressor running, the buffer behind that
// handle has only the 8-byte header populated; entries past the
// first one are zero, and applying the call wipes 250+ device-
// CLUT slots to (0, 0, 0). The screen renders all-black even
// though the framebuffer holds valid index 247/248/249/254 etc.
//
// Real Mac OS hands the same call through, but on a real Mac the
// decompressed buffer has 256 valid ColorSpec entries so the
// replace is benign. Until Systemless implements POD's decoder,
// detect the case where the caller passed `ctab + 8` (i.e. the
// 8 bytes preceding `table_ptr` form a plausible device-owned
// CTAB header with `ctFlags` high bit set and `ctSize < count`)
// and clamp the update to the header-declared size. The fade-
// down path (stack-built CSpecArray with no preceding header)
// is unaffected because random stack bytes almost never satisfy
// the `ctFlags & 0x8000` test.
let mut effective_count = count;
let mut undersized_ctab_replace = false;
if start >= 0 && count > 0 && table_ptr >= 8 {
let header_addr = table_ptr - 8;
let ct_flags = bus.read_word(header_addr + 4);
let ct_size = bus.read_word(header_addr + 6) as i16;
if (ct_flags & 0x8000) != 0 && ct_size >= 0 && ct_size < count {
if trace_palette_enabled() {
eprintln!(
"[PALETTE] SetEntries clamp: header@${:08X} ctFlags=${:04X} ctSize={} < count={} → effective count={}",
header_addr, ct_flags, ct_size, count, ct_size
);
}
effective_count = ct_size;
// Treat the call as a full-replace whose source data is
// truncated (POD's 'ctab' decompression isn't running, so
// entries past `ctSize` are zero garbage). Refresh the
// un-updated slots from `color_manager_clut`, which still
// holds the canonical palette installed before the fade-
// down sequence. Without this, the post-fade dark device
// CLUT bleeds through and the screen stays nearly black
// even though the dialog and its content draw correctly.
undersized_ctab_replace = start == 0;
}
}
let num_entries = (effective_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];
}
}
}
if undersized_ctab_replace && target_is_screen {
// Restore canonical palette to the slots the truncated source
// CTAB couldn't cover. See `undersized_ctab_replace` setup
// above for the POD MARS Master rationale.
let cm = self.color_manager_clut;
let written = (effective_count as usize).saturating_add(1);
for (idx, cm_entry) in cm.iter().enumerate().skip(written) {
if !self.clut_protected[idx] && !self.clut_reserved[idx] {
self.device_clut[idx] = *cm_entry;
}
}
}
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,
) {
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;
}
// 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 target_is_screen = self.set_entries_target_is_screen(bus);
let is_full_replace = start == 0 && count == 255;
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);
// Detect the "source CTAB undersized" case: the caller passed a
// `count` parameter larger than the source CTAB's declared
// ctSize. The matching clamp in `apply_set_entries` (line ~12939)
// only updates [start .. start+ctSize+1] in device_clut and
// restores the remaining slots from `color_manager_clut`. So the
// outer count parameter still reads as 255 (full-replace), but
// the actual fresh data only covers a prefix. Publishing the
// full device_clut to cm would lock cm to a frankenstein palette
// and corrupt the inverse-table baseline. Skip the publish when
// the source CTAB header reports fewer entries than the caller's
// count — this is generic and triggers any time SetEntries is
// called with a mismatched count/ctSize (Inside Macintosh
// Volume V, V-143 documents `count` as the entry-index of the
// last entry, so it must be ≤ ctSize).
let source_ctab_undersized = if start >= 0 && count > 0 && table_ptr >= 8 {
let header_addr = table_ptr - 8;
let ct_flags = bus.read_word(header_addr + 4);
let ct_size = bus.read_word(header_addr + 6) as i16;
(ct_flags & 0x8000) != 0 && ct_size >= 0 && ct_size < count
} else {
false
};
if source_ctab_undersized && target_is_screen {
// The caller's count parameter exceeded the source CTAB's
// ctSize, so the apply step clamped its write. Treat the
// call as effectively a no-op for the screen CLUT: restore
// every slot in device_clut from color_manager_clut so the
// bad ColorSpec bytes the truncated source produced for
// [start .. start+ctSize+1] don't poison subsequent
// index→RGB lookups. The cm_clut is already untouched
// (we skip the publish below).
if trace_palette_enabled() {
eprintln!(
"[PALETTE] PublishCm-skip: source CTAB undersized (ctSize<count); restoring device_clut from cm_clut"
);
}
let cm = self.color_manager_clut;
for (idx, cm_entry) in cm.iter().enumerate() {
if !self.clut_protected[idx] && !self.clut_reserved[idx] {
self.device_clut[idx] = *cm_entry;
}
}
}
if target_is_screen
&& is_full_replace
&& !dimmed_scale_of_current
&& !source_ctab_undersized
{
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)
}
}
/// 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 + 46),
)
} 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;
}
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)
}
/// 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,
)
}
}
}
#[cfg(test)]
mod tests {
use super::super::test_helpers::{setup, setup_with_port, TEST_SP};
use crate::cpu::{CpuOps, Register};
use crate::memory::{MacMemoryBus, MemoryBus};
use crate::trap::quickdraw::CopyBitmapInfo;
use crate::trap::types::{Rect, ShapeOp};
use crate::trap::TrapDispatcher;
/// 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);
}
/// 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_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,
)
}
/// 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_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_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 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 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 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 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 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 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 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 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);
}
// 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_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_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);
}
#[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 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 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 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((32767, 32767, -32767, -32767));
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,
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 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 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);
}
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);
}
}
#[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_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_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
}
#[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 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_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 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 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.
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, 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_ne!(bus.read_word(TEST_SP + 12), 0);
}
#[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, 16); // 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_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_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_word(TEST_SP + 4), 1);
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), 1);
}
#[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), 1);
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), 1);
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), 1);
}
#[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), 1);
}
#[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_word(TEST_SP + 4), 1); // TRUE
}
#[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_word(TEST_SP + 4), 1);
}
#[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 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,
);
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_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,
);
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 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,
);
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,
);
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, ¤t
));
}
#[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, ¤t
));
}
#[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, ¤t
));
}
#[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, ¤t
));
}
#[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_ignored_for_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!(fetch.is_none());
assert!(d.recent_resource_ctable_fetch.is_some());
}
#[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 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 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"
);
}
}