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:
parent
20176716d9
commit
32eaffa0fe
15 changed files with 405 additions and 30 deletions
|
|
@ -6,6 +6,7 @@ use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
|||
mod calendar;
|
||||
mod flash;
|
||||
mod rtc;
|
||||
mod sram;
|
||||
pub use calendar::{RtcDateTime, RtcError};
|
||||
use flash::Flash;
|
||||
pub(crate) use flash::SaveMutation;
|
||||
|
|
@ -13,6 +14,7 @@ pub use flash::{
|
|||
SaveDevice, SaveError, FLASH_ERASE_CYCLES, FLASH_PROGRAM_CYCLES, SAVE_END, SAVE_START,
|
||||
};
|
||||
use rtc::Rtc;
|
||||
use sram::Sram;
|
||||
|
||||
pub const GPIO_DATA: u32 = 0x0800_00c4;
|
||||
pub const GPIO_DIRECTION: u32 = 0x0800_00c6;
|
||||
|
|
@ -31,6 +33,7 @@ pub enum CartridgeHardware {
|
|||
pub(crate) struct Cartridge {
|
||||
gpio: Option<Gpio>,
|
||||
flash: Option<Flash>,
|
||||
sram: Option<Sram>,
|
||||
}
|
||||
|
||||
impl Cartridge {
|
||||
|
|
@ -40,14 +43,23 @@ impl Cartridge {
|
|||
|
||||
pub(crate) fn set_save_device(&mut self, device: SaveDevice) {
|
||||
self.flash = Flash::new(device);
|
||||
self.sram = (device == SaveDevice::Sram).then(Sram::default);
|
||||
}
|
||||
|
||||
pub(crate) fn save_device(&self) -> SaveDevice {
|
||||
if self.sram.is_some() {
|
||||
return SaveDevice::Sram;
|
||||
}
|
||||
self.flash.map_or(SaveDevice::None, Flash::device)
|
||||
}
|
||||
|
||||
pub(crate) fn save_mapped(&self, address: u32) -> bool {
|
||||
self.flash.is_some() && (SAVE_START..=SAVE_END).contains(&address)
|
||||
(self.flash.is_some() || self.sram.is_some()) && (SAVE_START..=SAVE_END).contains(&address)
|
||||
}
|
||||
|
||||
/// Flash reads are restricted to work-RAM code; SRAM has no such limit.
|
||||
pub(crate) fn flash_mapped(&self, address: u32) -> bool {
|
||||
self.flash.is_some() && self.save_mapped(address)
|
||||
}
|
||||
|
||||
pub(crate) fn advance(&mut self, cycles: u32) {
|
||||
|
|
@ -57,14 +69,20 @@ impl Cartridge {
|
|||
}
|
||||
|
||||
pub(crate) fn save_unfinished(&self) -> bool {
|
||||
self.flash.is_some_and(Flash::unfinished)
|
||||
self.flash.is_some_and(Flash::unfinished) || self.sram.is_some_and(Sram::unfinished)
|
||||
}
|
||||
|
||||
pub(crate) fn take_save_mutation(&mut self) -> Option<SaveMutation> {
|
||||
if let Some(sram) = self.sram.as_mut() {
|
||||
return sram.take_mutation();
|
||||
}
|
||||
self.flash.as_mut().and_then(Flash::take_mutation)
|
||||
}
|
||||
|
||||
pub(crate) fn read_save8(&self, address: u32, image: &[u8]) -> Result<u8, MemoryError> {
|
||||
if let Some(sram) = self.sram {
|
||||
return Ok(sram.read(address, image));
|
||||
}
|
||||
self.flash.expect("mapped save device").read(address, image)
|
||||
}
|
||||
|
||||
|
|
@ -125,9 +143,16 @@ impl Cartridge {
|
|||
if bytes.len() != 1 {
|
||||
return Err(MemoryError::UnsupportedCartridgeAccess {
|
||||
address,
|
||||
operation: "Flash non-byte write",
|
||||
operation: if self.sram.is_some() {
|
||||
"SRAM non-byte write"
|
||||
} else {
|
||||
"Flash non-byte write"
|
||||
},
|
||||
});
|
||||
}
|
||||
if let Some(sram) = self.sram.as_mut() {
|
||||
return sram.write(address, bytes[0]);
|
||||
}
|
||||
return self.flash.as_mut().unwrap().write(address, bytes[0], image);
|
||||
}
|
||||
let fail = |at, value, operation| MemoryError::UnsupportedIo {
|
||||
|
|
@ -180,16 +205,14 @@ impl Gpio {
|
|||
impl Cartridge {
|
||||
/// Host configuration, not state: cartridge hardware then save device, one byte each.
|
||||
pub(crate) fn setup_identity(&self) -> [u8; 2] {
|
||||
let device = self
|
||||
.flash
|
||||
.as_ref()
|
||||
.map_or(SaveDevice::None, |flash| flash.device());
|
||||
let device = self.save_device();
|
||||
[
|
||||
u8::from(self.gpio.is_some()),
|
||||
match device {
|
||||
SaveDevice::None => 0,
|
||||
SaveDevice::Flash64 => 1,
|
||||
SaveDevice::Flash128 => 2,
|
||||
SaveDevice::Sram => 3,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
@ -197,7 +220,7 @@ impl Cartridge {
|
|||
|
||||
impl Snapshot for Cartridge {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self { gpio, flash } = self;
|
||||
let Self { gpio, flash, sram } = self;
|
||||
writer.tag(b"CART");
|
||||
writer.raw(&self.setup_identity());
|
||||
if let Some(gpio) = gpio {
|
||||
|
|
@ -206,10 +229,14 @@ impl Snapshot for Cartridge {
|
|||
if let Some(flash) = flash {
|
||||
flash.save(writer);
|
||||
}
|
||||
if let Some(sram) = sram {
|
||||
sram.save(writer);
|
||||
}
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self { gpio, flash } = self;
|
||||
let target_device = self.save_device();
|
||||
let Self { gpio, flash, sram } = self;
|
||||
reader.tag(b"CART")?;
|
||||
|
||||
let hardware = reader.u8()?;
|
||||
|
|
@ -224,11 +251,9 @@ impl Snapshot for Cartridge {
|
|||
0 => SaveDevice::None,
|
||||
1 => SaveDevice::Flash64,
|
||||
2 => SaveDevice::Flash128,
|
||||
3 => SaveDevice::Sram,
|
||||
_ => return Err(SnapshotError::Corrupt("save device")),
|
||||
};
|
||||
let target_device = flash
|
||||
.as_ref()
|
||||
.map_or(SaveDevice::None, |flash| flash.device());
|
||||
if save_device != target_device {
|
||||
return Err(SnapshotError::ConfigMismatch("save device"));
|
||||
}
|
||||
|
|
@ -239,6 +264,9 @@ impl Snapshot for Cartridge {
|
|||
if let Some(flash) = flash {
|
||||
flash.load(reader)?;
|
||||
}
|
||||
if let Some(sram) = sram {
|
||||
sram.load(reader)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ pub enum SaveDevice {
|
|||
Flash64,
|
||||
/// Macronix ID 09C2h, two 64 KiB banks.
|
||||
Flash128,
|
||||
/// 32 KiB battery-backed SRAM, byte addressed and mirrored through the save window.
|
||||
Sram,
|
||||
}
|
||||
|
||||
impl SaveDevice {
|
||||
|
|
@ -25,6 +27,7 @@ impl SaveDevice {
|
|||
Self::None => 0,
|
||||
Self::Flash64 => 64 * 1024,
|
||||
Self::Flash128 => 128 * 1024,
|
||||
Self::Sram => 32 * 1024,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -84,7 +87,7 @@ enum Phase {
|
|||
|
||||
impl Flash {
|
||||
pub(super) fn new(device: SaveDevice) -> Option<Self> {
|
||||
(device != SaveDevice::None).then_some(Self {
|
||||
matches!(device, SaveDevice::Flash64 | SaveDevice::Flash128).then_some(Self {
|
||||
device,
|
||||
phase: Phase::Ready,
|
||||
identify: false,
|
||||
|
|
@ -486,6 +489,7 @@ impl Snapshot for Flash {
|
|||
SaveDevice::None => 0,
|
||||
SaveDevice::Flash64 => 1,
|
||||
SaveDevice::Flash128 => 2,
|
||||
SaveDevice::Sram => unreachable!("SRAM has no Flash state"),
|
||||
});
|
||||
phase.save(writer);
|
||||
writer.bool(*identify);
|
||||
|
|
|
|||
128
crates/gba-core/src/cartridge/sram.rs
Normal file
128
crates/gba-core/src/cartridge/sram.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
//! Battery-backed SRAM: a 32 KiB byte-wide array with no command protocol.
|
||||
//! A CPU byte store is staged as a one-byte mutation and committed when its step completes.
|
||||
//! Halfword and word accesses remain diagnostic until their 8-bit bus behavior is verified.
|
||||
|
||||
use super::{SaveDevice, SaveMutation, SAVE_START};
|
||||
use crate::memory::MemoryError;
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
/// The 32 KiB array repeats through the 64 KiB save window.
|
||||
const MIRROR_MASK: u32 = 0x7fff;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct Sram {
|
||||
pending: Option<SaveMutation>,
|
||||
}
|
||||
|
||||
impl Sram {
|
||||
fn index(address: u32) -> usize {
|
||||
((address - SAVE_START) & MIRROR_MASK) as usize
|
||||
}
|
||||
|
||||
pub(super) fn read(self, address: u32, image: &[u8]) -> u8 {
|
||||
let index = Self::index(address);
|
||||
match self.pending {
|
||||
Some(mutation) if (mutation.start..mutation.end).contains(&index) => mutation.value,
|
||||
_ => image[index],
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn write(&mut self, address: u32, value: u8) -> Result<(), MemoryError> {
|
||||
if self.pending.is_some() {
|
||||
// One CPU step performs at most one byte store; DMA cannot write bytes.
|
||||
return Err(MemoryError::UnsupportedCartridgeAccess {
|
||||
address,
|
||||
operation: "second SRAM write before commit",
|
||||
});
|
||||
}
|
||||
let start = Self::index(address);
|
||||
self.pending = Some(SaveMutation {
|
||||
start,
|
||||
end: start + 1,
|
||||
value,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn unfinished(self) -> bool {
|
||||
self.pending.is_some()
|
||||
}
|
||||
|
||||
pub(super) fn take_mutation(&mut self) -> Option<SaveMutation> {
|
||||
self.pending.take()
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Sram {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self { pending } = self;
|
||||
writer.tag(b"SRAM");
|
||||
writer.option(pending.as_ref(), |pending, writer| pending.save(writer));
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self { pending } = self;
|
||||
reader.tag(b"SRAM")?;
|
||||
let decoded = reader.option(|reader| {
|
||||
let mut mutation = SaveMutation {
|
||||
start: 0,
|
||||
end: 0,
|
||||
value: 0,
|
||||
};
|
||||
mutation.load(reader)?;
|
||||
Ok(mutation)
|
||||
})?;
|
||||
if decoded.is_some_and(|m| m.end != m.start + 1 || m.end > SaveDevice::Sram.capacity()) {
|
||||
return Err(SnapshotError::Corrupt("SRAM mutation range"));
|
||||
}
|
||||
*pending = decoded;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn writes_stage_one_mirrored_byte_and_reads_see_it_before_commit() {
|
||||
let image = vec![0xff; SaveDevice::Sram.capacity()];
|
||||
let mut sram = Sram::default();
|
||||
sram.write(SAVE_START + 0x8001, 0x42).unwrap(); // Mirror of offset 1.
|
||||
assert_eq!(sram.read(SAVE_START + 1, &image), 0x42);
|
||||
assert_eq!(sram.read(SAVE_START + 0x8001, &image), 0x42);
|
||||
assert_eq!(sram.read(SAVE_START, &image), 0xff);
|
||||
assert!(sram.unfinished());
|
||||
assert!(sram.write(SAVE_START, 0).is_err());
|
||||
let mutation = sram.take_mutation().unwrap();
|
||||
assert_eq!((mutation.start, mutation.end, mutation.value), (1, 2, 0x42));
|
||||
assert!(!sram.unfinished());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_round_trips_and_rejects_out_of_range_mutations() {
|
||||
let mut sram = Sram::default();
|
||||
sram.write(SAVE_START + 0x7fff, 7).unwrap();
|
||||
let mut writer = Writer::new();
|
||||
sram.save(&mut writer);
|
||||
let bytes = writer.finish().unwrap();
|
||||
let mut restored = Sram::default();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, sram);
|
||||
|
||||
let mut writer = Writer::new();
|
||||
Sram {
|
||||
pending: Some(SaveMutation {
|
||||
start: 0x8000,
|
||||
end: 0x8001,
|
||||
value: 0,
|
||||
}),
|
||||
}
|
||||
.save(&mut writer);
|
||||
let bytes = writer.finish().unwrap();
|
||||
assert_eq!(
|
||||
Sram::default().load(&mut Reader::new(&bytes)),
|
||||
Err(SnapshotError::Corrupt("SRAM mutation range"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -864,7 +864,7 @@ impl Memory {
|
|||
|
||||
// Multi-byte reads use this helper to avoid charging each byte as a bus access.
|
||||
fn read_byte(&self, address: u32) -> Result<u8, MemoryError> {
|
||||
if self.cartridge_state().save_mapped(address)
|
||||
if self.cartridge_state().flash_mapped(address)
|
||||
&& self
|
||||
.cpu_access
|
||||
.is_some_and(|access| !matches!(access.pc >> 24, 0x02 | 0x03))
|
||||
|
|
@ -984,6 +984,8 @@ impl Memory {
|
|||
self.cartridge_step.set(Some(cartridge));
|
||||
} else {
|
||||
self.cartridge = cartridge;
|
||||
// Outside a machine step, an SRAM store has no step boundary to commit at.
|
||||
self.commit_save_mutation();
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -1143,7 +1145,11 @@ impl Memory {
|
|||
if N != 1 {
|
||||
return Err(MemoryError::UnsupportedCartridgeAccess {
|
||||
address,
|
||||
operation: "Flash non-byte write",
|
||||
operation: if self.cartridge_state().flash_mapped(address) {
|
||||
"Flash non-byte write"
|
||||
} else {
|
||||
"SRAM non-byte write"
|
||||
},
|
||||
});
|
||||
}
|
||||
Ok(0)
|
||||
|
|
@ -1246,10 +1252,15 @@ impl Memory {
|
|||
}
|
||||
|
||||
fn validate_save_read_width(&self, address: u32) -> Result<(), MemoryError> {
|
||||
if self.cartridge_state().save_mapped(address) {
|
||||
let cartridge = self.cartridge_state();
|
||||
if cartridge.save_mapped(address) {
|
||||
return Err(MemoryError::UnsupportedCartridgeAccess {
|
||||
address,
|
||||
operation: "Flash non-byte read",
|
||||
operation: if cartridge.flash_mapped(address) {
|
||||
"Flash non-byte read"
|
||||
} else {
|
||||
"SRAM non-byte read"
|
||||
},
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
|||
159
crates/gba-core/tests/sram.rs
Normal file
159
crates/gba-core/tests/sram.rs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
//! 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"))
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue