Initial Commit of nesfuzz
This commit is contained in:
parent
4afae10ece
commit
f4b677d52a
|
@ -10,3 +10,9 @@ Cargo.lock
|
|||
**/*.rs.bk
|
||||
|
||||
roms
|
||||
*.fm2
|
||||
*.nes
|
||||
*.cdl
|
||||
*.nl
|
||||
*.idea
|
||||
*cdl
|
||||
|
|
13
Cargo.toml
13
Cargo.toml
|
@ -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"
|
||||
|
|
85
README.md
85
README.md
|
@ -1,65 +1,54 @@
|
|||
# nestur
|
||||
# nesfuzz
|
||||
|
||||
Nestur is an NES emulator. There are plenty of full-featured emulators out there; this is primarily an educational project but it is usable. There may still be many bugs, but I'm probably not aware of them so please submit issues.
|
||||
- no use of `unsafe`
|
||||
- NTSC timing
|
||||
- supports mappers 0-4 which cover ~85% of [games](http://tuxnes.sourceforge.net/nesmapper.txt)
|
||||
nesfuzz is a fuzzer for Nes Games by [@SarahJamieLewis](https://twitter.com/sarahjamielewis)
|
||||
|
||||
<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.
|
Binary file not shown.
After Width: | Height: | Size: 79 KiB |
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,8 +16,8 @@ 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)]
|
||||
|
@ -41,8 +41,12 @@ 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),
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -68,7 +68,8 @@ 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.
|
||||
|| self.timer_period < 8
|
||||
// the timer has a value less than eight.
|
||||
{
|
||||
0
|
||||
} else if self.constant_volume_flag {
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
132
src/audio.rs
132
src/audio.rs
|
@ -1,133 +1 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
use sdl2::Sdl;
|
||||
use sdl2::audio::{AudioCallback, AudioSpecDesired};
|
||||
use std::f32::consts::PI;
|
||||
|
||||
const APU_SAMPLE_RATE: f32 = 894_886.5;
|
||||
const SDL_SAMPLE_RATE: i32 = 44_100;
|
||||
// Video runs at 60Hz, so console is clocked by doing enough work to create one frame of video, then sending the video and audio to their respective SDL
|
||||
// devices and then sleeping. So the audio device is set to play 44,100 samples per second, and grab them in 60 intervals over the course of that second.
|
||||
const SAMPLES_PER_FRAME: u16 = SDL_SAMPLE_RATE as u16/60;
|
||||
|
||||
|
||||
pub struct ApuSampler {
|
||||
// This buffer receives all of the raw audio produced by the APU.
|
||||
// The callback will take what it needs when it needs it and truncate the buffer for smooth audio output.
|
||||
buffer: Arc<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[i−1] + α × (x[i] − x[i−1])
|
||||
(self.gamma_90_hz * self.prev_output_90_hz) + (sample - self.prev_input_90_hz)
|
||||
}
|
||||
|
||||
fn high_pass_440_hz(&self, sample: f32) -> f32 {
|
||||
(self.gamma_440_hz * self.prev_output_440_hz) + (sample - self.prev_input_440_hz)
|
||||
}
|
||||
|
||||
fn low_pass_14_khz(&self, sample: f32) -> f32 {
|
||||
((1. - self.gamma_14_khz) * self.prev_output_14_khz) + (self.gamma_14_khz * sample)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl AudioCallback for ApuSampler {
|
||||
type Channel = f32;
|
||||
|
||||
fn callback(&mut self, out: &mut [f32]) {
|
||||
|
||||
let mut b = self.buffer.lock().unwrap();
|
||||
// if we have data in the buffer
|
||||
if b.len() > 0 {
|
||||
// copy samples at the appropriate interval from the raw APU buffer to the output device
|
||||
for (i, x) in out.iter_mut().enumerate() {
|
||||
let sample_idx = ((i as f32) * self.sample_ratio) as usize;
|
||||
if sample_idx < b.len() {
|
||||
let sample = b[sample_idx];
|
||||
|
||||
let filtered_90_hz = self.high_pass_90_hz(sample);
|
||||
self.prev_input_90_hz = sample;
|
||||
self.prev_output_90_hz = filtered_90_hz;
|
||||
|
||||
let filtered_440_hz = self.high_pass_440_hz(filtered_90_hz);
|
||||
self.prev_input_440_hz = filtered_90_hz;
|
||||
self.prev_output_440_hz = filtered_440_hz;
|
||||
|
||||
let filtered_14_khz = self.low_pass_14_khz(filtered_440_hz);
|
||||
self.prev_input_14_khz = filtered_440_hz;
|
||||
self.prev_output_14_khz = filtered_14_khz;
|
||||
*x = filtered_14_khz;
|
||||
}
|
||||
}
|
||||
let l = b.len();
|
||||
let target = (SAMPLES_PER_FRAME as f32 * self.sample_ratio) as usize;
|
||||
if l > target {
|
||||
*b = b.split_off(target);
|
||||
}
|
||||
} else {
|
||||
println!("buffer empty!"); // happens when the callback fires twice between video frames
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize(sdl_context: &Sdl, buffer: Arc<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[i−1] + gamma * (x[i] − x[i−1])
|
||||
|
||||
*/
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use super::{Cartridge, Mapper, Mirror, serialize::*};
|
||||
use super::{serialize::*, Cartridge, Mapper, Mirror};
|
||||
|
||||
pub struct Cnrom {
|
||||
cart: Cartridge,
|
||||
|
@ -22,7 +22,10 @@ impl Mapper for Cnrom {
|
|||
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},
|
||||
_ => {
|
||||
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 {
|
||||
MapperData::Cnrom(CnromData {
|
||||
cart: self.cart.clone(),
|
||||
chr_bank_select: self.chr_bank_select,
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn load_state(&mut self, mapper_data: MapperData) {
|
||||
|
|
|
@ -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};
|
||||
|
@ -89,9 +89,11 @@ impl Mmc1 {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -134,41 +137,44 @@ impl Mapper for Mmc1 {
|
|||
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,16 +212,18 @@ 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 {
|
||||
MapperData::Mmc1(Mmc1Data {
|
||||
cart: self.cart.clone(),
|
||||
step: self.step,
|
||||
shift_register: self.shift_register,
|
||||
|
@ -227,8 +237,7 @@ impl Mapper for Mmc1 {
|
|||
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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
@ -69,12 +68,12 @@ 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 {
|
||||
true => match address {
|
||||
0x0000..=0x03FF => 2,
|
||||
0x0400..=0x07FF => 3,
|
||||
0x0800..=0x0BFF => 4,
|
||||
|
@ -82,10 +81,8 @@ impl Mapper for Mmc3 {
|
|||
0x1000..=0x17FF => 0,
|
||||
0x1800..=0x1FFF => 1,
|
||||
_ => panic!("oh no"),
|
||||
}
|
||||
},
|
||||
false => {
|
||||
match address {
|
||||
false => match address {
|
||||
0x0000..=0x07FF => 0,
|
||||
0x0800..=0x0FFF => 1,
|
||||
0x1000..=0x13FF => 2,
|
||||
|
@ -93,53 +90,50 @@ impl Mapper for Mmc3 {
|
|||
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 {
|
||||
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 {
|
||||
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,8 +229,7 @@ impl Mapper for Mmc3 {
|
|||
}
|
||||
|
||||
fn save_state(&self) -> MapperData {
|
||||
MapperData::Mmc3(
|
||||
Mmc3Data {
|
||||
MapperData::Mmc3(Mmc3Data {
|
||||
cart: self.cart.clone(),
|
||||
mirroring: self.mirroring,
|
||||
bank_registers: self.bank_registers.clone(),
|
||||
|
@ -240,8 +244,7 @@ impl Mapper for Mmc3 {
|
|||
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) {
|
||||
|
|
|
@ -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;
|
||||
|
@ -60,7 +60,6 @@ pub struct Cartridge {
|
|||
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,13 +72,20 @@ 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,
|
||||
|
@ -102,15 +108,14 @@ impl Cartridge {
|
|||
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> {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use super::{Cartridge, Mapper, Mirror, serialize::*};
|
||||
use super::{serialize::*, Cartridge, Mapper, Mirror};
|
||||
|
||||
pub struct Nrom {
|
||||
cart: Cartridge,
|
||||
|
@ -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 {
|
||||
MapperData::Nrom(NromData {
|
||||
cart: self.cart.clone(),
|
||||
chr_ram: self.chr_ram.clone(),
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn load_state(&mut self, mapper_data: MapperData) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use super::{Cartridge, Mapper, Mirror, serialize::*};
|
||||
use super::{serialize::*, Cartridge, Mapper, Mirror};
|
||||
|
||||
pub struct Uxrom {
|
||||
cart: Cartridge,
|
||||
|
@ -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},
|
||||
_ => {
|
||||
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 {
|
||||
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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
683
src/cpu/mod.rs
683
src/cpu/mod.rs
|
@ -1,12 +1,13 @@
|
|||
mod addressing_modes;
|
||||
mod opcodes;
|
||||
mod utility;
|
||||
pub mod serialize;
|
||||
mod utility;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::cartridge::Mapper;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashSet;
|
||||
use std::rc::Rc;
|
||||
|
||||
// RAM locations
|
||||
const STACK_OFFSET: usize = 0x100;
|
||||
|
@ -23,18 +24,28 @@ const DECIMAL_FLAG: u8 = 1 << 3;
|
|||
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,15 +64,16 @@ impl Mode {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Cpu {
|
||||
mem: Vec<u8>, // CPU's RAM, $0000-$1FFF
|
||||
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
|
||||
|
||||
|
@ -72,6 +84,7 @@ 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
|
||||
|
@ -82,7 +95,9 @@ impl Cpu {
|
|||
pub fn new(mapper: Rc<RefCell<dyn Mapper>>, ppu: super::Ppu, apu: super::Apu) -> Self {
|
||||
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*/
|
||||
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*/
|
||||
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(),
|
||||
// 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,6 +704,7 @@ 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),
|
||||
|
@ -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) {
|
||||
|
@ -257,7 +778,7 @@ impl Cpu {
|
|||
self.ppu.write_oam_dma(data);
|
||||
let is_odd = self.clock % 2 != 0;
|
||||
self.delay = 513 + if is_odd { 1 } else { 0 };
|
||||
},
|
||||
}
|
||||
_ => panic!("wrote to bad ppu reg: {}", reg_num),
|
||||
}
|
||||
}
|
||||
|
@ -294,9 +815,17 @@ impl Cpu {
|
|||
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,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
];
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
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 };
|
||||
|
@ -41,7 +43,7 @@ impl super::Cpu {
|
|||
_ => {
|
||||
self.clock += 2;
|
||||
self.read(_address)
|
||||
},
|
||||
}
|
||||
};
|
||||
// put top bit in carry flag
|
||||
if val & (1 << 7) != 0 {
|
||||
|
@ -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,7 +370,7 @@ 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 };
|
||||
|
@ -379,8 +380,11 @@ impl super::Cpu {
|
|||
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);
|
||||
}
|
||||
|
@ -397,8 +401,11 @@ impl super::Cpu {
|
|||
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),
|
||||
|
@ -552,5 +559,4 @@ impl super::Cpu {
|
|||
+ (self.read(IRQ_VECTOR) as usize); // and low byte
|
||||
self.clock += 7;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,6 +15,7 @@ pub struct CpuData {
|
|||
delay: usize,
|
||||
strobe: u8,
|
||||
button_states: u8,
|
||||
button_states2: u8,
|
||||
button_number: u8,
|
||||
mode_table: Vec<Mode>,
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,5 +92,4 @@ impl super::Cpu {
|
|||
self.p &= 0xFF - ZERO_FLAG;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {}
|
163
src/input.rs
163
src/input.rs
|
@ -1,25 +1,148 @@
|
|||
use sdl2::keyboard::Scancode;
|
||||
use std::collections::HashSet;
|
||||
use crate::{Rng, DISABLE_START_PRESSES_AFTER, MUTATION_RATE, MUTATION_RATE_SOFT_RESET};
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::BufRead;
|
||||
|
||||
pub fn poll_buttons(strobe: &u8, event_pump: &sdl2::EventPump) -> Option<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;
|
||||
}
|
||||
}
|
||||
Some(button_states)
|
||||
} else {
|
||||
None
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
474
src/main.rs
474
src/main.rs
|
@ -1,255 +1,319 @@
|
|||
mod cpu;
|
||||
mod ppu;
|
||||
#![feature(total_cmp)]
|
||||
|
||||
mod apu;
|
||||
mod cartridge;
|
||||
mod input;
|
||||
mod screen;
|
||||
mod audio;
|
||||
mod cartridge;
|
||||
mod cpu;
|
||||
mod fuzzing_state;
|
||||
mod input;
|
||||
mod ppu;
|
||||
mod screen;
|
||||
mod state;
|
||||
|
||||
use cpu::Cpu;
|
||||
use ppu::Ppu;
|
||||
use apu::Apu;
|
||||
use cartridge::{check_signature, get_mapper};
|
||||
use input::poll_buttons;
|
||||
use screen::{init_window, draw_pixel, draw_to_window};
|
||||
use state::{save_state, load_state, find_next_filename, find_last_save_state};
|
||||
use ppu::Ppu;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Instant, Duration};
|
||||
use crate::fuzzing_state::{FuzzingInputState, FuzzingState};
|
||||
use crate::input::{ConsoleAction, FuzzingInput};
|
||||
use crate::screen::Screen;
|
||||
use minifb::{ScaleMode, Window, WindowOptions};
|
||||
use priority_queue::priority_queue::PriorityQueue;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
use sdl2::Sdl;
|
||||
use sdl2::render::{Canvas, Texture};
|
||||
use sdl2::keyboard::Keycode;
|
||||
use sdl2::EventPump;
|
||||
use sdl2::event::Event;
|
||||
use sdl2::pixels::PixelFormatEnum;
|
||||
use sdl2::video::Window;
|
||||
use sdl2::messagebox::*;
|
||||
// The number of cpu instances to spawn..
|
||||
const NUM_THREADS: usize = 28;
|
||||
|
||||
// use cpuprofiler::PROFILER;
|
||||
// The number of frames to fuzz and process
|
||||
// A small number exploits the current point more at the expense of
|
||||
// large exploration - and vice versa.
|
||||
const FRAMES_TO_CONSIDER: usize = 400;
|
||||
|
||||
enum GameExitMode {
|
||||
QuitApplication,
|
||||
NewGame(String),
|
||||
Reset,
|
||||
Nothing,
|
||||
}
|
||||
// Same input should generate the same output...
|
||||
// (I make no guarantee of that at the moment)
|
||||
const RNG_SEED: u32 = 0x5463753;
|
||||
|
||||
// If set to a low number, this disables start presses after the given frame
|
||||
// Useful for some games where pausing does nothing to advance the game...
|
||||
const DISABLE_START_PRESSES_AFTER: usize = 50;
|
||||
|
||||
// The rate at which seed inputs become corrupted..
|
||||
const MUTATION_RATE: f64 = 0.1;
|
||||
|
||||
// The rate at which seed inputs may become soft resets..
|
||||
const MUTATION_RATE_SOFT_RESET: f64 = 0.000;
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
// Set up screen
|
||||
let sdl_context = sdl2::init()?;
|
||||
let mut event_pump = sdl_context.event_pump()?;
|
||||
let (mut canvas, texture_creator) = init_window(&sdl_context).expect("Could not create window");
|
||||
let mut texture = texture_creator.create_texture_streaming(
|
||||
PixelFormatEnum::RGB24, 256*screen::SCALE_FACTOR as u32, 240*screen::SCALE_FACTOR as u32)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let byte_width = 256 * 3 * screen::SCALE_FACTOR; // 256 NES pixels, 3 bytes for each pixel (RGB 24-bit), and NES-to-SDL scale factor
|
||||
let byte_height = 240 * screen::SCALE_FACTOR; // NES image is 240 pixels tall, multiply by scale factor for total number of rows needed
|
||||
let mut screen_buffer = vec![0; byte_width * byte_height]; // contains raw RGB data for the screen
|
||||
|
||||
let argv = std::env::args().collect::<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),
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
name
|
||||
};
|
||||
|
||||
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);
|
||||
window.set_position(10 + ((i % 7) * 260), (240 * (i / 7)) + 100);
|
||||
window
|
||||
}
|
||||
_ => {
|
||||
panic!("unable to acquire mutex")
|
||||
}
|
||||
};
|
||||
|
||||
// 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;
|
||||
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 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);
|
||||
let (mut cpu, mut frames) = fuzzing_state.load_state(filename.clone());
|
||||
|
||||
// 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 {
|
||||
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 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
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
// 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),
|
||||
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 {
|
||||
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 => (),
|
||||
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();
|
||||
}
|
||||
// handle keyboard events
|
||||
match poll_buttons(&cpu.strobe, &event_pump) {
|
||||
Some(button_states) => cpu.button_states = button_states,
|
||||
None => (),
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
// 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),
|
||||
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?
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
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,
|
||||
};
|
||||
|
@ -71,7 +70,8 @@ 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
|
||||
|
@ -79,8 +79,9 @@ impl super::Ppu {
|
|||
// 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,7 +204,6 @@ 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) {
|
||||
|
|
|
@ -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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
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
|
||||
|
@ -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;
|
||||
|
@ -261,8 +259,68 @@ 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),
|
||||
(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),
|
||||
];
|
||||
|
|
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -207,7 +211,6 @@ impl super::Ppu {
|
|||
}
|
||||
}
|
||||
self.num_sprites = sprite_count;
|
||||
|
||||
}
|
||||
|
||||
pub fn fetch_sprites(&mut self) {
|
||||
|
@ -230,7 +233,11 @@ 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 = 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
|
||||
|
@ -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,7 +279,8 @@ impl super::Ppu {
|
|||
}
|
||||
|
||||
pub fn inc_coarse_x(&mut self) {
|
||||
if self.v & 0x001F == 0x001F { // if coarse X == 31
|
||||
if self.v & 0x001F == 0x001F {
|
||||
// if coarse X == 31
|
||||
self.v &= !0x001F; // coarse X = 0
|
||||
self.v ^= 1 << 10; // switch horizontal nametable
|
||||
} else {
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
24
src/state.rs
24
src/state.rs
|
@ -1,7 +1,7 @@
|
|||
use super::cpu;
|
||||
use super::ppu;
|
||||
use super::apu;
|
||||
use super::cartridge;
|
||||
use super::cpu;
|
||||
use super::ppu;
|
||||
|
||||
use std::fs::{DirEntry, File};
|
||||
use std::io::{Read, Write};
|
||||
|
@ -20,12 +20,10 @@ pub fn save_state(cpu: &cpu::Cpu, save_file: &PathBuf) -> Result<(), String> {
|
|||
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(¤t_name);
|
||||
if !save_file.exists() {
|
||||
return Some(save_file)
|
||||
return Some(save_file);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue