1use crate::cpu::{M68kCpu, Register, StepResult};
4use crate::debug_overlay::{DebugOverlayFrameStats, DebugOverlaySnapshot};
5use crate::loader::{
6 ApplicationSizeResource, Code0Header, CodeSegmentHeader, JumpTableEntry, LoadedApp,
7};
8use crate::managers::resource::ResourceFork;
9use crate::memory::{MacMemoryBus, MemoryBus};
10use crate::trap::TrapDispatcher;
11use crate::ui_theme::{ThemeMetricsMode, UiTheme, UiThemeId};
12use crate::{Error, Result};
13use std::collections::{BTreeSet, HashMap};
14use std::time::{Instant, SystemTime, UNIX_EPOCH};
15
16use std::sync::OnceLock;
18static TRACE_DIALOG_FILTER: OnceLock<bool> = OnceLock::new();
19static TRACE_TIMER: OnceLock<bool> = OnceLock::new();
20static TRACE_VBL: OnceLock<bool> = OnceLock::new();
21static TRACE_SOUND_RUNNER: OnceLock<bool> = OnceLock::new();
22static TRACE_DIALOG_PROCS: OnceLock<bool> = OnceLock::new();
23
24const APP_HEAP_FLOOR: u32 = 0x0020_0000;
25const APP_ZONE_HEADER_SIZE: u32 = 64;
26const APP_STACK_SAFETY_MARGIN: u32 = 0x2000;
27
28fn trace_dialog_filter_enabled() -> bool {
29 *TRACE_DIALOG_FILTER
30 .get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_FILTER").is_some())
31}
32
33fn trace_timer_enabled() -> bool {
34 *TRACE_TIMER.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_TIMER").is_some())
35}
36
37fn trace_vbl_enabled() -> bool {
38 *TRACE_VBL.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_VBL").is_some())
39}
40
41fn trace_sound_runner_enabled() -> bool {
42 *TRACE_SOUND_RUNNER.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_SOUND").is_some())
43}
44
45fn trace_dialog_procs_enabled() -> bool {
46 *TRACE_DIALOG_PROCS.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_PROCS").is_some())
47}
48
49#[cfg(not(target_arch = "wasm32"))]
56static TRACE_BUFFER_ENABLED: OnceLock<bool> = OnceLock::new();
57fn trace_buffer_enabled() -> bool {
58 #[cfg(target_arch = "wasm32")]
59 {
60 return false;
61 }
62 #[cfg(not(target_arch = "wasm32"))]
63 *TRACE_BUFFER_ENABLED.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_BUFFER").is_some())
64}
65
66#[cfg(not(target_arch = "wasm32"))]
67static TRACE_PC_RANGE: OnceLock<Option<(u32, u32)>> = OnceLock::new();
68#[cfg(not(target_arch = "wasm32"))]
69static TRACE_PC_RANGE_TICKS: OnceLock<Option<(Option<u32>, Option<u32>)>> = OnceLock::new();
70#[cfg(not(target_arch = "wasm32"))]
71fn trace_pc_range() -> Option<(u32, u32)> {
72 *TRACE_PC_RANGE.get_or_init(|| {
73 let value = std::env::var("SYSTEMLESS_TRACE_PC_RANGE").ok()?;
74 let mut parts = value.split(':');
75 let start = parts.next()?.trim();
76 let end = parts.next()?.trim();
77 let parse = |s: &str| u32::from_str_radix(s.trim_start_matches("0x"), 16).ok();
78 Some((parse(start)?, parse(end)?))
79 })
80}
81
82#[cfg(not(target_arch = "wasm32"))]
83fn trace_pc_range_ticks() -> Option<(Option<u32>, Option<u32>)> {
84 *TRACE_PC_RANGE_TICKS.get_or_init(|| {
85 let min = std::env::var("SYSTEMLESS_TRACE_PC_RANGE_TICK_MIN")
86 .ok()
87 .and_then(|v| v.parse::<u32>().ok());
88 let max = std::env::var("SYSTEMLESS_TRACE_PC_RANGE_TICK_MAX")
89 .ok()
90 .and_then(|v| v.parse::<u32>().ok());
91 (min.is_some() || max.is_some()).then_some((min, max))
92 })
93}
94
95fn trace_pc_range_contains(pc: u32, tick: u32) -> bool {
96 #[cfg(target_arch = "wasm32")]
97 {
98 let _ = (pc, tick);
99 return false;
100 }
101 #[cfg(not(target_arch = "wasm32"))]
102 {
103 let in_range = trace_pc_range()
104 .map(|(start, end)| pc >= start && pc <= end)
105 .unwrap_or(false);
106 if !in_range {
107 return false;
108 }
109 trace_pc_range_ticks()
110 .map(|(min, max)| {
111 min.map(|min| tick >= min).unwrap_or(true)
112 && max.map(|max| tick <= max).unwrap_or(true)
113 })
114 .unwrap_or(true)
115 }
116}
117
118static TRACE_LOAD_ENABLED: OnceLock<bool> = OnceLock::new();
124pub(crate) fn trace_load_enabled() -> bool {
125 *TRACE_LOAD_ENABLED.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_LOAD").is_some())
126}
127
128pub fn decode_fakeptr_pc(pc: u32) -> Option<String> {
148 if (0x00F00000..=0x00F0FFFF).contains(&pc) {
149 let trap_word = (pc & 0xFFFF) as u16;
150 Some(format!(
151 "PC matches GetTrapAddress fake-ptr ($A046/$A346/$A746) for trap ${:04X} — \
152 game likely JMP/JSR'd through the unique-address placeholder. Implementing \
153 a re-trap trampoline at the fake-ptr address would unblock this path.",
154 trap_word
155 ))
156 } else if (0xCAFE0000..=0xCAFE03FF).contains(&pc) {
157 let trap_num = (pc - 0xCAFE0000) as u16;
158 let trap_word = 0xA800 | trap_num;
159 Some(format!(
160 "PC matches GetToolTrapAddress fake-ptr for trap ${:04X} (tool num=${:03X}) — \
161 same trampoline gap as the OS-style fake-ptr range.",
162 trap_word, trap_num
163 ))
164 } else {
165 None
166 }
167}
168
169#[cfg(not(target_arch = "wasm32"))]
176static TRACE_OPCODE_COUNTS: OnceLock<bool> = OnceLock::new();
177fn trace_opcode_counts_enabled() -> bool {
178 #[cfg(target_arch = "wasm32")]
179 {
180 return false;
181 }
182 #[cfg(not(target_arch = "wasm32"))]
183 *TRACE_OPCODE_COUNTS
184 .get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_OPCODE_COUNTS").is_some())
185}
186
187const PC_SAMPLE_INTERVAL: u64 = 1000;
193#[cfg(not(target_arch = "wasm32"))]
194static TRACE_HOT_PC: OnceLock<bool> = OnceLock::new();
195fn trace_hot_pc_enabled() -> bool {
196 #[cfg(target_arch = "wasm32")]
197 {
198 return false;
199 }
200 #[cfg(not(target_arch = "wasm32"))]
201 *TRACE_HOT_PC.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_HOT_PC").is_some())
202}
203
204static SPIN_WAIT_FASTFWD_FORCE_ON: OnceLock<bool> = OnceLock::new();
213static SPIN_WAIT_FASTFWD_FORCE_OFF: OnceLock<bool> = OnceLock::new();
214
215fn spin_wait_fastfwd_force_on() -> bool {
216 *SPIN_WAIT_FASTFWD_FORCE_ON
217 .get_or_init(|| std::env::var_os("SYSTEMLESS_SPIN_WAIT_FASTFWD").is_some())
218}
219fn spin_wait_fastfwd_force_off() -> bool {
220 *SPIN_WAIT_FASTFWD_FORCE_OFF
221 .get_or_init(|| std::env::var_os("SYSTEMLESS_DISABLE_SPIN_FASTFWD").is_some())
222}
223
224fn spin_wait_fastfwd_enabled_for(yield_for_ui: bool) -> bool {
231 spin_wait_fastfwd_gate(
232 spin_wait_fastfwd_force_on(),
233 spin_wait_fastfwd_force_off(),
234 yield_for_ui,
235 )
236}
237
238fn spin_wait_fastfwd_gate(force_on: bool, force_off: bool, yield_for_ui: bool) -> bool {
244 if force_off {
245 return false;
246 }
247 if force_on {
248 return true;
249 }
250 !yield_for_ui
251}
252
253fn modaldialog_refire_is_noop(
263 yield_for_ui: bool,
264 has_tracking: bool,
265 filter_allows_noop: bool,
266 flash_remaining_zero: bool,
267 draw_procs_done: bool,
268 rendered_pixels_final: bool,
269 event_queue_empty: bool,
270) -> bool {
271 !yield_for_ui
272 && has_tracking
273 && filter_allows_noop
274 && flash_remaining_zero
275 && draw_procs_done
276 && rendered_pixels_final
277 && event_queue_empty
278}
279
280fn tracking_refire_should_freeze_ticks(opcode: u16) -> bool {
285 let trap_no_autopop = opcode & !0x0400;
286 trap_no_autopop == 0xA93D || trap_no_autopop == 0xA80B || trap_no_autopop == 0xA968 }
290
291fn canonical_trap_number(opcode: u16) -> (bool, u16) {
292 let is_tool = (opcode & 0x0800) != 0;
293 let trap_num = if is_tool {
294 opcode & 0x03FF
295 } else {
296 opcode & 0x00FF
297 };
298 (is_tool, trap_num)
299}
300
301fn hle_trap_extra_tick_cost(opcode: u16) -> i32 {
302 let (is_tool, trap_num) = canonical_trap_number(opcode);
303 match (is_tool, trap_num) {
304 (true, 0x0170) | (true, 0x0060) | (true, 0x0171) | (true, 0x0175) | (true, 0x0062) | (false, 0x0031) | (_, 0x003B) => 0, (true, 0x006C) | (true, 0x006E) | (true, 0x01EB) | (true, 0x01EC) => 0,
317
318 (true, 0x00EC) | (true, 0x00F6) => 96,
320
321 (true, 0x01A0) | (true, 0x01A1) | (true, 0x01A2) | (true, 0x01BC) => 96,
324
325 (true, 0x019D..=0x01CF) => 24,
328
329 (true, _) => 4,
332 (false, _) => 2,
333 }
334}
335
336fn event_manager_yield_trap(opcode: u16) -> bool {
337 matches!(
338 canonical_trap_number(opcode),
339 (true, 0x0170) | (true, 0x0060) | (true, 0x0171) )
343}
344
345const SPIN_FASTFWD_MAX_TICKS: u32 = 1_000_000;
351
352enum AdvanceResult {
360 Advanced,
361 CapHit,
362 Interrupted,
363 TooFar,
364}
365
366const DIALOG_DRAW_TRAMPOLINE_OFFSET: u32 = 0x00;
368const DIALOG_FILTER_TRAMPOLINE_OFFSET: u32 = 0x40;
369const DIALOG_FILTER_EVENT_OFFSET: u32 = 0x80;
370const DIALOG_FILTER_RESULT_OFFSET: u32 = 0x96;
372const MENU_HOOK_TRAMPOLINE_OFFSET: u32 = 0xA0;
373const DIALOG_CALLBACK_SCRATCH_FALLBACK: u32 = 0x0000_1200;
374pub const DEFAULT_VBL_HZ: f64 = 60.15;
376
377#[derive(Clone, Debug, Eq, PartialEq)]
378pub struct VfsFileSummary {
379 pub path: String,
380 pub data_len: usize,
381 pub resource_len: usize,
382 pub data_hash: u64,
383 pub resource_hash: u64,
384 pub file_type: u32,
385 pub creator: u32,
386 pub finder_flags: u16,
387 pub created_date: u32,
388 pub modified_date: u32,
389}
390
391#[derive(Clone, Debug, Eq, PartialEq)]
392pub struct VfsFileStat {
393 pub path: String,
394 pub data_len: usize,
395 pub resource_len: usize,
396 pub file_type: u32,
397 pub creator: u32,
398 pub finder_flags: u16,
399 pub created_date: u32,
400 pub modified_date: u32,
401}
402
403#[derive(Clone, Debug, Eq, PartialEq)]
404pub struct VfsFileSnapshot {
405 pub path: String,
406 pub data_fork: Vec<u8>,
407 pub resource_fork: Vec<u8>,
408 pub file_type: u32,
409 pub creator: u32,
410 pub finder_flags: u16,
411 pub created_date: u32,
412 pub modified_date: u32,
413}
414
415fn vfs_fork_hash(bytes: &[u8]) -> u64 {
416 let mut hash = 0xcbf2_9ce4_8422_2325u64;
417 for &byte in bytes {
418 hash ^= u64::from(byte);
419 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
420 }
421 hash
422}
423pub const DEFAULT_REALTIME_CPU_MHZ: f64 =
425 crate::machine_profile::ORACLE_MACHINE_PROFILE.realtime_cpu_mhz;
426pub const DEFAULT_REALTIME_INSTRUCTIONS_PER_SECOND: f64 = DEFAULT_REALTIME_CPU_MHZ * 1_000_000.0;
429const INSTRUCTIONS_PER_TICK: u32 = 12_000;
434const MAC_EPOCH_OFFSET_FROM_UNIX: u64 = 2_082_844_800;
435const CURSOR_TASK_NOOP_ADDR: u32 = 0x0000_0060;
436
437fn current_mac_epoch_seconds() -> u32 {
438 let unix_now = SystemTime::now()
439 .duration_since(UNIX_EPOCH)
440 .unwrap_or_default()
441 .as_secs();
442 unix_now
443 .saturating_add(MAC_EPOCH_OFFSET_FROM_UNIX)
444 .min(u32::MAX as u64) as u32
445}
446
447#[derive(Clone, Copy, Debug)]
448enum ActiveInterruptCallbackSource {
449 Timer,
450 Vbl,
451 CursorTask,
452 SoundCallback,
453 SoundFileCompletion,
454 SoundDoubleBack,
455 DialogDrawProc,
456 DialogFilterProc,
457 MenuHook,
458}
459
460#[derive(Clone, Copy, Debug)]
461struct ActiveInterruptCallback {
462 source: ActiveInterruptCallbackSource,
463 resume_pc: u32,
464 resume_sp: u32,
465 d_regs: [u32; 8],
466 a_regs: [u32; 8],
467 ccr: u8,
468 restore_port: Option<(u32, u32)>,
469}
470
471fn is_sound_interrupt_source(source: ActiveInterruptCallbackSource) -> bool {
472 matches!(
473 source,
474 ActiveInterruptCallbackSource::SoundCallback
475 | ActiveInterruptCallbackSource::SoundFileCompletion
476 | ActiveInterruptCallbackSource::SoundDoubleBack
477 )
478}
479
480fn align4(value: u32) -> u32 {
481 value.saturating_add(3) & !3
482}
483
484fn app_heap_start_for_loaded_app(app: &LoadedApp) -> u32 {
485 APP_HEAP_FLOOR.max(align4(app.loaded_image_end))
486}
487
488pub struct FixtureRunnerConfig {
493 pub max_instructions: usize,
498 pub load_address: u32,
503 pub arrows_as_numpad: bool,
507 pub ui_theme: UiThemeId,
511 pub theme_metrics_mode: ThemeMetricsMode,
514}
515
516impl Default for FixtureRunnerConfig {
517 fn default() -> Self {
518 Self {
519 max_instructions: 10_000_000,
520 load_address: 0x10000,
521 arrows_as_numpad: false,
522 ui_theme: UiThemeId::ClassicSystem7,
523 theme_metrics_mode: ThemeMetricsMode::ClassicGuestMetrics,
524 }
525 }
526}
527
528pub struct FixtureRunner {
558 cpu: M68kCpu,
559 bus: MacMemoryBus,
560 dispatcher: TrapDispatcher,
561 config: FixtureRunnerConfig,
562 trace_buffer: std::collections::VecDeque<(u32, u16, u32, u32, u32, u32)>, halted: bool,
565 halted_trap: Option<u16>,
567 halted_pc: Option<u32>,
569 halted_sp: Option<u32>,
571 halted_d0: Option<u32>,
573 total_instructions: u64,
575 instructions_per_tick: u32,
577 wait_sleep_cap_in_headless: Option<u32>,
583 tick_budget: i32,
587 frozen_ticks: Option<u32>,
591 timer_trampoline: u32,
594 vbl_trampoline: u32,
597 cursor_task_trampoline: u32,
600 active_interrupt_callback: Option<ActiveInterruptCallback>,
606 audio: Option<Box<dyn crate::audio::AudioBackend>>,
608 audio_buffer: Vec<u8>,
611 sound_doubleback_trampoline: u32,
614 sound_callback_trampoline: u32,
617 sound_file_completion_trampoline: u32,
620 dialog_draw_trampoline: u32,
623 dialog_filter_trampoline: u32,
626 menu_hook_trampoline: u32,
629 dialog_filter_event: u32,
631 dialog_filter_last_null_event_tick: Option<(u32, u32)>,
634 dialog_filter_last_update_event_tick: Option<(u32, u32, u32)>,
639 app_start_time: Option<u32>,
642 application_partition_size: Option<u32>,
646 opcode_histogram: Box<[u64; 65536]>,
656 pc_histogram: HashMap<u32, u64>,
663}
664
665impl FixtureRunner {
666 pub fn new(ram_size: usize, config: FixtureRunnerConfig) -> Self {
682 let mut dispatcher = TrapDispatcher::new();
683 dispatcher.set_ui_theme_id(config.ui_theme);
684 Self {
685 cpu: M68kCpu::new(),
686 bus: MacMemoryBus::new(ram_size),
687 dispatcher,
688 config,
689 trace_buffer: std::collections::VecDeque::with_capacity(2000),
690 halted: false,
691 halted_trap: None,
692 halted_pc: None,
693 halted_sp: None,
694 halted_d0: None,
695 total_instructions: 0,
696 instructions_per_tick: INSTRUCTIONS_PER_TICK,
697 wait_sleep_cap_in_headless: None,
698 tick_budget: INSTRUCTIONS_PER_TICK as i32,
699 frozen_ticks: None,
700 timer_trampoline: 0,
701 vbl_trampoline: 0,
702 cursor_task_trampoline: 0,
703 active_interrupt_callback: None,
704 audio: None,
705 audio_buffer: Vec::new(),
706 sound_doubleback_trampoline: 0,
707 sound_callback_trampoline: 0,
708 sound_file_completion_trampoline: 0,
709 dialog_draw_trampoline: 0,
710 dialog_filter_trampoline: 0,
711 menu_hook_trampoline: 0,
712 dialog_filter_event: 0,
713 dialog_filter_last_null_event_tick: None,
714 dialog_filter_last_update_event_tick: None,
715 app_start_time: None,
716 application_partition_size: None,
717 opcode_histogram: Box::new([0u64; 65536]),
718 pc_histogram: HashMap::new(),
719 }
720 }
721
722 pub fn is_halted(&self) -> bool {
724 self.halted
725 }
726
727 pub fn halted_by_exit_to_shell(&self) -> bool {
733 self.halted && self.halted_trap == Some(0xA9F4)
734 }
735
736 pub fn guest_tick(&self) -> u32 {
737 self.bus.read_long(0x016A)
738 }
739
740 pub fn host_now(&self) -> Instant {
741 Instant::now()
742 }
743
744 pub fn halted_trap(&self) -> Option<u16> {
745 self.halted_trap
746 }
747
748 pub fn halted_pc(&self) -> Option<u32> {
749 self.halted_pc
750 }
751
752 pub fn halted_sp(&self) -> Option<u32> {
753 self.halted_sp
754 }
755
756 pub fn halted_stack_word0(&self) -> Option<u16> {
757 self.halted_sp.map(|sp| self.bus.read_word(sp))
758 }
759
760 pub fn halted_stack_word(&self, word_index: u32) -> Option<u16> {
761 self.halted_sp
762 .map(|sp| self.bus.read_word(sp + word_index.saturating_mul(2)))
763 }
764
765 pub fn halted_d0(&self) -> Option<u32> {
766 self.halted_d0
767 }
768
769 pub fn total_instructions(&self) -> u64 {
770 self.total_instructions
771 }
772
773 pub fn debug_overlay_snapshot(
774 &self,
775 frame_stats: DebugOverlayFrameStats,
776 ) -> DebugOverlaySnapshot {
777 use crate::memory::globals::addr;
778
779 let dispatcher = &self.dispatcher;
780 let (_, _, screen_width, screen_height, pixel_size) = dispatcher.screen_mode;
781 let cursor_image = dispatcher.cursor_data();
782 let cursor_mask_nonzero_bytes = cursor_image
783 .as_ref()
784 .map(|(_, mask, _, _)| mask.iter().filter(|&&byte| byte != 0).count());
785 let cursor_hotspot = cursor_image
786 .as_ref()
787 .map(|(_, _, hot_v, hot_h)| (*hot_v, *hot_h));
788
789 DebugOverlaySnapshot {
790 frame_stats,
791 guest_tick: self.guest_tick(),
792 total_instructions: self.total_instructions,
793 trap_count: dispatcher.trap_count,
794 game_trap_count: dispatcher.game_trap_count,
795 cursor_visible: dispatcher.cursor_visible(),
796 cursor_level: dispatcher.cursor_level(),
797 cursor_data_present: dispatcher.cursor_data_present(),
798 cursor_mask_nonzero_bytes,
799 cursor_hotspot,
800 cursor_position: dispatcher.mouse_position(),
801 mouse_button: dispatcher.mouse_button,
802 fullscreen_locked: dispatcher.fullscreen_locked,
803 mbar_height: self.bus.read_word(addr::MBAR_HEIGHT),
804 screen_width,
805 screen_height,
806 pixel_size,
807 front_window: dispatcher.front_window(),
808 window_bounds: dispatcher.window_bounds(),
809 window_count: dispatcher.window_count(),
810 menu_count: dispatcher.menu_count(),
811 halted: self.halted,
812 halted_trap: self.halted_trap,
813 halted_pc: self.halted_pc,
814 }
815 }
816
817 pub fn cpu(&self) -> &M68kCpu {
818 &self.cpu
819 }
820
821 pub fn cpu_mut(&mut self) -> &mut M68kCpu {
822 &mut self.cpu
823 }
824
825 pub fn bus(&self) -> &MacMemoryBus {
826 &self.bus
827 }
828
829 pub fn bus_mut(&mut self) -> &mut MacMemoryBus {
830 &mut self.bus
831 }
832
833 pub fn dispatcher(&self) -> &crate::trap::dispatch::TrapDispatcher {
834 &self.dispatcher
835 }
836
837 pub fn dispatcher_mut(&mut self) -> &mut crate::trap::dispatch::TrapDispatcher {
838 &mut self.dispatcher
839 }
840
841 pub fn ui_theme(&self) -> &'static dyn UiTheme {
845 self.config.ui_theme.provider()
846 }
847
848 pub fn ui_theme_id(&self) -> UiThemeId {
849 self.config.ui_theme
850 }
851
852 pub fn theme_metrics_mode(&self) -> ThemeMetricsMode {
853 self.config.theme_metrics_mode
854 }
855
856 pub fn uses_classic_guest_metrics(&self) -> bool {
857 self.config
858 .theme_metrics_mode
859 .preserves_classic_guest_metrics()
860 }
861
862 pub fn set_menu_bar_visible(&mut self, visible: bool) {
885 self.dispatcher.menu_bar_hidden = !visible;
886 }
887
888 pub fn menu_bar_visible(&self) -> bool {
891 !self.dispatcher.menu_bar_hidden
892 }
893
894 pub fn disassemble_at(&self, pc: u32, count: usize) -> Vec<(u32, String, u32)> {
910 use crate::memory::MemoryBus;
911 let mut out = Vec::with_capacity(count);
912 let mut cur = pc;
913 for _ in 0..count {
914 let opcode = self.bus.read_word(cur);
919 let unmapped = (cur as u64) >= (8 * 1024 * 1024);
920 let (mnemonic, size) = if unmapped {
921 ("<unmapped>".to_string(), 2)
922 } else {
923 m68k::dasm::disassemble(cur, opcode, m68k::CpuType::M68000)
924 };
925 let size = size.clamp(2, 10);
929 out.push((cur, mnemonic, size));
930 cur = cur.wrapping_add(size);
931 }
932 out
933 }
934
935 pub fn print_opcode_histogram(&self, top_n: usize) {
940 if !trace_opcode_counts_enabled() {
941 return;
942 }
943 let mut entries: Vec<(u16, u64)> = self
944 .opcode_histogram
945 .iter()
946 .enumerate()
947 .filter_map(|(i, &c)| if c > 0 { Some((i as u16, c)) } else { None })
948 .collect();
949 entries.sort_by_key(|e| std::cmp::Reverse(e.1));
950 let total: u64 = entries.iter().map(|(_, c)| c).sum();
951 eprintln!(
952 "[OPCODE-HIST] top {} of {} distinct opcodes ({} total non-Aline instructions)",
953 top_n.min(entries.len()),
954 entries.len(),
955 total
956 );
957 for (opcode, count) in entries.iter().take(top_n) {
958 let group = (opcode >> 12) & 0xF;
959 let group_name = match group {
960 0x0 => "bit-op/MOVEP/immediate",
961 0x1 => "MOVE.B",
962 0x2 => "MOVE.L",
963 0x3 => "MOVE.W",
964 0x4 => "misc (LEA/JSR/etc.)",
965 0x5 => "ADDQ/SUBQ/Scc/DBcc",
966 0x6 => "Bcc/BSR",
967 0x7 => "MOVEQ",
968 0x8 => "OR/DIV/SBCD",
969 0x9 => "SUB/SUBX",
970 0xA => "A-line (should be in trap-hist)",
971 0xB => "CMP/EOR",
972 0xC => "AND/MUL/ABCD/EXG",
973 0xD => "ADD/ADDX",
974 0xE => "shift/rotate",
975 0xF => "F-line (FPU/coproc)",
976 _ => "?",
977 };
978 eprintln!(
979 "[OPCODE-HIST] {:>10} ${:04X} group {:X}: {}",
980 count, opcode, group, group_name
981 );
982 }
983 }
984
985 pub fn print_pc_histogram(&self, top_n: usize) {
991 if !trace_hot_pc_enabled() {
992 return;
993 }
994 let mut entries: Vec<(u32, u64)> =
995 self.pc_histogram.iter().map(|(&a, &c)| (a, c)).collect();
996 entries.sort_by_key(|e| std::cmp::Reverse(e.1));
997 let total: u64 = entries.iter().map(|(_, c)| c).sum();
998 eprintln!(
999 "[PC-HIST] top {} of {} distinct PCs ({} samples × {} = ~{} instructions)",
1000 top_n.min(entries.len()),
1001 entries.len(),
1002 total,
1003 PC_SAMPLE_INTERVAL,
1004 total * PC_SAMPLE_INTERVAL
1005 );
1006 for (pc, count) in entries.iter().take(top_n) {
1007 let region = match *pc {
1011 0x0000_0000..=0x0000_FFFF => "low-mem",
1012 0x0001_0000..=0x005F_FFFF => "app code",
1013 0x0060_0000..=0x00FF_FFFF => "heap/data",
1014 0x0100_0000..=0x01FF_FFFF => "ROM",
1015 _ => "other",
1016 };
1017 eprintln!("[PC-HIST] {:>8} PC=${:08X} ({})", count, pc, region);
1018 }
1019 }
1020
1021 pub fn install_application_clut(&mut self, clut: [[u16; 3]; 256]) {
1022 self.dispatcher
1023 .install_application_clut(&mut self.bus, clut);
1024 }
1025
1026 pub fn set_app_start_time(&mut self, secs: u32) {
1027 self.app_start_time = Some(secs);
1028 }
1029
1030 pub fn set_application_partition_size(&mut self, bytes: Option<u32>) {
1031 self.application_partition_size = bytes.filter(|&bytes| bytes >= 128 * 1024);
1032 }
1033
1034 pub fn app_start_time(&self) -> Option<u32> {
1040 self.app_start_time
1041 }
1042
1043 pub fn application_partition_size(&self) -> Option<u32> {
1044 self.application_partition_size
1045 }
1046
1047 pub fn enable_oracle_recording(
1048 &mut self,
1049 output_dir: impl AsRef<std::path::Path>,
1050 source: crate::oracle::OracleSource,
1051 ) -> Result<()> {
1052 self.dispatcher.enable_oracle_recording(output_dir, source)
1053 }
1054
1055 pub fn composite_frame(&mut self) {
1058 self.dispatcher.redraw_chrome(&mut self.bus);
1059 }
1060
1061 pub fn set_arrows_as_numpad(&mut self, enabled: bool) {
1063 self.config.arrows_as_numpad = enabled;
1064 }
1065
1066 pub fn arrows_as_numpad(&self) -> bool {
1068 self.config.arrows_as_numpad
1069 }
1070
1071 pub fn set_mouse_position(&mut self, v: i16, h: i16) {
1077 self.dispatcher.set_mouse_position(v, h);
1078 self.sync_mouse_position_lowmem();
1079 if !self.wake_pending_wait_next_event_if_input_available() {
1080 self.wake_pending_wait_next_event_with_null_event_for_polling_input();
1081 }
1082 self.wake_foreground_after_input();
1083 }
1084
1085 pub fn push_mouse_down(&mut self, v: i16, h: i16) {
1093 self.dispatcher.push_mouse_down(v, h);
1094 self.sync_mouse_lowmem();
1095 self.wake_pending_wait_next_event_if_input_available();
1096 self.wake_foreground_after_input();
1097 }
1098
1099 pub fn push_mouse_up(&mut self, v: i16, h: i16) {
1113 self.dispatcher.push_mouse_up(v, h);
1114 self.sync_mouse_lowmem();
1115 self.wake_pending_wait_next_event_if_input_available();
1116 self.wake_foreground_after_input();
1117 }
1118
1119 fn sync_mouse_position_lowmem(&mut self) {
1123 let (v, h) = self.dispatcher.mouse_pos;
1124 self.bus.write_word(0x0828, v as u16);
1125 self.bus.write_word(0x082A, h as u16);
1126 self.bus.write_word(0x082C, v as u16);
1127 self.bus.write_word(0x082E, h as u16);
1128 self.bus.write_word(0x0830, v as u16);
1129 self.bus.write_word(0x0832, h as u16);
1130 }
1131
1132 fn sync_mouse_lowmem(&mut self) {
1138 let mb_state: u8 = if self.dispatcher.mouse_button {
1139 0x00
1140 } else {
1141 0x80
1142 };
1143 self.bus.write_byte(0x0172, mb_state);
1144 self.sync_mouse_position_lowmem();
1145 }
1146
1147 fn sync_key_map_lowmem(&mut self) {
1152 use crate::memory::globals::addr;
1153
1154 self.bus
1155 .write_bytes(addr::KEY_MAP_LM, self.dispatcher.key_map_bytes());
1156 }
1157
1158 pub fn push_key_down(&mut self, mac_key: u8, char_code: u8) {
1160 let (key, char_code) = self.remap_key(mac_key, char_code);
1161 self.dispatcher.push_key_down(key, char_code);
1162 self.sync_key_map_lowmem();
1163 self.wake_pending_wait_next_event_if_input_available();
1164 self.wake_foreground_after_input();
1165 }
1166
1167 pub fn push_key_up(&mut self, mac_key: u8, char_code: u8) {
1169 let (key, char_code) = self.remap_key(mac_key, char_code);
1170 self.dispatcher.push_key_up(key, char_code);
1171 self.sync_key_map_lowmem();
1172 self.wake_pending_wait_next_event_if_input_available();
1173 self.wake_foreground_after_input();
1174 }
1175
1176 fn remap_key(&self, mac_key: u8, char_code: u8) -> (u8, u8) {
1181 if self.config.arrows_as_numpad {
1182 match mac_key {
1183 0x7B => (0x56, b'4'), 0x7C => (0x58, b'6'), 0x7D => (0x57, b'5'), 0x7E => (0x5B, b'8'), _ => (mac_key, char_code),
1188 }
1189 } else {
1190 (mac_key, char_code)
1191 }
1192 }
1193
1194 pub fn set_audio(&mut self, audio: Box<dyn crate::audio::AudioBackend>) {
1196 self.audio = Some(audio);
1197 }
1198
1199 pub fn set_instructions_per_tick(&mut self, instructions_per_tick: u32) {
1200 let old = self.instructions_per_tick.max(1);
1201 let new = instructions_per_tick.max(1);
1202 self.tick_budget = ((self.tick_budget as i64 * new as i64) / old as i64) as i32;
1205 self.instructions_per_tick = new;
1206 }
1207
1208 pub fn instructions_per_tick(&self) -> u32 {
1209 self.instructions_per_tick
1210 }
1211
1212 pub fn set_wait_sleep_cap_in_headless(&mut self, cap: Option<u32>) {
1216 self.wait_sleep_cap_in_headless = cap;
1217 }
1218
1219 pub fn wait_sleep_cap_in_headless(&self) -> Option<u32> {
1220 self.wait_sleep_cap_in_headless
1221 }
1222
1223 pub fn drain_audio(&mut self) -> Vec<u8> {
1226 std::mem::take(&mut self.audio_buffer)
1227 }
1228
1229 pub fn drain_audio_into(&mut self, out: &mut Vec<u8>) {
1234 out.clear();
1235 out.extend_from_slice(&self.audio_buffer);
1236 self.audio_buffer.clear();
1237 }
1238
1239 pub fn audio_buffer_len(&self) -> usize {
1241 self.audio_buffer.len()
1242 }
1243
1244 pub fn has_pending_sound_work(&self) -> bool {
1245 self.active_interrupt_callback
1246 .map(|callback| {
1247 matches!(
1248 callback.source,
1249 ActiveInterruptCallbackSource::SoundCallback
1250 | ActiveInterruptCallbackSource::SoundFileCompletion
1251 | ActiveInterruptCallbackSource::SoundDoubleBack
1252 )
1253 })
1254 .unwrap_or(false)
1255 || !self
1256 .dispatcher
1257 .sound_manager
1258 .pending_sound_callbacks
1259 .is_empty()
1260 || !self.dispatcher.sound_manager.pending_callbacks.is_empty()
1261 }
1262
1263 pub fn is_ui_tracking_active(&self) -> bool {
1264 self.frozen_ticks.is_some()
1265 || self.dispatcher.is_menu_tracking()
1266 || self.dispatcher.is_dialog_tracking()
1267 || self.dispatcher.is_control_tracking()
1268 }
1269
1270 pub fn force_advance_guest_tick(&mut self) {
1274 self.advance_guest_tick();
1275 }
1276
1277 pub fn set_output_path(&mut self, path: std::path::PathBuf) {
1278 if let Some(dir) = path.parent() {
1281 self.dispatcher.output_dir = Some(dir.to_path_buf());
1282 }
1283 }
1284
1285 pub fn vfs_read(&self, filename: &str) -> Option<&[u8]> {
1287 self.dispatcher.vfs.get(filename).map(|v| v.as_slice())
1288 }
1289
1290 pub fn vfs_file_summaries(&mut self) -> Vec<VfsFileSummary> {
1291 self.vfs_file_summaries_where(|_| true)
1292 }
1293
1294 pub fn vfs_file_summaries_where<F>(&mut self, mut include: F) -> Vec<VfsFileSummary>
1295 where
1296 F: FnMut(&str) -> bool,
1297 {
1298 self.vfs_file_paths()
1299 .into_iter()
1300 .filter(|path| include(path))
1301 .filter_map(|path| self.vfs_file_summary_for_path(&path))
1302 .collect()
1303 }
1304
1305 pub fn vfs_file_stats_where<F>(&mut self, mut include: F) -> Vec<VfsFileStat>
1306 where
1307 F: FnMut(&str) -> bool,
1308 {
1309 self.vfs_file_paths()
1310 .into_iter()
1311 .filter(|path| include(path))
1312 .filter_map(|path| self.vfs_file_stat_for_path(&path))
1313 .collect()
1314 }
1315
1316 pub fn vfs_file_summary(&mut self, path: &str) -> Option<VfsFileSummary> {
1317 let normalized = TrapDispatcher::normalize_vfs_path(path);
1318 if normalized.is_empty() || normalized.starts_with("__rsrc__") {
1319 return None;
1320 }
1321 self.vfs_file_summary_for_path(&normalized)
1322 }
1323
1324 pub fn vfs_file_snapshot(&mut self, path: &str) -> Option<VfsFileSnapshot> {
1325 let normalized = TrapDispatcher::normalize_vfs_path(path);
1326 if normalized.is_empty() || normalized.starts_with("__rsrc__") {
1327 return None;
1328 }
1329 if !self.dispatcher.vfs.contains_key(&normalized)
1330 && !self.dispatcher.vfs_rsrc.contains_key(&normalized)
1331 {
1332 return None;
1333 }
1334 let metadata = self.dispatcher.vfs_file_metadata(&normalized)?;
1335 Some(VfsFileSnapshot {
1336 path: normalized.clone(),
1337 data_fork: self
1338 .dispatcher
1339 .vfs
1340 .get(&normalized)
1341 .cloned()
1342 .unwrap_or_default(),
1343 resource_fork: self
1344 .dispatcher
1345 .vfs_rsrc
1346 .get(&normalized)
1347 .cloned()
1348 .unwrap_or_default(),
1349 file_type: metadata.file_type,
1350 creator: metadata.creator,
1351 finder_flags: metadata.finder_flags,
1352 created_date: metadata.created_date,
1353 modified_date: metadata.modified_date,
1354 })
1355 }
1356
1357 pub fn import_vfs_file(&mut self, file: &VfsFileSnapshot) {
1358 let normalized = TrapDispatcher::normalize_vfs_path(&file.path);
1359 if normalized.is_empty() || normalized.starts_with("__rsrc__") {
1360 return;
1361 }
1362
1363 self.dispatcher
1364 .vfs
1365 .insert(normalized.clone(), file.data_fork.clone());
1366 self.dispatcher
1367 .vfs_rsrc
1368 .insert(normalized.clone(), file.resource_fork.clone());
1369 self.dispatcher.ensure_vfs_file_metadata(&normalized);
1370 if let Some(metadata) = self.dispatcher.vfs_metadata.get_mut(&normalized) {
1371 metadata.file_type = file.file_type;
1372 metadata.creator = file.creator;
1373 metadata.finder_flags = file.finder_flags;
1374 if file.created_date != 0 {
1375 metadata.created_date = file.created_date;
1376 }
1377 if file.modified_date != 0 {
1378 metadata.modified_date = file.modified_date;
1379 }
1380 }
1381 }
1382
1383 pub fn import_vfs_file_relative_to_launched_app(
1384 &mut self,
1385 relative_dir: &str,
1386 file: &VfsFileSnapshot,
1387 ) -> std::result::Result<(), String> {
1388 let app_path = self
1389 .dispatcher
1390 .launched_app_path
1391 .clone()
1392 .ok_or_else(|| "launched app path is not available".to_string())?;
1393 let app_parent = TrapDispatcher::vfs_parent_path(&app_path);
1394 if app_parent.is_empty() {
1395 return Err("launched app has no parent folder".to_string());
1396 }
1397
1398 let relative_dir = TrapDispatcher::normalize_vfs_path(relative_dir);
1399 if relative_dir.is_empty() || relative_dir.starts_with('/') || relative_dir.contains("..") {
1400 return Err(format!("invalid relative VFS directory {relative_dir:?}"));
1401 }
1402 let file_path = TrapDispatcher::normalize_vfs_path(&file.path);
1403 let filename = TrapDispatcher::vfs_basename(&file_path);
1404 if filename.is_empty() {
1405 return Err("plugin file has no filename".to_string());
1406 }
1407
1408 let mut mounted = file.clone();
1409 mounted.path = format!("{app_parent}/{relative_dir}/{filename}");
1410 self.import_vfs_file(&mounted);
1411 Ok(())
1412 }
1413
1414 pub fn remove_vfs_file(&mut self, path: &str) -> bool {
1415 self.dispatcher.remove_vfs_path(path)
1416 }
1417
1418 fn vfs_file_paths(&mut self) -> Vec<String> {
1419 self.dispatcher.ensure_vfs_catalog();
1420 let mut paths = BTreeSet::new();
1421 for path in self.dispatcher.vfs.keys() {
1422 if !path.starts_with("__rsrc__") {
1423 paths.insert(TrapDispatcher::normalize_vfs_path(path));
1424 }
1425 }
1426 for path in self.dispatcher.vfs_rsrc.keys() {
1427 if !path.starts_with("__rsrc__") {
1428 paths.insert(TrapDispatcher::normalize_vfs_path(path));
1429 }
1430 }
1431 paths.into_iter().filter(|path| !path.is_empty()).collect()
1432 }
1433
1434 fn vfs_file_summary_for_path(&mut self, path: &str) -> Option<VfsFileSummary> {
1435 let stat = self.vfs_file_stat_for_path(path)?;
1436 let data_fork = self
1437 .dispatcher
1438 .vfs
1439 .get(path)
1440 .map(Vec::as_slice)
1441 .unwrap_or(&[]);
1442 let resource_fork = self
1443 .dispatcher
1444 .vfs_rsrc
1445 .get(path)
1446 .map(Vec::as_slice)
1447 .unwrap_or(&[]);
1448 Some(VfsFileSummary {
1449 path: stat.path,
1450 data_len: stat.data_len,
1451 resource_len: stat.resource_len,
1452 data_hash: vfs_fork_hash(data_fork),
1453 resource_hash: vfs_fork_hash(resource_fork),
1454 file_type: stat.file_type,
1455 creator: stat.creator,
1456 finder_flags: stat.finder_flags,
1457 created_date: stat.created_date,
1458 modified_date: stat.modified_date,
1459 })
1460 }
1461
1462 fn vfs_file_stat_for_path(&mut self, path: &str) -> Option<VfsFileStat> {
1463 let metadata = self.dispatcher.vfs_file_metadata(path)?;
1464 let data_len = self.dispatcher.vfs.get(path).map(Vec::len).unwrap_or(0);
1465 let resource_len = self
1466 .dispatcher
1467 .vfs_rsrc
1468 .get(path)
1469 .map(Vec::len)
1470 .unwrap_or(0);
1471 Some(VfsFileStat {
1472 path: path.to_string(),
1473 data_len,
1474 resource_len,
1475 file_type: metadata.file_type,
1476 creator: metadata.creator,
1477 finder_flags: metadata.finder_flags,
1478 created_date: metadata.created_date,
1479 modified_date: metadata.modified_date,
1480 })
1481 }
1482
1483 pub fn step(&mut self) -> StepResult {
1489 self.cpu.step(&mut self.bus)
1490 }
1491
1492 pub fn load_app(&mut self, fork: &ResourceFork) -> Option<LoadedApp> {
1504 let app = load_app_generic(fork, &mut self.bus, self.config.load_address)?;
1505 let heap_start = app_heap_start_for_loaded_app(&app);
1506
1507 self.bus
1512 .reserve_heap_until(heap_start.saturating_add(APP_ZONE_HEADER_SIZE));
1513 self.dispatcher.load_resources(fork, &mut self.bus);
1514
1515 let segments: HashMap<i16, u32> = app.segment_bases.iter().map(|(&k, &v)| (k, v)).collect();
1516 self.dispatcher.register_segments(segments);
1517
1518 Some(app)
1519 }
1520
1521 fn clear_startup_framebuffer(&mut self) {
1522 if self.menu_bar_visible() {
1523 let (scrn_base, row_bytes, screen_width, screen_height, pixel_size) =
1524 self.dispatcher.screen_mode;
1525 TrapDispatcher::fb_fill_pattern_rect(
1526 &mut self.bus,
1527 scrn_base,
1528 row_bytes,
1529 pixel_size,
1530 screen_width as i16,
1531 screen_height as i16,
1532 0,
1533 0,
1534 screen_height as i16,
1535 screen_width as i16,
1536 [0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55],
1537 );
1538 return;
1539 }
1540
1541 let (scrn_base, row_bytes, _, scrn_height, _) = self.dispatcher.screen_mode;
1542 self.bus
1543 .fill_bytes(scrn_base, row_bytes * scrn_height as u32, 0xFF);
1544 }
1545
1546 fn switch_to_launched_application(
1547 &mut self,
1548 app_path: &str,
1549 ) -> std::result::Result<(), String> {
1550 use crate::memory::globals::addr;
1551
1552 let normalized = TrapDispatcher::normalize_vfs_path(app_path);
1553 let rsrc_key = self
1554 .dispatcher
1555 .find_vfs_rsrc_file(&normalized)
1556 .ok_or_else(|| format!("no resource fork for launched application {normalized:?}"))?;
1557 let rsrc_bytes = self
1558 .dispatcher
1559 .vfs_rsrc
1560 .get(&rsrc_key)
1561 .cloned()
1562 .ok_or_else(|| format!("resource fork {rsrc_key:?} disappeared before launch"))?;
1563 let fork = ResourceFork::parse(&rsrc_bytes)
1564 .ok_or_else(|| format!("failed to parse launched application {normalized:?}"))?;
1565
1566 let ram_size = self.bus.ram_size() as usize;
1567 let config = FixtureRunnerConfig {
1568 max_instructions: self.config.max_instructions,
1569 load_address: self.config.load_address,
1570 arrows_as_numpad: self.config.arrows_as_numpad,
1571 ui_theme: self.config.ui_theme,
1572 theme_metrics_mode: self.config.theme_metrics_mode,
1573 };
1574 let menu_bar_visible = self.menu_bar_visible();
1575 let instructions_per_tick = self.instructions_per_tick;
1576 let wait_sleep_cap_in_headless = self.wait_sleep_cap_in_headless;
1577 let app_start_time = self.app_start_time;
1578 let application_partition_size = self.application_partition_size;
1579 let total_instructions = self.total_instructions;
1580 let launch_tick = self.guest_tick();
1581 let launch_time = self.bus.read_long(addr::TIME);
1582 let mouse_pos = self.dispatcher.mouse_pos;
1583 let mouse_button = self.dispatcher.mouse_button;
1584 let output_dir = self.dispatcher.output_dir.clone();
1585 let vfs = self.dispatcher.vfs.clone();
1586 let vfs_rsrc = self.dispatcher.vfs_rsrc.clone();
1587 let vfs_metadata = self.dispatcher.vfs_metadata.clone();
1588 let vfs_directories = self.dispatcher.vfs_directories.clone();
1589 let vfs_directory_paths = self.dispatcher.vfs_directory_paths.clone();
1590 let locked_files = self.dispatcher.locked_files.clone();
1591 let next_vfs_dir_id = self.dispatcher.next_vfs_dir_id;
1592 let next_vfs_file_id = self.dispatcher.next_vfs_file_id;
1593 let next_vfs_timestamp = self.dispatcher.next_vfs_timestamp;
1594 let next_working_dir_refnum = self.dispatcher.next_working_dir_refnum;
1595
1596 let mut replacement = FixtureRunner::new(ram_size, config);
1597 replacement.set_menu_bar_visible(menu_bar_visible);
1598 replacement.instructions_per_tick = instructions_per_tick;
1599 replacement.tick_budget = instructions_per_tick as i32;
1600 replacement.wait_sleep_cap_in_headless = wait_sleep_cap_in_headless;
1601 replacement.app_start_time = app_start_time;
1602 replacement.application_partition_size = application_partition_size;
1603 replacement.total_instructions = total_instructions;
1604
1605 replacement.dispatcher.output_dir = output_dir;
1606 replacement.dispatcher.vfs = vfs;
1607 replacement.dispatcher.vfs_rsrc = vfs_rsrc;
1608 replacement.dispatcher.vfs_metadata = vfs_metadata;
1609 replacement.dispatcher.vfs_directories = vfs_directories;
1610 replacement.dispatcher.vfs_directory_paths = vfs_directory_paths;
1611 replacement.dispatcher.locked_files = locked_files;
1612 replacement.dispatcher.next_vfs_dir_id = next_vfs_dir_id;
1613 replacement.dispatcher.next_vfs_file_id = next_vfs_file_id;
1614 replacement.dispatcher.next_vfs_timestamp = next_vfs_timestamp;
1615 replacement.dispatcher.next_working_dir_refnum = next_working_dir_refnum;
1616 replacement.dispatcher.set_launched_app_path(&normalized);
1617
1618 let app = replacement
1619 .load_app(&fork)
1620 .ok_or_else(|| format!("failed to load launched application {normalized:?}"))?;
1621 replacement.init_app(&app);
1622 replacement.bus.write_long(addr::TICKS, launch_tick);
1623 replacement.bus.write_long(addr::TIME, launch_time);
1624 replacement.dispatcher.tick_count = launch_tick;
1625 replacement.dispatcher.mouse_pos = mouse_pos;
1626 replacement.dispatcher.mouse_button = mouse_button;
1627 replacement
1628 .bus
1629 .write_byte(addr::MB_STATE, if mouse_button { 0x00 } else { 0x80 });
1630 replacement.clear_startup_framebuffer();
1631
1632 replacement.audio = self.audio.take();
1633 replacement.audio_buffer = std::mem::take(&mut self.audio_buffer);
1634
1635 eprintln!("[LAUNCH] Switched foreground application to {normalized}");
1636 *self = replacement;
1637 Ok(())
1638 }
1639
1640 fn service_pending_launch_application(&mut self, event_yield_reached: bool) -> bool {
1641 let Some(path) = self
1642 .dispatcher
1643 .take_pending_launch_application(event_yield_reached)
1644 else {
1645 return false;
1646 };
1647
1648 if let Err(err) = self.switch_to_launched_application(&path) {
1649 eprintln!("[LAUNCH] Failed to switch to queued application {path:?}: {err}");
1650 self.halted = true;
1651 self.halted_pc = Some(self.cpu.read_reg(Register::PC));
1652 self.halted_sp = Some(self.cpu.read_reg(Register::A7));
1653 self.halted_d0 = Some((-43i32) as u32);
1654 }
1655 true
1656 }
1657
1658 pub(crate) fn merge_resources_into_application(&mut self, fork: &ResourceFork) -> usize {
1659 self.dispatcher
1660 .merge_resources_into_existing_file(fork, &mut self.bus, 0)
1661 }
1662
1663 fn alloc_handle_with_bytes(&mut self, bytes: &[u8]) -> u32 {
1664 let data_ptr = if bytes.is_empty() {
1665 0
1666 } else {
1667 let data_ptr = self.bus.alloc(bytes.len() as u32);
1668 if data_ptr == 0 {
1669 return 0;
1670 }
1671 self.bus.write_bytes(data_ptr, bytes);
1672 data_ptr
1673 };
1674
1675 let handle = self.bus.alloc(4);
1676 if handle == 0 {
1677 if data_ptr != 0 {
1678 self.bus.free(data_ptr);
1679 }
1680 return 0;
1681 }
1682
1683 self.bus.write_long(handle, data_ptr);
1684 if data_ptr != 0 {
1685 self.dispatcher.ptr_to_handle.insert(data_ptr, handle);
1686 }
1687 handle
1688 }
1689
1690 pub fn init_app(&mut self, app: &LoadedApp) {
1702 use crate::memory::globals::addr;
1703 let ram_size = self.bus.ram_size();
1704
1705 self.bus.write_long(addr::MEM_TOP, ram_size);
1707 let app_globals_start = app.a5_base.saturating_sub(app.code0_header.below_a5);
1722 self.bus.write_long(addr::CUR_STACK_BASE, app_globals_start);
1723 self.bus.write_long(addr::CURRENT_A5, app.a5_base);
1724 self.bus.write_word(addr::ROM85, 0x0000);
1725 self.bus.write_byte(addr::MMU32_BIT, 1);
1730 self.bus.write_long(addr::TICKS, 0);
1739 let time = self
1740 .app_start_time
1741 .unwrap_or_else(current_mac_epoch_seconds);
1742 self.bus.write_long(addr::TIME, time);
1743 self.bus.write_long(addr::RND_SEED, time);
1750 self.bus.write_byte(addr::MB_STATE, 0x80);
1753 self.sync_key_map_lowmem();
1757 self.bus.write_word(CURSOR_TASK_NOOP_ADDR, 0x4E75);
1764 self.bus
1765 .write_long(addr::J_CRSR_TASK, CURSOR_TASK_NOOP_ADDR);
1766 self.bus.write_word(addr::MBAR_HEIGHT, 20);
1770 self.bus.write_byte(addr::SD_VOLUME, 1);
1779
1780 let heap_start = app_heap_start_for_loaded_app(app);
1787 let zone_header_size: u32 = APP_ZONE_HEADER_SIZE;
1788 let initial_heap_end = heap_start + zone_header_size;
1789 let stack_base = app.initial_sp;
1790 let default_appl_limit = stack_base - APP_STACK_SAFETY_MARGIN;
1791 let requested_partition_size = self.application_partition_size.or_else(|| {
1792 app.size_resource
1793 .and_then(|size| size.preferred_partition_size())
1794 });
1795 let appl_limit = requested_partition_size
1796 .and_then(|partition_size| {
1797 partition_size
1806 .checked_sub(APP_STACK_SAFETY_MARGIN)
1807 .and_then(|heap_span| heap_start.checked_add(heap_span))
1808 })
1809 .filter(|&limit| limit > initial_heap_end && limit < default_appl_limit)
1810 .unwrap_or(default_appl_limit.max(initial_heap_end));
1811 let buf_ptr = appl_limit; self.bus.write_long(addr::SYS_ZONE, heap_start);
1813 self.bus.write_long(addr::APP_L_ZONE, heap_start);
1814 self.bus.write_long(addr::HEAP_END, initial_heap_end);
1815 self.bus.write_long(addr::APPL_LIMIT, appl_limit);
1816 self.bus.write_long(addr::BUF_PTR, buf_ptr);
1817 self.bus.write_long(addr::THE_ZONE, heap_start);
1818
1819 self.bus.write_word(addr::CUR_APREF_NUM, 0); if let Some(app_path) = &self.dispatcher.launched_app_path {
1825 let app_name = crate::trap::dispatch::TrapDispatcher::vfs_basename(app_path);
1826 let name_bytes = app_name.as_bytes();
1827 let len = name_bytes.len().min(31);
1828 self.bus.write_byte(addr::CUR_APNAME, len as u8);
1829 for (i, &b) in name_bytes.iter().take(len).enumerate() {
1830 self.bus.write_byte(addr::CUR_APNAME + 1 + i as u32, b);
1831 }
1832 }
1833
1834 let app_dir_id = self.dispatcher.default_dir_id;
1840 self.bus.write_long(addr::CUR_DIR_STORE, app_dir_id);
1841 self.bus.write_word(
1842 addr::SF_SAVE_DISK,
1843 (-crate::trap::dispatch::BOOT_VOLUME_REF_NUM) as u16,
1844 );
1845 eprintln!(
1846 "[INIT] CurDirStore={} SFSaveDisk={}",
1847 app_dir_id,
1848 (-crate::trap::dispatch::BOOT_VOLUME_REF_NUM) as u16
1849 );
1850
1851 let stack_seed_start = stack_base.saturating_sub(0x8000);
1859 self.bus
1860 .fill_bytes(stack_seed_start, stack_base - stack_seed_start, 0xA5);
1861
1862 self.bus.reserve_heap_until(initial_heap_end);
1867 let zone_size = appl_limit.saturating_sub(heap_start);
1868 let free_bytes = zone_size.saturating_sub(zone_header_size);
1869 self.bus.write_long(heap_start, appl_limit); self.bus
1871 .write_long(heap_start + 8, heap_start + zone_header_size); self.bus.write_long(heap_start + 12, free_bytes); self.bus
1874 .write_long(heap_start + 56, heap_start + zone_header_size); eprintln!(
1876 "[INIT] Zone header: start=${:08X} bkLim=${:08X} zcbFree={} ({:.1}MB)",
1877 heap_start,
1878 appl_limit,
1879 free_bytes,
1880 free_bytes as f64 / (1024.0 * 1024.0)
1881 );
1882
1883 let app_param_handle = self.alloc_handle_with_bytes(&[0, 0, 0, 0]);
1889 self.bus.write_long(addr::APP_PARM_HANDLE, app_param_handle);
1890
1891 let exit_trampoline = 0x100u32;
1895 self.bus.write_word(exit_trampoline, 0xA9F4); let gdh = self.dispatcher.ensure_main_gdevice(&mut self.bus);
1901 let gd_ptr = self.bus.read_long(gdh);
1902 self.bus.write_long(0x8A4, gdh); self.bus.write_long(0xCC8, gdh); self.bus.write_long(0x8A8, gdh); eprintln!(
1906 "[INIT] Set MainDevice=${:08X}, TheGDevice=${:08X} (ptr=${:08X})",
1907 gdh, gdh, gd_ptr
1908 );
1909
1910 let pmap_h = self.bus.read_long(gd_ptr + 22);
1912 let pmap = self.bus.read_long(pmap_h);
1913 let scrn_base = self.bus.read_long(pmap);
1914 let rb = (self.bus.read_word(pmap + 4) & 0x3FFF) as u32;
1915 let top = self.bus.read_word(pmap + 6) as i16;
1916 let left = self.bus.read_word(pmap + 8) as i16;
1917 let bottom = self.bus.read_word(pmap + 10) as i16;
1918 let right = self.bus.read_word(pmap + 12) as i16;
1919 let pixel_size = self.bus.read_word(pmap + 32);
1920 let width = (right - left).max(1) as u16;
1921 let height = (bottom - top).max(1) as u16;
1922 self.dispatcher.screen_mode = (scrn_base, rb, width, height, pixel_size);
1923
1924 {
1926 let main_dev = self.bus.read_long(0x8A4);
1927 let gd = self.bus.read_long(main_dev);
1928 let pmap_h = self.bus.read_long(gd + 22);
1929 let pmap = self.bus.read_long(pmap_h);
1930 let rb = self.bus.read_word(pmap + 4);
1931 let top = self.bus.read_word(pmap + 6) as i16;
1932 let left = self.bus.read_word(pmap + 8) as i16;
1933 let bottom = self.bus.read_word(pmap + 10) as i16;
1934 let right = self.bus.read_word(pmap + 12) as i16;
1935 let ps = self.bus.read_word(pmap + 32);
1936 let gd_top = self.bus.read_word(gd + 34) as i16;
1937 let gd_left = self.bus.read_word(gd + 36) as i16;
1938 let gd_bottom = self.bus.read_word(gd + 38) as i16;
1939 let gd_right = self.bus.read_word(gd + 40) as i16;
1940 let gd_flags = self.bus.read_word(gd + 20);
1941 eprintln!(
1942 "[INIT] GDevice chain: $8A4→${:08X}→${:08X} gdPMap→${:08X}→${:08X}",
1943 main_dev, gd, pmap_h, pmap
1944 );
1945 eprintln!(
1946 "[INIT] PixMap: rowBytes=${:04X} bounds=({},{},{},{}) pixelSize={}",
1947 rb, top, left, bottom, right, ps
1948 );
1949 eprintln!(
1950 "[INIT] GDevice: gdRect=({},{},{},{}) gdFlags=${:04X}",
1951 gd_top, gd_left, gd_bottom, gd_right, gd_flags
1952 );
1953 }
1954
1955 self.cpu.write_reg(Register::A5, app.a5_base);
1957 let sp = app.initial_sp.wrapping_sub(4);
1959 self.bus.write_long(sp, exit_trampoline);
1960 self.cpu.write_reg(Register::A7, sp);
1961 self.cpu.write_reg(Register::A6, sp);
1966 self.cpu
1967 .write_reg(Register::PC, app.entry_point(app.a5_base));
1968 }
1969
1970 pub fn mix_audio(&mut self, num_samples: usize) {
1973 self.mix_host_audio(num_samples);
1974 }
1975
1976 fn queue_mixed_audio(&mut self, stereo_samples: &[u8]) {
1977 if stereo_samples.is_empty() {
1978 return;
1979 }
1980 if let Some(ref mut audio) = self.audio {
1981 audio.queue_stereo_samples(stereo_samples);
1982 }
1983 for frame in stereo_samples.chunks_exact(2) {
1984 let left = frame[0] as i32 - 0x80;
1985 let right = frame[1] as i32 - 0x80;
1986 self.audio_buffer
1987 .push(((left + right) / 2 + 0x80).clamp(0, 255) as u8);
1988 }
1989 }
1990
1991 fn queue_host_silence_audio(&mut self, num_samples: usize) {
1992 if num_samples == 0 {
1993 return;
1994 }
1995 if let Some(ref mut audio) = self.audio {
1996 audio.queue_stereo_samples(&vec![0x80; num_samples * 2]);
1997 }
1998 }
1999
2000 fn mix_host_audio(&mut self, mut remaining_samples: usize) {
2001 while remaining_samples > 0 {
2002 self.try_load_pending_double_buffers();
2003 self.dispatcher.service_guest_sound_queues(&mut self.bus);
2004
2005 let chunk = self
2006 .dispatcher
2007 .sound_manager
2008 .samples_until_next_exhaustion()
2009 .map(|samples| samples.max(1).min(remaining_samples))
2010 .unwrap_or(remaining_samples);
2011
2012 let samples = self.dispatcher.sound_manager.mix_frame_stereo(chunk);
2013 if samples.is_empty() {
2014 self.queue_host_silence_audio(remaining_samples);
2015 self.dispatcher
2016 .release_finished_internal_sound_channels(&mut self.bus);
2017 break;
2018 }
2019 self.queue_mixed_audio(&samples);
2020 self.dispatcher
2021 .release_finished_internal_sound_channels(&mut self.bus);
2022 remaining_samples -= chunk;
2023 }
2024 }
2025
2026 fn finish_host_frame(&mut self, audio_samples: usize, sound_interrupt_dispatched: bool) {
2027 self.dispatcher.redraw_chrome(&mut self.bus);
2032 self.finish_audio_frame(audio_samples, sound_interrupt_dispatched);
2033 }
2034
2035 fn finish_audio_frame(&mut self, audio_samples: usize, sound_interrupt_dispatched: bool) {
2036 self.try_load_pending_double_buffers();
2038
2039 self.dispatcher.service_guest_sound_queues(&mut self.bus);
2042
2043 if audio_samples > 0 {
2045 self.mix_host_audio(audio_samples);
2046 }
2047
2048 self.dispatcher
2049 .sync_guest_sound_channel_state(&mut self.bus);
2050
2051 if !sound_interrupt_dispatched {
2052 let fired_sound_callback = self.fire_sound_callbacks();
2053 if !fired_sound_callback {
2054 self.fire_sound_doubleback_callbacks();
2056 }
2057 }
2058 }
2059
2060 pub fn mix_gui_audio_slice(&mut self, audio_samples: usize) {
2065 self.finish_audio_frame(audio_samples, false);
2066 }
2067
2068 fn try_tickcount_spin_fastfwd(
2076 &mut self,
2077 pc_after_trap: u32,
2078 tick_cap: Option<u32>,
2079 count: &mut usize,
2080 ) -> bool {
2081 let w0 = self.bus.read_word(pc_after_trap);
2083 if (w0 & 0xF1FF) != 0x201F {
2084 return false;
2085 }
2086 let dn = ((w0 >> 9) & 7) as usize;
2087
2088 let w1 = self.bus.read_word(pc_after_trap.wrapping_add(2));
2089
2090 if (w1 & 0xF1F8) == 0x5180 && (w1 & 0x0007) as usize == dn {
2092 return self.try_spin_template_a(pc_after_trap, dn, w1, tick_cap, count);
2093 }
2094
2095 if (w1 & 0xF1F8) == 0xB0A8 && ((w1 >> 9) & 7) as usize == dn {
2097 return self.try_spin_template_b(pc_after_trap, dn, w1, tick_cap, count);
2098 }
2099
2100 if (w1 & 0xF1FF) == 0xB0B9 && ((w1 >> 9) & 7) as usize == dn {
2102 return self.try_spin_template_c(pc_after_trap, dn, tick_cap, count);
2103 }
2104
2105 false
2106 }
2107
2108 fn try_spin_template_a(
2114 &mut self,
2115 pc_after_trap: u32,
2116 dn: usize,
2117 w1: u16,
2118 tick_cap: Option<u32>,
2119 count: &mut usize,
2120 ) -> bool {
2121 let imm_bits = ((w1 >> 9) & 7) as u32;
2122 let imm = if imm_bits == 0 { 8 } else { imm_bits };
2123
2124 let w2 = self.bus.read_word(pc_after_trap.wrapping_add(4));
2125 let w3 = self.bus.read_word(pc_after_trap.wrapping_add(6));
2126
2127 if (w2 & 0xF1F8) != 0xB080 || (w2 & 0x0007) as usize != dn {
2129 return false;
2130 }
2131 let dm = ((w2 >> 9) & 7) as usize;
2132
2133 if (w3 & 0xFF00) != 0x6200 {
2135 return false;
2136 }
2137 let disp8 = (w3 & 0xFF) as i8 as i32;
2138 if disp8 == 0 {
2139 return false;
2140 }
2141 let branch_src = pc_after_trap.wrapping_add(6);
2142 let target = (branch_src.wrapping_add(2) as i32).wrapping_add(disp8) as u32;
2143 if target != pc_after_trap.wrapping_sub(4) {
2144 return false;
2145 }
2146
2147 let dm_val = self.cpu.core.d(dm);
2148 let target_tick = dm_val.wrapping_add(imm);
2149 match self.advance_until_tick(target_tick, tick_cap) {
2150 AdvanceResult::CapHit => return true,
2151 AdvanceResult::Interrupted | AdvanceResult::TooFar => return false,
2152 AdvanceResult::Advanced => {}
2153 }
2154
2155 let final_tick = self.dispatcher.tick_count;
2158 let sp = self.cpu.core.a(7);
2159 self.cpu.core.set_a(7, sp.wrapping_add(4));
2160 self.cpu.core.set_d(dn, final_tick.wrapping_sub(imm));
2161 self.cpu.core.pc = pc_after_trap.wrapping_add(8);
2162
2163 *count += 4;
2164 self.total_instructions = self.total_instructions.wrapping_add(4);
2165 false
2166 }
2167
2168 fn try_spin_template_b(
2176 &mut self,
2177 pc_after_trap: u32,
2178 dn: usize,
2179 w1: u16,
2180 tick_cap: Option<u32>,
2181 count: &mut usize,
2182 ) -> bool {
2183 let an = (w1 & 7) as usize;
2184 let d16 = self.bus.read_word(pc_after_trap.wrapping_add(4)) as i16 as i32;
2185 let w_brk = self.bus.read_word(pc_after_trap.wrapping_add(6));
2186
2187 if (w_brk & 0xFF00) != 0x6300 {
2189 return false;
2190 }
2191 let disp8 = (w_brk & 0xFF) as i8 as i32;
2192 if disp8 == 0 {
2193 return false;
2194 }
2195 let branch_src = pc_after_trap.wrapping_add(6);
2199 let target = (branch_src.wrapping_add(2) as i32).wrapping_add(disp8) as u32;
2200 if target >= pc_after_trap || pc_after_trap.wrapping_sub(target) > 128 {
2201 return false;
2202 }
2203
2204 let an_val = self.cpu.core.a(an);
2205 let mem_addr = (an_val as i32).wrapping_add(d16) as u32;
2206 let mem_target = self.bus.read_long(mem_addr);
2207 let target_tick = mem_target.wrapping_add(1);
2208
2209 match self.advance_until_tick(target_tick, tick_cap) {
2210 AdvanceResult::CapHit => return true,
2211 AdvanceResult::Interrupted | AdvanceResult::TooFar => return false,
2212 AdvanceResult::Advanced => {}
2213 }
2214
2215 let final_tick = self.dispatcher.tick_count;
2218 let sp = self.cpu.core.a(7);
2219 self.cpu.core.set_a(7, sp.wrapping_add(4));
2220 self.cpu.core.set_d(dn, final_tick);
2221 self.cpu.core.pc = pc_after_trap.wrapping_add(8);
2222
2223 *count += 3;
2224 self.total_instructions = self.total_instructions.wrapping_add(3);
2225 false
2226 }
2227
2228 fn try_spin_template_c(
2235 &mut self,
2236 pc_after_trap: u32,
2237 dn: usize,
2238 tick_cap: Option<u32>,
2239 count: &mut usize,
2240 ) -> bool {
2241 let target_addr = self.bus.read_long(pc_after_trap.wrapping_add(4));
2242 let w_brk = self.bus.read_word(pc_after_trap.wrapping_add(8));
2243
2244 if (w_brk & 0xFF00) != 0x6500 {
2246 return false;
2247 }
2248 let disp8 = (w_brk & 0xFF) as i8 as i32;
2249 if disp8 == 0 {
2250 return false;
2251 }
2252 let branch_src = pc_after_trap.wrapping_add(8);
2253 let target = (branch_src.wrapping_add(2) as i32).wrapping_add(disp8) as u32;
2254 if target != pc_after_trap.wrapping_sub(4) {
2255 return false;
2256 }
2257
2258 let target_tick = self.bus.read_long(target_addr);
2259 match self.advance_until_tick(target_tick, tick_cap) {
2260 AdvanceResult::CapHit => return true,
2261 AdvanceResult::Interrupted | AdvanceResult::TooFar => return false,
2262 AdvanceResult::Advanced => {}
2263 }
2264
2265 let final_tick = self.dispatcher.tick_count;
2266 let sp = self.cpu.core.a(7);
2267 self.cpu.core.set_a(7, sp.wrapping_add(4));
2268 self.cpu.core.set_d(dn, final_tick);
2269 self.cpu.core.pc = pc_after_trap.wrapping_add(10);
2270
2271 *count += 3;
2272 self.total_instructions = self.total_instructions.wrapping_add(3);
2273 false
2274 }
2275
2276 fn advance_until_tick(&mut self, target_tick: u32, tick_cap: Option<u32>) -> AdvanceResult {
2279 let current_tick = self.dispatcher.tick_count;
2280 let ticks_to_advance = target_tick.wrapping_sub(current_tick);
2281 if ticks_to_advance > SPIN_FASTFWD_MAX_TICKS {
2282 return AdvanceResult::TooFar;
2283 }
2284 for _ in 0..ticks_to_advance {
2285 if let Some(cap) = tick_cap {
2286 if self.bus.read_long(0x016A) >= cap {
2287 return AdvanceResult::CapHit;
2288 }
2289 }
2290 self.advance_guest_tick();
2291 if self.active_interrupt_callback.is_some() {
2292 return AdvanceResult::Interrupted;
2293 }
2294 }
2295 AdvanceResult::Advanced
2296 }
2297
2298 fn run_steps_internal(
2299 &mut self,
2300 max_steps: usize,
2301 tick_cap: Option<u32>,
2302 audio_samples: usize,
2303 yield_for_ui: bool,
2304 sound_work_only: bool,
2305 finish_frame: bool,
2306 ) -> (usize, bool) {
2307 let real_tick_cap = tick_cap;
2315 let tick_cap = match self.frozen_ticks {
2316 Some(frozen) => tick_cap.map(|_| frozen),
2317 None => tick_cap,
2318 };
2319
2320 self.dispatcher.instruction_count = self.total_instructions;
2321 let mut count = 0;
2322 let mut tick_cap_reached = false;
2323 let mut sound_interrupt_dispatched = self
2324 .active_interrupt_callback
2325 .map(|callback| is_sound_interrupt_source(callback.source))
2326 .unwrap_or(false);
2327
2328 while count < max_steps && !self.halted && !tick_cap_reached {
2329 if sound_work_only
2330 && self.active_interrupt_callback.is_none()
2331 && (sound_interrupt_dispatched || !self.has_pending_sound_work())
2332 {
2333 break;
2334 }
2335
2336 if self.active_interrupt_callback.is_none() && !sound_interrupt_dispatched {
2343 sound_interrupt_dispatched = self.fire_sound_callbacks();
2344 if !sound_interrupt_dispatched {
2345 sound_interrupt_dispatched = self.fire_sound_doubleback_callbacks();
2346 }
2347 if sound_work_only && !sound_interrupt_dispatched {
2348 continue;
2349 }
2350 }
2351
2352 if sound_work_only && self.active_interrupt_callback.is_none() {
2353 break;
2354 }
2355
2356 if !sound_work_only {
2358 if self.service_wait_sleep_ticks(tick_cap) {
2359 break;
2360 }
2361 if self.service_delay_ticks(tick_cap) {
2362 break;
2363 }
2364 }
2365 if self.cpu.is_stopped() {
2366 self.halted = true;
2367 self.halted_pc = Some(self.cpu.read_reg(Register::PC));
2368 self.halted_sp = Some(self.cpu.read_reg(Register::A7));
2369 self.halted_d0 = Some(self.cpu.read_reg(Register::D0));
2370 self.dump_trace();
2371 return (count, false);
2372 }
2373
2374 let pc = self.cpu.read_reg(Register::PC);
2375 if trace_buffer_enabled() {
2385 let opcode = self.bus.read_word(pc);
2386 let a0 = self.cpu.read_reg(Register::A0);
2387 let a6 = self.cpu.read_reg(Register::A6);
2388 let a5 = self.cpu.read_reg(Register::A5);
2389 let sp = self.cpu.read_reg(Register::A7);
2390 if self.trace_buffer.len() >= 200 {
2391 self.trace_buffer.pop_front();
2392 }
2393 self.trace_buffer.push_back((pc, opcode, a0, sp, a6, a5));
2394 }
2395
2396 if let Some(active_interrupt_callback) = self.active_interrupt_callback {
2397 let sp = self.cpu.read_reg(Register::A7);
2398 if pc == active_interrupt_callback.resume_pc
2399 && sp == active_interrupt_callback.resume_sp
2400 {
2401 if trace_timer_enabled() {
2402 eprintln!(
2403 "[TIMER] resume {:?} pc=${:08X} sp=${:08X} restore_ccr=${:02X}",
2404 active_interrupt_callback.source, pc, sp, active_interrupt_callback.ccr
2405 );
2406 }
2407 if trace_sound_runner_enabled()
2408 && is_sound_interrupt_source(active_interrupt_callback.source)
2409 {
2410 eprintln!(
2411 "[SOUND-CB] resume {:?} pc=${:08X} sp=${:08X} restore_ccr=${:02X}",
2412 active_interrupt_callback.source, pc, sp, active_interrupt_callback.ccr
2413 );
2414 }
2415 for (index, value) in
2416 active_interrupt_callback.d_regs.iter().copied().enumerate()
2417 {
2418 self.cpu.write_reg(
2419 match index {
2420 0 => Register::D0,
2421 1 => Register::D1,
2422 2 => Register::D2,
2423 3 => Register::D3,
2424 4 => Register::D4,
2425 5 => Register::D5,
2426 6 => Register::D6,
2427 _ => Register::D7,
2428 },
2429 value,
2430 );
2431 }
2432 for (index, value) in
2433 active_interrupt_callback.a_regs.iter().copied().enumerate()
2434 {
2435 self.cpu.write_reg(
2436 match index {
2437 0 => Register::A0,
2438 1 => Register::A1,
2439 2 => Register::A2,
2440 3 => Register::A3,
2441 4 => Register::A4,
2442 5 => Register::A5,
2443 6 => Register::A6,
2444 _ => Register::A7,
2445 },
2446 value,
2447 );
2448 }
2449 self.cpu.core.set_ccr(active_interrupt_callback.ccr);
2450 if let Some((port, gdevice)) = active_interrupt_callback.restore_port {
2451 self.dispatcher.set_current_port_state(
2452 &mut self.bus,
2453 &mut self.cpu,
2454 port,
2455 Some(gdevice),
2456 );
2457 }
2458 let completed_dialog_draw_proc = matches!(
2459 active_interrupt_callback.source,
2460 ActiveInterruptCallbackSource::DialogDrawProc
2461 );
2462 let completed_modeless_dialog_draw_proc = completed_dialog_draw_proc
2463 && self.dispatcher.active_modeless_dialog_draw_proc.is_some();
2464 if completed_dialog_draw_proc {
2465 self.dispatcher
2466 .finalize_dialog_draw_procs_if_idle(&mut self.bus);
2467 }
2468 self.active_interrupt_callback = None;
2469 self.refill_foreground_budget_after_async_return();
2470 if completed_modeless_dialog_draw_proc && self.fire_modeless_dialog_draw_proc()
2471 {
2472 continue;
2473 }
2474 if sound_work_only {
2475 break;
2476 }
2477 } else if trace_timer_enabled() {
2478 eprintln!(
2479 "[TIMER] pending {:?} pc=${:08X} sp=${:08X} waiting_for pc=${:08X} sp=${:08X}",
2480 active_interrupt_callback.source,
2481 pc,
2482 sp,
2483 active_interrupt_callback.resume_pc,
2484 active_interrupt_callback.resume_sp
2485 );
2486 }
2487 }
2488
2489 if pc == 0 {
2490 if trace_load_enabled() {
2498 eprintln!(
2499 "[RUN_STEPS] App reached PC=0 (clean exit via deep RTS chain) at count={}",
2500 count
2501 );
2502 }
2503 self.dump_trace();
2504 self.halted = true;
2505 self.halted_pc = Some(0);
2506 self.halted_sp = Some(self.cpu.read_reg(Register::A7));
2507 self.halted_d0 = Some(self.cpu.read_reg(Register::D0));
2508 return (count, false);
2509 }
2510
2511 if pc >= self.bus.ram_size() || pc < 0x60 {
2512 let opcode = self.bus.read_word(pc);
2514 let sp = self.cpu.read_reg(Register::A7);
2515 eprintln!(
2516 "[RUN_STEPS] Invalid PC ${:08X} at count={} sp=${:08X} op=${:04X}",
2517 pc, count, sp, opcode
2518 );
2519 self.dump_invalid_pc_state();
2520 if let Some(hint) = decode_fakeptr_pc(pc) {
2521 eprintln!("[RUN_STEPS] {}", hint);
2522 } else if let Some((entry_pc, hint)) = self.trace_find_fakeptr_entry() {
2523 eprintln!(
2524 "[RUN_STEPS] PC drifted ${:X} bytes from a fake-ptr entry at \
2525 ${:08X}. {}",
2526 pc.wrapping_sub(entry_pc),
2527 entry_pc,
2528 hint
2529 );
2530 }
2531 self.halted = true;
2532 self.halted_pc = Some(pc);
2533 self.halted_sp = Some(sp);
2534 self.halted_d0 = Some(self.cpu.read_reg(Register::D0));
2535 self.dump_trace();
2536 return (count, false);
2537 }
2538
2539 if !sound_work_only {
2542 tick_cap_reached = self.charge_tick_budget(1, tick_cap);
2543 if tick_cap_reached {
2544 break;
2545 }
2546 }
2547
2548 #[cfg(debug_assertions)]
2550 if crate::memory::bus::watchpoint_armed() {
2551 crate::memory::bus::increment_step();
2552 crate::memory::bus::set_current_pc(pc);
2553 crate::memory::bus::set_watch_registers(
2554 self.cpu.read_reg(Register::A0),
2555 self.cpu.read_reg(Register::A1),
2556 self.cpu.read_reg(Register::A6),
2557 self.cpu.read_reg(Register::A7),
2558 );
2559 }
2560
2561 if crate::memory::bus::fb_write_trace_active()
2564 || crate::memory::bus::mem_read_trace_active()
2565 {
2566 crate::memory::bus::set_current_pc(pc);
2567 }
2568
2569 let trace_pc_range_hit = trace_pc_range_contains(pc, self.dispatcher.tick_count);
2570 if trace_pc_range_hit {
2571 let sp = self.cpu.read_reg(Register::A7);
2572 let a6 = self.cpu.read_reg(Register::A6);
2573 let stack0 = self.bus.read_long(sp);
2574 let stack4 = self.bus.read_long(sp.wrapping_add(4));
2575 let stack8 = self.bus.read_word(sp.wrapping_add(8));
2576 let frame_ret = self.bus.read_long(a6.wrapping_add(4));
2577 let frame_arg = self.bus.read_word(a6.wrapping_add(8));
2578 eprintln!(
2579 "[TRACE-PC-RANGE] pc=${:08X} op=${:04X} 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}",
2580 pc,
2581 self.bus.read_word(pc),
2582 self.cpu.core.get_ccr(),
2583 self.cpu.read_reg(Register::D0),
2584 self.cpu.read_reg(Register::D1),
2585 self.cpu.read_reg(Register::D2),
2586 self.cpu.read_reg(Register::D3),
2587 self.cpu.read_reg(Register::D4),
2588 self.cpu.read_reg(Register::D5),
2589 self.cpu.read_reg(Register::D6),
2590 self.cpu.read_reg(Register::D7),
2591 self.cpu.read_reg(Register::A0),
2592 self.cpu.read_reg(Register::A1),
2593 self.cpu.read_reg(Register::A2),
2594 self.cpu.read_reg(Register::A3),
2595 self.cpu.read_reg(Register::A4),
2596 self.cpu.read_reg(Register::A5),
2597 a6,
2598 sp,
2599 stack0,
2600 stack4,
2601 stack8,
2602 frame_ret,
2603 frame_arg,
2604 );
2605 }
2606 match self.cpu.step(&mut self.bus) {
2608 StepResult::Ok => {
2609 count += 1;
2610 self.total_instructions = self.total_instructions.wrapping_add(1);
2611 if trace_opcode_counts_enabled() {
2618 let opcode = self.cpu.core.ir as u16 as usize;
2619 self.opcode_histogram[opcode] =
2620 self.opcode_histogram[opcode].saturating_add(1);
2621 }
2622 if trace_hot_pc_enabled()
2625 && self.total_instructions.is_multiple_of(PC_SAMPLE_INTERVAL)
2626 {
2627 *self.pc_histogram.entry(pc).or_insert(0) += 1;
2628 }
2629 }
2630 StepResult::Stopped => {
2631 let halted_pc = self.cpu.read_reg(Register::PC);
2632 eprintln!(
2633 "[RUN_STEPS] CPU stopped at count={} pc=${:08X} op=${:04X}",
2634 count,
2635 halted_pc,
2636 self.bus.read_word(halted_pc)
2637 );
2638 if let Some(hint) = decode_fakeptr_pc(halted_pc) {
2639 eprintln!("[RUN_STEPS] {}", hint);
2640 } else if let Some((entry_pc, hint)) = self.trace_find_fakeptr_entry() {
2641 eprintln!(
2642 "[RUN_STEPS] PC drifted ${:X} bytes from a fake-ptr entry at \
2643 ${:08X}. {}",
2644 halted_pc.wrapping_sub(entry_pc),
2645 entry_pc,
2646 hint
2647 );
2648 }
2649 self.halted = true;
2650 self.halted_pc = Some(halted_pc);
2651 self.halted_sp = Some(self.cpu.read_reg(Register::A7));
2652 self.halted_d0 = Some(self.cpu.read_reg(Register::D0));
2653 self.dump_trace();
2654 return (count, false);
2655 }
2656 StepResult::Aline(opcode) => {
2657 count += 1;
2658 self.total_instructions = self.total_instructions.wrapping_add(1);
2659
2660 if opcode == 0xA975 {
2689 let sp = self.cpu.core.a(7);
2690 let tick = self.dispatcher.tick_count;
2691 self.bus.write_long(sp, tick);
2692 self.dispatcher.trap_count += 1;
2693 self.dispatcher.current_trap_word = opcode;
2694 let idx = (opcode & 0xFFF) as usize;
2695 self.dispatcher.trap_histogram[idx] =
2696 self.dispatcher.trap_histogram[idx].saturating_add(1);
2697 self.dispatcher.inline_skipped[idx] =
2700 self.dispatcher.inline_skipped[idx].saturating_add(1);
2701
2702 if spin_wait_fastfwd_enabled_for(yield_for_ui) {
2709 let post_trap_pc = pc.wrapping_add(2);
2710 let hit_cap =
2711 self.try_tickcount_spin_fastfwd(post_trap_pc, tick_cap, &mut count);
2712 if hit_cap {
2713 break;
2714 }
2715 }
2716 continue;
2717 }
2718
2719 if opcode == 0xA991 {
2729 let (
2733 has_tracking,
2734 filter_allows_noop,
2735 flash_remaining_zero,
2736 draw_procs_done,
2737 rendered_pixels_final,
2738 ) = self
2739 .dispatcher
2740 .dialog_tracking
2741 .as_ref()
2742 .map(|t| {
2743 let idle_dialog_mouse =
2744 self.dispatcher.mouse_down_over_dialog_button()
2745 || self.dispatcher.mouse_down_over_dialog_plain_user_item()
2746 || (self.dispatcher.event_queue.is_empty()
2747 && self
2748 .dispatcher
2749 .pending_dialog_plain_user_item_mouse_down());
2750 let paced_filter_idle = t.filter_proc != 0
2751 && t.last_filter_event.is_none()
2752 && !idle_dialog_mouse
2753 && self.dialog_filter_null_event_already_sent_this_tick(
2754 t.dialog_ptr,
2755 )
2756 && !self.dialog_filter_has_real_event_pending(t.dialog_ptr);
2757 (
2758 true,
2759 t.filter_proc == 0 || paced_filter_idle,
2760 t.flash_remaining == 0,
2761 t.draw_procs_done,
2762 t.rendered_pixels_final,
2763 )
2764 })
2765 .unwrap_or((false, false, false, false, false));
2766 let noop_refire = modaldialog_refire_is_noop(
2767 yield_for_ui,
2768 has_tracking,
2769 filter_allows_noop,
2770 flash_remaining_zero,
2771 draw_procs_done,
2772 rendered_pixels_final,
2773 self.dispatcher.event_queue.is_empty(),
2774 );
2775 if noop_refire {
2776 let idx = (opcode & 0xFFF) as usize;
2782 self.dispatcher.trap_count += 1;
2783 self.dispatcher.current_trap_word = opcode;
2784 self.dispatcher.trap_histogram[idx] =
2785 self.dispatcher.trap_histogram[idx].saturating_add(1);
2786 self.dispatcher.inline_skipped[idx] =
2790 self.dispatcher.inline_skipped[idx].saturating_add(1);
2791 const BATCH: u32 = 64;
2792 let mut budget = BATCH - 1;
2793 while budget > 0 && count < max_steps && !tick_cap_reached {
2794 tick_cap_reached = self.charge_tick_budget(1, tick_cap);
2795 if tick_cap_reached {
2796 break;
2797 }
2798 count += 1;
2799 self.total_instructions = self.total_instructions.wrapping_add(1);
2800 self.dispatcher.trap_count += 1;
2801 self.dispatcher.trap_histogram[idx] =
2802 self.dispatcher.trap_histogram[idx].saturating_add(1);
2803 self.dispatcher.inline_skipped[idx] =
2804 self.dispatcher.inline_skipped[idx].saturating_add(1);
2805 budget -= 1;
2806 }
2807 self.cpu.write_reg(Register::PC, pc);
2808 continue;
2809 }
2810 }
2811
2812 if opcode == 0xA8AD {
2818 let sp = self.cpu.core.a(7);
2819 let rect_ptr = self.bus.read_long(sp);
2820 let pt_v = self.bus.read_word(sp + 4) as i16;
2821 let pt_h = self.bus.read_word(sp + 6) as i16;
2822 let top = self.bus.read_word(rect_ptr) as i16;
2823 let left = self.bus.read_word(rect_ptr + 2) as i16;
2824 let bottom = self.bus.read_word(rect_ptr + 4) as i16;
2825 let right = self.bus.read_word(rect_ptr + 6) as i16;
2826 let in_rect = pt_v >= top && pt_v < bottom && pt_h >= left && pt_h < right;
2827 self.bus
2828 .write_word(sp + 8, if in_rect { 0x0100 } else { 0 });
2829 self.cpu.write_reg(Register::A7, sp + 8);
2830
2831 self.dispatcher.trap_count += 1;
2832 self.dispatcher.current_trap_word = opcode;
2833 if pc < 0x0080_0000
2834 && !self.dispatcher.is_menu_tracking()
2835 && !self.dispatcher.is_dialog_tracking()
2836 && !self.dispatcher.is_control_tracking()
2837 {
2838 self.dispatcher.game_trap_count += 1;
2839 }
2840 let idx = (opcode & 0xFFF) as usize;
2841 self.dispatcher.trap_histogram[idx] =
2842 self.dispatcher.trap_histogram[idx].saturating_add(1);
2843 self.dispatcher.inline_skipped[idx] =
2844 self.dispatcher.inline_skipped[idx].saturating_add(1);
2845 if self.service_pending_launch_application(true) {
2846 if self.halted {
2847 return (count, false);
2848 }
2849 continue;
2850 }
2851 continue;
2852 }
2853
2854 if opcode == 0xA971 {
2859 let sp = self.cpu.core.a(7);
2860 let event_ptr = self.bus.read_long(sp);
2861 let event_mask = self.bus.read_word(sp + 4);
2862
2863 if let Some(ev) = self.dispatcher.peek_toolbox_event(&self.bus, event_mask)
2864 {
2865 self.dispatcher.write_event_record(
2866 &mut self.bus,
2867 event_ptr,
2868 ev.what,
2869 ev.message,
2870 ev.where_v,
2871 ev.where_h,
2872 ev.modifiers,
2873 );
2874 self.bus.write_word(sp + 6, 0xFFFF);
2875 } else {
2876 self.dispatcher.write_event_record(
2877 &mut self.bus,
2878 event_ptr,
2879 0,
2880 0,
2881 self.dispatcher.mouse_pos.0,
2882 self.dispatcher.mouse_pos.1,
2883 self.dispatcher.current_event_modifiers(),
2884 );
2885 self.bus.write_word(sp + 6, 0);
2886 }
2887 self.cpu.write_reg(Register::A7, sp + 6);
2888
2889 self.dispatcher.trap_count += 1;
2890 self.dispatcher.current_trap_word = opcode;
2891 let idx = (opcode & 0xFFF) as usize;
2892 self.dispatcher.trap_histogram[idx] =
2893 self.dispatcher.trap_histogram[idx].saturating_add(1);
2894 self.dispatcher.inline_skipped[idx] =
2895 self.dispatcher.inline_skipped[idx].saturating_add(1);
2896 continue;
2897 }
2898
2899 self.dispatcher.yield_for_ui = yield_for_ui;
2900 match self
2901 .dispatcher
2902 .dispatch(opcode, &mut self.cpu, &mut self.bus)
2903 {
2904 Ok(()) => {
2905 let extra_tick_cost = hle_trap_extra_tick_cost(opcode)
2906 .saturating_add(self.dispatcher.take_hle_tick_cost());
2907 if extra_tick_cost > 0
2908 && self.charge_tick_budget(extra_tick_cost, tick_cap)
2909 {
2910 tick_cap_reached = true;
2911 }
2912 let is_tracking_refire = self.dispatcher.is_tracking_refire(opcode);
2926 if is_tracking_refire {
2927 if yield_for_ui
2935 && self.frozen_ticks.is_none()
2936 && tracking_refire_should_freeze_ticks(opcode)
2937 {
2938 self.frozen_ticks = Some(self.bus.read_long(0x016A));
2939 }
2940 if yield_for_ui
2941 && self.frozen_ticks.is_some()
2942 && !tracking_refire_should_freeze_ticks(opcode)
2943 {
2944 self.unfreeze_ticks_to(real_tick_cap);
2945 }
2946 self.cpu.write_reg(Register::PC, pc);
2947
2948 let fired_menu_hook = self.fire_menu_hook_proc(opcode);
2954
2955 let fired_draw_proc = if fired_menu_hook {
2960 false
2961 } else {
2962 self.fire_dialog_draw_procs()
2963 };
2964 let mut fired_filter_proc = false;
2965 if !fired_menu_hook && !fired_draw_proc {
2966 if self.should_fire_dialog_filter_proc() {
2972 self.fire_dialog_filter_proc();
2973 fired_filter_proc = true;
2974 }
2975 }
2976
2977 if yield_for_ui
2983 && !fired_menu_hook
2984 && !fired_draw_proc
2985 && !fired_filter_proc
2986 {
2987 if (opcode & !0x0400) == 0xA991
2988 && !self.service_gui_modal_dialog_idle_tick(tick_cap)
2989 {
2990 continue;
2991 }
2992 if finish_frame {
2993 self.finish_host_frame(
2994 audio_samples,
2995 sound_interrupt_dispatched,
2996 );
2997 }
2998 return (count, true);
2999 }
3000 }
3001 if self.frozen_ticks.is_some() {
3006 self.unfreeze_ticks_to(real_tick_cap);
3007 }
3008
3009 if !is_tracking_refire && self.fire_modeless_dialog_draw_proc() {
3010 continue;
3011 }
3012
3013 self.service_delay_ticks(tick_cap);
3016 if self.service_pending_launch_application(event_manager_yield_trap(
3017 opcode,
3018 )) {
3019 if self.halted {
3020 return (count, false);
3021 }
3022 continue;
3023 }
3024 }
3025 Err(Error::Halted) => {
3026 let caller_str = self
3033 .dispatcher
3034 .current_trap_caller
3035 .map(|c| format!(" caller=${:08X}", c))
3036 .unwrap_or_default();
3037 eprintln!(
3038 "[RUN_STEPS] Application halted at count={} pc=${:08X} trap=${:04X}{}",
3039 count,
3040 pc,
3041 opcode,
3042 caller_str,
3043 );
3044 self.halted = true;
3045 self.halted_pc = Some(pc);
3046 self.halted_trap = Some(opcode);
3047 self.halted_sp = Some(self.cpu.read_reg(Register::A7));
3048 self.halted_d0 = Some(self.cpu.read_reg(Register::D0));
3049 self.dump_trace();
3050 return (count, false);
3051 }
3052 Err(Error::UnimplementedTrap(t)) => {
3053 eprintln!("[RUN_STEPS] Unimplemented trap ${:04X} — skipping", t);
3054 self.cpu.write_reg(Register::PC, pc + 2);
3055 }
3056 Err(e) => {
3057 eprintln!(
3058 "[RUN_STEPS] Error {:?} at PC=${:08X} trap=${:04X} count={}",
3059 e, pc, opcode, count
3060 );
3061 self.halted = true;
3062 self.halted_pc = Some(pc);
3063 self.halted_sp = Some(self.cpu.read_reg(Register::A7));
3064 self.halted_d0 = Some(self.cpu.read_reg(Register::D0));
3065 self.dump_trace();
3066 return (count, false);
3067 }
3068 }
3069 }
3070 }
3071
3072 self.dispatcher.instruction_count = self.total_instructions;
3073 }
3074
3075 if finish_frame {
3076 self.finish_host_frame(audio_samples, sound_interrupt_dispatched || sound_work_only);
3077 }
3078
3079 (count, !self.halted)
3080 }
3081
3082 pub fn run_steps_with_audio(
3089 &mut self,
3090 max_steps: usize,
3091 tick_override: Option<u32>,
3092 audio_samples: usize,
3093 ) -> (usize, bool) {
3094 self.run_steps_internal(
3095 max_steps,
3096 tick_override,
3097 audio_samples,
3098 tick_override.is_some(),
3099 false,
3100 true,
3101 )
3102 }
3103
3104 pub fn run_realtime_steps_with_audio(
3108 &mut self,
3109 max_steps: usize,
3110 audio_samples: usize,
3111 ) -> (usize, bool) {
3112 self.run_steps_internal(max_steps, None, audio_samples, true, false, true)
3113 }
3114
3115 pub fn run_gui_slice_with_audio(
3127 &mut self,
3128 max_steps: usize,
3129 deadline_tick: u32,
3130 audio_samples: usize,
3131 ) -> (usize, bool) {
3132 self.run_steps_internal(
3133 max_steps,
3134 Some(deadline_tick),
3135 audio_samples,
3136 true,
3137 false,
3138 true,
3139 )
3140 }
3141
3142 pub fn run_gui_cpu_slice(&mut self, max_steps: usize, deadline_tick: u32) -> (usize, bool) {
3146 self.run_steps_internal(max_steps, Some(deadline_tick), 0, true, false, false)
3147 }
3148
3149 pub fn run_pending_sound_work(&mut self, max_steps: usize) -> (usize, bool) {
3152 self.run_steps_internal(max_steps, None, 0, true, true, true)
3153 }
3154
3155 pub fn run_steps(&mut self, max_steps: usize, tick_override: Option<u32>) -> (usize, bool) {
3166 self.run_steps_internal(max_steps, tick_override, 0, false, false, true)
3167 }
3168
3169 fn charge_tick_budget(&mut self, units: i32, tick_cap: Option<u32>) -> bool {
3170 if units <= 0 {
3171 return false;
3172 }
3173 if self.active_interrupt_callback.is_some() {
3174 return false;
3175 }
3176
3177 self.tick_budget -= units;
3178 while self.tick_budget <= 0 && self.frozen_ticks.is_none() {
3179 if let Some(cap) = tick_cap {
3180 if self.bus.read_long(0x016A) >= cap {
3181 return true;
3182 }
3183 }
3184 self.advance_guest_tick();
3185 self.tick_budget += self.instructions_per_tick as i32;
3186 if self.active_interrupt_callback.is_some() {
3187 return false;
3188 }
3189 if let Some(cap) = tick_cap {
3190 if self.bus.read_long(0x016A) >= cap {
3191 return true;
3192 }
3193 }
3194 }
3195 false
3196 }
3197
3198 fn advance_guest_tick(&mut self) -> u32 {
3199 let new_tick = self.bus.read_long(0x016A).wrapping_add(1);
3200 self.bus.write_long(0x016A, new_tick);
3201 self.dispatcher.tick_count = new_tick;
3202
3203 let has_pending_unmatched_down = self.dispatcher.has_unmatched_queued_mouse_down();
3217 let pressed = self.dispatcher.mouse_button || has_pending_unmatched_down;
3218 let mb_state: u8 = if pressed { 0x00 } else { 0x80 };
3219 self.bus.write_byte(0x0172, mb_state);
3220
3221 if new_tick.is_multiple_of(60) {
3228 let time = self.bus.read_long(0x020C);
3229 self.bus.write_long(0x020C, time.wrapping_add(1));
3230 }
3231
3232 self.fire_cursor_task();
3237 self.fire_vbl_tasks();
3238 self.fire_timer_tasks(new_tick);
3239 new_tick
3240 }
3241
3242 fn deliver_pending_wait_next_event_if_available(&mut self) -> bool {
3243 let Some(pending) = self.dispatcher.pending_wait_next_event_return.take() else {
3244 if !self.dispatcher.event_queue.is_empty() {
3245 self.dispatcher.pending_wait_sleep_ticks = 0;
3246 return true;
3247 }
3248 return false;
3249 };
3250
3251 if let (Some(resume_pc), Some(resume_sp)) = (pending.resume_pc, pending.resume_sp) {
3252 let current_pc = self.cpu.read_reg(Register::PC);
3253 let current_sp = self.cpu.read_reg(Register::A7);
3254 if current_pc != resume_pc || current_sp != resume_sp {
3255 self.dispatcher.pending_wait_sleep_ticks = 0;
3256 if crate::trap::dispatch::trace_input_enabled() {
3257 eprintln!(
3258 "[INPUT] dropping stale WaitNextEvent sleep return parked pc=${:08X} sp=${:08X}; current pc=${:08X} sp=${:08X}",
3259 resume_pc, resume_sp, current_pc, current_sp
3260 );
3261 }
3262 return false;
3263 }
3264 }
3265
3266 let (mut what, mut message, mut where_v, mut where_h, mut modifiers, mut has_event) = self
3267 .dispatcher
3268 .dequeue_toolbox_event(&mut self.cpu, &mut self.bus, pending.event_mask);
3269 if !has_event {
3270 if let Some(event) = self.dispatcher.mouse_moved_event_for_region(
3271 &self.bus,
3272 pending.event_mask,
3273 pending.mouse_rgn,
3274 ) {
3275 what = event.what;
3276 message = event.message;
3277 where_v = event.where_v;
3278 where_h = event.where_h;
3279 modifiers = event.modifiers;
3280 has_event = true;
3281 self.dispatcher.debug_mouse_moved_event_count = self
3282 .dispatcher
3283 .debug_mouse_moved_event_count
3284 .saturating_add(1);
3285 }
3286 }
3287 if !has_event {
3288 self.dispatcher.pending_wait_next_event_return = Some(pending);
3289 return false;
3290 }
3291
3292 self.dispatcher.write_event_record(
3293 &mut self.bus,
3294 pending.event_ptr,
3295 what,
3296 message,
3297 where_v,
3298 where_h,
3299 modifiers,
3300 );
3301 self.bus.write_word(pending.result_ptr, 0xFFFF);
3302 self.dispatcher.pending_wait_sleep_ticks = 0;
3303 if crate::trap::dispatch::trace_input_enabled() {
3304 eprintln!(
3305 "[INPUT] WaitNextEvent sleep woke with input event what={} message=${:08X}",
3306 what, message
3307 );
3308 }
3309 true
3310 }
3311
3312 fn wake_pending_wait_next_event_if_input_available(&mut self) -> bool {
3313 if self.active_interrupt_callback.is_some() {
3314 return false;
3315 }
3316 if self.dispatcher.pending_wait_sleep_ticks == 0
3317 || self.dispatcher.pending_wait_next_event_return.is_none()
3318 {
3319 return false;
3320 }
3321 self.deliver_pending_wait_next_event_if_available()
3326 }
3327
3328 fn wake_pending_wait_next_event_with_null_event_for_polling_input(&mut self) -> bool {
3329 if self.active_interrupt_callback.is_some() {
3330 return false;
3331 }
3332 if self.dispatcher.pending_wait_sleep_ticks == 0 {
3333 return false;
3334 }
3335 let Some(pending) = self.dispatcher.pending_wait_next_event_return.take() else {
3336 return false;
3337 };
3338
3339 if let (Some(resume_pc), Some(resume_sp)) = (pending.resume_pc, pending.resume_sp) {
3340 let current_pc = self.cpu.read_reg(Register::PC);
3341 let current_sp = self.cpu.read_reg(Register::A7);
3342 if current_pc != resume_pc || current_sp != resume_sp {
3343 self.dispatcher.pending_wait_sleep_ticks = 0;
3344 if crate::trap::dispatch::trace_input_enabled() {
3345 eprintln!(
3346 "[INPUT] dropping stale WaitNextEvent polling wake parked pc=${:08X} sp=${:08X}; current pc=${:08X} sp=${:08X}",
3347 resume_pc, resume_sp, current_pc, current_sp
3348 );
3349 }
3350 return false;
3351 }
3352 }
3353
3354 let (where_v, where_h) = self.dispatcher.mouse_position();
3355 let modifiers = self.dispatcher.current_event_modifiers();
3356 self.dispatcher.write_event_record(
3357 &mut self.bus,
3358 pending.event_ptr,
3359 0,
3360 0,
3361 where_v,
3362 where_h,
3363 modifiers,
3364 );
3365 self.bus.write_word(pending.result_ptr, 0);
3366 self.dispatcher.pending_wait_sleep_ticks = 0;
3367 if crate::trap::dispatch::trace_input_enabled() {
3368 eprintln!("[INPUT] WaitNextEvent sleep woke with null event for polling input");
3369 }
3370 true
3371 }
3372
3373 fn wake_foreground_after_input(&mut self) {
3374 if self.tick_budget <= 0 {
3375 self.refill_foreground_budget_after_async_return();
3376 }
3377 }
3378
3379 fn refill_foreground_budget_after_async_return(&mut self) {
3380 if self.tick_budget <= 0 {
3381 self.tick_budget = self.instructions_per_tick.max(2) as i32;
3382 }
3383 }
3384
3385 fn service_wait_sleep_ticks(&mut self, tick_cap: Option<u32>) -> bool {
3386 if self.dispatcher.pending_wait_sleep_ticks == 0 || self.active_interrupt_callback.is_some()
3387 {
3388 return false;
3389 }
3390
3391 if self.frozen_ticks.is_some() {
3392 self.dispatcher.pending_wait_sleep_ticks = 0;
3393 self.dispatcher.pending_wait_next_event_return = None;
3394 return false;
3395 }
3396
3397 if self.deliver_pending_wait_next_event_if_available() {
3401 return false;
3402 }
3403
3404 let retained_modal_dialog_snapshot = self
3411 .dispatcher
3412 .dialog_visible_snapshots
3413 .keys()
3414 .any(|dialog_ptr| self.dispatcher.dialog_modal_entered.contains(dialog_ptr));
3415 let app_owned_visible_dialog_snapshot = self
3416 .dispatcher
3417 .dialog_visible_snapshots
3418 .keys()
3419 .any(|dialog_ptr| !self.dispatcher.dialog_modal_entered.contains(dialog_ptr));
3420 if tick_cap.is_some()
3421 && (self.dispatcher.is_dialog_tracking() || retained_modal_dialog_snapshot)
3422 {
3423 self.dispatcher.pending_wait_sleep_ticks = 0;
3424 self.dispatcher.pending_wait_next_event_return = None;
3425 return false;
3426 }
3427
3428 if let Some(cap) = tick_cap {
3435 while self.dispatcher.pending_wait_sleep_ticks > 0 && self.bus.read_long(0x016A) < cap {
3436 self.dispatcher.pending_wait_sleep_ticks -= 1;
3437 self.advance_guest_tick();
3438 self.tick_budget = self.instructions_per_tick as i32;
3439 if self.active_interrupt_callback.is_some() {
3440 break;
3441 }
3442 }
3443
3444 if self.dispatcher.pending_wait_sleep_ticks > 0 && self.bus.read_long(0x016A) >= cap {
3448 return true;
3449 }
3450 self.dispatcher.pending_wait_next_event_return = None;
3451 return false;
3452 }
3453
3454 if let Some(cap) = self.wait_sleep_cap_in_headless {
3465 let advance = if app_owned_visible_dialog_snapshot {
3466 self.dispatcher.pending_wait_sleep_ticks
3467 } else {
3468 self.dispatcher.pending_wait_sleep_ticks.min(cap)
3469 };
3470 self.dispatcher.pending_wait_sleep_ticks = 0;
3471 self.dispatcher.pending_wait_next_event_return = None;
3472 for _ in 0..advance {
3473 self.advance_guest_tick();
3474 self.tick_budget = self.instructions_per_tick as i32;
3475 if self.active_interrupt_callback.is_some() {
3476 break;
3477 }
3478 }
3479 return false;
3480 }
3481
3482 while self.dispatcher.pending_wait_sleep_ticks > 0 {
3483 self.dispatcher.pending_wait_sleep_ticks -= 1;
3484 self.advance_guest_tick();
3485 self.tick_budget = self.instructions_per_tick as i32;
3486
3487 if self.active_interrupt_callback.is_some() {
3488 break;
3489 }
3490 }
3491 self.dispatcher.pending_wait_next_event_return = None;
3492 false
3493 }
3494
3495 fn service_delay_ticks(&mut self, tick_cap: Option<u32>) -> bool {
3496 if self.dispatcher.pending_delay_ticks == 0 || self.active_interrupt_callback.is_some() {
3497 return false;
3498 }
3499
3500 if self.frozen_ticks.is_some() {
3501 self.dispatcher.pending_delay_ticks = 0;
3502 return false;
3503 }
3504
3505 while self.dispatcher.pending_delay_ticks > 0 {
3508 if let Some(cap) = tick_cap {
3509 if self.bus.read_long(0x016A) >= cap {
3510 return true;
3511 }
3512 }
3513 self.dispatcher.pending_delay_ticks -= 1;
3514 self.advance_guest_tick();
3515 self.tick_budget = self.instructions_per_tick as i32;
3516 if self.active_interrupt_callback.is_some() {
3517 break;
3518 }
3519 }
3520
3521 if self.dispatcher.pending_delay_ticks == 0 {
3522 let final_ticks = self.bus.read_long(0x016A);
3523 self.cpu.write_reg(Register::D0, final_ticks);
3524 }
3525
3526 false
3527 }
3528
3529 fn service_gui_modal_dialog_idle_tick(&mut self, tick_cap: Option<u32>) -> bool {
3530 if self.active_interrupt_callback.is_some() || self.frozen_ticks.is_some() {
3531 return true;
3532 }
3533
3534 if let Some(cap) = tick_cap {
3535 if self.bus.read_long(0x016A) >= cap {
3536 return true;
3537 }
3538 }
3539
3540 self.advance_guest_tick();
3541 self.tick_budget = self.instructions_per_tick as i32;
3542
3543 tick_cap
3544 .map(|cap| self.bus.read_long(0x016A) >= cap)
3545 .unwrap_or(true)
3546 }
3547
3548 fn unfreeze_ticks_to(&mut self, target_tick: Option<u32>) {
3549 self.frozen_ticks = None;
3550 if let Some(target_tick) = target_tick {
3551 self.bus.write_long(0x016A, target_tick);
3552 self.dispatcher.tick_count = target_tick;
3555 }
3556 }
3557
3558 fn fire_cursor_task(&mut self) {
3563 if self.active_interrupt_callback.is_some() {
3564 return;
3565 }
3566
3567 let callback_addr = self
3568 .bus
3569 .read_long(crate::memory::globals::addr::J_CRSR_TASK);
3570 if callback_addr == 0 || callback_addr == CURSOR_TASK_NOOP_ADDR {
3571 return;
3572 }
3573
3574 if self.cursor_task_trampoline == 0 {
3575 let tramp = self.bus.alloc(16);
3580 self.bus.write_word(tramp, 0x48E7); self.bus.write_word(tramp + 2, 0xF0F0);
3582 self.bus.write_word(tramp + 4, 0x4EB9); self.bus.write_word(tramp + 10, 0x4CDF); self.bus.write_word(tramp + 12, 0x0F0F);
3586 self.bus.write_word(tramp + 14, 0x4E75); self.cursor_task_trampoline = tramp;
3588 }
3589
3590 let tramp = self.cursor_task_trampoline;
3591 self.bus.write_long(tramp + 6, callback_addr);
3592 if trace_vbl_enabled() {
3593 eprintln!(
3594 "[VBL] fire JCrsrTask addr=${:08X} interrupted_pc=${:08X} interrupted_sp=${:08X}",
3595 callback_addr,
3596 self.cpu.read_reg(Register::PC),
3597 self.cpu.read_reg(Register::A7)
3598 );
3599 }
3600 self.inject_interrupt_callback(ActiveInterruptCallbackSource::CursorTask, tramp);
3601 }
3602
3603 fn fire_vbl_tasks(&mut self) {
3608 if self.active_interrupt_callback.is_some() {
3609 return;
3610 }
3611
3612 let mut due_task = None;
3613 for task in &self.dispatcher.vbl_tasks {
3614 let count = self.bus.read_word(task.task_ptr + 10) as i16;
3615 if count <= 0 {
3616 continue;
3617 }
3618 let new_count = count - 1;
3619 self.bus.write_word(task.task_ptr + 10, new_count as u16);
3620 if new_count == 0 {
3621 due_task = Some(task.task_ptr);
3622 break;
3623 }
3624 }
3625
3626 let Some(task_ptr) = due_task else {
3627 return;
3628 };
3629
3630 let callback_addr = self.bus.read_long(task_ptr + 6);
3631 if callback_addr == 0 {
3632 return;
3633 }
3634
3635 if self.vbl_trampoline == 0 {
3636 let tramp = self.bus.alloc(22);
3637 self.bus.write_word(tramp, 0x48E7); self.bus.write_word(tramp + 2, 0xF0F0);
3639 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);
3643 self.bus.write_word(tramp + 20, 0x4E75); self.vbl_trampoline = tramp;
3645 }
3646
3647 let tramp = self.vbl_trampoline;
3648 self.bus.write_long(tramp + 6, task_ptr);
3649 self.bus.write_long(tramp + 12, callback_addr);
3650
3651 let current_pc = self.cpu.read_reg(Register::PC);
3652 let sp = self.cpu.read_reg(Register::A7);
3653 let d_regs = [
3654 self.cpu.read_reg(Register::D0),
3655 self.cpu.read_reg(Register::D1),
3656 self.cpu.read_reg(Register::D2),
3657 self.cpu.read_reg(Register::D3),
3658 self.cpu.read_reg(Register::D4),
3659 self.cpu.read_reg(Register::D5),
3660 self.cpu.read_reg(Register::D6),
3661 self.cpu.read_reg(Register::D7),
3662 ];
3663 let a_regs = [
3664 self.cpu.read_reg(Register::A0),
3665 self.cpu.read_reg(Register::A1),
3666 self.cpu.read_reg(Register::A2),
3667 self.cpu.read_reg(Register::A3),
3668 self.cpu.read_reg(Register::A4),
3669 self.cpu.read_reg(Register::A5),
3670 self.cpu.read_reg(Register::A6),
3671 sp,
3672 ];
3673 let ccr = self.cpu.core.get_ccr();
3674 let new_sp = sp.wrapping_sub(4);
3675 self.bus.write_long(new_sp, current_pc);
3676 self.cpu.write_reg(Register::A7, new_sp);
3677 self.active_interrupt_callback = Some(ActiveInterruptCallback {
3678 source: ActiveInterruptCallbackSource::Vbl,
3679 resume_pc: current_pc,
3680 resume_sp: sp,
3681 d_regs,
3682 a_regs,
3683 ccr,
3684 restore_port: None,
3685 });
3686 self.cpu.write_reg(Register::PC, tramp);
3687
3688 if trace_vbl_enabled() {
3689 eprintln!(
3690 "[VBL] fire task=${:08X} addr=${:08X} interrupted_pc=${:08X} interrupted_sp=${:08X} count={}",
3691 task_ptr,
3692 callback_addr,
3693 current_pc,
3694 sp,
3695 self.bus.read_word(task_ptr + 10) as i16
3696 );
3697 }
3698 }
3699
3700 fn fire_timer_tasks(&mut self, current_tick: u32) {
3715 if self.active_interrupt_callback.is_some() {
3716 return;
3717 }
3718
3719 let mut to_fire: Vec<(u32, u32)> = Vec::new(); for task in &mut self.dispatcher.timer_tasks {
3722 if task.active && current_tick >= task.fire_at_tick {
3723 to_fire.push((task.task_ptr, task.tm_addr));
3724 task.active = false; }
3726 }
3727
3728 if let Some((task_ptr, tm_addr)) = to_fire.into_iter().next() {
3730 if tm_addr == 0 {
3731 return;
3732 }
3733
3734 if self.timer_trampoline == 0 {
3742 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;
3753 }
3754
3755 let tramp = self.timer_trampoline;
3757 self.bus.write_long(tramp + 6, task_ptr);
3758 self.bus.write_long(tramp + 12, tm_addr);
3759
3760 let current_pc = self.cpu.read_reg(Register::PC);
3764 let sp = self.cpu.read_reg(Register::A7);
3765 let d_regs = [
3766 self.cpu.read_reg(Register::D0),
3767 self.cpu.read_reg(Register::D1),
3768 self.cpu.read_reg(Register::D2),
3769 self.cpu.read_reg(Register::D3),
3770 self.cpu.read_reg(Register::D4),
3771 self.cpu.read_reg(Register::D5),
3772 self.cpu.read_reg(Register::D6),
3773 self.cpu.read_reg(Register::D7),
3774 ];
3775 let a_regs = [
3776 self.cpu.read_reg(Register::A0),
3777 self.cpu.read_reg(Register::A1),
3778 self.cpu.read_reg(Register::A2),
3779 self.cpu.read_reg(Register::A3),
3780 self.cpu.read_reg(Register::A4),
3781 self.cpu.read_reg(Register::A5),
3782 self.cpu.read_reg(Register::A6),
3783 sp,
3784 ];
3785 let ccr = self.cpu.core.get_ccr();
3786
3787 let new_sp = sp.wrapping_sub(4);
3789 self.bus.write_long(new_sp, current_pc);
3790 self.cpu.write_reg(Register::A7, new_sp);
3791 self.active_interrupt_callback = Some(ActiveInterruptCallback {
3792 source: ActiveInterruptCallbackSource::Timer,
3793 resume_pc: current_pc,
3794 resume_sp: sp,
3795 d_regs,
3796 a_regs,
3797 ccr,
3798 restore_port: None,
3799 });
3800 if trace_timer_enabled() {
3801 eprintln!(
3802 "[TIMER] fire task=${:08X} tm_addr=${:08X} interrupted_pc=${:08X} interrupted_sp=${:08X} ccr=${:02X}",
3803 task_ptr, tm_addr, current_pc, sp, ccr
3804 );
3805 }
3806 self.cpu.write_reg(Register::PC, tramp);
3807 }
3808 }
3809
3810 fn try_load_pending_double_buffers(&mut self) {
3814 if self.active_interrupt_callback.is_some() {
3815 return;
3816 }
3817
3818 let queued_doublebacks = self
3819 .dispatcher
3820 .sound_manager
3821 .pending_callbacks
3822 .iter()
3823 .map(|cb| (cb.chan_ptr, cb.exhausted_buffer_index))
3824 .collect::<Vec<_>>();
3825
3826 for chan in &mut self.dispatcher.sound_manager.channels {
3827 if chan.is_playing() {
3828 continue; }
3830 let (header_ptr, buf_idx, sample_rate, num_channels, sample_size) =
3831 match chan.double_buffer {
3832 Some(ref db) if !db.last_buffer_seen => (
3833 db.header_ptr,
3834 db.current_buffer,
3835 db.sample_rate,
3836 db.num_channels,
3837 db.sample_size,
3838 ),
3839 _ => continue,
3840 };
3841 let mut load_idx = buf_idx;
3842 let mut buf_ptr = self.bus.read_long(header_ptr + 12 + (buf_idx as u32) * 4);
3843 let mut can_load = buf_ptr != 0
3844 && self.bus.read_long(buf_ptr + 4) & 0x01 != 0
3845 && !queued_doublebacks
3846 .iter()
3847 .any(|&(pending_chan, pending_idx)| {
3848 pending_chan == chan.guest_ptr && pending_idx == buf_idx
3849 });
3850 let original_idx = load_idx;
3851 if !can_load {
3852 let other_idx = buf_idx ^ 1;
3853 let other_ptr = self.bus.read_long(header_ptr + 12 + (other_idx as u32) * 4);
3854 if other_ptr == 0 {
3855 continue;
3856 }
3857 let other_flags = self.bus.read_long(other_ptr + 4);
3858 let other_pending =
3859 queued_doublebacks
3860 .iter()
3861 .any(|&(pending_chan, pending_idx)| {
3862 pending_chan == chan.guest_ptr && pending_idx == other_idx
3863 });
3864 if other_flags & 0x01 == 0 || other_pending {
3865 continue; }
3867 load_idx = other_idx;
3868 buf_ptr = other_ptr;
3869 can_load = true;
3870 if let Some(ref mut db) = chan.double_buffer {
3871 db.current_buffer = other_idx;
3872 }
3873 }
3874 if !can_load {
3875 continue;
3876 }
3877 let flags = self.bus.read_long(buf_ptr + 4);
3878 if trace_sound_runner_enabled() {
3879 let preview = self
3880 .bus
3881 .read_bytes(buf_ptr + 16, 16)
3882 .iter()
3883 .map(|byte| format!("{:02X}", byte))
3884 .collect::<Vec<_>>()
3885 .join(" ");
3886 eprintln!(
3887 "[SOUND-DB] load-ready chan=${:08X} header=${:08X} requested_idx={} load_idx={} buf=${:08X} frames={} flags=${:08X} pending={:?} first={}",
3888 chan.guest_ptr,
3889 header_ptr,
3890 original_idx,
3891 load_idx,
3892 buf_ptr,
3893 self.bus.read_long(buf_ptr),
3894 flags,
3895 chan.double_buffer
3896 .as_ref()
3897 .map(|db| db.pending_callback_buffers)
3898 .unwrap_or([false; 2]),
3899 preview
3900 );
3901 }
3902 crate::trap::TrapDispatcher::load_double_buffer_samples(
3903 &mut self.bus,
3904 chan,
3905 buf_ptr,
3906 sample_rate,
3907 num_channels,
3908 sample_size,
3909 );
3910 if flags & 0x01 != 0 {
3911 if let Some(ref mut db) = chan.double_buffer {
3912 db.current_buffer = load_idx;
3913 db.complete_callback_for(load_idx);
3914 }
3915 }
3916 }
3917 }
3918
3919 fn dump_invalid_pc_state(&self) {
3920 let d_regs = [
3921 self.cpu.read_reg(Register::D0),
3922 self.cpu.read_reg(Register::D1),
3923 self.cpu.read_reg(Register::D2),
3924 self.cpu.read_reg(Register::D3),
3925 self.cpu.read_reg(Register::D4),
3926 self.cpu.read_reg(Register::D5),
3927 self.cpu.read_reg(Register::D6),
3928 self.cpu.read_reg(Register::D7),
3929 ];
3930 let a_regs = [
3931 self.cpu.read_reg(Register::A0),
3932 self.cpu.read_reg(Register::A1),
3933 self.cpu.read_reg(Register::A2),
3934 self.cpu.read_reg(Register::A3),
3935 self.cpu.read_reg(Register::A4),
3936 self.cpu.read_reg(Register::A5),
3937 self.cpu.read_reg(Register::A6),
3938 self.cpu.read_reg(Register::A7),
3939 ];
3940 eprintln!(
3941 "[RUN_STEPS] D0-D7: {:08X} {:08X} {:08X} {:08X} {:08X} {:08X} {:08X} {:08X}",
3942 d_regs[0], d_regs[1], d_regs[2], d_regs[3], d_regs[4], d_regs[5], d_regs[6], d_regs[7]
3943 );
3944 eprintln!(
3945 "[RUN_STEPS] A0-A7: {:08X} {:08X} {:08X} {:08X} {:08X} {:08X} {:08X} {:08X}",
3946 a_regs[0], a_regs[1], a_regs[2], a_regs[3], a_regs[4], a_regs[5], a_regs[6], a_regs[7]
3947 );
3948 eprintln!("[RUN_STEPS] CCR=${:02X}", self.cpu.core.get_ccr());
3949 if let Some(active) = self.active_interrupt_callback {
3950 eprintln!(
3951 "[RUN_STEPS] active_callback={:?} resume_pc=${:08X} resume_sp=${:08X}",
3952 active.source, active.resume_pc, active.resume_sp
3953 );
3954 }
3955 self.bus.dump_stack(a_regs[7], "invalid PC");
3956 }
3957
3958 fn inject_interrupt_callback(
3959 &mut self,
3960 source: ActiveInterruptCallbackSource,
3961 trampoline: u32,
3962 ) {
3963 let current_pc = self.cpu.read_reg(Register::PC);
3964 let sp = self.cpu.read_reg(Register::A7);
3965 let d_regs = [
3966 self.cpu.read_reg(Register::D0),
3967 self.cpu.read_reg(Register::D1),
3968 self.cpu.read_reg(Register::D2),
3969 self.cpu.read_reg(Register::D3),
3970 self.cpu.read_reg(Register::D4),
3971 self.cpu.read_reg(Register::D5),
3972 self.cpu.read_reg(Register::D6),
3973 self.cpu.read_reg(Register::D7),
3974 ];
3975 let a_regs = [
3976 self.cpu.read_reg(Register::A0),
3977 self.cpu.read_reg(Register::A1),
3978 self.cpu.read_reg(Register::A2),
3979 self.cpu.read_reg(Register::A3),
3980 self.cpu.read_reg(Register::A4),
3981 self.cpu.read_reg(Register::A5),
3982 self.cpu.read_reg(Register::A6),
3983 sp,
3984 ];
3985 let ccr = self.cpu.core.get_ccr();
3986 let new_sp = sp.wrapping_sub(4);
3987 self.bus.write_long(new_sp, current_pc);
3988 self.cpu.write_reg(Register::A7, new_sp);
3989 self.active_interrupt_callback = Some(ActiveInterruptCallback {
3990 source,
3991 resume_pc: current_pc,
3992 resume_sp: sp,
3993 d_regs,
3994 a_regs,
3995 ccr,
3996 restore_port: None,
3997 });
3998 self.cpu.write_reg(Register::PC, trampoline);
3999 }
4000
4001 fn fire_sound_callbacks(&mut self) -> bool {
4003 if self.active_interrupt_callback.is_some() {
4004 return false;
4005 }
4006
4007 if self
4008 .dispatcher
4009 .sound_manager
4010 .pending_sound_callbacks
4011 .is_empty()
4012 {
4013 return false;
4014 }
4015
4016 let cb = self
4017 .dispatcher
4018 .sound_manager
4019 .pending_sound_callbacks
4020 .remove(0);
4021 match cb {
4022 crate::sound::PendingSoundCallback::Command {
4023 callback_addr,
4024 chan_ptr,
4025 cmd,
4026 } => {
4027 if callback_addr == 0 {
4028 return false;
4029 }
4030
4031 if self.sound_callback_trampoline == 0 {
4033 let tramp = self.bus.alloc(42);
4042 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;
4052 }
4053
4054 let tramp = self.sound_callback_trampoline;
4055 let cmd_ptr = tramp + 34;
4056 let interrupted_sp = self.cpu.read_reg(Register::A7);
4057 let saved_regs_sp = interrupted_sp.wrapping_sub(4 + 32);
4058 self.bus.write_long(tramp + 6, chan_ptr);
4059 self.bus.write_long(tramp + 12, cmd_ptr);
4060 self.bus.write_long(tramp + 18, callback_addr);
4061 self.bus.write_long(tramp + 24, saved_regs_sp);
4062 self.bus.write_word(cmd_ptr, cmd.cmd);
4063 self.bus.write_word(cmd_ptr + 2, cmd.param1 as u16);
4064 self.bus.write_long(cmd_ptr + 4, cmd.param2);
4065 self.inject_interrupt_callback(ActiveInterruptCallbackSource::SoundCallback, tramp);
4066 true
4067 }
4068 crate::sound::PendingSoundCallback::FileCompletion {
4069 callback_addr,
4070 chan_ptr,
4071 } => {
4072 if callback_addr == 0 {
4073 return false;
4074 }
4075
4076 if self.sound_file_completion_trampoline == 0 {
4078 let tramp = self.bus.alloc(28);
4079 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;
4088 }
4089
4090 let tramp = self.sound_file_completion_trampoline;
4091 let interrupted_sp = self.cpu.read_reg(Register::A7);
4092 let saved_regs_sp = interrupted_sp.wrapping_sub(4 + 32);
4093 self.bus.write_long(tramp + 6, chan_ptr);
4094 self.bus.write_long(tramp + 12, callback_addr);
4095 self.bus.write_long(tramp + 18, saved_regs_sp);
4096 self.inject_interrupt_callback(
4097 ActiveInterruptCallbackSource::SoundFileCompletion,
4098 tramp,
4099 );
4100 true
4101 }
4102 }
4103 }
4104
4105 fn fire_sound_doubleback_callbacks(&mut self) -> bool {
4115 if self.active_interrupt_callback.is_some() {
4116 return false;
4117 }
4118
4119 if self.dispatcher.sound_manager.pending_callbacks.is_empty() {
4120 return false;
4121 }
4122
4123 let cb = self.dispatcher.sound_manager.pending_callbacks.remove(0);
4125
4126 let exhausted_buf_ptr = self
4129 .bus
4130 .read_long(cb.header_ptr + 12 + (cb.exhausted_buffer_index as u32) * 4);
4131
4132 if exhausted_buf_ptr != 0 {
4134 let flags = self.bus.read_long(exhausted_buf_ptr + 4);
4135 if trace_sound_runner_enabled() {
4136 let preview = self
4137 .bus
4138 .read_bytes(exhausted_buf_ptr + 16, 16)
4139 .iter()
4140 .map(|byte| format!("{:02X}", byte))
4141 .collect::<Vec<_>>()
4142 .join(" ");
4143 eprintln!(
4144 "[SOUND-DB] fire-doubleback tick={} chan=${:08X} header=${:08X} idx={} buf=${:08X} frames={} flags_before=${:08X} callback=${:08X} sr=${:04X} first={}",
4145 self.bus.read_long(0x016A),
4146 cb.chan_ptr,
4147 cb.header_ptr,
4148 cb.exhausted_buffer_index,
4149 exhausted_buf_ptr,
4150 self.bus.read_long(exhausted_buf_ptr),
4151 flags,
4152 cb.callback_addr,
4153 self.cpu.core.get_sr(),
4154 preview
4155 );
4156 }
4157 self.bus.write_long(exhausted_buf_ptr + 4, flags & !0x01);
4158 }
4159
4160 if cb.callback_addr == 0 {
4161 return false;
4162 }
4163
4164 if self.sound_doubleback_trampoline == 0 {
4178 let tramp = self.bus.alloc(34);
4179 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;
4193 }
4194
4195 let tramp = self.sound_doubleback_trampoline;
4196 let interrupted_sp = self.cpu.read_reg(Register::A7);
4197 let saved_regs_sp = interrupted_sp.wrapping_sub(4 + 32);
4198 self.bus.write_long(tramp + 6, cb.chan_ptr);
4199 self.bus.write_long(tramp + 12, exhausted_buf_ptr);
4200 self.bus.write_long(tramp + 18, cb.callback_addr);
4201 self.bus.write_long(tramp + 24, saved_regs_sp);
4202
4203 self.inject_interrupt_callback(ActiveInterruptCallbackSource::SoundDoubleBack, tramp);
4207 true
4208 }
4209
4210 fn dialog_callback_scratch_base(&self) -> u32 {
4211 DIALOG_CALLBACK_SCRATCH_FALLBACK
4212 }
4213
4214 fn looks_like_dialog_proc_entry(&self, addr: u32) -> bool {
4215 if addr == 0 {
4216 return false;
4217 }
4218 let entry = self.bus.read_word(addr);
4219 entry == 0x4E56 || entry == 0x48E7 || entry == 0x4EF9 || entry == 0x4EFA
4220 }
4221
4222 fn resolve_dialog_draw_proc_addr(&self, proc_addr: u32) -> Option<u32> {
4223 if self.looks_like_dialog_proc_entry(proc_addr) {
4224 return Some(proc_addr);
4225 }
4226 let a5_relative = self.cpu.read_reg(Register::A5).wrapping_add(proc_addr);
4227 if self.looks_like_dialog_proc_entry(a5_relative) {
4228 Some(a5_relative)
4229 } else {
4230 None
4231 }
4232 }
4233
4234 fn inject_dialog_draw_proc(
4235 &mut self,
4236 proc_addr: u32,
4237 item_no: i16,
4238 dialog_ptr: u32,
4239 modeless: bool,
4240 ) -> bool {
4241 if self.active_interrupt_callback.is_some() {
4242 return false;
4243 }
4244
4245 if proc_addr == 0 {
4246 return false;
4247 }
4248
4249 let Some(call_addr) = self.resolve_dialog_draw_proc_addr(proc_addr) else {
4252 if trace_dialog_procs_enabled() {
4253 let a5_relative = self.cpu.read_reg(Register::A5).wrapping_add(proc_addr);
4254 eprintln!(
4255 "[DIALOG-PROC] skip dialog=${:08X} item={} proc=${:08X} a5rel=${:08X} entry=${:04X} a5entry=${:04X}",
4256 dialog_ptr,
4257 item_no,
4258 proc_addr,
4259 a5_relative,
4260 self.bus.read_word(proc_addr),
4261 self.bus.read_word(a5_relative),
4262 );
4263 }
4264 return false;
4265 };
4266
4267 if self.dialog_draw_trampoline == 0 {
4276 let tramp = self.dialog_callback_scratch_base() + DIALOG_DRAW_TRAMPOLINE_OFFSET;
4277 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;
4291 }
4292
4293 let tramp = self.dialog_draw_trampoline;
4294 self.bus.write_long(tramp + 6, dialog_ptr);
4295 self.bus.write_word(tramp + 12, item_no as u16);
4296 self.bus.write_long(tramp + 16, call_addr);
4297
4298 self.dispatcher
4302 .set_current_port_state(&mut self.bus, &mut self.cpu, dialog_ptr, None);
4303
4304 let current_pc = self.cpu.read_reg(Register::PC);
4306 let sp = self.cpu.read_reg(Register::A7);
4307 let d_regs = [
4308 self.cpu.read_reg(Register::D0),
4309 self.cpu.read_reg(Register::D1),
4310 self.cpu.read_reg(Register::D2),
4311 self.cpu.read_reg(Register::D3),
4312 self.cpu.read_reg(Register::D4),
4313 self.cpu.read_reg(Register::D5),
4314 self.cpu.read_reg(Register::D6),
4315 self.cpu.read_reg(Register::D7),
4316 ];
4317 let a_regs = [
4318 self.cpu.read_reg(Register::A0),
4319 self.cpu.read_reg(Register::A1),
4320 self.cpu.read_reg(Register::A2),
4321 self.cpu.read_reg(Register::A3),
4322 self.cpu.read_reg(Register::A4),
4323 self.cpu.read_reg(Register::A5),
4324 self.cpu.read_reg(Register::A6),
4325 sp,
4326 ];
4327 let ccr = self.cpu.core.get_ccr();
4328 let new_sp = sp.wrapping_sub(4);
4329 let saved_regs_sp = new_sp.wrapping_sub(32);
4330 self.bus.write_long(tramp + 22, saved_regs_sp);
4331 self.bus.write_long(new_sp, current_pc);
4332 self.cpu.write_reg(Register::A7, new_sp);
4333 self.active_interrupt_callback = Some(ActiveInterruptCallback {
4334 source: ActiveInterruptCallbackSource::DialogDrawProc,
4335 resume_pc: current_pc,
4336 resume_sp: sp,
4337 d_regs,
4338 a_regs,
4339 ccr,
4340 restore_port: None,
4341 });
4342 if modeless {
4343 self.dispatcher.active_modeless_dialog_draw_proc = Some(dialog_ptr);
4344 }
4345 if trace_dialog_procs_enabled() {
4346 eprintln!(
4347 "[DIALOG-PROC] fire {} dialog=${:08X} item={} proc=${:08X} call=${:08X} return_pc=${:08X}",
4348 if modeless { "modeless" } else { "modal" },
4349 dialog_ptr,
4350 item_no,
4351 proc_addr,
4352 call_addr,
4353 current_pc,
4354 );
4355 }
4356 self.cpu.write_reg(Register::PC, tramp);
4357 true
4358 }
4359
4360 fn fire_modeless_dialog_draw_proc(&mut self) -> bool {
4361 if self.active_interrupt_callback.is_some() {
4362 return false;
4363 }
4364
4365 while let Some((dialog_ptr, proc_addr, item_no)) =
4366 self.dispatcher.modeless_dialog_draw_proc_queue.pop_front()
4367 {
4368 if self.inject_dialog_draw_proc(proc_addr, item_no, dialog_ptr, true) {
4369 return true;
4370 }
4371 }
4372 false
4373 }
4374
4375 fn fire_dialog_draw_procs(&mut self) -> bool {
4392 if self.active_interrupt_callback.is_some() {
4393 return false;
4394 }
4395
4396 if let Some(tracking) = self
4397 .dispatcher
4398 .dialog_tracking
4399 .as_mut()
4400 .filter(|tracking| !tracking.draw_procs_done)
4401 {
4402 let Some((proc_addr, item_no)) = tracking.draw_proc_queue.pop_front() else {
4403 tracking.draw_procs_done = true;
4405 return false;
4406 };
4407 let dialog_ptr = tracking.dialog_ptr;
4408 return self.inject_dialog_draw_proc(proc_addr, item_no, dialog_ptr, false);
4409 }
4410
4411 self.fire_modeless_dialog_draw_proc()
4412 }
4413
4414 fn fire_menu_hook_proc(&mut self, opcode: u16) -> bool {
4415 if self.active_interrupt_callback.is_some() || (opcode & !0x0400) != 0xA93D {
4416 return false;
4417 }
4418 if self.dispatcher.menu_tracking.is_none() || self.bus.read_byte(0x0172) != 0x00 {
4419 return false;
4420 }
4421
4422 let hook_addr = self.bus.read_long(0x0A30);
4428 let Some(call_addr) = self.resolve_dialog_draw_proc_addr(hook_addr) else {
4429 return false;
4430 };
4431
4432 if self.menu_hook_trampoline == 0 {
4439 let tramp = self.dialog_callback_scratch_base() + MENU_HOOK_TRAMPOLINE_OFFSET;
4440 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;
4450 }
4451
4452 let tramp = self.menu_hook_trampoline;
4453 self.bus.write_long(tramp + 6, call_addr);
4454
4455 let current_pc = self.cpu.read_reg(Register::PC);
4456 let sp = self.cpu.read_reg(Register::A7);
4457 let d_regs = [
4458 self.cpu.read_reg(Register::D0),
4459 self.cpu.read_reg(Register::D1),
4460 self.cpu.read_reg(Register::D2),
4461 self.cpu.read_reg(Register::D3),
4462 self.cpu.read_reg(Register::D4),
4463 self.cpu.read_reg(Register::D5),
4464 self.cpu.read_reg(Register::D6),
4465 self.cpu.read_reg(Register::D7),
4466 ];
4467 let a_regs = [
4468 self.cpu.read_reg(Register::A0),
4469 self.cpu.read_reg(Register::A1),
4470 self.cpu.read_reg(Register::A2),
4471 self.cpu.read_reg(Register::A3),
4472 self.cpu.read_reg(Register::A4),
4473 self.cpu.read_reg(Register::A5),
4474 self.cpu.read_reg(Register::A6),
4475 sp,
4476 ];
4477 let ccr = self.cpu.core.get_ccr();
4478 let new_sp = sp.wrapping_sub(4);
4479 let saved_regs_sp = new_sp.wrapping_sub(32);
4480 self.bus.write_long(tramp + 12, saved_regs_sp);
4481 self.bus.write_long(new_sp, current_pc);
4482 self.cpu.write_reg(Register::A7, new_sp);
4483 self.active_interrupt_callback = Some(ActiveInterruptCallback {
4484 source: ActiveInterruptCallbackSource::MenuHook,
4485 resume_pc: current_pc,
4486 resume_sp: sp,
4487 d_regs,
4488 a_regs,
4489 ccr,
4490 restore_port: None,
4491 });
4492 self.cpu.write_reg(Register::PC, tramp);
4493 true
4494 }
4495
4496 fn dialog_filter_has_real_event_pending(&self, dialog_ptr: u32) -> bool {
4497 self.dispatcher
4498 .event_queue
4499 .iter()
4500 .any(|event| matches!(event.what, 1 | 2 | 3 | 4 | 6))
4501 || self
4502 .dispatcher
4503 .pending_update_event(&self.bus, 1u16 << 6)
4504 .is_some_and(|event| {
4505 event.message == dialog_ptr
4506 && !self.dialog_filter_update_event_already_sent_this_tick(
4507 dialog_ptr,
4508 event.message,
4509 )
4510 })
4511 }
4512
4513 fn dialog_filter_null_event_already_sent_this_tick(&self, dialog_ptr: u32) -> bool {
4514 self.dialog_filter_last_null_event_tick
4515 .is_some_and(|(sent_dialog, sent_tick)| {
4516 sent_dialog == dialog_ptr && sent_tick == self.dispatcher.tick_count
4517 })
4518 }
4519
4520 fn dialog_filter_update_event_already_sent_this_tick(
4521 &self,
4522 dialog_ptr: u32,
4523 update_window: u32,
4524 ) -> bool {
4525 self.dialog_filter_last_update_event_tick.is_some_and(
4526 |(sent_dialog, sent_window, sent_tick)| {
4527 sent_dialog == dialog_ptr
4528 && sent_window == update_window
4529 && sent_tick == self.dispatcher.tick_count
4530 },
4531 )
4532 }
4533
4534 fn should_fire_dialog_filter_proc(&self) -> bool {
4535 let Some(tracking) = self.dispatcher.dialog_tracking.as_ref() else {
4536 return false;
4537 };
4538
4539 if tracking.filter_proc == 0
4540 || !tracking.draw_procs_done
4541 || tracking.last_filter_event.is_some()
4542 {
4543 return false;
4544 }
4545
4546 let dialog_ptr = tracking.dialog_ptr;
4547 let has_real_event = self.dialog_filter_has_real_event_pending(dialog_ptr);
4548
4549 if !has_real_event
4554 && (self.dispatcher.mouse_down_over_dialog_button()
4555 || self.dispatcher.mouse_down_over_dialog_plain_user_item()
4556 || self.dispatcher.pending_dialog_plain_user_item_mouse_down())
4557 {
4558 return false;
4559 }
4560
4561 if !has_real_event && self.dialog_filter_null_event_already_sent_this_tick(dialog_ptr) {
4568 return false;
4569 }
4570
4571 true
4572 }
4573
4574 fn fire_dialog_filter_proc(&mut self) -> bool {
4590 let (filter_proc, dialog_ptr, item_hit_ptr) = {
4591 let tracking = match self.dispatcher.dialog_tracking.as_ref() {
4592 Some(t) => t,
4593 None => return false,
4594 };
4595 (
4596 tracking.filter_proc,
4597 tracking.dialog_ptr,
4598 tracking.item_hit_ptr,
4599 )
4600 };
4601
4602 if filter_proc == 0 {
4603 return false;
4604 }
4605
4606 let entry = self.bus.read_word(filter_proc);
4615 if entry != 0x4E56 && entry != 0x48E7 && entry != 0x4EF9 && entry != 0x4EFA {
4616 if trace_dialog_filter_enabled() {
4617 eprintln!(
4618 "[DIALOG-FILTER] skip invalid-entry dialog=${:08X} proc=${:08X} entry=${:04X}",
4619 dialog_ptr, filter_proc, entry
4620 );
4621 }
4622 return false;
4623 }
4624 if self.dialog_filter_event == 0 {
4627 self.dialog_filter_event =
4628 self.dialog_callback_scratch_base() + DIALOG_FILTER_EVENT_OFFSET;
4629 }
4630 let evt = self.dialog_filter_event;
4631
4632 if self.dispatcher.dialog_filter_result_addr == 0 {
4634 self.dispatcher.dialog_filter_result_addr =
4635 self.dialog_callback_scratch_base() + DIALOG_FILTER_RESULT_OFFSET;
4636 }
4637 let result_addr = self.dispatcher.dialog_filter_result_addr;
4638
4639 self.bus.write_word(result_addr, 0);
4641
4642 let ticks = self.bus.read_long(0x016A);
4643
4644 let idx = self
4648 .dispatcher
4649 .event_queue
4650 .iter()
4651 .position(|e| matches!(e.what, 1 | 2 | 3 | 4 | 6));
4652 let next_event = idx.map(|i| self.dispatcher.event_queue.remove(i).unwrap());
4653
4654 let filter_event = if let Some(e) = next_event {
4655 e
4656 } else if let Some(update_event) = self
4657 .dispatcher
4658 .pending_update_event(&self.bus, 1u16 << 6)
4659 .filter(|event| {
4660 event.message == dialog_ptr
4661 && !self.dialog_filter_update_event_already_sent_this_tick(
4662 dialog_ptr,
4663 event.message,
4664 )
4665 })
4666 {
4667 self.dialog_filter_last_update_event_tick =
4682 Some((dialog_ptr, update_event.message, ticks));
4683 update_event
4684 } else {
4685 let (v, h) = self.dispatcher.mouse_position();
4688 crate::trap::dispatch::QueuedEvent {
4689 what: 0,
4690 message: 0,
4691 where_v: v,
4692 where_h: h,
4693 modifiers: self.dispatcher.current_event_modifiers(),
4694 }
4695 };
4696 if let Some(tracking) = self.dispatcher.dialog_tracking.as_mut() {
4697 tracking.last_filter_event = Some(filter_event.clone());
4698 }
4699 let what = filter_event.what;
4700 let message = filter_event.message;
4701 let where_v = filter_event.where_v;
4702 let where_h = filter_event.where_h;
4703 let modifiers = filter_event.modifiers;
4704 if what == 0 {
4705 self.dialog_filter_last_null_event_tick = Some((dialog_ptr, ticks));
4706 } else {
4707 self.dialog_filter_last_null_event_tick = None;
4708 }
4709 self.dispatcher.tick_count = ticks;
4710 self.dispatcher.write_event_record(
4711 &mut self.bus,
4712 evt,
4713 what,
4714 message,
4715 where_v,
4716 where_h,
4717 modifiers,
4718 );
4719 if trace_dialog_filter_enabled() {
4720 eprintln!(
4721 "[DIALOG-FILTER] call dialog=${:08X} proc=${:08X} event=what:{} message=${:08X} where=({}, {}) mods=${:04X}",
4722 dialog_ptr, filter_proc, what, message, where_v, where_h, modifiers
4723 );
4724 }
4725
4726 if self.dialog_filter_trampoline == 0 {
4747 let tramp = self.dialog_callback_scratch_base() + DIALOG_FILTER_TRAMPOLINE_OFFSET;
4748 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;
4767 }
4768
4769 let tramp = self.dialog_filter_trampoline;
4770 self.bus.write_long(tramp + 8, dialog_ptr);
4771 self.bus.write_long(tramp + 14, evt);
4772 self.bus.write_long(tramp + 20, item_hit_ptr);
4773 self.bus.write_long(tramp + 26, filter_proc);
4774 self.bus.write_long(tramp + 32, result_addr);
4775
4776 self.dispatcher
4781 .set_current_port_state(&mut self.bus, &mut self.cpu, dialog_ptr, None);
4782
4783 let current_pc = self.cpu.read_reg(Register::PC);
4785 let sp = self.cpu.read_reg(Register::A7);
4786 let d_regs = [
4787 self.cpu.read_reg(Register::D0),
4788 self.cpu.read_reg(Register::D1),
4789 self.cpu.read_reg(Register::D2),
4790 self.cpu.read_reg(Register::D3),
4791 self.cpu.read_reg(Register::D4),
4792 self.cpu.read_reg(Register::D5),
4793 self.cpu.read_reg(Register::D6),
4794 self.cpu.read_reg(Register::D7),
4795 ];
4796 let a_regs = [
4797 self.cpu.read_reg(Register::A0),
4798 self.cpu.read_reg(Register::A1),
4799 self.cpu.read_reg(Register::A2),
4800 self.cpu.read_reg(Register::A3),
4801 self.cpu.read_reg(Register::A4),
4802 self.cpu.read_reg(Register::A5),
4803 self.cpu.read_reg(Register::A6),
4804 sp,
4805 ];
4806 let ccr = self.cpu.core.get_ccr();
4807 let new_sp = sp.wrapping_sub(4);
4808 let saved_sp = new_sp.wrapping_sub(32); let filter_entry_sp = saved_sp.wrapping_sub(50); let clear_size: u32 = 2048;
4825 let clear_start = filter_entry_sp.wrapping_sub(clear_size);
4826 self.bus.fill_zeros(clear_start, clear_size);
4827
4828 self.bus.write_long(tramp + 38, saved_sp);
4829 self.bus.write_long(new_sp, current_pc);
4830 self.cpu.write_reg(Register::A7, new_sp);
4831 self.active_interrupt_callback = Some(ActiveInterruptCallback {
4832 source: ActiveInterruptCallbackSource::DialogFilterProc,
4833 resume_pc: current_pc,
4834 resume_sp: sp,
4835 d_regs,
4836 a_regs,
4837 ccr,
4838 restore_port: None,
4839 });
4840 self.cpu.write_reg(Register::PC, tramp);
4841
4842 if let Some(tracking) = self.dispatcher.dialog_tracking.as_mut() {
4847 tracking.rendered_pixels_final = false;
4848 }
4849 true
4850 }
4851
4852 pub fn run(&mut self) -> Result<()> {
4866 let mut count = 0;
4867
4868 if trace_load_enabled() {
4869 eprintln!("========================================");
4870 eprintln!(" FIXTURE RUNNER STARTING ");
4871 eprintln!("========================================");
4872
4873 eprintln!(
4874 "[RUN] Starting at PC=${:08X}, A5=${:08X}, A7=${:08X}",
4875 self.cpu.read_reg(Register::PC),
4876 self.cpu.read_reg(Register::A5),
4877 self.cpu.read_reg(Register::A7)
4878 );
4879 }
4880
4881 while count < self.config.max_instructions {
4882 if self.cpu.is_stopped() {
4883 if trace_load_enabled() {
4884 eprintln!(
4885 "[RUN] Stopped after {} instructions, PC=${:08X}",
4886 count,
4887 self.cpu.read_reg(Register::PC)
4888 );
4889 }
4890 return Ok(());
4891 }
4892
4893 let pc = self.cpu.read_reg(Register::PC);
4894
4895 if pc >= self.bus.ram_size() || (pc < 0x60 && pc > 0) {
4898 eprintln!(
4899 "[RUN] CRITICAL: PC jumped to invalid address ${:08X}! Halting trace.",
4900 pc
4901 );
4902 self.dump_trace();
4903 return Ok(());
4904 }
4905
4906 if trace_buffer_enabled() {
4908 let opcode = self.bus.read_word(pc);
4909 let a0 = self.cpu.read_reg(Register::A0);
4910 let sp = self.cpu.read_reg(Register::A7);
4911 let a6 = self.cpu.read_reg(Register::A6);
4912 let a5 = self.cpu.read_reg(Register::A5);
4913 if self.trace_buffer.len() >= 200 {
4914 self.trace_buffer.pop_front();
4915 }
4916 self.trace_buffer.push_back((pc, opcode, a0, sp, a6, a5));
4917 }
4918
4919 match self.cpu.step(&mut self.bus) {
4920 StepResult::Ok => {}
4921 StepResult::Stopped => {
4922 if trace_load_enabled() {
4923 let stopped_pc = self.cpu.read_reg(Register::PC);
4924 let opcode = self.bus.read_word(stopped_pc);
4925 eprintln!(
4926 "[RUN] Step returned Stopped after {} instructions, PC=${:08X}, Opcode=${:04X}",
4927 count, stopped_pc, opcode
4928 );
4929 }
4930 self.dump_trace();
4931 return Ok(());
4932 }
4933 StepResult::Aline(opcode) => {
4934 match self
4935 .dispatcher
4936 .dispatch(opcode, &mut self.cpu, &mut self.bus)
4937 {
4938 Ok(()) => {
4939 let pc_after = self.cpu.read_reg(Register::PC);
4943 if pc_after == pc {
4944 self.cpu.write_reg(Register::PC, pc + 2);
4945 }
4946
4947 }
4950 Err(Error::Halted) => {
4951 if trace_load_enabled() {
4952 eprintln!("[RUN] Halted via trap after {} instructions", count);
4953 }
4954 self.dump_trace();
4955 return Ok(());
4956 }
4957 Err(e) => {
4958 self.dump_trace();
4959 return Err(e);
4960 }
4961 }
4962 }
4963 }
4964 count += 1;
4965 }
4966 if trace_load_enabled() {
4967 eprintln!("[RUN] Timeout after {} instructions", count);
4968 }
4969 self.dump_trace();
4970 Err(Error::Timeout(count))
4971 }
4972
4973 fn trace_find_fakeptr_entry(&self) -> Option<(u32, String)> {
4982 for (pc, _op, _a0, _sp, _a6, _a5) in self.trace_buffer.iter().rev() {
4983 if let Some(hint) = decode_fakeptr_pc(*pc) {
4984 return Some((*pc, hint));
4985 }
4986 }
4987 None
4988 }
4989
4990 pub fn dump_trace(&self) {
4996 if self.trace_buffer.is_empty() {
4997 return;
4998 }
4999 eprintln!(
5000 "[TRACE] Last {} executed instructions:",
5001 self.trace_buffer.len()
5002 );
5003 eprintln!(" PC Op A0 SP A6 D0");
5004 for (pc, opcode, a0, sp, a6, d0) in &self.trace_buffer {
5005 eprintln!(
5006 " {:08X} {:04X} {:08X} {:08X} {:08X} {:08X}",
5007 pc, opcode, a0, sp, a6, d0
5008 );
5009 }
5010 }
5011}
5012
5013impl Drop for FixtureRunner {
5020 fn drop(&mut self) {
5021 self.dispatcher.print_trap_histogram(40);
5022 self.print_opcode_histogram(40);
5023 self.print_pc_histogram(40);
5024 self.dispatcher.print_trap_timing_histogram(40);
5025 }
5026}
5027
5028fn load_app_generic<M: MemoryBus>(
5033 fork: &ResourceFork,
5034 bus: &mut M,
5035 load_address: u32,
5036) -> Option<LoadedApp> {
5037 let code0 = fork.get_code(0)?;
5039 let header = Code0Header::parse(&code0.data)?;
5040 if trace_load_enabled() {
5041 eprintln!(
5042 "[LOAD] CODE 0 header: above_a5={}, below_a5={}, jt_size={}, jt_offset={}",
5043 header.above_a5, header.below_a5, header.jump_table_size, header.jump_table_offset
5044 );
5045 }
5046
5047 let a5_base = load_address + header.below_a5;
5048 let qd_globals_reserve = 48 * 1024; let globals_end = a5_base + header.above_a5 + qd_globals_reserve;
5053
5054 let globals_zero_end = globals_end + 0x40000;
5056 bus.fill_zeros(load_address, globals_zero_end.saturating_sub(load_address));
5057 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) {
5069 bus.write_word(addr, 0x4E75); }
5071
5072 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) {
5097 let data_dest = load_address;
5099 if trace_load_enabled() {
5100 eprintln!(
5101 "[LOAD] Writing DATA 0 ({} bytes) to ${:08X}",
5102 data.data.len(),
5103 data_dest
5104 );
5105 }
5106 bus.write_bytes(data_dest, &data.data);
5107 }
5108
5109 let mut jump_table = Vec::new();
5111 let jt_data = &code0.data[16..];
5112
5113 for i in 0..header.num_entries() {
5114 let entry_offset = i * 8;
5115 if entry_offset + 8 > jt_data.len() {
5116 break;
5117 }
5118
5119 let word_2_3 = u16::from_be_bytes([jt_data[entry_offset + 2], jt_data[entry_offset + 3]]);
5120 let (offset, segment) = if word_2_3 == 0xA9F0 {
5121 let seg = i16::from_be_bytes([jt_data[entry_offset], jt_data[entry_offset + 1]]);
5123 let off = u16::from_be_bytes([jt_data[entry_offset + 6], jt_data[entry_offset + 7]]);
5124 (off, seg)
5125 } else if word_2_3 == 0xFFFF {
5126 (0u16, 0i16)
5128 } else {
5129 let off = u16::from_be_bytes([jt_data[entry_offset], jt_data[entry_offset + 1]]);
5131 let seg = i16::from_be_bytes([jt_data[entry_offset + 4], jt_data[entry_offset + 5]]);
5132 (off, seg)
5133 };
5134
5135 jump_table.push(JumpTableEntry {
5136 offset,
5137 segment,
5138 loaded: false,
5139 address: 0,
5140 });
5141 if trace_load_enabled() {
5142 eprintln!(
5143 "[LOAD] Parsed JT[{}]: segment={}, offset=0x{:04X}, raw=[{:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}]",
5144 i, segment, offset,
5145 jt_data[entry_offset], jt_data[entry_offset+1],
5146 jt_data[entry_offset+2], jt_data[entry_offset+3],
5147 jt_data[entry_offset+4], jt_data[entry_offset+5],
5148 jt_data[entry_offset+6], jt_data[entry_offset+7]
5149 );
5150 }
5151 }
5152
5153 let code0_base = globals_end;
5155 let code0_size = code0.data.len() as u32;
5156 let code0_user = code0_base + 4;
5157 let jt_base = a5_base + header.jump_table_offset;
5158 if trace_load_enabled() {
5159 eprintln!("[LOAD] Memory layout: a5_base=${:08X}, globals_end=${:08X}, code0_user=${:08X}, code0_size={}, jt_base=${:08X} (jt_offset={})",
5160 a5_base, globals_end, code0_user, code0_size, jt_base, header.jump_table_offset);
5161 }
5162
5163 bus.write_long(code0_base, code0_size);
5165 bus.write_bytes(code0_user, &code0.data);
5166
5167 let jt_content = &code0.data[16..];
5173 if !jt_content.is_empty() {
5174 bus.write_bytes(jt_base, jt_content);
5175 }
5176
5177 let mut segment_bases = HashMap::new();
5179 segment_bases.insert(0, code0_user);
5180 crate::trap::dispatch::record_segment_base(0, code0_user);
5181
5182 let mut max_jt_end: u32 = jt_base + (jump_table.len() as u32 * 8);
5191 let mut all_codes = fork.get_all_code();
5192 all_codes.sort_by_key(|c| c.id);
5193
5194 for code_res in &all_codes {
5195 if code_res.id == 0 || code_res.data.len() < 4 {
5196 continue;
5197 }
5198 let Some(segment_header) = CodeSegmentHeader::parse(&code_res.data) else {
5199 continue;
5200 };
5201 let Some(tab_off) = segment_header.jump_table_start_offset() else {
5202 continue;
5203 };
5204 let Some(n_entries) = segment_header.jump_table_entry_count() else {
5205 continue;
5206 };
5207 let end = jt_base + tab_off + n_entries * 8;
5208 if end > max_jt_end {
5209 max_jt_end = end;
5210 }
5211
5212 for i in 0..n_entries {
5227 let entry = jt_base + tab_off + i * 8;
5228 let is_empty = bus.read_long(entry) == 0 && bus.read_long(entry + 4) == 0;
5229 let is_null_placeholder = bus.read_word(entry + 2) == 0xFFFF;
5230 if !is_empty && !is_null_placeholder {
5231 continue;
5232 }
5233 if is_null_placeholder {
5234 let routine_offset = bus.read_word(entry);
5239 bus.write_word(entry, 0xA9F0);
5240 bus.write_word(entry + 2, 0);
5241 bus.write_word(entry + 4, routine_offset);
5242 bus.write_word(entry + 6, code_res.id as u16);
5243 } else {
5244 bus.write_word(entry, 0);
5245 bus.write_word(entry + 2, 0x3F3C);
5246 bus.write_word(entry + 4, code_res.id as u16);
5247 bus.write_word(entry + 6, 0xA9F0);
5248 }
5249 }
5250 }
5251
5252 let reserved_boundary = std::cmp::max(code0_user + code0_size, max_jt_end);
5253 let mut current_load_ptr = (reserved_boundary + 4) & !3;
5254
5255 for code_res in all_codes {
5256 if code_res.id == 0 {
5257 continue;
5258 }
5259
5260 let size = code_res.data.len() as u32;
5261 let phys_addr = current_load_ptr;
5262 let user_addr = current_load_ptr + 4;
5263
5264 let segment_header = CodeSegmentHeader::parse(&code_res.data);
5266 let hdr_info = match segment_header {
5267 Some(CodeSegmentHeader::MpwFar) => "mpw-far-model".to_string(),
5268 Some(CodeSegmentHeader::Near {
5269 table_offset,
5270 entry_count,
5271 }) => format!("near-model taboff={} n={}", table_offset, entry_count),
5272 Some(CodeSegmentHeader::ThinkFar {
5273 has_relocations,
5274 first_entry_index,
5275 entry_count,
5276 }) => format!(
5277 "think-far-model first_jt={} n={} relocs={}",
5278 first_entry_index, entry_count, has_relocations
5279 ),
5280 None => "unknown".to_string(),
5281 };
5282 if trace_load_enabled() {
5283 eprintln!(
5284 "[LOAD] Loading CODE {} ({} bytes) to ${:08X} [{}]",
5285 code_res.id, size, user_addr, hdr_info
5286 );
5287 }
5288
5289 bus.write_long(phys_addr, size);
5290 bus.write_bytes(user_addr, &code_res.data);
5291
5292 segment_bases.insert(code_res.id, user_addr);
5293 crate::trap::dispatch::record_segment_base(code_res.id, user_addr);
5294
5295 if matches!(segment_header, Some(CodeSegmentHeader::MpwFar)) {
5299 let header_size = 40u32;
5301 for (i, entry) in jump_table.iter_mut().enumerate() {
5302 if entry.segment == code_res.id {
5303 entry.loaded = true;
5304 let effective_offset = entry.offset as u32;
5305 entry.address = user_addr + header_size + effective_offset;
5306
5307 let jt_addr = jt_base + (i as u32 * 8);
5308 bus.write_word(jt_addr, code_res.id as u16);
5309 bus.write_word(jt_addr + 2, 0x4EF9); bus.write_long(jt_addr + 4, entry.address);
5311 if trace_load_enabled() {
5312 eprintln!(
5313 "[LOAD] JT[{}] -> CODE {} @ ${:08X} (far-model, off=${:04X})",
5314 i, code_res.id, entry.address, effective_offset
5315 );
5316 }
5317 }
5318 }
5319 }
5320
5321 current_load_ptr = (user_addr + size + 4 + 3) & !3;
5322 }
5323
5324 let size_resource = fork
5325 .get(*b"SIZE", -1)
5326 .and_then(|res| ApplicationSizeResource::parse(&res.data));
5327 let loaded_image_end = align4(globals_zero_end.max(current_load_ptr));
5328 if trace_load_enabled() {
5329 eprintln!("[LOAD] Loaded image end=${:08X}", loaded_image_end);
5330 }
5331
5332 let stack_top = bus.ram_size() - 16;
5334
5335 Some(LoadedApp {
5336 code0_header: header,
5337 a5_base,
5338 jump_table,
5339 segment_bases,
5340 loaded_image_end,
5341 initial_sp: stack_top,
5342 size_resource,
5343 })
5344}
5345
5346#[cfg(test)]
5347mod tests {
5348 use super::*;
5349 use crate::audio::AudioBackend;
5350 use crate::loader::{ApplicationSizeResource, Code0Header, LoadedApp};
5351 use crate::sound::{
5352 DoubleBufferState, PendingDoubleBackCallback, PendingSoundCallback, PlaybackKind,
5353 SndChannel, SndCommand, OUTPUT_RATE,
5354 };
5355 use crate::trap::dispatch::{
5356 DialogItem, DialogTrackingState, PendingWaitNextEventReturn, QueuedEvent, TimerTask,
5357 VblTask,
5358 };
5359 use std::cell::RefCell;
5360 use std::collections::{HashMap, VecDeque};
5361 use std::rc::Rc;
5362
5363 #[derive(Clone, Default)]
5364 struct CapturingAudioBackend {
5365 stereo_samples: Rc<RefCell<Vec<u8>>>,
5366 }
5367
5368 impl CapturingAudioBackend {
5369 fn new() -> (Self, Rc<RefCell<Vec<u8>>>) {
5370 let stereo_samples = Rc::new(RefCell::new(Vec::new()));
5371 (
5372 Self {
5373 stereo_samples: stereo_samples.clone(),
5374 },
5375 stereo_samples,
5376 )
5377 }
5378 }
5379
5380 impl AudioBackend for CapturingAudioBackend {
5381 fn queue_samples(&mut self, samples: &[u8]) {
5382 self.stereo_samples.borrow_mut().extend(samples);
5383 }
5384
5385 fn queue_stereo_samples(&mut self, samples: &[u8]) {
5386 self.stereo_samples.borrow_mut().extend(samples);
5387 }
5388
5389 fn stop(&mut self) {}
5390 }
5391
5392 fn test_region_handle(
5393 bus: &mut crate::memory::MacMemoryBus,
5394 top: i16,
5395 left: i16,
5396 bottom: i16,
5397 right: i16,
5398 ) -> u32 {
5399 let rgn_ptr = 0x0030_0100;
5400 let rgn_handle = 0x0030_0140;
5401 bus.write_long(rgn_handle, rgn_ptr);
5402 bus.write_word(rgn_ptr, 10);
5403 bus.write_word(rgn_ptr + 2, top as u16);
5404 bus.write_word(rgn_ptr + 4, left as u16);
5405 bus.write_word(rgn_ptr + 6, bottom as u16);
5406 bus.write_word(rgn_ptr + 8, right as u16);
5407 rgn_handle
5408 }
5409
5410 #[test]
5411 fn vfs_file_snapshot_round_trips_both_forks_and_metadata() {
5412 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
5413 runner
5414 .dispatcher
5415 .vfs
5416 .insert("Pilots/Test Pilot".to_string(), vec![1, 2, 3]);
5417 runner
5418 .dispatcher
5419 .vfs_rsrc
5420 .insert("Pilots/Test Pilot".to_string(), vec![4, 5, 6, 7]);
5421 runner
5422 .dispatcher
5423 .vfs
5424 .insert("__rsrc__Pilots/Test Pilot".to_string(), vec![0xEE]);
5425 runner
5426 .dispatcher
5427 .vfs
5428 .insert("Game Data/Shapes".to_string(), vec![0xAA; 1024]);
5429 runner
5430 .dispatcher
5431 .set_vfs_entry_metadata("Pilots/Test Pilot", *b"PIL ", *b"EVO!", 0x4000);
5432 runner
5433 .dispatcher
5434 .set_vfs_entry_metadata("Game Data/Shapes", *b"shap", *b"26.2", 0);
5435
5436 let summaries = runner.vfs_file_summaries();
5437 assert_eq!(summaries.len(), 2);
5438 assert!(summaries
5439 .iter()
5440 .any(|summary| summary.path == "Game Data/Shapes"));
5441
5442 let summaries = runner.vfs_file_summaries_where(|path| path.starts_with("Pilots/"));
5443 assert_eq!(summaries.len(), 1);
5444 assert_eq!(summaries[0].path, "Pilots/Test Pilot");
5445 assert_eq!(summaries[0].data_len, 3);
5446 assert_eq!(summaries[0].resource_len, 4);
5447 assert_eq!(summaries[0].file_type, u32::from_be_bytes(*b"PIL "));
5448 assert_eq!(summaries[0].creator, u32::from_be_bytes(*b"EVO!"));
5449
5450 let snapshot = runner
5451 .vfs_file_snapshot("Pilots/Test Pilot")
5452 .expect("snapshot");
5453 assert_eq!(snapshot.data_fork, vec![1, 2, 3]);
5454 assert_eq!(snapshot.resource_fork, vec![4, 5, 6, 7]);
5455 assert_eq!(snapshot.finder_flags, 0x4000);
5456
5457 let mut restored = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
5458 restored.import_vfs_file(&snapshot);
5459 assert_eq!(
5460 restored.vfs_file_snapshot("Pilots/Test Pilot"),
5461 Some(snapshot)
5462 );
5463
5464 assert!(restored.remove_vfs_file("Pilots/Test Pilot"));
5465 assert_eq!(restored.vfs_file_snapshot("Pilots/Test Pilot"), None);
5466 }
5467
5468 #[test]
5469 fn import_vfs_file_relative_to_launched_app_mounts_under_app_parent() {
5470 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
5471 runner
5472 .dispatcher
5473 .set_launched_app_path("EV Override/EV Override");
5474 let plugin = VfsFileSnapshot {
5475 path: "Warblade".to_string(),
5476 data_fork: Vec::new(),
5477 resource_fork: vec![1, 2, 3, 4],
5478 file_type: u32::from_be_bytes(*b"Op.f"),
5479 creator: u32::from_be_bytes(*b"Es.O"),
5480 finder_flags: 0x4000,
5481 created_date: 123,
5482 modified_date: 456,
5483 };
5484
5485 runner
5486 .import_vfs_file_relative_to_launched_app("EV Plug-Ins", &plugin)
5487 .expect("relative plugin import");
5488
5489 let mounted = runner
5490 .vfs_file_snapshot("EV Override/EV Plug-Ins/Warblade")
5491 .expect("mounted plugin snapshot");
5492 assert_eq!(mounted.resource_fork, plugin.resource_fork);
5493 assert_eq!(mounted.file_type, plugin.file_type);
5494 assert_eq!(mounted.creator, plugin.creator);
5495 assert_eq!(mounted.finder_flags, plugin.finder_flags);
5496
5497 let parent_dir_id = runner
5498 .dispatcher
5499 .vfs_metadata
5500 .get("EV Override/EV Plug-Ins/Warblade")
5501 .expect("plugin metadata")
5502 .parent_dir_id;
5503 let entries = runner.dispatcher.list_vfs_catalog_entries(parent_dir_id);
5504 assert!(entries
5505 .iter()
5506 .any(|entry| !entry.is_directory && entry.name == "Warblade"));
5507 }
5508
5509 fn make_resource_fork_bytes(resources: &[([u8; 4], i16, &[u8])]) -> Vec<u8> {
5510 let mut type_groups: Vec<([u8; 4], Vec<(i16, &[u8], u32)>)> = Vec::new();
5511 for (res_type, res_id, data) in resources {
5512 let group_idx = type_groups
5513 .iter()
5514 .position(|(existing_type, _)| existing_type == res_type)
5515 .unwrap_or_else(|| {
5516 type_groups.push((*res_type, Vec::new()));
5517 type_groups.len() - 1
5518 });
5519 type_groups[group_idx].1.push((*res_id, *data, 0));
5520 }
5521 type_groups.sort_by_key(|(res_type, _)| *res_type);
5522 for (_, entries) in &mut type_groups {
5523 entries.sort_by_key(|(res_id, _, _)| *res_id);
5524 }
5525
5526 let data_offset = 16u32;
5527 let mut data_section = Vec::new();
5528 for (_, entries) in &mut type_groups {
5529 for (_, data, data_pos) in entries {
5530 *data_pos = data_section.len() as u32;
5531 data_section.extend_from_slice(&(data.len() as u32).to_be_bytes());
5532 data_section.extend_from_slice(data);
5533 }
5534 }
5535
5536 let map_offset = data_offset + data_section.len() as u32;
5537 let type_list_offset = 30u16;
5538 let type_count = type_groups.len();
5539 let resource_count: usize = type_groups.iter().map(|(_, entries)| entries.len()).sum();
5540 let ref_lists_offset = 2 + type_count * 8;
5541 let name_list_offset = type_list_offset as usize + ref_lists_offset + resource_count * 12;
5542 let map_length = name_list_offset as u32;
5543
5544 let mut bytes = vec![0u8; (map_offset + map_length) as usize];
5545 let mut header = [0u8; 16];
5546 header[0..4].copy_from_slice(&data_offset.to_be_bytes());
5547 header[4..8].copy_from_slice(&map_offset.to_be_bytes());
5548 header[8..12].copy_from_slice(&(data_section.len() as u32).to_be_bytes());
5549 header[12..16].copy_from_slice(&map_length.to_be_bytes());
5550 bytes[0..16].copy_from_slice(&header);
5551 bytes[data_offset as usize..data_offset as usize + data_section.len()]
5552 .copy_from_slice(&data_section);
5553
5554 let map_start = map_offset as usize;
5555 bytes[map_start..map_start + 16].copy_from_slice(&header);
5556 bytes[map_start + 24..map_start + 26].copy_from_slice(&type_list_offset.to_be_bytes());
5557 bytes[map_start + 26..map_start + 28]
5558 .copy_from_slice(&(name_list_offset as u16).to_be_bytes());
5559 bytes[map_start + 28..map_start + 30]
5560 .copy_from_slice(&((type_count as u16) - 1).to_be_bytes());
5561
5562 let type_list_start = map_start + type_list_offset as usize;
5563 bytes[type_list_start..type_list_start + 2]
5564 .copy_from_slice(&((type_count as u16) - 1).to_be_bytes());
5565 let mut next_ref_list_offset = ref_lists_offset;
5566 for (i, (res_type, entries)) in type_groups.iter().enumerate() {
5567 let type_entry = type_list_start + 2 + i * 8;
5568 bytes[type_entry..type_entry + 4].copy_from_slice(res_type);
5569 bytes[type_entry + 4..type_entry + 6]
5570 .copy_from_slice(&((entries.len() as u16) - 1).to_be_bytes());
5571 bytes[type_entry + 6..type_entry + 8]
5572 .copy_from_slice(&(next_ref_list_offset as u16).to_be_bytes());
5573
5574 let ref_list_start = type_list_start + next_ref_list_offset;
5575 for (j, (res_id, _, data_pos)) in entries.iter().enumerate() {
5576 let ref_entry = ref_list_start + j * 12;
5577 bytes[ref_entry..ref_entry + 2].copy_from_slice(&(*res_id as u16).to_be_bytes());
5578 bytes[ref_entry + 2..ref_entry + 4].copy_from_slice(&0xFFFFu16.to_be_bytes());
5579 bytes[ref_entry + 4] = 0;
5580 let data_offset_bytes = data_pos.to_be_bytes();
5581 bytes[ref_entry + 5..ref_entry + 8].copy_from_slice(&data_offset_bytes[1..4]);
5582 }
5583
5584 next_ref_list_offset += entries.len() * 12;
5585 }
5586
5587 bytes
5588 }
5589
5590 fn minimal_code0(above_a5: u32, below_a5: u32, jt_size: u32, jt_offset: u32) -> Vec<u8> {
5591 let mut code0 = Vec::with_capacity(16 + jt_size as usize);
5592 code0.extend_from_slice(&above_a5.to_be_bytes());
5593 code0.extend_from_slice(&below_a5.to_be_bytes());
5594 code0.extend_from_slice(&jt_size.to_be_bytes());
5595 code0.extend_from_slice(&jt_offset.to_be_bytes());
5596 code0.resize(16 + jt_size as usize, 0);
5597 code0
5598 }
5599
5600 #[test]
5601 fn init_app_preserves_resources_allocated_before_zone_header() {
5602 let code0 = minimal_code0(0, 0x2000, 0, 0);
5603 let bgas = [0x4E, 0x56, 0xFF, 0xA6, 0x2D, 0x7A, 0x1C, 0x72];
5604 let fork_bytes = make_resource_fork_bytes(&[(*b"BGAS", 128, &bgas), (*b"CODE", 0, &code0)]);
5605 let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
5606 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
5607
5608 let app = runner.load_app(&fork).expect("load app");
5609 let (_, bgas_ptr) = runner
5610 .dispatcher
5611 .find_resource_any(*b"BGAS", 128)
5612 .expect("BGAS resource loaded");
5613 assert_eq!(runner.bus.read_bytes(bgas_ptr, bgas.len()), bgas);
5614
5615 runner.init_app(&app);
5616
5617 assert_eq!(
5618 runner.bus.read_bytes(bgas_ptr, bgas.len()),
5619 bgas,
5620 "init_app must not overwrite resources loaded before zone setup"
5621 );
5622 }
5623
5624 #[test]
5625 fn load_app_places_resources_above_large_loaded_image() {
5626 use crate::memory::globals::addr;
5627
5628 let code0 = minimal_code0(0x001D_0000, 0x0340, 0, 0);
5629 let marker = [0xCA, 0xFE, 0xBA, 0xBE, 0x12, 0x34, 0x56, 0x78];
5630 let fork_bytes =
5631 make_resource_fork_bytes(&[(*b"BGAS", 128, &marker), (*b"CODE", 0, &code0)]);
5632 let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
5633 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
5634
5635 let app = runner.load_app(&fork).expect("load app");
5636 assert!(
5637 app.loaded_image_end > APP_HEAP_FLOOR,
5638 "fixture should force the loaded image across the default heap floor"
5639 );
5640
5641 let heap_start = app_heap_start_for_loaded_app(&app);
5642 let (_, marker_ptr) = runner
5643 .dispatcher
5644 .find_resource_any(*b"BGAS", 128)
5645 .expect("BGAS resource loaded");
5646 assert!(
5647 marker_ptr >= heap_start + APP_ZONE_HEADER_SIZE,
5648 "resource data must be allocated after the relocated zone header"
5649 );
5650 assert_eq!(runner.bus.read_bytes(marker_ptr, marker.len()), marker);
5651
5652 runner.init_app(&app);
5653
5654 assert_eq!(runner.bus.read_long(addr::APP_L_ZONE), heap_start);
5655 assert_eq!(
5656 runner.bus.read_long(addr::HEAP_END),
5657 heap_start + APP_ZONE_HEADER_SIZE
5658 );
5659 assert_eq!(
5660 runner.bus.read_bytes(marker_ptr, marker.len()),
5661 marker,
5662 "launch initialization must not clobber resources for large loaded images"
5663 );
5664 }
5665
5666 #[test]
5667 fn load_app_records_application_size_resource_id_minus_one() {
5668 let code0 = minimal_code0(0, 0x2000, 0, 0);
5669 let size = [
5670 0x00, 0x80, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, ];
5674 let fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &code0), (*b"SIZE", -1, &size)]);
5675 let fork = ResourceFork::parse(&fork_bytes).expect("parse synthetic app fork");
5676 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
5677
5678 let app = runner.load_app(&fork).expect("load app");
5679
5680 assert_eq!(
5681 app.size_resource,
5682 Some(ApplicationSizeResource {
5683 flags: 0x0080,
5684 preferred_size: 0x0030_0000,
5685 minimum_size: 0x0020_0000,
5686 })
5687 );
5688 }
5689
5690 #[test]
5691 fn event_yield_services_pending_launch_application_from_vfs() {
5692 use crate::memory::globals::addr;
5693
5694 let current_code0 = minimal_code0(0, 0x2000, 0, 0);
5695 let helper_code0 = minimal_code0(0, 0x2000, 0, 0);
5696 let current_fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, ¤t_code0)]);
5697 let helper_fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &helper_code0)]);
5698 let current_fork =
5699 ResourceFork::parse(¤t_fork_bytes).expect("parse current app fork");
5700 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
5701
5702 runner
5703 .dispatcher
5704 .vfs
5705 .insert("Apps/Main App".to_string(), Vec::new());
5706 runner
5707 .dispatcher
5708 .vfs_rsrc
5709 .insert("Apps/Main App".to_string(), current_fork_bytes);
5710 runner
5711 .dispatcher
5712 .vfs
5713 .insert("Apps/Register Helper".to_string(), Vec::new());
5714 runner
5715 .dispatcher
5716 .vfs_rsrc
5717 .insert("Apps/Register Helper".to_string(), helper_fork_bytes);
5718 runner.dispatcher.ensure_vfs_catalog();
5719 runner.dispatcher.set_launched_app_path("Apps/Main App");
5720
5721 let app = runner.load_app(¤t_fork).expect("load current app");
5722 runner.init_app(&app);
5723 runner.bus.write_long(addr::TICKS, 1234);
5724 runner.dispatcher.tick_count = 1234;
5725 runner
5726 .dispatcher
5727 .queue_pending_launch_application("Apps/Register Helper", true);
5728
5729 assert!(
5730 !runner.service_pending_launch_application(false),
5731 "launchContinue target must wait for an Event Manager yield"
5732 );
5733 let switched = runner.service_pending_launch_application(true);
5734
5735 assert!(
5736 switched,
5737 "event yield should service the queued helper launch"
5738 );
5739 assert!(
5740 !runner.is_halted(),
5741 "queued helper launch should not halt the runner"
5742 );
5743 assert_eq!(
5744 runner.dispatcher.launched_app_path.as_deref(),
5745 Some("Apps/Register Helper")
5746 );
5747 assert_eq!(
5748 runner.bus.read_long(addr::TICKS),
5749 1234,
5750 "Process Manager launch must preserve system TickCount"
5751 );
5752 let cur_ap_len = runner.bus.read_byte(addr::CUR_APNAME) as usize;
5753 let cur_ap_name = String::from_utf8(
5754 (0..cur_ap_len)
5755 .map(|i| runner.bus.read_byte(addr::CUR_APNAME + 1 + i as u32))
5756 .collect(),
5757 )
5758 .expect("CurApName is ASCII");
5759 assert_eq!(cur_ap_name, "Register Helper");
5760 assert!(
5761 runner.dispatcher.vfs.contains_key("Apps/Main App"),
5762 "archive VFS entries must survive the foreground app switch"
5763 );
5764 assert!(
5765 runner
5766 .dispatcher
5767 .vfs_rsrc
5768 .contains_key("Apps/Register Helper"),
5769 "launched app resource fork must remain available after the switch"
5770 );
5771 }
5772
5773 #[test]
5774 fn immediate_pending_launch_application_switches_from_vfs_without_event_yield() {
5775 use crate::memory::globals::addr;
5776
5777 let current_code0 = minimal_code0(0, 0x2000, 0, 0);
5778 let helper_code0 = minimal_code0(0, 0x2000, 0, 0);
5779 let current_fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, ¤t_code0)]);
5780 let helper_fork_bytes = make_resource_fork_bytes(&[(*b"CODE", 0, &helper_code0)]);
5781 let current_fork =
5782 ResourceFork::parse(¤t_fork_bytes).expect("parse current app fork");
5783 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
5784
5785 runner
5786 .dispatcher
5787 .vfs
5788 .insert("Apps/Main App".to_string(), Vec::new());
5789 runner
5790 .dispatcher
5791 .vfs_rsrc
5792 .insert("Apps/Main App".to_string(), current_fork_bytes);
5793 runner
5794 .dispatcher
5795 .vfs
5796 .insert("Apps/Register Helper".to_string(), Vec::new());
5797 runner
5798 .dispatcher
5799 .vfs_rsrc
5800 .insert("Apps/Register Helper".to_string(), helper_fork_bytes);
5801 runner.dispatcher.ensure_vfs_catalog();
5802 runner.dispatcher.set_launched_app_path("Apps/Main App");
5803
5804 let app = runner.load_app(¤t_fork).expect("load current app");
5805 runner.init_app(&app);
5806 runner.bus.write_long(addr::TICKS, 4321);
5807 runner.dispatcher.tick_count = 4321;
5808 runner
5809 .dispatcher
5810 .queue_pending_launch_application("Apps/Register Helper", false);
5811
5812 let switched = runner.service_pending_launch_application(false);
5813
5814 assert!(
5815 switched,
5816 "immediate pending launch should not require an Event Manager yield"
5817 );
5818 assert!(
5819 !runner.is_halted(),
5820 "immediate queued helper launch should not halt the runner"
5821 );
5822 assert_eq!(
5823 runner.dispatcher.launched_app_path.as_deref(),
5824 Some("Apps/Register Helper")
5825 );
5826 assert_eq!(
5827 runner.bus.read_long(addr::TICKS),
5828 4321,
5829 "foreground app switch must preserve system TickCount"
5830 );
5831 let cur_ap_len = runner.bus.read_byte(addr::CUR_APNAME) as usize;
5832 let cur_ap_name = String::from_utf8(
5833 (0..cur_ap_len)
5834 .map(|i| runner.bus.read_byte(addr::CUR_APNAME + 1 + i as u32))
5835 .collect(),
5836 )
5837 .expect("CurApName is ASCII");
5838 assert_eq!(cur_ap_name, "Register Helper");
5839 }
5840
5841 #[test]
5842 fn fixture_runner_defaults_to_classic_system7_theme() {
5843 let runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
5844
5845 assert_eq!(runner.ui_theme_id(), UiThemeId::ClassicSystem7);
5846 assert_eq!(runner.dispatcher().ui_theme_id(), UiThemeId::ClassicSystem7);
5847 assert_eq!(runner.ui_theme().id(), UiThemeId::ClassicSystem7);
5848 assert_eq!(
5849 runner.theme_metrics_mode(),
5850 ThemeMetricsMode::ClassicGuestMetrics
5851 );
5852 assert!(runner.uses_classic_guest_metrics());
5853 }
5854
5855 #[test]
5856 fn fixture_runner_accepts_explicit_systemless_theme_without_themed_metrics() {
5857 let runner = FixtureRunner::new(
5858 8 * 1024 * 1024,
5859 FixtureRunnerConfig {
5860 ui_theme: UiThemeId::SystemlessDefault,
5861 theme_metrics_mode: ThemeMetricsMode::ClassicGuestMetrics,
5862 ..FixtureRunnerConfig::default()
5863 },
5864 );
5865 let classic = UiThemeId::ClassicSystem7.provider();
5866
5867 assert_eq!(runner.ui_theme_id(), UiThemeId::SystemlessDefault);
5868 assert_eq!(
5869 runner.dispatcher().ui_theme_id(),
5870 UiThemeId::SystemlessDefault
5871 );
5872 assert_eq!(runner.ui_theme().id(), UiThemeId::SystemlessDefault);
5873 assert!(runner.uses_classic_guest_metrics());
5874 assert_eq!(runner.ui_theme().menu_metrics(), classic.menu_metrics());
5875 assert_eq!(
5876 runner.ui_theme().control_metrics(),
5877 classic.control_metrics()
5878 );
5879 assert_ne!(runner.ui_theme().palette(), classic.palette());
5880 }
5881
5882 fn write_double_buffer(bus: &mut MacMemoryBus, ptr: u32, samples: &[u8]) {
5883 bus.write_long(ptr, samples.len() as u32);
5884 bus.write_long(ptr + 4, 0x0000_0001);
5885 for (offset, sample) in samples.iter().copied().enumerate() {
5886 bus.write_byte(ptr + 16 + offset as u32, sample);
5887 }
5888 }
5889
5890 #[test]
5891 fn headless_run_steps_does_not_implicitly_mix_audio() {
5892 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
5893 let program_start = 0x0001_0000;
5894 for offset in (0..16).step_by(2) {
5895 runner.bus.write_word(program_start + offset, 0x4E71); }
5897 runner.cpu.write_reg(Register::PC, program_start);
5898 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
5899
5900 let mut chan = SndChannel::new(0x0039_38C8, false);
5901 chan.play_buffer(
5902 vec![0x90, 0x91, 0x92],
5903 OUTPUT_RATE << 16,
5904 PlaybackKind::Buffer,
5905 0,
5906 );
5907 runner.dispatcher.sound_manager.channels.push(chan);
5908
5909 let (steps, running) = runner.run_steps(2, None);
5910
5911 assert!(running);
5912 assert_eq!(steps, 2);
5913 assert_eq!(
5914 runner.audio_buffer_len(),
5915 0,
5916 "plain headless stepping must not consume sound buffers"
5917 );
5918 assert_eq!(runner.dispatcher.sound_manager.debug_samples_mixed, 0);
5919
5920 runner.mix_audio(2);
5921 assert_eq!(runner.drain_audio(), vec![0x90, 0x91]);
5922 assert_eq!(runner.dispatcher.sound_manager.debug_samples_mixed, 2);
5923 }
5924
5925 #[test]
5926 fn host_audio_backend_receives_silence_while_sound_manager_idle() {
5927 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
5928 let (audio_backend, stereo_samples) = CapturingAudioBackend::new();
5929 runner.set_audio(Box::new(audio_backend));
5930
5931 runner.mix_audio(4);
5932
5933 assert_eq!(
5934 stereo_samples.borrow().as_slice(),
5935 &[0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80]
5936 );
5937 assert_eq!(
5938 runner.audio_buffer_len(),
5939 0,
5940 "host silence must not become captured guest audio"
5941 );
5942 assert_eq!(runner.dispatcher.sound_manager.debug_samples_mixed, 0);
5943 }
5944
5945 #[test]
5946 fn host_audio_backend_receives_low_rate_sample_hold_output() {
5947 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
5948 let (audio_backend, stereo_samples) = CapturingAudioBackend::new();
5949 runner.set_audio(Box::new(audio_backend));
5950
5951 let mut chan = SndChannel::new(0x0039_38C8, false);
5952 chan.play_buffer(
5953 vec![0x90, 0xA0],
5954 (OUTPUT_RATE / 2) << 16,
5955 PlaybackKind::Buffer,
5956 0,
5957 );
5958 runner.dispatcher.sound_manager.channels.push(chan);
5959
5960 runner.mix_audio(4);
5961
5962 assert_eq!(
5963 stereo_samples.borrow().as_slice(),
5964 &[
5965 0x90, 0x90, 0x90, 0x90, 0xA0, 0xA0, 0xA0, 0xA0, ],
5970 "GUI/host audio path must receive the sample-hold low-rate output, not the old linear midpoint"
5971 );
5972 assert_eq!(runner.dispatcher.sound_manager.debug_samples_mixed, 4);
5973 }
5974
5975 fn dialog_tracking_for_test(filter_proc: u32, item_hit_ptr: u32) -> DialogTrackingState {
5976 DialogTrackingState {
5977 dialog_ptr: 0x0020_0000,
5978 bounds: (0, 0, 32, 32),
5979 title: String::new(),
5980 proc_id: 1,
5981 items: Vec::new(),
5982 default_item: 0,
5983 cancel_item: 0,
5984 edit_text: String::new(),
5985 edit_item: 0,
5986 saved_pixels: Vec::new(),
5987 stack_ptr: 0,
5988 item_hit_ptr,
5989 rendered_pixels: Vec::new(),
5990 flash_remaining: 0,
5991 flash_delay: 0,
5992 flash_item: 0,
5993 edit_text_modified: false,
5994 draw_proc_queue: VecDeque::new(),
5995 draw_procs_done: true,
5996 rendered_pixels_final: true,
5997 filter_proc,
5998 game_managed: false,
5999 last_filter_event: None,
6000 popup_draws: Vec::new(),
6001 active_popup: None,
6002 active_button: None,
6003 active_user_item: None,
6004 }
6005 }
6006
6007 #[test]
6008 fn arrows_as_numpad_remaps_key_and_char_together() {
6009 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6010 runner.set_arrows_as_numpad(true);
6011
6012 assert_eq!(runner.remap_key(0x7B, 28), (0x56, b'4'));
6013 assert_eq!(runner.remap_key(0x7C, 29), (0x58, b'6'));
6014 assert_eq!(runner.remap_key(0x7D, 31), (0x57, b'5'));
6015 assert_eq!(runner.remap_key(0x7E, 30), (0x5B, b'8'));
6016 assert_eq!(runner.remap_key(0x2E, b'm'), (0x2E, b'm'));
6017 }
6018
6019 #[test]
6020 fn arrows_not_remapped_by_default() {
6021 let runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6022
6023 assert!(!runner.arrows_as_numpad());
6024 assert_eq!(runner.remap_key(0x7B, 28), (0x7B, 28));
6025 assert_eq!(runner.remap_key(0x7C, 29), (0x7C, 29));
6026 assert_eq!(runner.remap_key(0x2E, b'm'), (0x2E, b'm'));
6027 }
6028
6029 #[test]
6030 fn key_events_sync_low_memory_keymap() {
6031 use crate::memory::globals::addr;
6032
6033 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6034
6035 assert_eq!(runner.bus.read_byte(addr::KEY_MAP_LM + 4), 0);
6036 assert_eq!(runner.bus.read_byte(addr::KEY_MAP_LM + 15), 0);
6037
6038 runner.push_key_down(0x26, b'j');
6039 runner.push_key_down(0x7E, 30);
6040
6041 assert_eq!(
6042 runner.bus.read_byte(addr::KEY_MAP_LM + 4),
6043 0x40,
6044 "J key should be visible to byte/bit KeyMap readers"
6045 );
6046 assert_eq!(
6047 runner.bus.read_byte(addr::KEY_MAP_LM + 5),
6048 0,
6049 "J key should not alias M at KeyMapLM byte 5 bit 6"
6050 );
6051 assert_eq!(
6052 runner.bus.read_byte(addr::KEY_MAP_LM + 15),
6053 0x40,
6054 "up arrow should be visible to byte/bit KeyMap readers"
6055 );
6056 assert_eq!(
6057 runner.bus.read_byte(addr::KEY_MAP_LM + 14),
6058 0,
6059 "up arrow should not be mirrored into the unused raw byte"
6060 );
6061
6062 runner.push_key_up(0x26, b'j');
6063
6064 assert_eq!(
6065 runner.bus.read_byte(addr::KEY_MAP_LM + 4),
6066 0,
6067 "J key release should clear the low-memory mirror"
6068 );
6069 assert_eq!(
6070 runner.bus.read_byte(addr::KEY_MAP_LM + 5),
6071 0,
6072 "J key release should leave the M-key byte clear"
6073 );
6074 assert_eq!(
6075 runner.bus.read_byte(addr::KEY_MAP_LM + 15),
6076 0x40,
6077 "unrelated byte/bit down keys should remain mirrored"
6078 );
6079 assert_eq!(
6080 runner.bus.read_byte(addr::KEY_MAP_LM + 14),
6081 0,
6082 "unused raw byte should stay clear"
6083 );
6084 }
6085
6086 #[test]
6087 fn init_app_seeds_top_of_stack_with_nonzero_bytes() {
6088 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6089 let app = LoadedApp {
6090 code0_header: Code0Header {
6091 above_a5: 0,
6092 below_a5: 0x2000,
6093 jump_table_size: 0,
6094 jump_table_offset: 0,
6095 },
6096 a5_base: 0x0040_0000,
6097 jump_table: Vec::new(),
6098 segment_bases: HashMap::new(),
6099 loaded_image_end: 0,
6100 initial_sp: 0x007F_FFC0,
6101 size_resource: None,
6102 };
6103
6104 runner.init_app(&app);
6105
6106 let stack_seed_start = app.initial_sp.saturating_sub(0x8000);
6107 assert_eq!(
6108 runner.bus.read_long(stack_seed_start),
6109 0xA5A5_A5A5,
6110 "top-of-stack window must not be zeroed"
6111 );
6112 }
6113
6114 #[test]
6115 fn init_app_leaves_application_heap_room_below_appllimit() {
6116 use crate::memory::globals::addr;
6117
6118 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6119 let app = LoadedApp {
6120 code0_header: Code0Header {
6121 above_a5: 0,
6122 below_a5: 0x2000,
6123 jump_table_size: 0,
6124 jump_table_offset: 0,
6125 },
6126 a5_base: 0x0040_0000,
6127 jump_table: Vec::new(),
6128 segment_bases: HashMap::new(),
6129 loaded_image_end: 0,
6130 initial_sp: 0x007F_FFC0,
6131 size_resource: None,
6132 };
6133
6134 runner.init_app(&app);
6135
6136 let heap_end = runner.bus.read_long(addr::HEAP_END);
6137 let appl_limit = runner.bus.read_long(addr::APPL_LIMIT);
6138 assert_eq!(
6139 heap_end,
6140 0x0020_0000 + APP_ZONE_HEADER_SIZE,
6141 "HeapEnd should expose the initial application-zone extent"
6142 );
6143 assert!(
6144 appl_limit.saturating_sub(heap_end) >= 2300 * 1024,
6145 "direct low-memory startup checks should see growable heap room below ApplLimit"
6146 );
6147 }
6148
6149 #[test]
6150 fn init_app_honors_size_resource_preferred_partition_for_heap_reporting() {
6151 use crate::memory::globals::addr;
6152
6153 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6154 let preferred_partition = 3 * 1024 * 1024;
6155 let app = LoadedApp {
6156 code0_header: Code0Header {
6157 above_a5: 0,
6158 below_a5: 0x2000,
6159 jump_table_size: 0,
6160 jump_table_offset: 0,
6161 },
6162 a5_base: 0x0040_0000,
6163 jump_table: Vec::new(),
6164 segment_bases: HashMap::new(),
6165 loaded_image_end: 0,
6166 initial_sp: 0x007F_FFC0,
6167 size_resource: Some(ApplicationSizeResource {
6168 flags: 0x0080,
6169 preferred_size: preferred_partition,
6170 minimum_size: 2 * 1024 * 1024,
6171 }),
6172 };
6173
6174 runner.init_app(&app);
6175
6176 let expected_limit = 0x0020_0000 + preferred_partition - APP_STACK_SAFETY_MARGIN;
6177 let expected_free = expected_limit - (0x0020_0000 + APP_ZONE_HEADER_SIZE);
6178 assert_eq!(runner.bus.read_long(addr::APPL_LIMIT), expected_limit);
6179 assert_eq!(runner.bus.read_long(addr::BUF_PTR), expected_limit);
6180 assert_eq!(runner.bus.read_long(0x0020_0000), expected_limit);
6181 assert_eq!(runner.bus.read_long(0x0020_0000 + 12), expected_free);
6182 assert_eq!(
6183 crate::memory::app_heap_free_bytes(runner.bus()),
6184 expected_free
6185 );
6186 assert!(
6187 expected_free < crate::memory::APP_HEAP_COMPAT_FREE_FLOOR,
6188 "explicit SIZE partitions must bypass the compatibility floor"
6189 );
6190 }
6191
6192 #[test]
6193 fn init_app_application_partition_override_takes_precedence_over_size_resource() {
6194 use crate::memory::globals::addr;
6195
6196 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6197 let size_partition = 3 * 1024 * 1024;
6198 let override_partition = 4 * 1024 * 1024;
6199 runner.set_application_partition_size(Some(override_partition));
6200 let app = LoadedApp {
6201 code0_header: Code0Header {
6202 above_a5: 0,
6203 below_a5: 0x2000,
6204 jump_table_size: 0,
6205 jump_table_offset: 0,
6206 },
6207 a5_base: 0x0040_0000,
6208 jump_table: Vec::new(),
6209 segment_bases: HashMap::new(),
6210 loaded_image_end: 0,
6211 initial_sp: 0x007F_FFC0,
6212 size_resource: Some(ApplicationSizeResource {
6213 flags: 0x0080,
6214 preferred_size: size_partition,
6215 minimum_size: 2 * 1024 * 1024,
6216 }),
6217 };
6218
6219 runner.init_app(&app);
6220
6221 let expected_limit = 0x0020_0000 + override_partition - APP_STACK_SAFETY_MARGIN;
6222 assert_eq!(runner.bus.read_long(addr::APPL_LIMIT), expected_limit);
6223 assert_eq!(
6224 crate::memory::app_heap_free_bytes(runner.bus()),
6225 expected_limit - (0x0020_0000 + APP_ZONE_HEADER_SIZE)
6226 );
6227 }
6228
6229 #[test]
6230 fn init_app_ignores_too_small_application_partition_override() {
6231 use crate::memory::globals::addr;
6232
6233 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6234 runner.set_application_partition_size(Some(64 * 1024));
6235 assert_eq!(runner.application_partition_size(), None);
6236 let app = LoadedApp {
6237 code0_header: Code0Header {
6238 above_a5: 0,
6239 below_a5: 0x2000,
6240 jump_table_size: 0,
6241 jump_table_offset: 0,
6242 },
6243 a5_base: 0x0040_0000,
6244 jump_table: Vec::new(),
6245 segment_bases: HashMap::new(),
6246 loaded_image_end: 0,
6247 initial_sp: 0x007F_FFC0,
6248 size_resource: None,
6249 };
6250
6251 runner.init_app(&app);
6252
6253 assert_eq!(
6254 runner.bus.read_long(addr::APPL_LIMIT),
6255 app.initial_sp - APP_STACK_SAFETY_MARGIN,
6256 "invalid tiny overrides must fall back to the default launch limit"
6257 );
6258 }
6259
6260 #[test]
6261 fn init_app_seeds_appparmhandle_with_empty_finder_information() {
6262 use crate::memory::globals::addr;
6263
6264 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6265 runner
6266 .dispatcher_mut()
6267 .set_launched_app_path("Games/Armor Alley");
6268 let app = LoadedApp {
6269 code0_header: Code0Header {
6270 above_a5: 0,
6271 below_a5: 0x2000,
6272 jump_table_size: 0,
6273 jump_table_offset: 0,
6274 },
6275 a5_base: 0x0040_0000,
6276 jump_table: Vec::new(),
6277 segment_bases: HashMap::new(),
6278 loaded_image_end: 0,
6279 initial_sp: 0x007F_FFC0,
6280 size_resource: None,
6281 };
6282
6283 runner.init_app(&app);
6284
6285 let handle = runner.bus.read_long(addr::APP_PARM_HANDLE);
6286 assert_ne!(
6287 handle, 0,
6288 "AppParmHandle should point at Finder launch information"
6289 );
6290 let data_ptr = runner.bus.read_long(handle);
6291 assert_ne!(
6292 data_ptr, 0,
6293 "Finder launch information handle should be loaded"
6294 );
6295 assert_eq!(
6296 runner.bus.get_alloc_size(data_ptr),
6297 Some(4),
6298 "empty Finder launch information is message/count only"
6299 );
6300 assert_eq!(
6301 runner.bus.read_word(data_ptr),
6302 0,
6303 "message should be appOpen for a normal application launch"
6304 );
6305 assert_eq!(
6306 runner.bus.read_word(data_ptr + 2),
6307 0,
6308 "normal application launch has no selected documents"
6309 );
6310 }
6311
6312 #[test]
6313 fn init_app_sets_legacy_sound_driver_low_memory_defaults() {
6314 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6315 let app = LoadedApp {
6316 code0_header: Code0Header {
6317 above_a5: 0,
6318 below_a5: 0x2000,
6319 jump_table_size: 0,
6320 jump_table_offset: 0,
6321 },
6322 a5_base: 0x0040_0000,
6323 jump_table: Vec::new(),
6324 segment_bases: HashMap::new(),
6325 loaded_image_end: 0,
6326 initial_sp: 0x007F_FFC0,
6327 size_resource: None,
6328 };
6329
6330 runner.init_app(&app);
6331
6332 assert_eq!(
6333 runner
6334 .bus
6335 .read_byte(crate::memory::globals::addr::SD_VOLUME),
6336 1,
6337 "SdVolume ($0260) should boot to the nonzero legacy compatibility value"
6338 );
6339 assert_eq!(
6340 runner
6341 .bus
6342 .read_byte(crate::memory::globals::addr::SOUND_LEVEL),
6343 0,
6344 "SoundLevel ($027F) is a distinct Sound Driver amplitude byte"
6345 );
6346 let sound_base = runner
6347 .bus
6348 .read_long(crate::memory::globals::addr::SOUND_BASE);
6349 assert_eq!(
6350 sound_base, 0x007F_5300,
6351 "SoundBase ($0266) should point at the 370-word legacy sound buffer in reserved display memory"
6352 );
6353 assert_eq!(
6354 runner.bus.read_byte(sound_base),
6355 0x80,
6356 "legacy SoundBase buffer starts at neutral amplitude"
6357 );
6358 }
6359
6360 #[test]
6361 fn init_app_seeds_mmu32bit_low_memory_flag() {
6362 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6363 let app = LoadedApp {
6364 code0_header: Code0Header {
6365 above_a5: 0,
6366 below_a5: 0x2000,
6367 jump_table_size: 0,
6368 jump_table_offset: 0,
6369 },
6370 a5_base: 0x0040_0000,
6371 jump_table: Vec::new(),
6372 segment_bases: HashMap::new(),
6373 loaded_image_end: 0,
6374 initial_sp: 0x007F_FFC0,
6375 size_resource: None,
6376 };
6377
6378 runner.init_app(&app);
6379
6380 assert_eq!(
6381 runner
6382 .bus
6383 .read_byte(crate::memory::globals::addr::MMU32_BIT),
6384 1,
6385 "MMU32Bit ($0CB2) should mirror Systemless's default 32-bit addressing mode"
6386 );
6387 }
6388
6389 #[test]
6390 fn init_app_seeds_cursor_task_low_memory_vector() {
6391 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6392 let app = LoadedApp {
6393 code0_header: Code0Header {
6394 above_a5: 0,
6395 below_a5: 0x2000,
6396 jump_table_size: 0,
6397 jump_table_offset: 0,
6398 },
6399 a5_base: 0x0040_0000,
6400 jump_table: Vec::new(),
6401 segment_bases: HashMap::new(),
6402 loaded_image_end: 0,
6403 initial_sp: 0x007F_FFC0,
6404 size_resource: None,
6405 };
6406
6407 runner.init_app(&app);
6408
6409 assert_eq!(
6410 runner.bus.read_word(CURSOR_TASK_NOOP_ADDR),
6411 0x4E75,
6412 "default cursor task target should be a callable RTS stub"
6413 );
6414 assert_eq!(
6415 runner
6416 .bus
6417 .read_long(crate::memory::globals::addr::J_CRSR_TASK),
6418 CURSOR_TASK_NOOP_ADDR,
6419 "JCrsrTask ($08EE) should boot to a callable no-op vector"
6420 );
6421 }
6422
6423 #[test]
6424 fn cursor_task_noop_vector_does_not_fire_on_guest_tick() {
6425 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6426 let interrupted_pc = 0x0002_0000;
6427 let interrupted_sp = 0x007F_FFC0;
6428
6429 runner.bus.write_long(
6430 crate::memory::globals::addr::J_CRSR_TASK,
6431 CURSOR_TASK_NOOP_ADDR,
6432 );
6433 runner.cpu.write_reg(Register::PC, interrupted_pc);
6434 runner.cpu.write_reg(Register::A7, interrupted_sp);
6435
6436 runner.advance_guest_tick();
6437
6438 assert!(runner.active_interrupt_callback.is_none());
6439 assert_eq!(runner.cursor_task_trampoline, 0);
6440 assert_eq!(runner.cpu.read_reg(Register::PC), interrupted_pc);
6441 assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
6442 assert_eq!(runner.bus.read_long(crate::memory::globals::addr::TICKS), 1);
6443 }
6444
6445 #[test]
6446 fn cursor_task_callback_arms_interrupt_from_low_memory_vector() {
6447 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6448 let interrupted_pc = 0x0002_0000;
6449 let interrupted_sp = 0x007F_FFC0;
6450 let callback_addr = 0x0004_1234;
6451
6452 runner
6453 .bus
6454 .write_long(crate::memory::globals::addr::J_CRSR_TASK, callback_addr);
6455 runner.cpu.write_reg(Register::PC, interrupted_pc);
6456 runner.cpu.write_reg(Register::A7, interrupted_sp);
6457 runner.cpu.write_reg(Register::D0, 0x1111_1111);
6458 runner.cpu.write_reg(Register::D7, 0x7777_7777);
6459 runner.cpu.write_reg(Register::A0, 0xAAAA_0000);
6460 runner.cpu.write_reg(Register::A6, 0xCCCC_0000);
6461 runner.cpu.core.set_ccr(0x04);
6462
6463 runner.advance_guest_tick();
6464
6465 let active = runner
6466 .active_interrupt_callback
6467 .expect("cursor task callback should have been armed");
6468 assert!(matches!(
6469 active.source,
6470 ActiveInterruptCallbackSource::CursorTask
6471 ));
6472 assert_eq!(active.resume_pc, interrupted_pc);
6473 assert_eq!(active.resume_sp, interrupted_sp);
6474 assert_eq!(active.a_regs[7], interrupted_sp);
6475 assert_eq!(active.a_regs[6], 0xCCCC_0000);
6476 assert_eq!(active.d_regs[0], 0x1111_1111);
6477 assert_eq!(active.d_regs[7], 0x7777_7777);
6478 assert_eq!(active.ccr, 0x04);
6479
6480 assert_ne!(runner.cursor_task_trampoline, 0);
6481 assert_eq!(
6482 runner.cpu.read_reg(Register::PC),
6483 runner.cursor_task_trampoline
6484 );
6485 assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp - 4);
6486 assert_eq!(runner.bus.read_long(interrupted_sp - 4), interrupted_pc);
6487 assert_eq!(runner.bus.read_word(runner.cursor_task_trampoline), 0x48E7);
6488 assert_eq!(
6489 runner.bus.read_word(runner.cursor_task_trampoline + 4),
6490 0x4EB9
6491 );
6492 assert_eq!(
6493 runner.bus.read_long(runner.cursor_task_trampoline + 6),
6494 callback_addr
6495 );
6496 assert_eq!(
6497 runner.bus.read_word(runner.cursor_task_trampoline + 10),
6498 0x4CDF
6499 );
6500 assert_eq!(
6501 runner.bus.read_word(runner.cursor_task_trampoline + 14),
6502 0x4E75
6503 );
6504 }
6505
6506 #[test]
6507 fn timer_callback_snapshot_preserves_interrupted_sp() {
6508 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6509 let interrupted_pc = 0x0002_8BAC;
6510 let interrupted_sp = 0x007F_FFC0;
6511
6512 runner.cpu.write_reg(Register::PC, interrupted_pc);
6513 runner.cpu.write_reg(Register::D0, 0x1111_1111);
6514 runner.cpu.write_reg(Register::D7, 0x7777_7777);
6515 runner.cpu.write_reg(Register::A0, 0xAAAA_0000);
6516 runner.cpu.write_reg(Register::A6, 0xCCCC_0000);
6517 runner.cpu.write_reg(Register::A7, interrupted_sp);
6518 runner.cpu.core.set_ccr(0x1F);
6519
6520 runner.dispatcher.timer_tasks.push(TimerTask {
6521 task_ptr: 0x0039_38C8,
6522 tm_addr: 0x0004_1234,
6523 active: true,
6524 fire_at_tick: 10,
6525 });
6526
6527 runner.fire_timer_tasks(10);
6528
6529 let active = runner
6530 .active_interrupt_callback
6531 .expect("timer callback should have been armed");
6532
6533 assert!(matches!(
6534 active.source,
6535 ActiveInterruptCallbackSource::Timer
6536 ));
6537 assert_eq!(active.resume_pc, interrupted_pc);
6538 assert_eq!(active.resume_sp, interrupted_sp);
6539 assert_eq!(active.a_regs[7], interrupted_sp);
6540 assert_eq!(active.a_regs[6], 0xCCCC_0000);
6541 assert_eq!(active.d_regs[0], 0x1111_1111);
6542 assert_eq!(active.d_regs[7], 0x7777_7777);
6543 assert_eq!(active.ccr, 0x1F);
6544
6545 assert_ne!(runner.timer_trampoline, 0);
6546 assert_eq!(runner.cpu.read_reg(Register::PC), runner.timer_trampoline);
6547 assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp - 4);
6548 assert_eq!(runner.bus.read_long(interrupted_sp - 4), interrupted_pc);
6549 }
6550
6551 #[test]
6552 fn timer_callback_fired_at_tick_cap_runs_before_yielding() {
6553 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6554 let interrupted_pc = 0x0001_0000;
6555 let interrupted_sp = 0x007F_FFC0;
6556 let callback_addr = 0x0002_0000;
6557
6558 runner.cpu.write_reg(Register::PC, interrupted_pc);
6559 runner.cpu.write_reg(Register::A7, interrupted_sp);
6560 runner.bus.write_long(0x016A, 100);
6561 runner.dispatcher.tick_count = 100;
6562 runner.tick_budget = 0;
6563 runner.bus.write_word(callback_addr, 0x4E75); runner.dispatcher.timer_tasks.push(TimerTask {
6565 task_ptr: 0x0039_38C8,
6566 tm_addr: callback_addr,
6567 active: true,
6568 fire_at_tick: 101,
6569 });
6570
6571 let (steps, running) = runner.run_steps(1, Some(101));
6572
6573 assert!(running);
6574 assert_eq!(
6575 steps, 1,
6576 "a timer fired while reaching the tick cap must get a CPU slice"
6577 );
6578 assert_eq!(runner.bus.read_long(0x016A), 101);
6579 assert!(runner.active_interrupt_callback.is_some());
6580 assert_ne!(runner.timer_trampoline, 0);
6581 assert_eq!(
6582 runner.cpu.read_reg(Register::PC),
6583 runner.timer_trampoline + 4
6584 );
6585 }
6586
6587 #[test]
6588 fn timer_callback_return_runs_foreground_before_next_due_timer() {
6589 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6590 let interrupted_pc = 0x0001_0000;
6591 let interrupted_sp = 0x007F_FFC0;
6592 let callback_addr = 0x0002_0000;
6593
6594 runner.bus.write_word(interrupted_pc, 0x4E71); runner.cpu.write_reg(Register::PC, interrupted_pc);
6596 runner.cpu.write_reg(Register::A7, interrupted_sp);
6597 runner.bus.write_long(0x016A, 101);
6598 runner.dispatcher.tick_count = 101;
6599 runner.set_instructions_per_tick(1);
6600 runner.tick_budget = 0;
6601 runner.active_interrupt_callback = Some(ActiveInterruptCallback {
6602 source: ActiveInterruptCallbackSource::Timer,
6603 resume_pc: interrupted_pc,
6604 resume_sp: interrupted_sp,
6605 d_regs: [0; 8],
6606 a_regs: [0, 0, 0, 0, 0, 0, 0, interrupted_sp],
6607 ccr: 0,
6608 restore_port: None,
6609 });
6610 runner.dispatcher.timer_tasks.push(TimerTask {
6611 task_ptr: 0x0039_38C8,
6612 tm_addr: callback_addr,
6613 active: true,
6614 fire_at_tick: 102,
6615 });
6616
6617 let (steps, running) = runner.run_steps(1, None);
6618
6619 assert!(running);
6620 assert_eq!(steps, 1);
6621 assert_eq!(
6622 runner.cpu.read_reg(Register::PC),
6623 interrupted_pc + 2,
6624 "resumed foreground instruction should run before the next timer interrupt"
6625 );
6626 assert_eq!(
6627 runner.guest_tick(),
6628 101,
6629 "returning from an interrupt must not immediately spend an exhausted budget on another tick"
6630 );
6631 assert!(runner.active_interrupt_callback.is_none());
6632 assert!(
6633 runner.dispatcher.timer_tasks[0].active,
6634 "the next due timer should remain queued until foreground code gets a slice"
6635 );
6636 }
6637
6638 #[test]
6639 fn sound_doubleback_callback_resume_restores_ccr_before_branch() {
6640 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6641 let interrupted_pc = 0x0001_0000;
6642 let interrupted_sp = 0x007F_FFC0;
6643 let header_ptr = 0x0020_0000;
6644 let exhausted_buf_ptr = 0x0020_1000;
6645
6646 runner.bus.write_word(interrupted_pc, 0x6704);
6648 runner.bus.write_word(interrupted_pc + 2, 0x7001);
6649 runner.bus.write_word(interrupted_pc + 4, 0x6002);
6650 runner.bus.write_word(interrupted_pc + 6, 0x7002);
6651 runner.bus.write_word(interrupted_pc + 8, 0x4E71);
6652
6653 runner.cpu.write_reg(Register::PC, interrupted_pc);
6654 runner.cpu.write_reg(Register::A7, interrupted_sp);
6655 runner.cpu.write_reg(Register::D0, 0);
6656 runner.cpu.core.set_ccr(0x04);
6657
6658 runner.bus.write_long(header_ptr + 12, exhausted_buf_ptr);
6659 runner.bus.write_long(exhausted_buf_ptr + 4, 0x0000_0001);
6660 runner
6661 .dispatcher
6662 .sound_manager
6663 .pending_callbacks
6664 .push(PendingDoubleBackCallback {
6665 callback_addr: 0x0004_1234,
6666 chan_ptr: 0x0039_38C8,
6667 header_ptr,
6668 exhausted_buffer_index: 0,
6669 });
6670
6671 runner.fire_sound_doubleback_callbacks();
6672
6673 let active = runner
6674 .active_interrupt_callback
6675 .expect("sound callback should have been armed");
6676 assert!(matches!(
6677 active.source,
6678 ActiveInterruptCallbackSource::SoundDoubleBack
6679 ));
6680 assert_eq!(active.resume_pc, interrupted_pc);
6681 assert_eq!(active.resume_sp, interrupted_sp);
6682
6683 runner.cpu.write_reg(Register::PC, interrupted_pc);
6685 runner.cpu.write_reg(Register::A7, interrupted_sp);
6686 runner.cpu.core.set_ccr(0);
6687
6688 let (steps, running) = runner.run_steps(3, None);
6689
6690 assert!(running);
6691 assert_eq!(steps, 3);
6692 assert_eq!(runner.cpu.read_reg(Register::D0), 2);
6693 assert!(runner.active_interrupt_callback.is_none());
6694 }
6695
6696 #[test]
6697 fn sound_doubleback_callback_trampoline_stacks_classic_pascal_order() {
6698 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6699 let interrupted_pc = 0x0001_0000;
6700 let interrupted_sp = 0x007F_FFC0;
6701 let callback_addr = runner.bus.alloc(2);
6702 let header_ptr = 0x0020_0000;
6703 let chan_ptr = 0x0039_38C8;
6704 let exhausted_buf_ptr = 0x0020_1000;
6705
6706 runner.bus.write_word(callback_addr, 0x4E75); runner.bus.write_word(interrupted_pc, 0x60FE); runner.cpu.write_reg(Register::PC, interrupted_pc);
6709 runner.cpu.write_reg(Register::A7, interrupted_sp);
6710
6711 runner.bus.write_long(header_ptr + 12, exhausted_buf_ptr);
6712 runner.bus.write_long(exhausted_buf_ptr + 4, 0x0000_0001);
6713 runner
6714 .dispatcher
6715 .sound_manager
6716 .pending_callbacks
6717 .push(PendingDoubleBackCallback {
6718 callback_addr,
6719 chan_ptr,
6720 header_ptr,
6721 exhausted_buffer_index: 0,
6722 });
6723
6724 runner.fire_sound_doubleback_callbacks();
6725 let (_steps, running) = runner.run_steps(24, None);
6726
6727 assert!(running);
6728 assert!(runner.active_interrupt_callback.is_none());
6729 let saved_regs_sp = interrupted_sp - 4 - 32;
6730 assert_eq!(
6731 runner.bus.read_long(saved_regs_sp - 4),
6732 chan_ptr,
6733 "first declared Pascal argument is deeper on the stack"
6734 );
6735 assert_eq!(
6736 runner.bus.read_long(saved_regs_sp - 8),
6737 exhausted_buf_ptr,
6738 "last declared Pascal argument is nearest the return address"
6739 );
6740 }
6741
6742 #[test]
6743 fn mix_audio_loads_ready_double_buffer_without_boundary_silence() {
6744 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6745 let chan_ptr = 0x0039_38C8;
6746 let callback_addr = 0x0004_1234;
6747 let header_ptr = runner.bus.alloc(24);
6748 let buf0_ptr = runner.bus.alloc(18);
6749 let buf1_ptr = runner.bus.alloc(18);
6750
6751 runner.bus.write_word(header_ptr, 1);
6752 runner.bus.write_word(header_ptr + 2, 8);
6753 runner.bus.write_long(header_ptr + 8, OUTPUT_RATE << 16);
6754 runner.bus.write_long(header_ptr + 12, buf0_ptr);
6755 runner.bus.write_long(header_ptr + 16, buf1_ptr);
6756 runner.bus.write_long(header_ptr + 20, callback_addr);
6757 write_double_buffer(&mut runner.bus, buf0_ptr, &[0x90, 0x91]);
6758 write_double_buffer(&mut runner.bus, buf1_ptr, &[0xA0, 0xA1]);
6759
6760 let mut chan = SndChannel::new(chan_ptr, false);
6761 chan.double_buffer = Some(DoubleBufferState {
6762 header_ptr,
6763 current_buffer: 0,
6764 callback_addr,
6765 chan_ptr,
6766 sample_rate: OUTPUT_RATE << 16,
6767 num_channels: 1,
6768 sample_size: 8,
6769 last_buffer_seen: false,
6770 waiting_for_callback: false,
6771 pending_callback_buffers: [false; 2],
6772 });
6773 crate::trap::TrapDispatcher::load_double_buffer_samples(
6774 &mut runner.bus,
6775 &mut chan,
6776 buf0_ptr,
6777 OUTPUT_RATE << 16,
6778 1,
6779 8,
6780 );
6781 runner.dispatcher.sound_manager.channels.push(chan);
6782
6783 runner.mix_audio(3);
6784
6785 assert_eq!(
6786 runner.audio_buffer,
6787 vec![0x90, 0x91, 0xA0],
6788 "host mixing must continue into the ready paired buffer, not emit boundary silence"
6789 );
6790 assert_eq!(
6791 runner.bus.read_long(buf0_ptr + 4) & 0x01,
6792 0x01,
6793 "dbBufferReady stays set until the doubleback callback starts"
6794 );
6795 assert_eq!(
6796 runner.bus.read_long(buf1_ptr + 4) & 0x01,
6797 0x01,
6798 "the paired buffer is still marked ready while it is playing"
6799 );
6800 assert_eq!(
6801 runner.dispatcher.sound_manager.pending_callbacks.len(),
6802 1,
6803 "exhausting buffer 0 still queues its doubleback refill"
6804 );
6805 assert_eq!(
6806 runner.dispatcher.sound_manager.pending_callbacks[0].exhausted_buffer_index,
6807 0
6808 );
6809
6810 let chan = &runner.dispatcher.sound_manager.channels[0];
6811 assert!(chan.is_playing(), "buffer 1 should still be playing");
6812 let db = chan.double_buffer.as_ref().expect("double-buffer active");
6813 assert_eq!(db.current_buffer, 1);
6814 assert!(db.waiting_for_callback);
6815 }
6816
6817 #[test]
6818 fn mix_audio_can_queue_other_doubleback_while_callback_is_active() {
6819 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6820 let chan_ptr = 0x0039_38C8;
6821 let callback_addr = 0x0004_1234;
6822 let header_ptr = runner.bus.alloc(24);
6823 let buf0_ptr = runner.bus.alloc(17);
6824 let buf1_ptr = runner.bus.alloc(17);
6825 let interrupted_pc = 0x0001_0000;
6826 let interrupted_sp = 0x007F_FFC0;
6827
6828 runner.bus.write_word(interrupted_pc, 0x4E71);
6829 runner.cpu.write_reg(Register::PC, interrupted_pc);
6830 runner.cpu.write_reg(Register::A7, interrupted_sp);
6831
6832 runner.bus.write_word(header_ptr, 1);
6833 runner.bus.write_word(header_ptr + 2, 8);
6834 runner.bus.write_long(header_ptr + 8, OUTPUT_RATE << 16);
6835 runner.bus.write_long(header_ptr + 12, buf0_ptr);
6836 runner.bus.write_long(header_ptr + 16, buf1_ptr);
6837 runner.bus.write_long(header_ptr + 20, callback_addr);
6838 write_double_buffer(&mut runner.bus, buf0_ptr, &[0xA0]);
6839 runner.bus.write_long(buf1_ptr, 1);
6840 runner.bus.write_long(buf1_ptr + 4, 0);
6841
6842 let mut chan = SndChannel::new(chan_ptr, false);
6843 chan.double_buffer = Some(DoubleBufferState {
6844 header_ptr,
6845 current_buffer: 0,
6846 callback_addr,
6847 chan_ptr,
6848 sample_rate: OUTPUT_RATE << 16,
6849 num_channels: 1,
6850 sample_size: 8,
6851 last_buffer_seen: false,
6852 waiting_for_callback: false,
6853 pending_callback_buffers: [false; 2],
6854 });
6855 crate::trap::TrapDispatcher::load_double_buffer_samples(
6856 &mut runner.bus,
6857 &mut chan,
6858 buf0_ptr,
6859 OUTPUT_RATE << 16,
6860 1,
6861 8,
6862 );
6863 runner.dispatcher.sound_manager.channels.push(chan);
6864
6865 runner.mix_audio(1);
6866 assert_eq!(runner.dispatcher.sound_manager.pending_callbacks.len(), 1);
6867 assert!(
6868 runner.dispatcher.sound_manager.channels[0]
6869 .double_buffer
6870 .as_ref()
6871 .expect("double-buffer active")
6872 .waiting_for_callback
6873 );
6874
6875 runner.fire_sound_doubleback_callbacks();
6876 assert!(matches!(
6877 runner
6878 .active_interrupt_callback
6879 .expect("doubleback callback should be active")
6880 .source,
6881 ActiveInterruptCallbackSource::SoundDoubleBack
6882 ));
6883 assert!(
6884 runner.dispatcher.sound_manager.channels[0]
6885 .double_buffer
6886 .as_ref()
6887 .expect("double-buffer active")
6888 .waiting_for_callback,
6889 "callback remains outstanding until guest refills a buffer"
6890 );
6891
6892 runner.mix_audio(16);
6893
6894 assert_eq!(
6895 runner.dispatcher.sound_manager.pending_callbacks.len(),
6896 1,
6897 "the paired unready buffer may queue its own callback while buffer 0 is active"
6898 );
6899 assert_eq!(
6900 runner.dispatcher.sound_manager.pending_callbacks[0].exhausted_buffer_index, 1,
6901 "buffer 0 must not be duplicated; buffer 1 gets the new callback"
6902 );
6903 let db = runner.dispatcher.sound_manager.channels[0]
6904 .double_buffer
6905 .as_ref()
6906 .expect("double-buffer active");
6907 assert!(db.waiting_for_callback);
6908 assert_eq!(db.pending_callback_buffers, [true, true]);
6909 }
6910
6911 #[test]
6912 fn mix_audio_does_not_load_ready_double_buffer_while_callback_is_active() {
6913 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6914 let chan_ptr = 0x0039_38C8;
6915 let callback_addr = 0x0004_1234;
6916 let header_ptr = runner.bus.alloc(24);
6917 let buf0_ptr = runner.bus.alloc(17);
6918
6919 runner.bus.write_word(header_ptr, 1);
6920 runner.bus.write_word(header_ptr + 2, 8);
6921 runner.bus.write_long(header_ptr + 8, OUTPUT_RATE << 16);
6922 runner.bus.write_long(header_ptr + 12, buf0_ptr);
6923 runner.bus.write_long(header_ptr + 20, callback_addr);
6924 write_double_buffer(&mut runner.bus, buf0_ptr, &[0xA0]);
6925
6926 let mut chan = SndChannel::new(chan_ptr, false);
6927 chan.double_buffer = Some(DoubleBufferState {
6928 header_ptr,
6929 current_buffer: 0,
6930 callback_addr,
6931 chan_ptr,
6932 sample_rate: OUTPUT_RATE << 16,
6933 num_channels: 1,
6934 sample_size: 8,
6935 last_buffer_seen: false,
6936 waiting_for_callback: true,
6937 pending_callback_buffers: [true, false],
6938 });
6939 runner.dispatcher.sound_manager.channels.push(chan);
6940 runner.active_interrupt_callback = Some(ActiveInterruptCallback {
6941 source: ActiveInterruptCallbackSource::SoundDoubleBack,
6942 resume_pc: 0x0001_0000,
6943 resume_sp: 0x007F_FFC0,
6944 d_regs: [0; 8],
6945 a_regs: [0; 8],
6946 ccr: 0,
6947 restore_port: None,
6948 });
6949
6950 runner.mix_audio(1);
6951
6952 assert_eq!(
6953 runner.bus.read_long(buf0_ptr + 4) & 0x01,
6954 0x01,
6955 "ready buffer must not be consumed before the callback returns"
6956 );
6957 assert!(
6958 !runner.dispatcher.sound_manager.channels[0].is_playing(),
6959 "callback-active buffer load should be deferred"
6960 );
6961 assert_eq!(
6962 runner.audio_buffer,
6963 vec![0x80],
6964 "the host stream stays alive with silence while waiting"
6965 );
6966
6967 runner.active_interrupt_callback = None;
6968 runner.try_load_pending_double_buffers();
6969
6970 assert_eq!(
6971 runner.bus.read_long(buf0_ptr + 4) & 0x01,
6972 0x01,
6973 "returned callback buffer stays marked ready while playback owns it"
6974 );
6975 assert!(
6976 runner.dispatcher.sound_manager.channels[0].is_playing(),
6977 "returned callback makes the refilled buffer available to the mixer"
6978 );
6979 let db = runner.dispatcher.sound_manager.channels[0]
6980 .double_buffer
6981 .as_ref()
6982 .expect("double-buffer active");
6983 assert_eq!(db.pending_callback_buffers, [false, false]);
6984
6985 runner.mix_audio(1);
6986 assert_eq!(runner.audio_buffer, vec![0x80, 0xA0]);
6987 }
6988
6989 #[test]
6990 fn try_load_pending_double_buffers_recovers_ready_alternate_after_underrun() {
6991 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
6992 let chan_ptr = 0x0039_38C8;
6993 let callback_addr = 0x0004_1234;
6994 let header_ptr = runner.bus.alloc(24);
6995 let buf0_ptr = runner.bus.alloc(18);
6996 let buf1_ptr = runner.bus.alloc(18);
6997
6998 runner.bus.write_word(header_ptr, 1);
6999 runner.bus.write_word(header_ptr + 2, 8);
7000 runner.bus.write_long(header_ptr + 8, OUTPUT_RATE << 16);
7001 runner.bus.write_long(header_ptr + 12, buf0_ptr);
7002 runner.bus.write_long(header_ptr + 16, buf1_ptr);
7003 runner.bus.write_long(header_ptr + 20, callback_addr);
7004 write_double_buffer(&mut runner.bus, buf0_ptr, &[0xA0, 0xA1]);
7005 runner.bus.write_long(buf1_ptr, 2);
7006 runner.bus.write_long(buf1_ptr + 4, 0);
7007
7008 let mut chan = SndChannel::new(chan_ptr, false);
7009 chan.double_buffer = Some(DoubleBufferState {
7010 header_ptr,
7011 current_buffer: 1,
7012 callback_addr,
7013 chan_ptr,
7014 sample_rate: OUTPUT_RATE << 16,
7015 num_channels: 1,
7016 sample_size: 8,
7017 last_buffer_seen: false,
7018 waiting_for_callback: false,
7019 pending_callback_buffers: [false; 2],
7020 });
7021 runner.dispatcher.sound_manager.channels.push(chan);
7022
7023 runner.try_load_pending_double_buffers();
7024
7025 let chan = &runner.dispatcher.sound_manager.channels[0];
7026 assert!(chan.is_playing(), "ready alternate buffer should load");
7027 let db = chan.double_buffer.as_ref().expect("double-buffer active");
7028 assert_eq!(db.current_buffer, 0);
7029 assert!(
7030 !db.waiting_for_callback,
7031 "loading a ready buffer completes the outstanding refill wait"
7032 );
7033 assert_eq!(
7034 runner.bus.read_long(buf0_ptr + 4) & 0x01,
7035 0x01,
7036 "loading a ready alternate must not clear dbBufferReady before playback exhausts"
7037 );
7038 }
7039
7040 #[test]
7041 fn try_load_pending_double_buffers_does_not_replay_callback_pending_slot() {
7042 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7043 let chan_ptr = 0x0039_38C8;
7044 let callback_addr = 0x0004_1234;
7045 let header_ptr = runner.bus.alloc(24);
7046 let buf0_ptr = runner.bus.alloc(17);
7047 let buf1_ptr = runner.bus.alloc(17);
7048
7049 runner.bus.write_word(header_ptr, 1);
7050 runner.bus.write_word(header_ptr + 2, 8);
7051 runner.bus.write_long(header_ptr + 8, OUTPUT_RATE << 16);
7052 runner.bus.write_long(header_ptr + 12, buf0_ptr);
7053 runner.bus.write_long(header_ptr + 16, buf1_ptr);
7054 runner.bus.write_long(header_ptr + 20, callback_addr);
7055 write_double_buffer(&mut runner.bus, buf0_ptr, &[0xA0]);
7056 runner.bus.write_long(buf1_ptr, 1);
7057 runner.bus.write_long(buf1_ptr + 4, 0);
7058
7059 let mut chan = SndChannel::new(chan_ptr, false);
7060 chan.double_buffer = Some(DoubleBufferState {
7061 header_ptr,
7062 current_buffer: 0,
7063 callback_addr,
7064 chan_ptr,
7065 sample_rate: OUTPUT_RATE << 16,
7066 num_channels: 1,
7067 sample_size: 8,
7068 last_buffer_seen: false,
7069 waiting_for_callback: true,
7070 pending_callback_buffers: [true, false],
7071 });
7072 runner.dispatcher.sound_manager.channels.push(chan);
7073 runner
7074 .dispatcher
7075 .sound_manager
7076 .pending_callbacks
7077 .push(PendingDoubleBackCallback {
7078 callback_addr,
7079 chan_ptr,
7080 header_ptr,
7081 exhausted_buffer_index: 0,
7082 });
7083
7084 runner.try_load_pending_double_buffers();
7085
7086 assert!(
7087 !runner.dispatcher.sound_manager.channels[0].is_playing(),
7088 "an exhausted slot must not replay just because dbBufferReady remains set"
7089 );
7090 assert_eq!(
7091 runner.bus.read_long(buf0_ptr + 4) & 0x01,
7092 0x01,
7093 "the flag remains ready until fire_sound_doubleback_callbacks clears it"
7094 );
7095 }
7096
7097 #[test]
7098 fn sound_command_callback_trampoline_passes_sndcommand_pointer() {
7099 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7100 let interrupted_pc = 0x0001_0000;
7101 let interrupted_sp = 0x007F_FFC0;
7102 runner.cpu.write_reg(Register::PC, interrupted_pc);
7103 runner.cpu.write_reg(Register::A7, interrupted_sp);
7104
7105 runner
7106 .dispatcher
7107 .sound_manager
7108 .pending_sound_callbacks
7109 .push(crate::sound::PendingSoundCallback::Command {
7110 callback_addr: 0x0004_5678,
7111 chan_ptr: 0x0039_38C8,
7112 cmd: crate::sound::SndCommand {
7113 cmd: crate::sound::cmd::CALLBACK,
7114 param1: 0x1234,
7115 param2: 0x0001_43FC,
7116 },
7117 });
7118
7119 runner.fire_sound_callbacks();
7120
7121 let active = runner
7122 .active_interrupt_callback
7123 .expect("sound callback should have been armed");
7124 assert!(matches!(
7125 active.source,
7126 ActiveInterruptCallbackSource::SoundCallback
7127 ));
7128 assert_eq!(active.resume_pc, interrupted_pc);
7129 assert_eq!(active.resume_sp, interrupted_sp);
7130
7131 let tramp = runner.sound_callback_trampoline;
7132 let cmd_ptr = tramp + 34;
7133 let saved_regs_sp = interrupted_sp - 4 - 32;
7134 assert_eq!(runner.bus.read_long(tramp + 6), 0x0039_38C8);
7135 assert_eq!(runner.bus.read_long(tramp + 12), cmd_ptr);
7136 assert_eq!(runner.bus.read_long(tramp + 18), 0x0004_5678);
7137 assert_eq!(runner.bus.read_long(tramp + 24), saved_regs_sp);
7138 assert_eq!(runner.bus.read_word(cmd_ptr), crate::sound::cmd::CALLBACK);
7139 assert_eq!(runner.bus.read_word(cmd_ptr + 2), 0x1234);
7140 assert_eq!(runner.bus.read_long(cmd_ptr + 4), 0x0001_43FC);
7141 }
7142
7143 #[test]
7144 fn sound_command_callback_trampoline_tolerates_one_long_pascal_cleanup() {
7145 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7146 let interrupted_pc = 0x0001_0000;
7147 let interrupted_sp = 0x007F_FFC0;
7148 let callback_addr = runner.bus.alloc(4);
7149
7150 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);
7156 runner.cpu.write_reg(Register::A7, interrupted_sp);
7157
7158 runner
7159 .dispatcher
7160 .sound_manager
7161 .pending_sound_callbacks
7162 .push(crate::sound::PendingSoundCallback::Command {
7163 callback_addr,
7164 chan_ptr: 0x0039_38C8,
7165 cmd: crate::sound::SndCommand {
7166 cmd: crate::sound::cmd::CALLBACK,
7167 param1: 0x1234,
7168 param2: 0x0001_43FC,
7169 },
7170 });
7171
7172 runner.fire_sound_callbacks();
7173 let (steps, running) = runner.run_steps(10, None);
7174
7175 assert!(running, "callback trampoline should resume foreground code");
7176 assert_eq!(steps, 10);
7177 assert!(runner.active_interrupt_callback.is_none());
7178 assert_eq!(runner.cpu.read_reg(Register::PC), interrupted_pc + 2);
7179 assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
7180 assert!(!runner.is_halted());
7181 }
7182
7183 #[test]
7184 fn sound_file_completion_callback_trampoline_tolerates_c_style_cleanup() {
7185 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7186 let interrupted_pc = 0x0001_0000;
7187 let interrupted_sp = 0x007F_FFC0;
7188 let callback_addr = runner.bus.alloc(2);
7189
7190 runner.bus.write_word(callback_addr, 0x4E75); runner.bus.write_word(interrupted_pc, 0x4E71); runner.cpu.write_reg(Register::PC, interrupted_pc);
7193 runner.cpu.write_reg(Register::A7, interrupted_sp);
7194
7195 runner
7196 .dispatcher
7197 .sound_manager
7198 .pending_sound_callbacks
7199 .push(crate::sound::PendingSoundCallback::FileCompletion {
7200 callback_addr,
7201 chan_ptr: 0x0039_38C8,
7202 });
7203
7204 runner.fire_sound_callbacks();
7205 let (steps, running) = runner.run_steps(10, None);
7206
7207 assert!(
7208 running,
7209 "file completion trampoline should resume foreground code"
7210 );
7211 assert_eq!(steps, 10);
7212 assert!(runner.active_interrupt_callback.is_none());
7213 assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
7214 assert!(!runner.is_halted());
7215 }
7216
7217 #[test]
7218 fn sound_doubleback_callback_trampoline_tolerates_c_style_cleanup() {
7219 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7220 let interrupted_pc = 0x0001_0000;
7221 let interrupted_sp = 0x007F_FFC0;
7222 let callback_addr = runner.bus.alloc(2);
7223 let header_ptr = 0x0020_0000;
7224 let exhausted_buf_ptr = 0x0020_1000;
7225
7226 runner.bus.write_word(callback_addr, 0x4E75); runner.bus.write_word(interrupted_pc, 0x4E71); runner.cpu.write_reg(Register::PC, interrupted_pc);
7229 runner.cpu.write_reg(Register::A7, interrupted_sp);
7230
7231 runner.bus.write_long(header_ptr + 12, exhausted_buf_ptr);
7232 runner.bus.write_long(exhausted_buf_ptr + 4, 0x0000_0001);
7233 runner
7234 .dispatcher
7235 .sound_manager
7236 .pending_callbacks
7237 .push(PendingDoubleBackCallback {
7238 callback_addr,
7239 chan_ptr: 0x0039_38C8,
7240 header_ptr,
7241 exhausted_buffer_index: 0,
7242 });
7243
7244 runner.fire_sound_doubleback_callbacks();
7245 let (steps, running) = runner.run_steps(12, None);
7246
7247 assert!(
7248 running,
7249 "doubleback trampoline should resume foreground code"
7250 );
7251 assert_eq!(steps, 12);
7252 assert!(runner.active_interrupt_callback.is_none());
7253 assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
7254 assert!(!runner.is_halted());
7255 }
7256
7257 #[test]
7258 fn run_pending_sound_work_does_not_advance_ticks_or_foreground_code() {
7259 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7260 let interrupted_pc = 0x0001_0000;
7261 let interrupted_sp = 0x007F_FFC0;
7262 let callback_addr = runner.bus.alloc(2);
7263
7264 runner.bus.write_word(callback_addr, 0x4E75); runner.bus.write_word(interrupted_pc, 0x4E71); runner.cpu.write_reg(Register::PC, interrupted_pc);
7267 runner.cpu.write_reg(Register::A7, interrupted_sp);
7268 runner.bus.write_long(0x016A, 41);
7269 runner.dispatcher.tick_count = 41;
7270 runner.set_instructions_per_tick(1);
7271 runner.tick_budget = 0;
7272
7273 runner
7274 .dispatcher
7275 .sound_manager
7276 .pending_sound_callbacks
7277 .push(PendingSoundCallback::Command {
7278 callback_addr,
7279 chan_ptr: 0x0039_38C8,
7280 cmd: SndCommand {
7281 cmd: crate::sound::cmd::CALLBACK,
7282 param1: 0,
7283 param2: 0,
7284 },
7285 });
7286
7287 let (steps, running) = runner.run_pending_sound_work(32);
7288
7289 assert!(running);
7290 assert!(steps > 0, "sound callback trampoline should execute");
7291 assert_eq!(
7292 runner.guest_tick(),
7293 41,
7294 "callback-only slices must not advance application-visible ticks"
7295 );
7296 assert_eq!(
7297 runner.cpu.read_reg(Register::PC),
7298 interrupted_pc,
7299 "sound callback service must stop before resumed foreground code runs"
7300 );
7301 assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
7302 assert!(runner.active_interrupt_callback.is_none());
7303 assert!(!runner.has_pending_sound_work());
7304 }
7305
7306 #[test]
7307 fn gui_cpu_slice_does_not_finalize_host_frame() {
7308 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7309 let callback_addr = runner.bus.alloc(2);
7310
7311 runner.bus.write_word(callback_addr, 0x4E75); runner
7313 .dispatcher
7314 .sound_manager
7315 .pending_sound_callbacks
7316 .push(PendingSoundCallback::Command {
7317 callback_addr,
7318 chan_ptr: 0x0039_38C8,
7319 cmd: SndCommand {
7320 cmd: crate::sound::cmd::CALLBACK,
7321 param1: 0,
7322 param2: 0,
7323 },
7324 });
7325
7326 let (steps, running) = runner.run_gui_cpu_slice(0, 0);
7327
7328 assert!(running);
7329 assert_eq!(steps, 0);
7330 assert!(
7331 runner.active_interrupt_callback.is_none(),
7332 "CPU-only GUI slices must not fire host-frame sound callbacks"
7333 );
7334 assert!(runner.has_pending_sound_work());
7335 }
7336
7337 #[test]
7338 fn run_steps_paces_pending_sound_doublebacks_to_one_per_slice() {
7339 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7340 let interrupted_pc = 0x0001_0000;
7341 let interrupted_sp = 0x007F_FFC0;
7342 let callback_addr = runner.bus.alloc(2);
7343 let header_ptr = runner.bus.alloc(24);
7344 let buf0_ptr = runner.bus.alloc(16);
7345 let buf1_ptr = runner.bus.alloc(16);
7346
7347 runner.bus.write_word(callback_addr, 0x4E75); for offset in (0..512).step_by(2) {
7349 runner.bus.write_word(interrupted_pc + offset, 0x4E71); }
7351 runner.cpu.write_reg(Register::PC, interrupted_pc);
7352 runner.cpu.write_reg(Register::A7, interrupted_sp);
7353
7354 runner.bus.write_long(header_ptr + 12, buf0_ptr);
7355 runner.bus.write_long(header_ptr + 16, buf1_ptr);
7356 runner.bus.write_long(buf0_ptr + 4, 0x0000_0001);
7357 runner.bus.write_long(buf1_ptr + 4, 0x0000_0001);
7358 runner
7359 .dispatcher
7360 .sound_manager
7361 .pending_callbacks
7362 .push(PendingDoubleBackCallback {
7363 callback_addr,
7364 chan_ptr: 0x0039_38C8,
7365 header_ptr,
7366 exhausted_buffer_index: 0,
7367 });
7368 runner
7369 .dispatcher
7370 .sound_manager
7371 .pending_callbacks
7372 .push(PendingDoubleBackCallback {
7373 callback_addr,
7374 chan_ptr: 0x0039_38C8,
7375 header_ptr,
7376 exhausted_buffer_index: 1,
7377 });
7378
7379 let (_steps, running) = runner.run_steps(96, None);
7380
7381 assert!(running);
7382 assert!(runner.active_interrupt_callback.is_none());
7383 assert_eq!(
7384 runner.dispatcher.sound_manager.pending_callbacks.len(),
7385 1,
7386 "one CPU slice must not drain back-to-back doubleback interrupts"
7387 );
7388 assert_eq!(
7389 runner.dispatcher.sound_manager.pending_callbacks[0].exhausted_buffer_index,
7390 1
7391 );
7392
7393 let (_steps, running) = runner.run_steps(96, None);
7394
7395 assert!(running);
7396 assert!(runner.active_interrupt_callback.is_none());
7397 assert!(
7398 runner.dispatcher.sound_manager.pending_callbacks.is_empty(),
7399 "the next CPU slice may dispatch the next pending doubleback"
7400 );
7401 }
7402
7403 #[test]
7404 fn vbl_callback_arms_interrupt_with_task_ptr_in_a0() {
7405 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7406 let interrupted_pc = 0x0002_0000;
7407 let interrupted_sp = 0x007F_FFC0;
7408 let task_ptr = 0x0020_2000;
7409
7410 runner.cpu.write_reg(Register::PC, interrupted_pc);
7411 runner.cpu.write_reg(Register::A7, interrupted_sp);
7412 runner.cpu.write_reg(Register::A0, 0xAAAA_0000);
7413 runner.cpu.core.set_ccr(0x04);
7414
7415 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 {
7420 task_ptr,
7421 slot: Some(9),
7422 });
7423
7424 runner.fire_vbl_tasks();
7425
7426 let active = runner
7427 .active_interrupt_callback
7428 .expect("vbl callback should have been armed");
7429 assert!(matches!(active.source, ActiveInterruptCallbackSource::Vbl));
7430 assert_eq!(active.resume_pc, interrupted_pc);
7431 assert_eq!(active.resume_sp, interrupted_sp);
7432 assert_eq!(runner.bus.read_word(task_ptr + 10), 0);
7433
7434 assert_ne!(runner.vbl_trampoline, 0);
7435 assert_eq!(runner.cpu.read_reg(Register::PC), runner.vbl_trampoline);
7436 assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp - 4);
7437 assert_eq!(runner.bus.read_long(interrupted_sp - 4), interrupted_pc);
7438 assert_eq!(runner.bus.read_word(runner.vbl_trampoline + 4), 0x207C);
7439 assert_eq!(runner.bus.read_long(runner.vbl_trampoline + 6), task_ptr);
7440 assert_eq!(
7441 runner.bus.read_long(runner.vbl_trampoline + 12),
7442 0x0004_1234
7443 );
7444 }
7445
7446 #[test]
7447 fn custom_instructions_per_tick_controls_tick_cadence() {
7448 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7449 let program_start = 0x0001_0000;
7450 let program_words = 14;
7451
7452 for offset in (0..program_words).step_by(2) {
7453 runner.bus.write_word(program_start + offset, 0x4E71);
7454 }
7455
7456 runner.cpu.write_reg(Register::PC, program_start);
7457 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
7458 runner.bus.write_long(0x016A, 0);
7459 runner.set_instructions_per_tick(3);
7460
7461 let (steps, running) = runner.run_steps(7, None);
7462
7463 assert!(running);
7464 assert_eq!(steps, 7);
7465 assert_eq!(runner.bus.read_long(0x016A), 2);
7466 }
7467
7468 #[test]
7469 fn non_idle_hle_trap_cost_advances_tick_budget() {
7470 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7471 let base = 0x0001_0000u32;
7472 let sp = 0x0010_0000u32;
7473 let rect = 0x0020_0000u32;
7474
7475 runner.bus.write_word(base, 0xA8A8); runner.cpu.write_reg(Register::PC, base);
7477 runner.cpu.write_reg(Register::A7, sp);
7478 runner.bus.write_word(sp, 1); runner.bus.write_word(sp + 2, 2); runner.bus.write_long(sp + 4, rect);
7481 runner.bus.write_word(rect, 10);
7482 runner.bus.write_word(rect + 2, 20);
7483 runner.bus.write_word(rect + 4, 30);
7484 runner.bus.write_word(rect + 6, 40);
7485 runner.bus.write_long(0x016A, 0);
7486 runner.dispatcher.tick_count = 0;
7487 runner.set_instructions_per_tick(5);
7488
7489 let (steps, running) = runner.run_steps(1, None);
7490
7491 assert!(running);
7492 assert_eq!(steps, 1);
7493 assert_eq!(
7494 runner.guest_tick(),
7495 1,
7496 "non-idle HLE traps should consume tick budget beyond the base instruction"
7497 );
7498 assert_eq!(runner.bus.read_word(rect), 11);
7499 assert_eq!(runner.bus.read_word(rect + 2), 22);
7500 }
7501
7502 #[test]
7503 fn idle_hle_traps_do_not_apply_extra_tick_cost() {
7504 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7505 let base = 0x0001_0000u32;
7506
7507 runner.bus.write_word(base, 0xA975); runner.cpu.write_reg(Register::PC, base);
7509 runner.cpu.write_reg(Register::A7, 0x0010_0000);
7510 runner.bus.write_long(0x016A, 42);
7511 runner.dispatcher.tick_count = 42;
7512 runner.set_instructions_per_tick(5);
7513
7514 let (steps, running) = runner.run_steps(1, None);
7515
7516 assert!(running);
7517 assert_eq!(steps, 1);
7518 assert_eq!(
7519 runner.guest_tick(),
7520 42,
7521 "polling traps should not add synthetic HLE manager cost"
7522 );
7523 assert_eq!(runner.tick_budget, 4);
7524 }
7525
7526 #[test]
7527 fn hle_trap_cost_stops_gui_slice_at_tick_cap() {
7528 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7529 let base = 0x0001_0000u32;
7530 let sp = 0x0010_0000u32;
7531 let rect = 0x0020_0000u32;
7532
7533 runner.bus.write_word(base, 0xA8A8); runner.bus.write_word(base + 2, 0x4E71); runner.cpu.write_reg(Register::PC, base);
7536 runner.cpu.write_reg(Register::A7, sp);
7537 runner.bus.write_word(sp, 1);
7538 runner.bus.write_word(sp + 2, 2);
7539 runner.bus.write_long(sp + 4, rect);
7540 runner.bus.write_word(rect, 10);
7541 runner.bus.write_word(rect + 2, 20);
7542 runner.bus.write_word(rect + 4, 30);
7543 runner.bus.write_word(rect + 6, 40);
7544 runner.bus.write_long(0x016A, 0);
7545 runner.dispatcher.tick_count = 0;
7546 runner.set_instructions_per_tick(5);
7547
7548 let (steps, running) = runner.run_gui_slice_with_audio(8, 1, 0);
7549
7550 assert!(running);
7551 assert_eq!(steps, 1);
7552 assert_eq!(runner.guest_tick(), 1);
7553 assert_eq!(
7554 runner.cpu.read_reg(Register::PC),
7555 base + 2,
7556 "the next guest instruction should be deferred once HLE cost reaches the GUI tick cap"
7557 );
7558 }
7559
7560 #[test]
7568 fn dispatcher_tick_count_stays_in_sync_with_bus() {
7569 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7570 let program_start = 0x0001_0000;
7571 let program_words = 20;
7572
7573 for offset in (0..program_words).step_by(2) {
7576 runner.bus.write_word(program_start + offset, 0x4E71);
7577 }
7578
7579 runner.cpu.write_reg(Register::PC, program_start);
7580 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
7581 runner.bus.write_long(0x016A, 0);
7582 runner.dispatcher.tick_count = 0;
7584 runner.set_instructions_per_tick(3);
7585
7586 for _ in 0..3 {
7590 let (_, running) = runner.run_steps(3, None);
7591 assert!(running);
7592 assert_eq!(
7593 runner.bus.read_long(0x016A),
7594 runner.dispatcher.tick_count,
7595 "bus $016A ({}) diverged from dispatcher.tick_count ({})",
7596 runner.bus.read_long(0x016A),
7597 runner.dispatcher.tick_count,
7598 );
7599 }
7600 }
7601
7602 #[test]
7608 fn spin_fastfwd_template_a_advances_ticks_and_skips_loop() {
7609 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7610 let base = 0x0001_0000u32;
7611 runner.bus.write_word(base, 0x594F);
7620 runner.bus.write_word(base + 2, 0xA975);
7621 runner.bus.write_word(base + 4, 0x201F);
7622 runner.bus.write_word(base + 6, 0x5380);
7623 runner.bus.write_word(base + 8, 0xB680);
7624 runner.bus.write_word(base + 10, 0x62F4);
7625 runner.bus.write_word(base + 12, 0x4E71);
7626
7627 runner.bus.write_long(0x016A, 100);
7629 runner.dispatcher.tick_count = 100;
7630 runner.cpu.write_reg(Register::D3, 500);
7631 runner.cpu.write_reg(Register::A7, 0x0010_0000);
7632
7633 let pc_after_trap = base + 4;
7634 let mut count = 0usize;
7635 let hit_cap = runner.try_tickcount_spin_fastfwd(pc_after_trap, None, &mut count);
7636
7637 assert!(!hit_cap, "no tick_cap was set, cap should not trip");
7638 assert_eq!(runner.dispatcher.tick_count, 501, "advanced to D3+imm");
7639 assert_eq!(runner.bus.read_long(0x016A), 501, "bus $016A in sync");
7640 assert_eq!(runner.cpu.read_reg(Register::D0), 500);
7642 assert_eq!(runner.cpu.read_reg(Register::A7), 0x0010_0004);
7644 assert_eq!(runner.cpu.read_reg(Register::PC), base + 12);
7646 assert_eq!(count, 4);
7648 }
7649
7650 #[test]
7656 fn spin_fastfwd_rejects_register_mismatch() {
7657 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7658 let base = 0x0001_0000u32;
7659 runner.bus.write_word(base, 0x594F);
7662 runner.bus.write_word(base + 2, 0xA975);
7663 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);
7669 runner.dispatcher.tick_count = 100;
7670 runner.cpu.write_reg(Register::D3, 500);
7671 runner.cpu.write_reg(Register::A7, 0x0010_0000);
7672 let pc_after_trap = base + 4;
7673 let mut count = 0usize;
7674 runner.try_tickcount_spin_fastfwd(pc_after_trap, None, &mut count);
7675
7676 assert_eq!(runner.dispatcher.tick_count, 100);
7678 assert_eq!(runner.cpu.read_reg(Register::PC), 0);
7681 assert_eq!(count, 0);
7682 }
7683
7684 #[test]
7690 fn spin_fastfwd_template_b_memory_target_variant() {
7691 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7692 let base = 0x0001_0000u32;
7693 runner.bus.write_word(base, 0x594F);
7702 runner.bus.write_word(base + 2, 0xA975);
7703 runner.bus.write_word(base + 4, 0x201F);
7704 runner.bus.write_word(base + 6, 0xB0AE);
7705 runner.bus.write_word(base + 8, 0xFFFC);
7706 runner.bus.write_word(base + 10, 0x63F6);
7711 runner.bus.write_word(base + 12, 0x4E71);
7712
7713 let a6 = 0x0010_1000u32;
7716 runner.bus.write_long(a6.wrapping_sub(4), 400);
7717
7718 runner.bus.write_long(0x016A, 100);
7720 runner.dispatcher.tick_count = 100;
7721 runner.cpu.write_reg(Register::A6, a6);
7722 runner.cpu.write_reg(Register::A7, 0x0010_0000);
7723
7724 let pc_after_trap = base + 4;
7725 let mut count = 0usize;
7726 let hit_cap = runner.try_tickcount_spin_fastfwd(pc_after_trap, None, &mut count);
7727
7728 assert!(!hit_cap);
7729 assert_eq!(runner.dispatcher.tick_count, 401);
7731 assert_eq!(runner.bus.read_long(0x016A), 401);
7732 assert_eq!(runner.cpu.read_reg(Register::D0), 401);
7734 assert_eq!(runner.cpu.read_reg(Register::A7), 0x0010_0004);
7735 assert_eq!(runner.cpu.read_reg(Register::PC), base + 12);
7736 assert_eq!(count, 3);
7738 }
7739
7740 #[test]
7753 fn spin_fastfwd_template_c_absolute_long_target_variant() {
7754 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7755 let base = 0x0001_0000u32;
7756 let target_addr = 0x0002_FE44u32;
7757
7758 runner.bus.write_word(base, 0x594F);
7765 runner.bus.write_word(base + 2, 0xA975);
7766 runner.bus.write_word(base + 4, 0x201F);
7767 runner.bus.write_word(base + 6, 0xB0B9);
7768 runner.bus.write_long(base + 8, target_addr);
7769 runner.bus.write_word(base + 12, 0x65F2);
7770 runner.bus.write_word(base + 14, 0x4E71);
7771
7772 runner.bus.write_long(target_addr, 400);
7773 runner.bus.write_long(0x016A, 100);
7774 runner.dispatcher.tick_count = 100;
7775 runner.cpu.write_reg(Register::A7, 0x0010_0000);
7776
7777 let pc_after_trap = base + 4;
7778 let mut count = 0usize;
7779 let hit_cap = runner.try_tickcount_spin_fastfwd(pc_after_trap, None, &mut count);
7780
7781 assert!(!hit_cap);
7782 assert_eq!(runner.dispatcher.tick_count, 400);
7783 assert_eq!(runner.bus.read_long(0x016A), 400);
7784 assert_eq!(runner.cpu.read_reg(Register::D0), 400);
7785 assert_eq!(runner.cpu.read_reg(Register::A7), 0x0010_0004);
7786 assert_eq!(runner.cpu.read_reg(Register::PC), base + 14);
7787 assert_eq!(count, 3);
7788 }
7789
7790 #[test]
7791 fn spin_fastfwd_leaves_interrupt_callback_state_unsynthesized() {
7792 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7793 let base = 0x0001_0000u32;
7794 let target_addr = 0x0002_FE44u32;
7795 let task_ptr = 0x0020_2000u32;
7796 let sp = 0x0010_0000u32;
7797
7798 runner.bus.write_word(base, 0x594F);
7802 runner.bus.write_word(base + 2, 0xA975);
7803 runner.bus.write_word(base + 4, 0x201F);
7804 runner.bus.write_word(base + 6, 0xB0B9);
7805 runner.bus.write_long(base + 8, target_addr);
7806 runner.bus.write_word(base + 12, 0x65F2);
7807 runner.bus.write_word(base + 14, 0x4E71);
7808
7809 runner.bus.write_long(target_addr, 400);
7810 runner.bus.write_long(0x016A, 100);
7811 runner.dispatcher.tick_count = 100;
7812 runner.cpu.write_reg(Register::PC, base + 4);
7813 runner.cpu.write_reg(Register::A7, sp);
7814 runner.cpu.write_reg(Register::D0, 0xDEAD_BEEF);
7815 runner.bus.write_long(sp, 100);
7816
7817 runner.bus.write_word(task_ptr + 4, 1);
7818 runner.bus.write_long(task_ptr + 6, 0x0004_1234);
7819 runner.bus.write_word(task_ptr + 10, 1);
7820 runner.bus.write_word(task_ptr + 12, 0);
7821 runner.dispatcher.vbl_tasks.push(VblTask {
7822 task_ptr,
7823 slot: None,
7824 });
7825
7826 let mut count = 0usize;
7827 let hit_cap = runner.try_tickcount_spin_fastfwd(base + 4, None, &mut count);
7828
7829 assert!(!hit_cap);
7830 assert_eq!(runner.dispatcher.tick_count, 101);
7831 assert_eq!(runner.bus.read_long(0x016A), 101);
7832 assert_eq!(count, 0);
7833 assert_eq!(runner.cpu.read_reg(Register::D0), 0xDEAD_BEEF);
7834 assert_eq!(runner.cpu.read_reg(Register::PC), runner.vbl_trampoline);
7835 assert_eq!(runner.cpu.read_reg(Register::A7), sp - 4);
7836 assert_eq!(runner.bus.read_long(sp - 4), base + 4);
7837
7838 let active = runner
7839 .active_interrupt_callback
7840 .expect("VBL callback should remain active for normal resume handling");
7841 assert!(matches!(active.source, ActiveInterruptCallbackSource::Vbl));
7842 assert_eq!(active.resume_pc, base + 4);
7843 assert_eq!(active.resume_sp, sp);
7844 }
7845
7846 #[test]
7850 fn spin_fastfwd_gate_default_headless_on_gui_off() {
7851 assert!(spin_wait_fastfwd_gate(false, false, false));
7855 assert!(!spin_wait_fastfwd_gate(false, false, true));
7856 }
7857
7858 #[test]
7859 fn spin_fastfwd_gate_force_off_wins() {
7860 assert!(!spin_wait_fastfwd_gate(false, true, false));
7863 assert!(!spin_wait_fastfwd_gate(false, true, true));
7864 assert!(!spin_wait_fastfwd_gate(true, true, false));
7865 assert!(!spin_wait_fastfwd_gate(true, true, true));
7866 }
7867
7868 #[test]
7869 fn spin_fastfwd_gate_force_on_overrides_gui_default() {
7870 assert!(spin_wait_fastfwd_gate(true, false, false));
7872 assert!(spin_wait_fastfwd_gate(true, false, true));
7873 }
7874
7875 #[test]
7879 fn modaldialog_refire_skip_gui_mode_never_fires() {
7880 for has_tracking in [false, true] {
7883 for events in [false, true] {
7884 assert!(
7885 !modaldialog_refire_is_noop(
7886 true, has_tracking,
7888 true, true,
7890 true,
7891 true,
7892 events,
7893 ),
7894 "GUI mode must never skip refires (has_tracking={}, events={})",
7895 has_tracking,
7896 events
7897 );
7898 }
7899 }
7900 }
7901
7902 #[test]
7903 fn tracking_refire_freeze_policy_keeps_modaldialog_ticks_live() {
7904 assert!(tracking_refire_should_freeze_ticks(0xA93D));
7907 assert!(tracking_refire_should_freeze_ticks(0xAD3D));
7908 assert!(tracking_refire_should_freeze_ticks(0xA80B));
7909 assert!(tracking_refire_should_freeze_ticks(0xAC0B));
7910 assert!(tracking_refire_should_freeze_ticks(0xA968));
7911 assert!(tracking_refire_should_freeze_ticks(0xAD68));
7912
7913 assert!(!tracking_refire_should_freeze_ticks(0xA991));
7916 assert!(!tracking_refire_should_freeze_ticks(0xAD91));
7917 }
7918
7919 #[test]
7920 fn gui_modaldialog_idle_refire_advances_one_tick() {
7921 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7922 let base = 0x0001_0000u32;
7923 runner.bus.write_word(base, 0xA991); runner.cpu.write_reg(Register::PC, base);
7925 runner.cpu.write_reg(Register::A7, 0x0010_0000);
7926 runner.bus.write_long(0x016A, 0);
7927 runner.dispatcher.tick_count = 0;
7928 runner.set_instructions_per_tick(1_000_000);
7929 runner.dispatcher.dialog_tracking = Some(dialog_tracking_for_test(0, 0));
7930
7931 let (steps, running) = runner.run_gui_slice_with_audio(1, 1, 0);
7932
7933 assert!(running);
7934 assert_eq!(steps, 1);
7935 assert_eq!(runner.guest_tick(), 1);
7936 assert_eq!(runner.tick_budget, runner.instructions_per_tick() as i32);
7937 assert_eq!(runner.cpu.read_reg(Register::PC), base);
7938 }
7939
7940 #[test]
7941 fn gui_modaldialog_idle_refire_runs_until_tick_cap() {
7942 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7943 let base = 0x0001_0000u32;
7944 runner.bus.write_word(base, 0xA991); runner.cpu.write_reg(Register::PC, base);
7946 runner.cpu.write_reg(Register::A7, 0x0010_0000);
7947 runner.bus.write_long(0x016A, 0);
7948 runner.dispatcher.tick_count = 0;
7949 runner.set_instructions_per_tick(1_000_000);
7950 runner.dispatcher.dialog_tracking = Some(dialog_tracking_for_test(0, 0));
7951
7952 let (steps, running) = runner.run_gui_slice_with_audio(16, 2, 0);
7953
7954 assert!(running);
7955 assert_eq!(steps, 2);
7956 assert_eq!(runner.guest_tick(), 2);
7957 assert_eq!(runner.cpu.read_reg(Register::PC), base);
7958 }
7959
7960 #[test]
7961 fn gui_modaldialog_refire_unfreezes_prior_control_tracking() {
7962 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7963 let base = 0x0001_0000u32;
7964 runner.bus.write_word(base, 0xA991); runner.cpu.write_reg(Register::PC, base);
7966 runner.cpu.write_reg(Register::A7, 0x0010_0000);
7967 runner.bus.write_long(0x016A, 182);
7968 runner.dispatcher.tick_count = 182;
7969 runner.frozen_ticks = Some(182);
7970 runner.set_instructions_per_tick(1_000_000);
7971 runner.dispatcher.dialog_tracking = Some(dialog_tracking_for_test(0, 0));
7972
7973 let (steps, running) = runner.run_gui_slice_with_audio(16, 184, 0);
7974
7975 assert!(running);
7976 assert_eq!(steps, 1);
7977 assert_eq!(runner.frozen_ticks, None);
7978 assert_eq!(runner.guest_tick(), 184);
7979 assert_eq!(runner.dispatcher.tick_count, 184);
7980 }
7981
7982 #[test]
7983 fn gui_modaldialog_null_filter_fires_at_tick_cap() {
7984 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
7985 let base = 0x0001_0000u32;
7986 let filter_proc = 0x0001_1000u32;
7987 runner.bus.write_word(base, 0xA991); runner.bus.write_word(filter_proc, 0x4E56); runner.cpu.write_reg(Register::PC, base);
7990 runner.cpu.write_reg(Register::A7, 0x0010_0000);
7991 runner.bus.write_long(0x016A, 0);
7992 runner.dispatcher.tick_count = 0;
7993 runner.dispatcher.dialog_tracking =
7994 Some(dialog_tracking_for_test(filter_proc, 0x0010_0100));
7995
7996 let (steps, running) = runner.run_gui_slice_with_audio(1, 0, 0);
7997
7998 assert!(running);
7999 assert_eq!(steps, 1);
8000 assert_eq!(runner.guest_tick(), 0);
8001 assert_ne!(runner.cpu.read_reg(Register::PC), base);
8002 assert!(!runner.has_pending_sound_work());
8003 assert!(
8004 !runner
8005 .dispatcher
8006 .dialog_tracking
8007 .as_ref()
8008 .unwrap()
8009 .rendered_pixels_final
8010 );
8011 }
8012
8013 #[test]
8014 fn gui_modaldialog_update_filter_fires_at_tick_cap() {
8015 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8016 let base = 0x0001_0000u32;
8017 let filter_proc = 0x0001_1000u32;
8018 runner.bus.write_word(base, 0xA991); runner.bus.write_word(filter_proc, 0x4E56); runner.cpu.write_reg(Register::PC, base);
8021 runner.cpu.write_reg(Register::A7, 0x0010_0000);
8022 runner.bus.write_long(0x016A, 0);
8023 runner.dispatcher.tick_count = 0;
8024 runner.dispatcher.event_queue.push_back(QueuedEvent {
8025 what: 6,
8026 message: 0,
8027 where_v: 0,
8028 where_h: 0,
8029 modifiers: 0,
8030 });
8031 runner.dispatcher.dialog_tracking =
8032 Some(dialog_tracking_for_test(filter_proc, 0x0010_0100));
8033
8034 let (steps, running) = runner.run_gui_slice_with_audio(1, 0, 0);
8035
8036 assert!(running);
8037 assert_eq!(steps, 1);
8038 assert_eq!(runner.guest_tick(), 0);
8039 assert_ne!(runner.cpu.read_reg(Register::PC), base);
8040 assert!(
8041 !runner
8042 .dispatcher
8043 .dialog_tracking
8044 .as_ref()
8045 .unwrap()
8046 .rendered_pixels_final
8047 );
8048 }
8049
8050 #[test]
8051 fn gui_modaldialog_mouse_down_goes_to_filter_before_default_handling() {
8052 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8053 let base = 0x0001_0000u32;
8054 let filter_proc = 0x0001_1000u32;
8055 runner.bus.write_word(base, 0xA991); runner.bus.write_word(filter_proc, 0x4E56); runner.cpu.write_reg(Register::PC, base);
8058 runner.cpu.write_reg(Register::A7, 0x0010_0000);
8059 runner.bus.write_long(0x016A, 0);
8060 runner.dispatcher.tick_count = 0;
8061 runner.dispatcher.event_queue.push_back(QueuedEvent {
8062 what: 1,
8063 message: 0,
8064 where_v: 12,
8065 where_h: 24,
8066 modifiers: 0,
8067 });
8068 let mut tracking = dialog_tracking_for_test(filter_proc, 0x0010_0100);
8069 tracking.items.push(DialogItem {
8070 item_type: 4,
8071 rect: (8, 16, 20, 30),
8072 text: String::from("OK"),
8073 resource_id: 0,
8074 proc_ptr: 0,
8075 sel_start: 0,
8076 sel_end: 0,
8077 });
8078 runner.dispatcher.dialog_tracking = Some(tracking);
8079
8080 let (steps, running) = runner.run_gui_slice_with_audio(1, 0, 0);
8081
8082 assert!(running);
8083 assert_eq!(steps, 1);
8084 assert_ne!(runner.cpu.read_reg(Register::PC), base);
8085 let event_ptr = runner.dialog_filter_event;
8086 assert_eq!(runner.bus.read_word(event_ptr), 1);
8087 assert_eq!(runner.bus.read_word(event_ptr + 10), 12);
8088 assert_eq!(runner.bus.read_word(event_ptr + 12), 24);
8089 assert!(
8090 runner.dispatcher.event_queue.is_empty(),
8091 "the filter callback should consume a queued button mouseDown event"
8092 );
8093 }
8094
8095 #[test]
8096 fn modaldialog_filter_null_event_is_paced_per_guest_tick() {
8097 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8098 let filter_proc = 0x0001_1000u32;
8099 let dialog_ptr = 0x0020_0000u32;
8100 runner.dispatcher.dialog_tracking =
8101 Some(dialog_tracking_for_test(filter_proc, 0x0010_0100));
8102 runner.dispatcher.tick_count = 42;
8103 runner.bus.write_long(0x016A, 42);
8104
8105 assert!(runner.should_fire_dialog_filter_proc());
8106
8107 runner.dialog_filter_last_null_event_tick = Some((dialog_ptr, 42));
8108 assert!(
8109 !runner.should_fire_dialog_filter_proc(),
8110 "a synthetic null event should not refire twice in the same guest tick"
8111 );
8112
8113 runner.dispatcher.tick_count = 43;
8114 runner.bus.write_long(0x016A, 43);
8115 assert!(
8116 runner.should_fire_dialog_filter_proc(),
8117 "the next guest tick should allow another ModalDialog null-event filter call"
8118 );
8119 }
8120
8121 #[test]
8122 fn modaldialog_filter_real_events_bypass_null_event_pacing() {
8123 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8124 let filter_proc = 0x0001_1000u32;
8125 let dialog_ptr = 0x0020_0000u32;
8126 runner.dispatcher.dialog_tracking =
8127 Some(dialog_tracking_for_test(filter_proc, 0x0010_0100));
8128 runner.dispatcher.tick_count = 42;
8129 runner.bus.write_long(0x016A, 42);
8130 runner.dialog_filter_last_null_event_tick = Some((dialog_ptr, 42));
8131 runner.dispatcher.event_queue.push_back(QueuedEvent {
8132 what: 1,
8133 message: 0,
8134 where_v: 12,
8135 where_h: 34,
8136 modifiers: 0,
8137 });
8138
8139 assert!(
8140 runner.should_fire_dialog_filter_proc(),
8141 "mouse/key/update events must still enter the filter immediately"
8142 );
8143
8144 runner.dispatcher.event_queue.clear();
8145 let window_ptr = runner.bus.alloc(170);
8146 runner.dispatcher.init_cgraf_window(
8147 &mut runner.bus,
8148 &mut runner.cpu,
8149 window_ptr,
8150 0,
8151 100,
8152 120,
8153 220,
8154 360,
8155 "",
8156 2,
8157 true,
8158 false,
8159 false,
8160 0,
8161 );
8162 runner
8163 .dispatcher
8164 .dialog_tracking
8165 .as_mut()
8166 .unwrap()
8167 .dialog_ptr = window_ptr;
8168 runner.dialog_filter_last_null_event_tick = Some((window_ptr, 42));
8169
8170 assert!(
8171 runner.should_fire_dialog_filter_proc(),
8172 "a pending updateEvt for the active dialog must bypass null-event pacing"
8173 );
8174 }
8175
8176 #[test]
8177 fn modaldialog_refire_skip_headless_requires_all_conditions() {
8178 assert!(modaldialog_refire_is_noop(
8181 false, true, true, true, true, true, true,
8182 ));
8183
8184 assert!(!modaldialog_refire_is_noop(
8186 false, false, true, true, true, true, true,
8187 ));
8188 assert!(!modaldialog_refire_is_noop(
8189 false, true, false, true, true, true, true,
8190 ));
8191 assert!(!modaldialog_refire_is_noop(
8192 false, true, true, false, true, true, true,
8193 ));
8194 assert!(!modaldialog_refire_is_noop(
8195 false, true, true, true, false, true, true,
8196 ));
8197 assert!(!modaldialog_refire_is_noop(
8198 false, true, true, true, true, false, true,
8199 ));
8200 assert!(!modaldialog_refire_is_noop(
8201 false, true, true, true, true, true, false,
8202 ));
8203 }
8204
8205 #[test]
8206 fn spin_fastfwd_rejects_wrong_branch_target() {
8207 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8208 let base = 0x0001_0000u32;
8209 runner.bus.write_word(base, 0x594F);
8210 runner.bus.write_word(base + 2, 0xA975);
8211 runner.bus.write_word(base + 4, 0x201F);
8212 runner.bus.write_word(base + 6, 0x5380);
8213 runner.bus.write_word(base + 8, 0xB680);
8214 runner.bus.write_word(base + 10, 0x62F6);
8217
8218 runner.bus.write_long(0x016A, 100);
8219 runner.dispatcher.tick_count = 100;
8220 runner.cpu.write_reg(Register::D3, 500);
8221 runner.cpu.write_reg(Register::A7, 0x0010_0000);
8222 let pc_after_trap = base + 4;
8223 let mut count = 0usize;
8224 runner.try_tickcount_spin_fastfwd(pc_after_trap, None, &mut count);
8225
8226 assert_eq!(runner.dispatcher.tick_count, 100);
8227 assert_eq!(count, 0);
8228 }
8229
8230 #[test]
8237 fn tickcount_inline_skip_increments_inline_skipped() {
8238 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8239 let base = 0x0001_0000u32;
8240 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);
8248 runner.cpu.write_reg(Register::A7, 0x0010_0000);
8249 runner.dispatcher.tick_count = 0;
8250 runner.bus.write_long(0x016A, 0);
8251 runner.set_instructions_per_tick(1_000_000);
8252
8253 let idx = (0xA975u16 & 0xFFF) as usize;
8254 let before = runner.dispatcher.inline_skipped[idx];
8255
8256 let (steps, running) = runner.run_steps(2, None);
8258 assert!(running, "runner should not halt on a plain trap fast path");
8259 assert_eq!(steps, 2);
8260
8261 let after = runner.dispatcher.inline_skipped[idx];
8262 assert_eq!(
8263 after - before,
8264 1,
8265 "TickCount fast path must increment inline_skipped[$0175]"
8266 );
8267 assert_eq!(
8268 runner.dispatcher.trap_histogram[idx], 1,
8269 "the same path must also increment trap_histogram[$0175]"
8270 );
8271 }
8272
8273 #[test]
8274 fn halted_by_exit_to_shell_classifies_clean_application_quit() {
8275 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8276 let base = 0x0001_0000u32;
8277 runner.bus.write_word(base, 0xA9F4); runner.cpu.write_reg(Register::PC, base);
8279 runner.cpu.write_reg(Register::A7, 0x0010_0000);
8280
8281 let (_steps, running) = runner.run_steps(1, None);
8282
8283 assert!(!running, "ExitToShell should stop the runner");
8284 assert!(runner.is_halted());
8285 assert_eq!(runner.halted_trap(), Some(0xA9F4));
8286 assert!(
8287 runner.halted_by_exit_to_shell(),
8288 "ExitToShell halt must be classified as a clean application exit"
8289 );
8290 }
8291
8292 #[test]
8293 fn halted_by_exit_to_shell_rejects_invalid_pc_halts() {
8294 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8295 runner.cpu.write_reg(Register::PC, runner.bus.ram_size());
8296 runner.cpu.write_reg(Register::A7, 0x0010_0000);
8297
8298 let (_steps, running) = runner.run_steps(1, None);
8299
8300 assert!(!running, "invalid PC should stop the runner");
8301 assert!(runner.is_halted());
8302 assert_eq!(runner.halted_trap(), None);
8303 assert!(
8304 !runner.halted_by_exit_to_shell(),
8305 "invalid-PC halts must not be reported as clean application exits"
8306 );
8307 }
8308
8309 #[test]
8310 fn ptinrect_inline_path_matches_pascal_stack_contract() {
8311 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8312 let base = 0x0001_0000u32;
8313 let sp = 0x0010_0000u32;
8314 let rect = 0x0020_0000u32;
8315
8316 runner.bus.write_word(base, 0xA8AD); runner.cpu.write_reg(Register::PC, base);
8318 runner.cpu.write_reg(Register::A7, sp);
8319 runner.set_instructions_per_tick(1_000_000);
8320
8321 runner.bus.write_long(sp, rect);
8322 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;
8330 let before_inline = runner.dispatcher.inline_skipped[idx];
8331 let before_game = runner.dispatcher.game_trap_count;
8332
8333 let (steps, running) = runner.run_steps(1, None);
8334
8335 assert!(running, "runner should not halt on PtInRect inline path");
8336 assert_eq!(steps, 1);
8337 assert_eq!(runner.cpu.read_reg(Register::PC), base + 2);
8338 assert_eq!(runner.cpu.read_reg(Register::A7), sp + 8);
8339 assert_eq!(runner.bus.read_word(sp + 8), 0x0100);
8340 assert_eq!(runner.dispatcher.inline_skipped[idx] - before_inline, 1);
8341 assert_eq!(runner.dispatcher.trap_histogram[idx], 1);
8342 assert_eq!(runner.dispatcher.game_trap_count - before_game, 1);
8343 }
8344
8345 #[test]
8346 fn eventavail_inline_path_peeks_without_dequeueing() {
8347 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8348 let base = 0x0001_0000u32;
8349 let sp = 0x0010_0000u32;
8350 let event = 0x0020_0000u32;
8351
8352 runner.bus.write_word(base, 0xA971); runner.cpu.write_reg(Register::PC, base);
8354 runner.cpu.write_reg(Register::A7, sp);
8355 runner.set_instructions_per_tick(1_000_000);
8356 runner.bus.write_long(sp, event);
8357 runner.bus.write_word(sp + 4, 0x0008); runner.dispatcher.push_key_down(0x31, b' ');
8359
8360 let idx = (0xA971u16 & 0xFFF) as usize;
8361 let before_inline = runner.dispatcher.inline_skipped[idx];
8362 let before_game = runner.dispatcher.game_trap_count;
8363
8364 let (steps, running) = runner.run_steps(1, None);
8365
8366 assert!(running, "runner should not halt on EventAvail inline path");
8367 assert_eq!(steps, 1);
8368 assert_eq!(runner.cpu.read_reg(Register::PC), base + 2);
8369 assert_eq!(runner.cpu.read_reg(Register::A7), sp + 6);
8370 assert_eq!(runner.bus.read_word(sp + 6), 0xFFFF);
8371 assert_eq!(runner.bus.read_word(event), 3);
8372 assert_eq!(
8373 runner.bus.read_long(event + 2),
8374 (0x31u32 << 8) | u32::from(b' ')
8375 );
8376 assert_eq!(
8377 runner.dispatcher.event_queue.len(),
8378 1,
8379 "EventAvail must not dequeue the matching event"
8380 );
8381 assert_eq!(runner.dispatcher.inline_skipped[idx] - before_inline, 1);
8382 assert_eq!(runner.dispatcher.trap_histogram[idx], 1);
8383 assert_eq!(
8384 runner.dispatcher.game_trap_count, before_game,
8385 "EventAvail remains excluded from game_trap_count as an idle trap"
8386 );
8387 }
8388
8389 #[test]
8397 fn modaldialog_batched_skip_increments_inline_skipped_by_batch() {
8398 use crate::trap::dispatch::DialogTrackingState;
8399 use std::collections::VecDeque;
8400
8401 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8402 let base = 0x0001_0000u32;
8403 runner.bus.write_word(base, 0xA991); runner.cpu.write_reg(Register::PC, base);
8408 runner.cpu.write_reg(Register::A7, 0x0010_0000);
8409 runner.dispatcher.tick_count = 0;
8410 runner.bus.write_long(0x016A, 0);
8411 runner.set_instructions_per_tick(1_000_000);
8412
8413 runner.dispatcher.dialog_tracking = Some(DialogTrackingState {
8419 dialog_ptr: 0x0020_0000,
8420 bounds: (0, 0, 32, 32),
8421 title: String::new(),
8422 proc_id: 1,
8423 items: Vec::new(),
8424 default_item: 0,
8425 cancel_item: 0,
8426 edit_text: String::new(),
8427 edit_item: 0,
8428 saved_pixels: Vec::new(),
8429 stack_ptr: 0,
8430 item_hit_ptr: 0,
8431 rendered_pixels: Vec::new(),
8432 flash_remaining: 0,
8433 flash_delay: 0,
8434 flash_item: 0,
8435 edit_text_modified: false,
8436 draw_proc_queue: VecDeque::new(),
8437 draw_procs_done: true,
8438 rendered_pixels_final: true,
8439 filter_proc: 0,
8440 game_managed: false,
8441 last_filter_event: None,
8442 popup_draws: Vec::new(),
8443 active_popup: None,
8444 active_button: None,
8445 active_user_item: None,
8446 });
8447
8448 let idx = (0xA991u16 & 0xFFF) as usize;
8449 let before_inline = runner.dispatcher.inline_skipped[idx];
8450 let before_hist = runner.dispatcher.trap_histogram[idx];
8451
8452 let (steps, _running) = runner.run_steps(64, None);
8455
8456 assert_eq!(
8457 steps, 64,
8458 "max_steps cap exhausted by 64 batched no-op refires"
8459 );
8460 let after_inline = runner.dispatcher.inline_skipped[idx];
8461 let after_hist = runner.dispatcher.trap_histogram[idx];
8462 assert_eq!(
8463 after_inline - before_inline,
8464 64,
8465 "batched skip must increment inline_skipped[$0191] by BATCH=64"
8466 );
8467 assert_eq!(
8468 after_hist - before_hist,
8469 64,
8470 "trap_histogram and inline_skipped must increment in lockstep on the inline path"
8471 );
8472 }
8473
8474 #[test]
8475 fn modaldialog_batched_skip_applies_after_paced_filter_null_event() {
8476 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8477 let base = 0x0001_0000u32;
8478 let filter_proc = 0x0001_1000u32;
8479 let dialog_ptr = 0x0020_0000u32;
8480 runner.bus.write_word(base, 0xA991); runner.cpu.write_reg(Register::PC, base);
8482 runner.cpu.write_reg(Register::A7, 0x0010_0000);
8483 runner.dispatcher.tick_count = 42;
8484 runner.bus.write_long(0x016A, 42);
8485 runner.set_instructions_per_tick(1_000_000);
8486 runner.dispatcher.dialog_tracking =
8487 Some(dialog_tracking_for_test(filter_proc, 0x0010_0100));
8488 runner.dialog_filter_last_null_event_tick = Some((dialog_ptr, 42));
8489
8490 let idx = (0xA991u16 & 0xFFF) as usize;
8491 let before_inline = runner.dispatcher.inline_skipped[idx];
8492 let before_hist = runner.dispatcher.trap_histogram[idx];
8493
8494 let (steps, running) = runner.run_steps(64, None);
8495
8496 assert!(running);
8497 assert_eq!(steps, 64);
8498 assert_eq!(runner.cpu.read_reg(Register::PC), base);
8499 assert_eq!(runner.dispatcher.inline_skipped[idx] - before_inline, 64);
8500 assert_eq!(runner.dispatcher.trap_histogram[idx] - before_hist, 64);
8501 }
8502
8503 #[test]
8504 fn tick_progress_persists_across_multiple_run_slices() {
8505 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8506 let program_start = 0x0001_0000;
8507 let program_words = 12;
8508
8509 for offset in (0..program_words).step_by(2) {
8510 runner.bus.write_word(program_start + offset, 0x4E71);
8511 }
8512
8513 runner.cpu.write_reg(Register::PC, program_start);
8514 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
8515 runner.bus.write_long(0x016A, 0);
8516 runner.set_instructions_per_tick(5);
8517
8518 let (steps1, running1) = runner.run_steps(3, None);
8519 let (steps2, running2) = runner.run_steps(3, None);
8520
8521 assert!(running1);
8522 assert!(running2);
8523 assert_eq!(steps1, 3);
8524 assert_eq!(steps2, 3);
8525 assert_eq!(runner.bus.read_long(0x016A), 1);
8526 }
8527
8528 #[test]
8529 fn tick_override_breaks_once_target_tick_is_reached() {
8530 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8531 let program_start = 0x0001_0000;
8532 let program_words = 16;
8533
8534 for offset in (0..program_words).step_by(2) {
8535 runner.bus.write_word(program_start + offset, 0x4E71);
8536 }
8537
8538 runner.cpu.write_reg(Register::PC, program_start);
8539 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
8540 runner.bus.write_long(0x016A, 0);
8541 runner.set_instructions_per_tick(4);
8542
8543 let (steps, running) = runner.run_steps_with_audio(16, Some(0), 0);
8544
8545 assert!(running);
8546 assert_eq!(steps, 3);
8547 assert_eq!(runner.bus.read_long(0x016A), 0);
8548 }
8549
8550 #[test]
8551 fn pending_wait_sleep_ticks_advance_in_headless_mode() {
8552 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8553 let program_start = 0x0001_0000;
8554
8555 runner.bus.write_word(program_start, 0x4E71);
8556 runner.cpu.write_reg(Register::PC, program_start);
8557 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
8558 runner.bus.write_long(0x016A, 0);
8559 runner.dispatcher.pending_wait_sleep_ticks = 3;
8560
8561 let (steps, running) = runner.run_steps(1, None);
8562
8563 assert!(running);
8564 assert_eq!(steps, 1);
8565 assert_eq!(runner.bus.read_long(0x016A), 3);
8566 assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
8567 }
8568
8569 #[test]
8570 fn pending_wait_sleep_ticks_capped_to_zero_in_headless() {
8571 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8576 let program_start = 0x0001_0000;
8577
8578 runner.bus.write_word(program_start, 0x4E71);
8579 runner.cpu.write_reg(Register::PC, program_start);
8580 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
8581 runner.bus.write_long(0x016A, 0);
8582 runner.set_wait_sleep_cap_in_headless(Some(0));
8583 runner.dispatcher.pending_wait_sleep_ticks = 60;
8584
8585 let (_steps, _running) = runner.run_steps(1, None);
8586
8587 assert_eq!(runner.bus.read_long(0x016A), 0);
8589 assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
8591 }
8592
8593 #[test]
8594 fn pending_wait_sleep_ticks_capped_in_headless_when_opt_in() {
8595 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8600 let program_start = 0x0001_0000;
8601
8602 runner.bus.write_word(program_start, 0x4E71);
8603 runner.cpu.write_reg(Register::PC, program_start);
8604 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
8605 runner.bus.write_long(0x016A, 0);
8606 runner.set_wait_sleep_cap_in_headless(Some(1));
8607 runner.dispatcher.pending_wait_sleep_ticks = 60;
8608
8609 let (steps, running) = runner.run_steps(1, None);
8610
8611 assert!(running);
8612 assert_eq!(steps, 1);
8613 assert_eq!(runner.bus.read_long(0x016A), 1);
8615 assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
8616 assert_eq!(runner.wait_sleep_cap_in_headless(), Some(1));
8617 }
8618
8619 #[test]
8620 fn pending_wait_sleep_ticks_suspends_foreground_until_gui_tick_cap() {
8621 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8627 let program_start = 0x0001_0000;
8628
8629 runner.bus.write_word(program_start, 0x4E71);
8630 runner.cpu.write_reg(Register::PC, program_start);
8631 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
8632 runner.bus.write_long(0x016A, 0);
8633 runner.dispatcher.pending_wait_sleep_ticks = 60;
8634
8635 let (steps, running) = runner.run_steps(1, Some(10));
8636
8637 assert!(running);
8638 assert_eq!(
8639 steps, 0,
8640 "foreground code should not resume while WNE sleep remains pending"
8641 );
8642 assert_eq!(runner.bus.read_long(0x016A), 10);
8643 assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 50);
8644 }
8645
8646 #[test]
8647 fn pending_wait_sleep_ticks_wakes_wait_next_event_with_queued_input() {
8648 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8649 let program_start = 0x0001_0000;
8650 let event_ptr = 0x0020_0000;
8651 let result_ptr = 0x0020_0020;
8652
8653 runner.bus.write_word(program_start, 0x4E71);
8654 runner.cpu.write_reg(Register::PC, program_start);
8655 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
8656 runner.bus.write_long(0x016A, 0);
8657 runner.bus.write_word(result_ptr, 0);
8658 runner.dispatcher.sent_open_app_event = true;
8659 runner
8660 .dispatcher
8661 .write_event_record(&mut runner.bus, event_ptr, 0, 0, 0, 0, 0);
8662 runner.dispatcher.pending_wait_sleep_ticks = 60;
8663 runner.dispatcher.pending_wait_next_event_return = Some(PendingWaitNextEventReturn {
8664 event_ptr,
8665 result_ptr,
8666 event_mask: 0xFFFF,
8667 mouse_rgn: 0,
8668 resume_pc: None,
8669 resume_sp: None,
8670 });
8671 runner.push_mouse_down(123, 456);
8672
8673 let (steps, running) = runner.run_steps(1, Some(10));
8674
8675 assert!(running);
8676 assert_eq!(steps, 1);
8677 assert_eq!(
8678 runner.bus.read_word(event_ptr),
8679 1,
8680 "queued mouseDown should replace the pending null EventRecord"
8681 );
8682 assert_eq!(runner.bus.read_word(event_ptr + 10), 123u16);
8683 assert_eq!(runner.bus.read_word(event_ptr + 12), 456u16);
8684 assert_eq!(
8685 runner.bus.read_word(result_ptr),
8686 0xFFFF,
8687 "WaitNextEvent result slot should be rewritten to TRUE"
8688 );
8689 assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
8690 assert!(runner.dispatcher.pending_wait_next_event_return.is_none());
8691 }
8692
8693 #[test]
8694 fn push_mouse_down_wakes_pending_wait_next_event_immediately() {
8695 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8696 let event_ptr = 0x0020_0000;
8697 let result_ptr = 0x0020_0020;
8698
8699 runner.bus.write_word(result_ptr, 0);
8700 runner.dispatcher.sent_open_app_event = true;
8701 runner
8702 .dispatcher
8703 .write_event_record(&mut runner.bus, event_ptr, 0, 0, 0, 0, 0);
8704 runner.dispatcher.pending_wait_sleep_ticks = 60;
8705 runner.dispatcher.pending_wait_next_event_return = Some(PendingWaitNextEventReturn {
8706 event_ptr,
8707 result_ptr,
8708 event_mask: 0xFFFF,
8709 mouse_rgn: 0,
8710 resume_pc: None,
8711 resume_sp: None,
8712 });
8713
8714 runner.push_mouse_down(123, 456);
8715
8716 assert_eq!(
8717 runner.bus.read_word(event_ptr),
8718 1,
8719 "input injection should wake a sleeping WaitNextEvent before the next CPU slice"
8720 );
8721 assert_eq!(runner.bus.read_word(event_ptr + 10), 123u16);
8722 assert_eq!(runner.bus.read_word(event_ptr + 12), 456u16);
8723 assert_eq!(runner.bus.read_word(result_ptr), 0xFFFF);
8724 assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
8725 assert!(runner.dispatcher.pending_wait_next_event_return.is_none());
8726 }
8727
8728 #[test]
8729 fn set_mouse_position_wakes_pending_wait_next_event_with_mouse_moved_region() {
8730 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8731 let event_ptr = 0x0020_0000;
8732 let result_ptr = 0x0020_0020;
8733 let mouse_rgn = test_region_handle(&mut runner.bus, 10, 20, 30, 40);
8734
8735 runner.set_mouse_position(20, 25);
8736 runner.bus.write_word(result_ptr, 0);
8737 runner.dispatcher.sent_open_app_event = true;
8738 runner
8739 .dispatcher
8740 .write_event_record(&mut runner.bus, event_ptr, 0, 0, 0, 0, 0);
8741 runner.dispatcher.pending_wait_sleep_ticks = 60;
8742 runner.dispatcher.pending_wait_next_event_return = Some(PendingWaitNextEventReturn {
8743 event_ptr,
8744 result_ptr,
8745 event_mask: 0x8000,
8746 mouse_rgn,
8747 resume_pc: None,
8748 resume_sp: None,
8749 });
8750
8751 runner.set_mouse_position(50, 25);
8752
8753 assert_eq!(
8754 runner.bus.read_word(event_ptr),
8755 15,
8756 "mouse movement outside the pending mouseRgn should wake WaitNextEvent with osEvt"
8757 );
8758 assert_eq!(runner.bus.read_long(event_ptr + 2), 0xFA00_0000);
8759 assert_eq!(runner.bus.read_word(event_ptr + 10), 50u16);
8760 assert_eq!(runner.bus.read_word(event_ptr + 12), 25u16);
8761 assert_eq!(runner.bus.read_word(result_ptr), 0xFFFF);
8762 assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
8763 assert!(runner.dispatcher.pending_wait_next_event_return.is_none());
8764 assert_eq!(
8765 runner.dispatcher.debug_mouse_moved_event_count, 1,
8766 "async wake path should share the normal mouse-moved event accounting"
8767 );
8768 }
8769
8770 #[test]
8771 fn set_mouse_position_wakes_pending_wait_next_event_with_null_for_polling_loop() {
8772 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8773 let program_start = 0x0001_0000;
8774 let event_ptr = 0x0020_0000;
8775 let result_ptr = 0x0020_0020;
8776
8777 runner.bus.write_word(program_start, 0x4E71); runner.cpu.write_reg(Register::PC, program_start);
8779 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
8780 runner.bus.write_long(0x016A, 100);
8781 runner.dispatcher.tick_count = 100;
8782 runner.tick_budget = 0;
8783 runner.bus.write_word(result_ptr, 0xFFFF);
8784 runner.dispatcher.sent_open_app_event = true;
8785 runner.dispatcher.write_event_record(
8786 &mut runner.bus,
8787 event_ptr,
8788 0xFFFF,
8789 0xABCD_EF01,
8790 1,
8791 2,
8792 3,
8793 );
8794 runner.dispatcher.pending_wait_sleep_ticks = 60;
8795 runner.dispatcher.pending_wait_next_event_return = Some(PendingWaitNextEventReturn {
8796 event_ptr,
8797 result_ptr,
8798 event_mask: 0xFFFF,
8799 mouse_rgn: 0,
8800 resume_pc: None,
8801 resume_sp: None,
8802 });
8803
8804 runner.set_mouse_position(123, 456);
8805
8806 assert_eq!(
8807 runner.bus.read_word(event_ptr),
8808 0,
8809 "mouse movement with no mouseRgn event should wake WNE as a null event for polling loops"
8810 );
8811 assert_eq!(runner.bus.read_long(event_ptr + 2), 0);
8812 assert_eq!(runner.bus.read_word(event_ptr + 10), 123u16);
8813 assert_eq!(runner.bus.read_word(event_ptr + 12), 456u16);
8814 assert_eq!(
8815 runner.bus.read_word(result_ptr),
8816 0,
8817 "WaitNextEvent should return FALSE when the wake is only for polling input"
8818 );
8819 assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
8820 assert!(runner.dispatcher.pending_wait_next_event_return.is_none());
8821
8822 let (steps, running) = runner.run_steps(1, Some(110));
8823 assert!(running);
8824 assert_eq!(
8825 steps, 1,
8826 "polling wake should let foreground code resume before the old sleep expires"
8827 );
8828 assert_eq!(
8829 runner.bus.read_long(0x016A),
8830 100,
8831 "polling wake must not spend the next slice only advancing ticks"
8832 );
8833 }
8834
8835 #[test]
8836 fn push_mouse_down_leaves_pending_wait_next_event_parked_during_interrupt_callback() {
8837 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8838 let event_ptr = 0x0020_0000;
8839 let result_ptr = 0x0020_0020;
8840 let interrupted_pc = 0x0001_0000;
8841 let interrupted_sp = 0x007F_FFC0;
8842
8843 runner.bus.write_word(result_ptr, 0);
8844 runner.dispatcher.sent_open_app_event = true;
8845 runner
8846 .dispatcher
8847 .write_event_record(&mut runner.bus, event_ptr, 0, 0, 0, 0, 0);
8848 runner.dispatcher.pending_wait_sleep_ticks = 60;
8849 runner.dispatcher.pending_wait_next_event_return = Some(PendingWaitNextEventReturn {
8850 event_ptr,
8851 result_ptr,
8852 event_mask: 0xFFFF,
8853 mouse_rgn: 0,
8854 resume_pc: None,
8855 resume_sp: None,
8856 });
8857 runner.active_interrupt_callback = Some(ActiveInterruptCallback {
8858 source: ActiveInterruptCallbackSource::Timer,
8859 resume_pc: interrupted_pc,
8860 resume_sp: interrupted_sp,
8861 d_regs: [0; 8],
8862 a_regs: [0, 0, 0, 0, 0, 0, 0, interrupted_sp],
8863 ccr: 0,
8864 restore_port: None,
8865 });
8866
8867 runner.push_mouse_down(123, 456);
8868
8869 assert_eq!(
8870 runner.bus.read_word(event_ptr),
8871 0,
8872 "input must not rewrite a foreground WaitNextEvent record while an interrupt callback is active"
8873 );
8874 assert_eq!(runner.bus.read_word(result_ptr), 0);
8875 assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 60);
8876 assert!(runner.dispatcher.pending_wait_next_event_return.is_some());
8877 assert!(
8878 runner
8879 .dispatcher
8880 .event_queue
8881 .iter()
8882 .any(|event| event.what == 1 && event.where_v == 123 && event.where_h == 456),
8883 "the mouseDown should remain queued for the foreground event loop"
8884 );
8885 }
8886
8887 #[test]
8888 fn pending_wait_next_event_drops_stale_return_after_foreground_moves_on() {
8889 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8890 let parked_pc = 0x0001_0000;
8891 let stale_pc = 0x0001_0010;
8892 let parked_sp = 0x007F_FFC0;
8893 let event_ptr = 0x0020_0000;
8894 let result_ptr = 0x0020_0020;
8895
8896 runner.bus.write_word(stale_pc, 0x4E71); runner.cpu.write_reg(Register::PC, stale_pc);
8898 runner.cpu.write_reg(Register::A7, parked_sp);
8899 runner.bus.write_word(result_ptr, 0xA582);
8900 runner.dispatcher.sent_open_app_event = true;
8901 runner
8902 .dispatcher
8903 .write_event_record(&mut runner.bus, event_ptr, 0, 0, 0, 0, 0);
8904 runner.dispatcher.pending_wait_sleep_ticks = 60;
8905 runner.dispatcher.pending_wait_next_event_return = Some(PendingWaitNextEventReturn {
8906 event_ptr,
8907 result_ptr,
8908 event_mask: 0xFFFF,
8909 mouse_rgn: 0,
8910 resume_pc: Some(parked_pc),
8911 resume_sp: Some(parked_sp),
8912 });
8913 runner.dispatcher.push_mouse_down(123, 456);
8914
8915 let (steps, running) = runner.run_steps(1, Some(10));
8916
8917 assert!(running);
8918 assert_eq!(steps, 1);
8919 assert_eq!(
8920 runner.bus.read_word(result_ptr),
8921 0xA582,
8922 "a stale WaitNextEvent return slot may now belong to a caller frame"
8923 );
8924 assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
8925 assert!(runner.dispatcher.pending_wait_next_event_return.is_none());
8926 assert!(
8927 runner
8928 .dispatcher
8929 .event_queue
8930 .iter()
8931 .any(|event| event.what == 1 && event.where_v == 123 && event.where_h == 456),
8932 "stale WNE cleanup should not silently consume a queued event"
8933 );
8934 }
8935
8936 #[test]
8937 fn push_mouse_down_restores_foreground_budget_before_next_tick_cap_run() {
8938 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8939 let program_start = 0x0001_0000;
8940
8941 runner.bus.write_word(program_start, 0x4E71); runner.cpu.write_reg(Register::PC, program_start);
8943 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
8944 runner.bus.write_long(0x016A, 100);
8945 runner.dispatcher.tick_count = 100;
8946 runner.tick_budget = 0;
8947
8948 runner.push_mouse_down(123, 456);
8949
8950 let (steps, running) = runner.run_steps(1, Some(110));
8951
8952 assert!(running);
8953 assert_eq!(
8954 steps, 1,
8955 "input injected at an exhausted tick boundary should let foreground code run"
8956 );
8957 assert_eq!(
8958 runner.bus.read_long(0x016A),
8959 100,
8960 "foreground input wake must not spend the next slice only advancing ticks"
8961 );
8962 }
8963
8964 #[test]
8965 fn set_mouse_position_restores_foreground_budget_before_next_tick_cap_run() {
8966 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8967 let program_start = 0x0001_0000;
8968
8969 runner.bus.write_word(program_start, 0x4E71); runner.cpu.write_reg(Register::PC, program_start);
8971 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
8972 runner.bus.write_long(0x016A, 100);
8973 runner.dispatcher.tick_count = 100;
8974 runner.tick_budget = 0;
8975
8976 runner.set_mouse_position(123, 456);
8977
8978 let (steps, running) = runner.run_steps(1, Some(110));
8979
8980 assert!(running);
8981 assert_eq!(
8982 steps, 1,
8983 "mouse movement at an exhausted tick boundary should let polling foreground code run"
8984 );
8985 assert_eq!(
8986 runner.bus.read_long(0x016A),
8987 100,
8988 "foreground mouse-move wake must not spend the next slice only advancing ticks"
8989 );
8990 }
8991
8992 #[test]
8993 fn pending_wait_sleep_ticks_honors_app_owned_visible_dialog_snapshot_in_gui_mode() {
8994 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
8995 let program_start = 0x0001_0000;
8996 let dialog_ptr = 0x0020_0000;
8997
8998 runner.bus.write_word(program_start, 0x4E71);
8999 runner.cpu.write_reg(Register::PC, program_start);
9000 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
9001 runner.bus.write_long(0x016A, 0);
9002 runner.dispatcher.dialog_visible_snapshots.insert(
9003 dialog_ptr,
9004 crate::trap::dispatch::PersistentDialogSnapshot {
9005 bounds: (10, 10, 40, 40),
9006 pixels: Vec::new(),
9007 },
9008 );
9009 runner.dispatcher.pending_wait_sleep_ticks = 60;
9010
9011 let (steps, running) = runner.run_steps(1, Some(10));
9012
9013 assert!(running);
9014 assert_eq!(
9015 steps, 0,
9016 "app-owned visible dialogs must not collapse WaitNextEvent sleep before ModalDialog"
9017 );
9018 assert_eq!(runner.bus.read_long(0x016A), 10);
9019 assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 50);
9020 }
9021
9022 #[test]
9023 fn pending_wait_sleep_ticks_honors_app_owned_visible_dialog_snapshot_in_headless_cap_zero() {
9024 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9025 let program_start = 0x0001_0000;
9026 let dialog_ptr = 0x0020_0000;
9027
9028 runner.bus.write_word(program_start, 0x4E71);
9029 runner.cpu.write_reg(Register::PC, program_start);
9030 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
9031 runner.bus.write_long(0x016A, 0);
9032 runner.set_wait_sleep_cap_in_headless(Some(0));
9033 runner.dispatcher.dialog_visible_snapshots.insert(
9034 dialog_ptr,
9035 crate::trap::dispatch::PersistentDialogSnapshot {
9036 bounds: (10, 10, 40, 40),
9037 pixels: Vec::new(),
9038 },
9039 );
9040 runner.dispatcher.pending_wait_sleep_ticks = 60;
9041
9042 let (steps, running) = runner.run_steps(1, None);
9043
9044 assert!(running);
9045 assert_eq!(
9046 steps, 1,
9047 "headless cap zero must not collapse app-owned dialog sleep"
9048 );
9049 assert_eq!(runner.bus.read_long(0x016A), 60);
9050 assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
9051 }
9052
9053 #[test]
9054 fn pending_wait_sleep_ticks_collapses_retained_modaldialog_snapshot_in_gui_mode() {
9055 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9056 let program_start = 0x0001_0000;
9057 let dialog_ptr = 0x0020_0000;
9058
9059 runner.bus.write_word(program_start, 0x4E71);
9060 runner.cpu.write_reg(Register::PC, program_start);
9061 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
9062 runner.bus.write_long(0x016A, 0);
9063 runner.dispatcher.dialog_visible_snapshots.insert(
9064 dialog_ptr,
9065 crate::trap::dispatch::PersistentDialogSnapshot {
9066 bounds: (10, 10, 40, 40),
9067 pixels: Vec::new(),
9068 },
9069 );
9070 runner.dispatcher.dialog_modal_entered.insert(dialog_ptr);
9071 runner.dispatcher.pending_wait_sleep_ticks = 60;
9072
9073 let (steps, running) = runner.run_steps(1, Some(10));
9074
9075 assert!(running);
9076 assert_eq!(
9077 steps, 1,
9078 "retained ModalDialog snapshots keep the existing app-yield path"
9079 );
9080 assert_eq!(runner.bus.read_long(0x016A), 0);
9081 assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
9082 }
9083
9084 #[test]
9085 fn pending_wait_sleep_ticks_resumes_when_gui_sleep_expires_before_cap() {
9086 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9087 let program_start = 0x0001_0000;
9088
9089 runner.bus.write_word(program_start, 0x4E71);
9090 runner.cpu.write_reg(Register::PC, program_start);
9091 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
9092 runner.bus.write_long(0x016A, 0);
9093 runner.dispatcher.pending_wait_sleep_ticks = 3;
9094
9095 let (steps, running) = runner.run_steps(1, Some(10));
9096
9097 assert!(running);
9098 assert_eq!(steps, 1);
9099 assert_eq!(runner.bus.read_long(0x016A), 3);
9100 assert_eq!(runner.dispatcher.pending_wait_sleep_ticks, 0);
9101 }
9102
9103 #[test]
9104 fn pending_delay_ticks_advance_in_gui_mode() {
9105 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9106 let program_start = 0x0001_0000;
9107
9108 runner.bus.write_word(program_start, 0x4E71);
9109 runner.cpu.write_reg(Register::PC, program_start);
9110 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
9111 runner.bus.write_long(0x016A, 0);
9112 runner.dispatcher.pending_delay_ticks = 3;
9113
9114 let (steps, _running) = runner.run_steps(1, Some(10));
9115
9116 assert_eq!(steps, 1);
9117 assert_eq!(runner.bus.read_long(0x016A), 3);
9118 assert_eq!(runner.dispatcher.pending_delay_ticks, 0);
9119 assert_eq!(runner.cpu.read_reg(Register::D0), 3);
9120 }
9121
9122 #[test]
9123 fn dialog_filter_synthesized_null_event_uses_live_modifiers() {
9124 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9125 let filter_proc = 0x0004_2000u32;
9126
9127 runner.bus.write_word(filter_proc, 0x4E56);
9128 runner.cpu.write_reg(Register::PC, 0x0001_0000);
9129 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
9130 runner.dispatcher.set_mouse_position(222, 333);
9131 runner.dispatcher.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
9132 dialog_ptr: 0x0020_0000,
9133 bounds: (100, 200, 200, 360),
9134 title: String::new(),
9135 proc_id: 2,
9136 items: Vec::new(),
9137 default_item: 1,
9138 cancel_item: 2,
9139 edit_text: String::new(),
9140 edit_item: 0,
9141 saved_pixels: Vec::new(),
9142 stack_ptr: 0x007F_FFC0,
9143 item_hit_ptr: 0x0030_0000,
9144 rendered_pixels: Vec::new(),
9145 flash_remaining: 0,
9146 flash_delay: 0,
9147 flash_item: 0,
9148 edit_text_modified: false,
9149 draw_proc_queue: std::collections::VecDeque::new(),
9150 draw_procs_done: true,
9151 rendered_pixels_final: true,
9152 filter_proc,
9153 game_managed: true,
9154 last_filter_event: None,
9155 popup_draws: Vec::new(),
9156 active_popup: None,
9157 active_button: None,
9158 active_user_item: None,
9159 });
9160
9161 assert!(runner.fire_dialog_filter_proc());
9162 let event_ptr = runner.dialog_filter_event;
9163 assert_eq!(runner.bus.read_word(event_ptr), 0);
9164 assert_eq!(runner.bus.read_word(event_ptr + 10), 222);
9165 assert_eq!(runner.bus.read_word(event_ptr + 12), 333);
9166 assert_eq!(
9167 runner.bus.read_word(event_ptr + 14),
9168 runner.dispatcher.current_event_modifiers()
9169 );
9170 assert_eq!(
9171 runner.dialog_filter_last_null_event_tick,
9172 Some((0x0020_0000, 0))
9173 );
9174 }
9175
9176 #[test]
9177 fn dialog_filter_uses_active_dialog_pending_update_before_null_event() {
9178 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9179 let filter_proc = 0x0004_2000u32;
9180 let dialog_ptr = runner.bus.alloc(170);
9181
9182 runner.bus.write_word(filter_proc, 0x4E56);
9183 runner.cpu.write_reg(Register::PC, 0x0001_0000);
9184 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
9185 runner.dispatcher.init_cgraf_window(
9186 &mut runner.bus,
9187 &mut runner.cpu,
9188 dialog_ptr,
9189 0,
9190 100,
9191 120,
9192 220,
9193 360,
9194 "",
9195 2,
9196 true,
9197 false,
9198 false,
9199 0,
9200 );
9201 runner.dispatcher.event_queue.clear();
9202 runner.dispatcher.dialog_tracking =
9203 Some(dialog_tracking_for_test(filter_proc, 0x0030_0000));
9204 runner
9205 .dispatcher
9206 .dialog_tracking
9207 .as_mut()
9208 .unwrap()
9209 .dialog_ptr = dialog_ptr;
9210
9211 assert!(runner.fire_dialog_filter_proc());
9212 let event_ptr = runner.dialog_filter_event;
9213 assert_eq!(runner.bus.read_word(event_ptr), 6);
9214 assert_eq!(runner.bus.read_long(event_ptr + 2), dialog_ptr);
9215 assert_eq!(runner.dialog_filter_last_null_event_tick, None);
9216 }
9217
9218 #[test]
9219 fn dialog_filter_paces_synthetic_update_without_starving_queued_input() {
9220 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9221 let filter_proc = 0x0004_2000u32;
9222 let dialog_ptr = runner.bus.alloc(170);
9223
9224 runner.bus.write_word(filter_proc, 0x4E56);
9225 runner.cpu.write_reg(Register::PC, 0x0001_0000);
9226 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
9227 runner.bus.write_long(0x016A, 17);
9228 runner.dispatcher.tick_count = 17;
9229 runner.dispatcher.init_cgraf_window(
9230 &mut runner.bus,
9231 &mut runner.cpu,
9232 dialog_ptr,
9233 0,
9234 100,
9235 120,
9236 220,
9237 360,
9238 "",
9239 2,
9240 true,
9241 false,
9242 false,
9243 0,
9244 );
9245 runner.dispatcher.event_queue.clear();
9246 runner.dispatcher.dialog_tracking =
9247 Some(dialog_tracking_for_test(filter_proc, 0x0030_0000));
9248 runner
9249 .dispatcher
9250 .dialog_tracking
9251 .as_mut()
9252 .unwrap()
9253 .dialog_ptr = dialog_ptr;
9254
9255 assert!(runner.fire_dialog_filter_proc());
9256 let event_ptr = runner.dialog_filter_event;
9257 assert_eq!(runner.bus.read_word(event_ptr), 6);
9258 assert_eq!(runner.bus.read_long(event_ptr + 2), dialog_ptr);
9259
9260 runner.active_interrupt_callback = None;
9261 runner.cpu.write_reg(Register::PC, 0x0001_0000);
9262 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
9263 runner
9264 .dispatcher
9265 .dialog_tracking
9266 .as_mut()
9267 .unwrap()
9268 .last_filter_event = None;
9269 assert!(
9270 !runner.dialog_filter_has_real_event_pending(dialog_ptr),
9271 "the same invalid-region update should not refire indefinitely in one guest tick"
9272 );
9273
9274 runner.dispatcher.event_queue.push_back(QueuedEvent {
9275 what: 1,
9276 message: 0,
9277 where_v: 123,
9278 where_h: 234,
9279 modifiers: 0,
9280 });
9281 assert!(
9282 runner.dialog_filter_has_real_event_pending(dialog_ptr),
9283 "queued user input must bypass synthetic update pacing"
9284 );
9285 assert!(runner.fire_dialog_filter_proc());
9286 assert_eq!(runner.bus.read_word(event_ptr), 1);
9287 assert_eq!(runner.bus.read_word(event_ptr + 10), 123);
9288 assert_eq!(runner.bus.read_word(event_ptr + 12), 234);
9289 assert!(
9290 runner.dispatcher.event_queue.is_empty(),
9291 "the queued mouse event should be consumed by the filter call"
9292 );
9293
9294 runner.active_interrupt_callback = None;
9295 runner
9296 .dispatcher
9297 .dialog_tracking
9298 .as_mut()
9299 .unwrap()
9300 .last_filter_event = None;
9301 runner.bus.write_long(0x016A, 18);
9302 runner.dispatcher.tick_count = 18;
9303 assert!(
9304 runner.dialog_filter_has_real_event_pending(dialog_ptr),
9305 "a still-invalid dialog can surface another update event on the next guest tick"
9306 );
9307 }
9308
9309 #[test]
9310 fn dialog_filter_proc_leaves_dialog_port_current() {
9311 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9312 let interrupted_pc = 0x0001_0000u32;
9313 let interrupted_sp = 0x007F_FFC0u32;
9314 let main_port = runner.bus.alloc(170);
9315 let dialog_ptr = runner.bus.alloc(170);
9316
9317 runner.bus.write_word(interrupted_pc, 0x60FE); runner.cpu.write_reg(Register::PC, interrupted_pc);
9319 runner.cpu.write_reg(Register::A7, interrupted_sp);
9320
9321 runner.dispatcher.init_cgraf_window(
9322 &mut runner.bus,
9323 &mut runner.cpu,
9324 main_port,
9325 0,
9326 0,
9327 0,
9328 600,
9329 800,
9330 "",
9331 2,
9332 true,
9333 false,
9334 false,
9335 0,
9336 );
9337 runner.dispatcher.init_cgraf_window(
9338 &mut runner.bus,
9339 &mut runner.cpu,
9340 dialog_ptr,
9341 0,
9342 120,
9343 180,
9344 240,
9345 420,
9346 "",
9347 2,
9348 true,
9349 false,
9350 false,
9351 0,
9352 );
9353 runner
9354 .dispatcher
9355 .set_current_port_state(&mut runner.bus, &mut runner.cpu, main_port, None);
9356 let filter_proc = runner.bus.alloc(8);
9357 runner.bus.write_word(filter_proc, 0x4E56); runner.dispatcher.dialog_tracking =
9360 Some(dialog_tracking_for_test(filter_proc, 0x0030_0000));
9361 runner
9362 .dispatcher
9363 .dialog_tracking
9364 .as_mut()
9365 .unwrap()
9366 .dialog_ptr = dialog_ptr;
9367
9368 assert!(runner.fire_dialog_filter_proc());
9369 assert_eq!(runner.dispatcher.current_port, dialog_ptr);
9370 assert_eq!(
9371 runner
9372 .active_interrupt_callback
9373 .as_ref()
9374 .and_then(|callback| callback.restore_port),
9375 None
9376 );
9377
9378 runner.cpu.write_reg(Register::PC, interrupted_pc);
9379 runner.cpu.write_reg(Register::A7, interrupted_sp);
9380 let (_steps, running) = runner.run_steps(1, None);
9381
9382 assert!(running);
9383 assert_eq!(runner.dispatcher.current_port, dialog_ptr);
9384 assert!(runner.active_interrupt_callback.is_none());
9385 }
9386
9387 #[test]
9388 fn dialog_draw_proc_trampoline_passes_item_first_and_tolerates_plain_rts() {
9389 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9390 let interrupted_pc = 0x0001_0000u32;
9391 let interrupted_sp = 0x007F_FFC0u32;
9392 let dialog_ptr = 0x0020_0000u32;
9393 let proc_addr = 0x0004_2000u32;
9394 let item_no = 5i16;
9395
9396 runner.bus.write_word(interrupted_pc, 0x60FE); runner.cpu.write_reg(Register::PC, interrupted_pc);
9399 runner.cpu.write_reg(Register::A7, interrupted_sp);
9400
9401 runner.bus.write_word(proc_addr, 0x4E56); runner.bus.write_word(proc_addr + 2, 0x0000);
9405 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 {
9409 dialog_ptr,
9410 bounds: (0, 0, 64, 64),
9411 title: String::new(),
9412 proc_id: 1,
9413 items: Vec::new(),
9414 default_item: 0,
9415 cancel_item: 0,
9416 edit_text: String::new(),
9417 edit_item: 0,
9418 saved_pixels: Vec::new(),
9419 stack_ptr: interrupted_sp,
9420 item_hit_ptr: 0,
9421 rendered_pixels: Vec::new(),
9422 flash_remaining: 0,
9423 flash_delay: 0,
9424 flash_item: 0,
9425 edit_text_modified: false,
9426 draw_proc_queue: VecDeque::from([(proc_addr, item_no)]),
9427 draw_procs_done: false,
9428 rendered_pixels_final: false,
9429 filter_proc: 0,
9430 game_managed: false,
9431 last_filter_event: None,
9432 popup_draws: Vec::new(),
9433 active_popup: None,
9434 active_button: None,
9435 active_user_item: None,
9436 });
9437
9438 assert!(runner.fire_dialog_draw_procs());
9439 let tramp = runner.dialog_draw_trampoline;
9440 assert_eq!(runner.bus.read_word(tramp), 0x48E7);
9441 assert_eq!(runner.bus.read_word(tramp + 4), 0x2F3C);
9442 assert_eq!(runner.bus.read_long(tramp + 6), dialog_ptr);
9443 assert_eq!(runner.bus.read_word(tramp + 10), 0x3F3C);
9444 assert_eq!(runner.bus.read_word(tramp + 12), item_no as u16);
9445 assert_eq!(runner.bus.read_word(tramp + 14), 0x4EB9);
9446 assert_eq!(runner.bus.read_long(tramp + 16), proc_addr);
9447 assert_eq!(runner.bus.read_word(tramp + 20), 0x4FF9);
9448 assert_eq!(runner.bus.read_long(tramp + 22), interrupted_sp - 36);
9449
9450 let (_steps, running) = runner.run_steps(16, None);
9451
9452 assert!(running);
9453 assert!(
9454 runner.active_interrupt_callback.is_none(),
9455 "dialog callback should have resumed foreground code"
9456 );
9457 assert_eq!(runner.cpu.read_reg(Register::PC), interrupted_pc);
9458 assert_eq!(runner.cpu.read_reg(Register::A7), interrupted_sp);
9459 }
9460
9461 #[test]
9462 fn modeless_dialog_draw_proc_accepts_a5_relative_proc_ptr() {
9463 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9464 let interrupted_pc = 0x0001_0000u32;
9465 let interrupted_sp = 0x007F_FFC0u32;
9466 let a5 = 0x0020_0000u32;
9467 let proc_offset = 0x0000_4200u32;
9468 let proc_addr = a5 + proc_offset;
9469 let dialog_ptr = runner.bus.alloc(170);
9470
9471 runner.bus.write_word(interrupted_pc, 0x60FE);
9472 runner.cpu.write_reg(Register::PC, interrupted_pc);
9473 runner.cpu.write_reg(Register::A7, interrupted_sp);
9474 runner.cpu.write_reg(Register::A5, a5);
9475 runner.dispatcher.init_cgraf_window(
9476 &mut runner.bus,
9477 &mut runner.cpu,
9478 dialog_ptr,
9479 0,
9480 120,
9481 180,
9482 240,
9483 420,
9484 "",
9485 2,
9486 true,
9487 false,
9488 false,
9489 0,
9490 );
9491
9492 runner.bus.write_word(proc_addr, 0x4E56); runner.bus.write_word(proc_addr + 2, 0x0000);
9494 runner.bus.write_word(proc_addr + 4, 0x4E5E); runner.bus.write_word(proc_addr + 6, 0x4E75); runner
9497 .dispatcher
9498 .modeless_dialog_draw_proc_queue
9499 .push_back((dialog_ptr, proc_offset, 5));
9500
9501 assert!(runner.fire_modeless_dialog_draw_proc());
9502
9503 let tramp = runner.dialog_draw_trampoline;
9504 assert_eq!(runner.bus.read_long(tramp + 16), proc_addr);
9505 assert_eq!(
9506 runner.dispatcher.active_modeless_dialog_draw_proc,
9507 Some(dialog_ptr)
9508 );
9509 }
9510
9511 #[test]
9512 fn modeless_dialog_draw_procs_drain_after_plain_trap() {
9513 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9514 let base = 0x0001_0000u32;
9515 let interrupted_sp = 0x007F_FFC0u32;
9516 let dialog_ptr = runner.bus.alloc(170);
9517 let proc_1 = 0x0004_2000u32;
9518 let proc_2 = 0x0004_2100u32;
9519
9520 runner.bus.write_word(base, 0xA861); runner.bus.write_word(base + 2, 0x60FE); runner.cpu.write_reg(Register::PC, base);
9523 runner.cpu.write_reg(Register::A7, interrupted_sp);
9524 runner.dispatcher.init_cgraf_window(
9525 &mut runner.bus,
9526 &mut runner.cpu,
9527 dialog_ptr,
9528 0,
9529 120,
9530 180,
9531 240,
9532 420,
9533 "",
9534 2,
9535 true,
9536 false,
9537 false,
9538 0,
9539 );
9540
9541 for proc_addr in [proc_1, proc_2] {
9542 runner.bus.write_word(proc_addr, 0x4E56); runner.bus.write_word(proc_addr + 2, 0x0000);
9544 runner.bus.write_word(proc_addr + 4, 0x4E5E); runner.bus.write_word(proc_addr + 6, 0x4E75); }
9547 runner.dispatcher.modeless_dialog_draw_proc_queue =
9548 VecDeque::from([(dialog_ptr, proc_1, 3), (dialog_ptr, proc_2, 5)]);
9549
9550 let (_steps, running) = runner.run_steps(128, None);
9551
9552 assert!(running);
9553 assert!(runner.dispatcher.modeless_dialog_draw_proc_queue.is_empty());
9554 assert_eq!(runner.dispatcher.active_modeless_dialog_draw_proc, None);
9555 assert!(
9556 runner.active_interrupt_callback.is_none(),
9557 "modeless draw callbacks should have returned to foreground code"
9558 );
9559 }
9560
9561 #[test]
9562 fn dialog_draw_proc_does_not_restore_over_guest_selected_dialog_port() {
9563 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9564 let interrupted_pc = 0x0001_0000u32;
9565 let interrupted_sp = 0x007F_FFC0u32;
9566 let main_port = runner.bus.alloc(170);
9567 let dialog_ptr = runner.bus.alloc(170);
9568 let proc_addr = 0x0004_2000u32;
9569 let item_no = 5i16;
9570
9571 runner.bus.write_word(interrupted_pc, 0x60FE); runner.cpu.write_reg(Register::PC, interrupted_pc);
9573 runner.cpu.write_reg(Register::A7, interrupted_sp);
9574
9575 runner.dispatcher.init_cgraf_window(
9576 &mut runner.bus,
9577 &mut runner.cpu,
9578 main_port,
9579 0,
9580 0,
9581 0,
9582 600,
9583 800,
9584 "",
9585 2,
9586 true,
9587 false,
9588 false,
9589 0,
9590 );
9591 runner.dispatcher.init_cgraf_window(
9592 &mut runner.bus,
9593 &mut runner.cpu,
9594 dialog_ptr,
9595 0,
9596 120,
9597 180,
9598 240,
9599 420,
9600 "",
9601 2,
9602 true,
9603 false,
9604 false,
9605 0,
9606 );
9607 runner
9608 .dispatcher
9609 .set_current_port_state(&mut runner.bus, &mut runner.cpu, main_port, None);
9610
9611 runner.bus.write_word(proc_addr, 0x4E56); runner.bus.write_word(proc_addr + 2, 0x0000);
9613 runner.bus.write_word(proc_addr + 4, 0x2F3C); runner.bus.write_long(proc_addr + 6, dialog_ptr);
9615 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 {
9620 dialog_ptr,
9621 bounds: (120, 180, 240, 420),
9622 title: String::new(),
9623 proc_id: 1,
9624 items: Vec::new(),
9625 default_item: 0,
9626 cancel_item: 0,
9627 edit_text: String::new(),
9628 edit_item: 0,
9629 saved_pixels: Vec::new(),
9630 stack_ptr: interrupted_sp,
9631 item_hit_ptr: 0,
9632 rendered_pixels: Vec::new(),
9633 flash_remaining: 0,
9634 flash_delay: 0,
9635 flash_item: 0,
9636 edit_text_modified: false,
9637 draw_proc_queue: VecDeque::from([(proc_addr, item_no)]),
9638 draw_procs_done: false,
9639 rendered_pixels_final: false,
9640 filter_proc: 0,
9641 game_managed: false,
9642 last_filter_event: None,
9643 popup_draws: Vec::new(),
9644 active_popup: None,
9645 active_button: None,
9646 active_user_item: None,
9647 });
9648
9649 assert!(runner.fire_dialog_draw_procs());
9650 assert_eq!(
9651 runner
9652 .active_interrupt_callback
9653 .as_ref()
9654 .and_then(|callback| callback.restore_port),
9655 None
9656 );
9657
9658 let (_steps, running) = runner.run_steps(32, None);
9659
9660 assert!(running);
9661 assert!(runner.active_interrupt_callback.is_none());
9662 assert_eq!(
9663 runner.dispatcher.current_port, dialog_ptr,
9664 "Dialog Manager must leave the dialog port current after the draw proc"
9665 );
9666 }
9667
9668 #[test]
9669 fn dialog_draw_proc_pascal_stack_places_item_number_before_window_pointer() {
9670 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9671 let interrupted_pc = 0x0001_0000u32;
9672 let interrupted_sp = 0x007F_FFC0u32;
9673 let dialog_ptr = 0x0029_4240u32;
9674 let proc_addr = 0x0004_2000u32;
9675 let item_no = 2i16;
9676 let seen_item_addr = 0x0004_3000u32;
9677 let seen_dialog_addr = 0x0004_3004u32;
9678
9679 runner.bus.write_word(interrupted_pc, 0x60FE); runner.cpu.write_reg(Register::PC, interrupted_pc);
9681 runner.cpu.write_reg(Register::A7, interrupted_sp);
9682
9683 runner.bus.write_word(proc_addr, 0x4E56); runner.bus.write_word(proc_addr + 2, 0x0000);
9688 runner.bus.write_word(proc_addr + 4, 0x302E); runner.bus.write_word(proc_addr + 6, 0x0008);
9690 runner.bus.write_word(proc_addr + 8, 0x33C0); runner.bus.write_long(proc_addr + 10, seen_item_addr);
9692 runner.bus.write_word(proc_addr + 14, 0x222E); runner.bus.write_word(proc_addr + 16, 0x000A);
9694 runner.bus.write_word(proc_addr + 18, 0x23C1); runner.bus.write_long(proc_addr + 20, seen_dialog_addr);
9696 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 {
9700 dialog_ptr,
9701 bounds: (120, 180, 240, 420),
9702 title: String::new(),
9703 proc_id: 1,
9704 items: Vec::new(),
9705 default_item: 0,
9706 cancel_item: 0,
9707 edit_text: String::new(),
9708 edit_item: 0,
9709 saved_pixels: Vec::new(),
9710 stack_ptr: interrupted_sp,
9711 item_hit_ptr: 0,
9712 rendered_pixels: Vec::new(),
9713 flash_remaining: 0,
9714 flash_delay: 0,
9715 flash_item: 0,
9716 edit_text_modified: false,
9717 draw_proc_queue: VecDeque::from([(proc_addr, item_no)]),
9718 draw_procs_done: false,
9719 rendered_pixels_final: false,
9720 filter_proc: 0,
9721 game_managed: false,
9722 last_filter_event: None,
9723 popup_draws: Vec::new(),
9724 active_popup: None,
9725 active_button: None,
9726 active_user_item: None,
9727 });
9728
9729 assert!(runner.fire_dialog_draw_procs());
9730 let (_steps, running) = runner.run_steps(48, None);
9731
9732 assert!(running);
9733 assert!(runner.active_interrupt_callback.is_none());
9734 assert_eq!(runner.bus.read_word(seen_item_addr) as i16, item_no);
9735 assert_eq!(runner.bus.read_long(seen_dialog_addr), dialog_ptr);
9736 }
9737
9738 #[test]
9739 fn pending_delay_ticks_fire_vbl_tasks_in_headless_mode() {
9740 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9741 let interrupted_pc = 0x0001_0000;
9742 let interrupted_sp = 0x007F_FFC0;
9743 let task_ptr = 0x0020_2000;
9744
9745 runner.bus.write_word(interrupted_pc, 0x4E71);
9746 runner.cpu.write_reg(Register::PC, interrupted_pc);
9747 runner.cpu.write_reg(Register::A7, interrupted_sp);
9748 runner.bus.write_long(0x016A, 0);
9749 runner.dispatcher.pending_delay_ticks = 1;
9750
9751 runner.bus.write_word(task_ptr + 4, 1);
9752 runner.bus.write_long(task_ptr + 6, 0x0004_1234);
9753 runner.bus.write_word(task_ptr + 10, 1);
9754 runner.bus.write_word(task_ptr + 12, 0);
9755 runner.dispatcher.vbl_tasks.push(VblTask {
9756 task_ptr,
9757 slot: None,
9758 });
9759
9760 let (steps, running) = runner.run_steps(1, None);
9761
9762 assert!(running);
9763 assert_eq!(steps, 1);
9764 assert_eq!(runner.bus.read_long(0x016A), 1);
9765 assert_eq!(runner.dispatcher.pending_delay_ticks, 0);
9766 assert_eq!(runner.cpu.read_reg(Register::D0), 1);
9767 assert!(matches!(
9768 runner.active_interrupt_callback,
9769 Some(ActiveInterruptCallback {
9770 source: ActiveInterruptCallbackSource::Vbl,
9771 ..
9772 })
9773 ));
9774 }
9775
9776 #[test]
9782 fn set_mouse_position_updates_dispatcher_and_low_mem_globals() {
9783 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9784
9785 runner.set_mouse_position(123, 456);
9786
9787 assert_eq!(runner.dispatcher.mouse_pos, (123, 456));
9788 for off in [0x0828u32, 0x082C, 0x0830] {
9789 assert_eq!(runner.bus.read_word(off), 123u16, "v at ${:04X}", off);
9790 assert_eq!(runner.bus.read_word(off + 2), 456u16, "h at ${:04X}", off);
9791 }
9792 }
9793
9794 #[test]
9803 fn zero_divide_rte_handler_resumes_after_divu_by_zero() {
9804 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9805
9806 runner.bus.write_word(0x00FE, 0x4E73); runner.bus.write_long(0x0014, 0x0000_00FE);
9809
9810 let prog = 0x0010_0000u32;
9811 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);
9816 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
9817 runner.cpu.write_reg(Register::D0, 0);
9818 runner.cpu.write_reg(Register::D1, 100);
9819
9820 let (steps, running) = runner.run_steps(3, None);
9824
9825 assert!(running, "runner must not halt on zero-divide");
9826 assert_eq!(steps, 3);
9827 assert_eq!(
9828 runner.cpu.read_reg(Register::PC),
9829 prog + 4,
9830 "PC must advance past the DIVU+NOP without re-entering the trap"
9831 );
9832 assert_eq!(
9833 runner.cpu.read_reg(Register::D1),
9834 100,
9835 "DIVU by zero must leave the destination register unchanged"
9836 );
9837 }
9838
9839 #[test]
9846 fn chk_rte_handler_resumes_after_bounds_violation() {
9847 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9848
9849 runner.bus.write_word(0x00FE, 0x4E73); runner.bus.write_long(0x0018, 0x0000_00FE); let prog = 0x0010_0000u32;
9853 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);
9858 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
9859 runner.cpu.write_reg(Register::D0, 100);
9860
9861 let (steps, running) = runner.run_steps(3, None);
9865
9866 assert!(running, "runner must not halt on CHK bounds violation");
9867 assert_eq!(steps, 3);
9868 assert_eq!(
9869 runner.cpu.read_reg(Register::PC),
9870 prog + 6,
9871 "PC must advance past CHK (4 bytes) + NOP (2 bytes)"
9872 );
9873 assert_eq!(runner.cpu.read_reg(Register::D0), 100);
9874 }
9875
9876 #[test]
9881 fn trapv_rte_handler_resumes_when_v_flag_is_set() {
9882 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9883
9884 runner.bus.write_word(0x00FE, 0x4E73); runner.bus.write_long(0x001C, 0x0000_00FE); let prog = 0x0010_0000u32;
9888 runner.bus.write_word(prog, 0x4E76); runner.bus.write_word(prog + 2, 0x4E71); runner.cpu.write_reg(Register::PC, prog);
9892 runner.cpu.write_reg(Register::A7, 0x007F_FFC0);
9893 runner.cpu.core.set_ccr(0x02); let (steps, running) = runner.run_steps(3, None);
9897
9898 assert!(running, "runner must not halt on TRAPV");
9899 assert_eq!(steps, 3);
9900 assert_eq!(runner.cpu.read_reg(Register::PC), prog + 4);
9901 }
9902
9903 #[test]
9908 fn set_mouse_position_leaves_mb_state_untouched() {
9909 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9910
9911 runner.bus.write_byte(0x0172, 0x80);
9912 runner.set_mouse_position(50, 60);
9913 assert_eq!(runner.bus.read_byte(0x0172), 0x80);
9914
9915 runner.bus.write_byte(0x0172, 0x00);
9916 runner.set_mouse_position(70, 80);
9917 assert_eq!(runner.bus.read_byte(0x0172), 0x00);
9918 }
9919
9920 #[test]
9927 fn push_mouse_down_writes_mb_state_pressed_and_position() {
9928 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9929 runner.bus.write_byte(0x0172, 0x80); runner.push_mouse_down(123, 456);
9932
9933 assert_eq!(
9934 runner.bus.read_byte(0x0172),
9935 0x00,
9936 "MBState must be 0x00 (pressed) immediately after push_mouse_down"
9937 );
9938 assert_eq!(runner.bus.read_word(0x0828), 123u16);
9942 assert_eq!(runner.bus.read_word(0x082A), 456u16);
9943 assert_eq!(runner.bus.read_word(0x082C), 123u16);
9944 assert_eq!(runner.bus.read_word(0x082E), 456u16);
9945 assert_eq!(runner.bus.read_word(0x0830), 123u16);
9946 assert_eq!(runner.bus.read_word(0x0832), 456u16);
9947 }
9948
9949 #[test]
9957 fn push_mouse_up_writes_mb_state_released_immediately() {
9958 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9959
9960 runner.push_mouse_down(10, 20);
9961 assert_eq!(runner.bus.read_byte(0x0172), 0x00);
9962
9963 runner.push_mouse_up(10, 20);
9964 assert_eq!(
9965 runner.bus.read_byte(0x0172),
9966 0x80,
9967 "MBState must flip back to 0x80 (released) immediately on push_mouse_up — \
9968 not deferred to the next tick"
9969 );
9970 }
9971
9972 #[test]
9982 fn mb_state_releases_when_paired_mouseup_queued_but_unconsumed() {
9983 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
9984
9985 runner.push_mouse_down(10, 20);
9986 runner.push_mouse_up(10, 20);
9987 runner.advance_guest_tick();
9990
9991 assert_eq!(
9992 runner.bus.read_byte(0x0172),
9993 0x80,
9994 "advance_guest_tick must release MBState to 0x80 once a \
9995 paired mouseUp is queued behind the mouseDown — even when \
9996 nothing has drained the event queue"
9997 );
9998 assert!(
10003 runner.dispatcher.event_queue.iter().any(|e| e.what == 1),
10004 "mouseDown event must remain in the queue (would be drained by GetNextEvent)"
10005 );
10006 assert!(
10007 runner.dispatcher.event_queue.iter().any(|e| e.what == 2),
10008 "mouseUp event must remain in the queue"
10009 );
10010 }
10011
10012 #[test]
10019 fn mb_state_stays_pressed_with_solo_pending_mousedown() {
10020 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
10021
10022 runner.push_mouse_down(10, 20);
10023 runner.advance_guest_tick();
10024 assert_eq!(
10025 runner.bus.read_byte(0x0172),
10026 0x00,
10027 "MBState must stay pressed across a tick advance while only \
10028 a mouseDown is queued (no paired mouseUp yet)"
10029 );
10030 }
10031
10032 #[test]
10033 fn set_menu_bar_visible_round_trips_through_public_api() {
10034 if std::env::var_os("SYSTEMLESS_SHOW_MENU_BAR").is_some() {
10048 return;
10049 }
10050 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
10051 assert!(
10052 !runner.menu_bar_visible(),
10053 "kiosk default: menu_bar_visible() must report false"
10054 );
10055 runner.set_menu_bar_visible(true);
10056 assert!(
10057 runner.menu_bar_visible(),
10058 "after set_menu_bar_visible(true), menu_bar_visible() must report true"
10059 );
10060 runner.set_menu_bar_visible(false);
10061 assert!(
10062 !runner.menu_bar_visible(),
10063 "after set_menu_bar_visible(false), menu_bar_visible() must report false"
10064 );
10065 assert!(
10069 runner.dispatcher().menu_bar_hidden,
10070 "set_menu_bar_visible(false) must clear the kiosk-bypass bit"
10071 );
10072 }
10073
10074 #[test]
10075 fn disassemble_at_decodes_known_opcodes_with_correct_advance() {
10076 let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
10094 let pc = 0x10000u32;
10095 runner.bus.write_word(pc, 0x4E71);
10097 runner.bus.write_word(pc + 2, 0xA8EC);
10099 runner.bus.write_word(pc + 4, 0x4E71);
10101 let out = runner.disassemble_at(pc, 3);
10102 assert_eq!(
10103 out.len(),
10104 3,
10105 "disassemble_at must return exactly count entries"
10106 );
10107 assert_eq!(
10108 out[0].0, pc,
10109 "first entry's PC must equal the requested start"
10110 );
10111 assert!(
10112 out[0].1.contains("NOP"),
10113 "$4E71 must disassemble to NOP, got: {}",
10114 out[0].1
10115 );
10116 assert!(
10117 out[0].2 >= 2 && out[0].2 <= 10,
10118 "instruction size must be in clamp range [2, 10], got {}",
10119 out[0].2
10120 );
10121 assert_eq!(
10122 out[1].0,
10123 pc + out[0].2,
10124 "second entry's PC must equal first PC + first size"
10125 );
10126 assert!(
10127 out[1].1.contains("$A8EC"),
10128 "A-line trap $A8EC must surface in mnemonic (DC.W form), got: {}",
10129 out[1].1
10130 );
10131 assert!(
10132 out[2].1.contains("NOP"),
10133 "third entry must be the second NOP we seeded"
10134 );
10135 }
10136}