pub struct FixtureRunner { /* private fields */ }Expand description
Canonical entry point of the systemless library.
FixtureRunner owns the three pieces of guest state — the M68kCpu
interpreter, the MacMemoryBus, and the TrapDispatcher (Toolbox
- OS trap handlers) — and exposes the load / step / halt-inspect surface that drives them.
Lifecycle:
FixtureRunner::new— allocate guest RAM + dispatcher.crate::game::load_game— auto-detect StuffIt / MacBinary, populate guest memory, seed CPU state.run_steps(preferred) orrun— drive the CPU.run_stepsreturns(steps_executed, still_running);runruns until halt orFixtureRunnerConfig::max_instructions.- After halt:
halted_pc/halted_trap/halted_sp/halted_d0expose per-halt detail, andhalted_by_exit_to_shellclassifies the common clean application-exit path.
Defaults: kiosk mode (Mac menu bar suppressed regardless of the
guest’s MBarHeight); arrow keys NOT remapped to numpad. Override
each via set_menu_bar_visible /
set_arrows_as_numpad or the
SYSTEMLESS_SHOW_MENU_BAR env var.
See examples/run_headless.rs for a runnable end-to-end example.
Implementations§
Source§impl FixtureRunner
impl FixtureRunner
Sourcepub fn new(ram_size: usize, config: FixtureRunnerConfig) -> Self
pub fn new(ram_size: usize, config: FixtureRunnerConfig) -> Self
Construct a fresh runner with ram_size bytes of guest RAM and
the given FixtureRunnerConfig. The CPU starts halted at
PC = 0; the framebuffer is whatever bytes the host allocator
hands us. Call load_app (or the higher-level
systemless::game::load_game) to populate guest memory and seed the
run state, then drive the guest with run_steps.
ram_size is typically 4 MiB to 16 MiB — most games never push
past 8 MiB. The runner allocates a single contiguous host
region of this size; bumping it costs only the upfront alloc.
The dispatcher defaults to kiosk mode (Mac menu bar
suppressed, regardless of the guest’s MBarHeight). Call
set_menu_bar_visible to opt back
in to the original Mac menu-bar behaviour.
Sourcepub fn halted_by_exit_to_shell(&self) -> bool
pub fn halted_by_exit_to_shell(&self) -> bool
Returns true when the halt was the documented clean application
termination path, _ExitToShell ($A9F4).
This lets runners and tests distinguish apps that intentionally quit from halts caused by faults, invalid PCs, or other fatal errors.
pub fn guest_tick(&self) -> u32
pub fn host_now(&self) -> Instant
pub fn halted_trap(&self) -> Option<u16>
pub fn halted_pc(&self) -> Option<u32>
pub fn halted_sp(&self) -> Option<u32>
pub fn halted_stack_word0(&self) -> Option<u16>
pub fn halted_stack_word(&self, word_index: u32) -> Option<u16>
pub fn halted_d0(&self) -> Option<u32>
pub fn total_instructions(&self) -> u64
pub fn debug_overlay_snapshot( &self, frame_stats: DebugOverlayFrameStats, ) -> DebugOverlaySnapshot
pub fn cpu(&self) -> &M68kCpu
pub fn cpu_mut(&mut self) -> &mut M68kCpu
pub fn bus(&self) -> &MacMemoryBus
pub fn bus_mut(&mut self) -> &mut MacMemoryBus
pub fn dispatcher(&self) -> &TrapDispatcher
pub fn dispatcher_mut(&mut self) -> &mut TrapDispatcher
Sourcepub fn ui_theme(&self) -> &'static dyn UiTheme
pub fn ui_theme(&self) -> &'static dyn UiTheme
Returns the selected UI theme provider. classic-system7 is the
default and maps to the existing renderer path; non-classic providers
are explicit Systemless-owned rendering contracts.
pub fn ui_theme_id(&self) -> UiThemeId
pub fn theme_metrics_mode(&self) -> ThemeMetricsMode
pub fn uses_classic_guest_metrics(&self) -> bool
Show or hide the Mac menu bar.
systemless runs in kiosk mode by default — the Mac menu bar is
suppressed regardless of the guest’s MBarHeight ($0BAA) value
and DrawMenuBar is a no-op. This matches the typical embedding
case (running a single classic Mac game inside a fullscreen
host window) where the host owns the chrome and the guest’s
menu bar would just diverge from the original-machine
appearance whenever the cursor entered y < 20.
Pass true to opt back in to original Mac behavior — for
example, when running a Mac application that relies on the
menu bar as its primary user surface.
The same toggle is also accessible via the SYSTEMLESS_SHOW_MENU_BAR
environment variable (set to any value to show) and via
systemless --show-menu-bar. This library method is the
preferred entry point for library embedders that don’t want
to depend on environment-variable plumbing.
Inside Macintosh Volume I, I-354 (DrawMenuBar); Inside Macintosh Volume V, V-245 (MBarHeight global).
Returns true when the Mac menu bar is currently being rendered.
In the default kiosk configuration this returns false.
Sourcepub fn disassemble_at(&self, pc: u32, count: usize) -> Vec<(u32, String, u32)>
pub fn disassemble_at(&self, pc: u32, count: usize) -> Vec<(u32, String, u32)>
Disassemble M68K instructions starting at pc for count
instruction words. Returns one entry per word: (pc, mnemonic, size_in_bytes). The size includes any operand words consumed
by the instruction; advance pc by size to reach the next.
Unknown opcodes (including A-line traps and other reserved
patterns) come back as DC.W $XXXX with size 2 — the same
convention the underlying m68k::dasm::disassemble uses.
Reads past the end of guest RAM yield (addr, "<unmapped>", 2)
rather than panicking.
Diagnostic helper for pixel-divergence and trap-misroute
investigations: pair with the framebuffer-write tracer
(SYSTEMLESS_TRACE_FB_WRITE_RANGE) to see what the guest is
actually executing at a suspect PC.
Sourcepub fn print_opcode_histogram(&self, top_n: usize)
pub fn print_opcode_histogram(&self, top_n: usize)
Dump the top-N M68K opcodes by execution count. No-op when
SYSTEMLESS_TRACE_OPCODE_COUNTS wasn’t set at startup. Format:
[OPCODE-HIST] 43210123 $3F3C MOVE.W #imm,-(SP)
Unknown opcodes fall back to showing just the hex word.
Sourcepub fn print_pc_histogram(&self, top_n: usize)
pub fn print_pc_histogram(&self, top_n: usize)
Dump the top-N hottest PCs by sampled hit count. No-op when
SYSTEMLESS_TRACE_HOT_PC is unset. Each count represents one
PC_SAMPLE_INTERVAL (=1000) M68K instructions; multiply by
1000 for an approximate instruction count attributed to that
PC.
pub fn install_application_clut(&mut self, clut: [[u16; 3]; 256])
pub fn set_app_start_time(&mut self, secs: u32)
pub fn set_application_partition_size(&mut self, bytes: Option<u32>)
Sourcepub fn app_start_time(&self) -> Option<u32>
pub fn app_start_time(&self) -> Option<u32>
Getter for the pinned Mac epoch seconds. Returns None when no
pin has been applied — without a pin, init_app falls back to
current_mac_epoch_seconds() (host wall-clock), which leaks
into the guest’s Time global ($020C) and breaks
reproducibility.
pub fn application_partition_size(&self) -> Option<u32>
pub fn enable_oracle_recording( &mut self, output_dir: impl AsRef<Path>, source: OracleSource, ) -> Result<()>
Sourcepub fn composite_frame(&mut self)
pub fn composite_frame(&mut self)
Composite chrome/dialog overlays onto the framebuffer. Call before reading raw pixels for screenshots.
Sourcepub fn set_arrows_as_numpad(&mut self, enabled: bool)
pub fn set_arrows_as_numpad(&mut self, enabled: bool)
Enable or disable arrow-key-to-numpad remapping.
Sourcepub fn arrows_as_numpad(&self) -> bool
pub fn arrows_as_numpad(&self) -> bool
Returns true when arrow keys are remapped to numpad key codes.
Sourcepub fn set_mouse_position(&mut self, v: i16, h: i16)
pub fn set_mouse_position(&mut self, v: i16, h: i16)
Move the mouse without changing the button state. Updates the dispatcher’s tracked position and the six mouse-position low-memory globals (MTemp / RawMouse / Mouse) so guest code that reads them directly sees the new coordinates immediately. Leaves MBState ($0172) untouched. Inside Macintosh Volume II, II-371.
Sourcepub fn push_mouse_down(&mut self, v: i16, h: i16)
pub fn push_mouse_down(&mut self, v: i16, h: i16)
Inject a mouse-down event and sync low-memory globals.
On real hardware the VBL interrupt handler updates MBState ($0172) and the mouse-position globals whenever the button state changes. Since our HLE has no interrupt-driven mouse driver, we sync these globals here so that code polling the low-memory locations directly (instead of calling Button or GetNextEvent) sees the correct state.
Sourcepub fn push_mouse_up(&mut self, v: i16, h: i16)
pub fn push_mouse_up(&mut self, v: i16, h: i16)
Inject a mouse-up event.
Sync MBState ($0172) immediately so code that polls the low-memory byte directly (rather than calling Button or GetNextEvent) sees the release without waiting for the next tick advance.
On real hardware the ADB manager polls the mouse at ~200 Hz, updating MBState within a few milliseconds of the physical release. Deferring the update to the next advance_guest_tick left MBState stale for an entire tick (~16 ms), which is longer than real hardware and caused frame-rate-dependent games to read the wrong button state for too many loop iterations after a click-up. Inside Macintosh Volume II, II-371
Sourcepub fn push_key_down(&mut self, mac_key: u8, char_code: u8)
pub fn push_key_down(&mut self, mac_key: u8, char_code: u8)
Inject a key-down event, applying arrow→numpad remapping if configured.
Sourcepub fn push_key_up(&mut self, mac_key: u8, char_code: u8)
pub fn push_key_up(&mut self, mac_key: u8, char_code: u8)
Inject a key-up event, applying arrow→numpad remapping if configured.
Sourcepub fn set_audio(&mut self, audio: Box<dyn AudioBackend>)
pub fn set_audio(&mut self, audio: Box<dyn AudioBackend>)
Set the audio output backend. If not set, no audio is produced.
pub fn set_instructions_per_tick(&mut self, instructions_per_tick: u32)
pub fn instructions_per_tick(&self) -> u32
Sourcepub fn set_wait_sleep_cap_in_headless(&mut self, cap: Option<u32>)
pub fn set_wait_sleep_cap_in_headless(&mut self, cap: Option<u32>)
Cap the per-WaitNextEvent-call sleep tick advance in headless mode. None (default) preserves the legacy drain-all behavior. Some(n) caps each WNE sleep to at most n tick advances, mirroring GUI mode.
pub fn wait_sleep_cap_in_headless(&self) -> Option<u32>
Sourcepub fn drain_audio(&mut self) -> Vec<u8> ⓘ
pub fn drain_audio(&mut self) -> Vec<u8> ⓘ
Drain accumulated audio samples for external consumers (e.g. WASM). Returns unsigned 8-bit mono PCM at 22050 Hz (silence = 0x80).
Sourcepub fn drain_audio_into(&mut self, out: &mut Vec<u8>)
pub fn drain_audio_into(&mut self, out: &mut Vec<u8>)
Drain accumulated audio samples into a caller-owned buffer.
This avoids transferring the runner’s Vec allocation out on every
browser frame, so the next audio mix can reuse its existing capacity.
Sourcepub fn audio_buffer_len(&self) -> usize
pub fn audio_buffer_len(&self) -> usize
Current number of buffered audio samples (for diagnostics).
pub fn has_pending_sound_work(&self) -> bool
pub fn is_ui_tracking_active(&self) -> bool
Sourcepub fn force_advance_guest_tick(&mut self)
pub fn force_advance_guest_tick(&mut self)
Advance the guest tick counter by one, firing VBL and timer tasks. Used by the GUI runner to force-advance ticks when the CPU can’t keep up with wall-clock time (e.g. during expensive PICT draws).
pub fn set_output_path(&mut self, path: PathBuf)
Sourcepub fn vfs_read(&self, filename: &str) -> Option<&[u8]>
pub fn vfs_read(&self, filename: &str) -> Option<&[u8]>
Get the contents of a file from the virtual filesystem.
pub fn vfs_file_summaries(&mut self) -> Vec<VfsFileSummary>
pub fn vfs_file_summaries_where<F>(&mut self, include: F) -> Vec<VfsFileSummary>
pub fn vfs_file_stats_where<F>(&mut self, include: F) -> Vec<VfsFileStat>
pub fn vfs_file_summary(&mut self, path: &str) -> Option<VfsFileSummary>
pub fn vfs_file_snapshot(&mut self, path: &str) -> Option<VfsFileSnapshot>
pub fn import_vfs_file(&mut self, file: &VfsFileSnapshot)
pub fn import_vfs_file_relative_to_launched_app( &mut self, relative_dir: &str, file: &VfsFileSnapshot, ) -> Result<(), String>
pub fn remove_vfs_file(&mut self, path: &str) -> bool
Sourcepub fn step(&mut self) -> StepResult
pub fn step(&mut self) -> StepResult
Execute exactly one 68k instruction. Returns the per-step result
(continue / halted / unimplemented opcode). Most embedders
should call run_steps instead — it amortises
the per-step bookkeeping (tick advancement, halt detection,
trace ring filling) across the whole budget.
Sourcepub fn load_app(&mut self, fork: &ResourceFork) -> Option<LoadedApp>
pub fn load_app(&mut self, fork: &ResourceFork) -> Option<LoadedApp>
Load a parsed Mac resource fork into guest memory: registers
every resource with the Resource Manager, links the application
CODE segments through the trap dispatcher’s segment table, and
returns a LoadedApp describing the entry-point base address
and per-segment offsets.
Lower-level than systemless::game::load_game —
that helper auto-detects StuffIt / MacBinary / raw-resource-fork
containers and calls this method internally. Use load_app
directly only when you’ve already parsed the resource fork
yourself (e.g. building a custom test fixture).
Sourcepub fn init_app(&mut self, app: &LoadedApp)
pub fn init_app(&mut self, app: &LoadedApp)
Seed the Mac-canonical low-memory globals (MemTop,
CurStackBase, ApplLimit, Lo3Bytes, Ticks, etc.) and
the A5 World start so run_steps lands the guest in a
runnable state. Must be called after load_app
and before the first call to run_steps;
the higher-level systemless::game::load_game helper invokes it
automatically.
Without init_app, A5-relative startup code (CodeWarrior /
Think C runtimes, e.g. Koji / Munchies) sees CurStackBase =
0 and spins forever in the globals-decompression loop.
Sourcepub fn mix_audio(&mut self, num_samples: usize)
pub fn mix_audio(&mut self, num_samples: usize)
Mix and queue audio samples without full frame finalization. Used to keep the audio buffer fed during long CPU frames.
Sourcepub fn mix_gui_audio_slice(&mut self, audio_samples: usize)
pub fn mix_gui_audio_slice(&mut self, audio_samples: usize)
Mix a GUI-only audio slice without running foreground guest code or redrawing the frame. Used by realtime frontends to let Sound Manager doubleback callbacks run between small audio chunks when TickCount is already caught up to the wall clock.
Sourcepub fn run_steps_with_audio(
&mut self,
max_steps: usize,
tick_override: Option<u32>,
audio_samples: usize,
) -> (usize, bool)
pub fn run_steps_with_audio( &mut self, max_steps: usize, tick_override: Option<u32>, audio_samples: usize, ) -> (usize, bool)
Run for a specific number of steps and mix the supplied amount of host audio. Returns the number of instructions executed and whether the CPU is still running.
tick_override: If Some(ticks), Ticks is capped to the supplied external
wall-clock target. If None, Ticks advances from the runner’s configured
instruction cadence.
Sourcepub fn run_realtime_steps_with_audio(
&mut self,
max_steps: usize,
audio_samples: usize,
) -> (usize, bool)
pub fn run_realtime_steps_with_audio( &mut self, max_steps: usize, audio_samples: usize, ) -> (usize, bool)
Run a realtime GUI/WASM slice using the runner’s internal tick cadence.
The caller is responsible for converting wall-clock time into max_steps
and audio_samples.
Sourcepub fn run_gui_slice_with_audio(
&mut self,
max_steps: usize,
deadline_tick: u32,
audio_samples: usize,
) -> (usize, bool)
pub fn run_gui_slice_with_audio( &mut self, max_steps: usize, deadline_tick: u32, audio_samples: usize, ) -> (usize, bool)
Run a GUI frame slice paced by wall-clock time.
Wall-clock GUI pacing works differently from the reference-runtime oracle:
in the oracle, ticks are driven purely by the instruction budget
(deterministic, host-speed-independent). In the GUI, the user expects
the game to run at real time regardless of how fast the emulator can
execute instructions, so the caller computes a deadline_tick from
host wall-clock time and we cap $016A advancement there. The CPU
runs flat out (up to max_steps) until either the tick cap is hit
or the instruction budget is exhausted, at which point the caller
yields to the UI thread for rendering.
Sourcepub fn run_gui_cpu_slice(
&mut self,
max_steps: usize,
deadline_tick: u32,
) -> (usize, bool)
pub fn run_gui_cpu_slice( &mut self, max_steps: usize, deadline_tick: u32, ) -> (usize, bool)
Run a GUI CPU slice paced by wall-clock time without finalizing a host frame. Browser frontends use this to execute several small CPU batches and then redraw chrome / mix queued audio once for the outer frame.
Sourcepub fn run_pending_sound_work(&mut self, max_steps: usize) -> (usize, bool)
pub fn run_pending_sound_work(&mut self, max_steps: usize) -> (usize, bool)
Run pending Sound Manager interrupt work without advancing TickCount or continuing into foreground guest code after the callback returns.
Sourcepub fn run_steps(
&mut self,
max_steps: usize,
tick_override: Option<u32>,
) -> (usize, bool)
pub fn run_steps( &mut self, max_steps: usize, tick_override: Option<u32>, ) -> (usize, bool)
Run for a specific number of steps (for GUI/headless callers that don’t provide a real wall-clock audio budget).
Returns (steps_executed, still_running) — note that the bool is
still_running, not halted. false means the CPU halted
(via ExitToShell, an unimplemented opcode, or a memory fault).
The per-halt detail (trap word, PC, SP, D0) is exposed via the
halted_trap, halted_pc,
halted_sp, halted_d0
accessors after this call returns.
Sourcepub fn run(&mut self) -> Result<()>
pub fn run(&mut self) -> Result<()>
Run the 68k guest until it halts or FixtureRunnerConfig::max_instructions
is reached. Returns:
Ok(())on a clean halt (Stopped, ExitToShell, or invalid PC).Err(Error::Halted)is not returned here — halt-via-trap maps toOk(()). Trap dispatch errors (other thanHalted) propagate.Error::Timeoutwhen the instruction count cap is reached before any halt condition fires.
Most embedders should prefer FixtureRunner::run_steps, which
gives you per-call budget control, returns whether the CPU is
still running, and exposes per-halt detail via the
halted_pc / halted_trap
accessors.
Sourcepub fn dump_trace(&self)
pub fn dump_trace(&self)
Print the last N executed instructions to stderr in PC/Op/Reg
form. Used by halt paths in run / run_steps_internal to
surface the run-up to a crash. Early-exits when the trace
buffer is empty (the default — SYSTEMLESS_TRACE_BUFFER=1
must be set to populate the buffer in the first place).
Trait Implementations§
Source§impl Drop for FixtureRunner
Dump the diagnostic histograms when the runner is dropped. Each
print_*_histogram already early-returns when its env-var gate
isn’t set, so this is a no-op for normal runs (including tests).
Investigate interactive-mode behavior with
SYSTEMLESS_TRACE_TRAP_COUNTS=1, SYSTEMLESS_TRACE_OPCODE_COUNTS=1,
SYSTEMLESS_TRACE_HOT_PC=1, or SYSTEMLESS_TRACE_TRAP_TIMING=1.
impl Drop for FixtureRunner
Dump the diagnostic histograms when the runner is dropped. Each
print_*_histogram already early-returns when its env-var gate
isn’t set, so this is a no-op for normal runs (including tests).
Investigate interactive-mode behavior with
SYSTEMLESS_TRACE_TRAP_COUNTS=1, SYSTEMLESS_TRACE_OPCODE_COUNTS=1,
SYSTEMLESS_TRACE_HOT_PC=1, or SYSTEMLESS_TRACE_TRAP_TIMING=1.