use crate::cpu::{M68kCpu, Register, StepResult};
use crate::debug_overlay::{DebugOverlayFrameStats, DebugOverlaySnapshot};
use crate::display::CursorImage;
use crate::loader::ppc::{
PpcDecodedAiffPlaybackRecord, PpcDecodedBufferCommandRecord, PpcDrawSprocketTraceEntry,
PpcFrontBuffer, PpcGWorldRecord, PpcHleImportTraceEntry, PpcImportBinding,
PpcImportDispatcherTarget, PpcInputSnapshot, PpcInputSprocketSimpleStateTraceEntry,
PpcLoadedApp, PpcQ3GpuFrame, PpcQ3SceneReplay, PpcQ3SoftwareRenderStats, PpcRgbColor,
PpcSoundCompletionRecord, PpcSoundDoubleBackRecord, PpcSoundDoubleBufferPlaybackRecord,
};
use crate::loader::{
decode_retro68_relocations, ApplicationSizeResource, Code0Header, CodeSegmentHeader,
JumpTableEntry, LoadedApp, MpwFarSegmentHeader, Retro68RelocationError,
};
use crate::managers::resource::ResourceFork;
use crate::memory::GuestAddressSpace as PpcSectionMem;
use crate::memory::{MacMemoryBus, MemoryBus};
use crate::menu_model::GuestMenuSnapshot;
use crate::trap::TrapDispatcher;
use crate::ui_theme::{ThemeMetricsMode, UiTheme, UiThemeId};
use crate::{Error, Result};
use m68k::BatchExit;
use ppc::{PpcException, PpcFetchHistogram, PpcMemory, PpcRunResult};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
use std::sync::OnceLock;
static TRACE_DIALOG_FILTER: OnceLock<bool> = OnceLock::new();
static TRACE_TIMER: OnceLock<bool> = OnceLock::new();
static TRACE_VBL: OnceLock<bool> = OnceLock::new();
static TRACE_SOUND_RUNNER: OnceLock<bool> = OnceLock::new();
static TRACE_DIALOG_PROCS: OnceLock<bool> = OnceLock::new();
fn ppc_run_result_cycles(result: PpcRunResult) -> u64 {
match result {
PpcRunResult::CycleLimit { cycles }
| PpcRunResult::Halted { cycles, .. }
| PpcRunResult::Unimplemented { cycles, .. }
| PpcRunResult::MemoryFault { cycles, .. }
| PpcRunResult::Exception { cycles, .. }
| PpcRunResult::FetchFault { cycles, .. } => cycles,
}
}
fn ppc_halted_by_exit_to_shell(
imports: &[PpcImportBinding],
result: PpcRunResult,
last_import_index: Option<u32>,
unsupported_import_index: Option<u32>,
) -> bool {
unsupported_import_index.is_none()
&& matches!(result, PpcRunResult::Halted { .. })
&& last_import_index.is_some_and(|symbol_index| {
imports
.iter()
.find(|binding| binding.symbol_index == symbol_index)
.is_some_and(|binding| {
binding.dispatcher_target == PpcImportDispatcherTarget::ExitToShell
})
})
}
fn ppc_decoded_playback_duration_ticks(decoded: &PpcDecodedAiffPlaybackRecord) -> Option<u32> {
if decoded.sample_rate_fixed == 0 {
return None;
}
let sample_count = u128::try_from(decoded.samples.len()).ok()?;
if sample_count == 0 {
return Some(0);
}
let numerator = sample_count.checked_mul(1203)?.checked_mul(1u128 << 16)?;
let denominator = u128::from(decoded.sample_rate_fixed).checked_mul(20)?;
let ticks = numerator
.checked_add(denominator.saturating_sub(1))?
.checked_div(denominator)?;
Some(u32::try_from(ticks).unwrap_or(u32::MAX))
}
fn ppc_resource_sidecar_parent(
root: &std::path::Path,
normalized_path: &str,
) -> Option<(std::path::PathBuf, std::ffi::OsString)> {
let host_path = root.join(normalized_path);
Some((
host_path.parent()?.to_path_buf(),
host_path.file_name()?.to_os_string(),
))
}
const APP_HEAP_FLOOR: u32 = 0x0020_0000;
const APP_ZONE_HEADER_SIZE: u32 = 64;
const APP_STACK_SAFETY_MARGIN: u32 = 0x2000;
const DEFAULT_LOAD_ADDRESS: u32 = 0x0001_0000;
const APP_QD_GLOBALS_RESERVE: u32 = 48 * 1024;
const APP_LOADER_CLEAR_RESERVE: u32 = 0x40000;
const APP_HIGH_MEMORY_RESERVE: u32 = 2 * 1024 * 1024;
const APPLICATION_RESOURCE_REFNUM: u16 = 2;
const HFS_FCB_SIZE: u16 = 94;
const HFS_FCB_BUFFER_SIZE: u16 = 2 + HFS_FCB_SIZE;
const HFS_VCB_SIZE: u32 = 178;
const PPC_SOUND_COMPLETION_CALLBACK_MAX_CYCLES: u64 = 250_000;
fn trace_dialog_filter_enabled() -> bool {
*TRACE_DIALOG_FILTER
.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_FILTER").is_some())
}
fn trace_timer_enabled() -> bool {
*TRACE_TIMER.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_TIMER").is_some())
}
fn trace_vbl_enabled() -> bool {
*TRACE_VBL.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_VBL").is_some())
}
fn trace_sound_runner_enabled() -> bool {
*TRACE_SOUND_RUNNER.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_SOUND").is_some())
}
fn trace_dialog_procs_enabled() -> bool {
*TRACE_DIALOG_PROCS.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_PROCS").is_some())
}
#[cfg(not(target_arch = "wasm32"))]
static TRACE_BUFFER_ENABLED: OnceLock<bool> = OnceLock::new();
fn trace_buffer_enabled() -> bool {
#[cfg(target_arch = "wasm32")]
{
return false;
}
#[cfg(not(target_arch = "wasm32"))]
*TRACE_BUFFER_ENABLED.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_BUFFER").is_some())
}
#[cfg(not(target_arch = "wasm32"))]
static TRACE_PC_RANGE: OnceLock<Option<(u32, u32)>> = OnceLock::new();
#[cfg(not(target_arch = "wasm32"))]
static TRACE_PC_RANGE_TICKS: OnceLock<Option<(Option<u32>, Option<u32>)>> = OnceLock::new();
#[cfg(not(target_arch = "wasm32"))]
fn trace_pc_range() -> Option<(u32, u32)> {
*TRACE_PC_RANGE.get_or_init(|| {
let value = std::env::var("SYSTEMLESS_TRACE_PC_RANGE").ok()?;
let mut parts = value.split(':');
let start = parts.next()?.trim();
let end = parts.next()?.trim();
let parse = |s: &str| u32::from_str_radix(s.trim_start_matches("0x"), 16).ok();
Some((parse(start)?, parse(end)?))
})
}
#[cfg(not(target_arch = "wasm32"))]
fn trace_pc_range_ticks() -> Option<(Option<u32>, Option<u32>)> {
*TRACE_PC_RANGE_TICKS.get_or_init(|| {
let min = std::env::var("SYSTEMLESS_TRACE_PC_RANGE_TICK_MIN")
.ok()
.and_then(|v| v.parse::<u32>().ok());
let max = std::env::var("SYSTEMLESS_TRACE_PC_RANGE_TICK_MAX")
.ok()
.and_then(|v| v.parse::<u32>().ok());
(min.is_some() || max.is_some()).then_some((min, max))
})
}
fn trace_pc_range_contains(pc: u32, tick: u32) -> bool {
#[cfg(target_arch = "wasm32")]
{
let _ = (pc, tick);
return false;
}
#[cfg(not(target_arch = "wasm32"))]
{
let in_range = trace_pc_range()
.map(|(start, end)| pc >= start && pc <= end)
.unwrap_or(false);
if !in_range {
return false;
}
trace_pc_range_ticks()
.map(|(min, max)| {
min.map(|min| tick >= min).unwrap_or(true)
&& max.map(|max| tick <= max).unwrap_or(true)
})
.unwrap_or(true)
}
}
static TRACE_LOAD_ENABLED: OnceLock<bool> = OnceLock::new();
pub(crate) fn trace_load_enabled() -> bool {
*TRACE_LOAD_ENABLED.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_LOAD").is_some())
}
pub fn decode_fakeptr_pc(pc: u32) -> Option<String> {
if (0x00F00000..=0x00F0FFFF).contains(&pc) {
let trap_word = (pc & 0xFFFF) as u16;
Some(format!(
"PC matches GetTrapAddress fake-ptr ($A046/$A346/$A746) for trap ${:04X} — \
game likely JMP/JSR'd through the unique-address placeholder. Implementing \
a re-trap trampoline at the fake-ptr address would unblock this path.",
trap_word
))
} else if (0xCAFE0000..=0xCAFE03FF).contains(&pc) {
let trap_num = (pc - 0xCAFE0000) as u16;
let trap_word = 0xA800 | trap_num;
Some(format!(
"PC matches GetToolTrapAddress fake-ptr for trap ${:04X} (tool num=${:03X}) — \
same trampoline gap as the OS-style fake-ptr range.",
trap_word, trap_num
))
} else {
None
}
}
#[cfg(not(target_arch = "wasm32"))]
static TRACE_OPCODE_COUNTS: OnceLock<bool> = OnceLock::new();
fn trace_opcode_counts_enabled() -> bool {
#[cfg(target_arch = "wasm32")]
{
return false;
}
#[cfg(not(target_arch = "wasm32"))]
*TRACE_OPCODE_COUNTS
.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_OPCODE_COUNTS").is_some())
}
static TRACE_PPC_FETCH_COUNTS: OnceLock<bool> = OnceLock::new();
fn trace_ppc_fetch_counts_enabled() -> bool {
*TRACE_PPC_FETCH_COUNTS
.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_PPC_FETCH_COUNTS").is_some())
}
static INPUT_TICK_TRACE_ENABLED: OnceLock<bool> = OnceLock::new();
fn input_tick_trace_enabled() -> bool {
*INPUT_TICK_TRACE_ENABLED
.get_or_init(|| std::env::var_os("SYSTEMLESS_INPUT_TICK_TRACE").is_some())
}
static PPC_IMPORT_HIST_ENABLED: OnceLock<bool> = OnceLock::new();
fn ppc_import_hist_enabled() -> bool {
*PPC_IMPORT_HIST_ENABLED
.get_or_init(|| std::env::var_os("SYSTEMLESS_PPC_IMPORT_HIST").is_some())
}
static PPC_UNIMPL_HIST_ENABLED: OnceLock<bool> = OnceLock::new();
fn ppc_unimpl_hist_enabled() -> bool {
*PPC_UNIMPL_HIST_ENABLED
.get_or_init(|| std::env::var_os("SYSTEMLESS_PPC_UNIMPL_HIST").is_some())
}
static PPC_PROFILE_ENABLED: OnceLock<bool> = OnceLock::new();
fn ppc_profile_enabled() -> bool {
*PPC_PROFILE_ENABLED.get_or_init(|| std::env::var_os("SYSTEMLESS_PPC_PROFILE").is_some())
}
static PPC_GWORLD_DUMP_ENABLED: OnceLock<bool> = OnceLock::new();
fn ppc_gworld_dump_enabled() -> bool {
*PPC_GWORLD_DUMP_ENABLED
.get_or_init(|| std::env::var_os("SYSTEMLESS_PPC_GWORLD_DUMP").is_some())
}
static QD3D_DUMP_FRAME: OnceLock<Option<usize>> = OnceLock::new();
fn qd3d_dump_frame_index() -> Option<usize> {
*QD3D_DUMP_FRAME.get_or_init(|| {
let value = std::env::var_os("SYSTEMLESS_QD3D_DUMP_FRAME")?;
parse_qd3d_dump_frame_value(&value)
})
}
static QD3D_DUMP_REPLAY_DIR: OnceLock<Option<std::path::PathBuf>> = OnceLock::new();
fn qd3d_dump_replay_dir() -> Option<&'static std::path::Path> {
QD3D_DUMP_REPLAY_DIR
.get_or_init(|| {
let value = std::env::var_os("SYSTEMLESS_QD3D_DUMP_REPLAY_DIR")?;
if value.is_empty() {
return None;
}
Some(std::path::PathBuf::from(value))
})
.as_deref()
}
fn parse_qd3d_dump_frame_value(value: &std::ffi::OsStr) -> Option<usize> {
let value = value.to_str()?.trim();
if value.is_empty() {
return None;
}
value
.strip_prefix("0x")
.or_else(|| value.strip_prefix("0X"))
.map_or_else(
|| value.parse().ok(),
|hex| usize::from_str_radix(hex, 16).ok(),
)
}
fn format_qd3d_frame_dump(frame_index: usize, replay: &PpcQ3SceneReplay) -> String {
use std::fmt::Write as _;
let mut out = String::new();
let memory_bytes: usize = replay
.memory_regions
.iter()
.map(|region| region.data.len())
.sum();
let _ = writeln!(
out,
"[QD3D-FRAME] frame={} commands={} memory_regions={} memory_bytes={}",
frame_index,
replay.commands.len(),
replay.memory_regions.len(),
memory_bytes
);
for (region_index, region) in replay.memory_regions.iter().enumerate() {
let _ = writeln!(
out,
"[QD3D-FRAME] frame={} memory={} base=${:08X} bytes={} checksum=${:08X}",
frame_index,
region_index,
region.base_addr,
region.data.len(),
qd3d_frame_dump_checksum(®ion.data)
);
}
for (command_index, command) in replay.commands.iter().enumerate() {
let _ = writeln!(
out,
"[QD3D-FRAME] frame={} command={} {:#?}",
frame_index, command_index, command
);
}
out
}
fn qd3d_frame_replay_dump_path(
output_dir: &std::path::Path,
frame_index: usize,
) -> std::path::PathBuf {
output_dir.join(format!("q3_frame_{frame_index:06}_replay.json"))
}
fn write_qd3d_frame_replay_dump(
output_dir: &std::path::Path,
frame_index: usize,
replay: &PpcQ3SceneReplay,
) -> std::io::Result<std::path::PathBuf> {
std::fs::create_dir_all(output_dir)?;
let path = qd3d_frame_replay_dump_path(output_dir, frame_index);
let json = replay
.to_json_pretty()
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
std::fs::write(&path, json)?;
Ok(path)
}
fn qd3d_frame_dump_checksum(bytes: &[u8]) -> u32 {
bytes.iter().fold(0x811c_9dc5u32, |hash, byte| {
hash.wrapping_mul(0x0100_0193) ^ u32::from(*byte)
})
}
fn format_input_key_map(key_map: &[u8; 16]) -> String {
use std::fmt::Write as _;
let mut out = String::with_capacity(32);
for byte in key_map {
let _ = write!(out, "{:02X}", byte);
}
out
}
fn sync_ppc_cursor_state(
dispatcher: &mut TrapDispatcher,
cursor_level: i16,
toolbox_startup: &crate::loader::ppc::PpcToolboxStartupState,
) {
dispatcher.cursor_level = cursor_level;
dispatcher.cursor_visible = cursor_level == 0;
dispatcher.cursor_data = Some(if toolbox_startup.cursor_installed {
CursorImage::mono(
toolbox_startup.cursor_data,
toolbox_startup.cursor_mask,
toolbox_startup.cursor_hot_v,
toolbox_startup.cursor_hot_h,
)
} else {
TrapDispatcher::default_arrow_cursor_image()
});
}
fn format_oracle_hex32(value: u32) -> String {
format!("{value:08X}")
}
fn format_oracle_optional_hex32(value: Option<u32>) -> String {
value.map_or_else(|| "none".to_string(), format_oracle_hex32)
}
fn format_oracle_optional_u32(value: Option<u32>) -> String {
value.map_or_else(|| "none".to_string(), |value| value.to_string())
}
fn format_oracle_optional_u16(value: Option<u16>) -> String {
value.map_or_else(|| "none".to_string(), |value| value.to_string())
}
fn format_oracle_optional_bool(value: Option<bool>) -> String {
value.map_or_else(|| "none".to_string(), |value| value.to_string())
}
fn format_input_tick_trace(
tick: u32,
total_instructions: u64,
input: PpcInputSnapshot,
event_queue_len: usize,
mb_state: u8,
) -> String {
format!(
"[INPUT-TICK] tick={} total_instructions={} mouse_button={} mouse_v={} mouse_h={} key_map={} event_queue={} mb_state=${:02X}",
tick,
total_instructions,
input.mouse_button,
input.mouse_v,
input.mouse_h,
format_input_key_map(&input.key_map),
event_queue_len,
mb_state
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct PpcProfileSample {
max_steps: usize,
cycles: u64,
total_instructions: u64,
tick: u32,
pc: u32,
lr: u32,
current_gworld: u32,
screen_events: u64,
handled_import_count: u32,
last_import_index: Option<u32>,
unsupported_import_index: Option<u32>,
q3_frames: usize,
q3_frame_start: usize,
q3_frame_end: usize,
q3_commands: usize,
q3_vertices: usize,
q3_triangles: usize,
q3_pixels: usize,
dsp_front_gworld: u32,
dsp_back_gworld: u32,
run_us: u128,
render_us: u128,
sync_us: u128,
total_us: u128,
}
fn format_optional_profile_u32(value: Option<u32>) -> String {
value.map_or_else(|| "none".to_string(), |value| value.to_string())
}
fn format_profile_hex32(value: u32) -> String {
format!("${value:08X}")
}
fn format_ppc_profile_sample(sample: PpcProfileSample) -> String {
format!(
"[PPC-PROFILE] max_steps={} cycles={} total_instructions={} tick={} screen_events={} \
pc={} lr={} current_gworld={} imports={} last_import={} unsupported_import={} q3_frames={} q3_frame_start={} \
q3_frame_end={} q3_commands={} q3_vertices={} q3_triangles={} q3_pixels={} \
dsp_front_gworld={} dsp_back_gworld={} run_us={} render_us={} sync_us={} total_us={}",
sample.max_steps,
sample.cycles,
sample.total_instructions,
sample.tick,
sample.screen_events,
format_profile_hex32(sample.pc),
format_profile_hex32(sample.lr),
format_profile_hex32(sample.current_gworld),
sample.handled_import_count,
format_optional_profile_u32(sample.last_import_index),
format_optional_profile_u32(sample.unsupported_import_index),
sample.q3_frames,
sample.q3_frame_start,
sample.q3_frame_end,
sample.q3_commands,
sample.q3_vertices,
sample.q3_triangles,
sample.q3_pixels,
format_profile_hex32(sample.dsp_front_gworld),
format_profile_hex32(sample.dsp_back_gworld),
sample.run_us,
sample.render_us,
sample.sync_us,
sample.total_us
)
}
fn elapsed_profile_micros(start: Option<Instant>) -> u128 {
start.map_or(0, |start| start.elapsed().as_micros())
}
fn merge_ppc_import_histogram(
histogram: &mut HashMap<(String, String), u64>,
trace: &[PpcHleImportTraceEntry],
) {
for entry in trace {
let key = (entry.library_name.clone(), entry.symbol_name.clone());
*histogram.entry(key).or_insert(0) += entry.repeat_count;
}
}
fn ppc_import_trace_runs(trace: &[PpcHleImportTraceEntry]) -> Vec<(&PpcHleImportTraceEntry, u64)> {
let mut runs = Vec::new();
let mut iter = trace.iter();
let Some(mut current) = iter.next() else {
return runs;
};
let mut count = current.repeat_count;
for entry in iter {
if entry.library_name == current.library_name && entry.symbol_name == current.symbol_name {
count = count.saturating_add(entry.repeat_count);
} else {
runs.push((current, count));
current = entry;
count = entry.repeat_count;
}
}
runs.push((current, count));
runs
}
fn ppc_import_trace_run_eq(
left: &(&PpcHleImportTraceEntry, u64),
right: &(&PpcHleImportTraceEntry, u64),
) -> bool {
left.1 == right.1
&& left.0.library_name == right.0.library_name
&& left.0.symbol_name == right.0.symbol_name
}
fn ppc_import_trace_blocks_equal(
runs: &[(&PpcHleImportTraceEntry, u64)],
left: usize,
right: usize,
len: usize,
) -> bool {
(0..len).all(|offset| ppc_import_trace_run_eq(&runs[left + offset], &runs[right + offset]))
}
fn ppc_import_trace_repeated_block_len(
runs: &[(&PpcHleImportTraceEntry, u64)],
start: usize,
) -> Option<(usize, u64)> {
const MAX_BLOCK_LEN: usize = 128;
let remaining = runs.len().saturating_sub(start);
let max_block_len = MAX_BLOCK_LEN.min(remaining / 2);
let mut best: Option<(usize, u64, usize)> = None;
for block_len in 2..=max_block_len {
if !ppc_import_trace_blocks_equal(runs, start, start + block_len, block_len) {
continue;
}
let mut repeats = 2u64;
let mut next = start + block_len * 2;
while next + block_len <= runs.len()
&& ppc_import_trace_blocks_equal(runs, start, next, block_len)
{
repeats = repeats.saturating_add(1);
next += block_len;
}
let saved_rows = block_len
.saturating_mul(usize::try_from(repeats).unwrap_or(usize::MAX))
.saturating_sub(1);
if saved_rows >= block_len.saturating_mul(2)
&& best
.as_ref()
.is_none_or(|(_, _, best_saved)| saved_rows > *best_saved)
{
best = Some((block_len, repeats, saved_rows));
}
}
best.map(|(block_len, repeats, _)| (block_len, repeats))
}
fn format_ppc_import_histogram(histogram: &HashMap<(String, String), u64>, top_n: usize) -> String {
use std::fmt::Write as _;
let mut entries: Vec<((String, String), u64)> = histogram
.iter()
.map(|((library, symbol), &count)| ((library.clone(), symbol.clone()), count))
.collect();
entries.sort_by(|left, right| {
right
.1
.cmp(&left.1)
.then_with(|| left.0 .0.cmp(&right.0 .0))
.then_with(|| left.0 .1.cmp(&right.0 .1))
});
let total: u64 = entries.iter().map(|(_, count)| *count).sum();
let mut out = String::new();
let _ = writeln!(
out,
"[PPC-IMPORT-HIST] top {} of {} imports ({} total import calls)",
top_n.min(entries.len()),
entries.len(),
total
);
for ((library, symbol), count) in entries.iter().take(top_n) {
let _ = writeln!(
out,
"[PPC-IMPORT-HIST] {:>10} {}:{}",
count, library, symbol
);
}
out
}
#[derive(Debug, Clone, Copy)]
struct PpcGWorldDumpState {
current_gworld: u32,
front_gworld: u32,
back_gworld: u32,
swap_count: u32,
}
fn format_ppc_gworld_sample(
memory: &mut PpcSectionMem,
record: PpcGWorldRecord,
point: (u32, u32),
) -> String {
let (x, y) = point;
if x >= record.width || y >= record.height {
return "out".to_string();
}
let Some(row_base) = record
.base_addr
.checked_add(y.saturating_mul(record.row_bytes))
else {
return "bad".to_string();
};
match record.depth {
8 => row_base
.checked_add(x)
.and_then(|addr| memory.read_u8(addr))
.map_or_else(|| "unmapped".to_string(), |value| format!("${value:02X}")),
16 => row_base
.checked_add(x.saturating_mul(2))
.and_then(|addr| memory.read_u16_be(addr))
.map_or_else(|| "unmapped".to_string(), |value| format!("${value:04X}")),
_ => "unsupported-depth".to_string(),
}
}
fn format_ppc_gworld_dump(
memory: &mut PpcSectionMem,
gworlds: &[PpcGWorldRecord],
state: PpcGWorldDumpState,
top_n: usize,
) -> String {
use std::fmt::Write as _;
let mut out = String::new();
let _ = writeln!(
out,
"[PPC-GWORLD-DUMP] {} gworlds current=${:08X} front=${:08X} back=${:08X} swap_count={}",
gworlds.len(),
state.current_gworld,
state.front_gworld,
state.back_gworld,
state.swap_count
);
for record in gworlds.iter().copied() {
let mut bytes = Vec::new();
let mut checksum = 0;
let mut nonzero_units = 0u64;
let mut value_counts: BTreeMap<u32, u64> = BTreeMap::new();
let row_len = match record.depth {
8 => record.width.min(record.row_bytes),
16 => record.width.saturating_mul(2).min(record.row_bytes),
_ => record.row_bytes,
};
if row_len > 0 && record.height > 0 && row_len <= record.row_bytes && row_len <= 64 * 1024 {
let mut row = vec![0u8; row_len as usize];
for y in 0..record.height {
let Some(row_addr) = record
.base_addr
.checked_add(y.saturating_mul(record.row_bytes))
else {
continue;
};
if memory.read_bytes_into(row_addr, &mut row).is_none() {
continue;
}
bytes.extend_from_slice(&row);
match record.depth {
8 => {
for value in &row {
if *value != 0 {
nonzero_units = nonzero_units.saturating_add(1);
}
*value_counts.entry(u32::from(*value)).or_insert(0) += 1;
}
}
16 => {
for chunk in row.chunks_exact(2) {
let value = u16::from_be_bytes([chunk[0], chunk[1]]);
if value != 0 {
nonzero_units = nonzero_units.saturating_add(1);
}
*value_counts.entry(u32::from(value)).or_insert(0) += 1;
}
}
_ => {
for value in &row {
if *value != 0 {
nonzero_units = nonzero_units.saturating_add(1);
}
*value_counts.entry(u32::from(*value)).or_insert(0) += 1;
}
}
}
}
checksum = qd3d_frame_dump_checksum(&bytes);
}
let mut roles = Vec::new();
if record.port == state.current_gworld {
roles.push("current");
}
if record.port == state.front_gworld {
roles.push("front");
}
if record.port == state.back_gworld {
roles.push("back");
}
let role = if roles.is_empty() {
"none".to_string()
} else {
roles.join(",")
};
let unique_units = value_counts.len();
let mut top_values: Vec<(u32, u64)> = value_counts.into_iter().collect();
top_values.sort_by(|left, right| right.1.cmp(&left.1).then_with(|| left.0.cmp(&right.0)));
let value_width = if record.depth == 16 { 4 } else { 2 };
let top_values = top_values
.iter()
.take(top_n)
.map(|(value, count)| format!("${value:0width$X}:{count}", width = value_width))
.collect::<Vec<_>>()
.join(",");
let center = (record.width / 2, record.height / 2);
let bottom_right = (
record.width.saturating_sub(1),
record.height.saturating_sub(1),
);
let _ = writeln!(
out,
"[PPC-GWORLD-DUMP] port=${:08X} role={} base=${:08X} row_bytes={} size={}x{}x{} bytes={} checksum=${:08X} nonzero_units={} unique_units={} top={} samples=(0,0):{},center:{},last:{}",
record.port,
role,
record.base_addr,
record.row_bytes,
record.width,
record.height,
record.depth,
bytes.len(),
checksum,
nonzero_units,
unique_units,
top_values,
format_ppc_gworld_sample(memory, record, (0, 0)),
format_ppc_gworld_sample(memory, record, center),
format_ppc_gworld_sample(memory, record, bottom_right)
);
}
out
}
fn merge_ppc_unimpl_histogram(histogram: &mut HashMap<String, u64>, key: String) {
*histogram.entry(key).or_insert(0) += 1;
}
fn ppc_unimpl_histogram_key(
imports: &[PpcImportBinding],
result: PpcRunResult,
unsupported_import_index: Option<u32>,
) -> Option<String> {
if let Some(index) = unsupported_import_index {
return Some(
imports
.iter()
.find(|binding| binding.symbol_index == index)
.map_or_else(
|| format!("import #{} <unknown>", index),
|binding| {
format!(
"import #{} {}:{}",
index, binding.library_name, binding.symbol_name
)
},
),
);
}
match result {
PpcRunResult::Unimplemented { pc, error, .. } => {
Some(format!("instruction pc=${:08X} {:?}", pc, error))
}
PpcRunResult::Exception {
pc,
exception: PpcException::IllegalInstruction { word, reason },
..
} => Some(format!(
"illegal-instruction pc=${:08X} word=${:08X} {:?}",
pc, word, reason
)),
_ => None,
}
}
fn format_ppc_unimpl_histogram(histogram: &HashMap<String, u64>, top_n: usize) -> String {
use std::fmt::Write as _;
let mut entries: Vec<(String, u64)> = histogram
.iter()
.map(|(key, &count)| (key.clone(), count))
.collect();
entries.sort_by(|left, right| right.1.cmp(&left.1).then_with(|| left.0.cmp(&right.0)));
let total: u64 = entries.iter().map(|(_, count)| *count).sum();
let mut out = String::new();
let _ = writeln!(
out,
"[PPC-UNIMPL-HIST] top {} of {} unsupported stops ({} total)",
top_n.min(entries.len()),
entries.len(),
total
);
for (key, count) in entries.iter().take(top_n) {
let _ = writeln!(out, "[PPC-UNIMPL-HIST] {:>10} {}", count, key);
}
out
}
const PC_SAMPLE_INTERVAL: u64 = 1000;
const BATCH_CHUNK: usize = 8192;
#[cfg(not(target_arch = "wasm32"))]
fn trace_pc_range_active() -> bool {
trace_pc_range().is_some()
}
#[cfg(target_arch = "wasm32")]
fn trace_pc_range_active() -> bool {
false
}
fn per_instruction_diagnostics_active() -> bool {
#[cfg(debug_assertions)]
{
if crate::memory::bus::watchpoint_armed() {
return true;
}
}
trace_buffer_enabled()
|| trace_timer_enabled()
|| trace_opcode_counts_enabled()
|| trace_hot_pc_enabled()
|| trace_pc_range_active()
|| crate::memory::bus::fb_write_trace_active()
|| crate::memory::bus::mem_read_trace_active()
|| crate::memory::bus::mem_write_trace_active()
}
#[cfg(not(target_arch = "wasm32"))]
static TRACE_HOT_PC: OnceLock<bool> = OnceLock::new();
fn trace_hot_pc_enabled() -> bool {
#[cfg(target_arch = "wasm32")]
{
return false;
}
#[cfg(not(target_arch = "wasm32"))]
*TRACE_HOT_PC.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_HOT_PC").is_some())
}
static SPIN_WAIT_FASTFWD_FORCE_ON: OnceLock<bool> = OnceLock::new();
static SPIN_WAIT_FASTFWD_FORCE_OFF: OnceLock<bool> = OnceLock::new();
static WAIT_STATS_ON: OnceLock<bool> = OnceLock::new();
fn wait_stats_enabled() -> bool {
*WAIT_STATS_ON.get_or_init(|| std::env::var_os("SYSTEMLESS_WAIT_STATS").is_some())
}
static WS_ANCHOR_CALLS: AtomicU64 = AtomicU64::new(0);
static WS_PROBE_STARTS: AtomicU64 = AtomicU64::new(0);
static WS_EXACT_REPEATS: AtomicU64 = AtomicU64::new(0);
static WS_FAIL_TICK: AtomicU64 = AtomicU64::new(0);
static WS_FAIL_CPU: AtomicU64 = AtomicU64::new(0);
static WS_FAIL_MEM: AtomicU64 = AtomicU64::new(0);
static WS_CANCEL_TRAP: AtomicU64 = AtomicU64::new(0);
static WS_PARKED: AtomicU64 = AtomicU64::new(0);
static WS_PERIOD_STEPS: AtomicU64 = AtomicU64::new(0);
static WS_PROBE_OVERFLOWS: AtomicU64 = AtomicU64::new(0);
static WS_CANCEL_TRAP_WORDS: std::sync::Mutex<Option<std::collections::BTreeMap<u16, u64>>> =
std::sync::Mutex::new(None);
fn ws_note_cancel_trap(opcode: u16) {
if !wait_stats_enabled() {
return;
}
WS_CANCEL_TRAP.fetch_add(1, AtomicOrdering::Relaxed);
if let Ok(mut guard) = WS_CANCEL_TRAP_WORDS.lock() {
*guard
.get_or_insert_with(Default::default)
.entry(opcode)
.or_insert(0) += 1;
}
}
pub fn dump_wait_stats() {
if !wait_stats_enabled() {
return;
}
eprintln!(
"[WAIT-STATS] anchor_calls={} probe_starts={} probe_overflows={} exact_repeats={} parked={} period_steps={} fail_tick={} fail_cpu={} fail_mem={} cancel_trap={}",
WS_ANCHOR_CALLS.load(AtomicOrdering::Relaxed),
WS_PROBE_STARTS.load(AtomicOrdering::Relaxed),
WS_PROBE_OVERFLOWS.load(AtomicOrdering::Relaxed),
WS_EXACT_REPEATS.load(AtomicOrdering::Relaxed),
WS_PARKED.load(AtomicOrdering::Relaxed),
WS_PERIOD_STEPS.load(AtomicOrdering::Relaxed),
WS_FAIL_TICK.load(AtomicOrdering::Relaxed),
WS_FAIL_CPU.load(AtomicOrdering::Relaxed),
WS_FAIL_MEM.load(AtomicOrdering::Relaxed),
WS_CANCEL_TRAP.load(AtomicOrdering::Relaxed),
);
if let Ok(guard) = WS_CANCEL_TRAP_WORDS.lock() {
if let Some(map) = guard.as_ref() {
let mut rows: Vec<_> = map.iter().collect();
rows.sort_by_key(|(_, n)| std::cmp::Reverse(**n));
for (word, n) in rows.iter().take(12) {
eprintln!("[WAIT-STATS] cancel trap {word:04X}: {n}");
}
}
}
}
fn spin_wait_fastfwd_force_on() -> bool {
*SPIN_WAIT_FASTFWD_FORCE_ON
.get_or_init(|| std::env::var_os("SYSTEMLESS_SPIN_WAIT_FASTFWD").is_some())
}
fn spin_wait_fastfwd_force_off() -> bool {
*SPIN_WAIT_FASTFWD_FORCE_OFF
.get_or_init(|| std::env::var_os("SYSTEMLESS_DISABLE_SPIN_FASTFWD").is_some())
}
fn spin_wait_fastfwd_enabled_for(yield_for_ui: bool, tick_cap: Option<u32>) -> bool {
spin_wait_fastfwd_gate(
spin_wait_fastfwd_force_on(),
spin_wait_fastfwd_force_off(),
yield_for_ui,
tick_cap.is_some(),
)
}
fn spin_wait_fastfwd_gate(
force_on: bool,
force_off: bool,
yield_for_ui: bool,
has_tick_cap: bool,
) -> bool {
if force_off {
return false;
}
if force_on {
return true;
}
!yield_for_ui || has_tick_cap
}
fn modaldialog_refire_is_noop(
yield_for_ui: bool,
has_tracking: bool,
filter_allows_noop: bool,
flash_remaining_zero: bool,
draw_procs_done: bool,
rendered_pixels_final: bool,
event_queue_empty: bool,
) -> bool {
!yield_for_ui
&& has_tracking
&& filter_allows_noop
&& flash_remaining_zero
&& draw_procs_done
&& rendered_pixels_final
&& event_queue_empty
}
fn tracking_refire_should_freeze_ticks(opcode: u16) -> bool {
let trap_no_autopop = opcode & !0x0400;
trap_no_autopop == 0xA93D || trap_no_autopop == 0xA80B || trap_no_autopop == 0xA968 || trap_no_autopop == 0xA91E || trap_no_autopop == 0xA925 || trap_no_autopop == 0xA905 || trap_no_autopop == 0xA926 }
fn tracking_refire_uses_dialog_callbacks(opcode: u16) -> bool {
matches!(opcode & !0x0400, 0xA991 | 0xA985 | 0xA986 | 0xA987 | 0xA988)
}
fn tracking_refire_advances_gui_idle_tick(opcode: u16) -> bool {
matches!(opcode & !0x0400, 0xA991 | 0xA9EA)
}
fn canonical_trap_number(opcode: u16) -> (bool, u16) {
let is_tool = (opcode & 0x0800) != 0;
let trap_num = if is_tool {
opcode & 0x03FF
} else {
opcode & 0x00FF
};
(is_tool, trap_num)
}
fn idle_cycle_trap_is_journal_complete(
opcode: u16,
null_event: bool,
system_task_idle: bool,
) -> bool {
match canonical_trap_number(opcode) {
(true, 0x0170) | (true, 0x0171) => null_event,
(true, 0x0071) => true,
(true, 0x0172..=0x0176) => true,
(true, 0x01EB) | (true, 0x01EC) => true,
(true, 0x01B4) => system_task_idle,
_ => false,
}
}
fn is_poll_anchor_trap(opcode: u16) -> bool {
matches!(canonical_trap_number(opcode), (true, 0x0172..=0x0176))
}
fn hle_trap_extra_tick_cost(opcode: u16) -> i32 {
let (is_tool, trap_num) = canonical_trap_number(opcode);
match (is_tool, trap_num) {
(true, 0x0170) | (true, 0x0060) | (true, 0x0171) | (true, 0x0175) | (true, 0x0062) | (false, 0x0031) | (_, 0x003B) => 0,
(true, 0x006C) | (true, 0x006E) | (true, 0x01EB) | (true, 0x01EC) => 0,
(true, 0x00EC) | (true, 0x00F6) => 96,
(true, 0x01A0) | (true, 0x01A1) | (true, 0x01A2) | (true, 0x01BC) => 96,
(true, 0x019D..=0x01CF) => 24,
(true, _) => 4,
(false, _) => 2,
}
}
fn event_manager_yield_trap(opcode: u16) -> bool {
matches!(
canonical_trap_number(opcode),
(true, 0x0170) | (true, 0x0060) | (true, 0x0171) )
}
const SPIN_FASTFWD_MAX_TICKS: u32 = 1_000_000;
enum AdvanceResult {
Advanced,
CapHit,
Interrupted,
TooFar,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct CpuArchitecturalSnapshot {
dar: [u32; 16],
dar_save: [u32; 16],
sr_save: u16,
ppc: u32,
stack_pointers: [u32; 8],
pc: u32,
sr: u16,
vbr: u32,
sfc: u32,
dfc: u32,
cacr: u32,
caar: u32,
cacr_pending_ops: u32,
itt: [u32; 2],
dtt: [u32; 2],
ir: u32,
fpr: [m68k::fpu::FloatX80; 8],
fpiar: u32,
fpsr: u32,
fpcr: u32,
mmu: [u32; 14],
pmmu_enabled: bool,
int_level: u32,
stopped: u32,
change_of_flow: bool,
loop_mode: bool,
loop_body_word: u16,
loop_dbcc_word: u16,
prefetch: [u16; 2],
prefetch_count: u8,
consume_without_prefetch: bool,
pending_sync_clocks: u32,
run_mode: u32,
fpu_just_reset: bool,
reset_cycles: u32,
virq_state: u32,
nmi_pending: u32,
exception_processing: bool,
}
impl CpuArchitecturalSnapshot {
fn capture(cpu: &m68k::CpuCore) -> Self {
Self {
dar: cpu.dar,
dar_save: cpu.dar_save,
sr_save: cpu.sr_save,
ppc: cpu.ppc,
stack_pointers: cpu.sp,
pc: cpu.pc,
sr: cpu.get_sr(),
vbr: cpu.vbr,
sfc: cpu.sfc,
dfc: cpu.dfc,
cacr: cpu.cacr,
caar: cpu.caar,
cacr_pending_ops: cpu.cacr_pending_ops,
itt: [cpu.itt0, cpu.itt1],
dtt: [cpu.dtt0, cpu.dtt1],
ir: cpu.ir,
fpr: cpu.fpr,
fpiar: cpu.fpiar,
fpsr: cpu.fpsr,
fpcr: cpu.fpcr,
mmu: [
cpu.mmu_crp_aptr,
cpu.mmu_crp_limit,
cpu.mmu_srp_aptr,
cpu.mmu_srp_limit,
cpu.mmu_tc,
cpu.mmu_sr,
cpu.mmu_tt0,
cpu.mmu_tt1,
cpu.dacr0,
cpu.dacr1,
cpu.iacr0,
cpu.iacr1,
cpu.pcr,
cpu.buscr,
],
pmmu_enabled: cpu.pmmu_enabled,
int_level: cpu.int_level,
stopped: cpu.stopped,
change_of_flow: false,
loop_mode: cpu.loop_mode,
loop_body_word: cpu.loop_body_word,
loop_dbcc_word: cpu.loop_dbcc_word,
prefetch: cpu.prefetch_queue,
prefetch_count: cpu.prefetch_count,
consume_without_prefetch: cpu.consume_without_prefetch,
pending_sync_clocks: cpu.pending_sync_clocks,
run_mode: cpu.run_mode,
fpu_just_reset: cpu.fpu_just_reset,
reset_cycles: cpu.reset_cycles,
virq_state: cpu.virq_state,
nmi_pending: cpu.nmi_pending,
exception_processing: cpu.exception_processing,
}
}
}
struct IdleCycleProbe {
trap_pc: u32,
tick: u32,
cpu: CpuArchitecturalSnapshot,
arrivals: u8,
}
const IDLE_CYCLE_MAX_PERIOD: u8 = 4;
const IDLE_CYCLE_MAX_PROBES_PER_TICK: u8 = 2;
#[derive(Clone, Debug, Eq, PartialEq)]
struct IdleCycleHostSnapshot {
mouse_pos: (i16, i16),
mouse_button: bool,
key_map: [u8; 16],
caps_lock_physically_pressed: bool,
}
impl IdleCycleHostSnapshot {
fn capture(dispatcher: &TrapDispatcher) -> Self {
Self {
mouse_pos: dispatcher.mouse_pos,
mouse_button: dispatcher.mouse_button,
key_map: *dispatcher.key_map_bytes(),
caps_lock_physically_pressed: dispatcher.caps_lock_physically_pressed,
}
}
}
struct ProvenIdleCycleSleep {
trap_pc: u32,
wake_tick: u32,
tick: u32,
cpu: CpuArchitecturalSnapshot,
host: IdleCycleHostSnapshot,
}
const DIALOG_DRAW_TRAMPOLINE_OFFSET: u32 = 0x00;
const DIALOG_FILTER_TRAMPOLINE_OFFSET: u32 = 0x40;
const DIALOG_FILTER_EVENT_OFFSET: u32 = 0x80;
const DIALOG_FILTER_RESULT_OFFSET: u32 = 0x96;
const MENU_HOOK_TRAMPOLINE_OFFSET: u32 = 0xA0;
const DIALOG_CALLBACK_SCRATCH_FALLBACK: u32 = 0x0000_1200;
pub const DEFAULT_VBL_HZ: f64 = 60.15;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VfsFileSummary {
pub path: String,
pub data_len: usize,
pub resource_len: usize,
pub data_hash: u64,
pub resource_hash: u64,
pub file_type: u32,
pub creator: u32,
pub finder_flags: u16,
pub created_date: u32,
pub modified_date: u32,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VfsFileStat {
pub path: String,
pub data_len: usize,
pub resource_len: usize,
pub file_type: u32,
pub creator: u32,
pub finder_flags: u16,
pub created_date: u32,
pub modified_date: u32,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VfsFileSnapshot {
pub path: String,
pub data_fork: Vec<u8>,
pub resource_fork: Vec<u8>,
pub file_type: u32,
pub creator: u32,
pub finder_flags: u16,
pub created_date: u32,
pub modified_date: u32,
}
fn vfs_fork_hash(bytes: &[u8]) -> u64 {
let mut hash = 0xcbf2_9ce4_8422_2325u64;
for &byte in bytes {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
pub const DEFAULT_REALTIME_CPU_MHZ: f64 =
crate::machine_profile::REFERENCE_MACHINE_PROFILE.realtime_cpu_mhz;
pub const DEFAULT_REALTIME_PPC_CPU_MHZ: f64 = 120.0;
pub const DEFAULT_POWERPC_SCREEN_DEPTH: u32 = 16;
pub const DEFAULT_REALTIME_INSTRUCTIONS_PER_SECOND: f64 = DEFAULT_REALTIME_CPU_MHZ * 1_000_000.0;
pub fn default_realtime_instructions_per_tick(powerpc: bool) -> u32 {
let mhz = if powerpc {
DEFAULT_REALTIME_PPC_CPU_MHZ
} else {
DEFAULT_REALTIME_CPU_MHZ
};
(mhz * 1_000_000.0 / DEFAULT_VBL_HZ).round() as u32
}
const INSTRUCTIONS_PER_TICK: u32 = 12_000;
const DEFAULT_LAUNCH_TICKS: u32 = 600;
const DEFAULT_DOUBLE_TIME_TICKS: u32 = 20;
const MAC_EPOCH_OFFSET_FROM_UNIX: u64 = 2_082_844_800;
const CURSOR_TASK_NOOP_ADDR: u32 = 0x0000_0060;
fn current_mac_epoch_seconds() -> u32 {
let unix_now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
unix_now
.saturating_add(MAC_EPOCH_OFFSET_FROM_UNIX)
.min(u32::MAX as u64) as u32
}
#[derive(Clone, Copy, Debug)]
enum ActiveInterruptCallbackSource {
Adb,
Timer,
Vbl,
CursorTask,
SoundCallback,
SoundFileCompletion,
SoundDoubleBack,
FileCompletion,
DialogDrawProc,
DialogFilterProc,
MenuHook,
}
#[derive(Clone, Copy, Debug)]
struct ActiveInterruptCallback {
source: ActiveInterruptCallbackSource,
resume_pc: u32,
resume_sp: u32,
d_regs: [u32; 8],
a_regs: [u32; 8],
sr: u16,
ccr: u8,
restore_port: Option<(u32, u32)>,
}
fn is_sound_interrupt_source(source: ActiveInterruptCallbackSource) -> bool {
matches!(
source,
ActiveInterruptCallbackSource::SoundCallback
| ActiveInterruptCallbackSource::SoundFileCompletion
| ActiveInterruptCallbackSource::SoundDoubleBack
)
}
fn interrupt_callback_sr(source: ActiveInterruptCallbackSource, saved_sr: u16) -> u16 {
match source {
ActiveInterruptCallbackSource::Vbl | ActiveInterruptCallbackSource::CursorTask => {
(saved_sr & !0x0700) | 0x2100
}
_ => saved_sr,
}
}
fn align4(value: u32) -> u32 {
value.saturating_add(3) & !3
}
fn app_heap_start_for_loaded_app(app: &LoadedApp) -> u32 {
APP_HEAP_FLOOR.max(align4(app.loaded_image_end))
}
fn app_image_start_for_loaded_app(app: &LoadedApp) -> u32 {
app.a5_base.saturating_sub(app.code0_header.below_a5)
}
fn app_visible_zone_start_for_loaded_app(app: &LoadedApp) -> u32 {
let image_start = app_image_start_for_loaded_app(app);
if image_start >= APP_HEAP_FLOOR.saturating_add(APP_ZONE_HEADER_SIZE) {
APP_HEAP_FLOOR
} else {
app_heap_start_for_loaded_app(app)
}
}
fn load_address_for_size_partition(
configured_load_address: u32,
header: &Code0Header,
size_resource: Option<ApplicationSizeResource>,
application_memory_limit: u32,
) -> u32 {
if configured_load_address != DEFAULT_LOAD_ADDRESS {
return configured_load_address;
}
let Some(size) = size_resource else {
return configured_load_address;
};
let Some(preferred_partition_size) = size.preferred_partition_size() else {
return configured_load_address;
};
let desired_a5 = APP_HEAP_FLOOR
.saturating_add(preferred_partition_size.saturating_sub(APP_STACK_SAFETY_MARGIN));
let default_a5 = configured_load_address.saturating_add(header.below_a5);
if desired_a5 <= default_a5 {
return configured_load_address;
}
let relocated_load = align4(desired_a5.saturating_sub(header.below_a5));
let loader_headroom = header
.below_a5
.saturating_add(header.above_a5)
.saturating_add(APP_QD_GLOBALS_RESERVE)
.saturating_add(APP_LOADER_CLEAR_RESERVE)
.saturating_add(APP_HIGH_MEMORY_RESERVE);
let max_safe_load = application_memory_limit.saturating_sub(loader_headroom) & !3;
relocated_load
.min(max_safe_load)
.max(configured_load_address)
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum MenuBarPolicy {
#[default]
GuestControlled,
InitialKiosk,
ForceHidden,
}
pub struct FixtureRunnerConfig {
pub max_instructions: usize,
pub load_address: u32,
pub arrows_as_numpad: bool,
pub menu_bar_policy: MenuBarPolicy,
pub ui_theme: UiThemeId,
pub theme_metrics_mode: ThemeMetricsMode,
pub addressing_32_bit: bool,
pub screen_depth: u16,
}
impl Default for FixtureRunnerConfig {
fn default() -> Self {
Self {
max_instructions: 10_000_000,
load_address: DEFAULT_LOAD_ADDRESS,
arrows_as_numpad: false,
menu_bar_policy: MenuBarPolicy::GuestControlled,
ui_theme: UiThemeId::ClassicSystem7,
theme_metrics_mode: ThemeMetricsMode::ClassicGuestMetrics,
addressing_32_bit: true,
screen_depth: 8,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct PpcHostSoundPlayback {
file_playback_index: usize,
channel: u32,
}
struct PpcDecodedDoubleBuffer {
buffer_ptr: u32,
flags: u32,
samples: Vec<crate::sound::StereoSample>,
}
impl FixtureRunnerConfig {
pub fn with_screen_depth(mut self, screen_depth: u16) -> std::result::Result<Self, String> {
if !matches!(screen_depth, 1 | 2 | 4 | 8) {
return Err(format!(
"unsupported screen depth {screen_depth}; expected 1, 2, 4, or 8"
));
}
self.screen_depth = screen_depth;
Ok(self)
}
}
pub struct FixtureRunner {
cpu: M68kCpu,
ppc_app: Option<PpcLoadedApp>,
ppc_sound_synced_file_playback_count: usize,
ppc_sound_host_playbacks: Vec<PpcHostSoundPlayback>,
bus: MacMemoryBus,
dispatcher: TrapDispatcher,
config: FixtureRunnerConfig,
powerpc_screen_depth_override: Option<u16>,
ppc_host_indexed_ctab_handle: u32,
ppc_host_mirror_base: u32,
ppc_host_mirror_capacity: u32,
prefer_powerpc_executables: bool,
trace_buffer: std::collections::VecDeque<(u32, u16, u32, u32, u32, u32)>, halted: bool,
halted_trap: Option<u16>,
halted_pc: Option<u32>,
halted_sp: Option<u32>,
halted_d0: Option<u32>,
total_instructions: u64,
instructions_per_tick: u32,
wait_sleep_cap_in_headless: Option<u32>,
tick_budget: i32,
idle_cycle_last_seen: Option<(u32, u32)>,
idle_cycle_probe: Option<IdleCycleProbe>,
idle_cycle_site_probes: Option<(u32, u32, u8)>,
idle_cycle_sleep: Option<ProvenIdleCycleSleep>,
frozen_ticks: Option<u32>,
timer_trampoline: u32,
vbl_trampoline: u32,
cursor_task_trampoline: u32,
adb_callback_trampoline: u32,
adb_packet_buffer: u32,
active_interrupt_callback: Option<ActiveInterruptCallback>,
dialog_draw_port_snapshot: Option<crate::trap::dispatch::PortStateSnapshot>,
deferred_tracking_refire_pc: Option<u32>,
audio: Option<Box<dyn crate::audio::AudioBackend>>,
audio_buffer: Vec<u8>,
sound_doubleback_trampoline: u32,
sound_callback_trampoline: u32,
sound_file_completion_trampoline: u32,
file_completion_trampoline: u32,
dialog_draw_trampoline: u32,
dialog_filter_trampoline: u32,
menu_hook_trampoline: u32,
dialog_filter_event: u32,
dialog_filter_last_null_event_tick: Option<(u32, u32)>,
dialog_filter_last_update_event_tick: Option<(u32, u32, u32)>,
app_start_time: Option<u32>,
launch_ticks_override: Option<u32>,
launch_rnd_seed_override: Option<u32>,
launch_ppc_time_base_override: Option<u64>,
application_partition_size: Option<u32>,
opcode_histogram: Box<[u64; 65536]>,
pc_histogram: HashMap<u32, u64>,
ppc_fetch_histogram: PpcFetchHistogram,
ppc_import_histogram: HashMap<(String, String), u64>,
ppc_unimpl_histogram: HashMap<String, u64>,
q3_completed_frame_index: usize,
external_q3_renderer_enabled: bool,
pending_q3_gpu_frame: Option<PpcQ3GpuFrame>,
}
impl FixtureRunner {
pub fn new(ram_size: usize, config: FixtureRunnerConfig) -> Self {
let ram_size = if config.addressing_32_bit {
ram_size
} else {
ram_size.min(0x0100_0000)
};
assert!(
matches!(config.screen_depth, 1 | 2 | 4 | 8),
"screen_depth must be 1, 2, 4, or 8"
);
let mut dispatcher = TrapDispatcher::new();
dispatcher.set_menu_bar_policy(config.menu_bar_policy);
dispatcher.mmu_mode = u8::from(config.addressing_32_bit);
dispatcher.set_ui_theme_id(config.ui_theme);
let mut bus = MacMemoryBus::new(ram_size);
bus.set_addressing_32_bit(config.addressing_32_bit);
bus.configure_screen_depth(config.screen_depth);
bus.write_word(
crate::memory::globals::addr::SYS_EVT_MASK,
crate::memory::globals::DEFAULT_SYS_EVT_MASK,
);
let profile = crate::machine_profile::reference_machine_profile();
let visible_row_bytes =
(u32::from(profile.screen_width) * u32::from(config.screen_depth)).div_ceil(8);
let row_bytes = (visible_row_bytes / 16 + 1) * 16;
dispatcher.screen_mode = (
bus.read_long(crate::memory::globals::addr::SCRN_BASE),
row_bytes,
profile.screen_width,
profile.screen_height,
config.screen_depth,
);
let (clut, _) = TrapDispatcher::standard_mac_indexed_clut(config.screen_depth)
.expect("validated indexed screen depth");
dispatcher.device_clut = clut;
dispatcher.color_manager_clut = clut;
dispatcher.seeded_picture_palette = clut;
let standard_adb_service = bus.alloc_synthetic(2);
bus.write_word(standard_adb_service, 0x4E75); dispatcher
.adb
.install_standard_service_routine(standard_adb_service);
Self {
cpu: M68kCpu::new(),
ppc_app: None,
ppc_sound_synced_file_playback_count: 0,
ppc_sound_host_playbacks: Vec::new(),
bus,
dispatcher,
config,
powerpc_screen_depth_override: None,
ppc_host_indexed_ctab_handle: 0,
ppc_host_mirror_base: 0,
ppc_host_mirror_capacity: 0,
prefer_powerpc_executables: false,
trace_buffer: std::collections::VecDeque::with_capacity(2000),
halted: false,
halted_trap: None,
halted_pc: None,
halted_sp: None,
halted_d0: None,
total_instructions: 0,
instructions_per_tick: INSTRUCTIONS_PER_TICK,
wait_sleep_cap_in_headless: None,
tick_budget: INSTRUCTIONS_PER_TICK as i32,
idle_cycle_last_seen: None,
idle_cycle_probe: None,
idle_cycle_site_probes: None,
idle_cycle_sleep: None,
frozen_ticks: None,
timer_trampoline: 0,
vbl_trampoline: 0,
cursor_task_trampoline: 0,
adb_callback_trampoline: 0,
adb_packet_buffer: 0,
active_interrupt_callback: None,
dialog_draw_port_snapshot: None,
deferred_tracking_refire_pc: None,
audio: None,
audio_buffer: Vec::new(),
sound_doubleback_trampoline: 0,
sound_callback_trampoline: 0,
sound_file_completion_trampoline: 0,
file_completion_trampoline: 0,
dialog_draw_trampoline: 0,
dialog_filter_trampoline: 0,
menu_hook_trampoline: 0,
dialog_filter_event: 0,
dialog_filter_last_null_event_tick: None,
dialog_filter_last_update_event_tick: None,
app_start_time: None,
launch_ticks_override: None,
launch_rnd_seed_override: None,
launch_ppc_time_base_override: None,
application_partition_size: None,
opcode_histogram: Box::new([0u64; 65536]),
pc_histogram: HashMap::new(),
ppc_fetch_histogram: PpcFetchHistogram::new(),
ppc_import_histogram: HashMap::new(),
ppc_unimpl_histogram: HashMap::new(),
q3_completed_frame_index: 0,
external_q3_renderer_enabled: false,
pending_q3_gpu_frame: None,
}
}
pub fn set_prefer_powerpc_executables(&mut self, enabled: bool) {
self.prefer_powerpc_executables = enabled;
}
pub fn prefers_powerpc_executables(&self) -> bool {
self.prefer_powerpc_executables
}
pub fn set_external_q3_renderer_enabled(&mut self, enabled: bool) {
self.external_q3_renderer_enabled = enabled;
if !enabled {
self.pending_q3_gpu_frame = None;
}
}
pub fn take_q3_gpu_frame(&mut self) -> Option<PpcQ3GpuFrame> {
self.pending_q3_gpu_frame.take()
}
pub fn is_halted(&self) -> bool {
self.halted
}
pub fn halted_by_exit_to_shell(&self) -> bool {
self.halted && self.halted_trap == Some(0xA9F4)
}
pub fn guest_tick(&self) -> u32 {
self.bus.read_long(0x016A)
}
pub fn host_now(&self) -> Instant {
Instant::now()
}
pub fn halted_trap(&self) -> Option<u16> {
self.halted_trap
}
pub fn halted_pc(&self) -> Option<u32> {
self.halted_pc
}
pub fn halted_sp(&self) -> Option<u32> {
self.halted_sp
}
pub fn halted_stack_word0(&self) -> Option<u16> {
self.halted_sp.map(|sp| self.bus.read_word(sp))
}
pub fn halted_stack_word(&self, word_index: u32) -> Option<u16> {
self.halted_sp
.map(|sp| self.bus.read_word(sp + word_index.saturating_mul(2)))
}
pub fn halted_d0(&self) -> Option<u32> {
self.halted_d0
}
pub fn total_instructions(&self) -> u64 {
self.total_instructions
}
pub fn debug_overlay_snapshot(
&self,
frame_stats: DebugOverlayFrameStats,
) -> DebugOverlaySnapshot {
use crate::memory::globals::addr;
let dispatcher = &self.dispatcher;
let (_, _, screen_width, screen_height, pixel_size) = dispatcher.screen_mode;
let cursor_image = dispatcher.cursor_data();
let cursor_mask_nonzero_bytes = cursor_image
.as_ref()
.map(|(_, mask, _, _)| mask.iter().filter(|&&byte| byte != 0).count());
let cursor_hotspot = cursor_image
.as_ref()
.map(|(_, _, hot_v, hot_h)| (*hot_v, *hot_h));
DebugOverlaySnapshot {
frame_stats,
guest_tick: self.guest_tick(),
total_instructions: self.total_instructions,
trap_count: dispatcher.trap_count,
game_trap_count: dispatcher.game_trap_count,
cursor_visible: dispatcher.cursor_visible(),
cursor_level: dispatcher.cursor_level(),
cursor_data_present: dispatcher.cursor_data_present(),
cursor_mask_nonzero_bytes,
cursor_hotspot,
cursor_position: dispatcher.mouse_position(),
mouse_button: dispatcher.mouse_button,
fullscreen_locked: dispatcher.fullscreen_locked,
mbar_height: self.bus.read_word(addr::MBAR_HEIGHT),
screen_width,
screen_height,
pixel_size,
front_window: dispatcher.front_window(),
window_bounds: dispatcher.window_bounds(),
window_count: dispatcher.window_count(),
menu_count: dispatcher.menu_count(),
halted: self.halted,
halted_trap: self.halted_trap,
halted_pc: self.halted_pc,
}
}
pub fn cpu(&self) -> &M68kCpu {
&self.cpu
}
pub fn cpu_mut(&mut self) -> &mut M68kCpu {
&mut self.cpu
}
pub fn bus(&self) -> &MacMemoryBus {
&self.bus
}
pub fn bus_mut(&mut self) -> &mut MacMemoryBus {
&mut self.bus
}
pub fn dispatcher(&self) -> &crate::trap::dispatch::TrapDispatcher {
&self.dispatcher
}
pub fn dispatcher_mut(&mut self) -> &mut crate::trap::dispatch::TrapDispatcher {
&mut self.dispatcher
}
pub fn ui_theme(&self) -> &'static dyn UiTheme {
self.config.ui_theme.provider()
}
pub fn ui_theme_id(&self) -> UiThemeId {
self.config.ui_theme
}
pub fn theme_metrics_mode(&self) -> ThemeMetricsMode {
self.config.theme_metrics_mode
}
pub fn uses_classic_guest_metrics(&self) -> bool {
self.config
.theme_metrics_mode
.preserves_classic_guest_metrics()
}
pub fn set_menu_bar_policy(&mut self, policy: MenuBarPolicy) {
self.config.menu_bar_policy = policy;
self.dispatcher.set_menu_bar_policy(policy);
}
pub fn menu_bar_policy(&self) -> MenuBarPolicy {
self.dispatcher.menu_bar_policy
}
pub fn set_menu_bar_visible(&mut self, visible: bool) {
self.set_menu_bar_policy(if visible {
MenuBarPolicy::GuestControlled
} else {
MenuBarPolicy::ForceHidden
});
}
pub fn menu_bar_visible(&self) -> bool {
!self.dispatcher.menu_bar_hidden
}
pub fn disassemble_at(&self, pc: u32, count: usize) -> Vec<(u32, String, u32)> {
use crate::memory::MemoryBus;
let mut out = Vec::with_capacity(count);
let mut cur = pc;
for _ in 0..count {
let opcode = self.bus.read_word(cur);
let unmapped = self.bus.translate_guest_address(cur) >= self.bus.ram_size();
let (mnemonic, size) = if unmapped {
("<unmapped>".to_string(), 2)
} else {
m68k::dasm::disassemble(cur, opcode, m68k::CpuType::M68000)
};
let size = size.clamp(2, 10);
out.push((cur, mnemonic, size));
cur = cur.wrapping_add(size);
}
out
}
pub fn print_opcode_histogram(&self, top_n: usize) {
if !trace_opcode_counts_enabled() {
return;
}
let mut entries: Vec<(u16, u64)> = self
.opcode_histogram
.iter()
.enumerate()
.filter_map(|(i, &c)| if c > 0 { Some((i as u16, c)) } else { None })
.collect();
entries.sort_by_key(|e| std::cmp::Reverse(e.1));
let total: u64 = entries.iter().map(|(_, c)| c).sum();
eprintln!(
"[OPCODE-HIST] top {} of {} distinct opcodes ({} total non-Aline instructions)",
top_n.min(entries.len()),
entries.len(),
total
);
for (opcode, count) in entries.iter().take(top_n) {
let group = (opcode >> 12) & 0xF;
let group_name = match group {
0x0 => "bit-op/MOVEP/immediate",
0x1 => "MOVE.B",
0x2 => "MOVE.L",
0x3 => "MOVE.W",
0x4 => "misc (LEA/JSR/etc.)",
0x5 => "ADDQ/SUBQ/Scc/DBcc",
0x6 => "Bcc/BSR",
0x7 => "MOVEQ",
0x8 => "OR/DIV/SBCD",
0x9 => "SUB/SUBX",
0xA => "A-line (should be in trap-hist)",
0xB => "CMP/EOR",
0xC => "AND/MUL/ABCD/EXG",
0xD => "ADD/ADDX",
0xE => "shift/rotate",
0xF => "F-line (FPU/coproc)",
_ => "?",
};
eprintln!(
"[OPCODE-HIST] {:>10} ${:04X} group {:X}: {}",
count, opcode, group, group_name
);
}
}
pub fn print_pc_histogram(&self, top_n: usize) {
if !trace_hot_pc_enabled() {
return;
}
let mut entries: Vec<(u32, u64)> =
self.pc_histogram.iter().map(|(&a, &c)| (a, c)).collect();
entries.sort_by_key(|e| std::cmp::Reverse(e.1));
let total: u64 = entries.iter().map(|(_, c)| c).sum();
eprintln!(
"[PC-HIST] top {} of {} distinct PCs ({} samples × {} = ~{} instructions)",
top_n.min(entries.len()),
entries.len(),
total,
PC_SAMPLE_INTERVAL,
total * PC_SAMPLE_INTERVAL
);
for (pc, count) in entries.iter().take(top_n) {
let region = match *pc {
0x0000_0000..=0x0000_FFFF => "low-mem",
0x0001_0000..=0x005F_FFFF => "app code",
0x0060_0000..=0x00FF_FFFF => "heap/data",
0x0100_0000..=0x01FF_FFFF => "ROM",
_ => "other",
};
eprintln!("[PC-HIST] {:>8} PC=${:08X} ({})", count, pc, region);
}
}
pub fn print_ppc_fetch_histogram(&self, top_n: usize) {
if !trace_ppc_fetch_counts_enabled() {
return;
}
let decode_summary = self.ppc_fetch_histogram.decode_summary();
eprintln!(
"[PPC-FETCH-HIST] decoder coverage: {} decoded / {} total fetched instructions ({} unsupported)",
decode_summary.decoded(),
decode_summary.total(),
decode_summary.unsupported()
);
if !decode_summary.is_fully_decoded() {
let mut unsupported_primary: Vec<(u8, u64)> = decode_summary
.unsupported_primary()
.iter()
.map(|(&primary, &count)| (primary, count))
.collect();
unsupported_primary.sort_by_key(|entry| std::cmp::Reverse(entry.1));
for (primary, count) in unsupported_primary.iter().take(top_n) {
eprintln!(
"[PPC-FETCH-HIST] {:>10} unsupported primary {:02}",
count, primary
);
}
let mut unsupported_secondary: Vec<((u8, u16), u64)> = decode_summary
.unsupported_secondary()
.iter()
.map(|(&key, &count)| (key, count))
.collect();
unsupported_secondary.sort_by_key(|entry| std::cmp::Reverse(entry.1));
for ((primary, secondary), count) in unsupported_secondary.iter().take(top_n) {
eprintln!(
"[PPC-FETCH-HIST] {:>10} unsupported primary {:02} secondary {:03}",
count, primary, secondary
);
}
}
let mut primaries: Vec<(u8, u64)> = (0u8..64)
.filter_map(|primary| {
let count = self.ppc_fetch_histogram.primary_count(primary);
(count > 0).then_some((primary, count))
})
.collect();
primaries.sort_by_key(|entry| std::cmp::Reverse(entry.1));
eprintln!(
"[PPC-FETCH-HIST] top {} of {} primary opcodes ({} total fetched instructions)",
top_n.min(primaries.len()),
primaries.len(),
self.ppc_fetch_histogram.total()
);
for (primary, count) in primaries.iter().take(top_n) {
eprintln!("[PPC-FETCH-HIST] {:>10} primary {:02}", count, primary);
}
let mut secondaries: Vec<((u8, u16), u64)> = self
.ppc_fetch_histogram
.secondary_counts()
.iter()
.map(|(&key, &count)| (key, count))
.collect();
secondaries.sort_by_key(|entry| std::cmp::Reverse(entry.1));
eprintln!(
"[PPC-FETCH-HIST] top {} of {} secondary opcode buckets",
top_n.min(secondaries.len()),
secondaries.len()
);
for ((primary, secondary), count) in secondaries.iter().take(top_n) {
eprintln!(
"[PPC-FETCH-HIST] {:>10} primary {:02} secondary {:03}",
count, primary, secondary
);
}
let mut pcs: Vec<(u32, u64)> = self
.ppc_fetch_histogram
.pc_counts()
.iter()
.map(|(&pc, &count)| (pc, count))
.collect();
pcs.sort_by_key(|entry| std::cmp::Reverse(entry.1));
eprintln!(
"[PPC-FETCH-HIST] top {} of {} fetched PCs",
top_n.min(pcs.len()),
pcs.len()
);
for (pc, count) in pcs.iter().take(top_n) {
eprintln!("[PPC-FETCH-HIST] {:>10} pc ${:08X}", count, pc);
}
let mut words: Vec<(u32, u64)> = self
.ppc_fetch_histogram
.word_counts()
.iter()
.map(|(&word, &count)| (word, count))
.collect();
words.sort_by_key(|entry| std::cmp::Reverse(entry.1));
eprintln!(
"[PPC-FETCH-HIST] top {} of {} fetched instruction words",
top_n.min(words.len()),
words.len()
);
for (word, count) in words.iter().take(top_n) {
eprintln!("[PPC-FETCH-HIST] {:>10} word ${:08X}", count, word);
}
}
pub fn print_ppc_import_histogram(&self, top_n: usize) {
if !ppc_import_hist_enabled() {
return;
}
eprint!(
"{}",
format_ppc_import_histogram(&self.ppc_import_histogram, top_n)
);
}
fn record_ppc_import_histogram(&mut self, trace: &[PpcHleImportTraceEntry]) {
merge_ppc_import_histogram(&mut self.ppc_import_histogram, trace);
}
pub fn print_ppc_gworld_dump(&mut self, top_n: usize) {
if !ppc_gworld_dump_enabled() {
return;
}
let Some(ppc_app) = self.ppc_app.as_mut() else {
return;
};
eprint!(
"{}",
format_ppc_gworld_dump(
&mut ppc_app.memory,
&ppc_app.gworlds,
PpcGWorldDumpState {
current_gworld: ppc_app.current_gworld,
front_gworld: ppc_app.draw_sprocket.front_buffer_gworld,
back_gworld: ppc_app.draw_sprocket.back_buffer_gworld,
swap_count: ppc_app.draw_sprocket.swap_count,
},
top_n
)
);
}
pub fn print_ppc_unimpl_histogram(&self, top_n: usize) {
if !ppc_unimpl_hist_enabled() {
return;
}
eprint!(
"{}",
format_ppc_unimpl_histogram(&self.ppc_unimpl_histogram, top_n)
);
}
fn record_ppc_unimpl_histogram(
&mut self,
imports: &[PpcImportBinding],
result: PpcRunResult,
unsupported_import_index: Option<u32>,
) {
if let Some(key) = ppc_unimpl_histogram_key(imports, result, unsupported_import_index) {
merge_ppc_unimpl_histogram(&mut self.ppc_unimpl_histogram, key);
}
}
pub fn install_application_clut(&mut self, clut: [[u16; 3]; 256]) {
self.dispatcher
.install_application_clut(&mut self.bus, clut);
}
pub fn set_app_start_time(&mut self, secs: u32) {
self.app_start_time = Some(secs);
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_launch_state(&mut self, ticks: u32, rnd_seed: u32, ppc_time_base: u64) {
self.set_optional_launch_state(Some(ticks), Some(rnd_seed), Some(ppc_time_base));
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_optional_launch_state(
&mut self,
ticks: Option<u32>,
rnd_seed: Option<u32>,
ppc_time_base: Option<u64>,
) {
if let Some(ticks) = ticks {
self.launch_ticks_override = Some(ticks);
}
if let Some(rnd_seed) = rnd_seed {
self.launch_rnd_seed_override = Some(rnd_seed);
}
if let Some(ppc_time_base) = ppc_time_base {
self.launch_ppc_time_base_override = Some(ppc_time_base);
}
}
pub fn set_application_partition_size(&mut self, bytes: Option<u32>) {
self.application_partition_size = bytes.filter(|&bytes| bytes >= 128 * 1024);
}
pub fn app_start_time(&self) -> Option<u32> {
self.app_start_time
}
pub fn application_partition_size(&self) -> Option<u32> {
self.application_partition_size
}
pub fn set_trace_sink(&mut self, sink: Box<dyn crate::trace::TraceSink>) {
self.dispatcher.set_trace_sink(sink)
}
pub fn record_oracle_checkpoint_snapshot(&mut self, name: &str) -> Result<u64> {
self.composite_frame();
self.dispatcher.record_trace_event(
&self.bus,
self.current_oracle_pc(),
"checkpoint_snapshot",
BTreeMap::from([("name".to_string(), name.to_string())]),
true,
)?;
Ok(self.dispatcher.screen_event_count)
}
pub fn record_oracle_script_input(&mut self, fields: BTreeMap<String, String>) -> Result<()> {
self.dispatcher.record_trace_event(
&self.bus,
self.current_oracle_pc(),
"script_input",
fields,
false,
)
}
fn current_oracle_pc(&self) -> u32 {
self.ppc_app
.as_ref()
.map(|app| app.cpu.pc)
.unwrap_or_else(|| self.cpu.read_reg(Register::PC))
}
pub fn composite_frame(&mut self) {
self.sync_ppc_deferred_host_state();
self.dispatcher.redraw_chrome(&mut self.bus);
}
pub fn set_arrows_as_numpad(&mut self, enabled: bool) {
self.config.arrows_as_numpad = enabled;
}
pub fn arrows_as_numpad(&self) -> bool {
self.config.arrows_as_numpad
}
pub fn configured_screen_depth(&self) -> u16 {
self.config.screen_depth
}
pub fn set_powerpc_screen_depth(
&mut self,
screen_depth: u16,
) -> std::result::Result<(), String> {
if !matches!(screen_depth, 1 | 2 | 4 | 8 | 16) {
return Err(format!(
"unsupported PowerPC screen depth {screen_depth}; expected 1, 2, 4, 8, or 16"
));
}
self.powerpc_screen_depth_override = Some(screen_depth);
Ok(())
}
pub(crate) fn configured_powerpc_screen_depth(&self) -> u32 {
u32::from(
self.powerpc_screen_depth_override
.unwrap_or(DEFAULT_POWERPC_SCREEN_DEPTH as u16),
)
}
pub fn set_mouse_position(&mut self, v: i16, h: i16) {
let (v, h) = self.canonical_mouse_position(v, h);
self.dispatcher.set_mouse_position(v, h);
self.sync_mouse_position_lowmem();
self.wake_pending_wait_next_event_if_input_available();
self.wake_foreground_after_input();
}
pub fn guest_menu_snapshot(&mut self) -> GuestMenuSnapshot {
if let Some(ppc_app) = self.ppc_app.as_mut() {
ppc_app.guest_menu_snapshot()
} else {
self.dispatcher.guest_menu_snapshot(&self.bus)
}
}
pub fn select_guest_menu_item(&mut self, menu_id: i16, item_number: i16) -> bool {
if let Some(ppc_app) = self.ppc_app.as_mut() {
if !ppc_app.queue_native_menu_selection(menu_id, item_number) {
return false;
}
self.push_canonical_mouse_down(10, 15);
self.push_canonical_mouse_up(10, 15);
return true;
}
let Some((_v, _h)) =
self.dispatcher
.queue_native_menu_selection(&self.bus, menu_id, item_number)
else {
return false;
};
self.wake_pending_wait_next_event_if_input_available();
self.wake_foreground_after_input();
true
}
pub fn push_mouse_down(&mut self, v: i16, h: i16) {
let (v, h) = self.canonical_mouse_position(v, h);
self.push_canonical_mouse_down(v, h);
}
fn push_canonical_mouse_down(&mut self, v: i16, h: i16) {
self.dispatcher.push_mouse_down(v, h);
self.sync_mouse_lowmem();
self.wake_pending_wait_next_event_if_input_available();
self.wake_foreground_after_input();
}
#[cfg(any(test, feature = "test-support"))]
pub fn pending_mouse_down_count(&self) -> usize {
self.dispatcher
.event_queue
.iter()
.filter(|event| event.what == 1)
.count()
}
pub fn push_mouse_up(&mut self, v: i16, h: i16) {
let (v, h) = self.canonical_mouse_position(v, h);
self.push_canonical_mouse_up(v, h);
}
fn push_canonical_mouse_up(&mut self, v: i16, h: i16) {
self.dispatcher.push_mouse_up(v, h);
self.sync_mouse_lowmem();
self.wake_pending_wait_next_event_if_input_available();
self.wake_foreground_after_input();
}
fn sync_mouse_position_lowmem(&mut self) {
let (v, h) = self.dispatcher.mouse_pos;
self.bus.write_word(0x0828, v as u16);
self.bus.write_word(0x082A, h as u16);
self.bus.write_word(0x082C, v as u16);
self.bus.write_word(0x082E, h as u16);
self.bus.write_word(0x0830, v as u16);
self.bus.write_word(0x0832, h as u16);
}
fn sync_mouse_lowmem(&mut self) {
let mb_state: u8 = if self.dispatcher.mouse_button {
0x00
} else {
0x80
};
self.bus.write_byte(0x0172, mb_state);
self.sync_mouse_position_lowmem();
}
fn sync_key_map_lowmem(&mut self) {
use crate::memory::globals::addr;
self.bus
.write_bytes(addr::KEY_MAP_LM, self.dispatcher.key_map_bytes());
}
pub fn push_key_down(&mut self, mac_key: u8, char_code: u8) {
let (key, char_code) = self.remap_key(mac_key, char_code);
self.dispatcher.push_key_down(key, char_code);
self.sync_key_map_lowmem();
self.wake_pending_wait_next_event_if_input_available();
self.wake_foreground_after_input();
}
pub fn push_key_up(&mut self, mac_key: u8, char_code: u8) {
let (key, char_code) = self.remap_key(mac_key, char_code);
self.dispatcher.push_key_up_with_system_event_mask(
self.bus
.read_word(crate::memory::globals::addr::SYS_EVT_MASK),
key,
char_code,
);
self.sync_key_map_lowmem();
self.wake_pending_wait_next_event_if_input_available();
self.wake_foreground_after_input();
}
fn remap_key(&self, mac_key: u8, char_code: u8) -> (u8, u8) {
if self.config.arrows_as_numpad {
match mac_key {
0x7B => (0x56, b'4'), 0x7C => (0x58, b'6'), 0x7D => (0x57, b'5'), 0x7E => (0x5B, b'8'), _ => (mac_key, char_code),
}
} else {
(mac_key, char_code)
}
}
pub fn set_audio(&mut self, audio: Box<dyn crate::audio::AudioBackend>) {
self.audio = Some(audio);
}
pub fn set_instructions_per_tick(&mut self, instructions_per_tick: u32) {
let old = self.instructions_per_tick.max(1);
let new = instructions_per_tick.max(1);
self.tick_budget = ((self.tick_budget as i64 * new as i64) / old as i64) as i32;
self.instructions_per_tick = new;
}
pub fn instructions_per_tick(&self) -> u32 {
self.instructions_per_tick
}
pub fn is_powerpc_app(&self) -> bool {
self.ppc_app.is_some()
}
pub fn set_wait_sleep_cap_in_headless(&mut self, cap: Option<u32>) {
self.wait_sleep_cap_in_headless = cap;
}
pub fn wait_sleep_cap_in_headless(&self) -> Option<u32> {
self.wait_sleep_cap_in_headless
}
pub fn drain_audio(&mut self) -> Vec<u8> {
std::mem::take(&mut self.audio_buffer)
}
pub fn drain_audio_into(&mut self, out: &mut Vec<u8>) {
out.clear();
out.extend_from_slice(&self.audio_buffer);
self.audio_buffer.clear();
}
pub fn audio_buffer_len(&self) -> usize {
self.audio_buffer.len()
}
pub fn has_pending_sound_work(&self) -> bool {
self.active_interrupt_callback
.map(|callback| {
matches!(
callback.source,
ActiveInterruptCallbackSource::SoundCallback
| ActiveInterruptCallbackSource::SoundFileCompletion
| ActiveInterruptCallbackSource::SoundDoubleBack
)
})
.unwrap_or(false)
|| !self
.dispatcher
.sound_manager
.pending_sound_callbacks
.is_empty()
|| !self.dispatcher.sound_manager.pending_callbacks.is_empty()
|| self
.ppc_app
.as_ref()
.is_some_and(|app| !app.sound.pending_completions.is_empty())
}
pub fn is_ui_tracking_active(&self) -> bool {
self.frozen_ticks.is_some()
|| self.dispatcher.is_menu_tracking()
|| self.dispatcher.is_dialog_tracking()
|| self.dispatcher.is_control_tracking()
|| self.dispatcher.is_window_tracking()
|| self.dispatcher.is_grow_window_tracking()
|| self.dispatcher.is_region_tracking()
}
pub fn force_advance_guest_tick(&mut self) {
self.advance_guest_tick();
self.queue_due_ppc_sound_completions();
}
pub fn set_output_path(&mut self, path: std::path::PathBuf) {
if let Some(dir) = path.parent() {
self.dispatcher.output_dir = Some(dir.to_path_buf());
}
}
pub fn vfs_read(&self, filename: &str) -> Option<&[u8]> {
self.dispatcher.vfs.get(filename).map(|v| v.as_slice())
}
pub fn vfs_file_summaries(&mut self) -> Vec<VfsFileSummary> {
self.vfs_file_summaries_where(|_| true)
}
pub fn vfs_file_summaries_where<F>(&mut self, mut include: F) -> Vec<VfsFileSummary>
where
F: FnMut(&str) -> bool,
{
self.vfs_file_paths()
.into_iter()
.filter(|path| include(path))
.filter_map(|path| self.vfs_file_summary_for_path(&path))
.collect()
}
pub fn vfs_file_stats_where<F>(&mut self, mut include: F) -> Vec<VfsFileStat>
where
F: FnMut(&str) -> bool,
{
self.vfs_file_paths()
.into_iter()
.filter(|path| include(path))
.filter_map(|path| self.vfs_file_stat_for_path(&path))
.collect()
}
pub fn vfs_file_summary(&mut self, path: &str) -> Option<VfsFileSummary> {
let normalized = TrapDispatcher::normalize_vfs_path(path);
if normalized.is_empty() || normalized.starts_with("__rsrc__") {
return None;
}
self.vfs_file_summary_for_path(&normalized)
}
pub fn vfs_file_snapshot(&mut self, path: &str) -> Option<VfsFileSnapshot> {
let normalized = TrapDispatcher::normalize_vfs_path(path);
if normalized.is_empty() || normalized.starts_with("__rsrc__") {
return None;
}
if !self.dispatcher.vfs.contains_key(&normalized)
&& !self.dispatcher.vfs_rsrc.contains_key(&normalized)
{
return None;
}
let metadata = self.dispatcher.vfs_file_metadata(&normalized)?;
Some(VfsFileSnapshot {
path: normalized.clone(),
data_fork: self
.dispatcher
.vfs
.get(&normalized)
.cloned()
.unwrap_or_default(),
resource_fork: self
.dispatcher
.vfs_rsrc
.get(&normalized)
.cloned()
.unwrap_or_default(),
file_type: metadata.file_type,
creator: metadata.creator,
finder_flags: metadata.finder_flags,
created_date: metadata.created_date,
modified_date: metadata.modified_date,
})
}
pub fn prepare_vfs_resource_forks_for_native_export(&mut self) -> usize {
let Some(mut ppc_app) = self.ppc_app.take() else {
return 0;
};
let materialized_count = ppc_app.prepare_vfs_resource_forks_for_native_export();
self.sync_ppc_vfs_to_dispatcher(&mut ppc_app);
self.ppc_app = Some(ppc_app);
materialized_count
}
pub fn import_vfs_file(&mut self, file: &VfsFileSnapshot) {
let normalized = TrapDispatcher::normalize_vfs_path(&file.path);
if normalized.is_empty() || normalized.starts_with("__rsrc__") {
return;
}
self.dispatcher
.vfs
.insert(normalized.clone(), file.data_fork.clone());
self.dispatcher
.vfs_rsrc
.insert(normalized.clone(), file.resource_fork.clone());
self.dispatcher.ensure_vfs_file_metadata(&normalized);
if let Some(metadata) = self.dispatcher.vfs_metadata.get_mut(&normalized) {
metadata.file_type = file.file_type;
metadata.creator = file.creator;
metadata.finder_flags = file.finder_flags;
if file.created_date != 0 {
metadata.created_date = file.created_date;
}
if file.modified_date != 0 {
metadata.modified_date = file.modified_date;
}
}
}
pub fn import_vfs_file_relative_to_launched_app(
&mut self,
relative_dir: &str,
file: &VfsFileSnapshot,
) -> std::result::Result<(), String> {
let app_path = self
.dispatcher
.launched_app_path
.clone()
.ok_or_else(|| "launched app path is not available".to_string())?;
let app_parent = TrapDispatcher::vfs_parent_path(&app_path);
if app_parent.is_empty() {
return Err("launched app has no parent folder".to_string());
}
let relative_dir = TrapDispatcher::normalize_vfs_path(relative_dir);
if relative_dir.is_empty() || relative_dir.starts_with('/') || relative_dir.contains("..") {
return Err(format!("invalid relative VFS directory {relative_dir:?}"));
}
let file_path = TrapDispatcher::normalize_vfs_path(&file.path);
let filename = TrapDispatcher::vfs_basename(&file_path);
if filename.is_empty() {
return Err("plugin file has no filename".to_string());
}
let mut mounted = file.clone();
mounted.path = format!("{app_parent}/{relative_dir}/{filename}");
self.import_vfs_file(&mounted);
Ok(())
}
pub fn remove_vfs_file(&mut self, path: &str) -> bool {
self.dispatcher.remove_vfs_path(path)
}
fn vfs_file_paths(&mut self) -> Vec<String> {
self.dispatcher.ensure_vfs_catalog();
let mut paths = BTreeSet::new();
for path in self.dispatcher.vfs.keys() {
if !path.starts_with("__rsrc__") {
paths.insert(TrapDispatcher::normalize_vfs_path(path));
}
}
for path in self.dispatcher.vfs_rsrc.keys() {
if !path.starts_with("__rsrc__") {
paths.insert(TrapDispatcher::normalize_vfs_path(path));
}
}
paths.into_iter().filter(|path| !path.is_empty()).collect()
}
fn vfs_file_summary_for_path(&mut self, path: &str) -> Option<VfsFileSummary> {
let stat = self.vfs_file_stat_for_path(path)?;
let data_fork = self
.dispatcher
.vfs
.get(path)
.map(Vec::as_slice)
.unwrap_or(&[]);
let resource_fork = self
.dispatcher
.vfs_rsrc
.get(path)
.map(Vec::as_slice)
.unwrap_or(&[]);
Some(VfsFileSummary {
path: stat.path,
data_len: stat.data_len,
resource_len: stat.resource_len,
data_hash: vfs_fork_hash(data_fork),
resource_hash: vfs_fork_hash(resource_fork),
file_type: stat.file_type,
creator: stat.creator,
finder_flags: stat.finder_flags,
created_date: stat.created_date,
modified_date: stat.modified_date,
})
}
fn vfs_file_stat_for_path(&mut self, path: &str) -> Option<VfsFileStat> {
let metadata = self.dispatcher.vfs_file_metadata(path)?;
let data_len = self.dispatcher.vfs.get(path).map(Vec::len).unwrap_or(0);
let resource_len = self
.dispatcher
.vfs_rsrc
.get(path)
.map(Vec::len)
.unwrap_or(0);
Some(VfsFileStat {
path: path.to_string(),
data_len,
resource_len,
file_type: metadata.file_type,
creator: metadata.creator,
finder_flags: metadata.finder_flags,
created_date: metadata.created_date,
modified_date: metadata.modified_date,
})
}
pub fn step(&mut self) -> StepResult {
self.cpu.step(&mut self.bus)
}
pub fn load_app(&mut self, fork: &ResourceFork) -> Option<LoadedApp> {
let app = load_app_generic(fork, &mut self.bus, self.config.load_address)?;
let heap_start = app_heap_start_for_loaded_app(&app);
let image_start = app_image_start_for_loaded_app(&app);
self.bus.reserve_heap(APP_ZONE_HEADER_SIZE);
self.bus.reserve_heap_range(image_start, heap_start);
self.dispatcher.load_resources(fork, &mut self.bus);
let segments: HashMap<i16, u32> = app.segment_bases.iter().map(|(&k, &v)| (k, v)).collect();
self.dispatcher.register_segments(segments);
Some(app)
}
fn clear_startup_framebuffer(&mut self) {
if self.menu_bar_visible() {
let (scrn_base, row_bytes, screen_width, screen_height, pixel_size) =
self.dispatcher.screen_mode;
TrapDispatcher::fb_fill_pattern_rect(
&mut self.bus,
scrn_base,
row_bytes,
pixel_size,
screen_width as i16,
screen_height as i16,
0,
0,
screen_height as i16,
screen_width as i16,
[0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55],
);
return;
}
let (scrn_base, row_bytes, _, scrn_height, _) = self.dispatcher.screen_mode;
self.bus
.fill_bytes(scrn_base, row_bytes * scrn_height as u32, 0xFF);
}
#[cold]
#[inline(never)]
fn switch_to_launched_application(
&mut self,
app_path: &str,
) -> std::result::Result<(), String> {
use crate::memory::globals::addr;
let normalized = TrapDispatcher::normalize_vfs_path(app_path);
let rsrc_key = self
.dispatcher
.find_vfs_rsrc_file(&normalized)
.ok_or_else(|| format!("no resource fork for launched application {normalized:?}"))?;
let rsrc_bytes = self
.dispatcher
.vfs_rsrc
.get(&rsrc_key)
.cloned()
.ok_or_else(|| format!("resource fork {rsrc_key:?} disappeared before launch"))?;
let fork = ResourceFork::parse(&rsrc_bytes)
.ok_or_else(|| format!("failed to parse launched application {normalized:?}"))?;
let ram_size = self.bus.ram_size() as usize;
let config = FixtureRunnerConfig {
max_instructions: self.config.max_instructions,
load_address: self.config.load_address,
arrows_as_numpad: self.config.arrows_as_numpad,
menu_bar_policy: self.config.menu_bar_policy,
ui_theme: self.config.ui_theme,
theme_metrics_mode: self.config.theme_metrics_mode,
addressing_32_bit: self.bus.addressing_32_bit(),
screen_depth: self.config.screen_depth,
};
let menu_bar_policy = self.dispatcher.menu_bar_policy;
let menu_bar_hidden = self.dispatcher.menu_bar_hidden;
let initial_kiosk_guest_hide_observed = self.dispatcher.initial_kiosk_guest_hide_observed;
let instructions_per_tick = self.instructions_per_tick;
let wait_sleep_cap_in_headless = self.wait_sleep_cap_in_headless;
let app_start_time = self.app_start_time;
let launch_ticks_override = self.launch_ticks_override;
let launch_rnd_seed_override = self.launch_rnd_seed_override;
let launch_ppc_time_base_override = self.launch_ppc_time_base_override;
let application_partition_size = self.application_partition_size;
let powerpc_screen_depth_override = self.powerpc_screen_depth_override;
let ppc_host_mirror_capacity = self.ppc_host_mirror_capacity;
let total_instructions = self.total_instructions;
let launch_tick = self.guest_tick();
let launch_time = self.bus.read_long(addr::TIME);
let launch_rnd_seed = self.bus.read_long(addr::RND_SEED);
let mouse_pos = self.dispatcher.mouse_pos;
let mouse_button = self.dispatcher.mouse_button;
let output_dir = self.dispatcher.output_dir.clone();
let vfs = self.dispatcher.vfs.clone();
let vfs_rsrc = self.dispatcher.vfs_rsrc.clone();
let vfs_metadata = self.dispatcher.vfs_metadata.clone();
let vfs_directories = self.dispatcher.vfs_directories.clone();
let vfs_directory_paths = self.dispatcher.vfs_directory_paths.clone();
let vfs_volumes = self.dispatcher.vfs_volumes.clone();
let vfs_volume_names = self.dispatcher.vfs_volume_names.clone();
let next_vfs_volume_ref_num = self.dispatcher.next_vfs_volume_ref_num;
let locked_files = self.dispatcher.locked_files.clone();
let next_vfs_dir_id = self.dispatcher.next_vfs_dir_id;
let next_vfs_file_id = self.dispatcher.next_vfs_file_id;
let next_vfs_timestamp = self.dispatcher.next_vfs_timestamp;
let next_working_dir_refnum = self.dispatcher.next_working_dir_refnum;
let mut replacement = FixtureRunner::new(ram_size, config);
replacement.dispatcher.menu_bar_policy = menu_bar_policy;
replacement.dispatcher.menu_bar_hidden = menu_bar_hidden;
replacement.dispatcher.initial_kiosk_guest_hide_observed =
initial_kiosk_guest_hide_observed;
replacement.instructions_per_tick = instructions_per_tick;
replacement.tick_budget = instructions_per_tick as i32;
replacement.wait_sleep_cap_in_headless = wait_sleep_cap_in_headless;
replacement.app_start_time = app_start_time;
replacement.launch_ticks_override = launch_ticks_override;
replacement.launch_rnd_seed_override = launch_rnd_seed_override;
replacement.launch_ppc_time_base_override = launch_ppc_time_base_override;
replacement.application_partition_size = application_partition_size;
replacement.powerpc_screen_depth_override = powerpc_screen_depth_override;
replacement.total_instructions = total_instructions;
replacement.dispatcher.output_dir = output_dir;
replacement.dispatcher.vfs = vfs;
replacement.dispatcher.vfs_rsrc = vfs_rsrc;
replacement.dispatcher.vfs_metadata = vfs_metadata;
replacement.dispatcher.vfs_directories = vfs_directories;
replacement.dispatcher.vfs_directory_paths = vfs_directory_paths;
replacement.dispatcher.vfs_volumes = vfs_volumes;
replacement.dispatcher.vfs_volume_names = vfs_volume_names;
replacement.dispatcher.next_vfs_volume_ref_num = next_vfs_volume_ref_num;
replacement.dispatcher.locked_files = locked_files;
replacement.dispatcher.next_vfs_dir_id = next_vfs_dir_id;
replacement.dispatcher.next_vfs_file_id = next_vfs_file_id;
replacement.dispatcher.next_vfs_timestamp = next_vfs_timestamp;
replacement.dispatcher.next_working_dir_refnum = next_working_dir_refnum;
replacement.dispatcher.set_launched_app_path(&normalized);
let app = replacement
.load_app(&fork)
.ok_or_else(|| format!("failed to load launched application {normalized:?}"))?;
replacement.init_app(&app);
if ppc_host_mirror_capacity != 0 {
let base = replacement.bus.alloc(ppc_host_mirror_capacity);
if base == 0 {
return Err("failed to preserve the PowerPC host framebuffer mirror".to_string());
}
replacement.ppc_host_mirror_base = base;
replacement.ppc_host_mirror_capacity = ppc_host_mirror_capacity;
}
replacement.bus.write_long(addr::TICKS, launch_tick);
replacement.bus.write_long(addr::TIME, launch_time);
replacement.bus.write_long(addr::RND_SEED, launch_rnd_seed);
replacement.dispatcher.tick_count = launch_tick;
if let Some(ppc_app) = replacement.ppc_app.as_mut() {
ppc_app.set_tick_count(launch_tick);
}
replacement.dispatcher.mouse_pos = mouse_pos;
replacement.dispatcher.mouse_button = mouse_button;
replacement
.bus
.write_byte(addr::MB_STATE, if mouse_button { 0x00 } else { 0x80 });
replacement.clear_startup_framebuffer();
replacement.audio = self.audio.take();
replacement.audio_buffer = std::mem::take(&mut self.audio_buffer);
eprintln!("[LAUNCH] Switched foreground application to {normalized}");
*self = replacement;
Ok(())
}
fn service_pending_launch_application(
&mut self,
event_yield_reached: bool,
caller_exited: bool,
) -> bool {
let Some(path) = self
.dispatcher
.take_pending_launch_application(event_yield_reached, caller_exited)
else {
return false;
};
if let Err(err) = self.switch_to_launched_application(&path) {
eprintln!("[LAUNCH] Failed to switch to queued application {path:?}: {err}");
self.halted = true;
self.halted_pc = Some(self.cpu.read_reg(Register::PC));
self.halted_sp = Some(self.cpu.read_reg(Register::A7));
self.halted_d0 = Some((-43i32) as u32);
}
true
}
pub(crate) fn merge_resources_into_application(&mut self, fork: &ResourceFork) -> usize {
self.dispatcher
.merge_resources_into_existing_file(fork, &mut self.bus, 0)
}
fn alloc_handle_with_bytes(&mut self, bytes: &[u8]) -> u32 {
let data_ptr = if bytes.is_empty() {
0
} else {
let data_ptr = self.bus.alloc(bytes.len() as u32);
if data_ptr == 0 {
return 0;
}
self.bus.write_bytes(data_ptr, bytes);
data_ptr
};
let handle = self.bus.alloc(4);
if handle == 0 {
if data_ptr != 0 {
self.bus.free(data_ptr);
}
return 0;
}
self.bus.write_long(handle, data_ptr);
if data_ptr != 0 {
self.dispatcher.ptr_to_handle.insert(data_ptr, handle);
}
handle
}
fn write_fixed_pstring(&mut self, ptr: u32, value: &str, max_len: usize) {
let bytes = value.as_bytes();
let len = bytes.len().min(max_len);
self.bus.write_byte(ptr, len as u8);
for (i, &byte) in bytes.iter().take(len).enumerate() {
self.bus.write_byte(ptr + 1 + i as u32, byte);
}
}
fn seed_current_application_file_manager_state(&mut self) {
use crate::memory::globals::addr;
let Some(app_path) = self.dispatcher.launched_app_path.clone() else {
return;
};
self.dispatcher.ensure_vfs_file_metadata(&app_path);
let metadata = self
.dispatcher
.vfs_metadata
.get(&app_path)
.copied()
.unwrap_or(crate::trap::dispatch::VfsMetadata {
file_id: 0,
parent_dir_id: self.dispatcher.default_dir_id,
file_type: u32::from_be_bytes(*b"APPL"),
creator: u32::from_be_bytes(*b"????"),
finder_flags: 0,
created_date: 0,
modified_date: 0,
});
let resource_len = self
.dispatcher
.vfs_rsrc
.get(&app_path)
.map(|bytes| bytes.len() as u32)
.unwrap_or(0);
let vcb_ptr = self.bus.alloc(HFS_VCB_SIZE);
if vcb_ptr == 0 {
return;
}
self.bus.fill_bytes(vcb_ptr, HFS_VCB_SIZE, 0);
self.bus.write_word(vcb_ptr + 8, 0x4244); self.write_fixed_pstring(
vcb_ptr + 44,
crate::trap::dispatch::TrapDispatcher::boot_volume_name(),
27,
);
self.bus.write_word(
vcb_ptr + 78,
crate::trap::dispatch::BOOT_VOLUME_REF_NUM as u16,
); self.bus
.write_long(vcb_ptr + 172, self.dispatcher.default_dir_id);
self.bus.write_long(addr::DEF_VCB_PTR, vcb_ptr);
self.bus.write_word(addr::VCB_Q_HDR, 0);
self.bus.write_long(addr::VCB_Q_HDR + 2, vcb_ptr);
self.bus.write_long(addr::VCB_Q_HDR + 6, vcb_ptr);
let fcb_buffer = self.bus.alloc(HFS_FCB_BUFFER_SIZE as u32);
if fcb_buffer == 0 {
return;
}
self.bus
.fill_bytes(fcb_buffer, HFS_FCB_BUFFER_SIZE as u32, 0);
self.bus.write_word(fcb_buffer, HFS_FCB_BUFFER_SIZE);
let fcb = fcb_buffer + APPLICATION_RESOURCE_REFNUM as u32;
self.bus.write_long(fcb, metadata.file_id);
self.bus.write_word(fcb + 4, 0x0200); self.bus.write_long(fcb + 8, resource_len);
self.bus.write_long(fcb + 12, resource_len);
self.bus.write_long(fcb + 20, vcb_ptr);
self.bus.write_long(fcb + 50, metadata.file_type);
self.bus.write_long(fcb + 58, metadata.parent_dir_id);
let app_name = crate::trap::dispatch::TrapDispatcher::vfs_basename(&app_path).to_string();
self.write_fixed_pstring(fcb + 62, &app_name, 31);
self.bus.write_long(addr::FCB_S_PTR, fcb_buffer);
self.bus.write_word(addr::FS_FCB_LEN, HFS_FCB_SIZE);
self.bus
.write_word(addr::CUR_APREF_NUM, APPLICATION_RESOURCE_REFNUM);
self.dispatcher
.open_files
.insert(APPLICATION_RESOURCE_REFNUM, format!("__rsrc__{}", app_path));
self.dispatcher
.file_positions
.insert(APPLICATION_RESOURCE_REFNUM, 0);
}
pub fn init_app(&mut self, app: &LoadedApp) {
if let Some(ppc_app) = app.ppc.clone() {
self.init_ppc_app(ppc_app);
return;
}
self.ppc_app = None;
self.ppc_sound_synced_file_playback_count = 0;
self.ppc_sound_host_playbacks.clear();
use crate::memory::globals::addr;
let ram_size = self.bus.ram_size();
self.dispatcher.application_high_level_event_aware = app
.size_resource
.is_some_and(ApplicationSizeResource::is_high_level_event_aware);
let launch_sr = (self.cpu.core.get_sr() & !0x0700) | 0x2000;
self.cpu.core.set_sr_noint_nosp(launch_sr);
self.bus.write_long(addr::MEM_TOP, ram_size);
let app_globals_start = app.a5_base.saturating_sub(app.code0_header.below_a5);
self.bus.write_long(addr::CUR_STACK_BASE, app_globals_start);
self.bus.write_long(addr::CURRENT_A5, app.a5_base);
self.bus.write_word(addr::ROM85, 0x0000);
self.bus
.write_byte(addr::MMU32_BIT, u8::from(self.bus.addressing_32_bit()));
let launch_ticks = self
.launch_ticks_override
.map_or(DEFAULT_LAUNCH_TICKS, |floor| {
DEFAULT_LAUNCH_TICKS.max(floor)
});
self.bus.write_long(addr::TICKS, launch_ticks);
self.dispatcher.tick_count = launch_ticks;
let time = self
.app_start_time
.unwrap_or_else(current_mac_epoch_seconds);
self.bus.write_long(addr::TIME, time);
self.bus
.write_long(addr::DOUBLE_TIME, DEFAULT_DOUBLE_TIME_TICKS);
self.bus.write_long(
addr::RND_SEED,
self.launch_rnd_seed_override.unwrap_or(time),
);
self.bus.write_byte(addr::MB_STATE, 0x80);
self.sync_key_map_lowmem();
self.bus.write_word(CURSOR_TASK_NOOP_ADDR, 0x4E75);
self.bus
.write_long(addr::J_CRSR_TASK, CURSOR_TASK_NOOP_ADDR);
self.bus.write_word(addr::MBAR_HEIGHT, 20);
self.bus.write_byte(addr::SD_VOLUME, 1);
let allocation_heap_start = app_heap_start_for_loaded_app(app);
let visible_zone_start = app_visible_zone_start_for_loaded_app(app);
let zone_header_size: u32 = APP_ZONE_HEADER_SIZE;
let initial_heap_end = visible_zone_start + zone_header_size;
let minimum_safe_appl_limit = self.bus.heap_bump_ptr().max(allocation_heap_start);
let stack_base = app.initial_sp;
let default_appl_limit = stack_base - APP_STACK_SAFETY_MARGIN;
let requested_partition_size = self.application_partition_size.or_else(|| {
app.size_resource
.and_then(|size| size.preferred_partition_size())
});
let appl_limit = requested_partition_size
.and_then(|partition_size| {
partition_size
.checked_sub(APP_STACK_SAFETY_MARGIN)
.and_then(|heap_span| visible_zone_start.checked_add(heap_span))
})
.map(|limit| limit.max(minimum_safe_appl_limit))
.filter(|&limit| limit > initial_heap_end && limit < default_appl_limit)
.unwrap_or(default_appl_limit.max(initial_heap_end));
let buf_ptr = appl_limit; self.bus.write_long(addr::SYS_ZONE, visible_zone_start);
self.bus.write_long(addr::APP_L_ZONE, visible_zone_start);
self.bus.write_long(addr::HEAP_END, initial_heap_end);
self.bus.write_long(addr::APPL_LIMIT, appl_limit);
self.bus.write_long(addr::BUF_PTR, buf_ptr);
self.bus.write_long(addr::THE_ZONE, visible_zone_start);
self.bus.write_word(addr::CUR_APREF_NUM, 0);
if let Some(app_path) = &self.dispatcher.launched_app_path {
let app_name = crate::trap::dispatch::TrapDispatcher::vfs_basename(app_path);
let name_bytes = app_name.as_bytes();
let len = name_bytes.len().min(31);
self.bus.write_byte(addr::CUR_APNAME, len as u8);
for (i, &b) in name_bytes.iter().take(len).enumerate() {
self.bus.write_byte(addr::CUR_APNAME + 1 + i as u32, b);
}
}
let app_dir_id = self.dispatcher.default_dir_id;
self.bus.write_long(addr::CUR_DIR_STORE, app_dir_id);
self.bus.write_word(
addr::SF_SAVE_DISK,
(-crate::trap::dispatch::BOOT_VOLUME_REF_NUM) as u16,
);
eprintln!(
"[INIT] CurDirStore={} SFSaveDisk={}",
app_dir_id,
(-crate::trap::dispatch::BOOT_VOLUME_REF_NUM) as u16
);
let stack_seed_start = stack_base.saturating_sub(0x8000);
self.bus
.fill_zeros(stack_seed_start, stack_base - stack_seed_start);
self.bus.reserve_heap(zone_header_size);
let zone_size = appl_limit.saturating_sub(visible_zone_start);
let free_bytes = zone_size.saturating_sub(zone_header_size);
self.bus.write_long(visible_zone_start, appl_limit); self.bus.write_long(
visible_zone_start + 8,
visible_zone_start + zone_header_size,
); self.bus.write_long(visible_zone_start + 12, free_bytes); self.bus.write_long(
visible_zone_start + 56,
visible_zone_start + zone_header_size,
); eprintln!(
"[INIT] Zone header: start=${:08X} allocStart=${:08X} bkLim=${:08X} zcbFree={} ({:.1}MB)",
visible_zone_start,
allocation_heap_start,
appl_limit,
free_bytes,
free_bytes as f64 / (1024.0 * 1024.0)
);
self.seed_current_application_file_manager_state();
let app_param_handle = self.alloc_handle_with_bytes(&[0, 0, 0, 0]);
self.bus.write_long(addr::APP_PARM_HANDLE, app_param_handle);
let exit_trampoline = 0x100u32;
self.bus.write_word(exit_trampoline, 0xA9F4);
let gdh = self.dispatcher.ensure_main_gdevice(&mut self.bus);
let gd_ptr = self.bus.read_long(gdh);
self.bus.write_long(0x8A4, gdh); self.bus.write_long(0xCC8, gdh); self.bus.write_long(0x8A8, gdh); eprintln!(
"[INIT] Set MainDevice=${:08X}, TheGDevice=${:08X} (ptr=${:08X})",
gdh, gdh, gd_ptr
);
let pmap_h = self.bus.read_long(gd_ptr + 22);
let pmap = self.bus.read_long(pmap_h);
let scrn_base = self.bus.read_long(pmap);
let rb = (self.bus.read_word(pmap + 4) & 0x3FFF) as u32;
let top = self.bus.read_word(pmap + 6) as i16;
let left = self.bus.read_word(pmap + 8) as i16;
let bottom = self.bus.read_word(pmap + 10) as i16;
let right = self.bus.read_word(pmap + 12) as i16;
let pixel_size = self.bus.read_word(pmap + 32);
let width = (right - left).max(1) as u16;
let height = (bottom - top).max(1) as u16;
self.dispatcher.screen_mode = (scrn_base, rb, width, height, pixel_size);
{
let main_dev = self.bus.read_long(0x8A4);
let gd = self.bus.read_long(main_dev);
let pmap_h = self.bus.read_long(gd + 22);
let pmap = self.bus.read_long(pmap_h);
let rb = self.bus.read_word(pmap + 4);
let top = self.bus.read_word(pmap + 6) as i16;
let left = self.bus.read_word(pmap + 8) as i16;
let bottom = self.bus.read_word(pmap + 10) as i16;
let right = self.bus.read_word(pmap + 12) as i16;
let ps = self.bus.read_word(pmap + 32);
let gd_top = self.bus.read_word(gd + 34) as i16;
let gd_left = self.bus.read_word(gd + 36) as i16;
let gd_bottom = self.bus.read_word(gd + 38) as i16;
let gd_right = self.bus.read_word(gd + 40) as i16;
let gd_flags = self.bus.read_word(gd + 20);
eprintln!(
"[INIT] GDevice chain: $8A4→${:08X}→${:08X} gdPMap→${:08X}→${:08X}",
main_dev, gd, pmap_h, pmap
);
eprintln!(
"[INIT] PixMap: rowBytes=${:04X} bounds=({},{},{},{}) pixelSize={}",
rb, top, left, bottom, right, ps
);
eprintln!(
"[INIT] GDevice: gdRect=({},{},{},{}) gdFlags=${:04X}",
gd_top, gd_left, gd_bottom, gd_right, gd_flags
);
}
let swap_mmu_mode_trampoline = self.bus.alloc(4);
self.bus.write_word(swap_mmu_mode_trampoline, 0xA05D); self.bus.write_word(swap_mmu_mode_trampoline + 2, 0x4E75); self.bus
.write_long(addr::SWAP_MMU_MODE_TRAP, swap_mmu_mode_trampoline);
let hide_cursor_trampoline = self.bus.alloc(6);
self.bus.write_word(hide_cursor_trampoline, 0x205F); self.bus.write_word(hide_cursor_trampoline + 2, 0xA852); self.bus.write_word(hide_cursor_trampoline + 4, 0x4ED0); self.bus
.write_long(addr::J_HIDE_CURSOR, hide_cursor_trampoline);
let show_cursor_trampoline = self.bus.alloc(4);
self.bus.write_word(show_cursor_trampoline, 0xA853); self.bus.write_word(show_cursor_trampoline + 2, 0x4E75); self.bus
.write_long(addr::J_SHOW_CURSOR, show_cursor_trampoline);
let shield_cursor_trampoline = self.bus.alloc(6);
self.bus.write_word(shield_cursor_trampoline, 0x205F); self.bus.write_word(shield_cursor_trampoline + 2, 0xA855); self.bus.write_word(shield_cursor_trampoline + 4, 0x4ED0); self.bus
.write_long(addr::J_SHIELD_CURSOR, shield_cursor_trampoline);
let init_cursor_trampoline = self.bus.alloc(4);
self.bus.write_word(init_cursor_trampoline, 0xA850); self.bus.write_word(init_cursor_trampoline + 2, 0x4E75); self.bus
.write_long(addr::J_INIT_CRSR, init_cursor_trampoline);
let swap_font_trampoline = self.bus.alloc(6);
self.bus.write_word(swap_font_trampoline, 0x205F); self.bus.write_word(swap_font_trampoline + 2, 0xA901); self.bus.write_word(swap_font_trampoline + 4, 0x4ED0); self.bus.write_long(addr::J_SWAP_FONT, swap_font_trampoline);
self.cpu.write_reg(Register::A5, app.a5_base);
let sp = app.initial_sp.wrapping_sub(4);
self.bus.write_long(sp, exit_trampoline);
self.cpu.write_reg(Register::A7, sp);
self.cpu.write_reg(Register::A6, sp);
self.cpu
.write_reg(Register::PC, app.entry_point(app.a5_base));
}
fn init_ppc_app(&mut self, mut ppc_app: PpcLoadedApp) {
use crate::memory::globals::addr;
let ram_size = self.bus.ram_size();
self.bus.write_long(addr::MEM_TOP, ram_size);
let launch_ticks = self.launch_ticks_override.unwrap_or(0);
self.bus.write_long(addr::TICKS, launch_ticks);
self.dispatcher.tick_count = launch_ticks;
ppc_app.set_tick_count(launch_ticks);
let time = self
.app_start_time
.unwrap_or_else(current_mac_epoch_seconds);
self.bus.write_long(addr::TIME, time);
let rnd_seed = self.launch_rnd_seed_override.unwrap_or(time);
self.bus.write_long(addr::RND_SEED, rnd_seed);
self.share_ppc_runtime_globals(&mut ppc_app);
ppc_app.share_event_queue(&self.dispatcher.event_queue);
if let Some(time_base) = self.launch_ppc_time_base_override {
ppc_app.cpu.set_time_base(time_base);
}
self.bus.write_byte(addr::MB_STATE, 0x80);
self.bus.write_word(addr::MBAR_HEIGHT, 20);
self.bus.write_byte(addr::SOUND_LEVEL, 1);
let app_dir_id = self.dispatcher.default_dir_id;
self.bus.write_long(addr::CUR_DIR_STORE, app_dir_id);
self.bus.write_word(
addr::SF_SAVE_DISK,
(-crate::trap::dispatch::BOOT_VOLUME_REF_NUM) as u16,
);
let gdh = self.dispatcher.ensure_main_gdevice(&mut self.bus);
self.bus.write_long(0x8A4, gdh);
self.bus.write_long(0xCC8, gdh);
self.bus.write_long(0x8A8, gdh);
if let Some(front_buffer) = ppc_app.presented_front_buffer() {
self.ensure_ppc_host_screen_mode(front_buffer);
} else {
let gd_ptr = self.bus.read_long(gdh);
let pmap_h = self.bus.read_long(gd_ptr + 22);
let pmap = self.bus.read_long(pmap_h);
let scrn_base = self.bus.read_long(pmap);
let rb = (self.bus.read_word(pmap + 4) & 0x3FFF) as u32;
let top = self.bus.read_word(pmap + 6) as i16;
let left = self.bus.read_word(pmap + 8) as i16;
let bottom = self.bus.read_word(pmap + 10) as i16;
let right = self.bus.read_word(pmap + 12) as i16;
let pixel_size = self.bus.read_word(pmap + 32);
let width = (right - left).max(1) as u16;
let height = (bottom - top).max(1) as u16;
self.dispatcher.screen_mode = (scrn_base, rb, width, height, pixel_size);
}
self.halted = false;
self.halted_trap = None;
self.halted_pc = None;
self.halted_sp = None;
self.halted_d0 = None;
self.cpu.write_reg(Register::PC, 0);
self.ppc_sound_synced_file_playback_count = 0;
self.ppc_sound_host_playbacks.clear();
self.ppc_app = Some(ppc_app);
}
fn share_ppc_runtime_globals(&mut self, ppc_app: &mut PpcLoadedApp) {
use crate::memory::globals::addr;
for (address, len) in [
(addr::SYS_EVT_MASK, 2),
(addr::RND_SEED, 4),
(addr::TICKS, 4),
(addr::TIME, 4),
(addr::MB_STATE, 1),
(addr::KEY_MAP_LM, 16),
(addr::M_TEMP, 12),
] {
let region = self
.bus
.shared_ram_region(address, len)
.expect("FixtureRunner owns stable guest RAM");
unsafe {
ppc_app.memory.add_shared_region(address, region);
}
}
}
pub fn mix_audio(&mut self, num_samples: usize) {
self.mix_host_audio(num_samples);
}
fn advance_headless_callback_audio(&mut self, elapsed_ticks: u32) {
if elapsed_ticks == 0 || !self.dispatcher.sound_manager.has_playback_gated_callback() {
return;
}
let samples_per_tick = (crate::sound::OUTPUT_RATE as f64 / DEFAULT_VBL_HZ).round() as usize;
let samples = (elapsed_ticks as usize).saturating_mul(samples_per_tick);
self.mix_host_audio(samples);
self.dispatcher
.sync_guest_sound_channel_state(&mut self.bus);
}
fn queue_mixed_audio(&mut self, stereo_samples: &[u8]) {
if stereo_samples.is_empty() {
return;
}
if let Some(ref mut audio) = self.audio {
audio.queue_stereo_samples(stereo_samples);
}
for frame in stereo_samples.chunks_exact(2) {
let left = frame[0] as i32 - 0x80;
let right = frame[1] as i32 - 0x80;
self.audio_buffer
.push(((left + right) / 2 + 0x80).clamp(0, 255) as u8);
}
}
fn queue_host_silence_audio(&mut self, num_samples: usize) {
if num_samples == 0 {
return;
}
if let Some(ref mut audio) = self.audio {
audio.queue_stereo_samples(&vec![0x80; num_samples * 2]);
}
}
fn mix_host_audio(&mut self, mut remaining_samples: usize) {
self.service_ppc_double_buffer_playbacks();
while remaining_samples > 0 {
self.try_load_pending_double_buffers();
self.dispatcher.service_guest_sound_queues(&mut self.bus);
let chunk = self
.dispatcher
.sound_manager
.samples_until_next_exhaustion()
.map(|samples| samples.max(1).min(remaining_samples))
.unwrap_or(remaining_samples);
let samples = self.dispatcher.sound_manager.mix_frame_stereo(chunk);
if samples.is_empty() {
self.queue_host_silence_audio(remaining_samples);
self.dispatcher
.release_finished_internal_sound_channels(&mut self.bus);
break;
}
self.queue_mixed_audio(&samples);
self.dispatcher
.release_finished_internal_sound_channels(&mut self.bus);
remaining_samples -= chunk;
self.service_ppc_double_buffer_playbacks();
}
self.sync_ppc_sound_completions_from_dispatcher();
}
fn finish_host_frame(&mut self, audio_samples: usize, sound_interrupt_dispatched: bool) {
self.dispatcher.redraw_chrome(&mut self.bus);
self.finish_audio_frame(audio_samples, sound_interrupt_dispatched);
}
fn finish_audio_frame(&mut self, audio_samples: usize, sound_interrupt_dispatched: bool) {
self.try_load_pending_double_buffers();
self.dispatcher.service_guest_sound_queues(&mut self.bus);
if audio_samples > 0 {
self.mix_host_audio(audio_samples);
}
self.dispatcher
.sync_guest_sound_channel_state(&mut self.bus);
if !sound_interrupt_dispatched {
let fired_sound_callback = self.fire_sound_callbacks();
if !fired_sound_callback {
self.fire_sound_doubleback_callbacks();
}
}
}
pub fn mix_gui_audio_slice(&mut self, audio_samples: usize) {
self.finish_audio_frame(audio_samples, false);
}
fn try_tickcount_spin_fastfwd(
&mut self,
pc_after_trap: u32,
tick_cap: Option<u32>,
count: &mut usize,
) -> bool {
let w0 = self.bus.read_word(pc_after_trap);
if (w0 & 0xF1FF) == 0xB09F {
let dn = ((w0 >> 9) & 7) as usize;
return self.try_spin_template_d(pc_after_trap, dn, tick_cap);
}
if (w0 & 0xF1F8) == 0x3028 {
let dn = ((w0 >> 9) & 7) as usize;
return self.try_spin_template_e(pc_after_trap, dn, w0, tick_cap);
}
if (w0 & 0xF1FF) != 0x201F {
return false;
}
let dn = ((w0 >> 9) & 7) as usize;
let w1 = self.bus.read_word(pc_after_trap.wrapping_add(2));
if (w1 & 0xF1F8) == 0xB080 && (w1 & 7) as usize == dn {
return self.try_spin_template_f(pc_after_trap, w1, tick_cap);
}
if (w1 & 0xF1F8) == 0x2140 && (w1 & 7) as usize == dn {
return self.try_spin_template_g(pc_after_trap, dn, w1, tick_cap);
}
if (w1 & 0xF1F8) == 0x5180 && (w1 & 0x0007) as usize == dn {
return self.try_spin_template_a(pc_after_trap, dn, w1, tick_cap, count);
}
if (w1 & 0xF1F8) == 0xB0A8 && ((w1 >> 9) & 7) as usize == dn {
return self.try_spin_template_b(pc_after_trap, dn, w1, tick_cap, count);
}
if (w1 & 0xF1FF) == 0xB0B9 && ((w1 >> 9) & 7) as usize == dn {
return self.try_spin_template_c(pc_after_trap, dn, tick_cap, count);
}
false
}
fn try_spin_template_f(
&mut self,
pc_after_trap: u32,
w_cmp: u16,
tick_cap: Option<u32>,
) -> bool {
let dm = ((w_cmp >> 9) & 7) as usize;
let branch_pc = pc_after_trap.wrapping_add(4);
let w_branch = self.bus.read_word(branch_pc);
if (w_branch & 0xFF00) != 0x6700 || (w_branch & 0x00FF) == 0 {
return false;
}
let displacement = (w_branch & 0xFF) as i8 as i32;
let target = (branch_pc.wrapping_add(2) as i32).wrapping_add(displacement) as u32;
let canonical_target = pc_after_trap.wrapping_sub(4);
if target != canonical_target
|| !matches!(self.bus.read_word(canonical_target), 0x594F | 0x598F)
|| self.bus.read_word(canonical_target.wrapping_add(2)) != 0xA975
{
return false;
}
let sp = self.cpu.core.a(7);
let captured_tick = self.bus.read_long(sp);
if captured_tick != self.cpu.core.d(dm) {
return false;
}
let target_tick = captured_tick.wrapping_add(1);
match self.advance_until_tick(target_tick, tick_cap) {
AdvanceResult::CapHit => {
self.bus.write_long(sp, self.dispatcher.tick_count);
true
}
AdvanceResult::Advanced => {
self.bus.write_long(sp, self.dispatcher.tick_count);
false
}
AdvanceResult::Interrupted | AdvanceResult::TooFar => false,
}
}
fn try_spin_template_g(
&mut self,
pc_after_trap: u32,
dn: usize,
w_store: u16,
tick_cap: Option<u32>,
) -> bool {
let local_an = ((w_store >> 9) & 7) as usize;
let local_disp = self.bus.read_word(pc_after_trap.wrapping_add(4));
let w_reload = self.bus.read_word(pc_after_trap.wrapping_add(6));
if (w_reload & 0xF1F8) != 0x2028
|| ((w_reload >> 9) & 7) as usize != dn
|| (w_reload & 7) as usize != local_an
|| self.bus.read_word(pc_after_trap.wrapping_add(8)) != local_disp
{
return false;
}
let w_sub = self.bus.read_word(pc_after_trap.wrapping_add(10));
if (w_sub & 0xF1F8) != 0x90A8 || ((w_sub >> 9) & 7) as usize != dn {
return false;
}
let base_disp = self.bus.read_word(pc_after_trap.wrapping_add(12)) as i16 as i32;
let base_an = (w_sub & 7) as usize;
let w_delay = self.bus.read_word(pc_after_trap.wrapping_add(14));
if (w_delay & 0xF1F8) != 0x3068 || (w_delay & 7) as usize != local_an {
return false;
}
let delay_an = ((w_delay >> 9) & 7) as usize;
let delay_disp = self.bus.read_word(pc_after_trap.wrapping_add(16)) as i16 as i32;
let w_cmp = self.bus.read_word(pc_after_trap.wrapping_add(18));
if (w_cmp & 0xF1F8) != 0xB1C0
|| ((w_cmp >> 9) & 7) as usize != delay_an
|| (w_cmp & 7) as usize != dn
{
return false;
}
let branch_pc = pc_after_trap.wrapping_add(20);
let w_branch = self.bus.read_word(branch_pc);
if (w_branch & 0xFF00) != 0x6E00 || (w_branch & 0x00FF) == 0 {
return false;
}
let displacement = (w_branch & 0xFF) as i8 as i32;
let target = (branch_pc.wrapping_add(2) as i32).wrapping_add(displacement) as u32;
let canonical_target = pc_after_trap.wrapping_sub(4);
if target != canonical_target
|| !matches!(self.bus.read_word(canonical_target), 0x594F | 0x598F)
|| self.bus.read_word(canonical_target.wrapping_add(2)) != 0xA975
{
return false;
}
let base_addr = (self.cpu.core.a(base_an) as i32).wrapping_add(base_disp) as u32;
let base_tick = self.bus.read_long(base_addr);
let delay_addr = (self.cpu.core.a(local_an) as i32).wrapping_add(delay_disp) as u32;
let delay = self.bus.read_word(delay_addr) as i16 as i32;
let sp = self.cpu.core.a(7);
let captured_tick = self.bus.read_long(sp);
let elapsed = captured_tick.wrapping_sub(base_tick) as i32;
if delay <= elapsed {
return false;
}
let target_tick = base_tick.wrapping_add(delay as u32);
match self.advance_until_tick(target_tick, tick_cap) {
AdvanceResult::CapHit => {
self.bus.write_long(sp, self.dispatcher.tick_count);
true
}
AdvanceResult::Advanced => {
self.bus.write_long(sp, self.dispatcher.tick_count);
false
}
AdvanceResult::Interrupted | AdvanceResult::TooFar => false,
}
}
fn cancel_idle_cycle_observation(&mut self) {
if self.idle_cycle_probe.is_some() {
self.bus.cancel_write_probe();
}
self.idle_cycle_probe = None;
self.idle_cycle_last_seen = None;
}
fn cancel_idle_cycle_detector(&mut self) {
self.bus.cancel_write_probe();
self.idle_cycle_probe = None;
self.idle_cycle_last_seen = None;
self.idle_cycle_sleep = None;
}
fn idle_cycle_site_is_busy(&self, trap_pc: u32, tick: u32) -> bool {
matches!(
self.idle_cycle_site_probes,
Some((site, site_tick, probes))
if site == trap_pc && site_tick == tick && probes > IDLE_CYCLE_MAX_PROBES_PER_TICK
)
}
fn mark_idle_cycle_site_busy(&mut self, trap_pc: u32, tick: u32) {
self.idle_cycle_site_probes = Some((trap_pc, tick, IDLE_CYCLE_MAX_PROBES_PER_TICK + 1));
self.idle_cycle_last_seen = None;
}
fn begin_idle_cycle_probe(&mut self, trap_pc: u32, tick: u32, cpu: CpuArchitecturalSnapshot) {
let probes = match self.idle_cycle_site_probes {
Some((site, site_tick, probes)) if site == trap_pc && site_tick == tick => probes,
_ => 0,
};
if probes >= IDLE_CYCLE_MAX_PROBES_PER_TICK {
self.mark_idle_cycle_site_busy(trap_pc, tick);
return;
}
self.idle_cycle_site_probes = Some((trap_pc, tick, probes + 1));
if wait_stats_enabled() {
WS_PROBE_STARTS.fetch_add(1, AtomicOrdering::Relaxed);
}
self.bus.begin_write_probe();
self.idle_cycle_probe = Some(IdleCycleProbe {
trap_pc,
tick,
cpu,
arrivals: 0,
});
self.idle_cycle_last_seen = Some((trap_pc, tick));
}
fn park_proven_idle_cycle(&mut self, trap_pc: u32, wake_tick: u32) {
if wait_stats_enabled() {
WS_PARKED.fetch_add(1, AtomicOrdering::Relaxed);
}
self.bus.cancel_write_probe();
self.idle_cycle_probe = None;
self.idle_cycle_last_seen = None;
let tick = self.dispatcher.tick_count;
self.idle_cycle_sleep = Some(ProvenIdleCycleSleep {
trap_pc,
wake_tick,
tick,
cpu: CpuArchitecturalSnapshot::capture(&self.cpu.core),
host: IdleCycleHostSnapshot::capture(&self.dispatcher),
});
self.bus.begin_write_probe();
}
fn try_resume_proven_idle_cycle(&mut self, tick_cap: Option<u32>) -> bool {
let Some(sleep) = self.idle_cycle_sleep.take() else {
return false;
};
let memory_unchanged = self.bus.finish_write_probe_unchanged();
let cpu_unchanged = sleep.cpu == CpuArchitecturalSnapshot::capture(&self.cpu.core);
let tick_unchanged =
self.dispatcher.tick_count == sleep.tick && self.bus.read_long(0x016A) == sleep.tick;
let host_unchanged = sleep.host == IdleCycleHostSnapshot::capture(&self.dispatcher);
let can_observe_events = self.active_interrupt_callback.is_none()
&& self.dispatcher.key_repeat.is_none()
&& self.dispatcher.pending_launch_app.is_none()
&& !self.dispatcher.system_task_has_periodic_work();
let event_stream_empty = can_observe_events
&& self
.dispatcher
.peek_toolbox_event(&self.bus, u16::MAX)
.is_none();
if !memory_unchanged
|| !cpu_unchanged
|| !tick_unchanged
|| !host_unchanged
|| !event_stream_empty
{
self.cancel_idle_cycle_detector();
return false;
}
let Some(cap) = tick_cap else {
self.cancel_idle_cycle_detector();
return false;
};
match self.advance_until_tick(sleep.wake_tick, Some(cap)) {
AdvanceResult::CapHit => {
self.park_proven_idle_cycle(sleep.trap_pc, sleep.wake_tick);
true
}
AdvanceResult::Advanced => {
self.cancel_idle_cycle_detector();
false
}
AdvanceResult::Interrupted | AdvanceResult::TooFar => {
self.cancel_idle_cycle_detector();
false
}
}
}
fn note_idle_cycle_trap_result(&mut self, opcode: u16) -> bool {
let null_event = matches!(
canonical_trap_number(opcode),
(true, 0x0170) | (true, 0x0171)
) && self.bus.read_word(self.cpu.core.a(7)) == 0;
if self.idle_cycle_probe.is_none() {
return null_event;
}
let quiescent = idle_cycle_trap_is_journal_complete(
opcode,
null_event,
!self.dispatcher.system_task_has_periodic_work(),
);
if !quiescent {
ws_note_cancel_trap(opcode);
self.cancel_idle_cycle_detector();
}
null_event
}
fn try_exact_idle_cycle_fastfwd(
&mut self,
trap_pc: u32,
wake_tick: u32,
tick_cap: Option<u32>,
) -> bool {
let tick = self.dispatcher.tick_count;
if self.idle_cycle_site_is_busy(trap_pc, tick) {
return false;
}
let cpu = CpuArchitecturalSnapshot::capture(&self.cpu.core);
if let Some(probe) = self.idle_cycle_probe.take() {
if self.bus.take_write_probe_overflow() {
if wait_stats_enabled() {
WS_PROBE_OVERFLOWS.fetch_add(1, AtomicOrdering::Relaxed);
}
self.mark_idle_cycle_site_busy(probe.trap_pc, probe.tick);
return false;
}
let same_site_tick = probe.trap_pc == trap_pc && probe.tick == tick;
if same_site_tick && probe.cpu == cpu {
let memory_unchanged = self.bus.finish_write_probe_unchanged();
if wait_stats_enabled() {
if memory_unchanged {
WS_EXACT_REPEATS.fetch_add(1, AtomicOrdering::Relaxed);
} else {
WS_FAIL_MEM.fetch_add(1, AtomicOrdering::Relaxed);
}
}
if memory_unchanged {
self.idle_cycle_last_seen = Some((trap_pc, tick));
if tick_cap.is_some_and(|cap| tick >= cap) {
self.park_proven_idle_cycle(trap_pc, wake_tick);
return true;
}
match self.advance_until_tick(wake_tick, tick_cap) {
AdvanceResult::CapHit => {
self.park_proven_idle_cycle(trap_pc, wake_tick);
return true;
}
AdvanceResult::Advanced => {
self.cancel_idle_cycle_detector();
return false;
}
AdvanceResult::Interrupted | AdvanceResult::TooFar => {
self.cancel_idle_cycle_detector();
return false;
}
}
}
self.begin_idle_cycle_probe(trap_pc, tick, cpu);
return false;
}
if same_site_tick && probe.arrivals + 1 < IDLE_CYCLE_MAX_PERIOD {
if wait_stats_enabled() {
WS_PERIOD_STEPS.fetch_add(1, AtomicOrdering::Relaxed);
}
self.idle_cycle_probe = Some(IdleCycleProbe {
arrivals: probe.arrivals + 1,
..probe
});
return false;
}
if wait_stats_enabled() {
if !same_site_tick && probe.tick != tick {
WS_FAIL_TICK.fetch_add(1, AtomicOrdering::Relaxed);
} else if same_site_tick {
let n = WS_FAIL_CPU.fetch_add(1, AtomicOrdering::Relaxed);
if n < 40 {
let a = &probe.cpu;
let b = &cpu;
let mut diffs: Vec<String> = Vec::new();
for i in 0..16 {
if a.dar[i] != b.dar[i] {
diffs.push(format!(
"{}{}:{:08X}->{:08X}",
if i < 8 { "D" } else { "A" },
i & 7,
a.dar[i],
b.dar[i]
));
}
}
if a.ppc != b.ppc {
diffs.push(format!("ppc:{:08X}->{:08X}", a.ppc, b.ppc));
}
if a.pc != b.pc {
diffs.push(format!("pc:{:08X}->{:08X}", a.pc, b.pc));
}
if a.ir != b.ir {
diffs.push(format!("ir:{:04X}->{:04X}", a.ir, b.ir));
}
if a.sr != b.sr {
diffs.push(format!("sr:{:04X}->{:04X}", a.sr, b.sr));
}
if a.prefetch_count != b.prefetch_count {
diffs.push(format!("pfn:{}->{}", a.prefetch_count, b.prefetch_count));
}
if a.prefetch != b.prefetch {
diffs.push(format!("pf:{:04X?}->{:04X?}", a.prefetch, b.prefetch));
}
if a.change_of_flow != b.change_of_flow {
diffs.push(format!("cof:{}->{}", a.change_of_flow, b.change_of_flow));
}
if a.stack_pointers != b.stack_pointers {
diffs.push("sp".to_owned());
}
if diffs.is_empty() {
diffs.push("other-field".to_owned());
}
eprintln!(
"[WAIT-STATS] cpu-diff sample {n} pc={trap_pc:08X}: {}",
diffs.join(" ")
);
}
}
}
self.bus.cancel_write_probe();
if same_site_tick {
self.begin_idle_cycle_probe(trap_pc, tick, cpu);
} else {
self.idle_cycle_last_seen = Some((trap_pc, tick));
}
return false;
}
if self.idle_cycle_last_seen == Some((trap_pc, tick)) {
self.begin_idle_cycle_probe(trap_pc, tick, cpu);
} else {
self.idle_cycle_last_seen = Some((trap_pc, tick));
}
false
}
fn try_exact_null_event_cycle_fastfwd(&mut self, trap_pc: u32, tick_cap: Option<u32>) -> bool {
if wait_stats_enabled() {
WS_ANCHOR_CALLS.fetch_add(1, AtomicOrdering::Relaxed);
}
if let Some(probe) = self.idle_cycle_probe.as_ref() {
if probe.trap_pc != trap_pc {
return false;
}
} else if let Some((anchor_pc, anchor_tick)) = self.idle_cycle_last_seen {
if anchor_pc != trap_pc && anchor_tick == self.dispatcher.tick_count {
return false;
}
}
let wake_tick = self.dispatcher.tick_count.wrapping_add(1);
self.try_exact_idle_cycle_fastfwd(trap_pc, wake_tick, tick_cap)
}
fn try_spin_template_e(
&mut self,
pc_after_trap: u32,
dn: usize,
w_move: u16,
tick_cap: Option<u32>,
) -> bool {
let w_ext = self.bus.read_word(pc_after_trap.wrapping_add(4));
if w_ext != (0x48C0 | dn as u16) {
return false;
}
let w_add = self.bus.read_word(pc_after_trap.wrapping_add(6));
if (w_add & 0xF1F8) != 0xD0A8 || ((w_add >> 9) & 7) as usize != dn {
return false;
}
let w_cmp = self.bus.read_word(pc_after_trap.wrapping_add(10));
if (w_cmp & 0xF1FF) != 0xB09F || ((w_cmp >> 9) & 7) as usize != dn {
return false;
}
let branch_pc = pc_after_trap.wrapping_add(12);
let w_branch = self.bus.read_word(branch_pc);
let condition = w_branch & 0xFF00;
if condition != 0x6E00 && condition != 0x6C00 {
return false;
}
let displacement = (w_branch & 0xFF) as i8 as i32;
if displacement == 0 {
return false;
}
let target = (branch_pc.wrapping_add(2) as i32).wrapping_add(displacement) as u32;
if target != pc_after_trap.wrapping_sub(4) || self.bus.read_word(target) != 0x594F {
return false;
}
let move_an = (w_move & 7) as usize;
let move_disp = self.bus.read_word(pc_after_trap.wrapping_add(2)) as i16 as i32;
let delay_addr = (self.cpu.core.a(move_an) as i32).wrapping_add(move_disp) as u32;
let delay = self.bus.read_word(delay_addr) as i16 as i32;
let add_an = (w_add & 7) as usize;
let add_disp = self.bus.read_word(pc_after_trap.wrapping_add(8)) as i16 as i32;
let base_addr = (self.cpu.core.a(add_an) as i32).wrapping_add(add_disp) as u32;
let deadline = self.bus.read_long(base_addr).wrapping_add(delay as u32);
let sp = self.cpu.core.a(7);
let captured_tick = self.bus.read_long(sp);
let deadline_signed = deadline as i32;
let captured_tick_signed = captured_tick as i32;
let still_waiting = match condition {
0x6E00 => deadline_signed > captured_tick_signed,
0x6C00 => deadline_signed >= captured_tick_signed,
_ => unreachable!(),
};
if !still_waiting {
return false;
}
let exit_tick = if condition == 0x6C00 {
if deadline_signed == i32::MAX {
return false;
}
deadline.wrapping_add(1)
} else {
deadline
};
match self.advance_until_tick(exit_tick, tick_cap) {
AdvanceResult::CapHit => {
self.bus.write_long(sp, self.dispatcher.tick_count);
true
}
AdvanceResult::Advanced => {
self.bus.write_long(sp, self.dispatcher.tick_count);
false
}
AdvanceResult::Interrupted | AdvanceResult::TooFar => false,
}
}
fn try_spin_template_d(
&mut self,
pc_after_trap: u32,
dn: usize,
tick_cap: Option<u32>,
) -> bool {
let w_branch = self.bus.read_word(pc_after_trap.wrapping_add(2));
let branch_condition = w_branch & 0xFF00;
if branch_condition != 0x6400 && branch_condition != 0x6700 {
return false;
}
let displacement = (w_branch & 0xFF) as i8 as i32;
if displacement == 0 {
return false;
}
let branch_pc = pc_after_trap.wrapping_add(2);
let target = (branch_pc.wrapping_add(2) as i32).wrapping_add(displacement) as u32;
if target != pc_after_trap.wrapping_sub(4) || self.bus.read_word(target) != 0x42A7 {
return false;
}
let dn_value = self.cpu.core.d(dn);
let captured_tick = self.bus.read_long(self.cpu.core.a(7));
if branch_condition == 0x6700 && captured_tick != dn_value {
return false;
}
let target_tick = dn_value.wrapping_add(1);
match self.advance_until_tick(target_tick, tick_cap) {
AdvanceResult::CapHit => {
let sp = self.cpu.core.a(7);
self.bus.write_long(sp, self.dispatcher.tick_count);
true
}
AdvanceResult::Advanced => {
let sp = self.cpu.core.a(7);
self.bus.write_long(sp, self.dispatcher.tick_count);
false
}
AdvanceResult::Interrupted | AdvanceResult::TooFar => false,
}
}
fn try_spin_template_a(
&mut self,
pc_after_trap: u32,
dn: usize,
w1: u16,
tick_cap: Option<u32>,
count: &mut usize,
) -> bool {
let imm_bits = ((w1 >> 9) & 7) as u32;
let imm = if imm_bits == 0 { 8 } else { imm_bits };
let w2 = self.bus.read_word(pc_after_trap.wrapping_add(4));
let w3 = self.bus.read_word(pc_after_trap.wrapping_add(6));
if (w2 & 0xF1F8) != 0xB080 || (w2 & 0x0007) as usize != dn {
return false;
}
let dm = ((w2 >> 9) & 7) as usize;
if (w3 & 0xFF00) != 0x6200 {
return false;
}
let disp8 = (w3 & 0xFF) as i8 as i32;
if disp8 == 0 {
return false;
}
let branch_src = pc_after_trap.wrapping_add(6);
let target = (branch_src.wrapping_add(2) as i32).wrapping_add(disp8) as u32;
if target != pc_after_trap.wrapping_sub(4) {
return false;
}
let dm_val = self.cpu.core.d(dm);
let target_tick = dm_val.wrapping_add(imm);
match self.advance_until_tick(target_tick, tick_cap) {
AdvanceResult::CapHit => return true,
AdvanceResult::Interrupted | AdvanceResult::TooFar => return false,
AdvanceResult::Advanced => {}
}
let final_tick = self.dispatcher.tick_count;
let sp = self.cpu.core.a(7);
self.cpu.core.set_a(7, sp.wrapping_add(4));
self.cpu.core.set_d(dn, final_tick.wrapping_sub(imm));
self.cpu.core.pc = pc_after_trap.wrapping_add(8);
*count += 4;
self.total_instructions = self.total_instructions.wrapping_add(4);
false
}
fn try_spin_template_b(
&mut self,
pc_after_trap: u32,
dn: usize,
w1: u16,
tick_cap: Option<u32>,
count: &mut usize,
) -> bool {
let an = (w1 & 7) as usize;
let d16 = self.bus.read_word(pc_after_trap.wrapping_add(4)) as i16 as i32;
let w_brk = self.bus.read_word(pc_after_trap.wrapping_add(6));
let branch_condition = w_brk & 0xFF00;
if branch_condition != 0x6300 && branch_condition != 0x6D00 && branch_condition != 0x6F00 {
return false;
}
let disp8 = (w_brk & 0xFF) as i8 as i32;
if disp8 == 0 {
return false;
}
let branch_src = pc_after_trap.wrapping_add(6);
let target = (branch_src.wrapping_add(2) as i32).wrapping_add(disp8) as u32;
let canonical_target = pc_after_trap.wrapping_sub(4);
if target != canonical_target
|| self.bus.read_word(canonical_target) != 0x594F
|| self.bus.read_word(canonical_target.wrapping_add(2)) != 0xA975
{
return false;
}
let an_val = self.cpu.core.a(an);
let mem_addr = (an_val as i32).wrapping_add(d16) as u32;
let mem_target = self.bus.read_long(mem_addr);
if branch_condition == 0x6D00 || branch_condition == 0x6F00 {
let captured_tick = self.bus.read_long(self.cpu.core.a(7));
let still_waiting = if branch_condition == 0x6D00 {
(captured_tick as i32) < (mem_target as i32)
} else {
(captured_tick as i32) <= (mem_target as i32)
};
if !still_waiting || (branch_condition == 0x6F00 && mem_target == i32::MAX as u32) {
return false;
}
}
let target_tick = if branch_condition == 0x6D00 {
mem_target
} else {
mem_target.wrapping_add(1)
};
match self.advance_until_tick(target_tick, tick_cap) {
AdvanceResult::CapHit => return true,
AdvanceResult::Interrupted | AdvanceResult::TooFar => return false,
AdvanceResult::Advanced => {}
}
let final_tick = self.dispatcher.tick_count;
let sp = self.cpu.core.a(7);
self.cpu.core.set_a(7, sp.wrapping_add(4));
self.cpu.core.set_d(dn, final_tick);
self.cpu.core.pc = pc_after_trap.wrapping_add(8);
*count += 3;
self.total_instructions = self.total_instructions.wrapping_add(3);
false
}
fn try_spin_template_c(
&mut self,
pc_after_trap: u32,
dn: usize,
tick_cap: Option<u32>,
count: &mut usize,
) -> bool {
let target_addr = self.bus.read_long(pc_after_trap.wrapping_add(4));
let w_brk = self.bus.read_word(pc_after_trap.wrapping_add(8));
if (w_brk & 0xFF00) != 0x6500 {
return false;
}
let disp8 = (w_brk & 0xFF) as i8 as i32;
if disp8 == 0 {
return false;
}
let branch_src = pc_after_trap.wrapping_add(8);
let target = (branch_src.wrapping_add(2) as i32).wrapping_add(disp8) as u32;
if target != pc_after_trap.wrapping_sub(4) {
return false;
}
let target_tick = self.bus.read_long(target_addr);
match self.advance_until_tick(target_tick, tick_cap) {
AdvanceResult::CapHit => return true,
AdvanceResult::Interrupted | AdvanceResult::TooFar => return false,
AdvanceResult::Advanced => {}
}
let final_tick = self.dispatcher.tick_count;
let sp = self.cpu.core.a(7);
self.cpu.core.set_a(7, sp.wrapping_add(4));
self.cpu.core.set_d(dn, final_tick);
self.cpu.core.pc = pc_after_trap.wrapping_add(10);
*count += 3;
self.total_instructions = self.total_instructions.wrapping_add(3);
false
}
fn advance_until_tick(&mut self, target_tick: u32, tick_cap: Option<u32>) -> AdvanceResult {
let current_tick = self.dispatcher.tick_count;
let ticks_to_advance = target_tick.wrapping_sub(current_tick);
if ticks_to_advance > SPIN_FASTFWD_MAX_TICKS {
return AdvanceResult::TooFar;
}
for _ in 0..ticks_to_advance {
if let Some(cap) = tick_cap {
if self.bus.read_long(0x016A) >= cap {
return AdvanceResult::CapHit;
}
}
self.advance_guest_tick();
if self.active_interrupt_callback.is_some() {
return AdvanceResult::Interrupted;
}
}
AdvanceResult::Advanced
}
fn run_steps_internal(
&mut self,
max_steps: usize,
tick_cap: Option<u32>,
audio_samples: usize,
yield_for_ui: bool,
sound_work_only: bool,
finish_frame: bool,
) -> (usize, bool) {
if self.ppc_app.is_some() {
if yield_for_ui {
return self.run_ppc_steps(max_steps, tick_cap, audio_samples, false, finish_frame);
}
let mut count = 0usize;
let mut running = !self.halted;
while count < max_steps && running {
let (steps, still_running) =
self.run_ppc_steps(max_steps - count, tick_cap, 0, true, false);
count = count.saturating_add(steps);
running = still_running;
if steps == 0 {
break;
}
}
if finish_frame {
self.sync_ppc_deferred_host_state();
if self.halted_by_exit_to_shell() {
self.dispatcher.redraw_chrome(&mut self.bus);
} else {
self.finish_host_frame(audio_samples, false);
}
}
return (count, running);
}
self.cancel_idle_cycle_observation();
let real_tick_cap = tick_cap;
let tick_cap = match self.frozen_ticks {
Some(frozen) => tick_cap.map(|_| frozen),
None => tick_cap,
};
self.dispatcher.instruction_count = self.total_instructions;
let mut count = 0;
let mut tick_cap_reached = false;
let mut sound_interrupt_dispatched = self
.active_interrupt_callback
.map(|callback| is_sound_interrupt_source(callback.source))
.unwrap_or(false);
while count < max_steps && !self.halted && !tick_cap_reached {
if sound_work_only
&& self.active_interrupt_callback.is_none()
&& (sound_interrupt_dispatched || !self.has_pending_sound_work())
{
break;
}
if !sound_work_only && self.active_interrupt_callback.is_none() {
self.fire_file_completion_callback();
self.fire_adb_callback();
}
if self.active_interrupt_callback.is_none() && !sound_interrupt_dispatched {
sound_interrupt_dispatched = self.fire_sound_callbacks();
if !sound_interrupt_dispatched {
sound_interrupt_dispatched = self.fire_sound_doubleback_callbacks();
}
if sound_work_only && !sound_interrupt_dispatched {
continue;
}
}
if sound_work_only && self.active_interrupt_callback.is_none() {
break;
}
if !sound_work_only && self.active_interrupt_callback.is_none() {
self.fire_timer_tasks_at(self.current_timer_subtick());
if self.active_interrupt_callback.is_some() {
continue;
}
}
if !sound_work_only {
if self.service_wait_sleep_ticks(tick_cap) {
break;
}
if self.service_delay_ticks(tick_cap) {
break;
}
}
if self.cpu.is_stopped() {
self.halted = true;
self.halted_pc = Some(self.cpu.read_reg(Register::PC));
self.halted_sp = Some(self.cpu.read_reg(Register::A7));
self.halted_d0 = Some(self.cpu.read_reg(Register::D0));
self.dump_trace();
return (count, false);
}
let pc = self.cpu.read_reg(Register::PC);
if trace_buffer_enabled() {
let opcode = self.bus.read_word(pc);
let a0 = self.cpu.read_reg(Register::A0);
let a6 = self.cpu.read_reg(Register::A6);
let a5 = self.cpu.read_reg(Register::A5);
let sp = self.cpu.read_reg(Register::A7);
if self.trace_buffer.len() >= 200 {
self.trace_buffer.pop_front();
}
self.trace_buffer.push_back((pc, opcode, a0, sp, a6, a5));
}
if let Some(active_interrupt_callback) = self.active_interrupt_callback {
let sp = self.cpu.read_reg(Register::A7);
if pc == active_interrupt_callback.resume_pc
&& sp == active_interrupt_callback.resume_sp
{
if trace_timer_enabled() {
eprintln!(
"[TIMER] resume {:?} pc=${:08X} sp=${:08X} restore_ccr=${:02X}",
active_interrupt_callback.source, pc, sp, active_interrupt_callback.ccr
);
}
if trace_sound_runner_enabled()
&& is_sound_interrupt_source(active_interrupt_callback.source)
{
eprintln!(
"[SOUND-CB] resume {:?} pc=${:08X} sp=${:08X} restore_ccr=${:02X}",
active_interrupt_callback.source, pc, sp, active_interrupt_callback.ccr
);
}
for (index, value) in
active_interrupt_callback.d_regs.iter().copied().enumerate()
{
self.cpu.write_reg(
match index {
0 => Register::D0,
1 => Register::D1,
2 => Register::D2,
3 => Register::D3,
4 => Register::D4,
5 => Register::D5,
6 => Register::D6,
_ => Register::D7,
},
value,
);
}
for (index, value) in
active_interrupt_callback.a_regs.iter().copied().enumerate()
{
self.cpu.write_reg(
match index {
0 => Register::A0,
1 => Register::A1,
2 => Register::A2,
3 => Register::A3,
4 => Register::A4,
5 => Register::A5,
6 => Register::A6,
_ => Register::A7,
},
value,
);
}
self.cpu
.core
.set_sr_noint_nosp(active_interrupt_callback.sr);
if matches!(
active_interrupt_callback.source,
ActiveInterruptCallbackSource::DialogDrawProc
) {
if let Some(snapshot) = self.dialog_draw_port_snapshot.take() {
self.dispatcher.restore_current_port_state(
&mut self.bus,
&mut self.cpu,
&snapshot,
);
}
}
if let Some((port, gdevice)) = active_interrupt_callback.restore_port {
self.dispatcher.set_current_port_state(
&mut self.bus,
&mut self.cpu,
port,
Some(gdevice),
);
}
let completed_dialog_draw_proc = matches!(
active_interrupt_callback.source,
ActiveInterruptCallbackSource::DialogDrawProc
);
let completed_modeless_dialog_draw_proc = completed_dialog_draw_proc
&& self.dispatcher.active_modeless_dialog_draw_proc.is_some();
let completed_modal_dialog_draw_proc =
completed_dialog_draw_proc && !completed_modeless_dialog_draw_proc;
if completed_dialog_draw_proc {
self.dispatcher
.finalize_dialog_draw_procs_if_idle(&mut self.bus);
}
self.active_interrupt_callback = None;
self.refill_foreground_budget_after_async_return();
if completed_modeless_dialog_draw_proc && self.fire_modeless_dialog_draw_proc()
{
continue;
}
if completed_modal_dialog_draw_proc
&& self
.dispatcher
.dialog_tracking
.as_ref()
.is_some_and(|tracking| !tracking.draw_procs_done)
&& self.fire_dialog_draw_procs()
{
continue;
}
if completed_modal_dialog_draw_proc {
self.deferred_tracking_refire_pc = None;
continue;
}
if let Some(refire_pc) = self.deferred_tracking_refire_pc.take() {
self.cpu.write_reg(Register::PC, refire_pc);
continue;
}
if sound_work_only {
break;
}
} else if trace_timer_enabled() {
eprintln!(
"[TIMER] pending {:?} pc=${:08X} sp=${:08X} waiting_for pc=${:08X} sp=${:08X}",
active_interrupt_callback.source,
pc,
sp,
active_interrupt_callback.resume_pc,
active_interrupt_callback.resume_sp
);
}
}
if !sound_work_only && self.try_resume_proven_idle_cycle(tick_cap) {
break;
}
if pc == 0 {
if trace_load_enabled() {
eprintln!(
"[RUN_STEPS] App reached PC=0 (clean exit via deep RTS chain) at count={}",
count
);
}
self.dump_trace();
self.halted = true;
self.halted_pc = Some(0);
self.halted_sp = Some(self.cpu.read_reg(Register::A7));
self.halted_d0 = Some(self.cpu.read_reg(Register::D0));
return (count, false);
}
let translated_pc = self.bus.translate_guest_address(pc);
if translated_pc >= self.bus.ram_size() || translated_pc < 0x60 {
let opcode = self.bus.read_word(pc);
let sp = self.cpu.read_reg(Register::A7);
eprintln!(
"[RUN_STEPS] Invalid PC ${:08X} at count={} sp=${:08X} op=${:04X}",
pc, count, sp, opcode
);
self.dump_invalid_pc_state();
if let Some(hint) = decode_fakeptr_pc(pc) {
eprintln!("[RUN_STEPS] {}", hint);
} else if let Some((entry_pc, hint)) = self.trace_find_fakeptr_entry() {
eprintln!(
"[RUN_STEPS] PC drifted ${:X} bytes from a fake-ptr entry at \
${:08X}. {}",
pc.wrapping_sub(entry_pc),
entry_pc,
hint
);
}
self.halted = true;
self.halted_pc = Some(pc);
self.halted_sp = Some(sp);
self.halted_d0 = Some(self.cpu.read_reg(Register::D0));
self.dump_trace();
return (count, false);
}
#[cfg(debug_assertions)]
if crate::memory::bus::watchpoint_armed() {
crate::memory::bus::increment_step();
crate::memory::bus::set_current_pc(pc);
crate::memory::bus::set_watch_registers(
self.cpu.read_reg(Register::A0),
self.cpu.read_reg(Register::A1),
self.cpu.read_reg(Register::A6),
self.cpu.read_reg(Register::A7),
);
}
if crate::memory::bus::fb_write_trace_active()
|| crate::memory::bus::mem_read_trace_active()
|| crate::memory::bus::mem_write_trace_active()
{
crate::memory::bus::set_current_pc(pc);
}
let trace_pc_range_hit = trace_pc_range_contains(pc, self.dispatcher.tick_count);
if trace_pc_range_hit {
let sp = self.cpu.read_reg(Register::A7);
let a6 = self.cpu.read_reg(Register::A6);
let stack0 = self.bus.read_long(sp);
let stack4 = self.bus.read_long(sp.wrapping_add(4));
let stack8 = self.bus.read_word(sp.wrapping_add(8));
let frame_ret = self.bus.read_long(a6.wrapping_add(4));
let frame_arg = self.bus.read_word(a6.wrapping_add(8));
eprintln!(
"[TRACE-PC-RANGE] pc=${:08X} op=${:04X} next=${:08X} ccr=${:02X} d0=${:08X} d1=${:08X} d2=${:08X} d3=${:08X} d4=${:08X} d5=${:08X} d6=${:08X} d7=${:08X} a0=${:08X} a1=${:08X} a2=${:08X} a3=${:08X} a4=${:08X} a5=${:08X} a6=${:08X} sp=${:08X} stack0=${:08X} stack4=${:08X} stack8=${:04X} frame_ret=${:08X} frame_arg=${:04X}",
pc,
self.bus.read_word(pc),
self.bus.read_long(pc.wrapping_add(2)),
self.cpu.core.get_ccr(),
self.cpu.read_reg(Register::D0),
self.cpu.read_reg(Register::D1),
self.cpu.read_reg(Register::D2),
self.cpu.read_reg(Register::D3),
self.cpu.read_reg(Register::D4),
self.cpu.read_reg(Register::D5),
self.cpu.read_reg(Register::D6),
self.cpu.read_reg(Register::D7),
self.cpu.read_reg(Register::A0),
self.cpu.read_reg(Register::A1),
self.cpu.read_reg(Register::A2),
self.cpu.read_reg(Register::A3),
self.cpu.read_reg(Register::A4),
self.cpu.read_reg(Register::A5),
a6,
sp,
stack0,
stack4,
stack8,
frame_ret,
frame_arg,
);
}
let charging = !sound_work_only
&& self.active_interrupt_callback.is_none()
&& self.frozen_ticks.is_none();
let mut precharged = false;
if charging && self.tick_budget <= 1 {
if self.charge_tick_budget(1, tick_cap) {
break;
}
precharged = true;
}
let batch_max = if per_instruction_diagnostics_active() {
1
} else {
let mut n = (max_steps - count).min(BATCH_CHUNK);
if charging && self.active_interrupt_callback.is_none() {
n = n.min((self.tick_budget - 1).max(1) as usize);
}
n as u32
};
let mut watch_buf = [0u32; 2];
let watch: &[u32] = if let Some(callback) = self.active_interrupt_callback {
watch_buf[1] = callback.resume_pc;
&watch_buf
} else {
&watch_buf[..1]
};
let batch = self.cpu.run_batch(&mut self.bus, batch_max, watch);
let executed = batch.instructions as usize
+ usize::from(matches!(
batch.exit,
BatchExit::AlineTrap { .. } | BatchExit::FlineTrap { .. }
));
if executed > 0 {
count += executed;
self.total_instructions = self.total_instructions.wrapping_add(executed as u64);
if !sound_work_only {
let charge_units = executed as i32 - i32::from(precharged);
if self.charge_tick_budget(charge_units, tick_cap) {
tick_cap_reached = true;
}
}
if batch.instructions > 0 {
if trace_opcode_counts_enabled() {
let opcode = self.cpu.core.ir as u16 as usize;
self.opcode_histogram[opcode] =
self.opcode_histogram[opcode].saturating_add(1);
}
if trace_hot_pc_enabled()
&& self.total_instructions.is_multiple_of(PC_SAMPLE_INTERVAL)
{
*self.pc_histogram.entry(pc).or_insert(0) += 1;
}
}
}
match batch.exit {
BatchExit::BudgetExhausted | BatchExit::WatchedPc { .. } => {
}
BatchExit::FlineTrap { .. } => {
}
BatchExit::Stopped => {
self.halted = true;
self.halted_pc = Some(self.cpu.read_reg(Register::PC));
self.halted_sp = Some(self.cpu.read_reg(Register::A7));
self.halted_d0 = Some(self.cpu.read_reg(Register::D0));
self.dump_trace();
return (count, false);
}
BatchExit::TrapInstruction { trap_num } => {
eprintln!("[CPU] TrapInstruction: #{}", trap_num);
self.halt_with_stop_diagnostics(count);
return (count, false);
}
BatchExit::Breakpoint { bp_num } => {
eprintln!("[CPU] Breakpoint: #{}", bp_num);
self.halt_with_stop_diagnostics(count);
return (count, false);
}
BatchExit::IllegalInstruction { opcode } => {
eprintln!(
"[CPU] IllegalInstruction: ${:04X} at PC=${:08X}",
opcode, self.cpu.core.pc
);
self.halt_with_stop_diagnostics(count);
return (count, false);
}
BatchExit::AlineTrap { opcode } => {
let pc = self.cpu.core.ppc;
if self.idle_cycle_probe.is_some()
&& !idle_cycle_trap_is_journal_complete(opcode, true, true)
{
ws_note_cancel_trap(opcode);
self.cancel_idle_cycle_detector();
}
if opcode == 0xA975 {
let sp = self.cpu.core.a(7);
let tick = self.dispatcher.tick_count;
self.bus.write_long(sp, tick);
self.dispatcher.trap_count += 1;
self.dispatcher.current_trap_word = opcode;
let idx = (opcode & 0xFFF) as usize;
self.dispatcher.trap_histogram[idx] =
self.dispatcher.trap_histogram[idx].saturating_add(1);
self.dispatcher.inline_skipped[idx] =
self.dispatcher.inline_skipped[idx].saturating_add(1);
if spin_wait_fastfwd_enabled_for(yield_for_ui, tick_cap) {
let post_trap_pc = pc.wrapping_add(2);
let hit_cap =
self.try_tickcount_spin_fastfwd(post_trap_pc, tick_cap, &mut count);
if !hit_cap && self.try_exact_null_event_cycle_fastfwd(pc, tick_cap) {
break;
}
if hit_cap {
break;
}
}
continue;
}
if opcode == 0xA991 {
let (
has_tracking,
filter_allows_noop,
flash_remaining_zero,
draw_procs_done,
rendered_pixels_final,
) = self
.dispatcher
.dialog_tracking
.as_ref()
.map(|t| {
let idle_dialog_mouse =
self.dispatcher.mouse_down_over_dialog_button()
|| self.dispatcher.mouse_down_over_dialog_plain_user_item()
|| (self.dispatcher.event_queue.is_empty()
&& self
.dispatcher
.pending_dialog_plain_user_item_mouse_down());
let paced_filter_idle = t.filter_proc != 0
&& t.last_filter_event.is_none()
&& !idle_dialog_mouse
&& self.dialog_filter_null_event_already_sent_this_tick(
t.dialog_ptr,
)
&& !self.dialog_filter_has_real_event_pending(t.dialog_ptr);
(
true,
t.filter_proc == 0 || paced_filter_idle,
t.flash_remaining == 0,
t.draw_procs_done,
t.rendered_pixels_final,
)
})
.unwrap_or((false, false, false, false, false));
let noop_refire = modaldialog_refire_is_noop(
yield_for_ui,
has_tracking,
filter_allows_noop,
flash_remaining_zero,
draw_procs_done,
rendered_pixels_final,
self.dispatcher.event_queue.is_empty(),
);
if noop_refire {
let idx = (opcode & 0xFFF) as usize;
self.dispatcher.trap_count += 1;
self.dispatcher.current_trap_word = opcode;
self.dispatcher.trap_histogram[idx] =
self.dispatcher.trap_histogram[idx].saturating_add(1);
self.dispatcher.inline_skipped[idx] =
self.dispatcher.inline_skipped[idx].saturating_add(1);
const BATCH: u32 = 64;
let mut budget = BATCH - 1;
while budget > 0 && count < max_steps && !tick_cap_reached {
tick_cap_reached = self.charge_tick_budget(1, tick_cap);
if tick_cap_reached {
break;
}
count += 1;
self.total_instructions = self.total_instructions.wrapping_add(1);
self.dispatcher.trap_count += 1;
self.dispatcher.trap_histogram[idx] =
self.dispatcher.trap_histogram[idx].saturating_add(1);
self.dispatcher.inline_skipped[idx] =
self.dispatcher.inline_skipped[idx].saturating_add(1);
budget -= 1;
}
self.cpu.write_reg(Register::PC, pc);
continue;
}
}
if opcode == 0xA8AD {
let sp = self.cpu.core.a(7);
let rect_ptr = self.bus.read_long(sp);
let pt_v = self.bus.read_word(sp + 4) as i16;
let pt_h = self.bus.read_word(sp + 6) as i16;
let top = self.bus.read_word(rect_ptr) as i16;
let left = self.bus.read_word(rect_ptr + 2) as i16;
let bottom = self.bus.read_word(rect_ptr + 4) as i16;
let right = self.bus.read_word(rect_ptr + 6) as i16;
let in_rect = pt_v >= top && pt_v < bottom && pt_h >= left && pt_h < right;
self.bus
.write_word(sp + 8, if in_rect { 0x0100 } else { 0 });
self.cpu.write_reg(Register::A7, sp + 8);
self.dispatcher.trap_count += 1;
self.dispatcher.current_trap_word = opcode;
if pc < 0x0080_0000
&& !self.dispatcher.is_menu_tracking()
&& !self.dispatcher.is_dialog_tracking()
&& !self.dispatcher.is_control_tracking()
&& !self.dispatcher.is_window_tracking()
&& !self.dispatcher.is_grow_window_tracking()
&& !self.dispatcher.is_region_tracking()
{
self.dispatcher.game_trap_count += 1;
}
let idx = (opcode & 0xFFF) as usize;
self.dispatcher.trap_histogram[idx] =
self.dispatcher.trap_histogram[idx].saturating_add(1);
self.dispatcher.inline_skipped[idx] =
self.dispatcher.inline_skipped[idx].saturating_add(1);
if self.service_pending_launch_application(true, false) {
if self.halted {
return (count, false);
}
continue;
}
continue;
}
if opcode == 0xA971 {
let sp = self.cpu.core.a(7);
let event_ptr = self.bus.read_long(sp);
let event_mask = self.bus.read_word(sp + 4);
if let Some(ev) = self.dispatcher.peek_toolbox_event(&self.bus, event_mask)
{
self.dispatcher.write_event_record(
&mut self.bus,
event_ptr,
ev.what,
ev.message,
ev.where_v,
ev.where_h,
ev.modifiers,
);
self.bus.write_word(sp + 6, 0xFFFF);
} else {
self.dispatcher.write_event_record(
&mut self.bus,
event_ptr,
0,
0,
self.dispatcher.mouse_pos.0,
self.dispatcher.mouse_pos.1,
self.dispatcher.current_event_modifiers(),
);
self.bus.write_word(sp + 6, 0);
}
self.cpu.write_reg(Register::A7, sp + 6);
self.dispatcher.trap_count += 1;
self.dispatcher.current_trap_word = opcode;
let idx = (opcode & 0xFFF) as usize;
self.dispatcher.trap_histogram[idx] =
self.dispatcher.trap_histogram[idx].saturating_add(1);
self.dispatcher.inline_skipped[idx] =
self.dispatcher.inline_skipped[idx].saturating_add(1);
let null_event = self.note_idle_cycle_trap_result(opcode);
if null_event
&& spin_wait_fastfwd_enabled_for(yield_for_ui, tick_cap)
&& self.try_exact_null_event_cycle_fastfwd(pc, tick_cap)
{
break;
}
continue;
}
self.dispatcher.yield_for_ui = yield_for_ui;
match self
.dispatcher
.dispatch(opcode, &mut self.cpu, &mut self.bus)
{
Ok(()) => {
let null_event = self.note_idle_cycle_trap_result(opcode);
let extra_tick_cost = hle_trap_extra_tick_cost(opcode)
.saturating_add(self.dispatcher.take_hle_tick_cost());
if extra_tick_cost > 0
&& self.charge_tick_budget(extra_tick_cost, tick_cap)
{
tick_cap_reached = true;
}
let is_tracking_refire = self.dispatcher.is_tracking_refire(opcode);
if is_tracking_refire {
if self.active_interrupt_callback.is_some() {
self.deferred_tracking_refire_pc = Some(pc);
continue;
}
if self.dispatcher.is_control_action_callback_pending() {
continue;
}
if yield_for_ui
&& self.frozen_ticks.is_none()
&& tracking_refire_should_freeze_ticks(opcode)
{
self.frozen_ticks = Some(self.bus.read_long(0x016A));
}
if yield_for_ui
&& self.frozen_ticks.is_some()
&& !tracking_refire_should_freeze_ticks(opcode)
{
self.unfreeze_ticks_to(real_tick_cap);
}
self.cpu.write_reg(Register::PC, pc);
let fired_menu_hook = self.fire_menu_hook_proc(opcode);
let uses_dialog_callbacks =
tracking_refire_uses_dialog_callbacks(opcode);
let fired_draw_proc = if fired_menu_hook || !uses_dialog_callbacks {
false
} else {
self.fire_dialog_draw_procs()
};
let mut fired_filter_proc = false;
if uses_dialog_callbacks && !fired_menu_hook && !fired_draw_proc {
if self.should_fire_dialog_filter_proc() {
self.fire_dialog_filter_proc();
fired_filter_proc = true;
}
}
if yield_for_ui
&& !fired_menu_hook
&& !fired_draw_proc
&& !fired_filter_proc
{
if tracking_refire_advances_gui_idle_tick(opcode)
&& !self.service_gui_retained_idle_tick(tick_cap)
{
continue;
}
if finish_frame {
self.finish_host_frame(
audio_samples,
sound_interrupt_dispatched,
);
}
return (count, true);
}
}
if self.frozen_ticks.is_some() {
self.unfreeze_ticks_to(real_tick_cap);
}
if !is_tracking_refire && self.fire_modeless_dialog_draw_proc() {
continue;
}
self.service_delay_ticks(tick_cap);
if self.service_pending_launch_application(
event_manager_yield_trap(opcode),
false,
) {
if self.halted {
return (count, false);
}
continue;
}
if (null_event || is_poll_anchor_trap(opcode))
&& spin_wait_fastfwd_enabled_for(yield_for_ui, tick_cap)
&& self.try_exact_null_event_cycle_fastfwd(pc, tick_cap)
{
break;
}
}
Err(Error::Halted) => {
if matches!(opcode, 0xA9F2 | 0xA9F4)
&& self.service_pending_launch_application(false, true)
{
if self.halted {
return (count, false);
}
continue;
}
let caller_str = self
.dispatcher
.current_trap_caller
.map(|c| format!(" caller=${:08X}", c))
.unwrap_or_default();
eprintln!(
"[RUN_STEPS] Application halted at count={} pc=${:08X} trap=${:04X}{}",
count,
pc,
opcode,
caller_str,
);
self.halted = true;
self.halted_pc = Some(pc);
self.halted_trap = Some(opcode);
self.halted_sp = Some(self.cpu.read_reg(Register::A7));
self.halted_d0 = Some(self.cpu.read_reg(Register::D0));
self.dump_trace();
return (count, false);
}
Err(Error::UnimplementedTrap(t)) => {
eprintln!("[RUN_STEPS] Unimplemented trap ${:04X} — skipping", t);
self.cpu.write_reg(Register::PC, pc + 2);
}
Err(e) => {
eprintln!(
"[RUN_STEPS] Error {:?} at PC=${:08X} trap=${:04X} count={}",
e, pc, opcode, count
);
self.halted = true;
self.halted_pc = Some(pc);
self.halted_sp = Some(self.cpu.read_reg(Register::A7));
self.halted_d0 = Some(self.cpu.read_reg(Register::D0));
self.dump_trace();
return (count, false);
}
}
}
}
self.dispatcher.instruction_count = self.total_instructions;
}
self.cancel_idle_cycle_observation();
if finish_frame {
self.finish_host_frame(audio_samples, sound_interrupt_dispatched || sound_work_only);
}
(count, !self.halted)
}
fn run_ppc_steps(
&mut self,
max_steps: usize,
tick_cap: Option<u32>,
audio_samples: usize,
coalesce_to_tick_cap: bool,
finish_frame: bool,
) -> (usize, bool) {
if max_steps == 0 || self.halted {
return (0, !self.halted);
}
let ppc_max_steps = if coalesce_to_tick_cap {
self.ppc_cycle_budget_for_tick_cap(max_steps, tick_cap)
} else {
max_steps
};
let cycles_to_next_tick = usize::try_from(self.tick_budget.max(1)).unwrap_or(1);
let ppc_max_steps = ppc_max_steps.min(cycles_to_next_tick);
if ppc_max_steps == 0 {
if finish_frame {
self.finish_host_frame(audio_samples, false);
}
return (0, true);
}
self.dispatcher.instruction_count = self.total_instructions;
let input = self.ppc_input_snapshot();
let Some(mut ppc_app) = self.ppc_app.take() else {
return (0, !self.halted);
};
ppc_app.toolbox_startup.host_menu_bar_hidden = self.dispatcher.menu_bar_hidden;
ppc_app.set_input_snapshot(input);
self.prepare_ppc_execution_clock(&mut ppc_app);
let cycles_per_tick = self.instructions_per_tick.max(1);
let remaining_cycles =
(i64::from(self.tick_budget).clamp(0, i64::from(cycles_per_tick))) as u32;
ppc_app.set_clock_cycle_timing(
cycles_per_tick,
cycles_per_tick.saturating_sub(remaining_cycles),
);
let ppc_start_tick = self.dispatcher.tick_count;
let ppc_start_time = self.bus.read_long(crate::memory::globals::addr::TIME);
let profile_ppc = ppc_profile_enabled();
let profile_total_start = profile_ppc.then(Instant::now);
let record_ppc_imports = self.dispatcher.is_trace_recording();
let trace_ppc_import_hist = ppc_import_hist_enabled();
let trace_ppc_imports = record_ppc_imports || trace_ppc_import_hist;
let trace_ppc_fetches = trace_ppc_fetch_counts_enabled();
let profile_run_start = profile_ppc.then(Instant::now);
let probe = match (trace_ppc_imports, trace_ppc_fetches) {
(true, true) => {
ppc_app.run_with_hle_import_trace_and_fetch_histogram(ppc_max_steps as u64)
}
(true, false) => ppc_app.run_with_hle_import_trace(ppc_max_steps as u64),
(false, true) => ppc_app.run_with_hle_import_fetch_histogram(ppc_max_steps as u64),
(false, false) => ppc_app.run_with_hle_imports(ppc_max_steps as u64),
};
let profile_run_us = elapsed_profile_micros(profile_run_start);
let cycles = ppc_run_result_cycles(probe.result);
let pc = ppc_app.cpu.pc;
let sp = ppc_app.cpu.gpr[1];
let gpr3 = ppc_app.cpu.gpr[3];
let unsupported_import_index = probe.unsupported_import_index;
let exited_via_ppc_exit_to_shell = ppc_halted_by_exit_to_shell(
&ppc_app.imports,
probe.result,
probe.last_import_index,
unsupported_import_index,
);
let still_running = matches!(probe.result, PpcRunResult::CycleLimit { .. })
&& unsupported_import_index.is_none();
self.total_instructions = self.total_instructions.saturating_add(cycles);
self.dispatcher.instruction_count = self.total_instructions;
let (vbl_probes, timer_probes) = if exited_via_ppc_exit_to_shell {
(Vec::new(), Vec::new())
} else {
self.advance_ticks_for_ppc_cycles(cycles, tick_cap);
let elapsed_ticks = self.dispatcher.tick_count.wrapping_sub(ppc_start_tick);
self.fire_ppc_tick_callbacks(
&mut ppc_app,
ppc_start_tick,
ppc_start_time,
elapsed_ticks,
u64::from(cycles_per_tick),
trace_ppc_imports,
trace_ppc_fetches,
)
};
if trace_vbl_enabled() {
for vbl_probe in &vbl_probes {
eprintln!(
"[VBL-PPC] task=${:08X} callback=${:08X} entry=${:08X} tick={} cycles={} end_r3=${:08X} result={:?}",
vbl_probe.invocation.task_ptr,
vbl_probe.invocation.callback,
vbl_probe.invocation.callback_entry,
vbl_probe.invocation.tick,
vbl_probe.invocation.cycles,
vbl_probe.invocation.end_r3,
vbl_probe.invocation.result,
);
}
}
if trace_timer_enabled() {
for timer_probe in &timer_probes {
eprintln!(
"[TIMER-PPC] task=${:08X} callback=${:08X} entry=${:08X} tick={} cycles={} end_r3=${:08X} result={:?}",
timer_probe.invocation.task_ptr,
timer_probe.invocation.callback,
timer_probe.invocation.callback_entry,
timer_probe.invocation.tick,
timer_probe.invocation.cycles,
timer_probe.invocation.end_r3,
timer_probe.invocation.result,
);
}
}
let vbl_cycles = vbl_probes
.iter()
.map(|probe| probe.invocation.cycles)
.sum::<u64>();
let timer_cycles = timer_probes
.iter()
.map(|probe| probe.invocation.cycles)
.sum::<u64>();
self.prepare_ppc_execution_clock(&mut ppc_app);
self.total_instructions = self
.total_instructions
.saturating_add(vbl_cycles)
.saturating_add(timer_cycles);
self.dispatcher.instruction_count = self.total_instructions;
if record_ppc_imports {
self.record_ppc_import_trace(&probe.import_trace);
for vbl_probe in &vbl_probes {
self.record_ppc_import_trace(&vbl_probe.import_trace);
}
for timer_probe in &timer_probes {
self.record_ppc_import_trace(&timer_probe.import_trace);
}
self.record_draw_sprocket_trace(&probe.draw_sprocket_trace);
self.record_input_sprocket_trace(&probe.input_sprocket_trace);
}
if trace_ppc_import_hist {
self.record_ppc_import_histogram(&probe.import_trace);
for vbl_probe in &vbl_probes {
self.record_ppc_import_histogram(&vbl_probe.import_trace);
}
for timer_probe in &timer_probes {
self.record_ppc_import_histogram(&timer_probe.import_trace);
}
}
if let Some(histogram) = probe.fetch_histogram.as_ref() {
self.ppc_fetch_histogram.merge_from(histogram);
}
for vbl_probe in &vbl_probes {
if let Some(histogram) = vbl_probe.fetch_histogram.as_ref() {
self.ppc_fetch_histogram.merge_from(histogram);
}
}
for timer_probe in &timer_probes {
if let Some(histogram) = timer_probe.fetch_histogram.as_ref() {
self.ppc_fetch_histogram.merge_from(histogram);
}
}
let q3_frame_start = self.q3_completed_frame_index;
let profile_render_start = profile_ppc.then(Instant::now);
let render_stats = if finish_frame {
self.render_ppc_completed_frames(&mut ppc_app, pc)
} else {
PpcQ3SoftwareRenderStats::default()
};
let profile_render_us = elapsed_profile_micros(profile_render_start);
let next_q3_frame_index = self.q3_completed_frame_index;
let profile_sync_start = profile_ppc.then(Instant::now);
if finish_frame {
self.sync_ppc_front_buffer_to_host(&mut ppc_app);
self.sync_ppc_vfs_to_dispatcher(&mut ppc_app);
}
self.sync_ppc_sound_to_dispatcher(&mut ppc_app);
sync_ppc_cursor_state(
&mut self.dispatcher,
ppc_app.quickdraw_cursor_level,
&ppc_app.toolbox_startup,
);
let profile_sync_us = elapsed_profile_micros(profile_sync_start);
if profile_ppc {
eprintln!(
"{}",
format_ppc_profile_sample(PpcProfileSample {
max_steps,
cycles,
total_instructions: self.total_instructions,
tick: self.dispatcher.tick_count,
pc,
lr: ppc_app.cpu.lr,
current_gworld: ppc_app.current_gworld,
screen_events: self.dispatcher.screen_event_count,
handled_import_count: probe.handled_import_count,
last_import_index: probe.last_import_index,
unsupported_import_index,
q3_frames: render_stats.frames,
q3_frame_start,
q3_frame_end: next_q3_frame_index,
q3_commands: render_stats.commands,
q3_vertices: render_stats.vertices,
q3_triangles: render_stats.triangles,
q3_pixels: render_stats.pixels,
dsp_front_gworld: ppc_app.draw_sprocket.front_buffer_gworld,
dsp_back_gworld: ppc_app.draw_sprocket.back_buffer_gworld,
run_us: profile_run_us,
render_us: profile_render_us,
sync_us: profile_sync_us,
total_us: elapsed_profile_micros(profile_total_start),
})
);
}
if ppc_unimpl_hist_enabled() && !still_running {
self.record_ppc_unimpl_histogram(
&ppc_app.imports,
probe.result,
unsupported_import_index,
);
}
if !still_running {
self.halted = true;
if exited_via_ppc_exit_to_shell {
self.halted_trap = Some(0xA9F4);
}
self.halted_pc = Some(pc);
self.halted_sp = Some(sp);
self.halted_d0 = Some(gpr3);
if trace_load_enabled() {
let word = ppc_app.memory.read_u32_be(pc);
let word_text = word
.map(|word| format!(" word=${word:08X}"))
.unwrap_or_default();
let lr = ppc_app.cpu.lr;
let gpr4 = ppc_app.cpu.gpr[4];
let gpr29 = ppc_app.cpu.gpr[29];
let gpr30 = ppc_app.cpu.gpr[30];
let gpr31 = ppc_app.cpu.gpr[31];
let node_text = if gpr30 != 0 {
let prev = ppc_app.memory.read_u32_be(gpr30).unwrap_or(0);
let next = ppc_app
.memory
.read_u32_be(gpr30.wrapping_add(4))
.unwrap_or(0);
format!(" r30[0..8]=${prev:08X}/${next:08X}")
} else {
String::new()
};
let global_text = {
let g29 = ppc_app.memory.read_u32_be(gpr29).unwrap_or(0);
let g31 = ppc_app.memory.read_u32_be(gpr31).unwrap_or(0);
let g31_8 = ppc_app
.memory
.read_u32_be(gpr31.wrapping_add(8))
.unwrap_or(0);
format!(" [r29]=${g29:08X} [r31]=${g31:08X} [r31+8]=${g31_8:08X}")
};
if let Some(index) = unsupported_import_index {
eprintln!(
"[PPC] halted at unsupported import #{} pc=${:08X} sp=${:08X} lr=${:08X} r3=${:08X} r4=${:08X} r29=${:08X} r30=${:08X} r31=${:08X}{}{}{}",
index, pc, sp, lr, gpr3, gpr4, gpr29, gpr30, gpr31, word_text, node_text, global_text
);
} else {
eprintln!(
"[PPC] halted after {:?} pc=${:08X} sp=${:08X} lr=${:08X} r3=${:08X} r4=${:08X} r29=${:08X} r30=${:08X} r31=${:08X}{}{}{}",
probe.result, pc, sp, lr, gpr3, gpr4, gpr29, gpr30, gpr31, word_text, node_text, global_text
);
}
}
}
self.ppc_app = Some(ppc_app);
if exited_via_ppc_exit_to_shell {
if finish_frame {
self.dispatcher.redraw_chrome(&mut self.bus);
}
} else {
self.queue_due_ppc_sound_completions();
if finish_frame {
self.finish_host_frame(audio_samples, false);
}
}
(
usize::try_from(
cycles
.saturating_add(vbl_cycles)
.saturating_add(timer_cycles),
)
.unwrap_or(usize::MAX),
still_running,
)
}
fn prepare_ppc_execution_clock(&self, ppc_app: &mut PpcLoadedApp) {
use crate::memory::globals::addr;
ppc_app.set_tick_count(self.bus.read_long(addr::TICKS));
}
#[allow(clippy::too_many_arguments)]
fn fire_ppc_tick_callbacks(
&self,
ppc_app: &mut PpcLoadedApp,
start_tick: u32,
start_time: u32,
elapsed_ticks: u32,
max_cycles: u64,
trace_imports: bool,
trace_fetches: bool,
) -> (
Vec<crate::loader::ppc::PpcVblCallbackProbe>,
Vec<crate::loader::ppc::PpcTimerCallbackProbe>,
) {
use crate::memory::globals::addr;
let mut vbl_probes = Vec::new();
let mut timer_probes = Vec::new();
let mut callback_time = start_time;
for tick_offset in 0..elapsed_ticks {
let callback_tick = start_tick.wrapping_add(tick_offset).wrapping_add(1);
if callback_tick.is_multiple_of(60) {
callback_time = callback_time.wrapping_add(1);
}
ppc_app.set_tick_count(callback_tick);
let _ = ppc_app.memory.write_u32_be(addr::TICKS, callback_tick);
let _ = ppc_app.memory.write_u32_be(addr::TIME, callback_time);
vbl_probes.extend(ppc_app.fire_vbl_tasks_for_ticks(
callback_tick.wrapping_sub(1),
1,
usize::MAX,
max_cycles,
trace_imports,
trace_fetches,
));
timer_probes.extend(ppc_app.fire_timer_tasks_for_ticks(
callback_tick.wrapping_sub(1),
1,
usize::MAX,
max_cycles,
trace_imports,
trace_fetches,
));
}
(vbl_probes, timer_probes)
}
fn render_ppc_completed_frames(
&mut self,
ppc_app: &mut PpcLoadedApp,
pc: u32,
) -> PpcQ3SoftwareRenderStats {
if self.external_q3_renderer_enabled {
if let Some(frame) = ppc_app.take_completed_q3_gpu_frame() {
self.pending_q3_gpu_frame = Some(frame);
self.q3_completed_frame_index = self.q3_completed_frame_index.saturating_add(1);
return PpcQ3SoftwareRenderStats::default();
}
}
let qd3d_dump_frame = qd3d_dump_frame_index();
let q3_frame_start = self.q3_completed_frame_index;
let mut next_q3_frame_index = self.q3_completed_frame_index;
let render_stats = if qd3d_dump_frame.is_some() {
ppc_app.render_completed_q3_frames_to_front_buffer_with_observer(|_, replay| {
if qd3d_dump_frame == Some(next_q3_frame_index) {
eprint!("{}", format_qd3d_frame_dump(next_q3_frame_index, replay));
if let Some(output_dir) = qd3d_dump_replay_dir() {
match write_qd3d_frame_replay_dump(output_dir, next_q3_frame_index, replay)
{
Ok(path) => eprintln!(
"[QD3D-FRAME] frame={} replay_json={}",
next_q3_frame_index,
path.display()
),
Err(err) => eprintln!(
"[QD3D-FRAME] frame={} replay_json_error={}",
next_q3_frame_index, err
),
}
}
}
next_q3_frame_index = next_q3_frame_index.saturating_add(1);
})
} else {
let stats = ppc_app.render_completed_q3_frames_to_front_buffer_fast();
next_q3_frame_index = next_q3_frame_index.saturating_add(stats.frames);
stats
};
self.q3_completed_frame_index = next_q3_frame_index;
self.record_q3_frame_trace(pc, q3_frame_start, next_q3_frame_index, render_stats);
render_stats
}
fn sync_ppc_deferred_host_state(&mut self) {
let Some(mut ppc_app) = self.ppc_app.take() else {
return;
};
let pc = ppc_app.cpu.pc;
self.render_ppc_completed_frames(&mut ppc_app, pc);
self.sync_ppc_front_buffer_to_host(&mut ppc_app);
self.sync_ppc_vfs_to_dispatcher(&mut ppc_app);
self.ppc_app = Some(ppc_app);
}
fn record_ppc_import_trace(&mut self, trace: &[PpcHleImportTraceEntry]) {
let runs = ppc_import_trace_runs(trace);
let mut index = 0usize;
while index < runs.len() {
if let Some((block_len, sequence_repeat_count)) =
ppc_import_trace_repeated_block_len(&runs, index)
{
let (entry, _) = runs[index];
let sequence = runs[index..index + block_len]
.iter()
.map(|(entry, repeat_count)| {
serde_json::json!({
"library": entry.library_name,
"symbol": entry.symbol_name,
"repeat_count": repeat_count,
})
})
.collect::<Vec<_>>();
let fields = TrapDispatcher::trace_field_map(&[
("import_index", entry.import_index.to_string()),
("library", entry.library_name.clone()),
("symbol", entry.symbol_name.clone()),
("lr", format!("{:08X}", entry.lr)),
("rtoc", format!("{:08X}", entry.rtoc)),
("sp", format!("{:08X}", entry.sp)),
(
"dispatcher_target",
format!("{:?}", entry.dispatcher_target),
),
("repeat_count", sequence_repeat_count.to_string()),
("sequence", serde_json::Value::Array(sequence).to_string()),
]);
if let Err(err) = self.dispatcher.record_trace_event(
&self.bus,
entry.pc,
"ppc_import",
fields,
false,
) {
eprintln!("[ORACLE] failed to record PPC import event: {:?}", err);
}
index += block_len
.saturating_mul(usize::try_from(sequence_repeat_count).unwrap_or(usize::MAX));
continue;
}
let (entry, repeat_count) = runs[index];
let fields = TrapDispatcher::trace_field_map(&[
("import_index", entry.import_index.to_string()),
("library", entry.library_name.clone()),
("symbol", entry.symbol_name.clone()),
("lr", format!("{:08X}", entry.lr)),
("rtoc", format!("{:08X}", entry.rtoc)),
("sp", format!("{:08X}", entry.sp)),
(
"dispatcher_target",
format!("{:?}", entry.dispatcher_target),
),
("repeat_count", repeat_count.to_string()),
]);
if let Err(err) =
self.dispatcher
.record_trace_event(&self.bus, entry.pc, "ppc_import", fields, false)
{
eprintln!("[ORACLE] failed to record PPC import event: {:?}", err);
}
index += 1;
}
}
fn record_input_sprocket_trace(&mut self, trace: &[PpcInputSprocketSimpleStateTraceEntry]) {
for entry in trace {
let fields = TrapDispatcher::trace_field_map(&[
("import_index", entry.import_index.to_string()),
("element", format!("{:08X}", entry.element)),
("state_ptr", format!("{:08X}", entry.state_ptr)),
("state", format!("{:08X}", entry.state)),
("kind", format!("{:08X}", entry.kind)),
("kind_name", entry.kind_name.clone()),
("fallback_state", format!("{:08X}", entry.fallback_state)),
("need_name", entry.need_name.clone()),
("action_binding", entry.action_binding.clone()),
("key_map", format_input_key_map(&entry.input.key_map)),
("mouse_button", entry.input.mouse_button.to_string()),
("mouse_v", entry.input.mouse_v.to_string()),
("mouse_h", entry.input.mouse_h.to_string()),
("initialized", entry.input_sprocket.initialized.to_string()),
("suspended", entry.input_sprocket.suspended.to_string()),
(
"keyboard_active",
entry.input_sprocket.keyboard_active.to_string(),
),
(
"mouse_active",
entry.input_sprocket.mouse_active.to_string(),
),
]);
if let Err(err) = self.dispatcher.record_trace_event(
&self.bus,
entry.pc,
"input_sprocket",
fields,
false,
) {
eprintln!(
"[ORACLE] failed to record InputSprocket simple-state event: {:?}",
err
);
}
}
}
fn record_draw_sprocket_trace(&mut self, trace: &[PpcDrawSprocketTraceEntry]) {
for entry in trace {
let fields = TrapDispatcher::trace_field_map(&[
("import_index", entry.import_index.to_string()),
("action", entry.action.clone()),
("result", entry.result.to_string()),
("result_hex", format!("{:04X}", entry.result as u16)),
("context", format_oracle_optional_hex32(entry.context)),
(
"requested_state",
entry
.requested_state
.clone()
.unwrap_or_else(|| "none".to_string()),
),
(
"requested_frequency",
format_oracle_optional_u32(entry.requested_frequency),
),
(
"requested_width",
format_oracle_optional_u32(entry.requested_width),
),
(
"requested_height",
format_oracle_optional_u32(entry.requested_height),
),
(
"requested_context_options",
format_oracle_optional_u32(entry.requested_context_options),
),
(
"requested_display_depth_mask",
format_oracle_optional_u32(entry.requested_display_depth_mask),
),
(
"requested_back_buffer_depth_mask",
format_oracle_optional_u32(entry.requested_back_buffer_depth_mask),
),
(
"requested_display_depth",
format_oracle_optional_u32(entry.requested_display_depth),
),
(
"requested_back_buffer_depth",
format_oracle_optional_u32(entry.requested_back_buffer_depth),
),
(
"requested_page_count",
format_oracle_optional_u32(entry.requested_page_count),
),
(
"can_user_select",
format_oracle_optional_bool(entry.can_user_select),
),
(
"fade_kind",
entry
.fade_kind
.clone()
.unwrap_or_else(|| "none".to_string()),
),
(
"fade_percent",
entry
.fade_percent
.map_or_else(|| "none".to_string(), |percent| percent.to_string()),
),
(
"fade_zero_red",
format_oracle_optional_u16(entry.fade_zero_red),
),
(
"fade_zero_green",
format_oracle_optional_u16(entry.fade_zero_green),
),
(
"fade_zero_blue",
format_oracle_optional_u16(entry.fade_zero_blue),
),
(
"reserved_context",
format_oracle_optional_hex32(entry.reserved_context),
),
(
"active_context",
format_oracle_optional_hex32(entry.active_context),
),
(
"has_reserved_context",
entry.reserved_context.is_some().to_string(),
),
(
"has_active_context",
entry.active_context.is_some().to_string(),
),
("context_state", entry.context_state.clone()),
(
"front_gworld",
format_oracle_hex32(entry.front_buffer_gworld),
),
("back_gworld", format_oracle_hex32(entry.back_buffer_gworld)),
(
"last_swap_context",
format_oracle_optional_hex32(entry.last_swap_context),
),
("swap_count", entry.swap_count.to_string()),
("fade_count", entry.fade_count.to_string()),
("frequency", entry.frequency.to_string()),
("width", entry.width.to_string()),
("height", entry.height.to_string()),
("context_options", entry.context_options.to_string()),
("display_depth_mask", entry.display_depth_mask.to_string()),
(
"back_buffer_depth_mask",
entry.back_buffer_depth_mask.to_string(),
),
("display_depth", entry.display_depth.to_string()),
("back_buffer_depth", entry.back_buffer_depth.to_string()),
("page_count", entry.page_count.to_string()),
]);
if let Err(err) = self.dispatcher.record_trace_event(
&self.bus,
entry.pc,
"draw_sprocket",
fields,
false,
) {
eprintln!("[ORACLE] failed to record DrawSprocket event: {:?}", err);
}
}
}
fn record_q3_frame_trace(
&mut self,
pc: u32,
frame_start: usize,
frame_end: usize,
stats: PpcQ3SoftwareRenderStats,
) {
if stats.frames == 0 || !self.dispatcher.is_trace_recording() {
return;
}
let fields = TrapDispatcher::trace_field_map(&[
("frame_start", frame_start.to_string()),
("frame_end", frame_end.to_string()),
("frames", stats.frames.to_string()),
("commands", stats.commands.to_string()),
("vertices", stats.vertices.to_string()),
("triangles", stats.triangles.to_string()),
("pixels", stats.pixels.to_string()),
(
"target_base",
format_oracle_optional_hex32(stats.target_base),
),
(
"target_row_bytes",
format_oracle_optional_u32(stats.target_row_bytes),
),
(
"target_width",
format_oracle_optional_u32(stats.target_width),
),
(
"target_height",
format_oracle_optional_u32(stats.target_height),
),
(
"target_depth",
format_oracle_optional_u32(stats.target_depth),
),
(
"target_source",
stats.target_source.unwrap_or("none").to_string(),
),
(
"target_draw_context",
format_oracle_optional_hex32(stats.target_draw_context),
),
(
"target_gworld",
format_oracle_optional_hex32(stats.target_gworld),
),
("target_consistent", stats.target_consistent.to_string()),
]);
if let Err(err) = self
.dispatcher
.record_trace_event(&self.bus, pc, "q3_frame", fields, false)
{
eprintln!("[ORACLE] failed to record QD3D frame event: {:?}", err);
}
}
fn ppc_input_snapshot(&self) -> PpcInputSnapshot {
PpcInputSnapshot {
key_map: self.dispatcher.key_map,
mouse_button: self.dispatcher.mouse_button,
mouse_v: self.dispatcher.mouse_pos.0,
mouse_h: self.dispatcher.mouse_pos.1,
}
}
fn canonical_mouse_position(&self, v: i16, h: i16) -> (i16, i16) {
let (offset_v, offset_h) = self.ppc_viewport_offset();
(v.saturating_sub(offset_v), h.saturating_sub(offset_h))
}
fn ppc_viewport_offset(&self) -> (i16, i16) {
let Some(front_buffer) = self
.ppc_app
.as_ref()
.and_then(PpcLoadedApp::presented_front_buffer)
else {
return (0, 0);
};
let (canvas_width, canvas_height) = Self::ppc_host_canvas_dimensions(front_buffer);
(
i16::try_from(canvas_height.saturating_sub(front_buffer.height) / 2).unwrap_or(0),
i16::try_from(canvas_width.saturating_sub(front_buffer.width) / 2).unwrap_or(0),
)
}
fn sync_ppc_sound_to_dispatcher(&mut self, ppc_app: &mut PpcLoadedApp) {
let decoded_buffer_commands = std::mem::take(&mut ppc_app.sound.decoded_buffer_commands);
for decoded in decoded_buffer_commands {
self.play_ppc_decoded_buffer_command(decoded);
}
self.sync_ppc_double_buffer_playbacks(ppc_app);
let start = self
.ppc_sound_synced_file_playback_count
.min(ppc_app.sound.file_playbacks.len());
for index in start..ppc_app.sound.file_playbacks.len() {
let playback = &ppc_app.sound.file_playbacks[index];
if !playback.active || playback.paused {
continue;
}
let Some(decoded) = ppc_app
.sound
.decoded_file_playbacks
.iter()
.find(|decoded| decoded.file_playback_index as usize == index)
.cloned()
else {
continue;
};
let channel = playback.channel;
if trace_sound_runner_enabled() {
let non_silent = decoded
.samples
.iter()
.filter(|sample| **sample != 0x80)
.count();
eprintln!(
"[PPC-SOUND] host file play index={index} chan=${channel:08X} rate=${:08X} samples={} non_silent={non_silent}",
decoded.sample_rate_fixed,
decoded.samples.len()
);
}
self.ensure_ppc_sound_playback_timing(ppc_app, index, &decoded);
let chan_index = self
.dispatcher
.sound_manager
.channels
.iter()
.position(|chan| chan.guest_ptr == channel)
.unwrap_or_else(|| {
self.dispatcher
.sound_manager
.channels
.push(crate::sound::SndChannel::new(channel, false));
self.dispatcher.sound_manager.channels.len() - 1
});
let chan = &mut self.dispatcher.sound_manager.channels[chan_index];
chan.quiet();
chan.play_buffer(
decoded.samples.clone(),
decoded.sample_rate_fixed,
crate::sound::PlaybackKind::File,
0,
);
self.ppc_sound_host_playbacks
.retain(|record| record.file_playback_index != index);
self.ppc_sound_host_playbacks.push(PpcHostSoundPlayback {
file_playback_index: index,
channel,
});
self.dispatcher.sound_manager.debug_file_play_count = self
.dispatcher
.sound_manager
.debug_file_play_count
.saturating_add(1);
}
self.ppc_sound_synced_file_playback_count = ppc_app.sound.file_playbacks.len();
self.adjust_ppc_sound_pause_timing(ppc_app);
let mut seen_channels = Vec::new();
for playback in ppc_app.sound.file_playbacks.iter().rev() {
if seen_channels.contains(&playback.channel) {
continue;
}
seen_channels.push(playback.channel);
if let Some(chan) = self
.dispatcher
.sound_manager
.find_channel_mut(playback.channel)
{
if !playback.active || playback.quiet_now {
chan.quiet();
} else {
chan.set_file_paused(playback.paused);
}
}
}
self.ppc_sound_host_playbacks.retain(|record| {
ppc_app
.sound
.file_playbacks
.get(record.file_playback_index)
.is_some_and(|playback| playback.active && !playback.quiet_now)
});
}
fn play_ppc_decoded_buffer_command(&mut self, decoded: PpcDecodedBufferCommandRecord) {
let channel = decoded.channel;
if trace_sound_runner_enabled() {
let non_silent = decoded
.samples
.iter()
.filter(|sample| **sample != 0x80)
.count();
eprintln!(
"[PPC-SOUND] host bufferCmd chan=${channel:08X} rate=${:08X} samples={} non_silent={}",
decoded.sample_rate_fixed,
decoded.samples.len(),
non_silent
);
}
let chan_index = self
.dispatcher
.sound_manager
.channels
.iter()
.position(|chan| chan.guest_ptr == channel)
.unwrap_or_else(|| {
self.dispatcher
.sound_manager
.channels
.push(crate::sound::SndChannel::new(channel, false));
self.dispatcher.sound_manager.channels.len() - 1
});
self.dispatcher.sound_manager.channels[chan_index].play_buffer(
decoded.samples,
decoded.sample_rate_fixed,
crate::sound::PlaybackKind::Buffer,
0,
);
self.dispatcher.sound_manager.debug_cmd_count = self
.dispatcher
.sound_manager
.debug_cmd_count
.saturating_add(1);
self.dispatcher.sound_manager.debug_buffer_cmd_count = self
.dispatcher
.sound_manager
.debug_buffer_cmd_count
.saturating_add(1);
if !self
.dispatcher
.sound_manager
.debug_cmd_codes_seen
.contains(&crate::sound::cmd::BUFFER)
{
self.dispatcher
.sound_manager
.debug_cmd_codes_seen
.push(crate::sound::cmd::BUFFER);
}
}
fn sync_ppc_double_buffer_playbacks(&mut self, ppc_app: &mut PpcLoadedApp) {
let stopped_channels = ppc_app
.sound
.double_buffer_playbacks
.iter()
.filter(|playback| !playback.active && playback.host_buffer_loaded)
.filter(|playback| {
!ppc_app
.sound
.double_buffer_playbacks
.iter()
.any(|other| other.active && other.channel == playback.channel)
})
.map(|playback| playback.channel)
.collect::<Vec<_>>();
for channel in stopped_channels {
if let Some(host_channel) = self.dispatcher.sound_manager.find_channel_mut(channel) {
host_channel.quiet();
}
}
for playback in &mut ppc_app.sound.double_buffer_playbacks {
if !playback.active {
playback.host_buffer_loaded = false;
}
}
for index in 0..ppc_app.sound.double_buffer_playbacks.len() {
let playback = ppc_app.sound.double_buffer_playbacks[index];
if !playback.active {
continue;
}
if !playback.host_initialized {
ppc_app.sound.double_buffer_playbacks[index].host_initialized = true;
self.dispatcher.sound_manager.debug_double_buffer_count = self
.dispatcher
.sound_manager
.debug_double_buffer_count
.saturating_add(1);
}
if playback.host_buffer_loaded {
continue;
}
let Some(decoded) = Self::decode_ppc_double_buffer(&mut ppc_app.memory, playback)
else {
if trace_sound_runner_enabled() {
eprintln!(
"[PPC-SOUND] unsupported double buffer chan=${:08X} header=${:08X} channels={} bits={} compression={} packet={}",
playback.channel,
playback.header,
playback.num_channels,
playback.sample_size,
playback.compression_id,
playback.packet_size
);
}
continue;
};
if decoded.flags & 0x01 == 0 || decoded.samples.is_empty() {
Self::queue_ppc_doubleback(
&mut ppc_app.sound,
index,
self.dispatcher.tick_count,
self.total_instructions,
);
continue;
}
self.play_ppc_decoded_double_buffer(playback, &decoded);
ppc_app.sound.double_buffer_playbacks[index].host_buffer_loaded = true;
}
}
fn decode_ppc_double_buffer(
memory: &mut PpcSectionMem,
playback: PpcSoundDoubleBufferPlaybackRecord,
) -> Option<PpcDecodedDoubleBuffer> {
const MAX_RETAINED_SAMPLE_BYTES: usize = 64 * 1024 * 1024;
if playback.compression_id != 0 {
return None;
}
let buffer_index = usize::from(playback.current_buffer_index & 1);
let buffer_ptr = playback.buffers[buffer_index];
if buffer_ptr == 0 {
return None;
}
let num_frames = usize::try_from(memory.read_u32_be(buffer_ptr)?).ok()?;
let flags = memory.read_u32_be(buffer_ptr.checked_add(4)?)?;
if flags & 0x01 == 0 || num_frames == 0 {
return Some(PpcDecodedDoubleBuffer {
buffer_ptr,
flags,
samples: Vec::new(),
});
}
let num_channels = usize::from(playback.num_channels);
let sample_size = usize::from(playback.sample_size);
let bytes_per_sample = match sample_size {
8 => 1usize,
16 => 2usize,
_ => return None,
};
let byte_count = num_frames
.checked_mul(num_channels)?
.checked_mul(bytes_per_sample)?;
if byte_count > MAX_RETAINED_SAMPLE_BYTES {
return None;
}
let mut raw = vec![0; byte_count];
memory.read_bytes_into(buffer_ptr.checked_add(16)?, &mut raw)?;
let samples = crate::trap::decode_interleaved_stereo_samples(
&raw,
num_frames,
num_channels,
sample_size,
)?;
Some(PpcDecodedDoubleBuffer {
buffer_ptr,
flags,
samples,
})
}
fn play_ppc_decoded_double_buffer(
&mut self,
playback: PpcSoundDoubleBufferPlaybackRecord,
decoded: &PpcDecodedDoubleBuffer,
) {
let channel_index = self
.dispatcher
.sound_manager
.channels
.iter()
.position(|channel| channel.guest_ptr == playback.channel)
.unwrap_or_else(|| {
self.dispatcher
.sound_manager
.channels
.push(crate::sound::SndChannel::new(playback.channel, false));
self.dispatcher.sound_manager.channels.len() - 1
});
let host_channel = &mut self.dispatcher.sound_manager.channels[channel_index];
let non_silent_frames = decoded
.samples
.iter()
.filter(|sample| sample.left != 0x80 || sample.right != 0x80)
.count();
host_channel.debug_double_buffer_loads =
host_channel.debug_double_buffer_loads.saturating_add(1);
host_channel.debug_double_buffer_frames_loaded = host_channel
.debug_double_buffer_frames_loaded
.saturating_add(decoded.samples.len() as u64);
host_channel.debug_double_buffer_non_silent_frames = host_channel
.debug_double_buffer_non_silent_frames
.saturating_add(non_silent_frames as u64);
if non_silent_frames > 0 {
host_channel.debug_double_buffer_non_silent_loads = host_channel
.debug_double_buffer_non_silent_loads
.saturating_add(1);
}
let capture_remaining = crate::sound::DEBUG_DOUBLE_BUFFER_CAPTURE_LIMIT
.saturating_sub(host_channel.debug_double_buffer_captured_samples.len());
host_channel.debug_double_buffer_captured_samples.extend(
decoded
.samples
.iter()
.take(capture_remaining)
.copied()
.map(crate::sound::StereoSample::downmix),
);
if trace_sound_runner_enabled() {
eprintln!(
"[PPC-SOUND] host double buffer chan=${:08X} buf=${:08X} index={} frames={} non_silent={} flags=${:08X}",
playback.channel,
decoded.buffer_ptr,
playback.current_buffer_index,
decoded.samples.len(),
non_silent_frames,
decoded.flags
);
}
host_channel.play_stereo_buffer(
decoded.samples.clone(),
playback.sample_rate_fixed,
crate::sound::PlaybackKind::Buffer,
0,
);
}
fn queue_ppc_doubleback(
sound: &mut crate::loader::ppc::PpcSoundState,
playback_index: usize,
tick: u32,
instruction_count: u64,
) {
let Some(playback) = sound.double_buffer_playbacks.get_mut(playback_index) else {
return;
};
let buffer_index = playback.current_buffer_index & 1;
let pending_bit = 1u8 << buffer_index;
let buffer_ptr = playback.buffers[usize::from(buffer_index)];
if playback.callback == 0
|| buffer_ptr == 0
|| playback.callback_pending_mask & pending_bit != 0
{
return;
}
playback.callback_pending_mask |= pending_bit;
sound.pending_doublebacks.push(PpcSoundDoubleBackRecord {
channel: playback.channel,
header: playback.header,
exhausted_buffer: buffer_ptr,
exhausted_buffer_index: u32::from(buffer_index),
callback: playback.callback,
tick,
instruction_count,
});
}
fn ensure_ppc_sound_playback_timing(
&mut self,
ppc_app: &mut PpcLoadedApp,
index: usize,
decoded: &PpcDecodedAiffPlaybackRecord,
) {
let Some(playback) = ppc_app.sound.file_playbacks.get_mut(index) else {
return;
};
if playback.timing_valid {
return;
}
let duration_ticks = ppc_decoded_playback_duration_ticks(decoded).unwrap_or(1);
let duration_instructions =
u64::from(duration_ticks).saturating_mul(u64::from(self.instructions_per_tick));
playback.timing_valid = true;
playback.start_tick = self.dispatcher.tick_count;
playback.start_instruction_count = self.total_instructions;
playback.due_tick = playback.start_tick.saturating_add(duration_ticks);
playback.due_instruction_count = playback
.start_instruction_count
.saturating_add(duration_instructions);
playback.pause_timing_valid = false;
playback.pause_started_tick = 0;
playback.pause_started_instruction_count = 0;
}
fn adjust_ppc_sound_pause_timing(&mut self, ppc_app: &mut PpcLoadedApp) {
let current_tick = self.dispatcher.tick_count;
let current_instructions = self.total_instructions;
for playback in &mut ppc_app.sound.file_playbacks {
if !playback.active || playback.quiet_now || !playback.timing_valid {
playback.pause_timing_valid = false;
continue;
}
if playback.paused {
if !playback.pause_timing_valid {
playback.pause_timing_valid = true;
playback.pause_started_tick = current_tick;
playback.pause_started_instruction_count = current_instructions;
}
continue;
}
if playback.pause_timing_valid {
let paused_ticks = current_tick.saturating_sub(playback.pause_started_tick);
let paused_instructions =
current_instructions.saturating_sub(playback.pause_started_instruction_count);
playback.due_tick = playback.due_tick.saturating_add(paused_ticks);
playback.due_instruction_count = playback
.due_instruction_count
.saturating_add(paused_instructions);
playback.pause_timing_valid = false;
playback.pause_started_tick = 0;
playback.pause_started_instruction_count = 0;
}
}
}
fn queue_due_ppc_sound_completions(&mut self) {
let current_tick = self.dispatcher.tick_count;
let current_instructions = self.total_instructions;
let completed = {
let Some(ppc_app) = self.ppc_app.as_mut() else {
return;
};
let mut completed = Vec::new();
for (index, playback) in ppc_app.sound.file_playbacks.iter_mut().enumerate() {
if !playback.active
|| playback.paused
|| playback.quiet_now
|| !playback.timing_valid
|| current_tick < playback.due_tick
|| current_instructions < playback.due_instruction_count
{
continue;
}
playback.active = false;
playback.paused = false;
let scheduled_tick = playback.due_tick;
let scheduled_instruction_count = playback.due_instruction_count;
if playback.async_play && playback.completion != 0 {
ppc_app
.sound
.pending_completions
.push(PpcSoundCompletionRecord {
file_playback_index: u32::try_from(index).unwrap_or(u32::MAX),
channel: playback.channel,
completion: playback.completion,
command: playback.completion_command,
tick: current_tick,
instruction_count: current_instructions,
scheduled_tick,
scheduled_instruction_count,
});
}
completed.push((index, playback.channel));
}
completed
};
if completed.is_empty() {
return;
}
for (_, channel) in &completed {
if let Some(chan) = self.dispatcher.sound_manager.find_channel_mut(*channel) {
chan.quiet();
}
}
self.ppc_sound_host_playbacks.retain(|record| {
!completed
.iter()
.any(|(index, _)| *index == record.file_playback_index)
});
self.fire_pending_ppc_sound_completions();
}
fn sync_ppc_sound_completions_from_dispatcher(&mut self) {
if self.ppc_app.is_none() || self.ppc_sound_host_playbacks.is_empty() {
self.fire_pending_ppc_sound_completions();
return;
}
let mut remaining = Vec::with_capacity(self.ppc_sound_host_playbacks.len());
let mut completed = Vec::new();
for record in self.ppc_sound_host_playbacks.drain(..) {
let still_active = self
.dispatcher
.sound_manager
.channels
.iter()
.find(|channel| channel.guest_ptr == record.channel)
.is_some_and(|channel| channel.has_active_playback());
if still_active {
remaining.push(record);
} else {
completed.push(record);
}
}
self.ppc_sound_host_playbacks = remaining;
if completed.is_empty() {
self.fire_pending_ppc_sound_completions();
return;
}
{
let Some(ppc_app) = self.ppc_app.as_mut() else {
return;
};
for record in completed {
let Some(playback) = ppc_app
.sound
.file_playbacks
.get_mut(record.file_playback_index)
else {
continue;
};
if !playback.active || playback.quiet_now {
continue;
}
playback.active = false;
playback.paused = false;
if playback.async_play && playback.completion != 0 {
let scheduled_tick = if playback.timing_valid {
playback.due_tick
} else {
self.dispatcher.tick_count
};
let scheduled_instruction_count = if playback.timing_valid {
playback.due_instruction_count
} else {
self.total_instructions
};
ppc_app
.sound
.pending_completions
.push(PpcSoundCompletionRecord {
file_playback_index: u32::try_from(record.file_playback_index)
.unwrap_or(u32::MAX),
channel: record.channel,
completion: playback.completion,
command: playback.completion_command,
tick: self.dispatcher.tick_count,
instruction_count: self.total_instructions,
scheduled_tick,
scheduled_instruction_count,
});
}
}
}
self.fire_pending_ppc_sound_completions();
}
fn service_ppc_double_buffer_playbacks(&mut self) {
let Some(mut ppc_app) = self.ppc_app.take() else {
return;
};
for index in 0..ppc_app.sound.double_buffer_playbacks.len() {
let playback = ppc_app.sound.double_buffer_playbacks[index];
if !playback.active || !playback.host_buffer_loaded {
continue;
}
let host_is_playing = self
.dispatcher
.sound_manager
.channels
.iter()
.find(|channel| channel.guest_ptr == playback.channel)
.is_some_and(crate::sound::SndChannel::is_playing);
if host_is_playing {
continue;
}
let buffer_index = playback.current_buffer_index & 1;
let buffer_ptr = playback.buffers[usize::from(buffer_index)];
let flags = ppc_app
.memory
.read_u32_be(buffer_ptr.wrapping_add(4))
.unwrap_or(0);
if buffer_ptr != 0 {
let _ = ppc_app
.memory
.write_u32_be(buffer_ptr.wrapping_add(4), flags & !0x01);
}
ppc_app.sound.double_buffer_playbacks[index].host_buffer_loaded = false;
if flags & 0x04 != 0 {
ppc_app.sound.double_buffer_playbacks[index].active = false;
continue;
}
Self::queue_ppc_doubleback(
&mut ppc_app.sound,
index,
self.dispatcher.tick_count,
self.total_instructions,
);
ppc_app.sound.double_buffer_playbacks[index].current_buffer_index = buffer_index ^ 1;
}
self.sync_ppc_double_buffer_playbacks(&mut ppc_app);
self.ppc_app = Some(ppc_app);
self.fire_pending_ppc_sound_doublebacks();
let Some(mut ppc_app) = self.ppc_app.take() else {
return;
};
self.sync_ppc_double_buffer_playbacks(&mut ppc_app);
self.ppc_app = Some(ppc_app);
}
fn fire_pending_ppc_sound_doublebacks(&mut self) {
let Some(ppc_app) = self.ppc_app.as_ref() else {
return;
};
if ppc_app.sound.pending_doublebacks.is_empty() {
return;
}
let record_ppc_imports = self.dispatcher.is_trace_recording();
let trace_ppc_import_hist = ppc_import_hist_enabled();
let trace_ppc_imports = record_ppc_imports || trace_ppc_import_hist;
let trace_ppc_fetches = trace_ppc_fetch_counts_enabled();
let Some(mut ppc_app) = self.ppc_app.take() else {
return;
};
self.prepare_ppc_execution_clock(&mut ppc_app);
let mut fired_count = 0usize;
while !ppc_app.sound.pending_doublebacks.is_empty() && fired_count < 16 {
let doubleback = ppc_app.sound.pending_doublebacks.remove(0);
let resume_pc = ppc_app.cpu.pc;
let probe = ppc_app.run_sound_doubleback_callback(
doubleback,
PPC_SOUND_COMPLETION_CALLBACK_MAX_CYCLES,
trace_ppc_imports,
trace_ppc_fetches,
);
let invocation = probe.invocation;
if trace_sound_runner_enabled() {
eprintln!(
"[PPC-SOUND] doubleback chan=${:08X} buffer={} callback=${:08X} entry=${:08X} resume_pc=${:08X} end_pc=${:08X} cycles={} result={:?}",
doubleback.channel,
doubleback.exhausted_buffer_index,
doubleback.callback,
invocation.callback_entry,
resume_pc,
invocation.end_pc,
invocation.cycles,
invocation.result
);
}
if record_ppc_imports {
self.record_ppc_import_trace(&probe.import_trace);
}
if trace_ppc_import_hist {
self.record_ppc_import_histogram(&probe.import_trace);
}
if let Some(histogram) = probe.fetch_histogram.as_ref() {
self.ppc_fetch_histogram.merge_from(histogram);
}
if ppc_unimpl_hist_enabled() {
self.record_ppc_unimpl_histogram(
&ppc_app.imports,
invocation.result,
invocation.unsupported_import_index,
);
}
self.total_instructions = self.total_instructions.saturating_add(invocation.cycles);
self.dispatcher.instruction_count = self.total_instructions;
self.advance_ticks_for_ppc_cycles(invocation.cycles, None);
self.prepare_ppc_execution_clock(&mut ppc_app);
let buffer_bit = 1u8 << (doubleback.exhausted_buffer_index.min(1) as u8);
if let Some(playback) =
ppc_app
.sound
.double_buffer_playbacks
.iter_mut()
.rev()
.find(|playback| {
playback.channel == doubleback.channel
&& playback.header == doubleback.header
})
{
playback.callback_pending_mask &= !buffer_bit;
}
let callback_failed = invocation.unsupported_import_index.is_some()
|| !matches!(invocation.result, PpcRunResult::Halted { .. });
if callback_failed {
self.halted = true;
self.halted_pc = Some(invocation.end_pc);
self.halted_sp = Some(invocation.end_sp);
self.halted_d0 = Some(invocation.end_r3);
if trace_load_enabled() {
eprintln!(
"[PPC] sound doubleback callback failed pc=${:08X} callback=${:08X} result={:?} unsupported_import={:?}",
invocation.end_pc,
invocation.completion,
invocation.result,
invocation.unsupported_import_index
);
}
}
ppc_app.sound.completion_invocations.push(invocation);
fired_count += 1;
if callback_failed {
break;
}
}
self.sync_ppc_vfs_to_dispatcher(&mut ppc_app);
self.sync_ppc_sound_to_dispatcher(&mut ppc_app);
self.ppc_app = Some(ppc_app);
}
fn fire_pending_ppc_sound_completions(&mut self) {
let Some(ppc_app) = self.ppc_app.as_ref() else {
return;
};
if ppc_app.sound.pending_completions.is_empty() {
return;
}
let record_ppc_imports = self.dispatcher.is_trace_recording();
let trace_ppc_import_hist = ppc_import_hist_enabled();
let trace_ppc_imports = record_ppc_imports || trace_ppc_import_hist;
let trace_ppc_fetches = trace_ppc_fetch_counts_enabled();
let Some(mut ppc_app) = self.ppc_app.take() else {
return;
};
self.prepare_ppc_execution_clock(&mut ppc_app);
let mut fired_count = 0usize;
while !ppc_app.sound.pending_completions.is_empty() && fired_count < 16 {
let completion = ppc_app.sound.pending_completions.remove(0);
let probe = ppc_app.run_sound_completion_callback(
completion,
PPC_SOUND_COMPLETION_CALLBACK_MAX_CYCLES,
trace_ppc_imports,
trace_ppc_fetches,
);
let invocation = probe.invocation;
if record_ppc_imports {
self.record_ppc_import_trace(&probe.import_trace);
}
if trace_ppc_import_hist {
self.record_ppc_import_histogram(&probe.import_trace);
}
if let Some(histogram) = probe.fetch_histogram.as_ref() {
self.ppc_fetch_histogram.merge_from(histogram);
}
if ppc_unimpl_hist_enabled() {
self.record_ppc_unimpl_histogram(
&ppc_app.imports,
invocation.result,
invocation.unsupported_import_index,
);
}
self.total_instructions = self.total_instructions.saturating_add(invocation.cycles);
self.dispatcher.instruction_count = self.total_instructions;
self.advance_ticks_for_ppc_cycles(invocation.cycles, None);
self.prepare_ppc_execution_clock(&mut ppc_app);
let callback_failed = invocation.unsupported_import_index.is_some()
|| !matches!(invocation.result, PpcRunResult::Halted { .. });
if callback_failed {
self.halted = true;
self.halted_pc = Some(invocation.end_pc);
self.halted_sp = Some(invocation.end_sp);
self.halted_d0 = Some(invocation.end_r3);
if trace_load_enabled() {
eprintln!(
"[PPC] sound completion callback failed pc=${:08X} completion=${:08X} result={:?} unsupported_import={:?}",
invocation.end_pc,
invocation.completion,
invocation.result,
invocation.unsupported_import_index
);
}
}
ppc_app.sound.completion_invocations.push(invocation);
fired_count += 1;
if callback_failed {
break;
}
}
self.sync_ppc_vfs_to_dispatcher(&mut ppc_app);
self.sync_ppc_sound_to_dispatcher(&mut ppc_app);
self.ppc_app = Some(ppc_app);
}
fn sync_ppc_vfs_to_dispatcher(&mut self, ppc_app: &mut PpcLoadedApp) {
for path in ppc_app.take_deleted_vfs_file_paths() {
let normalized = crate::trap::dispatch::TrapDispatcher::normalize_vfs_path(&path);
if normalized.is_empty() {
continue;
}
self.dispatcher.vfs.remove(&normalized);
self.dispatcher.vfs_rsrc.remove(&normalized);
self.dispatcher.locked_files.remove(&normalized);
self.dispatcher.remove_vfs_entry_metadata(&normalized);
if let Some(dir) = &self.dispatcher.output_dir {
let host_path = dir.join(&normalized);
let _ = std::fs::remove_file(&host_path);
if let Some((parent, file_name)) = ppc_resource_sidecar_parent(dir, &normalized) {
let _ = std::fs::remove_file(parent.join(".rsrc").join(file_name));
}
}
}
for directory in ppc_app.take_dirty_vfs_directories() {
let normalized =
crate::trap::dispatch::TrapDispatcher::normalize_vfs_path(&directory.path);
if normalized.is_empty() {
continue;
}
self.dispatcher.ensure_vfs_directory(&normalized);
self.dispatcher.set_vfs_entry_finfo(
&normalized,
directory.file_type,
directory.creator,
directory.finder_flags,
);
if let Some(dir) = &self.dispatcher.output_dir {
let _ = std::fs::create_dir_all(dir.join(&normalized));
}
}
for file in ppc_app.take_dirty_vfs_files() {
let normalized = crate::trap::dispatch::TrapDispatcher::normalize_vfs_path(&file.path);
if normalized.is_empty() {
continue;
}
self.dispatcher
.vfs
.insert(normalized.clone(), file.data.clone());
self.dispatcher.set_vfs_entry_finfo(
&normalized,
file.file_type,
file.creator,
file.finder_flags,
);
self.dispatcher.touch_vfs_entry(&normalized);
if let Some(dir) = &self.dispatcher.output_dir {
let host_path = dir.join(&normalized);
if let Some(parent) = host_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(host_path, &file.data);
}
}
for fork in ppc_app.take_dirty_vfs_resource_forks() {
let normalized = crate::trap::dispatch::TrapDispatcher::normalize_vfs_path(&fork.path);
if normalized.is_empty() {
continue;
}
self.dispatcher
.vfs_rsrc
.insert(normalized.clone(), fork.data.clone());
self.dispatcher.set_vfs_entry_finfo(
&normalized,
fork.file_type,
fork.creator,
fork.finder_flags,
);
self.dispatcher.touch_vfs_entry(&normalized);
if let Some(dir) = &self.dispatcher.output_dir {
if let Some((parent, file_name)) = ppc_resource_sidecar_parent(dir, &normalized) {
let rsrc_dir = parent.join(".rsrc");
if std::fs::create_dir_all(&rsrc_dir).is_ok() {
let _ = std::fs::write(rsrc_dir.join(file_name), &fork.data);
}
}
}
}
}
fn sync_ppc_front_buffer_to_host(&mut self, ppc_app: &mut PpcLoadedApp) {
let Some(primary_buffer) = ppc_app.presented_front_buffer() else {
return;
};
if matches!(primary_buffer.depth, 1 | 2 | 4 | 8) {
self.dispatcher.device_clut = ppc_app.screen_clut;
self.dispatcher.color_manager_clut = ppc_app.color_manager_clut;
self.dispatcher.device_gamma = ppc_app.device_gamma;
}
let (canvas_width, canvas_height) = Self::ppc_host_canvas_dimensions(primary_buffer);
let Some(canvas_row_bytes) = Self::ppc_host_row_bytes(canvas_width, primary_buffer.depth)
else {
return;
};
let canvas = PpcFrontBuffer {
base_addr: primary_buffer.base_addr,
row_bytes: canvas_row_bytes,
width: canvas_width,
height: canvas_height,
depth: primary_buffer.depth,
};
let Some(host_base) = self.ensure_ppc_host_screen_mode(canvas) else {
return;
};
if primary_buffer.depth <= 8
&& !self.sync_ppc_host_indexed_color_table(
primary_buffer.depth as u16,
&ppc_app.screen_clut,
)
{
return;
}
let matte_byte =
Self::ppc_indexed_matte_byte(primary_buffer.depth, &ppc_app.screen_clut).unwrap_or(0);
let Some(canvas_size) = canvas_row_bytes.checked_mul(canvas_height) else {
return;
};
self.bus
.write_bytes(host_base, &vec![matte_byte; canvas_size as usize]);
let primary_destination_x = canvas_width.saturating_sub(primary_buffer.width) / 2;
let primary_destination_y = canvas_height.saturating_sub(primary_buffer.height) / 2;
if !Self::copy_ppc_front_buffer_rows_to_host(
&mut self.bus,
ppc_app,
primary_buffer,
host_base,
canvas_row_bytes,
primary_destination_x,
primary_destination_y,
) {
return;
}
}
fn ppc_host_canvas_dimensions(front_buffer: PpcFrontBuffer) -> (u32, u32) {
if front_buffer.width >= 512 && front_buffer.height >= 342 {
(
front_buffer.width.max(u32::from(
crate::machine_profile::REFERENCE_MACHINE_PROFILE.screen_width,
)),
front_buffer.height.max(u32::from(
crate::machine_profile::REFERENCE_MACHINE_PROFILE.screen_height,
)),
)
} else {
(front_buffer.width, front_buffer.height)
}
}
fn ppc_host_row_bytes(width: u32, depth: u32) -> Option<u32> {
if !matches!(depth, 1 | 2 | 4 | 8 | 16) {
return None;
}
width.checked_mul(depth)?.checked_add(7)?.checked_div(8)
}
fn sync_ppc_host_indexed_color_table(&mut self, depth: u16, clut: &[[u16; 3]; 256]) -> bool {
if !matches!(depth, 1 | 2 | 4 | 8) {
return false;
}
let gdevice_handle = self.dispatcher.ensure_main_gdevice(&mut self.bus);
let gdevice = self.bus.read_long(gdevice_handle);
if gdevice == 0 {
return false;
}
let pixmap_handle = self.bus.read_long(gdevice + 22);
if pixmap_handle == 0 {
return false;
}
let pixmap = self.bus.read_long(pixmap_handle);
if pixmap == 0 {
return false;
}
let color_table_handle = self.bus.read_long(pixmap + 42);
if color_table_handle == 0 {
return false;
}
let entry_count = 1u32 << depth;
let color_table = self.dispatcher.ensure_color_table_capacity(
&mut self.bus,
color_table_handle,
entry_count,
);
if color_table == 0 {
return false;
}
self.bus.write_long(color_table, u32::from(depth));
self.bus.write_word(color_table + 4, 0x8000);
self.bus.write_word(color_table + 6, entry_count as u16 - 1);
for index in 0..entry_count {
let entry = color_table + 8 + index * 8;
let rgb = clut[index as usize];
self.bus.write_word(entry, 0);
self.bus.write_word(entry + 2, rgb[0]);
self.bus.write_word(entry + 4, rgb[1]);
self.bus.write_word(entry + 6, rgb[2]);
}
true
}
fn ppc_indexed_matte_byte(depth: u32, clut: &[[u16; 3]; 256]) -> Option<u8> {
if !matches!(depth, 1 | 2 | 4 | 8) {
return None;
}
let color_count = 1usize.checked_shl(depth)?;
let index = clut
.iter()
.take(color_count)
.enumerate()
.min_by_key(|(_, color)| {
u32::from(color[0]) + u32::from(color[1]) + u32::from(color[2])
})?
.0 as u8;
let field_mask = ((1u16 << depth) - 1) as u8;
let mut packed = 0u8;
for field in 0..(8 / depth) {
let shift = 8 - depth * (field + 1);
packed |= (index & field_mask) << shift;
}
Some(packed)
}
fn copy_ppc_packed_indexed_row(
source: &[u8],
depth: u32,
width: u32,
destination: &mut [u8],
destination_x: u32,
) -> bool {
if !matches!(depth, 1 | 2 | 4) {
return false;
}
let Some(visible_bits) = width.checked_mul(depth) else {
return false;
};
let Some(source_bytes) = visible_bits.checked_add(7).map(|bits| bits / 8) else {
return false;
};
let Some(destination_start_bit) = destination_x.checked_mul(depth) else {
return false;
};
let Some(destination_end_bit) = destination_start_bit.checked_add(visible_bits) else {
return false;
};
if usize::try_from(source_bytes)
.ok()
.is_none_or(|bytes| bytes > source.len())
|| usize::try_from(destination_end_bit.div_ceil(8))
.ok()
.is_none_or(|bytes| bytes > destination.len())
{
return false;
}
let mut first_scalar_pixel = 0u32;
if destination_start_bit & 7 == 0 {
let full_bytes = visible_bits / 8;
let Some(source_end) = usize::try_from(full_bytes).ok() else {
return false;
};
let Some(destination_start) = usize::try_from(destination_start_bit / 8).ok() else {
return false;
};
let Some(destination_end) = destination_start.checked_add(source_end) else {
return false;
};
destination[destination_start..destination_end].copy_from_slice(&source[..source_end]);
first_scalar_pixel = full_bytes * 8 / depth;
}
let field_mask = ((1u16 << depth) - 1) as u8;
for x in first_scalar_pixel..width {
let source_bit = x * depth;
let source_byte = source[(source_bit / 8) as usize];
let source_shift = 8 - depth - (source_bit & 7);
let pixel = (source_byte >> source_shift) & field_mask;
let destination_bit = destination_start_bit + x * depth;
let destination_byte = &mut destination[(destination_bit / 8) as usize];
let destination_shift = 8 - depth - (destination_bit & 7);
let mask = field_mask << destination_shift;
*destination_byte =
(*destination_byte & !mask) | ((pixel & field_mask) << destination_shift);
}
true
}
fn copy_ppc_front_buffer_rows_to_host(
bus: &mut MacMemoryBus,
ppc_app: &mut PpcLoadedApp,
front_buffer: PpcFrontBuffer,
host_base: u32,
host_row_bytes: u32,
destination_x: u32,
destination_y: u32,
) -> bool {
let Some(visible_row_bytes) = Self::ppc_front_buffer_visible_row_bytes(front_buffer) else {
return false;
};
let Some(destination_end_bits) = destination_x
.checked_add(front_buffer.width)
.and_then(|pixels| pixels.checked_mul(front_buffer.depth))
else {
return false;
};
let Some(host_row_bits) = host_row_bytes.checked_mul(8) else {
return false;
};
if destination_end_bits > host_row_bits {
return false;
}
let Ok(source_row_len) = usize::try_from(front_buffer.row_bytes) else {
return false;
};
let Ok(visible_row_len) = usize::try_from(visible_row_bytes) else {
return false;
};
let Ok(host_row_len) = usize::try_from(host_row_bytes) else {
return false;
};
let mut row = vec![0u8; source_row_len];
for y in 0..front_buffer.height {
if ppc_app
.read_front_buffer_row(front_buffer, y, &mut row)
.is_none()
{
return false;
}
Self::apply_ppc_draw_sprocket_gamma_fade(
&mut row[..visible_row_len],
front_buffer.depth,
ppc_app.draw_sprocket.last_fade_percent,
ppc_app.draw_sprocket.last_fade_zero_color,
);
let Some(destination_row) = destination_y.checked_add(y) else {
return false;
};
let Some(destination_row_addr) = destination_row
.checked_mul(host_row_bytes)
.and_then(|offset| host_base.checked_add(offset))
else {
return false;
};
if matches!(front_buffer.depth, 1 | 2 | 4) {
let mut packed_destination = bus.read_bytes(destination_row_addr, host_row_len);
if packed_destination.len() != host_row_len
|| !Self::copy_ppc_packed_indexed_row(
&row[..visible_row_len],
front_buffer.depth,
front_buffer.width,
&mut packed_destination,
destination_x,
)
{
return false;
}
bus.write_bytes(destination_row_addr, &packed_destination);
} else {
let bytes_per_pixel = front_buffer.depth / 8;
let Some(destination_x_bytes) = destination_x.checked_mul(bytes_per_pixel) else {
return false;
};
bus.write_bytes(
destination_row_addr + destination_x_bytes,
&row[..visible_row_len],
);
}
}
true
}
fn ppc_front_buffer_visible_row_bytes(front_buffer: PpcFrontBuffer) -> Option<u32> {
let visible = Self::ppc_host_row_bytes(front_buffer.width, front_buffer.depth)?;
(visible <= front_buffer.row_bytes).then_some(visible)
}
fn apply_ppc_draw_sprocket_gamma_fade(
row: &mut [u8],
depth: u32,
percent: Option<i32>,
zero_color: Option<PpcRgbColor>,
) {
let Some(percent) = percent else {
return;
};
if depth != 16 {
return;
}
let percent = percent.clamp(0, 100) as u32;
if percent == 100 && zero_color.is_none() {
return;
}
let zero = Self::ppc_rgb_color_to_rgb555_channels(zero_color.unwrap_or(PpcRgbColor {
red: 0,
green: 0,
blue: 0,
}));
for pixel in row.chunks_exact_mut(2) {
let value = u16::from_be_bytes([pixel[0], pixel[1]]);
let red = (value >> 10) & 0x1f;
let green = (value >> 5) & 0x1f;
let blue = value & 0x1f;
let faded = Self::rgb555_channels_to_pixel(
Self::ppc_draw_sprocket_fade_channel(red, zero.0, percent),
Self::ppc_draw_sprocket_fade_channel(green, zero.1, percent),
Self::ppc_draw_sprocket_fade_channel(blue, zero.2, percent),
);
pixel.copy_from_slice(&faded.to_be_bytes());
}
}
fn ppc_rgb_color_to_rgb555_channels(color: PpcRgbColor) -> (u16, u16, u16) {
(
Self::ppc_rgb_component_to_rgb555(color.red),
Self::ppc_rgb_component_to_rgb555(color.green),
Self::ppc_rgb_component_to_rgb555(color.blue),
)
}
fn ppc_rgb_component_to_rgb555(component: u16) -> u16 {
((u32::from(component) * 31 + 32_767) / 65_535) as u16
}
fn ppc_draw_sprocket_fade_channel(channel: u16, zero: u16, percent: u32) -> u16 {
let channel = i32::from(channel);
let zero = i32::from(zero);
let blended = zero + ((channel - zero) * percent as i32 + 50) / 100;
blended.clamp(0, 31) as u16
}
fn rgb555_channels_to_pixel(red: u16, green: u16, blue: u16) -> u16 {
((red & 0x1f) << 10) | ((green & 0x1f) << 5) | (blue & 0x1f)
}
fn ensure_ppc_host_screen_mode(&mut self, front_buffer: PpcFrontBuffer) -> Option<u32> {
let width = u16::try_from(front_buffer.width).ok()?;
let height = u16::try_from(front_buffer.height).ok()?;
let depth = u16::try_from(front_buffer.depth).ok()?;
let bytes_needed = front_buffer.row_bytes.checked_mul(front_buffer.height)?;
if bytes_needed == 0 || front_buffer.row_bytes > u32::from(u16::MAX) {
return None;
}
let (base, row_bytes, current_width, current_height, current_depth) =
self.dispatcher.screen_mode;
let base_valid = base != 0
&& base
.checked_add(bytes_needed)
.is_some_and(|end| end <= self.bus.ram_size());
let owned_base_valid = self.ppc_host_mirror_base != 0
&& self.ppc_host_mirror_capacity >= bytes_needed
&& self
.bus
.get_alloc_size(self.ppc_host_mirror_base)
.is_some_and(|size| size >= self.ppc_host_mirror_capacity)
&& self
.ppc_host_mirror_base
.checked_add(bytes_needed)
.is_some_and(|end| end <= self.bus.ram_size());
if base_valid
&& row_bytes == front_buffer.row_bytes
&& current_width == width
&& current_height == height
&& current_depth == depth
&& (self.ppc_host_mirror_base == 0
|| (owned_base_valid && base == self.ppc_host_mirror_base))
{
return Some(base);
}
let base = if owned_base_valid {
self.ppc_host_mirror_base
} else {
let new_base = self.bus.alloc(bytes_needed);
if new_base == 0 {
return None;
}
let old_base = self.ppc_host_mirror_base;
self.ppc_host_mirror_base = new_base;
self.ppc_host_mirror_capacity = bytes_needed;
if old_base != 0 {
self.bus.free(old_base);
}
new_base
};
self.dispatcher.screen_mode = (base, front_buffer.row_bytes, width, height, depth);
self.write_ppc_host_screen_lowmem(base, front_buffer.row_bytes, width, height, depth);
Some(base)
}
fn write_ppc_host_screen_lowmem(
&mut self,
base: u32,
row_bytes: u32,
width: u16,
height: u16,
depth: u16,
) {
use crate::memory::globals::addr;
self.bus.write_long(addr::SCRN_BASE, base);
self.bus.write_word(addr::SCREEN_ROW, row_bytes as u16);
self.bus.write_long(addr::SCREEN_BITS, base);
self.bus.write_word(addr::SCREEN_BITS + 4, row_bytes as u16);
self.bus.write_word(addr::SCREEN_BITS + 6, 0);
self.bus.write_word(addr::SCREEN_BITS + 8, 0);
self.bus.write_word(addr::SCREEN_BITS + 10, height);
self.bus.write_word(addr::SCREEN_BITS + 12, width);
let gdevice_handle = self.dispatcher.ensure_main_gdevice(&mut self.bus);
self.bus.write_long(0x08A4, gdevice_handle);
self.bus.write_long(0x0CC8, gdevice_handle);
self.bus.write_long(0x08A8, gdevice_handle);
let gdevice = self.bus.read_long(gdevice_handle);
if gdevice == 0 {
return;
}
let pixmap_handle = self.bus.read_long(gdevice + 22);
let pixmap = self.bus.read_long(pixmap_handle);
if pixmap == 0 {
return;
}
let current_ctab_handle = self.bus.read_long(pixmap + 42);
if current_ctab_handle != 0 {
self.ppc_host_indexed_ctab_handle = current_ctab_handle;
}
let ctab_handle = if depth <= 8 {
self.ppc_host_indexed_ctab_handle
} else {
0
};
self.bus.write_long(pixmap, base);
self.bus.write_word(pixmap + 4, (row_bytes as u16) | 0x8000);
self.bus.write_word(pixmap + 6, 0);
self.bus.write_word(pixmap + 8, 0);
self.bus.write_word(pixmap + 10, height);
self.bus.write_word(pixmap + 12, width);
let (pixel_type, component_count, component_size, gdevice_type) = if depth <= 8 {
(0, 1, depth, 0)
} else {
(16, 3, 5, 2)
};
self.bus.write_word(pixmap + 30, pixel_type);
self.bus.write_word(pixmap + 32, depth);
self.bus.write_word(pixmap + 34, component_count);
self.bus.write_word(pixmap + 36, component_size);
self.bus.write_long(pixmap + 42, ctab_handle);
self.bus.write_word(gdevice + 4, gdevice_type);
let mut gdevice_flags = self.bus.read_word(gdevice + 20);
if depth == 1 {
gdevice_flags &= !1;
} else {
gdevice_flags |= 1;
}
self.bus.write_word(gdevice + 20, gdevice_flags);
self.bus.write_word(gdevice + 34, 0);
self.bus.write_word(gdevice + 36, 0);
self.bus.write_word(gdevice + 38, height);
self.bus.write_word(gdevice + 40, width);
let depth_mode = crate::display::classic_depth_mode(depth)
.expect("validated PPC host depth has a classic Video.h mode");
self.bus.write_long(gdevice + 42, u32::from(depth_mode));
}
fn advance_ticks_for_ppc_cycles(&mut self, cycles: u64, tick_cap: Option<u32>) -> bool {
let mut remaining = cycles;
while remaining > 0 {
if self.tick_budget > 0 && remaining < self.tick_budget as u64 {
self.tick_budget -= remaining as i32;
return false;
}
let consumed = self.tick_budget.max(1) as u64;
remaining = remaining.saturating_sub(consumed);
self.tick_budget = 0;
if self.frozen_ticks.is_none() {
if let Some(cap) = tick_cap {
if self.bus.read_long(0x016A) >= cap {
return true;
}
}
self.advance_guest_tick();
}
self.tick_budget += self.instructions_per_tick as i32;
}
false
}
fn ppc_cycle_budget_for_tick_cap(&self, max_steps: usize, tick_cap: Option<u32>) -> usize {
let Some(cap) = tick_cap else {
return max_steps;
};
if self.frozen_ticks.is_some() || self.bus.read_long(0x016A) >= cap {
return 0;
}
let mut budget = u64::try_from(max_steps).unwrap_or(u64::MAX);
let mut ticks_remaining = u64::from(cap.wrapping_sub(self.bus.read_long(0x016A)));
if ticks_remaining == 0 {
return 0;
}
let current_tick_budget = self.tick_budget.max(1) as u64;
budget = budget.max(current_tick_budget);
ticks_remaining = ticks_remaining.saturating_sub(1);
budget = budget.saturating_add(
ticks_remaining.saturating_mul(u64::from(self.instructions_per_tick.max(1))),
);
usize::try_from(budget).unwrap_or(usize::MAX)
}
pub fn run_steps_with_audio(
&mut self,
max_steps: usize,
tick_override: Option<u32>,
audio_samples: usize,
) -> (usize, bool) {
self.run_steps_internal(
max_steps,
tick_override,
audio_samples,
tick_override.is_some(),
false,
true,
)
}
pub fn run_realtime_steps_with_audio(
&mut self,
max_steps: usize,
audio_samples: usize,
) -> (usize, bool) {
self.run_steps_internal(max_steps, None, audio_samples, true, false, true)
}
pub fn run_gui_slice_with_audio(
&mut self,
max_steps: usize,
deadline_tick: u32,
audio_samples: usize,
) -> (usize, bool) {
self.run_steps_internal(
max_steps,
Some(deadline_tick),
audio_samples,
true,
false,
true,
)
}
pub fn run_gui_cpu_slice(&mut self, max_steps: usize, deadline_tick: u32) -> (usize, bool) {
self.run_steps_internal(max_steps, Some(deadline_tick), 0, true, false, false)
}
pub fn run_pending_sound_work(&mut self, max_steps: usize) -> (usize, bool) {
self.run_steps_internal(max_steps, None, 0, true, true, true)
}
pub fn run_steps(&mut self, max_steps: usize, tick_override: Option<u32>) -> (usize, bool) {
let start_tick = self.guest_tick();
let result = self.run_steps_internal(max_steps, tick_override, 0, false, false, true);
self.advance_headless_callback_audio(self.guest_tick().wrapping_sub(start_tick));
result
}
fn halt_with_stop_diagnostics(&mut self, count: usize) {
let halted_pc = self.cpu.read_reg(Register::PC);
eprintln!(
"[RUN_STEPS] CPU stopped at count={} pc=${:08X} op=${:04X}",
count,
halted_pc,
self.bus.read_word(halted_pc)
);
if let Some(hint) = decode_fakeptr_pc(halted_pc) {
eprintln!("[RUN_STEPS] {}", hint);
} else if let Some((entry_pc, hint)) = self.trace_find_fakeptr_entry() {
eprintln!(
"[RUN_STEPS] PC drifted ${:X} bytes from a fake-ptr entry at \
${:08X}. {}",
halted_pc.wrapping_sub(entry_pc),
entry_pc,
hint
);
}
self.halted = true;
self.halted_pc = Some(halted_pc);
self.halted_sp = Some(self.cpu.read_reg(Register::A7));
self.halted_d0 = Some(self.cpu.read_reg(Register::D0));
self.dump_trace();
}
fn charge_tick_budget(&mut self, units: i32, tick_cap: Option<u32>) -> bool {
if units <= 0 {
return false;
}
if self.active_interrupt_callback.is_some() {
return false;
}
self.tick_budget -= units;
while self.tick_budget <= 0 && self.frozen_ticks.is_none() {
if let Some(cap) = tick_cap {
if self.bus.read_long(0x016A) >= cap {
return true;
}
}
self.advance_guest_tick();
self.tick_budget += self.instructions_per_tick as i32;
if self.active_interrupt_callback.is_some() {
return false;
}
if let Some(cap) = tick_cap {
if self.bus.read_long(0x016A) >= cap {
return true;
}
}
}
false
}
fn advance_guest_tick(&mut self) -> u32 {
self.cancel_idle_cycle_detector();
self.dispatcher
.refresh_menu_bar_policy_from_guest(&self.bus);
let new_tick = self.bus.read_long(0x016A).wrapping_add(1);
self.bus.write_long(0x016A, new_tick);
self.dispatcher.tick_count = new_tick;
self.dispatcher.post_auto_key_if_due(
self.bus
.read_word(crate::memory::globals::addr::SYS_EVT_MASK),
);
let has_pending_unmatched_down = self.dispatcher.has_unmatched_queued_mouse_down();
let pressed = self.dispatcher.mouse_button || has_pending_unmatched_down;
let mb_state: u8 = if pressed { 0x00 } else { 0x80 };
self.bus.write_byte(0x0172, mb_state);
if input_tick_trace_enabled() {
eprintln!(
"{}",
format_input_tick_trace(
new_tick,
self.total_instructions,
self.ppc_input_snapshot(),
self.dispatcher.event_queue.len(),
mb_state,
)
);
}
if new_tick.is_multiple_of(60) {
let time = self.bus.read_long(0x020C);
self.bus.write_long(0x020C, time.wrapping_add(1));
}
self.fire_cursor_task();
self.fire_vbl_tasks();
self.fire_timer_tasks(new_tick);
new_tick
}
fn deliver_pending_wait_next_event_if_available(&mut self) -> bool {
let Some(pending) = self.dispatcher.pending_wait_next_event_return.take() else {
if !self.dispatcher.event_queue.is_empty()
|| self.dispatcher.has_pending_native_menu_event()
{
self.dispatcher.pending_wait_sleep_ticks = 0;
return true;
}
return false;
};
if let (Some(resume_pc), Some(resume_sp)) = (pending.resume_pc, pending.resume_sp) {
let current_pc = self.cpu.read_reg(Register::PC);
let current_sp = self.cpu.read_reg(Register::A7);
if current_pc != resume_pc || current_sp != resume_sp {
self.dispatcher.pending_wait_sleep_ticks = 0;
if crate::trap::dispatch::trace_input_enabled() {
eprintln!(
"[INPUT] dropping stale WaitNextEvent sleep return parked pc=${:08X} sp=${:08X}; current pc=${:08X} sp=${:08X}",
resume_pc, resume_sp, current_pc, current_sp
);
}
return false;
}
}
let (mut what, mut message, mut where_v, mut where_h, mut modifiers, mut has_event) = self
.dispatcher
.dequeue_toolbox_event(&mut self.cpu, &mut self.bus, pending.event_mask);
if !has_event {
if let Some(event) = self.dispatcher.mouse_moved_event_for_region(
&self.bus,
pending.event_mask,
pending.mouse_rgn,
) {
what = event.what;
message = event.message;
where_v = event.where_v;
where_h = event.where_h;
modifiers = event.modifiers;
has_event = true;
self.dispatcher.debug_mouse_moved_event_count = self
.dispatcher
.debug_mouse_moved_event_count
.saturating_add(1);
}
}
if !has_event {
self.dispatcher.pending_wait_next_event_return = Some(pending);
return false;
}
self.dispatcher.write_event_record(
&mut self.bus,
pending.event_ptr,
what,
message,
where_v,
where_h,
modifiers,
);
self.bus.write_word(pending.result_ptr, 0xFFFF);
self.dispatcher.pending_wait_sleep_ticks = 0;
if crate::trap::dispatch::trace_input_enabled() {
eprintln!(
"[INPUT] WaitNextEvent sleep woke with input event what={} message=${:08X}",
what, message
);
}
true
}
fn wake_pending_wait_next_event_if_input_available(&mut self) -> bool {
if self.active_interrupt_callback.is_some() {
return false;
}
if self.dispatcher.pending_wait_sleep_ticks == 0
|| self.dispatcher.pending_wait_next_event_return.is_none()
{
return false;
}
self.deliver_pending_wait_next_event_if_available()
}
fn wake_foreground_after_input(&mut self) {
if self.tick_budget <= 0 {
self.refill_foreground_budget_after_async_return();
}
}
fn refill_foreground_budget_after_async_return(&mut self) {
if self.tick_budget <= 0 {
self.tick_budget = self.instructions_per_tick.max(2) as i32;
}
}
fn service_wait_sleep_ticks(&mut self, tick_cap: Option<u32>) -> bool {
if self.dispatcher.pending_wait_sleep_ticks == 0 || self.active_interrupt_callback.is_some()
{
return false;
}
if self.frozen_ticks.is_some() {
self.dispatcher.pending_wait_sleep_ticks = 0;
self.dispatcher.pending_wait_next_event_return = None;
return false;
}
if self.deliver_pending_wait_next_event_if_available() {
return false;
}
let retained_modal_dialog_snapshot = self
.dispatcher
.dialog_visible_snapshots
.keys()
.any(|dialog_ptr| self.dispatcher.dialog_modal_entered.contains(dialog_ptr));
let app_owned_visible_dialog_snapshot = self
.dispatcher
.dialog_visible_snapshots
.keys()
.any(|dialog_ptr| !self.dispatcher.dialog_modal_entered.contains(dialog_ptr));
if tick_cap.is_some()
&& (self.dispatcher.is_dialog_tracking() || retained_modal_dialog_snapshot)
{
self.dispatcher.pending_wait_sleep_ticks = 0;
self.dispatcher.pending_wait_next_event_return = None;
return false;
}
if let Some(cap) = tick_cap {
while self.dispatcher.pending_wait_sleep_ticks > 0 && self.bus.read_long(0x016A) < cap {
self.dispatcher.pending_wait_sleep_ticks -= 1;
self.advance_guest_tick();
self.tick_budget = self.instructions_per_tick as i32;
if self.active_interrupt_callback.is_some() {
break;
}
}
if self.dispatcher.pending_wait_sleep_ticks > 0 && self.bus.read_long(0x016A) >= cap {
return true;
}
self.dispatcher.pending_wait_next_event_return = None;
return false;
}
if let Some(cap) = self.wait_sleep_cap_in_headless {
let advance = if app_owned_visible_dialog_snapshot {
self.dispatcher.pending_wait_sleep_ticks
} else {
self.dispatcher.pending_wait_sleep_ticks.min(cap)
};
self.dispatcher.pending_wait_sleep_ticks = 0;
self.dispatcher.pending_wait_next_event_return = None;
for _ in 0..advance {
self.advance_guest_tick();
self.tick_budget = self.instructions_per_tick as i32;
if self.active_interrupt_callback.is_some() {
break;
}
}
return false;
}
while self.dispatcher.pending_wait_sleep_ticks > 0 {
self.dispatcher.pending_wait_sleep_ticks -= 1;
self.advance_guest_tick();
self.tick_budget = self.instructions_per_tick as i32;
if self.active_interrupt_callback.is_some() {
break;
}
}
self.dispatcher.pending_wait_next_event_return = None;
false
}
fn service_delay_ticks(&mut self, tick_cap: Option<u32>) -> bool {
if self.dispatcher.pending_delay_ticks == 0 || self.active_interrupt_callback.is_some() {
return false;
}
if self.frozen_ticks.is_some() {
self.dispatcher.pending_delay_ticks = 0;
return false;
}
while self.dispatcher.pending_delay_ticks > 0 {
if let Some(cap) = tick_cap {
if self.bus.read_long(0x016A) >= cap {
return true;
}
}
self.dispatcher.pending_delay_ticks -= 1;
self.advance_guest_tick();
self.tick_budget = self.instructions_per_tick as i32;
if self.active_interrupt_callback.is_some() {
break;
}
}
if self.dispatcher.pending_delay_ticks == 0 {
let final_ticks = self.bus.read_long(0x016A);
self.cpu.write_reg(Register::D0, final_ticks);
}
false
}
fn service_gui_retained_idle_tick(&mut self, tick_cap: Option<u32>) -> bool {
if self.active_interrupt_callback.is_some() || self.frozen_ticks.is_some() {
return true;
}
if let Some(cap) = tick_cap {
if self.bus.read_long(0x016A) >= cap {
return true;
}
}
self.advance_guest_tick();
self.tick_budget = self.instructions_per_tick as i32;
tick_cap
.map(|cap| self.bus.read_long(0x016A) >= cap)
.unwrap_or(true)
}
fn unfreeze_ticks_to(&mut self, target_tick: Option<u32>) {
self.frozen_ticks = None;
if let Some(target_tick) = target_tick {
self.bus.write_long(0x016A, target_tick);
self.dispatcher.tick_count = target_tick;
}
}
fn fire_cursor_task(&mut self) {
if self.active_interrupt_callback.is_some() {
return;
}
if (self.cpu.core.get_sr() & 0x0700) >= 0x0100 {
if trace_vbl_enabled() {
eprintln!(
"[VBL] defer JCrsrTask masked sr=${:04X} pc=${:08X}",
self.cpu.core.get_sr(),
self.cpu.read_reg(Register::PC)
);
}
return;
}
let callback_addr = self
.bus
.read_long(crate::memory::globals::addr::J_CRSR_TASK);
if callback_addr == 0 || callback_addr == CURSOR_TASK_NOOP_ADDR {
return;
}
if self.cursor_task_trampoline == 0 {
let tramp = self.bus.alloc(16);
self.bus.write_word(tramp, 0x48E7); self.bus.write_word(tramp + 2, 0xF0F0);
self.bus.write_word(tramp + 4, 0x4EB9); self.bus.write_word(tramp + 10, 0x4CDF); self.bus.write_word(tramp + 12, 0x0F0F);
self.bus.write_word(tramp + 14, 0x4E75); self.cursor_task_trampoline = tramp;
}
let tramp = self.cursor_task_trampoline;
self.bus.write_long(tramp + 6, callback_addr);
if trace_vbl_enabled() {
eprintln!(
"[VBL] fire JCrsrTask addr=${:08X} interrupted_pc=${:08X} interrupted_sp=${:08X}",
callback_addr,
self.cpu.read_reg(Register::PC),
self.cpu.read_reg(Register::A7)
);
}
self.inject_interrupt_callback(ActiveInterruptCallbackSource::CursorTask, tramp);
}
fn fire_vbl_tasks(&mut self) {
if self.active_interrupt_callback.is_some() {
return;
}
if (self.cpu.core.get_sr() & 0x0700) >= 0x0100 {
if trace_vbl_enabled() {
eprintln!(
"[VBL] defer masked sr=${:04X} pc=${:08X}",
self.cpu.core.get_sr(),
self.cpu.read_reg(Register::PC)
);
}
return;
}
let pending_before: Vec<bool> = self
.dispatcher
.vbl_tasks
.iter()
.map(|task| task.pending)
.collect();
for task in &mut self.dispatcher.vbl_tasks {
let count = self.bus.read_word(task.task_ptr + 10) as i16;
if count <= 0 {
continue;
}
let new_count = count - 1;
self.bus.write_word(task.task_ptr + 10, new_count as u16);
if new_count == 0 {
task.pending = true;
}
}
let due_index = pending_before
.iter()
.position(|pending| *pending)
.or_else(|| {
self.dispatcher
.vbl_tasks
.iter()
.position(|task| task.pending)
});
let due_task = due_index.map(|index| {
let task = &mut self.dispatcher.vbl_tasks[index];
task.pending = false;
task.task_ptr
});
let Some(task_ptr) = due_task else {
return;
};
let callback_addr = self.bus.read_long(task_ptr + 6);
if callback_addr == 0 {
return;
}
if self.vbl_trampoline == 0 {
let tramp = self.bus.alloc_synthetic(22);
self.bus.write_word(tramp, 0x48E7); self.bus.write_word(tramp + 2, 0xF0F0);
self.bus.write_word(tramp + 4, 0x207C); self.bus.write_word(tramp + 10, 0x4EB9); self.bus.write_word(tramp + 16, 0x4CDF); self.bus.write_word(tramp + 18, 0x0F0F);
self.bus.write_word(tramp + 20, 0x4E75); self.vbl_trampoline = tramp;
}
let tramp = self.vbl_trampoline;
self.bus.write_long(tramp + 6, task_ptr);
self.bus.write_long(tramp + 12, callback_addr);
let current_pc = self.cpu.read_reg(Register::PC);
let sp = self.cpu.read_reg(Register::A7);
let d_regs = [
self.cpu.read_reg(Register::D0),
self.cpu.read_reg(Register::D1),
self.cpu.read_reg(Register::D2),
self.cpu.read_reg(Register::D3),
self.cpu.read_reg(Register::D4),
self.cpu.read_reg(Register::D5),
self.cpu.read_reg(Register::D6),
self.cpu.read_reg(Register::D7),
];
let a_regs = [
self.cpu.read_reg(Register::A0),
self.cpu.read_reg(Register::A1),
self.cpu.read_reg(Register::A2),
self.cpu.read_reg(Register::A3),
self.cpu.read_reg(Register::A4),
self.cpu.read_reg(Register::A5),
self.cpu.read_reg(Register::A6),
sp,
];
let ccr = self.cpu.core.get_ccr();
let sr = self.cpu.core.get_sr();
let new_sp = sp.wrapping_sub(4);
self.bus.write_long(new_sp, current_pc);
self.cpu.write_reg(Register::A7, new_sp);
let source = ActiveInterruptCallbackSource::Vbl;
self.active_interrupt_callback = Some(ActiveInterruptCallback {
source,
resume_pc: current_pc,
resume_sp: sp,
d_regs,
a_regs,
sr,
ccr,
restore_port: None,
});
self.cpu
.core
.set_sr_noint_nosp(interrupt_callback_sr(source, sr));
self.cpu.write_reg(Register::PC, tramp);
if trace_vbl_enabled() {
eprintln!(
"[VBL] fire task=${:08X} addr=${:08X} interrupted_pc=${:08X} interrupted_sp=${:08X} count={}",
task_ptr,
callback_addr,
current_pc,
sp,
self.bus.read_word(task_ptr + 10) as i16
);
}
}
fn fire_timer_tasks(&mut self, current_tick: u32) {
self.fire_timer_tasks_at(current_tick as u64 * 1_000_000);
}
fn current_timer_subtick(&self) -> u64 {
const SUBTICKS_PER_TICK: u64 = 1_000_000;
let tick_base = self.guest_tick() as u64 * SUBTICKS_PER_TICK;
if self.tick_budget <= 0 {
return tick_base;
}
let instructions_per_tick = self.instructions_per_tick.max(1) as i64;
let remaining = (self.tick_budget as i64).clamp(0, instructions_per_tick);
let elapsed = instructions_per_tick - remaining;
tick_base + (elapsed as u64 * SUBTICKS_PER_TICK) / instructions_per_tick as u64
}
fn fire_timer_tasks_at(&mut self, current_subtick: u64) {
if self.active_interrupt_callback.is_some() {
return;
}
const SUBTICKS_PER_TICK: u64 = 1_000_000;
let current_tick = (current_subtick / SUBTICKS_PER_TICK) as u32;
if let Some(task) = self
.dispatcher
.timer_tasks
.iter_mut()
.filter(|task| task.active && current_subtick >= task.fire_at_subtick)
.min_by_key(|task| task.fire_at_subtick)
{
let task_ptr = task.task_ptr;
let tm_addr = task.tm_addr;
self.dispatcher.timer_current_subtick = current_subtick;
task.active = false;
task.last_fired_tick = Some(current_tick);
let q_type = self.bus.read_word(task_ptr + 4);
self.bus.write_word(task_ptr + 4, q_type & 0x7FFF);
if tm_addr == 0 {
return;
}
if self.timer_trampoline == 0 {
let tramp = self.bus.alloc(24); self.bus.write_word(tramp, 0x48E7); self.bus.write_word(tramp + 2, 0xF0F0); self.bus.write_word(tramp + 4, 0x227C); self.bus.write_word(tramp + 10, 0x4EB9); self.bus.write_word(tramp + 16, 0x4CDF); self.bus.write_word(tramp + 18, 0x0F0F); self.bus.write_word(tramp + 20, 0x4E75); self.timer_trampoline = tramp;
}
let tramp = self.timer_trampoline;
self.bus.write_long(tramp + 6, task_ptr);
self.bus.write_long(tramp + 12, tm_addr);
let current_pc = self.cpu.read_reg(Register::PC);
let sp = self.cpu.read_reg(Register::A7);
let d_regs = [
self.cpu.read_reg(Register::D0),
self.cpu.read_reg(Register::D1),
self.cpu.read_reg(Register::D2),
self.cpu.read_reg(Register::D3),
self.cpu.read_reg(Register::D4),
self.cpu.read_reg(Register::D5),
self.cpu.read_reg(Register::D6),
self.cpu.read_reg(Register::D7),
];
let a_regs = [
self.cpu.read_reg(Register::A0),
self.cpu.read_reg(Register::A1),
self.cpu.read_reg(Register::A2),
self.cpu.read_reg(Register::A3),
self.cpu.read_reg(Register::A4),
self.cpu.read_reg(Register::A5),
self.cpu.read_reg(Register::A6),
sp,
];
let ccr = self.cpu.core.get_ccr();
let sr = self.cpu.core.get_sr();
let new_sp = sp.wrapping_sub(4);
self.bus.write_long(new_sp, current_pc);
self.cpu.write_reg(Register::A7, new_sp);
self.active_interrupt_callback = Some(ActiveInterruptCallback {
source: ActiveInterruptCallbackSource::Timer,
resume_pc: current_pc,
resume_sp: sp,
d_regs,
a_regs,
sr,
ccr,
restore_port: None,
});
if trace_timer_enabled() {
eprintln!(
"[TIMER] fire task=${:08X} tm_addr=${:08X} interrupted_pc=${:08X} interrupted_sp=${:08X} ccr=${:02X}",
task_ptr, tm_addr, current_pc, sp, ccr
);
}
self.cpu.write_reg(Register::PC, tramp);
} else {
self.dispatcher.timer_current_subtick = current_subtick;
}
}
fn try_load_pending_double_buffers(&mut self) {
if self.active_interrupt_callback.is_some() {
return;
}
let queued_doublebacks = self
.dispatcher
.sound_manager
.pending_callbacks
.iter()
.map(|cb| (cb.chan_ptr, cb.exhausted_buffer_index))
.collect::<Vec<_>>();
for chan in &mut self.dispatcher.sound_manager.channels {
if chan.is_playing() {
continue; }
let (header_ptr, buf_idx, sample_rate, num_channels, sample_size) =
match chan.double_buffer {
Some(ref db) if !db.last_buffer_seen => (
db.header_ptr,
db.current_buffer,
db.sample_rate,
db.num_channels,
db.sample_size,
),
_ => continue,
};
let mut load_idx = buf_idx;
let mut buf_ptr = self.bus.read_long(header_ptr + 12 + (buf_idx as u32) * 4);
let mut can_load = buf_ptr != 0
&& self.bus.read_long(buf_ptr + 4) & 0x01 != 0
&& !queued_doublebacks
.iter()
.any(|&(pending_chan, pending_idx)| {
pending_chan == chan.guest_ptr && pending_idx == buf_idx
});
let original_idx = load_idx;
if !can_load {
let other_idx = buf_idx ^ 1;
let other_ptr = self.bus.read_long(header_ptr + 12 + (other_idx as u32) * 4);
if other_ptr == 0 {
continue;
}
let other_flags = self.bus.read_long(other_ptr + 4);
let other_pending =
queued_doublebacks
.iter()
.any(|&(pending_chan, pending_idx)| {
pending_chan == chan.guest_ptr && pending_idx == other_idx
});
if other_flags & 0x01 == 0 || other_pending {
continue; }
load_idx = other_idx;
buf_ptr = other_ptr;
can_load = true;
if let Some(ref mut db) = chan.double_buffer {
db.current_buffer = other_idx;
}
}
if !can_load {
continue;
}
let flags = self.bus.read_long(buf_ptr + 4);
if trace_sound_runner_enabled() {
let preview = self
.bus
.read_bytes(buf_ptr + 16, 16)
.iter()
.map(|byte| format!("{:02X}", byte))
.collect::<Vec<_>>()
.join(" ");
eprintln!(
"[SOUND-DB] load-ready chan=${:08X} header=${:08X} requested_idx={} load_idx={} buf=${:08X} frames={} flags=${:08X} pending={:?} first={}",
chan.guest_ptr,
header_ptr,
original_idx,
load_idx,
buf_ptr,
self.bus.read_long(buf_ptr),
flags,
chan.double_buffer
.as_ref()
.map(|db| db.pending_callback_buffers)
.unwrap_or([false; 2]),
preview
);
}
crate::trap::TrapDispatcher::load_double_buffer_samples(
&mut self.bus,
chan,
buf_ptr,
sample_rate,
num_channels,
sample_size,
);
if flags & 0x01 != 0 {
if let Some(ref mut db) = chan.double_buffer {
db.current_buffer = load_idx;
db.complete_callback_for(load_idx);
}
}
}
}
fn dump_invalid_pc_state(&self) {
let d_regs = [
self.cpu.read_reg(Register::D0),
self.cpu.read_reg(Register::D1),
self.cpu.read_reg(Register::D2),
self.cpu.read_reg(Register::D3),
self.cpu.read_reg(Register::D4),
self.cpu.read_reg(Register::D5),
self.cpu.read_reg(Register::D6),
self.cpu.read_reg(Register::D7),
];
let a_regs = [
self.cpu.read_reg(Register::A0),
self.cpu.read_reg(Register::A1),
self.cpu.read_reg(Register::A2),
self.cpu.read_reg(Register::A3),
self.cpu.read_reg(Register::A4),
self.cpu.read_reg(Register::A5),
self.cpu.read_reg(Register::A6),
self.cpu.read_reg(Register::A7),
];
eprintln!(
"[RUN_STEPS] D0-D7: {:08X} {:08X} {:08X} {:08X} {:08X} {:08X} {:08X} {:08X}",
d_regs[0], d_regs[1], d_regs[2], d_regs[3], d_regs[4], d_regs[5], d_regs[6], d_regs[7]
);
eprintln!(
"[RUN_STEPS] A0-A7: {:08X} {:08X} {:08X} {:08X} {:08X} {:08X} {:08X} {:08X}",
a_regs[0], a_regs[1], a_regs[2], a_regs[3], a_regs[4], a_regs[5], a_regs[6], a_regs[7]
);
eprintln!("[RUN_STEPS] CCR=${:02X}", self.cpu.core.get_ccr());
if let Some(active) = self.active_interrupt_callback {
eprintln!(
"[RUN_STEPS] active_callback={:?} resume_pc=${:08X} resume_sp=${:08X}",
active.source, active.resume_pc, active.resume_sp
);
}
self.bus.dump_stack(a_regs[7], "invalid PC");
}
fn inject_interrupt_callback(
&mut self,
source: ActiveInterruptCallbackSource,
trampoline: u32,
) {
let current_pc = self.cpu.read_reg(Register::PC);
let sp = self.cpu.read_reg(Register::A7);
let d_regs = [
self.cpu.read_reg(Register::D0),
self.cpu.read_reg(Register::D1),
self.cpu.read_reg(Register::D2),
self.cpu.read_reg(Register::D3),
self.cpu.read_reg(Register::D4),
self.cpu.read_reg(Register::D5),
self.cpu.read_reg(Register::D6),
self.cpu.read_reg(Register::D7),
];
let a_regs = [
self.cpu.read_reg(Register::A0),
self.cpu.read_reg(Register::A1),
self.cpu.read_reg(Register::A2),
self.cpu.read_reg(Register::A3),
self.cpu.read_reg(Register::A4),
self.cpu.read_reg(Register::A5),
self.cpu.read_reg(Register::A6),
sp,
];
let ccr = self.cpu.core.get_ccr();
let sr = self.cpu.core.get_sr();
let new_sp = sp.wrapping_sub(4);
self.bus.write_long(new_sp, current_pc);
self.cpu.write_reg(Register::A7, new_sp);
self.active_interrupt_callback = Some(ActiveInterruptCallback {
source,
resume_pc: current_pc,
resume_sp: sp,
d_regs,
a_regs,
sr,
ccr,
restore_port: None,
});
self.cpu
.core
.set_sr_noint_nosp(interrupt_callback_sr(source, sr));
self.cpu.write_reg(Register::PC, trampoline);
}
fn fire_file_completion_callback(&mut self) -> bool {
if self.active_interrupt_callback.is_some() {
return false;
}
let Some(completion) = self.dispatcher.pending_file_completions.pop_front() else {
return false;
};
self.bus
.write_word(completion.parameter_block + 16, completion.result as u16);
if completion.completion_addr == 0 {
return false;
}
if self.file_completion_trampoline == 0 {
let tramp = self.bus.alloc_synthetic(8);
self.bus.write_word(tramp, 0x4EB9); self.bus.write_word(tramp + 6, 0x4E75); self.file_completion_trampoline = tramp;
}
let tramp = self.file_completion_trampoline;
self.bus.write_long(tramp + 2, completion.completion_addr);
self.inject_interrupt_callback(ActiveInterruptCallbackSource::FileCompletion, tramp);
self.cpu.write_reg(Register::A0, completion.parameter_block);
self.cpu
.write_reg(Register::D0, completion.result as i32 as u32);
true
}
fn fire_adb_callback(&mut self) -> bool {
if self.active_interrupt_callback.is_some() {
return false;
}
let Some(packet) = self.dispatcher.adb.pop_pending_packet() else {
return false;
};
if packet.service_routine == 0 {
return false;
}
if self.adb_callback_trampoline == 0 {
let tramp = self.bus.alloc_synthetic(8);
self.bus.write_word(tramp, 0x4EB9); self.bus.write_word(tramp + 6, 0x4E75); self.adb_callback_trampoline = tramp;
}
if self.adb_packet_buffer == 0 {
self.adb_packet_buffer = self.bus.alloc_synthetic(4);
}
let tramp = self.adb_callback_trampoline;
self.bus.write_long(tramp + 2, packet.service_routine);
for (offset, byte) in packet.packet.into_iter().enumerate() {
self.bus
.write_byte(self.adb_packet_buffer + offset as u32, byte);
}
self.inject_interrupt_callback(ActiveInterruptCallbackSource::Adb, tramp);
self.cpu.write_reg(Register::A0, self.adb_packet_buffer);
self.cpu.write_reg(Register::A1, packet.service_routine);
self.cpu.write_reg(Register::A2, packet.data_area);
self.cpu.write_reg(Register::D0, u32::from(packet.command));
true
}
fn fire_sound_callbacks(&mut self) -> bool {
if self.active_interrupt_callback.is_some() {
return false;
}
if self
.dispatcher
.sound_manager
.pending_sound_callbacks
.is_empty()
{
return false;
}
let cb = self
.dispatcher
.sound_manager
.pending_sound_callbacks
.remove(0);
match cb {
crate::sound::PendingSoundCallback::Command {
callback_addr,
chan_ptr,
cmd,
} => {
if callback_addr == 0 {
return false;
}
if self.sound_callback_trampoline == 0 {
let tramp = self.bus.alloc_synthetic(42);
self.bus.write_word(tramp, 0x48E7); self.bus.write_word(tramp + 2, 0xF0F0); self.bus.write_word(tramp + 4, 0x2F3C); self.bus.write_word(tramp + 10, 0x2F3C); self.bus.write_word(tramp + 16, 0x4EB9); self.bus.write_word(tramp + 22, 0x2E7C); self.bus.write_word(tramp + 28, 0x4CDF); self.bus.write_word(tramp + 30, 0x0F0F); self.bus.write_word(tramp + 32, 0x4E75); self.sound_callback_trampoline = tramp;
}
let tramp = self.sound_callback_trampoline;
let cmd_ptr = tramp + 34;
let interrupted_sp = self.cpu.read_reg(Register::A7);
let saved_regs_sp = interrupted_sp.wrapping_sub(4 + 32);
self.bus.write_long(tramp + 6, chan_ptr);
self.bus.write_long(tramp + 12, cmd_ptr);
self.bus.write_long(tramp + 18, callback_addr);
self.bus.write_long(tramp + 24, saved_regs_sp);
self.bus.write_word(cmd_ptr, cmd.cmd);
self.bus.write_word(cmd_ptr + 2, cmd.param1 as u16);
self.bus.write_long(cmd_ptr + 4, cmd.param2);
self.inject_interrupt_callback(ActiveInterruptCallbackSource::SoundCallback, tramp);
true
}
crate::sound::PendingSoundCallback::FileCompletion {
callback_addr,
chan_ptr,
} => {
if callback_addr == 0 {
return false;
}
if self.sound_file_completion_trampoline == 0 {
let tramp = self.bus.alloc_synthetic(28);
self.bus.write_word(tramp, 0x48E7); self.bus.write_word(tramp + 2, 0xF0F0); self.bus.write_word(tramp + 4, 0x2F3C); self.bus.write_word(tramp + 10, 0x4EB9); self.bus.write_word(tramp + 16, 0x2E7C); self.bus.write_word(tramp + 22, 0x4CDF); self.bus.write_word(tramp + 24, 0x0F0F); self.bus.write_word(tramp + 26, 0x4E75); self.sound_file_completion_trampoline = tramp;
}
let tramp = self.sound_file_completion_trampoline;
let interrupted_sp = self.cpu.read_reg(Register::A7);
let saved_regs_sp = interrupted_sp.wrapping_sub(4 + 32);
self.bus.write_long(tramp + 6, chan_ptr);
self.bus.write_long(tramp + 12, callback_addr);
self.bus.write_long(tramp + 18, saved_regs_sp);
self.inject_interrupt_callback(
ActiveInterruptCallbackSource::SoundFileCompletion,
tramp,
);
true
}
}
}
fn fire_sound_doubleback_callbacks(&mut self) -> bool {
if self.active_interrupt_callback.is_some() {
return false;
}
if self.dispatcher.sound_manager.pending_callbacks.is_empty() {
return false;
}
let cb = self.dispatcher.sound_manager.pending_callbacks.remove(0);
let exhausted_buf_ptr = self
.bus
.read_long(cb.header_ptr + 12 + (cb.exhausted_buffer_index as u32) * 4);
if exhausted_buf_ptr != 0 {
let flags = self.bus.read_long(exhausted_buf_ptr + 4);
if trace_sound_runner_enabled() {
let preview = self
.bus
.read_bytes(exhausted_buf_ptr + 16, 16)
.iter()
.map(|byte| format!("{:02X}", byte))
.collect::<Vec<_>>()
.join(" ");
eprintln!(
"[SOUND-DB] fire-doubleback tick={} chan=${:08X} header=${:08X} idx={} buf=${:08X} frames={} flags_before=${:08X} callback=${:08X} sr=${:04X} first={}",
self.bus.read_long(0x016A),
cb.chan_ptr,
cb.header_ptr,
cb.exhausted_buffer_index,
exhausted_buf_ptr,
self.bus.read_long(exhausted_buf_ptr),
flags,
cb.callback_addr,
self.cpu.core.get_sr(),
preview
);
}
self.bus.write_long(exhausted_buf_ptr + 4, flags & !0x01);
}
if cb.callback_addr == 0 {
return false;
}
if self.sound_doubleback_trampoline == 0 {
let tramp = self.bus.alloc_synthetic(34);
self.bus.write_word(tramp, 0x48E7); self.bus.write_word(tramp + 2, 0xF0F0); self.bus.write_word(tramp + 4, 0x2F3C); self.bus.write_word(tramp + 10, 0x2F3C); self.bus.write_word(tramp + 16, 0x4EB9); self.bus.write_word(tramp + 22, 0x2E7C); self.bus.write_word(tramp + 28, 0x4CDF); self.bus.write_word(tramp + 30, 0x0F0F); self.bus.write_word(tramp + 32, 0x4E75); self.sound_doubleback_trampoline = tramp;
}
let tramp = self.sound_doubleback_trampoline;
let interrupted_sp = self.cpu.read_reg(Register::A7);
let saved_regs_sp = interrupted_sp.wrapping_sub(4 + 32);
self.bus.write_long(tramp + 6, cb.chan_ptr);
self.bus.write_long(tramp + 12, exhausted_buf_ptr);
self.bus.write_long(tramp + 18, cb.callback_addr);
self.bus.write_long(tramp + 24, saved_regs_sp);
self.inject_interrupt_callback(ActiveInterruptCallbackSource::SoundDoubleBack, tramp);
true
}
fn dialog_callback_scratch_base(&self) -> u32 {
DIALOG_CALLBACK_SCRATCH_FALLBACK
}
fn looks_like_dialog_proc_entry(&self, addr: u32) -> bool {
if addr == 0 {
return false;
}
let entry = self.bus.read_word(addr);
entry == 0x4E56 || entry == 0x48E7 || entry == 0x4EF9 || entry == 0x4EFA
}
fn resolve_dialog_draw_proc_addr(&self, proc_addr: u32) -> Option<u32> {
if self.looks_like_dialog_proc_entry(proc_addr) {
return Some(proc_addr);
}
let a5_relative = self.cpu.read_reg(Register::A5).wrapping_add(proc_addr);
if self.looks_like_dialog_proc_entry(a5_relative) {
Some(a5_relative)
} else {
None
}
}
fn inject_dialog_draw_proc(
&mut self,
proc_addr: u32,
item_no: i16,
dialog_ptr: u32,
modeless: bool,
) -> bool {
if self.active_interrupt_callback.is_some() {
return false;
}
if proc_addr == 0 {
return false;
}
let Some(call_addr) = self.resolve_dialog_draw_proc_addr(proc_addr) else {
if trace_dialog_procs_enabled() {
let a5_relative = self.cpu.read_reg(Register::A5).wrapping_add(proc_addr);
eprintln!(
"[DIALOG-PROC] skip dialog=${:08X} item={} proc=${:08X} a5rel=${:08X} entry=${:04X} a5entry=${:04X}",
dialog_ptr,
item_no,
proc_addr,
a5_relative,
self.bus.read_word(proc_addr),
self.bus.read_word(a5_relative),
);
}
return false;
};
if self.dialog_draw_trampoline == 0 {
let tramp = self.dialog_callback_scratch_base() + DIALOG_DRAW_TRAMPOLINE_OFFSET;
self.bus.write_word(tramp, 0x48E7); self.bus.write_word(tramp + 2, 0xF0F0); self.bus.write_word(tramp + 4, 0x2F3C); self.bus.write_word(tramp + 10, 0x3F3C); self.bus.write_word(tramp + 14, 0x4EB9); self.bus.write_word(tramp + 20, 0x4FF9); self.bus.write_word(tramp + 26, 0x4CDF); self.bus.write_word(tramp + 28, 0x0F0F); self.bus.write_word(tramp + 30, 0x4E75); self.dialog_draw_trampoline = tramp;
}
let tramp = self.dialog_draw_trampoline;
self.bus.write_long(tramp + 6, dialog_ptr);
self.bus.write_word(tramp + 12, item_no as u16);
self.bus.write_long(tramp + 16, call_addr);
self.dialog_draw_port_snapshot = Some(self.dispatcher.prepare_dialog_user_item_port_state(
&mut self.bus,
&mut self.cpu,
dialog_ptr,
));
let current_pc = self.cpu.read_reg(Register::PC);
let sp = self.cpu.read_reg(Register::A7);
let d_regs = [
self.cpu.read_reg(Register::D0),
self.cpu.read_reg(Register::D1),
self.cpu.read_reg(Register::D2),
self.cpu.read_reg(Register::D3),
self.cpu.read_reg(Register::D4),
self.cpu.read_reg(Register::D5),
self.cpu.read_reg(Register::D6),
self.cpu.read_reg(Register::D7),
];
let a_regs = [
self.cpu.read_reg(Register::A0),
self.cpu.read_reg(Register::A1),
self.cpu.read_reg(Register::A2),
self.cpu.read_reg(Register::A3),
self.cpu.read_reg(Register::A4),
self.cpu.read_reg(Register::A5),
self.cpu.read_reg(Register::A6),
sp,
];
let ccr = self.cpu.core.get_ccr();
let sr = self.cpu.core.get_sr();
let new_sp = sp.wrapping_sub(4);
let saved_regs_sp = new_sp.wrapping_sub(32);
self.bus.write_long(tramp + 22, saved_regs_sp);
self.bus.write_long(new_sp, current_pc);
self.cpu.write_reg(Register::A7, new_sp);
self.active_interrupt_callback = Some(ActiveInterruptCallback {
source: ActiveInterruptCallbackSource::DialogDrawProc,
resume_pc: current_pc,
resume_sp: sp,
d_regs,
a_regs,
sr,
ccr,
restore_port: None,
});
if modeless {
self.dispatcher.active_modeless_dialog_draw_proc = Some(dialog_ptr);
}
if trace_dialog_procs_enabled() {
eprintln!(
"[DIALOG-PROC] fire {} dialog=${:08X} item={} proc=${:08X} call=${:08X} return_pc=${:08X}",
if modeless { "modeless" } else { "modal" },
dialog_ptr,
item_no,
proc_addr,
call_addr,
current_pc,
);
}
self.cpu.write_reg(Register::PC, tramp);
true
}
fn fire_modeless_dialog_draw_proc(&mut self) -> bool {
if self.active_interrupt_callback.is_some() {
return false;
}
while let Some((dialog_ptr, proc_addr, item_no)) =
self.dispatcher.modeless_dialog_draw_proc_queue.pop_front()
{
if self.inject_dialog_draw_proc(proc_addr, item_no, dialog_ptr, true) {
return true;
}
}
while let Some(dialog_ptr) = self.dispatcher.modeless_dialog_cdef_draw_queue.pop_front() {
if self.dispatcher.arm_dialog_control_def_draws(
&mut self.cpu,
&mut self.bus,
dialog_ptr,
) {
return true;
}
}
false
}
fn fire_dialog_draw_procs(&mut self) -> bool {
if self.active_interrupt_callback.is_some() {
return false;
}
if let Some(tracking) = self
.dispatcher
.dialog_tracking
.as_mut()
.filter(|tracking| !tracking.draw_procs_done)
{
let Some((proc_addr, item_no)) = tracking.draw_proc_queue.pop_front() else {
tracking.draw_procs_done = true;
return false;
};
let dialog_ptr = tracking.dialog_ptr;
return self.inject_dialog_draw_proc(proc_addr, item_no, dialog_ptr, false);
}
self.fire_modeless_dialog_draw_proc()
}
fn fire_menu_hook_proc(&mut self, opcode: u16) -> bool {
if self.active_interrupt_callback.is_some() || (opcode & !0x0400) != 0xA93D {
return false;
}
if self.dispatcher.menu_tracking.is_none() || self.bus.read_byte(0x0172) != 0x00 {
return false;
}
let hook_addr = self.bus.read_long(0x0A30);
let Some(call_addr) = self.resolve_dialog_draw_proc_addr(hook_addr) else {
return false;
};
if self.menu_hook_trampoline == 0 {
let tramp = self.dialog_callback_scratch_base() + MENU_HOOK_TRAMPOLINE_OFFSET;
self.bus.write_word(tramp, 0x48E7); self.bus.write_word(tramp + 2, 0xF0F0); self.bus.write_word(tramp + 4, 0x4EB9); self.bus.write_word(tramp + 10, 0x4FF9); self.bus.write_word(tramp + 16, 0x4CDF); self.bus.write_word(tramp + 18, 0x0F0F); self.bus.write_word(tramp + 20, 0x4E75); self.menu_hook_trampoline = tramp;
}
let tramp = self.menu_hook_trampoline;
self.bus.write_long(tramp + 6, call_addr);
let current_pc = self.cpu.read_reg(Register::PC);
let sp = self.cpu.read_reg(Register::A7);
let d_regs = [
self.cpu.read_reg(Register::D0),
self.cpu.read_reg(Register::D1),
self.cpu.read_reg(Register::D2),
self.cpu.read_reg(Register::D3),
self.cpu.read_reg(Register::D4),
self.cpu.read_reg(Register::D5),
self.cpu.read_reg(Register::D6),
self.cpu.read_reg(Register::D7),
];
let a_regs = [
self.cpu.read_reg(Register::A0),
self.cpu.read_reg(Register::A1),
self.cpu.read_reg(Register::A2),
self.cpu.read_reg(Register::A3),
self.cpu.read_reg(Register::A4),
self.cpu.read_reg(Register::A5),
self.cpu.read_reg(Register::A6),
sp,
];
let ccr = self.cpu.core.get_ccr();
let sr = self.cpu.core.get_sr();
let new_sp = sp.wrapping_sub(4);
let saved_regs_sp = new_sp.wrapping_sub(32);
self.bus.write_long(tramp + 12, saved_regs_sp);
self.bus.write_long(new_sp, current_pc);
self.cpu.write_reg(Register::A7, new_sp);
self.active_interrupt_callback = Some(ActiveInterruptCallback {
source: ActiveInterruptCallbackSource::MenuHook,
resume_pc: current_pc,
resume_sp: sp,
d_regs,
a_regs,
sr,
ccr,
restore_port: None,
});
self.cpu.write_reg(Register::PC, tramp);
true
}
fn dialog_filter_has_real_event_pending(&self, dialog_ptr: u32) -> bool {
self.dispatcher
.event_queue
.iter()
.any(|event| matches!(event.what, 1 | 2 | 3 | 4 | 6))
|| self
.dispatcher
.pending_update_event(&self.bus, 1u16 << 6)
.is_some_and(|event| {
event.message == dialog_ptr
&& !self.dialog_filter_update_event_already_sent_this_tick(
dialog_ptr,
event.message,
)
})
}
fn dialog_filter_null_event_already_sent_this_tick(&self, dialog_ptr: u32) -> bool {
self.dialog_filter_last_null_event_tick
.is_some_and(|(sent_dialog, sent_tick)| {
sent_dialog == dialog_ptr && sent_tick == self.dispatcher.tick_count
})
}
fn dialog_filter_update_event_already_sent_this_tick(
&self,
dialog_ptr: u32,
update_window: u32,
) -> bool {
self.dialog_filter_last_update_event_tick.is_some_and(
|(sent_dialog, sent_window, sent_tick)| {
sent_dialog == dialog_ptr
&& sent_window == update_window
&& sent_tick == self.dispatcher.tick_count
},
)
}
fn should_fire_dialog_filter_proc(&self) -> bool {
let Some(tracking) = self.dispatcher.dialog_tracking.as_ref() else {
return false;
};
if tracking.filter_proc == 0
|| !tracking.draw_procs_done
|| tracking.last_filter_event.is_some()
{
return false;
}
let dialog_ptr = tracking.dialog_ptr;
let has_real_event = self.dialog_filter_has_real_event_pending(dialog_ptr);
if !has_real_event
&& (self.dispatcher.mouse_down_over_dialog_button()
|| self.dispatcher.mouse_down_over_dialog_plain_user_item()
|| self.dispatcher.pending_dialog_plain_user_item_mouse_down())
{
return false;
}
if !has_real_event && self.dialog_filter_null_event_already_sent_this_tick(dialog_ptr) {
return false;
}
true
}
fn fire_dialog_filter_proc(&mut self) -> bool {
let (filter_proc, dialog_ptr, item_hit_ptr) = {
let tracking = match self.dispatcher.dialog_tracking.as_ref() {
Some(t) => t,
None => return false,
};
(
tracking.filter_proc,
tracking.dialog_ptr,
tracking.item_hit_ptr,
)
};
if filter_proc == 0 {
return false;
}
let entry = self.bus.read_word(filter_proc);
if entry != 0x4E56 && entry != 0x48E7 && entry != 0x4EF9 && entry != 0x4EFA {
if trace_dialog_filter_enabled() {
eprintln!(
"[DIALOG-FILTER] skip invalid-entry dialog=${:08X} proc=${:08X} entry=${:04X}",
dialog_ptr, filter_proc, entry
);
}
return false;
}
if self.dialog_filter_event == 0 {
self.dialog_filter_event =
self.dialog_callback_scratch_base() + DIALOG_FILTER_EVENT_OFFSET;
}
let evt = self.dialog_filter_event;
if self.dispatcher.dialog_filter_result_addr == 0 {
self.dispatcher.dialog_filter_result_addr =
self.dialog_callback_scratch_base() + DIALOG_FILTER_RESULT_OFFSET;
}
let result_addr = self.dispatcher.dialog_filter_result_addr;
self.bus.write_word(result_addr, 0);
let ticks = self.bus.read_long(0x016A);
let idx = self
.dispatcher
.event_queue
.iter()
.position(|e| matches!(e.what, 1 | 2 | 3 | 4 | 6));
let next_event = idx.map(|i| self.dispatcher.event_queue.remove(i).unwrap());
let filter_event = if let Some(e) = next_event {
e
} else if let Some(update_event) = self
.dispatcher
.pending_update_event(&self.bus, 1u16 << 6)
.filter(|event| {
event.message == dialog_ptr
&& !self.dialog_filter_update_event_already_sent_this_tick(
dialog_ptr,
event.message,
)
})
{
self.dialog_filter_last_update_event_tick =
Some((dialog_ptr, update_event.message, ticks));
update_event
} else {
let (v, h) = self.dispatcher.mouse_position();
crate::trap::dispatch::QueuedEvent {
what: 0,
message: 0,
where_v: v,
where_h: h,
modifiers: self.dispatcher.current_event_modifiers(),
}
};
if let Some(tracking) = self.dispatcher.dialog_tracking.as_mut() {
tracking.last_filter_event = Some(filter_event.clone());
}
let what = filter_event.what;
let message = filter_event.message;
let where_v = filter_event.where_v;
let where_h = filter_event.where_h;
let modifiers = filter_event.modifiers;
if what == 0 {
self.dialog_filter_last_null_event_tick = Some((dialog_ptr, ticks));
} else {
self.dialog_filter_last_null_event_tick = None;
}
self.dispatcher.tick_count = ticks;
self.dispatcher.write_event_record(
&mut self.bus,
evt,
what,
message,
where_v,
where_h,
modifiers,
);
if trace_dialog_filter_enabled() {
eprintln!(
"[DIALOG-FILTER] call dialog=${:08X} proc=${:08X} event=what:{} message=${:08X} where=({}, {}) mods=${:04X}",
dialog_ptr, filter_proc, what, message, where_v, where_h, modifiers
);
}
if self.dialog_filter_trampoline == 0 {
let tramp = self.dialog_callback_scratch_base() + DIALOG_FILTER_TRAMPOLINE_OFFSET;
self.bus.write_word(tramp, 0x48E7); self.bus.write_word(tramp + 2, 0xF0F0); self.bus.write_word(tramp + 4, 0x4267); self.bus.write_word(tramp + 6, 0x2F3C); self.bus.write_word(tramp + 12, 0x2F3C); self.bus.write_word(tramp + 18, 0x2F3C); self.bus.write_word(tramp + 24, 0x4EB9); self.bus.write_word(tramp + 30, 0x33D7); self.bus.write_word(tramp + 36, 0x2E7C); self.bus.write_word(tramp + 42, 0x4CDF); self.bus.write_word(tramp + 44, 0x0F0F); self.bus.write_word(tramp + 46, 0x4E75); self.dialog_filter_trampoline = tramp;
}
let tramp = self.dialog_filter_trampoline;
self.bus.write_long(tramp + 8, dialog_ptr);
self.bus.write_long(tramp + 14, evt);
self.bus.write_long(tramp + 20, item_hit_ptr);
self.bus.write_long(tramp + 26, filter_proc);
self.bus.write_long(tramp + 32, result_addr);
self.dispatcher
.set_current_port_state(&mut self.bus, &mut self.cpu, dialog_ptr, None);
let current_pc = self.cpu.read_reg(Register::PC);
let sp = self.cpu.read_reg(Register::A7);
let d_regs = [
self.cpu.read_reg(Register::D0),
self.cpu.read_reg(Register::D1),
self.cpu.read_reg(Register::D2),
self.cpu.read_reg(Register::D3),
self.cpu.read_reg(Register::D4),
self.cpu.read_reg(Register::D5),
self.cpu.read_reg(Register::D6),
self.cpu.read_reg(Register::D7),
];
let a_regs = [
self.cpu.read_reg(Register::A0),
self.cpu.read_reg(Register::A1),
self.cpu.read_reg(Register::A2),
self.cpu.read_reg(Register::A3),
self.cpu.read_reg(Register::A4),
self.cpu.read_reg(Register::A5),
self.cpu.read_reg(Register::A6),
sp,
];
let ccr = self.cpu.core.get_ccr();
let sr = self.cpu.core.get_sr();
let new_sp = sp.wrapping_sub(4);
let saved_sp = new_sp.wrapping_sub(32);
let filter_entry_sp = saved_sp.wrapping_sub(50); let clear_size: u32 = 2048;
let clear_start = filter_entry_sp.wrapping_sub(clear_size);
self.bus.fill_zeros(clear_start, clear_size);
self.bus.write_long(tramp + 38, saved_sp);
self.bus.write_long(new_sp, current_pc);
self.cpu.write_reg(Register::A7, new_sp);
self.active_interrupt_callback = Some(ActiveInterruptCallback {
source: ActiveInterruptCallbackSource::DialogFilterProc,
resume_pc: current_pc,
resume_sp: sp,
d_regs,
a_regs,
sr,
ccr,
restore_port: None,
});
self.cpu.write_reg(Register::PC, tramp);
if let Some(tracking) = self.dispatcher.dialog_tracking.as_mut() {
tracking.rendered_pixels_final = false;
}
true
}
pub fn run(&mut self) -> Result<()> {
let mut count = 0;
if trace_load_enabled() {
eprintln!("========================================");
eprintln!(" FIXTURE RUNNER STARTING ");
eprintln!("========================================");
eprintln!(
"[RUN] Starting at PC=${:08X}, A5=${:08X}, A7=${:08X}",
self.cpu.read_reg(Register::PC),
self.cpu.read_reg(Register::A5),
self.cpu.read_reg(Register::A7)
);
}
while count < self.config.max_instructions {
if self.cpu.is_stopped() {
if trace_load_enabled() {
eprintln!(
"[RUN] Stopped after {} instructions, PC=${:08X}",
count,
self.cpu.read_reg(Register::PC)
);
}
return Ok(());
}
let pc = self.cpu.read_reg(Register::PC);
let translated_pc = self.bus.translate_guest_address(pc);
if translated_pc >= self.bus.ram_size() || (translated_pc < 0x60 && pc > 0) {
eprintln!(
"[RUN] CRITICAL: PC jumped to invalid address ${:08X}! Halting trace.",
pc
);
self.dump_trace();
return Ok(());
}
if trace_buffer_enabled() {
let opcode = self.bus.read_word(pc);
let a0 = self.cpu.read_reg(Register::A0);
let sp = self.cpu.read_reg(Register::A7);
let a6 = self.cpu.read_reg(Register::A6);
let a5 = self.cpu.read_reg(Register::A5);
if self.trace_buffer.len() >= 200 {
self.trace_buffer.pop_front();
}
self.trace_buffer.push_back((pc, opcode, a0, sp, a6, a5));
}
match self.cpu.step(&mut self.bus) {
StepResult::Ok => {}
StepResult::Stopped => {
if trace_load_enabled() {
let stopped_pc = self.cpu.read_reg(Register::PC);
let opcode = self.bus.read_word(stopped_pc);
eprintln!(
"[RUN] Step returned Stopped after {} instructions, PC=${:08X}, Opcode=${:04X}",
count, stopped_pc, opcode
);
}
self.dump_trace();
return Ok(());
}
StepResult::Aline(opcode) => {
match self
.dispatcher
.dispatch(opcode, &mut self.cpu, &mut self.bus)
{
Ok(()) => {
let pc_after = self.cpu.read_reg(Register::PC);
if pc_after == pc {
self.cpu.write_reg(Register::PC, pc + 2);
}
}
Err(Error::Halted) => {
if trace_load_enabled() {
eprintln!("[RUN] Halted via trap after {} instructions", count);
}
self.dump_trace();
return Ok(());
}
Err(e) => {
self.dump_trace();
return Err(e);
}
}
}
}
count += 1;
}
if trace_load_enabled() {
eprintln!("[RUN] Timeout after {} instructions", count);
}
self.dump_trace();
Err(Error::Timeout(count))
}
fn trace_find_fakeptr_entry(&self) -> Option<(u32, String)> {
for (pc, _op, _a0, _sp, _a6, _a5) in self.trace_buffer.iter().rev() {
if let Some(hint) = decode_fakeptr_pc(*pc) {
return Some((*pc, hint));
}
}
None
}
pub fn dump_trace(&self) {
if self.trace_buffer.is_empty() {
return;
}
eprintln!(
"[TRACE] Last {} executed instructions:",
self.trace_buffer.len()
);
eprintln!(" PC Op A0 SP A6 D0");
for (pc, opcode, a0, sp, a6, d0) in &self.trace_buffer {
eprintln!(
" {:08X} {:04X} {:08X} {:08X} {:08X} {:08X}",
pc, opcode, a0, sp, a6, d0
);
}
}
}
impl Drop for FixtureRunner {
fn drop(&mut self) {
self.dispatcher.print_trap_histogram(40);
self.print_opcode_histogram(40);
self.print_pc_histogram(40);
self.print_ppc_fetch_histogram(40);
self.print_ppc_import_histogram(usize::MAX);
self.print_ppc_unimpl_histogram(40);
self.print_ppc_gworld_dump(8);
self.dispatcher.print_trap_timing_histogram(40);
}
}
fn apply_retro68_rela_relocations<M: MemoryBus>(
bus: &mut M,
target_base: u32,
target_size: usize,
rela: &[u8],
displacements: [u32; 4],
) -> std::result::Result<usize, Retro68RelocationError> {
let relocations = decode_retro68_relocations(rela, target_size)?;
let addresses = relocations
.iter()
.map(|relocation| {
target_base.checked_add(relocation.offset).ok_or(
Retro68RelocationError::GuestAddressOverflow {
base: target_base,
offset: relocation.offset,
},
)
})
.collect::<std::result::Result<Vec<_>, _>>()?;
for (relocation, address) in relocations.iter().zip(addresses) {
let mut value = bus
.read_long(address)
.wrapping_add(displacements[relocation.base_index]);
if relocation.pc_relative {
value = value.wrapping_sub(address);
}
bus.write_long(address, value);
}
Ok(relocations.len())
}
fn update_far_segment_runtime_addresses<M: MemoryBus>(
bus: &mut M,
segment_addr: u32,
a5_base: u32,
) {
bus.write_long(
segment_addr + MpwFarSegmentHeader::CURRENT_A5_OFFSET,
a5_base,
);
bus.write_long(
segment_addr + MpwFarSegmentHeader::LOAD_ADDRESS_OFFSET,
segment_addr,
);
}
fn apply_mpw_far_segment_relocations<M: MemoryBus>(
bus: &mut M,
segment_id: i16,
segment_addr: u32,
data: &[u8],
a5_base: u32,
) {
let Some(header) = MpwFarSegmentHeader::parse(data) else {
return;
};
let mut a5_relocation_count = 0usize;
match header.a5_relocation_offsets(data) {
Some(offsets) => {
a5_relocation_count = offsets.len();
for offset in offsets {
let addr = segment_addr.wrapping_add(offset);
let relocated = bus.read_long(addr).wrapping_add(a5_base);
bus.write_long(addr, relocated);
}
}
None => {
if trace_load_enabled() {
eprintln!(
"[LOAD] CODE {} has invalid MPW far A5 relocation data at ${:08X}",
segment_id, header.a5_relocation_data_offset
);
}
}
}
let mut pc_relocation_count = 0usize;
match header.pc_relocation_offsets(data) {
Some(offsets) => {
pc_relocation_count = offsets.len();
for offset in offsets {
let addr = segment_addr.wrapping_add(offset);
let relocated = bus.read_long(addr).wrapping_add(segment_addr);
bus.write_long(addr, relocated);
}
}
None => {
if trace_load_enabled() {
eprintln!(
"[LOAD] CODE {} has invalid MPW far PC relocation data at ${:08X}",
segment_id, header.pc_relocation_data_offset
);
}
}
}
update_far_segment_runtime_addresses(bus, segment_addr, a5_base);
if trace_load_enabled() {
eprintln!(
"[LOAD] Applied MPW far relocations for CODE {}: a5={}, pc={}",
segment_id, a5_relocation_count, pc_relocation_count
);
}
}
fn load_app_generic<M: MemoryBus>(
fork: &ResourceFork,
bus: &mut M,
configured_load_address: u32,
) -> Option<LoadedApp> {
let code0 = fork.get_code(0)?;
let header = Code0Header::parse(&code0.data)?;
let size_resource = [0, -1].into_iter().find_map(|id| {
fork.get(*b"SIZE", id)
.and_then(|res| ApplicationSizeResource::parse(&res.data))
.filter(|size| size.preferred_partition_size().is_some())
.map(|size| (id, size))
});
let (size_resource_id, size_resource) = size_resource
.map(|(id, size)| (Some(id), Some(size)))
.unwrap_or((None, None));
let load_address = load_address_for_size_partition(
configured_load_address,
&header,
size_resource,
bus.application_memory_limit(),
);
if trace_load_enabled() {
eprintln!(
"[LOAD] CODE 0 header: above_a5={}, below_a5={}, jt_size={}, jt_offset={}",
header.above_a5, header.below_a5, header.jump_table_size, header.jump_table_offset
);
if load_address != configured_load_address {
if let Some(size) = size_resource {
eprintln!(
"[LOAD] Relocated load base for SIZE partition: configured=${:08X} effective=${:08X} preferred={} minimum={}",
configured_load_address,
load_address,
size.preferred_size,
size.minimum_size
);
}
}
if let (Some(id), Some(size)) = (size_resource_id, size_resource) {
eprintln!(
"[LOAD] SIZE {} flags=${:04X} highLevelEventAware={} preferred={} minimum={}",
id,
size.flags,
size.is_high_level_event_aware(),
size.preferred_size,
size.minimum_size
);
}
}
let a5_base = load_address + header.below_a5;
let mut all_codes = fork.get_all_code();
all_codes.sort_by_key(|code| code.id);
let is_retro68_multisegment = all_codes.iter().any(|code| {
code.id != 0
&& matches!(
CodeSegmentHeader::parse(&code.data),
Some(CodeSegmentHeader::MpwFar)
)
&& fork.get(*b"RELA", code.id).is_some()
});
let globals_end = a5_base + header.above_a5 + APP_QD_GLOBALS_RESERVE;
let globals_zero_end = globals_end + APP_LOADER_CLEAR_RESERVE;
bus.fill_zeros(load_address, globals_zero_end.saturating_sub(load_address));
bus.write_long(0x0904, a5_base); bus.write_word(0x0934, header.jump_table_offset as u16); bus.write_word(0x028E, 0x0000);
for addr in (0x0060..0x0100).step_by(2) {
bus.write_word(addr, 0x4E75); }
const BOOT_EXCEPTION_VECTORS: [u32; 64] = [
0x40810000, 0x40810000, 0x0001EAD6, 0x0001EAD8, 0x0001EADA, 0x0001EADC, 0x408026F8,
0x408026FA, 0x408026FC, 0x408026FE, 0x408099B0, 0x4088D9FE, 0x40802704, 0x40802704,
0x40802704, 0x40802704, 0x40802704, 0x40802704, 0x40802704, 0x40802704, 0x40802704,
0x40802704, 0x40802704, 0x40802704, 0x40802704, 0x00083C16, 0x40809B40, 0x4080A1B0,
0x00006436, 0x40809B00, 0x40809B00, 0x000737D6, 0x40802704, 0x40802704, 0x40802704,
0x40802704, 0x40802704, 0x40802704, 0x40802704, 0x40802704, 0x40802704, 0x40802704,
0x40802704, 0x40802704, 0x40802704, 0x40802704, 0x40802704, 0x40802704, 0x4088D252,
0x40802704, 0x40802704, 0x4088D856, 0x4088D28C, 0x4088D544, 0x4088D68E, 0x4088DAB0,
0x40802704, 0x40802704, 0x40802704, 0x40802704, 0x40802704, 0x40802704, 0x40802704,
0x40802704,
];
for (vector, &handler) in BOOT_EXCEPTION_VECTORS.iter().enumerate() {
bus.write_long((vector as u32) * 4, handler);
}
bus.write_word(0x00FE, 0x4E73); bus.write_long(0x0014, 0x0000_00FE); bus.write_long(0x0018, 0x0000_00FE); bus.write_long(0x001C, 0x0000_00FE);
if let Some(data) = fork.get(*b"DATA", 0) {
let data_dest = load_address;
if trace_load_enabled() {
eprintln!(
"[LOAD] Writing DATA 0 ({} bytes) to ${:08X}",
data.data.len(),
data_dest
);
}
bus.write_bytes(data_dest, &data.data);
if is_retro68_multisegment {
if let Some(rela) = fork.get(*b"RELA", 0) {
match apply_retro68_rela_relocations(
bus,
data_dest,
data.data.len(),
&rela.data,
[0, a5_base, a5_base, a5_base],
) {
Ok(count) => {
if trace_load_enabled() {
eprintln!("[LOAD] Applied Retro68 RELA 0: count={count}");
}
}
Err(error) => {
tracing::warn!("invalid Retro68 RELA 0 resource: {error:?}");
return None;
}
}
}
}
}
let mut jump_table = Vec::new();
let jt_data = &code0.data[16..];
for i in 0..header.num_entries() {
let entry_offset = i * 8;
if entry_offset + 8 > jt_data.len() {
break;
}
let word_2_3 = u16::from_be_bytes([jt_data[entry_offset + 2], jt_data[entry_offset + 3]]);
let (offset, segment) = if word_2_3 == 0xA9F0 {
let seg = i16::from_be_bytes([jt_data[entry_offset], jt_data[entry_offset + 1]]);
let off = u16::from_be_bytes([jt_data[entry_offset + 6], jt_data[entry_offset + 7]]);
(off, seg)
} else if word_2_3 == 0xFFFF {
(0u16, 0i16)
} else {
let off = u16::from_be_bytes([jt_data[entry_offset], jt_data[entry_offset + 1]]);
let seg = i16::from_be_bytes([jt_data[entry_offset + 4], jt_data[entry_offset + 5]]);
(off, seg)
};
jump_table.push(JumpTableEntry {
offset,
segment,
loaded: false,
address: 0,
});
if trace_load_enabled() {
eprintln!(
"[LOAD] Parsed JT[{}]: segment={}, offset=0x{:04X}, raw=[{:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}]",
i, segment, offset,
jt_data[entry_offset], jt_data[entry_offset+1],
jt_data[entry_offset+2], jt_data[entry_offset+3],
jt_data[entry_offset+4], jt_data[entry_offset+5],
jt_data[entry_offset+6], jt_data[entry_offset+7]
);
}
}
let code0_base = globals_end;
let code0_size = code0.data.len() as u32;
let code0_user = code0_base + 4;
let jt_base = a5_base + header.jump_table_offset;
if trace_load_enabled() {
eprintln!("[LOAD] Memory layout: a5_base=${:08X}, globals_end=${:08X}, code0_user=${:08X}, code0_size={}, jt_base=${:08X} (jt_offset={})",
a5_base, globals_end, code0_user, code0_size, jt_base, header.jump_table_offset);
}
bus.write_long(code0_base, code0_size);
bus.write_bytes(code0_user, &code0.data);
let jt_content = &code0.data[16..];
if !jt_content.is_empty() {
bus.write_bytes(jt_base, jt_content);
}
let mut segment_bases = HashMap::new();
segment_bases.insert(0, code0_user);
crate::trap::dispatch::record_segment_base(0, code0_user);
let mut max_jt_end: u32 = jt_base + (jump_table.len() as u32 * 8);
for code_res in &all_codes {
if code_res.id == 0 || code_res.data.len() < 4 {
continue;
}
let Some(segment_header) = CodeSegmentHeader::parse(&code_res.data) else {
continue;
};
let Some(tab_off) = segment_header.jump_table_start_offset() else {
continue;
};
let Some(n_entries) = segment_header.jump_table_entry_count() else {
continue;
};
let end = jt_base + tab_off + n_entries * 8;
if end > max_jt_end {
max_jt_end = end;
}
for i in 0..n_entries {
let entry = jt_base + tab_off + i * 8;
let is_empty = bus.read_long(entry) == 0 && bus.read_long(entry + 4) == 0;
let is_null_placeholder = bus.read_word(entry + 2) == 0xFFFF;
if !is_empty && !is_null_placeholder {
continue;
}
if is_null_placeholder {
let routine_offset = bus.read_word(entry);
bus.write_word(entry, 0xA9F0);
bus.write_word(entry + 2, 0);
bus.write_word(entry + 4, routine_offset);
bus.write_word(entry + 6, code_res.id as u16);
} else {
bus.write_word(entry, 0);
bus.write_word(entry + 2, 0x3F3C);
bus.write_word(entry + 4, code_res.id as u16);
bus.write_word(entry + 6, 0xA9F0);
}
}
}
let reserved_boundary = std::cmp::max(code0_user + code0_size, max_jt_end);
let mut current_load_ptr = (reserved_boundary + 4) & !3;
for code_res in all_codes {
if code_res.id == 0 {
continue;
}
let size = code_res.data.len() as u32;
let phys_addr = current_load_ptr;
let user_addr = current_load_ptr + 4;
let segment_header = CodeSegmentHeader::parse(&code_res.data);
let hdr_info = match segment_header {
Some(CodeSegmentHeader::MpwFar) => "mpw-far-model".to_string(),
Some(CodeSegmentHeader::Near {
table_offset,
entry_count,
}) => format!("near-model taboff={} n={}", table_offset, entry_count),
Some(CodeSegmentHeader::ThinkFar {
has_relocations,
first_entry_index,
entry_count,
}) => format!(
"think-far-model first_jt={} n={} relocs={}",
first_entry_index, entry_count, has_relocations
),
None => "unknown".to_string(),
};
if trace_load_enabled() {
eprintln!(
"[LOAD] Loading CODE {} ({} bytes) to ${:08X} [{}]",
code_res.id, size, user_addr, hdr_info
);
}
bus.write_long(phys_addr, size);
bus.write_bytes(user_addr, &code_res.data);
segment_bases.insert(code_res.id, user_addr);
crate::trap::dispatch::record_segment_base(code_res.id, user_addr);
if matches!(segment_header, Some(CodeSegmentHeader::MpwFar)) {
if let Some(rela) = fork.get(*b"RELA", code_res.id) {
let target_base = match user_addr.checked_add(MpwFarSegmentHeader::SIZE as u32) {
Some(address) => address,
None => {
tracing::warn!(
"Retro68 CODE {} relocation target address overflowed",
code_res.id
);
return None;
}
};
let target_size = code_res.data.len() - MpwFarSegmentHeader::SIZE;
match apply_retro68_rela_relocations(
bus,
target_base,
target_size,
&rela.data,
[user_addr, a5_base, a5_base, a5_base],
) {
Ok(count) => {
update_far_segment_runtime_addresses(bus, user_addr, a5_base);
if trace_load_enabled() {
eprintln!("[LOAD] Applied Retro68 RELA {}: count={count}", code_res.id);
}
}
Err(error) => {
tracing::warn!("invalid Retro68 RELA {} resource: {error:?}", code_res.id);
return None;
}
}
} else {
apply_mpw_far_segment_relocations(
bus,
code_res.id,
user_addr,
&code_res.data,
a5_base,
);
}
for (i, entry) in jump_table.iter_mut().enumerate() {
if entry.segment == code_res.id {
entry.loaded = true;
let effective_offset = entry.offset as u32;
entry.address = user_addr + effective_offset;
let jt_addr = jt_base + (i as u32 * 8);
bus.write_word(jt_addr, code_res.id as u16);
bus.write_word(jt_addr + 2, 0x4EF9); bus.write_long(jt_addr + 4, entry.address);
if trace_load_enabled() {
eprintln!(
"[LOAD] JT[{}] -> CODE {} @ ${:08X} (far-model, off=${:04X})",
i, code_res.id, entry.address, effective_offset
);
}
}
}
}
current_load_ptr = (user_addr + size + 4 + 3) & !3;
}
let loaded_image_end = align4(globals_zero_end.max(current_load_ptr));
if trace_load_enabled() {
eprintln!("[LOAD] Loaded image end=${:08X}", loaded_image_end);
}
let stack_top = bus.application_memory_limit() - 16;
Some(LoadedApp {
ppc: None,
code0_header: header,
a5_base,
jump_table,
segment_bases,
loaded_image_end,
initial_sp: stack_top,
size_resource,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::audio::AudioBackend;
use crate::loader::ppc::*;
use crate::loader::{ApplicationSizeResource, Code0Header, LoadedApp};
use crate::sound::{
DoubleBufferState, PendingDoubleBackCallback, PendingSoundCallback, PlaybackKind,
SndChannel, SndCommand, OUTPUT_RATE,
};
use crate::trap::dispatch::{
DialogItem, DialogTrackingState, PendingFileCompletion, PendingWaitNextEventReturn,
QueuedEvent, TimerTask, VblTask,
};
use ppc::PpcCpu;
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::rc::Rc;
#[test]
fn four_bit_runner_publishes_consistent_screen_metadata() {
use crate::memory::globals::addr;
let config = FixtureRunnerConfig {
addressing_32_bit: false,
..FixtureRunnerConfig::default()
}
.with_screen_depth(4)
.expect("4-bit indexed mode should be supported");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, config);
let gdevice_handle = runner.dispatcher.ensure_main_gdevice(&mut runner.bus);
let gdevice = runner.bus.read_long(gdevice_handle);
let pixmap = runner.bus.read_long(runner.bus.read_long(gdevice + 22));
let ctab = runner.bus.read_long(runner.bus.read_long(pixmap + 42));
assert_eq!(runner.dispatcher.screen_mode.1, 416);
assert_eq!(runner.dispatcher.screen_mode.4, 4);
assert_eq!(runner.bus.read_word(addr::SCREEN_ROW), 416);
assert_eq!(runner.bus.read_word(addr::SCREEN_BITS + 4), 416);
assert_eq!(runner.bus.read_word(pixmap + 4), 0x8000 | 416);
assert_eq!(runner.bus.read_word(pixmap + 32), 4);
assert_eq!(runner.bus.read_word(pixmap + 36), 4);
assert_eq!(runner.bus.read_word(ctab + 6), 15);
assert_eq!(runner.bus.read_long(gdevice + 42), 0x0082);
assert!(!runner.bus.addressing_32_bit());
assert_eq!(runner.dispatcher.mmu_mode, 0);
}
#[test]
fn one_bit_runner_publishes_consistent_screen_metadata() {
use crate::memory::globals::addr;
let config = FixtureRunnerConfig::default()
.with_screen_depth(1)
.expect("1-bit monochrome mode should be supported");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, config);
let gdevice_handle = runner.dispatcher.ensure_main_gdevice(&mut runner.bus);
let gdevice = runner.bus.read_long(gdevice_handle);
let pixmap = runner.bus.read_long(runner.bus.read_long(gdevice + 22));
let ctab = runner.bus.read_long(runner.bus.read_long(pixmap + 42));
assert_eq!(runner.dispatcher.screen_mode.1, 112);
assert_eq!(runner.dispatcher.screen_mode.4, 1);
assert_eq!(runner.bus.read_word(addr::SCREEN_ROW), 112);
assert_eq!(runner.bus.read_word(addr::SCREEN_BITS + 4), 112);
assert_eq!(runner.bus.read_word(pixmap + 4), 0x8000 | 112);
assert_eq!(runner.bus.read_word(pixmap + 32), 1);
assert_eq!(runner.bus.read_word(pixmap + 36), 1);
assert_eq!(runner.bus.read_word(ctab + 6), 1);
assert_eq!(runner.bus.read_long(gdevice + 42), 0x0080);
}
#[test]
fn two_bit_runner_publishes_consistent_screen_metadata() {
use crate::memory::globals::addr;
let config = FixtureRunnerConfig::default()
.with_screen_depth(2)
.expect("2-bit indexed mode should be supported");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, config);
let gdevice_handle = runner.dispatcher.ensure_main_gdevice(&mut runner.bus);
let gdevice = runner.bus.read_long(gdevice_handle);
let pixmap = runner.bus.read_long(runner.bus.read_long(gdevice + 22));
let ctab = runner.bus.read_long(runner.bus.read_long(pixmap + 42));
assert_eq!(runner.configured_screen_depth(), 2);
assert_eq!(runner.dispatcher.screen_mode.1, 208);
assert_eq!(runner.dispatcher.screen_mode.4, 2);
assert_eq!(runner.bus.read_word(addr::SCREEN_ROW), 208);
assert_eq!(runner.bus.read_word(addr::SCREEN_BITS + 4), 208);
assert_eq!(runner.bus.read_word(pixmap + 4), 0x8000 | 208);
assert_eq!(runner.bus.read_word(pixmap + 32), 2);
assert_eq!(runner.bus.read_word(pixmap + 36), 2);
assert_eq!(runner.bus.read_word(ctab + 6), 3);
assert_eq!(runner.bus.read_long(gdevice + 42), 0x0081);
}
#[test]
fn runner_config_rejects_nonselectable_screen_depths() {
assert!(FixtureRunnerConfig::default().with_screen_depth(3).is_err());
assert!(FixtureRunnerConfig::default().with_screen_depth(5).is_err());
}
#[test]
fn runner_config_preserves_architecture_defaults_until_depth_is_explicit() {
let default_runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
assert_eq!(default_runner.configured_screen_depth(), 8);
assert_eq!(default_runner.configured_powerpc_screen_depth(), 16);
for depth in [1, 2, 4, 8] {
let config = FixtureRunnerConfig::default()
.with_screen_depth(depth)
.expect("indexed depth should be supported");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, config);
assert_eq!(
runner.configured_powerpc_screen_depth(),
16,
"68K config depth {depth} must not become an implicit PPC override"
);
runner
.set_powerpc_screen_depth(depth)
.expect("PowerPC indexed depth should be supported");
assert_eq!(runner.configured_screen_depth(), depth);
assert_eq!(runner.configured_powerpc_screen_depth(), u32::from(depth));
}
let direct_nondefault = FixtureRunner::new(
8 * 1024 * 1024,
FixtureRunnerConfig {
screen_depth: 4,
..FixtureRunnerConfig::default()
},
);
assert_eq!(direct_nondefault.configured_powerpc_screen_depth(), 16);
let mut explicit_eight =
FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
explicit_eight.set_powerpc_screen_depth(8).unwrap();
assert_eq!(explicit_eight.configured_powerpc_screen_depth(), 8);
let mut default_runner =
FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
assert!(default_runner.set_powerpc_screen_depth(3).is_err());
}
#[derive(Clone, Default)]
struct CapturingAudioBackend {
stereo_samples: Rc<RefCell<Vec<u8>>>,
}
impl CapturingAudioBackend {
fn new() -> (Self, Rc<RefCell<Vec<u8>>>) {
let stereo_samples = Rc::new(RefCell::new(Vec::new()));
(
Self {
stereo_samples: stereo_samples.clone(),
},
stereo_samples,
)
}
}
impl AudioBackend for CapturingAudioBackend {
fn queue_samples(&mut self, samples: &[u8]) {
self.stereo_samples.borrow_mut().extend(samples);
}
fn queue_stereo_samples(&mut self, samples: &[u8]) {
self.stereo_samples.borrow_mut().extend(samples);
}
fn stop(&mut self) {}
}
fn test_region_handle(
bus: &mut crate::memory::MacMemoryBus,
top: i16,
left: i16,
bottom: i16,
right: i16,
) -> u32 {
let rgn_ptr = 0x0030_0100;
let rgn_handle = 0x0030_0140;
bus.write_long(rgn_handle, rgn_ptr);
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);
rgn_handle
}
#[test]
fn vfs_file_snapshot_round_trips_both_forks_and_metadata() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner
.dispatcher
.vfs
.insert("Pilots/Test Pilot".to_string(), vec![1, 2, 3]);
runner
.dispatcher
.vfs_rsrc
.insert("Pilots/Test Pilot".to_string(), vec![4, 5, 6, 7]);
runner
.dispatcher
.vfs
.insert("__rsrc__Pilots/Test Pilot".to_string(), vec![0xEE]);
runner
.dispatcher
.vfs
.insert("Game Data/Shapes".to_string(), vec![0xAA; 1024]);
runner
.dispatcher
.set_vfs_entry_metadata("Pilots/Test Pilot", *b"PIL ", *b"EVO!", 0x4000);
runner
.dispatcher
.set_vfs_entry_metadata("Game Data/Shapes", *b"shap", *b"26.2", 0);
let summaries = runner.vfs_file_summaries();
assert_eq!(summaries.len(), 2);
assert!(summaries
.iter()
.any(|summary| summary.path == "Game Data/Shapes"));
let summaries = runner.vfs_file_summaries_where(|path| path.starts_with("Pilots/"));
assert_eq!(summaries.len(), 1);
assert_eq!(summaries[0].path, "Pilots/Test Pilot");
assert_eq!(summaries[0].data_len, 3);
assert_eq!(summaries[0].resource_len, 4);
assert_eq!(summaries[0].file_type, u32::from_be_bytes(*b"PIL "));
assert_eq!(summaries[0].creator, u32::from_be_bytes(*b"EVO!"));
let snapshot = runner
.vfs_file_snapshot("Pilots/Test Pilot")
.expect("snapshot");
assert_eq!(snapshot.data_fork, vec![1, 2, 3]);
assert_eq!(snapshot.resource_fork, vec![4, 5, 6, 7]);
assert_eq!(snapshot.finder_flags, 0x4000);
let mut restored = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
restored.import_vfs_file(&snapshot);
assert_eq!(
restored.vfs_file_snapshot("Pilots/Test Pilot"),
Some(snapshot)
);
assert!(restored.remove_vfs_file("Pilots/Test Pilot"));
assert_eq!(restored.vfs_file_snapshot("Pilots/Test Pilot"), None);
}
#[test]
fn import_vfs_file_relative_to_launched_app_mounts_under_app_parent() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner
.dispatcher
.set_launched_app_path("EV Override/EV Override");
let plugin = VfsFileSnapshot {
path: "Warblade".to_string(),
data_fork: Vec::new(),
resource_fork: vec![1, 2, 3, 4],
file_type: u32::from_be_bytes(*b"Op.f"),
creator: u32::from_be_bytes(*b"Es.O"),
finder_flags: 0x4000,
created_date: 123,
modified_date: 456,
};
runner
.import_vfs_file_relative_to_launched_app("EV Plug-Ins", &plugin)
.expect("relative plugin import");
let mounted = runner
.vfs_file_snapshot("EV Override/EV Plug-Ins/Warblade")
.expect("mounted plugin snapshot");
assert_eq!(mounted.resource_fork, plugin.resource_fork);
assert_eq!(mounted.file_type, plugin.file_type);
assert_eq!(mounted.creator, plugin.creator);
assert_eq!(mounted.finder_flags, plugin.finder_flags);
let parent_dir_id = runner
.dispatcher
.vfs_metadata
.get("EV Override/EV Plug-Ins/Warblade")
.expect("plugin metadata")
.parent_dir_id;
let entries = runner.dispatcher.list_vfs_catalog_entries(parent_dir_id);
assert!(entries
.iter()
.any(|entry| !entry.is_directory && entry.name == "Warblade"));
}
fn make_resource_fork_bytes(resources: &[([u8; 4], i16, &[u8])]) -> Vec<u8> {
let mut type_groups: Vec<([u8; 4], Vec<(i16, &[u8], u32)>)> = Vec::new();
for (res_type, res_id, data) in resources {
let group_idx = type_groups
.iter()
.position(|(existing_type, _)| existing_type == res_type)
.unwrap_or_else(|| {
type_groups.push((*res_type, Vec::new()));
type_groups.len() - 1
});
type_groups[group_idx].1.push((*res_id, *data, 0));
}
type_groups.sort_by_key(|(res_type, _)| *res_type);
for (_, entries) in &mut type_groups {
entries.sort_by_key(|(res_id, _, _)| *res_id);
}
let data_offset = 16u32;
let mut data_section = Vec::new();
for (_, entries) in &mut type_groups {
for (_, data, data_pos) in entries {
*data_pos = data_section.len() as u32;
data_section.extend_from_slice(&(data.len() as u32).to_be_bytes());
data_section.extend_from_slice(data);
}
}
let map_offset = data_offset + data_section.len() as u32;
let type_list_offset = 30u16;
let type_count = type_groups.len();
let resource_count: usize = type_groups.iter().map(|(_, entries)| entries.len()).sum();
let ref_lists_offset = 2 + type_count * 8;
let name_list_offset = type_list_offset as usize + ref_lists_offset + resource_count * 12;
let map_length = name_list_offset as u32;
let mut bytes = vec![0u8; (map_offset + map_length) as usize];
let mut header = [0u8; 16];
header[0..4].copy_from_slice(&data_offset.to_be_bytes());
header[4..8].copy_from_slice(&map_offset.to_be_bytes());
header[8..12].copy_from_slice(&(data_section.len() as u32).to_be_bytes());
header[12..16].copy_from_slice(&map_length.to_be_bytes());
bytes[0..16].copy_from_slice(&header);
bytes[data_offset as usize..data_offset as usize + data_section.len()]
.copy_from_slice(&data_section);
let map_start = map_offset as usize;
bytes[map_start..map_start + 16].copy_from_slice(&header);
bytes[map_start + 24..map_start + 26].copy_from_slice(&type_list_offset.to_be_bytes());
bytes[map_start + 26..map_start + 28]
.copy_from_slice(&(name_list_offset as u16).to_be_bytes());
bytes[map_start + 28..map_start + 30]
.copy_from_slice(&((type_count as u16) - 1).to_be_bytes());
let type_list_start = map_start + type_list_offset as usize;
bytes[type_list_start..type_list_start + 2]
.copy_from_slice(&((type_count as u16) - 1).to_be_bytes());
let mut next_ref_list_offset = ref_lists_offset;
for (i, (res_type, entries)) in type_groups.iter().enumerate() {
let type_entry = type_list_start + 2 + i * 8;
bytes[type_entry..type_entry + 4].copy_from_slice(res_type);
bytes[type_entry + 4..type_entry + 6]
.copy_from_slice(&((entries.len() as u16) - 1).to_be_bytes());
bytes[type_entry + 6..type_entry + 8]
.copy_from_slice(&(next_ref_list_offset as u16).to_be_bytes());
let ref_list_start = type_list_start + next_ref_list_offset;
for (j, (res_id, _, data_pos)) in entries.iter().enumerate() {
let ref_entry = ref_list_start + j * 12;
bytes[ref_entry..ref_entry + 2].copy_from_slice(&(*res_id as u16).to_be_bytes());
bytes[ref_entry + 2..ref_entry + 4].copy_from_slice(&0xFFFFu16.to_be_bytes());
bytes[ref_entry + 4] = 0;
let data_offset_bytes = data_pos.to_be_bytes();
bytes[ref_entry + 5..ref_entry + 8].copy_from_slice(&data_offset_bytes[1..4]);
}
next_ref_list_offset += entries.len() * 12;
}
bytes
}
fn minimal_code0(above_a5: u32, below_a5: u32, jt_size: u32, jt_offset: u32) -> Vec<u8> {
let mut code0 = Vec::with_capacity(16 + jt_size as usize);
code0.extend_from_slice(&above_a5.to_be_bytes());
code0.extend_from_slice(&below_a5.to_be_bytes());
code0.extend_from_slice(&jt_size.to_be_bytes());
code0.extend_from_slice(&jt_offset.to_be_bytes());
code0.resize(16 + jt_size as usize, 0);
code0
}
fn size_resource_bytes(flags: u16, preferred_size: u32, minimum_size: u32) -> Vec<u8> {
let mut size = Vec::with_capacity(10);
size.extend_from_slice(&flags.to_be_bytes());
size.extend_from_slice(&preferred_size.to_be_bytes());
size.extend_from_slice(&minimum_size.to_be_bytes());
size
}
#[test]
fn init_app_preserves_resources_allocated_before_zone_header() {
let code0 = minimal_code0(0, 0x2000, 0, 0);
let bgas = [0x4E, 0x56, 0xFF, 0xA6, 0x2D, 0x7A, 0x1C, 0x72];
let fork_bytes = make_resource_fork_bytes(&[(*b"BGAS", 128, &bgas), (*b"CODE", 0, &code0)]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = runner.load_app(&fork).expect("load app");
let (_, bgas_ptr) = runner
.dispatcher
.find_or_load_resource_any(&mut runner.bus, *b"BGAS", 128)
.expect("BGAS resource loaded");
assert_eq!(runner.bus.read_bytes(bgas_ptr, bgas.len()), bgas);
runner.init_app(&app);
assert_eq!(
runner.bus.read_bytes(bgas_ptr, bgas.len()),
bgas,
"init_app must not overwrite resources loaded before zone setup"
);
}
#[test]
fn init_app_seeds_post_boot_ticks() {
use crate::memory::globals::addr;
let code0 = minimal_code0(0, 0x2000, 0, 0);
let fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &code0)]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = runner.load_app(&fork).expect("load app");
runner.init_app(&app);
assert_eq!(
runner.bus.read_long(addr::TICKS),
DEFAULT_LAUNCH_TICKS,
"fresh app launch should see a realistic nonzero post-boot TickCount"
);
assert_eq!(
runner.dispatcher.tick_count, DEFAULT_LAUNCH_TICKS,
"TickCount fast path must stay in sync with low-memory Ticks"
);
}
#[test]
fn init_app_applies_pinned_68k_launch_state() {
use crate::memory::globals::addr;
let code0 = minimal_code0(0, 0x2000, 0, 0);
let fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &code0)]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_launch_state(1234, 0x89AB_CDEF, 0x1122_3344_5566_7788);
let app = runner.load_app(&fork).expect("load app");
runner.init_app(&app);
assert_eq!(runner.bus.read_long(addr::TICKS), 1234);
assert_eq!(runner.dispatcher.tick_count, 1234);
assert_eq!(runner.bus.read_long(addr::RND_SEED), 0x89AB_CDEF);
}
#[test]
fn optional_68k_launch_state_uses_tick_floor_and_exact_seed() {
use crate::memory::globals::addr;
let code0 = minimal_code0(0, 0x2000, 0, 0);
let fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &code0)]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_optional_launch_state(Some(10), Some(0x1020_3040), None);
let app = runner.load_app(&fork).expect("load app");
runner.init_app(&app);
assert_eq!(runner.bus.read_long(addr::TICKS), DEFAULT_LAUNCH_TICKS);
assert_eq!(runner.dispatcher.tick_count, DEFAULT_LAUNCH_TICKS);
assert_eq!(runner.bus.read_long(addr::RND_SEED), 0x1020_3040);
}
#[test]
fn init_ppc_app_applies_pinned_launch_state_to_both_memories() {
use crate::memory::globals::addr;
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_launch_state(4321, 0x7654_3210, 0x8877_6655_4433_2211);
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
app.ppc
.as_mut()
.expect("synthetic PPC app")
.memory
.add_region(PPC_HALT_PC, vec![0; 64 * 1024]);
runner.init_app(&app);
assert_eq!(runner.bus.read_long(addr::TICKS), 4321);
assert_eq!(runner.dispatcher.tick_count, 4321);
assert_eq!(runner.bus.read_long(addr::RND_SEED), 0x7654_3210);
let ppc_app = runner.ppc_app.as_mut().expect("PPC app installed");
assert_eq!(ppc_app.tick_count, 4321);
assert_eq!(ppc_app.memory.read_u32_be(addr::TICKS), Some(4321));
assert_eq!(
ppc_app.memory.read_u32_be(addr::RND_SEED),
Some(0x7654_3210)
);
assert_eq!(ppc_app.cpu.time_base(), 0x8877_6655_4433_2211);
}
#[test]
fn optional_ppc_seed_preserves_tick_and_time_base_defaults() {
use crate::memory::globals::addr;
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_optional_launch_state(None, Some(0x1020_3040), None);
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
app.ppc
.as_mut()
.expect("synthetic PPC app")
.memory
.add_region(PPC_HALT_PC, vec![0; 64 * 1024]);
runner.init_app(&app);
assert_eq!(runner.bus.read_long(addr::TICKS), 0);
assert_eq!(runner.dispatcher.tick_count, 0);
assert_eq!(runner.bus.read_long(addr::RND_SEED), 0x1020_3040);
let ppc_app = runner.ppc_app.as_mut().expect("PPC app installed");
assert_eq!(ppc_app.memory.read_u32_be(addr::TICKS), Some(0));
assert_eq!(
ppc_app.memory.read_u32_be(addr::RND_SEED),
Some(0x1020_3040)
);
assert_eq!(ppc_app.cpu.time_base(), 0);
}
#[test]
fn repeated_partial_launch_state_updates_preserve_prior_controls() {
use crate::memory::globals::addr;
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_optional_launch_state(Some(4321), None, Some(0x1122_3344_5566_7788));
runner.set_optional_launch_state(None, Some(0x7654_3210), None);
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
app.ppc
.as_mut()
.expect("synthetic PPC app")
.memory
.add_region(PPC_HALT_PC, vec![0; 64 * 1024]);
runner.init_app(&app);
assert_eq!(runner.bus.read_long(addr::TICKS), 4321);
assert_eq!(runner.bus.read_long(addr::RND_SEED), 0x7654_3210);
let ppc_app = runner.ppc_app.as_mut().expect("PPC app installed");
assert_eq!(ppc_app.memory.read_u32_be(addr::TICKS), Some(4321));
assert_eq!(
ppc_app.memory.read_u32_be(addr::RND_SEED),
Some(0x7654_3210)
);
assert_eq!(ppc_app.cpu.time_base(), 0x1122_3344_5566_7788);
}
#[test]
fn init_ppc_app_preserves_launch_defaults_without_override() {
use crate::memory::globals::addr;
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_app_start_time(0x1020_3040);
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
app.ppc
.as_mut()
.expect("synthetic PPC app")
.memory
.add_region(PPC_HALT_PC, vec![0; 64 * 1024]);
runner.init_app(&app);
assert_eq!(runner.bus.read_long(addr::TICKS), 0);
assert_eq!(runner.dispatcher.tick_count, 0);
assert_eq!(runner.bus.read_long(addr::RND_SEED), 0x1020_3040);
let ppc_app = runner.ppc_app.as_mut().expect("PPC app installed");
assert_eq!(ppc_app.tick_count, 0);
assert_eq!(ppc_app.memory.read_u32_be(addr::TICKS), Some(0));
assert_eq!(ppc_app.memory.read_u32_be(addr::TIME), Some(0x1020_3040));
assert_eq!(
ppc_app.memory.read_u32_be(addr::RND_SEED),
Some(0x1020_3040)
);
assert_eq!(ppc_app.cpu.time_base(), 0);
}
#[test]
fn ppc_slice_keeps_ticks_coherent_across_runner_and_guest_memory() {
use crate::memory::globals::addr;
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
let ppc_app = app.ppc.as_mut().expect("synthetic PPC app");
ppc_app.cpu.alignment_policy = ppc::PpcAlignmentPolicy::EmulateData;
ppc_app
.memory
.write_u32_be(PPC_CODE_BASE, 0x4800_0000)
.expect("rewrite entry as infinite branch");
ppc_app.memory.add_region(PPC_HALT_PC, vec![0; 64 * 1024]);
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_launch_state(41, 0x1234_5678, 0);
runner.init_app(&app);
runner.set_instructions_per_tick(1_000_000);
runner.tick_budget = 1;
let (steps, running) = runner.run_steps(1, None);
assert_eq!(steps, 1);
assert!(running);
assert_eq!(runner.bus.read_long(addr::TICKS), 42);
assert_eq!(runner.dispatcher.tick_count, 42);
let ppc_app = runner.ppc_app.as_mut().expect("PPC app installed");
assert_eq!(ppc_app.tick_count, 42);
assert_eq!(ppc_app.memory.read_u32_be(addr::TICKS), Some(42));
}
#[test]
fn ppc_slice_shares_low_memory_time_after_sixtieth_tick() {
use crate::memory::globals::addr;
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
let ppc_app = app.ppc.as_mut().expect("synthetic PPC app");
ppc_app.cpu.alignment_policy = ppc::PpcAlignmentPolicy::EmulateData;
ppc_app
.memory
.write_u32_be(PPC_CODE_BASE, 0x4800_0000)
.expect("rewrite entry as infinite branch");
ppc_app.memory.add_region(PPC_HALT_PC, vec![0; 64 * 1024]);
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_app_start_time(0x1020_3040);
runner.set_launch_state(59, 1, 0);
runner.init_app(&app);
runner.set_instructions_per_tick(1);
let (steps, running) = runner.run_steps(1, None);
assert_eq!(steps, 1);
assert!(running);
assert_eq!(runner.bus.read_long(addr::TICKS), 60);
assert_eq!(runner.bus.read_long(addr::TIME), 0x1020_3041);
let ppc_app = runner.ppc_app.as_mut().expect("PPC app installed");
assert_eq!(ppc_app.memory.read_u32_be(addr::TICKS), Some(60));
assert_eq!(ppc_app.memory.read_u32_be(addr::TIME), Some(0x1020_3041));
}
#[test]
fn tick_count_poll_fast_forward_returns_for_due_ppc_vbl_callback() {
use crate::loader::ppc::PpcVblTaskRecord;
use crate::memory::globals::addr;
const LOOP: u32 = PPC_CODE_BASE + 0x1000;
const CALLBACK: u32 = PPC_CODE_BASE + 0x2000;
const TASK: u32 = PPC_DATA_BASE + 0x3000;
const CALLBACK_TICK: u32 = PPC_DATA_BASE + 0x3100;
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
let ppc_app = app.ppc.as_mut().expect("synthetic PPC app");
ppc_app.cpu.alignment_policy = ppc::PpcAlignmentPolicy::EmulateData;
ppc_app.memory.add_region(PPC_HALT_PC, vec![0; 64 * 1024]);
ppc_app.cpu.pc = LOOP;
ppc_app.memory.add_region(
LOOP,
[
0x3d80_01f0u32, 0x618c_0000, 0x7d89_03a6, 0x4e80_0421, 0x4bff_fffc, ]
.into_iter()
.flat_map(u32::to_be_bytes)
.collect(),
);
ppc_app.memory.add_region(
CALLBACK,
[
0x8060_016au32, 0x3c80_0200, 0x9064_3100, 0x4e80_0020, ]
.into_iter()
.flat_map(u32::to_be_bytes)
.collect(),
);
ppc_app.memory.add_region(TASK, vec![0; 0x104]);
ppc_app.memory.write_u32_be(TASK + 6, CALLBACK).unwrap();
ppc_app.memory.write_u16_be(TASK + 10, 1).unwrap();
ppc_app.vbl_tasks.push(PpcVblTaskRecord { task_ptr: TASK });
let mut tick_count = test_ppc_import_binding(0, "InterfaceLib", "LMGetTicks");
tick_count.address = PPC_IMPORT_TRAP_BASE;
tick_count.trap_pc = PPC_IMPORT_TRAP_BASE;
tick_count.dispatcher_target = PpcImportDispatcherTarget::TickCount;
ppc_app.import_count = 1;
ppc_app.imports = vec![tick_count];
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_optional_launch_state(Some(41), None, None);
runner.init_app(&app);
runner.set_instructions_per_tick(1_000);
let (_steps, running) = runner.run_steps(1_000, None);
assert!(running);
assert_eq!(runner.bus.read_long(addr::TICKS), 42);
let ppc_app = runner.ppc_app.as_mut().expect("PPC app installed");
assert_eq!(ppc_app.memory.read_u32_be(CALLBACK_TICK), Some(42));
}
#[test]
fn batched_ppc_vbl_callbacks_read_their_own_tick_from_low_memory() {
use crate::loader::ppc::PpcVblTaskRecord;
use crate::memory::globals::addr;
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
app.ppc
.as_mut()
.expect("synthetic PPC app")
.memory
.add_region(PPC_HALT_PC, vec![0; 64 * 1024]);
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_app_start_time(0x1020_3040);
runner.set_optional_launch_state(Some(41), None, None);
runner.init_app(&app);
let mut ppc_app = runner.ppc_app.take().expect("PPC app installed");
ppc_app.cpu.alignment_policy = ppc::PpcAlignmentPolicy::EmulateData;
for (index, count) in [1u16, 2].into_iter().enumerate() {
let task = PPC_DATA_BASE + 0x1000 + index as u32 * 0x100;
let callback = task + 0x20;
ppc_app.memory.add_region(task, vec![0; 0x60]);
ppc_app.memory.write_u32_be(task + 6, callback).unwrap();
ppc_app.memory.write_u16_be(task + 10, count).unwrap();
for (offset, instruction) in [
0x8060_016au32, 0x4e80_0020, ]
.into_iter()
.enumerate()
{
ppc_app
.memory
.write_u32_be(callback + offset as u32 * 4, instruction)
.unwrap();
}
ppc_app.vbl_tasks.push(PpcVblTaskRecord { task_ptr: task });
}
let (vbl, timer) =
runner.fire_ppc_tick_callbacks(&mut ppc_app, 41, 0x1020_3040, 2, 64, false, false);
assert_eq!(vbl.len(), 2);
assert!(timer.is_empty());
assert_eq!(vbl[0].invocation.tick, 42);
assert_eq!(vbl[0].invocation.end_r3, 42);
assert_eq!(vbl[1].invocation.tick, 43);
assert_eq!(vbl[1].invocation.end_r3, 43);
assert_eq!(ppc_app.memory.read_u32_be(addr::TICKS), Some(43));
}
#[test]
fn ppc_slice_shares_rnd_seed_in_both_directions() {
use crate::memory::globals::addr;
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
let ppc_app = app.ppc.as_mut().expect("synthetic PPC app");
ppc_app.cpu.alignment_policy = ppc::PpcAlignmentPolicy::EmulateData;
ppc_app
.memory
.write_u32_be(PPC_CODE_BASE, 0x3C60_1234) .unwrap();
ppc_app.memory.add_region(
PPC_CODE_BASE + 4,
[
0x6063_5678u32, 0x3880_0156, 0x9064_0000, 0x4800_0000, ]
.into_iter()
.flat_map(u32::to_be_bytes)
.collect(),
);
ppc_app.memory.add_region(PPC_HALT_PC, vec![0; 64 * 1024]);
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_launch_state(0, 1, 0);
runner.init_app(&app);
runner.set_instructions_per_tick(100);
let (steps, running) = runner.run_steps(5, None);
assert!(steps > 0);
assert!(running);
assert_eq!(runner.bus.read_long(addr::RND_SEED), 0x1234_5678);
runner.bus.write_long(addr::RND_SEED, 0x89AB_CDEF);
let (steps, running) = runner.run_steps(1, None);
assert_eq!(steps, 1);
assert!(running);
let ppc_app = runner.ppc_app.as_mut().expect("PPC app installed");
assert_eq!(
ppc_app.memory.read_u32_be(addr::RND_SEED),
Some(0x89AB_CDEF)
);
assert_eq!(runner.bus.read_long(addr::RND_SEED), 0x89AB_CDEF);
}
#[test]
fn ppc_runtime_globals_have_immediate_bidirectional_visibility() {
use crate::memory::globals::addr;
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
app.ppc
.as_mut()
.expect("synthetic PPC app")
.memory
.add_region(PPC_HALT_PC, vec![0; 64 * 1024]);
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_optional_launch_state(None, Some(1), None);
runner.init_app(&app);
let mut ppc_app = runner.ppc_app.take().expect("PPC app installed");
for (address, ppc_value, runner_value) in [
(addr::RND_SEED, 0x1234_5678, 0x89ab_cdef),
(addr::TICKS, 0x1122_3344, 0x5566_7788),
(addr::TIME, 0x1020_3040, 0x5060_7080),
] {
ppc_app.memory.write_u32_be(address, ppc_value).unwrap();
assert_eq!(runner.bus.read_long(address), ppc_value);
runner.bus.write_long(address, runner_value);
assert_eq!(ppc_app.memory.read_u32_be(address), Some(runner_value));
}
}
#[test]
fn ppc_input_globals_have_immediate_bidirectional_visibility() {
use crate::memory::globals::addr;
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
app.ppc
.as_mut()
.expect("synthetic PPC app")
.memory
.add_region(PPC_HALT_PC, vec![0; 64 * 1024]);
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
let mut ppc_app = runner.ppc_app.take().expect("PPC app installed");
assert_eq!(
runner.bus.read_word(addr::SYS_EVT_MASK),
crate::memory::globals::DEFAULT_SYS_EVT_MASK
);
assert_eq!(
ppc_app.memory.read_u16_be(addr::SYS_EVT_MASK),
Some(crate::memory::globals::DEFAULT_SYS_EVT_MASK)
);
ppc_app
.memory
.write_u16_be(addr::SYS_EVT_MASK, 0xffdf)
.unwrap();
assert_eq!(runner.bus.read_word(addr::SYS_EVT_MASK), 0xffdf);
runner.bus.write_word(addr::SYS_EVT_MASK, 0x1234);
assert_eq!(ppc_app.memory.read_u16_be(addr::SYS_EVT_MASK), Some(0x1234));
let mut detached = ppc_app.memory.clone();
detached.write_u16_be(addr::SYS_EVT_MASK, 0xabcd).unwrap();
assert_eq!(runner.bus.read_word(addr::SYS_EVT_MASK), 0x1234);
assert_eq!(ppc_app.memory.read_u16_be(addr::SYS_EVT_MASK), Some(0x1234));
ppc_app.memory.write_u8(addr::MB_STATE, 0).unwrap();
assert_eq!(runner.bus.read_byte(addr::MB_STATE), 0);
runner.bus.write_byte(addr::MB_STATE, 0x80);
assert_eq!(ppc_app.memory.read_u8(addr::MB_STATE), Some(0x80));
let ppc_key_map = [
0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
0x77, 0x88,
];
ppc_app
.memory
.write_bytes(addr::KEY_MAP_LM, &ppc_key_map)
.unwrap();
assert_eq!(runner.bus.read_bytes(addr::KEY_MAP_LM, 16), ppc_key_map);
let runner_key_map = [
0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20,
0x40, 0x80,
];
runner.bus.write_bytes(addr::KEY_MAP_LM, &runner_key_map);
let mut observed_key_map = [0; 16];
ppc_app
.memory
.read_bytes_into(addr::KEY_MAP_LM, &mut observed_key_map)
.unwrap();
assert_eq!(observed_key_map, runner_key_map);
let ppc_points = [
0x01, 0x02, 0x03, 0x04, 0x11, 0x12, 0x13, 0x14, 0x21, 0x22, 0x23, 0x24,
];
ppc_app
.memory
.write_bytes(addr::M_TEMP, &ppc_points)
.unwrap();
assert_eq!(runner.bus.read_bytes(addr::M_TEMP, 12), ppc_points);
let runner_points = [
0x24, 0x23, 0x22, 0x21, 0x14, 0x13, 0x12, 0x11, 0x04, 0x03, 0x02, 0x01,
];
runner.bus.write_bytes(addr::M_TEMP, &runner_points);
let mut observed_points = [0; 12];
ppc_app
.memory
.read_bytes_into(addr::M_TEMP, &mut observed_points)
.unwrap();
assert_eq!(observed_points, runner_points);
let native_key_map = [
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54,
0x32, 0x10,
];
ppc_app.set_input_snapshot(PpcInputSnapshot {
key_map: native_key_map,
mouse_button: true,
mouse_v: 123,
mouse_h: 456,
});
assert_eq!(runner.bus.read_byte(addr::MB_STATE), 0);
assert_eq!(runner.bus.read_bytes(addr::KEY_MAP_LM, 16), native_key_map);
for point_addr in [addr::M_TEMP, addr::MOUSE_LOC, addr::MOUSE_LOC2] {
assert_eq!(runner.bus.read_word(point_addr), 123);
assert_eq!(runner.bus.read_word(point_addr + 2), 456);
}
runner.push_key_down(6, b'z');
runner.push_mouse_down(-7, 321);
let mut host_key_map = [0; 16];
ppc_app
.memory
.read_bytes_into(addr::KEY_MAP_LM, &mut host_key_map)
.unwrap();
assert_eq!(host_key_map, runner.dispatcher.key_map);
assert_eq!(ppc_app.memory.read_u8(addr::MB_STATE), Some(0));
for point_addr in [addr::M_TEMP, addr::MOUSE_LOC, addr::MOUSE_LOC2] {
assert_eq!(ppc_app.memory.read_u16_be(point_addr), Some((-7i16) as u16));
assert_eq!(ppc_app.memory.read_u16_be(point_addr + 2), Some(321));
}
ppc_app
.memory
.write_u32_be(addr::THE_ZONE, 0x1122_3344)
.unwrap();
assert_ne!(runner.bus.read_long(addr::THE_ZONE), 0x1122_3344);
runner.bus.write_long(addr::THE_ZONE, 0x5566_7788);
assert_eq!(
ppc_app.memory.read_u32_be(addr::THE_ZONE),
Some(0x1122_3344)
);
}
#[test]
fn ppc_event_queue_has_immediate_bidirectional_visibility() {
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
let app_ppc = app.ppc.as_mut().expect("synthetic PPC app");
app_ppc.memory.add_region(PPC_HALT_PC, vec![0; 64 * 1024]);
app_ppc.event_queue.push_back(QueuedEvent {
what: 23,
message: 0xaabb_ccdd,
where_v: 1,
where_h: 2,
modifiers: 0,
});
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
assert_eq!(
runner
.dispatcher
.event_queue
.front()
.map(|event| event.message),
Some(0xaabb_ccdd)
);
let mut ppc_app = runner.ppc_app.take().expect("PPC app installed");
ppc_app.event_queue.pop_front();
assert!(runner.dispatcher.event_queue.is_empty());
let original = crate::trap::dispatch::QueuedEvent {
what: 3,
message: 0x1111,
where_v: 10,
where_h: 20,
modifiers: 0x0200,
};
runner.dispatcher.event_queue.push_back(original.clone());
let consumed = ppc_app.event_queue.pop_front().unwrap();
assert_eq!(consumed.message, original.message);
assert!(runner.dispatcher.event_queue.is_empty());
ppc_app.event_queue.push_back(original.clone());
assert_eq!(runner.dispatcher.event_queue.front(), Some(&original));
runner.dispatcher.event_queue.pop_front();
assert!(ppc_app.event_queue.is_empty());
runner.dispatcher.event_queue.push_back(original);
let reposted = ppc_app.event_queue.front().unwrap();
assert_eq!(reposted.what, 3);
assert_eq!(reposted.message, 0x1111);
assert_eq!((reposted.where_v, reposted.where_h), (10, 20));
assert_eq!(reposted.modifiers, 0x0200);
}
#[test]
fn init_app_propagates_size_high_level_event_capability() {
let code0 = minimal_code0(0, 0x2000, 0, 0);
let unaware_size = size_resource_bytes(0, 0x0008_0000, 0x0008_0000);
let unaware_fork_bytes = make_resource_fork_bytes(&[
(*b"CODE", 0, code0.as_slice()),
(*b"SIZE", -1, unaware_size.as_slice()),
]);
let unaware_fork =
ResourceFork::parse(&unaware_fork_bytes).expect("parse unaware app fork");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let unaware_app = runner.load_app(&unaware_fork).expect("load unaware app");
runner.init_app(&unaware_app);
assert!(!runner.dispatcher.application_high_level_event_aware);
let aware_size = size_resource_bytes(
ApplicationSizeResource::HIGH_LEVEL_EVENT_AWARE,
0x0008_0000,
0x0008_0000,
);
let aware_fork_bytes = make_resource_fork_bytes(&[
(*b"CODE", 0, code0.as_slice()),
(*b"SIZE", -1, aware_size.as_slice()),
]);
let aware_fork = ResourceFork::parse(&aware_fork_bytes).expect("parse aware app fork");
let aware_app = runner.load_app(&aware_fork).expect("load aware app");
runner.init_app(&aware_app);
assert!(runner.dispatcher.application_high_level_event_aware);
}
#[test]
fn init_app_seeds_classic_double_click_interval() {
use crate::memory::globals::addr;
let code0 = minimal_code0(0, 0x2000, 0, 0);
let fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &code0)]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = runner.load_app(&fork).expect("load app");
runner.init_app(&app);
assert_eq!(
runner.bus.read_long(addr::DOUBLE_TIME),
DEFAULT_DOUBLE_TIME_TICKS,
"a zero DoubleTime makes every application-level double-click test fail"
);
}
#[test]
fn load_app_places_resources_above_large_loaded_image() {
use crate::memory::globals::addr;
let code0 = minimal_code0(0x001D_0000, 0x0340, 0, 0);
let marker = [0xCA, 0xFE, 0xBA, 0xBE, 0x12, 0x34, 0x56, 0x78];
let fork_bytes =
make_resource_fork_bytes(&[(*b"BGAS", 128, &marker), (*b"CODE", 0, &code0)]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = runner.load_app(&fork).expect("load app");
assert!(
app.loaded_image_end > APP_HEAP_FLOOR,
"fixture should force the loaded image across the default heap floor"
);
let heap_start = app_heap_start_for_loaded_app(&app);
let (_, marker_ptr) = runner
.dispatcher
.find_or_load_resource_any(&mut runner.bus, *b"BGAS", 128)
.expect("BGAS resource loaded");
assert!(
marker_ptr >= heap_start + APP_ZONE_HEADER_SIZE,
"resource data must be allocated after the relocated zone header"
);
assert_eq!(runner.bus.read_bytes(marker_ptr, marker.len()), marker);
runner.init_app(&app);
assert_eq!(runner.bus.read_long(addr::APP_L_ZONE), heap_start);
assert_eq!(
runner.bus.read_long(addr::HEAP_END),
heap_start + APP_ZONE_HEADER_SIZE
);
assert_eq!(
runner.bus.read_bytes(marker_ptr, marker.len()),
marker,
"launch initialization must not clobber resources for large loaded images"
);
}
#[test]
fn load_app_records_application_size_resource_id_minus_one() {
let code0 = minimal_code0(0, 0x2000, 0, 0);
let size = size_resource_bytes(0x0080, 0x0030_0000, 0x0020_0000);
let fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &code0), (*b"SIZE", -1, &size)]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = runner.load_app(&fork).expect("load app");
assert_eq!(
app.size_resource,
Some(ApplicationSizeResource {
flags: 0x0080,
preferred_size: 0x0030_0000,
minimum_size: 0x0020_0000,
})
);
}
#[test]
fn load_app_prefers_valid_size_resource_id_zero() {
let code0 = minimal_code0(0, 0x2000, 0, 0);
let original = size_resource_bytes(0x0040, 0x0030_0000, 0x0020_0000);
let finder_override = size_resource_bytes(0x0080, 0x0050_0000, 0x0040_0000);
let fork_bytes = make_resource_fork_bytes(&[
(*b"CODE", 0, &code0),
(*b"SIZE", -1, &original),
(*b"SIZE", 0, &finder_override),
]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(16 * 1024 * 1024, FixtureRunnerConfig::default());
let app = runner.load_app(&fork).expect("load app");
assert_eq!(
app.size_resource,
Some(ApplicationSizeResource {
flags: 0x0080,
preferred_size: 0x0050_0000,
minimum_size: 0x0040_0000,
})
);
}
#[test]
fn load_app_falls_back_to_size_resource_id_minus_one_when_id_zero_is_invalid() {
let code0 = minimal_code0(0, 0x2000, 0, 0);
let original = size_resource_bytes(0x0040, 0x0030_0000, 0x0020_0000);
let invalid_override = size_resource_bytes(0x0080, 0x0010_0000, 0x0020_0000);
let fork_bytes = make_resource_fork_bytes(&[
(*b"CODE", 0, &code0),
(*b"SIZE", -1, &original),
(*b"SIZE", 0, &invalid_override),
]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = runner.load_app(&fork).expect("load app");
assert_eq!(
app.size_resource,
Some(ApplicationSizeResource {
flags: 0x0040,
preferred_size: 0x0030_0000,
minimum_size: 0x0020_0000,
})
);
}
#[test]
fn load_app_relocates_large_size_partition_a5_above_application_zone() {
let minimum_partition = 4_812_800;
let preferred_partition = 6_348_800;
let below_a5 = 0x7AF4;
let code0 = minimal_code0(0x11F8, below_a5, 0, 0);
let size = size_resource_bytes(0x5880, preferred_partition, minimum_partition);
let fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &code0), (*b"SIZE", -1, &size)]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(32 * 1024 * 1024, FixtureRunnerConfig::default());
let app = runner.load_app(&fork).expect("load app");
assert!(
app_image_start_for_loaded_app(&app) > APP_HEAP_FLOOR + APP_ZONE_HEADER_SIZE,
"relocated app image must leave room for the visible app-zone header"
);
assert!(
app.a5_base - APP_HEAP_FLOOR >= minimum_partition - APP_STACK_SAFETY_MARGIN,
"large SIZE partitions should place A5 high enough for direct A5-zone memory checks"
);
assert!(
app.a5_base - APP_HEAP_FLOOR >= 750 * 1024,
"Spectre-style startup gates compare A5 - GetZone against a 750K floor"
);
}
#[test]
fn large_size_partition_does_not_overwrite_synthetic_callbacks() {
let below_a5 = 0x7AF4;
let code0 = minimal_code0(0x11F8, below_a5, 0, 0);
let partition = 63 * 1024 * 1024;
let size = size_resource_bytes(0x5880, partition, partition);
let fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &code0), (*b"SIZE", -1, &size)]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(64 * 1024 * 1024, FixtureRunnerConfig::default());
let callback = runner.bus.alloc_synthetic(2);
runner.bus.write_word(callback, 0x4E75);
let app = runner.load_app(&fork).expect("load app");
assert_eq!(runner.bus.read_word(callback), 0x4E75);
assert!(
app.loaded_image_end + APP_HIGH_MEMORY_RESERVE <= runner.bus.application_memory_limit(),
"the loaded image must leave room for the application heap and stack"
);
assert!(app.initial_sp < callback);
}
#[test]
fn load_app_relocates_exact_2mb_size_partition() {
let below_a5 = 0x68E8;
let code0 = minimal_code0(0x0D18, below_a5, 0, 0);
let size = size_resource_bytes(0x5880, 0x0020_0000, 0x0020_0000);
let fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &code0), (*b"SIZE", -1, &size)]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(32 * 1024 * 1024, FixtureRunnerConfig::default());
let app = runner.load_app(&fork).expect("load app");
assert!(app_image_start_for_loaded_app(&app) > APP_HEAP_FLOOR);
assert!(app.a5_base - APP_HEAP_FLOOR >= 0x0020_0000 - APP_STACK_SAFETY_MARGIN);
}
#[test]
fn load_app_relocates_sub_2mb_size_partition() {
let preferred_partition = 1_843_200;
let minimum_partition = 768_000;
let below_a5 = 29_116;
let code0 = minimal_code0(3_816, below_a5, 0, 0);
let size = size_resource_bytes(0x5880, preferred_partition, minimum_partition);
let fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &code0), (*b"SIZE", -1, &size)]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(32 * 1024 * 1024, FixtureRunnerConfig::default());
let app = runner.load_app(&fork).expect("load app");
assert!(app_image_start_for_loaded_app(&app) > APP_HEAP_FLOOR);
assert!(
app.a5_base - APP_HEAP_FLOOR >= preferred_partition - APP_STACK_SAFETY_MARGIN,
"sub-2 MiB SIZE partitions must still govern the classic A5 layout"
);
}
#[test]
fn size_partition_does_not_cap_shared_resource_fork_materialization() {
let code0 = minimal_code0(0, 0x2000, 0, 0);
let size = size_resource_bytes(0x5880, 4_194_304, 3_584_000);
let large_resource = vec![0xA5; 6_481_428];
let fork_bytes = make_resource_fork_bytes(&[
(*b"CODE", 0, &code0),
(*b"SHAP", 128, &large_resource),
(*b"SIZE", -1, &size),
]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(32 * 1024 * 1024, FixtureRunnerConfig::default());
let app = runner.load_app(&fork).expect("load app");
runner.init_app(&app);
let (_, resource_ptr) = runner
.dispatcher
.find_or_load_resource_any(&mut runner.bus, *b"SHAP", 128)
.expect("large resource registered");
assert_ne!(
resource_ptr, 0,
"large resource data must remain allocatable"
);
assert_eq!(runner.bus.read_byte(resource_ptr), 0xA5);
assert_eq!(
runner
.bus
.read_byte(resource_ptr + large_resource.len() as u32 - 1),
0xA5
);
}
#[test]
fn init_app_exposes_low_visible_zone_for_relocated_size_partition() {
use crate::memory::globals::addr;
let minimum_partition = 4_812_800;
let preferred_partition = 6_348_800;
let below_a5 = 0x7AF4;
let code0 = minimal_code0(0x11F8, below_a5, 0, 0);
let marker = [0xCA, 0xFE, 0xBA, 0xBE, 0x12, 0x34, 0x56, 0x78];
let size = size_resource_bytes(0x5880, preferred_partition, minimum_partition);
let fork_bytes = make_resource_fork_bytes(&[
(*b"BGAS", 128, &marker),
(*b"CODE", 0, &code0),
(*b"SIZE", -1, &size),
]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(32 * 1024 * 1024, FixtureRunnerConfig::default());
let app = runner.load_app(&fork).expect("load app");
let image_start = app_image_start_for_loaded_app(&app);
let (_, marker_ptr) = runner
.dispatcher
.find_or_load_resource_any(&mut runner.bus, *b"BGAS", 128)
.expect("BGAS resource loaded");
let marker_end = marker_ptr + marker.len() as u32;
assert!(
marker_end <= image_start || marker_ptr >= app.loaded_image_end,
"resource allocation must not overlap the relocated image"
);
runner.init_app(&app);
assert_eq!(runner.bus.read_long(addr::APP_L_ZONE), APP_HEAP_FLOOR);
assert_eq!(runner.bus.read_long(addr::THE_ZONE), APP_HEAP_FLOOR);
assert_eq!(
runner.bus.read_long(addr::HEAP_END),
APP_HEAP_FLOOR + APP_ZONE_HEADER_SIZE
);
assert_eq!(
runner.bus.read_long(APP_HEAP_FLOOR),
runner.bus.read_long(addr::APPL_LIMIT)
);
assert!(
app.a5_base - runner.bus.read_long(addr::THE_ZONE) >= 750 * 1024,
"GetZone-visible partition span should satisfy direct startup memory gates"
);
assert_eq!(runner.bus.read_bytes(marker_ptr, marker.len()), marker);
}
#[test]
fn load_app_patches_mpw_far_jump_table_offsets_from_segment_start() {
let mut code0 = minimal_code0(40, 0x2000, 8, 32);
code0[16..24].copy_from_slice(&[
0x00, 0x01, 0xA9, 0xF0, 0x00, 0x00, 0x00, 0x28, ]);
let mut code1 = vec![0u8; 0x30];
code1[0] = 0xFF;
code1[1] = 0xFF;
code1[0x28] = 0x4E;
code1[0x29] = 0x75;
let fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &code0), (*b"CODE", 1, &code1)]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = runner.load_app(&fork).expect("load app");
let jt_base = app.a5_base + app.code0_header.jump_table_offset;
let code1_base = app.segment_bases[&1];
assert_eq!(runner.bus.read_word(jt_base), 1);
assert_eq!(runner.bus.read_word(jt_base + 2), 0x4EF9);
assert_eq!(
runner.bus.read_long(jt_base + 4),
code1_base + 0x28,
"MPW far offsets must not be adjusted by the 40-byte header twice"
);
}
#[test]
fn load_app_applies_mpw_far_a5_and_pc_relocations() {
let mut code0 = minimal_code0(40, 0x2000, 8, 32);
code0[16..24].copy_from_slice(&[
0x00, 0x01, 0xA9, 0xF0, 0x00, 0x00, 0x00, 0x28,
]);
let mut code1 = vec![0u8; 0x60];
code1[0..2].copy_from_slice(&0xFFFFu16.to_be_bytes());
code1[20..24].copy_from_slice(&0x50u32.to_be_bytes());
code1[28..32].copy_from_slice(&0x54u32.to_be_bytes());
code1[0x28..0x2A].copy_from_slice(&[0x4E, 0xB9]); code1[0x2A..0x2E].copy_from_slice(&0x100u32.to_be_bytes());
code1[0x30..0x32].copy_from_slice(&[0x20, 0x79]); code1[0x32..0x36].copy_from_slice(&0x40u32.to_be_bytes());
code1[0x50..0x54].copy_from_slice(&[
0x19, 0x00, 0x00, 0x00,
]);
code1[0x54..0x57].copy_from_slice(&[
0x15, 0x00, 0x00,
]);
let fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &code0), (*b"CODE", 1, &code1)]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = runner.load_app(&fork).expect("load app");
let code1_base = app.segment_bases[&1];
assert_eq!(
runner.bus.read_long(code1_base + 0x2A),
code1_base + 0x100,
"PC relocation stream must add the loaded segment address"
);
assert_eq!(
runner.bus.read_long(code1_base + 0x32),
app.a5_base + 0x40,
"A5 relocation stream must add the current A5"
);
assert_eq!(
runner
.bus
.read_long(code1_base + MpwFarSegmentHeader::CURRENT_A5_OFFSET),
app.a5_base
);
assert_eq!(
runner
.bus
.read_long(code1_base + MpwFarSegmentHeader::LOAD_ADDRESS_OFFSET),
code1_base
);
}
#[test]
fn load_app_applies_retro68_code_and_data_relocations() {
let mut code0 = minimal_code0(40, 0x2000, 8, 32);
code0[16..24].copy_from_slice(&[
0x00, 0x01, 0xA9, 0xF0, 0x00, 0x00, 0x00, 0x28,
]);
let mut code1 = vec![0u8; 0x60];
code1[0..2].copy_from_slice(&0xFFFFu16.to_be_bytes());
for (offset, value) in [0x20u32, 0x30, 0x40, 0x50, 0x60].into_iter().enumerate() {
let start = MpwFarSegmentHeader::SIZE + offset * 4;
code1[start..start + 4].copy_from_slice(&value.to_be_bytes());
}
let rela1 = [
0x04, 0x11, 0x12, 0x13, 0x00, 0x44, 0x00, ];
let mut data = Vec::new();
for value in [0x10u32, 0x20, 0x30, 0x40] {
data.extend_from_slice(&value.to_be_bytes());
}
let rela0 = [
0x04, 0x11, 0x12, 0x13, 0x00, 0x00, ];
let fork_bytes = make_resource_fork_bytes(&[
(*b"CODE", 0, &code0),
(*b"CODE", 1, &code1),
(*b"DATA", 0, &data),
(*b"RELA", 0, &rela0),
(*b"RELA", 1, &rela1),
]);
let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic Retro68 app fork");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = runner.load_app(&fork).expect("load Retro68 app");
let code1_base = app.segment_bases[&1];
let code_body = code1_base + MpwFarSegmentHeader::SIZE as u32;
let data_base = app.a5_base - app.code0_header.below_a5;
assert_eq!(runner.bus.read_long(code_body), code1_base + 0x20);
assert_eq!(runner.bus.read_long(code_body + 4), app.a5_base + 0x30);
assert_eq!(runner.bus.read_long(code_body + 8), app.a5_base + 0x40);
assert_eq!(runner.bus.read_long(code_body + 12), app.a5_base + 0x50);
assert_eq!(
runner.bus.read_long(code_body + 16),
0x28,
"the first PC-relative record must immediately follow the first terminator"
);
assert_eq!(runner.bus.read_long(data_base), 0x10);
assert_eq!(runner.bus.read_long(data_base + 4), app.a5_base + 0x20);
assert_eq!(runner.bus.read_long(data_base + 8), app.a5_base + 0x30);
assert_eq!(runner.bus.read_long(data_base + 12), app.a5_base + 0x40);
assert_eq!(
runner
.bus
.read_long(code1_base + MpwFarSegmentHeader::CURRENT_A5_OFFSET),
app.a5_base
);
assert_eq!(
runner
.bus
.read_long(code1_base + MpwFarSegmentHeader::LOAD_ADDRESS_OFFSET),
code1_base
);
}
#[test]
fn invalid_retro68_relocations_do_not_partially_modify_memory() {
let mut runner = FixtureRunner::new(1024 * 1024, FixtureRunnerConfig::default());
let target = 0x1000;
runner.bus.write_long(target, 0x20);
let result = apply_retro68_rela_relocations(
&mut runner.bus,
target,
4,
&[
0x04, 0x00, 0x08, 0x00,
],
[0x100, 0, 0, 0],
);
assert_eq!(
result,
Err(Retro68RelocationError::TargetOutOfBounds {
offset: 1,
target_size: 4,
})
);
assert_eq!(runner.bus.read_long(target), 0x20);
}
#[test]
fn event_yield_services_pending_launch_application_from_vfs() {
use crate::memory::globals::addr;
let current_code0 = minimal_code0(0, 0x2000, 0, 0);
let helper_code0 = minimal_code0(0, 0x2000, 0, 0);
let current_fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, ¤t_code0)]);
let helper_fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &helper_code0)]);
let current_fork =
ResourceFork::parse(¤t_fork_bytes).expect("parse current app fork");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner
.dispatcher
.vfs
.insert("Apps/Main App".to_string(), Vec::new());
runner
.dispatcher
.vfs_rsrc
.insert("Apps/Main App".to_string(), current_fork_bytes);
runner
.dispatcher
.vfs
.insert("Apps/Register Helper".to_string(), Vec::new());
runner
.dispatcher
.vfs_rsrc
.insert("Apps/Register Helper".to_string(), helper_fork_bytes);
runner.dispatcher.ensure_vfs_catalog();
runner.dispatcher.set_launched_app_path("Apps/Main App");
let app = runner.load_app(¤t_fork).expect("load current app");
runner.init_app(&app);
runner.bus.write_long(addr::TICKS, 1234);
runner.bus.write_long(addr::TIME, 0x1020_3040);
runner.bus.write_long(addr::RND_SEED, 0x89AB_CDEF);
runner.dispatcher.tick_count = 1234;
runner
.dispatcher
.queue_pending_launch_application("Apps/Register Helper", true);
assert!(
!runner.service_pending_launch_application(false, false),
"launchContinue target must wait for an Event Manager yield"
);
let switched = runner.service_pending_launch_application(true, false);
assert!(
switched,
"event yield should service the queued helper launch"
);
assert!(
!runner.is_halted(),
"queued helper launch should not halt the runner"
);
assert_eq!(
runner.dispatcher.launched_app_path.as_deref(),
Some("Apps/Register Helper")
);
assert_eq!(
runner.bus.read_long(addr::TICKS),
1234,
"Process Manager launch must preserve system TickCount"
);
assert_eq!(runner.bus.read_long(addr::TIME), 0x1020_3040);
assert_eq!(runner.bus.read_long(addr::RND_SEED), 0x89AB_CDEF);
let cur_ap_len = runner.bus.read_byte(addr::CUR_APNAME) as usize;
let cur_ap_name = String::from_utf8(
(0..cur_ap_len)
.map(|i| runner.bus.read_byte(addr::CUR_APNAME + 1 + i as u32))
.collect(),
)
.expect("CurApName is ASCII");
assert_eq!(cur_ap_name, "Register Helper");
assert!(
runner.dispatcher.vfs.contains_key("Apps/Main App"),
"archive VFS entries must survive the foreground app switch"
);
assert!(
runner
.dispatcher
.vfs_rsrc
.contains_key("Apps/Register Helper"),
"launched app resource fork must remain available after the switch"
);
}
#[test]
fn immediate_pending_launch_application_switches_from_vfs_without_event_yield() {
use crate::memory::globals::addr;
let current_code0 = minimal_code0(0, 0x2000, 0, 0);
let helper_code0 = minimal_code0(0, 0x2000, 0, 0);
let current_fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, ¤t_code0)]);
let helper_fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &helper_code0)]);
let current_fork =
ResourceFork::parse(¤t_fork_bytes).expect("parse current app fork");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner
.dispatcher
.vfs
.insert("Apps/Main App".to_string(), Vec::new());
runner
.dispatcher
.vfs_rsrc
.insert("Apps/Main App".to_string(), current_fork_bytes);
runner
.dispatcher
.vfs
.insert("Apps/Register Helper".to_string(), Vec::new());
runner
.dispatcher
.vfs_rsrc
.insert("Apps/Register Helper".to_string(), helper_fork_bytes);
runner.dispatcher.ensure_vfs_catalog();
runner.dispatcher.set_launched_app_path("Apps/Main App");
let app = runner.load_app(¤t_fork).expect("load current app");
runner.init_app(&app);
runner.bus.write_long(addr::TICKS, 4321);
runner.bus.write_long(addr::TIME, 0x5060_7080);
runner.bus.write_long(addr::RND_SEED, 0x7654_3210);
runner.dispatcher.tick_count = 4321;
runner
.dispatcher
.queue_pending_launch_application("Apps/Register Helper", false);
let switched = runner.service_pending_launch_application(false, false);
assert!(
switched,
"immediate pending launch should not require an Event Manager yield"
);
assert!(
!runner.is_halted(),
"immediate queued helper launch should not halt the runner"
);
assert_eq!(
runner.dispatcher.launched_app_path.as_deref(),
Some("Apps/Register Helper")
);
assert_eq!(
runner.bus.read_long(addr::TICKS),
4321,
"foreground app switch must preserve system TickCount"
);
assert_eq!(runner.bus.read_long(addr::TIME), 0x5060_7080);
assert_eq!(runner.bus.read_long(addr::RND_SEED), 0x7654_3210);
let cur_ap_len = runner.bus.read_byte(addr::CUR_APNAME) as usize;
let cur_ap_name = String::from_utf8(
(0..cur_ap_len)
.map(|i| runner.bus.read_byte(addr::CUR_APNAME + 1 + i as u32))
.collect(),
)
.expect("CurApName is ASCII");
assert_eq!(cur_ap_name, "Register Helper");
}
#[test]
fn fixture_runner_defaults_to_classic_system7_theme() {
let runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
assert_eq!(runner.ui_theme_id(), UiThemeId::ClassicSystem7);
assert_eq!(runner.dispatcher().ui_theme_id(), UiThemeId::ClassicSystem7);
assert_eq!(runner.ui_theme().id(), UiThemeId::ClassicSystem7);
assert_eq!(
runner.theme_metrics_mode(),
ThemeMetricsMode::ClassicGuestMetrics
);
assert!(runner.uses_classic_guest_metrics());
}
#[test]
fn fixture_runner_accepts_explicit_systemless_theme_without_themed_metrics() {
let runner = FixtureRunner::new(
8 * 1024 * 1024,
FixtureRunnerConfig {
ui_theme: UiThemeId::SystemlessDefault,
theme_metrics_mode: ThemeMetricsMode::ClassicGuestMetrics,
..FixtureRunnerConfig::default()
},
);
let classic = UiThemeId::ClassicSystem7.provider();
assert_eq!(runner.ui_theme_id(), UiThemeId::SystemlessDefault);
assert_eq!(
runner.dispatcher().ui_theme_id(),
UiThemeId::SystemlessDefault
);
assert_eq!(runner.ui_theme().id(), UiThemeId::SystemlessDefault);
assert!(runner.uses_classic_guest_metrics());
assert_eq!(runner.ui_theme().menu_metrics(), classic.menu_metrics());
assert_eq!(
runner.ui_theme().control_metrics(),
classic.control_metrics()
);
assert_ne!(runner.ui_theme().palette(), classic.palette());
}
fn write_double_buffer(bus: &mut MacMemoryBus, ptr: u32, samples: &[u8]) {
bus.write_long(ptr, samples.len() as u32);
bus.write_long(ptr + 4, 0x0000_0001);
for (offset, sample) in samples.iter().copied().enumerate() {
bus.write_byte(ptr + 16 + offset as u32, sample);
}
}
#[test]
fn headless_run_steps_does_not_implicitly_mix_audio() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
for offset in (0..16).step_by(2) {
runner.bus.write_word(program_start + offset, 0x4E71); }
runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
let mut chan = SndChannel::new(0x0039_38C8, false);
chan.play_buffer(
vec![0x90, 0x91, 0x92],
OUTPUT_RATE << 16,
PlaybackKind::Buffer,
0,
);
runner.dispatcher.sound_manager.channels.push(chan);
let (steps, running) = runner.run_steps(2, None);
assert!(running);
assert_eq!(steps, 2);
assert_eq!(
runner.audio_buffer_len(),
0,
"plain headless stepping must not consume sound buffers"
);
assert_eq!(runner.dispatcher.sound_manager.debug_samples_mixed, 0);
runner.mix_audio(2);
assert_eq!(runner.drain_audio(), vec![0x90, 0x91]);
assert_eq!(runner.dispatcher.sound_manager.debug_samples_mixed, 2);
}
#[test]
fn headless_run_steps_advances_playback_needed_for_sound_callbacks() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
runner.bus.write_word(program_start, 0x60FE); runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.set_instructions_per_tick(1);
let chan_ptr = 0x0039_38C8;
let callback_addr = 0x0001_1000;
let callback_cmd = SndCommand {
cmd: crate::sound::cmd::CALLBACK,
param1: 0x1234,
param2: 0x0056_7890,
};
let mut chan = SndChannel::new(chan_ptr, false);
chan.callback_addr = callback_addr;
chan.play_buffer(vec![0x90; 500], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
chan.queue_callback(callback_cmd.clone());
runner.dispatcher.sound_manager.channels.push(chan);
let (steps, running) = runner.run_steps(2, None);
assert!(running);
assert_eq!(steps, 2);
assert_eq!(
runner
.dispatcher
.sound_manager
.pending_sound_callbacks
.len(),
1
);
assert!(matches!(
&runner.dispatcher.sound_manager.pending_sound_callbacks[0],
PendingSoundCallback::Command {
callback_addr: actual_callback,
chan_ptr: actual_channel,
cmd,
} if *actual_callback == callback_addr
&& *actual_channel == chan_ptr
&& cmd.cmd == callback_cmd.cmd
&& cmd.param1 == callback_cmd.param1
&& cmd.param2 == callback_cmd.param2
));
assert_eq!(
runner.audio_buffer_len(),
500,
"the complete callback-gated buffer should be captured before completion"
);
}
#[test]
fn host_audio_backend_receives_silence_while_sound_manager_idle() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let (audio_backend, stereo_samples) = CapturingAudioBackend::new();
runner.set_audio(Box::new(audio_backend));
runner.mix_audio(4);
assert_eq!(
stereo_samples.borrow().as_slice(),
&[0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80]
);
assert_eq!(
runner.audio_buffer_len(),
0,
"host silence must not become captured guest audio"
);
assert_eq!(runner.dispatcher.sound_manager.debug_samples_mixed, 0);
}
#[test]
fn host_audio_backend_receives_low_rate_sample_hold_output() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let (audio_backend, stereo_samples) = CapturingAudioBackend::new();
runner.set_audio(Box::new(audio_backend));
let mut chan = SndChannel::new(0x0039_38C8, false);
chan.play_buffer(
vec![0x90, 0xA0],
(OUTPUT_RATE / 2) << 16,
PlaybackKind::Buffer,
0,
);
runner.dispatcher.sound_manager.channels.push(chan);
runner.mix_audio(4);
assert_eq!(
stereo_samples.borrow().as_slice(),
&[
0x90, 0x90, 0x90, 0x90, 0xA0, 0xA0, 0xA0, 0xA0, ],
"GUI/host audio path must receive the sample-hold low-rate output, not the old linear midpoint"
);
assert_eq!(runner.dispatcher.sound_manager.debug_samples_mixed, 4);
}
fn dialog_tracking_for_test(filter_proc: u32, item_hit_ptr: u32) -> DialogTrackingState {
DialogTrackingState {
dialog_ptr: 0x0020_0000,
bounds: (0, 0, 32, 32),
title: String::new(),
proc_id: 1,
items: Vec::new(),
default_item: 0,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: 0,
item_hit_ptr,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
}
}
fn test_ppc_import_binding(symbol_index: u32, library: &str, symbol: &str) -> PpcImportBinding {
PpcImportBinding {
library_index: 0,
symbol_index,
library_name: library.to_string(),
symbol_name: symbol.to_string(),
class: 0,
weak: false,
address: 0,
tvector_address: None,
trap_pc: 0,
dispatcher_target: PpcImportDispatcherTarget::Unsupported,
}
}
#[test]
fn ppc_exit_to_shell_halt_is_distinct_from_other_ppc_stops() {
let mut exit = test_ppc_import_binding(7, "InterfaceLib", "ExitToShell");
exit.dispatcher_target = PpcImportDispatcherTarget::ExitToShell;
let imports = vec![exit];
let halted = PpcRunResult::Halted {
pc: PPC_HALT_PC,
cycles: 4,
};
assert!(ppc_halted_by_exit_to_shell(&imports, halted, Some(7), None));
assert!(!ppc_halted_by_exit_to_shell(
&imports,
halted,
Some(7),
Some(7)
));
assert!(!ppc_halted_by_exit_to_shell(
&imports,
halted,
Some(8),
None
));
let mut unsupported = test_ppc_import_binding(7, "InterfaceLib", "MysteryCall");
unsupported.dispatcher_target = PpcImportDispatcherTarget::Unsupported;
let duplicate_imports = vec![unsupported, imports[0].clone()];
assert!(!ppc_halted_by_exit_to_shell(
&duplicate_imports,
halted,
Some(7),
None
));
assert!(!ppc_halted_by_exit_to_shell(
&imports,
PpcRunResult::MemoryFault {
pc: PPC_CODE_BASE,
addr: 0,
was_write: false,
cycles: 4,
},
Some(7),
None
));
}
#[test]
fn ppc_unimpl_histogram_key_names_unsupported_imports() {
let imports = vec![test_ppc_import_binding(7, "InterfaceLib", "MysteryCall")];
assert_eq!(
ppc_unimpl_histogram_key(&imports, PpcRunResult::CycleLimit { cycles: 0 }, Some(7)),
Some("import #7 InterfaceLib:MysteryCall".to_string())
);
assert_eq!(
ppc_unimpl_histogram_key(&imports, PpcRunResult::CycleLimit { cycles: 0 }, Some(8)),
Some("import #8 <unknown>".to_string())
);
}
#[test]
fn ppc_unimpl_histogram_key_records_instruction_decode_errors() {
assert_eq!(
ppc_unimpl_histogram_key(
&[],
PpcRunResult::Unimplemented {
pc: 0x0100_0000,
error: ppc::PpcDecodeError::UnsupportedPrimaryOpcode(1),
cycles: 12,
},
None,
),
Some("instruction pc=$01000000 UnsupportedPrimaryOpcode(1)".to_string())
);
}
#[test]
fn ppc_unimpl_histogram_formatter_sorts_by_count_then_key() {
let mut histogram = HashMap::new();
merge_ppc_unimpl_histogram(&mut histogram, "import #7 InterfaceLib:Foo".to_string());
merge_ppc_unimpl_histogram(&mut histogram, "import #7 InterfaceLib:Foo".to_string());
merge_ppc_unimpl_histogram(&mut histogram, "instruction pc=$01000000 Bar".to_string());
assert_eq!(
format_ppc_unimpl_histogram(&histogram, 2),
"[PPC-UNIMPL-HIST] top 2 of 2 unsupported stops (3 total)\n\
[PPC-UNIMPL-HIST] 2 import #7 InterfaceLib:Foo\n\
[PPC-UNIMPL-HIST] 1 instruction pc=$01000000 Bar\n"
);
}
fn halted_ppc_app_with_sound(sound: PpcSoundState) -> LoadedApp {
let mut memory = PpcSectionMem::new();
memory.add_region(PPC_CODE_BASE, 0x4e80_0020u32.to_be_bytes().to_vec());
let mut cpu = PpcCpu::new();
cpu.pc = PPC_CODE_BASE;
cpu.lr = PPC_HALT_PC;
cpu.gpr[1] = PPC_STACK_TOP - 64;
LoadedApp::from_ppc(PpcLoadedApp {
cpu,
memory,
entry_pc: PPC_CODE_BASE,
rtoc: 0,
stack_base: PPC_STACK_BASE,
stack_size: PPC_STACK_SIZE,
stack_pointer: PPC_STACK_TOP - 64,
heap_base: PPC_HEAP_BASE,
heap_cursor: PPC_HEAP_BASE,
heap_limit: PPC_STACK_BASE,
last_mem_error: 0,
heap_maximized: false,
master_pointer_blocks_requested: 0,
tick_count: 0,
clock_cycles_per_tick: 1,
clock_cycle_phase: 0,
current_resource_refnum: 0,
last_resource_error: 0,
resource_load_enabled: true,
native_exception_handler: 0,
native_exception_stack: Vec::new(),
stdc_qsort_stack: Vec::new(),
dialog_callback_stack: Vec::new(),
apple_events: Default::default(),
cfm_connections: Vec::new(),
cfm_library_fragments: Vec::new(),
next_cfm_connection_id: 1,
ptrs: Vec::new(),
free_ptr_blocks: Vec::new(),
handles: Vec::new(),
free_handle_blocks: Vec::new(),
handle_states: Vec::new(),
controls: Vec::new(),
screen_clut: TrapDispatcher::standard_mac_8bpp_clut(),
device_gamma: crate::display::default_display_gamma(),
device_gamma_explicit: false,
color_manager_clut: TrapDispatcher::standard_mac_8bpp_clut(),
aliases: Vec::new(),
gworlds: Vec::new(),
q3_objects: Vec::new(),
q3_object_refs: Vec::new(),
next_q3_object: 0,
q3_error_state: Default::default(),
q3_lifecycle: Default::default(),
q3_memory_storages: Vec::new(),
q3_files: Vec::new(),
q3_group_memberships: Vec::new(),
q3_file_groups: Vec::new(),
q3_views: Vec::new(),
q3_submissions: Vec::new(),
q3_view_transforms: Vec::new(),
q3_submission_transforms: Vec::new(),
q3_view_materials: Vec::new(),
q3_submission_materials: Vec::new(),
q3_submission_lights: Vec::new(),
q3_view_state_stack: Vec::new(),
q3_completed_frames: Vec::new(),
q3_retained_frames: Vec::new(),
q3_state_only_completed_frame_batches: Vec::new(),
q3_fog_styles: Vec::new(),
q3_attributes: Vec::new(),
q3_shader_uv_transforms: Vec::new(),
q3_shader_boundaries: Vec::new(),
q3_mipmap_textures: Vec::new(),
q3_texture_shaders: Vec::new(),
q3_renderer_preferences: Vec::new(),
q3_draw_contexts: Vec::new(),
q3_trimeshes: Vec::new(),
q3_styles: Vec::new(),
q3_cameras: Vec::new(),
q3_lights: Vec::new(),
input_sprocket: Default::default(),
input_sprocket_virtual_elements: Vec::new(),
toolbox_startup: Default::default(),
quicktime: Default::default(),
sound,
timer_tasks: Vec::new(),
vbl_tasks: Vec::new(),
files: Vec::new(),
stdio_streams: ppc_initial_stdio_streams(),
vfs_files: Vec::new(),
deleted_vfs_file_paths: Vec::new(),
resource_files: Vec::new(),
vfs_resource_files: Vec::new(),
vfs_resources: Vec::new(),
next_file_ref_num: 128,
current_gworld: PPC_MAIN_GWORLD,
current_gdevice: PPC_MAIN_GDEVICE,
quickdraw_fore_color: PpcRgbColor {
red: 0,
green: 0,
blue: 0,
},
quickdraw_fore_indices: Default::default(),
quickdraw_back_color: PpcRgbColor {
red: 0xffff,
green: 0xffff,
blue: 0xffff,
},
quickdraw_pen_h: 0,
quickdraw_pen_v: 0,
quickdraw_text_mode: PPC_QD_TEXT_MODE_SRC_OR,
quickdraw_text_size: PPC_QD_TEXT_SIZE_SYSTEM,
quickdraw_cursor_level: 0,
vfs_volumes: Vec::new(),
vfs_directories: Vec::new(),
next_vfs_dir_id: 18,
default_dir_id: 2,
launched_app_path: None,
default_output_volume: 0,
param_text: [Vec::new(), Vec::new(), Vec::new(), Vec::new()],
scrap: Default::default(),
list_manager: Default::default(),
halt_pc: PPC_HALT_PC,
import_trap_base: PPC_IMPORT_TRAP_BASE,
import_count: 0,
imports: Vec::new(),
section_bases: Vec::new(),
input: PpcInputSnapshot::default(),
event_queue: Default::default(),
draw_sprocket: PpcDrawSprocketState::default(),
})
}
#[test]
fn ppc_exit_to_shell_stops_before_tick_and_callback_phase() {
const CALLBACK: u32 = PPC_CODE_BASE + 0x1000;
let sound = PpcSoundState {
pending_completions: vec![PpcSoundCompletionRecord {
file_playback_index: 0,
channel: 0x0300_1000,
completion: CALLBACK,
command: None,
tick: 0,
instruction_count: 0,
scheduled_tick: 0,
scheduled_instruction_count: 0,
}],
..PpcSoundState::default()
};
let mut app = halted_ppc_app_with_sound(sound);
let ppc_app = app.ppc.as_mut().expect("PPC app");
ppc_app
.memory
.add_region(CALLBACK, 0x4e80_0020u32.to_be_bytes().to_vec());
ppc_app
.memory
.write_u32_be(
PPC_CODE_BASE,
ppc_test_relative_branch(PPC_CODE_BASE, PPC_IMPORT_TRAP_BASE),
)
.unwrap();
let mut exit = test_ppc_import_binding(0, "InterfaceLib", "ExitToShell");
exit.trap_pc = PPC_IMPORT_TRAP_BASE;
exit.dispatcher_target = PpcImportDispatcherTarget::ExitToShell;
ppc_app.import_count = 1;
ppc_app.imports = vec![exit];
ppc_app.vbl_tasks.push(PpcVblTaskRecord {
task_ptr: PPC_DATA_BASE + 0x3000,
});
ppc_app.timer_tasks.push(PpcTimerTaskRecord {
task_ptr: PPC_DATA_BASE + 0x3100,
callback: CALLBACK,
active: true,
fire_at_tick: 0,
last_fired_tick: None,
});
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
runner.set_instructions_per_tick(1_000_000);
runner.tick_budget = 1_000_000;
let initial_tick = runner.guest_tick();
let initial_screen_events = runner.dispatcher.screen_event_count;
let (_steps, running) = runner.run_steps(64, None);
assert!(!running);
assert!(runner.halted_by_exit_to_shell());
assert_eq!(runner.guest_tick(), initial_tick);
assert_eq!(runner.dispatcher.tick_count, initial_tick);
assert_eq!(runner.dispatcher.screen_event_count, initial_screen_events);
let ppc_app = runner.ppc_app.as_ref().expect("PPC app retained");
assert_eq!(ppc_app.vbl_tasks.len(), 1);
assert_eq!(ppc_app.timer_tasks[0].last_fired_tick, None);
assert_eq!(ppc_app.sound.pending_completions.len(), 1);
assert!(ppc_app.sound.completion_invocations.is_empty());
}
const PPC_SOUND_EVENT_RECORD: u32 = PPC_DATA_BASE + 0x2000;
const PPC_SOUND_GET_EVENT_CALLBACK: u32 = PPC_CODE_BASE + 0x1000;
const PPC_SOUND_POST_EVENT_CALLBACK: u32 = PPC_CODE_BASE + 0x1100;
const PPC_SOUND_SET_EVENT_MASK_CALLBACK: u32 = PPC_CODE_BASE + 0x1200;
fn ppc_test_relative_branch(from: u32, to: u32) -> u32 {
0x4800_0000 | (to.wrapping_sub(from) & 0x03ff_fffc)
}
fn install_ppc_sound_event_boundary_fixture(app: &mut LoadedApp) {
let ppc_app = app.ppc.as_mut().expect("PPC app");
let mut add_callback = |address: u32, mut words: Vec<u32>, import_index: u32| {
let branch_address = address + u32::try_from(words.len()).unwrap() * 4;
words.push(ppc_test_relative_branch(
branch_address,
PPC_IMPORT_TRAP_BASE + import_index * 4,
));
ppc_app.memory.add_region(
address,
words.into_iter().flat_map(u32::to_be_bytes).collect(),
);
};
add_callback(
PPC_SOUND_GET_EVENT_CALLBACK,
vec![
0x3860_ffff, 0x3c80_0200, 0x6084_2000, ],
0,
);
add_callback(
PPC_SOUND_POST_EVENT_CALLBACK,
vec![
0x3860_0005, 0x3c80_5566, 0x6084_7788, ],
1,
);
add_callback(
PPC_SOUND_SET_EVENT_MASK_CALLBACK,
vec![0x3860_1234], 2,
);
ppc_app
.memory
.add_region(PPC_SOUND_EVENT_RECORD, vec![0; 16]);
ppc_app.import_count = 3;
ppc_app.imports = [
("GetNextEvent", PpcImportDispatcherTarget::GetNextEvent),
("PostEvent", PpcImportDispatcherTarget::PostEvent),
("SetEventMask", PpcImportDispatcherTarget::SetEventMask),
]
.into_iter()
.enumerate()
.map(|(index, (symbol, dispatcher_target))| {
let index = u32::try_from(index).unwrap();
PpcImportBinding {
library_index: 0,
symbol_index: index,
library_name: "InterfaceLib".into(),
symbol_name: symbol.into(),
class: 0,
weak: false,
address: PPC_IMPORT_TRAP_BASE + index * 4,
tvector_address: None,
trap_pc: PPC_IMPORT_TRAP_BASE + index * 4,
dispatcher_target,
}
})
.collect();
ppc_app.gworlds.push(PpcGWorldRecord {
port: PPC_MAIN_GWORLD,
pixmap_handle: 0,
pixmap: 0,
base_addr: 0,
gdevice: PPC_MAIN_GDEVICE,
width: 512,
height: 342,
depth: 8,
row_bytes: 512,
pixels_locked: false,
pixels_no_purge: false,
});
ppc_app.set_input_snapshot(PpcInputSnapshot {
mouse_v: 17,
mouse_h: 19,
..PpcInputSnapshot::default()
});
}
fn assert_ppc_sound_event_boundary(
mut app: LoadedApp,
fire_callbacks: impl FnOnce(&mut FixtureRunner),
) {
install_ppc_sound_event_boundary_fixture(&mut app);
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
let (offset_v, offset_h) = runner.ppc_viewport_offset();
assert!(
offset_v > 0 && offset_h > 0,
"fixture must use a centered viewport"
);
runner.dispatcher.event_queue.push_back(QueuedEvent {
what: 3,
message: 0x1122_3344,
where_v: 120,
where_h: 180,
modifiers: 0x0080,
});
fire_callbacks(&mut runner);
let ppc_app = runner.ppc_app.as_mut().expect("PPC app");
assert_eq!(
ppc_app.memory.read_u16_be(PPC_SOUND_EVENT_RECORD + 10),
Some(120)
);
assert_eq!(
ppc_app.memory.read_u16_be(PPC_SOUND_EVENT_RECORD + 12),
Some(180)
);
assert_eq!(runner.dispatcher.event_queue.len(), 1);
let posted = &runner.dispatcher.event_queue[0];
assert_eq!((posted.what, posted.message), (5, 0x5566_7788));
assert_eq!((posted.where_v, posted.where_h), (17, 19));
assert_eq!(
runner
.bus
.read_word(crate::memory::globals::addr::SYS_EVT_MASK),
0x1234
);
assert_eq!(
ppc_app
.memory
.read_u16_be(crate::memory::globals::addr::SYS_EVT_MASK),
Some(0x1234)
);
}
#[test]
fn ppc_sound_doublebacks_preserve_centered_viewport_event_coordinates() {
let callback = |callback| PpcSoundDoubleBackRecord {
channel: 0x0300_1000,
header: 0x0300_2000,
exhausted_buffer: 0x0300_3000,
exhausted_buffer_index: 0,
callback,
tick: 0,
instruction_count: 0,
};
let app = halted_ppc_app_with_sound(PpcSoundState {
pending_doublebacks: vec![
callback(PPC_SOUND_GET_EVENT_CALLBACK),
callback(PPC_SOUND_POST_EVENT_CALLBACK),
callback(PPC_SOUND_SET_EVENT_MASK_CALLBACK),
],
..PpcSoundState::default()
});
assert_ppc_sound_event_boundary(app, FixtureRunner::fire_pending_ppc_sound_doublebacks);
}
#[test]
fn ppc_sound_completions_preserve_centered_viewport_event_coordinates() {
let callback = |completion| PpcSoundCompletionRecord {
file_playback_index: 0,
channel: 0x0300_1000,
completion,
command: None,
tick: 0,
instruction_count: 0,
scheduled_tick: 0,
scheduled_instruction_count: 0,
};
let app = halted_ppc_app_with_sound(PpcSoundState {
pending_completions: vec![
callback(PPC_SOUND_GET_EVENT_CALLBACK),
callback(PPC_SOUND_POST_EVENT_CALLBACK),
callback(PPC_SOUND_SET_EVENT_MASK_CALLBACK),
],
..PpcSoundState::default()
});
assert_ppc_sound_event_boundary(app, FixtureRunner::fire_pending_ppc_sound_completions);
}
#[test]
fn ppc_host_mouse_input_enters_the_shared_queue_in_global_coordinates() {
use crate::memory::globals::addr;
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
install_ppc_sound_event_boundary_fixture(&mut app);
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
let (offset_v, offset_h) = runner.ppc_viewport_offset();
assert!(offset_v > 0 && offset_h > 0);
runner.push_mouse_down(offset_v.saturating_add(12), offset_h.saturating_add(34));
let event = runner.dispatcher.event_queue.back().expect("mouseDown");
assert_eq!((event.where_v, event.where_h), (12, 34));
let ppc_app = runner.ppc_app.as_ref().expect("PPC app");
let native_event = ppc_app.event_queue.back().expect("shared mouseDown");
assert_eq!((native_event.where_v, native_event.where_h), (12, 34));
assert_eq!(runner.bus.read_word(addr::M_TEMP), 12);
assert_eq!(runner.bus.read_word(addr::M_TEMP + 2), 34);
}
#[test]
fn ppc_sound_doubleback_can_refill_for_more_than_fifty_thousand_cycles() {
const CALLBACK_CYCLES: usize = 60_000;
const CHANNEL: u32 = 0x0300_1000;
const HEADER: u32 = 0x0300_2000;
const BUFFER: u32 = 0x0300_3000;
const CALLBACK: u32 = PPC_CODE_BASE + 0x1000;
let sound = PpcSoundState {
pending_doublebacks: vec![PpcSoundDoubleBackRecord {
channel: CHANNEL,
header: HEADER,
exhausted_buffer: BUFFER,
exhausted_buffer_index: 0,
callback: CALLBACK,
tick: 1,
instruction_count: 1,
}],
..PpcSoundState::default()
};
let mut app = halted_ppc_app_with_sound(sound);
let ppc_app = app.ppc.as_mut().expect("PPC app");
let mut callback = Vec::with_capacity((CALLBACK_CYCLES + 1) * 4);
for _ in 0..CALLBACK_CYCLES {
callback.extend_from_slice(&0x6000_0000u32.to_be_bytes()); }
callback.extend_from_slice(&0x4e80_0020u32.to_be_bytes()); ppc_app.memory.add_region(CALLBACK, callback);
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
runner.fire_pending_ppc_sound_doublebacks();
assert!(!runner.is_halted());
let sound = &runner.ppc_app.as_ref().expect("PPC app").sound;
assert!(sound.pending_doublebacks.is_empty());
let invocation = sound
.completion_invocations
.last()
.expect("doubleback invocation");
assert!(invocation.cycles > 50_000);
assert_eq!(
invocation.result,
PpcRunResult::Halted {
pc: PPC_HALT_PC,
cycles: (CALLBACK_CYCLES + 1) as u64,
}
);
}
#[test]
fn ppc_sound_doubleback_callback_refills_and_plays_exhausted_buffer() {
const CHANNEL: u32 = 0x0300_1000;
const HEADER: u32 = 0x0300_2000;
const BUFFER: u32 = 0x0300_3000;
const CALLBACK: u32 = PPC_CODE_BASE + 0x1000;
const SAMPLES: [u8; 4] = [0x80, 0x90, 0x70, 0xa0];
let sound = PpcSoundState {
double_buffer_playbacks: vec![PpcSoundDoubleBufferPlaybackRecord {
channel: CHANNEL,
header: HEADER,
buffers: [BUFFER, 0],
callback: CALLBACK,
sample_rate_fixed: crate::sound::OUTPUT_RATE << 16,
num_channels: 1,
sample_size: 8,
compression_id: 0,
packet_size: 0,
current_buffer_index: 0,
callback_pending_mask: 0,
active: true,
host_initialized: false,
host_buffer_loaded: false,
}],
..PpcSoundState::default()
};
let mut app = halted_ppc_app_with_sound(sound);
let ppc_app = app.ppc.as_mut().expect("PPC app");
ppc_app.memory.add_region(BUFFER, vec![0; 32]);
let callback = [
0x38a0_0004u32, 0x90a4_0000, 0x3ca0_8090, 0x60a5_70a0, 0x90a4_0010, 0x38a0_0005, 0x90a4_0004, 0x4e80_0020, ]
.into_iter()
.flat_map(u32::to_be_bytes)
.collect::<Vec<_>>();
ppc_app.memory.add_region(CALLBACK, callback);
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
runner.mix_audio(SAMPLES.len());
assert_eq!(runner.drain_audio(), SAMPLES);
let ppc_app = runner.ppc_app.as_ref().expect("PPC app should stay loaded");
let mut memory = ppc_app.memory.clone();
assert_eq!(memory.read_u32_be(BUFFER), Some(SAMPLES.len() as u32));
assert_eq!(memory.read_u32_be(BUFFER + 4), Some(0x04));
assert_eq!(
memory.read_u32_be(BUFFER + 16),
Some(u32::from_be_bytes(SAMPLES))
);
assert_eq!(ppc_app.sound.completion_invocations.len(), 1);
assert!(!ppc_app.sound.double_buffer_playbacks[0].active);
assert_eq!(
runner.dispatcher().sound_manager.debug_samples_mixed,
SAMPLES.len() as u64
);
}
#[test]
fn ppc_sound_completion_preserves_callback_rnd_seed_update() {
use crate::memory::globals::addr;
const CALLBACK: u32 = PPC_CODE_BASE + 0x1000;
let sound = PpcSoundState {
pending_completions: vec![PpcSoundCompletionRecord {
file_playback_index: 0,
channel: 0x0300_1000,
completion: CALLBACK,
command: None,
tick: 0,
instruction_count: 0,
scheduled_tick: 0,
scheduled_instruction_count: 0,
}],
..PpcSoundState::default()
};
let mut app = halted_ppc_app_with_sound(sound);
let ppc_app = app.ppc.as_mut().expect("PPC app");
ppc_app.cpu.alignment_policy = ppc::PpcAlignmentPolicy::EmulateData;
ppc_app.memory.add_region(PPC_HALT_PC, vec![0; 64 * 1024]);
ppc_app.memory.add_region(
CALLBACK,
[
0x3c60_89abu32, 0x6063_cdef, 0x3880_0156, 0x9064_0000, 0x4e80_0020, ]
.into_iter()
.flat_map(u32::to_be_bytes)
.collect(),
);
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_optional_launch_state(None, Some(1), None);
runner.init_app(&app);
runner.fire_pending_ppc_sound_completions();
assert_eq!(runner.bus.read_long(addr::RND_SEED), 0x89ab_cdef);
assert_eq!(
runner
.ppc_app
.as_mut()
.expect("PPC app")
.memory
.read_u32_be(addr::RND_SEED),
Some(0x89ab_cdef)
);
}
#[test]
fn ppc_sound_completions_see_ticks_advanced_by_prior_callback() {
use crate::memory::globals::addr;
const FIRST_CALLBACK: u32 = PPC_CODE_BASE + 0x1000;
const SECOND_CALLBACK: u32 = PPC_CODE_BASE + 0x1100;
let completion = |completion| PpcSoundCompletionRecord {
file_playback_index: 0,
channel: 0x0300_1000,
completion,
command: None,
tick: 0,
instruction_count: 0,
scheduled_tick: 0,
scheduled_instruction_count: 0,
};
let sound = PpcSoundState {
pending_completions: vec![completion(FIRST_CALLBACK), completion(SECOND_CALLBACK)],
..PpcSoundState::default()
};
let mut app = halted_ppc_app_with_sound(sound);
let ppc_app = app.ppc.as_mut().expect("PPC app");
ppc_app.cpu.alignment_policy = ppc::PpcAlignmentPolicy::EmulateData;
ppc_app.memory.add_region(PPC_HALT_PC, vec![0; 64 * 1024]);
ppc_app.memory.add_region(
FIRST_CALLBACK,
[0x6000_0000u32, 0x4e80_0020]
.into_iter()
.flat_map(u32::to_be_bytes)
.collect(),
);
ppc_app.memory.add_region(
SECOND_CALLBACK,
[0x8060_016au32, 0x4e80_0020] .into_iter()
.flat_map(u32::to_be_bytes)
.collect(),
);
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_optional_launch_state(Some(41), None, None);
runner.init_app(&app);
runner.set_instructions_per_tick(2);
runner.fire_pending_ppc_sound_completions();
assert_eq!(runner.bus.read_long(addr::TICKS), 43);
assert_eq!(runner.dispatcher.tick_count, 43);
let ppc_app = runner.ppc_app.as_mut().expect("PPC app");
assert_eq!(ppc_app.tick_count, 43);
assert_eq!(ppc_app.memory.read_u32_be(addr::TICKS), Some(43));
assert_eq!(ppc_app.sound.completion_invocations.len(), 2);
assert_eq!(ppc_app.sound.completion_invocations[1].end_r3, 42);
}
#[test]
fn ppc_loaded_app_runs_through_fixture_runner() {
let mut memory = PpcSectionMem::new();
memory.add_region(PPC_CODE_BASE, 0x4e80_0020u32.to_be_bytes().to_vec());
let mut cpu = PpcCpu::new();
cpu.pc = PPC_CODE_BASE;
cpu.lr = PPC_HALT_PC;
cpu.gpr[1] = PPC_STACK_TOP - 64;
let app = LoadedApp::from_ppc(PpcLoadedApp {
cpu,
memory,
entry_pc: PPC_CODE_BASE,
rtoc: 0,
stack_base: PPC_STACK_BASE,
stack_size: PPC_STACK_SIZE,
stack_pointer: PPC_STACK_TOP - 64,
heap_base: PPC_HEAP_BASE,
heap_cursor: PPC_HEAP_BASE,
heap_limit: PPC_STACK_BASE,
last_mem_error: 0,
heap_maximized: false,
master_pointer_blocks_requested: 0,
tick_count: 0,
clock_cycles_per_tick: 1,
clock_cycle_phase: 0,
current_resource_refnum: 0,
last_resource_error: 0,
resource_load_enabled: true,
native_exception_handler: 0,
native_exception_stack: Vec::new(),
stdc_qsort_stack: Vec::new(),
dialog_callback_stack: Vec::new(),
apple_events: Default::default(),
cfm_connections: Vec::new(),
cfm_library_fragments: Vec::new(),
next_cfm_connection_id: 1,
ptrs: Vec::new(),
free_ptr_blocks: Vec::new(),
handles: Vec::new(),
free_handle_blocks: Vec::new(),
handle_states: Vec::new(),
controls: Vec::new(),
screen_clut: TrapDispatcher::standard_mac_8bpp_clut(),
device_gamma: crate::display::default_display_gamma(),
device_gamma_explicit: false,
color_manager_clut: TrapDispatcher::standard_mac_8bpp_clut(),
aliases: Vec::new(),
gworlds: Vec::new(),
q3_objects: Vec::new(),
q3_object_refs: Vec::new(),
next_q3_object: 0,
q3_error_state: Default::default(),
q3_lifecycle: Default::default(),
q3_memory_storages: Vec::new(),
q3_files: Vec::new(),
q3_group_memberships: Vec::new(),
q3_file_groups: Vec::new(),
q3_views: Vec::new(),
q3_submissions: Vec::new(),
q3_view_transforms: Vec::new(),
q3_submission_transforms: Vec::new(),
q3_view_materials: Vec::new(),
q3_submission_materials: Vec::new(),
q3_submission_lights: Vec::new(),
q3_view_state_stack: Vec::new(),
q3_completed_frames: Vec::new(),
q3_retained_frames: Vec::new(),
q3_state_only_completed_frame_batches: Vec::new(),
q3_fog_styles: Vec::new(),
q3_attributes: Vec::new(),
q3_shader_uv_transforms: Vec::new(),
q3_shader_boundaries: Vec::new(),
q3_mipmap_textures: Vec::new(),
q3_texture_shaders: Vec::new(),
q3_renderer_preferences: Vec::new(),
q3_draw_contexts: Vec::new(),
q3_trimeshes: Vec::new(),
q3_styles: Vec::new(),
q3_cameras: Vec::new(),
q3_lights: Vec::new(),
input_sprocket: Default::default(),
input_sprocket_virtual_elements: Vec::new(),
toolbox_startup: Default::default(),
quicktime: Default::default(),
sound: Default::default(),
timer_tasks: Vec::new(),
vbl_tasks: Vec::new(),
files: Vec::new(),
stdio_streams: ppc_initial_stdio_streams(),
vfs_files: vec![PpcVfsFileRecord {
path: "System Folder/Preferences/Test App Prefs".to_string(),
data: b"prefs".to_vec(),
creator: u32::from_be_bytes(*b"Nano"),
file_type: u32::from_be_bytes(*b"pref"),
finder_flags: 0x0200,
dirty: true,
}],
deleted_vfs_file_paths: vec!["System Folder/Preferences/Old Prefs".to_string()],
resource_files: Vec::new(),
vfs_resource_files: vec![PpcVfsResourceFileRecord {
path: "System Folder/Preferences/Test App HighScores".to_string(),
creator: u32::from_be_bytes(*b"Nano"),
file_type: u32::from_be_bytes(*b"pref"),
finder_flags: 0x0400,
resource_len: 0,
raw_data: None,
map_attrs: 0,
dirty: true,
}],
vfs_resources: vec![PpcVfsResourceRecord {
ref_num: 128,
path: "System Folder/Preferences/Test App HighScores".to_string(),
res_type: u32::from_be_bytes(*b"pref"),
res_id: 200,
name: b"Scores".to_vec(),
data: b"score".to_vec(),
raw_data: None,
raw_attrs: None,
attrs: 0,
handle: 0,
}],
next_file_ref_num: 128,
current_gworld: PPC_MAIN_GWORLD,
current_gdevice: PPC_MAIN_GDEVICE,
quickdraw_fore_color: PpcRgbColor {
red: 0,
green: 0,
blue: 0,
},
quickdraw_fore_indices: Default::default(),
quickdraw_back_color: PpcRgbColor {
red: 0xffff,
green: 0xffff,
blue: 0xffff,
},
quickdraw_pen_h: 0,
quickdraw_pen_v: 0,
quickdraw_text_mode: PPC_QD_TEXT_MODE_SRC_OR,
quickdraw_text_size: PPC_QD_TEXT_SIZE_SYSTEM,
quickdraw_cursor_level: 0,
vfs_volumes: Vec::new(),
vfs_directories: vec![PpcVfsDirectory {
dir_id: 18,
parent_dir_id: 17,
path: "System Folder/Preferences/Test App Saves".to_string(),
creator: u32::from_be_bytes(*b"Nano"),
file_type: u32::from_be_bytes(*b"dir "),
finder_flags: 0x0080,
dirty: true,
}],
next_vfs_dir_id: 18,
default_dir_id: 2,
launched_app_path: None,
default_output_volume: 0,
param_text: [Vec::new(), Vec::new(), Vec::new(), Vec::new()],
scrap: Default::default(),
list_manager: Default::default(),
halt_pc: PPC_HALT_PC,
import_trap_base: PPC_IMPORT_TRAP_BASE,
import_count: 0,
imports: Vec::new(),
section_bases: Vec::new(),
input: PpcInputSnapshot::default(),
event_queue: Default::default(),
draw_sprocket: PpcDrawSprocketState::default(),
});
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
let output_dir = tempfile::tempdir().unwrap();
let old_host_path = output_dir
.path()
.join("System Folder/Preferences/Old Prefs");
std::fs::create_dir_all(old_host_path.parent().unwrap()).unwrap();
std::fs::write(&old_host_path, b"old").unwrap();
let old_rsrc_path = output_dir
.path()
.join("System Folder/Preferences/.rsrc/Old Prefs");
std::fs::create_dir_all(old_rsrc_path.parent().unwrap()).unwrap();
std::fs::write(&old_rsrc_path, b"old-rsrc").unwrap();
runner.dispatcher_mut().output_dir = Some(output_dir.path().to_path_buf());
runner.dispatcher_mut().vfs.insert(
"System Folder/Preferences/Old Prefs".to_string(),
b"old".to_vec(),
);
runner.dispatcher_mut().vfs_rsrc.insert(
"System Folder/Preferences/Old Prefs".to_string(),
b"old-rsrc".to_vec(),
);
runner.dispatcher_mut().set_vfs_entry_finfo(
"System Folder/Preferences/Old Prefs",
u32::from_be_bytes(*b"pref"),
u32::from_be_bytes(*b"Nano"),
0x4000,
);
runner
.dispatcher_mut()
.locked_files
.insert("System Folder/Preferences/Old Prefs".to_string());
let (steps, running) = runner.run_steps(8, None);
assert_eq!(steps, 1);
assert!(!running);
assert!(runner.is_halted());
assert_eq!(runner.halted_pc(), Some(PPC_HALT_PC));
assert_eq!(runner.halted_sp(), Some(PPC_STACK_TOP - 64));
assert_eq!(
runner
.dispatcher()
.vfs
.get("System Folder/Preferences/Test App Prefs")
.map(Vec::as_slice),
Some(b"prefs".as_slice())
);
let prefs_metadata = runner
.dispatcher()
.vfs_metadata
.get("System Folder/Preferences/Test App Prefs")
.copied()
.expect("dirty PPC data fork should carry Finder metadata");
assert_eq!(prefs_metadata.creator, u32::from_be_bytes(*b"Nano"));
assert_eq!(prefs_metadata.file_type, u32::from_be_bytes(*b"pref"));
assert_eq!(prefs_metadata.finder_flags, 0x0200);
assert!(!runner
.dispatcher()
.vfs
.contains_key("System Folder/Preferences/Old Prefs"));
assert!(!runner
.dispatcher()
.vfs_rsrc
.contains_key("System Folder/Preferences/Old Prefs"));
assert!(!runner
.dispatcher()
.vfs_metadata
.contains_key("System Folder/Preferences/Old Prefs"));
assert!(!runner
.dispatcher()
.locked_files
.contains("System Folder/Preferences/Old Prefs"));
assert_eq!(
std::fs::read(
output_dir
.path()
.join("System Folder/Preferences/Test App Prefs")
)
.unwrap()
.as_slice(),
b"prefs".as_slice()
);
assert!(!old_host_path.exists());
assert!(!old_rsrc_path.exists());
let fork_bytes = runner
.dispatcher()
.vfs_rsrc
.get("System Folder/Preferences/Test App HighScores")
.expect("dirty PPC resource fork should sync to dispatcher VFS");
let fork = ResourceFork::parse(fork_bytes).unwrap();
assert_eq!(fork.get(*b"pref", 200).unwrap().data, b"score");
let scores_metadata = runner
.dispatcher()
.vfs_metadata
.get("System Folder/Preferences/Test App HighScores")
.copied()
.expect("dirty PPC resource fork should carry Finder metadata");
assert_eq!(scores_metadata.creator, u32::from_be_bytes(*b"Nano"));
assert_eq!(scores_metadata.file_type, u32::from_be_bytes(*b"pref"));
assert_eq!(scores_metadata.finder_flags, 0x0400);
let saves_directory = runner
.dispatcher()
.vfs_directories
.get("System Folder/Preferences/Test App Saves")
.expect("dirty PPC directory should sync to dispatcher catalog");
assert_eq!(saves_directory.name, "Test App Saves");
assert!(output_dir
.path()
.join("System Folder/Preferences/Test App Saves")
.is_dir());
assert_eq!(
std::fs::read(
output_dir
.path()
.join("System Folder/Preferences/.rsrc/Test App HighScores")
)
.unwrap()
.as_slice(),
fork_bytes.as_slice()
);
assert!(!runner.ppc_app.as_ref().unwrap().vfs_files[0].dirty);
assert!(!runner.ppc_app.as_ref().unwrap().vfs_directories[0].dirty);
assert!(runner
.ppc_app
.as_ref()
.unwrap()
.deleted_vfs_file_paths
.is_empty());
assert!(!runner.ppc_app.as_ref().unwrap().vfs_resource_files[0].dirty);
}
#[test]
fn ppc_double_buffer_playback_feeds_consecutive_host_audio_buffers() {
let channel = 0x0500_1000;
let header = PPC_DATA_BASE + 0x100;
let buffer0 = PPC_DATA_BASE + 0x200;
let buffer1 = PPC_DATA_BASE + 0x300;
let expected = vec![0x80, 0x90, 0x70, 0xa0];
let sound = PpcSoundState {
double_buffer_playbacks: vec![PpcSoundDoubleBufferPlaybackRecord {
channel,
header,
buffers: [buffer0, buffer1],
callback: 0,
sample_rate_fixed: crate::sound::OUTPUT_RATE << 16,
num_channels: 1,
sample_size: 8,
compression_id: 0,
packet_size: 0,
current_buffer_index: 0,
callback_pending_mask: 0,
active: true,
host_initialized: false,
host_buffer_loaded: false,
}],
..PpcSoundState::default()
};
let mut app = halted_ppc_app_with_sound(sound);
{
let ppc_app = app.ppc.as_mut().expect("PPC app");
ppc_app.memory.add_region(PPC_DATA_BASE, vec![0; 0x400]);
ppc_app.memory.write_u32_be(buffer0, 2).unwrap();
ppc_app.memory.write_u32_be(buffer0 + 4, 0x01).unwrap();
ppc_app
.memory
.write_bytes(buffer0 + 16, &expected[..2])
.unwrap();
ppc_app.memory.write_u32_be(buffer1, 2).unwrap();
ppc_app.memory.write_u32_be(buffer1 + 4, 0x05).unwrap();
ppc_app
.memory
.write_bytes(buffer1 + 16, &expected[2..])
.unwrap();
}
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
let (steps, running) = runner.run_steps_with_audio(8, None, expected.len());
assert_eq!(steps, 1);
assert!(!running);
assert_eq!(runner.drain_audio(), expected);
assert_eq!(
runner.dispatcher().sound_manager.debug_double_buffer_count,
1
);
assert_eq!(runner.dispatcher().sound_manager.debug_samples_mixed, 4);
let playback = runner
.ppc_app
.as_ref()
.expect("PPC app should stay loaded")
.sound
.double_buffer_playbacks[0];
assert!(!playback.active);
assert!(!playback.host_buffer_loaded);
assert_eq!(playback.current_buffer_index, 1);
}
#[test]
fn ppc_decoded_buffer_command_feeds_host_audio_buffer() {
let channel = 0x0500_1000;
let samples = vec![0x80, 0x90, 0x70, 0xa0];
let sound = PpcSoundState {
decoded_buffer_commands: vec![PpcDecodedBufferCommandRecord {
channel,
sample_rate_fixed: crate::sound::OUTPUT_RATE << 16,
samples: samples.clone(),
}],
..PpcSoundState::default()
};
let app = halted_ppc_app_with_sound(sound);
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
let (steps, running) = runner.run_steps_with_audio(8, None, samples.len());
assert_eq!(steps, 1);
assert!(!running);
assert_eq!(runner.drain_audio(), samples);
assert_eq!(runner.dispatcher().sound_manager.debug_cmd_count, 1);
assert_eq!(runner.dispatcher().sound_manager.debug_buffer_cmd_count, 1);
assert_eq!(runner.dispatcher().sound_manager.debug_samples_mixed, 4);
assert!(runner
.ppc_app
.as_ref()
.expect("PPC app should stay loaded")
.sound
.decoded_buffer_commands
.is_empty());
}
#[test]
fn ppc_decoded_file_playback_feeds_host_audio_buffer() {
let channel = 0x0500_1000;
let callback_entry = PPC_CODE_BASE + 0x40;
let completion = PPC_DATA_BASE + 0x20;
let completion_tvector = PPC_DATA_BASE + 0x80;
let samples = vec![0x80, 0x90, 0x70, 0x80];
let mut preview = [0; 16];
preview[..samples.len()].copy_from_slice(&samples);
let mut app = halted_ppc_app_with_sound(PpcSoundState {
queued_commands: Vec::new(),
immediate_commands: Vec::new(),
decoded_buffer_commands: Vec::new(),
double_buffer_playbacks: Vec::new(),
file_playbacks: vec![PpcSoundFilePlaybackRecord {
channel,
ref_num: 128,
resource_id: -1,
buffer_size: 20_480,
buffer: 0,
selection: 0,
completion,
completion_command: None,
async_play: true,
paused: false,
active: true,
quiet_now: false,
timing_valid: false,
start_tick: 0,
start_instruction_count: 0,
due_tick: 0,
due_instruction_count: 0,
pause_timing_valid: false,
pause_started_tick: 0,
pause_started_instruction_count: 0,
aiff: None,
decoded_aiff: Some(PpcDecodedAiffSamples {
sample_rate_fixed: crate::sound::OUTPUT_RATE << 16,
sample_count: samples.len() as u32,
preview_len: samples.len() as u8,
preview,
}),
}],
decoded_file_playbacks: vec![PpcDecodedAiffPlaybackRecord {
file_playback_index: 0,
channel,
sample_rate_fixed: crate::sound::OUTPUT_RATE << 16,
samples: samples.clone(),
}],
pending_completions: Vec::new(),
pending_doublebacks: Vec::new(),
completion_invocations: Vec::new(),
sys_beep_count: 0,
last_sys_beep_duration: 0,
start_count: 1,
pause_count: 0,
stop_count: 0,
double_buffer_play_count: 0,
last_double_buffer_channel: 0,
last_double_buffer_header: 0,
});
{
let ppc_app = app.ppc.as_mut().expect("PPC app");
let stw_r3_0_r2 = (36u32 << 26) | (3u32 << 21) | (2u32 << 16);
let mut callback_bytes = Vec::new();
callback_bytes.extend_from_slice(&stw_r3_0_r2.to_be_bytes());
callback_bytes.extend_from_slice(&0x4e80_0020u32.to_be_bytes());
ppc_app.memory.add_region(callback_entry, callback_bytes);
ppc_app.memory.add_region(PPC_DATA_BASE, vec![0; 0x100]);
ppc_app.rtoc = PPC_DATA_BASE;
ppc_app.cpu.gpr[2] = PPC_DATA_BASE;
ppc_app
.memory
.write_u16_be(completion, 0xAAFE)
.expect("write goMixedModeTrap");
ppc_app
.memory
.write_u8(completion + 2, 7)
.expect("write descriptor version");
ppc_app
.memory
.write_u16_be(completion + 10, 0)
.expect("write routine count");
let record = completion + 12;
ppc_app
.memory
.write_u8(record + 5, 1)
.expect("write PowerPC ISA");
ppc_app
.memory
.write_u16_be(record + 6, 0x0004)
.expect("write routine flags");
ppc_app
.memory
.write_u32_be(record + 8, completion_tvector)
.expect("write proc descriptor");
ppc_app
.memory
.write_u32_be(completion_tvector, callback_entry)
.expect("write callback TVector entry");
ppc_app
.memory
.write_u32_be(completion_tvector + 4, PPC_DATA_BASE)
.expect("write callback TVector RTOC");
}
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
let (steps, running) = runner.run_steps_with_audio(8, None, samples.len());
assert_eq!(steps, 1);
assert!(!running);
assert_eq!(runner.drain_audio(), samples);
assert_eq!(runner.dispatcher().sound_manager.debug_file_play_count, 1);
assert_eq!(runner.dispatcher().sound_manager.debug_samples_mixed, 4);
let ppc_app = runner.ppc_app.as_ref().expect("PPC app should stay loaded");
assert!(!ppc_app.sound.file_playbacks[0].active);
assert!(!ppc_app.sound.file_playbacks[0].paused);
assert!(ppc_app.sound.pending_completions.is_empty());
let mut memory = ppc_app.memory.clone();
assert_eq!(memory.read_u32_be(PPC_DATA_BASE), Some(channel));
assert_eq!(ppc_app.sound.completion_invocations.len(), 1);
let invocation = ppc_app.sound.completion_invocations[0];
assert_eq!(invocation.file_playback_index, 0);
assert_eq!(invocation.channel, channel);
assert_eq!(invocation.completion, completion);
assert_eq!(invocation.callback_entry, callback_entry);
assert_eq!(invocation.callback_rtoc, PPC_DATA_BASE);
assert_eq!(invocation.tick, 0);
assert_eq!(invocation.instruction_count, 1);
assert_eq!(invocation.scheduled_tick, 1);
assert_eq!(
invocation.scheduled_instruction_count,
1 + u64::from(INSTRUCTIONS_PER_TICK)
);
assert_eq!(invocation.cycles, 2);
assert_eq!(
invocation.result,
PpcRunResult::Halted {
pc: PPC_HALT_PC,
cycles: 2
}
);
assert_eq!(invocation.unsupported_import_index, None);
}
#[test]
fn ppc_decoded_file_playback_completes_from_guest_ticks_without_audio_mix() {
let channel = 0x0500_1000;
let callback_entry = PPC_CODE_BASE + 0x40;
let samples = vec![0x80, 0x90, 0x70, 0x80];
let mut preview = [0; 16];
preview[..samples.len()].copy_from_slice(&samples);
let mut app = halted_ppc_app_with_sound(PpcSoundState {
queued_commands: Vec::new(),
immediate_commands: Vec::new(),
decoded_buffer_commands: Vec::new(),
double_buffer_playbacks: Vec::new(),
file_playbacks: vec![PpcSoundFilePlaybackRecord {
channel,
ref_num: 128,
resource_id: -1,
buffer_size: 20_480,
buffer: 0,
selection: 0,
completion: callback_entry,
completion_command: None,
async_play: true,
paused: false,
active: true,
quiet_now: false,
timing_valid: false,
start_tick: 0,
start_instruction_count: 0,
due_tick: 0,
due_instruction_count: 0,
pause_timing_valid: false,
pause_started_tick: 0,
pause_started_instruction_count: 0,
aiff: None,
decoded_aiff: Some(PpcDecodedAiffSamples {
sample_rate_fixed: crate::sound::OUTPUT_RATE << 16,
sample_count: samples.len() as u32,
preview_len: samples.len() as u8,
preview,
}),
}],
decoded_file_playbacks: vec![PpcDecodedAiffPlaybackRecord {
file_playback_index: 0,
channel,
sample_rate_fixed: crate::sound::OUTPUT_RATE << 16,
samples,
}],
pending_completions: Vec::new(),
pending_doublebacks: Vec::new(),
completion_invocations: Vec::new(),
sys_beep_count: 0,
last_sys_beep_duration: 0,
start_count: 1,
pause_count: 0,
stop_count: 0,
double_buffer_play_count: 0,
last_double_buffer_channel: 0,
last_double_buffer_header: 0,
});
{
let ppc_app = app.ppc.as_mut().expect("PPC app");
ppc_app
.memory
.write_u32_be(PPC_CODE_BASE, 0x4800_0000)
.expect("rewrite entry as infinite branch");
let stw_r3_0_r2 = (36u32 << 26) | (3u32 << 21) | (2u32 << 16);
let mut callback_bytes = Vec::new();
callback_bytes.extend_from_slice(&stw_r3_0_r2.to_be_bytes());
callback_bytes.extend_from_slice(&0x4e80_0020u32.to_be_bytes());
ppc_app.memory.add_region(callback_entry, callback_bytes);
ppc_app.memory.add_region(PPC_DATA_BASE, vec![0; 0x100]);
ppc_app.rtoc = PPC_DATA_BASE;
ppc_app.cpu.gpr[2] = PPC_DATA_BASE;
}
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
let (steps, running) = runner.run_steps_with_audio(1, None, 0);
assert_eq!(steps, 1);
assert!(running);
{
let ppc_app = runner.ppc_app.as_ref().expect("PPC app should stay loaded");
let playback = &ppc_app.sound.file_playbacks[0];
assert!(playback.active);
assert!(playback.timing_valid);
assert_eq!(playback.start_tick, 0);
assert_eq!(playback.start_instruction_count, 1);
assert_eq!(playback.due_tick, 1);
assert_eq!(
playback.due_instruction_count,
1 + u64::from(INSTRUCTIONS_PER_TICK)
);
assert!(ppc_app.sound.completion_invocations.is_empty());
}
let (steps, running) = runner.run_steps_with_audio(INSTRUCTIONS_PER_TICK as usize, None, 0);
assert_eq!(steps, INSTRUCTIONS_PER_TICK as usize);
assert!(running);
assert!(runner.drain_audio().is_empty());
assert_eq!(runner.dispatcher().tick_count, 1);
assert_eq!(runner.dispatcher().sound_manager.debug_file_play_count, 1);
assert_eq!(runner.dispatcher().sound_manager.debug_samples_mixed, 0);
let ppc_app = runner.ppc_app.as_ref().expect("PPC app should stay loaded");
assert!(!ppc_app.sound.file_playbacks[0].active);
assert!(!ppc_app.sound.file_playbacks[0].paused);
assert!(ppc_app.sound.pending_completions.is_empty());
let mut memory = ppc_app.memory.clone();
assert_eq!(memory.read_u32_be(PPC_DATA_BASE), Some(channel));
assert_eq!(ppc_app.sound.completion_invocations.len(), 1);
let invocation = ppc_app.sound.completion_invocations[0];
assert_eq!(invocation.file_playback_index, 0);
assert_eq!(invocation.channel, channel);
assert_eq!(invocation.completion, callback_entry);
assert_eq!(invocation.callback_entry, callback_entry);
assert_eq!(invocation.callback_rtoc, PPC_DATA_BASE);
assert_eq!(invocation.tick, 1);
assert_eq!(
invocation.instruction_count,
1 + u64::from(INSTRUCTIONS_PER_TICK)
);
assert_eq!(invocation.scheduled_tick, 1);
assert_eq!(
invocation.scheduled_instruction_count,
1 + u64::from(INSTRUCTIONS_PER_TICK)
);
assert_eq!(invocation.cycles, 2);
assert_eq!(
invocation.result,
PpcRunResult::Halted {
pc: PPC_HALT_PC,
cycles: 2
}
);
assert_eq!(invocation.unsupported_import_index, None);
}
#[test]
fn ppc_decoded_file_playback_pause_resume_shifts_guest_due_time() {
let channel = 0x0500_1000;
let callback_entry = PPC_CODE_BASE + 0x40;
let samples = vec![0x80, 0x90, 0x70, 0x80];
let mut preview = [0; 16];
preview[..samples.len()].copy_from_slice(&samples);
let mut app = halted_ppc_app_with_sound(PpcSoundState {
queued_commands: Vec::new(),
immediate_commands: Vec::new(),
decoded_buffer_commands: Vec::new(),
double_buffer_playbacks: Vec::new(),
file_playbacks: vec![PpcSoundFilePlaybackRecord {
channel,
ref_num: 128,
resource_id: -1,
buffer_size: 20_480,
buffer: 0,
selection: 0,
completion: callback_entry,
completion_command: None,
async_play: true,
paused: false,
active: true,
quiet_now: false,
timing_valid: false,
start_tick: 0,
start_instruction_count: 0,
due_tick: 0,
due_instruction_count: 0,
pause_timing_valid: false,
pause_started_tick: 0,
pause_started_instruction_count: 0,
aiff: None,
decoded_aiff: Some(PpcDecodedAiffSamples {
sample_rate_fixed: crate::sound::OUTPUT_RATE << 16,
sample_count: samples.len() as u32,
preview_len: samples.len() as u8,
preview,
}),
}],
decoded_file_playbacks: vec![PpcDecodedAiffPlaybackRecord {
file_playback_index: 0,
channel,
sample_rate_fixed: crate::sound::OUTPUT_RATE << 16,
samples,
}],
pending_completions: Vec::new(),
pending_doublebacks: Vec::new(),
completion_invocations: Vec::new(),
sys_beep_count: 0,
last_sys_beep_duration: 0,
start_count: 1,
pause_count: 0,
stop_count: 0,
double_buffer_play_count: 0,
last_double_buffer_channel: 0,
last_double_buffer_header: 0,
});
{
let ppc_app = app.ppc.as_mut().expect("PPC app");
ppc_app
.memory
.write_u32_be(PPC_CODE_BASE, 0x4800_0000)
.expect("rewrite entry as infinite branch");
let stw_r3_0_r2 = (36u32 << 26) | (3u32 << 21) | (2u32 << 16);
let mut callback_bytes = Vec::new();
callback_bytes.extend_from_slice(&stw_r3_0_r2.to_be_bytes());
callback_bytes.extend_from_slice(&0x4e80_0020u32.to_be_bytes());
ppc_app.memory.add_region(callback_entry, callback_bytes);
ppc_app.memory.add_region(PPC_DATA_BASE, vec![0; 0x100]);
ppc_app.rtoc = PPC_DATA_BASE;
ppc_app.cpu.gpr[2] = PPC_DATA_BASE;
}
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
let (steps, running) = runner.run_steps_with_audio(1, None, 0);
assert_eq!(steps, 1);
assert!(running);
assert_eq!(
runner.ppc_app.as_ref().unwrap().sound.file_playbacks[0].due_tick,
1
);
runner.ppc_app.as_mut().unwrap().sound.file_playbacks[0].paused = true;
let (steps, running) = runner.run_steps_with_audio(1, None, 0);
assert_eq!(steps, 1);
assert!(running);
{
let playback = &runner.ppc_app.as_ref().unwrap().sound.file_playbacks[0];
assert!(playback.paused);
assert!(playback.active);
assert!(playback.pause_timing_valid);
assert_eq!(playback.pause_started_tick, 0);
assert_eq!(playback.pause_started_instruction_count, 2);
}
let (steps, running) = runner.run_steps_with_audio(INSTRUCTIONS_PER_TICK as usize, None, 0);
assert_eq!(steps, INSTRUCTIONS_PER_TICK as usize);
assert!(running);
assert_eq!(runner.dispatcher().tick_count, 1);
assert!(runner
.ppc_app
.as_ref()
.unwrap()
.sound
.completion_invocations
.is_empty());
runner.ppc_app.as_mut().unwrap().sound.file_playbacks[0].paused = false;
let (steps, running) = runner.run_steps_with_audio(1, None, 0);
assert_eq!(steps, 1);
assert!(running);
let shifted_due_instruction_count = {
let playback = &runner.ppc_app.as_ref().unwrap().sound.file_playbacks[0];
assert!(!playback.paused);
assert!(playback.active);
assert!(!playback.pause_timing_valid);
assert_eq!(playback.due_tick, 2);
assert!(
playback.due_instruction_count > 1 + u64::from(INSTRUCTIONS_PER_TICK),
"resume must shift due instructions past the original due time"
);
playback.due_instruction_count
};
let (steps, running) = runner.run_steps_with_audio(INSTRUCTIONS_PER_TICK as usize, None, 0);
assert_eq!(steps, INSTRUCTIONS_PER_TICK as usize);
assert!(running);
assert!(runner.dispatcher().tick_count >= 2);
let ppc_app = runner.ppc_app.as_ref().expect("PPC app should stay loaded");
assert!(!ppc_app.sound.file_playbacks[0].active);
assert!(ppc_app.sound.pending_completions.is_empty());
let mut memory = ppc_app.memory.clone();
assert_eq!(memory.read_u32_be(PPC_DATA_BASE), Some(channel));
assert_eq!(ppc_app.sound.completion_invocations.len(), 1);
let invocation = ppc_app.sound.completion_invocations[0];
assert!(invocation.tick >= 2);
assert!(invocation.instruction_count >= shifted_due_instruction_count);
assert_eq!(invocation.scheduled_tick, 2);
assert_eq!(
invocation.scheduled_instruction_count,
shifted_due_instruction_count
);
assert_eq!(invocation.unsupported_import_index, None);
}
#[test]
fn ppc_system_event_mask_write_enables_injected_key_up_events() {
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
app.ppc
.as_mut()
.expect("synthetic PPC app")
.memory
.add_region(PPC_HALT_PC, vec![0; 64 * 1024]);
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
runner
.ppc_app
.as_mut()
.expect("PPC app installed")
.memory
.write_u16_be(crate::memory::globals::addr::SYS_EVT_MASK, 0xffdf)
.unwrap();
runner.push_key_down(0x7c, 29);
runner.push_key_up(0x7c, 29);
assert!(runner
.dispatcher
.event_queue
.iter()
.any(|event| event.what == 4 && event.message == 0x0000_7c1d));
}
#[test]
fn ppc_cursor_level_controls_the_shared_host_cursor() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let mut toolbox_startup = crate::loader::ppc::PpcToolboxStartupState::default();
toolbox_startup.cursor_installed = true;
toolbox_startup.cursor_data[0] = 0x80;
toolbox_startup.cursor_mask[0] = 0xc0;
toolbox_startup.cursor_hot_v = 3;
toolbox_startup.cursor_hot_h = 4;
sync_ppc_cursor_state(&mut runner.dispatcher, -1, &toolbox_startup);
assert_eq!(runner.dispatcher.cursor_level(), -1);
assert!(!runner.dispatcher.cursor_visible());
assert_eq!(
runner.dispatcher.cursor_data(),
Some((
toolbox_startup.cursor_data,
toolbox_startup.cursor_mask,
3,
4
))
);
sync_ppc_cursor_state(&mut runner.dispatcher, 0, &toolbox_startup);
assert_eq!(runner.dispatcher.cursor_level(), 0);
assert!(runner.dispatcher.cursor_visible());
}
#[test]
fn ppc_queue_sync_preserves_autokey_posted_during_tick_advance() {
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
let ppc_app = app.ppc.as_mut().expect("PPC app");
ppc_app
.memory
.write_u32_be(PPC_CODE_BASE, 0x4800_0000)
.unwrap();
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
runner.set_instructions_per_tick(1);
runner.push_key_down(0x00, b'a');
for _ in 0..TrapDispatcher::AUTO_KEY_THRESHOLD_TICKS {
let (steps, running) = runner.run_steps(1, None);
assert_eq!(steps, 1);
assert!(running);
}
assert!(runner
.dispatcher
.event_queue
.iter()
.any(|event| { event.what == 5 && event.message == 0x0000_0061 }));
}
#[test]
fn ppc_getkeys_reads_runner_key_map_with_classic_packed_bit_order() {
let key_map_ptr = PPC_DATA_BASE;
let mut memory = PpcSectionMem::new();
memory.add_region(PPC_CODE_BASE, 0x4800_0002u32.to_be_bytes().to_vec());
memory.add_region(PPC_DATA_BASE, vec![0; 32]);
let mut cpu = PpcCpu::new();
cpu.pc = PPC_IMPORT_TRAP_BASE;
cpu.lr = PPC_CODE_BASE;
cpu.gpr[1] = PPC_STACK_TOP - 64;
cpu.gpr[3] = key_map_ptr;
let app = LoadedApp::from_ppc(PpcLoadedApp {
cpu,
memory,
entry_pc: PPC_IMPORT_TRAP_BASE,
rtoc: 0,
stack_base: PPC_STACK_BASE,
stack_size: PPC_STACK_SIZE,
stack_pointer: PPC_STACK_TOP - 64,
heap_base: PPC_HEAP_BASE,
heap_cursor: PPC_HEAP_BASE,
heap_limit: PPC_STACK_BASE,
last_mem_error: 0,
heap_maximized: false,
master_pointer_blocks_requested: 0,
tick_count: 0,
clock_cycles_per_tick: 1,
clock_cycle_phase: 0,
current_resource_refnum: 0,
last_resource_error: 0,
resource_load_enabled: true,
native_exception_handler: 0,
native_exception_stack: Vec::new(),
stdc_qsort_stack: Vec::new(),
dialog_callback_stack: Vec::new(),
apple_events: Default::default(),
cfm_connections: Vec::new(),
cfm_library_fragments: Vec::new(),
next_cfm_connection_id: 1,
ptrs: Vec::new(),
free_ptr_blocks: Vec::new(),
handles: Vec::new(),
free_handle_blocks: Vec::new(),
handle_states: Vec::new(),
controls: Vec::new(),
screen_clut: TrapDispatcher::standard_mac_8bpp_clut(),
device_gamma: crate::display::default_display_gamma(),
device_gamma_explicit: false,
color_manager_clut: TrapDispatcher::standard_mac_8bpp_clut(),
aliases: Vec::new(),
gworlds: Vec::new(),
q3_objects: Vec::new(),
q3_object_refs: Vec::new(),
next_q3_object: 0,
q3_error_state: Default::default(),
q3_lifecycle: Default::default(),
q3_memory_storages: Vec::new(),
q3_files: Vec::new(),
q3_group_memberships: Vec::new(),
q3_file_groups: Vec::new(),
q3_views: Vec::new(),
q3_submissions: Vec::new(),
q3_view_transforms: Vec::new(),
q3_submission_transforms: Vec::new(),
q3_view_materials: Vec::new(),
q3_submission_materials: Vec::new(),
q3_submission_lights: Vec::new(),
q3_view_state_stack: Vec::new(),
q3_completed_frames: Vec::new(),
q3_retained_frames: Vec::new(),
q3_state_only_completed_frame_batches: Vec::new(),
q3_fog_styles: Vec::new(),
q3_attributes: Vec::new(),
q3_shader_uv_transforms: Vec::new(),
q3_shader_boundaries: Vec::new(),
q3_mipmap_textures: Vec::new(),
q3_texture_shaders: Vec::new(),
q3_renderer_preferences: Vec::new(),
q3_draw_contexts: Vec::new(),
q3_trimeshes: Vec::new(),
q3_styles: Vec::new(),
q3_cameras: Vec::new(),
q3_lights: Vec::new(),
input_sprocket: Default::default(),
input_sprocket_virtual_elements: Vec::new(),
toolbox_startup: Default::default(),
quicktime: Default::default(),
sound: Default::default(),
timer_tasks: Vec::new(),
vbl_tasks: Vec::new(),
files: Vec::new(),
stdio_streams: ppc_initial_stdio_streams(),
vfs_files: Vec::new(),
deleted_vfs_file_paths: Vec::new(),
resource_files: Vec::new(),
vfs_resource_files: Vec::new(),
vfs_resources: Vec::new(),
next_file_ref_num: 128,
current_gworld: PPC_MAIN_GWORLD,
current_gdevice: PPC_MAIN_GDEVICE,
quickdraw_fore_color: PpcRgbColor {
red: 0,
green: 0,
blue: 0,
},
quickdraw_fore_indices: Default::default(),
quickdraw_back_color: PpcRgbColor {
red: 0xffff,
green: 0xffff,
blue: 0xffff,
},
quickdraw_pen_h: 0,
quickdraw_pen_v: 0,
quickdraw_text_mode: PPC_QD_TEXT_MODE_SRC_OR,
quickdraw_text_size: PPC_QD_TEXT_SIZE_SYSTEM,
quickdraw_cursor_level: 0,
vfs_volumes: Vec::new(),
vfs_directories: Vec::new(),
next_vfs_dir_id: 18,
default_dir_id: 2,
launched_app_path: None,
default_output_volume: 0,
param_text: [Vec::new(), Vec::new(), Vec::new(), Vec::new()],
scrap: Default::default(),
list_manager: Default::default(),
halt_pc: PPC_HALT_PC,
import_trap_base: PPC_IMPORT_TRAP_BASE,
import_count: 1,
imports: vec![PpcImportBinding {
library_index: 0,
symbol_index: 0,
library_name: "InterfaceLib".into(),
symbol_name: "GetKeys".into(),
class: 0,
weak: false,
address: PPC_IMPORT_TRAP_BASE,
tvector_address: None,
trap_pc: PPC_IMPORT_TRAP_BASE,
dispatcher_target: PpcImportDispatcherTarget::GetKeys,
}],
section_bases: Vec::new(),
input: PpcInputSnapshot::default(),
event_queue: Default::default(),
draw_sprocket: PpcDrawSprocketState::default(),
});
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
runner.push_key_down(0x00, b'a');
runner.push_key_down(0x7b, 28);
runner.push_key_down(0x31, b' ');
let (steps, running) = runner.run_steps(16, None);
assert!(!running);
assert_eq!(steps, 2);
let ppc_app = runner.ppc_app.as_mut().expect("PPC app should stay loaded");
assert_eq!(ppc_app.memory.read_u8(key_map_ptr), Some(0x01));
assert_eq!(ppc_app.memory.read_u8(key_map_ptr + 6), Some(0x02));
assert_eq!(ppc_app.memory.read_u8(key_map_ptr + 15), Some(0x08));
}
#[test]
#[cfg(any())]
fn ppc_imports_are_recorded_in_oracle_events_when_enabled() {
let key_map_ptr = PPC_DATA_BASE;
let mut memory = PpcSectionMem::new();
memory.add_region(PPC_CODE_BASE, 0x4800_0002u32.to_be_bytes().to_vec());
memory.add_region(PPC_DATA_BASE, vec![0; 32]);
let mut cpu = PpcCpu::new();
cpu.pc = PPC_IMPORT_TRAP_BASE;
cpu.lr = PPC_CODE_BASE;
cpu.gpr[1] = PPC_STACK_TOP - 64;
cpu.gpr[2] = 0x1234_5678;
cpu.gpr[3] = key_map_ptr;
let app = LoadedApp::from_ppc(PpcLoadedApp {
cpu,
memory,
entry_pc: PPC_IMPORT_TRAP_BASE,
rtoc: 0x1234_5678,
stack_base: PPC_STACK_BASE,
stack_size: PPC_STACK_SIZE,
stack_pointer: PPC_STACK_TOP - 64,
heap_base: PPC_HEAP_BASE,
heap_cursor: PPC_HEAP_BASE,
heap_limit: PPC_STACK_BASE,
last_mem_error: 0,
heap_maximized: false,
master_pointer_blocks_requested: 0,
tick_count: 0,
clock_cycles_per_tick: 1,
clock_cycle_phase: 0,
current_resource_refnum: 0,
last_resource_error: 0,
resource_load_enabled: true,
native_exception_handler: 0,
native_exception_stack: Vec::new(),
stdc_qsort_stack: Vec::new(),
dialog_callback_stack: Vec::new(),
apple_events: Default::default(),
cfm_connections: Vec::new(),
cfm_library_fragments: Vec::new(),
next_cfm_connection_id: 1,
ptrs: Vec::new(),
free_ptr_blocks: Vec::new(),
handles: Vec::new(),
free_handle_blocks: Vec::new(),
handle_states: Vec::new(),
controls: Vec::new(),
screen_clut: TrapDispatcher::standard_mac_8bpp_clut(),
device_gamma: crate::display::default_display_gamma(),
device_gamma_explicit: false,
color_manager_clut: TrapDispatcher::standard_mac_8bpp_clut(),
aliases: Vec::new(),
gworlds: Vec::new(),
q3_objects: Vec::new(),
q3_object_refs: Vec::new(),
next_q3_object: 0,
q3_error_state: Default::default(),
q3_lifecycle: Default::default(),
q3_memory_storages: Vec::new(),
q3_files: Vec::new(),
q3_group_memberships: Vec::new(),
q3_file_groups: Vec::new(),
q3_views: Vec::new(),
q3_submissions: Vec::new(),
q3_view_transforms: Vec::new(),
q3_submission_transforms: Vec::new(),
q3_view_materials: Vec::new(),
q3_submission_materials: Vec::new(),
q3_submission_lights: Vec::new(),
q3_view_state_stack: Vec::new(),
q3_completed_frames: Vec::new(),
q3_retained_frames: Vec::new(),
q3_state_only_completed_frame_batches: Vec::new(),
q3_fog_styles: Vec::new(),
q3_attributes: Vec::new(),
q3_shader_uv_transforms: Vec::new(),
q3_shader_boundaries: Vec::new(),
q3_mipmap_textures: Vec::new(),
q3_texture_shaders: Vec::new(),
q3_renderer_preferences: Vec::new(),
q3_draw_contexts: Vec::new(),
q3_trimeshes: Vec::new(),
q3_styles: Vec::new(),
q3_cameras: Vec::new(),
q3_lights: Vec::new(),
input_sprocket: Default::default(),
input_sprocket_virtual_elements: Vec::new(),
toolbox_startup: Default::default(),
quicktime: Default::default(),
sound: Default::default(),
timer_tasks: Vec::new(),
vbl_tasks: Vec::new(),
files: Vec::new(),
stdio_streams: ppc_initial_stdio_streams(),
vfs_files: Vec::new(),
deleted_vfs_file_paths: Vec::new(),
resource_files: Vec::new(),
vfs_resource_files: Vec::new(),
vfs_resources: Vec::new(),
next_file_ref_num: 128,
current_gworld: PPC_MAIN_GWORLD,
current_gdevice: PPC_MAIN_GDEVICE,
quickdraw_fore_color: PpcRgbColor {
red: 0,
green: 0,
blue: 0,
},
quickdraw_fore_indices: Default::default(),
quickdraw_back_color: PpcRgbColor {
red: 0xffff,
green: 0xffff,
blue: 0xffff,
},
quickdraw_pen_h: 0,
quickdraw_pen_v: 0,
quickdraw_text_mode: PPC_QD_TEXT_MODE_SRC_OR,
quickdraw_text_size: PPC_QD_TEXT_SIZE_SYSTEM,
quickdraw_cursor_level: 0,
vfs_volumes: Vec::new(),
vfs_directories: Vec::new(),
next_vfs_dir_id: 18,
default_dir_id: 2,
launched_app_path: None,
default_output_volume: 0,
param_text: [Vec::new(), Vec::new(), Vec::new(), Vec::new()],
scrap: Default::default(),
list_manager: Default::default(),
halt_pc: PPC_HALT_PC,
import_trap_base: PPC_IMPORT_TRAP_BASE,
import_count: 1,
imports: vec![PpcImportBinding {
library_index: 0,
symbol_index: 0,
library_name: "InterfaceLib".into(),
symbol_name: "GetKeys".into(),
class: 0,
weak: false,
address: PPC_IMPORT_TRAP_BASE,
tvector_address: None,
trap_pc: PPC_IMPORT_TRAP_BASE,
dispatcher_target: PpcImportDispatcherTarget::GetKeys,
}],
section_bases: Vec::new(),
input: PpcInputSnapshot::default(),
event_queue: Default::default(),
draw_sprocket: PpcDrawSprocketState::default(),
});
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
let output_dir = tempfile::tempdir().unwrap();
runner
.enable_oracle_recording(output_dir.path(), crate::oracle::OracleSource::Systemless)
.unwrap();
let (_steps, _running) = runner.run_steps(16, None);
let events_path = output_dir.path().join(crate::oracle::ORACLE_EVENTS_FILE);
let events = std::fs::read_to_string(events_path).unwrap();
let ppc_event: serde_json::Value = events
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.find(|value: &serde_json::Value| value["event"] == "ppc_import")
.expect("oracle recording should include a PPC import event");
assert_eq!(ppc_event["pc"], PPC_IMPORT_TRAP_BASE);
assert_eq!(ppc_event["fields"]["import_index"], "0");
assert_eq!(ppc_event["fields"]["library"], "InterfaceLib");
assert_eq!(ppc_event["fields"]["symbol"], "GetKeys");
assert_eq!(ppc_event["fields"]["rtoc"], "12345678");
assert_eq!(ppc_event["fields"]["dispatcher_target"], "GetKeys");
assert_eq!(ppc_event["fields"]["repeat_count"], "1");
}
#[test]
#[cfg(any())]
fn oracle_script_input_event_records_fields_without_snapshot() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let output_dir = tempfile::tempdir().unwrap();
runner
.enable_oracle_recording(output_dir.path(), crate::oracle::OracleSource::Systemless)
.unwrap();
runner
.record_oracle_script_input(BTreeMap::from([
("action".to_string(), "key_down".to_string()),
("key".to_string(), "space".to_string()),
("mac_key".to_string(), "31".to_string()),
("char_code".to_string(), "20".to_string()),
]))
.unwrap();
let events_path = output_dir.path().join(crate::oracle::ORACLE_EVENTS_FILE);
let events = std::fs::read_to_string(events_path).unwrap();
let script_input: serde_json::Value = events
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.find(|value: &serde_json::Value| value["event"] == "script_input")
.expect("oracle recording should include a script_input event");
assert_eq!(script_input["screen_event_count"], 0);
assert_eq!(script_input["fields"]["action"], "key_down");
assert_eq!(script_input["fields"]["key"], "space");
assert_eq!(script_input["fields"]["mac_key"], "31");
assert_eq!(script_input["fields"]["char_code"], "20");
let snapshots_path = output_dir.path().join(crate::oracle::ORACLE_SNAPSHOTS_FILE);
let snapshots = std::fs::read_to_string(snapshots_path).unwrap();
assert!(
snapshots.trim().is_empty(),
"script input events should not create screen snapshots"
);
}
#[test]
#[cfg(any())]
fn oracle_input_sprocket_event_records_simple_state_without_snapshot() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let output_dir = tempfile::tempdir().unwrap();
runner
.enable_oracle_recording(output_dir.path(), crate::oracle::OracleSource::Systemless)
.unwrap();
let mut input = PpcInputSnapshot::default();
input.key_map[0] = 0x01;
runner.record_input_sprocket_trace(&[PpcInputSprocketSimpleStateTraceEntry {
import_index: 7,
pc: 0x01f0_1234,
element: 0x0200_1000,
state_ptr: 0x0200_2000,
state: 0x0000_0001,
kind: 0x6275_746e,
kind_name: "button".to_string(),
fallback_state: 0,
need_name: "Fire".to_string(),
action_binding: "button/fire".to_string(),
input,
input_sprocket: PpcInputSprocketState {
initialized: true,
suspended: false,
keyboard_active: true,
mouse_active: true,
configure_count: 0,
virtual_element_count: 1,
last_virtual_need_count: 1,
last_virtual_needs_ptr: 0x0200_3000,
last_virtual_elements_out_ptr: 0x0200_4000,
},
}]);
let events_path = output_dir.path().join(crate::oracle::ORACLE_EVENTS_FILE);
let events = std::fs::read_to_string(events_path).unwrap();
let input_sprocket: serde_json::Value = events
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.find(|value: &serde_json::Value| value["event"] == "input_sprocket")
.expect("oracle recording should include an InputSprocket event");
assert_eq!(input_sprocket["pc"], 0x01f0_1234);
assert_eq!(input_sprocket["screen_event_count"], 0);
assert_eq!(input_sprocket["fields"]["import_index"], "7");
assert_eq!(input_sprocket["fields"]["element"], "02001000");
assert_eq!(input_sprocket["fields"]["state_ptr"], "02002000");
assert_eq!(input_sprocket["fields"]["state"], "00000001");
assert_eq!(input_sprocket["fields"]["kind"], "6275746E");
assert_eq!(input_sprocket["fields"]["kind_name"], "button");
assert_eq!(input_sprocket["fields"]["need_name"], "Fire");
assert_eq!(input_sprocket["fields"]["action_binding"], "button/fire");
assert_eq!(
input_sprocket["fields"]["key_map"],
"01000000000000000000000000000000"
);
assert_eq!(input_sprocket["fields"]["mouse_button"], "false");
assert_eq!(input_sprocket["fields"]["keyboard_active"], "true");
let snapshots_path = output_dir.path().join(crate::oracle::ORACLE_SNAPSHOTS_FILE);
let snapshots = std::fs::read_to_string(snapshots_path).unwrap();
assert!(
snapshots.trim().is_empty(),
"InputSprocket events should not create screen snapshots"
);
}
#[test]
#[cfg(any())]
fn oracle_draw_sprocket_event_records_swap_without_snapshot() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let output_dir = tempfile::tempdir().unwrap();
runner
.enable_oracle_recording(output_dir.path(), crate::oracle::OracleSource::Systemless)
.unwrap();
runner.record_draw_sprocket_trace(&[PpcDrawSprocketTraceEntry {
import_index: 12,
pc: 0x01f0_3456,
action: "swap_buffers".to_string(),
result: 0,
context: Some(0x0200_1000),
requested_state: None,
requested_frequency: None,
requested_width: None,
requested_height: None,
requested_context_options: None,
requested_display_depth_mask: None,
requested_back_buffer_depth_mask: None,
requested_display_depth: None,
requested_back_buffer_depth: None,
requested_page_count: None,
can_user_select: None,
fade_kind: None,
fade_percent: None,
fade_zero_red: None,
fade_zero_green: None,
fade_zero_blue: None,
reserved_context: Some(0x0200_1000),
active_context: Some(0x0200_1000),
context_state: "active".to_string(),
front_buffer_gworld: 0x00ab_cdef,
back_buffer_gworld: 0x0012_3456,
last_swap_context: Some(0x0200_1000),
swap_count: 3,
fade_count: 1,
frequency: 60 << 16,
width: 640,
height: 480,
context_options: 1,
display_depth_mask: 16,
back_buffer_depth_mask: 16,
display_depth: 16,
back_buffer_depth: 16,
page_count: 2,
}]);
let events_path = output_dir.path().join(crate::oracle::ORACLE_EVENTS_FILE);
let events = std::fs::read_to_string(events_path).unwrap();
let draw_sprocket: serde_json::Value = events
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.find(|value: &serde_json::Value| value["event"] == "draw_sprocket")
.expect("oracle recording should include a DrawSprocket event");
assert_eq!(draw_sprocket["pc"], 0x01f0_3456);
assert_eq!(draw_sprocket["screen_event_count"], 0);
assert_eq!(draw_sprocket["fields"]["import_index"], "12");
assert_eq!(draw_sprocket["fields"]["action"], "swap_buffers");
assert_eq!(draw_sprocket["fields"]["result"], "0");
assert_eq!(draw_sprocket["fields"]["result_hex"], "0000");
assert_eq!(draw_sprocket["fields"]["context"], "02001000");
assert_eq!(draw_sprocket["fields"]["requested_state"], "none");
assert_eq!(draw_sprocket["fields"]["requested_frequency"], "none");
assert_eq!(draw_sprocket["fields"]["requested_width"], "none");
assert_eq!(draw_sprocket["fields"]["requested_height"], "none");
assert_eq!(draw_sprocket["fields"]["requested_context_options"], "none");
assert_eq!(
draw_sprocket["fields"]["requested_display_depth_mask"],
"none"
);
assert_eq!(
draw_sprocket["fields"]["requested_back_buffer_depth_mask"],
"none"
);
assert_eq!(draw_sprocket["fields"]["requested_display_depth"], "none");
assert_eq!(
draw_sprocket["fields"]["requested_back_buffer_depth"],
"none"
);
assert_eq!(draw_sprocket["fields"]["requested_page_count"], "none");
assert_eq!(draw_sprocket["fields"]["can_user_select"], "none");
assert_eq!(draw_sprocket["fields"]["fade_kind"], "none");
assert_eq!(draw_sprocket["fields"]["fade_percent"], "none");
assert_eq!(draw_sprocket["fields"]["fade_zero_red"], "none");
assert_eq!(draw_sprocket["fields"]["fade_zero_green"], "none");
assert_eq!(draw_sprocket["fields"]["fade_zero_blue"], "none");
assert_eq!(draw_sprocket["fields"]["reserved_context"], "02001000");
assert_eq!(draw_sprocket["fields"]["active_context"], "02001000");
assert_eq!(draw_sprocket["fields"]["has_reserved_context"], "true");
assert_eq!(draw_sprocket["fields"]["has_active_context"], "true");
assert_eq!(draw_sprocket["fields"]["context_state"], "active");
assert_eq!(draw_sprocket["fields"]["front_gworld"], "00ABCDEF");
assert_eq!(draw_sprocket["fields"]["back_gworld"], "00123456");
assert_eq!(draw_sprocket["fields"]["last_swap_context"], "02001000");
assert_eq!(draw_sprocket["fields"]["swap_count"], "3");
assert_eq!(draw_sprocket["fields"]["fade_count"], "1");
assert_eq!(draw_sprocket["fields"]["frequency"], "3932160");
assert_eq!(draw_sprocket["fields"]["width"], "640");
assert_eq!(draw_sprocket["fields"]["height"], "480");
assert_eq!(draw_sprocket["fields"]["context_options"], "1");
assert_eq!(draw_sprocket["fields"]["display_depth_mask"], "16");
assert_eq!(draw_sprocket["fields"]["back_buffer_depth_mask"], "16");
assert_eq!(draw_sprocket["fields"]["display_depth"], "16");
assert_eq!(draw_sprocket["fields"]["back_buffer_depth"], "16");
assert_eq!(draw_sprocket["fields"]["page_count"], "2");
let snapshots_path = output_dir.path().join(crate::oracle::ORACLE_SNAPSHOTS_FILE);
let snapshots = std::fs::read_to_string(snapshots_path).unwrap();
assert!(
snapshots.trim().is_empty(),
"DrawSprocket events should not create screen snapshots"
);
}
#[test]
fn ppc_gui_cpu_slice_defers_front_buffer_sync_until_composite() {
let front_base = PPC_HEAP_BASE;
let presented_base = PPC_HEAP_BASE + 4;
let mut memory = PpcSectionMem::new();
memory.add_region(PPC_CODE_BASE, 0x4800_0002u32.to_be_bytes().to_vec());
memory.add_region(front_base, vec![0x00, 0x1f, 0x00, 0x1f]);
memory.add_region(presented_base, vec![0x7c, 0x00, 0x03, 0xe0]);
let mut cpu = PpcCpu::new();
cpu.pc = PPC_CODE_BASE;
cpu.lr = PPC_HALT_PC;
cpu.gpr[1] = PPC_STACK_TOP - 64;
let app = LoadedApp::from_ppc(PpcLoadedApp {
cpu,
memory,
entry_pc: PPC_CODE_BASE,
rtoc: 0,
stack_base: PPC_STACK_BASE,
stack_size: PPC_STACK_SIZE,
stack_pointer: PPC_STACK_TOP - 64,
heap_base: PPC_HEAP_BASE,
heap_cursor: PPC_HEAP_BASE + 8,
heap_limit: PPC_STACK_BASE,
last_mem_error: 0,
heap_maximized: false,
master_pointer_blocks_requested: 0,
tick_count: 0,
clock_cycles_per_tick: 1,
clock_cycle_phase: 0,
current_resource_refnum: 0,
last_resource_error: 0,
resource_load_enabled: true,
native_exception_handler: 0,
native_exception_stack: Vec::new(),
stdc_qsort_stack: Vec::new(),
dialog_callback_stack: Vec::new(),
apple_events: Default::default(),
cfm_connections: Vec::new(),
cfm_library_fragments: Vec::new(),
next_cfm_connection_id: 1,
ptrs: Vec::new(),
free_ptr_blocks: Vec::new(),
handles: Vec::new(),
free_handle_blocks: Vec::new(),
handle_states: Vec::new(),
controls: Vec::new(),
screen_clut: TrapDispatcher::standard_mac_8bpp_clut(),
device_gamma: crate::display::default_display_gamma(),
device_gamma_explicit: false,
color_manager_clut: TrapDispatcher::standard_mac_8bpp_clut(),
aliases: Vec::new(),
gworlds: vec![
PpcGWorldRecord {
port: PPC_MAIN_GWORLD,
pixmap_handle: 0,
pixmap: 0,
base_addr: front_base,
gdevice: PPC_MAIN_GDEVICE,
width: 2,
height: 1,
depth: 16,
row_bytes: 4,
pixels_locked: false,
pixels_no_purge: false,
},
PpcGWorldRecord {
port: PPC_DSP_BACK_GWORLD,
pixmap_handle: 0,
pixmap: 0,
base_addr: presented_base,
gdevice: PPC_MAIN_GDEVICE,
width: 2,
height: 1,
depth: 16,
row_bytes: 4,
pixels_locked: false,
pixels_no_purge: false,
},
],
q3_objects: Vec::new(),
q3_object_refs: Vec::new(),
next_q3_object: 0,
q3_error_state: Default::default(),
q3_lifecycle: Default::default(),
q3_memory_storages: Vec::new(),
q3_files: Vec::new(),
q3_group_memberships: Vec::new(),
q3_file_groups: Vec::new(),
q3_views: Vec::new(),
q3_submissions: Vec::new(),
q3_view_transforms: Vec::new(),
q3_submission_transforms: Vec::new(),
q3_view_materials: Vec::new(),
q3_submission_materials: Vec::new(),
q3_submission_lights: Vec::new(),
q3_view_state_stack: Vec::new(),
q3_completed_frames: Vec::new(),
q3_retained_frames: Vec::new(),
q3_state_only_completed_frame_batches: Vec::new(),
q3_fog_styles: Vec::new(),
q3_attributes: Vec::new(),
q3_shader_uv_transforms: Vec::new(),
q3_shader_boundaries: Vec::new(),
q3_mipmap_textures: Vec::new(),
q3_texture_shaders: Vec::new(),
q3_renderer_preferences: Vec::new(),
q3_draw_contexts: Vec::new(),
q3_trimeshes: Vec::new(),
q3_styles: Vec::new(),
q3_cameras: Vec::new(),
q3_lights: Vec::new(),
input_sprocket: Default::default(),
input_sprocket_virtual_elements: Vec::new(),
toolbox_startup: Default::default(),
quicktime: Default::default(),
sound: Default::default(),
timer_tasks: Vec::new(),
vbl_tasks: Vec::new(),
files: Vec::new(),
stdio_streams: ppc_initial_stdio_streams(),
vfs_files: Vec::new(),
deleted_vfs_file_paths: Vec::new(),
resource_files: Vec::new(),
vfs_resource_files: Vec::new(),
vfs_resources: Vec::new(),
next_file_ref_num: 128,
current_gworld: PPC_MAIN_GWORLD,
current_gdevice: PPC_MAIN_GDEVICE,
quickdraw_fore_color: PpcRgbColor {
red: 0,
green: 0,
blue: 0,
},
quickdraw_fore_indices: Default::default(),
quickdraw_back_color: PpcRgbColor {
red: 0xffff,
green: 0xffff,
blue: 0xffff,
},
quickdraw_pen_h: 0,
quickdraw_pen_v: 0,
quickdraw_text_mode: PPC_QD_TEXT_MODE_SRC_OR,
quickdraw_text_size: PPC_QD_TEXT_SIZE_SYSTEM,
quickdraw_cursor_level: 0,
vfs_volumes: Vec::new(),
vfs_directories: Vec::new(),
next_vfs_dir_id: 18,
default_dir_id: 2,
launched_app_path: None,
default_output_volume: 0,
param_text: [Vec::new(), Vec::new(), Vec::new(), Vec::new()],
scrap: Default::default(),
list_manager: Default::default(),
halt_pc: PPC_HALT_PC,
import_trap_base: PPC_IMPORT_TRAP_BASE,
import_count: 0,
imports: Vec::new(),
section_bases: Vec::new(),
input: PpcInputSnapshot::default(),
event_queue: Default::default(),
draw_sprocket: PpcDrawSprocketState {
front_buffer_gworld: PPC_DSP_BACK_GWORLD,
back_buffer_gworld: PPC_MAIN_GWORLD,
swap_count: 1,
..PpcDrawSprocketState::default()
},
});
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
let initial_screen_mode = runner.dispatcher.screen_mode;
let (steps, running) = runner.run_gui_cpu_slice(8, u32::MAX);
assert_eq!(steps, 1);
assert!(!running);
assert_eq!(
runner.dispatcher.screen_mode, initial_screen_mode,
"CPU-only slices must not copy or resize the host framebuffer"
);
runner.composite_frame();
let (host_base, row_bytes, width, height, depth) = runner.dispatcher.screen_mode;
assert_eq!((row_bytes, width, height, depth), (4, 2, 1, 16));
assert_eq!(runner.bus.read_word(host_base), 0x7c00);
assert_eq!(runner.bus.read_word(host_base + 2), 0x03e0);
assert_eq!(
runner
.bus
.read_long(crate::memory::globals::addr::SCRN_BASE),
host_base
);
assert_eq!(
runner
.bus
.read_long(crate::memory::globals::addr::SCREEN_BITS),
host_base
);
assert_eq!(
runner
.bus
.read_word(crate::memory::globals::addr::SCREEN_BITS + 4),
row_bytes as u16
);
let main_gdevice_handle = runner.dispatcher.main_gdevice_handle;
assert_ne!(main_gdevice_handle, 0);
let main_gdevice = runner.bus.read_long(main_gdevice_handle);
let main_pixmap_handle = runner.bus.read_long(main_gdevice + 22);
let main_pixmap = runner.bus.read_long(main_pixmap_handle);
assert_eq!(runner.bus.read_long(main_pixmap), host_base);
assert_eq!(
runner.bus.read_word(main_pixmap + 4),
0x8000 | row_bytes as u16
);
assert_eq!(runner.bus.read_word(main_pixmap + 10), height);
assert_eq!(runner.bus.read_word(main_pixmap + 12), width);
assert_eq!(runner.bus.read_word(main_pixmap + 30), 16);
assert_eq!(runner.bus.read_word(main_pixmap + 32), depth);
assert_eq!(runner.bus.read_word(main_pixmap + 34), 3);
assert_eq!(runner.bus.read_word(main_pixmap + 36), 5);
assert_eq!(runner.bus.read_long(main_pixmap + 42), 0);
assert_eq!(runner.bus.read_word(main_gdevice + 4), 2);
assert_eq!(runner.bus.read_word(main_gdevice + 38), height);
assert_eq!(runner.bus.read_word(main_gdevice + 40), width);
assert_eq!(
runner.bus.read_long(main_gdevice + 42),
u32::from(crate::display::classic_depth_mode(depth).unwrap())
);
let mut ppc_app = runner.ppc_app.take().expect("PPC app should stay loaded");
ppc_app.draw_sprocket.last_fade_percent = Some(0);
ppc_app.draw_sprocket.last_fade_zero_color = None;
assert_eq!(ppc_app.memory.read_u16_be(presented_base), Some(0x7c00));
assert_eq!(ppc_app.memory.read_u16_be(presented_base + 2), Some(0x03e0));
runner.sync_ppc_front_buffer_to_host(&mut ppc_app);
assert_eq!(runner.bus.read_word(host_base), 0x0000);
assert_eq!(runner.bus.read_word(host_base + 2), 0x0000);
assert_eq!(ppc_app.memory.read_u16_be(presented_base), Some(0x7c00));
assert_eq!(ppc_app.memory.read_u16_be(presented_base + 2), Some(0x03e0));
runner.ppc_app = Some(ppc_app);
}
#[test]
fn ppc_completed_q3_frame_renders_before_host_front_buffer_sync() {
const TRIMESH_NUM_TRIANGLES_OFFSET: u32 = 4;
const TRIMESH_TRIANGLES_OFFSET: u32 = 8;
const TRIMESH_NUM_POINTS_OFFSET: u32 = 36;
const TRIMESH_POINTS_OFFSET: u32 = 40;
let front_base = PPC_HEAP_BASE;
let trimesh_data = PPC_DATA_BASE;
let triangles_ptr = PPC_DATA_BASE + 0x80;
let points_ptr = PPC_DATA_BASE + 0xc0;
let view = 0x0100_0000;
let identity = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
];
let mut memory = PpcSectionMem::new();
memory.add_region(PPC_CODE_BASE, 0x4e80_0020u32.to_be_bytes().to_vec());
memory.add_region(front_base, vec![0; 8 * 16]);
memory.add_region(PPC_DATA_BASE, vec![0; 0x200]);
memory
.write_u32_be(trimesh_data + TRIMESH_NUM_TRIANGLES_OFFSET, 1)
.unwrap();
memory
.write_u32_be(trimesh_data + TRIMESH_TRIANGLES_OFFSET, triangles_ptr)
.unwrap();
memory
.write_u32_be(trimesh_data + TRIMESH_NUM_POINTS_OFFSET, 3)
.unwrap();
memory
.write_u32_be(trimesh_data + TRIMESH_POINTS_OFFSET, points_ptr)
.unwrap();
for (offset, value) in [
(0, 0.0f32),
(4, 0.0),
(8, 0.0),
(12, 0.0),
(16, 0.9),
(20, 0.0),
(24, 0.9),
(28, 0.0),
(32, 0.0),
] {
memory
.write_u32_be(points_ptr + offset, value.to_bits())
.unwrap();
}
memory.write_u32_be(triangles_ptr, 0).unwrap();
memory.write_u32_be(triangles_ptr + 4, 1).unwrap();
memory.write_u32_be(triangles_ptr + 8, 2).unwrap();
let mut cpu = PpcCpu::new();
cpu.pc = PPC_CODE_BASE;
cpu.lr = PPC_HALT_PC;
cpu.gpr[1] = PPC_STACK_TOP - 64;
let app = LoadedApp::from_ppc(PpcLoadedApp {
cpu,
memory,
entry_pc: PPC_CODE_BASE,
rtoc: 0,
stack_base: PPC_STACK_BASE,
stack_size: PPC_STACK_SIZE,
stack_pointer: PPC_STACK_TOP - 64,
heap_base: PPC_HEAP_BASE,
heap_cursor: PPC_HEAP_BASE + 8 * 16,
heap_limit: PPC_STACK_BASE,
last_mem_error: 0,
heap_maximized: false,
master_pointer_blocks_requested: 0,
tick_count: 0,
clock_cycles_per_tick: 1,
clock_cycle_phase: 0,
current_resource_refnum: 0,
last_resource_error: 0,
resource_load_enabled: true,
native_exception_handler: 0,
native_exception_stack: Vec::new(),
stdc_qsort_stack: Vec::new(),
dialog_callback_stack: Vec::new(),
apple_events: Default::default(),
cfm_connections: Vec::new(),
cfm_library_fragments: Vec::new(),
next_cfm_connection_id: 1,
ptrs: Vec::new(),
free_ptr_blocks: Vec::new(),
handles: Vec::new(),
free_handle_blocks: Vec::new(),
handle_states: Vec::new(),
controls: Vec::new(),
screen_clut: TrapDispatcher::standard_mac_8bpp_clut(),
device_gamma: crate::display::default_display_gamma(),
device_gamma_explicit: false,
color_manager_clut: TrapDispatcher::standard_mac_8bpp_clut(),
aliases: Vec::new(),
gworlds: vec![PpcGWorldRecord {
port: PPC_MAIN_GWORLD,
pixmap_handle: 0,
pixmap: 0,
base_addr: front_base,
gdevice: PPC_MAIN_GDEVICE,
width: 8,
height: 8,
depth: 16,
row_bytes: 16,
pixels_locked: false,
pixels_no_purge: false,
}],
q3_objects: Vec::new(),
q3_object_refs: Vec::new(),
next_q3_object: 0,
q3_error_state: Default::default(),
q3_lifecycle: Default::default(),
q3_memory_storages: Vec::new(),
q3_files: Vec::new(),
q3_group_memberships: Vec::new(),
q3_file_groups: Vec::new(),
q3_views: vec![PpcQ3ViewStateRecord {
view,
renderer: 0,
light_group: 0,
draw_context: 0,
camera: 0,
rendering_depth: 0,
bounding_box_depth: 0,
cancelled: false,
}],
q3_submissions: Vec::new(),
q3_view_transforms: Vec::new(),
q3_submission_transforms: Vec::new(),
q3_view_materials: Vec::new(),
q3_submission_materials: Vec::new(),
q3_submission_lights: Vec::new(),
q3_view_state_stack: Vec::new(),
q3_completed_frames: vec![PpcQ3CompletedFrameRecord {
view,
submissions: vec![PpcQ3SubmissionRecord {
view,
kind: PpcQ3SubmissionKind::TriMesh,
primary: trimesh_data,
secondary: 0,
}],
submission_transforms: vec![PpcQ3SubmissionTransformRecord {
view,
kind: PpcQ3SubmissionKind::TriMesh,
primary: trimesh_data,
secondary: 0,
local_to_world: identity,
}],
submission_materials: vec![PpcQ3SubmissionMaterialRecord {
view,
kind: PpcQ3SubmissionKind::TriMesh,
primary: trimesh_data,
secondary: 0,
shader: 0,
illumination_type: u32::from_be_bytes(*b"phil"),
styles: Vec::new(),
fog_style: None,
attributes: Vec::new(),
shader_uv_transform: None,
shader_boundary: None,
texture_shader: None,
mipmap_texture: None,
}],
submission_lights: vec![PpcQ3SubmissionLightRecord {
view,
kind: PpcQ3SubmissionKind::TriMesh,
primary: trimesh_data,
secondary: 0,
light_group: 0,
lights: Vec::new(),
}],
retained_trimeshes: Vec::new(),
}],
q3_retained_frames: Vec::new(),
q3_state_only_completed_frame_batches: Vec::new(),
q3_fog_styles: Vec::new(),
q3_attributes: Vec::new(),
q3_shader_uv_transforms: Vec::new(),
q3_shader_boundaries: Vec::new(),
q3_mipmap_textures: Vec::new(),
q3_texture_shaders: Vec::new(),
q3_renderer_preferences: Vec::new(),
q3_draw_contexts: Vec::new(),
q3_trimeshes: Vec::new(),
q3_styles: Vec::new(),
q3_cameras: Vec::new(),
q3_lights: Vec::new(),
input_sprocket: Default::default(),
input_sprocket_virtual_elements: Vec::new(),
toolbox_startup: Default::default(),
quicktime: Default::default(),
sound: Default::default(),
timer_tasks: Vec::new(),
vbl_tasks: Vec::new(),
files: Vec::new(),
stdio_streams: ppc_initial_stdio_streams(),
vfs_files: Vec::new(),
deleted_vfs_file_paths: Vec::new(),
resource_files: Vec::new(),
vfs_resource_files: Vec::new(),
vfs_resources: Vec::new(),
next_file_ref_num: 128,
current_gworld: PPC_MAIN_GWORLD,
current_gdevice: PPC_MAIN_GDEVICE,
quickdraw_fore_color: PpcRgbColor {
red: 0,
green: 0,
blue: 0,
},
quickdraw_fore_indices: Default::default(),
quickdraw_back_color: PpcRgbColor {
red: 0xffff,
green: 0xffff,
blue: 0xffff,
},
quickdraw_pen_h: 0,
quickdraw_pen_v: 0,
quickdraw_text_mode: PPC_QD_TEXT_MODE_SRC_OR,
quickdraw_text_size: PPC_QD_TEXT_SIZE_SYSTEM,
quickdraw_cursor_level: 0,
vfs_volumes: Vec::new(),
vfs_directories: Vec::new(),
next_vfs_dir_id: 18,
default_dir_id: 2,
launched_app_path: None,
default_output_volume: 0,
param_text: [Vec::new(), Vec::new(), Vec::new(), Vec::new()],
scrap: Default::default(),
list_manager: Default::default(),
halt_pc: PPC_HALT_PC,
import_trap_base: PPC_IMPORT_TRAP_BASE,
import_count: 0,
imports: Vec::new(),
section_bases: Vec::new(),
input: PpcInputSnapshot::default(),
event_queue: Default::default(),
draw_sprocket: PpcDrawSprocketState::default(),
});
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.init_app(&app);
let (steps, running) = runner.run_steps(8, None);
assert_eq!(steps, 1);
assert!(!running);
let (host_base, row_bytes, width, height, depth) = runner.dispatcher.screen_mode;
assert_eq!((row_bytes, width, height, depth), (16, 8, 8, 16));
assert_eq!(
runner.bus.read_word(host_base + 4 * row_bytes + 4 * 2),
0x7fff
);
let ppc_app = runner.ppc_app.as_ref().expect("PPC app should stay loaded");
assert!(ppc_app.q3_completed_frames.is_empty());
assert_eq!(runner.q3_completed_frame_index, 1);
}
#[test]
fn ppc_host_sync_excludes_scanline_padding_from_visible_rows() {
for (depth, padded_row_bytes, visible_row_bytes) in [
(1, 96, 80),
(2, 176, 160),
(4, 336, 320),
(8, 656, 640),
(16, 1296, 1280),
] {
let front_buffer = PpcFrontBuffer {
base_addr: 0x1000,
row_bytes: padded_row_bytes,
width: 640,
height: 480,
depth,
};
assert_eq!(
FixtureRunner::ppc_front_buffer_visible_row_bytes(front_buffer),
Some(visible_row_bytes),
"{depth}bpp visible row"
);
}
}
#[test]
fn ppc_packed_indexed_row_copy_preserves_neighbors_at_non_byte_offsets() {
let mut one_bit = [0u8; 2];
assert!(FixtureRunner::copy_ppc_packed_indexed_row(
&[0b1010_0000],
1,
4,
&mut one_bit,
3,
));
assert_eq!(one_bit, [0x14, 0x00]);
let mut two_bit = [0xffu8; 3];
assert!(FixtureRunner::copy_ppc_packed_indexed_row(
&[0b00_01_10_11, 0b01_00_00_00],
2,
5,
&mut two_bit,
1,
));
assert_eq!(two_bit, [0xc6, 0xdf, 0xff]);
let mut four_bit = [0xaau8; 3];
assert!(FixtureRunner::copy_ppc_packed_indexed_row(
&[0x12, 0x30],
4,
3,
&mut four_bit,
1,
));
assert_eq!(four_bit, [0xa1, 0x23, 0xaa]);
}
#[test]
fn ppc_indexed_matte_repeats_darkest_representable_clut_index() {
for depth in [1u16, 2, 4, 8] {
let (clut, _) =
TrapDispatcher::standard_mac_indexed_clut(depth).expect("standard indexed depth");
assert_eq!(
FixtureRunner::ppc_indexed_matte_byte(u32::from(depth), &clut),
Some(0xff),
"{depth}bpp standard black index"
);
}
let mut clut = [[0u16; 3]; 256];
clut[0] = [0xffff, 0xffff, 0xffff];
clut[1] = [0xaaaa, 0xaaaa, 0xaaaa];
clut[2] = [0x0000, 0x0000, 0x0000];
clut[3] = [0x5555, 0x5555, 0x5555];
assert_eq!(FixtureRunner::ppc_indexed_matte_byte(2, &clut), Some(0xaa));
}
#[test]
fn ppc_host_clut_sync_grows_a_lower_depth_main_color_table() {
let config = FixtureRunnerConfig::default()
.with_screen_depth(1)
.expect("1bpp mode");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, config);
let gdevice_handle = runner.dispatcher.ensure_main_gdevice(&mut runner.bus);
let gdevice = runner.bus.read_long(gdevice_handle);
let pixmap = runner.bus.read_long(runner.bus.read_long(gdevice + 22));
let color_table_handle = runner.bus.read_long(pixmap + 42);
let old_color_table = runner.bus.read_long(color_table_handle);
let (mut clut, _) = TrapDispatcher::standard_mac_indexed_clut(4).expect("4bpp CLUT");
clut[7] = [0x1234, 0x5678, 0x9abc];
assert!(runner.sync_ppc_host_indexed_color_table(4, &clut));
let color_table = runner.bus.read_long(color_table_handle);
assert_ne!(color_table, old_color_table);
assert_eq!(runner.bus.get_alloc_size(old_color_table), None);
assert_eq!(runner.bus.get_alloc_size(color_table), Some(8 + 16 * 8));
assert_eq!(runner.bus.read_word(color_table + 6), 15);
assert_eq!(
[
runner.bus.read_word(color_table + 8 + 7 * 8 + 2),
runner.bus.read_word(color_table + 8 + 7 * 8 + 4),
runner.bus.read_word(color_table + 8 + 7 * 8 + 6),
],
clut[7]
);
}
#[test]
fn ppc_host_sync_restores_indexed_pm_table_across_direct_color_transitions() {
const WIDTH: u32 = 8;
const HEIGHT: u32 = 1;
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
let ppc_app = app.ppc.as_mut().expect("PPC app");
ppc_app.memory.add_region(PPC_HEAP_BASE, vec![0; 16]);
ppc_app.heap_cursor = PPC_HEAP_BASE + 16;
ppc_app.gworlds.push(PpcGWorldRecord {
port: PPC_MAIN_GWORLD,
pixmap_handle: 0,
pixmap: 0,
base_addr: PPC_HEAP_BASE,
gdevice: PPC_MAIN_GDEVICE,
width: WIDTH,
height: HEIGHT,
depth: 16,
row_bytes: 16,
pixels_locked: false,
pixels_no_purge: false,
});
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let gdevice_handle = runner.dispatcher.ensure_main_gdevice(&mut runner.bus);
let gdevice = runner.bus.read_long(gdevice_handle);
let pixmap = runner.bus.read_long(runner.bus.read_long(gdevice + 22));
let indexed_ctab_handle = runner.bus.read_long(pixmap + 42);
assert_ne!(indexed_ctab_handle, 0);
runner.sync_ppc_front_buffer_to_host(ppc_app);
assert_eq!(runner.bus.read_word(pixmap + 30), 16);
assert_eq!(runner.bus.read_word(pixmap + 32), 16);
assert_eq!(runner.bus.read_word(pixmap + 34), 3);
assert_eq!(runner.bus.read_word(pixmap + 36), 5);
assert_eq!(runner.bus.read_long(pixmap + 42), 0);
assert_eq!(runner.bus.read_long(gdevice + 42), 0x0084);
let host_mirror_base = runner.ppc_host_mirror_base;
let host_mirror_capacity = runner.ppc_host_mirror_capacity;
assert_eq!(runner.dispatcher.screen_mode.0, host_mirror_base);
assert_eq!(host_mirror_capacity, 16);
assert_eq!(runner.bus.get_alloc_size(host_mirror_base), Some(16));
let canary = runner.bus.alloc(8);
runner.bus.fill_bytes(canary, 8, 0x5a);
let (two_bit_clut, _) = TrapDispatcher::standard_mac_indexed_clut(2).expect("2bpp CLUT");
ppc_app.screen_clut = two_bit_clut;
ppc_app.color_manager_clut = two_bit_clut;
ppc_app.gworlds[0].depth = 2;
ppc_app.gworlds[0].row_bytes = 2;
runner.sync_ppc_front_buffer_to_host(ppc_app);
assert_eq!(runner.bus.read_word(pixmap + 30), 0);
assert_eq!(runner.bus.read_word(pixmap + 32), 2);
assert_eq!(runner.bus.read_word(pixmap + 34), 1);
assert_eq!(runner.bus.read_word(pixmap + 36), 2);
assert_eq!(runner.bus.read_long(pixmap + 42), indexed_ctab_handle);
let indexed_ctab = runner.bus.read_long(indexed_ctab_handle);
assert_ne!(indexed_ctab, 0);
assert_eq!(runner.bus.read_word(indexed_ctab + 6), 3);
assert_eq!(runner.bus.read_long(gdevice + 42), 0x0081);
let heap_after_first_transition = runner.bus.heap_bump_ptr();
for _ in 0..8 {
ppc_app.gworlds[0].depth = 16;
ppc_app.gworlds[0].row_bytes = 16;
runner.sync_ppc_front_buffer_to_host(ppc_app);
assert_eq!(runner.dispatcher.screen_mode.0, host_mirror_base);
assert_eq!(runner.ppc_host_mirror_base, host_mirror_base);
assert_eq!(runner.ppc_host_mirror_capacity, host_mirror_capacity);
ppc_app.gworlds[0].depth = 2;
ppc_app.gworlds[0].row_bytes = 2;
runner.sync_ppc_front_buffer_to_host(ppc_app);
assert_eq!(runner.dispatcher.screen_mode.0, host_mirror_base);
assert_eq!(runner.ppc_host_mirror_base, host_mirror_base);
assert_eq!(runner.ppc_host_mirror_capacity, host_mirror_capacity);
}
ppc_app.gworlds[0].depth = 16;
ppc_app.gworlds[0].row_bytes = 16;
runner.sync_ppc_front_buffer_to_host(ppc_app);
assert_eq!(runner.bus.read_word(pixmap + 30), 16);
assert_eq!(runner.bus.read_word(pixmap + 32), 16);
assert_eq!(runner.bus.read_word(pixmap + 34), 3);
assert_eq!(runner.bus.read_word(pixmap + 36), 5);
assert_eq!(runner.bus.read_long(pixmap + 42), 0);
assert_eq!(runner.ppc_host_indexed_ctab_handle, indexed_ctab_handle);
assert_eq!(runner.bus.read_word(indexed_ctab + 6), 3);
assert_eq!(runner.bus.read_long(gdevice + 42), 0x0084);
assert_eq!(runner.bus.get_alloc_size(host_mirror_base), Some(16));
assert_eq!(runner.bus.read_bytes(canary, 8), vec![0x5a; 8]);
assert_eq!(runner.bus.heap_bump_ptr(), heap_after_first_transition);
}
#[test]
fn ppc_packed_front_buffers_sync_centered_pixels_and_color_state() {
const WIDTH: u32 = 513;
const HEIGHT: u32 = 342;
const CANVAS_WIDTH: u32 = 800;
const CANVAS_HEIGHT: u32 = 600;
const DESTINATION_X: u32 = (CANVAS_WIDTH - WIDTH) / 2;
const DESTINATION_Y: u32 = (CANVAS_HEIGHT - HEIGHT) / 2;
for (depth, source_pixels) in [
(1u16, [1u8, 0, 1, 0]),
(2u16, [0u8, 1, 2, 3]),
(4u16, [1u8, 2, 3, 0]),
] {
let row_bytes = WIDTH.checked_mul(u32::from(depth)).unwrap().div_ceil(8);
let mut pixels = vec![0u8; (row_bytes * HEIGHT) as usize];
let field_mask = ((1u16 << depth) - 1) as u8;
for (x, pixel) in source_pixels.into_iter().enumerate() {
let bit = x as u32 * u32::from(depth);
let shift = 8 - u32::from(depth) - (bit & 7);
pixels[(bit / 8) as usize] |= (pixel & field_mask) << shift;
}
let mut app = halted_ppc_app_with_sound(PpcSoundState::default());
let ppc_app = app.ppc.as_mut().expect("PPC app");
ppc_app.memory.add_region(PPC_HEAP_BASE, pixels);
ppc_app.heap_cursor = PPC_HEAP_BASE + row_bytes * HEIGHT;
ppc_app.gworlds.push(PpcGWorldRecord {
port: PPC_MAIN_GWORLD,
pixmap_handle: 0,
pixmap: 0,
base_addr: PPC_HEAP_BASE,
gdevice: PPC_MAIN_GDEVICE,
width: WIDTH,
height: HEIGHT,
depth: u32::from(depth),
row_bytes,
pixels_locked: false,
pixels_no_purge: false,
});
let (mut device_clut, _) =
TrapDispatcher::standard_mac_indexed_clut(depth).expect("standard indexed depth");
device_clut[0][0] = 0xfffe;
let mut color_manager_clut = device_clut;
color_manager_clut[0][1] = 0xfffd;
ppc_app.screen_clut = device_clut;
ppc_app.color_manager_clut = color_manager_clut;
ppc_app.device_gamma = crate::display::linear_display_gamma();
let config = FixtureRunnerConfig::default()
.with_screen_depth(depth)
.expect("packed indexed mode");
let mut runner = FixtureRunner::new(8 * 1024 * 1024, config);
let mut ppc_app = app.ppc.take().expect("PPC app");
runner.sync_ppc_front_buffer_to_host(&mut ppc_app);
let (base, host_row_bytes, width, height, host_depth) = runner.dispatcher.screen_mode;
assert_eq!(
(width, height, host_depth),
(CANVAS_WIDTH as u16, CANVAS_HEIGHT as u16, depth)
);
assert_eq!(
host_row_bytes,
CANVAS_WIDTH * u32::from(depth) / 8,
"{depth}bpp packed canvas stride"
);
assert_eq!(runner.dispatcher.device_clut, device_clut);
assert_eq!(runner.dispatcher.color_manager_clut, color_manager_clut);
assert_eq!(
runner.dispatcher.device_gamma,
crate::display::linear_display_gamma()
);
let gdevice = runner.bus.read_long(runner.dispatcher.main_gdevice_handle);
let pixmap = runner.bus.read_long(runner.bus.read_long(gdevice + 22));
let color_table = runner.bus.read_long(runner.bus.read_long(pixmap + 42));
assert_eq!(runner.bus.read_word(color_table + 6), (1u16 << depth) - 1);
for index in [0u32, (1u32 << depth) - 1] {
let entry = color_table + 8 + index * 8;
assert_eq!(
[
runner.bus.read_word(entry + 2),
runner.bus.read_word(entry + 4),
runner.bus.read_word(entry + 6),
],
device_clut[index as usize],
"{depth}bpp host GDevice CLUT entry {index}"
);
}
let pixel_at = |x: u32| {
let bit = x * u32::from(depth);
let packed = runner
.bus
.read_byte(base + DESTINATION_Y * host_row_bytes + bit / 8);
let shift = 8 - u32::from(depth) - (bit & 7);
(packed >> shift) & field_mask
};
assert_eq!(pixel_at(DESTINATION_X - 1), field_mask);
for (offset, expected) in source_pixels.into_iter().enumerate() {
assert_eq!(
pixel_at(DESTINATION_X + offset as u32),
expected,
"{depth}bpp source pixel {offset}"
);
}
assert_eq!(pixel_at(DESTINATION_X + WIDTH), field_mask);
}
}
#[test]
fn arrows_as_numpad_remaps_key_and_char_together() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_arrows_as_numpad(true);
assert_eq!(runner.remap_key(0x7B, 28), (0x56, b'4'));
assert_eq!(runner.remap_key(0x7C, 29), (0x58, b'6'));
assert_eq!(runner.remap_key(0x7D, 31), (0x57, b'5'));
assert_eq!(runner.remap_key(0x7E, 30), (0x5B, b'8'));
assert_eq!(runner.remap_key(0x2E, b'm'), (0x2E, b'm'));
}
#[test]
fn arrows_not_remapped_by_default() {
let runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
assert!(!runner.arrows_as_numpad());
assert_eq!(runner.remap_key(0x7B, 28), (0x7B, 28));
assert_eq!(runner.remap_key(0x7C, 29), (0x7C, 29));
assert_eq!(runner.remap_key(0x2E, b'm'), (0x2E, b'm'));
}
#[test]
fn key_events_sync_low_memory_keymap() {
use crate::memory::globals::addr;
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
assert_eq!(runner.bus.read_byte(addr::KEY_MAP_LM), 0);
assert_eq!(runner.bus.read_byte(addr::KEY_MAP_LM + 4), 0);
assert_eq!(runner.bus.read_byte(addr::KEY_MAP_LM + 6), 0);
assert_eq!(runner.bus.read_byte(addr::KEY_MAP_LM + 15), 0);
runner.push_key_down(0x00, b'a');
runner.push_key_down(0x26, b'j');
runner.push_key_down(0x31, b' ');
runner.push_key_down(0x7E, 30);
assert_eq!(runner.bus.read_byte(addr::KEY_MAP_LM), 0x01);
assert_eq!(
runner.bus.read_byte(addr::KEY_MAP_LM + 4),
0x40,
"J key should be visible to byte/bit KeyMap readers"
);
assert_eq!(
runner.bus.read_byte(addr::KEY_MAP_LM + 5),
0,
"J key should not alias M at KeyMapLM byte 5"
);
assert_eq!(runner.bus.read_byte(addr::KEY_MAP_LM + 6), 0x02);
assert_eq!(
runner.bus.read_byte(addr::KEY_MAP_LM + 15),
0x40,
"up arrow should be visible to byte/bit KeyMap readers"
);
assert_eq!(
runner.bus.read_byte(addr::KEY_MAP_LM + 14),
0,
"up arrow should not be mirrored into the unused raw byte"
);
runner.push_key_up(0x26, b'j');
assert_eq!(
runner.bus.read_byte(addr::KEY_MAP_LM + 4),
0,
"J key release should clear the low-memory mirror"
);
assert_eq!(
runner.bus.read_byte(addr::KEY_MAP_LM + 5),
0,
"J key release should leave the M-key byte clear"
);
assert_eq!(
runner.bus.read_byte(addr::KEY_MAP_LM + 15),
0x40,
"unrelated byte/bit down keys should remain mirrored"
);
assert_eq!(
runner.bus.read_byte(addr::KEY_MAP_LM + 14),
0,
"unused raw byte should stay clear"
);
}
#[test]
fn caps_lock_latch_is_preserved_in_low_memory_keymap() {
use crate::memory::globals::addr;
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let caps_lock_byte = addr::KEY_MAP_LM + 7;
runner.push_key_down(0x39, 0);
assert_eq!(runner.bus.read_byte(caps_lock_byte) & 0x02, 0x02);
runner.push_key_up(0x39, 0);
assert_eq!(
runner.bus.read_byte(caps_lock_byte) & 0x02,
0x02,
"physical release must keep the low-memory Caps Lock bit latched"
);
runner.push_key_down(0x39, 0);
assert_eq!(runner.bus.read_byte(caps_lock_byte) & 0x02, 0);
runner.push_key_up(0x39, 0);
assert_eq!(runner.bus.read_byte(caps_lock_byte) & 0x02, 0);
}
#[test]
fn init_app_zeroes_fresh_top_of_stack() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: None,
};
runner.init_app(&app);
let stack_seed_start = app.initial_sp.saturating_sub(0x8000);
assert_eq!(
runner.bus.read_long(stack_seed_start),
0,
"fresh process stack should match a newly initialized application partition"
);
}
#[test]
fn init_app_leaves_application_heap_room_below_appllimit() {
use crate::memory::globals::addr;
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: None,
};
runner.init_app(&app);
let heap_end = runner.bus.read_long(addr::HEAP_END);
let appl_limit = runner.bus.read_long(addr::APPL_LIMIT);
assert_eq!(
heap_end,
0x0020_0000 + APP_ZONE_HEADER_SIZE,
"HeapEnd should expose the initial application-zone extent"
);
assert!(
appl_limit.saturating_sub(heap_end) >= 2300 * 1024,
"direct low-memory startup checks should see growable heap room below ApplLimit"
);
}
#[test]
fn init_app_honors_size_resource_preferred_partition_for_heap_reporting() {
use crate::memory::globals::addr;
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let preferred_partition = 3 * 1024 * 1024;
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: Some(ApplicationSizeResource {
flags: 0x0080,
preferred_size: preferred_partition,
minimum_size: 2 * 1024 * 1024,
}),
};
runner.init_app(&app);
let expected_limit = 0x0020_0000 + preferred_partition - APP_STACK_SAFETY_MARGIN;
let expected_free = expected_limit - (0x0020_0000 + APP_ZONE_HEADER_SIZE);
assert_eq!(runner.bus.read_long(addr::APPL_LIMIT), expected_limit);
assert_eq!(runner.bus.read_long(addr::BUF_PTR), expected_limit);
assert_eq!(runner.bus.read_long(0x0020_0000), expected_limit);
assert_eq!(runner.bus.read_long(0x0020_0000 + 12), expected_free);
assert_eq!(
crate::memory::app_heap_free_bytes(runner.bus()),
expected_free
);
assert!(
expected_free < crate::memory::APP_HEAP_COMPAT_FREE_FLOOR,
"explicit SIZE partitions must bypass the compatibility floor"
);
}
#[test]
fn init_app_application_partition_override_takes_precedence_over_size_resource() {
use crate::memory::globals::addr;
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let size_partition = 3 * 1024 * 1024;
let override_partition = 4 * 1024 * 1024;
runner.set_application_partition_size(Some(override_partition));
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: Some(ApplicationSizeResource {
flags: 0x0080,
preferred_size: size_partition,
minimum_size: 2 * 1024 * 1024,
}),
};
runner.init_app(&app);
let expected_limit = 0x0020_0000 + override_partition - APP_STACK_SAFETY_MARGIN;
assert_eq!(runner.bus.read_long(addr::APPL_LIMIT), expected_limit);
assert_eq!(
crate::memory::app_heap_free_bytes(runner.bus()),
expected_limit - (0x0020_0000 + APP_ZONE_HEADER_SIZE)
);
}
#[test]
fn init_app_ignores_too_small_application_partition_override() {
use crate::memory::globals::addr;
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_application_partition_size(Some(64 * 1024));
assert_eq!(runner.application_partition_size(), None);
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: None,
};
runner.init_app(&app);
assert_eq!(
runner.bus.read_long(addr::APPL_LIMIT),
app.initial_sp - APP_STACK_SAFETY_MARGIN,
"invalid tiny overrides must fall back to the default launch limit"
);
}
#[test]
fn init_app_seeds_appparmhandle_with_empty_finder_information() {
use crate::memory::globals::addr;
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner
.dispatcher_mut()
.set_launched_app_path("Games/Armor Alley");
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: None,
};
runner.init_app(&app);
let handle = runner.bus.read_long(addr::APP_PARM_HANDLE);
assert_ne!(
handle, 0,
"AppParmHandle should point at Finder launch information"
);
let data_ptr = runner.bus.read_long(handle);
assert_ne!(
data_ptr, 0,
"Finder launch information handle should be loaded"
);
assert_eq!(
runner.bus.get_alloc_size(data_ptr),
Some(4),
"empty Finder launch information is message/count only"
);
assert_eq!(
runner.bus.read_word(data_ptr),
0,
"message should be appOpen for a normal application launch"
);
assert_eq!(
runner.bus.read_word(data_ptr + 2),
0,
"normal application launch has no selected documents"
);
}
#[test]
fn init_app_seeds_current_application_fcb_low_memory_state() {
use crate::memory::globals::addr;
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner
.dispatcher_mut()
.vfs_rsrc
.insert("Games/Armor Alley".to_string(), vec![0xA5; 1234]);
runner
.dispatcher_mut()
.set_vfs_entry_metadata("Games/Armor Alley", *b"APPL", *b"TEST", 0);
runner
.dispatcher_mut()
.set_launched_app_path("Games/Armor Alley");
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: None,
};
runner.init_app(&app);
assert_eq!(
runner.bus.read_word(addr::CUR_APREF_NUM),
APPLICATION_RESOURCE_REFNUM,
"CurApRefNum should be the app resource fork access path"
);
assert_eq!(
runner.bus.read_word(addr::FS_FCB_LEN),
HFS_FCB_SIZE,
"System 7 FCB size should be exposed for direct low-memory readers"
);
let fcb_buffer = runner.bus.read_long(addr::FCB_S_PTR);
assert_ne!(fcb_buffer, 0, "FCBSPtr should point to an FCB buffer");
assert_eq!(
runner.bus.read_word(fcb_buffer),
HFS_FCB_BUFFER_SIZE,
"FCB buffer length should include the leading length word"
);
let fcb = fcb_buffer + APPLICATION_RESOURCE_REFNUM as u32;
assert_eq!(
runner.bus.read_word(fcb + 4),
0x0200,
"the application access path should describe a resource fork"
);
assert_eq!(runner.bus.read_long(fcb + 8), 1234);
assert_eq!(runner.bus.read_long(fcb + 12), 1234);
assert_eq!(runner.bus.read_long(fcb + 50), u32::from_be_bytes(*b"APPL"));
assert_eq!(
runner.bus.read_long(fcb + 58),
runner.dispatcher.default_dir_id
);
let vcb = runner.bus.read_long(fcb + 20);
assert_ne!(vcb, 0, "fcbVPtr should point to the boot volume VCB");
assert_eq!(runner.bus.read_long(addr::DEF_VCB_PTR), vcb);
assert_eq!(runner.bus.read_long(addr::VCB_Q_HDR + 2), vcb);
assert_eq!(runner.bus.read_long(addr::VCB_Q_HDR + 6), vcb);
assert_eq!(runner.bus.read_word(vcb + 8), 0x4244);
assert_eq!(
runner.bus.read_word(vcb + 78) as i16,
crate::trap::dispatch::BOOT_VOLUME_REF_NUM
);
assert_eq!(
runner
.dispatcher
.open_files
.get(&APPLICATION_RESOURCE_REFNUM),
Some(&"__rsrc__Games/Armor Alley".to_string())
);
}
#[test]
fn init_app_sets_legacy_sound_driver_low_memory_defaults() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: None,
};
runner.init_app(&app);
assert_eq!(
runner
.bus
.read_byte(crate::memory::globals::addr::SD_VOLUME),
1,
"SdVolume ($0260) should boot to the nonzero legacy compatibility value"
);
assert_eq!(
runner
.bus
.read_byte(crate::memory::globals::addr::SOUND_LEVEL),
0,
"SoundLevel ($027F) is a distinct Sound Driver amplitude byte"
);
let sound_base = runner
.bus
.read_long(crate::memory::globals::addr::SOUND_BASE);
assert_eq!(
sound_base, 0x007F_7880,
"SoundBase ($0266) should point at the 370-word legacy sound buffer in reserved display memory"
);
assert_eq!(
runner.bus.read_byte(sound_base),
0x80,
"legacy SoundBase buffer starts at neutral amplitude"
);
}
#[test]
fn init_app_seeds_mmu32bit_low_memory_flag() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: None,
};
runner.init_app(&app);
assert_eq!(
runner
.bus
.read_byte(crate::memory::globals::addr::MMU32_BIT),
1,
"MMU32Bit ($0CB2) should mirror Systemless's default 32-bit addressing mode"
);
}
#[test]
fn init_app_can_start_in_twenty_four_bit_addressing_mode() {
let mut runner = FixtureRunner::new(
8 * 1024 * 1024,
FixtureRunnerConfig {
addressing_32_bit: false,
..FixtureRunnerConfig::default()
},
);
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: None,
};
runner.init_app(&app);
assert!(!runner.bus.addressing_32_bit());
assert_eq!(runner.bus.ram_size(), 8 * 1024 * 1024);
assert_eq!(runner.dispatcher.mmu_mode, 0);
assert_eq!(
runner
.bus
.read_byte(crate::memory::globals::addr::MMU32_BIT),
0
);
}
#[test]
fn twenty_four_bit_runner_caps_guest_ram_at_sixteen_megabytes() {
let runner = FixtureRunner::new(
64 * 1024 * 1024,
FixtureRunnerConfig {
addressing_32_bit: false,
..FixtureRunnerConfig::default()
},
);
assert_eq!(runner.bus.ram_size(), 0x0100_0000);
}
#[test]
fn init_app_seeds_callable_swap_mmu_mode_trap_table_entry() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: None,
};
runner.init_app(&app);
let entry = runner
.bus
.read_long(crate::memory::globals::addr::SWAP_MMU_MODE_TRAP);
assert_ne!(entry, 0);
assert_eq!(
[runner.bus.read_word(entry), runner.bus.read_word(entry + 2),],
[0xA05D, 0x4E75],
"the $0574 OS trap-table entry should target SwapMMUMode followed by RTS"
);
let call_site = 0x0002_0000u32;
let initial_sp = 0x007F_FE00u32;
runner.bus.write_word(call_site, 0x2078); runner.bus.write_word(call_site + 2, 0x0574);
runner.bus.write_word(call_site + 4, 0x4E90); runner.bus.write_word(call_site + 6, 0x4E71); runner.cpu.write_reg(Register::PC, call_site);
runner.cpu.write_reg(Register::A7, initial_sp);
runner.cpu.write_reg(Register::D0, 0);
let (steps, running) = runner.run_steps(4, None);
assert!(running);
assert_eq!(steps, 4);
assert_eq!(runner.cpu.read_reg(Register::PC), call_site + 6);
assert_eq!(runner.cpu.read_reg(Register::A7), initial_sp);
assert_eq!(
runner.cpu.read_reg(Register::D0),
1,
"SwapMMUMode should return the previous 32-bit mode in D0"
);
assert_eq!(
runner
.bus
.read_byte(crate::memory::globals::addr::MMU32_BIT),
0,
"the indirect call should update the requested addressing mode"
);
runner.bus.write_long(0x0002_1000, 0x1234_5678);
assert_eq!(
runner.bus.read_long(0xAB02_1000),
0x1234_5678,
"SwapMMUMode must change actual guest address translation"
);
}
#[test]
fn init_app_seeds_cursor_task_low_memory_vector() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: None,
};
runner.init_app(&app);
assert_eq!(
runner.bus.read_word(CURSOR_TASK_NOOP_ADDR),
0x4E75,
"default cursor task target should be a callable RTS stub"
);
assert_eq!(
runner
.bus
.read_long(crate::memory::globals::addr::J_CRSR_TASK),
CURSOR_TASK_NOOP_ADDR,
"JCrsrTask ($08EE) should boot to a callable no-op vector"
);
}
#[test]
fn init_app_seeds_callable_show_cursor_low_memory_vector() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: None,
};
runner.init_app(&app);
let entry = runner
.bus
.read_long(crate::memory::globals::addr::J_SHOW_CURSOR);
assert_ne!(entry, 0);
assert_eq!(
[runner.bus.read_word(entry), runner.bus.read_word(entry + 2),],
[0xA853, 0x4E75],
"JShowCursor should target ShowCursor followed by RTS"
);
let call_site = 0x0002_0000u32;
let initial_sp = 0x007F_FE00u32;
runner.bus.write_word(call_site, 0x2078); runner.bus.write_word(call_site + 2, 0x0804);
runner.bus.write_word(call_site + 4, 0x4E90); runner.bus.write_word(call_site + 6, 0x4E71); runner.cpu.write_reg(Register::PC, call_site);
runner.cpu.write_reg(Register::A7, initial_sp);
runner.dispatcher.cursor_level = -1;
runner.dispatcher.cursor_visible = false;
let (steps, running) = runner.run_steps(4, None);
assert!(running);
assert_eq!(steps, 4);
assert_eq!(runner.cpu.read_reg(Register::PC), call_site + 6);
assert_eq!(runner.cpu.read_reg(Register::A7), initial_sp);
assert_eq!(runner.dispatcher.cursor_level(), 0);
assert!(runner.dispatcher.cursor_visible());
}
#[test]
fn init_app_seeds_callable_init_cursor_low_memory_vector() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: None,
};
runner.init_app(&app);
let entry = runner
.bus
.read_long(crate::memory::globals::addr::J_INIT_CRSR);
assert_ne!(entry, 0);
assert_eq!(
[runner.bus.read_word(entry), runner.bus.read_word(entry + 2)],
[0xA850, 0x4E75],
"JInitCrsr should target InitCursor followed by RTS"
);
let call_site = 0x0002_0000u32;
let initial_sp = 0x007F_FE00u32;
runner.bus.write_word(call_site, 0x2078); runner.bus.write_word(call_site + 2, 0x0814);
runner.bus.write_word(call_site + 4, 0x4E90); runner.bus.write_word(call_site + 6, 0x4E71); runner.cpu.write_reg(Register::PC, call_site);
runner.cpu.write_reg(Register::A7, initial_sp);
runner.dispatcher.cursor_level = -1;
runner.dispatcher.cursor_visible = false;
let (steps, running) = runner.run_steps(4, None);
assert!(running);
assert_eq!(steps, 4);
assert_eq!(runner.cpu.read_reg(Register::PC), call_site + 6);
assert_eq!(runner.cpu.read_reg(Register::A7), initial_sp);
assert_eq!(runner.dispatcher.cursor_level(), 0);
assert!(runner.dispatcher.cursor_visible());
}
#[test]
fn init_app_seeds_callable_swap_font_low_memory_vector() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: None,
};
runner.init_app(&app);
let swap_font_trampoline = runner
.bus
.read_long(crate::memory::globals::addr::J_SWAP_FONT);
assert_ne!(swap_font_trampoline, 0);
assert_eq!(
[
runner.bus.read_word(swap_font_trampoline),
runner.bus.read_word(swap_font_trampoline + 2),
runner.bus.read_word(swap_font_trampoline + 4),
],
[0x205F, 0xA901, 0x4ED0]
);
let fm_input_sp = 0x007F_FE00u32;
let fm_input = 0x0002_1000u32;
let return_pc = 0x0002_0000u32;
runner.bus.write_word(fm_input, 3); runner.bus.write_word(fm_input + 2, 12); runner.bus.write_byte(fm_input + 4, 0); runner.bus.write_byte(fm_input + 5, 1); runner.bus.write_word(fm_input + 6, 0); runner.bus.write_word(fm_input + 8, 1); runner.bus.write_word(fm_input + 10, 1); runner.bus.write_word(fm_input + 12, 1); runner.bus.write_word(fm_input + 14, 1); runner.bus.write_long(fm_input_sp, fm_input); runner.bus.write_long(fm_input_sp + 4, 0); runner.bus.write_long(fm_input_sp - 4, return_pc);
runner.bus.write_word(return_pc, 0x4E71); runner.cpu.write_reg(Register::PC, swap_font_trampoline);
runner.cpu.write_reg(Register::A7, fm_input_sp - 4);
let (steps, running) = runner.run_steps(3, None);
assert!(running);
assert_eq!(steps, 3);
assert_eq!(runner.cpu.read_reg(Register::PC), return_pc);
assert_eq!(runner.cpu.read_reg(Register::A7), fm_input_sp + 4);
assert_ne!(
runner.bus.read_long(fm_input_sp + 4),
0,
"JSwapFont should return a non-NIL FMOutPtr through the Pascal result slot"
);
}
#[test]
fn init_app_seeds_callable_shield_cursor_low_memory_vector() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: None,
};
runner.init_app(&app);
let shield_cursor_trampoline = runner
.bus
.read_long(crate::memory::globals::addr::J_SHIELD_CURSOR);
assert_ne!(shield_cursor_trampoline, 0);
assert_eq!(
[
runner.bus.read_word(shield_cursor_trampoline),
runner.bus.read_word(shield_cursor_trampoline + 2),
runner.bus.read_word(shield_cursor_trampoline + 4),
],
[0x205F, 0xA855, 0x4ED0]
);
let args_sp = 0x007F_FE00u32;
let return_pc = 0x0002_0000u32;
runner.bus.write_word(args_sp, 100); runner.bus.write_word(args_sp + 2, 120); runner.bus.write_word(args_sp + 4, 500); runner.bus.write_word(args_sp + 6, 420); runner.bus.write_long(args_sp - 4, return_pc);
runner.bus.write_word(return_pc, 0x4E71); runner.cpu.write_reg(Register::PC, shield_cursor_trampoline);
runner.cpu.write_reg(Register::A7, args_sp - 4);
let (steps, running) = runner.run_steps(3, None);
assert!(running);
assert_eq!(steps, 3);
assert_eq!(runner.cpu.read_reg(Register::PC), return_pc);
assert_eq!(
runner.cpu.read_reg(Register::A7),
args_sp + 8,
"JShieldCursor should consume its four Pascal INTEGER arguments"
);
}
#[test]
fn init_app_seeds_callable_hide_cursor_low_memory_vector() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let app = LoadedApp {
ppc: None,
code0_header: Code0Header {
above_a5: 0,
below_a5: 0x2000,
jump_table_size: 0,
jump_table_offset: 0,
},
a5_base: 0x0040_0000,
jump_table: Vec::new(),
segment_bases: HashMap::new(),
loaded_image_end: 0,
initial_sp: 0x007F_FFC0,
size_resource: None,
};
runner.init_app(&app);
let hide_cursor_trampoline = runner
.bus
.read_long(crate::memory::globals::addr::J_HIDE_CURSOR);
assert_ne!(hide_cursor_trampoline, 0);
assert_eq!(
[
runner.bus.read_word(hide_cursor_trampoline),
runner.bus.read_word(hide_cursor_trampoline + 2),
runner.bus.read_word(hide_cursor_trampoline + 4),
],
[0x205F, 0xA852, 0x4ED0],
"JHideCursor should pop the JSR return address, trap, and jump back"
);
let call_sp = 0x007F_FE00u32;
let return_pc = 0x0002_0000u32;
runner.bus.write_long(call_sp - 4, return_pc);
runner.bus.write_word(return_pc, 0x4E71);
runner.cpu.write_reg(Register::PC, hide_cursor_trampoline);
runner.cpu.write_reg(Register::A7, call_sp - 4);
let (steps, running) = runner.run_steps(3, None);
assert!(running);
assert_eq!(steps, 3);
assert_eq!(runner.cpu.read_reg(Register::PC), return_pc);
assert_eq!(runner.cpu.read_reg(Register::A7), call_sp);
assert_eq!(runner.dispatcher().cursor_level(), -1);
}
#[test]
fn cursor_task_noop_vector_does_not_fire_on_guest_tick() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0002_0000;
let interrupted_sp = 0x007F_FFC0;
runner.bus.write_long(
crate::memory::globals::addr::J_CRSR_TASK,
CURSOR_TASK_NOOP_ADDR,
);
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.advance_guest_tick();
assert!(runner.active_interrupt_callback.is_none());
assert_eq!(runner.cursor_task_trampoline, 0);
assert_eq!(runner.cpu.read_reg(Register::PC), interrupted_pc);
assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
assert_eq!(runner.bus.read_long(crate::memory::globals::addr::TICKS), 1);
}
#[test]
fn cursor_task_callback_arms_interrupt_from_low_memory_vector() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0002_0000;
let interrupted_sp = 0x007F_FFC0;
let callback_addr = 0x0004_1234;
runner
.bus
.write_long(crate::memory::globals::addr::J_CRSR_TASK, callback_addr);
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.cpu.write_reg(Register::D0, 0x1111_1111);
runner.cpu.write_reg(Register::D7, 0x7777_7777);
runner.cpu.write_reg(Register::A0, 0xAAAA_0000);
runner.cpu.write_reg(Register::A6, 0xCCCC_0000);
runner.cpu.core.set_ccr(0x04);
runner.cpu.core.set_sr_noint_nosp(0x2004);
runner.advance_guest_tick();
let active = runner
.active_interrupt_callback
.expect("cursor task callback should have been armed");
assert!(matches!(
active.source,
ActiveInterruptCallbackSource::CursorTask
));
assert_eq!(active.resume_pc, interrupted_pc);
assert_eq!(active.resume_sp, interrupted_sp);
assert_eq!(active.a_regs[7], interrupted_sp);
assert_eq!(active.a_regs[6], 0xCCCC_0000);
assert_eq!(active.d_regs[0], 0x1111_1111);
assert_eq!(active.d_regs[7], 0x7777_7777);
assert_eq!(active.sr, 0x2004);
assert_eq!(active.ccr, 0x04);
assert_eq!(runner.cpu.core.get_sr(), 0x2104);
assert_ne!(runner.cursor_task_trampoline, 0);
assert_eq!(
runner.cpu.read_reg(Register::PC),
runner.cursor_task_trampoline
);
assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp - 4);
assert_eq!(runner.bus.read_long(interrupted_sp - 4), interrupted_pc);
assert_eq!(runner.bus.read_word(runner.cursor_task_trampoline), 0x48E7);
assert_eq!(
runner.bus.read_word(runner.cursor_task_trampoline + 4),
0x4EB9
);
assert_eq!(
runner.bus.read_long(runner.cursor_task_trampoline + 6),
callback_addr
);
assert_eq!(
runner.bus.read_word(runner.cursor_task_trampoline + 10),
0x4CDF
);
assert_eq!(
runner.bus.read_word(runner.cursor_task_trampoline + 14),
0x4E75
);
}
#[test]
fn cursor_task_defers_while_processor_priority_masks_level_one() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0002_0000;
let interrupted_sp = 0x007F_FFC0;
runner
.bus
.write_long(crate::memory::globals::addr::J_CRSR_TASK, 0x0004_1234);
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.cpu.core.set_sr_noint_nosp(0x2100);
runner.advance_guest_tick();
assert!(runner.active_interrupt_callback.is_none());
assert_eq!(runner.cursor_task_trampoline, 0);
assert_eq!(runner.cpu.read_reg(Register::PC), interrupted_pc);
assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
}
#[test]
fn timer_callback_snapshot_preserves_interrupted_sp() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0002_8BAC;
let interrupted_sp = 0x007F_FFC0;
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::D0, 0x1111_1111);
runner.cpu.write_reg(Register::D7, 0x7777_7777);
runner.cpu.write_reg(Register::A0, 0xAAAA_0000);
runner.cpu.write_reg(Register::A6, 0xCCCC_0000);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.cpu.core.set_ccr(0x1F);
runner.bus.write_word(0x0039_38C8 + 4, 0x8001);
runner.dispatcher.timer_tasks.push(TimerTask {
task_ptr: 0x0039_38C8,
tm_addr: 0x0004_1234,
active: true,
fire_at_tick: 10,
fire_at_subtick: 10_000_000,
last_fired_tick: None,
});
runner.fire_timer_tasks(10);
let active = runner
.active_interrupt_callback
.expect("timer callback should have been armed");
assert!(matches!(
active.source,
ActiveInterruptCallbackSource::Timer
));
assert_eq!(active.resume_pc, interrupted_pc);
assert_eq!(active.resume_sp, interrupted_sp);
assert_eq!(active.a_regs[7], interrupted_sp);
assert_eq!(active.a_regs[6], 0xCCCC_0000);
assert_eq!(active.d_regs[0], 0x1111_1111);
assert_eq!(active.d_regs[7], 0x7777_7777);
assert_eq!(active.sr & 0x001F, 0x001F);
assert_eq!(active.ccr, 0x1F);
assert_eq!(
runner.bus.read_word(0x0039_38C8 + 4),
1,
"an expired Time Manager task must be inactive before tmAddr runs"
);
assert_ne!(runner.timer_trampoline, 0);
assert_eq!(runner.cpu.read_reg(Register::PC), runner.timer_trampoline);
assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp - 4);
assert_eq!(runner.bus.read_long(interrupted_sp - 4), interrupted_pc);
}
#[test]
fn timer_callback_fired_at_tick_cap_runs_before_yielding() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
let callback_addr = 0x0002_0000;
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.tick_budget = 0;
runner.bus.write_word(callback_addr, 0x4E75); runner.dispatcher.timer_tasks.push(TimerTask {
task_ptr: 0x0039_38C8,
tm_addr: callback_addr,
active: true,
fire_at_tick: 101,
fire_at_subtick: 101_000_000,
last_fired_tick: None,
});
let (steps, running) = runner.run_steps(1, Some(101));
assert!(running);
assert_eq!(
steps, 1,
"a timer fired while reaching the tick cap must get a CPU slice"
);
assert_eq!(runner.bus.read_long(0x016A), 101);
assert!(runner.active_interrupt_callback.is_some());
assert_ne!(runner.timer_trampoline, 0);
assert_eq!(
runner.cpu.read_reg(Register::PC),
runner.timer_trampoline + 4
);
}
#[test]
fn sub_vbl_timer_callback_fires_before_next_guest_tick() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
let callback_addr = 0x0002_0000;
runner.bus.write_word(interrupted_pc, 0x60FE); runner.bus.write_word(callback_addr, 0x4E75); runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.tick_budget = runner.instructions_per_tick as i32;
runner.dispatcher.timer_tasks.push(TimerTask {
task_ptr: 0x0039_38C8,
tm_addr: callback_addr,
active: true,
fire_at_tick: 101,
fire_at_subtick: 100_200_000,
last_fired_tick: None,
});
let steps = runner.instructions_per_tick as usize / 4;
let (executed, running) = runner.run_steps(steps, None);
let (_, still_running) = runner.run_steps(1, None);
assert!(running);
assert!(still_running);
assert_eq!(executed, steps);
assert_eq!(runner.guest_tick(), 100);
assert!(!runner.dispatcher.timer_tasks[0].active);
assert_ne!(runner.timer_trampoline, 0);
}
#[test]
fn timer_callback_return_runs_foreground_before_next_due_timer() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
let callback_addr = 0x0002_0000;
runner.bus.write_word(interrupted_pc, 0x4E71); runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.bus.write_long(0x016A, 101);
runner.dispatcher.tick_count = 101;
runner.set_instructions_per_tick(1);
runner.tick_budget = 0;
runner.active_interrupt_callback = Some(ActiveInterruptCallback {
source: ActiveInterruptCallbackSource::Timer,
resume_pc: interrupted_pc,
resume_sp: interrupted_sp,
d_regs: [0; 8],
a_regs: [0, 0, 0, 0, 0, 0, 0, interrupted_sp],
sr: 0x2000,
ccr: 0,
restore_port: None,
});
runner.dispatcher.timer_tasks.push(TimerTask {
task_ptr: 0x0039_38C8,
tm_addr: callback_addr,
active: true,
fire_at_tick: 102,
fire_at_subtick: 102_000_000,
last_fired_tick: None,
});
let (steps, running) = runner.run_steps(1, None);
assert!(running);
assert_eq!(steps, 1);
assert_eq!(
runner.cpu.read_reg(Register::PC),
interrupted_pc + 2,
"resumed foreground instruction should run before the next timer interrupt"
);
assert_eq!(
runner.guest_tick(),
101,
"returning from an interrupt must not immediately spend an exhausted budget on another tick"
);
assert!(runner.active_interrupt_callback.is_none());
assert!(
runner.dispatcher.timer_tasks[0].active,
"the next due timer should remain queued until foreground code gets a slice"
);
}
#[test]
fn simultaneous_timer_callbacks_keep_undelivered_tasks_active() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.dispatcher.timer_tasks.extend([
TimerTask {
task_ptr: 0x0039_38C8,
tm_addr: 0x0002_0000,
active: true,
fire_at_tick: 10,
fire_at_subtick: 10_000_000,
last_fired_tick: None,
},
TimerTask {
task_ptr: 0x0039_3900,
tm_addr: 0x0002_1000,
active: true,
fire_at_tick: 10,
fire_at_subtick: 10_000_000,
last_fired_tick: None,
},
]);
runner.fire_timer_tasks(10);
assert!(!runner.dispatcher.timer_tasks[0].active);
assert!(
runner.dispatcher.timer_tasks[1].active,
"a second task due on the same tick must remain queued"
);
runner.dispatcher.timer_tasks[0].active = true;
runner.dispatcher.timer_tasks[0].fire_at_tick = 11;
runner.dispatcher.timer_tasks[0].fire_at_subtick = 11_000_000;
runner.active_interrupt_callback = None;
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.fire_timer_tasks(11);
assert!(
runner.dispatcher.timer_tasks[0].active,
"the newly re-primed task must wait behind the older due task"
);
assert!(!runner.dispatcher.timer_tasks[1].active);
assert_eq!(
runner.bus.read_long(runner.timer_trampoline + 6),
0x0039_3900
);
}
#[test]
fn self_reprimed_timer_can_fire_again_within_the_same_vbl() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
let task_ptr = 0x0039_38C8;
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.dispatcher.timer_tasks.push(TimerTask {
task_ptr,
tm_addr: 0x0002_0000,
active: true,
fire_at_tick: 10,
fire_at_subtick: 10_100_000,
last_fired_tick: None,
});
runner.fire_timer_tasks_at(10_100_000);
assert_eq!(runner.dispatcher.timer_tasks[0].last_fired_tick, Some(10));
runner.active_interrupt_callback = None;
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.dispatcher.timer_tasks[0].active = true;
runner.dispatcher.timer_tasks[0].fire_at_tick = 11;
runner.dispatcher.timer_tasks[0].fire_at_subtick = 10_300_000;
runner.fire_timer_tasks_at(10_300_000);
assert!(
runner.active_interrupt_callback.is_some(),
"a revised Time Manager task must honor a new sub-VBL deadline"
);
assert!(!runner.dispatcher.timer_tasks[0].active);
assert_eq!(runner.dispatcher.timer_tasks[0].last_fired_tick, Some(10));
}
#[test]
fn sound_doubleback_callback_resume_restores_ccr_before_branch() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
let header_ptr = 0x0020_0000;
let exhausted_buf_ptr = 0x0020_1000;
runner.bus.write_word(interrupted_pc, 0x6704);
runner.bus.write_word(interrupted_pc + 2, 0x7001);
runner.bus.write_word(interrupted_pc + 4, 0x6002);
runner.bus.write_word(interrupted_pc + 6, 0x7002);
runner.bus.write_word(interrupted_pc + 8, 0x4E71);
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.cpu.write_reg(Register::D0, 0);
runner.cpu.core.set_ccr(0x04);
runner.bus.write_long(header_ptr + 12, exhausted_buf_ptr);
runner.bus.write_long(exhausted_buf_ptr + 4, 0x0000_0001);
runner
.dispatcher
.sound_manager
.pending_callbacks
.push(PendingDoubleBackCallback {
callback_addr: 0x0004_1234,
chan_ptr: 0x0039_38C8,
header_ptr,
exhausted_buffer_index: 0,
});
runner.fire_sound_doubleback_callbacks();
let active = runner
.active_interrupt_callback
.expect("sound callback should have been armed");
assert!(matches!(
active.source,
ActiveInterruptCallbackSource::SoundDoubleBack
));
assert_eq!(active.resume_pc, interrupted_pc);
assert_eq!(active.resume_sp, interrupted_sp);
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.cpu.core.set_ccr(0);
let (steps, running) = runner.run_steps(3, None);
assert!(running);
assert_eq!(steps, 3);
assert_eq!(runner.cpu.read_reg(Register::D0), 2);
assert!(runner.active_interrupt_callback.is_none());
}
#[test]
fn sound_doubleback_callback_trampoline_stacks_classic_pascal_order() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
let callback_addr = runner.bus.alloc(2);
let header_ptr = 0x0020_0000;
let chan_ptr = 0x0039_38C8;
let exhausted_buf_ptr = 0x0020_1000;
runner.bus.write_word(callback_addr, 0x4E75); runner.bus.write_word(interrupted_pc, 0x60FE); runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.bus.write_long(header_ptr + 12, exhausted_buf_ptr);
runner.bus.write_long(exhausted_buf_ptr + 4, 0x0000_0001);
runner
.dispatcher
.sound_manager
.pending_callbacks
.push(PendingDoubleBackCallback {
callback_addr,
chan_ptr,
header_ptr,
exhausted_buffer_index: 0,
});
runner.fire_sound_doubleback_callbacks();
let (_steps, running) = runner.run_steps(24, None);
assert!(running);
assert!(runner.active_interrupt_callback.is_none());
let saved_regs_sp = interrupted_sp - 4 - 32;
assert_eq!(
runner.bus.read_long(saved_regs_sp - 4),
chan_ptr,
"the first declared Pascal argument is pushed first"
);
assert_eq!(
runner.bus.read_long(saved_regs_sp - 8),
exhausted_buf_ptr,
"the last declared Pascal argument is nearest the return address"
);
}
#[test]
fn mix_audio_loads_ready_double_buffer_without_boundary_silence() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let chan_ptr = 0x0039_38C8;
let callback_addr = 0x0004_1234;
let header_ptr = runner.bus.alloc(24);
let buf0_ptr = runner.bus.alloc(18);
let buf1_ptr = runner.bus.alloc(18);
runner.bus.write_word(header_ptr, 1);
runner.bus.write_word(header_ptr + 2, 8);
runner.bus.write_long(header_ptr + 8, OUTPUT_RATE << 16);
runner.bus.write_long(header_ptr + 12, buf0_ptr);
runner.bus.write_long(header_ptr + 16, buf1_ptr);
runner.bus.write_long(header_ptr + 20, callback_addr);
write_double_buffer(&mut runner.bus, buf0_ptr, &[0x90, 0x91]);
write_double_buffer(&mut runner.bus, buf1_ptr, &[0xA0, 0xA1]);
let mut chan = SndChannel::new(chan_ptr, false);
chan.double_buffer = Some(DoubleBufferState {
header_ptr,
current_buffer: 0,
callback_addr,
chan_ptr,
sample_rate: OUTPUT_RATE << 16,
num_channels: 1,
sample_size: 8,
last_buffer_seen: false,
waiting_for_callback: false,
pending_callback_buffers: [false; 2],
});
crate::trap::TrapDispatcher::load_double_buffer_samples(
&mut runner.bus,
&mut chan,
buf0_ptr,
OUTPUT_RATE << 16,
1,
8,
);
runner.dispatcher.sound_manager.channels.push(chan);
runner.mix_audio(3);
assert_eq!(
runner.audio_buffer,
vec![0x90, 0x91, 0xA0],
"host mixing must continue into the ready paired buffer, not emit boundary silence"
);
assert_eq!(
runner.bus.read_long(buf0_ptr + 4) & 0x01,
0x01,
"dbBufferReady stays set until the doubleback callback starts"
);
assert_eq!(
runner.bus.read_long(buf1_ptr + 4) & 0x01,
0x01,
"the paired buffer is still marked ready while it is playing"
);
assert_eq!(
runner.dispatcher.sound_manager.pending_callbacks.len(),
1,
"exhausting buffer 0 still queues its doubleback refill"
);
assert_eq!(
runner.dispatcher.sound_manager.pending_callbacks[0].exhausted_buffer_index,
0
);
let chan = &runner.dispatcher.sound_manager.channels[0];
assert!(chan.is_playing(), "buffer 1 should still be playing");
let db = chan.double_buffer.as_ref().expect("double-buffer active");
assert_eq!(db.current_buffer, 1);
assert!(db.waiting_for_callback);
}
#[test]
fn mix_audio_can_queue_other_doubleback_while_callback_is_active() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let chan_ptr = 0x0039_38C8;
let callback_addr = 0x0004_1234;
let header_ptr = runner.bus.alloc(24);
let buf0_ptr = runner.bus.alloc(17);
let buf1_ptr = runner.bus.alloc(17);
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
runner.bus.write_word(interrupted_pc, 0x4E71);
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.bus.write_word(header_ptr, 1);
runner.bus.write_word(header_ptr + 2, 8);
runner.bus.write_long(header_ptr + 8, OUTPUT_RATE << 16);
runner.bus.write_long(header_ptr + 12, buf0_ptr);
runner.bus.write_long(header_ptr + 16, buf1_ptr);
runner.bus.write_long(header_ptr + 20, callback_addr);
write_double_buffer(&mut runner.bus, buf0_ptr, &[0xA0]);
runner.bus.write_long(buf1_ptr, 1);
runner.bus.write_long(buf1_ptr + 4, 0);
let mut chan = SndChannel::new(chan_ptr, false);
chan.double_buffer = Some(DoubleBufferState {
header_ptr,
current_buffer: 0,
callback_addr,
chan_ptr,
sample_rate: OUTPUT_RATE << 16,
num_channels: 1,
sample_size: 8,
last_buffer_seen: false,
waiting_for_callback: false,
pending_callback_buffers: [false; 2],
});
crate::trap::TrapDispatcher::load_double_buffer_samples(
&mut runner.bus,
&mut chan,
buf0_ptr,
OUTPUT_RATE << 16,
1,
8,
);
runner.dispatcher.sound_manager.channels.push(chan);
runner.mix_audio(1);
assert_eq!(runner.dispatcher.sound_manager.pending_callbacks.len(), 1);
assert!(
runner.dispatcher.sound_manager.channels[0]
.double_buffer
.as_ref()
.expect("double-buffer active")
.waiting_for_callback
);
runner.fire_sound_doubleback_callbacks();
assert!(matches!(
runner
.active_interrupt_callback
.expect("doubleback callback should be active")
.source,
ActiveInterruptCallbackSource::SoundDoubleBack
));
assert!(
runner.dispatcher.sound_manager.channels[0]
.double_buffer
.as_ref()
.expect("double-buffer active")
.waiting_for_callback,
"callback remains outstanding until guest refills a buffer"
);
runner.mix_audio(16);
assert_eq!(
runner.dispatcher.sound_manager.pending_callbacks.len(),
1,
"the paired unready buffer may queue its own callback while buffer 0 is active"
);
assert_eq!(
runner.dispatcher.sound_manager.pending_callbacks[0].exhausted_buffer_index, 1,
"buffer 0 must not be duplicated; buffer 1 gets the new callback"
);
let db = runner.dispatcher.sound_manager.channels[0]
.double_buffer
.as_ref()
.expect("double-buffer active");
assert!(db.waiting_for_callback);
assert_eq!(db.pending_callback_buffers, [true, true]);
}
#[test]
fn mix_audio_does_not_load_ready_double_buffer_while_callback_is_active() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let chan_ptr = 0x0039_38C8;
let callback_addr = 0x0004_1234;
let header_ptr = runner.bus.alloc(24);
let buf0_ptr = runner.bus.alloc(17);
runner.bus.write_word(header_ptr, 1);
runner.bus.write_word(header_ptr + 2, 8);
runner.bus.write_long(header_ptr + 8, OUTPUT_RATE << 16);
runner.bus.write_long(header_ptr + 12, buf0_ptr);
runner.bus.write_long(header_ptr + 20, callback_addr);
write_double_buffer(&mut runner.bus, buf0_ptr, &[0xA0]);
let mut chan = SndChannel::new(chan_ptr, false);
chan.double_buffer = Some(DoubleBufferState {
header_ptr,
current_buffer: 0,
callback_addr,
chan_ptr,
sample_rate: OUTPUT_RATE << 16,
num_channels: 1,
sample_size: 8,
last_buffer_seen: false,
waiting_for_callback: true,
pending_callback_buffers: [true, false],
});
runner.dispatcher.sound_manager.channels.push(chan);
runner.active_interrupt_callback = Some(ActiveInterruptCallback {
source: ActiveInterruptCallbackSource::SoundDoubleBack,
resume_pc: 0x0001_0000,
resume_sp: 0x007F_FFC0,
d_regs: [0; 8],
a_regs: [0; 8],
sr: 0x2000,
ccr: 0,
restore_port: None,
});
runner.mix_audio(1);
assert_eq!(
runner.bus.read_long(buf0_ptr + 4) & 0x01,
0x01,
"ready buffer must not be consumed before the callback returns"
);
assert!(
!runner.dispatcher.sound_manager.channels[0].is_playing(),
"callback-active buffer load should be deferred"
);
assert_eq!(
runner.audio_buffer,
vec![0x80],
"the host stream stays alive with silence while waiting"
);
runner.active_interrupt_callback = None;
runner.try_load_pending_double_buffers();
assert_eq!(
runner.bus.read_long(buf0_ptr + 4) & 0x01,
0x01,
"returned callback buffer stays marked ready while playback owns it"
);
assert!(
runner.dispatcher.sound_manager.channels[0].is_playing(),
"returned callback makes the refilled buffer available to the mixer"
);
let db = runner.dispatcher.sound_manager.channels[0]
.double_buffer
.as_ref()
.expect("double-buffer active");
assert_eq!(db.pending_callback_buffers, [false, false]);
runner.mix_audio(1);
assert_eq!(runner.audio_buffer, vec![0x80, 0xA0]);
}
#[test]
fn try_load_pending_double_buffers_recovers_ready_alternate_after_underrun() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let chan_ptr = 0x0039_38C8;
let callback_addr = 0x0004_1234;
let header_ptr = runner.bus.alloc(24);
let buf0_ptr = runner.bus.alloc(18);
let buf1_ptr = runner.bus.alloc(18);
runner.bus.write_word(header_ptr, 1);
runner.bus.write_word(header_ptr + 2, 8);
runner.bus.write_long(header_ptr + 8, OUTPUT_RATE << 16);
runner.bus.write_long(header_ptr + 12, buf0_ptr);
runner.bus.write_long(header_ptr + 16, buf1_ptr);
runner.bus.write_long(header_ptr + 20, callback_addr);
write_double_buffer(&mut runner.bus, buf0_ptr, &[0xA0, 0xA1]);
runner.bus.write_long(buf1_ptr, 2);
runner.bus.write_long(buf1_ptr + 4, 0);
let mut chan = SndChannel::new(chan_ptr, false);
chan.double_buffer = Some(DoubleBufferState {
header_ptr,
current_buffer: 1,
callback_addr,
chan_ptr,
sample_rate: OUTPUT_RATE << 16,
num_channels: 1,
sample_size: 8,
last_buffer_seen: false,
waiting_for_callback: false,
pending_callback_buffers: [false; 2],
});
runner.dispatcher.sound_manager.channels.push(chan);
runner.try_load_pending_double_buffers();
let chan = &runner.dispatcher.sound_manager.channels[0];
assert!(chan.is_playing(), "ready alternate buffer should load");
let db = chan.double_buffer.as_ref().expect("double-buffer active");
assert_eq!(db.current_buffer, 0);
assert!(
!db.waiting_for_callback,
"loading a ready buffer completes the outstanding refill wait"
);
assert_eq!(
runner.bus.read_long(buf0_ptr + 4) & 0x01,
0x01,
"loading a ready alternate must not clear dbBufferReady before playback exhausts"
);
}
#[test]
fn try_load_pending_double_buffers_does_not_replay_callback_pending_slot() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let chan_ptr = 0x0039_38C8;
let callback_addr = 0x0004_1234;
let header_ptr = runner.bus.alloc(24);
let buf0_ptr = runner.bus.alloc(17);
let buf1_ptr = runner.bus.alloc(17);
runner.bus.write_word(header_ptr, 1);
runner.bus.write_word(header_ptr + 2, 8);
runner.bus.write_long(header_ptr + 8, OUTPUT_RATE << 16);
runner.bus.write_long(header_ptr + 12, buf0_ptr);
runner.bus.write_long(header_ptr + 16, buf1_ptr);
runner.bus.write_long(header_ptr + 20, callback_addr);
write_double_buffer(&mut runner.bus, buf0_ptr, &[0xA0]);
runner.bus.write_long(buf1_ptr, 1);
runner.bus.write_long(buf1_ptr + 4, 0);
let mut chan = SndChannel::new(chan_ptr, false);
chan.double_buffer = Some(DoubleBufferState {
header_ptr,
current_buffer: 0,
callback_addr,
chan_ptr,
sample_rate: OUTPUT_RATE << 16,
num_channels: 1,
sample_size: 8,
last_buffer_seen: false,
waiting_for_callback: true,
pending_callback_buffers: [true, false],
});
runner.dispatcher.sound_manager.channels.push(chan);
runner
.dispatcher
.sound_manager
.pending_callbacks
.push(PendingDoubleBackCallback {
callback_addr,
chan_ptr,
header_ptr,
exhausted_buffer_index: 0,
});
runner.try_load_pending_double_buffers();
assert!(
!runner.dispatcher.sound_manager.channels[0].is_playing(),
"an exhausted slot must not replay just because dbBufferReady remains set"
);
assert_eq!(
runner.bus.read_long(buf0_ptr + 4) & 0x01,
0x01,
"the flag remains ready until fire_sound_doubleback_callbacks clears it"
);
}
#[test]
fn sound_command_callback_trampoline_passes_sndcommand_pointer() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner
.dispatcher
.sound_manager
.pending_sound_callbacks
.push(crate::sound::PendingSoundCallback::Command {
callback_addr: 0x0004_5678,
chan_ptr: 0x0039_38C8,
cmd: crate::sound::SndCommand {
cmd: crate::sound::cmd::CALLBACK,
param1: 0x1234,
param2: 0x0001_43FC,
},
});
runner.fire_sound_callbacks();
let active = runner
.active_interrupt_callback
.expect("sound callback should have been armed");
assert!(matches!(
active.source,
ActiveInterruptCallbackSource::SoundCallback
));
assert_eq!(active.resume_pc, interrupted_pc);
assert_eq!(active.resume_sp, interrupted_sp);
let tramp = runner.sound_callback_trampoline;
let cmd_ptr = tramp + 34;
let saved_regs_sp = interrupted_sp - 4 - 32;
assert_eq!(runner.bus.read_long(tramp + 6), 0x0039_38C8);
assert_eq!(runner.bus.read_long(tramp + 12), cmd_ptr);
assert_eq!(runner.bus.read_long(tramp + 18), 0x0004_5678);
assert_eq!(runner.bus.read_long(tramp + 24), saved_regs_sp);
assert_eq!(runner.bus.read_word(cmd_ptr), crate::sound::cmd::CALLBACK);
assert_eq!(runner.bus.read_word(cmd_ptr + 2), 0x1234);
assert_eq!(runner.bus.read_long(cmd_ptr + 4), 0x0001_43FC);
assert_eq!(
runner.bus.get_alloc_size(tramp),
None,
"Systemless-owned command callback trampoline must stay outside the guest heap"
);
}
#[test]
fn sound_command_callback_trampoline_does_not_perturb_guest_allocations() {
let mut baseline = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let _baseline_callback = baseline.bus.alloc(2);
let expected_next_guest_ptr = baseline.bus.alloc(64);
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let callback_addr = runner.bus.alloc(2);
runner.cpu.write_reg(Register::PC, 0x0001_0000);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner
.dispatcher
.sound_manager
.pending_sound_callbacks
.push(crate::sound::PendingSoundCallback::Command {
callback_addr,
chan_ptr: 0x0039_38C8,
cmd: crate::sound::SndCommand {
cmd: crate::sound::cmd::CALLBACK,
param1: 0,
param2: 0,
},
});
runner.fire_sound_callbacks();
let actual_next_guest_ptr = runner.bus.alloc(64);
assert_eq!(
actual_next_guest_ptr, expected_next_guest_ptr,
"lazy callback setup must not consume application-visible heap space"
);
}
#[test]
fn file_completion_callback_uses_documented_registers_and_restores_foreground() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
let parameter_block = runner.bus.alloc(64);
let callback_addr = runner.bus.alloc(2);
runner.bus.write_word(callback_addr, 0x4E75); for offset in (0..20).step_by(2) {
runner.bus.write_word(interrupted_pc + offset, 0x4E71); }
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.cpu.write_reg(Register::A0, 0x1111_1111);
runner.cpu.write_reg(Register::D0, 0x2222_2222);
runner
.dispatcher
.pending_file_completions
.push_back(PendingFileCompletion {
parameter_block,
completion_addr: callback_addr,
result: -39,
});
assert!(runner.fire_file_completion_callback());
assert_eq!(runner.bus.read_word(parameter_block + 16) as i16, -39);
assert_eq!(runner.cpu.read_reg(Register::A0), parameter_block);
assert_eq!(runner.cpu.read_reg(Register::D0) as i32, -39);
assert!(matches!(
runner.active_interrupt_callback.map(|active| active.source),
Some(ActiveInterruptCallbackSource::FileCompletion)
));
assert!(
runner
.bus
.get_alloc_size(runner.file_completion_trampoline)
.is_none(),
"Systemless-owned completion trampoline must stay outside the guest heap"
);
let (_, running) = runner.run_steps(6, None);
assert!(running);
assert!(runner.active_interrupt_callback.is_none());
assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
assert_eq!(runner.cpu.read_reg(Register::A0), 0x1111_1111);
assert_eq!(runner.cpu.read_reg(Register::D0), 0x2222_2222);
assert!(!runner.is_halted());
}
#[test]
fn adb_mouse_callback_uses_documented_registers_and_restores_foreground() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
let callback_addr = runner.bus.alloc(2);
let data_area = runner.bus.alloc(16);
runner.bus.write_word(callback_addr, 0x4E75); for offset in (0..20).step_by(2) {
runner.bus.write_word(interrupted_pc + offset, 0x4E71); }
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.cpu.write_reg(Register::A0, 0x1111_1111);
runner.cpu.write_reg(Register::A1, 0x2222_2222);
runner.cpu.write_reg(Register::A2, 0x3333_3333);
runner.cpu.write_reg(Register::D0, 0x4444_4444);
assert!(runner
.dispatcher
.adb
.set_device_handler(3, callback_addr, data_area, false,));
runner.dispatcher.adb.note_mouse_state((5, -10), true);
assert!(runner.fire_adb_callback());
let packet_ptr = runner.cpu.read_reg(Register::A0);
assert_eq!(runner.bus.read_bytes(packet_ptr, 3), &[2, 5, 0xF6]);
assert_eq!(runner.cpu.read_reg(Register::A1), callback_addr);
assert_eq!(runner.cpu.read_reg(Register::A2), data_area);
assert_eq!(runner.cpu.read_reg(Register::D0), 0x3C);
assert!(matches!(
runner.active_interrupt_callback.map(|active| active.source),
Some(ActiveInterruptCallbackSource::Adb)
));
assert!(
runner
.bus
.get_alloc_size(runner.adb_callback_trampoline)
.is_none(),
"Systemless-owned ADB trampoline must stay outside the guest heap"
);
let (_, running) = runner.run_steps(6, None);
assert!(running);
assert!(runner.active_interrupt_callback.is_none());
assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
assert_eq!(runner.cpu.read_reg(Register::A0), 0x1111_1111);
assert_eq!(runner.cpu.read_reg(Register::A1), 0x2222_2222);
assert_eq!(runner.cpu.read_reg(Register::A2), 0x3333_3333);
assert_eq!(runner.cpu.read_reg(Register::D0), 0x4444_4444);
assert!(!runner.is_halted());
}
#[test]
fn sound_command_callback_trampoline_tolerates_one_long_pascal_cleanup() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
let callback_addr = runner.bus.alloc(4);
runner.bus.write_word(callback_addr, 0x2E9F); runner.bus.write_word(callback_addr + 2, 0x4E75); runner.bus.write_word(interrupted_pc, 0x4E71); runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner
.dispatcher
.sound_manager
.pending_sound_callbacks
.push(crate::sound::PendingSoundCallback::Command {
callback_addr,
chan_ptr: 0x0039_38C8,
cmd: crate::sound::SndCommand {
cmd: crate::sound::cmd::CALLBACK,
param1: 0x1234,
param2: 0x0001_43FC,
},
});
runner.fire_sound_callbacks();
let (steps, running) = runner.run_steps(10, None);
assert!(running, "callback trampoline should resume foreground code");
assert_eq!(steps, 10);
assert!(runner.active_interrupt_callback.is_none());
assert_eq!(runner.cpu.read_reg(Register::PC), interrupted_pc + 2);
assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
assert!(!runner.is_halted());
assert_eq!(
runner
.bus
.get_alloc_size(runner.sound_file_completion_trampoline),
None,
"Systemless-owned file completion trampoline must stay outside the guest heap"
);
}
#[test]
fn sound_file_completion_callback_trampoline_tolerates_c_style_cleanup() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
let callback_addr = runner.bus.alloc(2);
runner.bus.write_word(callback_addr, 0x4E75); runner.bus.write_word(interrupted_pc, 0x4E71); runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner
.dispatcher
.sound_manager
.pending_sound_callbacks
.push(crate::sound::PendingSoundCallback::FileCompletion {
callback_addr,
chan_ptr: 0x0039_38C8,
});
runner.fire_sound_callbacks();
let (steps, running) = runner.run_steps(10, None);
assert!(
running,
"file completion trampoline should resume foreground code"
);
assert_eq!(steps, 10);
assert!(runner.active_interrupt_callback.is_none());
assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
assert!(!runner.is_halted());
assert_eq!(
runner
.bus
.get_alloc_size(runner.sound_doubleback_trampoline),
None,
"Systemless-owned double-back trampoline must stay outside the guest heap"
);
}
#[test]
fn sound_doubleback_callback_trampoline_tolerates_c_style_cleanup() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
let callback_addr = runner.bus.alloc(2);
let header_ptr = 0x0020_0000;
let exhausted_buf_ptr = 0x0020_1000;
runner.bus.write_word(callback_addr, 0x4E75); runner.bus.write_word(interrupted_pc, 0x4E71); runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.bus.write_long(header_ptr + 12, exhausted_buf_ptr);
runner.bus.write_long(exhausted_buf_ptr + 4, 0x0000_0001);
runner
.dispatcher
.sound_manager
.pending_callbacks
.push(PendingDoubleBackCallback {
callback_addr,
chan_ptr: 0x0039_38C8,
header_ptr,
exhausted_buffer_index: 0,
});
runner.fire_sound_doubleback_callbacks();
let (steps, running) = runner.run_steps(12, None);
assert!(
running,
"doubleback trampoline should resume foreground code"
);
assert_eq!(steps, 12);
assert!(runner.active_interrupt_callback.is_none());
assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
assert!(!runner.is_halted());
}
#[test]
fn run_pending_sound_work_does_not_advance_ticks_or_foreground_code() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
let callback_addr = runner.bus.alloc(2);
runner.bus.write_word(callback_addr, 0x4E75); runner.bus.write_word(interrupted_pc, 0x4E71); runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.bus.write_long(0x016A, 41);
runner.dispatcher.tick_count = 41;
runner.set_instructions_per_tick(1);
runner.tick_budget = 0;
runner
.dispatcher
.sound_manager
.pending_sound_callbacks
.push(PendingSoundCallback::Command {
callback_addr,
chan_ptr: 0x0039_38C8,
cmd: SndCommand {
cmd: crate::sound::cmd::CALLBACK,
param1: 0,
param2: 0,
},
});
let (steps, running) = runner.run_pending_sound_work(32);
assert!(running);
assert!(steps > 0, "sound callback trampoline should execute");
assert_eq!(
runner.guest_tick(),
41,
"callback-only slices must not advance application-visible ticks"
);
assert_eq!(
runner.cpu.read_reg(Register::PC),
interrupted_pc,
"sound callback service must stop before resumed foreground code runs"
);
assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
assert!(runner.active_interrupt_callback.is_none());
assert!(!runner.has_pending_sound_work());
}
#[test]
fn gui_cpu_slice_does_not_finalize_host_frame() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let callback_addr = runner.bus.alloc(2);
runner.bus.write_word(callback_addr, 0x4E75); runner
.dispatcher
.sound_manager
.pending_sound_callbacks
.push(PendingSoundCallback::Command {
callback_addr,
chan_ptr: 0x0039_38C8,
cmd: SndCommand {
cmd: crate::sound::cmd::CALLBACK,
param1: 0,
param2: 0,
},
});
let (steps, running) = runner.run_gui_cpu_slice(0, 0);
assert!(running);
assert_eq!(steps, 0);
assert!(
runner.active_interrupt_callback.is_none(),
"CPU-only GUI slices must not fire host-frame sound callbacks"
);
assert!(runner.has_pending_sound_work());
}
#[test]
fn run_steps_paces_pending_sound_doublebacks_to_one_per_slice() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
let callback_addr = runner.bus.alloc(2);
let header_ptr = runner.bus.alloc(24);
let buf0_ptr = runner.bus.alloc(16);
let buf1_ptr = runner.bus.alloc(16);
runner.bus.write_word(callback_addr, 0x4E75); for offset in (0..512).step_by(2) {
runner.bus.write_word(interrupted_pc + offset, 0x4E71); }
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.bus.write_long(header_ptr + 12, buf0_ptr);
runner.bus.write_long(header_ptr + 16, buf1_ptr);
runner.bus.write_long(buf0_ptr + 4, 0x0000_0001);
runner.bus.write_long(buf1_ptr + 4, 0x0000_0001);
runner
.dispatcher
.sound_manager
.pending_callbacks
.push(PendingDoubleBackCallback {
callback_addr,
chan_ptr: 0x0039_38C8,
header_ptr,
exhausted_buffer_index: 0,
});
runner
.dispatcher
.sound_manager
.pending_callbacks
.push(PendingDoubleBackCallback {
callback_addr,
chan_ptr: 0x0039_38C8,
header_ptr,
exhausted_buffer_index: 1,
});
let (_steps, running) = runner.run_steps(96, None);
assert!(running);
assert!(runner.active_interrupt_callback.is_none());
assert_eq!(
runner.dispatcher.sound_manager.pending_callbacks.len(),
1,
"one CPU slice must not drain back-to-back doubleback interrupts"
);
assert_eq!(
runner.dispatcher.sound_manager.pending_callbacks[0].exhausted_buffer_index,
1
);
let (_steps, running) = runner.run_steps(96, None);
assert!(running);
assert!(runner.active_interrupt_callback.is_none());
assert!(
runner.dispatcher.sound_manager.pending_callbacks.is_empty(),
"the next CPU slice may dispatch the next pending doubleback"
);
}
#[test]
fn vbl_callback_arms_interrupt_with_task_ptr_in_a0() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0002_0000;
let interrupted_sp = 0x007F_FFC0;
let task_ptr = 0x0020_2000;
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.cpu.write_reg(Register::A0, 0xAAAA_0000);
runner.cpu.core.set_ccr(0x04);
runner.cpu.core.set_sr_noint_nosp(0x2004);
runner.bus.write_word(task_ptr + 4, 1); runner.bus.write_long(task_ptr + 6, 0x0004_1234); runner.bus.write_word(task_ptr + 10, 1); runner.bus.write_word(task_ptr + 12, 0); runner.dispatcher.vbl_tasks.push(VblTask {
task_ptr,
slot: Some(9),
pending: false,
});
runner.fire_vbl_tasks();
let active = runner
.active_interrupt_callback
.expect("vbl callback should have been armed");
assert!(matches!(active.source, ActiveInterruptCallbackSource::Vbl));
assert_eq!(active.resume_pc, interrupted_pc);
assert_eq!(active.resume_sp, interrupted_sp);
assert_eq!(active.sr, 0x2004);
assert_eq!(active.ccr, 0x04);
assert_eq!(runner.bus.read_word(task_ptr + 10), 0);
assert_ne!(runner.vbl_trampoline, 0);
assert_eq!(runner.cpu.core.get_sr(), 0x2104);
assert_eq!(runner.cpu.read_reg(Register::PC), runner.vbl_trampoline);
assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp - 4);
assert_eq!(runner.bus.read_long(interrupted_sp - 4), interrupted_pc);
assert_eq!(runner.bus.read_word(runner.vbl_trampoline + 4), 0x207C);
assert_eq!(runner.bus.read_long(runner.vbl_trampoline + 6), task_ptr);
assert_eq!(
runner.bus.read_long(runner.vbl_trampoline + 12),
0x0004_1234
);
}
#[test]
fn simultaneous_vbl_callbacks_do_not_starve_later_queue_elements() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0002_0000;
let interrupted_sp = 0x007F_FFC0;
let first_ptr = 0x0020_2000;
let second_ptr = 0x0020_2020;
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.cpu.core.set_sr_noint_nosp(0x2000);
for (task_ptr, callback) in [(first_ptr, 0x0004_1234), (second_ptr, 0x0004_5678)] {
runner.bus.write_word(task_ptr + 4, 1);
runner.bus.write_long(task_ptr + 6, callback);
runner.bus.write_word(task_ptr + 10, 1);
runner.dispatcher.vbl_tasks.push(VblTask {
task_ptr,
slot: None,
pending: false,
});
}
runner.fire_vbl_tasks();
assert_eq!(runner.bus.read_long(runner.vbl_trampoline + 6), first_ptr);
assert!(runner.dispatcher.vbl_tasks[1].pending);
runner.bus.write_word(first_ptr + 10, 1);
runner.active_interrupt_callback = None;
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.cpu.core.set_sr_noint_nosp(0x2000);
runner.fire_vbl_tasks();
assert_eq!(runner.bus.read_long(runner.vbl_trampoline + 6), second_ptr);
assert!(runner.dispatcher.vbl_tasks[0].pending);
assert!(!runner.dispatcher.vbl_tasks[1].pending);
}
#[test]
fn vbl_callback_defers_while_processor_priority_masks_level_one() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0002_0000;
let interrupted_sp = 0x007F_FFC0;
let task_ptr = 0x0020_2000;
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.cpu.core.set_sr_noint_nosp(0x2100);
runner.bus.write_word(task_ptr + 4, 1);
runner.bus.write_long(task_ptr + 6, 0x0004_1234);
runner.bus.write_word(task_ptr + 10, 1);
runner.bus.write_word(task_ptr + 12, 0);
runner.dispatcher.vbl_tasks.push(VblTask {
task_ptr,
slot: None,
pending: false,
});
runner.fire_vbl_tasks();
assert!(runner.active_interrupt_callback.is_none());
assert_eq!(runner.bus.read_word(task_ptr + 10), 1);
assert_eq!(runner.cpu.read_reg(Register::PC), interrupted_pc);
assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
}
#[test]
fn vbl_callback_restores_foreground_sr_after_return() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0002_0000;
let interrupted_sp = 0x007F_FFC0;
let task_ptr = 0x0020_2000;
let callback_addr = 0x0004_1234;
runner.bus.write_word(interrupted_pc, 0x4E71); runner.bus.write_word(callback_addr, 0x4E75); runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.cpu.core.set_sr_noint_nosp(0x2004);
runner.bus.write_word(task_ptr + 4, 1);
runner.bus.write_long(task_ptr + 6, callback_addr);
runner.bus.write_word(task_ptr + 10, 1);
runner.bus.write_word(task_ptr + 12, 0);
runner.dispatcher.vbl_tasks.push(VblTask {
task_ptr,
slot: None,
pending: false,
});
runner.fire_vbl_tasks();
assert_eq!(runner.cpu.core.get_sr(), 0x2104);
let (_steps, running) = runner.run_steps(8, None);
assert!(running);
assert!(runner.active_interrupt_callback.is_none());
assert_eq!(runner.cpu.core.get_sr(), 0x2004);
assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
}
#[test]
fn custom_instructions_per_tick_controls_tick_cadence() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
let program_words = 14;
for offset in (0..program_words).step_by(2) {
runner.bus.write_word(program_start + offset, 0x4E71);
}
runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 0);
runner.set_instructions_per_tick(3);
let (steps, running) = runner.run_steps(7, None);
assert!(running);
assert_eq!(steps, 7);
assert_eq!(runner.bus.read_long(0x016A), 2);
}
#[test]
fn non_idle_hle_trap_cost_advances_tick_budget() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let sp = 0x0010_0000u32;
let rect = 0x0020_0000u32;
runner.bus.write_word(base, 0xA8A8); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, sp);
runner.bus.write_word(sp, 1); runner.bus.write_word(sp + 2, 2); runner.bus.write_long(sp + 4, rect);
runner.bus.write_word(rect, 10);
runner.bus.write_word(rect + 2, 20);
runner.bus.write_word(rect + 4, 30);
runner.bus.write_word(rect + 6, 40);
runner.bus.write_long(0x016A, 0);
runner.dispatcher.tick_count = 0;
runner.set_instructions_per_tick(5);
let (steps, running) = runner.run_steps(1, None);
assert!(running);
assert_eq!(steps, 1);
assert_eq!(
runner.guest_tick(),
1,
"non-idle HLE traps should consume tick budget beyond the base instruction"
);
assert_eq!(runner.bus.read_word(rect), 11);
assert_eq!(runner.bus.read_word(rect + 2), 22);
}
#[test]
fn idle_hle_traps_do_not_apply_extra_tick_cost() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
runner.bus.write_word(base, 0xA975); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.bus.write_long(0x016A, 42);
runner.dispatcher.tick_count = 42;
runner.set_instructions_per_tick(5);
let (steps, running) = runner.run_steps(1, None);
assert!(running);
assert_eq!(steps, 1);
assert_eq!(
runner.guest_tick(),
42,
"polling traps should not add synthetic HLE manager cost"
);
assert_eq!(runner.tick_budget, 4);
}
#[test]
fn hle_trap_cost_stops_gui_slice_at_tick_cap() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let sp = 0x0010_0000u32;
let rect = 0x0020_0000u32;
runner.bus.write_word(base, 0xA8A8); runner.bus.write_word(base + 2, 0x4E71); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, sp);
runner.bus.write_word(sp, 1);
runner.bus.write_word(sp + 2, 2);
runner.bus.write_long(sp + 4, rect);
runner.bus.write_word(rect, 10);
runner.bus.write_word(rect + 2, 20);
runner.bus.write_word(rect + 4, 30);
runner.bus.write_word(rect + 6, 40);
runner.bus.write_long(0x016A, 0);
runner.dispatcher.tick_count = 0;
runner.set_instructions_per_tick(5);
let (steps, running) = runner.run_gui_slice_with_audio(8, 1, 0);
assert!(running);
assert_eq!(steps, 1);
assert_eq!(runner.guest_tick(), 1);
assert_eq!(
runner.cpu.read_reg(Register::PC),
base + 2,
"the next guest instruction should be deferred once HLE cost reaches the GUI tick cap"
);
}
#[test]
fn dispatcher_tick_count_stays_in_sync_with_bus() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
let program_words = 20;
for offset in (0..program_words).step_by(2) {
runner.bus.write_word(program_start + offset, 0x4E71);
}
runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 0);
runner.dispatcher.tick_count = 0;
runner.set_instructions_per_tick(3);
for _ in 0..3 {
let (_, running) = runner.run_steps(3, None);
assert!(running);
assert_eq!(
runner.bus.read_long(0x016A),
runner.dispatcher.tick_count,
"bus $016A ({}) diverged from dispatcher.tick_count ({})",
runner.bus.read_long(0x016A),
runner.dispatcher.tick_count,
);
}
}
#[test]
fn idle_snapshot_preserves_extended_cpu_state() {
let mut cpu = m68k::CpuCore::new();
cpu.fpr[0] = m68k::fpu::FloatX80::from_extended(0x3FFF, 0x8000_0000_0000_0000);
let baseline = CpuArchitecturalSnapshot::capture(&cpu);
cpu.change_of_flow = !cpu.change_of_flow;
assert_eq!(
baseline,
CpuArchitecturalSnapshot::capture(&cpu),
"internal flow bookkeeping must not participate in an idle-cycle proof"
);
cpu.fpr[0].mantissa ^= 1;
assert_ne!(
baseline,
CpuArchitecturalSnapshot::capture(&cpu),
"80-bit FPU precision must participate in an idle-cycle proof"
);
cpu.fpr[0].mantissa ^= 1;
cpu.mmu_crp_aptr = 0x1234_5000;
assert_ne!(
baseline,
CpuArchitecturalSnapshot::capture(&cpu),
"canonical MMU state must participate in an idle-cycle proof"
);
cpu.mmu_crp_aptr = 0;
cpu.prefetch_queue[1] = 0x4E71;
assert_ne!(
baseline,
CpuArchitecturalSnapshot::capture(&cpu),
"precise prefetch state must participate in an idle-cycle proof"
);
}
#[test]
fn exact_idle_cycle_requires_cpu_and_memory_repeat() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let trap_pc = 0x0002_0000u32;
let sp = 0x0010_0000u32;
let scratch = 0x0020_0000u32;
runner.cpu.write_reg(Register::PC, trap_pc + 2);
runner.cpu.core.ppc = trap_pc;
runner.cpu.core.ir = 0xA975;
runner.cpu.write_reg(Register::A7, sp);
runner.bus.write_long(sp, 100);
runner.bus.write_long(scratch, 0x1122_3344);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(runner.idle_cycle_probe.is_some());
assert!(runner.bus.fast_mem_window().is_none());
runner.bus.write_long(scratch, 0xAABB_CCDD);
runner.bus.write_long(scratch, 0x1122_3344);
runner.note_idle_cycle_trap_result(0xA971);
assert!(runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert_eq!(runner.dispatcher.tick_count, 105);
assert_eq!(runner.bus.read_long(0x016A), 105);
assert_eq!(runner.bus.read_long(sp), 100);
assert!(runner.idle_cycle_probe.is_none());
assert!(runner.idle_cycle_sleep.is_some());
assert!(
runner.bus.fast_mem_window().is_none(),
"the parked sleep keeps a write guard armed across the frontend boundary"
);
runner.dispatcher.sent_open_app_event = true;
assert!(runner.try_resume_proven_idle_cycle(Some(110)));
assert_eq!(runner.dispatcher.tick_count, 110);
assert_eq!(runner.bus.read_long(sp), 100);
assert!(runner.idle_cycle_sleep.is_some());
runner.push_mouse_down(20, 30);
assert!(
!runner.try_resume_proven_idle_cycle(Some(115)),
"new host input must revoke a proof before the guest event loop is skipped"
);
assert_eq!(runner.dispatcher.tick_count, 110);
assert!(runner.idle_cycle_sleep.is_none());
assert!(runner.bus.fast_mem_window().is_some());
}
#[test]
fn busy_poller_overflows_one_journal_then_backs_off_for_the_tick() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let trap_pc = 0x0002_0000u32;
let work = 0x0030_0000u32;
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.dispatcher.tick_count = 100;
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(runner.idle_cycle_probe.is_some());
assert!(runner.bus.fast_mem_window().is_none());
for offset in 0..(8 * crate::memory::bus::WRITE_PROBE_MAX_ENTRIES as u32) {
runner.bus.write_byte(work + offset, 0xAA);
}
assert!(
runner.bus.fast_mem_window().is_some(),
"the bus voids an overflowing journal immediately, without waiting for the runner"
);
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(runner.idle_cycle_probe.is_none());
assert!(runner.idle_cycle_site_is_busy(trap_pc, 100));
for _ in 0..4 {
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(runner.idle_cycle_probe.is_none());
assert!(runner.bus.fast_mem_window().is_some());
}
assert_eq!(
runner.dispatcher.tick_count, 100,
"a busy site is never fast-forwarded"
);
runner.dispatcher.tick_count = 101;
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(runner.idle_cycle_probe.is_some());
assert!(runner.bus.fast_mem_window().is_none());
}
#[test]
fn a_site_that_keeps_failing_gets_two_probes_per_tick_then_none() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let trap_pc = 0x0002_0000u32;
let scratch = 0x0020_0000u32;
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.dispatcher.tick_count = 100;
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(runner.idle_cycle_probe.is_some());
runner.bus.write_long(scratch, 0x1111_1111);
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(runner.idle_cycle_probe.is_some(), "one retry is allowed");
assert!(runner.bus.fast_mem_window().is_none());
runner.bus.write_long(scratch, 0x2222_2222);
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(runner.idle_cycle_probe.is_none());
assert!(runner.bus.fast_mem_window().is_some());
for _ in 0..8 {
runner.bus.write_long(scratch, 0x3333_3333);
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(runner.idle_cycle_probe.is_none());
assert!(runner.bus.fast_mem_window().is_some());
}
assert_eq!(runner.dispatcher.tick_count, 100);
runner.dispatcher.tick_count = 101;
runner.bus.write_long(0x016A, 101);
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(runner.idle_cycle_probe.is_some());
runner.bus.write_long(scratch, 0xAAAA_AAAA);
runner.bus.write_long(scratch, 0x3333_3333);
runner.note_idle_cycle_trap_result(0xA971);
assert!(runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert_eq!(runner.dispatcher.tick_count, 105);
assert!(runner.idle_cycle_sleep.is_some());
}
#[test]
fn poll_traps_and_sane_do_not_cancel_an_idle_probe() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let trap_pc = 0x0002_0000u32;
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.dispatcher.tick_count = 100;
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(runner.idle_cycle_probe.is_some());
for opcode in [0xA972u16, 0xA973, 0xA974, 0xA975, 0xA976, 0xA9EB, 0xA9EC] {
runner.note_idle_cycle_trap_result(opcode);
assert!(
runner.idle_cycle_probe.is_some(),
"poll/SANE trap {opcode:04X} must not cancel the probe"
);
}
runner.note_idle_cycle_trap_result(0xA893);
assert!(runner.idle_cycle_probe.is_none());
}
#[test]
fn poll_anchor_covers_the_input_family_only() {
for opcode in [0xA972u16, 0xA973, 0xA974, 0xA975, 0xA976, 0xAD76] {
assert!(is_poll_anchor_trap(opcode), "{opcode:04X}");
}
for opcode in [0xA970u16, 0xA971, 0xA991, 0xA9EB, 0xA893, 0x4E71] {
assert!(!is_poll_anchor_trap(opcode), "{opcode:04X}");
}
}
#[test]
fn input_poll_cycle_proves_and_parks_like_a_null_event_cycle() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let trap_pc = 0x0002_0000u32;
let sp = 0x0010_0000u32;
let keymap = 0x0020_0000u32;
runner.cpu.write_reg(Register::PC, trap_pc + 2);
runner.cpu.core.ppc = trap_pc;
runner.cpu.core.ir = 0xA976;
runner.cpu.write_reg(Register::A7, sp);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.bus.write_long(keymap, 0);
runner.bus.write_long(keymap + 16, 0x0001_0000);
assert!(!runner.try_exact_null_event_cycle_fastfwd(trap_pc, Some(105)));
assert!(!runner.try_exact_null_event_cycle_fastfwd(trap_pc, Some(105)));
assert!(runner.idle_cycle_probe.is_some());
runner.bus.write_long(keymap, 0);
runner.note_idle_cycle_trap_result(0xA976);
runner.note_idle_cycle_trap_result(0xA974);
runner.note_idle_cycle_trap_result(0xA975);
runner.bus.write_long(keymap + 16, 0x0001_0000);
runner.bus.write_long(keymap + 16, 0x0001_0000);
runner.note_idle_cycle_trap_result(0xA9EB);
assert!(
runner.idle_cycle_probe.is_some(),
"cycle traps kept the probe"
);
assert!(runner.try_exact_null_event_cycle_fastfwd(trap_pc, Some(100)));
assert_eq!(runner.dispatcher.tick_count, 100);
assert!(runner.idle_cycle_sleep.is_some());
}
#[test]
fn period_two_poll_cycle_proves_and_advances() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let trap_pc = 0x0002_0000u32;
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
let set_d5 = |runner: &mut FixtureRunner, v: u32| runner.cpu.core.set_d(5, v);
set_d5(&mut runner, 0x39);
assert!(!runner.try_exact_null_event_cycle_fastfwd(trap_pc, Some(100)));
assert!(!runner.try_exact_null_event_cycle_fastfwd(trap_pc, Some(100)));
assert!(
runner.idle_cycle_probe.is_some(),
"probe armed at 0x39 state"
);
set_d5(&mut runner, 0x2C);
assert!(
!runner.try_exact_null_event_cycle_fastfwd(trap_pc, Some(100)),
"mid-period arrival must not prove"
);
assert!(
runner.idle_cycle_probe.is_some(),
"mid-period arrival must keep the probe alive"
);
set_d5(&mut runner, 0x39);
assert!(
runner.try_exact_null_event_cycle_fastfwd(trap_pc, Some(100)),
"origin state closes the period-2 proof and parks at the cap"
);
assert!(runner.idle_cycle_sleep.is_some());
}
#[test]
fn aperiodic_state_walk_never_proves() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let trap_pc = 0x0002_0000u32;
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
for step in 0..24u32 {
runner.cpu.core.set_d(5, 0x1000 + step);
assert!(
!runner.try_exact_null_event_cycle_fastfwd(trap_pc, Some(100)),
"step {step} must not prove"
);
}
assert!(runner.idle_cycle_sleep.is_none());
}
#[test]
fn cycle_longer_than_period_cap_never_proves() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let trap_pc = 0x0002_0000u32;
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.core.set_d(5, 0);
assert!(!runner.try_exact_null_event_cycle_fastfwd(trap_pc, Some(100)));
assert!(!runner.try_exact_null_event_cycle_fastfwd(trap_pc, Some(100)));
for state in 1..=IDLE_CYCLE_MAX_PERIOD {
runner.cpu.core.set_d(5, u32::from(state));
assert!(!runner.try_exact_null_event_cycle_fastfwd(trap_pc, Some(100)));
}
runner.cpu.core.set_d(5, 0);
assert!(!runner.try_exact_null_event_cycle_fastfwd(trap_pc, Some(100)));
assert!(runner.idle_cycle_sleep.is_none());
}
#[test]
fn exact_null_event_cycle_supports_alternating_sites_headlessly() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let site_a = 0x0002_0000u32;
let site_b = 0x0002_1000u32;
runner.cpu.write_reg(Register::PC, site_a + 2);
runner.cpu.core.ppc = site_a;
runner.cpu.core.ir = 0xA970;
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
assert!(!runner.try_exact_null_event_cycle_fastfwd(site_a, None));
assert!(!runner.try_exact_null_event_cycle_fastfwd(site_b, None));
assert_eq!(runner.idle_cycle_last_seen, Some((site_a, 100)));
assert!(!runner.try_exact_null_event_cycle_fastfwd(site_a, None));
assert!(runner.idle_cycle_probe.is_some());
assert!(!runner.try_exact_null_event_cycle_fastfwd(site_b, None));
assert!(runner.idle_cycle_probe.is_some());
assert!(!runner.try_exact_null_event_cycle_fastfwd(site_a, None));
assert_eq!(runner.guest_tick(), 101);
assert!(runner.idle_cycle_probe.is_none());
assert!(runner.idle_cycle_sleep.is_none());
}
#[test]
fn exact_idle_cycle_rejects_an_architectural_cpu_change() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let trap_pc = 0x0002_0000u32;
runner.cpu.write_reg(Register::PC, trap_pc + 2);
runner.cpu.core.ppc = trap_pc;
runner.cpu.core.ir = 0xA970;
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 101, Some(100)));
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 101, Some(100)));
assert!(runner.idle_cycle_probe.is_some());
runner.cpu.write_reg(Register::D3, 1);
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 101, Some(100)));
assert_eq!(runner.dispatcher.tick_count, 100);
assert!(runner.idle_cycle_sleep.is_none());
assert!(
runner.idle_cycle_probe.is_some(),
"a changed state may start a new observation but must not reuse the old proof"
);
}
#[test]
fn exact_null_event_cycle_covers_the_complete_guest_state_machine() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let code = 0x0002_0000u32;
let event = 0x0020_0000u32;
let delay_base = 0x0021_0000u32;
let tick_base = 0x0022_0000u32;
let flag_base = 0x0023_0000u32;
let stack = 0x0010_0000u32;
for (offset, word) in [
(0, 0x554F),
(2, 0x3F3C),
(4, 0xFFFF),
(6, 0x4879),
(8, (event >> 16) as u16),
(10, event as u16),
(12, 0xA970),
(14, 0x4A5F),
(16, 0x4879),
(18, ((event + 10) >> 16) as u16),
(20, (event + 10) as u16),
(22, 0xA871),
(24, 0xA9B4),
(26, 0x594F),
(28, 0xA975),
(30, 0x3028),
(32, 16),
(34, 0x48C0),
(36, 0xD0A9),
(38, 32),
(40, 0xB09F),
(42, 0x5DC0),
(44, 0x4A6A),
(46, 48),
(48, 0x56C1),
(50, 0x8001),
(52, 0x6700),
(54, 0xFFCA),
] {
runner.bus.write_word(code + offset, word);
}
runner.cpu.write_reg(Register::PC, code);
runner.cpu.write_reg(Register::A7, stack);
runner.cpu.write_reg(Register::A0, delay_base);
runner.cpu.write_reg(Register::A1, tick_base);
runner.cpu.write_reg(Register::A2, flag_base);
runner.cpu.write_reg(Register::D0, 0);
runner.cpu.write_reg(Register::D1, 0);
runner.bus.write_word(delay_base + 16, 5);
runner.bus.write_long(tick_base + 32, 100);
runner.bus.write_word(flag_base + 48, 0);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.dispatcher.sent_open_app_event = true;
let (_, running) = runner.run_steps_internal(1_000, Some(100), 0, true, false, false);
assert!(running);
let sleep = runner
.idle_cycle_sleep
.as_ref()
.expect("the complete null-event state machine should prove an identity cycle");
assert_eq!(sleep.trap_pc, code + 12);
assert_eq!(sleep.wake_tick, 101);
assert!(!runner.try_resume_proven_idle_cycle(Some(101)));
assert_eq!(runner.dispatcher.tick_count, 101);
assert!(runner.idle_cycle_sleep.is_none());
assert_eq!(runner.cpu.core.pc, code + 14);
}
#[test]
fn proven_idle_cycle_stops_sleeping_at_its_known_dependency() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let trap_pc = 0x0002_0000u32;
let sp = 0x0010_0000u32;
runner.cpu.write_reg(Register::PC, trap_pc + 2);
runner.cpu.core.ppc = trap_pc;
runner.cpu.core.ir = 0xA975;
runner.cpu.write_reg(Register::A7, sp);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.dispatcher.sent_open_app_event = true;
runner.park_proven_idle_cycle(trap_pc, 103);
assert!(!runner.try_resume_proven_idle_cycle(Some(110)));
assert_eq!(runner.dispatcher.tick_count, 103);
assert!(runner.idle_cycle_sleep.is_none());
assert!(runner.bus.fast_mem_window().is_some());
}
#[test]
fn exact_idle_cycle_rejects_changed_memory_and_non_quiescent_traps() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let trap_pc = 0x0002_0000u32;
let scratch = 0x0020_0000u32;
runner.cpu.write_reg(Register::PC, trap_pc + 2);
runner.cpu.core.ppc = trap_pc;
runner.cpu.core.ir = 0xA975;
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
runner.bus.write_byte(scratch, 1);
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert_eq!(runner.dispatcher.tick_count, 100);
runner.note_idle_cycle_trap_result(0xA8AD); assert!(runner.idle_cycle_probe.is_none());
assert!(runner.idle_cycle_last_seen.is_none());
assert!(runner.bus.fast_mem_window().is_some());
}
#[test]
fn ordinary_tick_advance_cancels_an_exact_idle_cycle_probe() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let trap_pc = 0x0002_0000u32;
runner.cpu.write_reg(Register::PC, trap_pc + 2);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(!runner.try_exact_idle_cycle_fastfwd(trap_pc, 200, Some(105)));
assert!(runner.idle_cycle_probe.is_some());
runner.advance_guest_tick();
assert!(runner.idle_cycle_probe.is_none());
assert!(runner.idle_cycle_last_seen.is_none());
assert!(runner.bus.fast_mem_window().is_some());
}
#[test]
fn spin_fastfwd_template_f_saved_register_beq_variant() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let sp = 0x0010_0000u32;
runner.bus.write_word(base, 0x598F);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0x201F);
runner.bus.write_word(base + 6, 0xBE80);
runner.bus.write_word(base + 8, 0x67F6);
runner.bus.write_word(base + 10, 0x4E71);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::D7, 100);
runner.cpu.write_reg(Register::A7, sp - 4);
runner.cpu.write_reg(Register::PC, base + 4);
runner.bus.write_long(sp - 4, 100);
let mut count = 0usize;
let hit_cap = runner.try_tickcount_spin_fastfwd(base + 4, None, &mut count);
assert!(!hit_cap);
assert_eq!(runner.guest_tick(), 101);
assert_eq!(runner.bus.read_long(sp - 4), 101);
assert_eq!(runner.cpu.read_reg(Register::A7), sp - 4);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 4);
assert_eq!(count, 0, "post-trap body remains for exact CPU execution");
for _ in 0..3 {
assert!(matches!(
runner.cpu.step(&mut runner.bus),
crate::cpu::StepResult::Ok
));
}
assert_eq!(runner.cpu.read_reg(Register::D0), 101);
assert_eq!(runner.cpu.read_reg(Register::A7), sp);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 10);
}
#[test]
fn spin_fastfwd_template_f_rejects_stateful_branch_target() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let sp = 0x0010_0000u32;
runner.bus.write_word(base, 0x52B8);
runner.bus.write_word(base + 2, 0x0002);
runner.bus.write_word(base + 4, 0x594F);
runner.bus.write_word(base + 6, 0xA975);
runner.bus.write_word(base + 8, 0x201F);
runner.bus.write_word(base + 10, 0xBE80);
runner.bus.write_word(base + 12, 0x67F2);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::D7, 100);
runner.cpu.write_reg(Register::A7, sp - 4);
runner.bus.write_long(sp - 4, 100);
let mut count = 0usize;
runner.try_tickcount_spin_fastfwd(base + 8, None, &mut count);
assert_eq!(runner.guest_tick(), 100);
assert_eq!(runner.bus.read_long(sp - 4), 100);
assert_eq!(runner.cpu.read_reg(Register::A7), sp - 4);
assert_eq!(count, 0);
}
#[test]
fn spin_fastfwd_template_g_elapsed_frame_local_variant() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let a5 = 0x0001_8000u32;
let a6 = 0x0010_1000u32;
let sp = 0x0010_0000u32;
runner.bus.write_word(base, 0x598F);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0x201F);
runner.bus.write_word(base + 6, 0x2D40);
runner.bus.write_word(base + 8, 0xFFFC);
runner.bus.write_word(base + 10, 0x202E);
runner.bus.write_word(base + 12, 0xFFFC);
runner.bus.write_word(base + 14, 0x90AD);
runner.bus.write_word(base + 16, 0xFFDC);
runner.bus.write_word(base + 18, 0x306E);
runner.bus.write_word(base + 20, 0x0008);
runner.bus.write_word(base + 22, 0xB1C0);
runner.bus.write_word(base + 24, 0x6EE6);
runner.bus.write_word(base + 26, 0x4E71);
runner.bus.write_long(a5 - 36, 400);
runner.bus.write_word(a6 + 8, 5);
runner.bus.write_long(0x016A, 401);
runner.dispatcher.tick_count = 401;
runner.cpu.write_reg(Register::A5, a5);
runner.cpu.write_reg(Register::A6, a6);
runner.cpu.write_reg(Register::A7, sp - 4);
runner.cpu.write_reg(Register::PC, base + 4);
runner.bus.write_long(sp - 4, 401);
let mut count = 0usize;
let hit_cap = runner.try_tickcount_spin_fastfwd(base + 4, None, &mut count);
assert!(!hit_cap);
assert_eq!(runner.guest_tick(), 405);
assert_eq!(runner.bus.read_long(sp - 4), 405);
assert_eq!(runner.cpu.read_reg(Register::A7), sp - 4);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 4);
assert_eq!(count, 0, "post-trap body remains for exact CPU execution");
for _ in 0..7 {
assert!(matches!(
runner.cpu.step(&mut runner.bus),
crate::cpu::StepResult::Ok
));
}
assert_eq!(runner.bus.read_long(a6 - 4), 405);
assert_eq!(runner.cpu.read_reg(Register::D0), 5);
assert_eq!(runner.cpu.read_reg(Register::A0), 5);
assert_eq!(runner.cpu.read_reg(Register::A7), sp);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 26);
}
#[test]
fn spin_fastfwd_template_a_advances_ticks_and_skips_loop() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
runner.bus.write_word(base, 0x594F);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0x201F);
runner.bus.write_word(base + 6, 0x5380);
runner.bus.write_word(base + 8, 0xB680);
runner.bus.write_word(base + 10, 0x62F4);
runner.bus.write_word(base + 12, 0x4E71);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::D3, 500);
runner.cpu.write_reg(Register::A7, 0x0010_0000);
let pc_after_trap = base + 4;
let mut count = 0usize;
let hit_cap = runner.try_tickcount_spin_fastfwd(pc_after_trap, None, &mut count);
assert!(!hit_cap, "no tick_cap was set, cap should not trip");
assert_eq!(runner.dispatcher.tick_count, 501, "advanced to D3+imm");
assert_eq!(runner.bus.read_long(0x016A), 501, "bus $016A in sync");
assert_eq!(runner.cpu.read_reg(Register::D0), 500);
assert_eq!(runner.cpu.read_reg(Register::A7), 0x0010_0004);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 12);
assert_eq!(count, 4);
}
#[test]
fn spin_fastfwd_rejects_register_mismatch() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
runner.bus.write_word(base, 0x594F);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0x221F); runner.bus.write_word(base + 6, 0x5380); runner.bus.write_word(base + 8, 0xB680); runner.bus.write_word(base + 10, 0x62F4);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::D3, 500);
runner.cpu.write_reg(Register::A7, 0x0010_0000);
let pc_after_trap = base + 4;
let mut count = 0usize;
runner.try_tickcount_spin_fastfwd(pc_after_trap, None, &mut count);
assert_eq!(runner.dispatcher.tick_count, 100);
assert_eq!(runner.cpu.read_reg(Register::PC), 0);
assert_eq!(count, 0);
}
#[test]
fn spin_fastfwd_template_b_memory_target_variant() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
runner.bus.write_word(base, 0x594F);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0x201F);
runner.bus.write_word(base + 6, 0xB0AE);
runner.bus.write_word(base + 8, 0xFFFC);
runner.bus.write_word(base + 10, 0x63F4);
runner.bus.write_word(base + 12, 0x4E71);
let a6 = 0x0010_1000u32;
runner.bus.write_long(a6.wrapping_sub(4), 400);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::A6, a6);
runner.cpu.write_reg(Register::A7, 0x0010_0000);
let pc_after_trap = base + 4;
let mut count = 0usize;
let hit_cap = runner.try_tickcount_spin_fastfwd(pc_after_trap, None, &mut count);
assert!(!hit_cap);
assert_eq!(runner.dispatcher.tick_count, 401);
assert_eq!(runner.bus.read_long(0x016A), 401);
assert_eq!(runner.cpu.read_reg(Register::D0), 401);
assert_eq!(runner.cpu.read_reg(Register::A7), 0x0010_0004);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 12);
assert_eq!(count, 3);
}
#[test]
fn spin_fastfwd_template_b_rejects_stateful_pretrap_body() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let counter = 0x0001_8000u32;
let a6 = 0x0001_9000u32;
let sp = 0x0010_0000u32;
runner.bus.write_word(base, 0x52B0);
runner.bus.write_word(base + 2, 0x3C00);
runner.bus.write_word(base + 4, 0x594F);
runner.bus.write_word(base + 6, 0xA975);
runner.bus.write_word(base + 8, 0x201F);
runner.bus.write_word(base + 10, 0xB0AE);
runner.bus.write_word(base + 12, 0xFFFC);
runner.bus.write_word(base + 14, 0x63F0);
runner.bus.write_word(base + 16, 0x4E71);
runner.bus.write_long(counter, 7);
runner.bus.write_long(a6 - 4, 400);
runner.bus.write_long(0x016A, 100);
runner.bus.write_long(sp, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::A0, counter);
runner.cpu.write_reg(Register::D3, 0);
runner.cpu.write_reg(Register::A6, a6);
runner.cpu.write_reg(Register::A7, sp);
let mut count = 0usize;
let hit_cap = runner.try_tickcount_spin_fastfwd(base + 8, None, &mut count);
assert!(!hit_cap);
assert_eq!(runner.guest_tick(), 100);
assert_eq!(runner.bus.read_long(counter), 7);
assert_eq!(runner.cpu.read_reg(Register::PC), 0);
assert_eq!(runner.cpu.read_reg(Register::A7), sp);
assert_eq!(count, 0);
}
#[test]
fn spin_fastfwd_template_b_signed_ble_variant() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let a5 = 0x0001_8000u32;
let sp = 0x0010_0000u32;
runner.bus.write_word(base, 0x594F);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0x201F);
runner.bus.write_word(base + 6, 0xB0AD);
runner.bus.write_word(base + 8, 0xFFF0);
runner.bus.write_word(base + 10, 0x6FF4);
runner.bus.write_word(base + 12, 0x4E71);
runner.bus.write_long(a5 - 16, 400);
runner.bus.write_long(0x016A, 100);
runner.bus.write_long(sp, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::A5, a5);
runner.cpu.write_reg(Register::A7, sp);
let mut count = 0usize;
let hit_cap = runner.try_tickcount_spin_fastfwd(base + 4, None, &mut count);
assert!(!hit_cap);
assert_eq!(runner.guest_tick(), 401);
assert_eq!(runner.cpu.read_reg(Register::D0), 401);
assert_eq!(runner.cpu.read_reg(Register::A7), sp + 4);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 12);
assert_eq!(count, 3);
}
#[test]
fn spin_fastfwd_template_b_signed_blt_variant() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let a5 = 0x0001_8000u32;
let sp = 0x0010_0000u32;
runner.bus.write_word(base, 0x594F);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0x201F);
runner.bus.write_word(base + 6, 0xB0AD);
runner.bus.write_word(base + 8, 0xFFF0);
runner.bus.write_word(base + 10, 0x6DF4);
runner.bus.write_word(base + 12, 0x4E71);
runner.bus.write_long(a5 - 16, 400);
runner.bus.write_long(0x016A, 100);
runner.bus.write_long(sp, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::A5, a5);
runner.cpu.write_reg(Register::A7, sp);
let mut count = 0usize;
let hit_cap = runner.try_tickcount_spin_fastfwd(base + 4, None, &mut count);
assert!(!hit_cap);
assert_eq!(runner.guest_tick(), 400);
assert_eq!(runner.cpu.read_reg(Register::D0), 400);
assert_eq!(runner.cpu.read_reg(Register::A7), sp + 4);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 12);
assert_eq!(count, 3);
}
#[test]
fn spin_fastfwd_template_b_signed_ble_rejects_overflow_target() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let a5 = 0x0001_8000u32;
let sp = 0x0010_0000u32;
runner.bus.write_word(base, 0x594F);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0x201F);
runner.bus.write_word(base + 6, 0xB0AD);
runner.bus.write_word(base + 8, 0xFFF0);
runner.bus.write_word(base + 10, 0x6FF4);
runner.bus.write_long(a5 - 16, i32::MAX as u32);
runner.bus.write_long(0x016A, 100);
runner.bus.write_long(sp, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::A5, a5);
runner.cpu.write_reg(Register::A7, sp);
let mut count = 0usize;
runner.try_tickcount_spin_fastfwd(base + 4, None, &mut count);
assert_eq!(runner.guest_tick(), 100);
assert_eq!(runner.cpu.read_reg(Register::PC), 0);
assert_eq!(runner.cpu.read_reg(Register::A7), sp);
assert_eq!(count, 0);
}
#[test]
fn spin_fastfwd_template_c_absolute_long_target_variant() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let target_addr = 0x0002_FE44u32;
runner.bus.write_word(base, 0x594F);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0x201F);
runner.bus.write_word(base + 6, 0xB0B9);
runner.bus.write_long(base + 8, target_addr);
runner.bus.write_word(base + 12, 0x65F2);
runner.bus.write_word(base + 14, 0x4E71);
runner.bus.write_long(target_addr, 400);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::A7, 0x0010_0000);
let pc_after_trap = base + 4;
let mut count = 0usize;
let hit_cap = runner.try_tickcount_spin_fastfwd(pc_after_trap, None, &mut count);
assert!(!hit_cap);
assert_eq!(runner.dispatcher.tick_count, 400);
assert_eq!(runner.bus.read_long(0x016A), 400);
assert_eq!(runner.cpu.read_reg(Register::D0), 400);
assert_eq!(runner.cpu.read_reg(Register::A7), 0x0010_0004);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 14);
assert_eq!(count, 3);
}
#[test]
fn spin_fastfwd_template_e_computed_signed_deadline_variant() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let a5 = 0x0001_8000u32;
let a6 = 0x0010_1000u32;
let sp = 0x0010_0000u32;
runner.bus.write_word(base, 0x594F);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0x302E);
runner.bus.write_word(base + 6, 0xFFFE);
runner.bus.write_word(base + 8, 0x48C0);
runner.bus.write_word(base + 10, 0xD0AD);
runner.bus.write_word(base + 12, 0xFFF0);
runner.bus.write_word(base + 14, 0xB09F);
runner.bus.write_word(base + 16, 0x6EEE);
runner.bus.write_word(base + 18, 0x4E71);
runner.bus.write_word(a6 - 2, 5);
runner.bus.write_long(a5 - 16, 400);
runner.bus.write_long(0x016A, 100);
runner.bus.write_long(sp - 4, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::A5, a5);
runner.cpu.write_reg(Register::A6, a6);
runner.cpu.write_reg(Register::A7, sp - 4);
runner.cpu.write_reg(Register::PC, base + 4);
let mut count = 0usize;
let hit_cap = runner.try_tickcount_spin_fastfwd(base + 4, None, &mut count);
assert!(!hit_cap);
assert_eq!(runner.guest_tick(), 405);
assert_eq!(runner.bus.read_long(sp - 4), 405);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 4);
assert_eq!(runner.cpu.read_reg(Register::A7), sp - 4);
assert_eq!(count, 0, "post-trap body remains for exact CPU execution");
for _ in 0..5 {
assert!(matches!(
runner.cpu.step(&mut runner.bus),
crate::cpu::StepResult::Ok
));
}
assert_eq!(runner.cpu.read_reg(Register::D0), 405);
assert_eq!(runner.cpu.read_reg(Register::A7), sp);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 18);
}
#[test]
fn spin_fastfwd_template_e_computed_signed_deadline_inclusive_variant() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let a5 = 0x0001_8000u32;
let a6 = 0x0010_1000u32;
let sp = 0x0010_0000u32;
runner.bus.write_word(base, 0x594F);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0x302E);
runner.bus.write_word(base + 6, 0xFFFE);
runner.bus.write_word(base + 8, 0x48C0);
runner.bus.write_word(base + 10, 0xD0AD);
runner.bus.write_word(base + 12, 0xFFF0);
runner.bus.write_word(base + 14, 0xB09F);
runner.bus.write_word(base + 16, 0x6CEE);
runner.bus.write_word(base + 18, 0x4E71);
runner.bus.write_word(a6 - 2, 5);
runner.bus.write_long(a5 - 16, 400);
runner.bus.write_long(0x016A, 100);
runner.bus.write_long(sp - 4, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::A5, a5);
runner.cpu.write_reg(Register::A6, a6);
runner.cpu.write_reg(Register::A7, sp - 4);
runner.cpu.write_reg(Register::PC, base + 4);
let mut count = 0usize;
let hit_cap = runner.try_tickcount_spin_fastfwd(base + 4, None, &mut count);
assert!(!hit_cap);
assert_eq!(runner.guest_tick(), 406);
assert_eq!(runner.bus.read_long(sp - 4), 406);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 4);
assert_eq!(runner.cpu.read_reg(Register::A7), sp - 4);
assert_eq!(count, 0, "post-trap body remains for exact CPU execution");
for _ in 0..5 {
assert!(matches!(
runner.cpu.step(&mut runner.bus),
crate::cpu::StepResult::Ok
));
}
assert_eq!(runner.cpu.read_reg(Register::D0), 405);
assert_eq!(runner.cpu.read_reg(Register::A7), sp);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 18);
}
#[test]
fn spin_fastfwd_template_e_rejects_inclusive_signed_overflow() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let a5 = 0x0001_8000u32;
let a6 = 0x0010_1000u32;
let sp = 0x0010_0000u32;
runner.bus.write_word(base, 0x594F);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0x302E);
runner.bus.write_word(base + 6, 0xFFFE);
runner.bus.write_word(base + 8, 0x48C0);
runner.bus.write_word(base + 10, 0xD0AD);
runner.bus.write_word(base + 12, 0xFFF0);
runner.bus.write_word(base + 14, 0xB09F);
runner.bus.write_word(base + 16, 0x6CEE);
runner.bus.write_word(a6 - 2, 0);
runner.bus.write_long(a5 - 16, i32::MAX as u32);
runner.bus.write_long(0x016A, 100);
runner.bus.write_long(sp - 4, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::A5, a5);
runner.cpu.write_reg(Register::A6, a6);
runner.cpu.write_reg(Register::A7, sp - 4);
let mut count = 0usize;
let hit_cap = runner.try_tickcount_spin_fastfwd(base + 4, None, &mut count);
assert!(!hit_cap);
assert_eq!(runner.guest_tick(), 100);
assert_eq!(runner.bus.read_long(sp - 4), 100);
assert_eq!(count, 0);
}
#[test]
fn spin_fastfwd_template_e_rejects_mismatched_extension_register() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
runner.bus.write_word(base, 0x594F);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0x302E); runner.bus.write_word(base + 6, 0xFFFE);
runner.bus.write_word(base + 8, 0x48C1); runner.bus.write_word(base + 10, 0xD0AD);
runner.bus.write_word(base + 12, 0xFFF0);
runner.bus.write_word(base + 14, 0xB09F);
runner.bus.write_word(base + 16, 0x6EEE);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
let mut count = 0usize;
runner.try_tickcount_spin_fastfwd(base + 4, None, &mut count);
assert_eq!(runner.guest_tick(), 100);
assert_eq!(count, 0);
}
#[test]
fn spin_fastfwd_template_d_bcc_stack_compare_variant() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let sp = 0x0010_0000u32;
runner.bus.write_word(base, 0x42A7);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0xBE9F);
runner.bus.write_word(base + 6, 0x64F8);
runner.bus.write_word(base + 8, 0x4E71);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::D7, 500);
runner.cpu.write_reg(Register::A7, sp - 4);
runner.cpu.write_reg(Register::PC, base + 4);
runner.bus.write_long(sp - 4, 100);
let mut count = 0usize;
let hit_cap = runner.try_tickcount_spin_fastfwd(base + 4, None, &mut count);
assert!(!hit_cap);
assert_eq!(runner.guest_tick(), 501);
assert_eq!(runner.bus.read_long(sp - 4), 501);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 4);
assert_eq!(runner.cpu.read_reg(Register::A7), sp - 4);
assert_eq!(count, 0, "CMP/BCC remain for exact CPU execution");
assert!(matches!(
runner.cpu.step(&mut runner.bus),
crate::cpu::StepResult::Ok
));
assert!(matches!(
runner.cpu.step(&mut runner.bus),
crate::cpu::StepResult::Ok
));
assert_eq!(runner.cpu.read_reg(Register::PC), base + 8);
assert_eq!(runner.cpu.read_reg(Register::A7), sp);
}
#[test]
fn spin_fastfwd_template_d_lemmings_beq_variant() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let sp = 0x0010_0000u32;
runner.bus.write_word(base, 0x42A7);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0xBE9F);
runner.bus.write_word(base + 6, 0x67F8);
runner.bus.write_word(base + 8, 0x4E71);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::D7, 100);
runner.cpu.write_reg(Register::A7, sp - 4);
runner.cpu.write_reg(Register::PC, base + 4);
runner.bus.write_long(sp - 4, 100);
let mut count = 0usize;
let hit_cap = runner.try_tickcount_spin_fastfwd(base + 4, None, &mut count);
assert!(!hit_cap);
assert_eq!(runner.guest_tick(), 101);
assert_eq!(runner.bus.read_long(sp - 4), 101);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 4);
assert_eq!(runner.cpu.read_reg(Register::A7), sp - 4);
assert_eq!(count, 0, "CMP/BEQ remain for exact CPU execution");
assert!(matches!(
runner.cpu.step(&mut runner.bus),
crate::cpu::StepResult::Ok
));
assert!(matches!(
runner.cpu.step(&mut runner.bus),
crate::cpu::StepResult::Ok
));
assert_eq!(runner.cpu.read_reg(Register::PC), base + 8);
assert_eq!(runner.cpu.read_reg(Register::A7), sp);
}
#[test]
fn spin_fastfwd_template_d_beq_does_not_advance_after_tick_changed() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let sp = 0x0010_0000u32;
runner.bus.write_word(base, 0x42A7);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0xBE9F);
runner.bus.write_word(base + 6, 0x67F8);
runner.bus.write_long(0x016A, 101);
runner.dispatcher.tick_count = 101;
runner.cpu.write_reg(Register::D7, 100);
runner.cpu.write_reg(Register::A7, sp - 4);
runner.cpu.write_reg(Register::PC, base + 4);
runner.bus.write_long(sp - 4, 101);
let mut count = 0usize;
let hit_cap = runner.try_tickcount_spin_fastfwd(base + 4, None, &mut count);
assert!(!hit_cap);
assert_eq!(runner.guest_tick(), 101);
assert_eq!(runner.bus.read_long(sp - 4), 101);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 4);
assert_eq!(runner.cpu.read_reg(Register::A7), sp - 4);
assert_eq!(count, 0);
}
#[test]
fn spin_fastfwd_template_d_honors_gui_tick_cap() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let sp = 0x0010_0000u32;
runner.bus.write_word(base, 0x42A7);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0xBE9F);
runner.bus.write_word(base + 6, 0x64F8);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::D7, 500);
runner.cpu.write_reg(Register::A7, sp - 4);
runner.cpu.write_reg(Register::PC, base + 4);
runner.bus.write_long(sp - 4, 100);
let mut count = 0usize;
let hit_cap = runner.try_tickcount_spin_fastfwd(base + 4, Some(102), &mut count);
assert!(hit_cap);
assert_eq!(runner.guest_tick(), 102);
assert_eq!(runner.bus.read_long(sp - 4), 102);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 4);
assert_eq!(runner.cpu.read_reg(Register::A7), sp - 4);
}
#[test]
fn spin_fastfwd_leaves_interrupt_callback_state_unsynthesized() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let target_addr = 0x0002_FE44u32;
let task_ptr = 0x0020_2000u32;
let sp = 0x0010_0000u32;
runner.bus.write_word(base, 0x594F);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0x201F);
runner.bus.write_word(base + 6, 0xB0B9);
runner.bus.write_long(base + 8, target_addr);
runner.bus.write_word(base + 12, 0x65F2);
runner.bus.write_word(base + 14, 0x4E71);
runner.bus.write_long(target_addr, 400);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::PC, base + 4);
runner.cpu.write_reg(Register::A7, sp);
runner.cpu.write_reg(Register::D0, 0xDEAD_BEEF);
runner.cpu.core.set_sr_noint_nosp(0x2000);
runner.bus.write_long(sp, 100);
runner.bus.write_word(task_ptr + 4, 1);
runner.bus.write_long(task_ptr + 6, 0x0004_1234);
runner.bus.write_word(task_ptr + 10, 1);
runner.bus.write_word(task_ptr + 12, 0);
runner.dispatcher.vbl_tasks.push(VblTask {
task_ptr,
slot: None,
pending: false,
});
let mut count = 0usize;
let hit_cap = runner.try_tickcount_spin_fastfwd(base + 4, None, &mut count);
assert!(!hit_cap);
assert_eq!(runner.dispatcher.tick_count, 101);
assert_eq!(runner.bus.read_long(0x016A), 101);
assert_eq!(count, 0);
assert_eq!(runner.cpu.read_reg(Register::D0), 0xDEAD_BEEF);
assert_eq!(runner.cpu.read_reg(Register::PC), runner.vbl_trampoline);
assert_eq!(runner.cpu.read_reg(Register::A7), sp - 4);
assert_eq!(runner.bus.read_long(sp - 4), base + 4);
let active = runner
.active_interrupt_callback
.expect("VBL callback should remain active for normal resume handling");
assert!(matches!(active.source, ActiveInterruptCallbackSource::Vbl));
assert_eq!(active.resume_pc, base + 4);
assert_eq!(active.resume_sp, sp);
}
#[test]
fn spin_fastfwd_gate_defaults_on_with_gui_cadence_guarded_by_tick_cap() {
assert!(spin_wait_fastfwd_gate(false, false, false, false));
assert!(spin_wait_fastfwd_gate(false, false, false, true));
assert!(spin_wait_fastfwd_gate(false, false, true, true));
assert!(!spin_wait_fastfwd_gate(false, false, true, false));
}
#[test]
fn spin_fastfwd_gate_force_off_wins() {
assert!(!spin_wait_fastfwd_gate(false, true, false, false));
assert!(!spin_wait_fastfwd_gate(false, true, true, true));
assert!(!spin_wait_fastfwd_gate(true, true, false, true));
assert!(!spin_wait_fastfwd_gate(true, true, true, false));
}
#[test]
fn spin_fastfwd_gate_force_on_remains_enabled() {
assert!(spin_wait_fastfwd_gate(true, false, false, false));
assert!(spin_wait_fastfwd_gate(true, false, true, false));
}
#[test]
fn modaldialog_refire_skip_gui_mode_never_fires() {
for has_tracking in [false, true] {
for events in [false, true] {
assert!(
!modaldialog_refire_is_noop(
true, has_tracking,
true, true,
true,
true,
events,
),
"GUI mode must never skip refires (has_tracking={}, events={})",
has_tracking,
events
);
}
}
}
#[test]
fn tracking_refire_freeze_policy_keeps_modaldialog_ticks_live() {
assert!(tracking_refire_should_freeze_ticks(0xA93D));
assert!(tracking_refire_should_freeze_ticks(0xAD3D));
assert!(tracking_refire_should_freeze_ticks(0xA80B));
assert!(tracking_refire_should_freeze_ticks(0xAC0B));
assert!(tracking_refire_should_freeze_ticks(0xA968));
assert!(tracking_refire_should_freeze_ticks(0xAD68));
assert!(tracking_refire_should_freeze_ticks(0xA91E));
assert!(tracking_refire_should_freeze_ticks(0xAD1E));
assert!(tracking_refire_should_freeze_ticks(0xA925));
assert!(tracking_refire_should_freeze_ticks(0xAD25));
assert!(tracking_refire_should_freeze_ticks(0xA905));
assert!(tracking_refire_should_freeze_ticks(0xAD05));
assert!(tracking_refire_should_freeze_ticks(0xA926));
assert!(tracking_refire_should_freeze_ticks(0xAD26));
assert!(!tracking_refire_should_freeze_ticks(0xA991));
assert!(!tracking_refire_should_freeze_ticks(0xAD91));
assert!(tracking_refire_uses_dialog_callbacks(0xA991));
assert!(!tracking_refire_uses_dialog_callbacks(0xA9EA));
assert!(tracking_refire_advances_gui_idle_tick(0xA991));
assert!(tracking_refire_advances_gui_idle_tick(0xA9EA));
}
#[test]
fn trackcontrol_refire_allows_guest_scrollbar_callback_to_execute() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let trap_pc = 0x0001_0000u32;
let sp = 0x0010_0000u32;
let marker = runner.bus.alloc(2);
let action_proc = runner.bus.alloc(12);
let ctrl_ptr = runner.bus.alloc(40);
let ctrl_handle = runner.bus.alloc(4);
runner.bus.write_word(trap_pc, 0xA968); runner.bus.write_word(action_proc, 0x33FC); runner.bus.write_word(action_proc + 2, 0x7A5A);
runner.bus.write_long(action_proc + 4, marker);
runner.bus.write_word(action_proc + 8, 0x4E74); runner.bus.write_word(action_proc + 10, 6);
runner.bus.write_long(ctrl_handle, ctrl_ptr);
runner.bus.write_word(ctrl_ptr + 8, 60);
runner.bus.write_word(ctrl_ptr + 10, 240);
runner.bus.write_word(ctrl_ptr + 12, 220);
runner.bus.write_word(ctrl_ptr + 14, 256);
runner.bus.write_byte(ctrl_ptr + 16, 0xFF);
runner.bus.write_word(ctrl_ptr + 18, 40);
runner.bus.write_word(ctrl_ptr + 20, 0);
runner.bus.write_word(ctrl_ptr + 22, 100);
runner.dispatcher.control_proc_ids.insert(ctrl_ptr, 16);
runner.dispatcher.mouse_button = true;
runner.dispatcher.mouse_pos = (210, 248);
runner.bus.write_long(sp, action_proc);
runner.bus.write_word(sp + 4, 210);
runner.bus.write_word(sp + 6, 248);
runner.bus.write_long(sp + 8, ctrl_handle);
runner.bus.write_word(sp + 12, 0xBEEF);
runner.cpu.write_reg(Register::PC, trap_pc);
runner.cpu.write_reg(Register::A7, sp);
let (_steps, running) = runner.run_steps(20, None);
assert!(running);
assert_eq!(runner.bus.read_word(marker), 0x7A5A);
assert!(runner.dispatcher.is_control_tracking());
assert!(!runner.dispatcher.is_control_action_callback_pending());
}
#[test]
fn standard_file_refires_yield_before_unrelated_modeless_callbacks() {
for (selector, pop_total) in [(0x0002u16, 28u32), (0x0006, 16)] {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let sp = 0x0010_0000u32;
let reply_ptr = runner.bus.alloc(80);
let proc_addr = 0x0001_1000u32;
runner.bus.write_word(base, 0xA9EA); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, sp);
runner.bus.write_word(sp, selector);
runner.bus.write_long(sp + 2, reply_ptr);
if selector == 0x0002 {
runner.bus.write_long(sp + 10, 0); runner.bus.write_word(sp + 14, 0); } else {
runner.bus.write_long(sp + 6, 0); runner.bus.write_word(sp + 10, 0); }
runner.bus.write_word(proc_addr, 0x4E56); runner
.dispatcher
.modeless_dialog_draw_proc_queue
.push_back((0, proc_addr, 1));
let (steps, running) = runner.run_gui_slice_with_audio(1, 0, 0);
assert!(running);
assert_eq!(steps, 1);
assert!(runner.dispatcher.is_standard_file_get_tracking());
assert_eq!(runner.cpu.read_reg(Register::PC), base);
assert_eq!(runner.cpu.read_reg(Register::A7), sp);
assert!(runner.active_interrupt_callback.is_none());
assert_eq!(
runner.dispatcher.modeless_dialog_draw_proc_queue.len(),
1,
"selector ${selector:04X} must not consume another manager's callback"
);
let (idle_steps, idle_running) = runner.run_gui_slice_with_audio(16, 3, 0);
assert!(idle_running);
assert_eq!(idle_steps, 3);
assert_eq!(runner.guest_tick(), 3);
assert_eq!(runner.cpu.read_reg(Register::PC), base);
runner.dispatcher.modeless_dialog_draw_proc_queue.clear();
runner.dispatcher.event_queue.push_back(QueuedEvent {
what: 3,
message: 0x0000_351B, where_v: 0,
where_h: 0,
modifiers: 0,
});
let (cancel_steps, cancel_running) = runner.run_gui_slice_with_audio(1, 3, 0);
assert!(cancel_running);
assert_eq!(cancel_steps, 1);
assert!(!runner.dispatcher.is_standard_file_get_tracking());
assert_eq!(runner.cpu.read_reg(Register::PC), base + 2);
assert_eq!(runner.cpu.read_reg(Register::A7), sp + pop_total);
assert_eq!(runner.bus.read_byte(reply_ptr), 0);
}
}
#[test]
fn modal_dialog_refire_still_schedules_its_draw_callback() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let sp = 0x0010_0000u32;
let proc_addr = 0x0001_1000u32;
runner.bus.write_word(base, 0xA991); runner.bus.write_word(proc_addr, 0x4E56); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, sp);
let mut tracking = dialog_tracking_for_test(0, 0);
tracking.draw_proc_queue.push_back((proc_addr, 1));
tracking.draw_procs_done = false;
tracking.rendered_pixels_final = false;
runner.dispatcher.dialog_tracking = Some(tracking);
let (steps, running) = runner.run_gui_slice_with_audio(1, 0, 0);
assert!(running);
assert_eq!(steps, 1);
assert!(matches!(
runner.active_interrupt_callback,
Some(ActiveInterruptCallback {
source: ActiveInterruptCallbackSource::DialogDrawProc,
..
})
));
assert_ne!(runner.cpu.read_reg(Register::PC), base);
}
#[test]
fn tracking_refire_survives_async_callback_injection() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let sp = 0x0010_0000u32;
runner.bus.write_word(base, 0xA991); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, sp);
let mut tracking = dialog_tracking_for_test(0, 0);
tracking.flash_remaining = 6;
tracking.flash_delay = 3;
runner.dispatcher.dialog_tracking = Some(tracking);
runner.active_interrupt_callback = Some(ActiveInterruptCallback {
source: ActiveInterruptCallbackSource::Timer,
resume_pc: base + 2,
resume_sp: sp,
d_regs: [0; 8],
a_regs: [0, 0, 0, 0, 0, 0, 0, sp],
sr: 0x2000,
ccr: 0,
restore_port: None,
});
let (steps, running) = runner.run_steps(2, None);
assert!(running);
assert_eq!(steps, 2);
assert!(
runner.active_interrupt_callback.is_none(),
"active={:?} deferred={:?} pc=${:08X} sp=${:08X}",
runner.active_interrupt_callback.map(|active| (
active.source,
active.resume_pc,
active.resume_sp
)),
runner.deferred_tracking_refire_pc,
runner.cpu.read_reg(Register::PC),
runner.cpu.read_reg(Register::A7),
);
assert!(runner.deferred_tracking_refire_pc.is_none());
assert_eq!(runner.cpu.read_reg(Register::PC), base);
}
#[test]
fn gui_modaldialog_idle_refire_advances_one_tick() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
runner.bus.write_word(base, 0xA991); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.bus.write_long(0x016A, 0);
runner.dispatcher.tick_count = 0;
runner.set_instructions_per_tick(1_000_000);
runner.dispatcher.dialog_tracking = Some(dialog_tracking_for_test(0, 0));
let (steps, running) = runner.run_gui_slice_with_audio(1, 1, 0);
assert!(running);
assert_eq!(steps, 1);
assert_eq!(runner.guest_tick(), 1);
assert_eq!(runner.tick_budget, runner.instructions_per_tick() as i32);
assert_eq!(runner.cpu.read_reg(Register::PC), base);
}
#[test]
fn gui_modaldialog_idle_refire_runs_until_tick_cap() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
runner.bus.write_word(base, 0xA991); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.bus.write_long(0x016A, 0);
runner.dispatcher.tick_count = 0;
runner.set_instructions_per_tick(1_000_000);
runner.dispatcher.dialog_tracking = Some(dialog_tracking_for_test(0, 0));
let (steps, running) = runner.run_gui_slice_with_audio(16, 2, 0);
assert!(running);
assert_eq!(steps, 2);
assert_eq!(runner.guest_tick(), 2);
assert_eq!(runner.cpu.read_reg(Register::PC), base);
}
#[test]
fn gui_modaldialog_refire_unfreezes_prior_control_tracking() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
runner.bus.write_word(base, 0xA991); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.bus.write_long(0x016A, 182);
runner.dispatcher.tick_count = 182;
runner.frozen_ticks = Some(182);
runner.set_instructions_per_tick(1_000_000);
runner.dispatcher.dialog_tracking = Some(dialog_tracking_for_test(0, 0));
let (steps, running) = runner.run_gui_slice_with_audio(16, 184, 0);
assert!(running);
assert_eq!(steps, 1);
assert_eq!(runner.frozen_ticks, None);
assert_eq!(runner.guest_tick(), 184);
assert_eq!(runner.dispatcher.tick_count, 184);
}
#[test]
fn gui_modaldialog_null_filter_fires_at_tick_cap() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let filter_proc = 0x0001_1000u32;
runner.bus.write_word(base, 0xA991); runner.bus.write_word(filter_proc, 0x4E56); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.bus.write_long(0x016A, 0);
runner.dispatcher.tick_count = 0;
runner.dispatcher.dialog_tracking =
Some(dialog_tracking_for_test(filter_proc, 0x0010_0100));
let (steps, running) = runner.run_gui_slice_with_audio(1, 0, 0);
assert!(running);
assert_eq!(steps, 1);
assert_eq!(runner.guest_tick(), 0);
assert_ne!(runner.cpu.read_reg(Register::PC), base);
assert!(!runner.has_pending_sound_work());
assert!(
!runner
.dispatcher
.dialog_tracking
.as_ref()
.unwrap()
.rendered_pixels_final
);
}
#[test]
fn gui_modaldialog_update_filter_fires_at_tick_cap() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let filter_proc = 0x0001_1000u32;
runner.bus.write_word(base, 0xA991); runner.bus.write_word(filter_proc, 0x4E56); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.bus.write_long(0x016A, 0);
runner.dispatcher.tick_count = 0;
runner.dispatcher.event_queue.push_back(QueuedEvent {
what: 6,
message: 0,
where_v: 0,
where_h: 0,
modifiers: 0,
});
runner.dispatcher.dialog_tracking =
Some(dialog_tracking_for_test(filter_proc, 0x0010_0100));
let (steps, running) = runner.run_gui_slice_with_audio(1, 0, 0);
assert!(running);
assert_eq!(steps, 1);
assert_eq!(runner.guest_tick(), 0);
assert_ne!(runner.cpu.read_reg(Register::PC), base);
assert!(
!runner
.dispatcher
.dialog_tracking
.as_ref()
.unwrap()
.rendered_pixels_final
);
}
#[test]
fn gui_modaldialog_mouse_down_goes_to_filter_before_default_handling() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let filter_proc = 0x0001_1000u32;
runner.bus.write_word(base, 0xA991); runner.bus.write_word(filter_proc, 0x4E56); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.bus.write_long(0x016A, 0);
runner.dispatcher.tick_count = 0;
runner.dispatcher.event_queue.push_back(QueuedEvent {
what: 1,
message: 0,
where_v: 12,
where_h: 24,
modifiers: 0,
});
let mut tracking = dialog_tracking_for_test(filter_proc, 0x0010_0100);
tracking.items.push(DialogItem {
item_type: 4,
rect: (8, 16, 20, 30),
text: String::from("OK"),
resource_id: 0,
proc_ptr: 0,
sel_start: 0,
sel_end: 0,
});
runner.dispatcher.dialog_tracking = Some(tracking);
let (steps, running) = runner.run_gui_slice_with_audio(1, 0, 0);
assert!(running);
assert_eq!(steps, 1);
assert_ne!(runner.cpu.read_reg(Register::PC), base);
let event_ptr = runner.dialog_filter_event;
assert_eq!(runner.bus.read_word(event_ptr), 1);
assert_eq!(runner.bus.read_word(event_ptr + 10), 12);
assert_eq!(runner.bus.read_word(event_ptr + 12), 24);
assert!(
runner.dispatcher.event_queue.is_empty(),
"the filter callback should consume a queued button mouseDown event"
);
}
#[test]
fn modaldialog_filter_null_event_is_paced_per_guest_tick() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let filter_proc = 0x0001_1000u32;
let dialog_ptr = 0x0020_0000u32;
runner.dispatcher.dialog_tracking =
Some(dialog_tracking_for_test(filter_proc, 0x0010_0100));
runner.dispatcher.tick_count = 42;
runner.bus.write_long(0x016A, 42);
assert!(runner.should_fire_dialog_filter_proc());
runner.dialog_filter_last_null_event_tick = Some((dialog_ptr, 42));
assert!(
!runner.should_fire_dialog_filter_proc(),
"a synthetic null event should not refire twice in the same guest tick"
);
runner.dispatcher.tick_count = 43;
runner.bus.write_long(0x016A, 43);
assert!(
runner.should_fire_dialog_filter_proc(),
"the next guest tick should allow another ModalDialog null-event filter call"
);
}
#[test]
fn modaldialog_filter_real_events_bypass_null_event_pacing() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let filter_proc = 0x0001_1000u32;
let dialog_ptr = 0x0020_0000u32;
runner.dispatcher.dialog_tracking =
Some(dialog_tracking_for_test(filter_proc, 0x0010_0100));
runner.dispatcher.tick_count = 42;
runner.bus.write_long(0x016A, 42);
runner.dialog_filter_last_null_event_tick = Some((dialog_ptr, 42));
runner.dispatcher.event_queue.push_back(QueuedEvent {
what: 1,
message: 0,
where_v: 12,
where_h: 34,
modifiers: 0,
});
assert!(
runner.should_fire_dialog_filter_proc(),
"mouse/key/update events must still enter the filter immediately"
);
runner.dispatcher.event_queue.clear();
let window_ptr = runner.bus.alloc(170);
runner.dispatcher.init_cgraf_window(
&mut runner.bus,
&mut runner.cpu,
window_ptr,
0,
100,
120,
220,
360,
"",
2,
true,
false,
false,
0,
);
runner
.dispatcher
.dialog_tracking
.as_mut()
.unwrap()
.dialog_ptr = window_ptr;
runner.dialog_filter_last_null_event_tick = Some((window_ptr, 42));
assert!(
runner.should_fire_dialog_filter_proc(),
"a pending updateEvt for the active dialog must bypass null-event pacing"
);
}
#[test]
fn modaldialog_refire_skip_headless_requires_all_conditions() {
assert!(modaldialog_refire_is_noop(
false, true, true, true, true, true, true,
));
assert!(!modaldialog_refire_is_noop(
false, false, true, true, true, true, true,
));
assert!(!modaldialog_refire_is_noop(
false, true, false, true, true, true, true,
));
assert!(!modaldialog_refire_is_noop(
false, true, true, false, true, true, true,
));
assert!(!modaldialog_refire_is_noop(
false, true, true, true, false, true, true,
));
assert!(!modaldialog_refire_is_noop(
false, true, true, true, true, false, true,
));
assert!(!modaldialog_refire_is_noop(
false, true, true, true, true, true, false,
));
}
#[test]
fn spin_fastfwd_rejects_wrong_branch_target() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
runner.bus.write_word(base, 0x594F);
runner.bus.write_word(base + 2, 0xA975);
runner.bus.write_word(base + 4, 0x201F);
runner.bus.write_word(base + 6, 0x5380);
runner.bus.write_word(base + 8, 0xB680);
runner.bus.write_word(base + 10, 0x62F6);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.cpu.write_reg(Register::D3, 500);
runner.cpu.write_reg(Register::A7, 0x0010_0000);
let pc_after_trap = base + 4;
let mut count = 0usize;
runner.try_tickcount_spin_fastfwd(pc_after_trap, None, &mut count);
assert_eq!(runner.dispatcher.tick_count, 100);
assert_eq!(count, 0);
}
#[test]
fn tickcount_inline_skip_increments_inline_skipped() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
runner.bus.write_word(base, 0x594F); runner.bus.write_word(base + 2, 0xA975); runner.bus.write_word(base + 4, 0x4E71); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.dispatcher.tick_count = 0;
runner.bus.write_long(0x016A, 0);
runner.set_instructions_per_tick(1_000_000);
let idx = (0xA975u16 & 0xFFF) as usize;
let before = runner.dispatcher.inline_skipped[idx];
let (steps, running) = runner.run_steps(2, None);
assert!(running, "runner should not halt on a plain trap fast path");
assert_eq!(steps, 2);
let after = runner.dispatcher.inline_skipped[idx];
assert_eq!(
after - before,
1,
"TickCount fast path must increment inline_skipped[$0175]"
);
assert_eq!(
runner.dispatcher.trap_histogram[idx], 1,
"the same path must also increment trap_histogram[$0175]"
);
}
#[test]
fn halted_by_exit_to_shell_classifies_clean_application_quit() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
runner.bus.write_word(base, 0xA9F4); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, 0x0010_0000);
let (_steps, running) = runner.run_steps(1, None);
assert!(!running, "ExitToShell should stop the runner");
assert!(runner.is_halted());
assert_eq!(runner.halted_trap(), Some(0xA9F4));
assert!(
runner.halted_by_exit_to_shell(),
"ExitToShell halt must be classified as a clean application exit"
);
}
#[test]
fn exit_to_shell_activates_launch_target_queued_until_event_yield() {
let helper_code0 = minimal_code0(0, 0x2000, 0, 0);
let helper_fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &helper_code0)]);
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
runner
.dispatcher
.vfs
.insert("Apps/Register Helper".to_string(), Vec::new());
runner
.dispatcher
.vfs_rsrc
.insert("Apps/Register Helper".to_string(), helper_fork_bytes);
runner.dispatcher.ensure_vfs_catalog();
runner
.dispatcher
.queue_pending_launch_application("Apps/Register Helper", true);
runner.bus.write_word(base, 0xA9F4); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, 0x0010_0000);
let (_steps, running) = runner.run_steps(1, None);
assert!(
running,
"ExitToShell should activate a valid queued launch target"
);
assert!(!runner.is_halted());
assert_eq!(
runner.dispatcher.launched_app_path.as_deref(),
Some("Apps/Register Helper")
);
}
#[test]
fn halted_by_exit_to_shell_rejects_invalid_pc_halts() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.cpu.write_reg(Register::PC, runner.bus.ram_size());
runner.cpu.write_reg(Register::A7, 0x0010_0000);
let (_steps, running) = runner.run_steps(1, None);
assert!(!running, "invalid PC should stop the runner");
assert!(runner.is_halted());
assert_eq!(runner.halted_trap(), None);
assert!(
!runner.halted_by_exit_to_shell(),
"invalid-PC halts must not be reported as clean application exits"
);
}
#[test]
fn ptinrect_inline_path_matches_pascal_stack_contract() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let sp = 0x0010_0000u32;
let rect = 0x0020_0000u32;
runner.bus.write_word(base, 0xA8AD); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, sp);
runner.set_instructions_per_tick(1_000_000);
runner.bus.write_long(sp, rect);
runner.bus.write_word(sp + 4, 20); runner.bus.write_word(sp + 6, 30); runner.bus.write_word(rect, 10); runner.bus.write_word(rect + 2, 25); runner.bus.write_word(rect + 4, 40); runner.bus.write_word(rect + 6, 50);
let idx = (0xA8ADu16 & 0xFFF) as usize;
let before_inline = runner.dispatcher.inline_skipped[idx];
let before_game = runner.dispatcher.game_trap_count;
let (steps, running) = runner.run_steps(1, None);
assert!(running, "runner should not halt on PtInRect inline path");
assert_eq!(steps, 1);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 2);
assert_eq!(runner.cpu.read_reg(Register::A7), sp + 8);
assert_eq!(runner.bus.read_word(sp + 8), 0x0100);
assert_eq!(runner.dispatcher.inline_skipped[idx] - before_inline, 1);
assert_eq!(runner.dispatcher.trap_histogram[idx], 1);
assert_eq!(runner.dispatcher.game_trap_count - before_game, 1);
}
#[test]
fn eventavail_inline_path_peeks_without_dequeueing() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let sp = 0x0010_0000u32;
let event = 0x0020_0000u32;
runner.bus.write_word(base, 0xA971); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, sp);
runner.set_instructions_per_tick(1_000_000);
runner.bus.write_long(sp, event);
runner.bus.write_word(sp + 4, 0x0008); runner.dispatcher.push_key_down(0x31, b' ');
let idx = (0xA971u16 & 0xFFF) as usize;
let before_inline = runner.dispatcher.inline_skipped[idx];
let before_game = runner.dispatcher.game_trap_count;
let (steps, running) = runner.run_steps(1, None);
assert!(running, "runner should not halt on EventAvail inline path");
assert_eq!(steps, 1);
assert_eq!(runner.cpu.read_reg(Register::PC), base + 2);
assert_eq!(runner.cpu.read_reg(Register::A7), sp + 6);
assert_eq!(runner.bus.read_word(sp + 6), 0xFFFF);
assert_eq!(runner.bus.read_word(event), 3);
assert_eq!(
runner.bus.read_long(event + 2),
(0x31u32 << 8) | u32::from(b' ')
);
assert_eq!(
runner.dispatcher.event_queue.len(),
1,
"EventAvail must not dequeue the matching event"
);
assert_eq!(runner.dispatcher.inline_skipped[idx] - before_inline, 1);
assert_eq!(runner.dispatcher.trap_histogram[idx], 1);
assert_eq!(
runner.dispatcher.game_trap_count, before_game,
"EventAvail remains excluded from game_trap_count as an idle trap"
);
}
#[test]
fn modaldialog_batched_skip_increments_inline_skipped_by_batch() {
use crate::trap::dispatch::DialogTrackingState;
use std::collections::VecDeque;
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
runner.bus.write_word(base, 0xA991); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.dispatcher.tick_count = 0;
runner.bus.write_long(0x016A, 0);
runner.set_instructions_per_tick(1_000_000);
runner.dispatcher.dialog_tracking = Some(DialogTrackingState {
dialog_ptr: 0x0020_0000,
bounds: (0, 0, 32, 32),
title: String::new(),
proc_id: 1,
items: Vec::new(),
default_item: 0,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: 0,
item_hit_ptr: 0,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
let idx = (0xA991u16 & 0xFFF) as usize;
let before_inline = runner.dispatcher.inline_skipped[idx];
let before_hist = runner.dispatcher.trap_histogram[idx];
let (steps, _running) = runner.run_steps(64, None);
assert_eq!(
steps, 64,
"max_steps cap exhausted by 64 batched no-op refires"
);
let after_inline = runner.dispatcher.inline_skipped[idx];
let after_hist = runner.dispatcher.trap_histogram[idx];
assert_eq!(
after_inline - before_inline,
64,
"batched skip must increment inline_skipped[$0191] by BATCH=64"
);
assert_eq!(
after_hist - before_hist,
64,
"trap_histogram and inline_skipped must increment in lockstep on the inline path"
);
}
#[test]
fn modaldialog_batched_skip_applies_after_paced_filter_null_event() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let filter_proc = 0x0001_1000u32;
let dialog_ptr = 0x0020_0000u32;
runner.bus.write_word(base, 0xA991); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, 0x0010_0000);
runner.dispatcher.tick_count = 42;
runner.bus.write_long(0x016A, 42);
runner.set_instructions_per_tick(1_000_000);
runner.dispatcher.dialog_tracking =
Some(dialog_tracking_for_test(filter_proc, 0x0010_0100));
runner.dialog_filter_last_null_event_tick = Some((dialog_ptr, 42));
let idx = (0xA991u16 & 0xFFF) as usize;
let before_inline = runner.dispatcher.inline_skipped[idx];
let before_hist = runner.dispatcher.trap_histogram[idx];
let (steps, running) = runner.run_steps(64, None);
assert!(running);
assert_eq!(steps, 64);
assert_eq!(runner.cpu.read_reg(Register::PC), base);
assert_eq!(runner.dispatcher.inline_skipped[idx] - before_inline, 64);
assert_eq!(runner.dispatcher.trap_histogram[idx] - before_hist, 64);
}
#[test]
fn tick_progress_persists_across_multiple_run_slices() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
let program_words = 12;
for offset in (0..program_words).step_by(2) {
runner.bus.write_word(program_start + offset, 0x4E71);
}
runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 0);
runner.set_instructions_per_tick(5);
let (steps1, running1) = runner.run_steps(3, None);
let (steps2, running2) = runner.run_steps(3, None);
assert!(running1);
assert!(running2);
assert_eq!(steps1, 3);
assert_eq!(steps2, 3);
assert_eq!(runner.bus.read_long(0x016A), 1);
}
#[test]
fn tick_override_breaks_once_target_tick_is_reached() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
let program_words = 16;
for offset in (0..program_words).step_by(2) {
runner.bus.write_word(program_start + offset, 0x4E71);
}
runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 0);
runner.set_instructions_per_tick(4);
let (steps, running) = runner.run_steps_with_audio(16, Some(0), 0);
assert!(running);
assert_eq!(steps, 3);
assert_eq!(runner.bus.read_long(0x016A), 0);
}
#[test]
fn pending_wait_sleep_ticks_advance_in_headless_mode() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
runner.bus.write_word(program_start, 0x4E71);
runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 0);
runner.dispatcher.pending_wait_sleep_ticks = 3;
let (steps, running) = runner.run_steps(1, None);
assert!(running);
assert_eq!(steps, 1);
assert_eq!(runner.bus.read_long(0x016A), 3);
assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
}
#[test]
fn pending_wait_sleep_ticks_capped_to_zero_in_headless() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
runner.bus.write_word(program_start, 0x4E71);
runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 0);
runner.set_wait_sleep_cap_in_headless(Some(0));
runner.dispatcher.pending_wait_sleep_ticks = 60;
let (_steps, _running) = runner.run_steps(1, None);
assert_eq!(runner.bus.read_long(0x016A), 0);
assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
}
#[test]
fn pending_wait_sleep_ticks_capped_in_headless_when_opt_in() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
runner.bus.write_word(program_start, 0x4E71);
runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 0);
runner.set_wait_sleep_cap_in_headless(Some(1));
runner.dispatcher.pending_wait_sleep_ticks = 60;
let (steps, running) = runner.run_steps(1, None);
assert!(running);
assert_eq!(steps, 1);
assert_eq!(runner.bus.read_long(0x016A), 1);
assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
assert_eq!(runner.wait_sleep_cap_in_headless(), Some(1));
}
#[test]
fn pending_wait_sleep_ticks_suspends_foreground_until_gui_tick_cap() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
runner.bus.write_word(program_start, 0x4E71);
runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 0);
runner.dispatcher.pending_wait_sleep_ticks = 60;
let (steps, running) = runner.run_steps(1, Some(10));
assert!(running);
assert_eq!(
steps, 0,
"foreground code should not resume while WNE sleep remains pending"
);
assert_eq!(runner.bus.read_long(0x016A), 10);
assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 50);
}
#[test]
fn pending_wait_sleep_ticks_wakes_wait_next_event_with_queued_input() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
let event_ptr = 0x0020_0000;
let result_ptr = 0x0020_0020;
runner.bus.write_word(program_start, 0x4E71);
runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 0);
runner.bus.write_word(result_ptr, 0);
runner.dispatcher.sent_open_app_event = true;
runner
.dispatcher
.write_event_record(&mut runner.bus, event_ptr, 0, 0, 0, 0, 0);
runner.dispatcher.pending_wait_sleep_ticks = 60;
runner.dispatcher.pending_wait_next_event_return = Some(PendingWaitNextEventReturn {
event_ptr,
result_ptr,
event_mask: 0xFFFF,
mouse_rgn: 0,
resume_pc: None,
resume_sp: None,
});
runner.push_mouse_down(123, 456);
let (steps, running) = runner.run_steps(1, Some(10));
assert!(running);
assert_eq!(steps, 1);
assert_eq!(
runner.bus.read_word(event_ptr),
1,
"queued mouseDown should replace the pending null EventRecord"
);
assert_eq!(runner.bus.read_word(event_ptr + 10), 123u16);
assert_eq!(runner.bus.read_word(event_ptr + 12), 456u16);
assert_eq!(
runner.bus.read_word(result_ptr),
0xFFFF,
"WaitNextEvent result slot should be rewritten to TRUE"
);
assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
assert!(runner.dispatcher.pending_wait_next_event_return.is_none());
}
#[test]
fn push_mouse_down_wakes_pending_wait_next_event_immediately() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let event_ptr = 0x0020_0000;
let result_ptr = 0x0020_0020;
runner.bus.write_word(result_ptr, 0);
runner.dispatcher.sent_open_app_event = true;
runner
.dispatcher
.write_event_record(&mut runner.bus, event_ptr, 0, 0, 0, 0, 0);
runner.dispatcher.pending_wait_sleep_ticks = 60;
runner.dispatcher.pending_wait_next_event_return = Some(PendingWaitNextEventReturn {
event_ptr,
result_ptr,
event_mask: 0xFFFF,
mouse_rgn: 0,
resume_pc: None,
resume_sp: None,
});
runner.push_mouse_down(123, 456);
assert_eq!(
runner.bus.read_word(event_ptr),
1,
"input injection should wake a sleeping WaitNextEvent before the next CPU slice"
);
assert_eq!(runner.bus.read_word(event_ptr + 10), 123u16);
assert_eq!(runner.bus.read_word(event_ptr + 12), 456u16);
assert_eq!(runner.bus.read_word(result_ptr), 0xFFFF);
assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
assert!(runner.dispatcher.pending_wait_next_event_return.is_none());
}
#[test]
fn set_mouse_position_wakes_pending_wait_next_event_with_mouse_moved_region() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let event_ptr = 0x0020_0000;
let result_ptr = 0x0020_0020;
let mouse_rgn = test_region_handle(&mut runner.bus, 10, 20, 30, 40);
runner.set_mouse_position(20, 25);
runner.bus.write_word(result_ptr, 0);
runner.dispatcher.sent_open_app_event = true;
runner
.dispatcher
.write_event_record(&mut runner.bus, event_ptr, 0, 0, 0, 0, 0);
runner.dispatcher.pending_wait_sleep_ticks = 60;
runner.dispatcher.pending_wait_next_event_return = Some(PendingWaitNextEventReturn {
event_ptr,
result_ptr,
event_mask: 0x8000,
mouse_rgn,
resume_pc: None,
resume_sp: None,
});
runner.set_mouse_position(50, 25);
assert_eq!(
runner.bus.read_word(event_ptr),
15,
"mouse movement outside the pending mouseRgn should wake WaitNextEvent with osEvt"
);
assert_eq!(runner.bus.read_long(event_ptr + 2), 0xFA00_0000);
assert_eq!(runner.bus.read_word(event_ptr + 10), 50u16);
assert_eq!(runner.bus.read_word(event_ptr + 12), 25u16);
assert_eq!(runner.bus.read_word(result_ptr), 0xFFFF);
assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
assert!(runner.dispatcher.pending_wait_next_event_return.is_none());
assert_eq!(
runner.dispatcher.debug_mouse_moved_event_count, 1,
"async wake path should share the normal mouse-moved event accounting"
);
}
#[test]
fn set_mouse_position_leaves_pending_wait_next_event_asleep_without_event() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
let event_ptr = 0x0020_0000;
let result_ptr = 0x0020_0020;
runner.bus.write_word(program_start, 0x4E71); runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.tick_budget = 0;
runner.bus.write_word(result_ptr, 0xFFFF);
runner.dispatcher.sent_open_app_event = true;
runner.dispatcher.write_event_record(
&mut runner.bus,
event_ptr,
0xFFFF,
0xABCD_EF01,
1,
2,
3,
);
runner.dispatcher.pending_wait_sleep_ticks = 60;
runner.dispatcher.pending_wait_next_event_return = Some(PendingWaitNextEventReturn {
event_ptr,
result_ptr,
event_mask: 0xFFFF,
mouse_rgn: 0,
resume_pc: None,
resume_sp: None,
});
runner.set_mouse_position(123, 456);
assert_eq!(
runner.bus.read_word(event_ptr),
0xFFFF,
"mouse movement with no mouseRgn event should not rewrite the parked event record"
);
assert_eq!(runner.bus.read_long(event_ptr + 2), 0xABCD_EF01);
assert_eq!(runner.bus.read_word(event_ptr + 10), 1);
assert_eq!(runner.bus.read_word(event_ptr + 12), 2);
assert_eq!(
runner.bus.read_word(result_ptr),
0xFFFF,
"the pending WaitNextEvent result must remain untouched"
);
assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 60);
assert!(runner.dispatcher.pending_wait_next_event_return.is_some());
assert_eq!(runner.bus.read_word(0x0828), 123u16);
assert_eq!(runner.bus.read_word(0x082A), 456u16);
let (steps, running) = runner.run_steps(1, Some(110));
assert!(running);
assert_eq!(
steps, 0,
"foreground code must remain suspended while WaitNextEvent is asleep"
);
assert_eq!(
runner.bus.read_long(0x016A),
110,
"the original WaitNextEvent sleep should continue toward expiry"
);
assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 50);
}
#[test]
fn push_mouse_down_leaves_pending_wait_next_event_parked_during_interrupt_callback() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let event_ptr = 0x0020_0000;
let result_ptr = 0x0020_0020;
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
runner.bus.write_word(result_ptr, 0);
runner.dispatcher.sent_open_app_event = true;
runner
.dispatcher
.write_event_record(&mut runner.bus, event_ptr, 0, 0, 0, 0, 0);
runner.dispatcher.pending_wait_sleep_ticks = 60;
runner.dispatcher.pending_wait_next_event_return = Some(PendingWaitNextEventReturn {
event_ptr,
result_ptr,
event_mask: 0xFFFF,
mouse_rgn: 0,
resume_pc: None,
resume_sp: None,
});
runner.active_interrupt_callback = Some(ActiveInterruptCallback {
source: ActiveInterruptCallbackSource::Timer,
resume_pc: interrupted_pc,
resume_sp: interrupted_sp,
d_regs: [0; 8],
a_regs: [0, 0, 0, 0, 0, 0, 0, interrupted_sp],
sr: 0x2000,
ccr: 0,
restore_port: None,
});
runner.push_mouse_down(123, 456);
assert_eq!(
runner.bus.read_word(event_ptr),
0,
"input must not rewrite a foreground WaitNextEvent record while an interrupt callback is active"
);
assert_eq!(runner.bus.read_word(result_ptr), 0);
assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 60);
assert!(runner.dispatcher.pending_wait_next_event_return.is_some());
assert!(
runner
.dispatcher
.event_queue
.iter()
.any(|event| event.what == 1 && event.where_v == 123 && event.where_h == 456),
"the mouseDown should remain queued for the foreground event loop"
);
}
#[test]
fn pending_wait_next_event_drops_stale_return_after_foreground_moves_on() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let parked_pc = 0x0001_0000;
let stale_pc = 0x0001_0010;
let parked_sp = 0x007F_FFC0;
let event_ptr = 0x0020_0000;
let result_ptr = 0x0020_0020;
runner.bus.write_word(stale_pc, 0x4E71); runner.cpu.write_reg(Register::PC, stale_pc);
runner.cpu.write_reg(Register::A7, parked_sp);
runner.bus.write_word(result_ptr, 0xA582);
runner.dispatcher.sent_open_app_event = true;
runner
.dispatcher
.write_event_record(&mut runner.bus, event_ptr, 0, 0, 0, 0, 0);
runner.dispatcher.pending_wait_sleep_ticks = 60;
runner.dispatcher.pending_wait_next_event_return = Some(PendingWaitNextEventReturn {
event_ptr,
result_ptr,
event_mask: 0xFFFF,
mouse_rgn: 0,
resume_pc: Some(parked_pc),
resume_sp: Some(parked_sp),
});
runner.dispatcher.push_mouse_down(123, 456);
let (steps, running) = runner.run_steps(1, Some(10));
assert!(running);
assert_eq!(steps, 1);
assert_eq!(
runner.bus.read_word(result_ptr),
0xA582,
"a stale WaitNextEvent return slot may now belong to a caller frame"
);
assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
assert!(runner.dispatcher.pending_wait_next_event_return.is_none());
assert!(
runner
.dispatcher
.event_queue
.iter()
.any(|event| event.what == 1 && event.where_v == 123 && event.where_h == 456),
"stale WNE cleanup should not silently consume a queued event"
);
}
#[test]
fn push_mouse_down_restores_foreground_budget_before_next_tick_cap_run() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
runner.bus.write_word(program_start, 0x4E71); runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.tick_budget = 0;
runner.push_mouse_down(123, 456);
let (steps, running) = runner.run_steps(1, Some(110));
assert!(running);
assert_eq!(
steps, 1,
"input injected at an exhausted tick boundary should let foreground code run"
);
assert_eq!(
runner.bus.read_long(0x016A),
100,
"foreground input wake must not spend the next slice only advancing ticks"
);
}
#[test]
fn set_mouse_position_restores_foreground_budget_before_next_tick_cap_run() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
runner.bus.write_word(program_start, 0x4E71); runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 100);
runner.dispatcher.tick_count = 100;
runner.tick_budget = 0;
runner.set_mouse_position(123, 456);
let (steps, running) = runner.run_steps(1, Some(110));
assert!(running);
assert_eq!(
steps, 1,
"mouse movement at an exhausted tick boundary should let polling foreground code run"
);
assert_eq!(
runner.bus.read_long(0x016A),
100,
"foreground mouse-move wake must not spend the next slice only advancing ticks"
);
}
#[test]
fn pending_wait_sleep_ticks_honors_app_owned_visible_dialog_snapshot_in_gui_mode() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
let dialog_ptr = 0x0020_0000;
runner.bus.write_word(program_start, 0x4E71);
runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 0);
runner.dispatcher.dialog_visible_snapshots.insert(
dialog_ptr,
crate::trap::dispatch::PersistentDialogSnapshot {
bounds: (10, 10, 40, 40),
pixels: Vec::new(),
},
);
runner.dispatcher.pending_wait_sleep_ticks = 60;
let (steps, running) = runner.run_steps(1, Some(10));
assert!(running);
assert_eq!(
steps, 0,
"app-owned visible dialogs must not collapse WaitNextEvent sleep before ModalDialog"
);
assert_eq!(runner.bus.read_long(0x016A), 10);
assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 50);
}
#[test]
fn pending_wait_sleep_ticks_honors_app_owned_visible_dialog_snapshot_in_headless_cap_zero() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
let dialog_ptr = 0x0020_0000;
runner.bus.write_word(program_start, 0x4E71);
runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 0);
runner.set_wait_sleep_cap_in_headless(Some(0));
runner.dispatcher.dialog_visible_snapshots.insert(
dialog_ptr,
crate::trap::dispatch::PersistentDialogSnapshot {
bounds: (10, 10, 40, 40),
pixels: Vec::new(),
},
);
runner.dispatcher.pending_wait_sleep_ticks = 60;
let (steps, running) = runner.run_steps(1, None);
assert!(running);
assert_eq!(
steps, 1,
"headless cap zero must not collapse app-owned dialog sleep"
);
assert_eq!(runner.bus.read_long(0x016A), 60);
assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
}
#[test]
fn pending_wait_sleep_ticks_collapses_retained_modaldialog_snapshot_in_gui_mode() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
let dialog_ptr = 0x0020_0000;
runner.bus.write_word(program_start, 0x4E71);
runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 0);
runner.dispatcher.dialog_visible_snapshots.insert(
dialog_ptr,
crate::trap::dispatch::PersistentDialogSnapshot {
bounds: (10, 10, 40, 40),
pixels: Vec::new(),
},
);
runner.dispatcher.dialog_modal_entered.insert(dialog_ptr);
runner.dispatcher.pending_wait_sleep_ticks = 60;
let (steps, running) = runner.run_steps(1, Some(10));
assert!(running);
assert_eq!(
steps, 1,
"retained ModalDialog snapshots keep the existing app-yield path"
);
assert_eq!(runner.bus.read_long(0x016A), 0);
assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
}
#[test]
fn pending_wait_sleep_ticks_resumes_when_gui_sleep_expires_before_cap() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
runner.bus.write_word(program_start, 0x4E71);
runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 0);
runner.dispatcher.pending_wait_sleep_ticks = 3;
let (steps, running) = runner.run_steps(1, Some(10));
assert!(running);
assert_eq!(steps, 1);
assert_eq!(runner.bus.read_long(0x016A), 3);
assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
}
#[test]
fn pending_delay_ticks_advance_in_gui_mode() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let program_start = 0x0001_0000;
runner.bus.write_word(program_start, 0x4E71);
runner.cpu.write_reg(Register::PC, program_start);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 0);
runner.dispatcher.pending_delay_ticks = 3;
let (steps, _running) = runner.run_steps(1, Some(10));
assert_eq!(steps, 1);
assert_eq!(runner.bus.read_long(0x016A), 3);
assert_eq!(runner.dispatcher.pending_delay_ticks, 0);
assert_eq!(runner.cpu.read_reg(Register::D0), 3);
}
#[test]
fn dialog_filter_synthesized_null_event_uses_live_modifiers() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let filter_proc = 0x0004_2000u32;
runner.bus.write_word(filter_proc, 0x4E56);
runner.cpu.write_reg(Register::PC, 0x0001_0000);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.dispatcher.set_mouse_position(222, 333);
runner.dispatcher.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr: 0x0020_0000,
bounds: (100, 200, 200, 360),
title: String::new(),
proc_id: 2,
items: Vec::new(),
default_item: 1,
cancel_item: 2,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: 0x007F_FFC0,
item_hit_ptr: 0x0030_0000,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: std::collections::VecDeque::new(),
draw_procs_done: true,
rendered_pixels_final: true,
filter_proc,
game_managed: true,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
assert!(runner.fire_dialog_filter_proc());
let event_ptr = runner.dialog_filter_event;
assert_eq!(runner.bus.read_word(event_ptr), 0);
assert_eq!(runner.bus.read_word(event_ptr + 10), 222);
assert_eq!(runner.bus.read_word(event_ptr + 12), 333);
assert_eq!(
runner.bus.read_word(event_ptr + 14),
runner.dispatcher.current_event_modifiers()
);
assert_eq!(
runner.dialog_filter_last_null_event_tick,
Some((0x0020_0000, 0))
);
}
#[test]
fn dialog_filter_uses_active_dialog_pending_update_before_null_event() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let filter_proc = 0x0004_2000u32;
let dialog_ptr = runner.bus.alloc(170);
runner.bus.write_word(filter_proc, 0x4E56);
runner.cpu.write_reg(Register::PC, 0x0001_0000);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.dispatcher.init_cgraf_window(
&mut runner.bus,
&mut runner.cpu,
dialog_ptr,
0,
100,
120,
220,
360,
"",
2,
true,
false,
false,
0,
);
runner.dispatcher.event_queue.clear();
runner.dispatcher.dialog_tracking =
Some(dialog_tracking_for_test(filter_proc, 0x0030_0000));
runner
.dispatcher
.dialog_tracking
.as_mut()
.unwrap()
.dialog_ptr = dialog_ptr;
assert!(runner.fire_dialog_filter_proc());
let event_ptr = runner.dialog_filter_event;
assert_eq!(runner.bus.read_word(event_ptr), 6);
assert_eq!(runner.bus.read_long(event_ptr + 2), dialog_ptr);
assert_eq!(runner.dialog_filter_last_null_event_tick, None);
}
#[test]
fn dialog_filter_paces_synthetic_update_without_starving_queued_input() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let filter_proc = 0x0004_2000u32;
let dialog_ptr = runner.bus.alloc(170);
runner.bus.write_word(filter_proc, 0x4E56);
runner.cpu.write_reg(Register::PC, 0x0001_0000);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.bus.write_long(0x016A, 17);
runner.dispatcher.tick_count = 17;
runner.dispatcher.init_cgraf_window(
&mut runner.bus,
&mut runner.cpu,
dialog_ptr,
0,
100,
120,
220,
360,
"",
2,
true,
false,
false,
0,
);
runner.dispatcher.event_queue.clear();
runner.dispatcher.dialog_tracking =
Some(dialog_tracking_for_test(filter_proc, 0x0030_0000));
runner
.dispatcher
.dialog_tracking
.as_mut()
.unwrap()
.dialog_ptr = dialog_ptr;
assert!(runner.fire_dialog_filter_proc());
let event_ptr = runner.dialog_filter_event;
assert_eq!(runner.bus.read_word(event_ptr), 6);
assert_eq!(runner.bus.read_long(event_ptr + 2), dialog_ptr);
runner.active_interrupt_callback = None;
runner.cpu.write_reg(Register::PC, 0x0001_0000);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner
.dispatcher
.dialog_tracking
.as_mut()
.unwrap()
.last_filter_event = None;
assert!(
!runner.dialog_filter_has_real_event_pending(dialog_ptr),
"the same invalid-region update should not refire indefinitely in one guest tick"
);
runner.dispatcher.event_queue.push_back(QueuedEvent {
what: 1,
message: 0,
where_v: 123,
where_h: 234,
modifiers: 0,
});
assert!(
runner.dialog_filter_has_real_event_pending(dialog_ptr),
"queued user input must bypass synthetic update pacing"
);
assert!(runner.fire_dialog_filter_proc());
assert_eq!(runner.bus.read_word(event_ptr), 1);
assert_eq!(runner.bus.read_word(event_ptr + 10), 123);
assert_eq!(runner.bus.read_word(event_ptr + 12), 234);
assert!(
runner.dispatcher.event_queue.is_empty(),
"the queued mouse event should be consumed by the filter call"
);
runner.active_interrupt_callback = None;
runner
.dispatcher
.dialog_tracking
.as_mut()
.unwrap()
.last_filter_event = None;
runner.bus.write_long(0x016A, 18);
runner.dispatcher.tick_count = 18;
assert!(
runner.dialog_filter_has_real_event_pending(dialog_ptr),
"a still-invalid dialog can surface another update event on the next guest tick"
);
}
#[test]
fn dialog_filter_proc_leaves_dialog_port_current() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000u32;
let interrupted_sp = 0x007F_FFC0u32;
let main_port = runner.bus.alloc(170);
let dialog_ptr = runner.bus.alloc(170);
runner.bus.write_word(interrupted_pc, 0x60FE); runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.dispatcher.init_cgraf_window(
&mut runner.bus,
&mut runner.cpu,
main_port,
0,
0,
0,
600,
800,
"",
2,
true,
false,
false,
0,
);
runner.dispatcher.init_cgraf_window(
&mut runner.bus,
&mut runner.cpu,
dialog_ptr,
0,
120,
180,
240,
420,
"",
2,
true,
false,
false,
0,
);
runner
.dispatcher
.set_current_port_state(&mut runner.bus, &mut runner.cpu, main_port, None);
let filter_proc = runner.bus.alloc(8);
runner.bus.write_word(filter_proc, 0x4E56);
runner.dispatcher.dialog_tracking =
Some(dialog_tracking_for_test(filter_proc, 0x0030_0000));
runner
.dispatcher
.dialog_tracking
.as_mut()
.unwrap()
.dialog_ptr = dialog_ptr;
assert!(runner.fire_dialog_filter_proc());
assert_eq!(runner.dispatcher.current_port, dialog_ptr);
assert_eq!(
runner
.active_interrupt_callback
.as_ref()
.and_then(|callback| callback.restore_port),
None
);
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
let (_steps, running) = runner.run_steps(1, None);
assert!(running);
assert_eq!(runner.dispatcher.current_port, dialog_ptr);
assert!(runner.active_interrupt_callback.is_none());
}
#[test]
fn dialog_draw_proc_trampoline_passes_item_first_and_tolerates_plain_rts() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000u32;
let interrupted_sp = 0x007F_FFC0u32;
let dialog_ptr = 0x0020_0000u32;
let proc_addr = 0x0004_2000u32;
let item_no = 5i16;
runner.bus.write_word(interrupted_pc, 0x60FE); runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.bus.write_word(proc_addr, 0x4E56); runner.bus.write_word(proc_addr + 2, 0x0000);
runner.bus.write_word(proc_addr + 4, 0x4E5E); runner.bus.write_word(proc_addr + 6, 0x4E75);
runner.dispatcher.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (0, 0, 64, 64),
title: String::new(),
proc_id: 1,
items: Vec::new(),
default_item: 0,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: interrupted_sp,
item_hit_ptr: 0,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::from([(proc_addr, item_no)]),
draw_procs_done: false,
rendered_pixels_final: false,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
assert!(runner.fire_dialog_draw_procs());
let tramp = runner.dialog_draw_trampoline;
assert_eq!(runner.bus.read_word(tramp), 0x48E7);
assert_eq!(runner.bus.read_word(tramp + 4), 0x2F3C);
assert_eq!(runner.bus.read_long(tramp + 6), dialog_ptr);
assert_eq!(runner.bus.read_word(tramp + 10), 0x3F3C);
assert_eq!(runner.bus.read_word(tramp + 12), item_no as u16);
assert_eq!(runner.bus.read_word(tramp + 14), 0x4EB9);
assert_eq!(runner.bus.read_long(tramp + 16), proc_addr);
assert_eq!(runner.bus.read_word(tramp + 20), 0x4FF9);
assert_eq!(runner.bus.read_long(tramp + 22), interrupted_sp - 36);
let (_steps, running) = runner.run_steps(16, None);
assert!(running);
assert!(
runner.active_interrupt_callback.is_none(),
"dialog callback should have resumed foreground code"
);
assert_eq!(runner.cpu.read_reg(Register::PC), interrupted_pc);
assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
}
#[test]
fn modal_dialog_draw_procs_drain_before_foreground_code_resumes() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000u32;
let interrupted_sp = 0x007F_FFC0u32;
let proc_1 = 0x0004_2000u32;
let proc_2 = 0x0004_2100u32;
runner.bus.write_word(interrupted_pc, 0x60FE); runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
for proc_addr in [proc_1, proc_2] {
runner.bus.write_word(proc_addr, 0x4E56); runner.bus.write_word(proc_addr + 2, 0x0000);
runner.bus.write_word(proc_addr + 4, 0x4E5E); runner.bus.write_word(proc_addr + 6, 0x4E75); }
let mut tracking = dialog_tracking_for_test(0, 0);
tracking.draw_proc_queue = VecDeque::from([(proc_1, 3), (proc_2, 4)]);
tracking.draw_procs_done = false;
tracking.rendered_pixels_final = false;
runner.dispatcher.dialog_tracking = Some(tracking);
assert!(runner.fire_dialog_draw_procs());
runner.deferred_tracking_refire_pc = Some(interrupted_pc + 2);
let (_steps, running) = runner.run_steps(128, None);
assert!(running);
let tracking = runner.dispatcher.dialog_tracking.as_ref().unwrap();
assert!(tracking.draw_proc_queue.is_empty());
assert!(tracking.draw_procs_done);
assert!(runner.active_interrupt_callback.is_none());
assert!(runner.deferred_tracking_refire_pc.is_none());
assert_eq!(runner.cpu.read_reg(Register::PC), interrupted_pc);
assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
}
#[test]
fn modeless_dialog_draw_proc_accepts_a5_relative_proc_ptr() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000u32;
let interrupted_sp = 0x007F_FFC0u32;
let a5 = 0x0020_0000u32;
let proc_offset = 0x0000_4200u32;
let proc_addr = a5 + proc_offset;
let dialog_ptr = runner.bus.alloc(170);
runner.bus.write_word(interrupted_pc, 0x60FE);
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.cpu.write_reg(Register::A5, a5);
runner.dispatcher.init_cgraf_window(
&mut runner.bus,
&mut runner.cpu,
dialog_ptr,
0,
120,
180,
240,
420,
"",
2,
true,
false,
false,
0,
);
runner.bus.write_word(proc_addr, 0x4E56); runner.bus.write_word(proc_addr + 2, 0x0000);
runner.bus.write_word(proc_addr + 4, 0x4E5E); runner.bus.write_word(proc_addr + 6, 0x4E75); runner
.dispatcher
.modeless_dialog_draw_proc_queue
.push_back((dialog_ptr, proc_offset, 5));
assert!(runner.fire_modeless_dialog_draw_proc());
let tramp = runner.dialog_draw_trampoline;
assert_eq!(runner.bus.read_long(tramp + 16), proc_addr);
assert_eq!(
runner.dispatcher.active_modeless_dialog_draw_proc,
Some(dialog_ptr)
);
}
#[test]
fn modeless_dialog_draw_procs_drain_after_plain_trap() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let base = 0x0001_0000u32;
let interrupted_sp = 0x007F_FFC0u32;
let dialog_ptr = runner.bus.alloc(170);
let proc_1 = 0x0004_2000u32;
let proc_2 = 0x0004_2100u32;
runner.bus.write_word(base, 0xA861); runner.bus.write_word(base + 2, 0x60FE); runner.cpu.write_reg(Register::PC, base);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.dispatcher.init_cgraf_window(
&mut runner.bus,
&mut runner.cpu,
dialog_ptr,
0,
120,
180,
240,
420,
"",
2,
true,
false,
false,
0,
);
for proc_addr in [proc_1, proc_2] {
runner.bus.write_word(proc_addr, 0x4E56); runner.bus.write_word(proc_addr + 2, 0x0000);
runner.bus.write_word(proc_addr + 4, 0x4E5E); runner.bus.write_word(proc_addr + 6, 0x4E75); }
runner.dispatcher.modeless_dialog_draw_proc_queue =
VecDeque::from([(dialog_ptr, proc_1, 3), (dialog_ptr, proc_2, 5)]);
let (_steps, running) = runner.run_steps(128, None);
assert!(running);
assert!(runner.dispatcher.modeless_dialog_draw_proc_queue.is_empty());
assert_eq!(runner.dispatcher.active_modeless_dialog_draw_proc, None);
assert!(
runner.active_interrupt_callback.is_none(),
"modeless draw callbacks should have returned to foreground code"
);
}
#[test]
fn dialog_draw_proc_does_not_restore_over_guest_selected_dialog_port() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000u32;
let interrupted_sp = 0x007F_FFC0u32;
let main_port = runner.bus.alloc(170);
let dialog_ptr = runner.bus.alloc(170);
let proc_addr = 0x0004_2000u32;
let item_no = 5i16;
runner.bus.write_word(interrupted_pc, 0x60FE); runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.dispatcher.init_cgraf_window(
&mut runner.bus,
&mut runner.cpu,
main_port,
0,
0,
0,
600,
800,
"",
2,
true,
false,
false,
0,
);
runner.dispatcher.init_cgraf_window(
&mut runner.bus,
&mut runner.cpu,
dialog_ptr,
0,
120,
180,
240,
420,
"",
2,
true,
false,
false,
0,
);
runner
.dispatcher
.set_current_port_state(&mut runner.bus, &mut runner.cpu, main_port, None);
runner.bus.write_word(proc_addr, 0x4E56); runner.bus.write_word(proc_addr + 2, 0x0000);
runner.bus.write_word(proc_addr + 4, 0x2F3C); runner.bus.write_long(proc_addr + 6, dialog_ptr);
runner.bus.write_word(proc_addr + 10, 0xA873); runner.bus.write_word(proc_addr + 12, 0x4E5E); runner.bus.write_word(proc_addr + 14, 0x4E75);
runner.dispatcher.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (120, 180, 240, 420),
title: String::new(),
proc_id: 1,
items: Vec::new(),
default_item: 0,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: interrupted_sp,
item_hit_ptr: 0,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::from([(proc_addr, item_no)]),
draw_procs_done: false,
rendered_pixels_final: false,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
assert!(runner.fire_dialog_draw_procs());
assert_eq!(
runner
.active_interrupt_callback
.as_ref()
.and_then(|callback| callback.restore_port),
None
);
let (_steps, running) = runner.run_steps(32, None);
assert!(running);
assert!(runner.active_interrupt_callback.is_none());
assert_eq!(
runner.dispatcher.current_port, dialog_ptr,
"Dialog Manager must leave the dialog port current after the draw proc"
);
}
#[test]
fn dialog_draw_proc_restores_parent_grafport_state_and_clip() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000u32;
let interrupted_sp = 0x007F_FFC0u32;
let dialog_ptr = runner.bus.alloc(170);
let other_port = runner.bus.alloc(170);
let proc_addr = 0x0004_2000u32;
let clip_rect = 0x0004_2100u32;
runner.bus.write_word(interrupted_pc, 0x60FE);
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
for port in [dialog_ptr, other_port] {
runner.dispatcher.init_cgraf_window(
&mut runner.bus,
&mut runner.cpu,
port,
0,
120,
180,
240,
420,
"",
2,
true,
false,
false,
0,
);
}
runner.dispatcher.set_current_port_state(
&mut runner.bus,
&mut runner.cpu,
dialog_ptr,
None,
);
let clip_handle = runner.bus.read_long(dialog_ptr + 28);
let clip_ptr = runner.bus.read_long(clip_handle);
let expected_clip: Vec<u8> = (0..10)
.map(|i| runner.bus.read_byte(clip_ptr + i))
.collect();
runner.bus.write_word(clip_rect, 1);
runner.bus.write_word(clip_rect + 2, 2);
runner.bus.write_word(clip_rect + 4, 3);
runner.bus.write_word(clip_rect + 6, 4);
let words = [
0x4E56,
0x0000, 0x3F3C,
0x0007, 0x3F3C,
0x0006, 0xA89B, 0x3F3C,
0x000C, 0xA89C, 0x2F3C,
(clip_rect >> 16) as u16,
clip_rect as u16, 0xA87B, 0x2F3C,
(other_port >> 16) as u16,
other_port as u16, 0xA873, 0x4E5E,
0x4E75, ];
for (i, word) in words.into_iter().enumerate() {
runner.bus.write_word(proc_addr + i as u32 * 2, word);
}
runner.dispatcher.modeless_dialog_draw_proc_queue =
VecDeque::from([(dialog_ptr, proc_addr, 5)]);
assert!(runner.fire_modeless_dialog_draw_proc());
let (_steps, running) = runner.run_steps(64, None);
assert!(running);
assert!(runner.active_interrupt_callback.is_none());
assert_eq!(runner.dispatcher.current_port, dialog_ptr);
assert_eq!(runner.bus.read_word(dialog_ptr + 52), 1);
assert_eq!(runner.bus.read_word(dialog_ptr + 54), 1);
assert_eq!(runner.bus.read_word(dialog_ptr + 56), 8);
assert_eq!(runner.bus.read_long(dialog_ptr + 28), clip_handle);
let restored_clip_ptr = runner.bus.read_long(clip_handle);
assert_eq!(
(0..10)
.map(|i| runner.bus.read_byte(restored_clip_ptr + i))
.collect::<Vec<_>>(),
expected_clip
);
}
#[test]
fn dialog_draw_proc_pascal_stack_places_item_number_before_window_pointer() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000u32;
let interrupted_sp = 0x007F_FFC0u32;
let dialog_ptr = 0x0029_4240u32;
let proc_addr = 0x0004_2000u32;
let item_no = 2i16;
let seen_item_addr = 0x0004_3000u32;
let seen_dialog_addr = 0x0004_3004u32;
runner.bus.write_word(interrupted_pc, 0x60FE); runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.bus.write_word(proc_addr, 0x4E56); runner.bus.write_word(proc_addr + 2, 0x0000);
runner.bus.write_word(proc_addr + 4, 0x302E); runner.bus.write_word(proc_addr + 6, 0x0008);
runner.bus.write_word(proc_addr + 8, 0x33C0); runner.bus.write_long(proc_addr + 10, seen_item_addr);
runner.bus.write_word(proc_addr + 14, 0x222E); runner.bus.write_word(proc_addr + 16, 0x000A);
runner.bus.write_word(proc_addr + 18, 0x23C1); runner.bus.write_long(proc_addr + 20, seen_dialog_addr);
runner.bus.write_word(proc_addr + 24, 0x4E5E); runner.bus.write_word(proc_addr + 26, 0x4E75);
runner.dispatcher.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
dialog_ptr,
bounds: (120, 180, 240, 420),
title: String::new(),
proc_id: 1,
items: Vec::new(),
default_item: 0,
cancel_item: 0,
edit_text: String::new(),
edit_item: 0,
saved_pixels: Vec::new(),
stack_ptr: interrupted_sp,
item_hit_ptr: 0,
rendered_pixels: Vec::new(),
flash_remaining: 0,
flash_delay: 0,
flash_item: 0,
edit_text_modified: false,
draw_proc_queue: VecDeque::from([(proc_addr, item_no)]),
draw_procs_done: false,
rendered_pixels_final: false,
filter_proc: 0,
game_managed: false,
last_filter_event: None,
popup_draws: Vec::new(),
active_popup: None,
active_button: None,
active_user_item: None,
});
assert!(runner.fire_dialog_draw_procs());
let (_steps, running) = runner.run_steps(48, None);
assert!(running);
assert!(runner.active_interrupt_callback.is_none());
assert_eq!(runner.bus.read_word(seen_item_addr) as i16, item_no);
assert_eq!(runner.bus.read_long(seen_dialog_addr), dialog_ptr);
}
#[test]
fn pending_delay_ticks_fire_vbl_tasks_in_headless_mode() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let interrupted_pc = 0x0001_0000;
let interrupted_sp = 0x007F_FFC0;
let task_ptr = 0x0020_2000;
runner.bus.write_word(interrupted_pc, 0x4E71);
runner.cpu.write_reg(Register::PC, interrupted_pc);
runner.cpu.write_reg(Register::A7, interrupted_sp);
runner.cpu.core.set_sr_noint_nosp(0x2000);
runner.bus.write_long(0x016A, 0);
runner.dispatcher.pending_delay_ticks = 1;
runner.bus.write_word(task_ptr + 4, 1);
runner.bus.write_long(task_ptr + 6, 0x0004_1234);
runner.bus.write_word(task_ptr + 10, 1);
runner.bus.write_word(task_ptr + 12, 0);
runner.dispatcher.vbl_tasks.push(VblTask {
task_ptr,
slot: None,
pending: false,
});
let (steps, running) = runner.run_steps(1, None);
assert!(running);
assert_eq!(steps, 1);
assert_eq!(runner.bus.read_long(0x016A), 1);
assert_eq!(runner.dispatcher.pending_delay_ticks, 0);
assert_eq!(runner.cpu.read_reg(Register::D0), 1);
assert!(matches!(
runner.active_interrupt_callback,
Some(ActiveInterruptCallback {
source: ActiveInterruptCallbackSource::Vbl,
..
})
));
}
#[test]
fn set_mouse_position_updates_dispatcher_and_low_mem_globals() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_mouse_position(123, 456);
assert_eq!(runner.dispatcher.mouse_pos, (123, 456));
for off in [0x0828u32, 0x082C, 0x0830] {
assert_eq!(runner.bus.read_word(off), 123u16, "v at ${:04X}", off);
assert_eq!(runner.bus.read_word(off + 2), 456u16, "h at ${:04X}", off);
}
}
#[test]
fn zero_divide_rte_handler_resumes_after_divu_by_zero() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.bus.write_word(0x00FE, 0x4E73); runner.bus.write_long(0x0014, 0x0000_00FE);
let prog = 0x0010_0000u32;
runner.bus.write_word(prog, 0x82C0); runner.bus.write_word(prog + 2, 0x4E71); runner.bus.write_word(prog + 4, 0x4E71);
runner.cpu.write_reg(Register::PC, prog);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.cpu.write_reg(Register::D0, 0);
runner.cpu.write_reg(Register::D1, 100);
let (steps, running) = runner.run_steps(3, None);
assert!(running, "runner must not halt on zero-divide");
assert_eq!(steps, 3);
assert_eq!(
runner.cpu.read_reg(Register::PC),
prog + 4,
"PC must advance past the DIVU+NOP without re-entering the trap"
);
assert_eq!(
runner.cpu.read_reg(Register::D1),
100,
"DIVU by zero must leave the destination register unchanged"
);
}
#[test]
fn chk_rte_handler_resumes_after_bounds_violation() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.bus.write_word(0x00FE, 0x4E73); runner.bus.write_long(0x0018, 0x0000_00FE);
let prog = 0x0010_0000u32;
runner.bus.write_word(prog, 0x41BC); runner.bus.write_word(prog + 2, 0x0005); runner.bus.write_word(prog + 4, 0x4E71);
runner.cpu.write_reg(Register::PC, prog);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.cpu.write_reg(Register::D0, 100);
let (steps, running) = runner.run_steps(3, None);
assert!(running, "runner must not halt on CHK bounds violation");
assert_eq!(steps, 3);
assert_eq!(
runner.cpu.read_reg(Register::PC),
prog + 6,
"PC must advance past CHK (4 bytes) + NOP (2 bytes)"
);
assert_eq!(runner.cpu.read_reg(Register::D0), 100);
}
#[test]
fn trapv_rte_handler_resumes_when_v_flag_is_set() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.bus.write_word(0x00FE, 0x4E73); runner.bus.write_long(0x001C, 0x0000_00FE);
let prog = 0x0010_0000u32;
runner.bus.write_word(prog, 0x4E76); runner.bus.write_word(prog + 2, 0x4E71);
runner.cpu.write_reg(Register::PC, prog);
runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
runner.cpu.core.set_ccr(0x02);
let (steps, running) = runner.run_steps(3, None);
assert!(running, "runner must not halt on TRAPV");
assert_eq!(steps, 3);
assert_eq!(runner.cpu.read_reg(Register::PC), prog + 4);
}
#[test]
fn set_mouse_position_leaves_mb_state_untouched() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.bus.write_byte(0x0172, 0x80);
runner.set_mouse_position(50, 60);
assert_eq!(runner.bus.read_byte(0x0172), 0x80);
runner.bus.write_byte(0x0172, 0x00);
runner.set_mouse_position(70, 80);
assert_eq!(runner.bus.read_byte(0x0172), 0x00);
}
#[test]
fn push_mouse_down_writes_mb_state_pressed_and_position() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.bus.write_byte(0x0172, 0x80);
runner.push_mouse_down(123, 456);
assert_eq!(
runner.bus.read_byte(0x0172),
0x00,
"MBState must be 0x00 (pressed) immediately after push_mouse_down"
);
assert_eq!(runner.bus.read_word(0x0828), 123u16);
assert_eq!(runner.bus.read_word(0x082A), 456u16);
assert_eq!(runner.bus.read_word(0x082C), 123u16);
assert_eq!(runner.bus.read_word(0x082E), 456u16);
assert_eq!(runner.bus.read_word(0x0830), 123u16);
assert_eq!(runner.bus.read_word(0x0832), 456u16);
}
#[test]
fn pending_mouse_down_count_tracks_queued_clicks() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
assert_eq!(runner.pending_mouse_down_count(), 0);
runner.push_mouse_down(10, 20);
assert_eq!(runner.pending_mouse_down_count(), 1);
runner.dispatcher.event_queue.pop_front();
assert_eq!(runner.pending_mouse_down_count(), 0);
}
#[test]
fn push_mouse_up_writes_mb_state_released_immediately() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.push_mouse_down(10, 20);
assert_eq!(runner.bus.read_byte(0x0172), 0x00);
runner.push_mouse_up(10, 20);
assert_eq!(
runner.bus.read_byte(0x0172),
0x80,
"MBState must flip back to 0x80 (released) immediately on push_mouse_up — \
not deferred to the next tick"
);
}
#[test]
fn mb_state_releases_when_paired_mouseup_queued_but_unconsumed() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.push_mouse_down(10, 20);
runner.push_mouse_up(10, 20);
runner.advance_guest_tick();
assert_eq!(
runner.bus.read_byte(0x0172),
0x80,
"advance_guest_tick must release MBState to 0x80 once a \
paired mouseUp is queued behind the mouseDown — even when \
nothing has drained the event queue"
);
assert!(
runner.dispatcher.event_queue.iter().any(|e| e.what == 1),
"mouseDown event must remain in the queue (would be drained by GetNextEvent)"
);
assert!(
runner.dispatcher.event_queue.iter().any(|e| e.what == 2),
"mouseUp event must remain in the queue"
);
}
#[test]
fn mb_state_stays_pressed_with_solo_pending_mousedown() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.push_mouse_down(10, 20);
runner.advance_guest_tick();
assert_eq!(
runner.bus.read_byte(0x0172),
0x00,
"MBState must stay pressed across a tick advance while only \
a mouseDown is queued (no paired mouseUp yet)"
);
}
#[test]
fn menu_bar_policy_defaults_to_guest_control_and_supports_explicit_kiosk_modes() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
assert_eq!(runner.menu_bar_policy(), MenuBarPolicy::GuestControlled);
assert!(
runner.menu_bar_visible(),
"library runners should permit guest menu chrome by default"
);
runner.set_menu_bar_policy(MenuBarPolicy::InitialKiosk);
assert_eq!(runner.menu_bar_policy(), MenuBarPolicy::InitialKiosk);
assert!(!runner.menu_bar_visible());
runner.set_menu_bar_visible(false);
assert_eq!(runner.menu_bar_policy(), MenuBarPolicy::ForceHidden);
assert!(!runner.menu_bar_visible());
runner.set_menu_bar_visible(true);
assert_eq!(runner.menu_bar_policy(), MenuBarPolicy::GuestControlled);
assert!(runner.menu_bar_visible());
}
#[test]
fn initial_kiosk_releases_after_guest_hides_and_reveals_menu_bar() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
runner.set_menu_bar_policy(MenuBarPolicy::InitialKiosk);
runner.dispatcher.front_window = 1;
runner
.bus
.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 0);
runner.force_advance_guest_tick();
assert_eq!(runner.menu_bar_policy(), MenuBarPolicy::InitialKiosk);
assert!(!runner.menu_bar_visible());
runner
.bus
.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 20);
runner.force_advance_guest_tick();
assert_eq!(runner.menu_bar_policy(), MenuBarPolicy::GuestControlled);
assert!(runner.menu_bar_visible());
}
#[test]
fn disassemble_at_decodes_known_opcodes_with_correct_advance() {
let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
let pc = 0x10000u32;
runner.bus.write_word(pc, 0x4E71);
runner.bus.write_word(pc + 2, 0xA8EC);
runner.bus.write_word(pc + 4, 0x4E71);
let out = runner.disassemble_at(pc, 3);
assert_eq!(
out.len(),
3,
"disassemble_at must return exactly count entries"
);
assert_eq!(
out[0].0, pc,
"first entry's PC must equal the requested start"
);
assert!(
out[0].1.contains("NOP"),
"$4E71 must disassemble to NOP, got: {}",
out[0].1
);
assert!(
out[0].2 >= 2 && out[0].2 <= 10,
"instruction size must be in clamp range [2, 10], got {}",
out[0].2
);
assert_eq!(
out[1].0,
pc + out[0].2,
"second entry's PC must equal first PC + first size"
);
assert!(
out[1].1.contains("$A8EC"),
"A-line trap $A8EC must surface in mnemonic (DC.W form), got: {}",
out[1].1
);
assert!(
out[2].1.contains("NOP"),
"third entry must be the second NOP we seeded"
);
}
#[test]
fn disassemble_at_uses_the_configured_ram_boundary() {
let mut runner = FixtureRunner::new(16 * 1024 * 1024, FixtureRunnerConfig::default());
let pc = 12 * 1024 * 1024;
runner.bus.write_word(pc, 0x4E71);
let out = runner.disassemble_at(pc, 1);
assert_eq!(out.len(), 1);
assert!(
out[0].1.contains("NOP"),
"mapped RAM above 8 MiB must not be reported as unmapped"
);
}
}