vertical scroll bug fixed, mmc3 fixed, pics, readme

This commit is contained in:
Theron Spiegl 2020-01-30 20:33:29 -06:00
parent e4d8bba720
commit 15431e8fec
16 changed files with 46 additions and 59 deletions

View File

@ -1,12 +1,12 @@
# nestur # nestur
Nestur is an NES emulator and a work in progress. There are likely still several bugs, and MMC3 support is not great. There are plenty of full-featured emulators out there; this is primarily an educational project but I do want it to run well. 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.
- SDL2 is the only dependency - SDL2 is the only dependency
- no use of `unsafe` - no use of `unsafe`
- NTSC timing - NTSC timing
- may only implement mappers 0-4 as these cover ~85% of games according to http://tuxnes.sourceforge.net/nesmapper.txt - supports mappers 0-4 which cover ~85% of [games](http://tuxnes.sourceforge.net/nesmapper.txt)
<img src="pics/smb.png" width=400> <img src="pics/zelda_dungeon.png" width=400> <img src="pics/smb.png" width=300> <img src="pics/zelda_dungeon.png" width=300> <img src="pics/kirby.png" width=300> <img src="pics/metroid.png" width=300> <img src="pics/smb3.png" width=300> <img src="pics/contra.png" width=300>
## Controls: ## Controls:
``` ```
@ -24,11 +24,11 @@ ___________________
``` ```
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. 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.
## Compilation and Use ## Compilation and use
1. Install Rust: https://www.rust-lang.org/tools/install 1. Install [Rust](https://www.rust-lang.org/tools/install)
2. Configure SDL2 for your platform: 2. Configure SDL2 for your platform:
- Windows: `SDL2.dll` is already in the repo so you don't have to do anything. - Windows: `SDL2.dll` is already in the repo so you don't have to do anything
- macOS: Install [Homebrew](https://brew.sh/) and run `brew install sdl2` - macOS: Install [Homebrew](https://brew.sh/) and run `brew install sdl2`
- Linux: `sudo apt-get install libsdl2-dev` (or whatever your package manager is) - Linux: `sudo apt-get install libsdl2-dev` (or whatever your package manager is)
3. `cd nestur/ && cargo build --release` (be sure to build/run with the release flag or it will run very slowly) 3. `cd nestur/ && cargo build --release` (be sure to build/run with the release flag or it will run very slowly)
@ -38,13 +38,17 @@ The code aims to follow the explanations from the [NES dev wiki](https://wiki.ne
## To do: ## To do:
- Fix any bugs with mappers 0-3 and improve mapper 4 (MMC3)
- DMC audio channel, high- and low-pass filters - DMC audio channel, high- and low-pass filters
- Save state/load functionality - Better GUI and distributable solution
- Save states
- Player 2 controller? - Player 2 controller?
## Known problem games
- Paperboy: input doesn't work
Please also check out [Cloaker](https://github.com/spieglt/cloaker) and [Flying Carpet](https://github.com/spieglt/flyingcarpet)! Please also check out [Cloaker](https://github.com/spieglt/cloaker) and [Flying Carpet](https://github.com/spieglt/flyingcarpet)!

BIN
pics/contra.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
pics/dk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
pics/excitebike.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
pics/final_fantasy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
pics/kirby.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
pics/metroid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
pics/smb3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -51,8 +51,8 @@ pub fn initialize(sdl_context: &Sdl, buffer: Arc<Mutex<Vec<f32>>>)
channels: Some(1), // mono channels: Some(1), // mono
samples: Some(SAMPLES_PER_FRAME) samples: Some(SAMPLES_PER_FRAME)
}; };
audio_subsystem.open_playback(None, &desired_spec, |spec| { audio_subsystem.open_playback(None, &desired_spec, |_spec| {
// println!("{:?}", spec); // println!("{:?}", _spec);
ApuSampler{buffer, sample_ratio: APU_SAMPLE_RATE / (SDL_SAMPLE_RATE as f32)} ApuSampler{buffer, sample_ratio: APU_SAMPLE_RATE / (SDL_SAMPLE_RATE as f32)}
}) })
} }

View File

@ -158,7 +158,7 @@ impl Mapper for Mmc3 {
0x8000..=0x9FFF => self.bank_select(value), 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, 0xC000..=0xDFFF => self.irq_latch = value,
0xE000..=0xFFFF => self.irq_enable = false, 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), _ => println!("bad address written to MMC3: 0x{:X}", address),
} }
}, },
@ -186,19 +186,16 @@ impl Mapper for Mmc3 {
fn load_battery_backed_ram(&mut self) {} fn load_battery_backed_ram(&mut self) {}
fn save_battery_backed_ram(&self) {} fn save_battery_backed_ram(&self) {}
// This function is called by the PPU when the A12 address line changes.
// It's supposed to only be called when A12 goes from 0 to 1, but that doesn't work
// for my emulator for some reason.
fn clock(&mut self) { fn clock(&mut self) {
if self.reload_counter { if self.reload_counter {
self.irq_counter = self.irq_latch; self.irq_counter = self.irq_latch;
self.reload_counter = false; self.reload_counter = false;
} }
// "Should reload and set IRQ every clock when reload is 0."
if self.irq_latch == 0 && self.irq_enable {
self.irq_counter = self.irq_latch;
self.trigger_irq = true;
}
if self.irq_counter == 0 { if self.irq_counter == 0 {
self.irq_counter = self.irq_latch; self.irq_counter = self.irq_latch;
return;
} else { } else {
self.irq_counter -= 1; self.irq_counter -= 1;
} }
@ -207,16 +204,15 @@ impl Mapper for Mmc3 {
} }
} }
// This function is called by the CPU every step (which takes more than one CPU clock cycle).
// I think I'm supposed to be tracking IRQ delays by the PPU, not letting an IRQ fire if
// there was one within the last 15 PPU cycles, but that didn't work and this does.
fn check_irq(&mut self) -> bool { fn check_irq(&mut self) -> bool {
// if self.trigger_irq {
// self.trigger_irq = false;
// true
// } else {
// false
// }
if self.trigger_irq { if self.trigger_irq {
self.trigger_irq = false; self.trigger_irq = false;
self.irq_delay = 15; if self.irq_delay == 0 {
self.irq_delay = 5;
}
} }
if self.irq_delay > 0 { if self.irq_delay > 0 {
self.irq_delay -= 1; self.irq_delay -= 1;

View File

@ -2,51 +2,49 @@ use super::{Cartridge, Mapper, Mirror};
pub struct Nrom { pub struct Nrom {
cart: Cartridge, cart: Cartridge,
chr_ram: Vec<u8>,
} }
impl Nrom { impl Nrom {
pub fn new(cart: Cartridge) -> Self { pub fn new(cart: Cartridge) -> Self {
Nrom{ Nrom{
cart: cart, cart: cart,
chr_ram: vec![0; 0x2000],
} }
} }
} }
impl Mapper for Nrom { impl Mapper for Nrom {
fn read(&mut self, address: usize) -> u8 { fn read(&mut self, address: usize) -> u8 {
let cl = self.cart.chr_rom.len();
let pl = self.cart.prg_rom.len();
let addr = address % 0x4000; let addr = address % 0x4000;
match address { match address {
0x0000..=0x1FFF => { 0x0000..=0x1FFF => {
if cl > 0 { if self.cart.chr_rom_size > 0 {
self.cart.chr_rom[cl-1][address] self.cart.chr_rom[0][address]
} else { } else {
0 self.chr_ram[address]
} }
}, },
0x8000..=0xBFFF => { 0x8000..=0xBFFF => {
self.cart.prg_rom[0][addr] self.cart.prg_rom[0][addr]
}, },
0xC000..=0xFFFF => { 0xC000..=0xFFFF => {
self.cart.prg_rom[pl-1][addr] self.cart.prg_rom[self.cart.prg_rom_size - 1][addr]
}, },
_ => {println!("bad address read from NROM mapper: 0x{:X}", address); 0}, _ => {println!("bad address read from NROM mapper: 0x{:X}", address); 0},
} }
} }
fn write(&mut self, address: usize, value: u8) { fn write(&mut self, address: usize, value: u8) {
let cl = self.cart.chr_rom.len();
let pl = self.cart.prg_rom.len();
let addr = address % 0x4000;
match address { match address {
0x0000..=0x1FFF => { 0x0000..=0x1FFF => {
if cl > 0 { // ROM isn't written to
self.cart.chr_rom[cl-1][address] = value; if self.cart.chr_rom_size == 0 {
self.chr_ram[address] = value;
} }
}, },
0x8000..=0xBFFF => self.cart.prg_rom[0][addr] = value, 0x8000..=0xBFFF => (),
0xC000..=0xFFFF => self.cart.prg_rom[pl-1][addr] = value, 0xC000..=0xFFFF => (),
_ => println!("bad address written to NROM mapper: 0x{:X}", address), _ => println!("bad address written to NROM mapper: 0x{:X}", address),
} }
} }

View File

@ -158,6 +158,8 @@ impl Cpu {
if self.mapper.borrow_mut().check_irq() && (self.P & INTERRUPT_DISABLE_FLAG == 0) { if self.mapper.borrow_mut().check_irq() && (self.P & INTERRUPT_DISABLE_FLAG == 0) {
self.irq(); self.irq();
} }
// TODO: should checks for APU and MMC3 IRQs be combined and acknowledged together?
// back up clock so we know how many cycles we complete // back up clock so we know how many cycles we complete
let clock = self.clock; let clock = self.clock;

View File

@ -118,7 +118,6 @@ fn main() -> Result<(), String> {
/* /*
TODO: TODO:
- fix MMC3
- high- and low-pass audio filters - high- and low-pass audio filters
- DMC audio channel - DMC audio channel
- untangle CPU and APU/PPU? - untangle CPU and APU/PPU?
@ -142,13 +141,4 @@ Failed tests from instr_test-v5/rom_singles/:
CB AXS #n CB AXS #n
7, abs_xy, 'illegal opcode using abs x: 9c' 7, abs_xy, 'illegal opcode using abs x: 9c'
1. A12 stuff controls when IRQs fire. Don't think there are serious problems with when IRQs fire anymore,
though vertical scroll is still shaky in Kirby.
2. Don't think the timing stuff is related to the palette and background table issues in SMB2/3.
How can the palette be wrong? Or is it not, but the attribute bits are reading wrong?
*/ */

View File

@ -110,7 +110,9 @@ impl super::Ppu {
set_bit(&mut self.t, 0xC, d, 0x4); set_bit(&mut self.t, 0xC, d, 0x4);
set_bit(&mut self.t, 0xD, d, 0x5); set_bit(&mut self.t, 0xD, d, 0x5);
// t: X...... ........ = 0 // t: X...... ........ = 0
set_bit(&mut self.t, 0xF, 0, 0); set_bit(&mut self.t, 0xE, 0, 0);
// 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; self.w = 1;
}, },
1 => { // second write 1 => { // second write
@ -169,7 +171,6 @@ impl super::Ppu {
}, },
_ => panic!("reading from invalid PPU address: 0x{:04x}", self.v), _ => panic!("reading from invalid PPU address: 0x{:04x}", self.v),
}; };
if self.rendering() && (self.scanline < 240 || self.scanline == 261) { if self.rendering() && (self.scanline < 240 || self.scanline == 261) {
// During rendering (on the pre-render line and the visible lines 0-239, provided either background or sprite rendering is enabled), // During rendering (on the pre-render line and the visible lines 0-239, provided either background or sprite rendering is enabled),
// it will update v in an odd way, triggering a coarse X increment and a Y increment simultaneously (with normal wrapping behavior). // it will update v in an odd way, triggering a coarse X increment and a Y increment simultaneously (with normal wrapping behavior).

View File

@ -2,9 +2,8 @@ use crate::cartridge::Mirror;
impl super::Ppu { impl super::Ppu {
pub fn read(&mut self, addr: usize) -> u8 { pub fn read(&mut self, address: usize) -> u8 {
let address = addr % 0x4000; match address {
match addr {
0x0000..=0x1FFF => self.mapper.borrow_mut().read(address), 0x0000..=0x1FFF => self.mapper.borrow_mut().read(address),
0x2000..=0x3EFF => self.read_nametable(address), 0x2000..=0x3EFF => self.read_nametable(address),
0x3F00..=0x3FFF => { 0x3F00..=0x3FFF => {
@ -16,9 +15,9 @@ impl super::Ppu {
} }
} }
pub fn write(&mut self, addr: usize, value: u8) { pub fn write(&mut self, address: usize, value: u8) {
let address = addr % 0x4000; // let address = addr % 0x4000;
match addr { match address {
0x0000..=0x1FFF => self.mapper.borrow_mut().write(address, value), 0x0000..=0x1FFF => self.mapper.borrow_mut().write(address, value),
0x2000..=0x3EFF => self.write_nametable(address, value), 0x2000..=0x3EFF => self.write_nametable(address, value),
0x3F00..=0x3FFF => { 0x3F00..=0x3FFF => {

View File

@ -249,13 +249,10 @@ impl Ppu {
let current_a12 = if self.v & 1 << 12 != 0 { 1 } else { 0 }; let current_a12 = if self.v & 1 << 12 != 0 { 1 } else { 0 };
if rendering if rendering
&& (0..241).contains(&self.scanline) && (0..241).contains(&self.scanline)
// && (current_a12 == 1 && self.previous_a12 == 0)
&& current_a12 != self.previous_a12 && current_a12 != self.previous_a12
{ {
// println!("clocking");
self.mapper.borrow_mut().clock() self.mapper.borrow_mut().clock()
} }
// println!("current: {}, previous: {}", current_a12, self.previous_a12);
self.previous_a12 = current_a12; self.previous_a12 = current_a12;
(pixel, end_of_frame) (pixel, end_of_frame)