diff --git a/Cargo.lock b/Cargo.lock index 6739e7fd..2a49fa4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bindgen" version = "0.69.4" @@ -246,6 +255,7 @@ name = "gb" version = "0.1.0" dependencies = [ "anyhow", + "bincode", "gb_apu", "gb_cartridge", "gb_cpu_sm83", @@ -253,6 +263,7 @@ dependencies = [ "gb_shared", "log", "mockall", + "serde", "web-time", ] @@ -278,6 +289,7 @@ dependencies = [ "blip_buf-rs", "gb_shared", "log", + "serde", ] [[package]] @@ -297,6 +309,7 @@ dependencies = [ "gb_shared", "log", "mockall", + "serde", "strum", "strum_macros", ] @@ -308,6 +321,7 @@ dependencies = [ "gb_shared", "log", "mockall", + "serde", "web-time", ] @@ -702,6 +716,26 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 2130b8f4..84d1667d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ anyhow = "1.0.81" mockall = "0.12.1" web-time = "1.1.0" wasm-bindgen = "0.2.92" +bincode = "1.3.3" +serde = { version = "1.0", features = ["derive"] } [profile.release] # Tell `rustc` to optimize for small code size. diff --git a/app/gameboy/src/App.tsx b/app/gameboy/src/App.tsx index 1d84f51f..a9c1de37 100644 --- a/app/gameboy/src/App.tsx +++ b/app/gameboy/src/App.tsx @@ -96,6 +96,66 @@ function App() { gameboy.play(); }} /> + + { + const file = evt.target.files?.[0]; + if (!file) { + return; + } + + const reader = new FileReader(); + reader.onload = () => { + const buffer = new Uint8Array(reader.result as ArrayBuffer); + try { + gameboy.restoreSnapshot(buffer); + evt.target.value = ""; + } catch (err) { + if (err instanceof Error) { + const message = err.message; + const parseGameBoyError = (errorMessage: string) => { + const RE = /^\[(E\w+?\d+?)\]/; + const match = RE.exec(errorMessage); + if (match) { + const code = match[1]; + const message = errorMessage.replace(RE, ""); + return { code, message }; + } else { + return null; + } + }; + const gbError = parseGameBoyError(message); + if (gbError) { + console.error(gbError); + return; + } + } + + throw err; + } + }; + reader.readAsArrayBuffer(file); + }} + /> ); } diff --git a/app/gameboy/src/gameboy.ts b/app/gameboy/src/gameboy.ts index 90ffd00b..c4385c1e 100644 --- a/app/gameboy/src/gameboy.ts +++ b/app/gameboy/src/gameboy.ts @@ -117,6 +117,16 @@ class GameBoyControl { this.keyState = state; } } + + takeSnapshot() { + this.ensureInstalled(); + return this.instance_!.takeSnapshot(); + } + + restoreSnapshot(snapshot: Uint8Array) { + this.ensureInstalled(); + this.instance_!.restoreSnapshot(snapshot); + } } export { GameBoyControl, JoypadKey }; diff --git a/crates/apu/Cargo.toml b/crates/apu/Cargo.toml index b39eb41a..7d012c05 100644 --- a/crates/apu/Cargo.toml +++ b/crates/apu/Cargo.toml @@ -7,3 +7,4 @@ edition = "2021" gb_shared = { workspace = true } log = { workspace = true } blip_buf-rs = "0.1.1" +serde = { workspace = true } diff --git a/crates/apu/src/blipbuf.rs b/crates/apu/src/blipbuf.rs index b5f98bae..8b67a9d7 100644 --- a/crates/apu/src/blipbuf.rs +++ b/crates/apu/src/blipbuf.rs @@ -29,4 +29,8 @@ impl BlipBuf { self.clock_time = 0; self.buf.read_samples(buffer, samples_avail, false) as usize } + + pub(crate) fn clear(&mut self) { + self.buf.clear(); + } } diff --git a/crates/apu/src/channel/envelope.rs b/crates/apu/src/channel/envelope.rs index 5e4ce309..61eb9f4e 100644 --- a/crates/apu/src/channel/envelope.rs +++ b/crates/apu/src/channel/envelope.rs @@ -1,7 +1,9 @@ use gb_shared::is_bit_set; +use serde::{Deserialize, Serialize}; use super::Frame; +#[derive(Clone, Serialize, Deserialize)] pub(super) struct Envelope { frame: Frame, /// Complete one iteration when it reaches zero. diff --git a/crates/apu/src/channel/frame_sequencer.rs b/crates/apu/src/channel/frame_sequencer.rs index b7df30af..064003ba 100644 --- a/crates/apu/src/channel/frame_sequencer.rs +++ b/crates/apu/src/channel/frame_sequencer.rs @@ -1,5 +1,8 @@ +use serde::{Deserialize, Serialize}; + use crate::{clock::Clock, utils::freq_to_period}; +#[derive(Clone, Serialize, Deserialize)] pub(crate) struct FrameSequencer { clock: Clock, frame: Frame, @@ -26,7 +29,7 @@ impl FrameSequencer { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub(crate) struct Frame(u8); impl Default for Frame { diff --git a/crates/apu/src/channel/length_counter.rs b/crates/apu/src/channel/length_counter.rs index 5e3fc49f..921069e9 100644 --- a/crates/apu/src/channel/length_counter.rs +++ b/crates/apu/src/channel/length_counter.rs @@ -1,7 +1,9 @@ use gb_shared::is_bit_set; +use serde::{Deserialize, Serialize}; use super::Frame; +#[derive(Clone, Serialize, Deserialize)] pub(crate) struct LengthCounter { pub(super) frame: Frame, /// When the length timer reaches MAX, the channel is turned off. diff --git a/crates/apu/src/channel/mod.rs b/crates/apu/src/channel/mod.rs index 11988300..472e4bbb 100644 --- a/crates/apu/src/channel/mod.rs +++ b/crates/apu/src/channel/mod.rs @@ -16,7 +16,17 @@ use pulse_channel::PulseChannel; use sweep::Sweep; use wave_channel::WaveChannel; +use self::{ + noise_channel::NoiseChannelSnapshot, pulse_channel::PulseChannelSnapshot, + wave_channel::WaveChannelSnapshot, +}; + pub(crate) type Channel1 = PulseChannel; pub(crate) type Channel2 = PulseChannel; pub(crate) type Channel3 = WaveChannel; pub(crate) type Channel4 = NoiseChannel; + +pub(crate) type Channel1Snapshot = PulseChannelSnapshot; +pub(crate) type Channel2Snapshot = PulseChannelSnapshot; +pub(crate) type Channel3Snapshot = WaveChannelSnapshot; +pub(crate) type Channel4Snapshot = NoiseChannelSnapshot; diff --git a/crates/apu/src/channel/noise_channel.rs b/crates/apu/src/channel/noise_channel.rs index 856b325a..95bc57ea 100644 --- a/crates/apu/src/channel/noise_channel.rs +++ b/crates/apu/src/channel/noise_channel.rs @@ -1,9 +1,11 @@ -use gb_shared::{is_bit_set, unset_bits, Memory}; +use gb_shared::{is_bit_set, unset_bits, Memory, Snapshot}; +use serde::{Deserialize, Serialize}; use crate::{blipbuf, clock::Clock}; -use super::{Frame, NoiseChannelLengthCounter as LengthCounter, Envelope}; +use super::{Envelope, Frame, NoiseChannelLengthCounter as LengthCounter}; +#[derive(Clone, Serialize, Deserialize)] struct Lfsr { value: u16, clock: Clock, @@ -80,7 +82,7 @@ pub(crate) struct NoiseChannel { /// Bit7, Trigger. /// Bit6, Length enable. nrx4: u8, - blipbuf: blipbuf::BlipBuf, + blipbuf: Option, length_counter: LengthCounter, envelope: Envelope, lfsr: Lfsr, @@ -99,13 +101,14 @@ impl std::fmt::Debug for NoiseChannel { } impl NoiseChannel { - pub(crate) fn new(frequency: u32, sample_rate: u32) -> Self { + pub(crate) fn new(frequency: u32, sample_rate: Option) -> Self { let nrx1 = 0; let nrx2 = 0; let nrx3 = 0; let nrx4 = 0; Self { - blipbuf: blipbuf::BlipBuf::new(frequency, sample_rate, 0), + blipbuf: sample_rate + .map(|sample_rate| blipbuf::BlipBuf::new(frequency, sample_rate, 0)), nrx1, nrx2, nrx3, @@ -125,12 +128,11 @@ impl NoiseChannel { #[inline] pub(crate) fn step(&mut self, frame: Option) { if let Some(use_volume) = self.lfsr.step(self.nrx3) { - let volume = if use_volume && (self.active()) { - self.envelope.volume() as i32 - } else { - 0 - }; - self.blipbuf.add_delta(self.lfsr.clock.div(), volume); + let volume = + if use_volume && (self.active()) { self.envelope.volume() as i32 } else { 0 }; + if let Some(blipbuf) = &mut self.blipbuf { + blipbuf.add_delta(self.lfsr.clock.div(), volume); + } } if let Some(frame) = frame { @@ -144,7 +146,7 @@ impl NoiseChannel { } pub(crate) fn read_samples(&mut self, buffer: &mut [i16], duration: u32) -> usize { - self.blipbuf.end(buffer, duration) + self.blipbuf.as_mut().map_or(0, |blipbuf| blipbuf.end(buffer, duration)) } pub(crate) fn power_off(&mut self) { @@ -207,3 +209,47 @@ impl Memory for NoiseChannel { } } } + +#[derive(Serialize, Deserialize)] +pub(crate) struct NoiseChannelSnapshot { + nrx1: u8, + nrx2: u8, + nrx3: u8, + nrx4: u8, + length_counter: LengthCounter, + envelope: Envelope, + lfsr: Lfsr, + active: bool, +} + +impl Snapshot for NoiseChannel { + type Snapshot = NoiseChannelSnapshot; + + fn snapshot(&self) -> Self::Snapshot { + NoiseChannelSnapshot { + nrx1: self.nrx1, + nrx2: self.nrx2, + nrx3: self.nrx3, + nrx4: self.nrx4, + length_counter: self.length_counter.clone(), + envelope: self.envelope.clone(), + lfsr: self.lfsr.clone(), + active: self.active, + } + } + + fn restore(&mut self, snapshot: Self::Snapshot) { + self.nrx1 = snapshot.nrx1; + self.nrx2 = snapshot.nrx2; + self.nrx3 = snapshot.nrx3; + self.nrx4 = snapshot.nrx4; + self.length_counter = snapshot.length_counter; + self.envelope = snapshot.envelope; + self.lfsr = snapshot.lfsr; + self.active = snapshot.active; + + if let Some(blipbuf) = &mut self.blipbuf { + blipbuf.clear(); + } + } +} diff --git a/crates/apu/src/channel/pulse_channel.rs b/crates/apu/src/channel/pulse_channel.rs index 685eb783..288dc4f1 100644 --- a/crates/apu/src/channel/pulse_channel.rs +++ b/crates/apu/src/channel/pulse_channel.rs @@ -1,8 +1,9 @@ -use gb_shared::{is_bit_set, Memory}; +use gb_shared::{is_bit_set, Memory, Snapshot}; +use serde::{Deserialize, Serialize}; use crate::{blipbuf, clock::Clock}; -use super::{Frame, Sweep, PulseChannelLengthCounter as LengthCounter, Envelope}; +use super::{Envelope, Frame, PulseChannelLengthCounter as LengthCounter, Sweep}; struct PulseChannelClock(Clock); @@ -32,6 +33,7 @@ impl PulseChannelClock { } } +#[derive(Clone, Copy, Serialize, Deserialize)] struct DutyCycle { index: u8, } @@ -82,7 +84,7 @@ where /// Bit4..=7, initial volume. Used to set volume envelope's volume. /// When Bit3..=7 are all 0, the DAC is off. nrx2: u8, - blipbuf: blipbuf::BlipBuf, + blipbuf: Option, channel_clock: PulseChannelClock, length_counter: LengthCounter, duty_cycle: DutyCycle, @@ -114,7 +116,7 @@ impl PulseChannel where SWEEP: Sweep, { - pub(crate) fn new(frequency: u32, sample_rate: u32) -> Self { + pub(crate) fn new(frequency: u32, sample_rate: Option) -> Self { let nrx0 = 0; let nrx1 = 0; let nrx2 = 0; @@ -126,7 +128,9 @@ where let channel_clock = PulseChannelClock::from_period(sweep.period_value()); Self { - blipbuf: blipbuf::BlipBuf::new(frequency, sample_rate, envelope.volume() as i32), + blipbuf: sample_rate.map(|sample_rate| { + blipbuf::BlipBuf::new(frequency, sample_rate, envelope.volume() as i32) + }), channel_clock, length_counter: LengthCounter::new_expired(), nrx0, @@ -151,13 +155,19 @@ where pub(crate) fn step(&mut self, frame: Option) { if self.channel_clock.step() { - if self.active() { + let volume = if self.active() { let is_high_signal = self.duty_cycle.step(self.nrx1); let volume = self.envelope.volume() as i32; - let volume = if is_high_signal { volume } else { -volume }; - self.blipbuf.add_delta(self.channel_clock.div(), volume); + if is_high_signal { + volume + } else { + -volume + } } else { - self.blipbuf.add_delta(self.channel_clock.div(), 0); + 0 + }; + if let Some(blipbuf) = &mut self.blipbuf { + blipbuf.add_delta(self.channel_clock.div(), volume); } } @@ -177,7 +187,7 @@ where } pub(crate) fn read_samples(&mut self, buffer: &mut [i16], duration: u32) -> usize { - self.blipbuf.end(buffer, duration) + self.blipbuf.as_mut().map_or(0, |blipbuf| blipbuf.end(buffer, duration)) } /// Called when the APU is turned off which resets all registers. @@ -253,3 +263,56 @@ where } } } + +#[derive(Serialize, Deserialize)] +pub(crate) struct PulseChannelSnapshot +where + SWEEP: Sweep, +{ + nrx0: u8, + nrx1: u8, + nrx2: u8, + channel_clock: Clock, + lenght_counter: LengthCounter, + duty_cycle: DutyCycle, + envelope: Envelope, + sweep: SWEEP, + active: bool, +} + +impl Snapshot for PulseChannel +where + SWEEP: Sweep + Clone, +{ + type Snapshot = PulseChannelSnapshot; + + fn snapshot(&self) -> Self::Snapshot { + PulseChannelSnapshot { + nrx0: self.nrx0, + nrx1: self.nrx1, + nrx2: self.nrx2, + channel_clock: self.channel_clock.0.clone(), + lenght_counter: self.length_counter.clone(), + duty_cycle: self.duty_cycle, + envelope: self.envelope.clone(), + sweep: self.sweep.clone(), + active: self.active, + } + } + + fn restore(&mut self, snapshot: Self::Snapshot) { + self.nrx0 = snapshot.nrx0; + self.nrx1 = snapshot.nrx1; + self.nrx2 = snapshot.nrx2; + self.channel_clock.0 = snapshot.channel_clock; + self.length_counter = snapshot.lenght_counter; + self.duty_cycle = snapshot.duty_cycle; + self.envelope = snapshot.envelope; + self.sweep = snapshot.sweep; + self.active = snapshot.active; + + if let Some(blipbuf) = &mut self.blipbuf { + blipbuf.clear(); + } + } +} diff --git a/crates/apu/src/channel/sweep.rs b/crates/apu/src/channel/sweep.rs index eeead025..726e9dd7 100644 --- a/crates/apu/src/channel/sweep.rs +++ b/crates/apu/src/channel/sweep.rs @@ -1,4 +1,5 @@ use gb_shared::is_bit_set; +use serde::{Deserialize, Serialize}; use super::Frame; @@ -13,6 +14,7 @@ pub(crate) trait Sweep: std::fmt::Debug { fn period_value(&self) -> u16; } +#[derive(Clone, Serialize, Deserialize)] pub(crate) struct SomeSweep { /// Complete one iteration when it reaches zero. /// Initialized and reset with `pace`. @@ -194,6 +196,7 @@ impl Sweep for SomeSweep { } } +#[derive(Clone, Serialize, Deserialize)] pub(crate) struct NoneSweep { nrx3: u8, nrx4: u8, diff --git a/crates/apu/src/channel/wave_channel.rs b/crates/apu/src/channel/wave_channel.rs index 2b2dd0db..763384fd 100644 --- a/crates/apu/src/channel/wave_channel.rs +++ b/crates/apu/src/channel/wave_channel.rs @@ -1,4 +1,5 @@ -use gb_shared::{is_bit_set, Memory}; +use gb_shared::{is_bit_set, Memory, Snapshot}; +use serde::{Deserialize, Serialize}; use crate::{blipbuf, clock::Clock}; @@ -13,7 +14,7 @@ enum OutputLevel { pub(crate) struct WaveRam { ram: [u8; 16], - index: usize, + index: u8, } impl std::fmt::Debug for WaveRam { @@ -32,7 +33,7 @@ impl WaveRam { } fn next_position(&mut self) -> u8 { - let value = self.ram[self.index / 2]; + let value = self.ram[self.index as usize / 2]; let value = if self.index % 2 == 0 { value >> 4 } else { value & 0x0F }; self.index = (self.index + 1) % 32; @@ -62,7 +63,7 @@ impl Memory for WaveRam { } } -pub struct WaveChannel { +pub(crate) struct WaveChannel { /// DAC enable. /// Bit7, 1: On, 0: Off nrx0: u8, @@ -90,7 +91,7 @@ pub struct WaveChannel { /// Bit 6: Length enable. /// Bit 2..=0: The upper 3 bits of the period value. nrx4: u8, - blipbuf: blipbuf::BlipBuf, + blipbuf: Option, pub(crate) wave_ram: WaveRam, length_counter: LengthCounter, channel_clock: Clock, @@ -117,7 +118,7 @@ impl WaveChannel { } impl WaveChannel { - pub(crate) fn new(frequency: u32, sample_rate: u32) -> Self { + pub(crate) fn new(frequency: u32, sample_rate: Option) -> Self { let nrx0 = 0; let nrx1 = 0; let nrx2 = 0; @@ -125,7 +126,8 @@ impl WaveChannel { let nrx4 = 0; Self { - blipbuf: blipbuf::BlipBuf::new(frequency, sample_rate, 0), + blipbuf: sample_rate + .map(|sample_rate| blipbuf::BlipBuf::new(frequency, sample_rate, 0)), nrx0, nrx1, nrx2, @@ -160,7 +162,7 @@ impl WaveChannel { pub(crate) fn step(&mut self, frame: Option) { if self.channel_clock.step() { - if self.active() { + let volume = if self.active() { let volume = self.wave_ram.next_position(); let volume = match self.output_level() { OutputLevel::Mute => 0, @@ -168,9 +170,13 @@ impl WaveChannel { OutputLevel::Half => volume >> 1, OutputLevel::Quarter => volume >> 2, }; - self.blipbuf.add_delta(self.channel_clock.div(), volume as i32); + + volume as i32 } else { - self.blipbuf.add_delta(self.channel_clock.div(), 0); + 0 + }; + if let Some(blipbuf) = &mut self.blipbuf { + blipbuf.add_delta(self.channel_clock.div(), volume); } } @@ -182,7 +188,7 @@ impl WaveChannel { } pub(crate) fn read_samples(&mut self, buffer: &mut [i16], duration: u32) -> usize { - self.blipbuf.end(buffer, duration) + self.blipbuf.as_mut().map_or(0, |blipbuf| blipbuf.end(buffer, duration)) } pub(crate) fn power_off(&mut self) { @@ -249,3 +255,53 @@ impl Memory for WaveChannel { } } } + +#[derive(Serialize, Deserialize)] +pub(crate) struct WaveChannelSnapshot { + nrx0: u8, + nrx1: u8, + nrx2: u8, + nrx3: u8, + nrx4: u8, + wave_ram: [u8; 16], + wave_ram_index: u8, + length_counter: LengthCounter, + channel_clock: Clock, + active: bool, +} + +impl Snapshot for WaveChannel { + type Snapshot = WaveChannelSnapshot; + + fn snapshot(&self) -> Self::Snapshot { + WaveChannelSnapshot { + nrx0: self.nrx0, + nrx1: self.nrx1, + nrx2: self.nrx2, + nrx3: self.nrx3, + nrx4: self.nrx4, + wave_ram: self.wave_ram.ram, + wave_ram_index: self.wave_ram.index, + length_counter: self.length_counter.clone(), + channel_clock: self.channel_clock.clone(), + active: self.active, + } + } + + fn restore(&mut self, snapshot: Self::Snapshot) { + self.nrx0 = snapshot.nrx0; + self.nrx1 = snapshot.nrx1; + self.nrx2 = snapshot.nrx2; + self.nrx3 = snapshot.nrx3; + self.nrx4 = snapshot.nrx4; + self.wave_ram.ram = snapshot.wave_ram; + self.wave_ram.index = snapshot.wave_ram_index; + self.length_counter = snapshot.length_counter; + self.channel_clock = snapshot.channel_clock; + self.active = snapshot.active; + + if let Some(blipbuf) = &mut self.blipbuf { + blipbuf.clear(); + } + } +} diff --git a/crates/apu/src/clock.rs b/crates/apu/src/clock.rs index 6f7f625b..fe33d9a8 100644 --- a/crates/apu/src/clock.rs +++ b/crates/apu/src/clock.rs @@ -1,3 +1,4 @@ +#[derive(Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct Clock { div: u32, clocks: u32, diff --git a/crates/apu/src/lib.rs b/crates/apu/src/lib.rs index 4a6a299d..d2fbca3f 100644 --- a/crates/apu/src/lib.rs +++ b/crates/apu/src/lib.rs @@ -3,9 +3,12 @@ mod channel; mod clock; mod utils; -use channel::{Channel1, Channel2, Channel3, Channel4, FrameSequencer}; +use channel::{ + Channel1, Channel1Snapshot, Channel2, Channel2Snapshot, Channel3, Channel3Snapshot, Channel4, + Channel4Snapshot, FrameSequencer, +}; use clock::Clock; -use gb_shared::{is_bit_set, Memory, CPU_FREQ}; +use gb_shared::{is_bit_set, Memory, Snapshot, CPU_FREQ}; pub type AudioOutHandle = dyn FnMut(&[(f32, f32)]); @@ -52,9 +55,10 @@ impl Apu { Clock::new(gb_shared::CPU_FREQ / Self::MIXER_FREQ) } - pub fn new(sample_rate: u32) -> Self { + pub fn new(sample_rate: Option) -> Self { let frequency = CPU_FREQ; - let buffer_size = sample_rate.div_ceil(Self::MIXER_FREQ) as usize; + let buffer_size = + sample_rate.map_or(0, |sample_rate| sample_rate.div_ceil(Self::MIXER_FREQ) as usize); let fs = FrameSequencer::new(); let instance = Self { ch1: Channel1::new(frequency, sample_rate), @@ -119,7 +123,7 @@ impl Apu { self.ch3.step(frame); self.ch4.step(frame); - if self.mixer_clock.step() { + if self.mixer_clock.step() && !self.samples_buffer.is_empty() { let left_volume_coefficient = ((self.master_left_volume() + 1) as f32 / 8.0) * (1.0 / 15.0) * 0.25; let right_volume_coefficient = @@ -310,3 +314,46 @@ impl std::fmt::Debug for Apu { .finish() } } + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct ApuSnapshot { + ch1: Channel1Snapshot, + ch2: Channel2Snapshot, + ch3: Channel3Snapshot, + ch4: Channel4Snapshot, + mixer_clock: Clock, + nr50: u8, + nr51: u8, + nr52: u8, + fs: FrameSequencer, +} + +impl Snapshot for Apu { + type Snapshot = ApuSnapshot; + + fn snapshot(&self) -> Self::Snapshot { + ApuSnapshot { + ch1: self.ch1.snapshot(), + ch2: self.ch2.snapshot(), + ch3: self.ch3.snapshot(), + ch4: self.ch4.snapshot(), + mixer_clock: self.mixer_clock.clone(), + nr50: self.nr50, + nr51: self.nr51, + nr52: self.nr52, + fs: self.fs.clone(), + } + } + + fn restore(&mut self, snapshot: Self::Snapshot) { + self.ch1.restore(snapshot.ch1); + self.ch2.restore(snapshot.ch2); + self.ch3.restore(snapshot.ch3); + self.ch4.restore(snapshot.ch4); + self.mixer_clock = snapshot.mixer_clock; + self.nr50 = snapshot.nr50; + self.nr51 = snapshot.nr51; + self.nr52 = snapshot.nr52; + self.fs = snapshot.fs; + } +} diff --git a/crates/cartridge/src/mbc/mbc1.rs b/crates/cartridge/src/mbc/mbc1.rs index 7e8ff774..5767499f 100644 --- a/crates/cartridge/src/mbc/mbc1.rs +++ b/crates/cartridge/src/mbc/mbc1.rs @@ -1,7 +1,6 @@ use super::{real_ram_size, RamBank}; use crate::CartridgeHeader; use gb_shared::{boxed_array, kib}; -use std::path::Path; /// https://gbdev.io/pandocs/MBC1.html pub(crate) struct Mbc1 { @@ -100,8 +99,8 @@ impl super::Mbc for Mbc1 { } } - fn store(&self, path: &Path) -> anyhow::Result<()> { - #[cfg(not(target_family = "wasm"))] + #[cfg(not(target_family = "wasm"))] + fn store(&self, path: &std::path::Path) -> anyhow::Result<()> { if self.with_battery { use std::io::Write; let mut file = std::fs::File::create(path)?; @@ -114,8 +113,8 @@ impl super::Mbc for Mbc1 { Ok(()) } - fn restore(&mut self, path: &Path) -> anyhow::Result<()> { - #[cfg(not(target_family = "wasm"))] + #[cfg(not(target_family = "wasm"))] + fn restore(&mut self, path: &std::path::Path) -> anyhow::Result<()> { if self.with_battery { use std::io::Read; let mut file = std::fs::File::open(path)?; diff --git a/crates/cartridge/src/mbc/mbc2.rs b/crates/cartridge/src/mbc/mbc2.rs index 10542dac..982a4d5d 100644 --- a/crates/cartridge/src/mbc/mbc2.rs +++ b/crates/cartridge/src/mbc/mbc2.rs @@ -65,8 +65,8 @@ impl Mbc for Mbc2 { } } + #[cfg(not(target_family = "wasm"))] fn store(&self, path: &std::path::Path) -> anyhow::Result<()> { - #[cfg(not(target_family = "wasm"))] if self.with_battery { use std::io::Write; let mut file = std::fs::File::create(path)?; @@ -77,8 +77,8 @@ impl Mbc for Mbc2 { Ok(()) } + #[cfg(not(target_family = "wasm"))] fn restore(&mut self, path: &std::path::Path) -> anyhow::Result<()> { - #[cfg(not(target_family = "wasm"))] if self.with_battery { use std::io::Read; let mut file = std::fs::File::open(path)?; diff --git a/crates/cartridge/src/mbc/mbc3.rs b/crates/cartridge/src/mbc/mbc3.rs index 21bc350c..450e1fd2 100644 --- a/crates/cartridge/src/mbc/mbc3.rs +++ b/crates/cartridge/src/mbc/mbc3.rs @@ -1,7 +1,6 @@ use super::{real_ram_size, Mbc, RamBank}; use crate::CartridgeHeader; use gb_shared::{boxed_array, kib}; -use std::path::Path; use web_time::SystemTime; pub(crate) struct Mbc3 { @@ -123,8 +122,8 @@ impl Mbc for Mbc3 { } } - fn store(&self, path: &Path) -> anyhow::Result<()> { - #[cfg(not(target_family = "wasm"))] + #[cfg(not(target_family = "wasm"))] + fn store(&self, path: &std::path::Path) -> anyhow::Result<()> { if self.with_battery { use std::io::Write; let mut file = std::fs::File::create(path)?; @@ -139,8 +138,8 @@ impl Mbc for Mbc3 { Ok(()) } - fn restore(&mut self, path: &Path) -> anyhow::Result<()> { - #[cfg(not(target_family = "wasm"))] + #[cfg(not(target_family = "wasm"))] + fn restore(&mut self, path: &std::path::Path) -> anyhow::Result<()> { if self.with_battery { use std::io::Read; let mut file = std::fs::File::open(path)?; @@ -200,33 +199,27 @@ impl RealTimeClock { } } - pub(crate) fn store>(&self, path: P) -> anyhow::Result<()> { - #[cfg(not(target_family = "wasm"))] - { - use std::io::Write; - let mut file = std::fs::File::create(path)?; - file.write_all(self.epoch.to_be_bytes().as_ref())?; - file.flush()?; - } + #[cfg(not(target_family = "wasm"))] + pub(crate) fn store>(&self, path: P) -> anyhow::Result<()> { + use std::io::Write; + let mut file = std::fs::File::create(path)?; + file.write_all(self.epoch.to_be_bytes().as_ref())?; + file.flush()?; Ok(()) } - pub(crate) fn restore>(&mut self, path: P) -> anyhow::Result<()> { - #[cfg(not(target_family = "wasm"))] - { - self.epoch = match std::fs::read(path) { - Ok(value) => { - let mut bytes: [u8; 8] = Default::default(); - debug_assert!(value.len() == 8); - bytes.copy_from_slice(&value); - u64::from_be_bytes(bytes) - } - Err(_) => { - SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() - } - }; - } + #[cfg(not(target_family = "wasm"))] + pub(crate) fn restore>(&mut self, path: P) -> anyhow::Result<()> { + self.epoch = match std::fs::read(path) { + Ok(value) => { + let mut bytes: [u8; 8] = Default::default(); + debug_assert!(value.len() == 8); + bytes.copy_from_slice(&value); + u64::from_be_bytes(bytes) + } + Err(_) => SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(), + }; Ok(()) } diff --git a/crates/cartridge/src/mbc/mbc5.rs b/crates/cartridge/src/mbc/mbc5.rs index 728f83ec..fedfa065 100644 --- a/crates/cartridge/src/mbc/mbc5.rs +++ b/crates/cartridge/src/mbc/mbc5.rs @@ -103,8 +103,8 @@ impl Mbc for Mbc5 { } } + #[cfg(not(target_family = "wasm"))] fn store(&self, path: &std::path::Path) -> anyhow::Result<()> { - #[cfg(not(target_family = "wasm"))] if self.with_battery { use std::io::Write; let mut file = std::fs::File::create(path)?; @@ -117,8 +117,8 @@ impl Mbc for Mbc5 { Ok(()) } + #[cfg(not(target_family = "wasm"))] fn restore(&mut self, path: &std::path::Path) -> anyhow::Result<()> { - #[cfg(not(target_family = "wasm"))] if self.with_battery { use std::io::Read; let mut file = std::fs::File::open(path)?; diff --git a/crates/cpu_sm83/Cargo.toml b/crates/cpu_sm83/Cargo.toml index 50d0797c..d1723768 100644 --- a/crates/cpu_sm83/Cargo.toml +++ b/crates/cpu_sm83/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] log = { workspace = true } gb_shared = { workspace = true } +serde = { workspace = true } [dev-dependencies] mockall = { workspace = true } diff --git a/crates/cpu_sm83/src/lib.rs b/crates/cpu_sm83/src/lib.rs index 23220ab8..a5c1f9ab 100644 --- a/crates/cpu_sm83/src/lib.rs +++ b/crates/cpu_sm83/src/lib.rs @@ -4,7 +4,7 @@ mod interrupt; mod proc; use cpu16::{Cpu16, Register16, Register8}; -use gb_shared::{is_bit_set, set_bits, unset_bits}; +use gb_shared::{is_bit_set, set_bits, unset_bits, Snapshot}; use interrupt::INTERRUPTS; impl Cpu16 for Cpu @@ -1519,3 +1519,73 @@ mod tests { } } } + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct CpuSnapshot { + a: u8, + f: u8, + b: u8, + c: u8, + d: u8, + e: u8, + h: u8, + l: u8, + sp: u16, + pc: u16, + ime: bool, + enabling_ime: bool, + halted: bool, + stopped: bool, + clocks: u8, + ir: u8, + handle_itr: bool, +} + +impl Snapshot for Cpu +where + BUS: gb_shared::Memory + gb_shared::Component, +{ + type Snapshot = CpuSnapshot; + + fn snapshot(&self) -> Self::Snapshot { + CpuSnapshot { + a: self.reg_a, + f: self.reg_f, + b: self.reg_b, + c: self.reg_c, + d: self.reg_d, + e: self.reg_e, + h: self.reg_h, + l: self.reg_l, + sp: self.sp, + pc: self.pc, + ime: self.ime, + enabling_ime: self.enabling_ime, + halted: self.halted, + stopped: self.stopped, + clocks: self.clocks, + ir: self.ir, + handle_itr: self.handle_itr, + } + } + + fn restore(&mut self, snapshot: Self::Snapshot) { + self.reg_a = snapshot.a; + self.reg_f = snapshot.f; + self.reg_b = snapshot.b; + self.reg_c = snapshot.c; + self.reg_d = snapshot.d; + self.reg_e = snapshot.e; + self.reg_h = snapshot.h; + self.reg_l = snapshot.l; + self.sp = snapshot.sp; + self.pc = snapshot.pc; + self.ime = snapshot.ime; + self.enabling_ime = snapshot.enabling_ime; + self.halted = snapshot.halted; + self.stopped = snapshot.stopped; + self.clocks = snapshot.clocks; + self.ir = snapshot.ir; + self.handle_itr = snapshot.handle_itr; + } +} diff --git a/crates/gb-wasm/src/lib.rs b/crates/gb-wasm/src/lib.rs index cc1be584..41b22f04 100644 --- a/crates/gb-wasm/src/lib.rs +++ b/crates/gb-wasm/src/lib.rs @@ -4,8 +4,10 @@ mod utils; use cpal::traits::StreamTrait; use cpal::Stream; use gb::wasm::{Cartridge, GameBoy, Manifest}; +use gb::GameBoySnapshot; use gb_shared::boxed::BoxedArray; use gb_shared::command::{Command, JoypadCommand, JoypadKey}; +use gb_shared::Snapshot; use js_sys::Uint8ClampedArray; use wasm_bindgen::{prelude::*, Clamped}; use web_sys::{js_sys, CanvasRenderingContext2d, HtmlCanvasElement, ImageData}; @@ -39,10 +41,9 @@ impl ScaleImageData { let y_end = y_begin + scale; for y in y_begin..y_end { - for x in x_begin..x_end { - let offset = (y * 160 * scale + x) * 4; - self.0[offset..(offset + 4)].copy_from_slice(color); - } + let begin = (y * 160 * scale + x_begin) * 4; + let end = (y * 160 * scale + x_end) * 4; + self.0[begin..end].chunks_mut(4).for_each(|chunk| chunk.copy_from_slice(color)); } } @@ -136,4 +137,27 @@ impl GameBoyHandle { pub fn change_key_state(&mut self, state: u8) { self.gb.exec_command(Command::Joypad(JoypadCommand::State(state))); } + + #[wasm_bindgen(js_name = takeSnapshot)] + pub fn take_snapshot(&self) -> js_sys::Uint8Array { + let snapshot = self.gb.snapshot(); + let bytes: Vec = Vec::try_from(&snapshot).unwrap(); + + js_sys::Uint8Array::from(bytes.as_slice()) + } + + #[wasm_bindgen(js_name = restoreSnapshot)] + pub fn restore_snapshot(&mut self, snapshot: js_sys::Uint8Array) -> Result<(), JsError> { + match GameBoySnapshot::try_from(snapshot.to_vec().as_slice()) { + Ok(snapshot) => { + if snapshot.cart_checksum() != self.gb.cart_checksum() { + return Err(JsError::new("[ESS2]The snapshot doesn't match the game")); + } + self.gb.restore(snapshot); + + Ok(()) + } + Err(_) => Err(JsError::new("[ESS1]Snapshot is broken")), + } + } } diff --git a/crates/gb/Cargo.toml b/crates/gb/Cargo.toml index 0d89807f..453b78d7 100644 --- a/crates/gb/Cargo.toml +++ b/crates/gb/Cargo.toml @@ -12,6 +12,8 @@ gb_shared = { workspace = true } log = { workspace = true } anyhow = { workspace = true } web-time = { workspace = true } +serde = { workspace = true } +bincode = { workspace = true } [dev-dependencies] mockall = { workspace = true } diff --git a/crates/gb/src/bus.rs b/crates/gb/src/bus.rs index 7b2f8fcd..ec70857a 100644 --- a/crates/gb/src/bus.rs +++ b/crates/gb/src/bus.rs @@ -1,10 +1,17 @@ -use gb_apu::Apu; +use gb_apu::{Apu, ApuSnapshot}; use gb_cartridge::Cartridge; -use gb_ppu::Ppu; -use gb_shared::{command::Command, Memory}; +use gb_ppu::{Ppu, PpuSnapshot}; +use gb_shared::{command::Command, Memory, Snapshot}; use std::ops::{Deref, DerefMut}; -use crate::{dma::DMA, hram::HighRam, joypad::Joypad, serial::Serial, timer::Timer, wram::WorkRam}; +use crate::{ + dma::{DmaSnapshot, DMA}, + hram::{HighRam, HighRamSnapshot}, + joypad::{Joypad, JoypadSnapshot}, + serial::{Serial, SerialSnapshot}, + timer::{Timer, TimerSnapshot}, + wram::{WorkRam, WorkRamSnapshot}, +}; pub(crate) struct BusInner { /// R/W. Set the bit to be 1 if the corresponding @@ -37,7 +44,7 @@ pub(crate) struct BusInner { joypad: Joypad, timer: Timer, pub(crate) ppu: Ppu, - pub(crate) apu: Option, + pub(crate) apu: Apu, ref_count: usize, } @@ -79,9 +86,7 @@ impl Memory for BusInner { self.interrupt_flag = 0xE0 | value } 0xFF10..=0xFF3F => { - if let Some(apu) = &mut self.apu { - apu.write(addr, value); - } + self.apu.write(addr, value); } 0xFF46 => { // DMA @@ -150,13 +155,7 @@ impl Memory for BusInner { // IF self.interrupt_flag } - 0xFF10..=0xFF3F => { - if let Some(apu) = &self.apu { - apu.read(addr) - } else { - 0 - } - } + 0xFF10..=0xFF3F => self.apu.read(addr), 0xFF46 => self.dma.read(addr), 0xFF40..=0xFF4B => self.ppu.read(addr), _ => { @@ -210,7 +209,7 @@ impl Bus { joypad: Joypad::new(), timer: Timer::new(), ppu: Ppu::new(), - apu: sample_rate.map(Apu::new), + apu: Apu::new(sample_rate), ref_count: 1, })), } @@ -255,9 +254,7 @@ impl gb_shared::Component for Bus { let irq = self.timer.take_irq(); self.set_irq(irq); - if let Some(apu) = self.apu.as_mut() { - apu.step(); - } + self.apu.step(); } // It costs 160 machine cycles to transfer 160 bytes of data. @@ -299,3 +296,49 @@ impl Memory for Bus { unsafe { (*self.ptr).read(addr) } } } + +#[derive(serde::Serialize, serde::Deserialize)] +pub(crate) struct BusSnapshot { + interrupt_enable: u8, + interrupt_flag: u8, + wram: WorkRamSnapshot, + hram: HighRamSnapshot, + dma: DmaSnapshot, + serial: SerialSnapshot, + joypad: JoypadSnapshot, + timer: TimerSnapshot, + ppu: PpuSnapshot, + apu: ApuSnapshot, +} + +impl Snapshot for Bus { + type Snapshot = BusSnapshot; + + fn snapshot(&self) -> Self::Snapshot { + BusSnapshot { + interrupt_enable: self.interrupt_enable, + interrupt_flag: self.interrupt_flag, + wram: self.wram.snapshot(), + hram: self.hram.snapshot(), + dma: self.dma.snapshot(), + serial: self.serial.snapshot(), + joypad: self.joypad.snapshot(), + timer: self.timer.snapshot(), + ppu: self.ppu.snapshot(), + apu: self.apu.snapshot(), + } + } + + fn restore(&mut self, snapshot: Self::Snapshot) { + self.interrupt_enable = snapshot.interrupt_enable; + self.interrupt_flag = snapshot.interrupt_flag; + self.wram.restore(snapshot.wram); + self.hram.restore(snapshot.hram); + self.dma.restore(snapshot.dma); + self.serial.restore(snapshot.serial); + self.joypad.restore(snapshot.joypad); + self.timer.restore(snapshot.timer); + self.ppu.restore(snapshot.ppu); + self.apu.restore(snapshot.apu); + } +} diff --git a/crates/gb/src/dma.rs b/crates/gb/src/dma.rs index 2d36c3cc..0c3e075e 100644 --- a/crates/gb/src/dma.rs +++ b/crates/gb/src/dma.rs @@ -1,4 +1,4 @@ -use gb_shared::Memory; +use gb_shared::{Memory, Snapshot}; #[allow(clippy::upper_case_acronyms)] #[derive(Debug)] @@ -45,6 +45,25 @@ impl DMA { } } +#[derive(serde::Serialize, serde::Deserialize)] +pub(crate) struct DmaSnapshot { + value: u8, + offset: u8, +} + +impl Snapshot for DMA { + type Snapshot = DmaSnapshot; + + fn snapshot(&self) -> Self::Snapshot { + DmaSnapshot { value: self.value, offset: self.offset } + } + + fn restore(&mut self, snapshot: Self::Snapshot) { + self.value = snapshot.value; + self.offset = snapshot.offset; + } +} + #[cfg(test)] mod tests { #[test] diff --git a/crates/gb/src/hram.rs b/crates/gb/src/hram.rs index fad9d106..ca971d4e 100644 --- a/crates/gb/src/hram.rs +++ b/crates/gb/src/hram.rs @@ -1,4 +1,4 @@ -use gb_shared::{boxed_array, Memory}; +use gb_shared::{boxed_array, boxed_array_try_from_vec, Memory, Snapshot}; pub(crate) struct HighRam { /// [FF80, FFFF) @@ -26,3 +26,20 @@ impl Memory for HighRam { self.ram[addr] } } + +#[derive(serde::Serialize, serde::Deserialize)] +pub(crate) struct HighRamSnapshot { + ram: Vec, +} + +impl Snapshot for HighRam { + type Snapshot = HighRamSnapshot; + + fn snapshot(&self) -> Self::Snapshot { + HighRamSnapshot { ram: self.ram.to_vec() } + } + + fn restore(&mut self, snapshot: Self::Snapshot) { + self.ram = boxed_array_try_from_vec(snapshot.ram).unwrap(); + } +} diff --git a/crates/gb/src/joypad.rs b/crates/gb/src/joypad.rs index e6e98bd3..5464af15 100644 --- a/crates/gb/src/joypad.rs +++ b/crates/gb/src/joypad.rs @@ -1,6 +1,6 @@ use gb_shared::{ command::{JoypadCommand, JoypadKey}, - is_bit_set, Interrupt, InterruptRequest, Memory, + is_bit_set, Interrupt, InterruptRequest, Memory, Snapshot, }; /// The state is true when the value is zero. @@ -81,6 +81,31 @@ impl Joypad { } } +#[derive(serde::Serialize, serde::Deserialize)] +pub(crate) struct JoypadSnapshot { + buttons: u8, + select_action: bool, + select_direction: bool, +} + +impl Snapshot for Joypad { + type Snapshot = JoypadSnapshot; + + fn snapshot(&self) -> Self::Snapshot { + JoypadSnapshot { + buttons: self.buttons, + select_action: self.select_action, + select_direction: self.select_direction, + } + } + + fn restore(&mut self, snapshot: Self::Snapshot) { + self.buttons = snapshot.buttons; + self.select_action = snapshot.select_action; + self.select_direction = snapshot.select_direction; + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/gb/src/lib.rs b/crates/gb/src/lib.rs index 36b368db..4648f476 100644 --- a/crates/gb/src/lib.rs +++ b/crates/gb/src/lib.rs @@ -12,12 +12,12 @@ mod wram; use web_time::{Duration, Instant}; -use bus::Bus; +use bus::{Bus, BusSnapshot}; use gb_apu::AudioOutHandle; use gb_cartridge::Cartridge; -use gb_cpu_sm83::Cpu; +use gb_cpu_sm83::{Cpu, CpuSnapshot}; pub use gb_ppu::FrameOutHandle; -use gb_shared::{command::Command, CPU_FREQ}; +use gb_shared::{command::Command, Snapshot, CPU_FREQ}; pub struct Manifest { pub cart: Cartridge, @@ -29,16 +29,18 @@ pub struct GameBoy { bus: Bus, clocks: u32, ts: Instant, + cart_checksum: u16, } impl GameBoy { const EXEC_DURATION: Duration = Duration::from_millis(1000 / 4); - const EXEC_CYCLES: u32 = (CPU_FREQ / 4); + const EXEC_CLOCKS: u32 = (CPU_FREQ / 4); pub fn new(manifest: Manifest) -> Self { let Manifest { cart, sample_rate } = manifest; let cart_header_checksum = cart.header.checksum; + let cart_global_checksum = cart.header.global_checksum; let bus = Bus::new(cart, sample_rate); let mut cpu = Cpu::new(bus.clone()); @@ -48,7 +50,7 @@ impl GameBoy { cpu.reg_f = 0x80; } - Self { cpu, bus, clocks: 0, ts: Instant::now() } + Self { cpu, bus, clocks: 0, ts: Instant::now(), cart_checksum: cart_global_checksum } } pub fn set_handles( @@ -57,9 +59,12 @@ impl GameBoy { audio_out_handle: Option>, ) { self.bus.ppu.set_frame_out_handle(frame_out_handle); - if let Some(apu) = self.bus.apu.as_mut() { - apu.set_audio_out_handle(audio_out_handle); - } + self.bus.apu.set_audio_out_handle(audio_out_handle); + } + + #[inline] + pub fn cart_checksum(&self) -> u16 { + self.cart_checksum } pub fn play( @@ -74,8 +79,8 @@ impl GameBoy { loop { self.cpu.step(); - let cycles = self.clocks + self.cpu.take_clocks() as u32; - self.clocks = cycles % Self::EXEC_CYCLES; + let clocks = self.clocks + self.cpu.take_clocks() as u32; + self.clocks = clocks % Self::EXEC_CLOCKS; match pull_command()? { Some(Command::Exit) => return Ok(()), @@ -83,7 +88,7 @@ impl GameBoy { None => {} } - if cycles >= Self::EXEC_CYCLES { + if clocks >= Self::EXEC_CLOCKS { let duration = self.ts.elapsed(); if duration < Self::EXEC_DURATION { std::thread::sleep(Self::EXEC_DURATION - duration); @@ -93,3 +98,50 @@ impl GameBoy { } } } + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct GameBoySnapshot { + cart_checksum: u16, + bus: BusSnapshot, + cpu: CpuSnapshot, +} + +impl GameBoySnapshot { + #[inline] + pub fn cart_checksum(&self) -> u16 { + self.cart_checksum + } +} + +impl Snapshot for GameBoy { + type Snapshot = GameBoySnapshot; + + fn snapshot(&self) -> Self::Snapshot { + Self::Snapshot { + bus: self.bus.snapshot(), + cpu: self.cpu.snapshot(), + cart_checksum: self.cart_checksum, + } + } + + fn restore(&mut self, snapshot: Self::Snapshot) { + self.bus.restore(snapshot.bus); + self.cpu.restore(snapshot.cpu); + } +} + +impl TryFrom<&[u8]> for GameBoySnapshot { + type Error = bincode::Error; + + fn try_from(value: &[u8]) -> Result { + bincode::deserialize(value) + } +} + +impl TryFrom<&GameBoySnapshot> for Vec { + type Error = bincode::Error; + + fn try_from(value: &GameBoySnapshot) -> Result { + bincode::serialize(value) + } +} diff --git a/crates/gb/src/serial.rs b/crates/gb/src/serial.rs index e416ec8d..f4e5bd8c 100644 --- a/crates/gb/src/serial.rs +++ b/crates/gb/src/serial.rs @@ -1,16 +1,16 @@ -use gb_shared::Memory; +use gb_shared::{Memory, Snapshot}; pub(crate) struct Serial { - serial_transfer_data: u8, - serial_transfer_control: u8, + data: u8, + control: u8, } impl Memory for Serial { fn write(&mut self, addr: u16, value: u8) { if addr == 0xFF01 { - self.serial_transfer_data = value; + self.data = value; } else if addr == 0xFF02 { - self.serial_transfer_control = value | 0x7C; + self.control = value | 0x7C; } else { unreachable!() } @@ -18,9 +18,9 @@ impl Memory for Serial { fn read(&self, addr: u16) -> u8 { if addr == 0xFF01 { - self.serial_transfer_data + self.data } else if addr == 0xFF02 { - self.serial_transfer_control + self.control } else { unreachable!() } @@ -29,6 +29,25 @@ impl Memory for Serial { impl Serial { pub(crate) fn new() -> Self { - Self { serial_transfer_data: 0, serial_transfer_control: 0x7E } + Self { data: 0, control: 0x7E } + } +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub(crate) struct SerialSnapshot { + data: u8, + control: u8, +} + +impl Snapshot for Serial { + type Snapshot = SerialSnapshot; + + fn snapshot(&self) -> Self::Snapshot { + SerialSnapshot { data: self.data, control: self.control } + } + + fn restore(&mut self, snapshot: Self::Snapshot) { + self.data = snapshot.data; + self.control = snapshot.control; } } diff --git a/crates/gb/src/timer.rs b/crates/gb/src/timer.rs index 8303a25d..ceb71534 100644 --- a/crates/gb/src/timer.rs +++ b/crates/gb/src/timer.rs @@ -1,4 +1,4 @@ -use gb_shared::{is_bit_set, Interrupt, InterruptRequest, Memory}; +use gb_shared::{is_bit_set, Interrupt, InterruptRequest, Memory, Snapshot}; enum CounterIncCycles { Cycles1024, @@ -106,6 +106,29 @@ impl Timer { } } +#[derive(serde::Serialize, serde::Deserialize)] +pub(crate) struct TimerSnapshot { + div: u16, + tima: u8, + tma: u8, + tac: u8, +} + +impl Snapshot for Timer { + type Snapshot = TimerSnapshot; + + fn snapshot(&self) -> Self::Snapshot { + TimerSnapshot { div: self.div, tima: self.tima, tma: self.tma, tac: self.tac } + } + + fn restore(&mut self, snapshot: Self::Snapshot) { + self.div = snapshot.div; + self.tima = snapshot.tima; + self.tma = snapshot.tma; + self.tac = snapshot.tac; + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/gb/src/wram.rs b/crates/gb/src/wram.rs index e12784f0..e472ccae 100644 --- a/crates/gb/src/wram.rs +++ b/crates/gb/src/wram.rs @@ -1,4 +1,4 @@ -use gb_shared::{boxed_array, Memory}; +use gb_shared::{boxed_array, boxed_array_try_from_vec, Memory, Snapshot}; pub(crate) struct WorkRam { /// [C000, E000) @@ -27,3 +27,20 @@ impl Memory for WorkRam { self.ram[addr] } } + +#[derive(serde::Serialize, serde::Deserialize)] +pub(crate) struct WorkRamSnapshot { + ram: Vec, +} + +impl Snapshot for WorkRam { + type Snapshot = WorkRamSnapshot; + + fn snapshot(&self) -> Self::Snapshot { + WorkRamSnapshot { ram: self.ram.to_vec() } + } + + fn restore(&mut self, snapshot: Self::Snapshot) { + self.ram = boxed_array_try_from_vec(snapshot.ram).unwrap(); + } +} diff --git a/crates/ppu/Cargo.toml b/crates/ppu/Cargo.toml index ed30a850..63737a24 100644 --- a/crates/ppu/Cargo.toml +++ b/crates/ppu/Cargo.toml @@ -8,3 +8,4 @@ gb_shared = { workspace = true } log = { workspace = true } mockall = { workspace = true } web-time = { workspace = true } +serde = { workspace = true } diff --git a/crates/ppu/src/lcd.rs b/crates/ppu/src/lcd.rs index 0b26be8a..668c00aa 100644 --- a/crates/ppu/src/lcd.rs +++ b/crates/ppu/src/lcd.rs @@ -1,4 +1,4 @@ -use gb_shared::is_bit_set; +use gb_shared::{is_bit_set, Snapshot}; #[repr(u8)] #[derive(Debug, PartialEq, Eq)] @@ -108,3 +108,43 @@ impl LCD { is_bit_set!(self.lcdc, 7) } } + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub(crate) struct LCDSnapshot { + lcdc: u8, + stat: u8, + ly: u8, + lyc: u8, + wy: u8, + wx: u8, + scy: u8, + scx: u8, +} + +impl Snapshot for LCD { + type Snapshot = LCDSnapshot; + + fn snapshot(&self) -> Self::Snapshot { + LCDSnapshot { + lcdc: self.lcdc, + stat: self.stat, + ly: self.ly, + lyc: self.lyc, + wy: self.wy, + wx: self.wx, + scy: self.scy, + scx: self.scx, + } + } + + fn restore(&mut self, snapshot: Self::Snapshot) { + self.lcdc = snapshot.lcdc; + self.stat = snapshot.stat; + self.ly = snapshot.ly; + self.lyc = snapshot.lyc; + self.wy = snapshot.wy; + self.wx = snapshot.wx; + self.scy = snapshot.scy; + self.scx = snapshot.scx; + } +} diff --git a/crates/ppu/src/lib.rs b/crates/ppu/src/lib.rs index b52167de..4b10dcf5 100644 --- a/crates/ppu/src/lib.rs +++ b/crates/ppu/src/lib.rs @@ -1,5 +1,4 @@ mod config; -mod fps; mod lcd; mod object; mod tile; @@ -7,9 +6,10 @@ mod tile; use crate::config::{DOTS_PER_SCANLINE, RESOLUTION_X, RESOLUTION_Y, SCANLINES_PER_FRAME}; use crate::lcd::{LCDMode, LCD}; use crate::object::Object; -use fps::Fps; use gb_shared::boxed::BoxedArray; -use gb_shared::{is_bit_set, set_bits, unset_bits, Interrupt, InterruptRequest, Memory}; +use gb_shared::{is_bit_set, set_bits, unset_bits, Interrupt, InterruptRequest, Memory, Snapshot}; +use lcd::LCDSnapshot; +use object::ObjectSnapshot; pub type VideoFrame = BoxedArray; // 160 * 144 @@ -39,7 +39,6 @@ pub(crate) struct PpuWorkState { /// Whether window is used in current scanline. /// Used for incrementing window_line. window_used: bool, - fps: Fps, } #[derive(Default)] @@ -192,7 +191,6 @@ impl Ppu { self.video_buffer.iter_mut().for_each(|pixel| { *pixel = 0; }); - self.work_state.fps.stop(); self.frame_id = 0; self.push_frame(); } @@ -410,9 +408,6 @@ impl Ppu { // https://gbdev.io/pandocs/Rendering.html#obj-penalty-algorithm:~:text=one%20frame%20takes%20~-,16.74,-ms%20instead%20of // (456 * 154) * (1/(2**22)) * 1000 = 16.74ms // Notify that a frame is rendered. - if let Some(fps) = self.work_state.fps.update() { - log::info!("Render FPS: {:.2}", fps); - } self.frame_id = self.frame_id.wrapping_add(1); self.push_frame(); } @@ -494,6 +489,73 @@ impl Memory for Ppu { } } +#[derive(serde::Serialize, serde::Deserialize)] +pub struct PpuSnapshot { + vram: Vec, // 0x2000 + oam: Vec, // 0xA0 + lcd: LCDSnapshot, + bgp: u8, + obp0: u8, + obp1: u8, + // #region Work state + scanline_x: u8, + scanline_dots: u16, + scanline_objects: Vec, + window_line: u8, + window_used: bool, + // #endregion + video_buffer: Vec, // 160 * 144 +} + +impl Snapshot for Ppu { + type Snapshot = PpuSnapshot; + + fn snapshot(&self) -> Self::Snapshot { + PpuSnapshot { + vram: self.vram.to_vec(), + oam: self.oam.to_vec(), + lcd: self.lcd.snapshot(), + bgp: self.bgp, + obp0: self.obp0, + obp1: self.obp1, + scanline_x: self.work_state.scanline_x, + scanline_dots: self.work_state.scanline_dots, + scanline_objects: self + .work_state + .scanline_objects + .iter() + .map(|o| o.snapshot()) + .collect(), + window_line: self.work_state.window_line, + window_used: self.work_state.window_used, + video_buffer: self.video_buffer.to_vec(), + } + } + + fn restore(&mut self, snapshot: Self::Snapshot) { + self.vram = BoxedArray::try_from_vec(snapshot.vram).unwrap(); + self.oam = BoxedArray::try_from_vec(snapshot.oam).unwrap(); + self.lcd.restore(snapshot.lcd); + self.bgp = snapshot.bgp; + self.obp0 = snapshot.obp0; + self.obp1 = snapshot.obp1; + self.work_state.scanline_x = snapshot.scanline_x; + self.work_state.scanline_dots = snapshot.scanline_dots; + self.work_state.scanline_objects = snapshot + .scanline_objects + .into_iter() + .map(|o| { + let mut object = Object::default(); + object.restore(o); + object + }) + .collect(); + self.work_state.window_line = snapshot.window_line; + self.work_state.window_used = snapshot.window_used; + self.video_buffer = BoxedArray::try_from_vec(snapshot.video_buffer).unwrap(); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/ppu/src/object.rs b/crates/ppu/src/object.rs index 071dcdd2..23855cff 100644 --- a/crates/ppu/src/object.rs +++ b/crates/ppu/src/object.rs @@ -1,4 +1,4 @@ -use gb_shared::is_bit_set; +use gb_shared::{is_bit_set, Snapshot}; /// https://gbdev.io/pandocs/OAM.html #[repr(C)] @@ -48,6 +48,29 @@ impl From for ObjectAttrs { } } +#[derive(serde::Serialize, serde::Deserialize)] +pub(crate) struct ObjectSnapshot { + y: u8, + x: u8, + tile_index: u8, + attrs: u8, +} + +impl Snapshot for Object { + type Snapshot = ObjectSnapshot; + + fn snapshot(&self) -> Self::Snapshot { + ObjectSnapshot { y: self.y, x: self.x, tile_index: self.tile_index, attrs: self.attrs.0 } + } + + fn restore(&mut self, snapshot: Self::Snapshot) { + self.y = snapshot.y; + self.x = snapshot.x; + self.tile_index = snapshot.tile_index; + self.attrs = ObjectAttrs(snapshot.attrs); + } +} + #[cfg(test)] mod tests { use super::Object; diff --git a/crates/shared/src/boxed.rs b/crates/shared/src/boxed.rs index 44e8c4bf..686eedd6 100644 --- a/crates/shared/src/boxed.rs +++ b/crates/shared/src/boxed.rs @@ -1,35 +1,6 @@ use std::ops::{Deref, DerefMut}; -use crate::boxed_array_fn; - -#[derive(Debug)] -pub struct BoxedMatrix(Box<[Box<[T; COLS]>; ROWS]>); - -impl Default for BoxedMatrix { - fn default() -> Self { - Self(boxed_array_fn(|_| boxed_array_fn(|_| T::default()))) - } -} - -impl Deref for BoxedMatrix { - type Target = Box<[Box<[T; COLS]>; ROWS]>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for BoxedMatrix { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Clone for BoxedMatrix { - fn clone(&self) -> Self { - Self(boxed_array_fn(|i| self[i].clone())) - } -} +use crate::{boxed_array_fn, boxed_array_try_from_vec}; #[derive(Debug)] pub struct BoxedArray(Box<[T; SIZE]>); @@ -66,6 +37,12 @@ impl Clone for BoxedArray { } } +impl BoxedArray { + pub fn try_from_vec(value: Vec) -> Result> { + Ok(Self(boxed_array_try_from_vec(value)?)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -86,14 +63,4 @@ mod tests { let value = BoxedArray::from(&value); assert_eq!(value.as_ref(), &[1, 2]); } - - #[test] - fn matrix_clone() { - let mut value: BoxedMatrix = BoxedMatrix::default(); - let value_clone = value.clone(); - assert_eq!(value.as_ref(), value_clone.as_ref()); - - value[0][0] = 12; - assert_ne!(value.as_ref(), value_clone.as_ref()); - } } diff --git a/crates/shared/src/event.rs b/crates/shared/src/event.rs deleted file mode 100644 index f7b790cb..00000000 --- a/crates/shared/src/event.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::boxed::BoxedMatrix; -use std::sync::mpsc::Sender; - -#[derive(Debug)] -pub enum Event { - /// A frame rendered. - OnFrame(BoxedMatrix), - #[cfg(debug_assertions)] - OnDebugFrame(u32, Vec<[[u8; 8]; 8]>), -} - -pub type EventSender = Sender; diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index 9144cd38..2b835970 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -1,7 +1,6 @@ pub mod bitwise; pub mod boxed; pub mod command; -pub mod event; pub fn boxed_array(val: T) -> Box<[T; SIZE]> { let boxed_slice = vec![val; SIZE].into_boxed_slice(); @@ -21,6 +20,17 @@ pub fn boxed_array_fn T, const SIZE: usize>(init_fn: F) -> Bo unsafe { Box::from_raw(ptr) } } +pub fn boxed_array_try_from_vec(vec: Vec) -> Result, Vec> { + if vec.len() == SIZE { + let boxed_slice = vec.into_boxed_slice(); + let ptr = Box::into_raw(boxed_slice) as *mut [T; SIZE]; + + Ok(unsafe { Box::from_raw(ptr) }) + } else { + Err(vec) + } +} + pub trait Memory { fn write(&mut self, addr: u16, value: u8); fn read(&self, addr: u16) -> u8; @@ -84,3 +94,9 @@ pub const fn mib(m: usize) -> usize { } pub const CPU_FREQ: u32 = 4_194_304; + +pub trait Snapshot { + type Snapshot; + fn snapshot(&self) -> Self::Snapshot; + fn restore(&mut self, snapshot: Self::Snapshot); +}