neser 1.2.0

NESER - Nintendo Emulation Systems Engine (Rust). Desktop and WebAssembly frontends.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
//! Game Boy Advance save-state serialization.
//!
//! Defines a versioned [`GbaSaveState`] struct that captures GBA emulator
//! state for snapshot save/restore.  Serialised as JSON (matching the NES
//! and GB save-state formats) via [`GbaSaveState::to_bytes`] /
//! [`GbaSaveState::from_bytes`].
//!
//! This is the initial scaffold for the GBA save-state pipeline.  It
//! captures the CPU plus bus memory regions (EWRAM, IWRAM, PRAM, VRAM, OAM,
//! SRAM mirror), bus-owned peripherals, PPU state, APU state, cartridge save
//! backend state, and a few simple bus scalars. Because every save-state
//! carries a `version` field, breaking changes to the captured shape will bump
//! [`GBA_SAVESTATE_VERSION`] and the loader will reject older states with
//! a clear [`GbaSaveStateError::IncompatibleVersion`] error.

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;

/// Current save-state format version for Game Boy Advance.
/// Increment this when making breaking changes to the state format.
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
    )
}

/// Serializable snapshot of the [`GbaBus`](crate::gba::GbaBus) memory
/// regions and a small number of associated scalar fields.
///
/// The BIOS image is intentionally **not** serialized: the GBA BIOS is
/// copyrighted firmware that the user supplies separately, and embedding
/// it in save-state files would both bloat them and risk leaking
/// firmware bytes when states are shared.  Only BIOS protection/latch
/// state is captured; the BIOS already loaded into the running emulator
/// is preserved across a load.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct BusMemoryState {
    /// 256 KB on-board work RAM (EWRAM).
    pub ewram: Vec<u8>,
    /// 32 KB on-chip work RAM (IWRAM).
    pub iwram: Vec<u8>,
    /// 1 KB Palette RAM.
    pub pram: Vec<u8>,
    /// 96 KB VRAM.
    pub vram: Vec<u8>,
    /// 1 KB OAM.
    pub oam: Vec<u8>,
    /// 64 KB cartridge SRAM region (battery-backed RAM).
    pub sram: Vec<u8>,
    /// Cartridge save backend, including SRAM/EEPROM/Flash data and volatile
    /// EEPROM/Flash command state.
    #[serde(default)]
    pub cart_save: SaveBackendState,
    /// Raw I/O backing store for registers not yet owned by a subsystem.
    pub io: IoRegisters,
    /// Interrupt controller (`IE`, `IF`, `IME`).
    pub ic: InterruptController,
    /// Timer bank including live counters and prescaler accumulators.
    pub timers: Timers,
    /// DMA controller including armed/live transfer state.
    pub dma: DmaController,
    /// Serial I/O controller including in-progress transfer countdown.
    pub sio: Sio,
    /// Keypad state including KEYCNT, pressed buttons, and IRQ edge latch.
    pub keypad: Keypad,
    /// PPU state including display registers, timing, and framebuffer.
    pub ppu: PpuState,
    /// APU state including channel, FIFO, mixer, and timing state.
    pub apu: ApuState,
    /// Whether external BIOS reads are currently locked out.
    pub bios_locked: bool,
    /// Last value driven on the bus (used to model open-bus reads).
    pub last_bus_value: u32,
    /// Last BIOS opcode fetched by the CPU prefetcher.
    #[serde(default)]
    pub bios_open_bus_value: u32,
    /// Whether the CPU execution context is currently in BIOS.
    #[serde(default)]
    pub executing_bios: bool,
    /// DMA internal data latch (separate from CPU open-bus).
    #[serde(default)]
    pub dma_latch: u32,
    /// Whether the DMA latch has been initialized by a DMA read.
    #[serde(default)]
    pub dma_latch_valid: bool,
    /// Remaining CPU-instruction window where unused reads see DMA bus value.
    #[serde(default)]
    pub dma_open_bus_instructions: u8,
    /// Last Game Pak opcode-prefetch value used by unused-area CPU reads.
    #[serde(default)]
    pub gamepak_prefetch_open_bus_value: u32,
    /// Whether `gamepak_prefetch_open_bus_value` has been initialized.
    #[serde(default)]
    pub gamepak_prefetch_open_bus_valid: bool,
    /// CPU-visible TM0 sample phase while software polls DISPSTAT H-Blank edges.
    #[serde(default)]
    pub hblank_edge_timer_sample_index: u8,
    /// Dynamic wait-state timing derived from WAITCNT.
    pub waitstates: Waitstates,
    /// Undocumented BIOS-written register at 0x04000410.
    pub undoc_0x410: u8,
    /// Pending HALTCNT halt request consumed by the CPU wrapper.
    pub halt_requested: bool,
    /// Global timer cycle phase used when newly enabling prescaled timers.
    #[serde(default)]
    pub timer_global_cycles: u32,
    /// Remaining cycles before a newly started SIO transfer begins shifting.
    #[serde(default)]
    pub sio_start_delay_cycles: u32,
    /// Remaining cycles before a newly asserted IRQ line reaches the CPU.
    #[serde(default)]
    pub irq_line_delay_cycles: u32,
    /// Enabled, pending IRQ sources after the previous CPU/peripheral step.
    #[serde(default)]
    pub irq_sources_were_asserted: u16,
}

/// Complete Game Boy Advance emulator state snapshot.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GbaSaveState {
    /// Version of the save-state format.
    pub version: u32,
    /// ARM7TDMI CPU state.
    pub cpu: Arm7tdmiState,
    /// Bus memory state (RAM regions and a few scalar fields).
    pub bus: BusMemoryState,
}

/// Errors that can occur when (de)serializing a GBA save-state.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GbaSaveStateError {
    /// The save-state format version is incompatible.
    IncompatibleVersion { expected: u32, found: u32 },
    /// Deserialization failed.
    DeserializationFailed(String),
    /// Serialization failed.
    SerializationFailed(String),
    /// Restoring the captured state into the running emulator failed
    /// (e.g. region size mismatch).
    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 {
    /// Serialize the save state to JSON-encoded UTF-8 bytes.
    pub fn to_bytes(&self) -> Result<Vec<u8>, GbaSaveStateError> {
        serde_json::to_vec(self).map_err(|e| GbaSaveStateError::SerializationFailed(e.to_string()))
    }

    /// Deserialize a save state from JSON-encoded UTF-8 bytes.
    ///
    /// Returns [`GbaSaveStateError::IncompatibleVersion`] when the
    /// deserialized state's `version` field is unsupported.
    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)
    }
}

// ── Convenience save / load on Gba ─────────────────────────────────────────

use super::gba::Gba;

impl Gba {
    /// Capture a full save-state snapshot of the current GBA state.
    pub fn save_state(&self) -> GbaSaveState {
        GbaSaveState {
            version: GBA_SAVESTATE_VERSION,
            cpu: self.capture_cpu_state(),
            bus: self.bus().capture_memory_state(),
        }
    }

    /// Restore the GBA state from a save-state snapshot.
    ///
    /// Returns [`GbaSaveStateError::IncompatibleVersion`] if the snapshot
    /// version is unsupported, or
    /// [`GbaSaveStateError::RestoreFailed`] if the captured state cannot
    /// be applied to the current bus (e.g. region size mismatch).
    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
    }

    // ── Version checks ─────────────────────────────────────────────────────

    #[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
        );
    }

    // ── Round-trip ─────────────────────────────────────────────────────────

    #[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"
        );
    }

    // ── Capture / restore preserves modified memory ────────────────────────

    #[test]
    fn test_save_state_captures_modified_memory() {
        let mut gba = make_gba();

        // Mutate EWRAM, IWRAM and SRAM through the bus.
        // EWRAM is at 0x0200_0000, IWRAM at 0x0300_0000, SRAM at 0x0E00_0000.
        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();

        // Overwrite memory after capture.
        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);

        // Restore — values should match the captured state.
        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);
    }

    // ── BIOS exclusion ─────────────────────────────────────────────────────

    #[test]
    fn test_save_state_does_not_embed_bios_bytes() {
        // Load a recognisable BIOS pattern.
        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");

        // The save-state must not embed BIOS bytes.  Search for a unique
        // recognisable run of the BIOS pattern (256 sequential bytes is
        // astronomically unlikely to appear in a clean state otherwise).
        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() {
        // Snapshot before BIOS is loaded.
        let mut gba = make_gba();
        let saved = gba.save_state();

        // Now load a BIOS image and overwrite EWRAM so the load actually
        // mutates the bus.
        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);

        // Restore from the pre-BIOS snapshot.  The loaded BIOS must
        // remain in place because the snapshot didn't capture it.
        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);
    }

    // ── Version mismatch ───────────────────────────────────────────────────

    #[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:?}"),
        }
    }

    // ── Invalid data ───────────────────────────────────────────────────────

    #[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();
        // Truncate EWRAM to an invalid size.
        save.bus.ewram.truncate(16);

        let result = gba.load_state(&save);
        assert!(matches!(result, Err(GbaSaveStateError::RestoreFailed(_))));
    }

    // ── Display impl ───────────────────────────────────────────────────────

    #[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"));
    }
}