From e945e026d8672a1daa11a77efa7c9b8125807c3c Mon Sep 17 00:00:00 2001 From: mars Date: Thu, 22 Sep 2022 11:55:02 -0600 Subject: [PATCH] Cruddy NES emulator --- Cargo.toml | 1 + crates/nes-emu/Cargo.toml | 13 ++ crates/nes-emu/src/lib.rs | 310 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 324 insertions(+) create mode 100644 crates/nes-emu/Cargo.toml create mode 100644 crates/nes-emu/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index fb95f14..13e8920 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/egui", + "crates/nes-emu", "crates/sao-ui-rs", "crates/script", "crates/types", diff --git a/crates/nes-emu/Cargo.toml b/crates/nes-emu/Cargo.toml new file mode 100644 index 0000000..bbe150e --- /dev/null +++ b/crates/nes-emu/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "nes-emu" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +glam = "^0.21" +canary_script = { path = "../script" } +ludus = "0.2" +wee_alloc = "^0.4" diff --git a/crates/nes-emu/src/lib.rs b/crates/nes-emu/src/lib.rs new file mode 100644 index 0000000..421311b --- /dev/null +++ b/crates/nes-emu/src/lib.rs @@ -0,0 +1,310 @@ +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +use canary_script::*; +use ludus::Console; + +const FRAME_DURATION: f32 = 1.0 / 60.0; +const PIX_WIDTH: usize = 256; +const PIX_HEIGHT: usize = 240; +const PIX_NUM: usize = PIX_WIDTH * PIX_HEIGHT; + +static ROM: &[u8] = include_bytes!("rom.nes"); + +export_abi!(NesPanel); + +pub struct Config { + pub fb_pix_size: f32, + pub fb_origin_x: f32, + pub fb_origin_y: f32, +} + +impl Default for Config { + fn default() -> Self { + Self { + fb_pix_size: 0.004, + fb_origin_x: -0.5, + fb_origin_y: -0.5, + } + } +} + +pub struct NesPanel { + panel: Panel, + fb: Framebuffer, + input: Input, + console: Console, + audio: DummyAudioDevice, + step: f32, +} + +impl BindPanel for NesPanel { + fn bind(panel: Panel, message: Message) -> Box { + let config = Config::default(); + + let cart = ludus::Cart::from_bytes(ROM).expect("Failed to decode rom"); + + let panel = Self { + panel, + fb: Framebuffer::new(&config), + input: Input::new(), + console: Console::new(cart, 8000), + audio: DummyAudioDevice, + step: 0.0, + }; + + Box::new(panel) + } +} + +impl PanelImpl for NesPanel { + fn update(&mut self, dt: f32) { + self.step += dt; + + self.input.update(); + + while self.step > FRAME_DURATION { + self.step -= FRAME_DURATION; + self.console.update_controller(self.input.get_state()); + self.console.step_frame(&mut self.audio, &mut self.fb); + } + } + + fn draw(&mut self) { + self.fb.draw(&self.panel); + self.input.draw(&self.panel); + } + + fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) { + for button in self.input.buttons.iter_mut() { + button.on_cursor_event(kind, at); + } + } + + fn on_message(&mut self, message: Message) {} +} + +pub struct DummyAudioDevice; + +impl ludus::ports::AudioDevice for DummyAudioDevice { + fn push_sample(&mut self, _sample: f32) {} +} + +pub struct Framebuffer { + pub vertices: Vec, + pub indices: Vec, + pub double_buffer: Vec, + pub dirty: bool, +} + +impl Framebuffer { + pub fn new(config: &Config) -> Self { + let mut vertices = Vec::with_capacity(PIX_NUM * 4); + let mut indices = Vec::with_capacity(PIX_NUM * 6); + + let color = Color::BLACK; + + for y in 0..PIX_HEIGHT { + for x in 0..PIX_WIDTH { + let index = vertices.len() as MeshIndex; + let y = PIX_HEIGHT - y; + let x = x as f32 * config.fb_pix_size + config.fb_origin_x; + let y = y as f32 * config.fb_pix_size + config.fb_origin_y; + + vertices.push(MeshVertex { + position: Vec2 { x, y }, + color, + }); + + vertices.push(MeshVertex { + position: Vec2 { + x: x + config.fb_pix_size, + y, + }, + color, + }); + + vertices.push(MeshVertex { + position: Vec2 { + x, + y: y + config.fb_pix_size, + }, + color, + }); + + vertices.push(MeshVertex { + position: Vec2 { + x: x + config.fb_pix_size, + y: y + config.fb_pix_size, + }, + color, + }); + + indices.extend_from_slice(&[ + index, + index + 1, + index + 2, + index + 1, + index + 2, + index + 3, + ]); + } + } + + Self { + vertices, + indices, + double_buffer: Vec::new(), + dirty: false, + } + } + + pub fn draw(&mut self, panel: &Panel) { + if self.dirty { + self.dirty = false; + + for (pixel, vertices) in self + .double_buffer + .iter() + .zip(self.vertices.chunks_exact_mut(4)) + { + let r = ((pixel >> 16) & 0xff) as f32 / 255.0; + let g = ((pixel >> 8) & 0xff) as f32 / 255.0; + let b = (pixel & 0xff) as f32 / 255.0; + let color = Color::new(r, g, b, 1.0); + + for vertex in vertices { + vertex.color = color; + } + } + } + + panel.draw_indexed(&self.vertices, &self.indices); + } +} + +impl ludus::ports::VideoDevice for Framebuffer { + fn blit_pixels(&mut self, pixels: &ludus::PixelBuffer) { + let pixels = pixels.as_ref(); + self.double_buffer.resize(pixels.len(), 0); + self.double_buffer.copy_from_slice(pixels); + self.dirty = true; + } +} + +pub struct Input { + pub buttons: [Button; 8], + pub state: ludus::ButtonState, +} + +impl Input { + pub fn new() -> Self { + let button_size = Vec2 { x: 0.1, y: 0.1 }; + + let button_positions = [ + Vec2 { x: -0.4, y: -0.3 }, + Vec2 { x: -0.3, y: -0.4 }, + Vec2 { x: -0.4, y: -0.5 }, + Vec2 { x: -0.5, y: -0.4 }, + Vec2 { x: -0.15, y: -0.5 }, + Vec2 { x: 0.15, y: -0.5 }, + Vec2 { x: 0.4, y: -0.4 }, + Vec2 { x: 0.55, y: -0.4 }, + ]; + + Self { + buttons: button_positions.map(|pos| Button::new(pos, button_size)), + state: Default::default(), + } + } + + pub fn get_state(&self) -> ludus::ButtonState { + ludus::ButtonState { ..self.state } + } + + pub fn update(&mut self) { + self.state.up = self.buttons[0].pressed; + self.state.right = self.buttons[1].pressed; + self.state.down = self.buttons[2].pressed; + self.state.left = self.buttons[3].pressed; + self.state.select = self.buttons[4].pressed; + self.state.start = self.buttons[5].pressed; + self.state.b = self.buttons[6].pressed; + self.state.a = self.buttons[7].pressed; + } + + pub fn draw(&self, panel: &Panel) { + for button in self.buttons.iter() { + button.draw(panel); + } + } +} + +pub struct Button { + pos: Vec2, + size: Vec2, + pressed: bool, +} + +impl Button { + pub fn new(pos: Vec2, size: Vec2) -> Self { + Self { + pos, + size, + pressed: false, + } + } + + pub fn draw(&self, panel: &Panel) { + let alpha = if self.pressed { 1.0 } else { 0.8 }; + let color = Color::new(1.0, 1.0, 1.0, alpha); + + let pos = self.pos; + let corner = Vec2 { + x: pos.x + self.size.x, + y: pos.y + self.size.y, + }; + + let vertices = &[ + MeshVertex { + position: pos, + color, + }, + MeshVertex { + position: Vec2 { + x: corner.x, + y: pos.y, + }, + color, + }, + MeshVertex { + position: Vec2 { + x: pos.x, + y: corner.y, + }, + color, + }, + MeshVertex { + position: corner, + color, + }, + ]; + + let indices = &[0, 1, 2, 1, 2, 3]; + + panel.draw_indexed(vertices.as_slice(), indices.as_slice()); + } + + pub fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) { + if kind == CursorEventKind::Deselect { + self.pressed = false; + } else if kind == CursorEventKind::Select + && at.x > self.pos.x + && at.y > self.pos.y + && at.x < self.pos.x + self.size.x + && at.y < self.pos.y + self.size.y + { + self.pressed = true; + } + } +}