feat(cartridge): add 32 KiB SRAM save memory

Select it with --save-type sram (SaveDevice::Sram). The array is byte-wide,
starts erased, and repeats every 32 KiB through the 0x0e00xxxx save window.
A CPU byte store is staged and committed when its step completes, so failed
steps roll back and reads later in the step see the byte. Unlike Flash, SRAM
can be read from ROM code. Halfword/word access and DMA stay diagnostic until
the 8-bit bus behavior for those widths is verified.

SRAM shares the save-image API, host save files, and save states. Its device
code is new, so existing save states keep loading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhfHfHEuzv2AUTd6erW1gU
This commit is contained in:
0xrsydn 2026-10-14 11:51:27 +07:00
commit 32eaffa0fe
15 changed files with 405 additions and 30 deletions

View file

@ -23,7 +23,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] [--speed MULTIPLIER] [--state-file PATH]); speed: integer 1..=16 (default 1, audio muted above 1); steps: 1..=100000000, frames: 1..=100000; state file (window only): F5 saves and F9 loads a save state, default is the ROM path with a .state extension; do not combine with demo or help options";
const USAGE: &str = "usage: gameboy-rust --rom PATH [--rtc] [--save-type flash64|flash128|sram] [--save-file PATH] (--steps COUNT | --window [--frames COUNT] [--audio] [--speed MULTIPLIER] [--state-file PATH]); speed: integer 1..=16 (default 1, audio muted above 1); steps: 1..=100000000, frames: 1..=100000; state file (window only): F5 saves and F9 loads a save state, default is the ROM path with a .state extension; do not combine with demo or help options";
#[derive(Debug, PartialEq, Eq)]
enum Mode {
@ -69,6 +69,7 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
save_device = match args.next().and_then(|value| value.to_str()) {
Some("flash64") => SaveDevice::Flash64,
Some("flash128") => SaveDevice::Flash128,
Some("sram") => SaveDevice::Sram,
_ => return Err(invalid()),
};
} else if arg == "--save-file" && save_file.is_none() {

View file

@ -18,7 +18,7 @@ pub struct Playback {
pub speed: u32,
}
/// Host files for one ROM window: the optional Flash save and the save-state slot.
/// Host files for one ROM window: the optional cartridge save and the save-state slot.
pub struct Persistence<'a> {
pub save_file: Option<&'a mut crate::cartridge::save_file::SaveFile>,
pub state_path: PathBuf,

View file

@ -91,7 +91,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] [--speed MULTIPLIER] [--state-file PATH]); 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|sram] [--save-file PATH] (--steps COUNT | --window [--frames COUNT] [--audio] [--speed MULTIPLIER] [--state-file PATH]); or gameboy-rust --test-suite PATH.json",
)),
}
}
@ -140,8 +140,8 @@ fn run() -> Result<(), Box<dyn Error>> {
--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 on clean exit or window diagnostic\n\
--save-type flash64|flash128|sram Select nominal Macronix Flash or 32 KiB SRAM\n\
--save-file PATH Load raw save image; persist on clean exit or window diagnostic\n\
--state-file PATH ROM window save state; F5 saves, F9 loads (default: ROM path + .state)\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\

View file

@ -383,7 +383,7 @@ fn explicit_flash_selection_runs_ram_byte_reader_and_leaves_files_unchanged() {
fn invalid_save_type_options_fail_before_file_access() {
for extra in [
vec!["--save-type"],
vec!["--save-type", "sram"],
vec!["--save-type", "eeprom"],
vec!["--save-type", ""],
vec!["--save-type", "flash128", "--save-type", "flash64"],
] {

View file

@ -224,3 +224,33 @@ fn native_rom_window_save_reloads_in_another_process() {
.to_string_lossy()
.contains(".bak.")));
}
#[test]
fn sram_bytes_written_and_read_from_rom_code_persist_across_processes() {
let f = Fixture::new();
let mut code = Vec::new();
load(&mut code, 0, 0x0e00_0000);
code.extend([0xe3a0_105a, 0xe5c0_1000]); // MOV r1,#0x5a; STRB r1,[r0]
load(&mut code, 0, 0x0e00_8001); // Mirror of offset 1.
code.extend([0xe3a0_103c, 0xe5c0_1000, 0xeaff_fffe]); // MOV r1,#0x3c; STRB; loop
let write = f.rom("sram-writer.gba", &code);
let save = f.save();
let output = run(&write, &save, "sram");
assert!(output.status.success(), "{}", text(&output));
assert!(text(&output).contains("Save persisted on clean exit"));
let image = fs::read(&save).unwrap();
assert_eq!(image.len(), 32768);
assert_eq!(&image[..2], &[0x5a, 0x3c]);
assert!(image[2..].iter().all(|&v| v == 255));
// Unlike Flash, SRAM reads need no work-RAM code.
let mut code = Vec::new();
load(&mut code, 3, 0x0e00_0000);
code.extend([0xe5d3_5000, 0xe5d3_6001, 0xeaff_fffe]); // LDRB r5,[r3]; LDRB r6,[r3,#1]
let read = f.rom("sram-reader.gba", &code);
let output = run(&read, &save, "sram");
assert!(output.status.success(), "{}", text(&output));
assert!(text(&output).contains("r5=0x0000005a"), "{}", text(&output));
assert!(text(&output).contains("r6=0x0000003c"), "{}", text(&output));
assert!(text(&output).contains("Save unchanged"));
}