diff --git a/src/bus.rs b/src/bus.rs index a35e264..a766358 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -38,6 +38,7 @@ impl NesBus { self.ppu.begin_frame(); } + pub fn controller_mut(&mut self) -> &mut Controller { &mut self.controller } pub fn ppu(&self) -> &Ppu { &self.ppu } pub fn ram(&self) -> &[u8; 0x800] { &self.ram } } diff --git a/src/input.rs b/src/input.rs index d31d2ec..15ce657 100644 --- a/src/input.rs +++ b/src/input.rs @@ -28,6 +28,7 @@ pub fn key_to_button(key: Key) -> u8 { pub trait InputSource { fn buttons(&mut self) -> u8; + fn set_buttons(&mut self, _buttons: u8) {} } pub struct MaskInput { @@ -42,6 +43,7 @@ impl MaskInput { impl InputSource for MaskInput { fn buttons(&mut self) -> u8 { self.state } + fn set_buttons(&mut self, buttons: u8) { self.state = buttons; } } // The fixed hardware protocol - ONE implementation, any InputSource behind it. @@ -56,6 +58,10 @@ impl Controller { Self { source, strobe: false, shift: 0 } } + pub fn set_buttons(&mut self, buttons: u8) { + self.source.set_buttons(buttons); + } + // $4016/$4017 write: strobe pulse. On the falling edge, latch the buttons. pub fn write_strobe(&mut self, value: u8) { let strobe = value & 1 == 1; diff --git a/src/main.rs b/src/main.rs index 3b2df1d..af94f23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,8 @@ use crate::bus::{Bus, NesBus}; use crate::cartridge::Cartridge; use crate::cpu::Cpu; use crate::render::{PpmRenderer, Renderer}; +use eframe::egui; +use crate::render::WindowRenderer; fn main() { @@ -20,8 +22,9 @@ fn main() { 1 => run_nestest(), 3 if args[1] == "blargg" => run_blargg(&args[2]), 3 if args[1] == "dbg" => run_dbg(&args[2]), + 3 if args[1] == "gui" => run_gui(&args[2]), 2 => run_cart(&args[1]), - _ => eprintln!("usage: rnes [ | blargg ]"), + _ => eprintln!("usage: rnes [ | blargg | dbg | gui ]"), } } @@ -34,13 +37,8 @@ fn run_cart(path: &str) { let frames: u32 = 180; for _ in 0..frames { - while !bus.frame_done() { - let cycles = cpu.step(&mut bus); - bus.tick_ppu(cycles as u64); - if bus.take_nmi() { cpu.set_nmi(); } // NMI fires when vblank hits, mid-frame - } + run_one_frame(&mut bus, &mut cpu); renderer.present(bus.framebuffer(), 256, 240); - bus.begin_frame(); } } @@ -94,4 +92,59 @@ fn run_dbg(path: &str) { bus.begin_frame(); } println!("wrote debug_out/ + renders/, {frames} frames"); +} + +fn run_one_frame(bus: &mut NesBus, cpu: &mut Cpu) { + while !bus.frame_done() { + let cycles = cpu.step(bus); + bus.tick_ppu(cycles as u64); + if bus.take_nmi() { cpu.set_nmi(); } + } + bus.begin_frame(); +} + +struct GuiApp { + bus: NesBus, + cpu: Cpu, + renderer: WindowRenderer, +} + +impl GuiApp { + fn new(cc: &eframe::CreationContext<'_>, cart: Cartridge) -> Self { + let mut bus = NesBus::new(cart); + let mut cpu = Cpu::new(); + cpu.reset(&mut bus); + let renderer = WindowRenderer::new(cc, 256, 240); + Self { bus, cpu, renderer } + } +} + +impl eframe::App for GuiApp { + // Non-UI work: emulation runs here, before painting. + fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + self.renderer.poll_input(self.bus.controller_mut()); // keyboard -> $4016/$4017 + run_one_frame(&mut self.bus, &mut self.cpu); // one emulated frame + self.renderer.present(self.bus.framebuffer(), 256, 240); + ctx.request_repaint(); // keep it running at 60fps + } + + // Painting: draw the framebuffer texture. + fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { + self.renderer.draw(ui); + } +} + +fn run_gui(path: &str) { + let cart = Cartridge::from_file(path).unwrap(); + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_inner_size([1024.0, 960.0]) + .with_title("rnes"), + ..Default::default() + }; + eframe::run_native( + "rnes", + options, + Box::new(move |cc| Ok(Box::new(GuiApp::new(cc, cart)))), + ).expect("failed to run GUI"); } \ No newline at end of file diff --git a/src/render.rs b/src/render.rs index 1f25194..0afad7f 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,15 +1,17 @@ +use eframe::egui; +use crate::input::{key_to_button, Controller, Key}; use crate::palette::index_to_rgb; pub trait Renderer { fn present(&mut self, framebuffer: &[u8], width: u16, height: u16); + fn should_close(&self) -> bool { false } // headless never closes + fn poll_input(&mut self, _controller: &mut Controller) {} // headless: no input } pub struct PpmRenderer { frame: u32, } - - impl PpmRenderer { pub fn new() -> Self { Self { frame: 0 } @@ -26,6 +28,71 @@ impl Renderer for PpmRenderer { } } +// The GUI renderer: egui window, texture upload, keyboard -> controller. +pub struct WindowRenderer { + ctx: egui::Context, + texture: Option, + rgb: Vec, // reusable RGB buffer (palette indices -> RGB) + width: usize, + height: usize, +} + +impl WindowRenderer { + pub fn new(cc: &eframe::CreationContext<'_>, width: u16, height: u16) -> Self { + Self { + ctx: cc.egui_ctx.clone(), + texture: None, + rgb: vec![0; width as usize * height as usize * 3], + width: width as usize, + height: height as usize, + } + } + + // Called from eframe::App::ui: draws the texture filling the window. + pub fn draw(&mut self, ui: &mut egui::Ui) { + if let Some(tex) = &self.texture { + let avail = ui.available_size(); + ui.add(egui::Image::new(tex).fit_to_exact_size(avail)); + } + } +} + +impl Renderer for WindowRenderer { + fn present(&mut self, framebuffer: &[u8], width: u16, height: u16) { + // palette indices -> RGB + for (i, &idx) in framebuffer.iter().enumerate() { + let (r, g, b) = index_to_rgb(idx); + self.rgb[i * 3] = r; + self.rgb[i * 3 + 1] = g; + self.rgb[i * 3 + 2] = b; + } + let image = egui::ColorImage::from_rgb([self.width, self.height], &self.rgb); + match &mut self.texture { + Some(t) => t.set(image, egui::TextureOptions::NEAREST), + None => { + let t = self.ctx.load_texture("nes-screen", image, egui::TextureOptions::NEAREST); + self.texture = Some(t); + } + } + } + + fn poll_input(&mut self, controller: &mut Controller) { + let buttons = self.ctx.input(|i| { + let mut b = 0u8; + if i.key_down(egui::Key::Z) { b |= key_to_button(Key::A); } + if i.key_down(egui::Key::X) { b |= key_to_button(Key::B); } + if i.modifiers.shift { b |= key_to_button(Key::Select); } + if i.key_down(egui::Key::Enter) { b |= key_to_button(Key::Start); } + if i.key_down(egui::Key::ArrowUp) { b |= key_to_button(Key::Up); } + if i.key_down(egui::Key::ArrowDown) { b |= key_to_button(Key::Down); } + if i.key_down(egui::Key::ArrowLeft) { b |= key_to_button(Key::Left); } + if i.key_down(egui::Key::ArrowRight) { b |= key_to_button(Key::Right); } + b + }); + controller.set_buttons(buttons); + } +} + pub fn ppm_bytes(framebuffer: &[u8], width: u16, height: u16) -> Vec { let mut out = format!("P6\n{width} {height}\n255\n").into_bytes(); for &idx in framebuffer {