diff --git a/Cargo.lock b/Cargo.lock index 72f6ced..aeb27e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,6 +5,7 @@ name = "NESemu" version = "0.1.0" dependencies = [ "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "fps_counter 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "gfx_core 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", "gfx_device_gl 0.16.2 (registry+https://github.com/rust-lang/crates.io-index)", "gfx_glyph 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -324,6 +325,11 @@ name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "fps_counter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "getrandom" version = "0.1.13" @@ -1547,6 +1553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" "checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" "checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +"checksum fps_counter 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e87181dcb1c9f3297845af26d5d78c4936ff12f8abe1c2a5507459d45f5236a6" "checksum getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407" "checksum gfx 0.18.2 (registry+https://github.com/rust-lang/crates.io-index)" = "01de46f9508a5c259aef105f0bff760ceddca832ea9c87ce03d1923e22ee155b" "checksum gfx_core 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "75fbddaef2e12b4995900539d7209d947b988a3d87ee8737484d049b526e5441" diff --git a/Cargo.toml b/Cargo.toml index 5cb9af0..4b18e9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,5 @@ gfx_core = "0.9.2" gfx_device_gl = "0.16.*" gfx_glyph = "0.16.*" image = "0.22.*" -rand = "0.7.2" \ No newline at end of file +rand = "0.7.2" +fps_counter = "1.0.0" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 8da7bc2..28585af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ extern crate simple_logger; extern crate phf; extern crate piston_window; extern crate rand; +extern crate fps_counter; mod nes; @@ -17,7 +18,7 @@ use nes::disasm::*; use opengl_graphics::OpenGL; use log::Level; use failure::Error; - +use fps_counter::FPSCounter; use gfx_glyph::{Section, GlyphBrushBuilder,GlyphBrush, Scale}; use gfx_device_gl::{Resources,Factory}; @@ -34,7 +35,7 @@ lazy_static! { } fn main() -> Result<(), Error> { - simple_logger::init_with_level(Level::Info).unwrap(); + simple_logger::init_with_level(Level::Warn).unwrap(); let mut nes = NES::new(); let cartridge = Path::new("test_roms/nestest.nes"); @@ -47,18 +48,24 @@ fn main() -> Result<(), Error> { let disasm = Disasm::disassemble(&nes.memory, 0xC000, 0xFFFF).unwrap(); // Prepare window and drawing resources - let mut window: PistonWindow = WindowSettings::new("xXx NESemu xXx", [256*3, 240*2+50]) + + // debugger + scaled nes resolution + border + let window_width = 300 + 256 * 2 + 5; + let window_height = 40 + 240*2 + 5; + let mut window: PistonWindow = WindowSettings::new("xXx NESemu xXx", [window_width, window_height]) .exit_on_esc(true).graphics_api(OpenGL::V3_2).build().unwrap(); let mut event_settings = EventSettings::new(); event_settings.max_fps = 60; - event_settings.ups = 107400; let mut events = Events::new(event_settings); // font for debug screens let font: &[u8] = include_bytes!("../resources/fonts/PressStart2P.ttf"); - let mut glyph_brush: GlyphBrush = GlyphBrushBuilder::using_font_bytes(font) + let mut glyphs: GlyphBrush = GlyphBrushBuilder::using_font_bytes(font) .initial_cache_size((1024, 1024)) .build(window.factory.clone()); + + // fps counter + let mut fps = FPSCounter::new(); // main screen texture let mut texture_ctx = TextureContext { @@ -67,7 +74,7 @@ fn main() -> Result<(), Error> { }; let mut texture: G2dTexture = Texture::from_image( &mut texture_ctx, - &nes.ppu.canvas_main, + &nes.ppu.borrow().canvas_main, &TextureSettings::new() ).unwrap(); @@ -75,31 +82,37 @@ fn main() -> Result<(), Error> { // Main loop let mut run = true; while let Some(event) = events.next(&mut window) { - if let Some(_) = event.update_args() { - // if cpu.regs.pc == 0xC6A2 { run = false } - if run { - nes.clock(); - } - } if let Some(_) = event.render_args() { - texture.update(&mut texture_ctx, &nes.ppu.canvas_main).unwrap(); + // Run enough clocks to render the next frame + if run { nes.clock_frame(); } + + texture.update(&mut texture_ctx, &nes.ppu.borrow().canvas_main).unwrap(); window.draw_2d(&event, |c, g, d| { clear(BG_COLOR, g); texture_ctx.encoder.flush(d); - let transform = c.transform.trans(256.0, 50.0).scale(2.0, 2.0); + let transform = c.transform.trans(300.0, 40.0).scale(2.0, 2.0); image(&texture, transform, g); + + // fps + let fps = fps.tick(); + let position = (410.0, 10.0 + FT_SIZE_PX); + let color = if fps < 60 { FT_COLOR_RED } else { FT_COLOR_WHITE }; + glyphs.queue(Section { + text: &format!("FPS: {}", fps), + scale: *FT_SCALE, + screen_position: position, + color: color, + ..Section::default() + }); + }); - render_debug(&mut window, &event, &mut glyph_brush, &nes.cpu, &nes, &disasm); + render_debug(&mut window, &event, &mut glyphs, &nes, &disasm); } if let Some(Button::Keyboard(key)) = event.press_args() { match key { Key::C => nes.clock(), // advance one clock - Key::S => { // advance to next instruction - nes.clock(); - while nes.cpu.is_ahead() { - nes.clock(); - } - } + Key::S => { nes.clock_instruction() } + Key::F => { nes.clock_frame() } Key::R => nes.reset(), Key::Space => run = !run, Key::Up => { @@ -121,14 +134,14 @@ fn main() -> Result<(), Error> { fn render_debug(window: &mut PistonWindow, event: &Event, glyphs: &mut GlyphBrush, - cpu: &CPU, _nes: &NES, disasm: &Disasm) { + nes: &NES, disasm: &Disasm) { window.draw_2d(event, |_c, _g, _d| { let debug_offset = [10.0, 10.0]; - render_cpu(glyphs, cpu, debug_offset); - render_disasm(glyphs, disasm, cpu.regs.pc, - [debug_offset[0], debug_offset[1] + (7.0 * (FT_LINE_DISTANCE+FT_SIZE_PX))]); + render_cpu(glyphs, &nes.cpu, debug_offset); + render_disasm(glyphs, disasm, nes.cpu.regs.pc, + [debug_offset[0], debug_offset[1] + (8.0 * (FT_LINE_DISTANCE+FT_SIZE_PX))]); // render_memory(glyphs, nes, // [debug_offset[0] + 400.0, debug_offset[1]]); }); @@ -236,6 +249,7 @@ fn render_cpu(glyphs: &mut GlyphBrush, cpu: &CPU, offset: [f &format!("Y: {0:#x} ({0})", cpu.regs.y), &format!("Stack P: {0:#x} ({0})", cpu.regs.sp), &format!("Program P: {:#x}", cpu.regs.pc), + &format!("#CPU Cycles: {}", cpu.cycles), ]; for (i, &text) in cpu_register_texts.iter().enumerate() { diff --git a/src/nes.rs b/src/nes.rs index b390640..16f08fa 100644 --- a/src/nes.rs +++ b/src/nes.rs @@ -1,3 +1,5 @@ +use core::cell::RefCell; +use std::rc::Rc; pub use crate::nes::cartridge::Cartridge; pub use crate::nes::ppu::PPU; pub use crate::nes::cpu::CPU; @@ -17,17 +19,23 @@ pub mod ppu; // as the mediator between the different components and hold the RAM pub struct NES { pub cpu: CPU, - pub ppu: PPU, + pub ppu: Rc>, pub memory: NESMemory, pub clock_count: u64, } impl NES { pub fn new() -> Self { + // The memory module needs access to some parts of the nes like the ppu + // to forward cpu reads to it. Because the NES also requires access + // to the PPU for clocking, we use shared ownership. Otherwise, all + // components except the CPU would have to live inside the memory + // instance, which is conceptually not what we are looking for. + let ppu = Rc::new(RefCell::new(PPU::new())); NES { cpu: CPU::new(), - ppu: PPU::new(), - memory: NESMemory::new(), + ppu: ppu.clone(), + memory: NESMemory::new(ppu.clone()), clock_count: 0, } } @@ -55,7 +63,28 @@ impl NES { if self.clock_count % 3 == 0 { self.cpu.clock(&mut self.memory); } - self.ppu.clock(&mut self.memory); + self.ppu.borrow_mut().clock(&mut self.memory); + } + + + // clock until the next cpu instruction is run + pub fn clock_instruction(&mut self) { + if !self.cpu.is_ahead() { + while !self.cpu.is_ahead() { + self.clock(); + } + } + while self.cpu.is_ahead() { + self.clock(); + } + } + + // clock until the next frame is ready + pub fn clock_frame(&mut self) { + while !self.ppu.borrow().frame_ready { + self.clock(); + } + self.ppu.borrow_mut().frame_ready = false; } } diff --git a/src/nes/cpu.rs b/src/nes/cpu.rs index ccc0079..b76ba7b 100644 --- a/src/nes/cpu.rs +++ b/src/nes/cpu.rs @@ -56,7 +56,7 @@ impl Debug for Registers { pub struct CPU { pub regs: Registers, curr_op: Byte, // current operation - cycles: u64, // number of clock clycles the CPU is ahead of global clock + pub cycles: u64, // number of clock clycles the CPU is ahead of global clock cycles_ahead: u8, stopped: bool, } diff --git a/src/nes/memory.rs b/src/nes/memory.rs index 1d1ccc3..96a97b8 100644 --- a/src/nes/memory.rs +++ b/src/nes/memory.rs @@ -1,24 +1,29 @@ +use crate::nes::ppu::PPU; +use std::rc::Rc; +use core::cell::RefCell; use crate::nes::cartridge::Cartridge; use crate::nes::types::*; -const RAM_SIZE: usize = 0x0800; -const RAM_ADDR_RANGE: [Addr; 2] = [0x0000, 0x1fff]; -const RAM_PHYS_RANGE: [Addr; 2] = [0x0000, 0x07ff]; -const PPU_ADDR_RANGE: [Addr; 2] = [0x2000, 0x3fff]; -const PPU_PHYS_RANGE: [Addr; 2] = [0x2000, 0x2007]; -const CART_ADDR_RANGE: [Addr; 2] = [0x4020, 0xffff]; +pub const RAM_SIZE: usize = 0x0800; +pub const RAM_ADDR_RANGE: [Addr; 2] = [0x0000, 0x1fff]; +pub const RAM_PHYS_RANGE: [Addr; 2] = [0x0000, 0x07ff]; +pub const PPU_ADDR_RANGE: [Addr; 2] = [0x2000, 0x3fff]; +pub const PPU_PHYS_RANGE: [Addr; 2] = [0x2000, 0x2007]; +pub const CART_ADDR_RANGE: [Addr; 2] = [0x4020, 0xffff]; // NES memory: Contains data from RAM, cartridge... pub struct NESMemory { ram: [Byte; RAM_SIZE], // 2kb cartridge: Option, + ppu: Rc>, } impl NESMemory { - pub fn new() -> Self { + pub fn new(ppu: Rc>) -> Self { NESMemory { ram: [0; RAM_SIZE], cartridge: None, + ppu: ppu, } } @@ -48,15 +53,14 @@ impl Memory for NESMemory { return cartridge.readb(addr) } } - if RAM_ADDR_RANGE[0] <= addr && addr <= RAM_ADDR_RANGE[1] { // Ram is 3x mirrored after 0x07ff return self.ram[(addr & RAM_PHYS_RANGE[1]) as usize] } - // if PPU_ADDR_RANGE[0] <= addr && addr <= PPU_ADDR_RANGE[1] { - // // PPU memory is mirrored after 0x2007 to 0x3fff - // return self.ram[(addr & PPU_PHYS_RANGE[1]) as usize] - // } + if PPU_ADDR_RANGE[0] <= addr && addr <= PPU_ADDR_RANGE[1] { + let ppu = self.ppu.borrow(); + return ppu.readb(addr & PPU_PHYS_RANGE[1]); + } 0x0000 // generic response } @@ -70,10 +74,10 @@ impl Memory for NESMemory { // Ram is 3x mirrored after 07ff self.ram[(addr & RAM_PHYS_RANGE[1]) as usize] = data } - // if PPU_ADDR_RANGE[0] <= addr && addr <= PPU_ADDR_RANGE[1] { - // // PPU memory is mirrored after 0x2007 to 0x3fff - // self.ram[(addr & PPU_PHYS_RANGE[1]) as usize] = data - // } + if PPU_ADDR_RANGE[0] <= addr && addr <= PPU_ADDR_RANGE[1] { + let mut ppu = self.ppu.borrow_mut(); + ppu.writeb(addr & PPU_PHYS_RANGE[1], data); + } } } diff --git a/src/nes/ppu.rs b/src/nes/ppu.rs index 5052447..84ce790 100644 --- a/src/nes/ppu.rs +++ b/src/nes/ppu.rs @@ -1,11 +1,85 @@ +use phf::{Map,phf_map}; +use crate::nes::types::*; use crate::nes::memory::Memory; use image::{ImageBuffer, Rgba}; use rand::Rng; +static PALETTE: Map> = phf_map! { + // 0x00 + 0x00u8 => Rgba([84, 84, 84, 255]), + 0x01u8 => Rgba([0, 30, 116, 255]), + 0x02u8 => Rgba([8, 16, 144, 255]), + 0x03u8 => Rgba([48, 0, 136, 255]), + 0x04u8 => Rgba([68, 0, 100, 255]), + 0x05u8 => Rgba([92, 0, 48, 255]), + 0x06u8 => Rgba([84, 4, 0, 255]), + 0x07u8 => Rgba([60, 24, 0, 255]), + 0x08u8 => Rgba([32, 42, 0, 255]), + 0x09u8 => Rgba([8, 58, 0, 255]), + 0x0au8 => Rgba([0, 64, 0, 255]), + 0x0bu8 => Rgba([0, 60, 0, 255]), + 0x0cu8 => Rgba([0, 50, 60, 255]), + 0x0du8 => Rgba([0, 0, 0, 255]), + 0x0eu8 => Rgba([0, 0, 0, 255]), + 0x0fu8 => Rgba([0, 0, 0, 255]), + // 0x10 + 0x10u8 => Rgba([152, 150, 152, 255]), + 0x11u8 => Rgba([8, 76, 196, 255]), + 0x12u8 => Rgba([48, 50, 236, 255]), + 0x13u8 => Rgba([92, 30, 228, 255]), + 0x14u8 => Rgba([136, 20, 176, 255]), + 0x15u8 => Rgba([160, 20, 100, 255]), + 0x16u8 => Rgba([152, 34, 32, 255]), + 0x17u8 => Rgba([120, 60, 0, 255]), + 0x18u8 => Rgba([84, 90, 0, 255]), + 0x19u8 => Rgba([40, 114, 0, 255]), + 0x1au8 => Rgba([8, 124, 0, 255]), + 0x1bu8 => Rgba([0, 118, 40, 255]), + 0x1cu8 => Rgba([0, 102, 120, 255]), + 0x1du8 => Rgba([0, 0, 0, 255]), + 0x1eu8 => Rgba([0, 0, 0, 255]), + 0x1fu8 => Rgba([0, 0, 0, 255]), + // 0x20 + 0x20u8 => Rgba([236, 238, 236, 255]), + 0x21u8 => Rgba([76, 154, 236, 255]), + 0x22u8 => Rgba([120, 124, 236, 255]), + 0x23u8 => Rgba([176, 98, 236, 255]), + 0x24u8 => Rgba([228, 84, 236, 255]), + 0x25u8 => Rgba([236, 88, 180, 255]), + 0x26u8 => Rgba([236, 106, 100, 255]), + 0x27u8 => Rgba([212, 136, 32, 255]), + 0x28u8 => Rgba([160, 170, 0, 255]), + 0x29u8 => Rgba([116, 196, 0, 255]), + 0x2au8 => Rgba([76, 208, 32, 255]), + 0x2bu8 => Rgba([56, 204, 108, 255]), + 0x2cu8 => Rgba([56, 180, 204, 255]), + 0x2du8 => Rgba([60, 60, 60, 255]), + 0x2eu8 => Rgba([0, 0, 0, 255]), + 0x2fu8 => Rgba([0, 0, 0, 255]), + // 0x30 + 0x30u8 => Rgba([236, 238, 236, 255]), + 0x31u8 => Rgba([168, 204, 236, 255]), + 0x32u8 => Rgba([188, 188, 236, 255]), + 0x33u8 => Rgba([212, 178, 236, 255]), + 0x34u8 => Rgba([236, 174, 236, 255]), + 0x35u8 => Rgba([236, 174, 212, 255]), + 0x36u8 => Rgba([236, 180, 176, 255]), + 0x37u8 => Rgba([228, 196, 144, 255]), + 0x38u8 => Rgba([204, 210, 120, 255]), + 0x39u8 => Rgba([180, 222, 120, 255]), + 0x3au8 => Rgba([168, 226, 144, 255]), + 0x3bu8 => Rgba([152, 226, 180, 255]), + 0x3cu8 => Rgba([160, 214, 228, 255]), + 0x3du8 => Rgba([160, 162, 160, 255]), + 0xe3u8 => Rgba([0, 0, 0, 255]), + 0x3fu8 => Rgba([0, 0, 0, 255]), +}; + pub struct PPU { - pub cycle: i16, - pub scanline: i16, - pub canvas_main: ImageBuffer, Vec> + pub cycle: u16, + pub scanline: u16, + pub canvas_main: ImageBuffer, Vec>, + pub frame_ready: bool } @@ -15,22 +89,73 @@ impl PPU { cycle: 0, scanline: 0, canvas_main: ImageBuffer::new(256, 240), + frame_ready: false, } } pub fn clock(&mut self, _mem: &mut T) { - // random noise - let mut rng = rand::thread_rng(); - let x = rng.gen_range(0, 256); - let y = rng.gen_range(0, 240); + if (self.cycle < self.canvas_main.width() as u16) && + (self.scanline < self.canvas_main.height() as u16) { + // random noise + let mut rng = rand::thread_rng(); + let x = self.cycle; + let y = self.scanline; - let color = rng.gen::(); - let px = if color { - Rgba([255, 255, 255, 255]) - } else { - Rgba([0, 0, 0, 255]) - }; + let color = rng.gen::(); + let px = if color { + PALETTE[&0x0f] + } else { + PALETTE[&0x20] + }; - self.canvas_main.put_pixel(x as u32, y as u32, px); + self.canvas_main.put_pixel(x as u32, y as u32, px); + } + + // increment cycle/scanline + self.cycle += 1; + if self.cycle > 340 { + self.cycle = 0; + self.scanline += 1; + if self.scanline >= 261 { + self.scanline = 0; + self.frame_ready = true; + } + } } } + +impl Memory for PPU { + fn readb(&self, addr: Addr) -> Byte { + // Only some of the PPU registers can actually by read + match addr { + // PPU status + 0x2002 => { 0x00 }, + // OAM data (object attribute memory = list of 64 sprites) + 0x2004 => { unimplemented!() }, + // ppu data + 0x2007 => { unimplemented!() }, + _ => 0x00 + } + + } + fn writeb(&mut self, addr: Addr, data: Byte) { + // Only some of the PPU registers can be written to + match addr { + // Control + 0x2000 => { unimplemented!() }, + // Mask + 0x2001 => { unimplemented!() }, + // OAM address + 0x2003 => { unimplemented!() }, + // OAM data + 0x2004 => { unimplemented!() }, + // Scroll + 0x2005 => { unimplemented!() }, + // Address + 0x2006 => { unimplemented!() }, + // ppu data + 0x2007 => { unimplemented!() }, + _ => { } + } + } +}