diff --git a/src/app.rs b/src/app.rs index 61408a1..3fa2fb4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -22,8 +22,8 @@ use crate::{ persistence::Persistence, vram::VramProcessor, window::{ - AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, GameWindow, GdbServerWindow, - InputWindow, ObjectWindow, WorldWindow, + AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow, + GdbServerWindow, InputWindow, ObjectWindow, WorldWindow, }, }; @@ -229,6 +229,10 @@ impl ApplicationHandler for Application { let world = WorldWindow::new(sim_id, &self.memory, &mut self.vram); self.open(event_loop, Box::new(world)); } + UserEvent::OpenFrameBuffers(sim_id) => { + let world = FrameBufferWindow::new(sim_id, &self.memory, &mut self.vram); + self.open(event_loop, Box::new(world)); + } UserEvent::OpenDebugger(sim_id) => { let debugger = GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone()); @@ -491,6 +495,7 @@ pub enum UserEvent { OpenBgMap(SimId), OpenObjects(SimId), OpenWorlds(SimId), + OpenFrameBuffers(SimId), OpenDebugger(SimId), OpenInput, OpenPlayer2, diff --git a/src/window.rs b/src/window.rs index 80d5179..9353a64 100644 --- a/src/window.rs +++ b/src/window.rs @@ -3,7 +3,7 @@ use egui::{Context, ViewportBuilder, ViewportId}; pub use game::GameWindow; pub use gdb::GdbServerWindow; pub use input::InputWindow; -pub use vram::{BgMapWindow, CharacterDataWindow, ObjectWindow, WorldWindow}; +pub use vram::{BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, WorldWindow}; use winit::event::KeyEvent; use crate::emulator::SimId; diff --git a/src/window/game.rs b/src/window/game.rs index 36ac27f..e1072a9 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -156,6 +156,12 @@ impl GameWindow { .unwrap(); ui.close_menu(); } + if ui.button("Frame Buffers").clicked() { + self.proxy + .send_event(UserEvent::OpenFrameBuffers(self.sim_id)) + .unwrap(); + ui.close_menu(); + } }); ui.menu_button("Help", |ui| { if ui.button("About").clicked() { diff --git a/src/window/vram.rs b/src/window/vram.rs index f1acc3a..4899486 100644 --- a/src/window/vram.rs +++ b/src/window/vram.rs @@ -1,10 +1,12 @@ mod bgmap; mod chardata; +mod framebuffer; mod object; mod utils; mod world; pub use bgmap::*; pub use chardata::*; +pub use framebuffer::*; pub use object::*; pub use world::*; diff --git a/src/window/vram/framebuffer.rs b/src/window/vram/framebuffer.rs new file mode 100644 index 0000000..b49a4bb --- /dev/null +++ b/src/window/vram/framebuffer.rs @@ -0,0 +1,267 @@ +use std::sync::Arc; + +use egui::{ + Align, CentralPanel, Color32, Context, Image, ScrollArea, Slider, TextEdit, TextureOptions, Ui, + ViewportBuilder, ViewportId, +}; +use egui_extras::{Column, Size, StripBuilder, TableBuilder}; + +use crate::{ + emulator::SimId, + memory::{MemoryClient, MemoryView}, + vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, + window::{ + utils::{NumberEdit, UiExt as _}, + AppWindow, + }, +}; + +use super::utils; + +pub struct FrameBufferWindow { + sim_id: SimId, + loader: Arc, + index: usize, + left: bool, + right: bool, + generic_palette: bool, + params: VramParams, + scale: f32, +} + +impl FrameBufferWindow { + pub fn new(sim_id: SimId, memory: &MemoryClient, vram: &mut VramProcessor) -> Self { + let initial_params = FrameBufferParams { + index: 0, + left: true, + right: true, + generic_palette: false, + left_color: Color32::from_rgb(0xff, 0x00, 0x00), + right_color: Color32::from_rgb(0x00, 0xc6, 0xf0), + }; + let renderer = FrameBufferRenderer::new(sim_id, memory); + let ([buffer], params) = vram.add(renderer, initial_params); + let loader = VramTextureLoader::new([("vram://buffer".into(), buffer)]); + Self { + sim_id, + loader: Arc::new(loader), + index: params.index, + left: params.left, + right: params.right, + generic_palette: params.generic_palette, + params, + scale: 2.0, + } + } + + fn show_form(&mut self, ui: &mut Ui) { + let row_height = ui.spacing().interact_size.y; + ui.vertical(|ui| { + TableBuilder::new(ui) + .column(Column::auto()) + .column(Column::remainder()) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Index"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut self.index).range(0..2)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Left"); + }); + row.col(|ui| { + let address = self.index * 0x00008000; + let mut address_str = format!("{address:08x}"); + ui.add_enabled( + false, + TextEdit::singleline(&mut address_str).horizontal_align(Align::Max), + ); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Right"); + }); + row.col(|ui| { + let address = self.index * 0x00008000 + 0x00010000; + let mut address_str = format!("{address:08x}"); + ui.add_enabled( + false, + TextEdit::singleline(&mut address_str).horizontal_align(Align::Max), + ); + }); + }); + }); + ui.section("Display", |ui| { + ui.horizontal(|ui| { + ui.label("Scale"); + ui.spacing_mut().slider_width = ui.available_width(); + let slider = Slider::new(&mut self.scale, 1.0..=10.0) + .step_by(1.0) + .show_value(false); + ui.add(slider); + }); + TableBuilder::new(ui) + .columns(Column::remainder(), 2) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.checkbox(&mut self.left, "Left"); + }); + row.col(|ui| { + ui.checkbox(&mut self.right, "Right"); + }); + }); + }); + ui.checkbox(&mut self.generic_palette, "Generic colors"); + }); + }); + + self.params.write(FrameBufferParams { + index: self.index, + left: self.left, + right: self.right, + generic_palette: self.generic_palette, + ..*self.params + }); + } + + fn show_buffers(&mut self, ui: &mut Ui) { + let image = Image::new("vram://buffer") + .fit_to_original_size(self.scale) + .texture_options(TextureOptions::NEAREST); + ui.add(image); + } +} + +impl AppWindow for FrameBufferWindow { + fn viewport_id(&self) -> ViewportId { + ViewportId::from_hash_of(format!("framebuffer-{}", self.sim_id)) + } + + fn sim_id(&self) -> SimId { + self.sim_id + } + + fn initial_viewport(&self) -> ViewportBuilder { + ViewportBuilder::default() + .with_title(format!("Frame Buffers ({})", self.sim_id)) + .with_inner_size((640.0, 480.0)) + } + + fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) { + ctx.add_texture_loader(self.loader.clone()); + } + + fn show(&mut self, ctx: &Context) { + CentralPanel::default().show(ctx, |ui| { + ui.horizontal_top(|ui| { + StripBuilder::new(ui) + .size(Size::relative(0.3)) + .size(Size::remainder()) + .horizontal(|mut strip| { + strip.cell(|ui| { + ScrollArea::vertical().show(ui, |ui| self.show_form(ui)); + }); + strip.cell(|ui| { + ScrollArea::both().show(ui, |ui| self.show_buffers(ui)); + }); + }); + }); + }); + } +} + +#[derive(Clone, PartialEq, Eq)] +struct FrameBufferParams { + index: usize, + left: bool, + right: bool, + generic_palette: bool, + left_color: Color32, + right_color: Color32, +} + +struct FrameBufferRenderer { + buffers: [MemoryView; 4], + brightness: MemoryView, +} + +impl FrameBufferRenderer { + fn new(sim_id: SimId, memory: &MemoryClient) -> Self { + Self { + buffers: [ + memory.watch(sim_id, 0x00000000, 0x6000), + memory.watch(sim_id, 0x00008000, 0x6000), + memory.watch(sim_id, 0x00010000, 0x6000), + memory.watch(sim_id, 0x00018000, 0x6000), + ], + brightness: memory.watch(sim_id, 0x0005f824, 8), + } + } +} + +impl VramRenderer<1> for FrameBufferRenderer { + type Params = FrameBufferParams; + + fn sizes(&self) -> [[usize; 2]; 1] { + [[384, 224]] + } + + fn render(&mut self, params: &Self::Params, images: &mut [VramImage; 1]) { + let image = &mut images[0]; + + let left_buffer = self.buffers[params.index * 2].borrow(); + let right_buffer = self.buffers[params.index * 2 + 1].borrow(); + + let colors = if params.generic_palette { + [ + utils::generic_palette(params.left_color), + utils::generic_palette(params.right_color), + ] + } else { + let brts = self.brightness.borrow().read::<[u8; 8]>(0); + let shades = utils::parse_shades(&brts); + [ + shades.map(|s| utils::shade(s, params.left_color)), + shades.map(|s| utils::shade(s, params.right_color)), + ] + }; + + let left_cols = left_buffer.range::(0, 0x6000); + let right_cols = right_buffer.range::(0, 0x6000); + for (index, (left, right)) in left_cols.zip(right_cols).enumerate() { + let top = (index % 64) * 4; + if top >= 224 { + continue; + } + + let pixels = [0, 2, 4, 6].map(|i| { + let left = if params.left { + colors[0][(left >> i) as usize & 0x3] + } else { + Color32::BLACK + }; + let right = if params.right { + colors[1][(right >> i) as usize & 0x3] + } else { + Color32::BLACK + }; + Color32::from_rgb( + left.r() + right.r(), + left.g() + right.g(), + left.b() + right.b(), + ) + }); + let x = index / 64; + for (i, pixel) in pixels.into_iter().enumerate() { + let y = top + i; + image.write((x, y), pixel); + } + } + } +}