feat(cartridge): add explicit GPIO and RTC control support

This commit is contained in:
0xrsydn 2026-10-07 05:28:52 +07:00
commit 692be36349
18 changed files with 889 additions and 26 deletions

View file

@ -13,13 +13,14 @@ use std::{
use gba_core::{
bios,
cartridge::CartridgeHardware,
machine::{Machine, MachineError, StepKind},
memory::ROM_CAPACITY,
};
const MAX_STEPS: u64 = 100_000_000;
const MAX_FRAMES: u64 = 100_000;
const USAGE: &str = "usage: gameboy-rust --rom PATH (--steps COUNT | --window [--frames COUNT]); steps: 1..=100000000, frames: 1..=100000; do not combine with demo or help options";
const USAGE: &str = "usage: gameboy-rust --rom PATH [--rtc] (--steps COUNT | --window [--frames COUNT]); steps: 1..=100000000, frames: 1..=100000; do not combine with demo or help options";
#[derive(Debug, PartialEq, Eq)]
enum Mode {
@ -31,6 +32,7 @@ enum Mode {
pub struct Options {
path: PathBuf,
mode: Mode,
hardware: CartridgeHardware,
}
pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
@ -39,6 +41,7 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
let mut steps = None;
let mut frames = None;
let mut window = false;
let mut hardware = CartridgeHardware::None;
let mut args = args.iter();
while let Some(arg) = args.next() {
if arg == "--rom" && path.is_none() {
@ -47,6 +50,8 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
.filter(|value| !value.is_empty())
.ok_or_else(invalid)?;
path = Some(PathBuf::from(value));
} else if arg == "--rtc" && hardware == CartridgeHardware::None {
hardware = CartridgeHardware::Rtc;
} else if arg == "--window" && !window {
window = true;
} else if (arg == "--steps" && steps.is_none()) || (arg == "--frames" && frames.is_none()) {
@ -83,6 +88,7 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
Ok(Options {
path: path.ok_or_else(invalid)?,
mode,
hardware,
})
}
@ -237,12 +243,20 @@ pub fn execute(options: Options, writer: &mut impl Write) -> Result<(), Box<dyn
writer,
"Original BIOS replacement; no commercial-game compatibility claim."
)?;
if options.hardware == CartridgeHardware::Rtc {
writeln!(writer, "RTC selected: GPIO and command/control only; calendar and IRQ commands remain diagnostic.")?;
}
let steps = match options.mode {
Mode::Terminal { steps } => steps,
Mode::Window { frames } => return crate::desktop::run_rom(bytes, frames, writer),
Mode::Window { frames } => {
return crate::desktop::run_rom(bytes, frames, options.hardware, writer)
}
};
writeln!(writer, "Machine-step limit: {steps} (includes boot)")?;
let mut machine = bios::boot(bytes)?;
machine
.memory_mut()
.set_cartridge_hardware(options.hardware);
let report = run_bounded(&mut machine, steps);
write_report(writer, &machine, &report)?;
writer.flush()?;

View file

@ -26,7 +26,8 @@ fn options_require_both_flags_in_either_order() {
parse_args(&arguments).unwrap(),
Options {
path: "original program.gba".into(),
mode: Mode::Terminal { steps: count }
mode: Mode::Terminal { steps: count },
hardware: CartridgeHardware::None
}
);
}

View file

@ -46,6 +46,10 @@ impl Session {
})
}
pub fn set_cartridge_hardware(&mut self, hardware: gba_core::cartridge::CartridgeHardware) {
self.machine.memory_mut().set_cartridge_hardware(hardware);
}
pub fn frame(&self) -> &Framebuffer {
&self.frame
}

View file

@ -8,9 +8,11 @@ use crate::cartridge::window::{Session, Update};
pub fn run(
bytes: Vec<u8>,
frame_limit: Option<u64>,
hardware: gba_core::cartridge::CartridgeHardware,
writer: &mut impl Write,
) -> Result<(), Box<dyn Error>> {
let mut session = Session::new(bytes)?;
session.set_cartridge_hardware(hardware);
writeln!(
writer,
"ROM window: Arrows=D-pad; Z/X=A/B; Q/W=L/R; Enter=Start; Backspace=Select; Escape exits."

View file

@ -47,7 +47,7 @@ fn parse_args(args: &[OsString]) -> Result<RunMode, io::Error> {
return cartridge::suite::parse_args(args).map(RunMode::TestSuite);
}
if args.iter().any(|arg| {
["--rom", "--steps", "--window", "--frames"]
["--rom", "--steps", "--window", "--frames", "--rtc"]
.iter()
.any(|flag| arg == flag)
}) {
@ -79,7 +79,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 (--steps COUNT | --window [--frames COUNT]); 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] (--steps COUNT | --window [--frames COUNT]); or gameboy-rust --test-suite PATH.json",
)),
}
}
@ -125,6 +125,7 @@ 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\
--rtc Select RTC GPIO/control subset for --rom (no calendar yet)\n\
--test-suite PATH.json Run bounded ROM assertions; write a JSON report\n\
--cpu-demo Run the terminal-only ARM/Thumb instruction demo\n\
--timer-demo Run the timer IRQ demo with nominal cycle costs\n\

View file

@ -77,6 +77,43 @@ fn raw_original_program_runs_with_a_bounded_report_without_changing_the_file() {
assert_eq!(fs::read(path).unwrap(), original);
}
fn rtc_setup() -> Vec<u32> {
// Original LDR/MOV/STRH/LDRH/loop: enable GPIO reads, then inspect control.
vec![
0xe59f100c, 0xe3a00001, 0xe1c100b0, 0xe1d120b0, 0xeafffffe, 0x080000c8,
]
}
#[test]
fn rtc_selection_is_explicit_and_does_not_modify_the_rom_file() {
let fixture = Fixture::new();
let path = fixture.rom(&rtc_setup());
let original = fs::read(&path).unwrap();
let absent = run(&path, "100");
assert_eq!(absent.status.code(), Some(1));
assert!(stderr(&absent).contains("read-only memory at 0x080000c8"));
let selected = command()
.arg("--rtc")
.arg("--rom")
.arg(&path)
.args(["--steps", "100"])
.output()
.unwrap();
assert!(selected.status.success(), "{}", stderr(&selected));
assert!(stdout(&selected).contains("RTC selected: GPIO and command/control only"));
assert!(stdout(&selected).contains("r2=0x00000001"));
assert_eq!(fs::read(&path).unwrap(), original);
let duplicate = command()
.arg("--rom")
.arg(&path)
.args(["--rtc", "--rtc", "--steps", "100"])
.output()
.unwrap();
assert_eq!(duplicate.status.code(), Some(1));
assert!(stderr(&duplicate).contains("usage:"));
assert!(duplicate.stdout.is_empty());
}
#[test]
fn stop_is_a_normal_bounded_outcome() {
let fixture = Fixture::new();
@ -216,8 +253,12 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
thread,
time::{Duration, Instant},
};
fn window(path: &Path) -> Output {
let mut child = command()
fn window(path: &Path, rtc: bool) -> Output {
let mut command = command();
if rtc {
command.arg("--rtc");
}
let mut child = command
.arg("--rom")
.arg(path)
.args(["--window", "--frames", "3"])
@ -243,7 +284,7 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
let fixture = Fixture::new();
let path = fixture.0.join("original-input.gba");
fs::write(&path, gba_demos::input_rom()).unwrap();
let output = window(&path);
let output = window(&path, false);
assert!(
output.status.success(),
"{} {}",
@ -252,6 +293,11 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
);
assert!(stdout(&output).contains("Captured ROM frames: 3"));
assert!(stdout(&output).contains("Result: window frame limit reached"));
let rtc_path = fixture.rom(&rtc_setup());
let rtc_output = window(&rtc_path, true);
assert!(rtc_output.status.success(), "{}", stderr(&rtc_output));
assert!(stdout(&rtc_output).contains("Captured ROM frames: 3"));
assert!(stdout(&rtc_output).contains("r2=0x00000001"));
for (code, error) in [
(vec![0xef03_0000, 0xeaff_fffe], "entered STOP"),
(vec![0xee00_0000], "instruction"),
@ -267,7 +313,7 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
.collect::<Vec<_>>(),
)
.unwrap();
let output = window(&path);
let output = window(&path, false);
assert_eq!(output.status.code(), Some(1), "{}", stdout(&output));
assert!(stdout(&output).contains("Result: ROM window diagnostic"));
assert!(stdout(&output).contains("Captured ROM frames: 0"));