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 } } } impl Renderer for PpmRenderer { fn present(&mut self, framebuffer: &[u8], width: u16, height: u16) { std::fs::create_dir_all("renders").expect("failed to create renders dir"); let path = format!("renders/frame_{:04}.ppm", self.frame); std::fs::write(&path, ppm_bytes(framebuffer, width, height)) .expect("failed to write PPM file"); self.frame += 1; } } // 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 { let (r, g, b) = index_to_rgb(idx); out.extend_from_slice(&[r, g, b]); } out } // Tests #[cfg(test)] mod tests { use super::*; #[test] fn ppm_bytes_has_correct_header() { let fb = [0u8; 4]; let bytes = ppm_bytes(&fb, 2, 2); let expected_header = "P6\n2 2\n255\n"; let got = String::from_utf8_lossy(&bytes[..expected_header.len()]); assert_eq!(got, expected_header); } #[test] fn ppm_bytes_emits_rgb_per_pixel() { let fb = [0x21u8, 0x21, 0x00, 0x3F]; // 4 pixels: 2x2 let bytes = ppm_bytes(&fb, 2, 2); let header = format!("P6\n2 2\n255\n"); let pixels = &bytes[header.len()..]; assert_eq!(pixels.len(), 2 * 2 * 3); // 12 RGB bytes assert_eq!(&pixels[0..3], &[0x64, 0x8D, 0xFF]); // pixel 0 = 0x21, unchanged assert_eq!(&pixels[9..12], &[0x66, 0x87, 0x96]); // pixel 3 = 0x3F (was 6..9) } }