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
|
|
@ -6,6 +6,7 @@ mod modulation;
|
|||
mod noise;
|
||||
mod pulse;
|
||||
mod wave;
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
use noise::Noise;
|
||||
use pulse::Pulse;
|
||||
use wave::Wave;
|
||||
|
|
@ -326,3 +327,279 @@ impl Audio {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for StereoLevel {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self { left, right } = self;
|
||||
writer.tag(b"STLV");
|
||||
writer.i16(*left);
|
||||
writer.i16(*right);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
reader.tag(b"STLV")?;
|
||||
let Self { left, right } = self;
|
||||
*left = reader.i16()?;
|
||||
*right = reader.i16()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Fifo {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
words,
|
||||
read,
|
||||
len,
|
||||
playback,
|
||||
remaining,
|
||||
sample,
|
||||
} = self;
|
||||
writer.tag(b"FIF0");
|
||||
for word in words {
|
||||
writer.u32(*word);
|
||||
}
|
||||
writer.u32(*read as u32);
|
||||
writer.u32(*len as u32);
|
||||
writer.u32(*playback);
|
||||
writer.u8(*remaining);
|
||||
writer.u8(*sample as u8);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
reader.tag(b"FIF0")?;
|
||||
let Self {
|
||||
words,
|
||||
read,
|
||||
len,
|
||||
playback,
|
||||
remaining,
|
||||
sample,
|
||||
} = self;
|
||||
for word in words {
|
||||
*word = reader.u32()?;
|
||||
}
|
||||
let read_value = reader.u32()?;
|
||||
let len_value = reader.u32()?;
|
||||
*playback = reader.u32()?;
|
||||
*remaining = reader.u8()?;
|
||||
*sample = reader.u8()? as i8;
|
||||
if read_value >= 7 || len_value > 7 || *remaining > 4 {
|
||||
return Err(SnapshotError::Corrupt("FIFO range"));
|
||||
}
|
||||
*read = read_value as usize;
|
||||
*len = len_value as usize;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Audio {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
enabled,
|
||||
psg,
|
||||
pulses,
|
||||
noise,
|
||||
wave_channel,
|
||||
sequencer_phase,
|
||||
next_step,
|
||||
control,
|
||||
bias,
|
||||
wave,
|
||||
fifo,
|
||||
requests,
|
||||
} = self;
|
||||
writer.tag(b"AUD0");
|
||||
writer.bool(*enabled);
|
||||
for register in psg {
|
||||
writer.u16(*register);
|
||||
}
|
||||
for pulse in pulses {
|
||||
pulse.save(writer);
|
||||
}
|
||||
noise.save(writer);
|
||||
wave_channel.save(writer);
|
||||
writer.u16(*sequencer_phase);
|
||||
writer.u8(*next_step);
|
||||
writer.u16(*control);
|
||||
writer.u16(*bias);
|
||||
writer.raw(&wave[0]);
|
||||
writer.raw(&wave[1]);
|
||||
for fifo in fifo {
|
||||
fifo.save(writer);
|
||||
}
|
||||
for request in requests {
|
||||
writer.bool(*request);
|
||||
}
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
reader.tag(b"AUD0")?;
|
||||
let Self {
|
||||
enabled,
|
||||
psg,
|
||||
pulses,
|
||||
noise,
|
||||
wave_channel,
|
||||
sequencer_phase,
|
||||
next_step,
|
||||
control,
|
||||
bias,
|
||||
wave,
|
||||
fifo,
|
||||
requests,
|
||||
} = self;
|
||||
*enabled = reader.bool()?;
|
||||
for register in psg {
|
||||
*register = reader.u16()?;
|
||||
}
|
||||
for pulse in pulses {
|
||||
pulse.load(reader)?;
|
||||
}
|
||||
noise.load(reader)?;
|
||||
wave_channel.load(reader)?;
|
||||
*sequencer_phase = reader.u16()?;
|
||||
*next_step = reader.u8()?;
|
||||
*control = reader.u16()?;
|
||||
*bias = reader.u16()?;
|
||||
reader.raw_into(&mut wave[0])?;
|
||||
reader.raw_into(&mut wave[1])?;
|
||||
for fifo in fifo {
|
||||
fifo.load(reader)?;
|
||||
}
|
||||
for request in requests {
|
||||
*request = reader.bool()?;
|
||||
}
|
||||
if u32::from(*sequencer_phase) >= SEQUENCER_PERIOD
|
||||
|| *next_step > 7
|
||||
|| *control & !0x770f != 0
|
||||
|| *bias & !0xc3fe != 0
|
||||
{
|
||||
return Err(SnapshotError::Corrupt("audio range"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
fn encode<T: Snapshot>(value: &T) -> Vec<u8> {
|
||||
let mut writer = Writer::new();
|
||||
value.save(&mut writer);
|
||||
writer.finish().unwrap()
|
||||
}
|
||||
|
||||
fn check_prefixes<T: Snapshot + Default>(bytes: &[u8], mut target: T) {
|
||||
for end in 0..bytes.len() {
|
||||
assert!(target.load(&mut Reader::new(&bytes[..end])).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stereo_level_round_trip_and_prefixes() {
|
||||
let source = StereoLevel {
|
||||
left: -371,
|
||||
right: 248,
|
||||
};
|
||||
let bytes = encode(&source);
|
||||
let mut loaded = StereoLevel::default();
|
||||
loaded.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(loaded, source);
|
||||
check_prefixes(&bytes, StereoLevel::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fifo_round_trip_prefixes_and_invalid_index() {
|
||||
let source = Fifo {
|
||||
words: [0x1234_5678, 0x9abc_def0, 3, 4, 5, 6, 7],
|
||||
read: 5,
|
||||
len: 6,
|
||||
playback: 0xabcd_9876,
|
||||
remaining: 3,
|
||||
sample: -47,
|
||||
};
|
||||
let bytes = encode(&source);
|
||||
let mut loaded = Fifo::default();
|
||||
loaded.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(loaded, source);
|
||||
check_prefixes(&bytes, Fifo::default());
|
||||
|
||||
let mut corrupt = bytes;
|
||||
corrupt[32..36].copy_from_slice(&7u32.to_le_bytes());
|
||||
assert_eq!(
|
||||
loaded.load(&mut Reader::new(&corrupt)),
|
||||
Err(SnapshotError::Corrupt("FIFO range"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_round_trip_prefixes_and_invalid_boolean() {
|
||||
let mut source = Audio {
|
||||
enabled: true,
|
||||
psg: [0x1234; 17],
|
||||
..Audio::default()
|
||||
};
|
||||
source.pulses[0].write(0, 0x09, 0);
|
||||
source.pulses[0].write(2, 0x9d, 0);
|
||||
source.pulses[0].write(3, 0xa3, 0);
|
||||
source.pulses[0].write(4, 0x7b, 0);
|
||||
source.pulses[0].write(5, 0x84, 0);
|
||||
source.pulses[0].advance(23);
|
||||
source.pulses[0].clock(2);
|
||||
source.pulses[1].write(2, 0x5b, 0);
|
||||
source.pulses[1].write(3, 0x71, 0);
|
||||
source.pulses[1].write(4, 0x42, 0);
|
||||
source.pulses[1].write(5, 0x83, 0);
|
||||
source.pulses[1].advance(11);
|
||||
source.noise.write(0, 5, 0);
|
||||
source.noise.write(1, 0xa3, 0);
|
||||
source.noise.write(4, 0x39, 0);
|
||||
source.noise.write(5, 0x80, 0);
|
||||
source.noise.advance(3_601);
|
||||
source.wave_channel.write(0, 0xa0, 0);
|
||||
source.wave_channel.write(2, 55, 0);
|
||||
source.wave_channel.write(3, 0xe0, 0);
|
||||
source.wave_channel.write(4, 0x7b, 0);
|
||||
source.wave_channel.write(5, 0xc1, 0);
|
||||
source.wave_channel.advance(13_352, &mut source.wave);
|
||||
source.sequencer_phase = 12_345;
|
||||
source.next_step = 5;
|
||||
source.control = 0x7105;
|
||||
source.bias = 0x83ac;
|
||||
source.wave = [[0x12; 16], [0xa5; 16]];
|
||||
source.fifo = [
|
||||
Fifo {
|
||||
words: [0x1122_3344; 7],
|
||||
read: 3,
|
||||
len: 5,
|
||||
playback: 0xaabb_ccdd,
|
||||
remaining: 2,
|
||||
sample: -91,
|
||||
},
|
||||
Fifo {
|
||||
words: [0x5566_7788; 7],
|
||||
read: 6,
|
||||
len: 7,
|
||||
playback: 0x1234_5678,
|
||||
remaining: 4,
|
||||
sample: 102,
|
||||
},
|
||||
];
|
||||
source.requests = [true, true];
|
||||
|
||||
let bytes = encode(&source);
|
||||
let mut loaded = Audio::default();
|
||||
loaded.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(loaded, source);
|
||||
check_prefixes(&bytes, Audio::default());
|
||||
|
||||
let mut corrupt = bytes;
|
||||
corrupt[4] = 2;
|
||||
assert_eq!(
|
||||
loaded.load(&mut Reader::new(&corrupt)),
|
||||
Err(SnapshotError::Corrupt("boolean"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//! Optional fixed-rate digital capture, separate from hardware channel state.
|
||||
use super::StereoLevel;
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Nominal stereo capture rate. This is not the GBA PWM/resolution model.
|
||||
|
|
@ -101,3 +102,107 @@ impl AudioCapture {
|
|||
count
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for AudioCapture {
|
||||
/// The sample-clock phase continues the emulated timeline, so it is state.
|
||||
/// The queue and drop count are host-facing output and are not.
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
clock,
|
||||
queue: _queue, // Host-facing output, not hardware state.
|
||||
dropped: _dropped, // Host-facing counter, not hardware state.
|
||||
} = self;
|
||||
writer.tag(b"ACAP");
|
||||
writer.option(clock.0.as_ref(), |phase, writer| writer.u16(*phase));
|
||||
}
|
||||
|
||||
/// Whether capture runs is host configuration and is kept. A running capture
|
||||
/// resumes the saved phase, or starts at phase zero if none was running.
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
clock,
|
||||
queue,
|
||||
dropped,
|
||||
} = self;
|
||||
reader.tag(b"ACAP")?;
|
||||
let saved = reader.option(Reader::u16)?;
|
||||
if saved.is_some_and(|phase| u32::from(phase) >= PERIOD) {
|
||||
return Err(SnapshotError::Corrupt("audio capture phase"));
|
||||
}
|
||||
if clock.0.is_some() {
|
||||
clock.0 = Some(saved.unwrap_or(0));
|
||||
}
|
||||
queue.clear();
|
||||
*dropped = 0;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
fn encoded(capture: &AudioCapture) -> Vec<u8> {
|
||||
let mut writer = Writer::new();
|
||||
capture.save(&mut writer);
|
||||
writer.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_restores_phase_and_clears_host_output() {
|
||||
let mut source = AudioCapture::default();
|
||||
source.set_enabled(true);
|
||||
source.clock.0 = Some(317);
|
||||
source.push(StereoLevel {
|
||||
left: 12,
|
||||
right: -9,
|
||||
});
|
||||
source.dropped = 7;
|
||||
let bytes = encoded(&source);
|
||||
|
||||
let mut loaded = AudioCapture::default();
|
||||
loaded.set_enabled(true);
|
||||
loaded.push(StereoLevel { left: 1, right: 2 });
|
||||
loaded.dropped = 4;
|
||||
loaded.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(loaded.clock.0, Some(317));
|
||||
assert!(loaded.queue.is_empty());
|
||||
assert_eq!(loaded.dropped, 0);
|
||||
for end in 0..bytes.len() {
|
||||
assert!(loaded.load(&mut Reader::new(&bytes[..end])).is_err());
|
||||
}
|
||||
|
||||
let mut corrupt = bytes.clone();
|
||||
corrupt[0] ^= 1;
|
||||
assert_eq!(
|
||||
loaded.load(&mut Reader::new(&corrupt)),
|
||||
Err(SnapshotError::Corrupt("section tag"))
|
||||
);
|
||||
let mut out_of_range = bytes;
|
||||
out_of_range[5..7].copy_from_slice(&512u16.to_le_bytes());
|
||||
assert_eq!(
|
||||
loaded.load(&mut Reader::new(&out_of_range)),
|
||||
Err(SnapshotError::Corrupt("audio capture phase"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capture_configuration_is_kept_not_restored() {
|
||||
let mut running = AudioCapture::default();
|
||||
running.set_enabled(true);
|
||||
running.clock.0 = Some(100);
|
||||
let running_bytes = encoded(&running);
|
||||
let idle_bytes = encoded(&AudioCapture::default());
|
||||
|
||||
// A state saved while capture ran does not enable capture on an idle target.
|
||||
let mut idle = AudioCapture::default();
|
||||
idle.load(&mut Reader::new(&running_bytes)).unwrap();
|
||||
assert_eq!(idle.clock.0, None);
|
||||
// A state saved while idle starts a running capture at phase zero.
|
||||
let mut target = AudioCapture::default();
|
||||
target.set_enabled(true);
|
||||
target.clock.0 = Some(200);
|
||||
target.load(&mut Reader::new(&idle_bytes)).unwrap();
|
||||
assert_eq!(target.clock.0, Some(0));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
//! Shared nominal PSG length, envelope, activity, and logical DAC gate.
|
||||
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct Modulation {
|
||||
pub(super) active: bool,
|
||||
|
|
@ -96,3 +98,84 @@ impl Modulation {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Modulation {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
active,
|
||||
envelope,
|
||||
length_enabled,
|
||||
length,
|
||||
volume,
|
||||
envelope_ticks,
|
||||
envelope_running,
|
||||
} = self;
|
||||
writer.tag(b"MOD0");
|
||||
writer.bool(*active);
|
||||
writer.u8(*envelope);
|
||||
writer.bool(*length_enabled);
|
||||
writer.u8(*length);
|
||||
writer.u8(*volume);
|
||||
writer.u8(*envelope_ticks);
|
||||
writer.bool(*envelope_running);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
reader.tag(b"MOD0")?;
|
||||
let Self {
|
||||
active,
|
||||
envelope,
|
||||
length_enabled,
|
||||
length,
|
||||
volume,
|
||||
envelope_ticks,
|
||||
envelope_running,
|
||||
} = self;
|
||||
*active = reader.bool()?;
|
||||
*envelope = reader.u8()?;
|
||||
*length_enabled = reader.bool()?;
|
||||
*length = reader.u8()?;
|
||||
*volume = reader.u8()?;
|
||||
*envelope_ticks = reader.u8()?;
|
||||
*envelope_running = reader.bool()?;
|
||||
if *length > 64 || *volume > 15 || *envelope_ticks > 8 {
|
||||
return Err(SnapshotError::Corrupt("modulation range"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn snapshot_round_trip_prefixes_and_invalid_boolean() {
|
||||
let source = Modulation {
|
||||
active: true,
|
||||
envelope: 0xf3,
|
||||
length_enabled: true,
|
||||
length: 29,
|
||||
volume: 7,
|
||||
envelope_ticks: 4,
|
||||
envelope_running: true,
|
||||
};
|
||||
let mut writer = Writer::new();
|
||||
source.save(&mut writer);
|
||||
let bytes = writer.finish().unwrap();
|
||||
|
||||
let mut loaded = Modulation::default();
|
||||
loaded.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(loaded, source);
|
||||
for end in 0..bytes.len() {
|
||||
assert!(loaded.load(&mut Reader::new(&bytes[..end])).is_err());
|
||||
}
|
||||
|
||||
let mut corrupt = bytes;
|
||||
corrupt[4] = 2;
|
||||
assert_eq!(
|
||||
loaded.load(&mut Reader::new(&corrupt)),
|
||||
Err(SnapshotError::Corrupt("boolean"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//! Nominal GBA channel 4 polynomial counter. No random numbers or host clocks.
|
||||
use super::modulation::Modulation;
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct Noise {
|
||||
|
|
@ -80,6 +81,78 @@ impl Noise {
|
|||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Noise {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
modulation,
|
||||
polynomial,
|
||||
lfsr,
|
||||
remaining,
|
||||
high,
|
||||
} = self;
|
||||
writer.tag(b"NOI0");
|
||||
modulation.save(writer);
|
||||
writer.u8(*polynomial);
|
||||
writer.u16(*lfsr);
|
||||
writer.u32(*remaining);
|
||||
writer.bool(*high);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
reader.tag(b"NOI0")?;
|
||||
let Self {
|
||||
modulation,
|
||||
polynomial,
|
||||
lfsr,
|
||||
remaining,
|
||||
high,
|
||||
} = self;
|
||||
modulation.load(reader)?;
|
||||
*polynomial = reader.u8()?;
|
||||
*lfsr = reader.u16()?;
|
||||
*remaining = reader.u32()?;
|
||||
*high = reader.bool()?;
|
||||
if *lfsr > 0x7fff || *remaining > 14_680_064 {
|
||||
return Err(SnapshotError::Corrupt("noise range"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn snapshot_round_trip_prefixes_and_invalid_boolean() {
|
||||
let mut source = Noise::default();
|
||||
source.write(0, 5, 0);
|
||||
source.write(1, 0xa3, 0);
|
||||
source.write(4, 0x39, 0);
|
||||
source.write(5, 0x80, 0);
|
||||
let cycles = 7 * source.period() + 17;
|
||||
source.advance(cycles);
|
||||
source.clock(7);
|
||||
|
||||
let mut writer = Writer::new();
|
||||
source.save(&mut writer);
|
||||
let bytes = writer.finish().unwrap();
|
||||
let mut loaded = Noise::default();
|
||||
loaded.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(loaded, source);
|
||||
for end in 0..bytes.len() {
|
||||
assert!(loaded.load(&mut Reader::new(&bytes[..end])).is_err());
|
||||
}
|
||||
|
||||
let mut corrupt = bytes;
|
||||
corrupt[8] = 2;
|
||||
assert_eq!(
|
||||
loaded.load(&mut Reader::new(&corrupt)),
|
||||
Err(SnapshotError::Corrupt("boolean"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const fn shift(state: u16, short: bool) -> u16 {
|
||||
(state >> 1)
|
||||
^ if state & 1 != 0 {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
//! Shared nominal pulse oscillator and sweep unit. No host-clock dependency.
|
||||
//! Channel 2 never receives sweep-register writes, so its sweep remains disabled.
|
||||
use super::modulation::Modulation;
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct Pulse {
|
||||
|
|
@ -134,5 +135,104 @@ impl Pulse {
|
|||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Pulse {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
modulation,
|
||||
sweep,
|
||||
duty,
|
||||
frequency,
|
||||
phase,
|
||||
remaining,
|
||||
sweep_shadow,
|
||||
sweep_ticks,
|
||||
sweep_running,
|
||||
subtracted,
|
||||
} = self;
|
||||
writer.tag(b"PUL0");
|
||||
modulation.save(writer);
|
||||
writer.u8(*sweep);
|
||||
writer.u8(*duty);
|
||||
writer.u16(*frequency);
|
||||
writer.u8(*phase);
|
||||
writer.u32(*remaining);
|
||||
writer.u16(*sweep_shadow);
|
||||
writer.u8(*sweep_ticks);
|
||||
writer.bool(*sweep_running);
|
||||
writer.bool(*subtracted);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
reader.tag(b"PUL0")?;
|
||||
let Self {
|
||||
modulation,
|
||||
sweep,
|
||||
duty,
|
||||
frequency,
|
||||
phase,
|
||||
remaining,
|
||||
sweep_shadow,
|
||||
sweep_ticks,
|
||||
sweep_running,
|
||||
subtracted,
|
||||
} = self;
|
||||
modulation.load(reader)?;
|
||||
*sweep = reader.u8()?;
|
||||
*duty = reader.u8()?;
|
||||
*frequency = reader.u16()?;
|
||||
*phase = reader.u8()?;
|
||||
*remaining = reader.u32()?;
|
||||
*sweep_shadow = reader.u16()?;
|
||||
*sweep_ticks = reader.u8()?;
|
||||
*sweep_running = reader.bool()?;
|
||||
*subtracted = reader.bool()?;
|
||||
if *sweep > 0x7f
|
||||
|| *duty > 3
|
||||
|| *frequency > 2047
|
||||
|| *phase > 7
|
||||
|| *remaining > 32768
|
||||
|| *sweep_shadow > 2047
|
||||
|| *sweep_ticks > 8
|
||||
{
|
||||
return Err(SnapshotError::Corrupt("pulse range"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn snapshot_round_trip_prefixes_and_invalid_boolean() {
|
||||
let mut source = Pulse::default();
|
||||
source.write(0, 0x09, 0);
|
||||
source.write(2, 0x9d, 0);
|
||||
source.write(3, 0xa3, 0);
|
||||
source.write(4, 0x7b, 0);
|
||||
source.write(5, 0x84, 0);
|
||||
source.advance(23);
|
||||
source.clock(2);
|
||||
|
||||
let mut writer = Writer::new();
|
||||
source.save(&mut writer);
|
||||
let bytes = writer.finish().unwrap();
|
||||
let mut loaded = Pulse::default();
|
||||
loaded.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(loaded, source);
|
||||
for end in 0..bytes.len() {
|
||||
assert!(loaded.load(&mut Reader::new(&bytes[..end])).is_err());
|
||||
}
|
||||
|
||||
let mut corrupt = bytes;
|
||||
corrupt[8] = 2;
|
||||
assert_eq!(
|
||||
loaded.load(&mut Reader::new(&corrupt)),
|
||||
Err(SnapshotError::Corrupt("boolean"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Nominal single-bank wave playback. RAM rotates in high-nibble-first order.
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct Wave {
|
||||
control: u8,
|
||||
|
|
@ -129,5 +131,95 @@ impl Wave {
|
|||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Wave {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
control,
|
||||
volume,
|
||||
frequency,
|
||||
length,
|
||||
length_enabled,
|
||||
active,
|
||||
remaining,
|
||||
sample,
|
||||
} = self;
|
||||
writer.tag(b"WAV0");
|
||||
writer.u8(*control);
|
||||
writer.u8(*volume);
|
||||
writer.u16(*frequency);
|
||||
writer.u16(*length);
|
||||
writer.bool(*length_enabled);
|
||||
writer.bool(*active);
|
||||
writer.u32(*remaining);
|
||||
writer.option(sample.as_ref(), |sample, writer| writer.u8(*sample));
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
reader.tag(b"WAV0")?;
|
||||
let Self {
|
||||
control,
|
||||
volume,
|
||||
frequency,
|
||||
length,
|
||||
length_enabled,
|
||||
active,
|
||||
remaining,
|
||||
sample,
|
||||
} = self;
|
||||
*control = reader.u8()?;
|
||||
*volume = reader.u8()?;
|
||||
*frequency = reader.u16()?;
|
||||
*length = reader.u16()?;
|
||||
*length_enabled = reader.bool()?;
|
||||
*active = reader.bool()?;
|
||||
*remaining = reader.u32()?;
|
||||
*sample = reader.option(|reader| reader.u8())?;
|
||||
if *control & !0xe0 != 0
|
||||
|| *volume & !0xe0 != 0
|
||||
|| *frequency > 2047
|
||||
|| *length > 256
|
||||
|| *remaining > 16384
|
||||
|| matches!(*sample, Some(value) if value > 15)
|
||||
{
|
||||
return Err(SnapshotError::Corrupt("wave range"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn snapshot_round_trip_prefixes_and_invalid_option_tag() {
|
||||
let mut source = Wave::default();
|
||||
source.write(0, 0xa0, 0);
|
||||
source.write(2, 55, 0);
|
||||
source.write(3, 0xe0, 0);
|
||||
source.write(4, 0x7b, 0);
|
||||
source.write(5, 0xc1, 0);
|
||||
let mut banks = [[0x12; 16]; 2];
|
||||
source.advance(source.remaining, &mut banks);
|
||||
|
||||
let mut writer = Writer::new();
|
||||
source.save(&mut writer);
|
||||
let bytes = writer.finish().unwrap();
|
||||
let mut loaded = Wave::default();
|
||||
loaded.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(loaded, source);
|
||||
for end in 0..bytes.len() {
|
||||
assert!(loaded.load(&mut Reader::new(&bytes[..end])).is_err());
|
||||
}
|
||||
|
||||
let mut corrupt = bytes;
|
||||
corrupt[16] = 2;
|
||||
assert_eq!(
|
||||
loaded.load(&mut Reader::new(&corrupt)),
|
||||
Err(SnapshotError::Corrupt("boolean"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
//! Validated 2000–2099 RTC calendar arithmetic. No clock sampling or timezone conversion.
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
const DAYS_PER_CENTURY: u64 = 36_525;
|
||||
|
|
@ -153,5 +154,23 @@ impl RtcDateTime {
|
|||
}
|
||||
}
|
||||
|
||||
impl Snapshot for RtcDateTime {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self(parts) = self;
|
||||
writer.tag(b"DATE");
|
||||
writer.raw(parts);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self(current) = self;
|
||||
reader.tag(b"DATE")?;
|
||||
let mut parts = [0; 7];
|
||||
reader.raw_into(&mut parts)?;
|
||||
let validated = Self::new(parts).map_err(|_| SnapshotError::Corrupt("RTC date/time"))?;
|
||||
*current = validated.0;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
|
|
|||
|
|
@ -139,3 +139,29 @@ fn large_and_split_advances_do_not_overflow_or_lose_time() {
|
|||
assert_eq!(batched, small);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_round_trips_and_validates_rtc_date_time() {
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
let value = RtcDateTime::new([24, 2, 29, 4, 23, 58, 59]).unwrap();
|
||||
let mut writer = Writer::new();
|
||||
value.save(&mut writer);
|
||||
let bytes = writer.finish().unwrap();
|
||||
|
||||
let mut restored = RtcDateTime::default();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, value);
|
||||
for end in 0..bytes.len() {
|
||||
assert!(RtcDateTime::default()
|
||||
.load(&mut Reader::new(&bytes[..end]))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
let mut corrupt = bytes;
|
||||
corrupt[4] = 0xff;
|
||||
assert_eq!(
|
||||
RtcDateTime::default().load(&mut Reader::new(&corrupt)),
|
||||
Err(SnapshotError::Corrupt("RTC date/time"))
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//! Bounded Macronix Flash commands with nominal delayed program/erase completion.
|
||||
use crate::memory::MemoryError;
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
pub const SAVE_START: u32 = 0x0e00_0000;
|
||||
|
|
@ -263,3 +264,408 @@ impl Flash {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SaveMutation {
|
||||
fn read(reader: &mut Reader<'_>) -> Result<Self, SnapshotError> {
|
||||
reader.tag(b"SMUT")?;
|
||||
let start = usize::try_from(reader.u64()?)
|
||||
.map_err(|_| SnapshotError::Corrupt("Flash mutation range"))?;
|
||||
let end = usize::try_from(reader.u64()?)
|
||||
.map_err(|_| SnapshotError::Corrupt("Flash mutation range"))?;
|
||||
let value = reader.u8()?;
|
||||
let mutation = Self { start, end, value };
|
||||
if !mutation.valid_for(SaveDevice::Flash128.capacity()) {
|
||||
return Err(SnapshotError::Corrupt("Flash mutation range"));
|
||||
}
|
||||
Ok(mutation)
|
||||
}
|
||||
|
||||
fn valid_for(self, capacity: usize) -> bool {
|
||||
let Self { start, end, value } = self;
|
||||
let Some(length) = end.checked_sub(start) else {
|
||||
return false;
|
||||
};
|
||||
if start >= end || end > capacity {
|
||||
return false;
|
||||
}
|
||||
match length {
|
||||
1 => true,
|
||||
0x1000 => value == 0xff && start & 0xfff == 0,
|
||||
0x1_0000 | 0x2_0000 => value == 0xff && start == 0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for SaveMutation {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self { start, end, value } = self;
|
||||
writer.tag(b"SMUT");
|
||||
writer.u64(*start as u64);
|
||||
writer.u64(*end as u64);
|
||||
writer.u8(*value);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self { start, end, value } = self;
|
||||
let decoded = Self::read(reader)?;
|
||||
*start = decoded.start;
|
||||
*end = decoded.end;
|
||||
*value = decoded.value;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod mutation_snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn save_mutation_snapshot_round_trips_and_rejects_prefixes() {
|
||||
let mutation = SaveMutation {
|
||||
start: 0x1000,
|
||||
end: 0x2000,
|
||||
value: 0xff,
|
||||
};
|
||||
let mut writer = Writer::new();
|
||||
mutation.save(&mut writer);
|
||||
let bytes = writer.finish().unwrap();
|
||||
|
||||
let mut restored = SaveMutation {
|
||||
start: 0,
|
||||
end: 1,
|
||||
value: 0,
|
||||
};
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, mutation);
|
||||
for end in 0..bytes.len() {
|
||||
let mut target = SaveMutation {
|
||||
start: 0,
|
||||
end: 1,
|
||||
value: 0,
|
||||
};
|
||||
assert!(target.load(&mut Reader::new(&bytes[..end])).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Phase {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
writer.tag(b"FLPH");
|
||||
writer.u8(match self {
|
||||
Self::Ready => 0,
|
||||
Self::Unlock => 1,
|
||||
Self::Command => 2,
|
||||
Self::Bank => 3,
|
||||
Self::Program => 4,
|
||||
Self::EraseUnlock => 5,
|
||||
Self::EraseCommand => 6,
|
||||
Self::EraseConfirm => 7,
|
||||
});
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
reader.tag(b"FLPH")?;
|
||||
let decoded = match reader.u8()? {
|
||||
0 => Self::Ready,
|
||||
1 => Self::Unlock,
|
||||
2 => Self::Command,
|
||||
3 => Self::Bank,
|
||||
4 => Self::Program,
|
||||
5 => Self::EraseUnlock,
|
||||
6 => Self::EraseCommand,
|
||||
7 => Self::EraseConfirm,
|
||||
_ => return Err(SnapshotError::Corrupt("Flash phase")),
|
||||
};
|
||||
let _current = match self {
|
||||
Self::Ready => 0,
|
||||
Self::Unlock => 1,
|
||||
Self::Command => 2,
|
||||
Self::Bank => 3,
|
||||
Self::Program => 4,
|
||||
Self::EraseUnlock => 5,
|
||||
Self::EraseCommand => 6,
|
||||
Self::EraseConfirm => 7,
|
||||
};
|
||||
*self = decoded;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Pending {
|
||||
fn read(reader: &mut Reader<'_>) -> Result<Self, SnapshotError> {
|
||||
reader.tag(b"PEND")?;
|
||||
let mutation = SaveMutation::read(reader)?;
|
||||
let remaining = reader.u32()?;
|
||||
let poll = reader.u32()?;
|
||||
let pending = Self {
|
||||
mutation,
|
||||
remaining,
|
||||
poll,
|
||||
};
|
||||
if !pending.valid_for(SaveDevice::Flash128.capacity()) {
|
||||
return Err(SnapshotError::Corrupt("Flash pending operation"));
|
||||
}
|
||||
Ok(pending)
|
||||
}
|
||||
|
||||
fn valid_for(self, capacity: usize) -> bool {
|
||||
let Self {
|
||||
mutation,
|
||||
remaining,
|
||||
poll,
|
||||
} = self;
|
||||
let SaveMutation { start, end, value } = mutation;
|
||||
let Some(length) = end.checked_sub(start) else {
|
||||
return false;
|
||||
};
|
||||
let Some(poll_offset) = poll
|
||||
.checked_sub(SAVE_START)
|
||||
.and_then(|offset| usize::try_from(offset).ok())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if !mutation.valid_for(capacity) || poll_offset > 0xffff || remaining == 0 {
|
||||
return false;
|
||||
}
|
||||
let bank_offset = start % 0x1_0000;
|
||||
match length {
|
||||
1 => remaining <= FLASH_PROGRAM_CYCLES && poll_offset == bank_offset,
|
||||
0x1000 => {
|
||||
remaining <= FLASH_ERASE_CYCLES
|
||||
&& value == 0xff
|
||||
&& bank_offset & 0xfff == 0
|
||||
&& poll_offset == bank_offset
|
||||
}
|
||||
0x1_0000 | 0x2_0000 => {
|
||||
remaining <= FLASH_ERASE_CYCLES && value == 0xff && start == 0 && poll == SAVE_START
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Pending {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
mutation,
|
||||
remaining,
|
||||
poll,
|
||||
} = self;
|
||||
writer.tag(b"PEND");
|
||||
mutation.save(writer);
|
||||
writer.u32(*remaining);
|
||||
writer.u32(*poll);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
mutation,
|
||||
remaining,
|
||||
poll,
|
||||
} = self;
|
||||
let decoded = Self::read(reader)?;
|
||||
*mutation = decoded.mutation;
|
||||
*remaining = decoded.remaining;
|
||||
*poll = decoded.poll;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Flash {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
device,
|
||||
phase,
|
||||
identify,
|
||||
bank,
|
||||
pending,
|
||||
} = self;
|
||||
writer.tag(b"FLAS");
|
||||
writer.u8(match device {
|
||||
SaveDevice::None => 0,
|
||||
SaveDevice::Flash64 => 1,
|
||||
SaveDevice::Flash128 => 2,
|
||||
});
|
||||
phase.save(writer);
|
||||
writer.bool(*identify);
|
||||
writer.u8(*bank);
|
||||
writer.option(pending.as_ref(), |pending, writer| pending.save(writer));
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
device,
|
||||
phase,
|
||||
identify,
|
||||
bank,
|
||||
pending,
|
||||
} = self;
|
||||
reader.tag(b"FLAS")?;
|
||||
let saved_device = match reader.u8()? {
|
||||
0 => SaveDevice::None,
|
||||
1 => SaveDevice::Flash64,
|
||||
2 => SaveDevice::Flash128,
|
||||
_ => return Err(SnapshotError::Corrupt("save device")),
|
||||
};
|
||||
if saved_device == SaveDevice::None {
|
||||
return Err(SnapshotError::Corrupt("save device"));
|
||||
}
|
||||
if saved_device != *device {
|
||||
return Err(SnapshotError::ConfigMismatch("save device"));
|
||||
}
|
||||
let mut saved_phase = Phase::Ready;
|
||||
saved_phase.load(reader)?;
|
||||
let saved_identify = reader.bool()?;
|
||||
let saved_bank = reader.u8()?;
|
||||
let saved_pending = reader.option(Pending::read)?;
|
||||
|
||||
if saved_bank > 1 || (saved_device == SaveDevice::Flash64 && saved_bank != 0) {
|
||||
return Err(SnapshotError::Corrupt("Flash bank"));
|
||||
}
|
||||
if saved_device == SaveDevice::Flash64 && saved_phase == Phase::Bank {
|
||||
return Err(SnapshotError::Corrupt("Flash bank phase"));
|
||||
}
|
||||
if let Some(saved_pending) = saved_pending {
|
||||
let pending_length = saved_pending.mutation.end - saved_pending.mutation.start;
|
||||
let mutation_start = saved_pending.mutation.start;
|
||||
if !saved_pending.valid_for(saved_device.capacity())
|
||||
|| saved_phase != Phase::Ready
|
||||
|| saved_identify
|
||||
|| ((pending_length == 1 || pending_length == 0x1000)
|
||||
&& mutation_start / 0x1_0000 != usize::from(saved_bank))
|
||||
|| (matches!(pending_length, 0x1_0000 | 0x2_0000)
|
||||
&& pending_length != saved_device.capacity())
|
||||
{
|
||||
return Err(SnapshotError::Corrupt("Flash pending state"));
|
||||
}
|
||||
}
|
||||
|
||||
*phase = saved_phase;
|
||||
*identify = saved_identify;
|
||||
*bank = saved_bank;
|
||||
*pending = saved_pending;
|
||||
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 assert_prefixes<T: Snapshot + Copy>(value: T) {
|
||||
let bytes = encoded(&value);
|
||||
for end in 0..bytes.len() {
|
||||
let mut restored = value;
|
||||
assert!(restored.load(&mut Reader::new(&bytes[..end])).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
fn erase_pending(start: usize, end: usize, remaining: u32, poll: u32) -> Pending {
|
||||
Pending {
|
||||
mutation: SaveMutation {
|
||||
start,
|
||||
end,
|
||||
value: 0xff,
|
||||
},
|
||||
remaining,
|
||||
poll,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flash_snapshot_round_trips_controller_and_pending_operation() {
|
||||
let flash = Flash {
|
||||
device: SaveDevice::Flash128,
|
||||
phase: Phase::Ready,
|
||||
identify: false,
|
||||
bank: 1,
|
||||
pending: Some(erase_pending(
|
||||
0x1_1000,
|
||||
0x1_2000,
|
||||
12_345,
|
||||
SAVE_START + 0x1000,
|
||||
)),
|
||||
};
|
||||
let bytes = encoded(&flash);
|
||||
let mut restored = Flash::new(SaveDevice::Flash128).unwrap();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, flash);
|
||||
assert_prefixes(flash);
|
||||
|
||||
let command_state = Flash {
|
||||
device: SaveDevice::Flash128,
|
||||
phase: Phase::Command,
|
||||
identify: true,
|
||||
bank: 1,
|
||||
pending: None,
|
||||
};
|
||||
let command_bytes = encoded(&command_state);
|
||||
let mut restored = Flash::new(SaveDevice::Flash128).unwrap();
|
||||
restored.load(&mut Reader::new(&command_bytes)).unwrap();
|
||||
assert_eq!(restored, command_state);
|
||||
|
||||
let mut other_device = Flash::new(SaveDevice::Flash64).unwrap();
|
||||
assert_eq!(
|
||||
other_device.load(&mut Reader::new(&bytes)),
|
||||
Err(SnapshotError::ConfigMismatch("save device"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flash_pending_phase_and_bank_encodings_are_checked() {
|
||||
let phases = [
|
||||
Phase::Ready,
|
||||
Phase::Unlock,
|
||||
Phase::Command,
|
||||
Phase::Bank,
|
||||
Phase::Program,
|
||||
Phase::EraseUnlock,
|
||||
Phase::EraseCommand,
|
||||
Phase::EraseConfirm,
|
||||
];
|
||||
for phase in phases {
|
||||
let bytes = encoded(&phase);
|
||||
let mut restored = Phase::Ready;
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, phase);
|
||||
assert_prefixes(phase);
|
||||
}
|
||||
|
||||
let pending = erase_pending(0x1000, 0x2000, 10_000, SAVE_START + 0x1000);
|
||||
let bytes = encoded(&pending);
|
||||
let mut restored = erase_pending(0, 1, 1, SAVE_START);
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, pending);
|
||||
assert_prefixes(pending);
|
||||
|
||||
let mut invalid_phase = encoded(&Phase::Ready);
|
||||
invalid_phase[4] = 0xff;
|
||||
assert_eq!(
|
||||
Phase::Ready.load(&mut Reader::new(&invalid_phase)),
|
||||
Err(SnapshotError::Corrupt("Flash phase"))
|
||||
);
|
||||
|
||||
let mut invalid_pending = encoded(&pending);
|
||||
invalid_pending[8..16].copy_from_slice(&u64::MAX.to_le_bytes());
|
||||
assert!(matches!(
|
||||
erase_pending(0, 1, 1, SAVE_START).load(&mut Reader::new(&invalid_pending)),
|
||||
Err(SnapshotError::Corrupt(_))
|
||||
));
|
||||
|
||||
let mut invalid_bank = encoded(&Flash::new(SaveDevice::Flash128).unwrap());
|
||||
invalid_bank[11] = 2;
|
||||
assert_eq!(
|
||||
Flash::new(SaveDevice::Flash128)
|
||||
.unwrap()
|
||||
.load(&mut Reader::new(&invalid_bank)),
|
||||
Err(SnapshotError::Corrupt("Flash bank"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//! Edge-driven RTC command framing with latched reads and complete-payload writes.
|
||||
use super::RtcDateTime;
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct Rtc {
|
||||
|
|
@ -237,3 +238,277 @@ impl Rtc {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Register {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
writer.tag(b"RTCR");
|
||||
writer.u8(match self {
|
||||
Self::Control => 0,
|
||||
Self::DateTime => 1,
|
||||
Self::Time => 2,
|
||||
});
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
reader.tag(b"RTCR")?;
|
||||
let decoded = match reader.u8()? {
|
||||
0 => Self::Control,
|
||||
1 => Self::DateTime,
|
||||
2 => Self::Time,
|
||||
_ => return Err(SnapshotError::Corrupt("RTC register")),
|
||||
};
|
||||
let _current = match self {
|
||||
Self::Control => 0,
|
||||
Self::DateTime => 1,
|
||||
Self::Time => 2,
|
||||
};
|
||||
*self = decoded;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Phase {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
writer.tag(b"RTPH");
|
||||
match self {
|
||||
Self::Command { byte, bits } => {
|
||||
writer.u8(0);
|
||||
writer.u8(*byte);
|
||||
writer.u8(*bits);
|
||||
}
|
||||
Self::Write {
|
||||
register,
|
||||
data,
|
||||
offset,
|
||||
byte,
|
||||
bits,
|
||||
} => {
|
||||
writer.u8(1);
|
||||
register.save(writer);
|
||||
writer.raw(data);
|
||||
writer.u8(*offset);
|
||||
writer.u8(*byte);
|
||||
writer.u8(*bits);
|
||||
}
|
||||
Self::Read { data, len, bit } => {
|
||||
writer.u8(2);
|
||||
writer.raw(data);
|
||||
writer.u8(*len);
|
||||
writer.u8(*bit);
|
||||
}
|
||||
Self::Done => writer.u8(3),
|
||||
}
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
reader.tag(b"RTPH")?;
|
||||
let decoded = match reader.u8()? {
|
||||
0 => {
|
||||
let byte = reader.u8()?;
|
||||
let bits = reader.u8()?;
|
||||
if bits >= 8 {
|
||||
return Err(SnapshotError::Corrupt("RTC command bit index"));
|
||||
}
|
||||
Self::Command { byte, bits }
|
||||
}
|
||||
1 => {
|
||||
let mut data = [0; 7];
|
||||
let mut register = Register::Control;
|
||||
register.load(reader)?;
|
||||
reader.raw_into(&mut data)?;
|
||||
let offset = reader.u8()?;
|
||||
let byte = reader.u8()?;
|
||||
let bits = reader.u8()?;
|
||||
if offset >= register.len() || bits >= 8 {
|
||||
return Err(SnapshotError::Corrupt("RTC write index"));
|
||||
}
|
||||
Self::Write {
|
||||
register,
|
||||
data,
|
||||
offset,
|
||||
byte,
|
||||
bits,
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
let mut data = [0; 7];
|
||||
reader.raw_into(&mut data)?;
|
||||
let len = reader.u8()?;
|
||||
let bit = reader.u8()?;
|
||||
if !matches!(len, 1 | 3 | 7) || bit >= len * 8 {
|
||||
return Err(SnapshotError::Corrupt("RTC read index"));
|
||||
}
|
||||
Self::Read { data, len, bit }
|
||||
}
|
||||
3 => Self::Done,
|
||||
_ => return Err(SnapshotError::Corrupt("RTC phase")),
|
||||
};
|
||||
let _current = match self {
|
||||
Self::Command { byte: _, bits: _ } => 0,
|
||||
Self::Write {
|
||||
register: _,
|
||||
data: _,
|
||||
offset: _,
|
||||
byte: _,
|
||||
bits: _,
|
||||
} => 1,
|
||||
Self::Read {
|
||||
data: _,
|
||||
len: _,
|
||||
bit: _,
|
||||
} => 2,
|
||||
Self::Done => 3,
|
||||
};
|
||||
*self = decoded;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Rtc {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
control,
|
||||
calendar,
|
||||
selected,
|
||||
clock,
|
||||
sample,
|
||||
output,
|
||||
phase,
|
||||
} = self;
|
||||
writer.tag(b"RTC0");
|
||||
writer.u8(*control);
|
||||
calendar.save(writer);
|
||||
writer.bool(*selected);
|
||||
writer.bool(*clock);
|
||||
writer.bool(*sample);
|
||||
writer.bool(*output);
|
||||
phase.save(writer);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
control,
|
||||
calendar,
|
||||
selected,
|
||||
clock,
|
||||
sample,
|
||||
output,
|
||||
phase,
|
||||
} = self;
|
||||
reader.tag(b"RTC0")?;
|
||||
let saved_control = reader.u8()?;
|
||||
if saved_control & !0x40 != 0 {
|
||||
return Err(SnapshotError::Corrupt("RTC control"));
|
||||
}
|
||||
calendar.load(reader)?;
|
||||
let saved_selected = reader.bool()?;
|
||||
let saved_clock = reader.bool()?;
|
||||
let saved_sample = reader.bool()?;
|
||||
let saved_output = reader.bool()?;
|
||||
phase.load(reader)?;
|
||||
*control = saved_control;
|
||||
*selected = saved_selected;
|
||||
*clock = saved_clock;
|
||||
*sample = saved_sample;
|
||||
*output = saved_output;
|
||||
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 assert_prefixes<T: Snapshot + Copy>(value: T) {
|
||||
let bytes = encoded(&value);
|
||||
for end in 0..bytes.len() {
|
||||
let mut restored = value;
|
||||
assert!(restored.load(&mut Reader::new(&bytes[..end])).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rtc_snapshot_round_trips_all_latches_and_calendar() {
|
||||
let rtc = Rtc {
|
||||
calendar: RtcDateTime::new([4, 3, 8, 2, 12, 34, 56]).unwrap(),
|
||||
selected: true,
|
||||
phase: Phase::Read {
|
||||
data: [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde],
|
||||
len: 7,
|
||||
bit: 19,
|
||||
},
|
||||
..Rtc::default()
|
||||
};
|
||||
let bytes = encoded(&rtc);
|
||||
|
||||
let mut restored = Rtc::default();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, rtc);
|
||||
assert_prefixes(rtc);
|
||||
|
||||
let mut invalid = bytes;
|
||||
invalid[4] = 0x41;
|
||||
assert_eq!(
|
||||
Rtc::default().load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("RTC control"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rtc_register_and_phase_encodings_are_exhaustive_and_checked() {
|
||||
for register in [Register::Control, Register::DateTime, Register::Time] {
|
||||
let bytes = encoded(®ister);
|
||||
let mut restored = Register::Control;
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, register);
|
||||
assert_prefixes(register);
|
||||
}
|
||||
|
||||
let phases = [
|
||||
Phase::Command {
|
||||
byte: 0x06,
|
||||
bits: 4,
|
||||
},
|
||||
Phase::Write {
|
||||
register: Register::Time,
|
||||
data: [1, 2, 3, 4, 5, 6, 7],
|
||||
offset: 2,
|
||||
byte: 0x55,
|
||||
bits: 3,
|
||||
},
|
||||
Phase::Read {
|
||||
data: [7, 6, 5, 4, 3, 2, 1],
|
||||
len: 3,
|
||||
bit: 17,
|
||||
},
|
||||
Phase::Done,
|
||||
];
|
||||
for phase in phases {
|
||||
let bytes = encoded(&phase);
|
||||
let mut restored = Phase::Done;
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, phase);
|
||||
assert_prefixes(phase);
|
||||
}
|
||||
|
||||
let mut invalid_register = encoded(&Register::Control);
|
||||
invalid_register[4] = 0xff;
|
||||
assert_eq!(
|
||||
Register::Control.load(&mut Reader::new(&invalid_register)),
|
||||
Err(SnapshotError::Corrupt("RTC register"))
|
||||
);
|
||||
|
||||
let mut invalid_phase = encoded(&Phase::Done);
|
||||
invalid_phase[4] = 0xff;
|
||||
assert_eq!(
|
||||
Phase::Done.load(&mut Reader::new(&invalid_phase)),
|
||||
Err(SnapshotError::Corrupt("RTC phase"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use std::{error::Error, fmt};
|
||||
|
||||
use crate::memory::{Memory, MemoryError};
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
// Semantic tests compare architectural state after successful instructions.
|
||||
// Error/rollback tests keep full Cpu equality, including buffered fetch results.
|
||||
|
|
@ -333,3 +334,201 @@ impl Cpu {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_instruction_set(instruction_set: InstructionSet, writer: &mut Writer) {
|
||||
writer.u8(match instruction_set {
|
||||
InstructionSet::Arm => 0,
|
||||
InstructionSet::Thumb => 1,
|
||||
});
|
||||
}
|
||||
|
||||
fn load_instruction_set(reader: &mut Reader<'_>) -> Result<InstructionSet, SnapshotError> {
|
||||
match reader.u8()? {
|
||||
0 => Ok(InstructionSet::Arm),
|
||||
1 => Ok(InstructionSet::Thumb),
|
||||
_ => Err(SnapshotError::Corrupt("instruction set")),
|
||||
}
|
||||
}
|
||||
|
||||
fn save_mode(mode: Mode, writer: &mut Writer) {
|
||||
writer.u8(match mode {
|
||||
Mode::User => 0,
|
||||
Mode::Fiq => 1,
|
||||
Mode::Irq => 2,
|
||||
Mode::Supervisor => 3,
|
||||
Mode::Abort => 4,
|
||||
Mode::Undefined => 5,
|
||||
Mode::System => 6,
|
||||
});
|
||||
}
|
||||
|
||||
fn load_mode(reader: &mut Reader<'_>) -> Result<Mode, SnapshotError> {
|
||||
match reader.u8()? {
|
||||
0 => Ok(Mode::User),
|
||||
1 => Ok(Mode::Fiq),
|
||||
2 => Ok(Mode::Irq),
|
||||
3 => Ok(Mode::Supervisor),
|
||||
4 => Ok(Mode::Abort),
|
||||
5 => Ok(Mode::Undefined),
|
||||
6 => Ok(Mode::System),
|
||||
_ => Err(SnapshotError::Corrupt("processor mode")),
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Cpu {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
registers,
|
||||
flags,
|
||||
instruction_set,
|
||||
mode,
|
||||
irq_disabled,
|
||||
fiq_disabled,
|
||||
banks,
|
||||
pipeline,
|
||||
} = self;
|
||||
writer.tag(b"CPU0");
|
||||
for register in registers {
|
||||
writer.u32(*register);
|
||||
}
|
||||
let Flags {
|
||||
negative,
|
||||
zero,
|
||||
carry,
|
||||
overflow,
|
||||
} = flags;
|
||||
writer.bool(*negative);
|
||||
writer.bool(*zero);
|
||||
writer.bool(*carry);
|
||||
writer.bool(*overflow);
|
||||
save_instruction_set(*instruction_set, writer);
|
||||
save_mode(*mode, writer);
|
||||
writer.bool(*irq_disabled);
|
||||
writer.bool(*fiq_disabled);
|
||||
banks.save_snapshot(writer);
|
||||
writer.option(pipeline.as_ref(), |pipeline, writer| {
|
||||
pipeline.save_snapshot(writer);
|
||||
});
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
registers,
|
||||
flags,
|
||||
instruction_set,
|
||||
mode,
|
||||
irq_disabled,
|
||||
fiq_disabled,
|
||||
banks,
|
||||
pipeline,
|
||||
} = self;
|
||||
reader.tag(b"CPU0")?;
|
||||
for register in registers {
|
||||
*register = reader.u32()?;
|
||||
}
|
||||
let Flags {
|
||||
negative,
|
||||
zero,
|
||||
carry,
|
||||
overflow,
|
||||
} = flags;
|
||||
*negative = reader.bool()?;
|
||||
*zero = reader.bool()?;
|
||||
*carry = reader.bool()?;
|
||||
*overflow = reader.bool()?;
|
||||
*instruction_set = load_instruction_set(reader)?;
|
||||
*mode = load_mode(reader)?;
|
||||
*irq_disabled = reader.bool()?;
|
||||
*fiq_disabled = reader.bool()?;
|
||||
banks.load_snapshot(reader)?;
|
||||
*pipeline = reader.option(|reader| pipeline::Pipeline::load_snapshot(reader))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
use crate::memory::ROM_START;
|
||||
|
||||
fn populated_cpu() -> Cpu {
|
||||
const THUMB_STORE: [u8; 2] = [0x08, 0x60];
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.instruction_set = InstructionSet::Thumb;
|
||||
cpu.registers[0] = 0xaabb_ccdd;
|
||||
cpu.registers[1] = 0x0200_0000;
|
||||
let mut memory = Memory::new(THUMB_STORE.repeat(8)).unwrap();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
|
||||
for (index, register) in cpu.registers[..15].iter_mut().enumerate() {
|
||||
*register = 0x1000 + index as u32;
|
||||
}
|
||||
cpu.registers[15] = ROM_START + 2;
|
||||
cpu.flags = Flags {
|
||||
negative: true,
|
||||
zero: false,
|
||||
carry: true,
|
||||
overflow: true,
|
||||
};
|
||||
cpu.irq_disabled = true;
|
||||
cpu.fiq_disabled = true;
|
||||
|
||||
for (mode, seed) in [
|
||||
(Mode::User, 0x10_000),
|
||||
(Mode::Fiq, 0x20_000),
|
||||
(Mode::Irq, 0x30_000),
|
||||
(Mode::Supervisor, 0x40_000),
|
||||
(Mode::Abort, 0x50_000),
|
||||
(Mode::Undefined, 0x60_000),
|
||||
(Mode::System, 0x70_000),
|
||||
] {
|
||||
cpu.switch_mode(mode);
|
||||
for (index, register) in cpu.registers[8..15].iter_mut().enumerate() {
|
||||
*register = seed + index as u32;
|
||||
}
|
||||
if !matches!(mode, Mode::User | Mode::System) {
|
||||
cpu.set_spsr(seed + 0x800);
|
||||
}
|
||||
}
|
||||
cpu.switch_mode(Mode::Irq);
|
||||
cpu
|
||||
}
|
||||
|
||||
fn save(cpu: &Cpu) -> Vec<u8> {
|
||||
let mut writer = Writer::new();
|
||||
cpu.save(&mut writer);
|
||||
writer.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_round_trips_register_banks_flags_and_pipeline() {
|
||||
let source = populated_cpu();
|
||||
let bytes = save(&source);
|
||||
let mut restored = Cpu::new(0);
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_strict_cpu_prefix_is_rejected() {
|
||||
let bytes = save(&populated_cpu());
|
||||
for end in 0..bytes.len() {
|
||||
let mut restored = Cpu::new(0);
|
||||
assert!(
|
||||
restored.load(&mut Reader::new(&bytes[..end])).is_err(),
|
||||
"accepted prefix of length {end}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_rejects_unknown_instruction_set_discriminant() {
|
||||
let mut bytes = save(&populated_cpu());
|
||||
bytes[72] = 0xff;
|
||||
let mut restored = Cpu::new(0);
|
||||
assert!(matches!(
|
||||
restored.load(&mut Reader::new(&bytes)),
|
||||
Err(SnapshotError::Corrupt(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
//! ARM/Thumb instruction buffering and the next CPU fetch access kind.
|
||||
use super::{Cpu, CpuError, InstructionSet};
|
||||
use crate::{
|
||||
memory::{InstructionFetch, Memory, MemoryError},
|
||||
memory::{load_fetch_result, save_fetch_result, InstructionFetch, Memory, MemoryError},
|
||||
timing::AccessKind,
|
||||
};
|
||||
|
||||
use crate::snapshot::{Reader, SnapshotError, Writer};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct Pipeline {
|
||||
pc: u32,
|
||||
|
|
@ -105,3 +107,40 @@ impl Cpu {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
pub(super) fn save_snapshot(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
pc,
|
||||
instruction_set,
|
||||
instructions,
|
||||
next_access,
|
||||
} = self;
|
||||
writer.u32(*pc);
|
||||
super::save_instruction_set(*instruction_set, writer);
|
||||
for instruction in instructions {
|
||||
save_fetch_result(instruction, writer);
|
||||
}
|
||||
writer.u8(match next_access {
|
||||
AccessKind::NonSequential => 0,
|
||||
AccessKind::Sequential => 1,
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn load_snapshot(reader: &mut Reader<'_>) -> Result<Self, SnapshotError> {
|
||||
let pc = reader.u32()?;
|
||||
let instruction_set = super::load_instruction_set(reader)?;
|
||||
let instructions = [load_fetch_result(reader)?, load_fetch_result(reader)?];
|
||||
let next_access = match reader.u8()? {
|
||||
0 => AccessKind::NonSequential,
|
||||
1 => AccessKind::Sequential,
|
||||
_ => return Err(SnapshotError::Corrupt("access kind")),
|
||||
};
|
||||
Ok(Self {
|
||||
pc,
|
||||
instruction_set,
|
||||
instructions,
|
||||
next_access,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
//! ARM7 processor modes, register banks, and status-register transfers.
|
||||
|
||||
use super::{Cpu, CpuError, Flags, InstructionSet};
|
||||
use crate::snapshot::{Reader, SnapshotError, Writer};
|
||||
|
||||
/// ARMv4T processor mode. User and System share a register bank.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -181,3 +182,40 @@ impl Cpu {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Banks {
|
||||
pub(super) fn save_snapshot(&self, writer: &mut Writer) {
|
||||
let Self { high, sp_lr, spsr } = self;
|
||||
for bank in high {
|
||||
for register in bank {
|
||||
writer.u32(*register);
|
||||
}
|
||||
}
|
||||
for bank in sp_lr {
|
||||
for register in bank {
|
||||
writer.u32(*register);
|
||||
}
|
||||
}
|
||||
for register in spsr {
|
||||
writer.u32(*register);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn load_snapshot(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self { high, sp_lr, spsr } = self;
|
||||
for bank in high {
|
||||
for register in bank {
|
||||
*register = reader.u32()?;
|
||||
}
|
||||
}
|
||||
for bank in sp_lr {
|
||||
for register in bank {
|
||||
*register = reader.u32()?;
|
||||
}
|
||||
}
|
||||
for register in spsr {
|
||||
*register = reader.u32()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
//! or HALT-idle boundaries; display bus contention and IRQ synchronization delays
|
||||
//! are not modeled. Forced blank does not stop this clock.
|
||||
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
pub const CPU_HZ: u32 = 16_777_216;
|
||||
pub const CYCLES_PER_LINE: u32 = 1232;
|
||||
pub const HBLANK_START: u32 = 1006;
|
||||
|
|
@ -130,3 +132,83 @@ fn crossings(start: u32, elapsed: u32, period: u32, offset: u32) -> u64 {
|
|||
let begin = u64::from(start) + shift;
|
||||
(begin + u64::from(elapsed)) / u64::from(period) - begin / u64::from(period)
|
||||
}
|
||||
|
||||
impl Snapshot for Display {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
phase,
|
||||
control,
|
||||
frames,
|
||||
vblanks,
|
||||
} = self;
|
||||
writer.tag(b"DISP");
|
||||
writer.u32(*phase);
|
||||
writer.u16(*control);
|
||||
writer.u64(*frames);
|
||||
writer.u64(*vblanks);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
phase,
|
||||
control,
|
||||
frames,
|
||||
vblanks,
|
||||
} = self;
|
||||
reader.tag(b"DISP")?;
|
||||
*phase = reader.u32()?;
|
||||
*control = reader.u16()?;
|
||||
*frames = reader.u64()?;
|
||||
*vblanks = reader.u64()?;
|
||||
if *phase >= CYCLES_PER_FRAME {
|
||||
return Err(SnapshotError::Corrupt("display phase"));
|
||||
}
|
||||
if *control & !0xff38 != 0 {
|
||||
return Err(SnapshotError::Corrupt("display control"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
fn encode(display: &Display) -> Vec<u8> {
|
||||
let mut writer = Writer::new();
|
||||
display.save(&mut writer);
|
||||
writer.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_and_reject_every_strict_prefix() {
|
||||
let source = Display {
|
||||
phase: CYCLES_PER_LINE * 137 + 1009,
|
||||
control: 0xab38,
|
||||
frames: 0x1234_5678,
|
||||
vblanks: 0x8765_4321,
|
||||
};
|
||||
let bytes = encode(&source);
|
||||
let mut restored = Display::default();
|
||||
let mut reader = Reader::new(&bytes);
|
||||
restored.load(&mut reader).unwrap();
|
||||
reader.finish().unwrap();
|
||||
assert_eq!(restored, source);
|
||||
|
||||
for end in 0..bytes.len() {
|
||||
let mut truncated = Reader::new(&bytes[..end]);
|
||||
assert!(
|
||||
Display::default().load(&mut truncated).is_err(),
|
||||
"accepted prefix of length {end}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_phase_is_corrupt() {
|
||||
let mut bytes = encode(&Display::default());
|
||||
bytes[4..8].copy_from_slice(&CYCLES_PER_FRAME.to_le_bytes());
|
||||
let result = Display::default().load(&mut Reader::new(&bytes));
|
||||
assert!(matches!(result, Err(SnapshotError::Corrupt(_))));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
use crate::{memory::MemoryError, timing::AccessWidth};
|
||||
|
||||
pub const DMA_BASE: u32 = 0x0400_00b0;
|
||||
|
|
@ -283,3 +284,185 @@ fn replace_byte(previous: u32, byte: u32, value: u8) -> u32 {
|
|||
let shift = byte * 8;
|
||||
(previous & !(0xff << shift)) | (u32::from(value) << shift)
|
||||
}
|
||||
|
||||
fn save_channel(channel: &Channel, writer: &mut Writer) {
|
||||
let Channel {
|
||||
source,
|
||||
destination,
|
||||
count,
|
||||
control,
|
||||
current_source,
|
||||
current_destination,
|
||||
remaining,
|
||||
active,
|
||||
first,
|
||||
data_latch,
|
||||
} = channel;
|
||||
writer.u32(*source);
|
||||
writer.u32(*destination);
|
||||
writer.u16(*count);
|
||||
writer.u16(*control);
|
||||
writer.u32(*current_source);
|
||||
writer.u32(*current_destination);
|
||||
writer.u32(*remaining);
|
||||
writer.bool(*active);
|
||||
writer.bool(*first);
|
||||
writer.option(data_latch.as_ref(), |value, writer| writer.u32(*value));
|
||||
}
|
||||
|
||||
fn load_channel(
|
||||
channel: &mut Channel,
|
||||
index: usize,
|
||||
reader: &mut Reader<'_>,
|
||||
) -> Result<(), SnapshotError> {
|
||||
let Channel {
|
||||
source,
|
||||
destination,
|
||||
count,
|
||||
control,
|
||||
current_source,
|
||||
current_destination,
|
||||
remaining,
|
||||
active,
|
||||
first,
|
||||
data_latch,
|
||||
} = channel;
|
||||
*source = reader.u32()?;
|
||||
*destination = reader.u32()?;
|
||||
*count = reader.u16()?;
|
||||
*control = reader.u16()?;
|
||||
*current_source = reader.u32()?;
|
||||
*current_destination = reader.u32()?;
|
||||
*remaining = reader.u32()?;
|
||||
*active = reader.bool()?;
|
||||
*first = reader.bool()?;
|
||||
*data_latch = reader.option(Reader::u32)?;
|
||||
|
||||
let source_mask = if index == 0 { 0x07ff_fffe } else { 0x0fff_fffe };
|
||||
let destination_mask = if index == 3 { 0x0fff_fffe } else { 0x07ff_fffe };
|
||||
let count_mask = if index == 3 { 0xffff } else { 0x3fff };
|
||||
let control_mask = if index == 3 { 0xffe0 } else { 0xf7e0 };
|
||||
let max_remaining = if index == 3 { 0x1_0000 } else { 0x4000 };
|
||||
if *source & !source_mask != 0
|
||||
|| *destination & !destination_mask != 0
|
||||
|| *count & !count_mask != 0
|
||||
|| *control & !control_mask != 0
|
||||
|| *remaining > max_remaining
|
||||
|| (*active && (*remaining == 0 || *control & 0x8000 == 0))
|
||||
{
|
||||
return Err(SnapshotError::Corrupt("DMA channel"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Snapshot for Dma {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self { channels } = self;
|
||||
writer.tag(b"DMA0");
|
||||
for channel in channels {
|
||||
save_channel(channel, writer);
|
||||
}
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self { channels } = self;
|
||||
reader.tag(b"DMA0")?;
|
||||
for (index, channel) in channels.iter_mut().enumerate() {
|
||||
load_channel(channel, index, reader)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
fn state() -> Dma {
|
||||
Dma {
|
||||
channels: [
|
||||
Channel {
|
||||
source: 0x0200_1000,
|
||||
destination: 0x0300_1000,
|
||||
count: 0x1234,
|
||||
control: 0x8000,
|
||||
current_source: 0x0200_2000,
|
||||
current_destination: 0x0300_3000,
|
||||
remaining: 0x1234,
|
||||
active: true,
|
||||
first: true,
|
||||
data_latch: Some(0x1122_3344),
|
||||
},
|
||||
Channel {
|
||||
source: 0x0800_0000,
|
||||
destination: 0x0600_1000,
|
||||
count: 0x2345,
|
||||
control: 0xc660,
|
||||
current_source: 0x0800_1234,
|
||||
current_destination: 0x0600_2344,
|
||||
remaining: 0x2345,
|
||||
active: true,
|
||||
first: false,
|
||||
data_latch: None,
|
||||
},
|
||||
Channel {
|
||||
source: 0x0800_2000,
|
||||
destination: 0x0600_2000,
|
||||
count: 0x0234,
|
||||
control: 0xa000,
|
||||
current_source: 0x0800_3000,
|
||||
current_destination: 0x0600_4000,
|
||||
remaining: 5,
|
||||
active: true,
|
||||
first: false,
|
||||
data_latch: Some(0x5566_7788),
|
||||
},
|
||||
Channel {
|
||||
source: 0x0a00_4000,
|
||||
destination: 0x0800_4000,
|
||||
count: 0xabcd,
|
||||
control: 0x4000,
|
||||
current_source: 0x0a00_5000,
|
||||
current_destination: 0x0800_6000,
|
||||
remaining: 0x4567,
|
||||
active: false,
|
||||
first: true,
|
||||
data_latch: None,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn encode(dma: &Dma) -> Vec<u8> {
|
||||
let mut writer = Writer::new();
|
||||
dma.save(&mut writer);
|
||||
writer.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_and_reject_every_strict_prefix() {
|
||||
let source = state();
|
||||
let bytes = encode(&source);
|
||||
let mut restored = Dma::default();
|
||||
let mut reader = Reader::new(&bytes);
|
||||
restored.load(&mut reader).unwrap();
|
||||
reader.finish().unwrap();
|
||||
assert_eq!(restored, source);
|
||||
|
||||
for end in 0..bytes.len() {
|
||||
let mut truncated = Reader::new(&bytes[..end]);
|
||||
assert!(
|
||||
Dma::default().load(&mut truncated).is_err(),
|
||||
"accepted prefix of length {end}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_boolean_discriminant_is_corrupt() {
|
||||
let mut bytes = encode(&state());
|
||||
bytes[28] = 2;
|
||||
let result = Dma::default().load(&mut Reader::new(&bytes));
|
||||
assert!(matches!(result, Err(SnapshotError::Corrupt(_))));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
//! snapshots, following mGBA's polling model. Exact hardware edges are unverified.
|
||||
|
||||
use super::Buttons;
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct Keypad {
|
||||
|
|
@ -102,3 +103,79 @@ mod tests {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Keypad {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
buttons,
|
||||
control,
|
||||
last_match,
|
||||
} = self;
|
||||
writer.tag(b"KEYP");
|
||||
writer.u16(buttons.bits());
|
||||
writer.u16(*control);
|
||||
writer.option(last_match.as_ref(), |value, writer| writer.u16(*value));
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
buttons,
|
||||
control,
|
||||
last_match,
|
||||
} = self;
|
||||
reader.tag(b"KEYP")?;
|
||||
let button_bits = reader.u16()?;
|
||||
*control = reader.u16()?;
|
||||
*last_match = reader.option(Reader::u16)?;
|
||||
if button_bits & !0x03ff != 0
|
||||
|| *control & !0xc3ff != 0
|
||||
|| last_match.is_some_and(|pressed| pressed & !0x03ff != 0)
|
||||
{
|
||||
return Err(SnapshotError::Corrupt("keypad state"));
|
||||
}
|
||||
*buttons = Buttons::from_bits(button_bits);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
fn encode(keypad: &Keypad) -> Vec<u8> {
|
||||
let mut writer = Writer::new();
|
||||
keypad.save(&mut writer);
|
||||
writer.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_and_reject_every_strict_prefix() {
|
||||
let source = Keypad {
|
||||
buttons: Buttons::from_bits(0x0155),
|
||||
control: 0xc155,
|
||||
last_match: Some(0x02aa),
|
||||
};
|
||||
let bytes = encode(&source);
|
||||
let mut restored = Keypad::default();
|
||||
let mut reader = Reader::new(&bytes);
|
||||
restored.load(&mut reader).unwrap();
|
||||
reader.finish().unwrap();
|
||||
assert_eq!(restored, source);
|
||||
|
||||
for end in 0..bytes.len() {
|
||||
let mut truncated = Reader::new(&bytes[..end]);
|
||||
assert!(
|
||||
Keypad::default().load(&mut truncated).is_err(),
|
||||
"accepted prefix of length {end}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_option_discriminant_is_corrupt() {
|
||||
let mut bytes = encode(&Keypad::default());
|
||||
bytes[8] = 2;
|
||||
let result = Keypad::default().load(&mut Reader::new(&bytes));
|
||||
assert!(matches!(result, Err(SnapshotError::Corrupt(_))));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ pub use serial::{
|
|||
JOYCNT, JOYSTAT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8, SIOMLT_SEND,
|
||||
SIOMULTI0, SIOMULTI1, SIOMULTI2, SIOMULTI3,
|
||||
};
|
||||
mod snapshot;
|
||||
mod timer_step;
|
||||
pub(crate) use timer_step::TimerStep;
|
||||
use timer_step::STAGED_IRQ_MASK;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//! Disconnected serial: normal shifting, idle multiplayer, and local Joybus registers.
|
||||
use super::replace_byte;
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
pub const SIODATA32: u32 = 0x0400_0120;
|
||||
pub const SIOMULTI0: u32 = SIODATA32;
|
||||
|
|
@ -283,3 +284,139 @@ impl Serial {
|
|||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Serial {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
data,
|
||||
multi_extra,
|
||||
send,
|
||||
control,
|
||||
rcnt,
|
||||
joy_control,
|
||||
joy_receive,
|
||||
joy_transmit,
|
||||
joy_status,
|
||||
bits_left,
|
||||
until_bit,
|
||||
} = self;
|
||||
writer.tag(b"SER0");
|
||||
writer.u32(*data);
|
||||
writer.u32(*multi_extra);
|
||||
writer.u16(*send);
|
||||
writer.u16(*control);
|
||||
writer.u16(*rcnt);
|
||||
writer.u8(*joy_control);
|
||||
writer.u32(*joy_receive);
|
||||
writer.u32(*joy_transmit);
|
||||
writer.u8(*joy_status);
|
||||
writer.u8(*bits_left);
|
||||
writer.u32(*until_bit);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
data,
|
||||
multi_extra,
|
||||
send,
|
||||
control,
|
||||
rcnt,
|
||||
joy_control,
|
||||
joy_receive,
|
||||
joy_transmit,
|
||||
joy_status,
|
||||
bits_left,
|
||||
until_bit,
|
||||
} = self;
|
||||
reader.tag(b"SER0")?;
|
||||
*data = reader.u32()?;
|
||||
*multi_extra = reader.u32()?;
|
||||
*send = reader.u16()?;
|
||||
*control = reader.u16()?;
|
||||
*rcnt = reader.u16()?;
|
||||
*joy_control = reader.u8()?;
|
||||
*joy_receive = reader.u32()?;
|
||||
*joy_transmit = reader.u32()?;
|
||||
*joy_status = reader.u8()?;
|
||||
*bits_left = reader.u8()?;
|
||||
*until_bit = reader.u32()?;
|
||||
|
||||
let control_mask = if *control & 0x3000 == 0x2000 {
|
||||
0x6f03
|
||||
} else {
|
||||
0x508b
|
||||
};
|
||||
if *control & !control_mask != 0 {
|
||||
return Err(SnapshotError::Corrupt("serial control"));
|
||||
}
|
||||
if *rcnt & !0xc1ff != 0 || *joy_control & !0x40 != 0 || *joy_status & !0x32 != 0 {
|
||||
return Err(SnapshotError::Corrupt("serial register"));
|
||||
}
|
||||
|
||||
let transfer = *rcnt & 0x8000 == 0 && *control & 0x3000 != 0x2000 && *control & 0x80 != 0;
|
||||
if transfer {
|
||||
let width = if *control & 0x1000 != 0 { 32 } else { 8 };
|
||||
let period = if *control & 2 != 0 { 8 } else { 64 };
|
||||
if *bits_left == 0 || *bits_left > width || *until_bit == 0 || *until_bit > period {
|
||||
return Err(SnapshotError::Corrupt("serial transfer"));
|
||||
}
|
||||
} else if *bits_left != 0 || *until_bit != 0 {
|
||||
return Err(SnapshotError::Corrupt("serial transfer"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
fn state() -> Serial {
|
||||
Serial {
|
||||
data: 0x1234_5678,
|
||||
multi_extra: 0x8765_4321,
|
||||
send: 0x5aa5,
|
||||
control: 0x1083,
|
||||
rcnt: 0x0123,
|
||||
joy_control: 0x40,
|
||||
joy_receive: 0x0bad_cafe,
|
||||
joy_transmit: 0xfeed_face,
|
||||
joy_status: 0x32,
|
||||
bits_left: 23,
|
||||
until_bit: 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn encode(serial: &Serial) -> Vec<u8> {
|
||||
let mut writer = Writer::new();
|
||||
serial.save(&mut writer);
|
||||
writer.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_and_reject_every_strict_prefix() {
|
||||
let source = state();
|
||||
let bytes = encode(&source);
|
||||
let mut restored = Serial::default();
|
||||
let mut reader = Reader::new(&bytes);
|
||||
restored.load(&mut reader).unwrap();
|
||||
reader.finish().unwrap();
|
||||
assert_eq!(restored, source);
|
||||
|
||||
for end in 0..bytes.len() {
|
||||
let mut truncated = Reader::new(&bytes[..end]);
|
||||
assert!(
|
||||
Serial::default().load(&mut truncated).is_err(),
|
||||
"accepted prefix of length {end}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_control_bits_are_corrupt() {
|
||||
let mut bytes = encode(&state());
|
||||
bytes[14..16].copy_from_slice(&u16::MAX.to_le_bytes());
|
||||
let result = Serial::default().load(&mut Reader::new(&bytes));
|
||||
assert!(matches!(result, Err(SnapshotError::Corrupt(_))));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
165
crates/gba-core/src/io/snapshot.rs
Normal file
165
crates/gba-core/src/io/snapshot.rs
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
//! Save-state encoding for the I/O register file and its timers.
|
||||
|
||||
use super::{Io, PowerState, Timer};
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
/// Timer control bits that the register can hold (see `Timer::set_control`).
|
||||
const TIMER_CONTROL_MASK: u8 = 0xc7;
|
||||
|
||||
impl Snapshot for Timer {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
reload,
|
||||
counter,
|
||||
control,
|
||||
} = self;
|
||||
writer.u16(*reload);
|
||||
writer.u16(*counter);
|
||||
writer.u8(*control);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
reload,
|
||||
counter,
|
||||
control,
|
||||
} = self;
|
||||
*reload = reader.u16()?;
|
||||
*counter = reader.u16()?;
|
||||
*control = reader.u8()?;
|
||||
if *control & !TIMER_CONTROL_MASK != 0 {
|
||||
return Err(SnapshotError::Corrupt("timer control"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PowerState {
|
||||
fn to_byte(self) -> u8 {
|
||||
match self {
|
||||
Self::Running => 0,
|
||||
Self::Halt => 1,
|
||||
Self::Stop => 2,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_byte(byte: u8) -> Result<Self, SnapshotError> {
|
||||
match byte {
|
||||
0 => Ok(Self::Running),
|
||||
1 => Ok(Self::Halt),
|
||||
2 => Ok(Self::Stop),
|
||||
_ => Err(SnapshotError::Corrupt("power state")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Io {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
serial,
|
||||
audio,
|
||||
audio_capture,
|
||||
timers,
|
||||
timer_phase,
|
||||
enable,
|
||||
pending,
|
||||
master_enable,
|
||||
waitcnt,
|
||||
dispcnt,
|
||||
greenswap,
|
||||
mosaic,
|
||||
vertical_mosaic,
|
||||
backgrounds,
|
||||
effects,
|
||||
window_edges,
|
||||
horizontal_windows,
|
||||
keypad,
|
||||
display,
|
||||
dma,
|
||||
postflg,
|
||||
power,
|
||||
} = self;
|
||||
writer.tag(b"IO00");
|
||||
serial.save(writer);
|
||||
audio.save(writer);
|
||||
audio_capture.save(writer);
|
||||
for timer in timers {
|
||||
timer.save(writer);
|
||||
}
|
||||
writer.u16(*timer_phase);
|
||||
writer.u16(*enable);
|
||||
writer.u16(*pending);
|
||||
writer.bool(*master_enable);
|
||||
writer.u16(*waitcnt);
|
||||
writer.u16(*dispcnt);
|
||||
writer.bool(*greenswap);
|
||||
writer.u16(*mosaic);
|
||||
vertical_mosaic.save(writer);
|
||||
for background in backgrounds {
|
||||
background.save(writer);
|
||||
}
|
||||
effects.save(writer);
|
||||
window_edges.save(writer);
|
||||
horizontal_windows.save(writer);
|
||||
keypad.save(writer);
|
||||
display.save(writer);
|
||||
dma.save(writer);
|
||||
writer.bool(*postflg);
|
||||
writer.u8(power.to_byte());
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
serial,
|
||||
audio,
|
||||
audio_capture,
|
||||
timers,
|
||||
timer_phase,
|
||||
enable,
|
||||
pending,
|
||||
master_enable,
|
||||
waitcnt,
|
||||
dispcnt,
|
||||
greenswap,
|
||||
mosaic,
|
||||
vertical_mosaic,
|
||||
backgrounds,
|
||||
effects,
|
||||
window_edges,
|
||||
horizontal_windows,
|
||||
keypad,
|
||||
display,
|
||||
dma,
|
||||
postflg,
|
||||
power,
|
||||
} = self;
|
||||
reader.tag(b"IO00")?;
|
||||
serial.load(reader)?;
|
||||
audio.load(reader)?;
|
||||
audio_capture.load(reader)?;
|
||||
for timer in timers {
|
||||
timer.load(reader)?;
|
||||
}
|
||||
*timer_phase = reader.u16()?;
|
||||
*enable = reader.u16()?;
|
||||
*pending = reader.u16()?;
|
||||
*master_enable = reader.bool()?;
|
||||
*waitcnt = reader.u16()?;
|
||||
*dispcnt = reader.u16()?;
|
||||
*greenswap = reader.bool()?;
|
||||
*mosaic = reader.u16()?;
|
||||
vertical_mosaic.load(reader)?;
|
||||
for background in backgrounds {
|
||||
background.load(reader)?;
|
||||
}
|
||||
effects.load(reader)?;
|
||||
window_edges.load(reader)?;
|
||||
horizontal_windows.load(reader)?;
|
||||
keypad.load(reader)?;
|
||||
display.load(reader)?;
|
||||
dma.load(reader)?;
|
||||
*postflg = reader.bool()?;
|
||||
*power = PowerState::from_byte(reader.u8()?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -10,5 +10,6 @@ pub mod input;
|
|||
pub mod io;
|
||||
pub mod machine;
|
||||
pub mod memory;
|
||||
pub mod snapshot;
|
||||
pub mod timing;
|
||||
pub mod video;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use crate::{
|
|||
memory::Memory,
|
||||
timing::StepTiming,
|
||||
};
|
||||
mod snapshot;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StepKind {
|
||||
|
|
|
|||
131
crates/gba-core/src/machine/snapshot.rs
Normal file
131
crates/gba-core/src/machine/snapshot.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
//! Whole-machine save states.
|
||||
//!
|
||||
//! Layout: `GBAS`, format version, ROM length and fingerprint, firmware
|
||||
//! presence and fingerprint, cartridge setup, payload length, payload, then a
|
||||
//! fingerprint of every preceding byte. The payload is CPU, memory bus (with all
|
||||
//! devices), and last step timing, each in the positional encoding of
|
||||
//! [`crate::snapshot`].
|
||||
|
||||
use super::Machine;
|
||||
use crate::snapshot::{fingerprint, Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
const MAGIC: &[u8; 4] = b"GBAS";
|
||||
/// Bump on any change to an encoded type; older states are rejected, not migrated.
|
||||
const VERSION: u32 = 1;
|
||||
const CHECKSUM_LEN: usize = 8;
|
||||
|
||||
impl Machine {
|
||||
/// Encode the emulated hardware at the current step boundary.
|
||||
///
|
||||
/// The state holds no ROM or firmware bytes: it records their fingerprints and
|
||||
/// can be restored only into a machine built from the same images and the same
|
||||
/// cartridge hardware/save-device selection. Fails with `MidStep` inside a step
|
||||
/// and `Unsupported` when a deferred diagnostic cannot be encoded.
|
||||
pub fn save_state(&self) -> Result<Vec<u8>, SnapshotError> {
|
||||
let mut payload = Writer::new();
|
||||
self.cpu.save(&mut payload);
|
||||
self.memory.save(&mut payload);
|
||||
save_timing(&self.last_timing, &mut payload);
|
||||
let payload = payload.finish()?;
|
||||
|
||||
let mut writer = Writer::new();
|
||||
writer.raw(MAGIC);
|
||||
writer.u32(VERSION);
|
||||
self.write_identity(&mut writer);
|
||||
writer.bytes(&payload);
|
||||
let mut bytes = writer.finish()?;
|
||||
let checksum = fingerprint(&bytes);
|
||||
bytes.extend_from_slice(&checksum.to_le_bytes());
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Restore a state produced by [`Machine::save_state`].
|
||||
///
|
||||
/// The format, checksum, and image/cartridge identity are all verified before
|
||||
/// any machine state changes, so every such failure leaves the machine intact.
|
||||
/// A later decoding error is reported as `RestoreFailed`: it indicates an encoder
|
||||
/// defect, and the machine is then unspecified and must be rebuilt from the ROM.
|
||||
/// Host configuration (ROM, firmware, cartridge setup, row-capture and
|
||||
/// audio-capture enablement) is kept; capture queues and counters are cleared.
|
||||
pub fn load_state(&mut self, bytes: &[u8]) -> Result<(), SnapshotError> {
|
||||
if bytes.len() < MAGIC.len() + 4 + CHECKSUM_LEN {
|
||||
return Err(SnapshotError::Truncated);
|
||||
}
|
||||
if &bytes[..MAGIC.len()] != MAGIC {
|
||||
return Err(SnapshotError::BadMagic);
|
||||
}
|
||||
let mut reader = Reader::new(&bytes[MAGIC.len()..]);
|
||||
let version = reader.u32()?;
|
||||
if version != VERSION {
|
||||
return Err(SnapshotError::UnsupportedVersion(version));
|
||||
}
|
||||
let (body, checksum) = bytes.split_at(bytes.len() - CHECKSUM_LEN);
|
||||
if fingerprint(body).to_le_bytes() != checksum {
|
||||
return Err(SnapshotError::Checksum);
|
||||
}
|
||||
let mut reader = Reader::new(&body[MAGIC.len() + 4..]);
|
||||
self.check_identity(&mut reader)?;
|
||||
let payload = reader.bytes_vec(body.len())?;
|
||||
reader.finish()?;
|
||||
|
||||
let mut payload = Reader::new(&payload);
|
||||
self.restore(&mut payload)
|
||||
.map_err(|cause| SnapshotError::RestoreFailed(Box::new(cause)))
|
||||
}
|
||||
|
||||
fn restore(&mut self, payload: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
self.cpu.load(payload)?;
|
||||
self.memory.load(payload)?;
|
||||
self.last_timing = load_timing(payload)?;
|
||||
payload.finish()
|
||||
}
|
||||
|
||||
fn write_identity(&self, writer: &mut Writer) {
|
||||
let (rom, bios) = self.memory.images();
|
||||
writer.u64(rom.len() as u64);
|
||||
writer.u64(fingerprint(rom));
|
||||
writer.option(bios.as_ref(), |bios, writer| {
|
||||
writer.u64(fingerprint(bios));
|
||||
});
|
||||
writer.raw(&self.memory.setup_identity());
|
||||
}
|
||||
|
||||
fn check_identity(&self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let (rom, bios) = self.memory.images();
|
||||
if reader.u64()? != rom.len() as u64 || reader.u64()? != fingerprint(rom) {
|
||||
return Err(SnapshotError::ConfigMismatch("ROM image"));
|
||||
}
|
||||
let saved_bios = reader.option(Reader::u64)?;
|
||||
if saved_bios != bios.map(fingerprint) {
|
||||
return Err(SnapshotError::ConfigMismatch("firmware image"));
|
||||
}
|
||||
let mut setup = [0; 2];
|
||||
reader.raw_into(&mut setup)?;
|
||||
if setup != self.memory.setup_identity() {
|
||||
return Err(SnapshotError::ConfigMismatch("cartridge setup"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn save_timing(timing: &crate::timing::StepTiming, writer: &mut Writer) {
|
||||
let crate::timing::StepTiming {
|
||||
code_cycles,
|
||||
data_cycles,
|
||||
internal_cycles,
|
||||
idle_cycles,
|
||||
} = timing;
|
||||
writer.u32(*code_cycles);
|
||||
writer.u32(*data_cycles);
|
||||
writer.u32(*internal_cycles);
|
||||
writer.u32(*idle_cycles);
|
||||
}
|
||||
|
||||
fn load_timing(reader: &mut Reader<'_>) -> Result<crate::timing::StepTiming, SnapshotError> {
|
||||
Ok(crate::timing::StepTiming {
|
||||
code_cycles: reader.u32()?,
|
||||
data_cycles: reader.u32()?,
|
||||
internal_cycles: reader.u32()?,
|
||||
idle_cycles: reader.u32()?,
|
||||
})
|
||||
}
|
||||
|
|
@ -26,13 +26,14 @@ mod multiplayer_step_tests;
|
|||
mod prefetch_tests;
|
||||
#[cfg(test)]
|
||||
mod serial_step_tests;
|
||||
mod snapshot;
|
||||
#[cfg(test)]
|
||||
mod timer_step_tests;
|
||||
#[cfg(test)]
|
||||
mod timing_event_tests;
|
||||
#[cfg(test)]
|
||||
mod wave_step_tests;
|
||||
pub(crate) use fetch::InstructionFetch;
|
||||
pub(crate) use fetch::{load_fetch_result, save_fetch_result, InstructionFetch};
|
||||
use iwram_bus::IwramBus;
|
||||
|
||||
use crate::{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
//! One mapped sample supplies instruction bits and the supported bus observation.
|
||||
//! Sampling is side-effect-free; execution commits history under the diagnostic policy.
|
||||
use super::{InstructionSet, Memory, MemoryError};
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct InstructionFetch {
|
||||
|
|
@ -75,3 +76,170 @@ impl Memory {
|
|||
Ok(u32::from_le_bytes(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for InstructionFetch {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
address,
|
||||
state,
|
||||
instruction,
|
||||
bus_word,
|
||||
} = self;
|
||||
writer.tag(b"FETC");
|
||||
writer.u32(*address);
|
||||
writer.u8(match state {
|
||||
InstructionSet::Arm => 0,
|
||||
InstructionSet::Thumb => 1,
|
||||
});
|
||||
save_fetch_result(instruction, writer);
|
||||
writer.option(bus_word.as_ref(), |word, writer| writer.u32(*word));
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
address,
|
||||
state,
|
||||
instruction,
|
||||
bus_word,
|
||||
} = self;
|
||||
reader.tag(b"FETC")?;
|
||||
*address = reader.u32()?;
|
||||
*state = match reader.u8()? {
|
||||
0 => InstructionSet::Arm,
|
||||
1 => InstructionSet::Thumb,
|
||||
_ => return Err(SnapshotError::Corrupt("instruction set")),
|
||||
};
|
||||
*instruction = load_fetch_result(reader)?;
|
||||
*bus_word = reader.option(|reader| reader.u32())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A deferred fetch diagnostic is stored beside the instruction it would have
|
||||
/// produced. Address-only errors are encoded; errors that carry a static
|
||||
/// description string are not, and fail the save.
|
||||
pub(crate) fn save_fetch_result(instruction: &Result<u32, MemoryError>, writer: &mut Writer) {
|
||||
match instruction {
|
||||
Ok(value) => {
|
||||
writer.u8(0);
|
||||
writer.u32(*value);
|
||||
}
|
||||
Err(MemoryError::Unmapped(address)) => {
|
||||
writer.u8(1);
|
||||
writer.u32(*address);
|
||||
}
|
||||
Err(MemoryError::ReadOnly(address)) => {
|
||||
writer.u8(2);
|
||||
writer.u32(*address);
|
||||
}
|
||||
Err(MemoryError::Unaligned(address)) => {
|
||||
writer.u8(3);
|
||||
writer.u32(*address);
|
||||
}
|
||||
Err(_) => {
|
||||
writer.u8(0xff);
|
||||
writer.fail(SnapshotError::Unsupported("deferred fetch diagnostic"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_fetch_result(
|
||||
reader: &mut Reader<'_>,
|
||||
) -> Result<Result<u32, MemoryError>, SnapshotError> {
|
||||
Ok(match reader.u8()? {
|
||||
0 => Ok(reader.u32()?),
|
||||
1 => Err(MemoryError::Unmapped(reader.u32()?)),
|
||||
2 => Err(MemoryError::ReadOnly(reader.u32()?)),
|
||||
3 => Err(MemoryError::Unaligned(reader.u32()?)),
|
||||
_ => return Err(SnapshotError::Corrupt("fetch result")),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
fn populated_fetch() -> InstructionFetch {
|
||||
InstructionFetch {
|
||||
address: 0x0800_0022,
|
||||
state: InstructionSet::Thumb,
|
||||
instruction: Ok(0x46c0),
|
||||
bus_word: Some(0x1234_46c0),
|
||||
}
|
||||
}
|
||||
|
||||
fn save(fetch: &InstructionFetch) -> Vec<u8> {
|
||||
let mut writer = Writer::new();
|
||||
fetch.save(&mut writer);
|
||||
writer.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instruction_fetch_round_trips_all_fields() {
|
||||
let source = populated_fetch();
|
||||
let bytes = save(&source);
|
||||
let mut restored = InstructionFetch {
|
||||
address: 0,
|
||||
state: InstructionSet::Arm,
|
||||
instruction: Ok(0),
|
||||
bus_word: None,
|
||||
};
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_strict_fetch_prefix_is_rejected() {
|
||||
let bytes = save(&populated_fetch());
|
||||
for end in 0..bytes.len() {
|
||||
let mut restored = populated_fetch();
|
||||
assert!(
|
||||
restored.load(&mut Reader::new(&bytes[..end])).is_err(),
|
||||
"accepted prefix of length {end}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instruction_fetch_rejects_unknown_instruction_set() {
|
||||
let mut bytes = save(&populated_fetch());
|
||||
bytes[8] = 0xff;
|
||||
let mut restored = populated_fetch();
|
||||
assert!(matches!(
|
||||
restored.load(&mut Reader::new(&bytes)),
|
||||
Err(SnapshotError::Corrupt(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn address_diagnostics_round_trip_and_described_ones_cannot_be_saved() {
|
||||
for error in [
|
||||
MemoryError::Unmapped(0x0200_0000),
|
||||
MemoryError::ReadOnly(0x0800_0000),
|
||||
MemoryError::Unaligned(0x0300_0001),
|
||||
] {
|
||||
let source = InstructionFetch {
|
||||
instruction: Err(error),
|
||||
..populated_fetch()
|
||||
};
|
||||
let bytes = save(&source);
|
||||
let mut restored = populated_fetch();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, source);
|
||||
}
|
||||
let fetch = InstructionFetch {
|
||||
instruction: Err(MemoryError::UnsupportedIo {
|
||||
address: 0x0400_0000,
|
||||
value: 0,
|
||||
operation: "write",
|
||||
}),
|
||||
..populated_fetch()
|
||||
};
|
||||
let mut writer = Writer::new();
|
||||
fetch.save(&mut writer);
|
||||
assert_eq!(
|
||||
writer.finish(),
|
||||
Err(SnapshotError::Unsupported("deferred fetch diagnostic"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
//! CPU instructions stage changes; successful DMA units commit accesses in order.
|
||||
use std::cell::Cell;
|
||||
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
use crate::timing::AccessWidth;
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
|
|
@ -115,3 +116,101 @@ mod tests {
|
|||
assert_eq!(bus.snapshot(), Some(0x5678_3344));
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for IwramBus {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self { committed, pending } = self;
|
||||
if pending.get().is_some() {
|
||||
writer.fail(SnapshotError::MidStep);
|
||||
return;
|
||||
}
|
||||
writer.tag(b"IWBS");
|
||||
let Lanes { value, known } = committed;
|
||||
writer.u32(*value);
|
||||
writer.u32(*known);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self { committed, pending } = self;
|
||||
reader.tag(b"IWBS")?;
|
||||
if pending.get().is_some() {
|
||||
return Err(SnapshotError::MidStep);
|
||||
}
|
||||
let value = reader.u32()?;
|
||||
let known = reader.u32()?;
|
||||
if value & !known != 0
|
||||
|| [0x0000_00ff, 0x0000_ff00, 0x00ff_0000, 0xff00_0000]
|
||||
.into_iter()
|
||||
.any(|lane| {
|
||||
let bits = known & lane;
|
||||
bits != 0 && bits != lane
|
||||
})
|
||||
{
|
||||
return Err(SnapshotError::Corrupt("IWRAM bus lanes"));
|
||||
}
|
||||
*committed = Lanes { value, known };
|
||||
pending.set(None);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
fn populated_bus() -> IwramBus {
|
||||
let mut bus = IwramBus::default();
|
||||
bus.dma_access(0x0300_0000, AccessWidth::Byte, 0x12);
|
||||
bus.dma_access(0x0300_0002, AccessWidth::Halfword, 0x3456);
|
||||
bus
|
||||
}
|
||||
|
||||
fn save(bus: &IwramBus) -> Vec<u8> {
|
||||
let mut writer = Writer::new();
|
||||
bus.save(&mut writer);
|
||||
writer.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iwram_bus_round_trips_committed_lane_history() {
|
||||
let source = populated_bus();
|
||||
let bytes = save(&source);
|
||||
let mut restored = IwramBus::default();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
let mut writer = Writer::new();
|
||||
restored.save(&mut writer);
|
||||
assert_eq!(writer.finish().unwrap(), bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_strict_iwram_bus_prefix_is_rejected() {
|
||||
let bytes = save(&populated_bus());
|
||||
for end in 0..bytes.len() {
|
||||
let mut restored = IwramBus::default();
|
||||
assert!(
|
||||
restored.load(&mut Reader::new(&bytes[..end])).is_err(),
|
||||
"accepted prefix of length {end}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iwram_bus_rejects_values_in_unknown_lanes() {
|
||||
let mut bytes = save(&populated_bus());
|
||||
bytes[5] |= 0x80;
|
||||
let mut restored = IwramBus::default();
|
||||
assert!(matches!(
|
||||
restored.load(&mut Reader::new(&bytes)),
|
||||
Err(SnapshotError::Corrupt(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iwram_bus_rejects_transaction_staging() {
|
||||
let mut bus = populated_bus();
|
||||
bus.begin();
|
||||
let mut writer = Writer::new();
|
||||
bus.save(&mut writer);
|
||||
assert_eq!(writer.finish(), Err(SnapshotError::MidStep));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
142
crates/gba-core/src/memory/snapshot.rs
Normal file
142
crates/gba-core/src/memory/snapshot.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
//! Save-state encoding for the memory bus and everything it owns.
|
||||
|
||||
use super::Memory;
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
use crate::video::capture::Capture;
|
||||
|
||||
impl Memory {
|
||||
/// ROM and firmware bytes, for state identity checks.
|
||||
pub(crate) fn images(&self) -> (&[u8], Option<&[u8]>) {
|
||||
(&self.rom, self.bios.as_deref())
|
||||
}
|
||||
|
||||
/// Cartridge hardware and save-device selection, which a state must match.
|
||||
pub(crate) fn setup_identity(&self) -> [u8; 2] {
|
||||
self.cartridge.setup_identity()
|
||||
}
|
||||
|
||||
/// Staged per-step transactions exist only inside `Machine::step`.
|
||||
fn mid_step(&self) -> bool {
|
||||
self.cartridge_step.get().is_some()
|
||||
|| self.timer_step.get().is_some()
|
||||
|| self.cpu_timing.get().is_some()
|
||||
|| self.cpu_access.is_some()
|
||||
|| self.cartridge_elapsed.get() != 0
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Memory {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
if self.mid_step() {
|
||||
writer.fail(SnapshotError::MidStep);
|
||||
return;
|
||||
}
|
||||
let Self {
|
||||
external_ram,
|
||||
internal_ram,
|
||||
palette_ram,
|
||||
video_ram,
|
||||
oam,
|
||||
rom: _, // Host image, identified by the state header.
|
||||
save_image,
|
||||
save_modified,
|
||||
cartridge_elapsed: _, // Zero between steps; checked above.
|
||||
cartridge,
|
||||
cartridge_step: _, // Staged transaction; empty between steps.
|
||||
bios: _, // Host image, identified by the state header.
|
||||
io,
|
||||
cycles,
|
||||
cpu_timing: _, // Staged transaction; empty between steps.
|
||||
timer_step: _, // Staged transaction; empty between steps.
|
||||
gamepak_prefetch,
|
||||
#[cfg(test)]
|
||||
last_cpu_timing: _, // Test trace only.
|
||||
cpu_resume_nonsequential,
|
||||
cpu_access: _, // Staged transaction; empty between steps.
|
||||
bios_prefetch,
|
||||
iwram_bus,
|
||||
scanline_capture,
|
||||
sprite_pipeline,
|
||||
} = self;
|
||||
writer.tag(b"MEM0");
|
||||
// The cartridge comes first so a differing save device reports a
|
||||
// configuration mismatch before any memory-size error.
|
||||
cartridge.save(writer);
|
||||
writer.bytes(external_ram);
|
||||
writer.bytes(internal_ram);
|
||||
writer.bytes(palette_ram);
|
||||
writer.bytes(video_ram);
|
||||
writer.bytes(oam);
|
||||
writer.bytes(save_image);
|
||||
writer.bool(*save_modified);
|
||||
io.save(writer);
|
||||
writer.u64(*cycles);
|
||||
gamepak_prefetch.save(writer);
|
||||
writer.bool(*cpu_resume_nonsequential);
|
||||
writer.option(bios_prefetch.as_ref(), |value, writer| writer.u32(*value));
|
||||
iwram_bus.save(writer);
|
||||
writer.option(scanline_capture.as_ref(), |capture, writer| {
|
||||
capture.save(writer);
|
||||
});
|
||||
sprite_pipeline.save(writer);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
if self.mid_step() {
|
||||
return Err(SnapshotError::MidStep);
|
||||
}
|
||||
let Self {
|
||||
external_ram,
|
||||
internal_ram,
|
||||
palette_ram,
|
||||
video_ram,
|
||||
oam,
|
||||
rom: _,
|
||||
save_image,
|
||||
save_modified,
|
||||
cartridge_elapsed: _,
|
||||
cartridge,
|
||||
cartridge_step: _,
|
||||
bios: _,
|
||||
io,
|
||||
cycles,
|
||||
cpu_timing: _,
|
||||
timer_step: _,
|
||||
gamepak_prefetch,
|
||||
#[cfg(test)]
|
||||
last_cpu_timing: _,
|
||||
cpu_resume_nonsequential,
|
||||
cpu_access: _,
|
||||
bios_prefetch,
|
||||
iwram_bus,
|
||||
scanline_capture,
|
||||
sprite_pipeline,
|
||||
} = self;
|
||||
reader.tag(b"MEM0")?;
|
||||
cartridge.load(reader)?;
|
||||
reader.bytes_into(external_ram)?;
|
||||
reader.bytes_into(internal_ram)?;
|
||||
reader.bytes_into(palette_ram)?;
|
||||
reader.bytes_into(video_ram)?;
|
||||
reader.bytes_into(oam)?;
|
||||
reader.bytes_into(save_image)?;
|
||||
*save_modified = reader.bool()?;
|
||||
io.load(reader)?;
|
||||
*cycles = reader.u64()?;
|
||||
gamepak_prefetch.load(reader)?;
|
||||
*cpu_resume_nonsequential = reader.bool()?;
|
||||
*bios_prefetch = reader.option(Reader::u32)?;
|
||||
iwram_bus.load(reader)?;
|
||||
// Whether row capture runs is host configuration; only its contents are state.
|
||||
let captured = reader.option(|reader| {
|
||||
let mut capture = Capture::default();
|
||||
capture.load(reader)?;
|
||||
Ok(capture)
|
||||
})?;
|
||||
if scanline_capture.is_some() {
|
||||
*scanline_capture = Some(captured.unwrap_or_default());
|
||||
}
|
||||
sprite_pipeline.load(reader)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
339
crates/gba-core/src/snapshot.rs
Normal file
339
crates/gba-core/src/snapshot.rs
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
//! Binary encoding primitives for save states.
|
||||
//!
|
||||
//! A state captures emulated hardware only. Configuration chosen by the host
|
||||
//! (ROM, firmware image, cartridge hardware and save-device selection) is
|
||||
//! supplied when the target machine is built and is checked, not restored.
|
||||
//! Every multi-byte value is little-endian. The encoding is positional: a type
|
||||
//! must read exactly what it wrote, in the same order. Section tags make a
|
||||
//! misaligned reader fail at the next boundary instead of decoding garbage.
|
||||
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SnapshotError {
|
||||
/// The stream ended before a value or section was complete.
|
||||
Truncated,
|
||||
/// Bytes remain after the final section.
|
||||
TrailingData,
|
||||
/// The header does not identify a save state.
|
||||
BadMagic,
|
||||
/// The format version is not understood by this build.
|
||||
UnsupportedVersion(u32),
|
||||
/// The payload does not match its recorded checksum.
|
||||
Checksum,
|
||||
/// The state was captured with a different ROM, firmware, or cartridge setup.
|
||||
ConfigMismatch(&'static str),
|
||||
/// A section tag, enum value, or length is outside its valid range.
|
||||
Corrupt(&'static str),
|
||||
/// The machine is inside a CPU step; states exist only between steps.
|
||||
MidStep,
|
||||
/// The machine holds live state that has no encoding, such as a deferred
|
||||
/// diagnostic error. Saving at a later step boundary normally succeeds.
|
||||
Unsupported(&'static str),
|
||||
/// Decoding failed after the machine began to change. The machine state is
|
||||
/// unspecified and must be rebuilt. Valid states never produce this.
|
||||
RestoreFailed(Box<SnapshotError>),
|
||||
}
|
||||
|
||||
impl fmt::Display for SnapshotError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Truncated => write!(formatter, "save state is truncated"),
|
||||
Self::TrailingData => write!(formatter, "save state has trailing data"),
|
||||
Self::BadMagic => write!(formatter, "not a save state"),
|
||||
Self::UnsupportedVersion(version) => {
|
||||
write!(formatter, "unsupported save-state version {version}")
|
||||
}
|
||||
Self::Checksum => write!(formatter, "save state checksum mismatch"),
|
||||
Self::ConfigMismatch(what) => {
|
||||
write!(formatter, "save state does not match this machine: {what}")
|
||||
}
|
||||
Self::Corrupt(what) => write!(formatter, "corrupt save state: {what}"),
|
||||
Self::MidStep => write!(formatter, "cannot save state inside a CPU step"),
|
||||
Self::Unsupported(what) => write!(formatter, "cannot save state: {what}"),
|
||||
Self::RestoreFailed(cause) => {
|
||||
write!(
|
||||
formatter,
|
||||
"restore failed midway, machine must be rebuilt: {cause}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for SnapshotError {}
|
||||
|
||||
/// Hardware state that survives in a save state.
|
||||
///
|
||||
/// `load` overwrites emulated state in place and keeps host configuration.
|
||||
/// A failed `load` may leave `self` partly overwritten; callers validate the
|
||||
/// whole stream (`Machine::load_state` does) before the first mutation.
|
||||
pub(crate) trait Snapshot {
|
||||
fn save(&self, writer: &mut Writer);
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Writer {
|
||||
bytes: Vec<u8>,
|
||||
error: Option<SnapshotError>,
|
||||
}
|
||||
|
||||
impl Writer {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// The first recorded failure, or the encoded bytes.
|
||||
pub(crate) fn finish(self) -> Result<Vec<u8>, SnapshotError> {
|
||||
match self.error {
|
||||
Some(error) => Err(error),
|
||||
None => Ok(self.bytes),
|
||||
}
|
||||
}
|
||||
|
||||
/// Record that the current state cannot be encoded. The first failure wins;
|
||||
/// callers may keep writing, and `finish` reports it.
|
||||
pub(crate) fn fail(&mut self, error: SnapshotError) {
|
||||
self.error.get_or_insert(error);
|
||||
}
|
||||
|
||||
/// Four-byte section marker, checked by `Reader::tag`.
|
||||
pub(crate) fn tag(&mut self, tag: &[u8; 4]) {
|
||||
self.bytes.extend_from_slice(tag);
|
||||
}
|
||||
|
||||
pub(crate) fn u8(&mut self, value: u8) {
|
||||
self.bytes.push(value);
|
||||
}
|
||||
|
||||
pub(crate) fn bool(&mut self, value: bool) {
|
||||
self.u8(u8::from(value));
|
||||
}
|
||||
|
||||
pub(crate) fn u16(&mut self, value: u16) {
|
||||
self.bytes.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
|
||||
pub(crate) fn i16(&mut self, value: i16) {
|
||||
self.bytes.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
|
||||
pub(crate) fn u32(&mut self, value: u32) {
|
||||
self.bytes.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
|
||||
pub(crate) fn u64(&mut self, value: u64) {
|
||||
self.bytes.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
|
||||
/// Raw bytes with no length prefix. The reader must know the length.
|
||||
pub(crate) fn raw(&mut self, bytes: &[u8]) {
|
||||
self.bytes.extend_from_slice(bytes);
|
||||
}
|
||||
|
||||
/// Length-prefixed bytes.
|
||||
pub(crate) fn bytes(&mut self, bytes: &[u8]) {
|
||||
self.u32(u32::try_from(bytes.len()).expect("snapshot field exceeds 4 GiB"));
|
||||
self.raw(bytes);
|
||||
}
|
||||
|
||||
/// `false`, or `true` followed by the value written by `save`.
|
||||
pub(crate) fn option<T>(&mut self, value: Option<&T>, save: impl FnOnce(&T, &mut Self)) {
|
||||
self.bool(value.is_some());
|
||||
if let Some(value) = value {
|
||||
save(value, self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Reader<'a> {
|
||||
bytes: &'a [u8],
|
||||
}
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
pub(crate) fn new(bytes: &'a [u8]) -> Self {
|
||||
Self { bytes }
|
||||
}
|
||||
|
||||
pub(crate) fn remaining(&self) -> usize {
|
||||
self.bytes.len()
|
||||
}
|
||||
|
||||
/// Fails with `TrailingData` unless every byte was consumed.
|
||||
pub(crate) fn finish(&self) -> Result<(), SnapshotError> {
|
||||
if self.bytes.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SnapshotError::TrailingData)
|
||||
}
|
||||
}
|
||||
|
||||
fn take(&mut self, length: usize) -> Result<&'a [u8], SnapshotError> {
|
||||
if self.bytes.len() < length {
|
||||
return Err(SnapshotError::Truncated);
|
||||
}
|
||||
let (head, tail) = self.bytes.split_at(length);
|
||||
self.bytes = tail;
|
||||
Ok(head)
|
||||
}
|
||||
|
||||
fn array<const N: usize>(&mut self) -> Result<[u8; N], SnapshotError> {
|
||||
let mut array = [0; N];
|
||||
array.copy_from_slice(self.take(N)?);
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
pub(crate) fn tag(&mut self, expected: &[u8; 4]) -> Result<(), SnapshotError> {
|
||||
if self.take(4)? == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SnapshotError::Corrupt("section tag"))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn u8(&mut self) -> Result<u8, SnapshotError> {
|
||||
Ok(self.array::<1>()?[0])
|
||||
}
|
||||
|
||||
pub(crate) fn bool(&mut self) -> Result<bool, SnapshotError> {
|
||||
match self.u8()? {
|
||||
0 => Ok(false),
|
||||
1 => Ok(true),
|
||||
_ => Err(SnapshotError::Corrupt("boolean")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn u16(&mut self) -> Result<u16, SnapshotError> {
|
||||
Ok(u16::from_le_bytes(self.array()?))
|
||||
}
|
||||
|
||||
pub(crate) fn i16(&mut self) -> Result<i16, SnapshotError> {
|
||||
Ok(i16::from_le_bytes(self.array()?))
|
||||
}
|
||||
|
||||
pub(crate) fn u32(&mut self) -> Result<u32, SnapshotError> {
|
||||
Ok(u32::from_le_bytes(self.array()?))
|
||||
}
|
||||
|
||||
pub(crate) fn u64(&mut self) -> Result<u64, SnapshotError> {
|
||||
Ok(u64::from_le_bytes(self.array()?))
|
||||
}
|
||||
|
||||
/// Fill `destination` from bytes written by `Writer::raw`.
|
||||
pub(crate) fn raw_into(&mut self, destination: &mut [u8]) -> Result<(), SnapshotError> {
|
||||
destination.copy_from_slice(self.take(destination.len())?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fill `destination` from bytes written by `Writer::bytes`. The recorded
|
||||
/// length must equal `destination.len()`; fixed-size memories never resize.
|
||||
pub(crate) fn bytes_into(&mut self, destination: &mut [u8]) -> Result<(), SnapshotError> {
|
||||
let length = self.u32()? as usize;
|
||||
if length != destination.len() {
|
||||
return Err(SnapshotError::Corrupt("memory size"));
|
||||
}
|
||||
self.raw_into(destination)
|
||||
}
|
||||
|
||||
/// Length-prefixed bytes as a vector, rejecting lengths above `limit`.
|
||||
pub(crate) fn bytes_vec(&mut self, limit: usize) -> Result<Vec<u8>, SnapshotError> {
|
||||
let length = self.u32()? as usize;
|
||||
if length > limit {
|
||||
return Err(SnapshotError::Corrupt("length"));
|
||||
}
|
||||
Ok(self.take(length)?.to_vec())
|
||||
}
|
||||
|
||||
pub(crate) fn option<T>(
|
||||
&mut self,
|
||||
load: impl FnOnce(&mut Self) -> Result<T, SnapshotError>,
|
||||
) -> Result<Option<T>, SnapshotError> {
|
||||
if self.bool()? {
|
||||
load(self).map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// FNV-1a over `bytes`; identifies images and detects accidental corruption.
|
||||
/// It is not a security boundary.
|
||||
pub(crate) fn fingerprint(bytes: &[u8]) -> u64 {
|
||||
bytes.iter().fold(0xcbf2_9ce4_8422_2325, |hash, &byte| {
|
||||
(hash ^ u64::from(byte)).wrapping_mul(0x0000_0100_0000_01b3)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn values_round_trip_in_order() {
|
||||
let mut writer = Writer::new();
|
||||
writer.tag(b"TEST");
|
||||
writer.u8(0xab);
|
||||
writer.bool(true);
|
||||
writer.u16(0xbeef);
|
||||
writer.i16(-2);
|
||||
writer.u32(0xdead_beef);
|
||||
writer.u64(u64::MAX - 1);
|
||||
writer.bytes(&[1, 2, 3]);
|
||||
writer.option(Some(&7u16), |value, writer| writer.u16(*value));
|
||||
writer.option(None::<&u16>, |value, writer| writer.u16(*value));
|
||||
let bytes = writer.finish().unwrap();
|
||||
|
||||
let mut reader = Reader::new(&bytes);
|
||||
reader.tag(b"TEST").unwrap();
|
||||
assert_eq!(reader.u8().unwrap(), 0xab);
|
||||
assert!(reader.bool().unwrap());
|
||||
assert_eq!(reader.u16().unwrap(), 0xbeef);
|
||||
assert_eq!(reader.i16().unwrap(), -2);
|
||||
assert_eq!(reader.u32().unwrap(), 0xdead_beef);
|
||||
assert_eq!(reader.u64().unwrap(), u64::MAX - 1);
|
||||
let mut three = [0; 3];
|
||||
reader.bytes_into(&mut three).unwrap();
|
||||
assert_eq!(three, [1, 2, 3]);
|
||||
assert_eq!(reader.option(Reader::u16).unwrap(), Some(7));
|
||||
assert_eq!(reader.option(Reader::u16).unwrap(), None);
|
||||
reader.finish().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_recorded_failure_is_reported() {
|
||||
let mut writer = Writer::new();
|
||||
writer.fail(SnapshotError::Unsupported("first"));
|
||||
writer.u8(1);
|
||||
writer.fail(SnapshotError::MidStep);
|
||||
assert_eq!(writer.finish(), Err(SnapshotError::Unsupported("first")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_input_is_rejected_without_panicking() {
|
||||
assert_eq!(Reader::new(&[1]).u16(), Err(SnapshotError::Truncated));
|
||||
assert_eq!(
|
||||
Reader::new(&[2]).bool(),
|
||||
Err(SnapshotError::Corrupt("boolean"))
|
||||
);
|
||||
assert_eq!(
|
||||
Reader::new(b"NOPE").tag(b"TEST"),
|
||||
Err(SnapshotError::Corrupt("section tag"))
|
||||
);
|
||||
assert_eq!(Reader::new(&[0]).finish(), Err(SnapshotError::TrailingData));
|
||||
// A length prefix larger than the stream or the destination fails cleanly.
|
||||
let mut huge = Writer::new();
|
||||
huge.u32(u32::MAX);
|
||||
let huge = huge.finish().unwrap();
|
||||
assert_eq!(
|
||||
Reader::new(&huge).bytes_vec(1 << 20),
|
||||
Err(SnapshotError::Corrupt("length"))
|
||||
);
|
||||
assert_eq!(
|
||||
Reader::new(&huge).bytes_into(&mut [0; 4]),
|
||||
Err(SnapshotError::Corrupt("memory size"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
//! Nominal Game Pak opcode prefetch timing. No ROM bytes or CPU instructions live here.
|
||||
//! Full-buffer pause/restart follows the independently reviewed ares/jgenesis model.
|
||||
use super::{bus_cycles, AccessKind, AccessWidth};
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Prefetch {
|
||||
|
|
@ -163,3 +164,100 @@ impl Prefetch {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
impl Snapshot for Prefetch {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self { waitcnt, stream } = self;
|
||||
writer.tag(b"PFCH");
|
||||
writer.u16(*waitcnt);
|
||||
writer.option(stream.as_ref(), |stream, writer| {
|
||||
let Stream {
|
||||
head,
|
||||
ready,
|
||||
remaining,
|
||||
running,
|
||||
} = stream;
|
||||
writer.u32(*head);
|
||||
writer.u8(*ready);
|
||||
writer.u32(*remaining);
|
||||
writer.bool(*running);
|
||||
});
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self { waitcnt, stream } = self;
|
||||
reader.tag(b"PFCH")?;
|
||||
*waitcnt = reader.u16()?;
|
||||
*stream = reader.option(|reader| {
|
||||
let head = reader.u32()?;
|
||||
let ready = reader.u8()?;
|
||||
let remaining = reader.u32()?;
|
||||
let running = reader.bool()?;
|
||||
if ready > 8 {
|
||||
return Err(SnapshotError::Corrupt("prefetch queue"));
|
||||
}
|
||||
Ok(Stream {
|
||||
head,
|
||||
ready,
|
||||
remaining,
|
||||
running,
|
||||
})
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
fn populated_prefetch() -> Prefetch {
|
||||
Prefetch {
|
||||
waitcnt: 0x5a34,
|
||||
stream: Some(Stream {
|
||||
head: 0x0a12_3456,
|
||||
ready: 3,
|
||||
remaining: 7,
|
||||
running: true,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn save(prefetch: &Prefetch) -> Vec<u8> {
|
||||
let mut writer = Writer::new();
|
||||
prefetch.save(&mut writer);
|
||||
writer.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefetch_round_trips_waitcnt_and_stream() {
|
||||
let source = populated_prefetch();
|
||||
let bytes = save(&source);
|
||||
let mut restored = Prefetch::default();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_strict_prefetch_prefix_is_rejected() {
|
||||
let bytes = save(&populated_prefetch());
|
||||
for end in 0..bytes.len() {
|
||||
let mut restored = Prefetch::default();
|
||||
assert!(
|
||||
restored.load(&mut Reader::new(&bytes[..end])).is_err(),
|
||||
"accepted prefix of length {end}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefetch_rejects_invalid_stream_values() {
|
||||
let mut bytes = save(&populated_prefetch());
|
||||
bytes[11] = 9;
|
||||
let mut restored = Prefetch::default();
|
||||
assert!(matches!(
|
||||
restored.load(&mut Reader::new(&bytes)),
|
||||
Err(SnapshotError::Corrupt(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ pub(crate) mod mosaic;
|
|||
pub(crate) mod sprites;
|
||||
pub(crate) mod windows;
|
||||
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
pub const WIDTH: usize = 240;
|
||||
|
|
@ -67,6 +68,86 @@ pub(crate) struct Background {
|
|||
pub affine: affine::AffineBackground,
|
||||
}
|
||||
|
||||
impl Snapshot for Background {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
control,
|
||||
x,
|
||||
y,
|
||||
affine,
|
||||
} = self;
|
||||
writer.tag(b"BGND");
|
||||
writer.u16(*control);
|
||||
writer.u16(*x);
|
||||
writer.u16(*y);
|
||||
affine.save(writer);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
control,
|
||||
x,
|
||||
y,
|
||||
affine,
|
||||
} = self;
|
||||
reader.tag(b"BGND")?;
|
||||
*control = reader.u16()?;
|
||||
*x = reader.u16()?;
|
||||
*y = reader.u16()?;
|
||||
affine.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
#[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 assert_prefixes(bytes: &[u8]) {
|
||||
for end in 0..bytes.len() {
|
||||
let mut value = Background::default();
|
||||
assert!(
|
||||
value.load(&mut Reader::new(&bytes[..end])).is_err(),
|
||||
"prefix {end}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_round_trips_and_rejects_bad_affine_boolean() {
|
||||
let mut affine = affine::AffineBackground::default();
|
||||
for offset in 0..8 {
|
||||
affine.write8(offset, offset as u8 + 1, false);
|
||||
}
|
||||
for offset in 8..16 {
|
||||
affine.write8(offset, offset as u8 + 1, true);
|
||||
}
|
||||
let source = Background {
|
||||
control: 0xa51d,
|
||||
x: 0x1357,
|
||||
y: 0x2468,
|
||||
affine,
|
||||
};
|
||||
let bytes = encoded(&source);
|
||||
let mut restored = Background::default();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, source);
|
||||
assert_prefixes(&bytes);
|
||||
|
||||
let mut invalid = bytes;
|
||||
*invalid.last_mut().unwrap() = 2;
|
||||
assert_eq!(
|
||||
Background::default().load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("boolean"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RenderSettings<'a> {
|
||||
pub control: u16,
|
||||
pub prepared_objects: Option<&'a Result<sprites::PreparedObjects, VideoError>>,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
use super::{halfword, vram_index, Background, VideoError, HEIGHT, WIDTH};
|
||||
#[cfg(test)]
|
||||
use crate::display::{CYCLES_PER_FRAME, CYCLES_PER_LINE};
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
const REFERENCE_MASK: u32 = 0x0fff_ffff;
|
||||
|
||||
|
|
@ -86,6 +87,89 @@ impl AffineBackground {
|
|||
}
|
||||
}
|
||||
|
||||
impl Snapshot for AffineBackground {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
matrix,
|
||||
reference,
|
||||
internal,
|
||||
reload_next,
|
||||
} = self;
|
||||
writer.tag(b"AFBG");
|
||||
for value in matrix {
|
||||
writer.u16(*value);
|
||||
}
|
||||
for value in reference {
|
||||
writer.u32(*value);
|
||||
}
|
||||
for value in internal {
|
||||
writer.u32(*value);
|
||||
}
|
||||
for value in reload_next {
|
||||
writer.bool(*value);
|
||||
}
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
matrix,
|
||||
reference,
|
||||
internal,
|
||||
reload_next,
|
||||
} = self;
|
||||
reader.tag(b"AFBG")?;
|
||||
for value in matrix {
|
||||
*value = reader.u16()?;
|
||||
}
|
||||
for value in reference {
|
||||
*value = reader.u32()?;
|
||||
}
|
||||
for value in internal {
|
||||
*value = reader.u32()?;
|
||||
}
|
||||
for value in reload_next {
|
||||
*value = reader.bool()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
fn encoded(value: &AffineBackground) -> Vec<u8> {
|
||||
let mut writer = Writer::new();
|
||||
value.save(&mut writer);
|
||||
writer.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn affine_background_round_trips_every_latch_and_checks_prefixes() {
|
||||
let source = AffineBackground {
|
||||
matrix: [1, 0x2345, 0x6789, 0xabcd],
|
||||
reference: [0x0123_4567, 0x089a_bcde],
|
||||
internal: [0x0765_4321, 0x0123_4567],
|
||||
reload_next: [true, false],
|
||||
};
|
||||
let bytes = encoded(&source);
|
||||
let mut restored = AffineBackground::default();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, source);
|
||||
for end in 0..bytes.len() {
|
||||
assert!(AffineBackground::default()
|
||||
.load(&mut Reader::new(&bytes[..end]))
|
||||
.is_err());
|
||||
}
|
||||
let mut invalid = bytes;
|
||||
*invalid.last_mut().unwrap() = 2;
|
||||
assert_eq!(
|
||||
AffineBackground::default().load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("boolean"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn pixel(
|
||||
bg: Background,
|
||||
vram: &[u8],
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
//! happens at VBlank entry. This is not a pixel fetch pipeline.
|
||||
|
||||
use super::{Framebuffer, VideoError, HEIGHT, WIDTH};
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
pub(crate) struct Capture {
|
||||
drawing: Vec<u32>,
|
||||
|
|
@ -74,3 +75,160 @@ impl Capture {
|
|||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Capture {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
drawing,
|
||||
completed,
|
||||
rows,
|
||||
drawing_error,
|
||||
completed_error,
|
||||
vblank,
|
||||
} = self;
|
||||
writer.tag(b"CAPT");
|
||||
let pixels = WIDTH * HEIGHT;
|
||||
if drawing.len() != pixels || completed.len() != pixels {
|
||||
writer.fail(SnapshotError::Unsupported("invalid capture buffer size"));
|
||||
}
|
||||
writer.u64(drawing.len() as u64);
|
||||
for pixel in drawing {
|
||||
writer.u32(*pixel);
|
||||
}
|
||||
writer.u64(completed.len() as u64);
|
||||
for pixel in completed {
|
||||
writer.u32(*pixel);
|
||||
}
|
||||
if *rows > HEIGHT {
|
||||
writer.fail(SnapshotError::Unsupported("invalid capture row count"));
|
||||
}
|
||||
writer.u64(*rows as u64);
|
||||
save_video_error(drawing_error, writer);
|
||||
save_video_error(completed_error, writer);
|
||||
writer.option(vblank.as_ref(), |value, writer| writer.u64(*value));
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
drawing,
|
||||
completed,
|
||||
rows,
|
||||
drawing_error,
|
||||
completed_error,
|
||||
vblank,
|
||||
} = self;
|
||||
reader.tag(b"CAPT")?;
|
||||
let pixels = WIDTH * HEIGHT;
|
||||
if drawing.len() != pixels || completed.len() != pixels {
|
||||
return Err(SnapshotError::Corrupt("capture buffer size"));
|
||||
}
|
||||
const MIN_PAYLOAD: usize = 8 + WIDTH * HEIGHT * 4 + 8 + WIDTH * HEIGHT * 4 + 8 + 1 + 1 + 1;
|
||||
if reader.remaining() < MIN_PAYLOAD {
|
||||
return Err(SnapshotError::Truncated);
|
||||
}
|
||||
let drawing_len = usize::try_from(reader.u64()?)
|
||||
.map_err(|_| SnapshotError::Corrupt("capture buffer size"))?;
|
||||
if drawing_len != pixels {
|
||||
return Err(SnapshotError::Corrupt("capture buffer size"));
|
||||
}
|
||||
for pixel in drawing {
|
||||
*pixel = reader.u32()?;
|
||||
}
|
||||
let completed_len = usize::try_from(reader.u64()?)
|
||||
.map_err(|_| SnapshotError::Corrupt("capture buffer size"))?;
|
||||
if completed_len != pixels {
|
||||
return Err(SnapshotError::Corrupt("capture buffer size"));
|
||||
}
|
||||
for pixel in completed {
|
||||
*pixel = reader.u32()?;
|
||||
}
|
||||
let loaded_rows = usize::try_from(reader.u64()?)
|
||||
.map_err(|_| SnapshotError::Corrupt("capture row count"))?;
|
||||
if loaded_rows > HEIGHT {
|
||||
return Err(SnapshotError::Corrupt("capture row count"));
|
||||
}
|
||||
*rows = loaded_rows;
|
||||
*drawing_error = load_video_error(reader)?;
|
||||
*completed_error = load_video_error(reader)?;
|
||||
*vblank = reader.option(|reader| reader.u64())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn save_video_error(error: &Option<VideoError>, writer: &mut Writer) {
|
||||
writer.bool(error.is_some());
|
||||
if error.is_some() {
|
||||
writer.fail(SnapshotError::Unsupported("deferred video diagnostic"));
|
||||
}
|
||||
}
|
||||
|
||||
fn load_video_error(reader: &mut Reader<'_>) -> Result<Option<VideoError>, SnapshotError> {
|
||||
if reader.bool()? {
|
||||
Err(SnapshotError::Corrupt("deferred video diagnostic"))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
fn encoded(value: &Capture) -> Vec<u8> {
|
||||
let mut writer = Writer::new();
|
||||
value.save(&mut writer);
|
||||
writer.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capture_round_trips_buffers_and_rejects_every_strict_prefix() {
|
||||
let mut source = Capture::default();
|
||||
for (index, pixel) in source.drawing.iter_mut().enumerate() {
|
||||
*pixel = index as u32 ^ 0x1234_5678;
|
||||
}
|
||||
for (index, pixel) in source.completed.iter_mut().enumerate() {
|
||||
*pixel = index as u32 ^ 0x8765_4321;
|
||||
}
|
||||
source.rows = 37;
|
||||
source.vblank = Some(0x1234_5678_9abc_def0);
|
||||
let bytes = encoded(&source);
|
||||
let mut restored = Capture::default();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(encoded(&restored), bytes);
|
||||
let mut prefix_target = Capture::default();
|
||||
for end in 0..bytes.len() {
|
||||
assert!(
|
||||
prefix_target.load(&mut Reader::new(&bytes[..end])).is_err(),
|
||||
"prefix {end}"
|
||||
);
|
||||
}
|
||||
|
||||
let mut invalid = bytes.clone();
|
||||
// The final nine bytes are the vblank option flag and its u64 payload.
|
||||
let option_flag = invalid.len() - 9;
|
||||
invalid[option_flag] = 2;
|
||||
assert_eq!(
|
||||
Capture::default().load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("boolean"))
|
||||
);
|
||||
invalid[4] = 1;
|
||||
assert_eq!(
|
||||
Capture::default().load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("capture buffer size"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_capture_errors_cannot_be_saved() {
|
||||
let capture = Capture {
|
||||
drawing_error: Some(VideoError::UnsupportedMode(7)),
|
||||
..Capture::default()
|
||||
};
|
||||
let mut writer = Writer::new();
|
||||
capture.save(&mut writer);
|
||||
assert_eq!(
|
||||
writer.finish(),
|
||||
Err(SnapshotError::Unsupported("deferred video diagnostic"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
//! and horizontal comparator history. Layer/effect selection remains row-sampled.
|
||||
|
||||
use super::Pixel;
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Effects {
|
||||
|
|
@ -93,3 +94,89 @@ fn channels(first: u16, second: u16, operation: impl Fn(u16, u16) -> u16) -> u16
|
|||
color | (operation((first >> shift) & 31, (second >> shift) & 31) << shift)
|
||||
})
|
||||
}
|
||||
|
||||
impl Snapshot for Effects {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
horizontal,
|
||||
vertical,
|
||||
inside,
|
||||
outside,
|
||||
control,
|
||||
alpha,
|
||||
brightness,
|
||||
} = self;
|
||||
writer.tag(b"EFCT");
|
||||
for value in horizontal {
|
||||
writer.u16(*value);
|
||||
}
|
||||
for value in vertical {
|
||||
writer.u16(*value);
|
||||
}
|
||||
writer.u16(*inside);
|
||||
writer.u16(*outside);
|
||||
writer.u16(*control);
|
||||
writer.u16(*alpha);
|
||||
writer.u16(*brightness);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
horizontal,
|
||||
vertical,
|
||||
inside,
|
||||
outside,
|
||||
control,
|
||||
alpha,
|
||||
brightness,
|
||||
} = self;
|
||||
reader.tag(b"EFCT")?;
|
||||
for value in horizontal {
|
||||
*value = reader.u16()?;
|
||||
}
|
||||
for value in vertical {
|
||||
*value = reader.u16()?;
|
||||
}
|
||||
*inside = reader.u16()?;
|
||||
*outside = reader.u16()?;
|
||||
*control = reader.u16()?;
|
||||
*alpha = reader.u16()?;
|
||||
*brightness = reader.u16()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn effects_round_trip_and_reject_every_strict_prefix() {
|
||||
let source = Effects {
|
||||
horizontal: [0x1234, 0x5678],
|
||||
vertical: [0x9abc, 0xdef0],
|
||||
inside: 0x1357,
|
||||
outside: 0x2468,
|
||||
control: 0xaaaa,
|
||||
alpha: 0x5555,
|
||||
brightness: 0xdead,
|
||||
};
|
||||
let mut writer = Writer::new();
|
||||
source.save(&mut writer);
|
||||
let bytes = writer.finish().unwrap();
|
||||
let mut restored = Effects::default();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, source);
|
||||
for end in 0..bytes.len() {
|
||||
assert!(Effects::default()
|
||||
.load(&mut Reader::new(&bytes[..end]))
|
||||
.is_err());
|
||||
}
|
||||
let mut invalid = bytes;
|
||||
invalid[0] ^= 1;
|
||||
assert_eq!(
|
||||
Effects::default().load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("section tag"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
//! for sprite preparation. Per-access size latching is not modeled.
|
||||
|
||||
use crate::display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, VISIBLE_LINES};
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct VerticalMosaic {
|
||||
|
|
@ -188,3 +189,83 @@ mod tests {
|
|||
assert_eq!((mosaic.background, mosaic.object), (0, 1)); // Row1 phase, row0 already prepared.
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for VerticalMosaic {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
background,
|
||||
object,
|
||||
started,
|
||||
} = self;
|
||||
writer.tag(b"VMOS");
|
||||
if *background >= 16 || *object >= 16 {
|
||||
writer.fail(SnapshotError::Unsupported(
|
||||
"invalid vertical mosaic counter",
|
||||
));
|
||||
}
|
||||
writer.u8(*background);
|
||||
writer.u8(*object);
|
||||
writer.bool(*started);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
background,
|
||||
object,
|
||||
started,
|
||||
} = self;
|
||||
reader.tag(b"VMOS")?;
|
||||
let loaded_background = reader.u8()?;
|
||||
let loaded_object = reader.u8()?;
|
||||
if loaded_background >= 16 || loaded_object >= 16 {
|
||||
return Err(SnapshotError::Corrupt("vertical mosaic counter"));
|
||||
}
|
||||
*background = loaded_background;
|
||||
*object = loaded_object;
|
||||
*started = reader.bool()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn vertical_mosaic_round_trips_and_rejects_invalid_values() {
|
||||
let source = VerticalMosaic {
|
||||
background: 13,
|
||||
object: 7,
|
||||
started: true,
|
||||
};
|
||||
let mut writer = Writer::new();
|
||||
source.save(&mut writer);
|
||||
let bytes = writer.finish().unwrap();
|
||||
let mut restored = VerticalMosaic::default();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, source);
|
||||
for end in 0..bytes.len() {
|
||||
assert!(VerticalMosaic::default()
|
||||
.load(&mut Reader::new(&bytes[..end]))
|
||||
.is_err());
|
||||
}
|
||||
let mut invalid = bytes;
|
||||
*invalid.last_mut().unwrap() = 2;
|
||||
assert_eq!(
|
||||
VerticalMosaic::default().load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("boolean"))
|
||||
);
|
||||
let mut invalid = encoded_mosaic(&source);
|
||||
invalid[4] = 16;
|
||||
assert_eq!(
|
||||
VerticalMosaic::default().load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("vertical mosaic counter"))
|
||||
);
|
||||
}
|
||||
|
||||
fn encoded_mosaic(value: &VerticalMosaic) -> Vec<u8> {
|
||||
let mut writer = Writer::new();
|
||||
value.save(&mut writer);
|
||||
writer.finish().unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
mod budget;
|
||||
pub(crate) mod pipeline;
|
||||
|
||||
use super::{halfword, Pixel, VideoError, WIDTH};
|
||||
use super::{halfword, Pixel, VideoError, HEIGHT, WIDTH};
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
const SIZES: [[(usize, usize); 4]; 3] = [
|
||||
[(8, 8), (16, 16), (32, 32), (64, 64)],
|
||||
|
|
@ -17,19 +18,20 @@ pub(super) struct Objects {
|
|||
pub window: Vec<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct PreparedObjects {
|
||||
samples: Vec<Sample>,
|
||||
window: Vec<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct ObjectPixel {
|
||||
palette_index: u16,
|
||||
semi_transparent: bool,
|
||||
}
|
||||
|
||||
// Transparent texels can change priority and mosaic without replacing color.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
struct Sample {
|
||||
pixel: Option<ObjectPixel>,
|
||||
priority: u8,
|
||||
|
|
@ -228,3 +230,308 @@ impl PreparedObjects {
|
|||
output
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectPixel {
|
||||
fn save_fields(palette_index: &u16, semi_transparent: &bool, writer: &mut Writer) {
|
||||
if *palette_index > 255 {
|
||||
writer.fail(SnapshotError::Unsupported("invalid OBJ palette index"));
|
||||
}
|
||||
writer.u16(*palette_index);
|
||||
writer.bool(*semi_transparent);
|
||||
}
|
||||
|
||||
fn load_fields(
|
||||
palette_index: &mut u16,
|
||||
semi_transparent: &mut bool,
|
||||
reader: &mut Reader<'_>,
|
||||
) -> Result<(), SnapshotError> {
|
||||
let loaded_index = reader.u16()?;
|
||||
let loaded_transparency = reader.bool()?;
|
||||
if loaded_index > 255 {
|
||||
return Err(SnapshotError::Corrupt("OBJ palette index"));
|
||||
}
|
||||
*palette_index = loaded_index;
|
||||
*semi_transparent = loaded_transparency;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_payload(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
palette_index,
|
||||
semi_transparent,
|
||||
} = self;
|
||||
Self::save_fields(palette_index, semi_transparent, writer);
|
||||
}
|
||||
|
||||
fn load_payload(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
palette_index,
|
||||
semi_transparent,
|
||||
} = self;
|
||||
Self::load_fields(palette_index, semi_transparent, reader)
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for ObjectPixel {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
palette_index,
|
||||
semi_transparent,
|
||||
} = self;
|
||||
writer.tag(b"OPXL");
|
||||
Self::save_fields(palette_index, semi_transparent, writer);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
palette_index,
|
||||
semi_transparent,
|
||||
} = self;
|
||||
reader.tag(b"OPXL")?;
|
||||
Self::load_fields(palette_index, semi_transparent, reader)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sample {
|
||||
fn save_fields(pixel: &Option<ObjectPixel>, priority: &u8, mosaic: &bool, writer: &mut Writer) {
|
||||
writer.option(pixel.as_ref(), |pixel, writer| {
|
||||
pixel.save_payload(writer);
|
||||
});
|
||||
if *priority > 3 {
|
||||
writer.fail(SnapshotError::Unsupported("invalid OBJ priority"));
|
||||
}
|
||||
writer.u8(*priority);
|
||||
writer.bool(*mosaic);
|
||||
}
|
||||
|
||||
fn load_fields(
|
||||
pixel: &mut Option<ObjectPixel>,
|
||||
priority: &mut u8,
|
||||
mosaic: &mut bool,
|
||||
reader: &mut Reader<'_>,
|
||||
) -> Result<(), SnapshotError> {
|
||||
let loaded_pixel = reader.option(|reader| {
|
||||
let mut pixel = ObjectPixel {
|
||||
palette_index: 0,
|
||||
semi_transparent: false,
|
||||
};
|
||||
pixel.load_payload(reader)?;
|
||||
Ok(pixel)
|
||||
})?;
|
||||
let loaded_priority = reader.u8()?;
|
||||
if loaded_priority > 3 {
|
||||
return Err(SnapshotError::Corrupt("OBJ priority"));
|
||||
}
|
||||
let loaded_mosaic = reader.bool()?;
|
||||
*pixel = loaded_pixel;
|
||||
*priority = loaded_priority;
|
||||
*mosaic = loaded_mosaic;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_payload(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
pixel,
|
||||
priority,
|
||||
mosaic,
|
||||
} = self;
|
||||
Self::save_fields(pixel, priority, mosaic, writer);
|
||||
}
|
||||
|
||||
fn load_payload(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
pixel,
|
||||
priority,
|
||||
mosaic,
|
||||
} = self;
|
||||
Self::load_fields(pixel, priority, mosaic, reader)
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for Sample {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self {
|
||||
pixel,
|
||||
priority,
|
||||
mosaic,
|
||||
} = self;
|
||||
writer.tag(b"SAMP");
|
||||
Self::save_fields(pixel, priority, mosaic, writer);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self {
|
||||
pixel,
|
||||
priority,
|
||||
mosaic,
|
||||
} = self;
|
||||
reader.tag(b"SAMP")?;
|
||||
Self::load_fields(pixel, priority, mosaic, reader)
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for PreparedObjects {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self { samples, window } = self;
|
||||
writer.tag(b"POBJ");
|
||||
if samples.len() != window.len()
|
||||
|| samples.len() % WIDTH != 0
|
||||
|| samples.len() > WIDTH * HEIGHT
|
||||
{
|
||||
writer.fail(SnapshotError::Unsupported(
|
||||
"invalid prepared OBJ dimensions",
|
||||
));
|
||||
}
|
||||
writer.u64(samples.len() as u64);
|
||||
writer.u64(window.len() as u64);
|
||||
for sample in samples {
|
||||
sample.save_payload(writer);
|
||||
}
|
||||
for value in window {
|
||||
writer.bool(*value);
|
||||
}
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self { samples, window } = self;
|
||||
reader.tag(b"POBJ")?;
|
||||
let sample_len = usize::try_from(reader.u64()?)
|
||||
.map_err(|_| SnapshotError::Corrupt("prepared OBJ dimensions"))?;
|
||||
let window_len = usize::try_from(reader.u64()?)
|
||||
.map_err(|_| SnapshotError::Corrupt("prepared OBJ dimensions"))?;
|
||||
if sample_len != window_len || sample_len % WIDTH != 0 || sample_len > WIDTH * HEIGHT {
|
||||
return Err(SnapshotError::Corrupt("prepared OBJ dimensions"));
|
||||
}
|
||||
samples.resize(sample_len, Sample::default());
|
||||
window.resize(window_len, false);
|
||||
for sample in samples {
|
||||
sample.load_payload(reader)?;
|
||||
}
|
||||
for value in window {
|
||||
*value = reader.bool()?;
|
||||
}
|
||||
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 assert_prefixes<T: Snapshot>(bytes: &[u8], mut fresh: impl FnMut() -> T) {
|
||||
for end in 0..bytes.len() {
|
||||
assert!(
|
||||
fresh().load(&mut Reader::new(&bytes[..end])).is_err(),
|
||||
"prefix {end}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_pixel_round_trips_and_rejects_invalid_boolean() {
|
||||
let source = ObjectPixel {
|
||||
palette_index: 219,
|
||||
semi_transparent: true,
|
||||
};
|
||||
let bytes = encoded(&source);
|
||||
let mut restored = ObjectPixel {
|
||||
palette_index: 0,
|
||||
semi_transparent: false,
|
||||
};
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, source);
|
||||
assert_prefixes(&bytes, || ObjectPixel {
|
||||
palette_index: 0,
|
||||
semi_transparent: false,
|
||||
});
|
||||
let mut invalid = bytes.clone();
|
||||
invalid[6] = 2;
|
||||
assert_eq!(
|
||||
ObjectPixel {
|
||||
palette_index: 0,
|
||||
semi_transparent: false,
|
||||
}
|
||||
.load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("boolean"))
|
||||
);
|
||||
invalid[4] = 0xff;
|
||||
invalid[5] = 0x01;
|
||||
invalid[6] = 1;
|
||||
assert_eq!(
|
||||
ObjectPixel {
|
||||
palette_index: 0,
|
||||
semi_transparent: false,
|
||||
}
|
||||
.load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("OBJ palette index"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_round_trips_and_validates_priority() {
|
||||
let source = Sample {
|
||||
pixel: Some(ObjectPixel {
|
||||
palette_index: 173,
|
||||
semi_transparent: true,
|
||||
}),
|
||||
priority: 3,
|
||||
mosaic: true,
|
||||
};
|
||||
let bytes = encoded(&source);
|
||||
let mut restored = Sample::default();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, source);
|
||||
assert_prefixes(&bytes, Sample::default);
|
||||
let mut invalid = bytes.clone();
|
||||
invalid[8] = 4;
|
||||
assert_eq!(
|
||||
Sample::default().load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("OBJ priority"))
|
||||
);
|
||||
invalid[4] = 2;
|
||||
assert_eq!(
|
||||
Sample::default().load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("boolean"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepared_objects_round_trip_and_bound_lengths() {
|
||||
let samples = (0..WIDTH * 2)
|
||||
.map(|index| Sample {
|
||||
pixel: (index % 2 == 0).then_some(ObjectPixel {
|
||||
palette_index: (index % 256) as u16,
|
||||
semi_transparent: index % 3 == 0,
|
||||
}),
|
||||
priority: (index % 4) as u8,
|
||||
mosaic: index % 5 == 0,
|
||||
})
|
||||
.collect();
|
||||
let window = (0..WIDTH * 2).map(|index| index % 7 == 0).collect();
|
||||
let source = PreparedObjects { samples, window };
|
||||
let bytes = encoded(&source);
|
||||
let mut restored = PreparedObjects::default();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, source);
|
||||
assert_prefixes(&bytes, PreparedObjects::default);
|
||||
let mut invalid = bytes;
|
||||
invalid[4] = 1;
|
||||
invalid[5..8].fill(0);
|
||||
assert_eq!(
|
||||
PreparedObjects::default().load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("prepared OBJ dimensions"))
|
||||
);
|
||||
let mut invalid = encoded(&source);
|
||||
invalid[20] = 2;
|
||||
assert_eq!(
|
||||
PreparedObjects::default().load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("boolean"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@
|
|||
//! individual fetches, fetch budgets, or OAM/VRAM contention.
|
||||
|
||||
use super::PreparedObjects;
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
use crate::{display::CYCLES_PER_LINE, video::VideoError};
|
||||
|
||||
pub(crate) const PREPARE_CYCLE: u32 = 40;
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct SpritePipeline {
|
||||
rows: [Option<(usize, Result<PreparedObjects, VideoError>)>; 2],
|
||||
pub started: bool,
|
||||
|
|
@ -120,3 +121,130 @@ mod tests {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for SpritePipeline {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self { rows, started } = self;
|
||||
writer.tag(b"SPRP");
|
||||
writer.bool(*started);
|
||||
for row in rows {
|
||||
writer.option(row.as_ref(), |(index, result), writer| {
|
||||
if *index >= crate::video::HEIGHT {
|
||||
writer.fail(SnapshotError::Unsupported("invalid sprite row index"));
|
||||
}
|
||||
writer.u64(*index as u64);
|
||||
match result {
|
||||
Ok(objects) => {
|
||||
writer.u8(0);
|
||||
objects.save(writer);
|
||||
}
|
||||
Err(_) => {
|
||||
writer.u8(1);
|
||||
writer.fail(SnapshotError::Unsupported("deferred video diagnostic"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self { rows, started } = self;
|
||||
reader.tag(b"SPRP")?;
|
||||
*started = reader.bool()?;
|
||||
for (slot, destination) in rows.iter_mut().enumerate() {
|
||||
*destination = reader.option(|reader| {
|
||||
let index = usize::try_from(reader.u64()?)
|
||||
.map_err(|_| SnapshotError::Corrupt("sprite row index"))?;
|
||||
if index >= crate::video::HEIGHT || index % 2 != slot {
|
||||
return Err(SnapshotError::Corrupt("sprite row index"));
|
||||
}
|
||||
let result = match reader.u8()? {
|
||||
0 => {
|
||||
let mut objects = PreparedObjects::default();
|
||||
objects.load(reader)?;
|
||||
Ok(objects)
|
||||
}
|
||||
1 => return Err(SnapshotError::Corrupt("deferred video diagnostic")),
|
||||
_ => return Err(SnapshotError::Corrupt("sprite row result")),
|
||||
};
|
||||
Ok((index, result))
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
use crate::video::sprites::{ObjectPixel, Sample};
|
||||
|
||||
fn encoded(value: &SpritePipeline) -> Vec<u8> {
|
||||
let mut writer = Writer::new();
|
||||
value.save(&mut writer);
|
||||
writer.finish().unwrap()
|
||||
}
|
||||
|
||||
fn prepared(index: usize) -> PreparedObjects {
|
||||
PreparedObjects {
|
||||
samples: vec![
|
||||
Sample {
|
||||
pixel: Some(ObjectPixel {
|
||||
palette_index: (index % 256) as u16,
|
||||
semi_transparent: true,
|
||||
}),
|
||||
priority: (index % 4) as u8,
|
||||
mosaic: true,
|
||||
};
|
||||
crate::video::WIDTH
|
||||
],
|
||||
window: vec![index % 2 == 0; crate::video::WIDTH],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sprite_pipeline_round_trips_rows_and_checks_prefixes() {
|
||||
let source = SpritePipeline {
|
||||
rows: [Some((4, Ok(prepared(13)))), Some((5, Ok(prepared(17))))],
|
||||
started: true,
|
||||
};
|
||||
let bytes = encoded(&source);
|
||||
let mut restored = SpritePipeline::default();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, source);
|
||||
for end in 0..bytes.len() {
|
||||
assert!(
|
||||
SpritePipeline::default()
|
||||
.load(&mut Reader::new(&bytes[..end]))
|
||||
.is_err(),
|
||||
"prefix {end}"
|
||||
);
|
||||
}
|
||||
|
||||
let mut invalid = bytes.clone();
|
||||
invalid[4] = 2;
|
||||
assert_eq!(
|
||||
SpritePipeline::default().load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("boolean"))
|
||||
);
|
||||
invalid.copy_from_slice(&bytes);
|
||||
invalid[6] = crate::video::HEIGHT as u8;
|
||||
invalid[7..14].fill(0);
|
||||
assert_eq!(
|
||||
SpritePipeline::default().load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("sprite row index"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_sprite_diagnostic_cannot_be_saved() {
|
||||
let mut source = SpritePipeline::default();
|
||||
source.rows[0] = Some((0, Err(VideoError::UnsupportedMode(7))));
|
||||
let mut writer = Writer::new();
|
||||
source.save(&mut writer);
|
||||
assert_eq!(
|
||||
writer.finish(),
|
||||
Err(SnapshotError::Unsupported("deferred video diagnostic"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
pub(crate) mod horizontal;
|
||||
|
||||
use crate::display::{CYCLES_PER_LINE, LINES_PER_FRAME};
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct WindowEdges {
|
||||
|
|
@ -131,3 +132,54 @@ mod tests {
|
|||
assert_eq!(edges.active, [true, false]); // The row1 comparator already ran.
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for WindowEdges {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self { active, started } = self;
|
||||
writer.tag(b"WEDG");
|
||||
for value in active {
|
||||
writer.bool(*value);
|
||||
}
|
||||
writer.bool(*started);
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self { active, started } = self;
|
||||
reader.tag(b"WEDG")?;
|
||||
for value in active {
|
||||
*value = reader.bool()?;
|
||||
}
|
||||
*started = reader.bool()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn window_edges_round_trip_and_validate_booleans() {
|
||||
let source = WindowEdges {
|
||||
active: [true, false],
|
||||
started: true,
|
||||
};
|
||||
let mut writer = Writer::new();
|
||||
source.save(&mut writer);
|
||||
let bytes = writer.finish().unwrap();
|
||||
let mut restored = WindowEdges::default();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, source);
|
||||
for end in 0..bytes.len() {
|
||||
assert!(WindowEdges::default()
|
||||
.load(&mut Reader::new(&bytes[..end]))
|
||||
.is_err());
|
||||
}
|
||||
let mut invalid = bytes;
|
||||
invalid[4] = 2;
|
||||
assert_eq!(
|
||||
WindowEdges::default().load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("boolean"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
//! Horizontal comparator history only, not pixel-fetch or color-register timing.
|
||||
//! Sample X=0..255 at line cycles 0,4,..1020; right-edge matches clear after left.
|
||||
|
||||
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
|
||||
use crate::{display::CYCLES_PER_LINE, video::WIDTH};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -166,3 +167,67 @@ mod tests {
|
|||
assert_eq!(state, before); // X0 already passed, X1 has not started.
|
||||
}
|
||||
}
|
||||
|
||||
impl Snapshot for HorizontalWindows {
|
||||
fn save(&self, writer: &mut Writer) {
|
||||
let Self { active, pixels } = self;
|
||||
writer.tag(b"HWIN");
|
||||
for value in active {
|
||||
writer.bool(*value);
|
||||
}
|
||||
for window in pixels {
|
||||
for value in window {
|
||||
writer.bool(*value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
|
||||
let Self { active, pixels } = self;
|
||||
reader.tag(b"HWIN")?;
|
||||
for value in active {
|
||||
*value = reader.bool()?;
|
||||
}
|
||||
for window in pixels {
|
||||
for value in window {
|
||||
*value = reader.bool()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod snapshot_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn horizontal_windows_round_trip_comparator_history() {
|
||||
let mut source = HorizontalWindows {
|
||||
active: [true, false],
|
||||
..HorizontalWindows::default()
|
||||
};
|
||||
for window in 0..2 {
|
||||
for x in 0..WIDTH {
|
||||
source.pixels[window][x] = (x + window) % 3 != 0;
|
||||
}
|
||||
}
|
||||
let mut writer = Writer::new();
|
||||
source.save(&mut writer);
|
||||
let bytes = writer.finish().unwrap();
|
||||
let mut restored = HorizontalWindows::default();
|
||||
restored.load(&mut Reader::new(&bytes)).unwrap();
|
||||
assert_eq!(restored, source);
|
||||
for end in 0..bytes.len() {
|
||||
assert!(HorizontalWindows::default()
|
||||
.load(&mut Reader::new(&bytes[..end]))
|
||||
.is_err());
|
||||
}
|
||||
let mut invalid = bytes;
|
||||
invalid[4] = 2;
|
||||
assert_eq!(
|
||||
HorizontalWindows::default().load(&mut Reader::new(&invalid)),
|
||||
Err(SnapshotError::Corrupt("boolean"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
197
crates/gba-core/tests/save_state.rs
Normal file
197
crates/gba-core/tests/save_state.rs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
//! Save states must be indistinguishable from uninterrupted execution.
|
||||
//! Original programs only; no game ROMs or recordings.
|
||||
use gba_core::{
|
||||
audio::{StereoLevel, AUDIO_QUEUE_CAPACITY},
|
||||
bios,
|
||||
cartridge::{CartridgeHardware, SaveDevice, SAVE_START},
|
||||
input::{Button, Buttons},
|
||||
io::{SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, SOUND_START, TIMER_BASE},
|
||||
machine::Machine,
|
||||
snapshot::SnapshotError,
|
||||
video::Framebuffer,
|
||||
};
|
||||
use gba_demos::{input_rom, tile_demo};
|
||||
|
||||
const FRAME_STEPS: usize = 400_000;
|
||||
|
||||
fn configured(rom: Vec<u8>, device: SaveDevice) -> Machine {
|
||||
let mut machine = bios::boot(rom).unwrap();
|
||||
let memory = machine.memory_mut();
|
||||
memory.set_cartridge_hardware(CartridgeHardware::Rtc);
|
||||
memory.set_save_device(device);
|
||||
memory.set_scanline_rendering(true);
|
||||
memory.set_audio_capture(true);
|
||||
machine
|
||||
}
|
||||
|
||||
/// Free-running timers, a triggered pulse channel, and master sound enable, so the
|
||||
/// state under test includes devices that the demo program itself never touches.
|
||||
fn start_devices(machine: &mut Machine) {
|
||||
let memory = machine.memory_mut();
|
||||
memory.write16(SOUNDCNT_X, 0x80).unwrap();
|
||||
memory.write16(SOUNDBIAS, 0x200).unwrap();
|
||||
memory.write16(SOUNDCNT_H, 0x0302).unwrap();
|
||||
memory.write16(SOUND_START + 2, 0xf141).unwrap(); // Duty, envelope sweep.
|
||||
memory.write16(SOUND_START + 4, 0x8600).unwrap(); // Frequency and trigger.
|
||||
memory.write32(TIMER_BASE, 0x0081_f000).unwrap(); // /1 timer.
|
||||
memory.write32(TIMER_BASE + 4, 0x0084_ffff).unwrap(); // Cascaded timer.
|
||||
}
|
||||
|
||||
fn buttons(frame: u64) -> Buttons {
|
||||
Buttons::default()
|
||||
.with(Button::Right, frame % 7 < 3)
|
||||
.with(Button::Down, frame % 11 < 4)
|
||||
.with(Button::A, frame % 5 == 0)
|
||||
.with(Button::L, frame % 13 < 6)
|
||||
}
|
||||
|
||||
fn fold(hash: &mut u64, value: u64) {
|
||||
*hash = (*hash ^ value).wrapping_mul(0x0000_0100_0000_01b3);
|
||||
}
|
||||
|
||||
/// One observation per frame: pixels, audio, registers, and the device clock.
|
||||
fn run(machine: &mut Machine, frames: std::ops::Range<u64>) -> Vec<u64> {
|
||||
let mut frame = Framebuffer::default();
|
||||
let mut samples = vec![StereoLevel::default(); AUDIO_QUEUE_CAPACITY];
|
||||
let mut trace = Vec::new();
|
||||
for index in frames {
|
||||
machine.memory_mut().set_buttons(buttons(index));
|
||||
machine.run_until_vblank(FRAME_STEPS).unwrap();
|
||||
let mut hash = 0xcbf2_9ce4_8422_2325;
|
||||
if machine.memory().present_frame(&mut frame).unwrap() {
|
||||
frame
|
||||
.pixels()
|
||||
.iter()
|
||||
.for_each(|&p| fold(&mut hash, p.into()));
|
||||
}
|
||||
let count = machine.memory_mut().drain_audio_samples(&mut samples);
|
||||
for level in &samples[..count] {
|
||||
fold(&mut hash, level.left as u64);
|
||||
fold(&mut hash, level.right as u64);
|
||||
}
|
||||
machine
|
||||
.cpu()
|
||||
.registers()
|
||||
.iter()
|
||||
.for_each(|&r| fold(&mut hash, r.into()));
|
||||
fold(&mut hash, machine.cycles());
|
||||
trace.push(hash);
|
||||
}
|
||||
trace
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restored_machine_continues_exactly_like_the_original() {
|
||||
let mut original = configured(tile_demo::rom(), SaveDevice::Flash128);
|
||||
start_devices(&mut original);
|
||||
run(&mut original, 0..40);
|
||||
let state = original.save_state().unwrap();
|
||||
let expected = run(&mut original, 40..70);
|
||||
let expected_end = original.save_state().unwrap();
|
||||
|
||||
let mut restored = configured(tile_demo::rom(), SaveDevice::Flash128);
|
||||
restored.load_state(&state).unwrap();
|
||||
assert_eq!(run(&mut restored, 40..70), expected);
|
||||
assert_eq!(restored.save_state().unwrap(), expected_end);
|
||||
assert_ne!(state, expected_end, "the run must change the state");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saving_and_loading_the_same_machine_is_idempotent() {
|
||||
let mut machine = configured(tile_demo::rom(), SaveDevice::Flash64);
|
||||
start_devices(&mut machine);
|
||||
run(&mut machine, 0..25);
|
||||
let state = machine.save_state().unwrap();
|
||||
let expected = run(&mut machine, 25..35);
|
||||
machine.load_state(&state).unwrap();
|
||||
assert_eq!(machine.save_state().unwrap(), state);
|
||||
assert_eq!(run(&mut machine, 25..35), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flash_operation_in_flight_completes_identically_after_restore() {
|
||||
let command = |machine: &mut Machine, byte: u8| {
|
||||
let memory = machine.memory_mut();
|
||||
memory.write8(SAVE_START + 0x5555, 0xaa).unwrap();
|
||||
memory.write8(SAVE_START + 0x2aaa, 0x55).unwrap();
|
||||
memory.write8(SAVE_START + 0x5555, byte).unwrap();
|
||||
};
|
||||
let mut original = configured(input_rom(), SaveDevice::Flash128);
|
||||
command(&mut original, 0xa0);
|
||||
original
|
||||
.memory_mut()
|
||||
.write8(SAVE_START + 0x123, 0x42)
|
||||
.unwrap();
|
||||
for _ in 0..10 {
|
||||
original.step().unwrap();
|
||||
}
|
||||
assert!(
|
||||
original.memory().save_write_pending(),
|
||||
"operation must be in flight"
|
||||
);
|
||||
let state = original.save_state().unwrap();
|
||||
|
||||
let mut restored = configured(input_rom(), SaveDevice::Flash128);
|
||||
restored.load_state(&state).unwrap();
|
||||
assert!(restored.memory().save_write_pending());
|
||||
for machine in [&mut original, &mut restored] {
|
||||
for _ in 0..200_000 {
|
||||
machine.step().unwrap();
|
||||
}
|
||||
}
|
||||
assert!(!original.memory().save_write_pending());
|
||||
assert_eq!(
|
||||
original.memory().save_image(),
|
||||
restored.memory().save_image()
|
||||
);
|
||||
assert_eq!(original.memory().save_image().unwrap()[0x123], 0x42);
|
||||
assert!(restored.memory().save_modified());
|
||||
assert_eq!(
|
||||
original.save_state().unwrap(),
|
||||
restored.save_state().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_states_leave_the_machine_untouched() {
|
||||
let mut source = configured(tile_demo::rom(), SaveDevice::Flash128);
|
||||
run(&mut source, 0..10);
|
||||
let state = source.save_state().unwrap();
|
||||
|
||||
let mut target = configured(tile_demo::rom(), SaveDevice::Flash128);
|
||||
run(&mut target, 0..3);
|
||||
let before = target.save_state().unwrap();
|
||||
|
||||
let mut wrong_rom = state.clone();
|
||||
// Same layout, different image: rebuild a source from another ROM.
|
||||
let other = configured(input_rom(), SaveDevice::Flash128);
|
||||
wrong_rom.splice(.., other.save_state().unwrap());
|
||||
let mut flipped = state.clone();
|
||||
let middle = flipped.len() / 2;
|
||||
flipped[middle] ^= 0x01;
|
||||
let mut version = state.clone();
|
||||
version[4..8].copy_from_slice(&99u32.to_le_bytes());
|
||||
let mut magic = state.clone();
|
||||
magic[0] = b'X';
|
||||
|
||||
let cases = [
|
||||
(wrong_rom, SnapshotError::ConfigMismatch("ROM image")),
|
||||
(flipped, SnapshotError::Checksum),
|
||||
(version, SnapshotError::UnsupportedVersion(99)),
|
||||
(magic, SnapshotError::BadMagic),
|
||||
(state[..state.len() - 1].to_vec(), SnapshotError::Checksum),
|
||||
(Vec::new(), SnapshotError::Truncated),
|
||||
];
|
||||
for (bytes, error) in cases {
|
||||
assert_eq!(target.load_state(&bytes), Err(error));
|
||||
assert_eq!(target.save_state().unwrap(), before);
|
||||
}
|
||||
|
||||
let mut other_device = configured(tile_demo::rom(), SaveDevice::Flash64);
|
||||
let before = other_device.save_state().unwrap();
|
||||
assert_eq!(
|
||||
other_device.load_state(&state),
|
||||
Err(SnapshotError::ConfigMismatch("cartridge setup"))
|
||||
);
|
||||
assert_eq!(other_device.save_state().unwrap(), before);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue