Browse Source

Initial Commit of nesfuzz

trunk
Sarah Jamie Lewis 9 months ago
parent
commit
f4b677d52a
  1. 6
      .gitignore
  2. 13
      Cargo.toml
  3. 91
      README.md
  4. BIN
      fuzzimages/screenshot.png
  5. 24
      src/apu/dmc.rs
  6. 4
      src/apu/envelope.rs
  7. 67
      src/apu/mod.rs
  8. 4
      src/apu/noise.rs
  9. 2
      src/apu/serialize.rs
  10. 32
      src/apu/square.rs
  11. 8
      src/apu/triangle.rs
  12. 132
      src/audio.rs
  13. 25
      src/cartridge/cnrom.rs
  14. 81
      src/cartridge/mmc1.rs
  15. 151
      src/cartridge/mmc3.rs
  16. 43
      src/cartridge/mod.rs
  17. 35
      src/cartridge/nrom.rs
  18. 15
      src/cartridge/serialize.rs
  19. 31
      src/cartridge/uxrom.rs
  20. 16
      src/cpu/addressing_modes.rs
  21. 735
      src/cpu/mod.rs
  22. 44
      src/cpu/opcodes.rs
  23. 11
      src/cpu/serialize.rs
  24. 10
      src/cpu/utility.rs
  25. 63
      src/fuzzing_state.rs
  26. 165
      src/input.rs
  27. 512
      src/main.rs
  28. 70
      src/ppu/cpu_registers.rs
  29. 81
      src/ppu/memory.rs
  30. 262
      src/ppu/mod.rs
  31. 93
      src/ppu/rendering.rs
  32. 8
      src/ppu/serialize.rs
  33. 76
      src/screen.rs
  34. 28
      src/state.rs

6
.gitignore

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

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"

91
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;
// The number of frames to fuzz and process
// A small number exploits the current point more at the expense of
// large exploration - and vice versa.
const FRAMES_TO_CONSIDER: usize = 400;
// Same input should generate the same output...
// (I make no guarantee of that at the moment)
const RNG_SEED: u32 = 0x5463753;
// If set to a low number, this disables start presses after the given frame
// Useful for some games where pausing does nothing to advance the game...
const DISABLE_START_PRESSES_AFTER: usize = 50;
// The rate at which seed inputs become corrupted..
const MUTATION_RATE: f64 = 0.1;
// The rate at which seed inputs may become soft resets..
const MUTATION_RATE_SOFT_RESET: f64 = 0.000;
## To do
- support other controllers?
## Known Issues
- more mappers?
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.
- better save file organization?
## Known problem games
- None currently, please report any issues
Please also check out [Cloaker](https://github.com/spieglt/cloaker) and [Flying Carpet](https://github.com/spieglt/flyingcarpet)!
Other games like Legend of Zelda, Megaman, Super Mario Bros. 3, Final Fantasy II etc. will run, but I have had any
tas inputs from them quickly become out of sync with the actual gameplay. Further research is needed to as to why
that is. Help appreciated.

BIN
fuzzimages/screenshot.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

24
src/apu/dmc.rs

@ -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,
}
}
@ -133,18 +143,18 @@ impl DMC {
self.loop_flag = value & 0b0100_0000 != 0;
self.rate_index = value as usize & 0b0000_1111;
}
pub fn direct_load(&mut self, value: u8) {
// $4011 -DDD.DDDD Direct load (write)
self.sample = value as u16 & 0b0111_1111;
}
pub fn write_sample_address(&mut self, value: u8) {
// $4012 AAAA.AAAA Sample address (write)
// bits 7-0 AAAA.AAAA Sample address = %11AAAAAA.AA000000 = $C000 + (A * 64)
self.sample_address = ((value as usize) << 6) + 0xC000;
}
pub fn write_sample_length(&mut self, value: u8) {
// $4013 LLLL.LLLL Sample length (write)
// bits 7-0 LLLL.LLLL Sample length = %LLLL.LLLL0001 = (L * 16) + 1 bytes

4
src/apu/envelope.rs

@ -2,8 +2,8 @@
pub struct Envelope {
pub period: u16, // constant volume/envelope period
divider: u16,
pub decay_counter: u16, // remainder of envelope divider
pub start: bool, // restarts envelope
pub decay_counter: u16, // remainder of envelope divider
pub start: bool, // restarts envelope
pub length_counter_halt: bool, // also the envelope loop flag
}

67
src/apu/mod.rs

@ -1,14 +1,14 @@
mod noise;
mod square;
mod triangle;
mod dmc;
mod envelope;
mod noise;
pub mod serialize;
mod square;
mod triangle;
use dmc::DMC;
use noise::Noise;
use square::Square;
use triangle::Triangle;
use dmc::DMC;
// APU clock ticks every other CPU cycle.
// Frame counter only ticks every 3728.5 APU ticks, and in audio frames of 4 or 5.
@ -16,17 +16,17 @@ use dmc::DMC;
const FRAME_COUNTER_STEPS: [usize; 5] = [3728, 7456, 11185, 14914, 18640];
const LENGTH_COUNTER_TABLE: [u8; 32] = [
10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14,
12, 16, 24, 18, 48, 20, 96, 22, 192, 24, 72, 26, 16, 28, 32, 30,
10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 12, 16, 24, 18, 48, 20, 96, 22,
192, 24, 72, 26, 16, 28, 32, 30,
];
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct Apu {
square1: Square,
square2: Square,
square1: Square,
square2: Square,
triangle: Triangle,
noise: Noise,
pub dmc: DMC,
noise: Noise,
pub dmc: DMC,
square_table: Vec<f32>,
tnd_table: Vec<f32>,
@ -41,14 +41,18 @@ pub struct Apu {
impl Apu {
pub fn new() -> Self {
let square_table = (0..31).map(|x| 95.52/((8128.0 / x as f32) + 100.0)).collect();
let tnd_table = (0..203).map(|x| 163.67/((24329.0 / x as f32) + 100.0)).collect();
let square_table = (0..31)
.map(|x| 95.52 / ((8128.0 / x as f32) + 100.0))
.collect();
let tnd_table = (0..203)
.map(|x| 163.67 / ((24329.0 / x as f32) + 100.0))
.collect();
Apu {
square1: Square::new(true),
square2: Square::new(false),
square1: Square::new(true),
square2: Square::new(false),
triangle: Triangle::new(),
noise: Noise::new(),
dmc: DMC::new(),
noise: Noise::new(),
dmc: DMC::new(),
square_table: square_table,
tnd_table: tnd_table,
@ -86,7 +90,8 @@ impl Apu {
fn mix(&self) -> f32 {
let square_out = self.square_table[(self.square1.sample + self.square2.sample) as usize];
let tnd_out = self.tnd_table[((3*self.triangle.sample)+(2*self.noise.sample) + self.dmc.sample) as usize];
let tnd_out = self.tnd_table
[((3 * self.triangle.sample) + (2 * self.noise.sample) + self.dmc.sample) as usize];
square_out + tnd_out
}
@ -160,31 +165,31 @@ impl Apu {
// Writing to this register clears the DMC interrupt flag.
self.dmc.interrupt = false;
// Writing a zero to any of the channel enable bits will silence that channel and immediately set its length counter to 0.
if value & (1<<0) != 0 {
if value & (1 << 0) != 0 {
self.square1.enabled = true;
} else {
self.square1.enabled = false;
self.square1.length_counter = 0;
}
if value & (1<<1) != 0 {
if value & (1 << 1) != 0 {
self.square2.enabled = true;
} else {
self.square2.enabled = false;
self.square2.length_counter = 0;
}
if value & (1<<2) != 0 {
if value & (1 << 2) != 0 {
self.triangle.enabled = true;
} else {
self.triangle.enabled = false;
self.triangle.length_counter = 0;
}
if value & (1<<3) != 0 {
if value & (1 << 3) != 0 {
self.noise.enabled = true;
} else {
self.noise.enabled = false;
self.noise.length_counter = 0;
}
if value & (1<<4) != 0 {
if value & (1 << 4) != 0 {
self.dmc.enabled = true;
// If the DMC bit is set, the DMC sample will be restarted only if its bytes remaining is 0.
// If there are bits remaining in the 1-byte sample buffer, these will finish playing before the next sample is fetched.
@ -205,26 +210,26 @@ impl Apu {
let mut val = 0;
// N/T/2/1 will read as 1 if the corresponding length counter is greater than 0. For the triangle channel, the status of the linear counter is irrelevant.
if self.square1.length_counter != 0 {
val |= 1<<0;
val |= 1 << 0;
}
if self.square2.length_counter != 0 {
val |= 1<<1;
val |= 1 << 1;
}
if self.triangle.length_counter != 0 {
val |= 1<<2;
val |= 1 << 2;
}
if self.noise.length_counter != 0 {
val |= 1<<3;
val |= 1 << 3;
}
// D will read as 1 if the DMC bytes remaining is more than 0.
if self.dmc.bytes_remaining != 0 {
val |= 1<<4;
val |= 1 << 4;
}
if self.frame_interrupt {
val |= 1<<6;
val |= 1 << 6;
}
if self.dmc.interrupt {
val |= 1<<7;
val |= 1 << 7;
}
// Reading this register clears the frame interrupt flag (but not the DMC interrupt flag).
self.frame_interrupt = false;
@ -235,9 +240,9 @@ impl Apu {
// $4017
fn write_frame_counter(&mut self, value: u8) {
// 0 selects 4-step sequence, 1 selects 5-step sequence
self.frame_sequence = if value & (1<<7) == 0 { 4 } else { 5 };
self.frame_sequence = if value & (1 << 7) == 0 { 4 } else { 5 };
// If set, the frame interrupt flag is cleared, otherwise it is unaffected.
if value & (1<<6) != 0 {
if value & (1 << 6) != 0 {
self.interrupt_inhibit = false;
}
// If the mode flag is set, then both "quarter frame" and "half frame" signals are also generated.

4
src/apu/noise.rs

@ -1,6 +1,8 @@
use super::envelope::Envelope;
const NOISE_TABLE: [u16; 16] = [4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068];
const NOISE_TABLE: [u16; 16] = [
4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068,
];
// $400E M---.PPPP Mode and period (write)
// bit 7 M--- ---- Mode flag

2
src/apu/serialize.rs

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

32
src/apu/square.rs

@ -12,7 +12,7 @@ pub struct Square {
pub sample: u16, // output value that gets sent to the mixer
pub enabled: bool,
constant_volume_flag: bool, // (0: use volume from envelope; 1: use constant volume)
first_channel: bool, // hack to detect timing difference in clock_sweep()
first_channel: bool, // hack to detect timing difference in clock_sweep()
timer: u16,
timer_period: u16,
@ -68,14 +68,15 @@ impl Square {
self.sample = if self.duty_cycle[self.duty_counter] == 0 // the sequencer output is zero, or
|| self.timer_period > 0x7FF // overflow from the sweep unit's adder is silencing the channel,
|| self.length_counter == 0 // the length counter is zero, or
|| self.timer_period < 8 // the timer has a value less than eight.
{
0
} else if self.constant_volume_flag {
self.envelope.period
} else {
self.envelope.decay_counter
};
|| self.timer_period < 8
// the timer has a value less than eight.
{
0
} else if self.constant_volume_flag {
self.envelope.period
} else {
self.envelope.decay_counter
};
}
pub fn clock_length_counter(&mut self) {
@ -88,14 +89,19 @@ impl Square {
self.calculate_target_period();
// When the frame counter sends a half-frame clock (at 120 or 96 Hz), two things happen.
// If the divider's counter is zero, the sweep is enabled, and the sweep unit is not muting the channel: The pulse's period is adjusted.
if self.sweep_counter == 0 && self.sweep_enabled && !(self.timer_period < 8 || self.target_period > 0x7FF) {
if self.sweep_counter == 0
&& self.sweep_enabled
&& !(self.timer_period < 8 || self.target_period > 0x7FF)
{
self.timer_period = self.target_period;
}
// If the divider's counter is zero or the reload flag is true: The counter is set to P and the reload flag is cleared. Otherwise, the counter is decremented.
if self.sweep_counter == 0 || self.sweep_reload {
self.sweep_counter = self.sweep_period;
self.sweep_reload = false;
if self.sweep_enabled { self.timer_period = self.target_period; } // This fixes the DK walking sound. Why? Not reflected in documentation.
if self.sweep_enabled {
self.timer_period = self.target_period;
} // This fixes the DK walking sound. Why? Not reflected in documentation.
} else {
self.sweep_counter -= 1;
}
@ -126,8 +132,8 @@ impl Square {
pub fn write_duty(&mut self, value: u8) {
// The duty cycle is changed (see table below), but the sequencer's current position isn't affected.
self.duty_cycle = DUTY_CYCLE_SEQUENCES[(value >> 6) as usize];
self.envelope.length_counter_halt = value & (1<<5) != 0;
self.constant_volume_flag = value & (1<<4) != 0;
self.envelope.length_counter_halt = value & (1 << 5) != 0;
self.constant_volume_flag = value & (1 << 4) != 0;
self.envelope.period = value as u16 & 0b1111;
}

8
src/apu/triangle.rs

@ -1,6 +1,6 @@
const WAVEFORM: [u16; 32] = [
15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15,
];
#[derive(serde::Serialize, serde::Deserialize, Clone)]
@ -53,7 +53,8 @@ impl Triangle {
// If the linear counter reload flag is set, the linear counter is reloaded with the counter reload value,
if self.linear_counter_reload {
self.linear_counter = self.counter_reload_value;
} else if self.linear_counter != 0 { // otherwise if the linear counter is non-zero, it is decremented.
} else if self.linear_counter != 0 {
// otherwise if the linear counter is non-zero, it is decremented.
self.linear_counter -= 1;
}
// If the control flag is clear, the linear counter reload flag is cleared.
@ -90,5 +91,4 @@ impl Triangle {
self.timer_period |= (timer_high as u16) << 8;
self.linear_counter_reload = true;
}
}

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[i1] + gamma * (x[i] x[i1])
*/

25
src/cartridge/cnrom.rs

@ -1,4 +1,4 @@
use super::{Cartridge, Mapper, Mirror, serialize::*};
use super::{serialize::*, Cartridge, Mapper, Mirror};
pub struct Cnrom {
cart: Cartridge,
@ -7,7 +7,7 @@ pub struct Cnrom {
impl Cnrom {
pub fn new(cart: Cartridge) -> Self {
Cnrom{
Cnrom {
cart: cart,
chr_bank_select: 0,
}
@ -21,8 +21,11 @@ impl Mapper for Cnrom {
match address {
0x0000..=0x1FFF => self.cart.chr_rom[self.chr_bank_select][address],
0x8000..=0xBFFF => self.cart.prg_rom[0][addr],
0xC000..=0xFFFF => self.cart.prg_rom[pl-1][addr],
_ => {println!("bad address read from CNROM mapper: 0x{:X}", address); 0},
0xC000..=0xFFFF => self.cart.prg_rom[pl - 1][addr],
_ => {
println!("bad address read from CNROM mapper: 0x{:X}", address);
0
}
}
}
@ -40,15 +43,15 @@ impl Mapper for Cnrom {
fn load_battery_backed_ram(&mut self) {}
fn save_battery_backed_ram(&self) {}
fn clock(&mut self) {}
fn check_irq(&mut self) -> bool {false}
fn check_irq(&mut self) -> bool {
false
}
fn save_state(&self) -> MapperData {
MapperData::Cnrom(
CnromData {
cart: self.cart.clone(),
chr_bank_select: self.chr_bank_select,
}
)
MapperData::Cnrom(CnromData {
cart: self.cart.clone(),
chr_bank_select: self.chr_bank_select,
})
}
fn load_state(&mut self, mapper_data: MapperData) {

81
src/cartridge/mmc1.rs

@ -1,4 +1,4 @@
use super::{Cartridge, Mapper, Mirror, serialize::*};
use super::{serialize::*, Cartridge, Mapper, Mirror};
use std::fs::File;
use std::io::{Read, Write};
@ -85,13 +85,15 @@ impl Mmc1 {
_ => panic!("invalid mirroring value"),
};
self.prg_bank_mode = (value >> 2) & 0b11;
self.chr_bank_mode = if value & (1<<4) == 0 {false} else {true};
self.chr_bank_mode = if value & (1 << 4) == 0 { false } else { true };
}
fn write_chr_bank_low(&mut self, value: u8) {
if self.chr_bank_mode { // 4 KB mode
if self.chr_bank_mode {
// 4 KB mode
self.chr_low_bank = value as usize;
} else { // 8 KB mode
} else {
// 8 KB mode
let v = value & (0xFF - 1); // turn off low bit
self.chr_low_bank = v as usize;
self.chr_high_bank = (v + 1) as usize;
@ -99,7 +101,8 @@ impl Mmc1 {
}
fn write_chr_bank_high(&mut self, value: u8) {
if self.chr_bank_mode { // 4 KB mode only, ignored in 8 KB mode
if self.chr_bank_mode {
// 4 KB mode only, ignored in 8 KB mode
self.chr_high_bank = value as usize;
}
}
@ -127,48 +130,51 @@ impl Mapper for Mmc1 {
_ => panic!("bad address read from MMC1: 0x{:X}", address),
};
let chunk_num = bank / 2;
let chunk_half = if bank % 2 == 0 {0x0} else {0x1000};
let chunk_half = if bank % 2 == 0 { 0x0 } else { 0x1000 };
self.cart.chr_rom[chunk_num][chunk_half + offset]
} else {
// if we're in 8K bank mode, the whole $0000-$1FFF region will be the 8K range referred to by chr_low_bank
self.cart.chr_rom[self.chr_low_bank][address]
}
}
},
}
0x6000..=0x7FFF => self.prg_ram_bank[address % 0x2000],
0x8000..=0xBFFF => {
match self.prg_bank_mode {
0 | 1 => { // switch 32 KB at $8000, ignoring low bit of bank number
0 | 1 => {
// switch 32 KB at $8000, ignoring low bit of bank number
let low_bank = self.prg_bank_select & (0xFF - 1);
self.cart.prg_rom[low_bank][address % 0x4000]
},
}
2 => self.cart.prg_rom[0][address % 0x4000],
3 => self.cart.prg_rom[self.prg_bank_select][address % 0x4000],
_ => panic!("invalid PRG bank mode"),
}
},
}
0xC000..=0xFFFF => {
match self.prg_bank_mode {
0 | 1 => { // switch 32 KB at $8000, ignoring low bit of bank number
0 | 1 => {
// switch 32 KB at $8000, ignoring low bit of bank number
let high_bank = (self.prg_bank_select & (0xFF - 1)) + 1;
self.cart.prg_rom[high_bank][address % 0x4000]
},
}
2 => self.cart.prg_rom[self.prg_bank_select][address % 0x4000],
3 => self.cart.prg_rom[self.cart.prg_rom_size - 1][address % 0x4000],
_ => panic!("invalid PRG bank mode"),
}
},
}
_ => panic!("invalid address passed to MMC1: 0x{:X}", address),
}
}
fn write(&mut self, address: usize, value: u8) {
match address {
0x0000..=0x1FFF => { // if we don't have CHR-ROM, write to CHR-RAM
0x0000..=0x1FFF => {
// if we don't have CHR-ROM, write to CHR-RAM
if self.cart.chr_rom_size == 0 {
self.chr_ram_bank[address] = value;
}
},
}
0x6000..=0x7FFF => self.prg_ram_bank[address % 0x2000] = value,
0x8000..=0xFFFF => self.write_serial_port(address, value),
_ => panic!("bad address write to MMC1: 0x{:X}", address),
@ -186,9 +192,11 @@ impl Mapper for Mmc1 {
let mut save_file = p.join(stem);
save_file.set_extension("sav");
if Path::new(&save_file).exists() {
let mut f = File::open(save_file.clone()).expect("save file exists but could not open it");
let mut f =
File::open(save_file.clone()).expect("save file exists but could not open it");
let mut battery_backed_ram_data = vec![];
f.read_to_end(&mut battery_backed_ram_data).expect("error reading save file");
f.read_to_end(&mut battery_backed_ram_data)
.expect("error reading save file");
println!("loading battery-backed RAM from file: {:?}", save_file);
self.prg_ram_bank = battery_backed_ram_data;
}
@ -204,31 +212,32 @@ impl Mapper for Mmc1 {
println!("saving battery-backed RAM to file: {:?}", save_file);
let mut f = File::create(&save_file)
.expect("could not create output file for battery-backed RAM");
f.write_all(&self.prg_ram_bank).expect("could not write battery-backed RAM to file");
f.write_all(&self.prg_ram_bank)
.expect("could not write battery-backed RAM to file");
}
}
fn clock(&mut self) {}
fn check_irq(&mut self) -> bool {false}
fn check_irq(&mut self) -> bool {
false
}
fn save_state(&self) -> MapperData {
MapperData::Mmc1(
Mmc1Data {
cart: self.cart.clone(),
step: self.step,
shift_register: self.shift_register,
mirroring: self.mirroring,
control: self.control,
prg_ram_bank: self.prg_ram_bank.clone(),
prg_ram_enabled: self.prg_ram_enabled,
prg_bank_mode: self.prg_bank_mode,
prg_bank_select: self.prg_bank_select,
chr_ram_bank: self.chr_ram_bank.clone(),
chr_low_bank: self.chr_low_bank,
chr_high_bank: self.chr_high_bank,
chr_bank_mode: self.chr_bank_mode,
}
)
MapperData::Mmc1(Mmc1Data {
cart: self.cart.clone(),
step: self.step,
shift_register: self.shift_register,
mirroring: self.mirroring,
control: self.control,
prg_ram_bank: self.prg_ram_bank.clone(),
prg_ram_enabled: self.prg_ram_enabled,
prg_bank_mode: self.prg_bank_mode,
prg_bank_select: self.prg_bank_select,
chr_ram_bank: self.chr_ram_bank.clone(),
chr_low_bank: self.chr_low_bank,
chr_high_bank: self.chr_high_bank,
chr_bank_mode: self.chr_bank_mode,
})
}
fn load_state(&mut self, mapper_data: MapperData) {

151
src/cartridge/mmc3.rs

@ -1,4 +1,4 @@
use super::{Cartridge, Mapper, Mirror, serialize::*};
use super::{serialize::*, Cartridge, Mapper, Mirror};
pub struct Mmc3 {
cart: Cartridge,
@ -20,7 +20,6 @@ pub struct Mmc3 {
prg_rom_bank_mode: bool,
// 0: two 2 KB banks at $0000-$0FFF, four 1 KB banks at $1000-$1FFF
// 1: two 2 KB banks at $1000-$1FFF, four 1 KB banks at $0000-$0FFF
chr_rom_bank_mode: bool,
chr_ram_bank: Vec<u8>, // used if cartridge doesn't have any CHR-ROM, 8KB, $0000-$1FFF
}
@ -28,7 +27,7 @@ pub struct Mmc3 {
impl Mmc3 {
pub fn new(cart: Cartridge) -> Self {
let m = cart.mirroring;
Mmc3{
Mmc3 {
cart: cart,
mirroring: m,
bank_registers: vec![0, 0, 0, 0, 0, 0, 0, 0],
@ -49,8 +48,8 @@ impl Mmc3 {
fn bank_select(&mut self, value: u8) {
self.next_bank = value & 0b111;
// ?? = value & (1<<5); // Nothing on the MMC3, see MMC6
self.prg_rom_bank_mode = value & (1<<6) != 0;
self.chr_rom_bank_mode = value & (1<<7) != 0;
self.prg_rom_bank_mode = value & (1 << 6) != 0;
self.chr_rom_bank_mode = value & (1 << 7) != 0;
}
fn bank_data(&mut self, value: u8) {
@ -69,77 +68,72 @@ impl Mmc3 {
impl Mapper for Mmc3 {
fn read(&self, address: usize) -> u8 {
let val = match address {
0x0000..=0x1FFF => { // reading from CHR-ROM
0x0000..=0x1FFF => {
// reading from CHR-ROM
let offset_1k = address % 0x400;
let offset_2k = address % 0x800;
let bank_reg_num = match self.chr_rom_bank_mode {
true => {
match address {
0x0000..=0x03FF => 2,
0x0400..=0x07FF => 3,
0x0800..=0x0BFF => 4,
0x0C00..=0x0FFF => 5,
0x1000..=0x17FF => 0,
0x1800..=0x1FFF => 1,
_ => panic!("oh no"),
}
true => match address {
0x0000..=0x03FF => 2,
0x0400..=0x07FF => 3,
0x0800..=0x0BFF => 4,
0x0C00..=0x0FFF => 5,
0x1000..=0x17FF => 0,
0x1800..=0x1FFF => 1,
_ => panic!("oh no"),
},
false => {
match address {
0x0000..=0x07FF => 0,
0x0800..=0x0FFF => 1,
0x1000..=0x13FF => 2,
0x1400..=0x17FF => 3,
0x1800..=0x1BFF => 4,
0x1C00..=0x1FFF => 5,
_ => panic!("oh no"),
}
false => match address {
0x0000..=0x07FF => 0,
0x0800..=0x0FFF => 1,
0x1000..=0x13FF => 2,
0x1400..=0x17FF => 3,
0x1800..=0x1BFF => 4,
0x1C00..=0x1FFF => 5,
_ => panic!("oh no"),
},
};
let bank_num = self.bank_registers[bank_reg_num];
let chunk_num = bank_num / 8;
let chunk_eighth = (bank_num % 8) * 0x400;
if bank_reg_num == 0 || bank_reg_num == 1 { // dealing with 2K banks of 8K chunks
if bank_reg_num == 0 || bank_reg_num == 1 {
// dealing with 2K banks of 8K chunks
self.cart.chr_rom[chunk_num][chunk_eighth + offset_2k]
} else { // dealing with 1K banks of 8K chunks
} else {
// dealing with 1K banks of 8K chunks
self.cart.chr_rom[chunk_num][chunk_eighth + offset_1k]
}
},
}
0x6000..=0x7FFF => self.prg_ram_bank[address % 0x2000], // PRG-RAM
0x8000..=0xFFFF => { // reading from PRG ROM, dealing with 8K banks of 16K chunks
0x8000..=0xFFFF => {
// reading from PRG ROM, dealing with 8K banks of 16K chunks
let offset_8k = address % 0x2000;
let num_banks = self.cart.prg_rom_size * 2;
let bank_num = match self.prg_rom_bank_mode {
true => {
match address {
0x8000..=0x9FFF => num_banks - 2,
0xA000..=0xBFFF => self.bank_registers[7],
0xC000..=0xDFFF => self.bank_registers[6],
0xE000..=0xFFFF => num_banks - 1,
_ => panic!("oh no"),
}
true => match address {
0x8000..=0x9FFF => num_banks - 2,
0xA000..=0xBFFF => self.bank_registers[7],
0xC000..=0xDFFF => self.bank_registers[6],
0xE000..=0xFFFF => num_banks - 1,
_ => panic!("oh no"),
},
false => {
match address {
0x8000..=0x9FFF => self.bank_registers[6],
0xA000..=0xBFFF => self.bank_registers[7],
0xC000..=0xDFFF => num_banks - 2,
0xE000..=0xFFFF => num_banks - 1,
_ => panic!("oh no"),
}
false => match address {
0x8000..=0x9FFF => self.bank_registers[6],
0xA000..=0xBFFF => self.bank_registers[7],
0xC000..=0xDFFF => num_banks - 2,
0xE000..=0xFFFF => num_banks - 1,
_ => panic!("oh no"),
},
};
let chunk_num = bank_num / 2;
let chunk_half = (bank_num % 2) * 0x2000;
self.cart.prg_rom[chunk_num][chunk_half + offset_8k]
},
}
_ => {
println!("bad address read from MMC3: 0x{:X}", address);
0
},
}
};
val
}
@ -149,20 +143,31 @@ impl Mapper for Mmc3 {
if self.cart.chr_rom_size == 0 {
self.chr_ram_bank[address] = value;
}
return
return;
}
match address % 2 == 0 {
true => { // even
true => {
// even
match address {
0x6000..=0x7FFF => self.prg_ram_bank[address % 0x2000] = value, // PRG-RAM
0x8000..=0x9FFF => self.bank_select(value),
0xA000..=0xBFFF => self.mirroring = if value & 1 == 0 {Mirror::Vertical} else {Mirror::Horizontal},
0xA000..=0xBFFF => {
self.mirroring = if value & 1 == 0 {
Mirror::Vertical
} else {
Mirror::Horizontal
}
}
0xC000..=0xDFFF => self.irq_latch = value,
0xE000..=0xFFFF => {self.irq_enable = false; self.trigger_irq = false}, // Writing any value to this register will disable MMC3 interrupts AND acknowledge any pending interrupts.
0xE000..=0xFFFF => {
self.irq_enable = false;
self.trigger_irq = false
} // Writing any value to this register will disable MMC3 interrupts AND acknowledge any pending interrupts.
_ => println!("bad address written to MMC3: 0x{:X}", address),
}
},
false => { // odd
}
false => {
// odd
match address {
0x6000..=0x7FFF => self.prg_ram_bank[address % 0x2000] = value, // PRG-RAM
0x8000..=0x9FFF => self.bank_data(value),
@ -171,7 +176,7 @@ impl Mapper for Mmc3 {
0xE000..=0xFFFF => self.irq_enable = true,
_ => println!("bad address written to MMC3: 0x{:X}", address),
}
},
}
}
}
@ -224,24 +229,22 @@ impl Mapper for Mmc3 {
}
fn save_state(&self) -> MapperData {
MapperData::Mmc3(
Mmc3Data {
cart: self.cart.clone(),
mirroring: self.mirroring,
bank_registers: self.bank_registers.clone(),
next_bank: self.next_bank,
irq_latch: self.irq_latch,
irq_counter: self.irq_counter,
irq_enable: self.irq_enable,
trigger_irq: self.trigger_irq,
reload_counter: self.reload_counter,
irq_delay: self.irq_delay,
prg_ram_bank: self.prg_ram_bank.clone(),
prg_rom_bank_mode: self.prg_rom_bank_mode,
chr_rom_bank_mode: self.chr_rom_bank_mode,
chr_ram_bank: self.chr_ram_bank.clone(),
}
)
MapperData::Mmc3(Mmc3Data {
cart: self.cart.clone(),
mirroring: self.mirroring,
bank_registers: self.bank_registers.clone(),
next_bank: self.next_bank,
irq_latch: self.irq_latch,
irq_counter: self.irq_counter,
irq_enable: self.irq_enable,
trigger_irq: self.trigger_irq,
reload_counter: self.reload_counter,
irq_delay: self.irq_delay,
prg_ram_bank: self.prg_ram_bank.clone(),
prg_rom_bank_mode: self.prg_rom_bank_mode,
chr_rom_bank_mode: self.chr_rom_bank_mode,
chr_ram_bank: self.chr_ram_bank.clone(),
})
}
fn load_state(&mut self, mapper_data: MapperData) {

43
src/cartridge/mod.rs

@ -1,15 +1,15 @@
mod nrom;
mod mmc1;
mod uxrom;
mod cnrom;
mod mmc1;
mod mmc3;
mod nrom;
pub mod serialize;
mod uxrom;
use nrom::Nrom;
use mmc1::Mmc1;
use uxrom::Uxrom;
use cnrom::Cnrom;
use mmc1::Mmc1;
use mmc3::Mmc3;
use nrom::Nrom;
use uxrom::Uxrom;
use std::cell::RefCell;
use std::fs::File;
@ -55,12 +55,11 @@ pub struct Cartridge {
filename: String,
prg_rom_size: usize,
chr_rom_size: usize,
pub mirroring: Mirror, // 0 horizontal, 1 vertical
pub mirroring: Mirror, // 0 horizontal, 1 vertical
battery_backed_ram: bool, // 1: Cartridge contains battery-backed PRG RAM ($6000-7FFF) or other persistent memory
trainer_present: bool, // 1: 512-byte trainer at $7000-$71FF (stored before PRG data)
trainer_present: bool, // 1: 512-byte trainer at $7000-$71FF (stored before PRG data)
four_screen_vram: bool, // 1: Ignore mirroring control or above mirroring bit; instead provide four-screen VRAM
// TODO: other iNES header flags
pub prg_rom: Vec<Vec<u8>>, // 16 KiB chunks for CPU
pub chr_rom: Vec<Vec<u8>>, // 8 KiB chunks for PPU
@ -73,16 +72,23 @@ impl Cartridge {
let mut f = std::fs::File::open(&filename).expect("could not open {}");
let mut data = vec![];
f.read_to_end(&mut data).unwrap();
assert!(data[0..4] == [0x4E, 0x45, 0x53, 0x1A], "signature mismatch, not an iNES file");
assert!(
data[0..4] == [0x4E, 0x45, 0x53, 0x1A],
"signature mismatch, not an iNES file"
);
let mapper_num = ((data[7] >> 4) << 4) + (data[6] >> 4);
let mut cart = Cartridge {
filename: filename.to_string(),
prg_rom_size: data[4] as usize,
chr_rom_size: data[5] as usize,
mirroring: if data[6] & (1 << 0) == 0 {Mirror::Horizontal} else {Mirror::Vertical},
mirroring: if data[6] & (1 << 0) == 0 {
Mirror::Horizontal
} else {
Mirror::Vertical
},
battery_backed_ram: data[6] & (1 << 1) != 0,
trainer_present: data[6] & (1 << 2) != 0,
four_screen_vram: data[6] & (1 << 3) != 0,
trainer_present: data[6] & (1 << 2) != 0,
four_screen_vram: data[6] & (1 << 3) != 0,
prg_rom: Vec::new(),
chr_rom: Vec::new(),
all_data: data,
@ -93,24 +99,23 @@ impl Cartridge {
}
fn fill(&mut self) {
let prg_chunk_size: usize = 1<<14;
let chr_chunk_size: usize = 1<<13;
let prg_chunk_size: usize = 1 << 14;
let chr_chunk_size: usize = 1 << 13;
let prg_offset: usize = 0x10 + if self.trainer_present { 0x200 } else { 0 }; // header plus trainer if present
let chr_offset: usize = prg_offset + (self.prg_rom_size * prg_chunk_size); // chr comes after prg
// fill vecs with chunks
// fill vecs with chunks
for i in 0..self.prg_rom_size {
let offset = prg_offset + (i * prg_chunk_size);
let chunk = self.all_data[offset..(offset + prg_chunk_size)].to_vec();
self.prg_rom.push(chunk.clone());
};
}
for i in 0..self.chr_rom_size {
let offset = chr_offset + (i * chr_chunk_size);
let chunk = self.all_data[offset..offset + chr_chunk_size].to_vec();
self.chr_rom.push(chunk);
};
}
self.all_data.clear();
}
}
pub fn check_signature(filename: &str) -> Result<(), String> {

35
src/cartridge/nrom.rs

@ -1,4 +1,4 @@
use super::{Cartridge, Mapper, Mirror, serialize::*};
use super::{serialize::*, Cartridge, Mapper, Mirror};
pub struct Nrom {
cart: Cartridge,
@ -7,7 +7,7 @@ pub struct Nrom {
impl Nrom {
pub fn new(cart: Cartridge) -> Self {
Nrom{
Nrom {
cart: cart,
chr_ram: vec![0; 0x2000],
}
@ -24,14 +24,13 @@ impl Mapper for Nrom {
} else {
self.chr_ram[address]
}
},
0x8000..=0xBFFF => {
self.cart.prg_rom[0][addr]
},
0xC000..=0xFFFF => {
self.cart.prg_rom[self.cart.prg_rom_size - 1][addr]
},
_ => {println!("bad address read from NROM mapper: 0x{:X}", address); 0},
}
0x8000..=0xBFFF => self.cart.prg_rom[0][addr],
0xC000..=0xFFFF => self.cart.prg_rom[self.cart.prg_rom_size - 1][addr],
_ => {
println!("bad address read from NROM mapper: 0x{:X}", address);
0
}
}
}
@ -42,7 +41,7 @@ impl Mapper for Nrom {
if self.cart.chr_rom_size == 0 {
self.chr_ram[address] = value;
}
},
}
0x8000..=0xBFFF => (),
0xC000..=0xFFFF => (),
_ => println!("bad address written to NROM mapper: 0x{:X}", address),
@ -56,15 +55,15 @@ impl Mapper for Nrom {
fn load_battery_backed_ram(&mut self) {}
fn save_battery_backed_ram(&self) {}
fn clock(&mut self) {}
fn check_irq(&mut self) -> bool {false}
fn check_irq(&mut self) -> bool {
false
}
fn save_state(&self) -> MapperData {
MapperData::Nrom(
NromData {
cart: self.cart.clone(),
chr_ram: self.chr_ram.clone(),
}
)
MapperData::Nrom(NromData {
cart: self.cart.clone(),
chr_ram: self.chr_ram.clone(),
})
}
fn load_state(&mut self, mapper_data: MapperData) {

15
src/cartridge/serialize.rs

@ -1,6 +1,6 @@
use super::{Cartridge, Mirror};
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub enum MapperData {
Nrom(NromData),
Mmc1(Mmc1Data),
@ -9,14 +9,13 @@ pub enum MapperData {
Mmc3(Mmc3Data),
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct NromData {
pub cart: Cartridge,
pub chr_ram: Vec<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,
}
<