save states are working on the first try! crude, only one at a time, and probably needing some cleanup, but working.

This commit is contained in:
Theron 2020-02-29 17:23:51 -06:00
parent 531240e94e
commit 7f5a71cf07
16 changed files with 359 additions and 36 deletions

View File

@ -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]

View File

@ -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.

View File

@ -1,3 +1,4 @@
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct DMC {
pub sample: u16,
pub enabled: bool,

View File

@ -1,3 +1,4 @@
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct Envelope {
pub period: u16, // constant volume/envelope period
divider: u16,

View File

@ -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,

View File

@ -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,

24
src/apu/serialize.rs Normal file
View File

@ -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;
}
}

View File

@ -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,

View File

@ -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,

View File

@ -33,8 +33,8 @@ pub enum Mirror {
FourScreen,
}
pub fn get_mapper() -> Rc<RefCell<dyn Mapper>> {
let cart = Cartridge::new();
pub fn get_mapper(filename: String) -> Rc<RefCell<dyn Mapper>> {
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<String> = 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");

View File

@ -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<u8>, // 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<RefCell<dyn Mapper>>, // cartridge data
pub ppu: super::Ppu,
pub apu: super::Apu,
@ -288,30 +286,6 @@ impl Cpu {
}
}
impl Serialize for Cpu {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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

56
src/cpu/serialize.rs Normal file
View File

@ -0,0 +1,56 @@
use super::Mode;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct CpuData {
mem: Vec<u8>,
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<Mode>,
}
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;
}
}

View File

@ -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<String> = 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:

View File

@ -1,6 +1,7 @@
mod cpu_registers;
mod rendering;
mod memory;
pub mod serialize;
use std::cell::RefCell;
use std::rc::Rc;

168
src/ppu/serialize.rs Normal file
View File

@ -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<u8>,
nametable_B: Vec<u8>,
nametable_C: Vec<u8>,
nametable_D: Vec<u8>,
palette_ram: Vec<u8>,
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<u8>,
secondary_oam: Vec<u8>,
sprite_attribute_latches: Vec<u8>,
sprite_counters: Vec<u8>,
sprite_indexes: Vec<u8>,
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;
}
}

73
src/state.rs Normal file
View File

@ -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))
}
}