use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use crate::gb::apu::Apu;
use crate::gb::cpu::{Registers, Sm83};
use crate::gb::input::joypad::Joypad;
use crate::gb::model::DmgModel;
use crate::gb::ppu::Ppu;
use crate::gb::timer::Timer;
pub const GB_SAVESTATE_VERSION: u32 = 1;
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum GbBusType {
Dmg,
Cgb,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Sm83State {
pub regs: Registers,
pub ime: bool,
pub halted: bool,
pub halt_bug: bool,
pub ime_pending: bool,
pub cycles: u64,
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct BusState {
pub bus_type: GbBusType,
pub ppu: Ppu,
#[serde_as(as = "[_; 0x2000]")]
pub wram: [u8; 0x2000],
#[serde_as(as = "[_; 0x7F]")]
pub hram: [u8; 0x7F],
pub timer: Timer,
pub joypad: Joypad,
pub apu: Apu,
pub if_reg: u8,
pub ie_reg: u8,
pub dma_active: bool,
pub dma_source: u8,
pub dma_position: u8,
pub dma_oam_blocked: bool,
pub boot_rom_active: Option<bool>,
pub sb: Option<u8>,
pub sc: Option<u8>,
pub serial_buf: Option<Vec<u8>>,
pub serial_bits_remaining: Option<u8>,
pub serial_master_clock: Option<bool>,
pub model: Option<DmgModel>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GbSaveState {
pub version: u32,
pub cpu: Sm83State,
pub bus: BusState,
pub cart_ram: Vec<u8>,
pub mbc_state: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GbSaveStateError {
IncompatibleVersion { expected: u32, found: u32 },
DeserializationFailed(String),
SerializationFailed(String),
}
impl std::fmt::Display for GbSaveStateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::IncompatibleVersion { expected, found } => write!(
f,
"incompatible save-state version (expected {expected}, found {found})"
),
Self::DeserializationFailed(msg) => write!(f, "deserialization failed: {msg}"),
Self::SerializationFailed(msg) => write!(f, "serialization failed: {msg}"),
}
}
}
impl std::error::Error for GbSaveStateError {}
impl GbSaveState {
pub fn to_bytes(&self) -> Result<Vec<u8>, GbSaveStateError> {
serde_json::to_vec(self).map_err(|e| GbSaveStateError::SerializationFailed(e.to_string()))
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, GbSaveStateError> {
let state: Self = serde_json::from_slice(bytes)
.map_err(|e| GbSaveStateError::DeserializationFailed(e.to_string()))?;
if state.version != GB_SAVESTATE_VERSION {
return Err(GbSaveStateError::IncompatibleVersion {
expected: GB_SAVESTATE_VERSION,
found: state.version,
});
}
Ok(state)
}
}
use super::Gb;
use crate::gb::bus::DmgBus;
impl Gb<DmgBus> {
pub fn save_state(&self) -> GbSaveState {
GbSaveState {
version: GB_SAVESTATE_VERSION,
cpu: self.cpu.capture_state(),
bus: self.cpu.bus.capture_bus_state(),
cart_ram: self.cpu.bus.cart_ram_snapshot(),
mbc_state: self.cpu.bus.mbc_state_snapshot(),
}
}
pub fn load_state(&mut self, state: &GbSaveState) -> Result<(), String> {
self.cpu.restore_state(&state.cpu);
self.cpu.bus.restore_bus_state(&state.bus)?;
self.cpu.bus.restore_cart_ram(&state.cart_ram);
self.cpu.bus.restore_mbc_state(&state.mbc_state);
Ok(())
}
}
impl<B: crate::gb::bus::GbBus> Sm83<B> {
pub fn capture_state(&self) -> Sm83State {
Sm83State {
regs: self.regs,
ime: self.ime,
halted: self.halted,
halt_bug: self.halt_bug,
ime_pending: self.ime_pending(),
cycles: self.cycles(),
}
}
pub fn restore_state(&mut self, state: &Sm83State) {
self.regs = state.regs;
self.ime = state.ime;
self.halted = state.halted;
self.halt_bug = state.halt_bug;
self.set_ime_pending(state.ime_pending);
self.set_cycles(state.cycles);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gb::bus::{CgbBus, DmgBus, GbBus};
use crate::gb::cartridge::load_cartridge;
use crate::gb::console::Gb;
use crate::gb::model::DmgModel;
fn minimal_rom() -> Vec<u8> {
let mut rom = vec![0u8; 0x8000];
rom[0x0147] = 0x00; rom[0x0148] = 0x00; rom[0x0149] = 0x00; let chk = rom[0x0134..=0x014C]
.iter()
.fold(0u8, |acc, &b| acc.wrapping_sub(b).wrapping_sub(1));
rom[0x014D] = chk;
rom
}
fn minimal_cgb_rom() -> Vec<u8> {
let mut rom = vec![0u8; 0x8000];
rom[0x0143] = 0xC0; rom[0x0147] = 0x00; rom[0x0148] = 0x00; rom[0x0149] = 0x00; let chk = rom[0x0134..=0x014C]
.iter()
.fold(0u8, |acc, &b| acc.wrapping_sub(b).wrapping_sub(1));
rom[0x014D] = chk;
rom
}
fn make_dmg() -> Gb<DmgBus> {
let cart = load_cartridge(&minimal_rom()).expect("valid ROM");
Gb::new(DmgBus::new(cart, DmgModel::DmgB))
}
fn make_cgb() -> Gb<CgbBus> {
let cart = load_cartridge(&minimal_cgb_rom()).expect("valid ROM");
let mut gb = Gb::new(CgbBus::new(cart));
gb.cpu.reset_registers_cgb();
gb
}
#[test]
fn test_gb_savestate_version_is_1() {
assert_eq!(GB_SAVESTATE_VERSION, 1);
}
#[test]
fn test_dmg_save_state_roundtrip() {
let mut gb = make_dmg();
for _ in 0..10 {
gb.step();
}
let save = gb.save_state();
let bytes = save.to_bytes().expect("serialization should succeed");
let loaded = GbSaveState::from_bytes(&bytes).expect("deserialization should succeed");
assert_eq!(loaded.version, GB_SAVESTATE_VERSION);
assert_eq!(loaded.cpu.regs, gb.cpu.regs);
assert_eq!(loaded.bus.bus_type, GbBusType::Dmg);
}
#[test]
fn test_cgb_save_state_roundtrip() {
let mut gb = make_cgb();
for _ in 0..10 {
gb.step();
}
let cpu_state = gb.cpu.capture_state();
let bus_state = gb.cpu.bus.capture_bus_state();
let save = GbSaveState {
version: GB_SAVESTATE_VERSION,
cpu: cpu_state,
bus: bus_state,
cart_ram: gb.cpu.bus.cart_ram_snapshot(),
mbc_state: gb.cpu.bus.mbc_state_snapshot(),
};
let bytes = save.to_bytes().expect("serialization should succeed");
let loaded = GbSaveState::from_bytes(&bytes).expect("deserialization should succeed");
assert_eq!(loaded.version, GB_SAVESTATE_VERSION);
assert_eq!(loaded.cpu.regs, gb.cpu.regs);
assert_eq!(loaded.bus.bus_type, GbBusType::Cgb);
}
#[test]
fn test_incompatible_version_error() {
let mut gb = make_dmg();
gb.step();
let mut save = gb.save_state();
save.version = 9999;
let bytes = serde_json::to_vec(&save).unwrap();
let result = GbSaveState::from_bytes(&bytes);
assert!(result.is_err());
match result {
Err(GbSaveStateError::IncompatibleVersion { expected, found }) => {
assert_eq!(expected, GB_SAVESTATE_VERSION);
assert_eq!(found, 9999);
}
_ => panic!("Expected IncompatibleVersion error"),
}
}
#[test]
fn test_invalid_json_returns_deserialization_error() {
let result = GbSaveState::from_bytes(b"not valid json");
assert!(matches!(
result,
Err(GbSaveStateError::DeserializationFailed(_))
));
}
#[test]
fn test_dmg_capture_restore_preserves_cpu_registers() {
let mut gb = make_dmg();
for _ in 0..10 {
gb.step();
}
let original_regs = gb.cpu.regs;
let state = gb.cpu.capture_state();
gb.cpu.regs.a = 0xFF;
gb.cpu.regs.pc = 0x1234;
gb.cpu.restore_state(&state);
assert_eq!(gb.cpu.regs, original_regs);
}
#[test]
fn test_dmg_capture_restore_preserves_bus_state() {
let mut gb = make_dmg();
for _ in 0..10 {
gb.step();
}
let bus_state = gb.cpu.bus.capture_bus_state();
gb.cpu.bus.write(0xC100, 0xAB);
assert_eq!(gb.cpu.bus.read(0xC100), 0xAB);
gb.cpu
.bus
.restore_bus_state(&bus_state)
.expect("restore should succeed");
assert_eq!(gb.cpu.bus.read(0xC100), 0x00);
}
#[test]
fn test_cgb_capture_restore_preserves_cpu_registers() {
let mut gb = make_cgb();
for _ in 0..10 {
gb.step();
}
let original_regs = gb.cpu.regs;
let state = gb.cpu.capture_state();
gb.cpu.regs.a = 0xFF;
gb.cpu.restore_state(&state);
assert_eq!(gb.cpu.regs, original_regs);
}
#[test]
fn test_dmg_restore_rejects_cgb_bus_state() {
let mut dmg = make_dmg();
let mut cgb = make_cgb();
for _ in 0..5 {
dmg.step();
cgb.step();
}
let cgb_state = cgb.cpu.bus.capture_bus_state();
let result = dmg.cpu.bus.restore_bus_state(&cgb_state);
assert!(result.is_err());
assert!(result.unwrap_err().contains("bus type mismatch"));
}
#[test]
fn test_cgb_restore_rejects_dmg_bus_state() {
let mut dmg = make_dmg();
let mut cgb = make_cgb();
for _ in 0..5 {
dmg.step();
cgb.step();
}
let dmg_state = dmg.cpu.bus.capture_bus_state();
let result = cgb.cpu.bus.restore_bus_state(&dmg_state);
assert!(result.is_err());
assert!(result.unwrap_err().contains("bus type mismatch"));
}
}