Skip to main content

rustynes_core/
bus_snapshot.rs

1//! Save-state encoding for the [`crate::bus::SystemBus`] (the "BUS"
2//! tagged section) — owns CPU RAM, controllers, the unified DMA engine's
3//! bookkeeping, the two data-bus latches, and the cumulative cycle counter.
4//!
5//! The chip sub-states (CPU / PPU / APU / mapper) are emitted as their own
6//! tagged sections by [`crate::bus::SystemBus::snapshot`].
7//!
8//! # Version 2 (v2.9.8, ADR 0042)
9//!
10//! Version 1 grew by appending fields to the tail and decoding each
11//! missing tail as its default, so a blob from any release since v0.9 still
12//! loaded. Version 2 drops that: every field is required, a short body is a
13//! truncation error, and trailing bytes are rejected, because a version-2
14//! reader is never handed anything but a version-2 body (the section version
15//! check in `SystemBus::restore` refuses version 1 first). It also drops
16//! the fields that no longer carry state: the NMI edge detector's
17//! `last_nmi_level` / `nmi_edge_latch` (they fed only the removed `poll_nmi`),
18//! the OAM-DMA owed-cycle counter and byte index (the unified engine's
19//! length is emergent), and `dma_mc_consumed` (structurally zero since
20//! v2.0.0, and decoded as zero regardless since v2.7.0).
21//!
22//! # Version 3 (v3.1.0)
23//!
24//! Appends, after the internal data bus:
25//!
26//! - the DMC load-DMA write-refusal latch (`dmc_load_write_delayed`, one
27//!   byte). It outlives an instruction, because the refusing write is the
28//!   last cycle of a store and the latch is consumed by the next opcode
29//!   fetch, so a snapshot at that boundary without it would restore a
30//!   three-cycle load where the machine was owed a four-cycle one;
31//! - the CPU overclock's stock-rate position (`overclock_phase`, one byte,
32//!   and `apu_cycle`, `u64`): under the overclock the APU and the mappers'
33//!   cycle hooks advance on only some CPU cycles, and run-ahead restores in
34//!   the middle of that pattern.
35//!
36//! Version 2 is refused rather than read with a default, as the version-2
37//! rules above require.
38
39use crate::bus::SystemBus;
40use crate::controller::Controller;
41use crate::input_device::{
42    FamilyKeyboardState, InputDevice, SnesMouseState, VausState, ZapperState,
43};
44use crate::save_state::{BinReader, BinWriter, SnapshotError};
45use alloc::format;
46use alloc::vec::Vec;
47
48/// Schema version for the BUS section payload.
49///
50/// 3 since v3.1.0, 2 since v2.9.8 (ADR 0042): see the module docs for what
51/// changed. An older section is refused with [`SnapshotError::VersionMismatch`].
52pub const BUS_SECTION_VERSION: u8 = 3;
53
54/// Largest encoding of one port's expansion device in the BUS section.
55///
56/// The tag byte plus that device's fields. The Family BASIC / Subor keyboards
57/// and the SNES mouse are the largest, at 14 bytes; an unplugged port is the
58/// 1-byte tag alone.
59pub const EXPANSION_DEVICE_MAX_LEN: usize = 14;
60
61/// How much a snapshot can grow after it is first measured.
62///
63/// Devices attach lazily: a port reads "unplugged" (1 byte) until the host
64/// plugs a device in, and the largest device then takes
65/// [`EXPANSION_DEVICE_MAX_LEN`]. Two ports, so twice the difference.
66///
67/// v2.8.0 (libretro audit §2.1): the libretro core adds this to the
68/// `retro_serialize_size` it reports at load, because the frontend sizes its
69/// save-state, rewind and run-ahead buffers from that one answer, and a Zapper
70/// plugged in mid-game otherwise made every later save fail. The padding it
71/// leaves is zeroed and read back as padding (`save_state::SectionIter`).
72pub const SAVE_STATE_DEVICE_HEADROOM: usize = 2 * (EXPANSION_DEVICE_MAX_LEN - 1);
73
74/// Encode the bus's own state (RAM, controllers, DMA, data-bus latches,
75/// cycle). The order is the on-wire layout [`decode_bus`] reads back.
76pub fn encode_bus(bus: &SystemBus) -> Vec<u8> {
77    let mut w = BinWriter::with_capacity(0x900);
78    // Cumulative cycle counter.
79    w.u64(bus.cycle());
80    // CPU RAM (2 KiB).
81    w.bytes(bus.ram_bytes());
82    // Standard controllers (players 1 and 2).
83    for c in bus.controllers_ref() {
84        encode_controller(&mut w, *c);
85    }
86    let s = bus.bus_misc_state();
87    // A `$4014` write awaiting its first DMA cycle: page, then presence.
88    w.u8(s.dma_pending.unwrap_or(0));
89    w.u8(u8::from(s.dma_pending.is_some()));
90    // The OAM DMA's scratch byte, source page and parked CPU address.
91    w.u8(s.dma_byte);
92    w.u8(s.dma_page);
93    w.u16(s.dma_halt_addr);
94    // The external data bus (open bus) and the last CPU read address.
95    w.u8(s.open_bus);
96    w.u16(s.last_read_addr);
97    w.u8(u8::from(s.in_dmc_dma));
98    w.u16(s.deferred_dma_replay_addr);
99    // The deferred controller-strobe write (Session-24 / Phase 3).
100    w.u8(s.controller_write_pending);
101    w.u8(s.controller_write_value);
102    // Four Score: the flag, players 3 and 4, and the per-port read index and
103    // signature shift register.
104    w.u8(u8::from(s.four_score));
105    for c in bus.controllers34_ref() {
106        encode_controller(&mut w, *c);
107    }
108    w.u8(s.four_score_idx[0]);
109    w.u8(s.four_score_idx[1]);
110    w.u8(s.four_score_sig[0]);
111    w.u8(s.four_score_sig[1]);
112    // The unified DMA engine: the DMC halt latch and the OAM engine's state.
113    w.u8(u8::from(s.dmc_halt));
114    w.u8(u8::from(s.uni_oam_active));
115    w.u8(u8::from(s.uni_oam_halt));
116    w.u8(u8::from(s.uni_oam_aligned));
117    w.u16(s.uni_oam_addr);
118    // `ppu_clock`, the PPU's progress in master clocks. It MUST travel with
119    // `Cpu::master_clock` (CPU section): restoring one without the other
120    // desynchronises `run_ppu_to`, and `check_restored_clocks` refuses a pair
121    // too far apart to be real.
122    w.u64(s.ppu_clock);
123    // A tag byte per port (0 = none, 1 = Zapper, 2 = Vaus, 3 = Power Pad,
124    // 4 = SNES mouse, 5 = Family BASIC keyboard, 6 = Family Trainer,
125    // 7 = Subor keyboard, 8 = Konami Hyper Shot, 9 = Bandai Hyper Shot)
126    // followed by that device's fields.
127    for port in 0..2 {
128        encode_expansion_device(&mut w, bus.expansion_device(port).as_ref());
129    }
130    // The per-game nametable mirroring override (0 = none).
131    w.u8(encode_mirroring_override(bus.mirroring_override()));
132    // The controller-port CLK run state (v2.6.5). Both halves outlive an
133    // instruction: a `$4016` read is the last cycle of `LDA $4016`, so a
134    // snapshot at that boundary has a shift owed and a run open, and
135    // restoring without them makes the next read return a bit the timeline
136    // already delivered.
137    for c in bus.controllers_ref() {
138        w.bool(c.pending_shift);
139    }
140    for c in bus.controllers34_ref() {
141        w.bool(c.pending_shift);
142    }
143    for port in 0..2 {
144        w.u64(bus.port_read_cycle(port));
145    }
146    // The Four Score chain's own owed edge, which clocks with the pads.
147    for f in bus.four_score_pending() {
148        w.bool(f);
149    }
150    // The 2A03's INTERNAL data bus (v2.8.0): a separate latch from
151    // `open_bus`, because a DMC DMA fetch drives only the external bus, and a
152    // `$4015` read takes bit 5 from this one.
153    w.u8(s.internal_data_bus);
154    // v3.1.0 (version 3): the DMC load-DMA write-refusal latch.
155    w.u8(u8::from(s.dmc_load_write_delayed));
156    // v3.1.0 (version 3): the CPU overclock's stock-rate position.
157    w.u8(s.overclock_phase);
158    w.u64(s.apu_cycle);
159    w.into_vec()
160}
161
162/// Encode the optional mirroring override as a tag byte (0 = none).
163const fn encode_mirroring_override(m: Option<rustynes_mappers::Mirroring>) -> u8 {
164    use rustynes_mappers::Mirroring;
165    match m {
166        None => 0,
167        Some(Mirroring::Horizontal) => 1,
168        Some(Mirroring::Vertical) => 2,
169        Some(Mirroring::SingleScreenA) => 3,
170        Some(Mirroring::SingleScreenB) => 4,
171        Some(Mirroring::FourScreen) => 5,
172        Some(Mirroring::MapperControlled) => 6,
173    }
174}
175
176/// Decode a mirroring-override tag byte (inverse of [`encode_mirroring_override`]).
177const fn decode_mirroring_override(tag: u8) -> Option<rustynes_mappers::Mirroring> {
178    use rustynes_mappers::Mirroring;
179    match tag {
180        1 => Some(Mirroring::Horizontal),
181        2 => Some(Mirroring::Vertical),
182        3 => Some(Mirroring::SingleScreenA),
183        4 => Some(Mirroring::SingleScreenB),
184        5 => Some(Mirroring::FourScreen),
185        6 => Some(Mirroring::MapperControlled),
186        _ => None,
187    }
188}
189
190/// Encode one port's optional overlay device (tag byte + fields).
191fn encode_expansion_device(w: &mut BinWriter, device: Option<&InputDevice>) {
192    match device {
193        None => w.u8(0),
194        Some(InputDevice::Zapper(z)) => {
195            w.u8(1);
196            w.u16(z.x_raw());
197            w.u16(z.y_raw());
198            w.bool(z.trigger_raw());
199            w.bool(z.light_seen_raw());
200        }
201        Some(InputDevice::Vaus(v)) => {
202            w.u8(2);
203            w.u8(v.position_raw());
204            w.bool(v.fire_raw());
205            w.u8(v.shift_raw());
206            w.bool(v.strobe_raw());
207        }
208        Some(InputDevice::PowerPad(p)) => {
209            w.u8(3);
210            w.u16(p.buttons_raw());
211            w.u8(p.shift_l_raw());
212            w.u8(p.shift_h_raw());
213            w.bool(p.strobe_raw());
214        }
215        Some(InputDevice::SnesMouse(m)) => {
216            w.u8(4);
217            w.i16(m.dx_raw());
218            w.i16(m.dy_raw());
219            w.bool(m.left_raw());
220            w.bool(m.right_raw());
221            w.u8(m.sensitivity_raw());
222            w.u32(m.shift_raw());
223            w.u8(m.read_count_raw());
224            w.bool(m.strobe_raw());
225        }
226        Some(InputDevice::FamilyKeyboard(k)) => {
227            w.u8(5);
228            w.bytes(&k.keys_raw());
229            w.u8(k.row_raw());
230            w.bool(k.column_raw());
231            w.bool(k.enabled_raw());
232            w.bool(k.clock_raw());
233        }
234        // v1.3.0 Workstream F1 — the Family Trainer reuses PowerPadState and
235        // the Subor keyboard reuses FamilyKeyboardState, but get distinct tags
236        // so a restore reattaches the SAME device variant the user selected.
237        Some(InputDevice::FamilyTrainer(p)) => {
238            w.u8(6);
239            w.u16(p.buttons_raw());
240            w.u8(p.shift_l_raw());
241            w.u8(p.shift_h_raw());
242            w.bool(p.strobe_raw());
243        }
244        Some(InputDevice::SuborKeyboard(k)) => {
245            w.u8(7);
246            w.bytes(&k.keys_raw());
247            w.u8(k.row_raw());
248            w.bool(k.column_raw());
249            w.bool(k.enabled_raw());
250            w.bool(k.clock_raw());
251        }
252        Some(InputDevice::KonamiHyperShot(h)) => {
253            w.u8(8);
254            w.u8(h.buttons_raw());
255            w.bool(h.p1_enabled_raw());
256            w.bool(h.p2_enabled_raw());
257        }
258        Some(InputDevice::BandaiHyperShot(b)) => {
259            w.u8(9);
260            w.u8(b.sensors_raw());
261            w.bool(b.select_raw());
262        }
263    }
264}
265
266/// Decode one port's optional overlay device (tag 0 is an empty port).
267// A flat one-arm-per-device-tag dispatch decoder; the length is inherent to the
268// device count, not a sign of tangled logic.
269#[allow(clippy::too_many_lines)]
270fn decode_expansion_device(r: &mut BinReader<'_>) -> Result<Option<InputDevice>, SnapshotError> {
271    Ok(match r.u8()? {
272        0 => None,
273        1 => {
274            let x = r.u16()?;
275            let y = r.u16()?;
276            let trigger = r.bool()?;
277            let light_seen = r.bool()?;
278            Some(InputDevice::Zapper(ZapperState::from_parts(
279                x, y, trigger, light_seen,
280            )))
281        }
282        2 => {
283            let position = r.u8()?;
284            let fire = r.bool()?;
285            let shift = r.u8()?;
286            let strobe = r.bool()?;
287            Some(InputDevice::Vaus(VausState::from_parts(
288                position, fire, shift, strobe,
289            )))
290        }
291        3 => {
292            let buttons = r.u16()?;
293            let shift_l = r.u8()?;
294            let shift_h = r.u8()?;
295            let strobe = r.bool()?;
296            Some(InputDevice::PowerPad(
297                crate::input_device::PowerPadState::from_parts(buttons, shift_l, shift_h, strobe),
298            ))
299        }
300        4 => {
301            let dx = r.i16()?;
302            let dy = r.i16()?;
303            let left = r.bool()?;
304            let right = r.bool()?;
305            let sensitivity = r.u8()?;
306            let shift = r.u32()?;
307            let read_count = r.u8()?;
308            let strobe = r.bool()?;
309            Some(InputDevice::SnesMouse(SnesMouseState::from_parts(
310                dx,
311                dy,
312                left,
313                right,
314                sensitivity,
315                shift,
316                read_count,
317                strobe,
318            )))
319        }
320        5 => {
321            let mut keys = [0u8; 9];
322            r.read_into(&mut keys)?;
323            let row = r.u8()?;
324            let column = r.bool()?;
325            let enabled = r.bool()?;
326            let clock = r.bool()?;
327            Some(InputDevice::FamilyKeyboard(
328                FamilyKeyboardState::from_parts(keys, row, column, enabled, clock),
329            ))
330        }
331        6 => {
332            let buttons = r.u16()?;
333            let shift_l = r.u8()?;
334            let shift_h = r.u8()?;
335            let strobe = r.bool()?;
336            Some(InputDevice::FamilyTrainer(
337                crate::input_device::PowerPadState::from_parts(buttons, shift_l, shift_h, strobe),
338            ))
339        }
340        7 => {
341            let mut keys = [0u8; 9];
342            r.read_into(&mut keys)?;
343            let row = r.u8()?;
344            let column = r.bool()?;
345            let enabled = r.bool()?;
346            let clock = r.bool()?;
347            Some(InputDevice::SuborKeyboard(FamilyKeyboardState::from_parts(
348                keys, row, column, enabled, clock,
349            )))
350        }
351        8 => {
352            let buttons = r.u8()?;
353            let p1_enabled = r.bool()?;
354            let p2_enabled = r.bool()?;
355            Some(InputDevice::KonamiHyperShot(
356                crate::input_device::KonamiHyperShotState::from_parts(
357                    buttons, p1_enabled, p2_enabled,
358                ),
359            ))
360        }
361        9 => {
362            let sensors = r.u8()?;
363            let select = r.bool()?;
364            Some(InputDevice::BandaiHyperShot(
365                crate::input_device::BandaiHyperShotState::from_parts(sensors, select),
366            ))
367        }
368        other => {
369            return Err(SnapshotError::SectionInvalid {
370                tag: "BUS ".into(),
371                reason: format!("unknown expansion-device tag {other}"),
372            });
373        }
374    })
375}
376
377/// Apply a previously [`encode_bus`]-emitted blob.
378///
379/// Every field is required (version 2, see the module docs): a short body is
380/// [`SnapshotError::Eof`], and bytes left over after the last field are
381/// [`SnapshotError::SectionInvalid`].
382///
383/// # Errors
384///
385/// Returns [`SnapshotError`] for malformed inputs.
386// The body is one straight-line field-by-field decode mirroring `encode_bus`;
387// splitting it would obscure the byte-order correspondence between the two.
388#[allow(clippy::too_many_lines)]
389pub fn decode_bus(bus: &mut SystemBus, data: &[u8]) -> Result<(), SnapshotError> {
390    let mut r = BinReader::new(data);
391    let cycle = r.u64()?;
392    let ram = r.take(0x800)?;
393    bus.set_cycle(cycle);
394    bus.set_ram_bytes(ram)?;
395    let mut controllers = [Controller::new(); 2];
396    for c in &mut controllers {
397        decode_controller(&mut r, c)?;
398    }
399    bus.set_controllers(controllers);
400
401    let dma_page_pending = r.u8()?;
402    let dma_present = r.u8()?;
403    let dma_pending = match dma_present {
404        0 => None,
405        1 => Some(dma_page_pending),
406        other => {
407            return Err(SnapshotError::SectionInvalid {
408                tag: "BUS ".into(),
409                reason: format!("invalid dma-pending presence {other}"),
410            });
411        }
412    };
413    let dma_byte = r.u8()?;
414    let dma_page = r.u8()?;
415    let dma_halt_addr = r.u16()?;
416    let open_bus = r.u8()?;
417    let last_read_addr = r.u16()?;
418    let in_dmc_dma = r.bool()?;
419    let deferred_dma_replay_addr = r.u16()?;
420    let controller_write_pending = r.u8()?;
421    let controller_write_value = r.u8()?;
422    let four_score = r.bool()?;
423    let mut controllers34 = [Controller::new(); 2];
424    for c in &mut controllers34 {
425        decode_controller(&mut r, c)?;
426    }
427    bus.set_controllers34(controllers34);
428    let four_score_idx = [r.u8()?, r.u8()?];
429    let four_score_sig = [r.u8()?, r.u8()?];
430    let dmc_halt = r.bool()?;
431    let uni_oam_active = r.bool()?;
432    let uni_oam_halt = r.bool()?;
433    let uni_oam_aligned = r.bool()?;
434    let uni_oam_addr = r.u16()?;
435    // The OAM-DMA byte index runs 0..=255 while a transfer is active and is
436    // left at 256 when the 256th write completes it (it is re-zeroed only when
437    // the next transfer starts). Any other value is a corrupt file: an active
438    // transfer at 256 or above never reaches the `== 256` completion test and
439    // counts on until the `u16` overflows -- a panic in dev profiles, found by
440    // the v2.7.0 `save_state` fuzz target in 287,867 runs.
441    if uni_oam_addr > 256 || (uni_oam_active && uni_oam_addr > 255) {
442        return Err(SnapshotError::SectionInvalid {
443            tag: "BUS ".into(),
444            reason: format!(
445                "OAM-DMA byte index {uni_oam_addr} out of range (active: {uni_oam_active})"
446            ),
447        });
448    }
449    let ppu_clock = r.u64()?;
450    let device0 = decode_expansion_device(&mut r)?;
451    let device1 = decode_expansion_device(&mut r)?;
452    bus.set_expansion_device(0, device0);
453    bus.set_expansion_device(1, device1);
454    bus.set_mirroring_override(decode_mirroring_override(r.u8()?));
455    let mut pending = [false; 4];
456    for p in &mut pending {
457        *p = r.bool()?;
458    }
459    let mut cycles = [0u64; 2];
460    for c in &mut cycles {
461        *c = r.u64()?;
462    }
463    // Through a setter, not locals: the controllers were installed above, and
464    // mutating copies here would decode cleanly and restore nothing.
465    bus.set_controller_run_state(pending, cycles);
466    let mut fs = [false; 2];
467    for f in &mut fs {
468        *f = r.bool()?;
469    }
470    bus.set_four_score_pending(fs);
471    let internal_data_bus = r.u8()?;
472    let dmc_load_write_delayed = r.bool()?;
473    let overclock_phase = r.u8()?;
474    // The phase indexes the overclocked cycles of one stock cycle, so it is
475    // below the largest multiplier; anything larger is a corrupt file,
476    // refused here rather than clamped later. (A phase valid for `x4` but not
477    // for the multiplier the restoring host runs is clamped to its last cycle
478    // by `restore`.)
479    if overclock_phase >= crate::MAX_CPU_OVERCLOCK {
480        return Err(SnapshotError::SectionInvalid {
481            tag: "BUS ".into(),
482            reason: format!("CPU-overclock phase {overclock_phase} out of range"),
483        });
484    }
485    let apu_cycle = r.u64()?;
486    if r.remaining() != 0 {
487        return Err(SnapshotError::SectionInvalid {
488            tag: "BUS ".into(),
489            reason: format!("{} unexpected trailing bytes", r.remaining()),
490        });
491    }
492    bus.set_bus_misc_state(BusMiscState {
493        dma_pending,
494        dma_byte,
495        dma_page,
496        dma_halt_addr,
497        deferred_dma_replay_addr,
498        open_bus,
499        internal_data_bus,
500        last_read_addr,
501        in_dmc_dma,
502        controller_write_pending,
503        controller_write_value,
504        four_score,
505        four_score_idx,
506        four_score_sig,
507        dmc_halt,
508        dmc_load_write_delayed,
509        overclock_phase,
510        apu_cycle,
511        uni_oam_active,
512        uni_oam_halt,
513        uni_oam_aligned,
514        uni_oam_addr,
515        ppu_clock,
516    });
517    Ok(())
518}
519
520fn encode_controller(w: &mut BinWriter, c: Controller) {
521    w.u8(c.buttons.bits());
522    w.u8(c.shift);
523    w.bool(c.strobe);
524}
525fn decode_controller(r: &mut BinReader<'_>, c: &mut Controller) -> Result<(), SnapshotError> {
526    let bits = r.u8()?;
527    c.buttons = crate::controller::Buttons::from_bits_truncate(bits);
528    c.shift = r.u8()?;
529    c.strobe = r.bool()?;
530    Ok(())
531}
532
533/// Bus-side bookkeeping fields not owned by the chips. Exists as a small
534/// struct so [`encode_bus`] / [`decode_bus`] can ferry them across the
535/// crate-private boundary without exposing the bus's many private fields
536/// individually.
537#[derive(Debug, Clone, Copy)]
538#[allow(clippy::struct_excessive_bools)] // independent state words, not a FSM
539pub struct BusMiscState {
540    /// Source page of a deferred OAM DMA (consumed at the next CPU access).
541    pub dma_pending: Option<u8>,
542    /// Scratch byte for the OAM DMA's read/write pair.
543    pub dma_byte: u8,
544    /// Active OAM DMA page.
545    pub dma_page: u8,
546    /// CPU read address repeated while OAM DMA has the CPU halted.
547    pub dma_halt_addr: u16,
548    /// Deferred DMC readout side-effect target for absolute register reads.
549    pub deferred_dma_replay_addr: u16,
550    /// Open-bus latch (the EXTERNAL data bus).
551    pub open_bus: u8,
552    /// The 2A03's INTERNAL data bus, which a DMC DMA fetch does not drive;
553    /// `$4015` bit 5 reads it (v2.8.0).
554    pub internal_data_bus: u8,
555    /// Most recent CPU read address (for the DMC-DMA readout-bug emulation).
556    pub last_read_addr: u16,
557    /// `true` while servicing a DMC DMA fetch.
558    pub in_dmc_dma: bool,
559    /// Session-24 / Phase 3 (Controller Strobing) deferred-write
560    /// pending counter (CPU cycles until commit; 0 means no pending
561    /// write).
562    pub controller_write_pending: u8,
563    /// Latched controller-write value waiting for the next M2-low
564    /// commit cycle.
565    pub controller_write_value: u8,
566    /// Whether the Four Score 4-player adapter is enabled (v1.7.0).
567    pub four_score: bool,
568    /// Per-port Four Score read counter.
569    pub four_score_idx: [u8; 2],
570    /// Per-port Four Score signature shift register.
571    pub four_score_sig: [u8; 2],
572    /// W3-Stage-4 (2026-06-10): the DMC-DMA halt latch (a DMC DMA is
573    /// pending/halted and waiting for its GET slot).
574    pub dmc_halt: bool,
575    /// v3.1.0 (BUS version 3): a pending load DMC DMA was refused by a CPU
576    /// write and enters on the next read whichever half it is.
577    pub dmc_load_write_delayed: bool,
578    /// v3.1.0 (BUS version 3): which of the `k` CPU cycles of the current
579    /// stock cycle is in progress under the CPU overclock, `0..k` (always 0
580    /// at `x1`); refused at or above `MAX_CPU_OVERCLOCK`.
581    pub overclock_phase: u8,
582    /// v3.1.0 (BUS version 3): the stock-rate domain's cycle counter under
583    /// the CPU overclock (unused at `x1`).
584    pub apu_cycle: u64,
585    /// W3-Stage-4: unified DMA engine (`mc-r1-dma-unified`) — OAM DMA active
586    /// (`TriCNES` `DoOAMDMA`).
587    pub uni_oam_active: bool,
588    /// W3-Stage-4: unified DMA engine — `TriCNES` `OAMDMA_Halt`.
589    pub uni_oam_halt: bool,
590    /// W3-Stage-4: unified DMA engine — `TriCNES` `OAMDMA_Aligned`.
591    pub uni_oam_aligned: bool,
592    /// W3-Stage-4: unified DMA engine — `TriCNES` `DMAAddress` (the OAM
593    /// byte index).
594    pub uni_oam_addr: u16,
595    /// PPU progress in master-clock units (the `run_ppu_to` cursor). Paired
596    /// with `Cpu::master_clock` (CPU section).
597    pub ppu_clock: u64,
598}
599
600#[cfg(test)]
601mod tests {
602    use super::*;
603    use crate::input_device::{
604        BandaiHyperShotState, KonamiHyperShotState, PowerPadState, SnesMouseState,
605    };
606    use alloc::vec;
607
608    /// One of every device, named by an exhaustive `match` so that adding an
609    /// `InputDevice` variant fails to compile here until its encoding is
610    /// checked against [`EXPANSION_DEVICE_MAX_LEN`].
611    fn every_device() -> Vec<InputDevice> {
612        let all = vec![
613            InputDevice::Zapper(ZapperState::from_parts(0, 0, false, false)),
614            InputDevice::Vaus(VausState::from_parts(0, false, 0, false)),
615            InputDevice::PowerPad(PowerPadState::from_parts(0, 0, 0, false)),
616            InputDevice::SnesMouse(SnesMouseState::from_parts(
617                0, 0, false, false, 0, 0, 0, false,
618            )),
619            InputDevice::FamilyKeyboard(FamilyKeyboardState::from_parts(
620                [0; 9], 0, false, false, false,
621            )),
622            InputDevice::FamilyTrainer(PowerPadState::from_parts(0, 0, 0, false)),
623            InputDevice::SuborKeyboard(FamilyKeyboardState::from_parts(
624                [0; 9], 0, false, false, false,
625            )),
626            InputDevice::KonamiHyperShot(KonamiHyperShotState::from_parts(0, false, false)),
627            InputDevice::BandaiHyperShot(BandaiHyperShotState::from_parts(0, false)),
628        ];
629        for d in &all {
630            match d {
631                InputDevice::Zapper(_)
632                | InputDevice::Vaus(_)
633                | InputDevice::PowerPad(_)
634                | InputDevice::SnesMouse(_)
635                | InputDevice::FamilyKeyboard(_)
636                | InputDevice::FamilyTrainer(_)
637                | InputDevice::SuborKeyboard(_)
638                | InputDevice::KonamiHyperShot(_)
639                | InputDevice::BandaiHyperShot(_) => {}
640            }
641        }
642        all
643    }
644
645    fn encoded_len(device: Option<&InputDevice>) -> usize {
646        let mut w = BinWriter::with_capacity(32);
647        encode_expansion_device(&mut w, device);
648        w.into_vec().len()
649    }
650
651    #[test]
652    fn no_expansion_device_encodes_past_the_documented_maximum() {
653        assert_eq!(
654            encoded_len(None),
655            1,
656            "an unplugged port is the tag byte alone"
657        );
658        let largest = every_device()
659            .iter()
660            .map(|d| encoded_len(Some(d)))
661            .max()
662            .expect("at least one device");
663        assert_eq!(
664            largest, EXPANSION_DEVICE_MAX_LEN,
665            "EXPANSION_DEVICE_MAX_LEN must be the largest device encoding exactly: \
666             larger and the libretro core's save-state buffer is too small, smaller \
667             and it is merely wasteful but the documented figure is wrong"
668        );
669    }
670}