Initial Commit of nesfuzz

This commit is contained in:
Sarah Jamie Lewis 2021-11-21 23:20:04 -08:00
parent 4afae10ece
commit f4b677d52a
34 changed files with 1833 additions and 1111 deletions

6
.gitignore vendored
View File

@ -10,3 +10,9 @@ Cargo.lock
**/*.rs.bk
roms
*.fm2
*.nes
*.cdl
*.nl
*.idea
*cdl

View File

@ -1,14 +1,13 @@
[package]
name = "nestur"
name = "nesfuzz"
version = "0.1.0"
authors = ["Theron <tspiegl@gmail.com>"]
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"

View File

@ -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)
<img src="pics/smb.png" width=250> <img src="pics/zelda_dungeon.png" width=250> <img src="pics/kirby.png" width=250> <img src="pics/dk.png" width=250> <img src="pics/smb3.png" width=250> <img src="pics/excitebike.png" width=250>
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 <rom> <tas file>`
`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`.
// The number of cpu instances to spawn..
const NUM_THREADS: usize = 28;
## To do
// 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;
- support other controllers?
// Same input should generate the same output...
// (I make no guarantee of that at the moment)
const RNG_SEED: u32 = 0x5463753;
- more mappers?
// 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;
- better save file organization?
// The rate at which seed inputs become corrupted..
const MUTATION_RATE: f64 = 0.1;
## Known problem games
- None currently, please report any issues
// 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.

BIN
fuzzimages/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -1,5 +1,7 @@
// number of CPU cycles between sample output level being adjusted
pub const SAMPLE_RATES: [u16; 16] = [428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54];
pub const SAMPLE_RATES: [u16; 16] = [
428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54,
];
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct DMC {
@ -96,8 +98,16 @@ impl DMC {
self.cpu_cycles_left = SAMPLE_RATES[self.rate_index];
if self.enabled {
match self.shift_register & 1 {
0 => 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,
}
}

View File

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

View File

@ -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<f32>,
tnd_table: Vec<f32>,
@ -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.

View File

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

View File

@ -1,6 +1,6 @@
pub type ApuData = super::Apu;
impl super::Apu{
impl super::Apu {
pub fn save_state(&self) -> ApuData {
self.clone()
}

View File

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

View File

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

View File

@ -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<Mutex<Vec<f32>>>,
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[i1] + α × (x[i] x[i1])
(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<Mutex<Vec<f32>>>)
-> Result<sdl2::audio::AudioDevice<ApuSampler>, 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[i1] + gamma * (x[i] x[i1])
*/

View File

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

View File

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

View File

@ -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<u8>, // 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) {

View File

@ -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<Vec<u8>>, // 16 KiB chunks for CPU
pub chr_rom: Vec<Vec<u8>>, // 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> {

View File

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

View File

@ -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<u8>,
}
#[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<u8>,
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,

View File

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

View File

@ -1,10 +1,9 @@
impl super::Cpu {
pub fn absolute(&mut self) -> usize {
self.clock += 4;
<usize>::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
}
}

View File

@ -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<u8>, // 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<u8>, // 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<usize>,
clock: u64, // number of ticks in current cycle
delay: usize, // for skipping cycles during OAM DMA
pub mapper: Rc<RefCell<dyn Mapper>>, // 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<fn(&mut Self, usize, Mode)>, // function table
mode_table: Vec<Mode>, // address mode table
mode_table: Vec<Mode>, // address mode table
}
impl Cpu {
pub fn new(mapper: Rc<RefCell<dyn Mapper>>, 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:
@ -163,8 +666,9 @@ impl Cpu {
// back up clock so we know how many cycles we complete
let clock = self.clock;
let opcode = <usize>::from(self.read(self.pc));
let opcode = <usize>::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_number) != 0) as u8,
true => (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),
}
}
@ -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<u8> {
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",
];

View File

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

View File

@ -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<u8>,
pub(crate) mem: Vec<u8>,
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<Mode>,
}
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;
}

View File

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

63
src/fuzzing_state.rs Normal file
View File

@ -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<H: Hasher>(&self, state: &mut H) {
return self.0.hash(state);
}
}
impl PartialEq<Self> for FuzzingInputState {
fn eq(&self, other: &Self) -> bool {
self.0.eq(&other.0)
}
}
impl Eq for FuzzingInputState {}

View File

@ -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<u8> {
if *strobe & 1 == 1 {
let mut button_states = 0;
let pressed_keys: HashSet<Scancode> = 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<FrameInput>,
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,
}
}
}

View File

@ -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::<Vec<String>>();
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 <rom> <fm2 seed input>")
}
// 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<FuzzingInputState, u64> = 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<Vec<u8>> = 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<u8>,
canvas: &mut Canvas<Window>,
texture: &mut Texture,
filename: &str
) -> Result<Option<GameExitMode>, String> {
i: isize,
mutex: Arc<Mutex<u8>>,
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::<f32>::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?

View File

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

View File

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

View File

@ -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<u8>, // 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<u8>, // Primary OAM (holds 64 sprites for the frame)
secondary_oam: Vec<u8>, // Secondary OAM (holds 8 sprites for the current scanline)
pub primary_oam: Vec<u8>, // Primary OAM (holds 64 sprites for the frame)
secondary_oam: Vec<u8>, // Secondary OAM (holds 8 sprites for the current scanline)
sprite_attribute_latches: Vec<u8>, // 8 latches - These contain the attribute bytes [palette data] for up to 8 sprites.
sprite_counters: Vec<u8>, // 8 counters - These contain the X positions for up to 8 sprites.
sprite_indexes: Vec<u8>, // Indexes of the sprites-in-the-attribute-latches' within primary OAM
sprite_counters: Vec<u8>, // 8 counters - These contain the X positions for up to 8 sprites.
sprite_indexes: Vec<u8>, // 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<RefCell<dyn Mapper>>) -> 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),
];

View File

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

View File

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

View File

@ -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<Window>, TextureCreator<WindowContext>), 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<u32>,
}
pub fn draw_pixel(buffer: &mut Vec<u8>, 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<Window>, buffer: &Vec<u8>) -> Result<(), String> {
texture.update(None, buffer, 256*3*SCALE_FACTOR)
.map_err(|e| e.to_string())?;
canvas.copy(&texture, None, None)?;
canvas.present();
Ok(())
}

View File

@ -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<P
let current_name = format!("{}{}{}-{}.{}", path, sep, stem, i, ext);
let save_file = PathBuf::from(&current_name);
if !save_file.exists() {
return Some(save_file)
return Some(save_file);
}
i += 1;
}
@ -88,7 +84,7 @@ pub fn find_last_save_state(filepath: &PathBuf, new_ext: Option<&str>) -> 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::<Vec<std::io::Result<DirEntry>>>();
save_states.sort_by(|a, b| {