feat(core): add whole-machine save states with F5/F9 window slots
Machine::save_state and load_state encode the CPU, every RAM, and every device through Snapshot impls into a versioned, checksummed format. Rejected loads leave the machine unchanged. The ROM window saves to and loads from one state file with F5 and F9.
This commit is contained in:
parent
be9c676476
commit
fd73788735
55 changed files with 5157 additions and 20 deletions
|
|
@ -1,6 +1,7 @@
|
|||
//! Optional Game Pak hardware. No host clock, filesystem, or ROM-byte mutation.
|
||||
|
||||
use crate::memory::MemoryError;
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
mod calendar;
|
||||
mod flash;
|
||||
|
|
@ -175,3 +176,199 @@ impl Gpio {
|
|||
(self.latch & self.direction) | (u8::from(self.rtc.output) << 1 & !self.direction)
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
[
|
||||
u8::from(self.gpio.is_some()),
|
||||
match device {
|
||||
SaveDevice::None => 0,
|
||||
SaveDevice::Flash64 => 1,
|
||||
SaveDevice::Flash128 => 2,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Cartridge {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self { gpio, flash } = self;
|
||||
writer.tag(b"CART");
|
||||
writer.raw(&self.setup_identity());
|
||||
if let Some(gpio) = gpio {
|
||||
gpio.save(writer);
|
||||
}
|
||||
if let Some(flash) = flash {
|
||||
flash.save(writer);
|
||||
}
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self { gpio, flash } = self;
|
||||
reader.tag(b"CART")?;
|
||||
|
||||
let hardware = reader.u8()?;
|
||||
if hardware > 1 {
|
||||
return Err(SnapshotError::Corrupt("cartridge hardware"));
|
||||
}
|
||||
if hardware != u8::from(gpio.is_some()) {
|
||||
return Err(SnapshotError::ConfigMismatch("cartridge hardware"));
|
||||
}
|
||||
|
||||
let save_device = match reader.u8()? {
|
||||
0 => SaveDevice::None,
|
||||
1 => SaveDevice::Flash64,
|
||||
2 => SaveDevice::Flash128,
|
||||
_ => 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"));
|
||||
}
|
||||
|
||||
if let Some(gpio) = gpio {
|
||||
gpio.load(reader)?;
|
||||
}
|
||||
if let Some(flash) = flash {
|
||||
flash.load(reader)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Gpio {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
latch,
|
||||
direction,
|
||||
readable,
|
||||
rtc,
|
||||
} = self;
|
||||
writer.tag(b"GPIO");
|
||||
writer.u8(*latch);
|
||||
writer.u8(*direction);
|
||||
writer.bool(*readable);
|
||||
rtc.save(writer);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
latch,
|
||||
direction,
|
||||
readable,
|
||||
rtc,
|
||||
} = self;
|
||||
reader.tag(b"GPIO")?;
|
||||
let saved_latch = reader.u8()?;
|
||||
let saved_direction = reader.u8()?;
|
||||
if saved_latch & !0x0f != 0 || saved_direction & !0x0f != 0 {
|
||||
return Err(SnapshotError::Corrupt("GPIO pins"));
|
||||
}
|
||||
let saved_readable = reader.bool()?;
|
||||
rtc.load(reader)?;
|
||||
*latch = saved_latch;
|
||||
*direction = saved_direction;
|
||||
*readable = saved_readable;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
fn encoded<T: Snapshot>(value: &T) -> Vec<u8> {
|
||||
let mut writer = Writer::new();
|
||||
value.save(&mut writer);
|
||||
writer.finish().unwrap()
|
||||
}
|
||||
|
||||
fn configured_cartridge(hardware: CartridgeHardware, device: SaveDevice) -> Cartridge {
|
||||
let mut cartridge = Cartridge::default();
|
||||
cartridge.set_hardware(hardware);
|
||||
cartridge.set_save_device(device);
|
||||
cartridge
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cartridge_snapshot_round_trips_and_checks_configuration() {
|
||||
let mut original = configured_cartridge(CartridgeHardware::Rtc, SaveDevice::Flash128);
|
||||
let gpio = original.gpio.as_mut().unwrap();
|
||||
gpio.latch = 0x0b;
|
||||
gpio.direction = 0x05;
|
||||
gpio.readable = true;
|
||||
let bytes = encoded(&original);
|
||||
|
||||
let mut restored = configured_cartridge(CartridgeHardware::Rtc, SaveDevice::Flash128);
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, original);
|
||||
|
||||
for end in 0..bytes.len() {
|
||||
let mut target = configured_cartridge(CartridgeHardware::Rtc, SaveDevice::Flash128);
|
||||
assert!(target.load(&mut Reader::new(&bytes[..end])).is_err());
|
||||
}
|
||||
|
||||
let mut no_rtc = configured_cartridge(CartridgeHardware::None, SaveDevice::Flash128);
|
||||
assert_eq!(
|
||||
no_rtc.load(&mut Reader::new(&bytes)),
|
||||
Err(SnapshotError::ConfigMismatch("cartridge hardware"))
|
||||
);
|
||||
let mut other_save = configured_cartridge(CartridgeHardware::Rtc, SaveDevice::Flash64);
|
||||
assert_eq!(
|
||||
other_save.load(&mut Reader::new(&bytes)),
|
||||
Err(SnapshotError::ConfigMismatch("save device"))
|
||||
);
|
||||
|
||||
let mut invalid_identity = bytes.clone();
|
||||
invalid_identity[4] = 0xff;
|
||||
assert_eq!(
|
||||
configured_cartridge(CartridgeHardware::Rtc, SaveDevice::Flash128)
|
||||
.load(&mut Reader::new(&invalid_identity)),
|
||||
Err(SnapshotError::Corrupt("cartridge hardware"))
|
||||
);
|
||||
|
||||
let empty = Cartridge::default();
|
||||
let empty_bytes = encoded(&empty);
|
||||
let mut empty_restored = Cartridge::default();
|
||||
empty_restored.load(&mut Reader::new(&empty_bytes)).unwrap();
|
||||
assert_eq!(empty_restored, empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gpio_snapshot_round_trips_every_gpio_latch() {
|
||||
let mut gpio = Gpio {
|
||||
latch: 0x0d,
|
||||
direction: 0x06,
|
||||
readable: true,
|
||||
..Gpio::default()
|
||||
};
|
||||
gpio.rtc.update(5, 2).unwrap();
|
||||
gpio.rtc.update(6, 2).unwrap();
|
||||
gpio.rtc.update(7, 2).unwrap();
|
||||
let bytes = encoded(&gpio);
|
||||
|
||||
let mut restored = Gpio::default();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, gpio);
|
||||
|
||||
for end in 0..bytes.len() {
|
||||
assert!(Gpio::default()
|
||||
.load(&mut Reader::new(&bytes[..end]))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
let mut corrupt = bytes;
|
||||
corrupt[4] = 0x10;
|
||||
assert_eq!(
|
||||
Gpio::default().load(&mut Reader::new(&corrupt)),
|
||||
Err(SnapshotError::Corrupt("GPIO pins"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue