feat(cartridge): add explicit Flash identification and banked reads
This commit is contained in:
parent
fce222eea2
commit
8b3b7ae62d
23 changed files with 1042 additions and 52 deletions
|
|
@ -14,14 +14,14 @@ use std::{
|
|||
|
||||
use gba_core::{
|
||||
bios,
|
||||
cartridge::{CartridgeHardware, RtcError},
|
||||
cartridge::{CartridgeHardware, RtcError, SaveDevice},
|
||||
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 [--rtc] (--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] [--save-type flash64|flash128] (--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 {
|
||||
|
|
@ -34,6 +34,7 @@ pub struct Options {
|
|||
path: PathBuf,
|
||||
mode: Mode,
|
||||
hardware: CartridgeHardware,
|
||||
save_device: SaveDevice,
|
||||
}
|
||||
|
||||
pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
||||
|
|
@ -43,6 +44,7 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
let mut frames = None;
|
||||
let mut window = false;
|
||||
let mut hardware = CartridgeHardware::None;
|
||||
let mut save_device = SaveDevice::None;
|
||||
let mut args = args.iter();
|
||||
while let Some(arg) = args.next() {
|
||||
if arg == "--rom" && path.is_none() {
|
||||
|
|
@ -53,6 +55,12 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
path = Some(PathBuf::from(value));
|
||||
} else if arg == "--rtc" && hardware == CartridgeHardware::None {
|
||||
hardware = CartridgeHardware::Rtc;
|
||||
} else if arg == "--save-type" && save_device == SaveDevice::None {
|
||||
save_device = match args.next().and_then(|value| value.to_str()) {
|
||||
Some("flash64") => SaveDevice::Flash64,
|
||||
Some("flash128") => SaveDevice::Flash128,
|
||||
_ => return Err(invalid()),
|
||||
};
|
||||
} else if arg == "--window" && !window {
|
||||
window = true;
|
||||
} else if (arg == "--steps" && steps.is_none()) || (arg == "--frames" && frames.is_none()) {
|
||||
|
|
@ -90,6 +98,7 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
path: path.ok_or_else(invalid)?,
|
||||
mode,
|
||||
hardware,
|
||||
save_device,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -264,10 +273,19 @@ pub fn execute(options: Options, writer: &mut impl Write) -> Result<(), Box<dyn
|
|||
if options.hardware == CartridgeHardware::Rtc {
|
||||
writeln!(writer, "RTC selected: UTC at startup, then host elapsed time; no persistence or RTC interrupts.")?;
|
||||
}
|
||||
if options.save_device != SaveDevice::None {
|
||||
writeln!(writer, "Flash selected: {} KiB Macronix, erased array; identification/bank reads only. No programming, erase, or save files.", options.save_device.capacity() / 1024)?;
|
||||
}
|
||||
let steps = match options.mode {
|
||||
Mode::Terminal { steps } => steps,
|
||||
Mode::Window { frames } => {
|
||||
return crate::desktop::run_rom(bytes, frames, options.hardware, writer)
|
||||
return crate::desktop::run_rom(
|
||||
bytes,
|
||||
frames,
|
||||
options.hardware,
|
||||
options.save_device,
|
||||
writer,
|
||||
)
|
||||
}
|
||||
};
|
||||
writeln!(writer, "Machine-step limit: {steps} (includes boot)")?;
|
||||
|
|
@ -275,6 +293,7 @@ pub fn execute(options: Options, writer: &mut impl Write) -> Result<(), Box<dyn
|
|||
machine
|
||||
.memory_mut()
|
||||
.set_cartridge_hardware(options.hardware);
|
||||
machine.memory_mut().set_save_device(options.save_device);
|
||||
let mut clock = if options.hardware == CartridgeHardware::Rtc {
|
||||
Some(rtc_clock::RtcHostClock::new(machine.memory_mut())?)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ fn options_require_both_flags_in_either_order() {
|
|||
Options {
|
||||
path: "original program.gba".into(),
|
||||
mode: Mode::Terminal { steps: count },
|
||||
hardware: CartridgeHardware::None
|
||||
hardware: CartridgeHardware::None,
|
||||
save_device: SaveDevice::None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ pub fn run(
|
|||
bytes: Vec<u8>,
|
||||
frame_limit: Option<u64>,
|
||||
hardware: gba_core::cartridge::CartridgeHardware,
|
||||
save_device: gba_core::cartridge::SaveDevice,
|
||||
writer: &mut impl Write,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut session = Session::new(bytes)?;
|
||||
session.set_cartridge_hardware(hardware);
|
||||
session.memory_mut().set_save_device(save_device);
|
||||
let mut rtc_clock = if hardware == gba_core::cartridge::CartridgeHardware::Rtc {
|
||||
Some(crate::cartridge::rtc_clock::RtcHostClock::new(
|
||||
session.memory_mut(),
|
||||
|
|
|
|||
|
|
@ -47,9 +47,16 @@ 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", "--rtc"]
|
||||
.iter()
|
||||
.any(|flag| arg == flag)
|
||||
[
|
||||
"--rom",
|
||||
"--steps",
|
||||
"--window",
|
||||
"--frames",
|
||||
"--rtc",
|
||||
"--save-type",
|
||||
]
|
||||
.iter()
|
||||
.any(|flag| arg == flag)
|
||||
}) {
|
||||
return cartridge::parse_args(args).map(RunMode::Rom);
|
||||
}
|
||||
|
|
@ -79,7 +86,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] (--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] [--save-type flash64|flash128] (--steps COUNT | --window [--frames COUNT]); or gameboy-rust --test-suite PATH.json",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
@ -126,6 +133,7 @@ fn run() -> Result<(), Box<dyn Error>> {
|
|||
--rom PATH --window Open a raw ROM with keyboard input\n\
|
||||
--frames COUNT Exit ROM window after 1..=100000 captured frames\n\
|
||||
--rtc Select cartridge RTC (UTC startup, host elapsed time)\n\
|
||||
--save-type flash64|flash128 Select Flash identification/read subset (no saving 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\
|
||||
|
|
|
|||
|
|
@ -142,6 +142,75 @@ fn rtc_calendar_commands_run_through_original_arm_code_with_host_clock_enabled()
|
|||
);
|
||||
}
|
||||
|
||||
fn flash_setup() -> Vec<u32> {
|
||||
// Original ARM unlock/identify, then install a byte reader in IWRAM and execute it.
|
||||
let mut code = Vec::new();
|
||||
let mut literals = Vec::new();
|
||||
fn load(code: &mut Vec<u32>, literals: &mut Vec<(usize, u32, u32)>, register: u32, value: u32) {
|
||||
literals.push((code.len(), register, value));
|
||||
code.push(0);
|
||||
}
|
||||
load(&mut code, &mut literals, 1, 0x0e005555);
|
||||
code.extend([0xe3a000aa, 0xe5c10000]);
|
||||
load(&mut code, &mut literals, 2, 0x0e002aaa);
|
||||
code.extend([0xe3a00055, 0xe5c20000, 0xe3a00090, 0xe5c10000]);
|
||||
load(&mut code, &mut literals, 3, 0x0e000000);
|
||||
load(&mut code, &mut literals, 4, 0x03000000);
|
||||
for (offset, instruction) in [(0, 0xe5d35000), (4, 0xe5d36001), (8, 0xeafffffe)] {
|
||||
load(&mut code, &mut literals, 0, instruction);
|
||||
code.push(0xe5840000 | offset);
|
||||
}
|
||||
code.push(0xe12fff14); // BX r4
|
||||
for (at, register, value) in literals {
|
||||
let offset = ((code.len() - at) * 4 - 8) as u32;
|
||||
code[at] = 0xe59f0000 | (register << 12) | offset;
|
||||
code.push(value);
|
||||
}
|
||||
code
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_flash_selection_runs_ram_byte_reader_and_leaves_files_unchanged() {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.rom(&flash_setup());
|
||||
let bytes = fs::read(&path).unwrap();
|
||||
assert!(stderr(&run(&path, "1000")).contains("unmapped memory at 0x0e005555"));
|
||||
for (device, id) in [("flash64", "r6=0x0000001c"), ("flash128", "r6=0x00000009")] {
|
||||
let output = command()
|
||||
.args(["--save-type", device, "--rtc", "--rom"])
|
||||
.arg(&path)
|
||||
.args(["--steps", "1000"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{}", stderr(&output));
|
||||
let text = stdout(&output);
|
||||
assert!(text.contains("identification/bank reads only"));
|
||||
assert!(text.contains("r5=0x000000c2"), "{text}");
|
||||
assert!(text.contains(id), "{text}");
|
||||
assert_eq!(fs::read(&path).unwrap(), bytes);
|
||||
}
|
||||
assert_eq!(fs::read_dir(&fixture.0).unwrap().count(), 1); // No save file is invented.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_save_type_options_fail_before_file_access() {
|
||||
for extra in [
|
||||
vec!["--save-type"],
|
||||
vec!["--save-type", "sram"],
|
||||
vec!["--save-type", ""],
|
||||
vec!["--save-type", "flash128", "--save-type", "flash64"],
|
||||
] {
|
||||
let output = command()
|
||||
.args(["--rom", "missing.gba", "--steps", "1"])
|
||||
.args(extra)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
assert!(stderr(&output).contains("usage:"));
|
||||
assert!(!stderr(&output).contains("cannot load"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rtc_selection_is_explicit_and_does_not_modify_the_rom_file() {
|
||||
let fixture = Fixture::new();
|
||||
|
|
@ -311,11 +380,14 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
|
|||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
fn window(path: &Path, rtc: bool) -> Output {
|
||||
fn window(path: &Path, rtc: bool, save: Option<&str>) -> Output {
|
||||
let mut command = command();
|
||||
if rtc {
|
||||
command.arg("--rtc");
|
||||
}
|
||||
if let Some(device) = save {
|
||||
command.args(["--save-type", device]);
|
||||
}
|
||||
let mut child = command
|
||||
.arg("--rom")
|
||||
.arg(path)
|
||||
|
|
@ -342,7 +414,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, false);
|
||||
let output = window(&path, false, None);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} {}",
|
||||
|
|
@ -352,12 +424,12 @@ 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);
|
||||
let rtc_output = window(&rtc_path, true, None);
|
||||
assert!(rtc_output.status.success(), "{}", stderr(&rtc_output));
|
||||
assert!(stdout(&rtc_output).contains("Captured ROM frames: 3"));
|
||||
assert!(stdout(&rtc_output).contains("r2=0x00000001"));
|
||||
let calendar_path = fixture.rom(&rtc_calendar_setup());
|
||||
let calendar_output = window(&calendar_path, true);
|
||||
let calendar_output = window(&calendar_path, true, None);
|
||||
assert!(
|
||||
calendar_output.status.success(),
|
||||
"{}",
|
||||
|
|
@ -365,6 +437,12 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
|
|||
);
|
||||
assert!(stdout(&calendar_output).contains("Captured ROM frames: 3"));
|
||||
assert!(stdout(&calendar_output).contains("r2=0x00000092"));
|
||||
let flash_path = fixture.rom(&flash_setup());
|
||||
let flash_output = window(&flash_path, true, Some("flash128"));
|
||||
assert!(flash_output.status.success(), "{}", stderr(&flash_output));
|
||||
assert!(stdout(&flash_output).contains("Captured ROM frames: 3"));
|
||||
assert!(stdout(&flash_output).contains("r5=0x000000c2"));
|
||||
assert!(stdout(&flash_output).contains("r6=0x00000009"));
|
||||
for (code, error) in [
|
||||
(vec![0xef03_0000, 0xeaff_fffe], "entered STOP"),
|
||||
(vec![0xee00_0000], "instruction"),
|
||||
|
|
@ -380,7 +458,7 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
|
|||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.unwrap();
|
||||
let output = window(&path, false);
|
||||
let output = window(&path, false, None);
|
||||
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"));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue