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
159 lines
5.4 KiB
Rust
159 lines
5.4 KiB
Rust
//! Original SRAM accesses and supplied save bytes; no game assets or save files.
|
|
use gba_core::{
|
|
cartridge::{SaveDevice, SaveError, SAVE_END, SAVE_START},
|
|
cpu::Cpu,
|
|
dma::DMA_BASE,
|
|
machine::{Machine, MachineError},
|
|
memory::{Memory, MemoryError, ROM_START},
|
|
snapshot::SnapshotError,
|
|
};
|
|
|
|
const SIZE: usize = 32 * 1024;
|
|
|
|
fn words(values: &[u32]) -> Vec<u8> {
|
|
values
|
|
.iter()
|
|
.flat_map(|value| value.to_le_bytes())
|
|
.collect()
|
|
}
|
|
|
|
fn memory(rom: &[u32]) -> Memory {
|
|
let mut bus = Memory::new(words(rom)).unwrap();
|
|
bus.set_save_device(SaveDevice::Sram);
|
|
bus
|
|
}
|
|
|
|
/// LDR r0,=address; then `instruction`; then loop.
|
|
fn program(address: u32, instruction: u32) -> Machine {
|
|
let mut bus = memory(&[0xe59f_0004, instruction, 0xeaff_fffe, address]);
|
|
let mut cpu = Cpu::new(ROM_START);
|
|
cpu.step(&mut bus).unwrap();
|
|
Machine::new(cpu, bus)
|
|
}
|
|
|
|
#[test]
|
|
fn selection_starts_erased_and_loads_only_exact_images() {
|
|
let mut bus = memory(&[0xeaff_fffe]);
|
|
assert_eq!(SaveDevice::Sram.capacity(), SIZE);
|
|
assert_eq!(bus.save_image().unwrap(), &[0xff; SIZE][..]);
|
|
assert!(!bus.save_modified());
|
|
assert_eq!(
|
|
bus.load_save_image(&[0; SIZE - 1]),
|
|
Err(SaveError::InvalidSize {
|
|
expected: SIZE,
|
|
actual: SIZE - 1
|
|
})
|
|
);
|
|
let image: Vec<u8> = (0..SIZE).map(|i| (i * 7) as u8).collect();
|
|
bus.load_save_image(&image).unwrap();
|
|
assert_eq!(bus.read8(SAVE_START + 3).unwrap(), 21);
|
|
assert!(!bus.save_modified());
|
|
}
|
|
|
|
#[test]
|
|
fn byte_writes_commit_immediately_and_mirror_every_32_kib() {
|
|
let mut bus = memory(&[0xeaff_fffe]);
|
|
bus.write8(SAVE_START + 0x8005, 0x42).unwrap();
|
|
assert_eq!(bus.read8(SAVE_START + 5).unwrap(), 0x42);
|
|
assert_eq!(bus.read8(SAVE_END).unwrap(), 0xff); // Mirror of offset 0x7fff.
|
|
assert_eq!(bus.save_image().unwrap()[5], 0x42);
|
|
assert!(bus.save_modified());
|
|
assert!(!bus.save_write_pending());
|
|
// Writing the stored value again does not mark an unchanged image as modified.
|
|
let mut bus = memory(&[0xeaff_fffe]);
|
|
bus.write8(SAVE_START, 0xff).unwrap();
|
|
assert!(!bus.save_modified());
|
|
assert_eq!(
|
|
bus.read8(SAVE_END + 1),
|
|
Err(MemoryError::Unmapped(SAVE_END + 1))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn halfword_and_word_accesses_remain_diagnostics() {
|
|
let mut bus = memory(&[0xeaff_fffe]);
|
|
for result in [
|
|
bus.read16(SAVE_START).map(|_| ()),
|
|
bus.read32(SAVE_START).map(|_| ()),
|
|
] {
|
|
assert_eq!(
|
|
result,
|
|
Err(MemoryError::UnsupportedCartridgeAccess {
|
|
address: SAVE_START,
|
|
operation: "SRAM non-byte read",
|
|
})
|
|
);
|
|
}
|
|
for result in [bus.write16(SAVE_START, 0), bus.write32(SAVE_START, 0)] {
|
|
assert_eq!(
|
|
result,
|
|
Err(MemoryError::UnsupportedCartridgeAccess {
|
|
address: SAVE_START,
|
|
operation: "SRAM non-byte write",
|
|
})
|
|
);
|
|
}
|
|
assert!(!bus.save_modified());
|
|
}
|
|
|
|
#[test]
|
|
fn cpu_byte_stores_and_loads_work_from_rom_code() {
|
|
let mut machine = program(SAVE_START + 9, 0xe5c0_0000); // STRB r0,[r0]
|
|
machine.step().unwrap();
|
|
assert_eq!(machine.memory().save_image().unwrap()[9], 0x09);
|
|
assert!(machine.memory().save_modified());
|
|
assert!(!machine.memory().save_write_pending());
|
|
|
|
let mut machine = program(SAVE_START + 9, 0xe5d0_1000); // LDRB r1,[r0]
|
|
machine.memory_mut().write8(SAVE_START + 9, 0x77).unwrap();
|
|
machine.step().unwrap();
|
|
assert_eq!(machine.cpu().registers()[1], 0x77);
|
|
}
|
|
|
|
#[test]
|
|
fn byte_swap_reads_the_old_value_then_stores_atomically() {
|
|
let mut machine = program(SAVE_START, 0xe140_1090); // SWPB r1,r0,[r0]
|
|
machine.memory_mut().write8(SAVE_START, 0x33).unwrap();
|
|
machine.step().unwrap();
|
|
assert_eq!(machine.cpu().registers()[1], 0x33);
|
|
assert_eq!(machine.memory().read8(SAVE_START).unwrap(), 0x00);
|
|
}
|
|
|
|
#[test]
|
|
fn failed_halfword_store_changes_nothing() {
|
|
let mut machine = program(SAVE_START, 0xe1c0_00b0); // STRH r0,[r0]
|
|
let before = machine.cpu().clone();
|
|
assert!(machine.step().is_err());
|
|
assert_eq!(machine.cpu(), &before);
|
|
assert!(!machine.memory().save_modified());
|
|
assert!(!machine.memory().save_write_pending());
|
|
}
|
|
|
|
#[test]
|
|
fn dma_into_sram_remains_a_diagnostic() {
|
|
let mut bus = memory(&[0xeaff_fffe]);
|
|
bus.write32(DMA_BASE + 0x24, 0x0300_0000).unwrap(); // DMA3 source
|
|
bus.write32(DMA_BASE + 0x28, SAVE_START).unwrap(); // DMA3 destination
|
|
bus.write32(DMA_BASE + 0x2c, 0x8000_0001).unwrap(); // One halfword, immediate
|
|
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
|
assert!(matches!(machine.step(), Err(MachineError::Dma(_))));
|
|
assert!(!machine.memory().save_modified());
|
|
}
|
|
|
|
#[test]
|
|
fn save_states_restore_sram_and_reject_other_save_devices() {
|
|
let mut machine = program(SAVE_START + 1, 0xe5c0_0000);
|
|
machine.step().unwrap();
|
|
let state = machine.save_state().unwrap();
|
|
machine.memory_mut().write8(SAVE_START + 1, 0x55).unwrap();
|
|
machine.load_state(&state).unwrap();
|
|
assert_eq!(machine.memory().read8(SAVE_START + 1).unwrap(), 0x01);
|
|
assert_eq!(machine.save_state().unwrap(), state);
|
|
|
|
let mut flash = program(SAVE_START + 1, 0xe5c0_0000);
|
|
flash.memory_mut().set_save_device(SaveDevice::Flash64);
|
|
assert_eq!(
|
|
flash.load_state(&state),
|
|
Err(SnapshotError::ConfigMismatch("cartridge setup"))
|
|
);
|
|
}
|