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);
+}