diff --git a/src/app.rs b/src/app.rs index 4c1b57d..6197675 100644 --- a/src/app.rs +++ b/src/app.rs @@ -23,7 +23,7 @@ use crate::{ vram::VramProcessor, window::{ AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, GameWindow, GdbServerWindow, - InputWindow, + InputWindow, ObjectWindow, }, }; @@ -221,6 +221,10 @@ impl ApplicationHandler for Application { let bgmap = BgMapWindow::new(sim_id, &mut self.memory, &mut self.vram); self.open(event_loop, Box::new(bgmap)); } + UserEvent::OpenObjects(sim_id) => { + let objects = ObjectWindow::new(sim_id, &mut self.memory, &mut self.vram); + self.open(event_loop, Box::new(objects)); + } UserEvent::OpenDebugger(sim_id) => { let debugger = GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone()); @@ -481,6 +485,7 @@ pub enum UserEvent { OpenAbout, OpenCharacterData(SimId), OpenBgMap(SimId), + OpenObjects(SimId), OpenDebugger(SimId), OpenInput, OpenPlayer2, diff --git a/src/vram.rs b/src/vram.rs index d260999..be6ee1d 100644 --- a/src/vram.rs +++ b/src/vram.rs @@ -101,34 +101,43 @@ impl VramProcessorWorker { pub struct VramImage { size: [usize; 2], - shades: Vec, + shades: Vec, } impl VramImage { pub fn new(width: usize, height: usize) -> Self { Self { size: [width, height], - shades: vec![0; width * height], + shades: vec![Color32::BLACK; width * height], } } - pub fn write(&mut self, coords: (usize, usize), shade: u8) { - self.shades[coords.1 * self.size[0] + coords.0] = shade; + pub fn clear(&mut self) { + for shade in self.shades.iter_mut() { + *shade = Color32::BLACK; + } + } + + pub fn write(&mut self, coords: (usize, usize), pixel: Color32) { + self.shades[coords.1 * self.size[0] + coords.0] = pixel; + } + + pub fn add(&mut self, coords: (usize, usize), pixel: Color32) { + let index = coords.1 * self.size[0] + coords.0; + let old = self.shades[index]; + self.shades[index] = Color32::from_rgb( + old.r() + pixel.r(), + old.g() + pixel.g(), + old.b() + pixel.b(), + ); } pub fn changed(&self, image: &ColorImage) -> bool { - image - .pixels - .iter() - .map(|p| p.r()) - .zip(&self.shades) - .any(|(a, b)| a != *b) + image.pixels.iter().zip(&self.shades).any(|(a, b)| a != b) } pub fn read(&self, image: &mut ColorImage) { - for (pixel, shade) in image.pixels.iter_mut().zip(&self.shades) { - *pixel = Color32::from_gray(*shade); - } + image.pixels.copy_from_slice(&self.shades); } } diff --git a/src/window.rs b/src/window.rs index 132caca..fd7cfe1 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}; +pub use vram::{BgMapWindow, CharacterDataWindow, ObjectWindow}; use winit::event::KeyEvent; use crate::emulator::SimId; diff --git a/src/window/game.rs b/src/window/game.rs index 4617850..256fc78 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -144,6 +144,12 @@ impl GameWindow { .unwrap(); ui.close_menu(); } + if ui.button("Objects").clicked() { + self.proxy + .send_event(UserEvent::OpenObjects(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 a2898a0..ad93bef 100644 --- a/src/window/vram.rs +++ b/src/window/vram.rs @@ -1,6 +1,8 @@ mod bgmap; mod chardata; +mod object; mod utils; pub use bgmap::*; pub use chardata::*; +pub use object::*; diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index 2119733..67f1279 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -16,7 +16,7 @@ use crate::{ }, }; -use super::utils::{parse_palette, CharacterGrid, GENERIC_PALETTE}; +use super::utils::{self, CharacterGrid}; pub struct BgMapWindow { sim_id: SimId, @@ -90,12 +90,11 @@ impl BgMapWindow { }); let image = Image::new("vram://cell") .maintain_aspect_ratio(true) - .tint(Color32::RED) .texture_options(TextureOptions::NEAREST); ui.add(image); ui.section("Cell", |ui| { let cell = self.bgmaps.borrow().read::(self.cell_index); - let (char_index, mut vflip, mut hflip, palette_index) = parse_cell(cell); + let (char_index, mut vflip, mut hflip, palette_index) = utils::parse_cell(cell); TableBuilder::new(ui) .column(Column::remainder()) .column(Column::remainder()) @@ -136,18 +135,18 @@ impl BgMapWindow { }); }); }); - 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); - }); - ui.checkbox(&mut self.show_grid, "Show grid"); - ui.checkbox(&mut self.generic_palette, "Generic palette"); + }); + 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); }); + ui.checkbox(&mut self.show_grid, "Show grid"); + ui.checkbox(&mut self.generic_palette, "Generic palette"); }); }); self.params.write(BgMapParams { @@ -205,14 +204,6 @@ impl AppWindow for BgMapWindow { } } -fn parse_cell(cell: u16) -> (usize, bool, bool, usize) { - let char_index = (cell & 0x7ff) as usize; - let vflip = cell & 0x1000 != 0; - let hflip = cell & 0x2000 != 0; - let palette_index = (cell >> 14) as usize; - (char_index, vflip, hflip, palette_index) -} - #[derive(Default, Clone, PartialEq, Eq)] struct BgMapParams { cell_index: usize, @@ -244,19 +235,9 @@ impl BgMapRenderer { let brts = brightness.range::(0, 8); let colors = if generic_palette { - [ - GENERIC_PALETTE, - GENERIC_PALETTE, - GENERIC_PALETTE, - GENERIC_PALETTE, - ] + [utils::generic_palette(Color32::RED); 4] } else { - [ - parse_palette(palettes.read(0), brts), - parse_palette(palettes.read(2), brts), - parse_palette(palettes.read(4), brts), - parse_palette(palettes.read(6), brts), - ] + [0, 2, 4, 6].map(|i| utils::parse_palette(palettes.read(i), brts, Color32::RED)) }; for (i, cell) in bgmaps @@ -264,13 +245,13 @@ impl BgMapRenderer { .iter() .enumerate() { - let (char_index, vflip, hflip, palette_index) = parse_cell(*cell); + let (char_index, vflip, hflip, palette_index) = utils::parse_cell(*cell); let char = chardata.range::(char_index * 8, 8); let palette = &colors[palette_index]; for row in 0..8 { let y = row + (i / 64) * 8; - for (col, pixel) in self.read_char_row(char, hflip, vflip, row).enumerate() { + for (col, pixel) in utils::read_char_row(char, hflip, vflip, row).enumerate() { let x = col + (i % 64) * 8; image.write((x, y), palette[pixel as usize]); } @@ -288,34 +269,20 @@ impl BgMapRenderer { let cell = bgmaps.read::(index); - let (char_index, vflip, hflip, palette_index) = parse_cell(cell); + let (char_index, vflip, hflip, palette_index) = utils::parse_cell(cell); let char = chardata.range::(char_index * 8, 8); let palette = if generic_palette { - GENERIC_PALETTE + utils::generic_palette(Color32::RED) } else { - parse_palette(palettes.read(palette_index * 2), brts) + utils::parse_palette(palettes.read(palette_index * 2), brts, Color32::RED) }; for row in 0..8 { - for (col, pixel) in self.read_char_row(char, hflip, vflip, row).enumerate() { + for (col, pixel) in utils::read_char_row(char, hflip, vflip, row).enumerate() { image.write((col, row), palette[pixel as usize]); } } } - - fn read_char_row( - &self, - char: &[u16], - hflip: bool, - vflip: bool, - row: usize, - ) -> impl Iterator { - let pixels = if vflip { char[7 - row] } else { char[row] }; - (0..16).step_by(2).map(move |i| { - let pixel = if hflip { 14 - i } else { i }; - ((pixels >> pixel) & 0x3) as u8 - }) - } } impl VramRenderer<2> for BgMapRenderer { diff --git a/src/window/vram/chardata.rs b/src/window/vram/chardata.rs index d03f0d8..00ee780 100644 --- a/src/window/vram/chardata.rs +++ b/src/window/vram/chardata.rs @@ -162,7 +162,6 @@ impl CharacterDataWindow { }); let image = Image::new("vram://char") .maintain_aspect_ratio(true) - .tint(Color32::RED) .texture_options(TextureOptions::NEAREST); ui.add(image); ui.section("Colors", |ui| { @@ -191,11 +190,7 @@ impl CharacterDataWindow { let rect = ui.available_rect_before_wrap(); let scale = rect.height() / rect.width(); let rect = rect.scale_from_center2(Vec2::new(scale, 1.0)); - ui.painter().rect_filled( - rect, - 0.0, - Color32::RED * Color32::from_gray(color), - ); + ui.painter().rect_filled(rect, 0.0, color); }); } }); @@ -220,14 +215,14 @@ impl CharacterDataWindow { }); } - fn load_palette_colors(&self) -> [u8; 4] { + fn load_palette_colors(&self) -> [Color32; 4] { let Some(offset) = self.palette.offset() else { - return utils::GENERIC_PALETTE; + return utils::generic_palette(Color32::RED); }; let palette = self.palettes.borrow().read(offset); let brightnesses = self.brightness.borrow(); let brts = brightnesses.range(0, 8); - utils::parse_palette(palette, brts) + utils::parse_palette(palette, brts, Color32::RED) } fn show_chardata(&mut self, ui: &mut Ui) { @@ -349,13 +344,13 @@ impl CharDataRenderer { } } - fn load_palette(&self, palette: VramPalette) -> [u8; 4] { + fn load_palette(&self, palette: VramPalette) -> [Color32; 4] { let Some(offset) = palette.offset() else { - return utils::GENERIC_PALETTE; + return utils::GENERIC_PALETTE.map(|p| utils::shade(p, Color32::RED)); }; let palette = self.palettes.borrow().read(offset); let brightnesses = self.brightness.borrow(); let brts = brightnesses.range(0, 8); - utils::parse_palette(palette, brts) + utils::parse_palette(palette, brts, Color32::RED) } } diff --git a/src/window/vram/object.rs b/src/window/vram/object.rs new file mode 100644 index 0000000..23dbb60 --- /dev/null +++ b/src/window/vram/object.rs @@ -0,0 +1,277 @@ +use std::sync::Arc; + +use egui::{ + Align, CentralPanel, Checkbox, Color32, Context, Image, ScrollArea, Slider, TextEdit, + TextureOptions, Ui, ViewportBuilder, ViewportId, +}; +use egui_extras::{Column, Size, StripBuilder, TableBuilder}; + +use crate::{ + emulator::SimId, + memory::{MemoryMonitor, MemoryView}, + vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, + window::{ + utils::{NumberEdit, UiExt as _}, + AppWindow, + }, +}; + +use super::utils; + +pub struct ObjectWindow { + sim_id: SimId, + loader: Arc, + objects: MemoryView, + index: usize, + generic_palette: bool, + params: VramParams, + scale: f32, +} + +impl ObjectWindow { + pub fn new(sim_id: SimId, memory: &mut MemoryMonitor, vram: &mut VramProcessor) -> Self { + let renderer = ObjectRenderer::new(sim_id, memory); + let ([object], params) = vram.add(renderer); + let loader = VramTextureLoader::new([("vram://object".into(), object)]); + Self { + sim_id, + loader: Arc::new(loader), + objects: memory.view(sim_id, 0x0003e000, 0x2000), + index: 0, + generic_palette: false, + params, + scale: 1.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..1024)); + }); + }); + }); + ui.section("Properties", |ui| { + let object = self.objects.borrow().read::<[u16; 4]>(self.index); + let (mut char_index, mut vflip, mut hflip, palette_index) = + utils::parse_cell(object[3]); + TableBuilder::new(ui) + .column(Column::remainder()) + .column(Column::remainder()) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Character"); + }); + row.col(|ui| { + ui.add_enabled( + false, + NumberEdit::new(&mut char_index).range(0..2048), + ); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Palette"); + }); + row.col(|ui| { + let mut palette = format!("OBJ {}", palette_index); + ui.add_enabled( + false, + TextEdit::singleline(&mut palette).horizontal_align(Align::Max), + ); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + let checkbox = Checkbox::new(&mut hflip, "H-flip"); + ui.add_enabled(false, checkbox); + }); + row.col(|ui| { + let checkbox = Checkbox::new(&mut vflip, "V-flip"); + ui.add_enabled(false, checkbox); + }); + }); + }); + }); + 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); + }); + ui.checkbox(&mut self.generic_palette, "Generic palette"); + }); + }); + self.params.write(ObjectParams { + index: self.index, + generic_palette: self.generic_palette, + ..ObjectParams::default() + }); + } + + fn show_object(&mut self, ui: &mut Ui) { + let image = Image::new("vram://object") + .fit_to_original_size(self.scale) + .texture_options(TextureOptions::NEAREST); + ui.add(image); + } +} + +impl AppWindow for ObjectWindow { + fn viewport_id(&self) -> egui::ViewportId { + ViewportId::from_hash_of(format!("object-{}", self.sim_id)) + } + + fn sim_id(&self) -> SimId { + self.sim_id + } + + fn initial_viewport(&self) -> ViewportBuilder { + ViewportBuilder::default() + .with_title(format!("Object Data ({})", 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_object(ui)); + }); + }); + }); + }); + } +} + +#[derive(Clone, PartialEq, Eq)] +struct ObjectParams { + index: usize, + generic_palette: bool, + left_color: Color32, + right_color: Color32, +} + +impl Default for ObjectParams { + fn default() -> Self { + Self { + index: 0, + generic_palette: false, + left_color: Color32::from_rgb(0xff, 0x00, 0x00), + right_color: Color32::from_rgb(0x00, 0xc6, 0xf0), + } + } +} + +enum Eye { + Left, + Right, +} + +struct ObjectRenderer { + chardata: MemoryView, + objects: MemoryView, + brightness: MemoryView, + palettes: MemoryView, +} + +impl ObjectRenderer { + pub fn new(sim_id: SimId, memory: &mut MemoryMonitor) -> Self { + Self { + chardata: memory.view(sim_id, 0x00078000, 0x8000), + objects: memory.view(sim_id, 0x0003e000, 0x2000), + brightness: memory.view(sim_id, 0x0005f824, 8), + palettes: memory.view(sim_id, 0x0005f860, 16), + } + } + + fn render_object(&self, image: &mut VramImage, params: &ObjectParams, eye: Eye) { + let chardata = self.chardata.borrow(); + let objects = self.objects.borrow(); + let brightness = self.brightness.borrow(); + let palettes = self.palettes.borrow(); + + let object: [u16; 4] = objects.read(params.index); + + let ron = object[1] & 0x4000 != 0; + let lon = object[1] & 0x8000 != 0; + if match eye { + Eye::Left => !lon, + Eye::Right => !ron, + } { + return; + } + + let brts = brightness.range::(0, 8); + + let x = ((object[0] & 0x3ff) << 6 >> 6) as i16; + let parallax = ((object[1] & 0x3ff) << 6 >> 6) as i16; + let y = ((object[2] & 0x0ff) << 8 >> 8) as i16; + + let (x, color) = match eye { + Eye::Left => (x - parallax, params.left_color), + Eye::Right => (x + parallax, params.right_color), + }; + + let (char_index, vflip, hflip, palette_index) = utils::parse_cell(object[3]); + let char = chardata.range::(char_index * 8, 8); + let palette = if params.generic_palette { + utils::generic_palette(color) + } else { + utils::parse_palette(palettes.read(8 + palette_index * 2), brts, color) + }; + + for row in 0..8 { + let real_y = y + row as i16; + if !(0..384).contains(&real_y) { + continue; + } + for (col, pixel) in utils::read_char_row(char, hflip, vflip, row).enumerate() { + let real_x = x + col as i16; + if !(0..224).contains(&real_x) { + continue; + } + image.add((real_x as usize, real_y as usize), palette[pixel as usize]); + } + } + } +} + +impl VramRenderer<1> for ObjectRenderer { + type Params = ObjectParams; + + fn sizes(&self) -> [[usize; 2]; 1] { + [[384, 224]] + } + + fn render(&mut self, params: &Self::Params, images: &mut [VramImage; 1]) { + let image = &mut images[0]; + image.clear(); + self.render_object(image, params, Eye::Left); + self.render_object(image, params, Eye::Right); + } +} diff --git a/src/window/vram/utils.rs b/src/window/vram/utils.rs index d8ddf51..e8bf69f 100644 --- a/src/window/vram/utils.rs +++ b/src/window/vram/utils.rs @@ -2,21 +2,53 @@ use egui::{Color32, Image, ImageSource, Response, Sense, TextureOptions, Ui, Wid pub const GENERIC_PALETTE: [u8; 4] = [0, 64, 128, 255]; -pub fn parse_palette(palette: u8, brts: &[u8]) -> [u8; 4] { +pub fn shade(brt: u8, color: Color32) -> Color32 { + color.gamma_multiply(brt as f32 / 255.0) +} + +pub fn generic_palette(color: Color32) -> [Color32; 4] { + GENERIC_PALETTE.map(|brt| shade(brt, color)) +} + +pub fn parse_palette(palette: u8, brts: &[u8], color: Color32) -> [Color32; 4] { let shades = [ - 0, - brts[0], - brts[2], - brts[0].saturating_add(brts[2]).saturating_add(brts[4]), + Color32::BLACK, + shade(brts[0], color), + shade(brts[2], color), + shade( + brts[0].saturating_add(brts[2]).saturating_add(brts[4]), + color, + ), ]; [ - 0, + Color32::BLACK, shades[(palette >> 2) as usize & 0x03], shades[(palette >> 4) as usize & 0x03], shades[(palette >> 6) as usize & 0x03], ] } +pub fn parse_cell(cell: u16) -> (usize, bool, bool, usize) { + let char_index = (cell & 0x7ff) as usize; + let vflip = cell & 0x1000 != 0; + let hflip = cell & 0x2000 != 0; + let palette_index = (cell >> 14) as usize; + (char_index, vflip, hflip, palette_index) +} + +pub fn read_char_row( + char: &[u16], + hflip: bool, + vflip: bool, + row: usize, +) -> impl Iterator { + let pixels = if vflip { char[7 - row] } else { char[row] }; + (0..16).step_by(2).map(move |i| { + let pixel = if hflip { 14 - i } else { i }; + ((pixels >> pixel) & 0x3) as u8 + }) +} + pub struct CharacterGrid<'a> { source: ImageSource<'a>, scale: f32, @@ -71,7 +103,6 @@ impl Widget for CharacterGrid<'_> { fn ui(self, ui: &mut Ui) -> Response { let image = Image::new(self.source) .fit_to_original_size(self.scale) - .tint(Color32::RED) .texture_options(TextureOptions::NEAREST) .sense(Sense::click()); let res = ui.add(image);