use serde::{Deserialize, Serialize};
use crate::gba::apu::ApuState;
use crate::gba::bus::{
DmaController, InterruptController, IoRegisters, Timers, Waitstates, sio::Sio,
};
use crate::gba::cartridge::SaveBackendState;
use crate::gba::cpu::Arm7tdmiState;
use crate::gba::input::Keypad;
use crate::gba::ppu::PpuState;
pub const GBA_SAVESTATE_VERSION: u32 = 7;
const GBA_LEGACY_SAVESTATE_VERSION_WITH_SINGLE_PENDING_APU_SAMPLE: u32 = 6;
fn is_supported_savestate_version(version: u32) -> bool {
matches!(
version,
GBA_SAVESTATE_VERSION | GBA_LEGACY_SAVESTATE_VERSION_WITH_SINGLE_PENDING_APU_SAMPLE
)
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct BusMemoryState {
pub ewram: Vec<u8>,
pub iwram: Vec<u8>,
pub pram: Vec<u8>,
pub vram: Vec<u8>,
pub oam: Vec<u8>,
pub sram: Vec<u8>,
#[serde(default)]
pub cart_save: SaveBackendState,
pub io: IoRegisters,
pub ic: InterruptController,
pub timers: Timers,
pub dma: DmaController,
pub sio: Sio,
pub keypad: Keypad,
pub ppu: PpuState,
pub apu: ApuState,
pub bios_locked: bool,
pub last_bus_value: u32,
#[serde(default)]
pub bios_open_bus_value: u32,
#[serde(default)]
pub executing_bios: bool,
#[serde(default)]
pub dma_latch: u32,
#[serde(default)]
pub dma_latch_valid: bool,
#[serde(default)]
pub dma_open_bus_instructions: u8,
#[serde(default)]
pub gamepak_prefetch_open_bus_value: u32,
#[serde(default)]
pub gamepak_prefetch_open_bus_valid: bool,
#[serde(default)]
pub hblank_edge_timer_sample_index: u8,
pub waitstates: Waitstates,
pub undoc_0x410: u8,
pub halt_requested: bool,
#[serde(default)]
pub timer_global_cycles: u32,
#[serde(default)]
pub sio_start_delay_cycles: u32,
#[serde(default)]
pub irq_line_delay_cycles: u32,
#[serde(default)]
pub irq_sources_were_asserted: u16,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GbaSaveState {
pub version: u32,
pub cpu: Arm7tdmiState,
pub bus: BusMemoryState,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GbaSaveStateError {
IncompatibleVersion { expected: u32, found: u32 },
DeserializationFailed(String),
SerializationFailed(String),
RestoreFailed(String),
}
impl std::fmt::Display for GbaSaveStateError {
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}"),
Self::RestoreFailed(msg) => write!(f, "restore failed: {msg}"),
}
}
}
impl std::error::Error for GbaSaveStateError {}
impl GbaSaveState {
pub fn to_bytes(&self) -> Result<Vec<u8>, GbaSaveStateError> {
serde_json::to_vec(self).map_err(|e| GbaSaveStateError::SerializationFailed(e.to_string()))
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, GbaSaveStateError> {
let state: Self = serde_json::from_slice(bytes)
.map_err(|e| GbaSaveStateError::DeserializationFailed(e.to_string()))?;
if !is_supported_savestate_version(state.version) {
return Err(GbaSaveStateError::IncompatibleVersion {
expected: GBA_SAVESTATE_VERSION,
found: state.version,
});
}
Ok(state)
}
}
use super::gba::Gba;
impl Gba {
pub fn save_state(&self) -> GbaSaveState {
GbaSaveState {
version: GBA_SAVESTATE_VERSION,
cpu: self.capture_cpu_state(),
bus: self.bus().capture_memory_state(),
}
}
pub fn load_state(&mut self, state: &GbaSaveState) -> Result<(), GbaSaveStateError> {
if !is_supported_savestate_version(state.version) {
return Err(GbaSaveStateError::IncompatibleVersion {
expected: GBA_SAVESTATE_VERSION,
found: state.version,
});
}
let current_input = self.bus().keypad.pressed_mask();
self.bus_mut()
.restore_memory_state(&state.bus)
.map_err(GbaSaveStateError::RestoreFailed)?;
let bus = self.bus_mut();
bus.keypad.set_pressed_mask(current_input, &mut bus.ic);
self.restore_cpu_state(&state.cpu);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gba::cartridge::header::{
COMPLEMENT_CHECK_OFFSET, FIXED_BYTE_OFFSET, FIXED_BYTE_VALUE, compute_complement_check,
};
use crate::gba::console::gba::Gba;
use crate::platform::app_context::AppContext;
use crate::platform::emulator::Emulator;
fn make_gba() -> Gba {
Gba::new(AppContext::default())
}
fn minimal_valid_gba_rom() -> Vec<u8> {
let mut rom = vec![0u8; 0xC0];
rom[FIXED_BYTE_OFFSET] = FIXED_BYTE_VALUE;
rom[COMPLEMENT_CHECK_OFFSET] = compute_complement_check(&rom);
rom
}
#[test]
fn test_gba_savestate_version_is_7() {
assert_eq!(GBA_SAVESTATE_VERSION, 7);
}
#[test]
fn test_version_6_save_state_without_pending_apu_samples_loads() {
let gba = make_gba();
let save = gba.save_state();
let mut json = serde_json::to_value(&save).expect("serialize save state");
json["version"] =
serde_json::json!(GBA_LEGACY_SAVESTATE_VERSION_WITH_SINGLE_PENDING_APU_SAMPLE);
let apu = json["bus"]["apu"]
.as_object_mut()
.expect("APU state should be an object");
apu.remove("pending_samples");
apu.insert(
"pending_sample".to_string(),
serde_json::json!([0.125, 0.5]),
);
let bytes = serde_json::to_vec(&json).expect("serialize legacy save state");
let loaded = GbaSaveState::from_bytes(&bytes).expect("legacy save state should load");
assert_eq!(
loaded.version,
GBA_LEGACY_SAVESTATE_VERSION_WITH_SINGLE_PENDING_APU_SAMPLE
);
}
#[test]
fn test_gba_save_state_roundtrip_through_bytes() {
let gba = make_gba();
let save = gba.save_state();
let bytes = save.to_bytes().expect("serialization should succeed");
let loaded = GbaSaveState::from_bytes(&bytes).expect("deserialization should succeed");
assert_eq!(loaded.version, GBA_SAVESTATE_VERSION);
assert_eq!(loaded.bus.ewram.len(), save.bus.ewram.len());
assert_eq!(loaded.bus.iwram.len(), save.bus.iwram.len());
assert_eq!(loaded.bus.pram.len(), save.bus.pram.len());
assert_eq!(loaded.bus.vram.len(), save.bus.vram.len());
assert_eq!(loaded.bus.oam.len(), save.bus.oam.len());
assert_eq!(loaded.bus.sram.len(), save.bus.sram.len());
assert_eq!(loaded.bus.ic.ie, save.bus.ic.ie);
assert_eq!(loaded.cpu.regs.r[15], save.cpu.regs.r[15]);
}
#[test]
fn test_load_state_preserves_current_physical_button_state() {
let mut gba = make_gba();
gba.set_button(0, 0, true);
let saved = gba.save_state();
gba.set_button(0, 0, false);
assert_eq!(
gba.get_joypad_button_states(0) & 0x01,
0,
"test setup must release A before restoring"
);
gba.load_state(&saved).expect("restore should succeed");
assert_eq!(
gba.get_joypad_button_states(0) & 0x01,
0,
"loading a save state must not resurrect a stale physical A press"
);
}
#[test]
fn test_load_state_preserves_current_physical_shoulder_button_state() {
let mut gba = make_gba();
gba.set_button(0, 8, true);
let saved = gba.save_state();
gba.set_button(0, 8, false);
assert_ne!(
gba.bus().keypad.read_keyinput() & (1 << 9),
0,
"test setup must release L before restoring"
);
gba.load_state(&saved).expect("restore should succeed");
assert_ne!(
gba.bus().keypad.read_keyinput() & (1 << 9),
0,
"loading a save state must not resurrect a stale physical L press"
);
}
#[test]
fn test_save_state_captures_modified_memory() {
let mut gba = make_gba();
use crate::gba::cpu::bus::Bus;
gba.bus_mut().write8(0x0200_0010, 0xAA);
gba.bus_mut().write8(0x0300_0020, 0xBB);
gba.bus_mut().write8(0x0E00_0030, 0xCC);
let saved = gba.save_state();
gba.bus_mut().write8(0x0200_0010, 0x11);
gba.bus_mut().write8(0x0300_0020, 0x22);
gba.bus_mut().write8(0x0E00_0030, 0x33);
assert_eq!(gba.bus_mut().read8(0x0200_0010), 0x11);
gba.load_state(&saved).expect("restore should succeed");
assert_eq!(gba.bus_mut().read8(0x0200_0010), 0xAA);
assert_eq!(gba.bus_mut().read8(0x0300_0020), 0xBB);
assert_eq!(gba.bus_mut().read8(0x0E00_0030), 0xCC);
}
#[test]
fn test_save_state_captures_and_restores_cpu_position() {
let mut gba = make_gba();
gba.load_rom(&minimal_valid_gba_rom(), "test.gba")
.expect("valid GBA ROM");
let saved = gba.save_state();
let saved_pc = gba.cpu_pc();
gba.run_tick_for_tests();
assert_ne!(gba.cpu_pc(), saved_pc, "test must dirty CPU PC after save");
gba.load_state(&saved).expect("restore should succeed");
assert_eq!(gba.cpu_pc(), saved_pc);
}
#[test]
fn test_save_state_does_not_embed_bios_bytes() {
let mut gba = make_gba();
let mut bios = vec![0u8; 16 * 1024];
for (i, b) in bios.iter_mut().enumerate() {
*b = (i & 0xFF) as u8;
}
gba.bus_mut().load_bios(&bios);
let bytes = gba.save_state().to_bytes().expect("serialization succeeds");
let needle: Vec<u8> = (0u8..=255).collect();
let found = bytes.windows(needle.len()).any(|w| w == needle.as_slice());
assert!(!found, "save-state must not embed BIOS firmware bytes");
}
#[test]
fn test_load_state_preserves_existing_bios() {
let mut gba = make_gba();
let saved = gba.save_state();
let mut bios = vec![0u8; 16 * 1024];
bios[0] = 0xDE;
bios[1] = 0xAD;
bios[2] = 0xBE;
bios[3] = 0xEF;
gba.bus_mut().load_bios(&bios);
use crate::gba::cpu::bus::Bus;
gba.load_state(&saved).expect("restore succeeds");
assert_eq!(gba.bus_mut().read8(0x0000_0000), 0xDE);
assert_eq!(gba.bus_mut().read8(0x0000_0001), 0xAD);
assert_eq!(gba.bus_mut().read8(0x0000_0002), 0xBE);
assert_eq!(gba.bus_mut().read8(0x0000_0003), 0xEF);
}
#[test]
fn test_incompatible_version_error_from_bytes() {
let gba = make_gba();
let mut save = gba.save_state();
save.version = 9999;
let bytes = serde_json::to_vec(&save).expect("raw serialization succeeds");
let result = GbaSaveState::from_bytes(&bytes);
match result {
Err(GbaSaveStateError::IncompatibleVersion { expected, found }) => {
assert_eq!(expected, GBA_SAVESTATE_VERSION);
assert_eq!(found, 9999);
}
other => panic!("Expected IncompatibleVersion error, got {other:?}"),
}
}
#[test]
fn test_incompatible_version_error_from_load_state() {
let mut gba = make_gba();
let mut save = gba.save_state();
save.version = 9999;
let result = gba.load_state(&save);
match result {
Err(GbaSaveStateError::IncompatibleVersion { expected, found }) => {
assert_eq!(expected, GBA_SAVESTATE_VERSION);
assert_eq!(found, 9999);
}
other => panic!("Expected IncompatibleVersion error, got {other:?}"),
}
}
#[test]
fn test_invalid_json_returns_deserialization_error() {
let result = GbaSaveState::from_bytes(b"not valid json");
assert!(matches!(
result,
Err(GbaSaveStateError::DeserializationFailed(_))
));
}
#[test]
fn test_region_size_mismatch_returns_restore_error() {
let mut gba = make_gba();
let mut save = gba.save_state();
save.bus.ewram.truncate(16);
let result = gba.load_state(&save);
assert!(matches!(result, Err(GbaSaveStateError::RestoreFailed(_))));
}
#[test]
fn test_error_display_formatting() {
let e = GbaSaveStateError::IncompatibleVersion {
expected: 1,
found: 2,
};
assert!(format!("{e}").contains("incompatible save-state version"));
let e = GbaSaveStateError::DeserializationFailed("oops".into());
assert!(format!("{e}").contains("deserialization failed"));
let e = GbaSaveStateError::SerializationFailed("oops".into());
assert!(format!("{e}").contains("serialization failed"));
let e = GbaSaveStateError::RestoreFailed("oops".into());
assert!(format!("{e}").contains("restore failed"));
}
}