From 7f5a71cf072882a4cde2cefc25c2758613fc55be Mon Sep 17 00:00:00 2001 From: Theron Date: Sat, 29 Feb 2020 17:23:51 -0600 Subject: [PATCH] save states are working on the first try! crude, only one at a time, and probably needing some cleanup, but working. --- Cargo.toml | 1 + README.md | 3 + src/apu/dmc.rs | 1 + src/apu/envelope.rs | 1 + src/apu/mod.rs | 2 + src/apu/noise.rs | 1 + src/apu/serialize.rs | 24 +++++++ src/apu/square.rs | 1 + src/apu/triangle.rs | 1 + src/cartridge/mod.rs | 11 ++- src/cpu/mod.rs | 28 +------- src/cpu/serialize.rs | 56 +++++++++++++++ src/main.rs | 23 +++++- src/ppu/mod.rs | 1 + src/ppu/serialize.rs | 168 +++++++++++++++++++++++++++++++++++++++++++ src/state.rs | 73 +++++++++++++++++++ 16 files changed, 359 insertions(+), 36 deletions(-) create mode 100644 src/apu/serialize.rs create mode 100644 src/cpu/serialize.rs create mode 100644 src/ppu/serialize.rs create mode 100644 src/state.rs diff --git a/Cargo.toml b/Cargo.toml index 1a87293..f4053ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" [dependencies] sdl2 = { version = "0.33", features = ["bundled", "static-link"] } serde = { version = "1.0.104", features = ["derive"] } +serde_json = "1.0" cpuprofiler = "0.0.3" [profile.release] diff --git a/README.md b/README.md index 70fe297..9898c3c 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,9 @@ ___________________ | Left | Left | | Right | Right | ------------------- +Save state: F5 +Load state: F9 +(Only one save state at a time supported currently: saving a second time overwrites the first.) ``` The code aims to follow the explanations from the [NES dev wiki](https://wiki.nesdev.com/w/index.php/NES_reference_guide) where possible, especially in the PPU, and the comments quote from it often. Thanks to everyone who contributes to that wiki/forum, and to Michael Fogleman's [NES](https://github.com/fogleman/nes) and Scott Ferguson's [Fergulator](https://github.com/scottferg/Fergulator) for getting me unstuck at several points. diff --git a/src/apu/dmc.rs b/src/apu/dmc.rs index a364a5a..38fdb01 100644 --- a/src/apu/dmc.rs +++ b/src/apu/dmc.rs @@ -1,3 +1,4 @@ +#[derive(serde::Serialize, serde::Deserialize, Clone)] pub struct DMC { pub sample: u16, pub enabled: bool, diff --git a/src/apu/envelope.rs b/src/apu/envelope.rs index 944ca7c..2bfb923 100644 --- a/src/apu/envelope.rs +++ b/src/apu/envelope.rs @@ -1,3 +1,4 @@ +#[derive(serde::Serialize, serde::Deserialize, Clone)] pub struct Envelope { pub period: u16, // constant volume/envelope period divider: u16, diff --git a/src/apu/mod.rs b/src/apu/mod.rs index 1da685c..8ae4340 100644 --- a/src/apu/mod.rs +++ b/src/apu/mod.rs @@ -3,6 +3,7 @@ mod square; mod triangle; mod dmc; mod envelope; +pub mod serialize; use noise::Noise; use square::Square; @@ -19,6 +20,7 @@ const LENGTH_COUNTER_TABLE: [u8; 32] = [ 12, 16, 24, 18, 48, 20, 96, 22, 192, 24, 72, 26, 16, 28, 32, 30, ]; +#[derive(serde::Serialize, serde::Deserialize, Clone)] pub struct Apu { square1: Square, square2: Square, diff --git a/src/apu/noise.rs b/src/apu/noise.rs index ae8fd96..0ea9899 100644 --- a/src/apu/noise.rs +++ b/src/apu/noise.rs @@ -4,6 +4,7 @@ const NOISE_TABLE: [u16; 16] = [4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 5 // $400E M---.PPPP Mode and period (write) // bit 7 M--- ---- Mode flag +#[derive(serde::Serialize, serde::Deserialize, Clone)] pub struct Noise { pub sample: u16, // output value that gets sent to the mixer pub enabled: bool, diff --git a/src/apu/serialize.rs b/src/apu/serialize.rs new file mode 100644 index 0000000..aa5450f --- /dev/null +++ b/src/apu/serialize.rs @@ -0,0 +1,24 @@ +pub type ApuData = super::Apu; + +impl super::Apu{ + pub fn save_state(&self) -> ApuData { + let x: ApuData = self.clone(); + x + } + + pub fn load_state(&mut self, data: ApuData) { + self.square1 = data.square1; + self.square2 = data.square2; + self.triangle = data.triangle; + self.noise = data.noise; + self.dmc = data.dmc; + self.square_table = data.square_table; + self.tnd_table = data.tnd_table; + self.frame_sequence = data.frame_sequence; + self.frame_counter = data.frame_counter; + self.interrupt_inhibit = data.interrupt_inhibit; + self.frame_interrupt = data.frame_interrupt; + self.cycle = data.cycle; + self.trigger_irq = data.trigger_irq; + } +} diff --git a/src/apu/square.rs b/src/apu/square.rs index 8694964..6d0dbe2 100644 --- a/src/apu/square.rs +++ b/src/apu/square.rs @@ -7,6 +7,7 @@ const DUTY_CYCLE_SEQUENCES: [[u8; 8]; 4] = [ [1, 0, 0, 1, 1, 1, 1, 1], ]; +#[derive(serde::Serialize, serde::Deserialize, Clone)] pub struct Square { pub sample: u16, // output value that gets sent to the mixer pub enabled: bool, diff --git a/src/apu/triangle.rs b/src/apu/triangle.rs index b059a99..c665d88 100644 --- a/src/apu/triangle.rs +++ b/src/apu/triangle.rs @@ -3,6 +3,7 @@ const WAVEFORM: [u16; 32] = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, ]; +#[derive(serde::Serialize, serde::Deserialize, Clone)] pub struct Triangle { pub sample: u16, pub enabled: bool, diff --git a/src/cartridge/mod.rs b/src/cartridge/mod.rs index c0ddb89..c49db90 100644 --- a/src/cartridge/mod.rs +++ b/src/cartridge/mod.rs @@ -33,8 +33,8 @@ pub enum Mirror { FourScreen, } -pub fn get_mapper() -> Rc> { - let cart = Cartridge::new(); +pub fn get_mapper(filename: String) -> Rc> { + let cart = Cartridge::new(filename); let num = cart.mapper_num; match num { 0 => Rc::new(RefCell::new(Nrom::new(cart))), @@ -64,11 +64,8 @@ pub struct Cartridge { } impl Cartridge { - pub fn new() -> Self { - let argv: Vec = std::env::args().collect(); - assert!(argv.len() > 1, "must include .nes ROM as argument"); - let filename = &argv[1]; - let mut f = std::fs::File::open(filename).expect("could not open {}"); + pub fn new(filename: String) -> Self { + let mut f = std::fs::File::open(&filename).expect("could not open {}"); let mut data = vec![]; f.read_to_end(&mut data).unwrap(); assert!(data[0..4] == [0x4E, 0x45, 0x53, 0x1A], "signature mismatch, not an iNES file"); diff --git a/src/cpu/mod.rs b/src/cpu/mod.rs index 23e0dd6..9a3252b 100644 --- a/src/cpu/mod.rs +++ b/src/cpu/mod.rs @@ -1,11 +1,11 @@ mod addressing_modes; mod opcodes; mod utility; +pub mod serialize; use std::cell::RefCell; use std::rc::Rc; use serde::{Serialize, Deserialize}; -use serde::ser::{Serializer, SerializeStruct}; use crate::cartridge::Mapper; // RAM locations @@ -53,7 +53,6 @@ impl Mode { } } -// #[derive(Serialize, Deserialize, Debug)] pub struct Cpu { mem: Vec, // CPU's RAM, $0000-$1FFF A: u8, // accumulator @@ -66,7 +65,6 @@ pub struct Cpu { clock: u64, // number of ticks in current cycle delay: usize, // for skipping cycles during OAM DMA - // #[serde(skip_serializing)] mapper: Rc>, // cartridge data pub ppu: super::Ppu, pub apu: super::Apu, @@ -288,30 +286,6 @@ impl Cpu { } } - -impl Serialize for Cpu { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut state = serializer.serialize_struct("Cpu", 13)?; - state.serialize_field("mem", &self.mem)?; - state.serialize_field("A", &self.A)?; - state.serialize_field("X", &self.X)?; - state.serialize_field("Y", &self.Y)?; - state.serialize_field("PC", &self.PC)?; - state.serialize_field("S", &self.S)?; - state.serialize_field("P", &self.P)?; - state.serialize_field("clock", &self.clock)?; - state.serialize_field("delay", &self.delay)?; - state.serialize_field("strobe", &self.strobe)?; - state.serialize_field("button_states", &self.button_states)?; - state.serialize_field("button_number", &self.button_number)?; - state.serialize_field("mode_table", &self.mode_table)?; - state.end() - } -} - /* Address range Size Device $0000-$07FF $0800 2KB internal RAM diff --git a/src/cpu/serialize.rs b/src/cpu/serialize.rs new file mode 100644 index 0000000..912f60f --- /dev/null +++ b/src/cpu/serialize.rs @@ -0,0 +1,56 @@ +use super::Mode; + +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct CpuData { + mem: Vec, + A: u8, + X: u8, + Y: u8, + PC: usize, + S: u8, + P: u8, + clock: u64, + delay: usize, + strobe: u8, + button_states: u8, + button_number: u8, + mode_table: Vec, +} + +impl super::Cpu { + pub fn save_state(&self) -> CpuData { + CpuData{ + mem: self.mem.clone(), + A: self.A, + X: self.X, + Y: self.Y, + PC: self.PC, + S: self.S, + P: self.P, + clock: self.clock, + delay: self.delay, + strobe: self.strobe, + button_states: self.button_states, + button_number: self.button_number, + mode_table: self.mode_table.clone(), + } + } + + pub fn load_state(&mut self, data: CpuData) { + self.mem = data.mem; + self.A = data.A; + self.X = data.X; + self.Y = data.Y; + self.PC = data.PC; + self.S = data.S; + self.P = data.P; + self.clock = data.clock; + self.delay = data.delay; + self.strobe = data.strobe; + self.button_states = data.button_states; + self.button_number = data.button_number; + self.mode_table = data.mode_table; + } +} diff --git a/src/main.rs b/src/main.rs index ed8fb46..03748d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod cartridge; mod input; mod screen; mod audio; +mod state; use cpu::Cpu; use ppu::Ppu; @@ -12,6 +13,7 @@ use apu::Apu; use cartridge::get_mapper; use input::poll_buttons; use screen::{init_window, draw_pixel, draw_to_window}; +use state::{save_state, load_state}; use std::sync::{Arc, Mutex}; use std::time::{Instant, Duration}; @@ -42,7 +44,8 @@ fn main() -> Result<(), String> { audio_device.resume(); // Initialize hardware components - let mapper = get_mapper(); + let filename = get_filename(); + let mapper = get_mapper(filename.clone()); let ppu = Ppu::new(mapper.clone()); let apu = Apu::new(); let mut cpu = Cpu::new(mapper.clone(), ppu, apu); @@ -92,6 +95,16 @@ fn main() -> Result<(), String> { match event { Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => break 'running, + Event::KeyDown{ keycode: Some(Keycode::F5), .. } => { + let res: Result<(), String> = save_state(&cpu, &filename) + .or_else(|e| {println!("{}", e); Ok(())}); + res.unwrap(); + }, + Event::KeyDown{ keycode: Some(Keycode::F9), .. } => { + let res: Result<(), String> = load_state(&mut cpu, &filename) + .or_else(|e| {println!("{}", e); Ok(())}); + res.unwrap(); + }, _ => (), } } @@ -115,6 +128,12 @@ fn main() -> Result<(), String> { Ok(()) } +fn get_filename() -> String { + let argv: Vec = std::env::args().collect(); + assert!(argv.len() > 1, "must include .nes ROM as argument"); + argv[1].clone() +} + /* TODO: @@ -123,7 +142,7 @@ TODO: - untangle CPU and APU/PPU? - GUI? drag and drop ROMs? - reset function -- save/load/pause functionality +- save states: multiple, search/select, and generalized "find file by different extension" functionality Timing notes: diff --git a/src/ppu/mod.rs b/src/ppu/mod.rs index 728c640..2d50fee 100644 --- a/src/ppu/mod.rs +++ b/src/ppu/mod.rs @@ -1,6 +1,7 @@ mod cpu_registers; mod rendering; mod memory; +pub mod serialize; use std::cell::RefCell; use std::rc::Rc; diff --git a/src/ppu/serialize.rs b/src/ppu/serialize.rs new file mode 100644 index 0000000..c923919 --- /dev/null +++ b/src/ppu/serialize.rs @@ -0,0 +1,168 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct PpuData { + line_cycle: usize, + scanline: usize, + frame: usize, + v: u16, + t: u16, + x: u8, + w: u8, + nametable_A: Vec, + nametable_B: Vec, + nametable_C: Vec, + nametable_D: Vec, + palette_ram: Vec, + background_pattern_sr_low: u16, + background_pattern_sr_high: u16, + nametable_byte: u8, + attribute_table_byte: u8, + low_pattern_table_byte: u8, + high_pattern_table_byte: u8, + background_palette_sr_low: u8, + background_palette_sr_high: u8, + background_palette_latch: u8, + primary_oam: Vec, + secondary_oam: Vec, + sprite_attribute_latches: Vec, + sprite_counters: Vec, + sprite_indexes: Vec, + sprite_pattern_table_srs: Vec<(u8, u8)>, + num_sprites: usize, + address_increment: u16, + sprite_pattern_table_base: usize, + background_pattern_table_base:usize, + oam_address: usize, + sprite_size: u8, + grayscale: bool, + show_background_left: bool, + show_sprites_left: bool, + show_background: bool, + show_sprites: bool, + emphasize_red: bool, + emphasize_green: bool, + emphasize_blue: bool, + sprite_overflow: bool, + sprite_zero_hit: bool, + should_generate_nmi: bool, + vertical_blank: bool, + trigger_nmi: bool, + previous_nmi: bool, + nmi_delay: usize, + read_buffer: u8, + recent_bits: u8, + previous_a12: u8, +} + +impl super::Ppu { + pub fn save_state(&self) -> PpuData { + PpuData{ + line_cycle: self.line_cycle, + scanline: self.scanline, + frame: self.frame, + v: self.v, + t: self.t, + x: self.x, + w: self.w, + nametable_A: self.nametable_A.clone(), + nametable_B: self.nametable_B.clone(), + nametable_C: self.nametable_C.clone(), + nametable_D: self.nametable_D.clone(), + palette_ram: self.palette_ram.clone(), + background_pattern_sr_low: self.background_pattern_sr_low, + background_pattern_sr_high: self.background_pattern_sr_high, + nametable_byte: self.nametable_byte, + attribute_table_byte: self.attribute_table_byte, + low_pattern_table_byte: self.low_pattern_table_byte, + high_pattern_table_byte: self.high_pattern_table_byte, + background_palette_sr_low: self.background_palette_sr_low, + background_palette_sr_high: self.background_palette_sr_high, + background_palette_latch: self.background_palette_latch, + primary_oam: self.primary_oam.clone(), + secondary_oam: self.secondary_oam.clone(), + sprite_attribute_latches: self.sprite_attribute_latches.clone(), + sprite_counters: self.sprite_counters.clone(), + sprite_indexes: self.sprite_indexes.clone(), + sprite_pattern_table_srs: self.sprite_pattern_table_srs.clone(), + num_sprites: self.num_sprites, + address_increment: self.address_increment, + sprite_pattern_table_base: self.sprite_pattern_table_base, + background_pattern_table_base: self.background_pattern_table_base, + oam_address: self.oam_address, + sprite_size: self.sprite_size, + grayscale: self.grayscale, + show_background_left: self.show_background_left, + show_sprites_left: self.show_sprites_left, + show_background: self.show_background, + show_sprites: self.show_sprites, + emphasize_red: self.emphasize_red, + emphasize_green: self.emphasize_green, + emphasize_blue: self.emphasize_blue, + sprite_overflow: self.sprite_overflow, + sprite_zero_hit: self.sprite_zero_hit, + should_generate_nmi: self.should_generate_nmi, + vertical_blank: self.vertical_blank, + trigger_nmi: self.trigger_nmi, + previous_nmi: self.previous_nmi, + nmi_delay: self.nmi_delay, + read_buffer: self.read_buffer, + recent_bits: self.recent_bits, + previous_a12: self.previous_a12, + } + } + + pub fn load_state(&mut self, data: PpuData) { + self.line_cycle = data.line_cycle; + self.scanline = data.scanline; + self.frame = data.frame; + self.v = data.v; + self.t = data.t; + self.x = data.x; + self.w = data.w; + self.nametable_A = data.nametable_A; + self.nametable_B = data.nametable_B; + self.nametable_C = data.nametable_C; + self.nametable_D = data.nametable_D; + self.palette_ram = data.palette_ram; + self.background_pattern_sr_low = data.background_pattern_sr_low; + self.background_pattern_sr_high = data.background_pattern_sr_high; + self.nametable_byte = data.nametable_byte; + self.attribute_table_byte = data.attribute_table_byte; + self.low_pattern_table_byte = data.low_pattern_table_byte; + self.high_pattern_table_byte = data.high_pattern_table_byte; + self.background_palette_sr_low = data.background_palette_sr_low; + self.background_palette_sr_high = data.background_palette_sr_high; + self.background_palette_latch = data.background_palette_latch; + self.primary_oam = data.primary_oam; + self.secondary_oam = data.secondary_oam; + self.sprite_attribute_latches = data.sprite_attribute_latches; + self.sprite_counters = data.sprite_counters; + self.sprite_indexes = data.sprite_indexes; + self.sprite_pattern_table_srs = data.sprite_pattern_table_srs; + self.num_sprites = data.num_sprites; + self.address_increment = data.address_increment; + self.sprite_pattern_table_base = data.sprite_pattern_table_base; + self.background_pattern_table_base = data.background_pattern_table_base; + self.oam_address = data.oam_address; + self.sprite_size = data.sprite_size; + self.grayscale = data.grayscale; + self.show_background_left = data.show_background_left; + self.show_sprites_left = data.show_sprites_left; + self.show_background = data.show_background; + self.show_sprites = data.show_sprites; + self.emphasize_red = data.emphasize_red; + self.emphasize_green = data.emphasize_green; + self.emphasize_blue = data.emphasize_blue; + self.sprite_overflow = data.sprite_overflow; + self.sprite_zero_hit = data.sprite_zero_hit; + self.should_generate_nmi = data.should_generate_nmi; + self.vertical_blank = data.vertical_blank; + self.trigger_nmi = data.trigger_nmi; + self.previous_nmi = data.previous_nmi; + self.nmi_delay = data.nmi_delay; + self.read_buffer = data.read_buffer; + self.recent_bits = data.recent_bits; + self.previous_a12 = data.previous_a12; + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..7b08953 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,73 @@ +use super::cpu; +use super::ppu; +use super::apu; + +use std::fs::File; +use std::io::{Read, Write}; +use std::path::Path; +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize)] +struct SaveState { + cpu: cpu::serialize::CpuData, + ppu: ppu::serialize::PpuData, + apu: apu::serialize::ApuData, +} + +pub fn save_state(cpu: &cpu::Cpu, filename: &str) -> Result<(), String> { + let data = SaveState{ + cpu: cpu.save_state(), + ppu: cpu.ppu.save_state(), + apu: cpu.apu.save_state(), + }; + let serialized = serde_json::to_string(&data) + .map_err(|e| e.to_string())?; + let path = match Path::new(&filename).parent() { + Some(p) => p, + None => return Err("couldn't convert filename to path".to_string()), + }; + let stem = match Path::new(&filename).file_stem() { + Some(s) => s, + None => return Err("couldn't get file stem".to_string()), + }; + let mut save_file = path.join(stem); + save_file.set_extension("dat"); + println!("state saved to file: {:?}", save_file); + let mut f = File::create(&save_file) + .expect("could not create output file for save state"); + f.write_all(serialized.as_bytes()) + .map_err(|_| "couldn't write serialized data to file".to_string()) +} + +pub fn load_state(cpu: &mut cpu::Cpu, filename: &str) -> Result<(), String> { + // load file, deserialize to cpudata, set cpu fields to data fields + let path = match Path::new(&filename).parent() { + Some(p) => p, + None => return Err("couldn't convert filename to path".to_string()), + }; + let stem = match Path::new(&filename).file_stem() { + Some(s) => s, + None => return Err("couldn't get file stem".to_string()), + }; + let mut save_file = path.join(stem); + save_file.set_extension("dat"); + + if Path::new(&save_file).exists() { + let mut f = File::open(save_file.clone()) + .map_err(|e| e.to_string())?; + let mut serialized_data = vec![]; + f.read_to_end(&mut serialized_data) + .map_err(|e| e.to_string())?; + let serialized_string = std::str::from_utf8(&serialized_data) + .map_err(|e| e.to_string())?; + let state: SaveState = serde_json::from_str(serialized_string) + .map_err(|e| e.to_string())?; + cpu.load_state(state.cpu); + cpu.ppu.load_state(state.ppu); + cpu.apu.load_state(state.apu); + println!("loading save state from file: {:?}", save_file); + Ok(()) + } else { + Err(format!("no save state file at {:?}", save_file)) + } +}