From f4b677d52a898c3766f6741dfdb433a0aa0289fb Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Sun, 21 Nov 2021 23:20:04 -0800 Subject: [PATCH] Initial Commit of nesfuzz --- .gitignore | 6 + Cargo.toml | 13 +- README.md | 95 ++--- fuzzimages/screenshot.png | Bin 0 -> 81339 bytes src/apu/dmc.rs | 24 +- src/apu/envelope.rs | 4 +- src/apu/mod.rs | 67 ++-- src/apu/noise.rs | 4 +- src/apu/serialize.rs | 2 +- src/apu/square.rs | 32 +- src/apu/triangle.rs | 8 +- src/audio.rs | 132 ------- src/cartridge/cnrom.rs | 25 +- src/cartridge/mmc1.rs | 81 ++-- src/cartridge/mmc3.rs | 151 ++++---- src/cartridge/mod.rs | 43 ++- src/cartridge/nrom.rs | 35 +- src/cartridge/serialize.rs | 15 +- src/cartridge/uxrom.rs | 31 +- src/cpu/addressing_modes.rs | 16 +- src/cpu/mod.rs | 737 ++++++++++++++++++++++++++++++------ src/cpu/opcodes.rs | 44 ++- src/cpu/serialize.rs | 11 +- src/cpu/utility.rs | 10 +- src/fuzzing_state.rs | 63 +++ src/input.rs | 163 +++++++- src/main.rs | 510 ++++++++++++++----------- src/ppu/cpu_registers.rs | 70 ++-- src/ppu/memory.rs | 81 ++-- src/ppu/mod.rs | 262 ++++++++----- src/ppu/rendering.rs | 93 +++-- src/ppu/serialize.rs | 8 +- src/screen.rs | 80 ++-- src/state.rs | 28 +- 34 files changed, 1833 insertions(+), 1111 deletions(-) create mode 100644 fuzzimages/screenshot.png create mode 100644 src/fuzzing_state.rs diff --git a/.gitignore b/.gitignore index 1dbc6fc..53fba62 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,9 @@ Cargo.lock **/*.rs.bk roms +*.fm2 +*.nes +*.cdl +*.nl +*.idea +*cdl diff --git a/Cargo.toml b/Cargo.toml index f4053ed..63deb37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,13 @@ [package] -name = "nestur" +name = "nesfuzz" version = "0.1.0" -authors = ["Theron "] +authors = ["sarah@openprivacy.ca"] edition = "2018" [dependencies] -sdl2 = { version = "0.33", features = ["bundled", "static-link"] } +minifb = "0.19.3" serde = { version = "1.0.104", features = ["derive"] } serde_json = "1.0" -cpuprofiler = "0.0.3" - -[profile.release] -debug = true +font8x8 = "0.3.1" +priority-queue = "1.2.0" +hamming = "0.1.3" diff --git a/README.md b/README.md index 81dcc51..618d0f7 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,54 @@ -# nestur +# nesfuzz -Nestur is an NES emulator. There are plenty of full-featured emulators out there; this is primarily an educational project but it is usable. There may still be many bugs, but I'm probably not aware of them so please submit issues. -- no use of `unsafe` -- NTSC timing -- supports mappers 0-4 which cover ~85% of [games](http://tuxnes.sourceforge.net/nesmapper.txt) +nesfuzz is a fuzzer for Nes Games by [@SarahJamieLewis](https://twitter.com/sarahjamielewis) - +nessfuzz built on top of the [nestur](https://github.com/spieglt/nestur) emulator by [@spieglt](https://github.com/spieglt). -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. +## Usage & Methodology -## Controls -``` - Button | Key -___________________ -| A | D | -| B | F | -| Start | Enter | -| Select | R-Shift| -| Up | Up | -| Down | Down | -| Left | Left | -| Right | Right | -------------------- +To begin fuzzing you will need a rom file, and a sample input file. For sample inputs see [TasVids](http://tasvideos.org/). -F2: reset console -F5: save game state -F9: load most recent save state -``` -If the game is called `mygame.nes`, the save state files will be called `mygame-#.dat`. To load any previous save state, drag and drop a `.dat` file onto the window. +`nessfuzz ` +`nessfuzz smb.rom happylee-supermariobros,warped.fm2` -## Use +nesfuzz uses the same input to see novel RAM configurations and search the possible input space. It will also +tile 28 (by default), windows to allow you to see the fuzzing happen. -Double-click or run the executable from a terminal by itself to launch with instructions. Then click Ok and drag a (iNES/`.nes`) ROM file onto the window. Or, drag and drop a ROM file onto the executable to run it directly, or use the path to the ROM file as the first argument to the terminal command. +![](./fuzzimages/screenshot.png) -If the game uses battery-backed RAM (if it can save data when the console is turned off), a save file like `rom_filename.sav` will be created in the same folder as the ROM when the program is exited. When Nestur is run again, it will look for a file matching the ROM name, with a `.sav` extension instead of `.nes`. +## Parameters -## Compilation +Found at the top of `main.rs` a few parameters control the types and effectiveness of fuzzing. -1. Install [Rust](https://www.rust-lang.org/tools/install) -2. Have a C compiler - - Linux: `sudo apt install build-essential` - - Mac: [XCode](https://apps.apple.com/us/app/xcode/id497799835) - - Windows: install the [Visual Studio Build Tools](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools&rel=16) (or [Visual Studio](https://docs.microsoft.com/en-us/cpp/build/vscpp-step-0-installation?view=vs-2019) with the "Desktop development with C++" workload). -3. Install CMake - - Linux: `sudo apt install cmake` - - Mac: install [Homebrew](https://brew.sh/) and run `brew install cmake` - - [Windows](https://cmake.org/download/) -4. `cd nestur/ && cargo build --release` (be sure to build/run with the release flag or it will run very slowly) -5. The `nestur` executable or `nestur.exe` will be in `nestur/target/release`. - -## To do - -- support other controllers? - -- more mappers? - -- better save file organization? - -## Known problem games - -- None currently, please report any issues + // The number of cpu instances to spawn.. + const NUM_THREADS: usize = 28; + + // The number of frames to fuzz and process + // A small number exploits the current point more at the expense of + // large exploration - and vice versa. + const FRAMES_TO_CONSIDER: usize = 400; + + // Same input should generate the same output... + // (I make no guarantee of that at the moment) + const RNG_SEED: u32 = 0x5463753; + + // If set to a low number, this disables start presses after the given frame + // Useful for some games where pausing does nothing to advance the game... + const DISABLE_START_PRESSES_AFTER: usize = 50; + + // The rate at which seed inputs become corrupted.. + const MUTATION_RATE: f64 = 0.1; + + // The rate at which seed inputs may become soft resets.. + const MUTATION_RATE_SOFT_RESET: f64 = 0.000; -Please also check out [Cloaker](https://github.com/spieglt/cloaker) and [Flying Carpet](https://github.com/spieglt/flyingcarpet)! +## Known Issues + +The only game that really works as expected is Super Mario Bros. with the `happylee-supermariobros,warped.fm2` input. +This is probably because of issues in the underlying emulator / differences in the expected behaviour of the system the +tas inputs are produced for v.s. the emulator. + +Other games like Legend of Zelda, Megaman, Super Mario Bros. 3, Final Fantasy II etc. will run, but I have had any +tas inputs from them quickly become out of sync with the actual gameplay. Further research is needed to as to why +that is. Help appreciated. \ No newline at end of file diff --git a/fuzzimages/screenshot.png b/fuzzimages/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..d59c01c43f5d593fa9112b4364db9da5cff24ef3 GIT binary patch literal 81339 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ⅇ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 if self.sample >= 2 { self.sample -= 2}, - 1 => if self.sample <= 125 { self.sample += 2 }, + 0 => { + if self.sample >= 2 { + self.sample -= 2 + } + } + 1 => { + if self.sample <= 125 { + self.sample += 2 + } + } _ => panic!("uh oh! magical bits!"), } } else { @@ -117,7 +127,7 @@ impl DMC { self.enabled = true; self.shift_register = s; self.sample_buffer = None; - }, + } None => self.enabled = false, } } @@ -133,18 +143,18 @@ impl DMC { self.loop_flag = value & 0b0100_0000 != 0; self.rate_index = value as usize & 0b0000_1111; } - + pub fn direct_load(&mut self, value: u8) { // $4011 -DDD.DDDD Direct load (write) self.sample = value as u16 & 0b0111_1111; } - + pub fn write_sample_address(&mut self, value: u8) { // $4012 AAAA.AAAA Sample address (write) // bits 7-0 AAAA.AAAA Sample address = %11AAAAAA.AA000000 = $C000 + (A * 64) self.sample_address = ((value as usize) << 6) + 0xC000; } - + pub fn write_sample_length(&mut self, value: u8) { // $4013 LLLL.LLLL Sample length (write) // bits 7-0 LLLL.LLLL Sample length = %LLLL.LLLL0001 = (L * 16) + 1 bytes diff --git a/src/apu/envelope.rs b/src/apu/envelope.rs index 2bfb923..2d0fa02 100644 --- a/src/apu/envelope.rs +++ b/src/apu/envelope.rs @@ -2,8 +2,8 @@ pub struct Envelope { pub period: u16, // constant volume/envelope period divider: u16, - pub decay_counter: u16, // remainder of envelope divider - pub start: bool, // restarts envelope + pub decay_counter: u16, // remainder of envelope divider + pub start: bool, // restarts envelope pub length_counter_halt: bool, // also the envelope loop flag } diff --git a/src/apu/mod.rs b/src/apu/mod.rs index 289b99c..01aa380 100644 --- a/src/apu/mod.rs +++ b/src/apu/mod.rs @@ -1,14 +1,14 @@ -mod noise; -mod square; -mod triangle; mod dmc; mod envelope; +mod noise; pub mod serialize; +mod square; +mod triangle; +use dmc::DMC; use noise::Noise; use square::Square; use triangle::Triangle; -use dmc::DMC; // APU clock ticks every other CPU cycle. // Frame counter only ticks every 3728.5 APU ticks, and in audio frames of 4 or 5. @@ -16,17 +16,17 @@ use dmc::DMC; const FRAME_COUNTER_STEPS: [usize; 5] = [3728, 7456, 11185, 14914, 18640]; const LENGTH_COUNTER_TABLE: [u8; 32] = [ - 10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, - 12, 16, 24, 18, 48, 20, 96, 22, 192, 24, 72, 26, 16, 28, 32, 30, + 10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 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, + square1: Square, + square2: Square, triangle: Triangle, - noise: Noise, - pub dmc: DMC, + noise: Noise, + pub dmc: DMC, square_table: Vec, tnd_table: Vec, @@ -41,14 +41,18 @@ pub struct Apu { impl Apu { pub fn new() -> Self { - let square_table = (0..31).map(|x| 95.52/((8128.0 / x as f32) + 100.0)).collect(); - let tnd_table = (0..203).map(|x| 163.67/((24329.0 / x as f32) + 100.0)).collect(); + let square_table = (0..31) + .map(|x| 95.52 / ((8128.0 / x as f32) + 100.0)) + .collect(); + let tnd_table = (0..203) + .map(|x| 163.67 / ((24329.0 / x as f32) + 100.0)) + .collect(); Apu { - square1: Square::new(true), - square2: Square::new(false), + square1: Square::new(true), + square2: Square::new(false), triangle: Triangle::new(), - noise: Noise::new(), - dmc: DMC::new(), + noise: Noise::new(), + dmc: DMC::new(), square_table: square_table, tnd_table: tnd_table, @@ -86,7 +90,8 @@ impl Apu { fn mix(&self) -> f32 { let square_out = self.square_table[(self.square1.sample + self.square2.sample) as usize]; - let tnd_out = self.tnd_table[((3*self.triangle.sample)+(2*self.noise.sample) + self.dmc.sample) as usize]; + let tnd_out = self.tnd_table + [((3 * self.triangle.sample) + (2 * self.noise.sample) + self.dmc.sample) as usize]; square_out + tnd_out } @@ -160,31 +165,31 @@ impl Apu { // Writing to this register clears the DMC interrupt flag. self.dmc.interrupt = false; // Writing a zero to any of the channel enable bits will silence that channel and immediately set its length counter to 0. - if value & (1<<0) != 0 { + if value & (1 << 0) != 0 { self.square1.enabled = true; } else { self.square1.enabled = false; self.square1.length_counter = 0; } - if value & (1<<1) != 0 { + if value & (1 << 1) != 0 { self.square2.enabled = true; } else { self.square2.enabled = false; self.square2.length_counter = 0; } - if value & (1<<2) != 0 { + if value & (1 << 2) != 0 { self.triangle.enabled = true; } else { self.triangle.enabled = false; self.triangle.length_counter = 0; } - if value & (1<<3) != 0 { + if value & (1 << 3) != 0 { self.noise.enabled = true; } else { self.noise.enabled = false; self.noise.length_counter = 0; } - if value & (1<<4) != 0 { + if value & (1 << 4) != 0 { self.dmc.enabled = true; // If the DMC bit is set, the DMC sample will be restarted only if its bytes remaining is 0. // If there are bits remaining in the 1-byte sample buffer, these will finish playing before the next sample is fetched. @@ -205,26 +210,26 @@ impl Apu { let mut val = 0; // N/T/2/1 will read as 1 if the corresponding length counter is greater than 0. For the triangle channel, the status of the linear counter is irrelevant. if self.square1.length_counter != 0 { - val |= 1<<0; + val |= 1 << 0; } if self.square2.length_counter != 0 { - val |= 1<<1; + val |= 1 << 1; } if self.triangle.length_counter != 0 { - val |= 1<<2; + val |= 1 << 2; } if self.noise.length_counter != 0 { - val |= 1<<3; + val |= 1 << 3; } // D will read as 1 if the DMC bytes remaining is more than 0. if self.dmc.bytes_remaining != 0 { - val |= 1<<4; + val |= 1 << 4; } if self.frame_interrupt { - val |= 1<<6; + val |= 1 << 6; } if self.dmc.interrupt { - val |= 1<<7; + val |= 1 << 7; } // Reading this register clears the frame interrupt flag (but not the DMC interrupt flag). self.frame_interrupt = false; @@ -235,9 +240,9 @@ impl Apu { // $4017 fn write_frame_counter(&mut self, value: u8) { // 0 selects 4-step sequence, 1 selects 5-step sequence - self.frame_sequence = if value & (1<<7) == 0 { 4 } else { 5 }; + self.frame_sequence = if value & (1 << 7) == 0 { 4 } else { 5 }; // If set, the frame interrupt flag is cleared, otherwise it is unaffected. - if value & (1<<6) != 0 { + if value & (1 << 6) != 0 { self.interrupt_inhibit = false; } // If the mode flag is set, then both "quarter frame" and "half frame" signals are also generated. diff --git a/src/apu/noise.rs b/src/apu/noise.rs index 0ea9899..9cf9b3c 100644 --- a/src/apu/noise.rs +++ b/src/apu/noise.rs @@ -1,6 +1,8 @@ use super::envelope::Envelope; -const NOISE_TABLE: [u16; 16] = [4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068]; +const NOISE_TABLE: [u16; 16] = [ + 4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068, +]; // $400E M---.PPPP Mode and period (write) // bit 7 M--- ---- Mode flag diff --git a/src/apu/serialize.rs b/src/apu/serialize.rs index 73d8b5c..7504d08 100644 --- a/src/apu/serialize.rs +++ b/src/apu/serialize.rs @@ -1,6 +1,6 @@ pub type ApuData = super::Apu; -impl super::Apu{ +impl super::Apu { pub fn save_state(&self) -> ApuData { self.clone() } diff --git a/src/apu/square.rs b/src/apu/square.rs index 6d0dbe2..a589410 100644 --- a/src/apu/square.rs +++ b/src/apu/square.rs @@ -12,7 +12,7 @@ pub struct Square { pub sample: u16, // output value that gets sent to the mixer pub enabled: bool, constant_volume_flag: bool, // (0: use volume from envelope; 1: use constant volume) - first_channel: bool, // hack to detect timing difference in clock_sweep() + first_channel: bool, // hack to detect timing difference in clock_sweep() timer: u16, timer_period: u16, @@ -68,14 +68,15 @@ impl Square { self.sample = if self.duty_cycle[self.duty_counter] == 0 // the sequencer output is zero, or || self.timer_period > 0x7FF // overflow from the sweep unit's adder is silencing the channel, || self.length_counter == 0 // the length counter is zero, or - || self.timer_period < 8 // the timer has a value less than eight. - { - 0 - } else if self.constant_volume_flag { - self.envelope.period - } else { - self.envelope.decay_counter - }; + || self.timer_period < 8 + // the timer has a value less than eight. + { + 0 + } else if self.constant_volume_flag { + self.envelope.period + } else { + self.envelope.decay_counter + }; } pub fn clock_length_counter(&mut self) { @@ -88,14 +89,19 @@ impl Square { self.calculate_target_period(); // When the frame counter sends a half-frame clock (at 120 or 96 Hz), two things happen. // If the divider's counter is zero, the sweep is enabled, and the sweep unit is not muting the channel: The pulse's period is adjusted. - if self.sweep_counter == 0 && self.sweep_enabled && !(self.timer_period < 8 || self.target_period > 0x7FF) { + if self.sweep_counter == 0 + && self.sweep_enabled + && !(self.timer_period < 8 || self.target_period > 0x7FF) + { self.timer_period = self.target_period; } // If the divider's counter is zero or the reload flag is true: The counter is set to P and the reload flag is cleared. Otherwise, the counter is decremented. if self.sweep_counter == 0 || self.sweep_reload { self.sweep_counter = self.sweep_period; self.sweep_reload = false; - if self.sweep_enabled { self.timer_period = self.target_period; } // This fixes the DK walking sound. Why? Not reflected in documentation. + if self.sweep_enabled { + self.timer_period = self.target_period; + } // This fixes the DK walking sound. Why? Not reflected in documentation. } else { self.sweep_counter -= 1; } @@ -126,8 +132,8 @@ impl Square { pub fn write_duty(&mut self, value: u8) { // The duty cycle is changed (see table below), but the sequencer's current position isn't affected. self.duty_cycle = DUTY_CYCLE_SEQUENCES[(value >> 6) as usize]; - self.envelope.length_counter_halt = value & (1<<5) != 0; - self.constant_volume_flag = value & (1<<4) != 0; + self.envelope.length_counter_halt = value & (1 << 5) != 0; + self.constant_volume_flag = value & (1 << 4) != 0; self.envelope.period = value as u16 & 0b1111; } diff --git a/src/apu/triangle.rs b/src/apu/triangle.rs index c665d88..39ea714 100644 --- a/src/apu/triangle.rs +++ b/src/apu/triangle.rs @@ -1,6 +1,6 @@ const WAVEFORM: [u16; 32] = [ - 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, + 13, 14, 15, ]; #[derive(serde::Serialize, serde::Deserialize, Clone)] @@ -53,7 +53,8 @@ impl Triangle { // If the linear counter reload flag is set, the linear counter is reloaded with the counter reload value, if self.linear_counter_reload { self.linear_counter = self.counter_reload_value; - } else if self.linear_counter != 0 { // otherwise if the linear counter is non-zero, it is decremented. + } else if self.linear_counter != 0 { + // otherwise if the linear counter is non-zero, it is decremented. self.linear_counter -= 1; } // If the control flag is clear, the linear counter reload flag is cleared. @@ -90,5 +91,4 @@ impl Triangle { self.timer_period |= (timer_high as u16) << 8; self.linear_counter_reload = true; } - } diff --git a/src/audio.rs b/src/audio.rs index 573ff9c..8b13789 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -1,133 +1 @@ -use std::sync::{Arc, Mutex}; -use sdl2::Sdl; -use sdl2::audio::{AudioCallback, AudioSpecDesired}; -use std::f32::consts::PI; -const APU_SAMPLE_RATE: f32 = 894_886.5; -const SDL_SAMPLE_RATE: i32 = 44_100; -// Video runs at 60Hz, so console is clocked by doing enough work to create one frame of video, then sending the video and audio to their respective SDL -// devices and then sleeping. So the audio device is set to play 44,100 samples per second, and grab them in 60 intervals over the course of that second. -const SAMPLES_PER_FRAME: u16 = SDL_SAMPLE_RATE as u16/60; - - -pub struct ApuSampler { - // This buffer receives all of the raw audio produced by the APU. - // The callback will take what it needs when it needs it and truncate the buffer for smooth audio output. - buffer: Arc>>, - sample_ratio: f32, - - prev_input_90_hz: f32, - prev_output_90_hz: f32, - gamma_90_hz: f32, - - prev_input_440_hz: f32, - prev_output_440_hz: f32, - gamma_440_hz: f32, - - prev_input_14_khz: f32, - prev_output_14_khz: f32, - gamma_14_khz: f32, -} - -impl ApuSampler { - fn high_pass_90_hz(&self, sample: f32) -> f32 { - // y[i] := α × y[i−1] + α × (x[i] − x[i−1]) - (self.gamma_90_hz * self.prev_output_90_hz) + (sample - self.prev_input_90_hz) - } - - fn high_pass_440_hz(&self, sample: f32) -> f32 { - (self.gamma_440_hz * self.prev_output_440_hz) + (sample - self.prev_input_440_hz) - } - - fn low_pass_14_khz(&self, sample: f32) -> f32 { - ((1. - self.gamma_14_khz) * self.prev_output_14_khz) + (self.gamma_14_khz * sample) - } - -} - -impl AudioCallback for ApuSampler { - type Channel = f32; - - fn callback(&mut self, out: &mut [f32]) { - - let mut b = self.buffer.lock().unwrap(); - // if we have data in the buffer - if b.len() > 0 { - // copy samples at the appropriate interval from the raw APU buffer to the output device - for (i, x) in out.iter_mut().enumerate() { - let sample_idx = ((i as f32) * self.sample_ratio) as usize; - if sample_idx < b.len() { - let sample = b[sample_idx]; - - let filtered_90_hz = self.high_pass_90_hz(sample); - self.prev_input_90_hz = sample; - self.prev_output_90_hz = filtered_90_hz; - - let filtered_440_hz = self.high_pass_440_hz(filtered_90_hz); - self.prev_input_440_hz = filtered_90_hz; - self.prev_output_440_hz = filtered_440_hz; - - let filtered_14_khz = self.low_pass_14_khz(filtered_440_hz); - self.prev_input_14_khz = filtered_440_hz; - self.prev_output_14_khz = filtered_14_khz; - *x = filtered_14_khz; - } - } - let l = b.len(); - let target = (SAMPLES_PER_FRAME as f32 * self.sample_ratio) as usize; - if l > target { - *b = b.split_off(target); - } - } else { - println!("buffer empty!"); // happens when the callback fires twice between video frames - } - } -} - -pub fn initialize(sdl_context: &Sdl, buffer: Arc>>) - -> Result, String> -{ - let audio_subsystem = sdl_context.audio()?; - let desired_spec = AudioSpecDesired { - freq: Some(SDL_SAMPLE_RATE), - channels: Some(1), // mono - samples: Some(SAMPLES_PER_FRAME) - }; - audio_subsystem.open_playback(None, &desired_spec, |_spec| { - // println!("{:?}", _spec); - ApuSampler{ - buffer, - sample_ratio: APU_SAMPLE_RATE / (SDL_SAMPLE_RATE as f32), - prev_input_90_hz: 0., - prev_output_90_hz: 0., - gamma_90_hz: high_pass_coefficient(90.), - prev_input_440_hz: 0., - prev_output_440_hz: 0., - gamma_440_hz: high_pass_coefficient(440.), - prev_input_14_khz: 0., - prev_output_14_khz: 0., - gamma_14_khz: low_pass_coefficient(14_000.), - } - }) -} - -fn low_pass_coefficient(cutoff_freq: f32) -> f32 { - (2.*PI*cutoff_freq/SDL_SAMPLE_RATE as f32) / ((2.*PI*cutoff_freq/SDL_SAMPLE_RATE as f32) + 1.) -} - -fn high_pass_coefficient(cutoff_freq: f32) -> f32 { - 1. / ((2.*PI*cutoff_freq/SDL_SAMPLE_RATE as f32) + 1.) -} - -/* - -https://en.wikipedia.org/wiki/High-pass_filter -https://en.wikipedia.org/wiki/Low-pass_filter - -low pass filter: -y = (1 - gamma) * y + gamma * x - -high pass filter: -y[i] := gamma * y[i−1] + gamma * (x[i] − x[i−1]) - -*/ diff --git a/src/cartridge/cnrom.rs b/src/cartridge/cnrom.rs index bc6a790..2e564e7 100644 --- a/src/cartridge/cnrom.rs +++ b/src/cartridge/cnrom.rs @@ -1,4 +1,4 @@ -use super::{Cartridge, Mapper, Mirror, serialize::*}; +use super::{serialize::*, Cartridge, Mapper, Mirror}; pub struct Cnrom { cart: Cartridge, @@ -7,7 +7,7 @@ pub struct Cnrom { impl Cnrom { pub fn new(cart: Cartridge) -> Self { - Cnrom{ + Cnrom { cart: cart, chr_bank_select: 0, } @@ -21,8 +21,11 @@ impl Mapper for Cnrom { match address { 0x0000..=0x1FFF => self.cart.chr_rom[self.chr_bank_select][address], 0x8000..=0xBFFF => self.cart.prg_rom[0][addr], - 0xC000..=0xFFFF => self.cart.prg_rom[pl-1][addr], - _ => {println!("bad address read from CNROM mapper: 0x{:X}", address); 0}, + 0xC000..=0xFFFF => self.cart.prg_rom[pl - 1][addr], + _ => { + println!("bad address read from CNROM mapper: 0x{:X}", address); + 0 + } } } @@ -40,15 +43,15 @@ impl Mapper for Cnrom { fn load_battery_backed_ram(&mut self) {} fn save_battery_backed_ram(&self) {} fn clock(&mut self) {} - fn check_irq(&mut self) -> bool {false} + fn check_irq(&mut self) -> bool { + false + } fn save_state(&self) -> MapperData { - MapperData::Cnrom( - CnromData { - cart: self.cart.clone(), - chr_bank_select: self.chr_bank_select, - } - ) + MapperData::Cnrom(CnromData { + cart: self.cart.clone(), + chr_bank_select: self.chr_bank_select, + }) } fn load_state(&mut self, mapper_data: MapperData) { diff --git a/src/cartridge/mmc1.rs b/src/cartridge/mmc1.rs index 08e25ba..149a61b 100644 --- a/src/cartridge/mmc1.rs +++ b/src/cartridge/mmc1.rs @@ -1,4 +1,4 @@ -use super::{Cartridge, Mapper, Mirror, serialize::*}; +use super::{serialize::*, Cartridge, Mapper, Mirror}; use std::fs::File; use std::io::{Read, Write}; @@ -85,13 +85,15 @@ impl Mmc1 { _ => panic!("invalid mirroring value"), }; self.prg_bank_mode = (value >> 2) & 0b11; - self.chr_bank_mode = if value & (1<<4) == 0 {false} else {true}; + self.chr_bank_mode = if value & (1 << 4) == 0 { false } else { true }; } fn write_chr_bank_low(&mut self, value: u8) { - if self.chr_bank_mode { // 4 KB mode + if self.chr_bank_mode { + // 4 KB mode self.chr_low_bank = value as usize; - } else { // 8 KB mode + } else { + // 8 KB mode let v = value & (0xFF - 1); // turn off low bit self.chr_low_bank = v as usize; self.chr_high_bank = (v + 1) as usize; @@ -99,7 +101,8 @@ impl Mmc1 { } fn write_chr_bank_high(&mut self, value: u8) { - if self.chr_bank_mode { // 4 KB mode only, ignored in 8 KB mode + if self.chr_bank_mode { + // 4 KB mode only, ignored in 8 KB mode self.chr_high_bank = value as usize; } } @@ -127,48 +130,51 @@ impl Mapper for Mmc1 { _ => panic!("bad address read from MMC1: 0x{:X}", address), }; let chunk_num = bank / 2; - let chunk_half = if bank % 2 == 0 {0x0} else {0x1000}; + let chunk_half = if bank % 2 == 0 { 0x0 } else { 0x1000 }; self.cart.chr_rom[chunk_num][chunk_half + offset] } else { // if we're in 8K bank mode, the whole $0000-$1FFF region will be the 8K range referred to by chr_low_bank self.cart.chr_rom[self.chr_low_bank][address] } } - }, + } 0x6000..=0x7FFF => self.prg_ram_bank[address % 0x2000], 0x8000..=0xBFFF => { match self.prg_bank_mode { - 0 | 1 => { // switch 32 KB at $8000, ignoring low bit of bank number + 0 | 1 => { + // switch 32 KB at $8000, ignoring low bit of bank number let low_bank = self.prg_bank_select & (0xFF - 1); self.cart.prg_rom[low_bank][address % 0x4000] - }, + } 2 => self.cart.prg_rom[0][address % 0x4000], 3 => self.cart.prg_rom[self.prg_bank_select][address % 0x4000], _ => panic!("invalid PRG bank mode"), } - }, + } 0xC000..=0xFFFF => { match self.prg_bank_mode { - 0 | 1 => { // switch 32 KB at $8000, ignoring low bit of bank number + 0 | 1 => { + // switch 32 KB at $8000, ignoring low bit of bank number let high_bank = (self.prg_bank_select & (0xFF - 1)) + 1; self.cart.prg_rom[high_bank][address % 0x4000] - }, + } 2 => self.cart.prg_rom[self.prg_bank_select][address % 0x4000], 3 => self.cart.prg_rom[self.cart.prg_rom_size - 1][address % 0x4000], _ => panic!("invalid PRG bank mode"), } - }, + } _ => panic!("invalid address passed to MMC1: 0x{:X}", address), } } fn write(&mut self, address: usize, value: u8) { match address { - 0x0000..=0x1FFF => { // if we don't have CHR-ROM, write to CHR-RAM + 0x0000..=0x1FFF => { + // if we don't have CHR-ROM, write to CHR-RAM if self.cart.chr_rom_size == 0 { self.chr_ram_bank[address] = value; } - }, + } 0x6000..=0x7FFF => self.prg_ram_bank[address % 0x2000] = value, 0x8000..=0xFFFF => self.write_serial_port(address, value), _ => panic!("bad address write to MMC1: 0x{:X}", address), @@ -186,9 +192,11 @@ impl Mapper for Mmc1 { let mut save_file = p.join(stem); save_file.set_extension("sav"); if Path::new(&save_file).exists() { - let mut f = File::open(save_file.clone()).expect("save file exists but could not open it"); + let mut f = + File::open(save_file.clone()).expect("save file exists but could not open it"); let mut battery_backed_ram_data = vec![]; - f.read_to_end(&mut battery_backed_ram_data).expect("error reading save file"); + f.read_to_end(&mut battery_backed_ram_data) + .expect("error reading save file"); println!("loading battery-backed RAM from file: {:?}", save_file); self.prg_ram_bank = battery_backed_ram_data; } @@ -204,31 +212,32 @@ impl Mapper for Mmc1 { println!("saving battery-backed RAM to file: {:?}", save_file); let mut f = File::create(&save_file) .expect("could not create output file for battery-backed RAM"); - f.write_all(&self.prg_ram_bank).expect("could not write battery-backed RAM to file"); + f.write_all(&self.prg_ram_bank) + .expect("could not write battery-backed RAM to file"); } } fn clock(&mut self) {} - fn check_irq(&mut self) -> bool {false} + fn check_irq(&mut self) -> bool { + false + } fn save_state(&self) -> MapperData { - MapperData::Mmc1( - Mmc1Data { - cart: self.cart.clone(), - step: self.step, - shift_register: self.shift_register, - mirroring: self.mirroring, - control: self.control, - prg_ram_bank: self.prg_ram_bank.clone(), - prg_ram_enabled: self.prg_ram_enabled, - prg_bank_mode: self.prg_bank_mode, - prg_bank_select: self.prg_bank_select, - chr_ram_bank: self.chr_ram_bank.clone(), - chr_low_bank: self.chr_low_bank, - chr_high_bank: self.chr_high_bank, - chr_bank_mode: self.chr_bank_mode, - } - ) + MapperData::Mmc1(Mmc1Data { + cart: self.cart.clone(), + step: self.step, + shift_register: self.shift_register, + mirroring: self.mirroring, + control: self.control, + prg_ram_bank: self.prg_ram_bank.clone(), + prg_ram_enabled: self.prg_ram_enabled, + prg_bank_mode: self.prg_bank_mode, + prg_bank_select: self.prg_bank_select, + chr_ram_bank: self.chr_ram_bank.clone(), + chr_low_bank: self.chr_low_bank, + chr_high_bank: self.chr_high_bank, + chr_bank_mode: self.chr_bank_mode, + }) } fn load_state(&mut self, mapper_data: MapperData) { diff --git a/src/cartridge/mmc3.rs b/src/cartridge/mmc3.rs index 218651e..2b1f3d7 100644 --- a/src/cartridge/mmc3.rs +++ b/src/cartridge/mmc3.rs @@ -1,4 +1,4 @@ -use super::{Cartridge, Mapper, Mirror, serialize::*}; +use super::{serialize::*, Cartridge, Mapper, Mirror}; pub struct Mmc3 { cart: Cartridge, @@ -20,7 +20,6 @@ pub struct Mmc3 { prg_rom_bank_mode: bool, // 0: two 2 KB banks at $0000-$0FFF, four 1 KB banks at $1000-$1FFF // 1: two 2 KB banks at $1000-$1FFF, four 1 KB banks at $0000-$0FFF - chr_rom_bank_mode: bool, chr_ram_bank: Vec, // used if cartridge doesn't have any CHR-ROM, 8KB, $0000-$1FFF } @@ -28,7 +27,7 @@ pub struct Mmc3 { impl Mmc3 { pub fn new(cart: Cartridge) -> Self { let m = cart.mirroring; - Mmc3{ + Mmc3 { cart: cart, mirroring: m, bank_registers: vec![0, 0, 0, 0, 0, 0, 0, 0], @@ -49,8 +48,8 @@ impl Mmc3 { fn bank_select(&mut self, value: u8) { self.next_bank = value & 0b111; // ?? = value & (1<<5); // Nothing on the MMC3, see MMC6 - self.prg_rom_bank_mode = value & (1<<6) != 0; - self.chr_rom_bank_mode = value & (1<<7) != 0; + self.prg_rom_bank_mode = value & (1 << 6) != 0; + self.chr_rom_bank_mode = value & (1 << 7) != 0; } fn bank_data(&mut self, value: u8) { @@ -69,77 +68,72 @@ impl Mmc3 { impl Mapper for Mmc3 { fn read(&self, address: usize) -> u8 { let val = match address { - 0x0000..=0x1FFF => { // reading from CHR-ROM + 0x0000..=0x1FFF => { + // reading from CHR-ROM let offset_1k = address % 0x400; let offset_2k = address % 0x800; let bank_reg_num = match self.chr_rom_bank_mode { - true => { - match address { - 0x0000..=0x03FF => 2, - 0x0400..=0x07FF => 3, - 0x0800..=0x0BFF => 4, - 0x0C00..=0x0FFF => 5, - 0x1000..=0x17FF => 0, - 0x1800..=0x1FFF => 1, - _ => panic!("oh no"), - } + true => match address { + 0x0000..=0x03FF => 2, + 0x0400..=0x07FF => 3, + 0x0800..=0x0BFF => 4, + 0x0C00..=0x0FFF => 5, + 0x1000..=0x17FF => 0, + 0x1800..=0x1FFF => 1, + _ => panic!("oh no"), }, - false => { - match address { - 0x0000..=0x07FF => 0, - 0x0800..=0x0FFF => 1, - 0x1000..=0x13FF => 2, - 0x1400..=0x17FF => 3, - 0x1800..=0x1BFF => 4, - 0x1C00..=0x1FFF => 5, - _ => panic!("oh no"), - } + false => match address { + 0x0000..=0x07FF => 0, + 0x0800..=0x0FFF => 1, + 0x1000..=0x13FF => 2, + 0x1400..=0x17FF => 3, + 0x1800..=0x1BFF => 4, + 0x1C00..=0x1FFF => 5, + _ => panic!("oh no"), }, }; let bank_num = self.bank_registers[bank_reg_num]; let chunk_num = bank_num / 8; let chunk_eighth = (bank_num % 8) * 0x400; - if bank_reg_num == 0 || bank_reg_num == 1 { // dealing with 2K banks of 8K chunks + if bank_reg_num == 0 || bank_reg_num == 1 { + // dealing with 2K banks of 8K chunks self.cart.chr_rom[chunk_num][chunk_eighth + offset_2k] - } else { // dealing with 1K banks of 8K chunks + } else { + // dealing with 1K banks of 8K chunks self.cart.chr_rom[chunk_num][chunk_eighth + offset_1k] } - }, + } 0x6000..=0x7FFF => self.prg_ram_bank[address % 0x2000], // PRG-RAM - 0x8000..=0xFFFF => { // reading from PRG ROM, dealing with 8K banks of 16K chunks + 0x8000..=0xFFFF => { + // reading from PRG ROM, dealing with 8K banks of 16K chunks let offset_8k = address % 0x2000; let num_banks = self.cart.prg_rom_size * 2; let bank_num = match self.prg_rom_bank_mode { - true => { - match address { - 0x8000..=0x9FFF => num_banks - 2, - 0xA000..=0xBFFF => self.bank_registers[7], - 0xC000..=0xDFFF => self.bank_registers[6], - 0xE000..=0xFFFF => num_banks - 1, - _ => panic!("oh no"), - } + true => match address { + 0x8000..=0x9FFF => num_banks - 2, + 0xA000..=0xBFFF => self.bank_registers[7], + 0xC000..=0xDFFF => self.bank_registers[6], + 0xE000..=0xFFFF => num_banks - 1, + _ => panic!("oh no"), }, - false => { - match address { - 0x8000..=0x9FFF => self.bank_registers[6], - 0xA000..=0xBFFF => self.bank_registers[7], - 0xC000..=0xDFFF => num_banks - 2, - 0xE000..=0xFFFF => num_banks - 1, - _ => panic!("oh no"), - } + false => match address { + 0x8000..=0x9FFF => self.bank_registers[6], + 0xA000..=0xBFFF => self.bank_registers[7], + 0xC000..=0xDFFF => num_banks - 2, + 0xE000..=0xFFFF => num_banks - 1, + _ => panic!("oh no"), }, }; let chunk_num = bank_num / 2; let chunk_half = (bank_num % 2) * 0x2000; self.cart.prg_rom[chunk_num][chunk_half + offset_8k] - - }, + } _ => { println!("bad address read from MMC3: 0x{:X}", address); 0 - }, + } }; val } @@ -149,20 +143,31 @@ impl Mapper for Mmc3 { if self.cart.chr_rom_size == 0 { self.chr_ram_bank[address] = value; } - return + return; } match address % 2 == 0 { - true => { // even + true => { + // even match address { 0x6000..=0x7FFF => self.prg_ram_bank[address % 0x2000] = value, // PRG-RAM 0x8000..=0x9FFF => self.bank_select(value), - 0xA000..=0xBFFF => self.mirroring = if value & 1 == 0 {Mirror::Vertical} else {Mirror::Horizontal}, + 0xA000..=0xBFFF => { + self.mirroring = if value & 1 == 0 { + Mirror::Vertical + } else { + Mirror::Horizontal + } + } 0xC000..=0xDFFF => self.irq_latch = value, - 0xE000..=0xFFFF => {self.irq_enable = false; self.trigger_irq = false}, // Writing any value to this register will disable MMC3 interrupts AND acknowledge any pending interrupts. + 0xE000..=0xFFFF => { + self.irq_enable = false; + self.trigger_irq = false + } // Writing any value to this register will disable MMC3 interrupts AND acknowledge any pending interrupts. _ => println!("bad address written to MMC3: 0x{:X}", address), } - }, - false => { // odd + } + false => { + // odd match address { 0x6000..=0x7FFF => self.prg_ram_bank[address % 0x2000] = value, // PRG-RAM 0x8000..=0x9FFF => self.bank_data(value), @@ -171,7 +176,7 @@ impl Mapper for Mmc3 { 0xE000..=0xFFFF => self.irq_enable = true, _ => println!("bad address written to MMC3: 0x{:X}", address), } - }, + } } } @@ -224,24 +229,22 @@ impl Mapper for Mmc3 { } fn save_state(&self) -> MapperData { - MapperData::Mmc3( - Mmc3Data { - cart: self.cart.clone(), - mirroring: self.mirroring, - bank_registers: self.bank_registers.clone(), - next_bank: self.next_bank, - irq_latch: self.irq_latch, - irq_counter: self.irq_counter, - irq_enable: self.irq_enable, - trigger_irq: self.trigger_irq, - reload_counter: self.reload_counter, - irq_delay: self.irq_delay, - prg_ram_bank: self.prg_ram_bank.clone(), - prg_rom_bank_mode: self.prg_rom_bank_mode, - chr_rom_bank_mode: self.chr_rom_bank_mode, - chr_ram_bank: self.chr_ram_bank.clone(), - } - ) + MapperData::Mmc3(Mmc3Data { + cart: self.cart.clone(), + mirroring: self.mirroring, + bank_registers: self.bank_registers.clone(), + next_bank: self.next_bank, + irq_latch: self.irq_latch, + irq_counter: self.irq_counter, + irq_enable: self.irq_enable, + trigger_irq: self.trigger_irq, + reload_counter: self.reload_counter, + irq_delay: self.irq_delay, + prg_ram_bank: self.prg_ram_bank.clone(), + prg_rom_bank_mode: self.prg_rom_bank_mode, + chr_rom_bank_mode: self.chr_rom_bank_mode, + chr_ram_bank: self.chr_ram_bank.clone(), + }) } fn load_state(&mut self, mapper_data: MapperData) { diff --git a/src/cartridge/mod.rs b/src/cartridge/mod.rs index c933ef9..c4d8838 100644 --- a/src/cartridge/mod.rs +++ b/src/cartridge/mod.rs @@ -1,15 +1,15 @@ -mod nrom; -mod mmc1; -mod uxrom; mod cnrom; +mod mmc1; mod mmc3; +mod nrom; pub mod serialize; +mod uxrom; -use nrom::Nrom; -use mmc1::Mmc1; -use uxrom::Uxrom; use cnrom::Cnrom; +use mmc1::Mmc1; use mmc3::Mmc3; +use nrom::Nrom; +use uxrom::Uxrom; use std::cell::RefCell; use std::fs::File; @@ -55,12 +55,11 @@ pub struct Cartridge { filename: String, prg_rom_size: usize, chr_rom_size: usize, - pub mirroring: Mirror, // 0 horizontal, 1 vertical + pub mirroring: Mirror, // 0 horizontal, 1 vertical battery_backed_ram: bool, // 1: Cartridge contains battery-backed PRG RAM ($6000-7FFF) or other persistent memory - trainer_present: bool, // 1: 512-byte trainer at $7000-$71FF (stored before PRG data) + trainer_present: bool, // 1: 512-byte trainer at $7000-$71FF (stored before PRG data) four_screen_vram: bool, // 1: Ignore mirroring control or above mirroring bit; instead provide four-screen VRAM // TODO: other iNES header flags - pub prg_rom: Vec>, // 16 KiB chunks for CPU pub chr_rom: Vec>, // 8 KiB chunks for PPU @@ -73,16 +72,23 @@ impl Cartridge { 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"); + assert!( + data[0..4] == [0x4E, 0x45, 0x53, 0x1A], + "signature mismatch, not an iNES file" + ); let mapper_num = ((data[7] >> 4) << 4) + (data[6] >> 4); let mut cart = Cartridge { filename: filename.to_string(), prg_rom_size: data[4] as usize, chr_rom_size: data[5] as usize, - mirroring: if data[6] & (1 << 0) == 0 {Mirror::Horizontal} else {Mirror::Vertical}, + mirroring: if data[6] & (1 << 0) == 0 { + Mirror::Horizontal + } else { + Mirror::Vertical + }, battery_backed_ram: data[6] & (1 << 1) != 0, - trainer_present: data[6] & (1 << 2) != 0, - four_screen_vram: data[6] & (1 << 3) != 0, + trainer_present: data[6] & (1 << 2) != 0, + four_screen_vram: data[6] & (1 << 3) != 0, prg_rom: Vec::new(), chr_rom: Vec::new(), all_data: data, @@ -93,24 +99,23 @@ impl Cartridge { } fn fill(&mut self) { - let prg_chunk_size: usize = 1<<14; - let chr_chunk_size: usize = 1<<13; + let prg_chunk_size: usize = 1 << 14; + let chr_chunk_size: usize = 1 << 13; let prg_offset: usize = 0x10 + if self.trainer_present { 0x200 } else { 0 }; // header plus trainer if present let chr_offset: usize = prg_offset + (self.prg_rom_size * prg_chunk_size); // chr comes after prg - // fill vecs with chunks + // fill vecs with chunks for i in 0..self.prg_rom_size { let offset = prg_offset + (i * prg_chunk_size); let chunk = self.all_data[offset..(offset + prg_chunk_size)].to_vec(); self.prg_rom.push(chunk.clone()); - }; + } for i in 0..self.chr_rom_size { let offset = chr_offset + (i * chr_chunk_size); let chunk = self.all_data[offset..offset + chr_chunk_size].to_vec(); self.chr_rom.push(chunk); - }; + } self.all_data.clear(); } - } pub fn check_signature(filename: &str) -> Result<(), String> { diff --git a/src/cartridge/nrom.rs b/src/cartridge/nrom.rs index 4246e95..960275c 100644 --- a/src/cartridge/nrom.rs +++ b/src/cartridge/nrom.rs @@ -1,4 +1,4 @@ -use super::{Cartridge, Mapper, Mirror, serialize::*}; +use super::{serialize::*, Cartridge, Mapper, Mirror}; pub struct Nrom { cart: Cartridge, @@ -7,7 +7,7 @@ pub struct Nrom { impl Nrom { pub fn new(cart: Cartridge) -> Self { - Nrom{ + Nrom { cart: cart, chr_ram: vec![0; 0x2000], } @@ -24,14 +24,13 @@ impl Mapper for Nrom { } else { self.chr_ram[address] } - }, - 0x8000..=0xBFFF => { - self.cart.prg_rom[0][addr] - }, - 0xC000..=0xFFFF => { - self.cart.prg_rom[self.cart.prg_rom_size - 1][addr] - }, - _ => {println!("bad address read from NROM mapper: 0x{:X}", address); 0}, + } + 0x8000..=0xBFFF => self.cart.prg_rom[0][addr], + 0xC000..=0xFFFF => self.cart.prg_rom[self.cart.prg_rom_size - 1][addr], + _ => { + println!("bad address read from NROM mapper: 0x{:X}", address); + 0 + } } } @@ -42,7 +41,7 @@ impl Mapper for Nrom { if self.cart.chr_rom_size == 0 { self.chr_ram[address] = value; } - }, + } 0x8000..=0xBFFF => (), 0xC000..=0xFFFF => (), _ => println!("bad address written to NROM mapper: 0x{:X}", address), @@ -56,15 +55,15 @@ impl Mapper for Nrom { fn load_battery_backed_ram(&mut self) {} fn save_battery_backed_ram(&self) {} fn clock(&mut self) {} - fn check_irq(&mut self) -> bool {false} + fn check_irq(&mut self) -> bool { + false + } fn save_state(&self) -> MapperData { - MapperData::Nrom( - NromData { - cart: self.cart.clone(), - chr_ram: self.chr_ram.clone(), - } - ) + MapperData::Nrom(NromData { + cart: self.cart.clone(), + chr_ram: self.chr_ram.clone(), + }) } fn load_state(&mut self, mapper_data: MapperData) { diff --git a/src/cartridge/serialize.rs b/src/cartridge/serialize.rs index c00e1c8..2ed1385 100644 --- a/src/cartridge/serialize.rs +++ b/src/cartridge/serialize.rs @@ -1,6 +1,6 @@ use super::{Cartridge, Mirror}; -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(serde::Serialize, serde::Deserialize, Clone)] pub enum MapperData { Nrom(NromData), Mmc1(Mmc1Data), @@ -9,14 +9,13 @@ pub enum MapperData { Mmc3(Mmc3Data), } - -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(serde::Serialize, serde::Deserialize, Clone)] pub struct NromData { pub cart: Cartridge, pub chr_ram: Vec, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(serde::Serialize, serde::Deserialize, Clone)] pub struct Mmc1Data { pub cart: Cartridge, pub step: u8, @@ -33,20 +32,20 @@ pub struct Mmc1Data { pub chr_bank_mode: bool, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(serde::Serialize, serde::Deserialize, Clone)] pub struct UxromData { pub cart: Cartridge, pub chr_ram: Vec, pub bank_select: usize, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(serde::Serialize, serde::Deserialize, Clone)] pub struct CnromData { pub cart: Cartridge, pub chr_bank_select: usize, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(serde::Serialize, serde::Deserialize, Clone)] pub struct Mmc3Data { pub cart: Cartridge, pub mirroring: Mirror, @@ -61,5 +60,5 @@ pub struct Mmc3Data { pub prg_ram_bank: Vec, pub prg_rom_bank_mode: bool, pub chr_rom_bank_mode: bool, - pub chr_ram_bank: Vec, + pub chr_ram_bank: Vec, } diff --git a/src/cartridge/uxrom.rs b/src/cartridge/uxrom.rs index 1e38dd7..a797ba3 100644 --- a/src/cartridge/uxrom.rs +++ b/src/cartridge/uxrom.rs @@ -1,4 +1,4 @@ -use super::{Cartridge, Mapper, Mirror, serialize::*}; +use super::{serialize::*, Cartridge, Mapper, Mirror}; pub struct Uxrom { cart: Cartridge, @@ -8,7 +8,7 @@ pub struct Uxrom { impl Uxrom { pub fn new(cart: Cartridge) -> Self { - Uxrom{ + Uxrom { cart: cart, chr_ram: vec![0; 0x2000], bank_select: 0, @@ -25,10 +25,13 @@ impl Mapper for Uxrom { } else { self.chr_ram[address] } - }, + } 0x8000..=0xBFFF => self.cart.prg_rom[self.bank_select][address % 0x4000], - 0xC000..=0xFFFF => self.cart.prg_rom[self.cart.prg_rom.len()-1][address % 0x4000], - _ => {println!("bad address read from UxROM mapper: 0x{:X}", address); 0}, + 0xC000..=0xFFFF => self.cart.prg_rom[self.cart.prg_rom.len() - 1][address % 0x4000], + _ => { + println!("bad address read from UxROM mapper: 0x{:X}", address); + 0 + } } } @@ -38,7 +41,7 @@ impl Mapper for Uxrom { if self.cart.chr_rom_size == 0 { self.chr_ram[address] = value; } - }, + } 0x8000..=0xFFFF => self.bank_select = value as usize, _ => println!("bad address written to UxROM mapper: 0x{:X}", address), } @@ -51,16 +54,16 @@ impl Mapper for Uxrom { fn load_battery_backed_ram(&mut self) {} fn save_battery_backed_ram(&self) {} fn clock(&mut self) {} - fn check_irq(&mut self) -> bool {false} + fn check_irq(&mut self) -> bool { + false + } fn save_state(&self) -> MapperData { - MapperData::Uxrom( - UxromData { - cart: self.cart.clone(), - chr_ram: self.chr_ram.clone(), - bank_select: self.bank_select, - } - ) + MapperData::Uxrom(UxromData { + cart: self.cart.clone(), + chr_ram: self.chr_ram.clone(), + bank_select: self.bank_select, + }) } fn load_state(&mut self, mapper_data: MapperData) { diff --git a/src/cpu/addressing_modes.rs b/src/cpu/addressing_modes.rs index 63aaa61..62e2a7e 100644 --- a/src/cpu/addressing_modes.rs +++ b/src/cpu/addressing_modes.rs @@ -1,10 +1,9 @@ impl super::Cpu { - pub fn absolute(&mut self) -> usize { self.clock += 4; ::from( ((self.read(self.pc + 2) as usize) << 8) + // high byte, little endian - (self.read(self.pc + 1)) as usize // low byte + (self.read(self.pc + 1)) as usize, // low byte ) } @@ -13,10 +12,10 @@ impl super::Cpu { let old_address = self.absolute(); let new_address = old_address + self.x as usize; match current_opcode { - 0x1C | 0x1D | 0x3C | 0x3D | 0x5C | 0x5D | 0x7C | 0x7D | 0xBC | 0xBD | 0xDC | 0xDD | 0xFC | 0xFD - => self.address_page_cross(old_address, new_address), - 0x1E | 0x1F | 0x3E | 0x3F | 0x5E | 0x5F | 0x7E | 0x7F | 0x9D | 0xC3 | 0xC7 | 0xCF | 0xD3 | 0xD7 | 0xDB | 0xDE | 0xDF | 0xFE | 0xFF - => self.clock += 1, + 0x1C | 0x1D | 0x3C | 0x3D | 0x5C | 0x5D | 0x7C | 0x7D | 0xBC | 0xBD | 0xDC | 0xDD + | 0xFC | 0xFD => self.address_page_cross(old_address, new_address), + 0x1E | 0x1F | 0x3E | 0x3F | 0x5E | 0x5F | 0x7E | 0x7F | 0x9D | 0xC3 | 0xC7 | 0xCF + | 0xD3 | 0xD7 | 0xDB | 0xDE | 0xDF | 0xFE | 0xFF => self.clock += 1, _ => panic!("illegal opcode using abs x: {:02x}", current_opcode), } new_address @@ -62,8 +61,8 @@ impl super::Cpu { } pub fn indirect(&mut self) -> usize { - let operand_address = ((self.read(self.pc + 2) as usize) << 8) - + (self.read(self.pc + 1) as usize); + let operand_address = + ((self.read(self.pc + 2) as usize) << 8) + (self.read(self.pc + 1) as usize); let low_byte = self.read(operand_address) as usize; // BUG TIME! from https://wiki.nesdev.com/w/index.php/Errata // "JMP ($xxyy), or JMP indirect, does not advance pages if the lower eight bits @@ -118,5 +117,4 @@ impl super::Cpu { self.clock += 4; operand.wrapping_add(self.y) as usize } - } diff --git a/src/cpu/mod.rs b/src/cpu/mod.rs index fade3cb..7e8f0a9 100644 --- a/src/cpu/mod.rs +++ b/src/cpu/mod.rs @@ -1,12 +1,13 @@ mod addressing_modes; mod opcodes; -mod utility; pub mod serialize; +mod utility; -use std::cell::RefCell; -use std::rc::Rc; -use serde::{Serialize, Deserialize}; use crate::cartridge::Mapper; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use std::collections::HashSet; +use std::rc::Rc; // RAM locations const STACK_OFFSET: usize = 0x100; @@ -15,26 +16,36 @@ const RESET_VECTOR: usize = 0xFFFC; const IRQ_VECTOR: usize = 0xFFFE; // status register flags -const CARRY_FLAG: u8 = 1 << 0; -const ZERO_FLAG: u8 = 1 << 1; +const CARRY_FLAG: u8 = 1 << 0; +const ZERO_FLAG: u8 = 1 << 1; const INTERRUPT_DISABLE_FLAG: u8 = 1 << 2; -const DECIMAL_FLAG: u8 = 1 << 3; +const DECIMAL_FLAG: u8 = 1 << 3; // bits 4 and 5 are unused except when status register is copied to stack -const OVERFLOW_FLAG: u8 = 1 << 6; -const NEGATIVE_FLAG: u8 = 1 << 7; +const OVERFLOW_FLAG: u8 = 1 << 6; +const NEGATIVE_FLAG: u8 = 1 << 7; -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Hash)] pub enum Mode { - ABS, ABX, ABY, ACC, - IMM, IMP, IDX, IND, - INX, REL, ZPG, ZPX, + ABS, + ABX, + ABY, + ACC, + IMM, + IMP, + IDX, + IND, + INX, + REL, + ZPG, + ZPX, ZPY, } type AddressingFunction = fn(&mut Cpu) -> usize; impl Mode { - fn get(&self) -> (AddressingFunction, usize) { // usize is number of bytes the instruction takes, used for debug printing + fn get(&self) -> (AddressingFunction, usize) { + // usize is number of bytes the instruction takes, used for debug printing match self { Mode::ABS => (Cpu::absolute, 3), Mode::ABX => (Cpu::absolute_x, 3), @@ -53,16 +64,17 @@ impl Mode { } } +#[derive(Clone)] pub struct Cpu { - mem: Vec, // CPU's RAM, $0000-$1FFF - a: u8, // accumulator - x: u8, // general purpose - y: u8, // general purpose - pc: usize, // 16-bit program counter - s: u8, // stack pointer - p: u8, // status - - clock: u64, // number of ticks in current cycle + pub mem: Vec, // CPU's RAM, $0000-$1FFF + a: u8, // accumulator + x: u8, // general purpose + y: u8, // general purpose + pc: usize, // 16-bit program counter + s: u8, // stack pointer + p: u8, // status + pub addresses_fetched: HashSet, + clock: u64, // number of ticks in current cycle delay: usize, // for skipping cycles during OAM DMA pub mapper: Rc>, // cartridge data @@ -72,17 +84,20 @@ pub struct Cpu { // controller pub strobe: u8, // signals to the controller that button inputs should be read pub button_states: u8, // Player 1 controller + pub button_states2: u8, // Player 2 controller button_number: u8, // counter that scans the bits of the input register serially opcode_table: Vec, // function table - mode_table: Vec, // address mode table + mode_table: Vec, // address mode table } impl Cpu { pub fn new(mapper: Rc>, ppu: super::Ppu, apu: super::Apu) -> Self { - let mut cpu = Cpu{ + let mut cpu = Cpu { mem: vec![0; 0x2000], - a: 0, x: 0, y: 0, + a: 0, + x: 0, + y: 0, pc: 0, s: 0xFD, p: 0x24, @@ -93,52 +108,540 @@ impl Cpu { apu: apu, strobe: 0, button_states: 0, + button_states2: 0, button_number: 0, + addresses_fetched: HashSet::new(), + opcode_table: vec![ - // 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F - /*00*/ Cpu::brk, Cpu::ora, Cpu::bad, Cpu::slo, Cpu::nop, Cpu::ora, Cpu::asl, Cpu::slo, Cpu::php, Cpu::ora, Cpu::asl, Cpu::nop, Cpu::nop, Cpu::ora, Cpu::asl, Cpu::slo, /*00*/ - /*10*/ Cpu::bpl, Cpu::ora, Cpu::bad, Cpu::slo, Cpu::nop, Cpu::ora, Cpu::asl, Cpu::slo, Cpu::clc, Cpu::ora, Cpu::nop, Cpu::slo, Cpu::nop, Cpu::ora, Cpu::asl, Cpu::slo, /*10*/ - /*20*/ Cpu::jsr, Cpu::and, Cpu::bad, Cpu::rla, Cpu::bit, Cpu::and, Cpu::rol, Cpu::rla, Cpu::plp, Cpu::and, Cpu::rol, Cpu::nop, Cpu::bit, Cpu::and, Cpu::rol, Cpu::rla, /*20*/ - /*30*/ Cpu::bmi, Cpu::and, Cpu::bad, Cpu::rla, Cpu::nop, Cpu::and, Cpu::rol, Cpu::rla, Cpu::sec, Cpu::and, Cpu::nop, Cpu::rla, Cpu::nop, Cpu::and, Cpu::rol, Cpu::rla, /*30*/ - /*40*/ Cpu::rti, Cpu::eor, Cpu::bad, Cpu::sre, Cpu::nop, Cpu::eor, Cpu::lsr, Cpu::sre, Cpu::pha, Cpu::eor, Cpu::lsr, Cpu::nop, Cpu::jmp, Cpu::eor, Cpu::lsr, Cpu::sre, /*40*/ - /*50*/ Cpu::bvc, Cpu::eor, Cpu::bad, Cpu::sre, Cpu::nop, Cpu::eor, Cpu::lsr, Cpu::sre, Cpu::cli, Cpu::eor, Cpu::nop, Cpu::sre, Cpu::nop, Cpu::eor, Cpu::lsr, Cpu::sre, /*50*/ - /*60*/ Cpu::rts, Cpu::adc, Cpu::bad, Cpu::rra, Cpu::nop, Cpu::adc, Cpu::ror, Cpu::rra, Cpu::pla, Cpu::adc, Cpu::ror, Cpu::nop, Cpu::jmp, Cpu::adc, Cpu::ror, Cpu::rra, /*60*/ - /*70*/ Cpu::bvs, Cpu::adc, Cpu::bad, Cpu::rra, Cpu::nop, Cpu::adc, Cpu::ror, Cpu::rra, Cpu::sei, Cpu::adc, Cpu::nop, Cpu::rra, Cpu::nop, Cpu::adc, Cpu::ror, Cpu::rra, /*70*/ - /*80*/ Cpu::nop, Cpu::sta, Cpu::nop, Cpu::sax, Cpu::sty, Cpu::sta, Cpu::stx, Cpu::sax, Cpu::dey, Cpu::nop, Cpu::txa, Cpu::nop, Cpu::sty, Cpu::sta, Cpu::stx, Cpu::sax, /*80*/ - /*90*/ Cpu::bcc, Cpu::sta, Cpu::bad, Cpu::nop, Cpu::sty, Cpu::sta, Cpu::stx, Cpu::sax, Cpu::tya, Cpu::sta, Cpu::txs, Cpu::nop, Cpu::nop, Cpu::sta, Cpu::nop, Cpu::nop, /*90*/ - /*A0*/ Cpu::ldy, Cpu::lda, Cpu::ldx, Cpu::lax, Cpu::ldy, Cpu::lda, Cpu::ldx, Cpu::lax, Cpu::tay, Cpu::lda, Cpu::tax, Cpu::nop, Cpu::ldy, Cpu::lda, Cpu::ldx, Cpu::lax, /*A0*/ - /*B0*/ Cpu::bcs, Cpu::lda, Cpu::bad, Cpu::lax, Cpu::ldy, Cpu::lda, Cpu::ldx, Cpu::lax, Cpu::clv, Cpu::lda, Cpu::tsx, Cpu::nop, Cpu::ldy, Cpu::lda, Cpu::ldx, Cpu::lax, /*B0*/ - /*C0*/ Cpu::cpy, Cpu::cmp, Cpu::nop, Cpu::dcp, Cpu::cpy, Cpu::cmp, Cpu::dec, Cpu::dcp, Cpu::iny, Cpu::cmp, Cpu::dex, Cpu::nop, Cpu::cpy, Cpu::cmp, Cpu::dec, Cpu::dcp, /*C0*/ - /*D0*/ Cpu::bne, Cpu::cmp, Cpu::bad, Cpu::dcp, Cpu::nop, Cpu::cmp, Cpu::dec, Cpu::dcp, Cpu::cld, Cpu::cmp, Cpu::nop, Cpu::dcp, Cpu::nop, Cpu::cmp, Cpu::dec, Cpu::dcp, /*D0*/ - /*E0*/ Cpu::cpx, Cpu::sbc, Cpu::nop, Cpu::isc, Cpu::cpx, Cpu::sbc, Cpu::inc, Cpu::isc, Cpu::inx, Cpu::sbc, Cpu::nop, Cpu::sbc, Cpu::cpx, Cpu::sbc, Cpu::inc, Cpu::isc, /*E0*/ - /*F0*/ Cpu::beq, Cpu::sbc, Cpu::bad, Cpu::isc, Cpu::nop, Cpu::sbc, Cpu::inc, Cpu::isc, Cpu::sed, Cpu::sbc, Cpu::nop, Cpu::isc, Cpu::nop, Cpu::sbc, Cpu::inc, Cpu::isc, /*F0*/ + // 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F + /*00*/ + Cpu::brk, + Cpu::ora, + Cpu::bad, + Cpu::slo, + Cpu::nop, + Cpu::ora, + Cpu::asl, + Cpu::slo, + Cpu::php, + Cpu::ora, + Cpu::asl, + Cpu::nop, + Cpu::nop, + Cpu::ora, + Cpu::asl, + Cpu::slo, /*00*/ + /*10*/ Cpu::bpl, + Cpu::ora, + Cpu::bad, + Cpu::slo, + Cpu::nop, + Cpu::ora, + Cpu::asl, + Cpu::slo, + Cpu::clc, + Cpu::ora, + Cpu::nop, + Cpu::slo, + Cpu::nop, + Cpu::ora, + Cpu::asl, + Cpu::slo, /*10*/ + /*20*/ Cpu::jsr, + Cpu::and, + Cpu::bad, + Cpu::rla, + Cpu::bit, + Cpu::and, + Cpu::rol, + Cpu::rla, + Cpu::plp, + Cpu::and, + Cpu::rol, + Cpu::nop, + Cpu::bit, + Cpu::and, + Cpu::rol, + Cpu::rla, /*20*/ + /*30*/ Cpu::bmi, + Cpu::and, + Cpu::bad, + Cpu::rla, + Cpu::nop, + Cpu::and, + Cpu::rol, + Cpu::rla, + Cpu::sec, + Cpu::and, + Cpu::nop, + Cpu::rla, + Cpu::nop, + Cpu::and, + Cpu::rol, + Cpu::rla, /*30*/ + /*40*/ Cpu::rti, + Cpu::eor, + Cpu::bad, + Cpu::sre, + Cpu::nop, + Cpu::eor, + Cpu::lsr, + Cpu::sre, + Cpu::pha, + Cpu::eor, + Cpu::lsr, + Cpu::nop, + Cpu::jmp, + Cpu::eor, + Cpu::lsr, + Cpu::sre, /*40*/ + /*50*/ Cpu::bvc, + Cpu::eor, + Cpu::bad, + Cpu::sre, + Cpu::nop, + Cpu::eor, + Cpu::lsr, + Cpu::sre, + Cpu::cli, + Cpu::eor, + Cpu::nop, + Cpu::sre, + Cpu::nop, + Cpu::eor, + Cpu::lsr, + Cpu::sre, /*50*/ + /*60*/ Cpu::rts, + Cpu::adc, + Cpu::bad, + Cpu::rra, + Cpu::nop, + Cpu::adc, + Cpu::ror, + Cpu::rra, + Cpu::pla, + Cpu::adc, + Cpu::ror, + Cpu::nop, + Cpu::jmp, + Cpu::adc, + Cpu::ror, + Cpu::rra, /*60*/ + /*70*/ Cpu::bvs, + Cpu::adc, + Cpu::bad, + Cpu::rra, + Cpu::nop, + Cpu::adc, + Cpu::ror, + Cpu::rra, + Cpu::sei, + Cpu::adc, + Cpu::nop, + Cpu::rra, + Cpu::nop, + Cpu::adc, + Cpu::ror, + Cpu::rra, /*70*/ + /*80*/ Cpu::nop, + Cpu::sta, + Cpu::nop, + Cpu::sax, + Cpu::sty, + Cpu::sta, + Cpu::stx, + Cpu::sax, + Cpu::dey, + Cpu::nop, + Cpu::txa, + Cpu::nop, + Cpu::sty, + Cpu::sta, + Cpu::stx, + Cpu::sax, /*80*/ + /*90*/ Cpu::bcc, + Cpu::sta, + Cpu::bad, + Cpu::nop, + Cpu::sty, + Cpu::sta, + Cpu::stx, + Cpu::sax, + Cpu::tya, + Cpu::sta, + Cpu::txs, + Cpu::nop, + Cpu::nop, + Cpu::sta, + Cpu::nop, + Cpu::nop, /*90*/ + /*A0*/ Cpu::ldy, + Cpu::lda, + Cpu::ldx, + Cpu::lax, + Cpu::ldy, + Cpu::lda, + Cpu::ldx, + Cpu::lax, + Cpu::tay, + Cpu::lda, + Cpu::tax, + Cpu::nop, + Cpu::ldy, + Cpu::lda, + Cpu::ldx, + Cpu::lax, /*A0*/ + /*B0*/ Cpu::bcs, + Cpu::lda, + Cpu::bad, + Cpu::lax, + Cpu::ldy, + Cpu::lda, + Cpu::ldx, + Cpu::lax, + Cpu::clv, + Cpu::lda, + Cpu::tsx, + Cpu::nop, + Cpu::ldy, + Cpu::lda, + Cpu::ldx, + Cpu::lax, /*B0*/ + /*C0*/ Cpu::cpy, + Cpu::cmp, + Cpu::nop, + Cpu::dcp, + Cpu::cpy, + Cpu::cmp, + Cpu::dec, + Cpu::dcp, + Cpu::iny, + Cpu::cmp, + Cpu::dex, + Cpu::nop, + Cpu::cpy, + Cpu::cmp, + Cpu::dec, + Cpu::dcp, /*C0*/ + /*D0*/ Cpu::bne, + Cpu::cmp, + Cpu::bad, + Cpu::dcp, + Cpu::nop, + Cpu::cmp, + Cpu::dec, + Cpu::dcp, + Cpu::cld, + Cpu::cmp, + Cpu::nop, + Cpu::dcp, + Cpu::nop, + Cpu::cmp, + Cpu::dec, + Cpu::dcp, /*D0*/ + /*E0*/ Cpu::cpx, + Cpu::sbc, + Cpu::nop, + Cpu::isc, + Cpu::cpx, + Cpu::sbc, + Cpu::inc, + Cpu::isc, + Cpu::inx, + Cpu::sbc, + Cpu::nop, + Cpu::sbc, + Cpu::cpx, + Cpu::sbc, + Cpu::inc, + Cpu::isc, /*E0*/ + /*F0*/ Cpu::beq, + Cpu::sbc, + Cpu::bad, + Cpu::isc, + Cpu::nop, + Cpu::sbc, + Cpu::inc, + Cpu::isc, + Cpu::sed, + Cpu::sbc, + Cpu::nop, + Cpu::isc, + Cpu::nop, + Cpu::sbc, + Cpu::inc, + Cpu::isc, /*F0*/ ], mode_table: vec![ - // 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F - /*00*/ Mode::IMP, Mode::IDX, Mode::IMP, Mode::IDX, Mode::ZPG, Mode::ZPG, Mode::ZPG, Mode::ZPG, Mode::IMP, Mode::IMM, Mode::ACC, Mode::IMM, Mode::ABS, Mode::ABS, Mode::ABS, Mode::ABS, /*00*/ - /*10*/ Mode::REL, Mode::INX, Mode::IMP, Mode::INX, Mode::ZPX, Mode::ZPX, Mode::ZPX, Mode::ZPX, Mode::IMP, Mode::ABY, Mode::IMP, Mode::ABY, Mode::ABX, Mode::ABX, Mode::ABX, Mode::ABX, /*10*/ - /*20*/ Mode::ABS, Mode::IDX, Mode::IMP, Mode::IDX, Mode::ZPG, Mode::ZPG, Mode::ZPG, Mode::ZPG, Mode::IMP, Mode::IMM, Mode::ACC, Mode::IMM, Mode::ABS, Mode::ABS, Mode::ABS, Mode::ABS, /*20*/ - /*30*/ Mode::REL, Mode::INX, Mode::IMP, Mode::INX, Mode::ZPX, Mode::ZPX, Mode::ZPX, Mode::ZPX, Mode::IMP, Mode::ABY, Mode::IMP, Mode::ABY, Mode::ABX, Mode::ABX, Mode::ABX, Mode::ABX, /*30*/ - /*40*/ Mode::IMP, Mode::IDX, Mode::IMP, Mode::IDX, Mode::ZPG, Mode::ZPG, Mode::ZPG, Mode::ZPG, Mode::IMP, Mode::IMM, Mode::ACC, Mode::IMM, Mode::ABS, Mode::ABS, Mode::ABS, Mode::ABS, /*40*/ - /*50*/ Mode::REL, Mode::INX, Mode::IMP, Mode::INX, Mode::ZPX, Mode::ZPX, Mode::ZPX, Mode::ZPX, Mode::IMP, Mode::ABY, Mode::IMP, Mode::ABY, Mode::ABX, Mode::ABX, Mode::ABX, Mode::ABX, /*50*/ - /*60*/ Mode::IMP, Mode::IDX, Mode::IMP, Mode::IDX, Mode::ZPG, Mode::ZPG, Mode::ZPG, Mode::ZPG, Mode::IMP, Mode::IMM, Mode::ACC, Mode::IMM, Mode::IND, Mode::ABS, Mode::ABS, Mode::ABS, /*60*/ - /*70*/ Mode::REL, Mode::INX, Mode::IMP, Mode::INX, Mode::ZPX, Mode::ZPX, Mode::ZPX, Mode::ZPX, Mode::IMP, Mode::ABY, Mode::IMP, Mode::ABY, Mode::ABX, Mode::ABX, Mode::ABX, Mode::ABX, /*70*/ - /*80*/ Mode::IMM, Mode::IDX, Mode::IMM, Mode::IDX, Mode::ZPG, Mode::ZPG, Mode::ZPG, Mode::ZPG, Mode::IMP, Mode::IMM, Mode::IMP, Mode::IMM, Mode::ABS, Mode::ABS, Mode::ABS, Mode::ABS, /*80*/ - /*90*/ Mode::REL, Mode::INX, Mode::IMP, Mode::INX, Mode::ZPX, Mode::ZPX, Mode::ZPY, Mode::ZPY, Mode::IMP, Mode::ABY, Mode::IMP, Mode::ABY, Mode::ABX, Mode::ABX, Mode::ABY, Mode::ABY, /*90*/ - /*A0*/ Mode::IMM, Mode::IDX, Mode::IMM, Mode::IDX, Mode::ZPG, Mode::ZPG, Mode::ZPG, Mode::ZPG, Mode::IMP, Mode::IMM, Mode::IMP, Mode::IMM, Mode::ABS, Mode::ABS, Mode::ABS, Mode::ABS, /*A0*/ - /*B0*/ Mode::REL, Mode::INX, Mode::IMP, Mode::INX, Mode::ZPX, Mode::ZPX, Mode::ZPY, Mode::ZPY, Mode::IMP, Mode::ABY, Mode::IMP, Mode::ABY, Mode::ABX, Mode::ABX, Mode::ABY, Mode::ABY, /*B0*/ - /*C0*/ Mode::IMM, Mode::IDX, Mode::IMM, Mode::IDX, Mode::ZPG, Mode::ZPG, Mode::ZPG, Mode::ZPG, Mode::IMP, Mode::IMM, Mode::IMP, Mode::IMM, Mode::ABS, Mode::ABS, Mode::ABS, Mode::ABS, /*C0*/ - /*D0*/ Mode::REL, Mode::INX, Mode::IMP, Mode::INX, Mode::ZPX, Mode::ZPX, Mode::ZPX, Mode::ZPX, Mode::IMP, Mode::ABY, Mode::IMP, Mode::ABY, Mode::ABX, Mode::ABX, Mode::ABX, Mode::ABX, /*D0*/ - /*E0*/ Mode::IMM, Mode::IDX, Mode::IMM, Mode::IDX, Mode::ZPG, Mode::ZPG, Mode::ZPG, Mode::ZPG, Mode::IMP, Mode::IMM, Mode::IMP, Mode::IMM, Mode::ABS, Mode::ABS, Mode::ABS, Mode::ABS, /*E0*/ - /*F0*/ Mode::REL, Mode::INX, Mode::IMP, Mode::INX, Mode::ZPX, Mode::ZPX, Mode::ZPX, Mode::ZPX, Mode::IMP, Mode::ABY, Mode::IMP, Mode::ABY, Mode::ABX, Mode::ABX, Mode::ABX, Mode::ABX, /*F0*/ + // 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F + /*00*/ + Mode::IMP, + Mode::IDX, + Mode::IMP, + Mode::IDX, + Mode::ZPG, + Mode::ZPG, + Mode::ZPG, + Mode::ZPG, + Mode::IMP, + Mode::IMM, + Mode::ACC, + Mode::IMM, + Mode::ABS, + Mode::ABS, + Mode::ABS, + Mode::ABS, /*00*/ + /*10*/ Mode::REL, + Mode::INX, + Mode::IMP, + Mode::INX, + Mode::ZPX, + Mode::ZPX, + Mode::ZPX, + Mode::ZPX, + Mode::IMP, + Mode::ABY, + Mode::IMP, + Mode::ABY, + Mode::ABX, + Mode::ABX, + Mode::ABX, + Mode::ABX, /*10*/ + /*20*/ Mode::ABS, + Mode::IDX, + Mode::IMP, + Mode::IDX, + Mode::ZPG, + Mode::ZPG, + Mode::ZPG, + Mode::ZPG, + Mode::IMP, + Mode::IMM, + Mode::ACC, + Mode::IMM, + Mode::ABS, + Mode::ABS, + Mode::ABS, + Mode::ABS, /*20*/ + /*30*/ Mode::REL, + Mode::INX, + Mode::IMP, + Mode::INX, + Mode::ZPX, + Mode::ZPX, + Mode::ZPX, + Mode::ZPX, + Mode::IMP, + Mode::ABY, + Mode::IMP, + Mode::ABY, + Mode::ABX, + Mode::ABX, + Mode::ABX, + Mode::ABX, /*30*/ + /*40*/ Mode::IMP, + Mode::IDX, + Mode::IMP, + Mode::IDX, + Mode::ZPG, + Mode::ZPG, + Mode::ZPG, + Mode::ZPG, + Mode::IMP, + Mode::IMM, + Mode::ACC, + Mode::IMM, + Mode::ABS, + Mode::ABS, + Mode::ABS, + Mode::ABS, /*40*/ + /*50*/ Mode::REL, + Mode::INX, + Mode::IMP, + Mode::INX, + Mode::ZPX, + Mode::ZPX, + Mode::ZPX, + Mode::ZPX, + Mode::IMP, + Mode::ABY, + Mode::IMP, + Mode::ABY, + Mode::ABX, + Mode::ABX, + Mode::ABX, + Mode::ABX, /*50*/ + /*60*/ Mode::IMP, + Mode::IDX, + Mode::IMP, + Mode::IDX, + Mode::ZPG, + Mode::ZPG, + Mode::ZPG, + Mode::ZPG, + Mode::IMP, + Mode::IMM, + Mode::ACC, + Mode::IMM, + Mode::IND, + Mode::ABS, + Mode::ABS, + Mode::ABS, /*60*/ + /*70*/ Mode::REL, + Mode::INX, + Mode::IMP, + Mode::INX, + Mode::ZPX, + Mode::ZPX, + Mode::ZPX, + Mode::ZPX, + Mode::IMP, + Mode::ABY, + Mode::IMP, + Mode::ABY, + Mode::ABX, + Mode::ABX, + Mode::ABX, + Mode::ABX, /*70*/ + /*80*/ Mode::IMM, + Mode::IDX, + Mode::IMM, + Mode::IDX, + Mode::ZPG, + Mode::ZPG, + Mode::ZPG, + Mode::ZPG, + Mode::IMP, + Mode::IMM, + Mode::IMP, + Mode::IMM, + Mode::ABS, + Mode::ABS, + Mode::ABS, + Mode::ABS, /*80*/ + /*90*/ Mode::REL, + Mode::INX, + Mode::IMP, + Mode::INX, + Mode::ZPX, + Mode::ZPX, + Mode::ZPY, + Mode::ZPY, + Mode::IMP, + Mode::ABY, + Mode::IMP, + Mode::ABY, + Mode::ABX, + Mode::ABX, + Mode::ABY, + Mode::ABY, /*90*/ + /*A0*/ Mode::IMM, + Mode::IDX, + Mode::IMM, + Mode::IDX, + Mode::ZPG, + Mode::ZPG, + Mode::ZPG, + Mode::ZPG, + Mode::IMP, + Mode::IMM, + Mode::IMP, + Mode::IMM, + Mode::ABS, + Mode::ABS, + Mode::ABS, + Mode::ABS, /*A0*/ + /*B0*/ Mode::REL, + Mode::INX, + Mode::IMP, + Mode::INX, + Mode::ZPX, + Mode::ZPX, + Mode::ZPY, + Mode::ZPY, + Mode::IMP, + Mode::ABY, + Mode::IMP, + Mode::ABY, + Mode::ABX, + Mode::ABX, + Mode::ABY, + Mode::ABY, /*B0*/ + /*C0*/ Mode::IMM, + Mode::IDX, + Mode::IMM, + Mode::IDX, + Mode::ZPG, + Mode::ZPG, + Mode::ZPG, + Mode::ZPG, + Mode::IMP, + Mode::IMM, + Mode::IMP, + Mode::IMM, + Mode::ABS, + Mode::ABS, + Mode::ABS, + Mode::ABS, /*C0*/ + /*D0*/ Mode::REL, + Mode::INX, + Mode::IMP, + Mode::INX, + Mode::ZPX, + Mode::ZPX, + Mode::ZPX, + Mode::ZPX, + Mode::IMP, + Mode::ABY, + Mode::IMP, + Mode::ABY, + Mode::ABX, + Mode::ABX, + Mode::ABX, + Mode::ABX, /*D0*/ + /*E0*/ Mode::IMM, + Mode::IDX, + Mode::IMM, + Mode::IDX, + Mode::ZPG, + Mode::ZPG, + Mode::ZPG, + Mode::ZPG, + Mode::IMP, + Mode::IMM, + Mode::IMP, + Mode::IMM, + Mode::ABS, + Mode::ABS, + Mode::ABS, + Mode::ABS, /*E0*/ + /*F0*/ Mode::REL, + Mode::INX, + Mode::IMP, + Mode::INX, + Mode::ZPX, + Mode::ZPX, + Mode::ZPX, + Mode::ZPX, + Mode::IMP, + Mode::ABY, + Mode::IMP, + Mode::ABY, + Mode::ABX, + Mode::ABX, + Mode::ABX, + Mode::ABX, /*F0*/ ], }; cpu.pc = ((cpu.read(RESET_VECTOR + 1) as usize) << 8) + cpu.read(RESET_VECTOR) as usize; cpu } - pub fn step(&mut self) -> u64 { + pub fn soft_reset(&mut self) { + self.pc = ((self.read(RESET_VECTOR + 1) as usize) << 8) + self.read(RESET_VECTOR) as usize; + } + pub fn step(&mut self) -> u64 { // The CPU is stalled for up to 4 CPU cycles to allow the longest possible write (the return address and write after an IRQ) to finish. // If OAM DMA is in progress, it is paused for two cycles. The sample fetch always occurs on an even CPU cycle due to its alignment with the APU. // Specific delay cases: @@ -152,7 +655,7 @@ impl Cpu { self.delay = 3; // TODO: not correct self.apu.dmc.cpu_stall = false; } - + // skip cycles from OAM DMA if necessary if self.delay > 0 { self.delay -= 1; @@ -163,8 +666,9 @@ impl Cpu { // back up clock so we know how many cycles we complete let clock = self.clock; - let opcode = ::from(self.read(self.pc)); + let opcode = ::from(self.read(self.pc)); + self.addresses_fetched.insert(self.pc); // get addressing mode let mode = self.mode_table[opcode].clone(); let (address_func, _num_bytes) = mode.get(); @@ -181,12 +685,15 @@ impl Cpu { // memory interface pub fn read(&mut self, address: usize) -> u8 { + // self.addresses_fetched.insert(address); let val = match address { 0x0000..=0x1FFF => self.mem[address % 0x0800], 0x2000..=0x3FFF => self.read_ppu_reg(address % 8), - 0x4014 => self.read_ppu_reg(8), - 0x4015 => self.apu.read_status(), - 0x4016 => self.read_controller(), + 0x4014 => self.read_ppu_reg(8), + 0x4015 => self.apu.read_status(), + 0x4016 => self.read_controller(), + // FIXME: Uncomment for player 2.. + // 0x4017 => self.read_controller2(), 0x4000..=0x4017 => 0, // can't read from these APU registers 0x4018..=0x401F => 0, // APU and I/O functionality that is normally disabled. See CPU Test Mode. 0x4020..=0xFFFF => self.mapper.borrow().read(address), @@ -197,11 +704,12 @@ impl Cpu { // memory interface fn write(&mut self, address: usize, val: u8) { + // self.addresses_fetched.insert(address); match address { 0x0000..=0x1FFF => self.mem[address % 0x0800] = val, 0x2000..=0x3FFF => self.write_ppu_reg(address % 8, val), - 0x4014 => self.write_ppu_reg(8, val), - 0x4016 => self.write_controller(val), + 0x4014 => self.write_ppu_reg(8, val), + 0x4016 => self.write_controller(val), 0x4000..=0x4017 => self.apu.write_reg(address, val), 0x4018..=0x401F => (), // APU and I/O functionality that is normally disabled. See CPU Test Mode. 0x4020..=0xFFFF => self.mapper.borrow_mut().write(address, val), @@ -211,7 +719,7 @@ impl Cpu { fn read_controller(&mut self) -> u8 { let bit = match self.button_number < 8 { - true => (self.button_states & (1< (self.button_states & (1 << self.button_number) != 0) as u8, false => 1, }; if self.strobe & 1 != 0 { @@ -219,7 +727,20 @@ impl Cpu { } else { self.button_number += 1; } - bit | 0x40 + bit + } + + fn read_controller2(&mut self) -> u8 { + let bit = match self.button_number < 8 { + true => (self.button_states2 & (1 << self.button_number) != 0) as u8, + false => 1, + }; + if self.strobe & 1 != 0 { + self.button_number = 0; + } else { + self.button_number += 1; + } + bit } fn write_controller(&mut self, val: u8) { @@ -256,8 +777,8 @@ impl Cpu { } self.ppu.write_oam_dma(data); let is_odd = self.clock % 2 != 0; - self.delay = 513 + if is_odd {1} else {0}; - }, + self.delay = 513 + if is_odd { 1 } else { 0 }; + } _ => panic!("wrote to bad ppu reg: {}", reg_num), } } @@ -278,7 +799,7 @@ impl Cpu { self.irq(); } // TODO: should checks for APU and MMC3 IRQs be combined and acknowledged together? - + // At any time, if the interrupt flag is set, the CPU's IRQ line is continuously asserted until the interrupt flag is cleared. // The processor will continue on from where it was stalled. if self.apu.dmc.interrupt { @@ -291,19 +812,27 @@ impl Cpu { let operands = match num_bytes { 1 => " ".to_string(), 2 => format!("{:02X} ", self.read(pc + 1)), - 3 => format!("{:02X} {:02X}", self.read(pc + 1), self.read(pc+2)), + 3 => format!("{:02X} {:02X}", self.read(pc + 1), self.read(pc + 2)), _ => "error".to_string(), }; - println!("{:04X} {:02X} {} {} A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X}", - pc, self.read(pc), operands, _OPCODE_DISPLAY_NAMES[opcode], - self.a, self.x, self.y, self.p, self.s, + println!( + "{:04X} {:02X} {} {} A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X}", + pc, + self.read(pc), + operands, + _OPCODE_DISPLAY_NAMES[opcode], + self.a, + self.x, + self.y, + self.p, + self.s, ); } pub fn _memory_at(&mut self, address: usize, amount: usize) -> Vec { let mut ret = vec![]; for i in 0..amount { - ret.push(self.read(address+i)); + ret.push(self.read(address + i)); } ret } @@ -324,36 +853,24 @@ $4020-$FFFF $BFE0 Cartridge space: PRG ROM, PRG RAM, and mapper registers (See // For debug output const _OPCODE_DISPLAY_NAMES: [&str; 256] = [ - "BRK", "ORA", "BAD", "SLO", "NOP", "ORA", "ASL", "SLO", - "PHP", "ORA", "ASL", "ANC", "NOP", "ORA", "ASL", "SLO", - "BPL", "ORA", "BAD", "SLO", "NOP", "ORA", "ASL", "SLO", - "CLC", "ORA", "NOP", "SLO", "NOP", "ORA", "ASL", "SLO", - "JSR", "AND", "BAD", "RLA", "BIT", "AND", "ROL", "RLA", - "PLP", "AND", "ROL", "ANC", "BIT", "AND", "ROL", "RLA", - "BMI", "AND", "BAD", "RLA", "NOP", "AND", "ROL", "RLA", - "SEC", "AND", "NOP", "RLA", "NOP", "AND", "ROL", "RLA", - "RTI", "EOR", "BAD", "SRE", "NOP", "EOR", "LSR", "SRE", - "PHA", "EOR", "LSR", "ALR", "JMP", "EOR", "LSR", "SRE", - "BVC", "EOR", "BAD", "SRE", "NOP", "EOR", "LSR", "SRE", - "CLI", "EOR", "NOP", "SRE", "NOP", "EOR", "LSR", "SRE", - "RTS", "ADC", "BAD", "RRA", "NOP", "ADC", "ROR", "RRA", - "PLA", "ADC", "ROR", "ARR", "JMP", "ADC", "ROR", "RRA", - "BVS", "ADC", "BAD", "RRA", "NOP", "ADC", "ROR", "RRA", - "SEI", "ADC", "NOP", "RRA", "NOP", "ADC", "ROR", "RRA", - "NOP", "STA", "NOP", "SAX", "STY", "STA", "STX", "SAX", - "DEY", "NOP", "TXA", "XAA", "STY", "STA", "STX", "SAX", - "BCC", "STA", "BAD", "AHX", "STY", "STA", "STX", "SAX", - "TYA", "STA", "TXS", "TAS", "SHY", "STA", "SHX", "AHX", - "LDY", "LDA", "LDX", "LAX", "LDY", "LDA", "LDX", "LAX", - "TAY", "LDA", "TAX", "LAX", "LDY", "LDA", "LDX", "LAX", - "BCS", "LDA", "BAD", "LAX", "LDY", "LDA", "LDX", "LAX", - "CLV", "LDA", "TSX", "LAS", "LDY", "LDA", "LDX", "LAX", - "CPY", "CMP", "NOP", "DCP", "CPY", "CMP", "DEC", "DCP", - "INY", "CMP", "DEX", "AXS", "CPY", "CMP", "DEC", "DCP", - "BNE", "CMP", "BAD", "DCP", "NOP", "CMP", "DEC", "DCP", - "CLD", "CMP", "NOP", "DCP", "NOP", "CMP", "DEC", "DCP", - "CPX", "SBC", "NOP", "ISC", "CPX", "SBC", "INC", "ISC", - "INX", "SBC", "NOP", "SBC", "CPX", "SBC", "INC", "ISC", - "BEQ", "SBC", "BAD", "ISC", "NOP", "SBC", "INC", "ISC", - "SED", "SBC", "NOP", "ISC", "NOP", "SBC", "INC", "ISC", + "BRK", "ORA", "BAD", "SLO", "NOP", "ORA", "ASL", "SLO", "PHP", "ORA", "ASL", "ANC", "NOP", + "ORA", "ASL", "SLO", "BPL", "ORA", "BAD", "SLO", "NOP", "ORA", "ASL", "SLO", "CLC", "ORA", + "NOP", "SLO", "NOP", "ORA", "ASL", "SLO", "JSR", "AND", "BAD", "RLA", "BIT", "AND", "ROL", + "RLA", "PLP", "AND", "ROL", "ANC", "BIT", "AND", "ROL", "RLA", "BMI", "AND", "BAD", "RLA", + "NOP", "AND", "ROL", "RLA", "SEC", "AND", "NOP", "RLA", "NOP", "AND", "ROL", "RLA", "RTI", + "EOR", "BAD", "SRE", "NOP", "EOR", "LSR", "SRE", "PHA", "EOR", "LSR", "ALR", "JMP", "EOR", + "LSR", "SRE", "BVC", "EOR", "BAD", "SRE", "NOP", "EOR", "LSR", "SRE", "CLI", "EOR", "NOP", + "SRE", "NOP", "EOR", "LSR", "SRE", "RTS", "ADC", "BAD", "RRA", "NOP", "ADC", "ROR", "RRA", + "PLA", "ADC", "ROR", "ARR", "JMP", "ADC", "ROR", "RRA", "BVS", "ADC", "BAD", "RRA", "NOP", + "ADC", "ROR", "RRA", "SEI", "ADC", "NOP", "RRA", "NOP", "ADC", "ROR", "RRA", "NOP", "STA", + "NOP", "SAX", "STY", "STA", "STX", "SAX", "DEY", "NOP", "TXA", "XAA", "STY", "STA", "STX", + "SAX", "BCC", "STA", "BAD", "AHX", "STY", "STA", "STX", "SAX", "TYA", "STA", "TXS", "TAS", + "SHY", "STA", "SHX", "AHX", "LDY", "LDA", "LDX", "LAX", "LDY", "LDA", "LDX", "LAX", "TAY", + "LDA", "TAX", "LAX", "LDY", "LDA", "LDX", "LAX", "BCS", "LDA", "BAD", "LAX", "LDY", "LDA", + "LDX", "LAX", "CLV", "LDA", "TSX", "LAS", "LDY", "LDA", "LDX", "LAX", "CPY", "CMP", "NOP", + "DCP", "CPY", "CMP", "DEC", "DCP", "INY", "CMP", "DEX", "AXS", "CPY", "CMP", "DEC", "DCP", + "BNE", "CMP", "BAD", "DCP", "NOP", "CMP", "DEC", "DCP", "CLD", "CMP", "NOP", "DCP", "NOP", + "CMP", "DEC", "DCP", "CPX", "SBC", "NOP", "ISC", "CPX", "SBC", "INC", "ISC", "INX", "SBC", + "NOP", "SBC", "CPX", "SBC", "INC", "ISC", "BEQ", "SBC", "BAD", "ISC", "NOP", "SBC", "INC", + "ISC", "SED", "SBC", "NOP", "ISC", "NOP", "SBC", "INC", "ISC", ]; diff --git a/src/cpu/opcodes.rs b/src/cpu/opcodes.rs index c677a6e..d193433 100644 --- a/src/cpu/opcodes.rs +++ b/src/cpu/opcodes.rs @@ -1,15 +1,17 @@ -use super::{CARRY_FLAG, DECIMAL_FLAG, INTERRUPT_DISABLE_FLAG, IRQ_VECTOR, NEGATIVE_FLAG, NMI_VECTOR, OVERFLOW_FLAG, ZERO_FLAG, Mode}; +use super::{ + Mode, CARRY_FLAG, DECIMAL_FLAG, INTERRUPT_DISABLE_FLAG, IRQ_VECTOR, NEGATIVE_FLAG, NMI_VECTOR, + OVERFLOW_FLAG, ZERO_FLAG, +}; // TODO: check unofficial opcodes for page crosses impl super::Cpu { - pub fn adc(&mut self, _address: usize, _mode: Mode) { let byte = self.read(_address); - let carry_bit = if self.p & CARRY_FLAG == 0 {0} else {1}; + let carry_bit = if self.p & CARRY_FLAG == 0 { 0 } else { 1 }; let mut new_val = self.a.wrapping_add(byte); // add the byte at the _address to accum new_val = new_val.wrapping_add(carry_bit); // add carry flag to accumulator - // set carry flag if we wrapped around and added something + // set carry flag if we wrapped around and added something if new_val <= self.a && (byte != 0 || carry_bit != 0) { self.p |= CARRY_FLAG; } else { @@ -41,10 +43,10 @@ impl super::Cpu { _ => { self.clock += 2; self.read(_address) - }, + } }; // put top bit in carry flag - if val & (1<<7) != 0 { + if val & (1 << 7) != 0 { self.p |= CARRY_FLAG; } else { self.p &= 0xFF - CARRY_FLAG; @@ -292,7 +294,7 @@ impl super::Cpu { _ => { self.clock += 2; self.read(_address) - }, + } }; if val & 0x1 == 0x1 { self.p |= CARRY_FLAG; @@ -308,8 +310,7 @@ impl super::Cpu { self.set_negative_flag(val); } - pub fn nop(&mut self, _address: usize, _mode: Mode) { - } + pub fn nop(&mut self, _address: usize, _mode: Mode) {} pub fn ora(&mut self, _address: usize, _mode: Mode) { self.a |= self.read(_address); @@ -369,18 +370,21 @@ impl super::Cpu { _ => { self.clock += 2; self.read(_address) - }, + } }; - let carry_flag_bit = if self.p & CARRY_FLAG != 0 {1} else {0}; - let new_cfb = if val & 0x80 != 0 {1} else {0}; + let carry_flag_bit = if self.p & CARRY_FLAG != 0 { 1 } else { 0 }; + let new_cfb = if val & 0x80 != 0 { 1 } else { 0 }; val <<= 1; val += carry_flag_bit; match _mode { Mode::ACC => self.a = val, _ => self.write(_address, val), }; - if new_cfb != 0 { self.p |= CARRY_FLAG; } - else { self.p &= 0xFF - CARRY_FLAG; } + if new_cfb != 0 { + self.p |= CARRY_FLAG; + } else { + self.p &= 0xFF - CARRY_FLAG; + } self.set_zero_flag(val); self.set_negative_flag(val); } @@ -393,12 +397,15 @@ impl super::Cpu { self.read(_address) } }; - let cfb = if self.p & CARRY_FLAG != 0 {1} else {0}; + let cfb = if self.p & CARRY_FLAG != 0 { 1 } else { 0 }; let new_cfb = val & 0x1; val >>= 1; val += cfb * 0x80; - if new_cfb != 0 { self.p |= CARRY_FLAG; } - else { self.p &= 0xFF - CARRY_FLAG; } + if new_cfb != 0 { + self.p |= CARRY_FLAG; + } else { + self.p &= 0xFF - CARRY_FLAG; + } match _mode { Mode::ACC => self.a = val, _ => self.write(_address, val), @@ -433,7 +440,7 @@ impl super::Cpu { pub fn sbc(&mut self, _address: usize, _mode: Mode) { let byte = self.read(_address); - let carry_bit = if self.p & CARRY_FLAG == 0 {1} else {0}; + let carry_bit = if self.p & CARRY_FLAG == 0 { 1 } else { 0 }; let mut new_val = self.a.wrapping_sub(byte); new_val = new_val.wrapping_sub(carry_bit); // if overflow occurs and we subtracted something, CLEAR the carry bit @@ -552,5 +559,4 @@ impl super::Cpu { + (self.read(IRQ_VECTOR) as usize); // and low byte self.clock += 7; } - } diff --git a/src/cpu/serialize.rs b/src/cpu/serialize.rs index aa20fdb..50d8053 100644 --- a/src/cpu/serialize.rs +++ b/src/cpu/serialize.rs @@ -1,10 +1,10 @@ use super::Mode; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct CpuData { - mem: Vec, + pub(crate) mem: Vec, a: u8, x: u8, y: u8, @@ -15,13 +15,14 @@ pub struct CpuData { delay: usize, strobe: u8, button_states: u8, + button_states2: u8, button_number: u8, mode_table: Vec, } impl super::Cpu { pub fn save_state(&self) -> CpuData { - CpuData{ + CpuData { mem: self.mem.clone(), a: self.a, x: self.x, @@ -33,6 +34,7 @@ impl super::Cpu { delay: self.delay, strobe: self.strobe, button_states: self.button_states, + button_states2: self.button_states2, button_number: self.button_number, mode_table: self.mode_table.clone(), } @@ -50,6 +52,7 @@ impl super::Cpu { self.delay = data.delay; self.strobe = data.strobe; self.button_states = data.button_states; + self.button_states2 = data.button_states2; self.button_number = data.button_number; self.mode_table = data.mode_table; } diff --git a/src/cpu/utility.rs b/src/cpu/utility.rs index e59b08a..fd61b21 100644 --- a/src/cpu/utility.rs +++ b/src/cpu/utility.rs @@ -1,7 +1,6 @@ -use super::{CARRY_FLAG, NEGATIVE_FLAG, STACK_OFFSET, ZERO_FLAG, Mode}; +use super::{Mode, CARRY_FLAG, NEGATIVE_FLAG, STACK_OFFSET, ZERO_FLAG}; impl super::Cpu { - pub fn advance_pc(&mut self, mode: Mode) { self.pc += match mode { Mode::ABS => 3, @@ -25,13 +24,13 @@ impl super::Cpu { true => { let decoded_offset = offset as usize; self.pc += decoded_offset; - }, + } false => { // instr_test-v5/rom_singles/11-stack.nes: // letting decoded_offset be (-offset) as usize was allowing for overflow if offset was -128/0b10000000 let decoded_offset = (-offset) as u8; self.pc -= decoded_offset as usize; - }, + } } } @@ -62,7 +61,7 @@ impl super::Cpu { } else { self.p &= 0xFF - CARRY_FLAG; } - self.set_zero_flag(if reg == byte {0} else {1}); + self.set_zero_flag(if reg == byte { 0 } else { 1 }); let diff = reg.wrapping_sub(byte); self.set_negative_flag(diff); } @@ -93,5 +92,4 @@ impl super::Cpu { self.p &= 0xFF - ZERO_FLAG; } } - } diff --git a/src/fuzzing_state.rs b/src/fuzzing_state.rs new file mode 100644 index 0000000..139734e --- /dev/null +++ b/src/fuzzing_state.rs @@ -0,0 +1,63 @@ +use crate::apu::Apu; +use crate::cartridge::get_mapper; +use crate::cartridge::serialize::MapperData; +use crate::cpu::serialize::CpuData; +use crate::cpu::Cpu; +use crate::input::FuzzingInput; +use crate::ppu::serialize::PpuData; +use crate::ppu::Ppu; +use std::hash::{Hash, Hasher}; + +#[derive(Clone)] +pub struct FuzzingState { + pub(crate) cpu_state: CpuData, + ppu_state: PpuData, + mapper_state: MapperData, + pub(crate) frames: usize, +} + +impl FuzzingState { + pub fn save_state(cpu: Cpu, frames: usize) -> FuzzingState { + FuzzingState { + cpu_state: cpu.save_state(), + ppu_state: cpu.ppu.save_state(), + mapper_state: cpu.mapper.borrow().save_state(), + frames: frames, + } + } + + pub fn default(filename: String) -> FuzzingState { + let mapper = get_mapper(filename); + let ppu = Ppu::new(mapper.clone()); + let apu = Apu::new(); + let cpu = Cpu::new(mapper.clone(), ppu, apu); + return FuzzingState::save_state(cpu, 0); + } + + pub fn load_state(&self, filename: String) -> (Cpu, usize) { + let mapper = get_mapper(filename); + mapper.borrow_mut().load_state(self.mapper_state.clone()); + let mut ppu = Ppu::new(mapper.clone()); + ppu.load_state(self.ppu_state.clone()); + let apu = Apu::new(); + let mut cpu = Cpu::new(mapper.clone(), ppu, apu); + cpu.load_state(self.cpu_state.clone()); + return (cpu, self.frames); + } +} + +pub struct FuzzingInputState(pub FuzzingInput, pub FuzzingState); + +impl Hash for FuzzingInputState { + fn hash(&self, state: &mut H) { + return self.0.hash(state); + } +} + +impl PartialEq for FuzzingInputState { + fn eq(&self, other: &Self) -> bool { + self.0.eq(&other.0) + } +} + +impl Eq for FuzzingInputState {} diff --git a/src/input.rs b/src/input.rs index 53c2176..189b00b 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,25 +1,148 @@ -use sdl2::keyboard::Scancode; -use std::collections::HashSet; +use crate::{Rng, DISABLE_START_PRESSES_AFTER, MUTATION_RATE, MUTATION_RATE_SOFT_RESET}; +use std::fs::File; +use std::io; +use std::io::BufRead; -pub fn poll_buttons(strobe: &u8, event_pump: &sdl2::EventPump) -> Option { - if *strobe & 1 == 1 { - let mut button_states = 0; - let pressed_keys: HashSet = event_pump.keyboard_state().pressed_scancodes().collect(); - for key in pressed_keys.iter() { - match key { - Scancode::D => button_states |= 1 << 0, // A - Scancode::F => button_states |= 1 << 1, // B - Scancode::RShift => button_states |= 1 << 2, // Select - Scancode::Return => button_states |= 1 << 3, // Start - Scancode::Up => button_states |= 1 << 4, // Up - Scancode::Down => button_states |= 1 << 5, // Down - Scancode::Left => button_states |= 1 << 6, // Left - Scancode::Right => button_states |= 1 << 7, // Right - _ => (), +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum ConsoleAction { + None, + SoftReset, +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct FrameInput { + pub console_action: ConsoleAction, + pub player_1_input: u8, + pub player_2_input: u8, +} + +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct FuzzingInput { + frames: Vec, + disable_start_after: usize, + pub(crate) mutated: bool, +} + +impl FuzzingInput { + pub fn disable_start_after(&mut self, frames: usize) { + self.disable_start_after = frames; + } + + pub fn mutate_from(&mut self, rng: &mut Rng, frame: usize, frames_to_consider: usize) { + self.mutated = true; + for frame_num in frame..(frame + frames_to_consider) { + while frame_num >= self.frames.len() { + self.frames.push(FrameInput { + console_action: ConsoleAction::None, + player_1_input: rng.next() as u8, + player_2_input: 0, + }); + } + + let check = (rng.next() % frames_to_consider as u32) as f64; + + if (check / frames_to_consider as f64) < MUTATION_RATE { + if frame_num < self.frames.len() { + self.frames[frame_num].player_1_input = rng.next() as u8; + } + } + + if (check / frames_to_consider as f64) < MUTATION_RATE_SOFT_RESET { + if frame_num < self.frames.len() { + self.frames[frame_num].console_action = ConsoleAction::SoftReset + } + } + + if frame_num > DISABLE_START_PRESSES_AFTER { + self.frames[frame_num].player_1_input &= 0b11110011; } } - Some(button_states) - } else { - None + } + + // Get the fuzzing input for the give frame... + pub fn get_frame_input(&self, frame: usize) -> Option<&FrameInput> { + self.frames.get(frame) + } + + // Hacky code to parse an fm2 movie/input file into our initial fuzzing input + pub fn load(fm2: &str) -> FuzzingInput { + let file = File::open(fm2).unwrap(); + let mut frames = vec![]; + let lines = io::BufReader::new(file).lines(); + for (_line_num, line) in lines.enumerate() { + if let Ok(lip) = line { + let ip = lip.as_bytes(); + if ip.is_empty() { + continue; + } + if ip[0] as char == '|' { + let mut frame_input = FrameInput { + console_action: ConsoleAction::None, + player_1_input: 0, + player_2_input: 0, + }; + if ip[1] as char == '1' { + frame_input.console_action = ConsoleAction::SoftReset; + } + // RLDUTSBA + if ip[3] as char == 'R' { + frame_input.player_1_input |= 128 + } + if ip[4] as char == 'L' { + frame_input.player_1_input |= 64 + } + if ip[5] as char == 'D' { + frame_input.player_1_input |= 32 + } + if ip[6] as char == 'U' { + frame_input.player_1_input |= 16 + } + if ip[7] as char == 'T' { + frame_input.player_1_input |= 8 + } + if ip[8] as char == 'S' { + frame_input.player_1_input |= 4 + } + if ip[9] as char == 'B' { + frame_input.player_1_input |= 2 + } + if ip[10] as char == 'A' { + frame_input.player_1_input |= 1 + } + // RLDUTSBA + if ip[12] as char == 'R' { + frame_input.player_2_input |= 128 + } + if ip[13] as char == 'L' { + frame_input.player_2_input |= 64 + } + if ip[14] as char == 'D' { + frame_input.player_2_input |= 32 + } + if ip[15] as char == 'U' { + frame_input.player_2_input |= 16 + } + if ip[16] as char == 'T' { + frame_input.player_2_input |= 8 + } + if ip[17] as char == 'S' { + frame_input.player_2_input |= 4 + } + if ip[18] as char == 'B' { + frame_input.player_2_input |= 2 + } + if ip[19] as char == 'A' { + frame_input.player_2_input |= 1 + } + frames.push(frame_input); + } + } + } + + FuzzingInput { + frames, + disable_start_after: 0xFFFFFFFF, + mutated: false, + } } } diff --git a/src/main.rs b/src/main.rs index 4170186..dffb488 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,255 +1,319 @@ -mod cpu; -mod ppu; +#![feature(total_cmp)] + mod apu; -mod cartridge; -mod input; -mod screen; mod audio; +mod cartridge; +mod cpu; +mod fuzzing_state; +mod input; +mod ppu; +mod screen; mod state; -use cpu::Cpu; -use ppu::Ppu; use apu::Apu; -use cartridge::{check_signature, get_mapper}; -use input::poll_buttons; -use screen::{init_window, draw_pixel, draw_to_window}; -use state::{save_state, load_state, find_next_filename, find_last_save_state}; +use ppu::Ppu; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; -use std::time::{Instant, Duration}; +use crate::fuzzing_state::{FuzzingInputState, FuzzingState}; +use crate::input::{ConsoleAction, FuzzingInput}; +use crate::screen::Screen; +use minifb::{ScaleMode, Window, WindowOptions}; +use priority_queue::priority_queue::PriorityQueue; +use std::collections::HashSet; +use std::sync::mpsc::{Receiver, Sender}; +use std::sync::{mpsc, Arc, Mutex}; +use std::thread; -use sdl2::Sdl; -use sdl2::render::{Canvas, Texture}; -use sdl2::keyboard::Keycode; -use sdl2::EventPump; -use sdl2::event::Event; -use sdl2::pixels::PixelFormatEnum; -use sdl2::video::Window; -use sdl2::messagebox::*; +// The number of cpu instances to spawn.. +const NUM_THREADS: usize = 28; -// use cpuprofiler::PROFILER; +// The number of frames to fuzz and process +// A small number exploits the current point more at the expense of +// large exploration - and vice versa. +const FRAMES_TO_CONSIDER: usize = 400; -enum GameExitMode { - QuitApplication, - NewGame(String), - Reset, - Nothing, -} +// Same input should generate the same output... +// (I make no guarantee of that at the moment) +const RNG_SEED: u32 = 0x5463753; + +// If set to a low number, this disables start presses after the given frame +// Useful for some games where pausing does nothing to advance the game... +const DISABLE_START_PRESSES_AFTER: usize = 50; + +// The rate at which seed inputs become corrupted.. +const MUTATION_RATE: f64 = 0.1; + +// The rate at which seed inputs may become soft resets.. +const MUTATION_RATE_SOFT_RESET: f64 = 0.000; fn main() -> Result<(), String> { - // Set up screen - let sdl_context = sdl2::init()?; - let mut event_pump = sdl_context.event_pump()?; - let (mut canvas, texture_creator) = init_window(&sdl_context).expect("Could not create window"); - let mut texture = texture_creator.create_texture_streaming( - PixelFormatEnum::RGB24, 256*screen::SCALE_FACTOR as u32, 240*screen::SCALE_FACTOR as u32) - .map_err(|e| e.to_string())?; - let byte_width = 256 * 3 * screen::SCALE_FACTOR; // 256 NES pixels, 3 bytes for each pixel (RGB 24-bit), and NES-to-SDL scale factor - let byte_height = 240 * screen::SCALE_FACTOR; // NES image is 240 pixels tall, multiply by scale factor for total number of rows needed - let mut screen_buffer = vec![0; byte_width * byte_height]; // contains raw RGB data for the screen - let argv = std::env::args().collect::>(); - let mut filename = if argv.len() > 1 { - argv[1].to_string() - } else { - show_simple_message_box( - MessageBoxFlag::INFORMATION, "Welcome to Nestur!", INSTRUCTIONS, canvas.window() - ).map_err(|e| e.to_string())?; - let name; - 'waiting: loop { - for event in event_pump.poll_iter() { - match event { - Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. } - => return Ok(()), - Event::DropFile{ filename: f, .. } => { - match check_signature(&f) { - Ok(()) => { - name = f; - break 'waiting; - }, - Err(e) => println!("{}", e), - } - }, - _ => (), - } - } - std::thread::sleep(Duration::from_millis(100)); - } - name - }; + + if argv.len() < 3 { + println!("usage: fuzz ") + } + + // The rom file...mostly tested with Super Mario Bros + let rom_filename = argv[1].to_string(); + // The tas file to use as seed input e.g. happylee-supermariobros,warped.fm2 + let fm2_file = argv[2].to_string(); + + // Seed for our Random Number Generator... + let mut rng = Rng::init(RNG_SEED); + + // Mutex for launching windows others we get double frees in the + // windowing library...(yup) + let mutex = Arc::new(Mutex::new(0)); + + let mut seed_input = FuzzingInput::load(fm2_file.as_str()); + seed_input.disable_start_after(DISABLE_START_PRESSES_AFTER); + + let mut fuzzing_queue: PriorityQueue = PriorityQueue::new(); + fuzzing_queue.push( + FuzzingInputState( + seed_input.clone(), + FuzzingState::default(rom_filename.clone()), + ), + 0, + ); + for _i in 1..NUM_THREADS { + let mut mutated_input = seed_input.clone(); + mutated_input.mutate_from(&mut rng, 0, FRAMES_TO_CONSIDER); + fuzzing_queue.push( + FuzzingInputState( + mutated_input.clone(), + FuzzingState::default(rom_filename.clone()), + ), + 0, + ); + } + + let mut result_channels = vec![]; + for i in 0..NUM_THREADS { + let i = i.clone(); + let mutex = mutex.clone(); + let rom_filename = rom_filename.clone(); + let (tx_inputs, rx_input): ( + Sender<(FuzzingInput, FuzzingState)>, + Receiver<(FuzzingInput, FuzzingState)>, + ) = mpsc::channel(); + let (tx_results, rx_results): ( + Sender<(FuzzingInput, FuzzingState)>, + Receiver<(FuzzingInput, FuzzingState)>, + ) = mpsc::channel(); + result_channels.push((tx_inputs, rx_results)); + thread::spawn(move || { + run_game(i as isize, mutex, rom_filename, rx_input, tx_results); + }); + } + + let mut previous_states: HashSet> = HashSet::new(); + previous_states.insert(vec![0; 8192]); + loop { - let res = run_game(&sdl_context, &mut event_pump, &mut screen_buffer, &mut canvas, &mut texture, &filename); - match res { - Ok(Some(GameExitMode::Reset)) => (), - Ok(Some(GameExitMode::NewGame(next_file))) => filename = next_file, - Ok(None) | Ok(Some(GameExitMode::QuitApplication)) => return Ok(()), - Err(e) => return Err(e), - Ok(Some(GameExitMode::Nothing)) => panic!("shouldn't have returned exit mode Nothing to main()"), + println!("Prospective Cases: {}", fuzzing_queue.len()); + + let mut temp_scores = vec![]; + for i in 0..NUM_THREADS { + if fuzzing_queue.is_empty() == false { + let (state, score) = fuzzing_queue.pop().unwrap(); + temp_scores.push(score); + // Send to thread for process... + result_channels[i] + .0 + .send((state.0.clone(), state.1.clone())) + .expect("error sending new fuzzing input to thread..."); + } else { + result_channels[i] + .0 + .send(( + seed_input.clone(), + FuzzingState::default(rom_filename.clone()), + )) + .expect("error sending new fuzzing input to thread"); + } } + + // Gather results from each thread + for i in 0..NUM_THREADS { + match result_channels[i].1.recv() { + Ok((fuzzing_input, fuzzing_state)) => { + let mut lowest_similarity = u64::MAX; + for (_j, state) in previous_states.iter().enumerate() { + let similiarty = hamming::distance( + state.as_slice(), + fuzzing_state.cpu_state.mem.as_slice(), + ); + // println!("{} {} {} ",i,j,similiarty); + if similiarty < lowest_similarity { + lowest_similarity = similiarty + } + } + + previous_states.insert(fuzzing_state.cpu_state.mem.clone()); + + // Seed Thread + if fuzzing_input.mutated == false { + // Seed input handling... + fuzzing_queue.push( + FuzzingInputState(fuzzing_input.clone(), fuzzing_state.clone()), + 10000, + ); + + // Add 10 random cases.. + for _i in 0..10 { + let mut mutated_input: FuzzingInput = fuzzing_input.clone(); + mutated_input.mutate_from( + &mut rng, + fuzzing_state.frames, + FRAMES_TO_CONSIDER, + ); + // Add the mutated input and the regular input to the queue... + fuzzing_queue.push( + FuzzingInputState(mutated_input.clone(), fuzzing_state.clone()), + lowest_similarity, + ); + } + } else if lowest_similarity > 0 { + // Only add to the queue if strictly better... + fuzzing_queue.push( + FuzzingInputState(fuzzing_input.clone(), fuzzing_state.clone()), + lowest_similarity, + ); + + let mut mutated_input: FuzzingInput = fuzzing_input.clone(); + mutated_input.mutate_from( + &mut rng, + fuzzing_state.frames, + FRAMES_TO_CONSIDER, + ); + // Add the mutated input and the regular input to the queue... + fuzzing_queue.push( + FuzzingInputState(mutated_input.clone(), fuzzing_state.clone()), + lowest_similarity, + ); + } + } + _ => {} + } + } + } +} + +pub struct Rng { + state: u32, +} + +impl Rng { + pub fn init(state: u32) -> Rng { + Rng { state } + } + + pub fn next(&mut self) -> u32 { + self.state ^= self.state << 13; + self.state ^= self.state >> 17; + self.state ^= self.state << 5; + return self.state; } } fn run_game( - sdl_context: &Sdl, - event_pump: &mut EventPump, - screen_buffer: &mut Vec, - canvas: &mut Canvas, - texture: &mut Texture, - filename: &str - ) -> Result, String> { + i: isize, + mutex: Arc>, + filename: String, + refresh_inputs: Receiver<(FuzzingInput, FuzzingState)>, + send_results: Sender<(FuzzingInput, FuzzingState)>, +) -> FuzzingState { + let mut window = match mutex.lock() { + Ok(_x) => { + let mut window = Window::new( + "NesFuzz", + 256, + 240, + WindowOptions { + resize: true, + borderless: true, + scale_mode: ScaleMode::UpperLeft, + ..WindowOptions::default() + }, + ) + .unwrap_or_else(|x| panic!("Unable to create window {}", x)); - println!("loading game {}", filename); - - // Set up audio - let mut temp_buffer = vec![]; // receives one sample each time the APU ticks. this is a staging buffer so we don't have to lock the mutex too much. - let apu_buffer = Arc::new(Mutex::new(Vec::::new())); // stays in this thread, receives raw samples between frames - let sdl_buffer = Arc::clone(&apu_buffer); // used in audio device's callback to select the samples it needs - let audio_device = audio::initialize(sdl_context, sdl_buffer).expect("Could not create audio device"); - let mut half_cycle = false; - let mut audio_started = false; - - // Initialize hardware components - let filepath = Path::new(filename).to_path_buf(); - let mapper = get_mapper(filename.to_string()); - let ppu = Ppu::new(mapper.clone()); - let apu = Apu::new(); - let mut cpu = Cpu::new(mapper.clone(), ppu, apu); - - // For throttling to 60 FPS - let mut timer = Instant::now(); - let mut fps_timer = Instant::now(); - let mut fps = 0; - - // PROFILER.lock().unwrap().start("./main.profile").unwrap(); - 'running: loop { - // step CPU: perform 1 cpu instruction, getting back number of clock cycles it took - let cpu_cycles = cpu.step(); - // clock APU every other CPU cycle - let mut apu_cycles = cpu_cycles / 2; - if cpu_cycles & 1 == 1 { // if cpu step took an odd number of cycles - if half_cycle { // and we have a half-cycle stored - apu_cycles += 1; // use it - half_cycle = false; - } else { - half_cycle = true; // or save it for next odd cpu step - } + window.set_position(10 + ((i % 7) * 260), (240 * (i / 7)) + 100); + window } - for _ in 0..apu_cycles { - // can't read CPU from APU so have to pass byte in here - let sample_byte = cpu.read(cpu.apu.dmc.current_address); - temp_buffer.push(cpu.apu.clock(sample_byte)); + _ => { + panic!("unable to acquire mutex") } - // clock PPU three times for every CPU cycle - for _ in 0..cpu_cycles * 3 { - let (pixel, end_of_frame) = cpu.ppu.clock(); - match pixel { - Some((x, y, color)) => draw_pixel(screen_buffer, x, y, color), - None => (), - }; - if end_of_frame { - fps += 1; // keep track of how many frames we've rendered this second - draw_to_window(texture, canvas, &screen_buffer)?; // draw the buffer to the window with SDL - let mut b = apu_buffer.lock().unwrap(); // unlock mutex to the real buffer - b.append(&mut temp_buffer); // send this frame's audio data, emptying the temp buffer - if !audio_started { - audio_started = true; - audio_device.resume(); - } - let now = Instant::now(); - // if we're running faster than 60Hz, kill time - if now < timer + Duration::from_millis(1000/60) { - std::thread::sleep(timer + Duration::from_millis(1000/60) - now); - } - timer = Instant::now(); - let outcome = process_events(event_pump, &filepath, &mut cpu); - match outcome { - GameExitMode::QuitApplication => break 'running, - GameExitMode::Reset => return Ok(Some(GameExitMode::Reset)), - GameExitMode::NewGame(g) => return Ok(Some(GameExitMode::NewGame(g))), - GameExitMode::Nothing => (), - } - } - } - // handle keyboard events - match poll_buttons(&cpu.strobe, &event_pump) { - Some(button_states) => cpu.button_states = button_states, - None => (), - }; - // calculate fps - let now = Instant::now(); - if now > fps_timer + Duration::from_secs(1) { - println!("frames per second: {}", fps); - fps = 0; - fps_timer = now; - } - } - // PROFILER.lock().unwrap().stop().unwrap(); - mapper.borrow().save_battery_backed_ram(); - Ok(None) -} + }; -fn process_events(event_pump: &mut EventPump, filepath: &PathBuf, cpu: &mut Cpu) -> GameExitMode { - for event in event_pump.poll_iter() { - match event { - Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. } - => return GameExitMode::QuitApplication, - Event::KeyDown{ keycode: Some(Keycode::F2), .. } - => return GameExitMode::Reset, - Event::KeyDown{ keycode: Some(Keycode::F5), .. } => { - let save_file = find_next_filename(filepath, Some("dat")) - .expect("could not generate save state filename"); - let res: Result<(), String> = save_state(cpu, &save_file) - .or_else(|e| {println!("{}", e); Ok(())}); - res.unwrap(); - }, - Event::KeyDown{ keycode: Some(Keycode::F9), .. } => { - match find_last_save_state(filepath, Some("dat")) { - Some(p) => { - let res: Result<(), String> = load_state(cpu, &p) - .or_else(|e| { println!("{}", e); Ok(()) } ); - res.unwrap(); - }, - None => println!("no save state found for {:?}", filepath) - } - }, - Event::DropFile{ timestamp: _t, window_id: _w, filename: f } => { - if f.len() > 4 && &f[f.len()-4..] == ".dat" { - let p = Path::new(&f).to_path_buf(); - let res: Result<(), String> = load_state(cpu, &p) - .or_else(|e| {println!("{}", e); Ok(())}); - res.unwrap(); - // } else if f.len() > 4 && &f[f.len()-4..] == ".nes" { - } else { - match check_signature(&f) { - Ok(()) => return GameExitMode::NewGame(f), - Err(e) => println!("{}", e), + let mut screen = Screen::new(); + + loop { + match refresh_inputs.recv() { + Err(_x) => {} + Ok((fuzzing_input, fuzzing_state)) => { + let mut new_frames = 0; + // Initialize hardware components + let (mut cpu, mut frames) = fuzzing_state.load_state(filename.clone()); + + while new_frames < FRAMES_TO_CONSIDER { + // step CPU: perform 1 cpu instruction, getting back number of clock cycles it took + let cpu_cycles = cpu.step(); + + // clock PPU three times for every CPU cycle + for _ in 0..cpu_cycles * 3 { + let (pixel, end_of_frame) = cpu.ppu.clock(); + match pixel { + Some((x, y, color)) => screen.plot_pixel( + x, + y, + color.0 as u32, + color.1 as u32, + color.2 as u32, + ), + None => (), + }; + if end_of_frame { + screen.draw_string(10, 210, &*format!("Frames: {}", new_frames)); + window + .update_with_buffer(&screen.display, 256, 240) + .unwrap(); + frames += 1; + new_frames += 1; + } } + + // Checking for Inputs... + + match fuzzing_input.get_frame_input(frames) { + Some(input) => { + match input.console_action { + ConsoleAction::None => {} + ConsoleAction::SoftReset => { + cpu.soft_reset(); + } + } + if cpu.strobe & 0x01 == 0x01 { + cpu.button_states = input.player_1_input; + // FIXME PLayer 2 doesn't play nicely with some games (e.g. mario) + // So to enable player 2 controls you also have to uncomment the + // bus in cpu/mod.rs + cpu.button_states2 = input.player_2_input; + } + } + _ => {} + }; } - }, - _ => (), + + send_results + .send((fuzzing_input.clone(), FuzzingState::save_state(cpu, frames))) + .expect("error sending result to thread"); + } } } - return GameExitMode::Nothing } -const INSTRUCTIONS: &str = "To play a game, drag an INES file (extension .nes) onto the main window. -To save the game state, press F5. To load the most recent save state, press F9. -To load another save state file, drag a .dat file onto the window while the game is running. -Battery-backed RAM saves (what the NES cartridges have) will be written to a .sav file if used. -To reset the console/current game, press F2. - -Controls ------------- -A: D -B: F -Start: enter -Select: (right) shift -Up/Down/Left/Right: arrow keys -"; - /* - TODO: - untangle CPU and APU/PPU? - better save file organization? diff --git a/src/ppu/cpu_registers.rs b/src/ppu/cpu_registers.rs index 5f734f6..7d1f2ee 100644 --- a/src/ppu/cpu_registers.rs +++ b/src/ppu/cpu_registers.rs @@ -1,27 +1,26 @@ impl super::Ppu { - // cpu writes to 0x2000, PPUCTRL pub fn write_controller(&mut self, byte: u8) { - // VRAM address increment per CPU read/write of PPUDATA - self.address_increment = match byte & (1<<2) == 0 { // (0: add 1, going across; 1: add 32, going down) + self.address_increment = match byte & (1 << 2) == 0 { + // (0: add 1, going across; 1: add 32, going down) true => 1, false => 32, }; // Sprite pattern table address for 8x8 sprites - self.sprite_pattern_table_base = match byte & (1<<3) == 0 { + self.sprite_pattern_table_base = match byte & (1 << 3) == 0 { true => 0x0, false => 0x1000, }; // Background pattern table address - self.background_pattern_table_base = match byte & (1<<4) == 0 { + self.background_pattern_table_base = match byte & (1 << 4) == 0 { true => 0x0, false => 0x1000, }; // Sprite size (0: 8x8 pixels; 1: 8x16 pixels) - self.sprite_size = if byte & (1<<5) != 0 { 16 } else {8}; + self.sprite_size = if byte & (1 << 5) != 0 { 16 } else { 8 }; // Ignoring PPU master/slave select for now - self.should_generate_nmi = byte & (1<<7) != 0; + self.should_generate_nmi = byte & (1 << 7) != 0; self.nmi_change(); // Take care of t set_bit(&mut self.t, 10, byte as u16, 0); @@ -30,22 +29,22 @@ impl super::Ppu { // cpu writes to 0x2001, PPUMASK pub fn write_mask(&mut self, byte: u8) { - self.grayscale = byte & (1<<0) != 0; - self.show_background_left = byte & (1<<1) != 0; - self.show_sprites_left = byte & (1<<2) != 0; - self.show_background = byte & (1<<3) != 0; - self.show_sprites = byte & (1<<4) != 0; - self.emphasize_red = byte & (1<<5) != 0; - self.emphasize_green = byte & (1<<6) != 0; - self.emphasize_blue = byte & (1<<7) != 0; + self.grayscale = byte & (1 << 0) != 0; + self.show_background_left = byte & (1 << 1) != 0; + self.show_sprites_left = byte & (1 << 2) != 0; + self.show_background = byte & (1 << 3) != 0; + self.show_sprites = byte & (1 << 4) != 0; + self.emphasize_red = byte & (1 << 5) != 0; + self.emphasize_green = byte & (1 << 6) != 0; + self.emphasize_blue = byte & (1 << 7) != 0; } // cpu reads ppu status from 0x2002, PPUSTATUS pub fn read_status(&mut self) -> u8 { let mut byte = self.recent_bits & 0b0001_1111; - byte |= if self.sprite_overflow { 0b0010_0000 } else {0}; - byte |= if self.sprite_zero_hit { 0b0100_0000 } else {0}; - byte |= if self.vertical_blank { 0b1000_0000 } else {0}; + byte |= if self.sprite_overflow { 0b0010_0000 } else { 0 }; + byte |= if self.sprite_zero_hit { 0b0100_0000 } else { 0 }; + byte |= if self.vertical_blank { 0b1000_0000 } else { 0 }; self.w = 0; self.vertical_blank = false; self.nmi_change(); @@ -71,16 +70,18 @@ impl super::Ppu { // cpu writes to 0x2005, PPUSCROLL pub fn write_scroll(&mut self, val: u8) { - match self.w { // first write + match self.w { + // first write 0 => { // t: ....... ...HGFED = d: HGFED... - self.t &= !((1<<5)-1); // turn off bottom 5 bits of t - self.t |= val as u16 >> 3; // set bottom 5 bits of t to top 5 bits of d - // x: CBA = d: .....CBA - self.x = val & ((1<<3) - 1); + self.t &= !((1 << 5) - 1); // turn off bottom 5 bits of t + self.t |= val as u16 >> 3; // set bottom 5 bits of t to top 5 bits of d + // x: CBA = d: .....CBA + self.x = val & ((1 << 3) - 1); self.w = 1; - }, - 1 => { // second write + } + 1 => { + // second write let d = val as u16; // t: CBA..HG FED..... = d: HGFEDCBA set_bit(&mut self.t, 0xC, d, 0x0); @@ -92,7 +93,7 @@ impl super::Ppu { set_bit(&mut self.t, 0x8, d, 0x6); set_bit(&mut self.t, 0x9, d, 0x7); self.w = 0; - }, + } _ => panic!("uh oh, somehow w was incremented past 1 to {}", self.w), } } @@ -100,7 +101,8 @@ impl super::Ppu { // cpu writes to 0x2006, PPUADDR pub fn write_address(&mut self, val: u8) { let d = val as u16; - match self.w { // first write + match self.w { + // first write 0 => { // t: .FEDCBA ........ = d: ..FEDCBA set_bit(&mut self.t, 0x8, d, 0x0); @@ -114,14 +116,15 @@ impl super::Ppu { // setting the 16th instead of the 15th bit in the line above was the longest, most frustrating oversight. // caused weird problems with vertical scrolling and backgrounds that I initially thought were bugs with MMC3. self.w = 1; - }, - 1 => { // second write + } + 1 => { + // second write // t: ....... HGFEDCBA = d: HGFEDCBA self.t &= 0xFF00; // mask off bottom byte self.t += d; // apply d self.v = self.t; // After t is updated, contents of t copied into v self.w = 0; - }, + } _ => panic!("uh oh, somehow w was incremented past 1 to {}", self.w), } } @@ -164,11 +167,11 @@ impl super::Ppu { 0x0000..=0x3EFF => { ret_val = self.read_buffer; self.read_buffer = mem_val; - }, + } 0x3F00..=0x3FFF => { ret_val = mem_val; self.read_buffer = self.read(self.v as usize - 0x1000); - }, + } _ => panic!("reading from invalid PPU address: 0x{:04x}", self.v), }; if self.rendering() && (self.scanline < 240 || self.scanline == 261) { @@ -201,10 +204,9 @@ impl super::Ppu { pub fn write_oam_dma(&mut self, data: Vec) { self.primary_oam = data; } - } pub fn set_bit(dest: &mut u16, dest_pos: usize, src: u16, src_pos: usize) { *dest &= 0xFFFF - (1 << dest_pos); // mask off destination bit - *dest += (if src & (1 << src_pos) == 0 {0} else {1}) << dest_pos; // apply bit from src in src position + *dest += (if src & (1 << src_pos) == 0 { 0 } else { 1 }) << dest_pos; // apply bit from src in src position } diff --git a/src/ppu/memory.rs b/src/ppu/memory.rs index ce32a61..b2490c3 100644 --- a/src/ppu/memory.rs +++ b/src/ppu/memory.rs @@ -1,7 +1,6 @@ use crate::cartridge::Mirror; impl super::Ppu { - pub fn read(&mut self, address: usize) -> u8 { match address { 0x0000..=0x1FFF => self.mapper.borrow().read(address), @@ -10,7 +9,7 @@ impl super::Ppu { let a = address % 0x0020; let value = self.palette_ram[a]; value - }, + } _ => 0, } } @@ -30,22 +29,22 @@ impl super::Ppu { 0x00 => { self.palette_ram[0] = value; self.palette_ram[0x10] = value; - }, + } 0x04 => { self.palette_ram[0x04] = value; self.palette_ram[0x14] = value; - }, + } 0x08 => { self.palette_ram[0x08] = value; self.palette_ram[0x18] = value; - }, + } 0x0C => { self.palette_ram[0x0C] = value; self.palette_ram[0x1C] = value; - }, + } _ => self.palette_ram[address % 0x0020] = value, } - }, + } _ => (), } } @@ -56,28 +55,22 @@ impl super::Ppu { match self.mapper.borrow().get_mirroring() { Mirror::LowBank => self.nametable_a[offset], Mirror::HighBank => self.nametable_b[offset], - Mirror::Horizontal => { - match base { - 0x0000..=0x07FF => self.nametable_a[offset], - 0x0800..=0x0FFF => self.nametable_b[offset], - _ => panic!("panicked reading nametable base: {}", base), - } + Mirror::Horizontal => match base { + 0x0000..=0x07FF => self.nametable_a[offset], + 0x0800..=0x0FFF => self.nametable_b[offset], + _ => panic!("panicked reading nametable base: {}", base), }, - Mirror::Vertical => { - match base { - 0x0000..=0x03FF | 0x0800..=0x0BFF => self.nametable_a[offset], - 0x0400..=0x07FF | 0x0C00..=0x0FFF => self.nametable_b[offset], - _ => panic!("panicked reading nametable base: {}", base), - } + Mirror::Vertical => match base { + 0x0000..=0x03FF | 0x0800..=0x0BFF => self.nametable_a[offset], + 0x0400..=0x07FF | 0x0C00..=0x0FFF => self.nametable_b[offset], + _ => panic!("panicked reading nametable base: {}", base), }, - Mirror::FourScreen => { - match base { - 0x0000..=0x03FF => self.nametable_a[offset], - 0x0400..=0x07FF => self.nametable_b[offset], - 0x0800..=0x0BFF => self.nametable_c[offset], - 0x0C00..=0x0FFF => self.nametable_d[offset], - _ => panic!("panicked reading nametable base: {}", base), - } + Mirror::FourScreen => match base { + 0x0000..=0x03FF => self.nametable_a[offset], + 0x0400..=0x07FF => self.nametable_b[offset], + 0x0800..=0x0BFF => self.nametable_c[offset], + 0x0C00..=0x0FFF => self.nametable_d[offset], + _ => panic!("panicked reading nametable base: {}", base), }, } } @@ -88,28 +81,22 @@ impl super::Ppu { match self.mapper.borrow().get_mirroring() { Mirror::LowBank => self.nametable_a[offset] = value, Mirror::HighBank => self.nametable_b[offset] = value, - Mirror::Horizontal => { - match base { - 0x0000..=0x07FF => self.nametable_a[offset] = value, - 0x0800..=0x0FFF => self.nametable_b[offset] = value, - _ => panic!("panicked writing nametable base: {}", base), - } + Mirror::Horizontal => match base { + 0x0000..=0x07FF => self.nametable_a[offset] = value, + 0x0800..=0x0FFF => self.nametable_b[offset] = value, + _ => panic!("panicked writing nametable base: {}", base), }, - Mirror::Vertical => { - match base { - 0x0000..=0x03FF | 0x0800..=0x0BFF => self.nametable_a[offset] = value, - 0x0400..=0x07FF | 0x0C00..=0x0FFF => self.nametable_b[offset] = value, - _ => panic!("panicked writing nametable base: {}", base), - } + Mirror::Vertical => match base { + 0x0000..=0x03FF | 0x0800..=0x0BFF => self.nametable_a[offset] = value, + 0x0400..=0x07FF | 0x0C00..=0x0FFF => self.nametable_b[offset] = value, + _ => panic!("panicked writing nametable base: {}", base), }, - Mirror::FourScreen => { - match base { - 0x0000..=0x03FF => self.nametable_a[offset] = value, - 0x0400..=0x07FF => self.nametable_b[offset] = value, - 0x0800..=0x0BFF => self.nametable_c[offset] = value, - 0x0C00..=0x0FFF => self.nametable_d[offset] = value, - _ => panic!("panicked writing nametable base: {}", base), - } + Mirror::FourScreen => match base { + 0x0000..=0x03FF => self.nametable_a[offset] = value, + 0x0400..=0x07FF => self.nametable_b[offset] = value, + 0x0800..=0x0BFF => self.nametable_c[offset] = value, + 0x0C00..=0x0FFF => self.nametable_d[offset] = value, + _ => panic!("panicked writing nametable base: {}", base), }, } } diff --git a/src/ppu/mod.rs b/src/ppu/mod.rs index f4c5fa4..364014c 100644 --- a/src/ppu/mod.rs +++ b/src/ppu/mod.rs @@ -1,16 +1,17 @@ mod cpu_registers; -mod rendering; mod memory; +mod rendering; pub mod serialize; +use crate::cartridge::Mapper; use std::cell::RefCell; use std::rc::Rc; -use crate::cartridge::Mapper; +#[derive(Clone)] pub struct Ppu { line_cycle: usize, // x coordinate - scanline: usize, // y coordinate - frame: usize, + scanline: usize, // y coordinate + frame: usize, // Internal registers v: u16, @@ -38,116 +39,116 @@ pub struct Ppu { palette_ram: Vec, // Palette RAM indexes. // Background pattern shift registers and latches - background_pattern_sr_low: u16, // 2 16-bit shift registers - These contain the pattern table data for two tiles. Every 8 cycles, the data for the next tile is loaded + background_pattern_sr_low: u16, // 2 16-bit shift registers - These contain the pattern table data for two tiles. Every 8 cycles, the data for the next tile is loaded background_pattern_sr_high: u16, // into the upper 8 bits of this shift register. Meanwhile, the pixel to render is fetched from one of the lower 8 bits. - nametable_byte: u8, // "The data fetched from these accesses is placed into internal latches, - attribute_table_byte: u8, // and then fed to the appropriate shift registers when it's time to do so - low_pattern_table_byte: u8, // (every 8 cycles)." - high_pattern_table_byte: u8, + nametable_byte: u8, // "The data fetched from these accesses is placed into internal latches, + attribute_table_byte: u8, // and then fed to the appropriate shift registers when it's time to do so + low_pattern_table_byte: u8, // (every 8 cycles)." + high_pattern_table_byte: u8, // Background palette shift registers and latches - background_palette_sr_low: u8, // 2 8-bit shift registers - - background_palette_sr_high: u8, // These contain the palette attributes for the lower 8 pixels of the 16-bit [pattern/tile] shift register. - background_palette_latch: u8, // These registers are fed by a latch which contains the palette attribute for the next tile. Every 8 cycles, + background_palette_sr_low: u8, // 2 8-bit shift registers - + background_palette_sr_high: u8, // These contain the palette attributes for the lower 8 pixels of the 16-bit [pattern/tile] shift register. + background_palette_latch: u8, // These registers are fed by a latch which contains the palette attribute for the next tile. Every 8 cycles, // the latch is loaded with the palette attribute for the next tile. Because the PPU can only fetch an attribute byte every 8 cycles, each // sequential string of 8 pixels is forced to have the same palette attribute. // Sprite memory, shift registers, and latch - pub primary_oam: Vec, // Primary OAM (holds 64 sprites for the frame) - secondary_oam: Vec, // Secondary OAM (holds 8 sprites for the current scanline) + pub primary_oam: Vec, // Primary OAM (holds 64 sprites for the frame) + secondary_oam: Vec, // Secondary OAM (holds 8 sprites for the current scanline) sprite_attribute_latches: Vec, // 8 latches - These contain the attribute bytes [palette data] for up to 8 sprites. - sprite_counters: Vec, // 8 counters - These contain the X positions for up to 8 sprites. - sprite_indexes: Vec, // Indexes of the sprites-in-the-attribute-latches' within primary OAM + sprite_counters: Vec, // 8 counters - These contain the X positions for up to 8 sprites. + sprite_indexes: Vec, // Indexes of the sprites-in-the-attribute-latches' within primary OAM sprite_pattern_table_srs: Vec<(u8, u8)>, // 8 pairs of 8-bit shift registers - These contain the pattern table data for up to 8 sprites, to be rendered on the current scanline. // Unused sprites are loaded with an all-transparent set of values. - num_sprites: usize, // Number of sprites in the shift registers for the current scanline + num_sprites: usize, // Number of sprites in the shift registers for the current scanline // Various flags set by registers - address_increment: u16, - sprite_pattern_table_base: 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, // 1: Show background in leftmost 8 pixels of screen, 0: Hide - show_sprites_left: bool, // 1: Show sprites in leftmost 8 pixels of screen, 0: Hide - show_background: bool, // 1: Show background - show_sprites: bool, // 1: Show sprites - emphasize_red: bool, // Emphasize red - emphasize_green: bool, // Emphasize green - emphasize_blue: bool, // Emphasize blue - sprite_overflow: bool, // Set if there are more than 8 sprites on a single line - sprite_zero_hit: bool, // Set when the first pixel of the sprite in the zero index of primary OAM is rendered - should_generate_nmi: bool, // Allows CPU to control whether NMIs trigger - vertical_blank: bool, // true == in vertical blank, false == not + oam_address: usize, + sprite_size: u8, + grayscale: bool, + show_background_left: bool, // 1: Show background in leftmost 8 pixels of screen, 0: Hide + show_sprites_left: bool, // 1: Show sprites in leftmost 8 pixels of screen, 0: Hide + show_background: bool, // 1: Show background + show_sprites: bool, // 1: Show sprites + emphasize_red: bool, // Emphasize red + emphasize_green: bool, // Emphasize green + emphasize_blue: bool, // Emphasize blue + sprite_overflow: bool, // Set if there are more than 8 sprites on a single line + sprite_zero_hit: bool, // Set when the first pixel of the sprite in the zero index of primary OAM is rendered + should_generate_nmi: bool, // Allows CPU to control whether NMIs trigger + vertical_blank: bool, // true == in vertical blank, false == not // These three: god knows. // TODO: experiment more with NMI - pub trigger_nmi: bool, // triggers NMI in the CPU when it checks in its step() - previous_nmi: bool, - nmi_delay: usize, + pub trigger_nmi: bool, // triggers NMI in the CPU when it checks in its step() + previous_nmi: bool, + nmi_delay: usize, - read_buffer: u8, // used with PPUDATA register - pub recent_bits: u8, // Least significant bits previously written into a PPU register + read_buffer: u8, // used with PPUDATA register + pub recent_bits: u8, // Least significant bits previously written into a PPU register - previous_a12: u8, + previous_a12: u8, } impl Ppu { pub fn new(mapper: Rc>) -> Self { Ppu { - line_cycle: 0, - scanline: 0, - frame: 0, - v: 0, - t: 0, - x: 0, - w: 0, - mapper: mapper, - nametable_a: vec![0u8; 0x0400], - nametable_b: vec![0u8; 0x0400], - nametable_c: vec![0u8; 0x0400], - nametable_d: vec![0u8; 0x0400], - palette_ram: vec![0u8; 0x0020], - background_pattern_sr_low: 0, - background_pattern_sr_high: 0, - nametable_byte: 0, - attribute_table_byte: 0, - low_pattern_table_byte: 0, - high_pattern_table_byte: 0, - background_palette_sr_low: 0, - background_palette_sr_high: 0, - background_palette_latch: 0, - primary_oam: vec![0u8; 0x0100], - secondary_oam: vec![0u8; 0x0020], - sprite_attribute_latches: vec![0u8; 8], - sprite_counters: vec![0u8; 8], - sprite_indexes: vec![0u8; 8], - sprite_pattern_table_srs: vec![(0u8, 0u8); 8], - num_sprites: 0, - address_increment: 0, - sprite_pattern_table_base: 0, + line_cycle: 0, + scanline: 0, + frame: 0, + v: 0, + t: 0, + x: 0, + w: 0, + mapper: mapper, + nametable_a: vec![0u8; 0x0400], + nametable_b: vec![0u8; 0x0400], + nametable_c: vec![0u8; 0x0400], + nametable_d: vec![0u8; 0x0400], + palette_ram: vec![0u8; 0x0020], + background_pattern_sr_low: 0, + background_pattern_sr_high: 0, + nametable_byte: 0, + attribute_table_byte: 0, + low_pattern_table_byte: 0, + high_pattern_table_byte: 0, + background_palette_sr_low: 0, + background_palette_sr_high: 0, + background_palette_latch: 0, + primary_oam: vec![0u8; 0x0100], + secondary_oam: vec![0u8; 0x0020], + sprite_attribute_latches: vec![0u8; 8], + sprite_counters: vec![0u8; 8], + sprite_indexes: vec![0u8; 8], + sprite_pattern_table_srs: vec![(0u8, 0u8); 8], + num_sprites: 0, + address_increment: 0, + sprite_pattern_table_base: 0, background_pattern_table_base: 0, - oam_address: 0, - sprite_size: 0, - grayscale: false, - show_background_left: false, - show_sprites_left: false, - show_background: false, - show_sprites: false, - emphasize_red: false, - emphasize_green: false, - emphasize_blue: false, - sprite_overflow: false, - sprite_zero_hit: false, - should_generate_nmi: false, - vertical_blank: false, - trigger_nmi: false, - previous_nmi: false, - nmi_delay: 0, - read_buffer: 0, - recent_bits: 0, - previous_a12: 0, + oam_address: 0, + sprite_size: 0, + grayscale: false, + show_background_left: false, + show_sprites_left: false, + show_background: false, + show_sprites: false, + emphasize_red: false, + emphasize_green: false, + emphasize_blue: false, + sprite_overflow: false, + sprite_zero_hit: false, + should_generate_nmi: false, + vertical_blank: false, + trigger_nmi: false, + previous_nmi: false, + nmi_delay: 0, + read_buffer: 0, + recent_bits: 0, + previous_a12: 0, } } @@ -174,13 +175,13 @@ impl Ppu { self.load_data_into_registers(); self.shift_registers(); self.perform_memory_fetch(); - }, + } 257 => self.copy_horizontal(), // At dot 257 of each scanline, if rendering is enabled, the PPU copies all bits related to horizontal position from t to v 321..=336 => { self.load_data_into_registers(); self.shift_registers(); self.perform_memory_fetch(); - }, + } x if x > 340 => panic!("cycle beyond 340"), _ => (), } @@ -193,7 +194,7 @@ impl Ppu { 257 => { self.evaluate_sprites(); // ignoring all timing details self.fetch_sprites(); - }, + } 321..=340 => (), // Read the first byte in secondary OAM (while the PPU fetches the first two background tiles for the next scanline) _ => (), } @@ -248,10 +249,7 @@ impl Ppu { // deal with mapper MMC3 let current_a12 = if self.v & 1 << 12 != 0 { 1 } else { 0 }; - if rendering - && (0..241).contains(&self.scanline) - && current_a12 != self.previous_a12 - { + if rendering && (0..241).contains(&self.scanline) && current_a12 != self.previous_a12 { self.mapper.borrow_mut().clock() } self.previous_a12 = current_a12; @@ -260,9 +258,69 @@ impl Ppu { } } -const PALETTE_TABLE: [(u8,u8,u8); 64] = [ - ( 84, 84, 84), ( 0, 30, 116), ( 8, 16, 144), ( 48, 0, 136), ( 68, 0, 100), ( 92, 0, 48), ( 84, 4, 0), ( 60, 24, 0), ( 32, 42, 0), ( 8, 58, 0), ( 0, 64, 0), ( 0, 60, 0), ( 0, 50, 60), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), - (152, 150, 152), ( 8, 76, 196), ( 48, 50, 236), ( 92, 30, 228), (136, 20, 176), (160, 20, 100), (152, 34, 32), (120, 60, 0), ( 84, 90, 0), ( 40, 114, 0), ( 8, 124, 0), ( 0, 118, 40), ( 0, 102, 120), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), - (236, 238, 236), ( 76, 154, 236), (120, 124, 236), (176, 98, 236), (228, 84, 236), (236, 88, 180), (236, 106, 100), (212, 136, 32), (160, 170, 0), (116, 196, 0), ( 76, 208, 32), ( 56, 204, 108), ( 56, 180, 204), ( 60, 60, 60), ( 0, 0, 0), ( 0, 0, 0), - (236, 238, 236), (168, 204, 236), (188, 188, 236), (212, 178, 236), (236, 174, 236), (236, 174, 212), (236, 180, 176), (228, 196, 144), (204, 210, 120), (180, 222, 120), (168, 226, 144), (152, 226, 180), (160, 214, 228), (160, 162, 160), ( 0, 0, 0), ( 0, 0, 0), +const PALETTE_TABLE: [(u8, u8, u8); 64] = [ + (84, 84, 84), + (0, 30, 116), + (8, 16, 144), + (48, 0, 136), + (68, 0, 100), + (92, 0, 48), + (84, 4, 0), + (60, 24, 0), + (32, 42, 0), + (8, 58, 0), + (0, 64, 0), + (0, 60, 0), + (0, 50, 60), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (152, 150, 152), + (8, 76, 196), + (48, 50, 236), + (92, 30, 228), + (136, 20, 176), + (160, 20, 100), + (152, 34, 32), + (120, 60, 0), + (84, 90, 0), + (40, 114, 0), + (8, 124, 0), + (0, 118, 40), + (0, 102, 120), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (236, 238, 236), + (76, 154, 236), + (120, 124, 236), + (176, 98, 236), + (228, 84, 236), + (236, 88, 180), + (236, 106, 100), + (212, 136, 32), + (160, 170, 0), + (116, 196, 0), + (76, 208, 32), + (56, 204, 108), + (56, 180, 204), + (60, 60, 60), + (0, 0, 0), + (0, 0, 0), + (236, 238, 236), + (168, 204, 236), + (188, 188, 236), + (212, 178, 236), + (236, 174, 236), + (236, 174, 212), + (236, 180, 176), + (228, 196, 144), + (204, 210, 120), + (180, 222, 120), + (168, 226, 144), + (152, 226, 180), + (160, 214, 228), + (160, 162, 160), + (0, 0, 0), + (0, 0, 0), ]; diff --git a/src/ppu/rendering.rs b/src/ppu/rendering.rs index 2c4d5a5..f878abf 100644 --- a/src/ppu/rendering.rs +++ b/src/ppu/rendering.rs @@ -1,7 +1,6 @@ use super::cpu_registers::set_bit; impl super::Ppu { - pub fn perform_memory_fetch(&mut self) { match self.line_cycle % 8 { 0 => self.inc_coarse_x(), @@ -27,7 +26,8 @@ impl super::Ppu { } pub fn load_data_into_registers(&mut self) { - if self.line_cycle % 8 == 1 { // The shifters are reloaded during ticks 9, 17, 25, ..., 257. + if self.line_cycle % 8 == 1 { + // The shifters are reloaded during ticks 9, 17, 25, ..., 257. // These contain the pattern table data for two tiles. Every 8 cycles, the data for the next // tile is loaded into the upper 8 bits of this shift register. Meanwhile, the pixel to render is fetched from one of the lower 8 bits. self.background_pattern_sr_low |= self.low_pattern_table_byte as u16; @@ -46,16 +46,16 @@ impl super::Ppu { let byte = self.read(address as usize); // figure out which two bits are being represented, ignoring fine x and fine y // left or right: - let coarse_x = self.v & 0b00000000_00011111; + let coarse_x = self.v & 0b00000000_00011111; let coarse_y = (self.v & 0b00000011_11100000) >> 5; let left_or_right = (coarse_x / 2) % 2; // 0 == left, 1 == right let top_or_bottom = (coarse_y / 2) % 2; // 0 == top, 1 == bottom - // grab the needed two bits + // grab the needed two bits self.attribute_table_byte = match (top_or_bottom, left_or_right) { - (0,0) => (byte >> 0) & 0b00000011, - (0,1) => (byte >> 2) & 0b00000011, - (1,0) => (byte >> 4) & 0b00000011, - (1,1) => (byte >> 6) & 0b00000011, + (0, 0) => (byte >> 0) & 0b00000011, + (0, 1) => (byte >> 2) & 0b00000011, + (1, 0) => (byte >> 4) & 0b00000011, + (1, 1) => (byte >> 6) & 0b00000011, _ => panic!("should not get here"), }; } @@ -84,8 +84,8 @@ impl super::Ppu { let (mut sprite_pixel, current_sprite) = self.select_sprite_pixel(); // extract low and high bits from palette shift registers according to fine x, starting from left - let low_palette_bit = (self.background_palette_sr_low & (1 << (7-self.x)) != 0) as u8; - let high_palette_bit = (self.background_palette_sr_high & (1 << (7-self.x)) != 0) as u8; + let low_palette_bit = (self.background_palette_sr_low & (1 << (7 - self.x)) != 0) as u8; + let high_palette_bit = (self.background_palette_sr_high & (1 << (7 - self.x)) != 0) as u8; let palette_offset = (high_palette_bit << 1) | low_palette_bit; if x < 8 && !self.show_background_left { @@ -95,18 +95,22 @@ impl super::Ppu { sprite_pixel = 0; } let mut palette_address = 0; - if background_pixel == 0 && sprite_pixel != 0 { // displaying the sprite + if background_pixel == 0 && sprite_pixel != 0 { + // displaying the sprite palette_address += 0x10; // second half of palette table, "Background/Sprite select" palette_address += (self.sprite_attribute_latches[current_sprite] & 0b11) << 2; // bottom two bits of attribute byte, left shifted by two palette_address += sprite_pixel; // bottom two bits are the value of the sprite pixel from pattern table - } else if background_pixel != 0 && sprite_pixel == 0 { // displaying the background pixel + } else if background_pixel != 0 && sprite_pixel == 0 { + // displaying the background pixel palette_address += palette_offset << 2; // Palette number from attribute table or OAM palette_address += background_pixel; // Pixel value from tile data } else if background_pixel != 0 && sprite_pixel != 0 { - if self.sprite_indexes[current_sprite] == 0 { // don't access index current_sprite. need to know which sprite we're on horizontally. + if self.sprite_indexes[current_sprite] == 0 { + // don't access index current_sprite. need to know which sprite we're on horizontally. self.sprite_zero_hit = true; } - if self.sprite_attribute_latches[current_sprite] & (1 << 5) == 0 { // sprite has high priority + if self.sprite_attribute_latches[current_sprite] & (1 << 5) == 0 { + // sprite has high priority palette_address += 0x10; palette_address += (self.sprite_attribute_latches[current_sprite] & 0b11) << 2; palette_address += sprite_pixel; @@ -133,7 +137,7 @@ impl super::Ppu { color.1 = deemphasize(&color.1); color.2 = emphasize(&color.2); } - (x,y,color) + (x, y, color) } pub fn select_background_pixel(&mut self) -> u8 { @@ -141,7 +145,7 @@ impl super::Ppu { // Returned background pixel is a value between 0 and 3. // the bit from background_pattern_sr_low (low pattern table byte) in the 0th place, // and the value of the background_pattern_sr_high (high pattern table byte) in the 1st place. - let low_bit = (self.background_pattern_sr_low & (1 << (15 - self.x)) != 0) as u8; + let low_bit = (self.background_pattern_sr_low & (1 << (15 - self.x)) != 0) as u8; let high_bit = (self.background_pattern_sr_high & (1 << (15 - self.x)) != 0) as u8; (high_bit << 1) | low_bit } else { @@ -153,7 +157,7 @@ impl super::Ppu { // Returns (sprite_pixel, index of sprite_pixel within secondary_oam/shift registers) if self.show_sprites { // sprite pixel is a value between 0 and 3 representing the two sprite pattern table shift registers - let mut low_bit = 0; + let mut low_bit = 0; let mut high_bit = 0; let mut secondary_index = 0; for i in 0..self.num_sprites { @@ -163,8 +167,8 @@ impl super::Ppu { // The current pixel for each "active" sprite is checked (from highest to lowest priority), // and the first non-transparent pixel moves on to a multiplexer, where it joins the BG pixel. secondary_index = i; - low_bit = (self.sprite_pattern_table_srs[i].0 & 1<<7 != 0) as u8; - high_bit = (self.sprite_pattern_table_srs[i].1 & 1<<7 != 0) as u8; + low_bit = (self.sprite_pattern_table_srs[i].0 & 1 << 7 != 0) as u8; + high_bit = (self.sprite_pattern_table_srs[i].1 & 1 << 7 != 0) as u8; if !(low_bit == 0 && high_bit == 0) { break; } @@ -192,10 +196,10 @@ impl super::Ppu { pub fn evaluate_sprites(&mut self) { let mut sprite_count = 0; for n in 0..64 { - let y_coord = self.primary_oam[(n*4)+0]; + let y_coord = self.primary_oam[(n * 4) + 0]; if self.y_in_range(y_coord) { for i in 0..4 { - self.secondary_oam[(sprite_count*4)+i] = self.primary_oam[(n*4)+i]; + self.secondary_oam[(sprite_count * 4) + i] = self.primary_oam[(n * 4) + i]; } self.sprite_indexes[sprite_count] = n as u8; sprite_count += 1; @@ -207,22 +211,21 @@ impl super::Ppu { } } self.num_sprites = sprite_count; - } pub fn fetch_sprites(&mut self) { for i in 0..self.num_sprites { let mut address: usize; - let sprite_y_position = self.secondary_oam[(4*i)+0] as usize; // byte 0 of sprite, sprite's vertical position on screen - let sprite_tile_index = self.secondary_oam[(4*i)+1] as usize; // byte 1 of sprite, sprite's location within pattern table - let sprite_attributes = self.secondary_oam[(4*i)+2]; // byte 2 of sprite, sprite's palette, priority, and flip attributes - let sprite_x_position = self.secondary_oam[(4*i)+3]; // byte 3 of sprite, sprite's horizontal position on screen - let flipped_vertically = sprite_attributes & (1<<7) != 0; - let flipped_horizontally = sprite_attributes & (1<<6) != 0; + let sprite_y_position = self.secondary_oam[(4 * i) + 0] as usize; // byte 0 of sprite, sprite's vertical position on screen + let sprite_tile_index = self.secondary_oam[(4 * i) + 1] as usize; // byte 1 of sprite, sprite's location within pattern table + let sprite_attributes = self.secondary_oam[(4 * i) + 2]; // byte 2 of sprite, sprite's palette, priority, and flip attributes + let sprite_x_position = self.secondary_oam[(4 * i) + 3]; // byte 3 of sprite, sprite's horizontal position on screen + let flipped_vertically = sprite_attributes & (1 << 7) != 0; + let flipped_horizontally = sprite_attributes & (1 << 6) != 0; // For 8x8 sprites, this is the tile number of this sprite within the pattern table selected in bit 3 of PPUCTRL ($2000). if self.sprite_size == 8 { address = self.sprite_pattern_table_base; - address += sprite_tile_index*16; + address += sprite_tile_index * 16; address += if !flipped_vertically { self.scanline - sprite_y_position // row-within-sprite offset is difference between current scanline and top of sprite } else { @@ -230,8 +233,12 @@ impl super::Ppu { }; // For 8x16 sprites, the PPU ignores the pattern table selection and selects a pattern table from bit 0 of this number. } else { - address = if sprite_tile_index & 1 == 0 { 0x0 } else { 0x1000 }; - address += (sprite_tile_index & 0xFFFF-1) << 4; // turn off bottom bit BEFORE shifting + address = if sprite_tile_index & 1 == 0 { + 0x0 + } else { + 0x1000 + }; + address += (sprite_tile_index & 0xFFFF - 1) << 4; // turn off bottom bit BEFORE shifting let fine_y = if !flipped_vertically { self.scanline - sprite_y_position } else { @@ -248,7 +255,10 @@ impl super::Ppu { let high_pattern_table_byte = self.read(address + 8); let mut shift_reg_vals = (0, 0); for j in 0..8 { - let current_bits = (low_pattern_table_byte & (1 << j), high_pattern_table_byte & (1 << j)); + let current_bits = ( + low_pattern_table_byte & (1 << j), + high_pattern_table_byte & (1 << j), + ); if !flipped_horizontally { // just copy each bit in same order shift_reg_vals.0 |= current_bits.0; @@ -269,9 +279,10 @@ impl super::Ppu { } pub fn inc_coarse_x(&mut self) { - if self.v & 0x001F == 0x001F { // if coarse X == 31 - self.v &= !0x001F; // coarse X = 0 - self.v ^= 1<<10; // switch horizontal nametable + if self.v & 0x001F == 0x001F { + // if coarse X == 31 + self.v &= !0x001F; // coarse X = 0 + self.v ^= 1 << 10; // switch horizontal nametable } else { self.v += 1; } @@ -280,7 +291,7 @@ impl super::Ppu { pub fn inc_y(&mut self) { // If rendering is enabled, fine Y is incremented at dot 256 of each scanline, // overflowing to coarse Y, and finally adjusted to wrap among the nametables vertically. - let mut fine_y = (self.v & 0b01110000_00000000) >> 12; + let mut fine_y = (self.v & 0b01110000_00000000) >> 12; let mut coarse_y = (self.v & 0b00000011_11100000) >> 5; if fine_y < 7 { fine_y += 1; @@ -290,7 +301,7 @@ impl super::Ppu { // incrementing coarse Y from 29, the vertical nametable is switched by toggling bit // 11, and coarse Y wraps to row 0. if coarse_y == 29 { - self.v ^= 1<<11; + self.v ^= 1 << 11; coarse_y = 0; // Coarse Y can be set out of bounds (> 29), which will cause the PPU to read the // attribute data stored there as tile data. If coarse Y is incremented from 31, @@ -317,8 +328,8 @@ impl super::Ppu { // v: ....F.. ...EDCBA = t: ....F.. ...EDCBA let mask = 0b00000100_00011111; let t_vals = self.t & mask; // grab bits of t - self.v &= !mask; // turn off bits of v - self.v |= t_vals; // apply bits of t + self.v &= !mask; // turn off bits of v + self.v |= t_vals; // apply bits of t } pub fn copy_vertical(&mut self) { @@ -334,8 +345,8 @@ impl super::Ppu { } pub fn y_in_range(&self, y_coord: u8) -> bool { - self.scanline >= (y_coord as usize) && - self.scanline - (y_coord as usize) < self.sprite_size as usize + self.scanline >= (y_coord as usize) + && self.scanline - (y_coord as usize) < self.sprite_size as usize } pub fn nmi_change(&mut self) { diff --git a/src/ppu/serialize.rs b/src/ppu/serialize.rs index ac658ca..fd0932b 100644 --- a/src/ppu/serialize.rs +++ b/src/ppu/serialize.rs @@ -1,6 +1,6 @@ -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone, Hash)] pub struct PpuData { line_cycle: usize, scanline: usize, @@ -32,7 +32,7 @@ pub struct PpuData { num_sprites: usize, address_increment: u16, sprite_pattern_table_base: usize, - background_pattern_table_base:usize, + background_pattern_table_base: usize, oam_address: usize, sprite_size: u8, grayscale: bool, @@ -57,7 +57,7 @@ pub struct PpuData { impl super::Ppu { pub fn save_state(&self) -> PpuData { - PpuData{ + PpuData { line_cycle: self.line_cycle, scanline: self.scanline, frame: self.frame, diff --git a/src/screen.rs b/src/screen.rs index edf3696..6d17593 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -1,51 +1,41 @@ -extern crate sdl2; +use font8x8::legacy::BASIC_LEGACY; -use sdl2::Sdl; -use sdl2::pixels::Color; -use sdl2::render::{Canvas, Texture, TextureCreator}; -use sdl2::video::{Window, WindowContext}; - -pub const SCALE_FACTOR: usize = 3; -const BYTES_IN_COL: usize = SCALE_FACTOR * 3; // 3 bytes per pixel in RGB24. This represents a thick, SCALE_FACTOR-pixel-wide column. -const BYTES_IN_ROW: usize = BYTES_IN_COL * 256; // 256 = screen width in NES pixels. This represents a thin, one-SDL-pixel-tall row. - -type RGBColor = (u8, u8, u8); - -pub fn init_window(context: &Sdl) -> Result<(Canvas, TextureCreator), String> { - let video_subsystem = context.video()?; - let window = video_subsystem.window("NESTUR", (256 * SCALE_FACTOR) as u32, (240 * SCALE_FACTOR) as u32) - .position_centered() - .opengl() - .build() - .map_err(|e| e.to_string())?; - let mut canvas = window.into_canvas().build().map_err(|e| e.to_string())?; - let texture_creator = canvas.texture_creator(); - canvas.set_draw_color(Color::RGB(0, 0, 0)); - canvas.clear(); - canvas.present(); - Ok((canvas, texture_creator)) +pub struct Screen { + pub(crate) display: Vec, } -pub fn draw_pixel(buffer: &mut Vec, x: usize, y: usize, color: RGBColor) { - let (r, g, b) = color; - let nes_y_offset = y * BYTES_IN_ROW * SCALE_FACTOR; // find offset for thick, SCALE_FACTOR-pixel tall row - for sdl_row_num in 0..SCALE_FACTOR { // looping over one-pixel tall rows up to SCALE_FACTOR - let row_offset = nes_y_offset + (sdl_row_num * BYTES_IN_ROW); // row_offset is the offset within buffer of the thin row we're on - let nes_x_offset = x * BYTES_IN_COL; // find horizontal offset within row (in byte terms) of NES x-coordinate - for sdl_col_num in 0..SCALE_FACTOR { // for pixels up to SCALE_FACTOR, moving horizontally - let col_offset = nes_x_offset + (sdl_col_num * 3); // skip 3 bytes at a time, R/G/B for each pixel - let offset = row_offset + col_offset; - buffer[offset + 0] = r; - buffer[offset + 1] = g; - buffer[offset + 2] = b; +impl Screen { + pub fn new() -> Screen { + Screen { + display: vec![0; 256 * 240], + } + } + + pub fn plot_pixel(&mut self, ox: usize, oy: usize, r: u32, g: u32, b: u32) { + let color = (r << 16) + (g << 8) + b; + + let x = ox.clamp(0, 256 - 1); + let y = oy.clamp(0, 240 - 1); + self.display[(y * 256) + x] = color + } + + pub fn draw_string(&mut self, ox: usize, oy: usize, text: &str) { + let mut xpos = ox; + let mut ypos = oy; + for char in text.chars() { + for x in &BASIC_LEGACY[char as usize] { + for bit in 0..8 { + match *x & 1 << bit { + 0 => {} //self.plot_pixel(xpos, ypos, 0, 0, 0), + _ => self.plot_pixel(xpos, ypos, 0xff, 0xff, 0xff), + } + xpos += 1 + } + xpos -= 8; + ypos += 1; + } + xpos += 10; + ypos -= 8; } } } - -pub fn draw_to_window(texture: &mut Texture, canvas: &mut Canvas, buffer: &Vec) -> Result<(), String> { - texture.update(None, buffer, 256*3*SCALE_FACTOR) - .map_err(|e| e.to_string())?; - canvas.copy(&texture, None, None)?; - canvas.present(); - Ok(()) -} diff --git a/src/state.rs b/src/state.rs index 712268b..0fc0037 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,7 +1,7 @@ -use super::cpu; -use super::ppu; use super::apu; use super::cartridge; +use super::cpu; +use super::ppu; use std::fs::{DirEntry, File}; use std::io::{Read, Write}; @@ -16,16 +16,14 @@ struct SaveState { } pub fn save_state(cpu: &cpu::Cpu, save_file: &PathBuf) -> Result<(), String> { - let data = SaveState{ + let data = SaveState { cpu: cpu.save_state(), ppu: cpu.ppu.save_state(), apu: cpu.apu.save_state(), - mapper: cpu.mapper.borrow().save_state(), + mapper: cpu.mapper.borrow_mut().save_state(), }; - let serialized = serde_json::to_string(&data) - .map_err(|e| e.to_string())?; - let mut f = File::create(&save_file) - .expect("could not create output file for save state"); + let serialized = serde_json::to_string(&data).map_err(|e| e.to_string())?; + 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())?; println!("state saved to file: {:?}", save_file); @@ -34,15 +32,13 @@ pub fn save_state(cpu: &cpu::Cpu, save_file: &PathBuf) -> Result<(), String> { pub fn load_state(cpu: &mut cpu::Cpu, save_file: &PathBuf) -> Result<(), String> { if Path::new(&save_file).exists() { - let mut f = File::open(save_file.clone()) - .map_err(|e| e.to_string())?; + 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())?; + 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); @@ -67,7 +63,7 @@ pub fn find_next_filename(filepath: &PathBuf, new_ext: Option<&str>) -> Option

) -> Option name.len() >= stem.len() && name.len() >= ext.len() && &name[..stem.len()] == stem - && &name[name.len()-ext.len()..] == ext + && &name[name.len() - ext.len()..] == ext }) .collect::>>(); save_states.sort_by(|a, b| {