feat(desktop): add muted accelerated ROM playback
This commit is contained in:
parent
86f29018fd
commit
423d0ba120
12 changed files with 270 additions and 16 deletions
|
|
@ -22,7 +22,7 @@ use gba_core::{
|
|||
|
||||
const MAX_STEPS: u64 = 100_000_000;
|
||||
const MAX_FRAMES: u64 = 100_000;
|
||||
const USAGE: &str = "usage: gameboy-rust --rom PATH [--rtc] [--save-type flash64|flash128] [--save-file PATH] (--steps COUNT | --window [--frames COUNT] [--audio]); steps: 1..=100000000, frames: 1..=100000; do not combine with demo or help options";
|
||||
const USAGE: &str = "usage: gameboy-rust --rom PATH [--rtc] [--save-type flash64|flash128] [--save-file PATH] (--steps COUNT | --window [--frames COUNT] [--audio] [--speed MULTIPLIER]); speed: integer 1..=16 (default 1, audio muted above 1); steps: 1..=100000000, frames: 1..=100000; do not combine with demo or help options";
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
|
|
@ -37,6 +37,7 @@ pub struct Options {
|
|||
hardware: CartridgeHardware,
|
||||
save_device: SaveDevice,
|
||||
audio: bool,
|
||||
speed: u32,
|
||||
save_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
|
|
@ -47,6 +48,7 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
let mut frames = None;
|
||||
let mut window = false;
|
||||
let mut audio = false;
|
||||
let mut speed = None;
|
||||
let mut hardware = CartridgeHardware::None;
|
||||
let mut save_device = SaveDevice::None;
|
||||
let mut save_file = None;
|
||||
|
|
@ -70,6 +72,19 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
save_file = Some(PathBuf::from(
|
||||
args.next().filter(|v| !v.is_empty()).ok_or_else(invalid)?,
|
||||
));
|
||||
} else if arg == "--speed" && speed.is_none() {
|
||||
let value = args
|
||||
.next()
|
||||
.and_then(|value| value.to_str())
|
||||
.ok_or_else(invalid)?;
|
||||
if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
|
||||
return Err(invalid());
|
||||
}
|
||||
let multiplier = value.parse::<u32>().map_err(|_| invalid())?;
|
||||
if !(1..=16).contains(&multiplier) {
|
||||
return Err(invalid());
|
||||
}
|
||||
speed = Some(multiplier);
|
||||
} else if arg == "--audio" && !audio {
|
||||
audio = true;
|
||||
} else if arg == "--window" && !window {
|
||||
|
|
@ -103,7 +118,7 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
if save_file.is_some() && save_device == SaveDevice::None {
|
||||
return Err(invalid());
|
||||
}
|
||||
if audio && !window {
|
||||
if (audio || speed.is_some()) && !window {
|
||||
return Err(invalid());
|
||||
}
|
||||
let mode = match (window, steps, frames) {
|
||||
|
|
@ -118,6 +133,7 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
save_device,
|
||||
save_file,
|
||||
audio,
|
||||
speed: speed.unwrap_or(1),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -320,7 +336,10 @@ pub fn execute(options: Options, writer: &mut impl Write) -> Result<(), Box<dyn
|
|||
frames,
|
||||
options.hardware,
|
||||
options.save_device,
|
||||
options.audio,
|
||||
crate::desktop::RomPlayback {
|
||||
audio: options.audio,
|
||||
speed: options.speed,
|
||||
},
|
||||
save_file.as_mut(),
|
||||
writer,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ fn options_require_both_flags_in_either_order() {
|
|||
save_device: SaveDevice::None,
|
||||
save_file: None,
|
||||
audio: false,
|
||||
speed: 1,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -70,6 +71,71 @@ fn save_file_requires_explicit_device_and_one_nonempty_path() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn speed_defaults_to_one_and_accepts_bounded_integers_in_any_order() {
|
||||
assert_eq!(
|
||||
parse_args(&args(&["--rom", "test.gba", "--window"]))
|
||||
.unwrap()
|
||||
.speed,
|
||||
1
|
||||
);
|
||||
for multiplier in 1..=16 {
|
||||
let value = multiplier.to_string();
|
||||
for arguments in [
|
||||
args(&["--rom", "test.gba", "--window", "--speed", &value]),
|
||||
args(&[
|
||||
"--speed", &value, "--audio", "--frames", "3", "--rom", "test.gba", "--window",
|
||||
]),
|
||||
] {
|
||||
assert_eq!(parse_args(&arguments).unwrap().speed, multiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn speed_rejects_invalid_values_duplicate_flags_and_nonwindow_modes() {
|
||||
for value in [
|
||||
"",
|
||||
"0",
|
||||
"17",
|
||||
"-1",
|
||||
"+2",
|
||||
"2.0",
|
||||
"1.5",
|
||||
"NaN",
|
||||
"inf",
|
||||
" 2",
|
||||
"2 ",
|
||||
"2",
|
||||
"0x2",
|
||||
"4294967296",
|
||||
] {
|
||||
assert!(
|
||||
parse_args(&args(&["--rom", "test.gba", "--window", "--speed", value])).is_err(),
|
||||
"{value:?}"
|
||||
);
|
||||
}
|
||||
for arguments in [
|
||||
vec!["--rom", "test.gba", "--window", "--speed"],
|
||||
vec![
|
||||
"--rom", "test.gba", "--window", "--speed", "2", "--speed", "2",
|
||||
],
|
||||
vec!["--rom", "test.gba", "--steps", "100", "--speed", "1"],
|
||||
vec!["--rom", "test.gba", "--speed", "2"],
|
||||
vec!["--speed", "2", "--cpu-demo"],
|
||||
vec!["--speed", "2", "--help"],
|
||||
] {
|
||||
assert!(parse_args(&args(&arguments)).is_err(), "{arguments:?}");
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
let mut arguments = args(&["--rom", "test.gba", "--window", "--speed"]);
|
||||
arguments.push(OsString::from_vec(vec![255]));
|
||||
assert!(parse_args(&arguments).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_is_opt_in_window_only_and_cannot_be_duplicated() {
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ pub use bitmap::run as run_bitmap;
|
|||
pub use effects::run as run_effects;
|
||||
pub use mosaic::run as run_mosaic;
|
||||
pub use raster::run as run_raster;
|
||||
pub use rom::run as run_rom;
|
||||
pub use rom::{run as run_rom, Playback as RomPlayback};
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,24 @@ use std::io::Write;
|
|||
use super::*;
|
||||
use crate::cartridge::window::{Session, Update};
|
||||
|
||||
/// Host-only playback policy. The CLI validates integer speeds from 1 through 16.
|
||||
pub struct Playback {
|
||||
pub audio: bool,
|
||||
pub speed: u32,
|
||||
}
|
||||
|
||||
impl Playback {
|
||||
fn audio_enabled(&self) -> bool {
|
||||
self.audio && self.speed == 1
|
||||
}
|
||||
|
||||
fn frame_period(&self) -> Duration {
|
||||
Duration::from_secs_f64(
|
||||
f64::from(CYCLES_PER_FRAME) / f64::from(CPU_HZ) / f64::from(self.speed),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn next_frame_start(previous: Instant, now: Instant, period: Duration, audio: bool) -> Instant {
|
||||
let target = previous + period;
|
||||
if audio && now.saturating_duration_since(target) <= period {
|
||||
|
|
@ -19,10 +37,11 @@ pub fn run(
|
|||
frame_limit: Option<u64>,
|
||||
hardware: gba_core::cartridge::CartridgeHardware,
|
||||
save_device: gba_core::cartridge::SaveDevice,
|
||||
audio_enabled: bool,
|
||||
playback: Playback,
|
||||
save_file: Option<&mut crate::cartridge::save_file::SaveFile>,
|
||||
writer: &mut impl Write,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let audio_enabled = playback.audio_enabled();
|
||||
let mut session = Session::new(bytes)?;
|
||||
session.set_cartridge_hardware(hardware);
|
||||
session.memory_mut().set_save_device(save_device);
|
||||
|
|
@ -44,6 +63,11 @@ pub fn run(
|
|||
writer,
|
||||
"Input releases and audio mutes on focus loss. Save files require --save-file and clean exit. Startup frames are not suppressed."
|
||||
)?;
|
||||
writeln!(
|
||||
writer,
|
||||
"Playback speed: {}x target; actual speed depends on host performance.",
|
||||
playback.speed
|
||||
)?;
|
||||
if let Some(limit) = frame_limit {
|
||||
writeln!(
|
||||
writer,
|
||||
|
|
@ -54,20 +78,31 @@ pub fn run(
|
|||
let mut audio = None;
|
||||
let mut samples = [gba_core::audio::StereoLevel::default(); 1024];
|
||||
let result = (|| -> Result<&str, Box<dyn Error>> {
|
||||
let title = "GBA Rust | ROM | Esc: exit";
|
||||
let mut window = create_window(title)?;
|
||||
let title = format!("GBA Rust | ROM | {}x target | Esc: exit", playback.speed);
|
||||
let mut window = create_window(&title)?;
|
||||
window.set_target_fps(0);
|
||||
if audio_enabled {
|
||||
let output = crate::audio::AudioOutput::new()?;
|
||||
writeln!(writer, "Audio enabled: {}", output.description())?;
|
||||
audio = Some(output);
|
||||
session.memory_mut().set_audio_capture(true);
|
||||
} else if playback.speed > 1 {
|
||||
writeln!(
|
||||
writer,
|
||||
"Audio muted for accelerated playback (--audio ignored above 1x)."
|
||||
)?;
|
||||
} else {
|
||||
writeln!(writer, "Audio muted; use --audio for macOS output.")?;
|
||||
}
|
||||
writer.flush()?;
|
||||
let mut was_audio_active = false;
|
||||
let period = Duration::from_secs_f64(f64::from(CYCLES_PER_FRAME) / f64::from(CPU_HZ));
|
||||
let period = playback.frame_period();
|
||||
// STOP has no emulation work to accelerate. Keep host input polling at normal speed.
|
||||
let stopped_period = Playback {
|
||||
audio: false,
|
||||
speed: 1,
|
||||
}
|
||||
.frame_period();
|
||||
let mut frame_started = Instant::now();
|
||||
let mut was_stopped = false;
|
||||
// Show a black buffer and pump initial events before boot starts.
|
||||
|
|
@ -104,7 +139,7 @@ pub fn run(
|
|||
window.set_title(if stopped {
|
||||
"GBA Rust | ROM STOP: waiting for enabled keypad input | Esc: exit"
|
||||
} else {
|
||||
title
|
||||
&title
|
||||
});
|
||||
was_stopped = stopped;
|
||||
}
|
||||
|
|
@ -131,7 +166,7 @@ pub fn run(
|
|||
}
|
||||
// Retain the last image. Host polling and sleep do not advance GBA clocks.
|
||||
window.update();
|
||||
thread::sleep(period);
|
||||
thread::sleep(stopped_period);
|
||||
frame_started = Instant::now();
|
||||
}
|
||||
}
|
||||
|
|
@ -159,6 +194,29 @@ pub fn run(
|
|||
#[cfg(test)]
|
||||
mod pacing_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn speed_scales_only_host_period_and_disables_audio_above_one() {
|
||||
let normal = Playback {
|
||||
audio: true,
|
||||
speed: 1,
|
||||
};
|
||||
assert!(normal.audio_enabled());
|
||||
assert!(!Playback {
|
||||
audio: false,
|
||||
speed: 1
|
||||
}
|
||||
.audio_enabled());
|
||||
for speed in 1..=16 {
|
||||
for audio in [false, true] {
|
||||
let playback = Playback { audio, speed };
|
||||
assert_eq!(playback.audio_enabled(), audio && speed == 1);
|
||||
let total = playback.frame_period().as_secs_f64() * f64::from(speed);
|
||||
assert!((total - normal.frame_period().as_secs_f64()).abs() < 0.00000002);
|
||||
assert!(!playback.frame_period().is_zero());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_deadlines_absorb_sleep_overshoot_but_bound_catch_up() {
|
||||
let start = Instant::now();
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ fn parse_args(args: &[OsString]) -> Result<RunMode, io::Error> {
|
|||
"--frames",
|
||||
"--rtc",
|
||||
"--audio",
|
||||
"--speed",
|
||||
"--save-type",
|
||||
"--save-file",
|
||||
]
|
||||
|
|
@ -89,7 +90,7 @@ fn parse_args(args: &[OsString]) -> Result<RunMode, io::Error> {
|
|||
[arg] if arg == "--help" || arg == "-h" => Ok(RunMode::Help),
|
||||
_ => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"usage: gameboy-rust [--cpu-demo | --timer-demo | --graphics-demo | --smoke-test | --graphics-smoke-test | --raster-demo | --raster-smoke-test | --mosaic-demo | --mosaic-smoke-test | --effects-demo | --effects-smoke-test | --tile-demo | --tile-smoke-test | --affine-demo | --affine-smoke-test | --affine-raster-demo | --affine-raster-smoke-test | --bitmap4-demo | --bitmap5-demo | --bitmap4-smoke-test | --bitmap5-smoke-test | --help]; or gameboy-rust --rom PATH [--rtc] [--save-type flash64|flash128] [--save-file PATH] (--steps COUNT | --window [--frames COUNT] [--audio]); or gameboy-rust --test-suite PATH.json",
|
||||
"usage: gameboy-rust [--cpu-demo | --timer-demo | --graphics-demo | --smoke-test | --graphics-smoke-test | --raster-demo | --raster-smoke-test | --mosaic-demo | --mosaic-smoke-test | --effects-demo | --effects-smoke-test | --tile-demo | --tile-smoke-test | --affine-demo | --affine-smoke-test | --affine-raster-demo | --affine-raster-smoke-test | --bitmap4-demo | --bitmap5-demo | --bitmap4-smoke-test | --bitmap5-smoke-test | --help]; or gameboy-rust --rom PATH [--rtc] [--save-type flash64|flash128] [--save-file PATH] (--steps COUNT | --window [--frames COUNT] [--audio] [--speed MULTIPLIER]); or gameboy-rust --test-suite PATH.json",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
@ -135,7 +136,8 @@ fn run() -> Result<(), Box<dyn Error>> {
|
|||
--rom PATH --steps COUNT Run a raw ROM in the terminal (1..=100000000 steps)\n\
|
||||
--rom PATH --window Open a raw ROM with keyboard input\n\
|
||||
--frames COUNT Exit ROM window after 1..=100000 captured frames\n\
|
||||
--audio Enable macOS output for ROM windows (default: muted)\n\
|
||||
--speed MULTIPLIER ROM-window speed: integer 1..=16 (default: 1); mutes audio above 1x\n\
|
||||
--audio Enable macOS output at 1x (default: muted)\n\
|
||||
--rtc Select cartridge RTC (UTC startup, host elapsed time)\n\
|
||||
--save-type flash64|flash128 Select nominal Macronix Flash emulation\n\
|
||||
--save-file PATH Load raw Flash image; persist only on clean exit\n\
|
||||
|
|
|
|||
|
|
@ -98,6 +98,78 @@ fn wave_setup() -> Vec<u32> {
|
|||
code
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn speed_options_fail_before_file_access_when_invalid() {
|
||||
for tail in [
|
||||
vec!["--speed"],
|
||||
vec!["--speed", "0"],
|
||||
vec!["--speed", "17"],
|
||||
vec!["--speed", "NaN"],
|
||||
vec!["--speed", "2.5"],
|
||||
vec!["--speed", "2", "--speed", "2"],
|
||||
vec!["--steps", "1", "--speed", "1"],
|
||||
] {
|
||||
let output = command()
|
||||
.args(["--rom", "missing.gba", "--window"])
|
||||
.args(tail)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!output.status.success());
|
||||
assert!(stderr(&output).contains("usage:"));
|
||||
assert!(!stderr(&output).contains("cannot load ROM"));
|
||||
}
|
||||
let output = command()
|
||||
.args(["--rom", "missing.gba", "--steps", "1", "--speed", "2"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!output.status.success());
|
||||
assert!(stderr(&output).contains("usage:"));
|
||||
for tail in [vec!["--speed", "2"], vec!["--speed", "2", "--audio"]] {
|
||||
let output = command()
|
||||
.args(["--rom", "missing.gba", "--window"])
|
||||
.args(tail)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(stderr(&output).contains("cannot load ROM"));
|
||||
}
|
||||
let output = command().arg("--help").output().unwrap();
|
||||
assert!(stdout(&output).contains("--speed MULTIPLIER"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a logged-in desktop session; accelerated audio stays muted"]
|
||||
fn native_rom_window_speed_preserves_state_and_mutes_requested_audio() {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.rom(&wave_setup());
|
||||
let mut baseline = None;
|
||||
for speed in ["1", "2", "16"] {
|
||||
let mut cmd = command();
|
||||
cmd.arg("--rom")
|
||||
.arg(&path)
|
||||
.args(["--window", "--frames", "60", "--speed", speed]);
|
||||
if speed != "1" {
|
||||
cmd.arg("--audio");
|
||||
}
|
||||
let started = std::time::Instant::now();
|
||||
let output = cmd.output().unwrap();
|
||||
eprintln!("speed {speed}x: {:?}", started.elapsed());
|
||||
assert!(output.status.success(), "{}", stderr(&output));
|
||||
let report = stdout(&output);
|
||||
assert!(report.contains(&format!("Playback speed: {speed}x target")));
|
||||
assert!(report.contains("Captured ROM frames: 60"));
|
||||
assert!(!report.contains("Audio enabled:"));
|
||||
if speed != "1" {
|
||||
assert!(report.contains("Audio muted for accelerated playback"));
|
||||
}
|
||||
let state = report.split("Steps: ").nth(1).unwrap().to_owned();
|
||||
if let Some(expected) = &baseline {
|
||||
assert_eq!(&state, expected);
|
||||
} else {
|
||||
baseline = Some(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_options_fail_before_file_access_unless_a_window_is_selected() {
|
||||
for tail in [
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ fn native_rom_window_save_reloads_in_another_process() {
|
|||
let f = Fixture::new();
|
||||
let write = f.rom("writer.gba", &writer(0xa5, false));
|
||||
let output = command(&write, &f.save(), "flash128")
|
||||
.args(["--window", "--frames", "3"])
|
||||
.args(["--window", "--frames", "3", "--speed", "2", "--audio"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{}", text(&output));
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ The repository is a Cargo workspace with three crates:
|
|||
| `crates/desktop/src/cartridge/rtc_clock.rs` | Host-only UTC seed and monotonic elapsed-time adapter, with injected epoch/duration tests |
|
||||
| `crates/desktop/src/cartridge/window.rs` | ROM-window session, bounded slices, capture, STOP/input handling, and progress limits |
|
||||
| `crates/desktop/src/cartridge/window/tests.rs` | Original ROM pixels, keyboard mapping, STOP wake, HALT, frame budgets, and diagnostics |
|
||||
| `crates/desktop/src/desktop/rom.rs` | Main-thread ROM window, input polling, optional audio draining/muting, bounded pacing, and frame-limited exit |
|
||||
| `crates/desktop/src/desktop/rom.rs` | Main-thread ROM window, input polling, optional audio draining/muting, speed-scaled host pacing, and frame-limited exit |
|
||||
| `crates/desktop/examples/write_rom_demo.rs` | Non-overwriting export of the original input-test ROM |
|
||||
| `crates/desktop/src/cartridge/tests.rs` | ROM options, bounded readers, instruction/DMA/IRQ budgets, HALT/STOP, and diagnostic reports |
|
||||
| `crates/desktop/tests/rom_cli.rs` | File loading and exit-status checks; opt-in native ROM-window integration test |
|
||||
|
|
|
|||
|
|
@ -264,8 +264,10 @@ Original tests cover exact sample boundaries, timer consumption, pulse phases, b
|
|||
|
||||
## Darwin output adapter
|
||||
|
||||
Use `--audio` with `--rom ... --window` to enable CPAL 0.16/CoreAudio output on macOS.
|
||||
Use `--audio` with `--rom ... --window` at 1× speed to enable CPAL 0.16/CoreAudio output on macOS.
|
||||
Windows remain muted by default. Terminal runs, ROM suites, and demos do not open an audio device.
|
||||
ROM-window `--speed` values above 1 override `--audio`: no device is opened and sample capture stays disabled.
|
||||
Sound hardware still executes at the same emulated cycle rates. Accelerated audio conversion is not implemented.
|
||||
The flag is invalid in terminal mode and cannot be repeated. Other platforms currently report an explicit unsupported-platform error.
|
||||
|
||||
The adapter opens the default output device at its default configuration.
|
||||
|
|
|
|||
|
|
@ -108,6 +108,27 @@ HALT continues device clocks and frame publication.
|
|||
Normal presentation targets approximately 59.73 frames per second, without frame skipping or accumulated catch-up work.
|
||||
Slow hosts can run slower than real time. Host sleeping never advances emulated clocks.
|
||||
|
||||
### Playback speed
|
||||
|
||||
Use `--speed 2` with `--window` to target twice normal speed.
|
||||
The option accepts integer multipliers from 1 through 16; the default is 1.
|
||||
Missing values, duplicate options, fractions, and values outside this range are rejected before ROM loading.
|
||||
The option is not available for terminal runs, test suites, or demos.
|
||||
|
||||
```sh
|
||||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --save-type flash128 --save-file roms/pokemon-emerald.sav --window --speed 2
|
||||
```
|
||||
|
||||
Speed changes only the host frame period. CPU/device cycle ratios, captured frames, execution budgets, and save operations remain unchanged.
|
||||
Every emulated frame is still presented. Actual speed depends on CPU, rendering, window-system, and display costs.
|
||||
Input remains sampled between bounded execution slices. Interactive STOP retains normal-speed host polling instead of busy waiting.
|
||||
RTC still follows host elapsed time; the speed multiplier does not accelerate the cartridge clock.
|
||||
|
||||
Above 1×, audio output and capture are disabled, even when `--audio` is present.
|
||||
No host audio device is opened. Sound hardware still executes normally, including diagnostics for unsupported behavior.
|
||||
At 1×, `--audio` retains its existing meaning. The window title and terminal report show the requested speed.
|
||||
Finish the in-game save before exiting normally; speed does not change clean-exit persistence rules.
|
||||
|
||||
In interactive mode, STOP retains the last image and changes the window title.
|
||||
The host continues polling input and window-close events while GBA clocks remain frozen.
|
||||
Only keypad conditions configured by the ROM through KEYCNT and IE can wake STOP.
|
||||
|
|
@ -138,7 +159,8 @@ Right takes priority over Z. Other buttons have no visible effect in this small
|
|||
No game or firmware content is included.
|
||||
|
||||
ROM windows can enable Darwin output with `--audio`; they are muted by default.
|
||||
The option requires a usable default audio device. Device/configuration errors fail explicitly; omit the flag to run muted.
|
||||
At 1×, the option requires a usable default audio device. Device/configuration errors fail explicitly; omit the flag to run muted.
|
||||
Above 1×, `--speed` overrides `--audio` and keeps output muted.
|
||||
Focus loss and STOP clear queued audio. See [audio output and its limits](audio.md#darwin-output-adapter).
|
||||
Terminal mode has no audio. Both modes support explicit save files but lack per-step tracing.
|
||||
Commercial-game compatibility remains unverified and unsupported.
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ Update this file when a feature lands or a limit is removed.
|
|||
- Two-bank wave playback, active bank changes, and unsupported serial operations remain diagnostic with whole-access and block-store rollback.
|
||||
- ROM windows with scanline capture, keyboard input, focus-loss release, bounded execution slices, and interactive keypad wake from STOP.
|
||||
- Opt-in macOS CoreAudio output, with tested original tones and wave-ROM playback; focus loss and STOP mute queued audio.
|
||||
- ROM-window `--speed 1..16` targets accelerated playback through host pacing only; audio is muted above 1×.
|
||||
- Optional ROM-window frame limits and an original input-test ROM generator.
|
||||
- Headless JSON ROM suites with PC or VBlank completion, register/CPSR/memory/pixel assertions, scheduled buttons, bounded budgets, and structured failure reports.
|
||||
- Hash-pinned preparation of public `jsmolka/gba-tests` ARM, Thumb, memory, and BIOS ROMs, with verified result-register checkpoints.
|
||||
|
|
|
|||
|
|
@ -99,6 +99,18 @@ Original terminal/native programs activate wave playback without external assets
|
|||
Fixed-rate capture tests verify sample boundaries, timer/PSG ordering, rollback, queue limits, drop counts, and HALT/STOP.
|
||||
Host conversion tests require no device and cover rates, stereo/mono mapping, prefill, overflow, underflow, DC removal, and callback splitting.
|
||||
CLI tests keep output muted by default and reject `--audio` outside ROM-window mode.
|
||||
Speed tests validate integer multipliers 1–16, default 1, invalid/duplicate options, and window-only use.
|
||||
Pacing tests check scaled host periods and forced muting above 1×, even with `--audio`.
|
||||
A native test compares identical final CPU/cycle reports after fixed frame counts at 1×, 2×, and 16×.
|
||||
It records elapsed host time without imposing a load-sensitive timing assertion.
|
||||
The original native save/restart test also runs its saving window at 2× with `--audio` requested, but muted.
|
||||
|
||||
```sh
|
||||
direnv exec . cargo test --locked --release -p gameboy-rust-desktop --test rom_cli native_rom_window_speed -- --ignored --nocapture --test-threads=1
|
||||
```
|
||||
|
||||
A Darwin arm64 release run took approximately 1.27 seconds at 1× and 0.63 seconds at 2× for the same original program.
|
||||
Process/window startup is included. This verifies acceleration in that probe, not a speed guarantee for other games.
|
||||
|
||||
The following ignored tests require a native audio device and play original test tones:
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue