From 6c9265cb780f522a8fedf7d6d4f1a4eebac03292 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 2 Feb 2025 15:16:11 -0500 Subject: [PATCH 01/34] First pass of character data viewer --- src/app.rs | 22 ++- src/main.rs | 1 + src/vram.rs | 372 +++++++++++++++++++++++++++++++++++ src/window.rs | 2 + src/window/character_data.rs | 253 ++++++++++++++++++++++++ src/window/game.rs | 6 + 6 files changed, 654 insertions(+), 2 deletions(-) create mode 100644 src/vram.rs create mode 100644 src/window/character_data.rs diff --git a/src/app.rs b/src/app.rs index 2cfa0a7..db0f77c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -19,7 +19,10 @@ use crate::{ emulator::{EmulatorClient, EmulatorCommand, SimId}, input::MappingProvider, persistence::Persistence, - window::{AboutWindow, AppWindow, GameWindow, GdbServerWindow, InputWindow}, + vram::VramLoader, + window::{ + AboutWindow, AppWindow, CharacterDataWindow, GameWindow, GdbServerWindow, InputWindow, + }, }; fn load_icon() -> anyhow::Result { @@ -36,6 +39,7 @@ fn load_icon() -> anyhow::Result { pub struct Application { icon: Option>, wgpu: WgpuState, + vram: Arc, client: EmulatorClient, proxy: EventLoopProxy, mappings: MappingProvider, @@ -65,6 +69,7 @@ impl Application { Self { icon, wgpu, + vram: Arc::new(VramLoader::new(client.clone())), client, proxy, mappings, @@ -83,7 +88,13 @@ impl Application { } self.viewports.insert( viewport_id, - Viewport::new(event_loop, &self.wgpu, self.icon.clone(), window), + Viewport::new( + event_loop, + &self.wgpu, + self.icon.clone(), + self.vram.clone(), + window, + ), ); } } @@ -197,6 +208,10 @@ impl ApplicationHandler for Application { let about = AboutWindow; self.open(event_loop, Box::new(about)); } + UserEvent::OpenCharacterData(sim_id) => { + let vram = CharacterDataWindow::new(sim_id); + self.open(event_loop, Box::new(vram)); + } UserEvent::OpenDebugger(sim_id) => { let debugger = GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone()); @@ -308,6 +323,7 @@ impl Viewport { event_loop: &ActiveEventLoop, wgpu: &WgpuState, icon: Option>, + vram: Arc, mut app: Box, ) -> Self { let ctx = Context::default(); @@ -329,6 +345,7 @@ impl Viewport { s.visuals.menu_rounding = Default::default(); }); egui_extras::install_image_loaders(&ctx); + ctx.add_image_loader(vram); let wgpu_config = egui_wgpu::WgpuConfiguration { present_mode: wgpu::PresentMode::AutoNoVsync, @@ -455,6 +472,7 @@ impl Drop for Viewport { pub enum UserEvent { GamepadEvent(gilrs::Event), OpenAbout, + OpenCharacterData(SimId), OpenDebugger(SimId), OpenInput, OpenPlayer2, diff --git a/src/main.rs b/src/main.rs index 33ae747..990851f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ mod gdbserver; mod graphics; mod input; mod persistence; +mod vram; mod window; #[derive(Parser)] diff --git a/src/vram.rs b/src/vram.rs new file mode 100644 index 0000000..5feb347 --- /dev/null +++ b/src/vram.rs @@ -0,0 +1,372 @@ +use serde::{Deserialize, Serialize}; +use std::{ + collections::{hash_map::Entry, HashMap}, + fmt::Display, + sync::{Arc, Mutex}, +}; +use tokio::sync::mpsc; + +use egui::{ + load::{ImageLoader, ImagePoll, LoadError}, + ColorImage, Context, Vec2, +}; + +use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; + +enum VramRequest { + Load(String, VramResource), +} + +enum VramResponse { + Loaded(String, ColorImage), +} + +pub struct VramResource { + sim: SimId, + kind: VramResourceKind, +} + +impl VramResource { + pub fn character_data(sim: SimId, palette: VramPalette) -> Self { + Self { + sim, + kind: VramResourceKind::CharacterData { palette }, + } + } + + pub fn character(sim: SimId, palette: VramPalette, index: usize) -> Self { + Self { + sim, + kind: VramResourceKind::Character { palette, index }, + } + } + + pub fn palette_color(sim: SimId, palette: VramPalette, index: usize) -> Self { + Self { + sim, + kind: VramResourceKind::PaletteColor { palette, index }, + } + } + + pub fn to_uri(&self) -> String { + format!( + "vram://{}:{}", + self.sim.to_index(), + serde_json::to_string(&self.kind).unwrap(), + ) + } + + fn from_uri(uri: &str) -> Option { + let uri = uri.strip_prefix("vram://")?; + let (sim, uri) = match uri.split_at_checked(2)? { + ("0:", rest) => (SimId::Player1, rest), + ("1:", rest) => (SimId::Player2, rest), + _ => return None, + }; + let kind = serde_json::from_str(uri).ok()?; + Some(Self { sim, kind }) + } +} + +#[derive(Serialize, Deserialize)] +enum VramResourceKind { + Character { palette: VramPalette, index: usize }, + CharacterData { palette: VramPalette }, + PaletteColor { palette: VramPalette, index: usize }, +} + +impl VramResourceKind { + fn size(&self) -> Option { + match self { + Self::Character { .. } => Some((8.0, 8.0).into()), + Self::CharacterData { .. } => Some((8.0 * 16.0, 8.0 * 128.0).into()), + Self::PaletteColor { .. } => Some((1.0, 1.0).into()), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum VramPalette { + Generic, + Bg0, + Bg1, + Bg2, + Bg3, + Obj0, + Obj1, + Obj2, + Obj3, +} + +impl VramPalette { + pub const fn values() -> [VramPalette; 9] { + [ + Self::Generic, + Self::Bg0, + Self::Bg1, + Self::Bg2, + Self::Bg3, + Self::Obj0, + Self::Obj1, + Self::Obj2, + Self::Obj3, + ] + } +} + +impl Display for VramPalette { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Generic => f.write_str("Generic"), + Self::Bg0 => f.write_str("BG 0"), + Self::Bg1 => f.write_str("BG 1"), + Self::Bg2 => f.write_str("BG 2"), + Self::Bg3 => f.write_str("BG 3"), + Self::Obj0 => f.write_str("OBJ 0"), + Self::Obj1 => f.write_str("OBJ 1"), + Self::Obj2 => f.write_str("OBJ 2"), + Self::Obj3 => f.write_str("OBJ 3"), + } + } +} + +pub struct VramLoader { + cache: Mutex>, + source: Mutex>, + sink: mpsc::UnboundedSender, +} + +impl VramLoader { + pub fn new(client: EmulatorClient) -> Self { + let (tx1, rx1) = mpsc::unbounded_channel(); + let (tx2, rx2) = mpsc::unbounded_channel(); + std::thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + let worker = VramLoadingWorker::new(rx2, tx1, client); + worker.run().await; + }) + }); + Self { + cache: Mutex::new(HashMap::new()), + source: Mutex::new(rx1), + sink: tx2, + } + } +} + +impl ImageLoader for VramLoader { + fn id(&self) -> &str { + concat!(module_path!(), "::VramLoader") + } + + fn load( + &self, + _ctx: &Context, + uri: &str, + _size_hint: egui::SizeHint, + ) -> Result { + let Some(resource) = VramResource::from_uri(uri) else { + return Err(LoadError::NotSupported); + }; + let mut cache = self.cache.lock().unwrap(); + { + let mut source = self.source.lock().unwrap(); + while let Ok(response) = source.try_recv() { + match response { + VramResponse::Loaded(uri, image) => { + cache.insert( + uri, + ImagePoll::Ready { + image: Arc::new(image), + }, + ); + } + } + } + } + let poll = match cache.entry(uri.to_string()) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let pending = ImagePoll::Pending { + size: resource.kind.size(), + }; + let _ = self.sink.send(VramRequest::Load(uri.to_string(), resource)); + entry.insert(pending) + } + }; + Ok(poll.clone()) + } + + fn forget(&self, uri: &str) { + self.cache.lock().unwrap().remove(uri); + } + + fn forget_all(&self) { + self.cache.lock().unwrap().clear(); + } + + fn byte_size(&self) -> usize { + self.cache + .lock() + .unwrap() + .values() + .map(|c| match c { + ImagePoll::Pending { .. } => 0, + ImagePoll::Ready { image } => image.pixels.len() * size_of::(), + }) + .sum() + } +} + +struct VramLoadingWorker { + source: mpsc::UnboundedReceiver, + sink: mpsc::UnboundedSender, + client: EmulatorClient, +} + +impl VramLoadingWorker { + fn new( + source: mpsc::UnboundedReceiver, + sink: mpsc::UnboundedSender, + client: EmulatorClient, + ) -> Self { + Self { + source, + sink, + client, + } + } + async fn run(mut self) { + while let Some(request) = self.source.recv().await { + #[allow(irrefutable_let_patterns)] + if let VramRequest::Load(uri, resource) = request { + let Some(image) = self.load(resource).await else { + continue; + }; + if self.sink.send(VramResponse::Loaded(uri, image)).is_err() { + return; + } + } + } + } + + async fn load(&self, resource: VramResource) -> Option { + let sim = resource.sim; + match resource.kind { + VramResourceKind::Character { palette, index } => { + self.load_character(sim, palette, index).await + } + VramResourceKind::CharacterData { palette } => { + self.load_character_data(sim, palette).await + } + VramResourceKind::PaletteColor { palette, index } => { + self.load_palette_color(sim, palette, index).await + } + } + } + + async fn load_character( + &self, + sim: SimId, + palette: VramPalette, + index: usize, + ) -> Option { + if index >= 2048 { + return None; + } + let address = 0x00078000 + (index as u32 * 16); + let (memory, palette) = tokio::join!( + self.read_memory(sim, address, 16), + self.load_palette_colors(sim, palette), + ); + let palette = palette?; + let mut buffer = vec![]; + for byte in memory? { + for offset in (0..8).step_by(2) { + let char = (byte >> offset) & 0x3; + buffer.push(palette[char as usize]); + } + } + Some(ColorImage::from_gray([8, 8], &buffer)) + } + + async fn load_character_data(&self, sim: SimId, palette: VramPalette) -> Option { + let (memory, palette) = tokio::join!( + self.read_memory(sim, 0x00078000, 16 * 2048), + self.load_palette_colors(sim, palette), + ); + let palette = palette?; + let mut buffer = vec![0; 8 * 8 * 2048]; + for (i, byte) in memory?.into_iter().enumerate() { + let bytes = [0, 2, 4, 6].map(|off| palette[(byte as usize >> off) & 0x3]); + let char_index = i / 16; + let in_char_pos = i % 16; + let x = ((char_index % 16) * 8) + ((in_char_pos % 2) * 4); + let y = ((char_index / 16) * 8) + (in_char_pos / 2); + let write_index = (y * 16 * 8) + x; + buffer[write_index..(write_index + 4)].copy_from_slice(&bytes); + } + Some(ColorImage::from_gray([8 * 16, 8 * 128], &buffer)) + } + + async fn load_palette_color( + &self, + sim: SimId, + palette: VramPalette, + index: usize, + ) -> Option { + if index == 0 { + return Some(ColorImage::from_gray([1, 1], &[0])); + } + if index > 3 { + return None; + } + let shade = *self.load_palette_colors(sim, palette).await?.get(index)?; + Some(ColorImage::from_gray([1, 1], &[shade])) + } + + async fn load_palette_colors(&self, sim: SimId, palette: VramPalette) -> Option<[u8; 4]> { + let offset = match palette { + VramPalette::Generic => { + return Some([0, 64, 128, 255]); + } + VramPalette::Bg0 => 0, + VramPalette::Bg1 => 2, + VramPalette::Bg2 => 4, + VramPalette::Bg3 => 6, + VramPalette::Obj0 => 8, + VramPalette::Obj1 => 10, + VramPalette::Obj2 => 12, + VramPalette::Obj3 => 14, + }; + let (palettes, brightnesses) = tokio::join!( + self.read_memory(sim, 0x0005f860, 16), + self.read_memory(sim, 0x0005f824, 6), + ); + let palette = *palettes?.get(offset)?; + let brts = brightnesses?; + let shades = [ + 0, + brts[0], + brts[2], + brts[0].saturating_add(brts[2]).saturating_add(brts[4]), + ]; + Some([ + 0, + shades[(palette >> 2) as usize & 0x03], + shades[(palette >> 4) as usize & 0x03], + shades[(palette >> 6) as usize & 0x03], + ]) + } + + async fn read_memory(&self, sim: SimId, address: u32, size: usize) -> Option> { + let (tx, rx) = oneshot::channel(); + self.client + .send_command(EmulatorCommand::ReadMemory(sim, address, size, vec![], tx)); + rx.await.ok() + } +} diff --git a/src/window.rs b/src/window.rs index 85048ff..2bccf7b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,4 +1,5 @@ pub use about::AboutWindow; +pub use character_data::CharacterDataWindow; use egui::{Context, ViewportBuilder, ViewportId}; pub use game::GameWindow; pub use gdb::GdbServerWindow; @@ -8,6 +9,7 @@ use winit::event::KeyEvent; use crate::emulator::SimId; mod about; +mod character_data; mod game; mod game_screen; mod gdb; diff --git a/src/window/character_data.rs b/src/window/character_data.rs new file mode 100644 index 0000000..b908988 --- /dev/null +++ b/src/window/character_data.rs @@ -0,0 +1,253 @@ +use egui::{ + Align, CentralPanel, Color32, ComboBox, Context, Frame, Image, RichText, ScrollArea, Sense, + Slider, TextEdit, TextureOptions, Ui, UiBuilder, Vec2, ViewportBuilder, ViewportId, +}; +use egui_extras::{Column, Size, StripBuilder, TableBuilder}; + +use crate::{ + emulator::SimId, + vram::{VramPalette, VramResource}, +}; + +use super::AppWindow; + +pub struct CharacterDataWindow { + sim_id: SimId, + palette: VramPalette, + index: usize, + index_str: String, + scale: f32, + show_grid: bool, +} + +impl CharacterDataWindow { + pub fn new(sim_id: SimId) -> Self { + Self { + sim_id, + palette: VramPalette::Generic, + index: 0, + index_str: "0".into(), + scale: 4.0, + show_grid: true, + } + } + + 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| { + let res = ui.add( + TextEdit::singleline(&mut self.index_str) + .horizontal_align(Align::Max), + ); + if res.changed() { + if let Some(index) = + self.index_str.parse().ok().filter(|id| *id < 2048) + { + self.index = index; + } + } + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Address"); + }); + row.col(|ui| { + let address = match self.index { + 0x000..0x200 => 0x00060000 + self.index, + 0x200..0x400 => 0x000e0000 + (self.index - 0x200), + 0x400..0x600 => 0x00160000 + (self.index - 0x400), + 0x600..0x800 => 0x001e0000 + (self.index - 0x600), + _ => unreachable!("can't happen"), + }; + 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("Mirror"); + }); + row.col(|ui| { + let mirror = 0x00078000 + (self.index * 16); + let mut mirror_str = format!("{mirror:08x}"); + ui.add_enabled( + false, + TextEdit::singleline(&mut mirror_str).horizontal_align(Align::Max), + ); + }); + }); + }); + let resource = VramResource::character(self.sim_id, self.palette, self.index); + let image = Image::new(resource.to_uri()) + .maintain_aspect_ratio(true) + .tint(Color32::RED) + .texture_options(TextureOptions::NEAREST); + ui.add(image); + ui.section("Colors", |ui| { + ui.horizontal(|ui| { + ui.label("Palette"); + ComboBox::from_id_salt("palette") + .selected_text(self.palette.to_string()) + .width(ui.available_width()) + .show_ui(ui, |ui| { + for palette in VramPalette::values() { + ui.selectable_value( + &mut self.palette, + palette, + palette.to_string(), + ); + } + }); + }); + TableBuilder::new(ui) + .columns(Column::remainder(), 4) + .body(|mut body| { + body.row(30.0, |mut row| { + for index in 0..4 { + let resource = + VramResource::palette_color(self.sim_id, self.palette, index); + row.col(|ui| { + let rect = ui.available_rect_before_wrap(); + let scale = rect.height() / rect.width(); + let rect = rect.scale_from_center2(Vec2::new(scale, 1.0)); + let image = Image::new(resource.to_uri()) + .tint(Color32::RED) + .fit_to_exact_size(rect.max - rect.min); + ui.put(rect, image); + }); + } + }); + }); + }); + 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"); + }); + }); + } + + fn show_chardata(&mut self, ui: &mut Ui) { + let start_pos = ui.cursor().min; + let resource = VramResource::character_data(self.sim_id, self.palette); + let image = Image::new(resource.to_uri()) + .fit_to_original_size(self.scale) + .tint(Color32::RED) + .texture_options(TextureOptions::NEAREST) + .sense(Sense::click()); + let res = ui.add(image); + if res.clicked() { + if let Some(click_pos) = res.interact_pointer_pos() { + let fixed_pos = (click_pos - start_pos) / self.scale; + let x = (fixed_pos.x / 8.0) as usize; + let y = (fixed_pos.y / 8.0) as usize; + self.index = (y * 16) + x; + self.index_str = self.index.to_string(); + } + } + let painter = ui.painter_at(res.rect); + if self.show_grid { + let stroke = ui.style().visuals.widgets.noninteractive.fg_stroke; + for x in (1..16).map(|i| (i as f32) * 8.0 * self.scale) { + let p1 = res.rect.min + (x, 0.0).into(); + let p2 = res.rect.min + (x, 128.0 * 8.0 * self.scale).into(); + painter.line(vec![p1, p2], stroke); + } + for y in (1..128).map(|i| (i as f32) * 8.0 * self.scale) { + let p1 = res.rect.min + (0.0, y).into(); + let p2 = res.rect.min + (16.0 * 8.0 * self.scale, y).into(); + painter.line(vec![p1, p2], stroke); + } + } + // draw box around selected + let x1 = (self.index % 16) as f32 * 8.0 * self.scale; + let x2 = x1 + (8.0 * self.scale); + let y1 = (self.index / 16) as f32 * 8.0 * self.scale; + let y2 = y1 + (8.0 * self.scale); + painter.line( + vec![ + (res.rect.min + (x1, y1).into()), + (res.rect.min + (x2, y1).into()), + (res.rect.min + (x2, y2).into()), + (res.rect.min + (x1, y2).into()), + (res.rect.min + (x1, y1).into()), + ], + ui.style().visuals.widgets.active.fg_stroke, + ); + } +} + +impl AppWindow for CharacterDataWindow { + fn viewport_id(&self) -> ViewportId { + ViewportId::from_hash_of(format!("chardata-{}", self.sim_id)) + } + + fn sim_id(&self) -> SimId { + self.sim_id + } + + fn initial_viewport(&self) -> ViewportBuilder { + ViewportBuilder::default() + .with_title(format!("Character Data ({})", self.sim_id)) + .with_inner_size((640.0, 480.0)) + } + + 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_chardata(ui)); + }); + }); + }); + }); + } +} + +trait UiExt { + fn section(&mut self, title: impl Into, add_contents: impl FnOnce(&mut Ui)); +} + +impl UiExt for Ui { + fn section(&mut self, title: impl Into, add_contents: impl FnOnce(&mut Ui)) { + let mut frame = Frame::group(self.style()); + frame.outer_margin.top += 10.0; + frame.inner_margin.top += 2.0; + let res = frame.show(self, add_contents); + let text = RichText::new(title).background_color(self.style().visuals.panel_fill); + let old_rect = res.response.rect; + let mut text_rect = old_rect; + text_rect.min.x += 6.0; + let new_rect = self + .allocate_new_ui(UiBuilder::new().max_rect(text_rect), |ui| ui.label(text)) + .response + .rect; + self.allocate_space((old_rect.max - new_rect.max) - (old_rect.min - new_rect.min)); + } +} diff --git a/src/window/game.rs b/src/window/game.rs index b3406bc..4912493 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -132,6 +132,12 @@ impl GameWindow { .unwrap(); ui.close_menu(); } + if ui.button("Character Data").clicked() { + self.proxy + .send_event(UserEvent::OpenCharacterData(self.sim_id)) + .unwrap(); + ui.close_menu(); + } }); ui.menu_button("About", |ui| { self.proxy.send_event(UserEvent::OpenAbout).unwrap(); -- 2.40.1 From 75779f2b24869a5892e27632406ec33a7647c7eb Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 2 Feb 2025 18:33:59 -0500 Subject: [PATCH 02/34] Dummy second window --- src/app.rs | 8 ++++- src/window.rs | 4 +-- src/window/game.rs | 6 ++++ src/window/vram.rs | 5 ++++ src/window/vram/bgmap.rs | 29 +++++++++++++++++++ .../{character_data.rs => vram/chardata.rs} | 3 +- 6 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 src/window/vram.rs create mode 100644 src/window/vram/bgmap.rs rename src/window/{character_data.rs => vram/chardata.rs} (99%) diff --git a/src/app.rs b/src/app.rs index db0f77c..a4f7c98 100644 --- a/src/app.rs +++ b/src/app.rs @@ -21,7 +21,8 @@ use crate::{ persistence::Persistence, vram::VramLoader, window::{ - AboutWindow, AppWindow, CharacterDataWindow, GameWindow, GdbServerWindow, InputWindow, + AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, GameWindow, GdbServerWindow, + InputWindow, }, }; @@ -212,6 +213,10 @@ impl ApplicationHandler for Application { let vram = CharacterDataWindow::new(sim_id); self.open(event_loop, Box::new(vram)); } + UserEvent::OpenBgMap(sim_id) => { + let bgmap = BgMapWindow::new(sim_id); + self.open(event_loop, Box::new(bgmap)); + } UserEvent::OpenDebugger(sim_id) => { let debugger = GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone()); @@ -473,6 +478,7 @@ pub enum UserEvent { GamepadEvent(gilrs::Event), OpenAbout, OpenCharacterData(SimId), + OpenBgMap(SimId), OpenDebugger(SimId), OpenInput, OpenPlayer2, diff --git a/src/window.rs b/src/window.rs index 2bccf7b..9c47b75 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,19 +1,19 @@ pub use about::AboutWindow; -pub use character_data::CharacterDataWindow; use egui::{Context, ViewportBuilder, ViewportId}; pub use game::GameWindow; pub use gdb::GdbServerWindow; pub use input::InputWindow; +pub use vram::{BgMapWindow, CharacterDataWindow}; use winit::event::KeyEvent; use crate::emulator::SimId; mod about; -mod character_data; mod game; mod game_screen; mod gdb; mod input; +mod vram; pub trait AppWindow { fn viewport_id(&self) -> ViewportId; diff --git a/src/window/game.rs b/src/window/game.rs index 4912493..c33194e 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -138,6 +138,12 @@ impl GameWindow { .unwrap(); ui.close_menu(); } + if ui.button("Background Maps").clicked() { + self.proxy + .send_event(UserEvent::OpenBgMap(self.sim_id)) + .unwrap(); + ui.close_menu(); + } }); ui.menu_button("About", |ui| { self.proxy.send_event(UserEvent::OpenAbout).unwrap(); diff --git a/src/window/vram.rs b/src/window/vram.rs new file mode 100644 index 0000000..99fb788 --- /dev/null +++ b/src/window/vram.rs @@ -0,0 +1,5 @@ +mod bgmap; +mod chardata; + +pub use bgmap::*; +pub use chardata::*; diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs new file mode 100644 index 0000000..1a9d164 --- /dev/null +++ b/src/window/vram/bgmap.rs @@ -0,0 +1,29 @@ +use egui::{CentralPanel, Context, ViewportBuilder, ViewportId}; + +use crate::{emulator::SimId, window::AppWindow}; + +pub struct BgMapWindow { + sim_id: SimId, +} + +impl BgMapWindow { + pub fn new(sim_id: SimId) -> Self { + Self { sim_id } + } +} + +impl AppWindow for BgMapWindow { + fn viewport_id(&self) -> ViewportId { + ViewportId::from_hash_of(format!("bgmap-{}", self.sim_id)) + } + + fn initial_viewport(&self) -> ViewportBuilder { + ViewportBuilder::default() + .with_title(format!("BG Map Data ({})", self.sim_id)) + .with_inner_size((640.0, 480.0)) + } + + fn show(&mut self, ctx: &Context) { + CentralPanel::default().show(ctx, |ui| ui.label("TODO")); + } +} diff --git a/src/window/character_data.rs b/src/window/vram/chardata.rs similarity index 99% rename from src/window/character_data.rs rename to src/window/vram/chardata.rs index b908988..81f49b3 100644 --- a/src/window/character_data.rs +++ b/src/window/vram/chardata.rs @@ -7,10 +7,9 @@ use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use crate::{ emulator::SimId, vram::{VramPalette, VramResource}, + window::AppWindow, }; -use super::AppWindow; - pub struct CharacterDataWindow { sim_id: SimId, palette: VramPalette, -- 2.40.1 From 57eebb5874dec788151caf5628baaa7f4fa38529 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Tue, 4 Feb 2025 23:23:06 -0500 Subject: [PATCH 03/34] New implementation for reading/showing vram --- src/app.rs | 23 +-- src/main.rs | 1 + src/memory.rs | 88 ++++++++ src/vram.rs | 395 +++++++----------------------------- src/window.rs | 3 +- src/window/game.rs | 2 +- src/window/vram.rs | 1 + src/window/vram/bgmap.rs | 132 +++++++++++- src/window/vram/chardata.rs | 207 +++++++++++++++++-- src/window/vram/utils.rs | 16 ++ 10 files changed, 510 insertions(+), 358 deletions(-) create mode 100644 src/memory.rs create mode 100644 src/window/vram/utils.rs diff --git a/src/app.rs b/src/app.rs index a4f7c98..8c36a44 100644 --- a/src/app.rs +++ b/src/app.rs @@ -18,8 +18,8 @@ use crate::{ controller::ControllerManager, emulator::{EmulatorClient, EmulatorCommand, SimId}, input::MappingProvider, + memory::MemoryMonitor, persistence::Persistence, - vram::VramLoader, window::{ AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, GameWindow, GdbServerWindow, InputWindow, @@ -40,11 +40,11 @@ fn load_icon() -> anyhow::Result { pub struct Application { icon: Option>, wgpu: WgpuState, - vram: Arc, client: EmulatorClient, proxy: EventLoopProxy, mappings: MappingProvider, controllers: ControllerManager, + memory: MemoryMonitor, persistence: Persistence, viewports: HashMap, focused: Option, @@ -62,6 +62,7 @@ impl Application { let persistence = Persistence::new(); let mappings = MappingProvider::new(persistence.clone()); let controllers = ControllerManager::new(client.clone(), &mappings); + let memory = MemoryMonitor::new(client.clone()); { let mappings = mappings.clone(); let proxy = proxy.clone(); @@ -70,10 +71,10 @@ impl Application { Self { icon, wgpu, - vram: Arc::new(VramLoader::new(client.clone())), client, proxy, mappings, + memory, controllers, persistence, viewports: HashMap::new(), @@ -89,13 +90,7 @@ impl Application { } self.viewports.insert( viewport_id, - Viewport::new( - event_loop, - &self.wgpu, - self.icon.clone(), - self.vram.clone(), - window, - ), + Viewport::new(event_loop, &self.wgpu, self.icon.clone(), window), ); } } @@ -210,11 +205,11 @@ impl ApplicationHandler for Application { self.open(event_loop, Box::new(about)); } UserEvent::OpenCharacterData(sim_id) => { - let vram = CharacterDataWindow::new(sim_id); + let vram = CharacterDataWindow::new(sim_id, &mut self.memory); self.open(event_loop, Box::new(vram)); } UserEvent::OpenBgMap(sim_id) => { - let bgmap = BgMapWindow::new(sim_id); + let bgmap = BgMapWindow::new(sim_id, &mut self.memory); self.open(event_loop, Box::new(bgmap)); } UserEvent::OpenDebugger(sim_id) => { @@ -328,7 +323,6 @@ impl Viewport { event_loop: &ActiveEventLoop, wgpu: &WgpuState, icon: Option>, - vram: Arc, mut app: Box, ) -> Self { let ctx = Context::default(); @@ -350,7 +344,6 @@ impl Viewport { s.visuals.menu_rounding = Default::default(); }); egui_extras::install_image_loaders(&ctx); - ctx.add_image_loader(vram); let wgpu_config = egui_wgpu::WgpuConfiguration { present_mode: wgpu::PresentMode::AutoNoVsync, @@ -374,7 +367,7 @@ impl Viewport { let (window, state) = create_window_and_state(&ctx, event_loop, &builder, &mut painter); egui_winit::update_viewport_info(&mut info, &ctx, &window, true); - app.on_init(painter.render_state().as_ref().unwrap()); + app.on_init(&ctx, painter.render_state().as_ref().unwrap()); Self { painter, ctx, diff --git a/src/main.rs b/src/main.rs index 990851f..5b43893 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ mod emulator; mod gdbserver; mod graphics; mod input; +mod memory; mod persistence; mod vram; mod window; diff --git a/src/memory.rs b/src/memory.rs new file mode 100644 index 0000000..9279b44 --- /dev/null +++ b/src/memory.rs @@ -0,0 +1,88 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex, MutexGuard}, +}; + +use bytemuck::BoxBytes; + +use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; + +pub struct MemoryMonitor { + client: EmulatorClient, + regions: HashMap>>, +} + +impl MemoryMonitor { + pub fn new(client: EmulatorClient) -> Self { + Self { + client, + regions: HashMap::new(), + } + } + + pub fn view(&mut self, sim: SimId, start: u32, length: usize) -> MemoryView { + let region = MemoryRegion { sim, start, length }; + let memory = self.regions.entry(region).or_insert_with(|| { + let mut buf = aligned_memory(start, length); + let (tx, rx) = oneshot::channel(); + self.client + .send_command(EmulatorCommand::ReadMemory(sim, start, length, vec![], tx)); + let bytes = pollster::block_on(rx).unwrap(); + buf.copy_from_slice(&bytes); + #[expect(clippy::arc_with_non_send_sync)] // TODO: remove after bytemuck upgrade + Arc::new(Mutex::new(buf)) + }); + MemoryView { + memory: memory.clone(), + } + } +} + +fn aligned_memory(start: u32, length: usize) -> BoxBytes { + if start % 4 == 0 && length % 4 == 0 { + let memory = vec![0u32; length / 4].into_boxed_slice(); + return bytemuck::box_bytes_of(memory); + } + if start % 2 == 0 && length % 2 == 0 { + let memory = vec![0u16; length / 2].into_boxed_slice(); + return bytemuck::box_bytes_of(memory); + } + let memory = vec![0u8; length].into_boxed_slice(); + bytemuck::box_bytes_of(memory) +} + +pub struct MemoryView { + memory: Arc>, +} +// SAFETY: BoxBytes is supposed to be Send+Sync, will be in a new version +unsafe impl Send for MemoryView {} + +impl MemoryView { + pub fn borrow(&self) -> MemoryRef<'_> { + MemoryRef { + inner: self.memory.lock().unwrap(), + } + } +} + +pub struct MemoryRef<'a> { + inner: MutexGuard<'a, BoxBytes>, +} + +impl MemoryRef<'_> { + pub fn read(&self, index: usize) -> u8 { + self.inner[index] + } + pub fn range(&self, start: usize, count: usize) -> &[T] { + let from = start * size_of::(); + let to = from + (count * size_of::()); + bytemuck::cast_slice(&self.inner[from..to]) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +struct MemoryRegion { + sim: SimId, + start: u32, + length: usize, +} diff --git a/src/vram.rs b/src/vram.rs index 5feb347..3bf9e07 100644 --- a/src/vram.rs +++ b/src/vram.rs @@ -1,208 +1,109 @@ -use serde::{Deserialize, Serialize}; use std::{ collections::{hash_map::Entry, HashMap}, - fmt::Display, - sync::{Arc, Mutex}, + hash::Hash, + sync::Mutex, }; -use tokio::sync::mpsc; use egui::{ - load::{ImageLoader, ImagePoll, LoadError}, - ColorImage, Context, Vec2, + epaint::ImageDelta, + load::{LoadError, SizedTexture, TextureLoader, TexturePoll}, + ColorImage, Context, TextureHandle, TextureOptions, }; +use serde::{Deserialize, Serialize}; -use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; - -enum VramRequest { - Load(String, VramResource), +pub trait VramResource: Sized + PartialEq + Eq + Hash { + fn to_uri(&self) -> String; + fn from_uri(uri: &str) -> Option; } -enum VramResponse { - Loaded(String, ColorImage), +impl Deserialize<'a> + PartialEq + Eq + Hash> VramResource for T { + fn to_uri(&self) -> String { + format!("vram://{}", serde_json::to_string(self).unwrap()) + } + + fn from_uri(uri: &str) -> Option { + let content = uri.strip_prefix("vram://")?; + serde_json::from_str(content).ok() + } } -pub struct VramResource { - sim: SimId, - kind: VramResourceKind, +pub trait VramImageLoader { + type Resource: VramResource; + + fn id(&self) -> &str; + fn add(&self, resource: &Self::Resource) -> Option; + fn update<'a>( + &'a self, + resources: impl Iterator, + ) -> Vec<(&'a Self::Resource, ColorImage)>; } -impl VramResource { - pub fn character_data(sim: SimId, palette: VramPalette) -> Self { - Self { - sim, - kind: VramResourceKind::CharacterData { palette }, - } - } - - pub fn character(sim: SimId, palette: VramPalette, index: usize) -> Self { - Self { - sim, - kind: VramResourceKind::Character { palette, index }, - } - } - - pub fn palette_color(sim: SimId, palette: VramPalette, index: usize) -> Self { - Self { - sim, - kind: VramResourceKind::PaletteColor { palette, index }, - } - } - - pub fn to_uri(&self) -> String { - format!( - "vram://{}:{}", - self.sim.to_index(), - serde_json::to_string(&self.kind).unwrap(), - ) - } - - fn from_uri(uri: &str) -> Option { - let uri = uri.strip_prefix("vram://")?; - let (sim, uri) = match uri.split_at_checked(2)? { - ("0:", rest) => (SimId::Player1, rest), - ("1:", rest) => (SimId::Player2, rest), - _ => return None, - }; - let kind = serde_json::from_str(uri).ok()?; - Some(Self { sim, kind }) - } -} - -#[derive(Serialize, Deserialize)] -enum VramResourceKind { - Character { palette: VramPalette, index: usize }, - CharacterData { palette: VramPalette }, - PaletteColor { palette: VramPalette, index: usize }, -} - -impl VramResourceKind { - fn size(&self) -> Option { - match self { - Self::Character { .. } => Some((8.0, 8.0).into()), - Self::CharacterData { .. } => Some((8.0 * 16.0, 8.0 * 128.0).into()), - Self::PaletteColor { .. } => Some((1.0, 1.0).into()), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum VramPalette { - Generic, - Bg0, - Bg1, - Bg2, - Bg3, - Obj0, - Obj1, - Obj2, - Obj3, -} - -impl VramPalette { - pub const fn values() -> [VramPalette; 9] { - [ - Self::Generic, - Self::Bg0, - Self::Bg1, - Self::Bg2, - Self::Bg3, - Self::Obj0, - Self::Obj1, - Self::Obj2, - Self::Obj3, - ] - } -} - -impl Display for VramPalette { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Generic => f.write_str("Generic"), - Self::Bg0 => f.write_str("BG 0"), - Self::Bg1 => f.write_str("BG 1"), - Self::Bg2 => f.write_str("BG 2"), - Self::Bg3 => f.write_str("BG 3"), - Self::Obj0 => f.write_str("OBJ 0"), - Self::Obj1 => f.write_str("OBJ 1"), - Self::Obj2 => f.write_str("OBJ 2"), - Self::Obj3 => f.write_str("OBJ 3"), - } - } -} - -pub struct VramLoader { - cache: Mutex>, - source: Mutex>, - sink: mpsc::UnboundedSender, -} - -impl VramLoader { - pub fn new(client: EmulatorClient) -> Self { - let (tx1, rx1) = mpsc::unbounded_channel(); - let (tx2, rx2) = mpsc::unbounded_channel(); - std::thread::spawn(move || { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(async move { - let worker = VramLoadingWorker::new(rx2, tx1, client); - worker.run().await; - }) - }); +pub struct VramTextureLoader { + id: String, + loader: Mutex, + cache: Mutex>, +} + +impl VramTextureLoader { + pub fn new(loader: T) -> Self { Self { + id: loader.id().to_string(), + loader: Mutex::new(loader), cache: Mutex::new(HashMap::new()), - source: Mutex::new(rx1), - sink: tx2, } } } -impl ImageLoader for VramLoader { +impl TextureLoader for VramTextureLoader { fn id(&self) -> &str { - concat!(module_path!(), "::VramLoader") + &self.id } fn load( &self, - _ctx: &Context, + ctx: &Context, uri: &str, + texture_options: TextureOptions, _size_hint: egui::SizeHint, - ) -> Result { - let Some(resource) = VramResource::from_uri(uri) else { + ) -> Result { + let Some(resource) = T::Resource::from_uri(uri) else { return Err(LoadError::NotSupported); }; + if texture_options != TextureOptions::NEAREST { + return Err(LoadError::Loading( + "Only TextureOptions::NEAREST are supported".into(), + )); + } + let loader = self.loader.lock().unwrap(); let mut cache = self.cache.lock().unwrap(); - { - let mut source = self.source.lock().unwrap(); - while let Ok(response) = source.try_recv() { - match response { - VramResponse::Loaded(uri, image) => { - cache.insert( - uri, - ImagePoll::Ready { - image: Arc::new(image), - }, - ); - } + for (resource, updated_image) in loader.update(cache.keys()) { + if let Some(handle) = cache.get(resource) { + let delta = ImageDelta::full(updated_image, TextureOptions::NEAREST); + ctx.tex_manager().write().set(handle.id(), delta); + } + } + match cache.entry(resource) { + Entry::Occupied(entry) => { + let texture = SizedTexture::from_handle(entry.get()); + Ok(TexturePoll::Ready { texture }) + } + Entry::Vacant(entry) => { + if let Some(image) = loader.add(entry.key()) { + let handle = + entry.insert(ctx.load_texture(uri, image, TextureOptions::NEAREST)); + let texture = SizedTexture::from_handle(handle); + Ok(TexturePoll::Ready { texture }) + } else { + Err(LoadError::Loading("could not load texture".into())) } } } - let poll = match cache.entry(uri.to_string()) { - Entry::Occupied(entry) => entry.into_mut(), - Entry::Vacant(entry) => { - let pending = ImagePoll::Pending { - size: resource.kind.size(), - }; - let _ = self.sink.send(VramRequest::Load(uri.to_string(), resource)); - entry.insert(pending) - } - }; - Ok(poll.clone()) } fn forget(&self, uri: &str) { - self.cache.lock().unwrap().remove(uri); + if let Some(resource) = T::Resource::from_uri(uri) { + self.cache.lock().unwrap().remove(&resource); + } } fn forget_all(&self) { @@ -214,159 +115,7 @@ impl ImageLoader for VramLoader { .lock() .unwrap() .values() - .map(|c| match c { - ImagePoll::Pending { .. } => 0, - ImagePoll::Ready { image } => image.pixels.len() * size_of::(), - }) + .map(|h| h.byte_size()) .sum() } } - -struct VramLoadingWorker { - source: mpsc::UnboundedReceiver, - sink: mpsc::UnboundedSender, - client: EmulatorClient, -} - -impl VramLoadingWorker { - fn new( - source: mpsc::UnboundedReceiver, - sink: mpsc::UnboundedSender, - client: EmulatorClient, - ) -> Self { - Self { - source, - sink, - client, - } - } - async fn run(mut self) { - while let Some(request) = self.source.recv().await { - #[allow(irrefutable_let_patterns)] - if let VramRequest::Load(uri, resource) = request { - let Some(image) = self.load(resource).await else { - continue; - }; - if self.sink.send(VramResponse::Loaded(uri, image)).is_err() { - return; - } - } - } - } - - async fn load(&self, resource: VramResource) -> Option { - let sim = resource.sim; - match resource.kind { - VramResourceKind::Character { palette, index } => { - self.load_character(sim, palette, index).await - } - VramResourceKind::CharacterData { palette } => { - self.load_character_data(sim, palette).await - } - VramResourceKind::PaletteColor { palette, index } => { - self.load_palette_color(sim, palette, index).await - } - } - } - - async fn load_character( - &self, - sim: SimId, - palette: VramPalette, - index: usize, - ) -> Option { - if index >= 2048 { - return None; - } - let address = 0x00078000 + (index as u32 * 16); - let (memory, palette) = tokio::join!( - self.read_memory(sim, address, 16), - self.load_palette_colors(sim, palette), - ); - let palette = palette?; - let mut buffer = vec![]; - for byte in memory? { - for offset in (0..8).step_by(2) { - let char = (byte >> offset) & 0x3; - buffer.push(palette[char as usize]); - } - } - Some(ColorImage::from_gray([8, 8], &buffer)) - } - - async fn load_character_data(&self, sim: SimId, palette: VramPalette) -> Option { - let (memory, palette) = tokio::join!( - self.read_memory(sim, 0x00078000, 16 * 2048), - self.load_palette_colors(sim, palette), - ); - let palette = palette?; - let mut buffer = vec![0; 8 * 8 * 2048]; - for (i, byte) in memory?.into_iter().enumerate() { - let bytes = [0, 2, 4, 6].map(|off| palette[(byte as usize >> off) & 0x3]); - let char_index = i / 16; - let in_char_pos = i % 16; - let x = ((char_index % 16) * 8) + ((in_char_pos % 2) * 4); - let y = ((char_index / 16) * 8) + (in_char_pos / 2); - let write_index = (y * 16 * 8) + x; - buffer[write_index..(write_index + 4)].copy_from_slice(&bytes); - } - Some(ColorImage::from_gray([8 * 16, 8 * 128], &buffer)) - } - - async fn load_palette_color( - &self, - sim: SimId, - palette: VramPalette, - index: usize, - ) -> Option { - if index == 0 { - return Some(ColorImage::from_gray([1, 1], &[0])); - } - if index > 3 { - return None; - } - let shade = *self.load_palette_colors(sim, palette).await?.get(index)?; - Some(ColorImage::from_gray([1, 1], &[shade])) - } - - async fn load_palette_colors(&self, sim: SimId, palette: VramPalette) -> Option<[u8; 4]> { - let offset = match palette { - VramPalette::Generic => { - return Some([0, 64, 128, 255]); - } - VramPalette::Bg0 => 0, - VramPalette::Bg1 => 2, - VramPalette::Bg2 => 4, - VramPalette::Bg3 => 6, - VramPalette::Obj0 => 8, - VramPalette::Obj1 => 10, - VramPalette::Obj2 => 12, - VramPalette::Obj3 => 14, - }; - let (palettes, brightnesses) = tokio::join!( - self.read_memory(sim, 0x0005f860, 16), - self.read_memory(sim, 0x0005f824, 6), - ); - let palette = *palettes?.get(offset)?; - let brts = brightnesses?; - let shades = [ - 0, - brts[0], - brts[2], - brts[0].saturating_add(brts[2]).saturating_add(brts[4]), - ]; - Some([ - 0, - shades[(palette >> 2) as usize & 0x03], - shades[(palette >> 4) as usize & 0x03], - shades[(palette >> 6) as usize & 0x03], - ]) - } - - async fn read_memory(&self, sim: SimId, address: u32, size: usize) -> Option> { - let (tx, rx) = oneshot::channel(); - self.client - .send_command(EmulatorCommand::ReadMemory(sim, address, size, vec![], tx)); - rx.await.ok() - } -} diff --git a/src/window.rs b/src/window.rs index 9c47b75..da1e265 100644 --- a/src/window.rs +++ b/src/window.rs @@ -22,7 +22,8 @@ pub trait AppWindow { } fn initial_viewport(&self) -> ViewportBuilder; fn show(&mut self, ctx: &Context); - fn on_init(&mut self, render_state: &egui_wgpu::RenderState) { + fn on_init(&mut self, ctx: &Context, render_state: &egui_wgpu::RenderState) { + let _ = ctx; let _ = render_state; } fn on_destroy(&mut self) {} diff --git a/src/window/game.rs b/src/window/game.rs index c33194e..aa7c2ca 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -377,7 +377,7 @@ impl AppWindow for GameWindow { toasts.show(ctx); } - fn on_init(&mut self, render_state: &egui_wgpu::RenderState) { + fn on_init(&mut self, _ctx: &Context, render_state: &egui_wgpu::RenderState) { let (screen, sink) = GameScreen::init(render_state); let (message_sink, message_source) = mpsc::channel(); self.client.send_command(EmulatorCommand::ConnectToSim( diff --git a/src/window/vram.rs b/src/window/vram.rs index 99fb788..a2898a0 100644 --- a/src/window/vram.rs +++ b/src/window/vram.rs @@ -1,5 +1,6 @@ mod bgmap; mod chardata; +mod utils; pub use bgmap::*; pub use chardata::*; diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index 1a9d164..5997e3a 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -1,14 +1,28 @@ -use egui::{CentralPanel, Context, ViewportBuilder, ViewportId}; +use std::sync::Arc; -use crate::{emulator::SimId, window::AppWindow}; +use egui::{CentralPanel, ColorImage, Context, Image, TextureOptions, ViewportBuilder, ViewportId}; +use serde::{Deserialize, Serialize}; + +use crate::{ + emulator::SimId, + memory::{MemoryMonitor, MemoryView}, + vram::{VramImageLoader, VramResource as _, VramTextureLoader}, + window::AppWindow, +}; + +use super::utils::parse_palette; pub struct BgMapWindow { sim_id: SimId, + loader: Option, } impl BgMapWindow { - pub fn new(sim_id: SimId) -> Self { - Self { sim_id } + pub fn new(sim_id: SimId, memory: &mut MemoryMonitor) -> Self { + Self { + sim_id, + loader: Some(BgMapLoader::new(sim_id, memory)), + } } } @@ -17,13 +31,121 @@ impl AppWindow for BgMapWindow { ViewportId::from_hash_of(format!("bgmap-{}", self.sim_id)) } + fn sim_id(&self) -> SimId { + self.sim_id + } + fn initial_viewport(&self) -> ViewportBuilder { ViewportBuilder::default() .with_title(format!("BG Map Data ({})", self.sim_id)) .with_inner_size((640.0, 480.0)) } + fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) { + let loader = self.loader.take().unwrap(); + ctx.add_texture_loader(Arc::new(VramTextureLoader::new(loader))); + } + fn show(&mut self, ctx: &Context) { - CentralPanel::default().show(ctx, |ui| ui.label("TODO")); + CentralPanel::default().show(ctx, |ui| { + let resource = BgMapResource { index: 0 }; + let image = Image::new(resource.to_uri()).texture_options(TextureOptions::NEAREST); + ui.add(image); + }); + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Hash)] +struct BgMapResource { + index: usize, +} + +struct BgMapLoader { + chardata: MemoryView, + bgmaps: MemoryView, + brightness: MemoryView, + palettes: MemoryView, +} + +impl BgMapLoader { + pub fn new(sim_id: SimId, memory: &mut MemoryMonitor) -> Self { + Self { + chardata: memory.view(sim_id, 0x00078000, 0x8000), + bgmaps: memory.view(sim_id, 0x00020000, 0x1d800), + brightness: memory.view(sim_id, 0x0005f824, 8), + palettes: memory.view(sim_id, 0x0005f860, 16), + } + } + + fn load_bgmap(&self, index: usize) -> Option { + let chardata = self.chardata.borrow(); + let bgmaps = self.bgmaps.borrow(); + let brightness = self.brightness.borrow(); + let palettes = self.palettes.borrow(); + + let brts = brightness.range::(0, 8); + let colors = [ + parse_palette(palettes.read(0), brts), + parse_palette(palettes.read(2), brts), + parse_palette(palettes.read(4), brts), + parse_palette(palettes.read(6), brts), + ]; + + let mut data = vec![0u8; 512 * 512]; + for (i, cell) in bgmaps.range::(index * 4096, 4096).iter().enumerate() { + let char_index = (cell & 0x7ff) as usize; + let char = chardata.range::(char_index * 8, 8); + let vflip = cell & 0x1000 != 0; + let hflip = cell & 0x2000 != 0; + let palette_index = (cell >> 14) as usize; + let palette = &colors[palette_index]; + + let mut target_idx = (i % 64) * 8 + (i / 64) * 8 * 512; + for row in 0..8 { + let dests = &mut data[target_idx..target_idx + 8]; + let pixels = self.read_char_row(char, hflip, vflip, row); + for (dest, pixel) in dests.iter_mut().zip(pixels) { + *dest = palette[pixel as usize]; + } + target_idx += 512; + } + } + + Some(ColorImage::from_gray([512, 512], &data)) + } + + 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 VramImageLoader for BgMapLoader { + type Resource = BgMapResource; + + fn id(&self) -> &str { + concat!(module_path!(), "::BgMapLoader") + } + + fn add(&self, resource: &Self::Resource) -> Option { + let BgMapResource { index } = resource; + self.load_bgmap(*index) + } + + fn update<'a>( + &'a self, + resources: impl Iterator, + ) -> Vec<(&'a Self::Resource, ColorImage)> { + let _ = resources; + vec![] } } diff --git a/src/window/vram/chardata.rs b/src/window/vram/chardata.rs index 81f49b3..65be884 100644 --- a/src/window/vram/chardata.rs +++ b/src/window/vram/chardata.rs @@ -1,17 +1,86 @@ +use std::{fmt::Display, sync::Arc}; + use egui::{ - Align, CentralPanel, Color32, ComboBox, Context, Frame, Image, RichText, ScrollArea, Sense, - Slider, TextEdit, TextureOptions, Ui, UiBuilder, Vec2, ViewportBuilder, ViewportId, + Align, CentralPanel, Color32, ColorImage, ComboBox, Context, Frame, Image, RichText, + ScrollArea, Sense, Slider, TextEdit, TextureOptions, Ui, UiBuilder, Vec2, ViewportBuilder, + ViewportId, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; +use serde::{Deserialize, Serialize}; use crate::{ emulator::SimId, - vram::{VramPalette, VramResource}, + memory::{MemoryMonitor, MemoryView}, + vram::{VramImageLoader, VramResource as _, VramTextureLoader}, window::AppWindow, }; +use super::utils; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum VramPalette { + Generic, + Bg0, + Bg1, + Bg2, + Bg3, + Obj0, + Obj1, + Obj2, + Obj3, +} + +impl VramPalette { + pub const fn values() -> [VramPalette; 9] { + [ + Self::Generic, + Self::Bg0, + Self::Bg1, + Self::Bg2, + Self::Bg3, + Self::Obj0, + Self::Obj1, + Self::Obj2, + Self::Obj3, + ] + } + + pub const fn offset(self) -> Option { + match self { + Self::Generic => None, + Self::Bg0 => Some(0), + Self::Bg1 => Some(2), + Self::Bg2 => Some(4), + Self::Bg3 => Some(6), + Self::Obj0 => Some(8), + Self::Obj1 => Some(10), + Self::Obj2 => Some(12), + Self::Obj3 => Some(14), + } + } +} + +impl Display for VramPalette { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Generic => f.write_str("Generic"), + Self::Bg0 => f.write_str("BG 0"), + Self::Bg1 => f.write_str("BG 1"), + Self::Bg2 => f.write_str("BG 2"), + Self::Bg3 => f.write_str("BG 3"), + Self::Obj0 => f.write_str("OBJ 0"), + Self::Obj1 => f.write_str("OBJ 1"), + Self::Obj2 => f.write_str("OBJ 2"), + Self::Obj3 => f.write_str("OBJ 3"), + } + } +} + pub struct CharacterDataWindow { sim_id: SimId, + loader: Option, + brightness: MemoryView, + palettes: MemoryView, palette: VramPalette, index: usize, index_str: String, @@ -20,9 +89,12 @@ pub struct CharacterDataWindow { } impl CharacterDataWindow { - pub fn new(sim_id: SimId) -> Self { + pub fn new(sim_id: SimId, memory: &mut MemoryMonitor) -> Self { Self { sim_id, + loader: Some(CharDataLoader::new(sim_id, memory)), + brightness: memory.view(sim_id, 0x0005f824, 8), + palettes: memory.view(sim_id, 0x0005f860, 16), palette: VramPalette::Generic, index: 0, index_str: "0".into(), @@ -89,7 +161,10 @@ impl CharacterDataWindow { }); }); }); - let resource = VramResource::character(self.sim_id, self.palette, self.index); + let resource = CharDataResource::Character { + palette: self.palette, + index: self.index, + }; let image = Image::new(resource.to_uri()) .maintain_aspect_ratio(true) .tint(Color32::RED) @@ -114,18 +189,18 @@ impl CharacterDataWindow { TableBuilder::new(ui) .columns(Column::remainder(), 4) .body(|mut body| { + let palette = self.load_palette_colors(); body.row(30.0, |mut row| { - for index in 0..4 { - let resource = - VramResource::palette_color(self.sim_id, self.palette, index); + for color in palette { row.col(|ui| { let rect = ui.available_rect_before_wrap(); let scale = rect.height() / rect.width(); let rect = rect.scale_from_center2(Vec2::new(scale, 1.0)); - let image = Image::new(resource.to_uri()) - .tint(Color32::RED) - .fit_to_exact_size(rect.max - rect.min); - ui.put(rect, image); + ui.painter().rect_filled( + rect, + 0.0, + Color32::RED * Color32::from_gray(color), + ); }); } }); @@ -145,9 +220,21 @@ impl CharacterDataWindow { }); } + fn load_palette_colors(&self) -> [u8; 4] { + let Some(offset) = self.palette.offset() else { + return utils::GENERIC_PALETTE; + }; + let palette = self.palettes.borrow().read(offset); + let brightnesses = self.brightness.borrow(); + let brts = brightnesses.range(0, 8); + utils::parse_palette(palette, brts) + } + fn show_chardata(&mut self, ui: &mut Ui) { let start_pos = ui.cursor().min; - let resource = VramResource::character_data(self.sim_id, self.palette); + let resource = CharDataResource::CharacterData { + palette: self.palette, + }; let image = Image::new(resource.to_uri()) .fit_to_original_size(self.scale) .tint(Color32::RED) @@ -210,6 +297,11 @@ impl AppWindow for CharacterDataWindow { .with_inner_size((640.0, 480.0)) } + fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) { + let loader = self.loader.take().unwrap(); + ctx.add_texture_loader(Arc::new(VramTextureLoader::new(loader))); + } + fn show(&mut self, ctx: &Context) { CentralPanel::default().show(ctx, |ui| { ui.horizontal_top(|ui| { @@ -229,6 +321,95 @@ impl AppWindow for CharacterDataWindow { } } +#[derive(Serialize, Deserialize, PartialEq, Eq, Hash)] +enum CharDataResource { + Character { palette: VramPalette, index: usize }, + CharacterData { palette: VramPalette }, +} + +struct CharDataLoader { + chardata: MemoryView, + brightness: MemoryView, + palettes: MemoryView, +} + +impl CharDataLoader { + pub fn new(sim_id: SimId, memory: &mut MemoryMonitor) -> Self { + Self { + chardata: memory.view(sim_id, 0x00078000, 0x8000), + brightness: memory.view(sim_id, 0x0005f824, 8), + palettes: memory.view(sim_id, 0x0005f860, 16), + } + } + + fn load_character(&self, palette: VramPalette, index: usize) -> Option { + if index >= 2048 { + return None; + } + let palette = self.load_palette(palette); + let chardata = self.chardata.borrow(); + let character = chardata.range::(index * 8, 8); + let mut buffer = Vec::with_capacity(8 * 8); + for row in character { + for offset in (0..16).step_by(2) { + let char = (row >> offset) & 0x3; + buffer.push(palette[char as usize]); + } + } + Some(ColorImage::from_gray([8, 8], &buffer)) + } + + fn load_character_data(&self, palette: VramPalette) -> Option { + let palette = self.load_palette(palette); + let chardata = self.chardata.borrow(); + let mut buffer = vec![0; 8 * 8 * 2048]; + for (i, row) in chardata.range::(0, 2048).iter().enumerate() { + let bytes = + [0, 2, 4, 6, 8, 10, 12, 14].map(|off| palette[(*row as usize >> off) & 0x3]); + let char_index = i / 8; + let row_index = i % 8; + let x = (char_index % 16) * 8; + let y = (char_index / 16) * 8 + row_index; + let write_index = (y * 16 * 8) + x; + buffer[write_index..write_index + 8].copy_from_slice(&bytes); + } + Some(ColorImage::from_gray([8 * 16, 8 * 128], &buffer)) + } + + fn load_palette(&self, palette: VramPalette) -> [u8; 4] { + let Some(offset) = palette.offset() else { + return utils::GENERIC_PALETTE; + }; + let palette = self.palettes.borrow().read(offset); + let brightnesses = self.brightness.borrow(); + let brts = brightnesses.range(0, 8); + utils::parse_palette(palette, brts) + } +} + +impl VramImageLoader for CharDataLoader { + type Resource = CharDataResource; + + fn id(&self) -> &str { + concat!(module_path!(), "::CharDataLoader") + } + + fn add(&self, resource: &Self::Resource) -> Option { + match resource { + CharDataResource::Character { palette, index } => self.load_character(*palette, *index), + CharDataResource::CharacterData { palette } => self.load_character_data(*palette), + } + } + + fn update<'a>( + &'a self, + resources: impl Iterator, + ) -> Vec<(&'a Self::Resource, ColorImage)> { + let _ = resources; + vec![] + } +} + trait UiExt { fn section(&mut self, title: impl Into, add_contents: impl FnOnce(&mut Ui)); } diff --git a/src/window/vram/utils.rs b/src/window/vram/utils.rs new file mode 100644 index 0000000..1889b3e --- /dev/null +++ b/src/window/vram/utils.rs @@ -0,0 +1,16 @@ +pub const GENERIC_PALETTE: [u8; 4] = [0, 64, 128, 255]; + +pub fn parse_palette(palette: u8, brts: &[u8]) -> [u8; 4] { + let shades = [ + 0, + brts[0], + brts[2], + brts[0].saturating_add(brts[2]).saturating_add(brts[4]), + ]; + [ + 0, + shades[(palette >> 2) as usize & 0x03], + shades[(palette >> 4) as usize & 0x03], + shades[(palette >> 6) as usize & 0x03], + ] +} -- 2.40.1 From 4601f1b52f6d2ef6ceb391e4e4f348fb5c0c0c21 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Thu, 6 Feb 2025 23:17:11 -0500 Subject: [PATCH 04/34] Flesh out bgmap viewer --- src/memory.rs | 7 +- src/window.rs | 1 + src/window/game.rs | 69 +--------- src/window/utils.rs | 107 +++++++++++++++ src/window/vram/bgmap.rs | 259 +++++++++++++++++++++++++++++++++--- src/window/vram/chardata.rs | 104 +++------------ src/window/vram/utils.rs | 99 ++++++++++++++ 7 files changed, 469 insertions(+), 177 deletions(-) create mode 100644 src/window/utils.rs diff --git a/src/memory.rs b/src/memory.rs index 9279b44..dcafe7a 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -70,9 +70,12 @@ pub struct MemoryRef<'a> { } impl MemoryRef<'_> { - pub fn read(&self, index: usize) -> u8 { - self.inner[index] + pub fn read(&self, index: usize) -> T { + let from = index * size_of::(); + let to = from + size_of::(); + *bytemuck::from_bytes(&self.inner[from..to]) } + pub fn range(&self, start: usize, count: usize) -> &[T] { let from = start * size_of::(); let to = from + (count * size_of::()); diff --git a/src/window.rs b/src/window.rs index da1e265..ac5922e 100644 --- a/src/window.rs +++ b/src/window.rs @@ -13,6 +13,7 @@ mod game; mod game_screen; mod gdb; mod input; +mod utils; mod vram; pub trait AppWindow { diff --git a/src/window/game.rs b/src/window/game.rs index aa7c2ca..1130b65 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -6,9 +6,8 @@ use crate::{ persistence::Persistence, }; use egui::{ - ecolor::HexColor, menu, Align2, Button, CentralPanel, Color32, Context, Direction, Frame, - Layout, Response, Sense, TopBottomPanel, Ui, Vec2, ViewportBuilder, ViewportCommand, - ViewportId, WidgetText, Window, + menu, Align2, Button, CentralPanel, Color32, Context, Direction, Frame, TopBottomPanel, Ui, + Vec2, ViewportBuilder, ViewportCommand, ViewportId, Window, }; use egui_toast::{Toast, Toasts}; use serde::{Deserialize, Serialize}; @@ -16,6 +15,7 @@ use winit::event_loop::EventLoopProxy; use super::{ game_screen::{DisplayMode, GameScreen}, + utils::UiExt as _, AppWindow, }; @@ -397,69 +397,6 @@ impl AppWindow for GameWindow { } } -trait UiExt { - fn selectable_button(&mut self, selected: bool, text: impl Into) -> Response; - fn selectable_option( - &mut self, - current_value: &mut T, - selected_value: T, - text: impl Into, - ) -> Response { - let response = self.selectable_button(*current_value == selected_value, text); - if response.clicked() { - *current_value = selected_value; - } - response - } - - fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response; - - fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response; -} - -impl UiExt for Ui { - fn selectable_button(&mut self, selected: bool, text: impl Into) -> Response { - self.style_mut().visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT; - self.style_mut().visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT; - self.style_mut().visuals.widgets.active.bg_fill = Color32::TRANSPARENT; - let mut selected = selected; - self.checkbox(&mut selected, text) - } - - fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response { - let button_size = Vec2::new(60.0, 20.0); - let (rect, response) = self.allocate_at_least(button_size, Sense::click()); - let center_x = rect.center().x; - let left_rect = rect.with_max_x(center_x); - self.painter().rect_filled(left_rect, 0.0, left); - let right_rect = rect.with_min_x(center_x); - self.painter().rect_filled(right_rect, 0.0, right); - - let style = self.style().interact(&response); - self.painter().rect_stroke(rect, 0.0, style.fg_stroke); - response - } - - fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response { - self.allocate_ui_with_layout( - Vec2::new(100.0, 130.0), - Layout::top_down_justified(egui::Align::Center), - |ui| { - let (rect, _) = ui.allocate_at_least(Vec2::new(100.0, 100.0), Sense::hover()); - ui.painter().rect_filled(rect, 0.0, *color); - let resp = ui.text_edit_singleline(hex); - if resp.changed() { - if let Ok(new_color) = HexColor::from_str_without_hash(hex) { - *color = new_color.color(); - } - } - resp - }, - ) - .inner - } -} - struct ColorPickerState { color_codes: [String; 2], just_opened: bool, diff --git a/src/window/utils.rs b/src/window/utils.rs new file mode 100644 index 0000000..1887671 --- /dev/null +++ b/src/window/utils.rs @@ -0,0 +1,107 @@ +use std::{ops::Range, str::FromStr}; + +use egui::{ + ecolor::HexColor, Align, Color32, Frame, Layout, Response, RichText, Sense, TextEdit, Ui, + UiBuilder, Vec2, WidgetText, +}; + +pub trait UiExt { + fn section(&mut self, title: impl Into, add_contents: impl FnOnce(&mut Ui)); + fn selectable_button(&mut self, selected: bool, text: impl Into) -> Response; + fn selectable_option( + &mut self, + current_value: &mut T, + selected_value: T, + text: impl Into, + ) -> Response { + let response = self.selectable_button(*current_value == selected_value, text); + if response.clicked() { + *current_value = selected_value; + } + response + } + + fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response; + + fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response; + + fn number_picker( + &mut self, + text: &mut String, + value: &mut T, + range: Range, + ); +} + +impl UiExt for Ui { + fn section(&mut self, title: impl Into, add_contents: impl FnOnce(&mut Ui)) { + let mut frame = Frame::group(self.style()); + frame.outer_margin.top += 10.0; + frame.inner_margin.top += 2.0; + let res = frame.show(self, add_contents); + let text = RichText::new(title).background_color(self.style().visuals.panel_fill); + let old_rect = res.response.rect; + let mut text_rect = old_rect; + text_rect.min.x += 6.0; + let new_rect = self + .allocate_new_ui(UiBuilder::new().max_rect(text_rect), |ui| ui.label(text)) + .response + .rect; + self.allocate_space((old_rect.max - new_rect.max) - (old_rect.min - new_rect.min)); + } + + fn selectable_button(&mut self, selected: bool, text: impl Into) -> Response { + self.style_mut().visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT; + self.style_mut().visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT; + self.style_mut().visuals.widgets.active.bg_fill = Color32::TRANSPARENT; + let mut selected = selected; + self.checkbox(&mut selected, text) + } + + fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response { + let button_size = Vec2::new(60.0, 20.0); + let (rect, response) = self.allocate_at_least(button_size, Sense::click()); + let center_x = rect.center().x; + let left_rect = rect.with_max_x(center_x); + self.painter().rect_filled(left_rect, 0.0, left); + let right_rect = rect.with_min_x(center_x); + self.painter().rect_filled(right_rect, 0.0, right); + + let style = self.style().interact(&response); + self.painter().rect_stroke(rect, 0.0, style.fg_stroke); + response + } + + fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response { + self.allocate_ui_with_layout( + Vec2::new(100.0, 130.0), + Layout::top_down_justified(egui::Align::Center), + |ui| { + let (rect, _) = ui.allocate_at_least(Vec2::new(100.0, 100.0), Sense::hover()); + ui.painter().rect_filled(rect, 0.0, *color); + let resp = ui.text_edit_singleline(hex); + if resp.changed() { + if let Ok(new_color) = HexColor::from_str_without_hash(hex) { + *color = new_color.color(); + } + } + resp + }, + ) + .inner + } + + fn number_picker( + &mut self, + text: &mut String, + value: &mut T, + range: Range, + ) { + let res = self.add(TextEdit::singleline(text).horizontal_align(Align::Max)); + if res.changed() { + if let Some(new_value) = text.parse().ok().filter(|v| range.contains(v)) { + *value = new_value; + } + } + } +} diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index 5997e3a..2a4962d 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -1,20 +1,30 @@ use std::sync::Arc; -use egui::{CentralPanel, ColorImage, Context, Image, TextureOptions, ViewportBuilder, ViewportId}; +use egui::{ + Align, CentralPanel, Checkbox, Color32, ColorImage, Context, Image, ScrollArea, Slider, + TextEdit, TextureOptions, Ui, ViewportBuilder, ViewportId, +}; +use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use serde::{Deserialize, Serialize}; use crate::{ emulator::SimId, memory::{MemoryMonitor, MemoryView}, vram::{VramImageLoader, VramResource as _, VramTextureLoader}, - window::AppWindow, + window::{utils::UiExt, AppWindow}, }; -use super::utils::parse_palette; +use super::utils::{parse_palette, CharacterGrid, GENERIC_PALETTE}; pub struct BgMapWindow { sim_id: SimId, loader: Option, + bgmaps: MemoryView, + cell_index: usize, + cell_index_str: String, + scale: f32, + show_grid: bool, + generic_palette: bool, } impl BgMapWindow { @@ -22,6 +32,142 @@ impl BgMapWindow { Self { sim_id, loader: Some(BgMapLoader::new(sim_id, memory)), + bgmaps: memory.view(sim_id, 0x00020000, 0x1d800), + cell_index: 0, + cell_index_str: "0".into(), + scale: 1.0, + show_grid: false, + generic_palette: false, + } + } + + 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("Map"); + }); + row.col(|ui| { + let mut bgmap_index = self.cell_index / 4096; + let mut bgmap_index_str = bgmap_index.to_string(); + ui.number_picker(&mut bgmap_index_str, &mut bgmap_index, 0..14); + if bgmap_index != self.cell_index / 4096 { + self.cell_index = (bgmap_index * 4096) + (self.cell_index % 4096); + self.cell_index_str = self.cell_index.to_string(); + } + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Cell"); + }); + row.col(|ui| { + ui.number_picker( + &mut self.cell_index_str, + &mut self.cell_index, + 0..16 * 4096, + ); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Address"); + }); + row.col(|ui| { + let address = 0x00020000 + (self.cell_index * 2); + let mut address_str = format!("{address:08x}"); + ui.add_enabled( + false, + TextEdit::singleline(&mut address_str).horizontal_align(Align::Max), + ); + }); + }); + }); + let resource = BgMapResource::Cell { + index: self.cell_index, + generic_palette: self.generic_palette, + }; + let image = Image::new(resource.to_uri()) + .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); + 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| { + let mut character_str = char_index.to_string(); + ui.add_enabled( + false, + TextEdit::singleline(&mut character_str) + .horizontal_align(Align::Max), + ); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Palette"); + }); + row.col(|ui| { + let mut palette = format!("BG {}", 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.show_grid, "Show grid"); + ui.checkbox(&mut self.generic_palette, "Generic palette"); + }); + }); + }); + } + + fn show_bgmap(&mut self, ui: &mut Ui) { + let resource = BgMapResource::BgMap { + index: self.cell_index / 4096, + generic_palette: self.generic_palette, + }; + let grid = CharacterGrid::new(resource.to_uri()) + .with_scale(self.scale) + .with_grid(self.show_grid) + .with_selected(self.cell_index % 4096); + if let Some(selected) = grid.show(ui) { + self.cell_index = (self.cell_index / 4096 * 4096) + selected; + self.cell_index_str = self.cell_index.to_string(); } } } @@ -48,16 +194,35 @@ impl AppWindow for BgMapWindow { fn show(&mut self, ctx: &Context) { CentralPanel::default().show(ctx, |ui| { - let resource = BgMapResource { index: 0 }; - let image = Image::new(resource.to_uri()).texture_options(TextureOptions::NEAREST); - ui.add(image); + 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_bgmap(ui)); + }); + }) + }); }); } } +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(Serialize, Deserialize, PartialEq, Eq, Hash)] -struct BgMapResource { - index: usize, +enum BgMapResource { + BgMap { index: usize, generic_palette: bool }, + Cell { index: usize, generic_palette: bool }, } struct BgMapLoader { @@ -77,27 +242,37 @@ impl BgMapLoader { } } - fn load_bgmap(&self, index: usize) -> Option { + fn load_bgmap(&self, bgmap_index: usize, generic_palette: bool) -> Option { let chardata = self.chardata.borrow(); let bgmaps = self.bgmaps.borrow(); let brightness = self.brightness.borrow(); let palettes = self.palettes.borrow(); let brts = brightness.range::(0, 8); - let colors = [ - parse_palette(palettes.read(0), brts), - parse_palette(palettes.read(2), brts), - parse_palette(palettes.read(4), brts), - parse_palette(palettes.read(6), brts), - ]; + let colors = if generic_palette { + [ + GENERIC_PALETTE, + GENERIC_PALETTE, + GENERIC_PALETTE, + GENERIC_PALETTE, + ] + } 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), + ] + }; let mut data = vec![0u8; 512 * 512]; - for (i, cell) in bgmaps.range::(index * 4096, 4096).iter().enumerate() { - let char_index = (cell & 0x7ff) as usize; + for (i, cell) in bgmaps + .range::(bgmap_index * 4096, 4096) + .iter() + .enumerate() + { + let (char_index, vflip, hflip, palette_index) = parse_cell(*cell); let char = chardata.range::(char_index * 8, 8); - let vflip = cell & 0x1000 != 0; - let hflip = cell & 0x2000 != 0; - let palette_index = (cell >> 14) as usize; let palette = &colors[palette_index]; let mut target_idx = (i % 64) * 8 + (i / 64) * 8 * 512; @@ -114,6 +289,38 @@ impl BgMapLoader { Some(ColorImage::from_gray([512, 512], &data)) } + fn load_bgmap_cell(&self, index: usize, generic_palette: bool) -> Option { + let chardata = self.chardata.borrow(); + let bgmaps = self.bgmaps.borrow(); + let brightness = self.brightness.borrow(); + let palettes = self.palettes.borrow(); + + let brts = brightness.range::(0, 8); + + let mut data = vec![0u8; 8 * 8]; + let cell = bgmaps.read::(index); + + let (char_index, vflip, hflip, palette_index) = parse_cell(cell); + let char = chardata.range::(char_index * 8, 8); + let palette = if generic_palette { + GENERIC_PALETTE + } else { + parse_palette(palettes.read(palette_index * 2), brts) + }; + + let mut target_idx = 0; + for row in 0..8 { + let dests = &mut data[target_idx..target_idx + 8]; + let pixels = self.read_char_row(char, hflip, vflip, row); + for (dest, pixel) in dests.iter_mut().zip(pixels) { + *dest = palette[pixel as usize]; + } + target_idx += 8; + } + + Some(ColorImage::from_gray([8, 8], &data)) + } + fn read_char_row( &self, char: &[u16], @@ -137,8 +344,16 @@ impl VramImageLoader for BgMapLoader { } fn add(&self, resource: &Self::Resource) -> Option { - let BgMapResource { index } = resource; - self.load_bgmap(*index) + match resource { + BgMapResource::BgMap { + index, + generic_palette, + } => self.load_bgmap(*index, *generic_palette), + BgMapResource::Cell { + index, + generic_palette, + } => self.load_bgmap_cell(*index, *generic_palette), + } } fn update<'a>( diff --git a/src/window/vram/chardata.rs b/src/window/vram/chardata.rs index 65be884..a8eb3f7 100644 --- a/src/window/vram/chardata.rs +++ b/src/window/vram/chardata.rs @@ -1,9 +1,8 @@ use std::{fmt::Display, sync::Arc}; use egui::{ - Align, CentralPanel, Color32, ColorImage, ComboBox, Context, Frame, Image, RichText, - ScrollArea, Sense, Slider, TextEdit, TextureOptions, Ui, UiBuilder, Vec2, ViewportBuilder, - ViewportId, + Align, CentralPanel, Color32, ColorImage, ComboBox, Context, Image, ScrollArea, Slider, + TextEdit, TextureOptions, Ui, Vec2, ViewportBuilder, ViewportId, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use serde::{Deserialize, Serialize}; @@ -12,10 +11,10 @@ use crate::{ emulator::SimId, memory::{MemoryMonitor, MemoryView}, vram::{VramImageLoader, VramResource as _, VramTextureLoader}, - window::AppWindow, + window::{utils::UiExt as _, AppWindow}, }; -use super::utils; +use super::utils::{self, CharacterGrid}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum VramPalette { @@ -115,17 +114,7 @@ impl CharacterDataWindow { ui.label("Index"); }); row.col(|ui| { - let res = ui.add( - TextEdit::singleline(&mut self.index_str) - .horizontal_align(Align::Max), - ); - if res.changed() { - if let Some(index) = - self.index_str.parse().ok().filter(|id| *id < 2048) - { - self.index = index; - } - } + ui.number_picker(&mut self.index_str, &mut self.index, 0..2048); }); }); body.row(row_height, |mut row| { @@ -134,10 +123,10 @@ impl CharacterDataWindow { }); row.col(|ui| { let address = match self.index { - 0x000..0x200 => 0x00060000 + self.index, - 0x200..0x400 => 0x000e0000 + (self.index - 0x200), - 0x400..0x600 => 0x00160000 + (self.index - 0x400), - 0x600..0x800 => 0x001e0000 + (self.index - 0x600), + 0x000..0x200 => 0x00060000 + self.index * 16, + 0x200..0x400 => 0x000e0000 + (self.index - 0x200) * 16, + 0x400..0x600 => 0x00160000 + (self.index - 0x400) * 16, + 0x600..0x800 => 0x001e0000 + (self.index - 0x600) * 16, _ => unreachable!("can't happen"), }; let mut address_str = format!("{address:08x}"); @@ -231,54 +220,17 @@ impl CharacterDataWindow { } fn show_chardata(&mut self, ui: &mut Ui) { - let start_pos = ui.cursor().min; let resource = CharDataResource::CharacterData { palette: self.palette, }; - let image = Image::new(resource.to_uri()) - .fit_to_original_size(self.scale) - .tint(Color32::RED) - .texture_options(TextureOptions::NEAREST) - .sense(Sense::click()); - let res = ui.add(image); - if res.clicked() { - if let Some(click_pos) = res.interact_pointer_pos() { - let fixed_pos = (click_pos - start_pos) / self.scale; - let x = (fixed_pos.x / 8.0) as usize; - let y = (fixed_pos.y / 8.0) as usize; - self.index = (y * 16) + x; - self.index_str = self.index.to_string(); - } + let grid = CharacterGrid::new(resource.to_uri()) + .with_scale(self.scale) + .with_grid(self.show_grid) + .with_selected(self.index); + if let Some(selected) = grid.show(ui) { + self.index = selected; + self.index_str = selected.to_string(); } - let painter = ui.painter_at(res.rect); - if self.show_grid { - let stroke = ui.style().visuals.widgets.noninteractive.fg_stroke; - for x in (1..16).map(|i| (i as f32) * 8.0 * self.scale) { - let p1 = res.rect.min + (x, 0.0).into(); - let p2 = res.rect.min + (x, 128.0 * 8.0 * self.scale).into(); - painter.line(vec![p1, p2], stroke); - } - for y in (1..128).map(|i| (i as f32) * 8.0 * self.scale) { - let p1 = res.rect.min + (0.0, y).into(); - let p2 = res.rect.min + (16.0 * 8.0 * self.scale, y).into(); - painter.line(vec![p1, p2], stroke); - } - } - // draw box around selected - let x1 = (self.index % 16) as f32 * 8.0 * self.scale; - let x2 = x1 + (8.0 * self.scale); - let y1 = (self.index / 16) as f32 * 8.0 * self.scale; - let y2 = y1 + (8.0 * self.scale); - painter.line( - vec![ - (res.rect.min + (x1, y1).into()), - (res.rect.min + (x2, y1).into()), - (res.rect.min + (x2, y2).into()), - (res.rect.min + (x1, y2).into()), - (res.rect.min + (x1, y1).into()), - ], - ui.style().visuals.widgets.active.fg_stroke, - ); } } @@ -363,7 +315,7 @@ impl CharDataLoader { let palette = self.load_palette(palette); let chardata = self.chardata.borrow(); let mut buffer = vec![0; 8 * 8 * 2048]; - for (i, row) in chardata.range::(0, 2048).iter().enumerate() { + for (i, row) in chardata.range::(0, 8 * 2048).iter().enumerate() { let bytes = [0, 2, 4, 6, 8, 10, 12, 14].map(|off| palette[(*row as usize >> off) & 0x3]); let char_index = i / 8; @@ -409,25 +361,3 @@ impl VramImageLoader for CharDataLoader { vec![] } } - -trait UiExt { - fn section(&mut self, title: impl Into, add_contents: impl FnOnce(&mut Ui)); -} - -impl UiExt for Ui { - fn section(&mut self, title: impl Into, add_contents: impl FnOnce(&mut Ui)) { - let mut frame = Frame::group(self.style()); - frame.outer_margin.top += 10.0; - frame.inner_margin.top += 2.0; - let res = frame.show(self, add_contents); - let text = RichText::new(title).background_color(self.style().visuals.panel_fill); - let old_rect = res.response.rect; - let mut text_rect = old_rect; - text_rect.min.x += 6.0; - let new_rect = self - .allocate_new_ui(UiBuilder::new().max_rect(text_rect), |ui| ui.label(text)) - .response - .rect; - self.allocate_space((old_rect.max - new_rect.max) - (old_rect.min - new_rect.min)); - } -} diff --git a/src/window/vram/utils.rs b/src/window/vram/utils.rs index 1889b3e..d8ddf51 100644 --- a/src/window/vram/utils.rs +++ b/src/window/vram/utils.rs @@ -1,3 +1,5 @@ +use egui::{Color32, Image, ImageSource, Response, Sense, TextureOptions, Ui, Widget}; + pub const GENERIC_PALETTE: [u8; 4] = [0, 64, 128, 255]; pub fn parse_palette(palette: u8, brts: &[u8]) -> [u8; 4] { @@ -14,3 +16,100 @@ pub fn parse_palette(palette: u8, brts: &[u8]) -> [u8; 4] { shades[(palette >> 6) as usize & 0x03], ] } + +pub struct CharacterGrid<'a> { + source: ImageSource<'a>, + scale: f32, + show_grid: bool, + selected: Option, +} + +impl<'a> CharacterGrid<'a> { + pub fn new(source: impl Into>) -> Self { + Self { + source: source.into(), + scale: 1.0, + show_grid: false, + selected: None, + } + } + + pub fn with_scale(self, scale: f32) -> Self { + Self { scale, ..self } + } + + pub fn with_grid(self, show_grid: bool) -> Self { + Self { show_grid, ..self } + } + + pub fn with_selected(self, selected: usize) -> Self { + Self { + selected: Some(selected), + ..self + } + } + + pub fn show(self, ui: &mut Ui) -> Option { + let start_pos = ui.cursor().min; + let cell_size = 8.0 * self.scale; + + let res = self.ui(ui); + + let grid_width_cells = ((res.rect.max.x - res.rect.min.x) / cell_size).round() as usize; + + if res.clicked() { + let click_pos = res.interact_pointer_pos()?; + let grid_pos = (click_pos - start_pos) / cell_size; + Some((grid_pos.y as usize * grid_width_cells) + grid_pos.x as usize) + } else { + None + } + } +} + +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); + + let cell_size = 8.0 * self.scale; + let grid_width_cells = ((res.rect.max.x - res.rect.min.x) / cell_size).round() as usize; + let grid_height_cells = ((res.rect.max.y - res.rect.min.y) / cell_size).round() as usize; + + let painter = ui.painter_at(res.rect); + if self.show_grid { + let stroke = ui.style().visuals.widgets.noninteractive.fg_stroke; + for x in (1..grid_width_cells).map(|i| (i as f32) * cell_size) { + let p1 = (res.rect.min.x + x, res.rect.min.y).into(); + let p2 = (res.rect.min.x + x, res.rect.max.y).into(); + painter.line(vec![p1, p2], stroke); + } + for y in (1..grid_height_cells).map(|i| (i as f32) * cell_size) { + let p1 = (res.rect.min.x, res.rect.min.y + y).into(); + let p2 = (res.rect.max.x, res.rect.min.y + y).into(); + painter.line(vec![p1, p2], stroke); + } + } + if let Some(selected) = self.selected { + let x1 = (selected % grid_width_cells) as f32 * cell_size; + let x2 = x1 + cell_size; + let y1 = (selected / grid_width_cells) as f32 * cell_size; + let y2 = y1 + cell_size; + painter.line( + vec![ + (res.rect.min + (x1, y1).into()), + (res.rect.min + (x2, y1).into()), + (res.rect.min + (x2, y2).into()), + (res.rect.min + (x1, y2).into()), + (res.rect.min + (x1, y1).into()), + ], + ui.style().visuals.widgets.active.fg_stroke, + ); + } + res + } +} -- 2.40.1 From 600148c781c394011479cee3a2de5c0575ea7dc3 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 8 Feb 2025 15:39:12 -0500 Subject: [PATCH 05/34] Rerender VRAM graphics every frame (slow in debug) --- src/vram.rs | 81 ++++++++++++++++++++++++++++--------- src/window/vram/bgmap.rs | 66 ++++++++++++++++-------------- src/window/vram/chardata.rs | 68 ++++++++++++++++++------------- 3 files changed, 138 insertions(+), 77 deletions(-) diff --git a/src/vram.rs b/src/vram.rs index 3bf9e07..2394617 100644 --- a/src/vram.rs +++ b/src/vram.rs @@ -1,13 +1,13 @@ use std::{ collections::{hash_map::Entry, HashMap}, hash::Hash, - sync::Mutex, + sync::{Arc, Mutex}, }; use egui::{ epaint::ImageDelta, load::{LoadError, SizedTexture, TextureLoader, TexturePoll}, - ColorImage, Context, TextureHandle, TextureOptions, + Color32, ColorImage, Context, TextureHandle, TextureOptions, }; use serde::{Deserialize, Serialize}; @@ -27,21 +27,60 @@ impl Deserialize<'a> + PartialEq + Eq + Hash> VramResourc } } +pub enum VramImage { + Unchanged(Arc), + Changed(ColorImage), +} + +impl VramImage { + pub fn new(width: usize, height: usize) -> Self { + Self::Changed(ColorImage::new([width, height], Color32::BLACK)) + } + + pub fn write(&mut self, coords: (usize, usize), shade: u8) { + match self { + Self::Unchanged(image) => { + let value = image[coords]; + if value.r() == shade { + return; + }; + let mut new_image = ColorImage::clone(image); + new_image[coords] = Color32::from_gray(shade); + *self = Self::Changed(new_image); + } + Self::Changed(image) => { + image[coords] = Color32::from_gray(shade); + } + } + } + + pub fn take(&mut self) -> Option> { + match self { + Self::Unchanged(_) => None, + Self::Changed(image) => { + let arced = Arc::new(std::mem::take(image)); + *self = Self::Unchanged(arced.clone()); + Some(arced) + } + } + } +} + pub trait VramImageLoader { type Resource: VramResource; fn id(&self) -> &str; - fn add(&self, resource: &Self::Resource) -> Option; + fn add(&self, resource: &Self::Resource) -> Option; fn update<'a>( &'a self, - resources: impl Iterator, - ) -> Vec<(&'a Self::Resource, ColorImage)>; + resources: impl Iterator, + ); } pub struct VramTextureLoader { id: String, loader: Mutex, - cache: Mutex>, + cache: Mutex>, } impl VramTextureLoader { @@ -76,26 +115,30 @@ impl TextureLoader for VramTextureLoader { } let loader = self.loader.lock().unwrap(); let mut cache = self.cache.lock().unwrap(); - for (resource, updated_image) in loader.update(cache.keys()) { - if let Some(handle) = cache.get(resource) { - let delta = ImageDelta::full(updated_image, TextureOptions::NEAREST); + let resources = cache.iter_mut().map(|(k, v)| (k, &mut v.1)); + loader.update(resources); + for (handle, image) in cache.values_mut() { + if let Some(data) = image.take() { + let delta = ImageDelta::full(data, TextureOptions::NEAREST); ctx.tex_manager().write().set(handle.id(), delta); } } match cache.entry(resource) { Entry::Occupied(entry) => { - let texture = SizedTexture::from_handle(entry.get()); + let texture = SizedTexture::from_handle(&entry.get().0); Ok(TexturePoll::Ready { texture }) } Entry::Vacant(entry) => { - if let Some(image) = loader.add(entry.key()) { - let handle = - entry.insert(ctx.load_texture(uri, image, TextureOptions::NEAREST)); - let texture = SizedTexture::from_handle(handle); - Ok(TexturePoll::Ready { texture }) - } else { - Err(LoadError::Loading("could not load texture".into())) - } + let Some(mut image) = loader.add(entry.key()) else { + return Err(LoadError::Loading("could not load texture".into())); + }; + let Some(data) = image.take() else { + return Err(LoadError::Loading("could not load texture".into())); + }; + let handle = ctx.load_texture(uri, data, TextureOptions::NEAREST); + let (handle, _) = entry.insert((handle, image)); + let texture = SizedTexture::from_handle(handle); + Ok(TexturePoll::Ready { texture }) } } } @@ -115,7 +158,7 @@ impl TextureLoader for VramTextureLoader { .lock() .unwrap() .values() - .map(|h| h.byte_size()) + .map(|h| h.0.byte_size()) .sum() } } diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index 2a4962d..32ed66f 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use egui::{ - Align, CentralPanel, Checkbox, Color32, ColorImage, Context, Image, ScrollArea, Slider, - TextEdit, TextureOptions, Ui, ViewportBuilder, ViewportId, + Align, CentralPanel, Checkbox, Color32, Context, Image, ScrollArea, Slider, TextEdit, + TextureOptions, Ui, ViewportBuilder, ViewportId, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use serde::{Deserialize, Serialize}; @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use crate::{ emulator::SimId, memory::{MemoryMonitor, MemoryView}, - vram::{VramImageLoader, VramResource as _, VramTextureLoader}, + vram::{VramImage, VramImageLoader, VramResource as _, VramTextureLoader}, window::{utils::UiExt, AppWindow}, }; @@ -242,7 +242,7 @@ impl BgMapLoader { } } - fn load_bgmap(&self, bgmap_index: usize, generic_palette: bool) -> Option { + fn update_bgmap(&self, image: &mut VramImage, bgmap_index: usize, generic_palette: bool) { let chardata = self.chardata.borrow(); let bgmaps = self.bgmaps.borrow(); let brightness = self.brightness.borrow(); @@ -265,7 +265,6 @@ impl BgMapLoader { ] }; - let mut data = vec![0u8; 512 * 512]; for (i, cell) in bgmaps .range::(bgmap_index * 4096, 4096) .iter() @@ -275,21 +274,17 @@ impl BgMapLoader { let char = chardata.range::(char_index * 8, 8); let palette = &colors[palette_index]; - let mut target_idx = (i % 64) * 8 + (i / 64) * 8 * 512; for row in 0..8 { - let dests = &mut data[target_idx..target_idx + 8]; - let pixels = self.read_char_row(char, hflip, vflip, row); - for (dest, pixel) in dests.iter_mut().zip(pixels) { - *dest = palette[pixel as usize]; + let y = row + (i / 64) * 8; + for (col, pixel) in self.read_char_row(char, hflip, vflip, row).enumerate() { + let x = col + (i % 64) * 8; + image.write((x, y), palette[pixel as usize]); } - target_idx += 512; } } - - Some(ColorImage::from_gray([512, 512], &data)) } - fn load_bgmap_cell(&self, index: usize, generic_palette: bool) -> Option { + fn update_bgmap_cell(&self, image: &mut VramImage, index: usize, generic_palette: bool) { let chardata = self.chardata.borrow(); let bgmaps = self.bgmaps.borrow(); let brightness = self.brightness.borrow(); @@ -297,7 +292,6 @@ impl BgMapLoader { let brts = brightness.range::(0, 8); - let mut data = vec![0u8; 8 * 8]; let cell = bgmaps.read::(index); let (char_index, vflip, hflip, palette_index) = parse_cell(cell); @@ -308,17 +302,11 @@ impl BgMapLoader { parse_palette(palettes.read(palette_index * 2), brts) }; - let mut target_idx = 0; for row in 0..8 { - let dests = &mut data[target_idx..target_idx + 8]; - let pixels = self.read_char_row(char, hflip, vflip, row); - for (dest, pixel) in dests.iter_mut().zip(pixels) { - *dest = palette[pixel as usize]; + for (col, pixel) in self.read_char_row(char, hflip, vflip, row).enumerate() { + image.write((col, row), palette[pixel as usize]); } - target_idx += 8; } - - Some(ColorImage::from_gray([8, 8], &data)) } fn read_char_row( @@ -343,24 +331,42 @@ impl VramImageLoader for BgMapLoader { concat!(module_path!(), "::BgMapLoader") } - fn add(&self, resource: &Self::Resource) -> Option { + fn add(&self, resource: &Self::Resource) -> Option { match resource { BgMapResource::BgMap { index, generic_palette, - } => self.load_bgmap(*index, *generic_palette), + } => { + let mut image = VramImage::new(64 * 8, 64 * 8); + self.update_bgmap(&mut image, *index, *generic_palette); + Some(image) + } BgMapResource::Cell { index, generic_palette, - } => self.load_bgmap_cell(*index, *generic_palette), + } => { + let mut image = VramImage::new(8, 8); + self.update_bgmap_cell(&mut image, *index, *generic_palette); + Some(image) + } } } fn update<'a>( &'a self, - resources: impl Iterator, - ) -> Vec<(&'a Self::Resource, ColorImage)> { - let _ = resources; - vec![] + resources: impl Iterator, + ) { + for (resource, image) in resources { + match resource { + BgMapResource::BgMap { + index, + generic_palette, + } => self.update_bgmap(image, *index, *generic_palette), + BgMapResource::Cell { + index, + generic_palette, + } => self.update_bgmap_cell(image, *index, *generic_palette), + } + } } } diff --git a/src/window/vram/chardata.rs b/src/window/vram/chardata.rs index a8eb3f7..504a6eb 100644 --- a/src/window/vram/chardata.rs +++ b/src/window/vram/chardata.rs @@ -1,8 +1,8 @@ use std::{fmt::Display, sync::Arc}; use egui::{ - Align, CentralPanel, Color32, ColorImage, ComboBox, Context, Image, ScrollArea, Slider, - TextEdit, TextureOptions, Ui, Vec2, ViewportBuilder, ViewportId, + Align, CentralPanel, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextEdit, + TextureOptions, Ui, Vec2, ViewportBuilder, ViewportId, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use serde::{Deserialize, Serialize}; @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use crate::{ emulator::SimId, memory::{MemoryMonitor, MemoryView}, - vram::{VramImageLoader, VramResource as _, VramTextureLoader}, + vram::{VramImage, VramImageLoader, VramResource as _, VramTextureLoader}, window::{utils::UiExt as _, AppWindow}, }; @@ -294,38 +294,34 @@ impl CharDataLoader { } } - fn load_character(&self, palette: VramPalette, index: usize) -> Option { + fn update_character(&self, image: &mut VramImage, palette: VramPalette, index: usize) { if index >= 2048 { - return None; + return; } let palette = self.load_palette(palette); let chardata = self.chardata.borrow(); let character = chardata.range::(index * 8, 8); - let mut buffer = Vec::with_capacity(8 * 8); - for row in character { - for offset in (0..16).step_by(2) { - let char = (row >> offset) & 0x3; - buffer.push(palette[char as usize]); + for (row, pixels) in character.iter().enumerate() { + for col in 0..8 { + let char = (pixels >> (col * 2)) & 0x03; + image.write((col, row), palette[char as usize]); } } - Some(ColorImage::from_gray([8, 8], &buffer)) } - fn load_character_data(&self, palette: VramPalette) -> Option { + fn update_character_data(&self, image: &mut VramImage, palette: VramPalette) { let palette = self.load_palette(palette); let chardata = self.chardata.borrow(); - let mut buffer = vec![0; 8 * 8 * 2048]; - for (i, row) in chardata.range::(0, 8 * 2048).iter().enumerate() { - let bytes = - [0, 2, 4, 6, 8, 10, 12, 14].map(|off| palette[(*row as usize >> off) & 0x3]); - let char_index = i / 8; - let row_index = i % 8; + for (row, pixels) in chardata.range::(0, 8 * 2048).iter().enumerate() { + let char_index = row / 8; + let row_index = row % 8; let x = (char_index % 16) * 8; let y = (char_index / 16) * 8 + row_index; - let write_index = (y * 16 * 8) + x; - buffer[write_index..write_index + 8].copy_from_slice(&bytes); + for col in 0..8 { + let char = (pixels >> (col * 2)) & 0x03; + image.write((x + col, y), palette[char as usize]); + } } - Some(ColorImage::from_gray([8 * 16, 8 * 128], &buffer)) } fn load_palette(&self, palette: VramPalette) -> [u8; 4] { @@ -346,18 +342,34 @@ impl VramImageLoader for CharDataLoader { concat!(module_path!(), "::CharDataLoader") } - fn add(&self, resource: &Self::Resource) -> Option { + fn add(&self, resource: &Self::Resource) -> Option { match resource { - CharDataResource::Character { palette, index } => self.load_character(*palette, *index), - CharDataResource::CharacterData { palette } => self.load_character_data(*palette), + CharDataResource::Character { palette, index } => { + let mut image = VramImage::new(8, 8); + self.update_character(&mut image, *palette, *index); + Some(image) + } + CharDataResource::CharacterData { palette } => { + let mut image = VramImage::new(8 * 16, 8 * 128); + self.update_character_data(&mut image, *palette); + Some(image) + } } } fn update<'a>( &'a self, - resources: impl Iterator, - ) -> Vec<(&'a Self::Resource, ColorImage)> { - let _ = resources; - vec![] + resources: impl Iterator, + ) { + for (resource, image) in resources { + match resource { + CharDataResource::Character { palette, index } => { + self.update_character(image, *palette, *index) + } + CharDataResource::CharacterData { palette } => { + self.update_character_data(image, *palette) + } + } + } } } -- 2.40.1 From a82389224f5dbca196b4e6ac0d44bd10a2e15742 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 9 Feb 2025 10:56:33 -0500 Subject: [PATCH 06/34] Avoid unnecessary computation/cloning in renderer --- src/vram.rs | 30 +++++++++++++++++++++--------- src/window/vram/bgmap.rs | 10 +++++----- src/window/vram/chardata.rs | 10 +++++----- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/vram.rs b/src/vram.rs index 2394617..04f6644 100644 --- a/src/vram.rs +++ b/src/vram.rs @@ -1,5 +1,5 @@ use std::{ - collections::{hash_map::Entry, HashMap}, + collections::{hash_map::Entry, HashMap, HashSet}, hash::Hash, sync::{Arc, Mutex}, }; @@ -11,12 +11,12 @@ use egui::{ }; use serde::{Deserialize, Serialize}; -pub trait VramResource: Sized + PartialEq + Eq + Hash { +pub trait VramResource: Sized + Clone + PartialEq + Eq + Hash { fn to_uri(&self) -> String; fn from_uri(uri: &str) -> Option; } -impl Deserialize<'a> + PartialEq + Eq + Hash> VramResource for T { +impl Deserialize<'a> + Clone + PartialEq + Eq + Hash> VramResource for T { fn to_uri(&self) -> String { format!("vram://{}", serde_json::to_string(self).unwrap()) } @@ -28,7 +28,7 @@ impl Deserialize<'a> + PartialEq + Eq + Hash> VramResourc } pub enum VramImage { - Unchanged(Arc), + Unchanged(Option>), Changed(ColorImage), } @@ -40,11 +40,12 @@ impl VramImage { pub fn write(&mut self, coords: (usize, usize), shade: u8) { match self { Self::Unchanged(image) => { - let value = image[coords]; - if value.r() == shade { + if image.as_ref().is_none_or(|i| i[coords].r() == shade) { + return; + } + let Some(mut new_image) = image.take().map(Arc::unwrap_or_clone) else { return; }; - let mut new_image = ColorImage::clone(image); new_image[coords] = Color32::from_gray(shade); *self = Self::Changed(new_image); } @@ -59,7 +60,7 @@ impl VramImage { Self::Unchanged(_) => None, Self::Changed(image) => { let arced = Arc::new(std::mem::take(image)); - *self = Self::Unchanged(arced.clone()); + *self = Self::Unchanged(Some(arced.clone())); Some(arced) } } @@ -81,6 +82,7 @@ pub struct VramTextureLoader { id: String, loader: Mutex, cache: Mutex>, + seen: Mutex>, } impl VramTextureLoader { @@ -89,8 +91,16 @@ impl VramTextureLoader { id: loader.id().to_string(), loader: Mutex::new(loader), cache: Mutex::new(HashMap::new()), + seen: Mutex::new(HashSet::new()), } } + + pub fn begin_pass(&self) { + let mut cache = self.cache.lock().unwrap(); + let mut seen = self.seen.lock().unwrap(); + cache.retain(|res, _| seen.contains(res)); + seen.clear(); + } } impl TextureLoader for VramTextureLoader { @@ -113,8 +123,10 @@ impl TextureLoader for VramTextureLoader { "Only TextureOptions::NEAREST are supported".into(), )); } - let loader = self.loader.lock().unwrap(); let mut cache = self.cache.lock().unwrap(); + let mut seen = self.seen.lock().unwrap(); + seen.insert(resource.clone()); + let loader = self.loader.lock().unwrap(); let resources = cache.iter_mut().map(|(k, v)| (k, &mut v.1)); loader.update(resources); for (handle, image) in cache.values_mut() { diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index 32ed66f..6129530 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -18,7 +18,7 @@ use super::utils::{parse_palette, CharacterGrid, GENERIC_PALETTE}; pub struct BgMapWindow { sim_id: SimId, - loader: Option, + loader: Arc>, bgmaps: MemoryView, cell_index: usize, cell_index_str: String, @@ -31,7 +31,7 @@ impl BgMapWindow { pub fn new(sim_id: SimId, memory: &mut MemoryMonitor) -> Self { Self { sim_id, - loader: Some(BgMapLoader::new(sim_id, memory)), + loader: Arc::new(VramTextureLoader::new(BgMapLoader::new(sim_id, memory))), bgmaps: memory.view(sim_id, 0x00020000, 0x1d800), cell_index: 0, cell_index_str: "0".into(), @@ -188,11 +188,11 @@ impl AppWindow for BgMapWindow { } fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) { - let loader = self.loader.take().unwrap(); - ctx.add_texture_loader(Arc::new(VramTextureLoader::new(loader))); + ctx.add_texture_loader(self.loader.clone()); } fn show(&mut self, ctx: &Context) { + self.loader.begin_pass(); CentralPanel::default().show(ctx, |ui| { ui.horizontal_top(|ui| { StripBuilder::new(ui) @@ -219,7 +219,7 @@ fn parse_cell(cell: u16) -> (usize, bool, bool, usize) { (char_index, vflip, hflip, palette_index) } -#[derive(Serialize, Deserialize, PartialEq, Eq, Hash)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] enum BgMapResource { BgMap { index: usize, generic_palette: bool }, Cell { index: usize, generic_palette: bool }, diff --git a/src/window/vram/chardata.rs b/src/window/vram/chardata.rs index 504a6eb..28c90eb 100644 --- a/src/window/vram/chardata.rs +++ b/src/window/vram/chardata.rs @@ -77,7 +77,7 @@ impl Display for VramPalette { pub struct CharacterDataWindow { sim_id: SimId, - loader: Option, + loader: Arc>, brightness: MemoryView, palettes: MemoryView, palette: VramPalette, @@ -91,7 +91,7 @@ impl CharacterDataWindow { pub fn new(sim_id: SimId, memory: &mut MemoryMonitor) -> Self { Self { sim_id, - loader: Some(CharDataLoader::new(sim_id, memory)), + loader: Arc::new(VramTextureLoader::new(CharDataLoader::new(sim_id, memory))), brightness: memory.view(sim_id, 0x0005f824, 8), palettes: memory.view(sim_id, 0x0005f860, 16), palette: VramPalette::Generic, @@ -250,11 +250,11 @@ impl AppWindow for CharacterDataWindow { } fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) { - let loader = self.loader.take().unwrap(); - ctx.add_texture_loader(Arc::new(VramTextureLoader::new(loader))); + ctx.add_texture_loader(self.loader.clone()); } fn show(&mut self, ctx: &Context) { + self.loader.begin_pass(); CentralPanel::default().show(ctx, |ui| { ui.horizontal_top(|ui| { StripBuilder::new(ui) @@ -273,7 +273,7 @@ impl AppWindow for CharacterDataWindow { } } -#[derive(Serialize, Deserialize, PartialEq, Eq, Hash)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] enum CharDataResource { Character { palette: VramPalette, index: usize }, CharacterData { palette: VramPalette }, -- 2.40.1 From a461faf89d474899f0c40ea3cb19350a35bdec4c Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 9 Feb 2025 16:17:08 -0500 Subject: [PATCH 07/34] Render images on background thread to keep UI responsive --- Cargo.toml | 2 +- src/app.rs | 8 +- src/vram.rs | 267 ++++++++++++++++++++++++++---------- src/window/vram/bgmap.rs | 104 +++++--------- src/window/vram/chardata.rs | 119 +++++++++++++--- 5 files changed, 337 insertions(+), 163 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 854b0c3..cd55b09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ rubato = "0.16" serde = { version = "1", features = ["derive"] } serde_json = "1" thread-priority = "1" -tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync"] } +tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync", "time"] } tracing = { version = "0.1", features = ["release_max_level_info"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } wgpu = "23" diff --git a/src/app.rs b/src/app.rs index 8c36a44..10a7d1d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -20,6 +20,7 @@ use crate::{ input::MappingProvider, memory::MemoryMonitor, persistence::Persistence, + vram::VramProcessor, window::{ AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, GameWindow, GdbServerWindow, InputWindow, @@ -45,6 +46,7 @@ pub struct Application { mappings: MappingProvider, controllers: ControllerManager, memory: MemoryMonitor, + vram: VramProcessor, persistence: Persistence, viewports: HashMap, focused: Option, @@ -63,6 +65,7 @@ impl Application { let mappings = MappingProvider::new(persistence.clone()); let controllers = ControllerManager::new(client.clone(), &mappings); let memory = MemoryMonitor::new(client.clone()); + let vram = VramProcessor::new(); { let mappings = mappings.clone(); let proxy = proxy.clone(); @@ -75,6 +78,7 @@ impl Application { proxy, mappings, memory, + vram, controllers, persistence, viewports: HashMap::new(), @@ -205,11 +209,11 @@ impl ApplicationHandler for Application { self.open(event_loop, Box::new(about)); } UserEvent::OpenCharacterData(sim_id) => { - let vram = CharacterDataWindow::new(sim_id, &mut self.memory); + let vram = CharacterDataWindow::new(sim_id, &mut self.memory, &mut self.vram); self.open(event_loop, Box::new(vram)); } UserEvent::OpenBgMap(sim_id) => { - let bgmap = BgMapWindow::new(sim_id, &mut self.memory); + let bgmap = BgMapWindow::new(sim_id, &mut self.memory, &mut self.vram); self.open(event_loop, Box::new(bgmap)); } UserEvent::OpenDebugger(sim_id) => { diff --git a/src/vram.rs b/src/vram.rs index 04f6644..ef36634 100644 --- a/src/vram.rs +++ b/src/vram.rs @@ -1,29 +1,103 @@ use std::{ - collections::{hash_map::Entry, HashMap, HashSet}, - hash::Hash, - sync::{Arc, Mutex}, + collections::HashMap, + ops::Deref, + sync::{Arc, Mutex, Weak}, + thread, + time::Duration, }; use egui::{ epaint::ImageDelta, load::{LoadError, SizedTexture, TextureLoader, TexturePoll}, - Color32, ColorImage, Context, TextureHandle, TextureOptions, + Color32, ColorImage, TextureHandle, TextureOptions, }; -use serde::{Deserialize, Serialize}; +use tokio::{sync::mpsc, time::timeout}; -pub trait VramResource: Sized + Clone + PartialEq + Eq + Hash { - fn to_uri(&self) -> String; - fn from_uri(uri: &str) -> Option; +pub struct VramProcessor { + sender: mpsc::UnboundedSender>, } -impl Deserialize<'a> + Clone + PartialEq + Eq + Hash> VramResource for T { - fn to_uri(&self) -> String { - format!("vram://{}", serde_json::to_string(self).unwrap()) +impl VramProcessor { + pub fn new() -> Self { + let (sender, receiver) = mpsc::unbounded_channel(); + thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + let mut worker = VramProcessorWorker { + receiver, + renderers: vec![], + }; + worker.run().await + }) + }); + Self { sender } } - fn from_uri(uri: &str) -> Option { - let content = uri.strip_prefix("vram://")?; - serde_json::from_str(content).ok() + pub fn add + 'static>( + &self, + renderer: R, + ) -> ([VramImageHandle; N], VramParams) { + let handles = renderer.sizes().map(|[width, height]| { + let data = Arc::new(Mutex::new(None)); + VramImageHandle { + size: [width as f32, height as f32], + data, + } + }); + let images = renderer + .sizes() + .map(|[width, height]| VramImage::new(width, height)); + let sink = Arc::new(Mutex::new(R::Params::default())); + let _ = self.sender.send(Box::new(VramRendererWrapper { + renderer, + params: Arc::downgrade(&sink), + images, + sinks: handles.clone().map(|i| i.data), + })); + let params = VramParams { + value: R::Params::default(), + sink, + }; + (handles, params) + } +} + +struct VramProcessorWorker { + receiver: mpsc::UnboundedReceiver>, + renderers: Vec>, +} + +impl VramProcessorWorker { + async fn run(&mut self) { + loop { + if self.renderers.is_empty() { + // if we have nothing to do, block until we have something to do + if self.receiver.recv_many(&mut self.renderers, 64).await == 0 { + // shutdown + return; + } + while let Ok(renderer) = self.receiver.try_recv() { + self.renderers.push(renderer); + } + } + self.renderers + .retain_mut(|renderer| renderer.try_update().is_ok()); + // wait up to 10 ms for more renderers + if timeout( + Duration::from_millis(10), + self.receiver.recv_many(&mut self.renderers, 64), + ) + .await + .is_ok() + { + while let Ok(renderer) = self.receiver.try_recv() { + self.renderers.push(renderer); + } + } + } } } @@ -67,55 +141,107 @@ impl VramImage { } } -pub trait VramImageLoader { - type Resource: VramResource; - - fn id(&self) -> &str; - fn add(&self, resource: &Self::Resource) -> Option; - fn update<'a>( - &'a self, - resources: impl Iterator, - ); +#[derive(Clone)] +pub struct VramImageHandle { + size: [f32; 2], + data: Arc>>>, } -pub struct VramTextureLoader { - id: String, - loader: Mutex, - cache: Mutex>, - seen: Mutex>, +impl VramImageHandle { + fn pull(&mut self) -> Option> { + self.data.lock().unwrap().take() + } } -impl VramTextureLoader { - pub fn new(loader: T) -> Self { - Self { - id: loader.id().to_string(), - loader: Mutex::new(loader), - cache: Mutex::new(HashMap::new()), - seen: Mutex::new(HashSet::new()), +pub struct VramParams { + value: T, + sink: Arc>, +} + +impl Deref for VramParams { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl VramParams { + pub fn write(&mut self, value: T) { + if self.value != value { + self.value = value.clone(); + *self.sink.lock().unwrap() = value; } } +} - pub fn begin_pass(&self) { - let mut cache = self.cache.lock().unwrap(); - let mut seen = self.seen.lock().unwrap(); - cache.retain(|res, _| seen.contains(res)); - seen.clear(); +pub trait VramRenderer: Send { + type Params: Clone + Default + Send; + fn sizes(&self) -> [[usize; 2]; N]; + fn render(&mut self, params: &Self::Params, images: &mut [VramImage; N]); +} + +struct VramRendererWrapper> { + renderer: R, + params: Weak>, + images: [VramImage; N], + sinks: [Arc>>>; N], +} + +trait VramRendererImpl: Send { + fn try_update(&mut self) -> Result<(), ()>; +} + +impl + Send> VramRendererImpl for VramRendererWrapper { + fn try_update(&mut self) -> Result<(), ()> { + let params = match self.params.upgrade() { + Some(params) => params.lock().unwrap().clone(), + None => { + // the UI isn't using this anymore + return Err(()); + } + }; + self.renderer.render(¶ms, &mut self.images); + + for (image, sink) in self.images.iter_mut().zip(&self.sinks) { + if let Some(update) = image.take() { + sink.lock().unwrap().replace(update); + } + } + Ok(()) } } -impl TextureLoader for VramTextureLoader { +pub struct VramTextureLoader { + cache: Mutex)>>, +} + +impl VramTextureLoader { + pub fn new(renderers: impl IntoIterator) -> Self { + let mut cache = HashMap::new(); + for (key, image) in renderers { + cache.insert(key, (image, None)); + } + Self { + cache: Mutex::new(cache), + } + } +} + +impl TextureLoader for VramTextureLoader { fn id(&self) -> &str { - &self.id + concat!(module_path!(), "VramTextureLoader") } fn load( &self, - ctx: &Context, + ctx: &egui::Context, uri: &str, texture_options: TextureOptions, _size_hint: egui::SizeHint, ) -> Result { - let Some(resource) = T::Resource::from_uri(uri) else { + let mut cache = self.cache.lock().unwrap(); + let Some((image, maybe_handle)) = cache.get_mut(uri) else { return Err(LoadError::NotSupported); }; if texture_options != TextureOptions::NEAREST { @@ -123,54 +249,45 @@ impl TextureLoader for VramTextureLoader { "Only TextureOptions::NEAREST are supported".into(), )); } - let mut cache = self.cache.lock().unwrap(); - let mut seen = self.seen.lock().unwrap(); - seen.insert(resource.clone()); - let loader = self.loader.lock().unwrap(); - let resources = cache.iter_mut().map(|(k, v)| (k, &mut v.1)); - loader.update(resources); - for (handle, image) in cache.values_mut() { - if let Some(data) = image.take() { - let delta = ImageDelta::full(data, TextureOptions::NEAREST); + match (image.pull(), maybe_handle.as_ref()) { + (Some(update), Some(handle)) => { + let delta = ImageDelta::full(update, texture_options); ctx.tex_manager().write().set(handle.id(), delta); - } - } - match cache.entry(resource) { - Entry::Occupied(entry) => { - let texture = SizedTexture::from_handle(&entry.get().0); + let texture = SizedTexture::new(handle, image.size); Ok(TexturePoll::Ready { texture }) } - Entry::Vacant(entry) => { - let Some(mut image) = loader.add(entry.key()) else { - return Err(LoadError::Loading("could not load texture".into())); - }; - let Some(data) = image.take() else { - return Err(LoadError::Loading("could not load texture".into())); - }; - let handle = ctx.load_texture(uri, data, TextureOptions::NEAREST); - let (handle, _) = entry.insert((handle, image)); - let texture = SizedTexture::from_handle(handle); + (Some(update), None) => { + let handle = ctx.load_texture(uri, update, texture_options); + let texture = SizedTexture::new(&handle, image.size); + maybe_handle.replace(handle); Ok(TexturePoll::Ready { texture }) } + (None, Some(handle)) => { + let texture = SizedTexture::new(handle, image.size); + Ok(TexturePoll::Ready { texture }) + } + (None, None) => { + let size = image.size.into(); + Ok(TexturePoll::Pending { size: Some(size) }) + } } } fn forget(&self, uri: &str) { - if let Some(resource) = T::Resource::from_uri(uri) { - self.cache.lock().unwrap().remove(&resource); - } + let _ = uri; } - fn forget_all(&self) { - self.cache.lock().unwrap().clear(); - } + fn forget_all(&self) {} fn byte_size(&self) -> usize { self.cache .lock() .unwrap() .values() - .map(|h| h.0.byte_size()) + .map(|(image, _)| { + let [width, height] = image.size; + width as usize * height as usize * 4 + }) .sum() } } diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index 6129530..a255914 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -5,12 +5,11 @@ use egui::{ TextureOptions, Ui, ViewportBuilder, ViewportId, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; -use serde::{Deserialize, Serialize}; use crate::{ emulator::SimId, memory::{MemoryMonitor, MemoryView}, - vram::{VramImage, VramImageLoader, VramResource as _, VramTextureLoader}, + vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, window::{utils::UiExt, AppWindow}, }; @@ -18,26 +17,32 @@ use super::utils::{parse_palette, CharacterGrid, GENERIC_PALETTE}; pub struct BgMapWindow { sim_id: SimId, - loader: Arc>, + loader: Arc, bgmaps: MemoryView, cell_index: usize, cell_index_str: String, + generic_palette: bool, + params: VramParams, scale: f32, show_grid: bool, - generic_palette: bool, } impl BgMapWindow { - pub fn new(sim_id: SimId, memory: &mut MemoryMonitor) -> Self { + pub fn new(sim_id: SimId, memory: &mut MemoryMonitor, vram: &mut VramProcessor) -> Self { + let renderer = BgMapRenderer::new(sim_id, memory); + let ([cell, bgmap], params) = vram.add(renderer); + let loader = + VramTextureLoader::new([("vram://cell".into(), cell), ("vram://bgmap".into(), bgmap)]); Self { sim_id, - loader: Arc::new(VramTextureLoader::new(BgMapLoader::new(sim_id, memory))), + loader: Arc::new(loader), bgmaps: memory.view(sim_id, 0x00020000, 0x1d800), cell_index: 0, cell_index_str: "0".into(), + generic_palette: false, + params, scale: 1.0, show_grid: false, - generic_palette: false, } } @@ -88,11 +93,7 @@ impl BgMapWindow { }); }); }); - let resource = BgMapResource::Cell { - index: self.cell_index, - generic_palette: self.generic_palette, - }; - let image = Image::new(resource.to_uri()) + let image = Image::new("vram://cell") .maintain_aspect_ratio(true) .tint(Color32::RED) .texture_options(TextureOptions::NEAREST); @@ -154,14 +155,14 @@ impl BgMapWindow { }); }); }); + self.params.write(BgMapParams { + cell_index: self.cell_index, + generic_palette: self.generic_palette, + }); } fn show_bgmap(&mut self, ui: &mut Ui) { - let resource = BgMapResource::BgMap { - index: self.cell_index / 4096, - generic_palette: self.generic_palette, - }; - let grid = CharacterGrid::new(resource.to_uri()) + let grid = CharacterGrid::new("vram://bgmap") .with_scale(self.scale) .with_grid(self.show_grid) .with_selected(self.cell_index % 4096); @@ -192,7 +193,6 @@ impl AppWindow for BgMapWindow { } fn show(&mut self, ctx: &Context) { - self.loader.begin_pass(); CentralPanel::default().show(ctx, |ui| { ui.horizontal_top(|ui| { StripBuilder::new(ui) @@ -219,20 +219,20 @@ fn parse_cell(cell: u16) -> (usize, bool, bool, usize) { (char_index, vflip, hflip, palette_index) } -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] -enum BgMapResource { - BgMap { index: usize, generic_palette: bool }, - Cell { index: usize, generic_palette: bool }, +#[derive(Default, Clone, PartialEq, Eq)] +struct BgMapParams { + cell_index: usize, + generic_palette: bool, } -struct BgMapLoader { +struct BgMapRenderer { chardata: MemoryView, bgmaps: MemoryView, brightness: MemoryView, palettes: MemoryView, } -impl BgMapLoader { +impl BgMapRenderer { pub fn new(sim_id: SimId, memory: &mut MemoryMonitor) -> Self { Self { chardata: memory.view(sim_id, 0x00078000, 0x8000), @@ -242,7 +242,7 @@ impl BgMapLoader { } } - fn update_bgmap(&self, image: &mut VramImage, bgmap_index: usize, generic_palette: bool) { + fn render_bgmap(&self, image: &mut VramImage, bgmap_index: usize, generic_palette: bool) { let chardata = self.chardata.borrow(); let bgmaps = self.bgmaps.borrow(); let brightness = self.brightness.borrow(); @@ -284,7 +284,7 @@ impl BgMapLoader { } } - fn update_bgmap_cell(&self, image: &mut VramImage, index: usize, generic_palette: bool) { + fn render_bgmap_cell(&self, image: &mut VramImage, index: usize, generic_palette: bool) { let chardata = self.chardata.borrow(); let bgmaps = self.bgmaps.borrow(); let brightness = self.brightness.borrow(); @@ -324,49 +324,19 @@ impl BgMapLoader { } } -impl VramImageLoader for BgMapLoader { - type Resource = BgMapResource; +impl VramRenderer<2> for BgMapRenderer { + type Params = BgMapParams; - fn id(&self) -> &str { - concat!(module_path!(), "::BgMapLoader") + fn sizes(&self) -> [[usize; 2]; 2] { + [[8, 8], [8 * 64, 8 * 64]] } - fn add(&self, resource: &Self::Resource) -> Option { - match resource { - BgMapResource::BgMap { - index, - generic_palette, - } => { - let mut image = VramImage::new(64 * 8, 64 * 8); - self.update_bgmap(&mut image, *index, *generic_palette); - Some(image) - } - BgMapResource::Cell { - index, - generic_palette, - } => { - let mut image = VramImage::new(8, 8); - self.update_bgmap_cell(&mut image, *index, *generic_palette); - Some(image) - } - } - } - - fn update<'a>( - &'a self, - resources: impl Iterator, - ) { - for (resource, image) in resources { - match resource { - BgMapResource::BgMap { - index, - generic_palette, - } => self.update_bgmap(image, *index, *generic_palette), - BgMapResource::Cell { - index, - generic_palette, - } => self.update_bgmap_cell(image, *index, *generic_palette), - } - } + fn render(&mut self, params: &Self::Params, images: &mut [VramImage; 2]) { + self.render_bgmap_cell(&mut images[0], params.cell_index, params.generic_palette); + self.render_bgmap( + &mut images[1], + params.cell_index / 4096, + params.generic_palette, + ); } } diff --git a/src/window/vram/chardata.rs b/src/window/vram/chardata.rs index 28c90eb..b317026 100644 --- a/src/window/vram/chardata.rs +++ b/src/window/vram/chardata.rs @@ -10,14 +10,15 @@ use serde::{Deserialize, Serialize}; use crate::{ emulator::SimId, memory::{MemoryMonitor, MemoryView}, - vram::{VramImage, VramImageLoader, VramResource as _, VramTextureLoader}, + vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, window::{utils::UiExt as _, AppWindow}, }; use super::utils::{self, CharacterGrid}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum VramPalette { + #[default] Generic, Bg0, Bg1, @@ -77,26 +78,34 @@ impl Display for VramPalette { pub struct CharacterDataWindow { sim_id: SimId, - loader: Arc>, + loader: Arc, brightness: MemoryView, palettes: MemoryView, palette: VramPalette, index: usize, index_str: String, + params: VramParams, scale: f32, show_grid: bool, } impl CharacterDataWindow { - pub fn new(sim_id: SimId, memory: &mut MemoryMonitor) -> Self { + pub fn new(sim_id: SimId, memory: &mut MemoryMonitor, vram: &mut VramProcessor) -> Self { + let renderer = CharDataRenderer::new(sim_id, memory); + let ([char, chardata], params) = vram.add(renderer); + let loader = VramTextureLoader::new([ + ("vram://char".into(), char), + ("vram://chardata".into(), chardata), + ]); Self { sim_id, - loader: Arc::new(VramTextureLoader::new(CharDataLoader::new(sim_id, memory))), + loader: Arc::new(loader), brightness: memory.view(sim_id, 0x0005f824, 8), palettes: memory.view(sim_id, 0x0005f860, 16), - palette: VramPalette::Generic, - index: 0, - index_str: "0".into(), + palette: params.palette, + index: params.index, + index_str: params.index.to_string(), + params, scale: 4.0, show_grid: true, } @@ -150,11 +159,7 @@ impl CharacterDataWindow { }); }); }); - let resource = CharDataResource::Character { - palette: self.palette, - index: self.index, - }; - let image = Image::new(resource.to_uri()) + let image = Image::new("vram://char") .maintain_aspect_ratio(true) .tint(Color32::RED) .texture_options(TextureOptions::NEAREST); @@ -207,6 +212,11 @@ impl CharacterDataWindow { ui.checkbox(&mut self.show_grid, "Show grid"); }); }); + + self.params.write(CharDataParams { + palette: self.palette, + index: self.index, + }); } fn load_palette_colors(&self) -> [u8; 4] { @@ -220,10 +230,7 @@ impl CharacterDataWindow { } fn show_chardata(&mut self, ui: &mut Ui) { - let resource = CharDataResource::CharacterData { - palette: self.palette, - }; - let grid = CharacterGrid::new(resource.to_uri()) + let grid = CharacterGrid::new("vram://chardata") .with_scale(self.scale) .with_grid(self.show_grid) .with_selected(self.index); @@ -254,7 +261,6 @@ impl AppWindow for CharacterDataWindow { } fn show(&mut self, ctx: &Context) { - self.loader.begin_pass(); CentralPanel::default().show(ctx, |ui| { ui.horizontal_top(|ui| { StripBuilder::new(ui) @@ -279,6 +285,82 @@ enum CharDataResource { CharacterData { palette: VramPalette }, } +#[derive(Clone, Default, PartialEq, Eq)] +struct CharDataParams { + palette: VramPalette, + index: usize, +} + +struct CharDataRenderer { + chardata: MemoryView, + brightness: MemoryView, + palettes: MemoryView, +} + +impl VramRenderer<2> for CharDataRenderer { + type Params = CharDataParams; + + fn sizes(&self) -> [[usize; 2]; 2] { + [[8, 8], [16 * 8, 128 * 8]] + } + + fn render(&mut self, params: &Self::Params, image: &mut [VramImage; 2]) { + self.render_character(&mut image[0], params.palette, params.index); + self.render_character_data(&mut image[1], params.palette); + } +} + +impl CharDataRenderer { + pub fn new(sim_id: SimId, memory: &mut MemoryMonitor) -> Self { + Self { + chardata: memory.view(sim_id, 0x00078000, 0x8000), + brightness: memory.view(sim_id, 0x0005f824, 8), + palettes: memory.view(sim_id, 0x0005f860, 16), + } + } + + fn render_character(&self, image: &mut VramImage, palette: VramPalette, index: usize) { + if index >= 2048 { + return; + } + let palette = self.load_palette(palette); + let chardata = self.chardata.borrow(); + let character = chardata.range::(index * 8, 8); + for (row, pixels) in character.iter().enumerate() { + for col in 0..8 { + let char = (pixels >> (col * 2)) & 0x03; + image.write((col, row), palette[char as usize]); + } + } + } + + fn render_character_data(&self, image: &mut VramImage, palette: VramPalette) { + let palette = self.load_palette(palette); + let chardata = self.chardata.borrow(); + for (row, pixels) in chardata.range::(0, 8 * 2048).iter().enumerate() { + let char_index = row / 8; + let row_index = row % 8; + let x = (char_index % 16) * 8; + let y = (char_index / 16) * 8 + row_index; + for col in 0..8 { + let char = (pixels >> (col * 2)) & 0x03; + image.write((x + col, y), palette[char as usize]); + } + } + } + + fn load_palette(&self, palette: VramPalette) -> [u8; 4] { + let Some(offset) = palette.offset() else { + return utils::GENERIC_PALETTE; + }; + let palette = self.palettes.borrow().read(offset); + let brightnesses = self.brightness.borrow(); + let brts = brightnesses.range(0, 8); + utils::parse_palette(palette, brts) + } +} + +/* struct CharDataLoader { chardata: MemoryView, brightness: MemoryView, @@ -373,3 +455,4 @@ impl VramImageLoader for CharDataLoader { } } } + */ -- 2.40.1 From ebe444870fa9a431abb332cf6f4c4da2c634b1d3 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 10 Feb 2025 22:39:36 -0500 Subject: [PATCH 08/34] Sync memory between emulator thread and UI --- src/emulator.rs | 32 +++++++- src/memory.rs | 147 +++++++++++++++++++++++++++++------- src/window/vram/chardata.rs | 97 ------------------------ 3 files changed, 149 insertions(+), 127 deletions(-) diff --git a/src/emulator.rs b/src/emulator.rs index 96df37d..066e318 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -7,7 +7,7 @@ use std::{ sync::{ atomic::{AtomicBool, Ordering}, mpsc::{self, RecvError, TryRecvError}, - Arc, + Arc, Weak, }, }; @@ -17,7 +17,11 @@ use bytemuck::NoUninit; use egui_toast::{Toast, ToastKind, ToastOptions}; use tracing::{error, warn}; -use crate::{audio::Audio, graphics::TextureSink}; +use crate::{ + audio::Audio, + graphics::TextureSink, + memory::{MemoryRange, MemoryRegion}, +}; use shrooms_vb_core::{Sim, StopReason, EXPECTED_FRAME_SIZE}; pub use shrooms_vb_core::{VBKey, VBRegister, VBWatchpointType}; @@ -165,8 +169,10 @@ pub struct Emulator { renderers: HashMap, messages: HashMap>, debuggers: HashMap, + watched_regions: HashMap>, eye_contents: Vec, audio_samples: Vec, + buffer: Vec, } impl Emulator { @@ -189,8 +195,10 @@ impl Emulator { renderers: HashMap::new(), messages: HashMap::new(), debuggers: HashMap::new(), + watched_regions: HashMap::new(), eye_contents: vec![0u8; 384 * 224 * 2], audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE), + buffer: vec![], }) } @@ -367,6 +375,10 @@ impl Emulator { } } + fn watch_memory(&mut self, range: MemoryRange, region: Weak) { + self.watched_regions.insert(range, region); + } + pub fn run(&mut self) { loop { let idle = self.tick(); @@ -391,6 +403,18 @@ impl Emulator { } } } + self.watched_regions.retain(|range, region| { + let Some(region) = region.upgrade() else { + return false; + }; + let Some(sim) = self.sims.get_mut(range.sim.to_index()) else { + return false; + }; + self.buffer.clear(); + sim.read_memory(range.start, range.length, &mut self.buffer); + region.update(&self.buffer); + true + }); } } @@ -557,6 +581,9 @@ impl Emulator { sim.write_memory(start, &buffer); let _ = done.send(buffer); } + EmulatorCommand::WatchMemory(range, region) => { + self.watch_memory(range, region); + } EmulatorCommand::AddBreakpoint(sim_id, address) => { let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { return; @@ -647,6 +674,7 @@ pub enum EmulatorCommand { WriteRegister(SimId, VBRegister, u32), ReadMemory(SimId, u32, usize, Vec, oneshot::Sender>), WriteMemory(SimId, u32, Vec, oneshot::Sender>), + WatchMemory(MemoryRange, Weak), AddBreakpoint(SimId, u32), RemoveBreakpoint(SimId, u32), AddWatchpoint(SimId, u32, usize, VBWatchpointType), diff --git a/src/memory.rs b/src/memory.rs index dcafe7a..6c20f37 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -1,15 +1,18 @@ use std::{ collections::HashMap, - sync::{Arc, Mutex, MutexGuard}, + fmt::Debug, + sync::{atomic::AtomicU64, Arc, RwLock, RwLockReadGuard, TryLockError, Weak}, }; use bytemuck::BoxBytes; +use itertools::Itertools; +use tracing::warn; use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; pub struct MemoryMonitor { client: EmulatorClient, - regions: HashMap>>, + regions: HashMap>, } impl MemoryMonitor { @@ -21,20 +24,19 @@ impl MemoryMonitor { } pub fn view(&mut self, sim: SimId, start: u32, length: usize) -> MemoryView { - let region = MemoryRegion { sim, start, length }; - let memory = self.regions.entry(region).or_insert_with(|| { - let mut buf = aligned_memory(start, length); - let (tx, rx) = oneshot::channel(); - self.client - .send_command(EmulatorCommand::ReadMemory(sim, start, length, vec![], tx)); - let bytes = pollster::block_on(rx).unwrap(); - buf.copy_from_slice(&bytes); - #[expect(clippy::arc_with_non_send_sync)] // TODO: remove after bytemuck upgrade - Arc::new(Mutex::new(buf)) - }); - MemoryView { - memory: memory.clone(), - } + let range = MemoryRange { sim, start, length }; + let region = self + .regions + .get(&range) + .and_then(|r| r.upgrade()) + .unwrap_or_else(|| { + let region = Arc::new(MemoryRegion::new(start, length)); + self.regions.insert(range, Arc::downgrade(®ion)); + self.client + .send_command(EmulatorCommand::WatchMemory(range, Arc::downgrade(®ion))); + region + }); + MemoryView { region } } } @@ -52,21 +54,17 @@ fn aligned_memory(start: u32, length: usize) -> BoxBytes { } pub struct MemoryView { - memory: Arc>, + region: Arc, } -// SAFETY: BoxBytes is supposed to be Send+Sync, will be in a new version -unsafe impl Send for MemoryView {} impl MemoryView { pub fn borrow(&self) -> MemoryRef<'_> { - MemoryRef { - inner: self.memory.lock().unwrap(), - } + self.region.borrow() } } pub struct MemoryRef<'a> { - inner: MutexGuard<'a, BoxBytes>, + inner: RwLockReadGuard<'a, BoxBytes>, } impl MemoryRef<'_> { @@ -83,9 +81,102 @@ impl MemoryRef<'_> { } } -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -struct MemoryRegion { - sim: SimId, - start: u32, - length: usize, +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct MemoryRange { + pub sim: SimId, + pub start: u32, + pub length: usize, +} + +const BUFFERS: usize = 4; +pub struct MemoryRegion { + gens: [AtomicU64; BUFFERS], + bufs: [RwLock; BUFFERS], +} +impl Debug for MemoryRegion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MemoryRegion") + .field("gens", &self.gens) + .finish_non_exhaustive() + } +} +// SAFETY: BoxBytes is meant to be Send+Sync, will be in a future version +unsafe impl Send for MemoryRegion {} +// SAFETY: BoxBytes is meant to be Send+Sync, will be in a future version +unsafe impl Sync for MemoryRegion {} + +impl MemoryRegion { + fn new(start: u32, length: usize) -> Self { + Self { + gens: std::array::from_fn(|i| AtomicU64::new(i as u64)), + bufs: std::array::from_fn(|_| RwLock::new(aligned_memory(start, length))), + } + } + + pub fn borrow(&self) -> MemoryRef<'_> { + /* + * When reading memory, a thread will grab the newest buffer (with the highest gen) + * It will only fail to grab the lock if the writer already has it, + * but the writer prioritizes older buffers (with lower gens). + * So this method will only block if the writer produces three full buffers + * in the time it takes the reader to do four atomic reads and grab a lock. + * In the unlikely event this happens... just try again. + */ + loop { + let newest_index = self + .gens + .iter() + .map(|i| i.load(std::sync::atomic::Ordering::Acquire)) + .enumerate() + .max_by_key(|(_, gen)| *gen) + .map(|(i, _)| i) + .unwrap(); + let inner = match self.bufs[newest_index].try_read() { + Ok(inner) => inner, + Err(TryLockError::Poisoned(e)) => e.into_inner(), + Err(TryLockError::WouldBlock) => { + continue; + } + }; + break MemoryRef { inner }; + } + } + + pub fn update(&self, data: &[u8]) { + let gens: Vec = self + .gens + .iter() + .map(|i| i.load(std::sync::atomic::Ordering::Acquire)) + .collect(); + let next_gen = gens.iter().max().unwrap() + 1; + let indices = gens + .into_iter() + .enumerate() + .sorted_by_key(|(_, val)| *val) + .map(|(i, _)| i); + for index in indices { + let mut lock = match self.bufs[index].try_write() { + Ok(inner) => inner, + Err(TryLockError::Poisoned(e)) => e.into_inner(), + Err(TryLockError::WouldBlock) => { + continue; + } + }; + lock.copy_from_slice(data); + self.gens[index].store(next_gen, std::sync::atomic::Ordering::Release); + return; + } + /* + * We have four buffers, and (at time of writing) only three threads interacting with memory: + * - The UI thread, reading small regions of memory + * - The "vram renderer" thread, reading large regions of memory + * - The emulation thread, writing memory every so often + * So it should be impossible for all four buffers to have a read lock at the same time, + * and (because readers always read the newest buffer) at least one of the oldest three + * buffers will be free the entire time we're in this method. + * TL;DR this should never happen. + * But if it does, do nothing. This isn't medical software, better to show stale data than crash. + */ + warn!("all buffers were locked by a reader at the same time") + } } diff --git a/src/window/vram/chardata.rs b/src/window/vram/chardata.rs index b317026..dc73b64 100644 --- a/src/window/vram/chardata.rs +++ b/src/window/vram/chardata.rs @@ -359,100 +359,3 @@ impl CharDataRenderer { utils::parse_palette(palette, brts) } } - -/* -struct CharDataLoader { - chardata: MemoryView, - brightness: MemoryView, - palettes: MemoryView, -} - -impl CharDataLoader { - pub fn new(sim_id: SimId, memory: &mut MemoryMonitor) -> Self { - Self { - chardata: memory.view(sim_id, 0x00078000, 0x8000), - brightness: memory.view(sim_id, 0x0005f824, 8), - palettes: memory.view(sim_id, 0x0005f860, 16), - } - } - - fn update_character(&self, image: &mut VramImage, palette: VramPalette, index: usize) { - if index >= 2048 { - return; - } - let palette = self.load_palette(palette); - let chardata = self.chardata.borrow(); - let character = chardata.range::(index * 8, 8); - for (row, pixels) in character.iter().enumerate() { - for col in 0..8 { - let char = (pixels >> (col * 2)) & 0x03; - image.write((col, row), palette[char as usize]); - } - } - } - - fn update_character_data(&self, image: &mut VramImage, palette: VramPalette) { - let palette = self.load_palette(palette); - let chardata = self.chardata.borrow(); - for (row, pixels) in chardata.range::(0, 8 * 2048).iter().enumerate() { - let char_index = row / 8; - let row_index = row % 8; - let x = (char_index % 16) * 8; - let y = (char_index / 16) * 8 + row_index; - for col in 0..8 { - let char = (pixels >> (col * 2)) & 0x03; - image.write((x + col, y), palette[char as usize]); - } - } - } - - fn load_palette(&self, palette: VramPalette) -> [u8; 4] { - let Some(offset) = palette.offset() else { - return utils::GENERIC_PALETTE; - }; - let palette = self.palettes.borrow().read(offset); - let brightnesses = self.brightness.borrow(); - let brts = brightnesses.range(0, 8); - utils::parse_palette(palette, brts) - } -} - -impl VramImageLoader for CharDataLoader { - type Resource = CharDataResource; - - fn id(&self) -> &str { - concat!(module_path!(), "::CharDataLoader") - } - - fn add(&self, resource: &Self::Resource) -> Option { - match resource { - CharDataResource::Character { palette, index } => { - let mut image = VramImage::new(8, 8); - self.update_character(&mut image, *palette, *index); - Some(image) - } - CharDataResource::CharacterData { palette } => { - let mut image = VramImage::new(8 * 16, 8 * 128); - self.update_character_data(&mut image, *palette); - Some(image) - } - } - } - - fn update<'a>( - &'a self, - resources: impl Iterator, - ) { - for (resource, image) in resources { - match resource { - CharDataResource::Character { palette, index } => { - self.update_character(image, *palette, *index) - } - CharDataResource::CharacterData { palette } => { - self.update_character_data(image, *palette) - } - } - } - } -} - */ -- 2.40.1 From ba15dc77aed7f2a616e9308fe45a4ecc94eafb54 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 10 Feb 2025 23:08:35 -0500 Subject: [PATCH 09/34] When the UI consumes an input, don't send it to the game --- src/app.rs | 59 ++++++++++++++++++++++++--------------------- src/window.rs | 6 +++-- src/window/input.rs | 29 +++++++++++++--------- 3 files changed, 53 insertions(+), 41 deletions(-) diff --git a/src/app.rs b/src/app.rs index 10a7d1d..4c1b57d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -130,19 +130,23 @@ impl ApplicationHandler for Application { return; }; let viewport_id = viewport.id(); - match &event { - WindowEvent::KeyboardInput { event, .. } => { - self.controllers.handle_key_event(event); - viewport.app.handle_key_event(event); - } - WindowEvent::Focused(new_focused) => { - self.focused = new_focused.then_some(viewport_id); - } - _ => {} - } let mut queue_redraw = false; let mut inactive_viewports = HashSet::new(); - match viewport.on_window_event(event) { + let (consumed, action) = viewport.on_window_event(&event); + if !consumed { + match event { + WindowEvent::KeyboardInput { event, .. } => { + if !viewport.app.handle_key_event(&event) { + self.controllers.handle_key_event(&event); + } + } + WindowEvent::Focused(new_focused) => { + self.focused = new_focused.then_some(viewport_id); + } + _ => {} + } + } + match action { Some(Action::Redraw) => { for viewport in self.viewports.values_mut() { match viewport.redraw(event_loop) { @@ -194,15 +198,16 @@ impl ApplicationHandler for Application { fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { match event { UserEvent::GamepadEvent(event) => { - self.controllers.handle_gamepad_event(&event); - let Some(viewport) = self + if let Some(viewport) = self .focused .as_ref() .and_then(|id| self.viewports.get_mut(id)) - else { - return; - }; - viewport.app.handle_gamepad_event(&event); + { + if viewport.app.handle_gamepad_event(&event) { + return; + } + } + self.controllers.handle_gamepad_event(&event); } UserEvent::OpenAbout => { let about = AboutWindow; @@ -388,8 +393,8 @@ impl Viewport { self.app.viewport_id() } - pub fn on_window_event(&mut self, event: WindowEvent) -> Option { - let response = self.state.on_window_event(&self.window, &event); + pub fn on_window_event(&mut self, event: &WindowEvent) -> (bool, Option) { + let response = self.state.on_window_event(&self.window, event); egui_winit::update_viewport_info( &mut self.info, self.state.egui_ctx(), @@ -397,22 +402,22 @@ impl Viewport { false, ); - match event { + let action = match event { WindowEvent::RedrawRequested => Some(Action::Redraw), WindowEvent::CloseRequested => Some(Action::Close), WindowEvent::Resized(size) => { - let (Some(width), Some(height)) = + if let (Some(width), Some(height)) = (NonZero::new(size.width), NonZero::new(size.height)) - else { - return None; - }; - self.painter - .on_window_resized(ViewportId::ROOT, width, height); + { + self.painter + .on_window_resized(ViewportId::ROOT, width, height); + } None } _ if response.repaint => Some(Action::Redraw), _ => None, - } + }; + (response.consumed, action) } fn redraw(&mut self, event_loop: &ActiveEventLoop) -> Option { diff --git a/src/window.rs b/src/window.rs index ac5922e..132caca 100644 --- a/src/window.rs +++ b/src/window.rs @@ -28,10 +28,12 @@ pub trait AppWindow { let _ = render_state; } fn on_destroy(&mut self) {} - fn handle_key_event(&mut self, event: &KeyEvent) { + fn handle_key_event(&mut self, event: &KeyEvent) -> bool { let _ = event; + false } - fn handle_gamepad_event(&mut self, event: &gilrs::Event) { + fn handle_gamepad_event(&mut self, event: &gilrs::Event) -> bool { let _ = event; + false } } diff --git a/src/window/input.rs b/src/window/input.rs index 8fc20c2..de23e9b 100644 --- a/src/window/input.rs +++ b/src/window/input.rs @@ -198,58 +198,63 @@ impl AppWindow for InputWindow { }); } - fn handle_key_event(&mut self, event: &winit::event::KeyEvent) { + fn handle_key_event(&mut self, event: &winit::event::KeyEvent) -> bool { if !event.state.is_pressed() { - return; + return false; } let sim_id = match self.active_tab { InputTab::Player1 => SimId::Player1, InputTab::Player2 => SimId::Player2, _ => { - return; + return false; } }; let Some(vb) = self.now_binding.take() else { - return; + return false; }; let mut mappings = self.mappings.for_sim(sim_id).write().unwrap(); mappings.add_keyboard_mapping(vb, event.physical_key); drop(mappings); self.mappings.save(); + true } - fn handle_gamepad_event(&mut self, event: &gilrs::Event) { + fn handle_gamepad_event(&mut self, event: &gilrs::Event) -> bool { let InputTab::RebindGamepad(gamepad_id) = self.active_tab else { - return; + return false; }; if gamepad_id != event.id { - return; + return false; } let Some(mappings) = self.mappings.for_gamepad(gamepad_id) else { - return; + return false; }; let Some(vb) = self.now_binding else { - return; + return false; }; match event.event { EventType::ButtonPressed(_, code) => { let mut mapping = mappings.write().unwrap(); mapping.add_button_mapping(vb, code); self.now_binding.take(); + true } EventType::AxisChanged(_, value, code) => { if value < -0.75 { let mut mapping = mappings.write().unwrap(); mapping.add_axis_neg_mapping(vb, code); self.now_binding.take(); - } - if value > 0.75 { + true + } else if value > 0.75 { let mut mapping = mappings.write().unwrap(); mapping.add_axis_pos_mapping(vb, code); self.now_binding.take(); + true + } else { + false } } - _ => {} + _ => false, } } } -- 2.40.1 From a5676c20d127bd5a71c032722a75eedb757afd52 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Tue, 11 Feb 2025 23:42:30 -0500 Subject: [PATCH 10/34] Add a number editor widget --- src/window/utils.rs | 191 ++++++++++++++++++++++++++++++++---- src/window/vram/bgmap.rs | 18 ++-- src/window/vram/chardata.rs | 10 +- 3 files changed, 181 insertions(+), 38 deletions(-) diff --git a/src/window/utils.rs b/src/window/utils.rs index 1887671..df2f748 100644 --- a/src/window/utils.rs +++ b/src/window/utils.rs @@ -1,8 +1,8 @@ -use std::{ops::Range, str::FromStr}; +use std::ops::{Bound, RangeBounds}; use egui::{ - ecolor::HexColor, Align, Color32, Frame, Layout, Response, RichText, Sense, TextEdit, Ui, - UiBuilder, Vec2, WidgetText, + ecolor::HexColor, Align, Color32, CursorIcon, Frame, Layout, Margin, Rect, Response, RichText, + Rounding, Sense, Shape, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, WidgetText, }; pub trait UiExt { @@ -24,13 +24,6 @@ pub trait UiExt { fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response; fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response; - - fn number_picker( - &mut self, - text: &mut String, - value: &mut T, - range: Range, - ); } impl UiExt for Ui { @@ -90,18 +83,174 @@ impl UiExt for Ui { ) .inner } +} - fn number_picker( - &mut self, - text: &mut String, - value: &mut T, - range: Range, - ) { - let res = self.add(TextEdit::singleline(text).horizontal_align(Align::Max)); - if res.changed() { - if let Some(new_value) = text.parse().ok().filter(|v| range.contains(v)) { - *value = new_value; - } +pub struct NumberEdit<'a> { + value: &'a mut usize, + min: Option, + max: Option, +} + +impl<'a> NumberEdit<'a> { + pub fn new(value: &'a mut usize) -> Self { + Self { + value, + min: None, + max: None, } } + + pub fn range(self, range: impl RangeBounds) -> Self { + let min = match range.start_bound() { + Bound::Unbounded => None, + Bound::Included(t) => Some(*t), + Bound::Excluded(t) => t.checked_add(1), + }; + let max = match range.end_bound() { + Bound::Unbounded => None, + Bound::Included(t) => Some(*t), + Bound::Excluded(t) => t.checked_sub(1), + }; + Self { min, max, ..self } + } +} + +impl Widget for NumberEdit<'_> { + fn ui(self, ui: &mut Ui) -> Response { + let (last_value, mut str) = ui.memory(|m| { + m.data + .get_temp(ui.id()) + .unwrap_or((*self.value, self.value.to_string())) + }); + let mut stale = false; + if *self.value != last_value { + str = self.value.to_string(); + stale = true; + } + let valid = str.parse().is_ok_and(|v: usize| v == *self.value); + let text = TextEdit::singleline(&mut str) + .horizontal_align(Align::Max) + .margin(Margin { + left: 4.0, + right: 20.0, + top: 2.0, + bottom: 2.0, + }); + let res = if valid { + ui.add(text) + } else { + let message = match (self.min, self.max) { + (Some(min), Some(max)) => format!("Please enter a number between {min} and {max}."), + (Some(min), None) => format!("Please enter a number greater than {min}."), + (None, Some(max)) => format!("Please enter a number less than {max}."), + (None, None) => "Please enter a number.".to_string(), + }; + ui.scope(|ui| { + let style = ui.style_mut(); + style.visuals.selection.stroke.color = style.visuals.error_fg_color; + style.visuals.widgets.hovered.bg_stroke.color = + style.visuals.error_fg_color.gamma_multiply(0.60); + ui.add(text) + }) + .inner + .on_hover_text(message) + }; + + let arrow_left = res.rect.max.x + 4.0; + let arrow_right = res.rect.max.x + 20.0; + let arrow_top = res.rect.min.y - 2.0; + let arrow_middle = (res.rect.min.y + res.rect.max.y) / 2.0; + let arrow_bottom = res.rect.max.y + 2.0; + + let mut delta = 0; + let top_arrow_rect = Rect { + min: (arrow_left, arrow_top).into(), + max: (arrow_right, arrow_middle).into(), + }; + if draw_arrow(ui, top_arrow_rect, true).clicked_or_dragged() { + delta = 1; + } + let bottom_arrow_rect = Rect { + min: (arrow_left, arrow_middle).into(), + max: (arrow_right, arrow_bottom).into(), + }; + if draw_arrow(ui, bottom_arrow_rect, false).clicked_or_dragged() { + delta = -1; + } + + let in_range = + |&val: &usize| self.min.is_none_or(|m| m <= val) && self.max.is_none_or(|m| m >= val); + if delta != 0 { + if let Some(new_value) = self.value.checked_add_signed(delta).filter(in_range) { + *self.value = new_value; + } + str = self.value.to_string(); + stale = true; + } else if res.changed { + if let Some(new_value) = str.parse().ok().filter(in_range) { + *self.value = new_value; + } + stale = true; + } + if stale { + ui.memory_mut(|m| m.data.insert_temp(ui.id(), (*self.value, str))); + } + res + } +} + +fn draw_arrow(ui: &mut Ui, rect: Rect, up: bool) -> Response { + let arrow_res = ui + .allocate_rect(rect, Sense::click_and_drag()) + .on_hover_cursor(CursorIcon::Default); + let visuals = ui.style().visuals.widgets.style(&arrow_res); + let painter = ui.painter_at(arrow_res.rect); + + let rounding = if up { + Rounding { + ne: 2.0, + ..Rounding::ZERO + } + } else { + Rounding { + se: 2.0, + ..Rounding::ZERO + } + }; + painter.rect_filled(arrow_res.rect, rounding, visuals.bg_fill); + + let left = rect.left() + 4.0; + let center = (rect.left() + rect.right()) / 2.0; + let right = rect.right() - 4.0; + let top = rect.top() + 3.0; + let bottom = rect.bottom() - 3.0; + let points = if up { + vec![ + (left, bottom).into(), + (center, top).into(), + (right, bottom).into(), + ] + } else { + vec![ + (right, top).into(), + (center, bottom).into(), + (left, top).into(), + ] + }; + painter.add(Shape::convex_polygon( + points, + visuals.fg_stroke.color, + Stroke::NONE, + )); + arrow_res +} + +trait ResponseExt { + fn clicked_or_dragged(&self) -> bool; +} + +impl ResponseExt for Response { + fn clicked_or_dragged(&self) -> bool { + self.clicked() || self.dragged() + } } diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index a255914..2119733 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -10,7 +10,10 @@ use crate::{ emulator::SimId, memory::{MemoryMonitor, MemoryView}, vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, - window::{utils::UiExt, AppWindow}, + window::{ + utils::{NumberEdit, UiExt}, + AppWindow, + }, }; use super::utils::{parse_palette, CharacterGrid, GENERIC_PALETTE}; @@ -20,7 +23,6 @@ pub struct BgMapWindow { loader: Arc, bgmaps: MemoryView, cell_index: usize, - cell_index_str: String, generic_palette: bool, params: VramParams, scale: f32, @@ -38,7 +40,6 @@ impl BgMapWindow { loader: Arc::new(loader), bgmaps: memory.view(sim_id, 0x00020000, 0x1d800), cell_index: 0, - cell_index_str: "0".into(), generic_palette: false, params, scale: 1.0, @@ -59,11 +60,9 @@ impl BgMapWindow { }); row.col(|ui| { let mut bgmap_index = self.cell_index / 4096; - let mut bgmap_index_str = bgmap_index.to_string(); - ui.number_picker(&mut bgmap_index_str, &mut bgmap_index, 0..14); + ui.add(NumberEdit::new(&mut bgmap_index).range(0..14)); if bgmap_index != self.cell_index / 4096 { self.cell_index = (bgmap_index * 4096) + (self.cell_index % 4096); - self.cell_index_str = self.cell_index.to_string(); } }); }); @@ -72,11 +71,7 @@ impl BgMapWindow { ui.label("Cell"); }); row.col(|ui| { - ui.number_picker( - &mut self.cell_index_str, - &mut self.cell_index, - 0..16 * 4096, - ); + ui.add(NumberEdit::new(&mut self.cell_index).range(0..16 * 4096)); }); }); body.row(row_height, |mut row| { @@ -168,7 +163,6 @@ impl BgMapWindow { .with_selected(self.cell_index % 4096); if let Some(selected) = grid.show(ui) { self.cell_index = (self.cell_index / 4096 * 4096) + selected; - self.cell_index_str = self.cell_index.to_string(); } } } diff --git a/src/window/vram/chardata.rs b/src/window/vram/chardata.rs index dc73b64..d03f0d8 100644 --- a/src/window/vram/chardata.rs +++ b/src/window/vram/chardata.rs @@ -11,7 +11,10 @@ use crate::{ emulator::SimId, memory::{MemoryMonitor, MemoryView}, vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, - window::{utils::UiExt as _, AppWindow}, + window::{ + utils::{NumberEdit, UiExt as _}, + AppWindow, + }, }; use super::utils::{self, CharacterGrid}; @@ -83,7 +86,6 @@ pub struct CharacterDataWindow { palettes: MemoryView, palette: VramPalette, index: usize, - index_str: String, params: VramParams, scale: f32, show_grid: bool, @@ -104,7 +106,6 @@ impl CharacterDataWindow { palettes: memory.view(sim_id, 0x0005f860, 16), palette: params.palette, index: params.index, - index_str: params.index.to_string(), params, scale: 4.0, show_grid: true, @@ -123,7 +124,7 @@ impl CharacterDataWindow { ui.label("Index"); }); row.col(|ui| { - ui.number_picker(&mut self.index_str, &mut self.index, 0..2048); + ui.add(NumberEdit::new(&mut self.index).range(0..2048)); }); }); body.row(row_height, |mut row| { @@ -236,7 +237,6 @@ impl CharacterDataWindow { .with_selected(self.index); if let Some(selected) = grid.show(ui) { self.index = selected; - self.index_str = selected.to_string(); } } } -- 2.40.1 From d16c5363da78cb889ea6bf0825b7264ace566d11 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Tue, 11 Feb 2025 23:46:22 -0500 Subject: [PATCH 11/34] Move about into a button --- src/window/game.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/window/game.rs b/src/window/game.rs index 1130b65..4617850 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -145,9 +145,11 @@ impl GameWindow { ui.close_menu(); } }); - ui.menu_button("About", |ui| { - self.proxy.send_event(UserEvent::OpenAbout).unwrap(); - ui.close_menu(); + ui.menu_button("Help", |ui| { + if ui.button("About").clicked() { + self.proxy.send_event(UserEvent::OpenAbout).unwrap(); + ui.close_menu(); + } }); } -- 2.40.1 From dfcfd17bfcf184303d070fe03aca79e3817c2c15 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Wed, 12 Feb 2025 20:55:27 -0500 Subject: [PATCH 12/34] Optimize image updates --- src/vram.rs | 107 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 41 deletions(-) diff --git a/src/vram.rs b/src/vram.rs index ef36634..d260999 100644 --- a/src/vram.rs +++ b/src/vram.rs @@ -40,12 +40,10 @@ impl VramProcessor { &self, renderer: R, ) -> ([VramImageHandle; N], VramParams) { - let handles = renderer.sizes().map(|[width, height]| { - let data = Arc::new(Mutex::new(None)); - VramImageHandle { - size: [width as f32, height as f32], - data, - } + let states = renderer.sizes().map(VramRenderImageState::new); + let handles = states.clone().map(|state| VramImageHandle { + size: state.size.map(|i| i as f32), + data: state.sink, }); let images = renderer .sizes() @@ -55,7 +53,7 @@ impl VramProcessor { renderer, params: Arc::downgrade(&sink), images, - sinks: handles.clone().map(|i| i.data), + states, })); let params = VramParams { value: R::Params::default(), @@ -101,42 +99,35 @@ impl VramProcessorWorker { } } -pub enum VramImage { - Unchanged(Option>), - Changed(ColorImage), +pub struct VramImage { + size: [usize; 2], + shades: Vec, } impl VramImage { pub fn new(width: usize, height: usize) -> Self { - Self::Changed(ColorImage::new([width, height], Color32::BLACK)) - } - - pub fn write(&mut self, coords: (usize, usize), shade: u8) { - match self { - Self::Unchanged(image) => { - if image.as_ref().is_none_or(|i| i[coords].r() == shade) { - return; - } - let Some(mut new_image) = image.take().map(Arc::unwrap_or_clone) else { - return; - }; - new_image[coords] = Color32::from_gray(shade); - *self = Self::Changed(new_image); - } - Self::Changed(image) => { - image[coords] = Color32::from_gray(shade); - } + Self { + size: [width, height], + shades: vec![0; width * height], } } - pub fn take(&mut self) -> Option> { - match self { - Self::Unchanged(_) => None, - Self::Changed(image) => { - let arced = Arc::new(std::mem::take(image)); - *self = Self::Unchanged(Some(arced.clone())); - Some(arced) - } + pub fn write(&mut self, coords: (usize, usize), shade: u8) { + self.shades[coords.1 * self.size[0] + coords.0] = shade; + } + + pub fn changed(&self, image: &ColorImage) -> bool { + image + .pixels + .iter() + .map(|p| p.r()) + .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); } } } @@ -181,11 +172,47 @@ pub trait VramRenderer: Send { fn render(&mut self, params: &Self::Params, images: &mut [VramImage; N]); } +#[derive(Clone)] +struct VramRenderImageState { + size: [usize; 2], + buffers: [Arc; 2], + last_buffer: usize, + sink: Arc>>>, +} + +impl VramRenderImageState { + fn new(size: [usize; 2]) -> Self { + let buffers = [ + Arc::new(ColorImage::new(size, Color32::BLACK)), + Arc::new(ColorImage::new(size, Color32::BLACK)), + ]; + let sink = buffers[0].clone(); + Self { + size, + buffers, + last_buffer: 0, + sink: Arc::new(Mutex::new(Some(sink))), + } + } + fn try_send_update(&mut self, image: &VramImage) { + let last = &self.buffers[self.last_buffer]; + if !image.changed(last) { + return; + } + + let next_buffer = (self.last_buffer + 1) % self.buffers.len(); + let next = &mut self.buffers[next_buffer]; + image.read(Arc::make_mut(next)); + self.last_buffer = next_buffer; + self.sink.lock().unwrap().replace(next.clone()); + } +} + struct VramRendererWrapper> { renderer: R, params: Weak>, images: [VramImage; N], - sinks: [Arc>>>; N], + states: [VramRenderImageState; N], } trait VramRendererImpl: Send { @@ -203,10 +230,8 @@ impl + Send> VramRendererImpl for VramRendere }; self.renderer.render(¶ms, &mut self.images); - for (image, sink) in self.images.iter_mut().zip(&self.sinks) { - if let Some(update) = image.take() { - sink.lock().unwrap().replace(update); - } + for (state, image) in self.states.iter_mut().zip(&self.images) { + state.try_send_update(image); } Ok(()) } -- 2.40.1 From c4f17bfc13ac254ac35da57d65f42251cf2901fe Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Wed, 12 Feb 2025 23:59:48 -0500 Subject: [PATCH 13/34] Start drawing objects --- src/app.rs | 7 +- src/vram.rs | 35 +++-- src/window.rs | 2 +- src/window/game.rs | 6 + src/window/vram.rs | 2 + src/window/vram/bgmap.rs | 75 +++------- src/window/vram/chardata.rs | 19 +-- src/window/vram/object.rs | 277 ++++++++++++++++++++++++++++++++++++ src/window/vram/utils.rs | 45 +++++- 9 files changed, 380 insertions(+), 88 deletions(-) create mode 100644 src/window/vram/object.rs 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); -- 2.40.1 From 92ccc482ae66f3aec8ca71133de242468e2972e8 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Thu, 13 Feb 2025 23:06:27 -0500 Subject: [PATCH 14/34] Fix keyboard navigation for numberedit --- src/window/utils.rs | 53 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/src/window/utils.rs b/src/window/utils.rs index df2f748..7465794 100644 --- a/src/window/utils.rs +++ b/src/window/utils.rs @@ -1,8 +1,9 @@ use std::ops::{Bound, RangeBounds}; use egui::{ - ecolor::HexColor, Align, Color32, CursorIcon, Frame, Layout, Margin, Rect, Response, RichText, - Rounding, Sense, Shape, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, WidgetText, + ecolor::HexColor, Align, Color32, CursorIcon, Event, Frame, Key, Layout, Margin, Rect, + Response, RichText, Rounding, Sense, Shape, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, + WidgetText, }; pub trait UiExt { @@ -117,10 +118,13 @@ impl<'a> NumberEdit<'a> { impl Widget for NumberEdit<'_> { fn ui(self, ui: &mut Ui) -> Response { - let (last_value, mut str) = ui.memory(|m| { - m.data + let (last_value, mut str, focus) = ui.memory(|m| { + let (lv, s) = m + .data .get_temp(ui.id()) - .unwrap_or((*self.value, self.value.to_string())) + .unwrap_or((*self.value, self.value.to_string())); + let focus = m.has_focus(ui.id()); + (lv, s, focus) }); let mut stale = false; if *self.value != last_value { @@ -128,8 +132,34 @@ impl Widget for NumberEdit<'_> { stale = true; } let valid = str.parse().is_ok_and(|v: usize| v == *self.value); + let mut up_pressed = false; + let mut down_pressed = false; + if focus { + ui.input_mut(|i| { + i.events.retain(|e| match e { + Event::Key { + key: Key::ArrowUp, + pressed: true, + .. + } => { + up_pressed = true; + false + } + Event::Key { + key: Key::ArrowDown, + pressed: true, + .. + } => { + down_pressed = true; + false + } + _ => true, + }) + }); + } let text = TextEdit::singleline(&mut str) .horizontal_align(Align::Max) + .id(ui.id()) .margin(Margin { left: 4.0, right: 20.0, @@ -167,14 +197,14 @@ impl Widget for NumberEdit<'_> { min: (arrow_left, arrow_top).into(), max: (arrow_right, arrow_middle).into(), }; - if draw_arrow(ui, top_arrow_rect, true).clicked_or_dragged() { + if draw_arrow(ui, top_arrow_rect, true).clicked_or_dragged() || up_pressed { delta = 1; } let bottom_arrow_rect = Rect { min: (arrow_left, arrow_middle).into(), max: (arrow_right, arrow_bottom).into(), }; - if draw_arrow(ui, bottom_arrow_rect, false).clicked_or_dragged() { + if draw_arrow(ui, bottom_arrow_rect, false).clicked_or_dragged() || down_pressed { delta = -1; } @@ -201,7 +231,14 @@ impl Widget for NumberEdit<'_> { fn draw_arrow(ui: &mut Ui, rect: Rect, up: bool) -> Response { let arrow_res = ui - .allocate_rect(rect, Sense::click_and_drag()) + .allocate_rect( + rect, + Sense { + click: true, + drag: true, + focusable: false, + }, + ) .on_hover_cursor(CursorIcon::Default); let visuals = ui.style().visuals.widgets.style(&arrow_res); let painter = ui.painter_at(arrow_res.rect); -- 2.40.1 From b5e1711a567d5180bb5869403d64ac6ebf3d3100 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Thu, 13 Feb 2025 23:37:40 -0500 Subject: [PATCH 15/34] Support signed numbers in number picker --- src/window/utils.rs | 55 ++++++++++++++++++++++++++------------- src/window/vram/object.rs | 30 +++++++++++++++++++++ 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/src/window/utils.rs b/src/window/utils.rs index 7465794..ec3fe98 100644 --- a/src/window/utils.rs +++ b/src/window/utils.rs @@ -1,10 +1,15 @@ -use std::ops::{Bound, RangeBounds}; +use std::{ + fmt::Display, + ops::{Bound, RangeBounds}, + str::FromStr, +}; use egui::{ ecolor::HexColor, Align, Color32, CursorIcon, Event, Frame, Key, Layout, Margin, Rect, Response, RichText, Rounding, Sense, Shape, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, WidgetText, }; +use num_traits::PrimInt; pub trait UiExt { fn section(&mut self, title: impl Into, add_contents: impl FnOnce(&mut Ui)); @@ -86,37 +91,47 @@ impl UiExt for Ui { } } -pub struct NumberEdit<'a> { - value: &'a mut usize, - min: Option, - max: Option, +enum Direction { + Up, + Down, } -impl<'a> NumberEdit<'a> { - pub fn new(value: &'a mut usize) -> Self { +pub trait Number: PrimInt + Display + FromStr + Send + Sync + 'static {} +impl Number for T {} + +pub struct NumberEdit<'a, T: Number> { + value: &'a mut T, + increment: T, + min: Option, + max: Option, +} + +impl<'a, T: Number> NumberEdit<'a, T> { + pub fn new(value: &'a mut T) -> Self { Self { value, + increment: T::one(), min: None, max: None, } } - pub fn range(self, range: impl RangeBounds) -> Self { + pub fn range(self, range: impl RangeBounds) -> Self { let min = match range.start_bound() { Bound::Unbounded => None, Bound::Included(t) => Some(*t), - Bound::Excluded(t) => t.checked_add(1), + Bound::Excluded(t) => t.checked_add(&self.increment), }; let max = match range.end_bound() { Bound::Unbounded => None, Bound::Included(t) => Some(*t), - Bound::Excluded(t) => t.checked_sub(1), + Bound::Excluded(t) => t.checked_sub(&self.increment), }; Self { min, max, ..self } } } -impl Widget for NumberEdit<'_> { +impl Widget for NumberEdit<'_, T> { fn ui(self, ui: &mut Ui) -> Response { let (last_value, mut str, focus) = ui.memory(|m| { let (lv, s) = m @@ -131,7 +146,7 @@ impl Widget for NumberEdit<'_> { str = self.value.to_string(); stale = true; } - let valid = str.parse().is_ok_and(|v: usize| v == *self.value); + let valid = str.parse().is_ok_and(|v: T| v == *self.value); let mut up_pressed = false; let mut down_pressed = false; if focus { @@ -192,26 +207,30 @@ impl Widget for NumberEdit<'_> { let arrow_middle = (res.rect.min.y + res.rect.max.y) / 2.0; let arrow_bottom = res.rect.max.y + 2.0; - let mut delta = 0; + let mut delta = None; let top_arrow_rect = Rect { min: (arrow_left, arrow_top).into(), max: (arrow_right, arrow_middle).into(), }; if draw_arrow(ui, top_arrow_rect, true).clicked_or_dragged() || up_pressed { - delta = 1; + delta = Some(Direction::Up); } let bottom_arrow_rect = Rect { min: (arrow_left, arrow_middle).into(), max: (arrow_right, arrow_bottom).into(), }; if draw_arrow(ui, bottom_arrow_rect, false).clicked_or_dragged() || down_pressed { - delta = -1; + delta = Some(Direction::Down); } let in_range = - |&val: &usize| self.min.is_none_or(|m| m <= val) && self.max.is_none_or(|m| m >= val); - if delta != 0 { - if let Some(new_value) = self.value.checked_add_signed(delta).filter(in_range) { + |val: &T| self.min.is_none_or(|m| &m <= val) && self.max.is_none_or(|m| &m >= val); + if let Some(dir) = delta { + let value = match dir { + Direction::Up => self.value.checked_add(&self.increment), + Direction::Down => self.value.checked_sub(&self.increment), + }; + if let Some(new_value) = value.filter(in_range) { *self.value = new_value; } str = self.value.to_string(); diff --git a/src/window/vram/object.rs b/src/window/vram/object.rs index 23dbb60..756aff0 100644 --- a/src/window/vram/object.rs +++ b/src/window/vram/object.rs @@ -62,6 +62,9 @@ impl ObjectWindow { }); ui.section("Properties", |ui| { let object = self.objects.borrow().read::<[u16; 4]>(self.index); + let mut x = ((object[0] & 0x3ff) << 6 >> 6) as i16; + let mut parallax = ((object[1] & 0x3ff) << 6 >> 6) as i16; + let mut y = ((object[2] & 0x0ff) << 8 >> 8) as i16; let (mut char_index, mut vflip, mut hflip, palette_index) = utils::parse_cell(object[3]); TableBuilder::new(ui) @@ -91,6 +94,33 @@ impl ObjectWindow { ); }); }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("X"); + }); + row.col(|ui| { + ui.add_enabled(false, NumberEdit::new(&mut x).range(-512..512)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Y"); + }); + row.col(|ui| { + ui.add_enabled(false, NumberEdit::new(&mut y).range(-8..=224)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Parallax"); + }); + row.col(|ui| { + ui.add_enabled( + false, + NumberEdit::new(&mut parallax).range(-512..512), + ); + }); + }); body.row(row_height, |mut row| { row.col(|ui| { let checkbox = Checkbox::new(&mut hflip, "H-flip"); -- 2.40.1 From 7356287030ab06816096c5cddaff7ca90145595c Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 15 Feb 2025 00:28:37 -0500 Subject: [PATCH 16/34] Finish object view --- src/window/vram/bgmap.rs | 23 ++++++-- src/window/vram/object.rs | 119 ++++++++++++++++++++++++-------------- src/window/vram/utils.rs | 57 ++++++++++++++++-- 3 files changed, 145 insertions(+), 54 deletions(-) diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index 67f1279..550933c 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -16,7 +16,7 @@ use crate::{ }, }; -use super::utils::{self, CharacterGrid}; +use super::utils::{self, CellData, CharacterGrid}; pub struct BgMapWindow { sim_id: SimId, @@ -94,7 +94,12 @@ impl BgMapWindow { 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) = utils::parse_cell(cell); + let CellData { + char_index, + mut vflip, + mut hflip, + palette_index, + } = CellData::parse(cell); TableBuilder::new(ui) .column(Column::remainder()) .column(Column::remainder()) @@ -245,7 +250,12 @@ impl BgMapRenderer { .iter() .enumerate() { - let (char_index, vflip, hflip, palette_index) = utils::parse_cell(*cell); + let CellData { + char_index, + vflip, + hflip, + palette_index, + } = CellData::parse(*cell); let char = chardata.range::(char_index * 8, 8); let palette = &colors[palette_index]; @@ -269,7 +279,12 @@ impl BgMapRenderer { let cell = bgmaps.read::(index); - let (char_index, vflip, hflip, palette_index) = utils::parse_cell(cell); + let CellData { + char_index, + vflip, + hflip, + palette_index, + } = CellData::parse(cell); let char = chardata.range::(char_index * 8, 8); let palette = if generic_palette { utils::generic_palette(Color32::RED) diff --git a/src/window/vram/object.rs b/src/window/vram/object.rs index 756aff0..0b543d7 100644 --- a/src/window/vram/object.rs +++ b/src/window/vram/object.rs @@ -16,7 +16,7 @@ use crate::{ }, }; -use super::utils; +use super::utils::{self, Object}; pub struct ObjectWindow { sim_id: SimId, @@ -31,8 +31,9 @@ pub struct ObjectWindow { 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)]); + let ([zoom, full], params) = vram.add(renderer); + let loader = + VramTextureLoader::new([("vram://zoom".into(), zoom), ("vram://full".into(), full)]); Self { sim_id, loader: Arc::new(loader), @@ -59,14 +60,27 @@ impl ObjectWindow { ui.add(NumberEdit::new(&mut self.index).range(0..1024)); }); }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Address"); + }); + row.col(|ui| { + let address = 0x3e000 + self.index * 8; + let mut address_str = format!("{address:08x}"); + ui.add_enabled( + false, + TextEdit::singleline(&mut address_str).horizontal_align(Align::Max), + ); + }); + }); }); + let image = Image::new("vram://zoom") + .maintain_aspect_ratio(true) + .texture_options(TextureOptions::NEAREST); + ui.add(image); ui.section("Properties", |ui| { let object = self.objects.borrow().read::<[u16; 4]>(self.index); - let mut x = ((object[0] & 0x3ff) << 6 >> 6) as i16; - let mut parallax = ((object[1] & 0x3ff) << 6 >> 6) as i16; - let mut y = ((object[2] & 0x0ff) << 8 >> 8) as i16; - let (mut char_index, mut vflip, mut hflip, palette_index) = - utils::parse_cell(object[3]); + let mut obj = Object::parse(object); TableBuilder::new(ui) .column(Column::remainder()) .column(Column::remainder()) @@ -78,7 +92,7 @@ impl ObjectWindow { row.col(|ui| { ui.add_enabled( false, - NumberEdit::new(&mut char_index).range(0..2048), + NumberEdit::new(&mut obj.data.char_index).range(0..2048), ); }); }); @@ -87,7 +101,7 @@ impl ObjectWindow { ui.label("Palette"); }); row.col(|ui| { - let mut palette = format!("OBJ {}", palette_index); + let mut palette = format!("OBJ {}", obj.data.palette_index); ui.add_enabled( false, TextEdit::singleline(&mut palette).horizontal_align(Align::Max), @@ -99,7 +113,7 @@ impl ObjectWindow { ui.label("X"); }); row.col(|ui| { - ui.add_enabled(false, NumberEdit::new(&mut x).range(-512..512)); + ui.add_enabled(false, NumberEdit::new(&mut obj.x).range(-512..512)); }); }); body.row(row_height, |mut row| { @@ -107,7 +121,7 @@ impl ObjectWindow { ui.label("Y"); }); row.col(|ui| { - ui.add_enabled(false, NumberEdit::new(&mut y).range(-8..=224)); + ui.add_enabled(false, NumberEdit::new(&mut obj.y).range(-8..=224)); }); }); body.row(row_height, |mut row| { @@ -117,17 +131,27 @@ impl ObjectWindow { row.col(|ui| { ui.add_enabled( false, - NumberEdit::new(&mut parallax).range(-512..512), + NumberEdit::new(&mut obj.parallax).range(-512..512), ); }); }); body.row(row_height, |mut row| { row.col(|ui| { - let checkbox = Checkbox::new(&mut hflip, "H-flip"); + let checkbox = Checkbox::new(&mut obj.data.hflip, "H-flip"); ui.add_enabled(false, checkbox); }); row.col(|ui| { - let checkbox = Checkbox::new(&mut vflip, "V-flip"); + let checkbox = Checkbox::new(&mut obj.data.vflip, "V-flip"); + ui.add_enabled(false, checkbox); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + let checkbox = Checkbox::new(&mut obj.lon, "Left"); + ui.add_enabled(false, checkbox); + }); + row.col(|ui| { + let checkbox = Checkbox::new(&mut obj.ron, "Right"); ui.add_enabled(false, checkbox); }); }); @@ -153,7 +177,7 @@ impl ObjectWindow { } fn show_object(&mut self, ui: &mut Ui) { - let image = Image::new("vram://object") + let image = Image::new("vram://full") .fit_to_original_size(self.scale) .texture_options(TextureOptions::NEAREST); ui.add(image); @@ -172,7 +196,7 @@ impl AppWindow for ObjectWindow { fn initial_viewport(&self) -> ViewportBuilder { ViewportBuilder::default() .with_title(format!("Object Data ({})", self.sim_id)) - .with_inner_size((640.0, 480.0)) + .with_inner_size((640.0, 500.0)) } fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) { @@ -239,50 +263,55 @@ impl ObjectRenderer { } } - fn render_object(&self, image: &mut VramImage, params: &ObjectParams, eye: Eye) { + fn render_object(&self, image: &mut VramImage, params: &ObjectParams, use_pos: bool, 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 obj = Object::parse(object); - let ron = object[1] & 0x4000 != 0; - let lon = object[1] & 0x8000 != 0; if match eye { - Eye::Left => !lon, - Eye::Right => !ron, + Eye::Left => !obj.lon, + Eye::Right => !obj.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 (x, y) = if use_pos { + let x = match eye { + Eye::Left => obj.x - obj.parallax, + Eye::Right => obj.x + obj.parallax, + }; + (x, obj.y) + } else { + (0, 0) }; - let (char_index, vflip, hflip, palette_index) = utils::parse_cell(object[3]); - let char = chardata.range::(char_index * 8, 8); + let color = match eye { + Eye::Left => params.left_color, + Eye::Right => params.right_color, + }; + + let char = chardata.range::(obj.data.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) + utils::parse_palette(palettes.read(8 + obj.data.palette_index * 2), brts, color) }; for row in 0..8 { let real_y = y + row as i16; - if !(0..384).contains(&real_y) { + if !(0..224).contains(&real_y) { continue; } - for (col, pixel) in utils::read_char_row(char, hflip, vflip, row).enumerate() { + for (col, pixel) in + utils::read_char_row(char, obj.data.hflip, obj.data.vflip, row).enumerate() + { let real_x = x + col as i16; - if !(0..224).contains(&real_x) { + if !(0..384).contains(&real_x) { continue; } image.add((real_x as usize, real_y as usize), palette[pixel as usize]); @@ -291,17 +320,19 @@ impl ObjectRenderer { } } -impl VramRenderer<1> for ObjectRenderer { +impl VramRenderer<2> for ObjectRenderer { type Params = ObjectParams; - fn sizes(&self) -> [[usize; 2]; 1] { - [[384, 224]] + fn sizes(&self) -> [[usize; 2]; 2] { + [[8, 8], [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); + fn render(&mut self, params: &Self::Params, images: &mut [VramImage; 2]) { + images[0].clear(); + self.render_object(&mut images[0], params, false, Eye::Left); + self.render_object(&mut images[0], params, false, Eye::Right); + images[1].clear(); + self.render_object(&mut images[1], params, true, Eye::Left); + self.render_object(&mut images[1], params, true, Eye::Right); } } diff --git a/src/window/vram/utils.rs b/src/window/vram/utils.rs index e8bf69f..c605302 100644 --- a/src/window/vram/utils.rs +++ b/src/window/vram/utils.rs @@ -28,12 +28,57 @@ pub fn parse_palette(palette: u8, brts: &[u8], color: Color32) -> [Color32; 4] { ] } -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 struct Object { + pub x: i16, + pub lon: bool, + pub ron: bool, + pub parallax: i16, + pub y: i16, + pub data: CellData, +} + +impl Object { + pub fn parse(object: [u16; 4]) -> Self { + let x = ((object[0] & 0x3ff) << 6 >> 6) as i16; + let parallax = ((object[1] & 0x3ff) << 6 >> 6) as i16; + let lon = object[1] & 0x8000 != 0; + let ron = object[1] & 0x4000 != 0; + let y = (object[2] & 0x0ff) as i16; + // Y is stored as the bottom 8 bits of an i16, + // so only sign extend if it's out of range. + let y = if y > 224 { y << 8 >> 8 } else { y }; + let data = CellData::parse(object[3]); + Self { + x, + lon, + ron, + parallax, + y, + data, + } + } +} + +pub struct CellData { + pub palette_index: usize, + pub hflip: bool, + pub vflip: bool, + pub char_index: usize, +} + +impl CellData { + pub fn parse(cell: u16) -> Self { + let char_index = (cell & 0x7ff) as usize; + let vflip = cell & 0x1000 != 0; + let hflip = cell & 0x2000 != 0; + let palette_index = (cell >> 14) as usize; + Self { + char_index, + vflip, + hflip, + palette_index, + } + } } pub fn read_char_row( -- 2.40.1 From b888d1140afc673999a68f1c0d26872c3d8920f7 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 15 Feb 2025 12:56:58 -0500 Subject: [PATCH 17/34] Make properties editable --- src/app.rs | 12 +++---- src/memory.rs | 25 +++++++++----- src/window/vram/bgmap.rs | 63 ++++++++++++++++----------------- src/window/vram/chardata.rs | 16 ++++----- src/window/vram/object.rs | 69 +++++++++++++++++++------------------ src/window/vram/utils.rs | 41 ++++++++++++++++++++-- 6 files changed, 136 insertions(+), 90 deletions(-) diff --git a/src/app.rs b/src/app.rs index 6197675..7566d91 100644 --- a/src/app.rs +++ b/src/app.rs @@ -18,7 +18,7 @@ use crate::{ controller::ControllerManager, emulator::{EmulatorClient, EmulatorCommand, SimId}, input::MappingProvider, - memory::MemoryMonitor, + memory::MemoryClient, persistence::Persistence, vram::VramProcessor, window::{ @@ -45,7 +45,7 @@ pub struct Application { proxy: EventLoopProxy, mappings: MappingProvider, controllers: ControllerManager, - memory: MemoryMonitor, + memory: Arc, vram: VramProcessor, persistence: Persistence, viewports: HashMap, @@ -64,7 +64,7 @@ impl Application { let persistence = Persistence::new(); let mappings = MappingProvider::new(persistence.clone()); let controllers = ControllerManager::new(client.clone(), &mappings); - let memory = MemoryMonitor::new(client.clone()); + let memory = Arc::new(MemoryClient::new(client.clone())); let vram = VramProcessor::new(); { let mappings = mappings.clone(); @@ -214,15 +214,15 @@ impl ApplicationHandler for Application { self.open(event_loop, Box::new(about)); } UserEvent::OpenCharacterData(sim_id) => { - let vram = CharacterDataWindow::new(sim_id, &mut self.memory, &mut self.vram); + let vram = CharacterDataWindow::new(sim_id, &self.memory, &mut self.vram); self.open(event_loop, Box::new(vram)); } UserEvent::OpenBgMap(sim_id) => { - let bgmap = BgMapWindow::new(sim_id, &mut self.memory, &mut self.vram); + let bgmap = BgMapWindow::new(sim_id, &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); + let objects = ObjectWindow::new(sim_id, &self.memory, &mut self.vram); self.open(event_loop, Box::new(objects)); } UserEvent::OpenDebugger(sim_id) => { diff --git a/src/memory.rs b/src/memory.rs index 6c20f37..9b54c8a 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -1,7 +1,7 @@ use std::{ collections::HashMap, fmt::Debug, - sync::{atomic::AtomicU64, Arc, RwLock, RwLockReadGuard, TryLockError, Weak}, + sync::{atomic::AtomicU64, Arc, Mutex, RwLock, RwLockReadGuard, TryLockError, Weak}, }; use bytemuck::BoxBytes; @@ -10,34 +10,41 @@ use tracing::warn; use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; -pub struct MemoryMonitor { +pub struct MemoryClient { client: EmulatorClient, - regions: HashMap>, + regions: Mutex>>, } -impl MemoryMonitor { +impl MemoryClient { pub fn new(client: EmulatorClient) -> Self { Self { client, - regions: HashMap::new(), + regions: Mutex::new(HashMap::new()), } } - pub fn view(&mut self, sim: SimId, start: u32, length: usize) -> MemoryView { + pub fn watch(&self, sim: SimId, start: u32, length: usize) -> MemoryView { let range = MemoryRange { sim, start, length }; - let region = self - .regions + let mut regions = self.regions.lock().unwrap_or_else(|e| e.into_inner()); + let region = regions .get(&range) .and_then(|r| r.upgrade()) .unwrap_or_else(|| { let region = Arc::new(MemoryRegion::new(start, length)); - self.regions.insert(range, Arc::downgrade(®ion)); + regions.insert(range, Arc::downgrade(®ion)); self.client .send_command(EmulatorCommand::WatchMemory(range, Arc::downgrade(®ion))); region }); MemoryView { region } } + + pub fn write(&self, sim: SimId, address: u32, data: &T) { + let data = bytemuck::bytes_of(data).to_vec(); + let (tx, _) = oneshot::channel(); + self.client + .send_command(EmulatorCommand::WriteMemory(sim, address, data, tx)); + } } fn aligned_memory(start: u32, length: usize) -> BoxBytes { diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index 550933c..93bca1a 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -1,14 +1,14 @@ use std::sync::Arc; use egui::{ - Align, CentralPanel, Checkbox, Color32, Context, Image, ScrollArea, Slider, TextEdit, + Align, CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextEdit, TextureOptions, Ui, ViewportBuilder, ViewportId, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use crate::{ emulator::SimId, - memory::{MemoryMonitor, MemoryView}, + memory::{MemoryClient, MemoryView}, vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, window::{ utils::{NumberEdit, UiExt}, @@ -21,6 +21,7 @@ use super::utils::{self, CellData, CharacterGrid}; pub struct BgMapWindow { sim_id: SimId, loader: Arc, + memory: Arc, bgmaps: MemoryView, cell_index: usize, generic_palette: bool, @@ -30,7 +31,7 @@ pub struct BgMapWindow { } impl BgMapWindow { - pub fn new(sim_id: SimId, memory: &mut MemoryMonitor, vram: &mut VramProcessor) -> Self { + pub fn new(sim_id: SimId, memory: &Arc, vram: &mut VramProcessor) -> Self { let renderer = BgMapRenderer::new(sim_id, memory); let ([cell, bgmap], params) = vram.add(renderer); let loader = @@ -38,7 +39,8 @@ impl BgMapWindow { Self { sim_id, loader: Arc::new(loader), - bgmaps: memory.view(sim_id, 0x00020000, 0x1d800), + memory: memory.clone(), + bgmaps: memory.watch(sim_id, 0x00020000, 0x1d800), cell_index: 0, generic_palette: false, params, @@ -93,13 +95,8 @@ impl BgMapWindow { .texture_options(TextureOptions::NEAREST); ui.add(image); ui.section("Cell", |ui| { - let cell = self.bgmaps.borrow().read::(self.cell_index); - let CellData { - char_index, - mut vflip, - mut hflip, - palette_index, - } = CellData::parse(cell); + let mut data = self.bgmaps.borrow().read::(self.cell_index); + let mut cell = CellData::parse(data); TableBuilder::new(ui) .column(Column::remainder()) .column(Column::remainder()) @@ -109,12 +106,7 @@ impl BgMapWindow { ui.label("Character"); }); row.col(|ui| { - let mut character_str = char_index.to_string(); - ui.add_enabled( - false, - TextEdit::singleline(&mut character_str) - .horizontal_align(Align::Max), - ); + ui.add(NumberEdit::new(&mut cell.char_index).range(0..2048)); }); }); body.row(row_height, |mut row| { @@ -122,24 +114,33 @@ impl BgMapWindow { ui.label("Palette"); }); row.col(|ui| { - let mut palette = format!("BG {}", palette_index); - ui.add_enabled( - false, - TextEdit::singleline(&mut palette).horizontal_align(Align::Max), - ); + ComboBox::from_id_salt("palette") + .selected_text(format!("BG {}", cell.palette_index)) + .width(ui.available_width()) + .show_ui(ui, |ui| { + for palette in 0..4 { + ui.selectable_value( + &mut cell.palette_index, + palette, + format!("BG {palette}"), + ); + } + }); }); }); body.row(row_height, |mut row| { row.col(|ui| { - let checkbox = Checkbox::new(&mut hflip, "H-flip"); - ui.add_enabled(false, checkbox); + ui.add(Checkbox::new(&mut cell.hflip, "H-flip")); }); row.col(|ui| { - let checkbox = Checkbox::new(&mut vflip, "V-flip"); - ui.add_enabled(false, checkbox); + ui.add(Checkbox::new(&mut cell.vflip, "V-flip")); }); }); }); + if cell.update(&mut data) { + let address = 0x00020000 + (self.cell_index * 2); + self.memory.write(self.sim_id, address as u32, &data); + } }); ui.section("Display", |ui| { ui.horizontal(|ui| { @@ -223,12 +224,12 @@ struct BgMapRenderer { } impl BgMapRenderer { - pub fn new(sim_id: SimId, memory: &mut MemoryMonitor) -> Self { + pub fn new(sim_id: SimId, memory: &MemoryClient) -> Self { Self { - chardata: memory.view(sim_id, 0x00078000, 0x8000), - bgmaps: memory.view(sim_id, 0x00020000, 0x1d800), - brightness: memory.view(sim_id, 0x0005f824, 8), - palettes: memory.view(sim_id, 0x0005f860, 16), + chardata: memory.watch(sim_id, 0x00078000, 0x8000), + bgmaps: memory.watch(sim_id, 0x00020000, 0x1d800), + brightness: memory.watch(sim_id, 0x0005f824, 8), + palettes: memory.watch(sim_id, 0x0005f860, 16), } } diff --git a/src/window/vram/chardata.rs b/src/window/vram/chardata.rs index 00ee780..dc102d3 100644 --- a/src/window/vram/chardata.rs +++ b/src/window/vram/chardata.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use crate::{ emulator::SimId, - memory::{MemoryMonitor, MemoryView}, + memory::{MemoryClient, MemoryView}, vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, window::{ utils::{NumberEdit, UiExt as _}, @@ -92,7 +92,7 @@ pub struct CharacterDataWindow { } impl CharacterDataWindow { - pub fn new(sim_id: SimId, memory: &mut MemoryMonitor, vram: &mut VramProcessor) -> Self { + pub fn new(sim_id: SimId, memory: &MemoryClient, vram: &mut VramProcessor) -> Self { let renderer = CharDataRenderer::new(sim_id, memory); let ([char, chardata], params) = vram.add(renderer); let loader = VramTextureLoader::new([ @@ -102,8 +102,8 @@ impl CharacterDataWindow { Self { sim_id, loader: Arc::new(loader), - brightness: memory.view(sim_id, 0x0005f824, 8), - palettes: memory.view(sim_id, 0x0005f860, 16), + brightness: memory.watch(sim_id, 0x0005f824, 8), + palettes: memory.watch(sim_id, 0x0005f860, 16), palette: params.palette, index: params.index, params, @@ -306,11 +306,11 @@ impl VramRenderer<2> for CharDataRenderer { } impl CharDataRenderer { - pub fn new(sim_id: SimId, memory: &mut MemoryMonitor) -> Self { + pub fn new(sim_id: SimId, memory: &MemoryClient) -> Self { Self { - chardata: memory.view(sim_id, 0x00078000, 0x8000), - brightness: memory.view(sim_id, 0x0005f824, 8), - palettes: memory.view(sim_id, 0x0005f860, 16), + chardata: memory.watch(sim_id, 0x00078000, 0x8000), + brightness: memory.watch(sim_id, 0x0005f824, 8), + palettes: memory.watch(sim_id, 0x0005f860, 16), } } diff --git a/src/window/vram/object.rs b/src/window/vram/object.rs index 0b543d7..6313779 100644 --- a/src/window/vram/object.rs +++ b/src/window/vram/object.rs @@ -1,14 +1,14 @@ use std::sync::Arc; use egui::{ - Align, CentralPanel, Checkbox, Color32, Context, Image, ScrollArea, Slider, TextEdit, + Align, CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextEdit, TextureOptions, Ui, ViewportBuilder, ViewportId, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use crate::{ emulator::SimId, - memory::{MemoryMonitor, MemoryView}, + memory::{MemoryClient, MemoryView}, vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, window::{ utils::{NumberEdit, UiExt as _}, @@ -21,6 +21,7 @@ use super::utils::{self, Object}; pub struct ObjectWindow { sim_id: SimId, loader: Arc, + memory: Arc, objects: MemoryView, index: usize, generic_palette: bool, @@ -29,7 +30,7 @@ pub struct ObjectWindow { } impl ObjectWindow { - pub fn new(sim_id: SimId, memory: &mut MemoryMonitor, vram: &mut VramProcessor) -> Self { + pub fn new(sim_id: SimId, memory: &Arc, vram: &mut VramProcessor) -> Self { let renderer = ObjectRenderer::new(sim_id, memory); let ([zoom, full], params) = vram.add(renderer); let loader = @@ -37,7 +38,8 @@ impl ObjectWindow { Self { sim_id, loader: Arc::new(loader), - objects: memory.view(sim_id, 0x0003e000, 0x2000), + memory: memory.clone(), + objects: memory.watch(sim_id, 0x0003e000, 0x2000), index: 0, generic_palette: false, params, @@ -79,7 +81,7 @@ impl ObjectWindow { .texture_options(TextureOptions::NEAREST); ui.add(image); ui.section("Properties", |ui| { - let object = self.objects.borrow().read::<[u16; 4]>(self.index); + let mut object = self.objects.borrow().read::<[u16; 4]>(self.index); let mut obj = Object::parse(object); TableBuilder::new(ui) .column(Column::remainder()) @@ -90,10 +92,7 @@ impl ObjectWindow { ui.label("Character"); }); row.col(|ui| { - ui.add_enabled( - false, - NumberEdit::new(&mut obj.data.char_index).range(0..2048), - ); + ui.add(NumberEdit::new(&mut obj.data.char_index).range(0..2048)); }); }); body.row(row_height, |mut row| { @@ -101,11 +100,18 @@ impl ObjectWindow { ui.label("Palette"); }); row.col(|ui| { - let mut palette = format!("OBJ {}", obj.data.palette_index); - ui.add_enabled( - false, - TextEdit::singleline(&mut palette).horizontal_align(Align::Max), - ); + ComboBox::from_id_salt("palette") + .selected_text(format!("OBJ {}", obj.data.palette_index)) + .width(ui.available_width()) + .show_ui(ui, |ui| { + for palette in 0..4 { + ui.selectable_value( + &mut obj.data.palette_index, + palette, + format!("OBJ {palette}"), + ); + } + }); }); }); body.row(row_height, |mut row| { @@ -113,7 +119,7 @@ impl ObjectWindow { ui.label("X"); }); row.col(|ui| { - ui.add_enabled(false, NumberEdit::new(&mut obj.x).range(-512..512)); + ui.add(NumberEdit::new(&mut obj.x).range(-512..512)); }); }); body.row(row_height, |mut row| { @@ -121,7 +127,7 @@ impl ObjectWindow { ui.label("Y"); }); row.col(|ui| { - ui.add_enabled(false, NumberEdit::new(&mut obj.y).range(-8..=224)); + ui.add(NumberEdit::new(&mut obj.y).range(-8..=224)); }); }); body.row(row_height, |mut row| { @@ -129,33 +135,30 @@ impl ObjectWindow { ui.label("Parallax"); }); row.col(|ui| { - ui.add_enabled( - false, - NumberEdit::new(&mut obj.parallax).range(-512..512), - ); + ui.add(NumberEdit::new(&mut obj.parallax).range(-512..512)); }); }); body.row(row_height, |mut row| { row.col(|ui| { - let checkbox = Checkbox::new(&mut obj.data.hflip, "H-flip"); - ui.add_enabled(false, checkbox); + ui.add(Checkbox::new(&mut obj.data.hflip, "H-flip")); }); row.col(|ui| { - let checkbox = Checkbox::new(&mut obj.data.vflip, "V-flip"); - ui.add_enabled(false, checkbox); + ui.add(Checkbox::new(&mut obj.data.vflip, "V-flip")); }); }); body.row(row_height, |mut row| { row.col(|ui| { - let checkbox = Checkbox::new(&mut obj.lon, "Left"); - ui.add_enabled(false, checkbox); + ui.add(Checkbox::new(&mut obj.lon, "Left")); }); row.col(|ui| { - let checkbox = Checkbox::new(&mut obj.ron, "Right"); - ui.add_enabled(false, checkbox); + ui.add(Checkbox::new(&mut obj.ron, "Right")); }); }); }); + if obj.update(&mut object) { + let address = 0x3e000 + self.index * 8; + self.memory.write(self.sim_id, address as u32, &object); + } }); ui.section("Display", |ui| { ui.horizontal(|ui| { @@ -254,12 +257,12 @@ struct ObjectRenderer { } impl ObjectRenderer { - pub fn new(sim_id: SimId, memory: &mut MemoryMonitor) -> Self { + pub fn new(sim_id: SimId, memory: &MemoryClient) -> 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), + chardata: memory.watch(sim_id, 0x00078000, 0x8000), + objects: memory.watch(sim_id, 0x0003e000, 0x2000), + brightness: memory.watch(sim_id, 0x0005f824, 8), + palettes: memory.watch(sim_id, 0x0005f860, 16), } } diff --git a/src/window/vram/utils.rs b/src/window/vram/utils.rs index c605302..6b0b469 100644 --- a/src/window/vram/utils.rs +++ b/src/window/vram/utils.rs @@ -39,11 +39,11 @@ pub struct Object { impl Object { pub fn parse(object: [u16; 4]) -> Self { - let x = ((object[0] & 0x3ff) << 6 >> 6) as i16; - let parallax = ((object[1] & 0x3ff) << 6 >> 6) as i16; + let x = ((object[0] & 0x03ff) << 6 >> 6) as i16; + let parallax = ((object[1] & 0x03ff) << 6 >> 6) as i16; let lon = object[1] & 0x8000 != 0; let ron = object[1] & 0x4000 != 0; - let y = (object[2] & 0x0ff) as i16; + let y = (object[2] & 0x00ff) as i16; // Y is stored as the bottom 8 bits of an i16, // so only sign extend if it's out of range. let y = if y > 224 { y << 8 >> 8 } else { y }; @@ -57,6 +57,30 @@ impl Object { data, } } + + pub fn update(&self, source: &mut [u16; 4]) -> bool { + let mut changed = false; + + let new_x = (self.x as u16 & 0x03ff) | (source[0] & 0xfc00); + changed |= source[0] != new_x; + source[0] = new_x; + + let new_p = if self.lon { 0x8000 } else { 0x0000 } + | if self.ron { 0x4000 } else { 0x0000 } + | (self.parallax as u16 & 0x3ff) + | (source[1] & 0x3c00); + changed |= source[1] != new_p; + source[1] = new_p; + + let new_y = (self.y as u16 & 0x00ff) | (source[2] & 0xff00); + changed |= source[2] != new_y; + source[2] = new_y; + + if self.data.update(&mut source[3]) { + changed = true; + } + changed + } } pub struct CellData { @@ -79,6 +103,17 @@ impl CellData { palette_index, } } + + pub fn update(&self, source: &mut u16) -> bool { + let new_value = (self.palette_index as u16) << 14 + | if self.hflip { 0x2000 } else { 0x0000 } + | if self.vflip { 0x1000 } else { 0x0000 } + | (self.char_index as u16 & 0x07ff) + | (*source & 0x0800); + let changed = *source != new_value; + *source = new_value; + changed + } } pub fn read_char_row( -- 2.40.1 From f7cf960b62de9edaacc9078aa72db2fb55d93e89 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 15 Feb 2025 16:14:03 -0500 Subject: [PATCH 18/34] Support hypothetical big-endian users --- src/memory.rs | 85 ++++++++++++++++++++++++++++++++++--- src/window/vram/bgmap.rs | 24 +++++------ src/window/vram/chardata.rs | 12 +++--- src/window/vram/object.rs | 8 ++-- src/window/vram/utils.rs | 4 +- 5 files changed, 100 insertions(+), 33 deletions(-) diff --git a/src/memory.rs b/src/memory.rs index 9b54c8a..769326f 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -39,11 +39,12 @@ impl MemoryClient { MemoryView { region } } - pub fn write(&self, sim: SimId, address: u32, data: &T) { - let data = bytemuck::bytes_of(data).to_vec(); + pub fn write(&self, sim: SimId, address: u32, data: &T) { + let mut buffer = vec![]; + data.to_bytes(&mut buffer); let (tx, _) = oneshot::channel(); self.client - .send_command(EmulatorCommand::WriteMemory(sim, address, data, tx)); + .send_command(EmulatorCommand::WriteMemory(sim, address, buffer, tx)); } } @@ -74,17 +75,87 @@ pub struct MemoryRef<'a> { inner: RwLockReadGuard<'a, BoxBytes>, } +pub trait MemoryValue { + fn from_bytes(bytes: &[u8]) -> Self; + fn to_bytes(&self, buffer: &mut Vec); +} + +macro_rules! primitive_memory_value_impl { + ($T:ty, $L: expr) => { + impl MemoryValue for $T { + #[inline] + fn from_bytes(bytes: &[u8]) -> Self { + let bytes: [u8; std::mem::size_of::<$T>()] = std::array::from_fn(|i| bytes[i]); + <$T>::from_le_bytes(bytes) + } + #[inline] + fn to_bytes(&self, buffer: &mut Vec) { + buffer.extend_from_slice(&self.to_le_bytes()) + } + } + }; +} + +primitive_memory_value_impl!(u8, 1); +primitive_memory_value_impl!(u16, 2); +primitive_memory_value_impl!(u32, 4); + +impl MemoryValue for [T; N] { + #[inline] + fn from_bytes(bytes: &[u8]) -> Self { + std::array::from_fn(|i| { + T::from_bytes(&bytes[i * std::mem::size_of::()..(i + 1) * std::mem::size_of::()]) + }) + } + #[inline] + fn to_bytes(&self, buffer: &mut Vec) { + for item in self { + item.to_bytes(buffer); + } + } +} + +pub struct MemoryIter<'a, T> { + bytes: &'a [u8], + index: usize, + _phantom: std::marker::PhantomData, +} + +impl<'a, T> MemoryIter<'a, T> { + fn new(bytes: &'a [u8]) -> Self { + Self { + bytes, + index: 0, + _phantom: std::marker::PhantomData, + } + } +} + +impl Iterator for MemoryIter<'_, T> { + type Item = T; + + #[inline] + fn next(&mut self) -> Option { + if self.index >= self.bytes.len() { + return None; + } + let bytes = &self.bytes[self.index..self.index + std::mem::size_of::()]; + self.index += std::mem::size_of::(); + Some(T::from_bytes(bytes)) + } +} + impl MemoryRef<'_> { - pub fn read(&self, index: usize) -> T { + pub fn read(&self, index: usize) -> T { let from = index * size_of::(); let to = from + size_of::(); - *bytemuck::from_bytes(&self.inner[from..to]) + T::from_bytes(&self.inner[from..to]) } - pub fn range(&self, start: usize, count: usize) -> &[T] { + pub fn range(&self, start: usize, count: usize) -> MemoryIter { let from = start * size_of::(); let to = from + (count * size_of::()); - bytemuck::cast_slice(&self.inner[from..to]) + MemoryIter::new(&self.inner[from..to]) } } diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index 93bca1a..8760347 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -239,30 +239,26 @@ impl BgMapRenderer { let brightness = self.brightness.borrow(); let palettes = self.palettes.borrow(); - let brts = brightness.range::(0, 8); + let brts = brightness.read::<[u8; 8]>(0); let colors = if generic_palette { [utils::generic_palette(Color32::RED); 4] } else { - [0, 2, 4, 6].map(|i| utils::parse_palette(palettes.read(i), brts, Color32::RED)) + [0, 2, 4, 6].map(|i| utils::parse_palette(palettes.read(i), &brts, Color32::RED)) }; - for (i, cell) in bgmaps - .range::(bgmap_index * 4096, 4096) - .iter() - .enumerate() - { + for (i, cell) in bgmaps.range::(bgmap_index * 4096, 4096).enumerate() { let CellData { char_index, vflip, hflip, palette_index, - } = CellData::parse(*cell); - let char = chardata.range::(char_index * 8, 8); + } = CellData::parse(cell); + let char = chardata.read::<[u16; 8]>(char_index); let palette = &colors[palette_index]; for row in 0..8 { let y = row + (i / 64) * 8; - for (col, pixel) in utils::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]); } @@ -276,7 +272,7 @@ impl BgMapRenderer { let brightness = self.brightness.borrow(); let palettes = self.palettes.borrow(); - let brts = brightness.range::(0, 8); + let brts = brightness.read::<[u8; 8]>(0); let cell = bgmaps.read::(index); @@ -286,15 +282,15 @@ impl BgMapRenderer { hflip, palette_index, } = CellData::parse(cell); - let char = chardata.range::(char_index * 8, 8); + let char = chardata.read::<[u16; 8]>(char_index); let palette = if generic_palette { utils::generic_palette(Color32::RED) } else { - utils::parse_palette(palettes.read(palette_index * 2), brts, Color32::RED) + utils::parse_palette(palettes.read(palette_index * 2), &brts, Color32::RED) }; for row in 0..8 { - for (col, pixel) in utils::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]); } } diff --git a/src/window/vram/chardata.rs b/src/window/vram/chardata.rs index dc102d3..1cde8d4 100644 --- a/src/window/vram/chardata.rs +++ b/src/window/vram/chardata.rs @@ -221,8 +221,8 @@ impl CharacterDataWindow { }; let palette = self.palettes.borrow().read(offset); let brightnesses = self.brightness.borrow(); - let brts = brightnesses.range(0, 8); - utils::parse_palette(palette, brts, Color32::RED) + let brts = brightnesses.read(0); + utils::parse_palette(palette, &brts, Color32::RED) } fn show_chardata(&mut self, ui: &mut Ui) { @@ -321,7 +321,7 @@ impl CharDataRenderer { let palette = self.load_palette(palette); let chardata = self.chardata.borrow(); let character = chardata.range::(index * 8, 8); - for (row, pixels) in character.iter().enumerate() { + for (row, pixels) in character.enumerate() { for col in 0..8 { let char = (pixels >> (col * 2)) & 0x03; image.write((col, row), palette[char as usize]); @@ -332,7 +332,7 @@ impl CharDataRenderer { fn render_character_data(&self, image: &mut VramImage, palette: VramPalette) { let palette = self.load_palette(palette); let chardata = self.chardata.borrow(); - for (row, pixels) in chardata.range::(0, 8 * 2048).iter().enumerate() { + for (row, pixels) in chardata.range::(0, 8 * 2048).enumerate() { let char_index = row / 8; let row_index = row % 8; let x = (char_index % 16) * 8; @@ -350,7 +350,7 @@ impl CharDataRenderer { }; let palette = self.palettes.borrow().read(offset); let brightnesses = self.brightness.borrow(); - let brts = brightnesses.range(0, 8); - utils::parse_palette(palette, brts, Color32::RED) + let brts = brightnesses.read(0); + utils::parse_palette(palette, &brts, Color32::RED) } } diff --git a/src/window/vram/object.rs b/src/window/vram/object.rs index 6313779..8c2aae3 100644 --- a/src/window/vram/object.rs +++ b/src/window/vram/object.rs @@ -282,7 +282,7 @@ impl ObjectRenderer { return; } - let brts = brightness.range::(0, 8); + let brts = brightness.read::<[u8; 8]>(0); let (x, y) = if use_pos { let x = match eye { Eye::Left => obj.x - obj.parallax, @@ -298,11 +298,11 @@ impl ObjectRenderer { Eye::Right => params.right_color, }; - let char = chardata.range::(obj.data.char_index * 8, 8); + let char = chardata.read::<[u16; 8]>(obj.data.char_index); let palette = if params.generic_palette { utils::generic_palette(color) } else { - utils::parse_palette(palettes.read(8 + obj.data.palette_index * 2), brts, color) + utils::parse_palette(palettes.read(8 + obj.data.palette_index * 2), &brts, color) }; for row in 0..8 { @@ -311,7 +311,7 @@ impl ObjectRenderer { continue; } for (col, pixel) in - utils::read_char_row(char, obj.data.hflip, obj.data.vflip, row).enumerate() + utils::read_char_row(&char, obj.data.hflip, obj.data.vflip, row).enumerate() { let real_x = x + col as i16; if !(0..384).contains(&real_x) { diff --git a/src/window/vram/utils.rs b/src/window/vram/utils.rs index 6b0b469..d95319c 100644 --- a/src/window/vram/utils.rs +++ b/src/window/vram/utils.rs @@ -10,7 +10,7 @@ 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] { +pub fn parse_palette(palette: u8, brts: &[u8; 8], color: Color32) -> [Color32; 4] { let shades = [ Color32::BLACK, shade(brts[0], color), @@ -117,7 +117,7 @@ impl CellData { } pub fn read_char_row( - char: &[u16], + char: &[u16; 8], hflip: bool, vflip: bool, row: usize, -- 2.40.1 From cfc08032e6ec77c19906ced0036e04f82f1b2648 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 16 Feb 2025 15:55:21 -0500 Subject: [PATCH 19/34] Implement world viewer with support for OBJ worlds --- src/app.rs | 7 +- src/memory.rs | 28 ++- src/vram.rs | 27 +-- src/window.rs | 2 +- src/window/game.rs | 6 + src/window/vram.rs | 2 + src/window/vram/bgmap.rs | 10 +- src/window/vram/chardata.rs | 6 +- src/window/vram/object.rs | 27 +-- src/window/vram/utils.rs | 33 +-- src/window/vram/world.rs | 409 ++++++++++++++++++++++++++++++++++++ 11 files changed, 497 insertions(+), 60 deletions(-) create mode 100644 src/window/vram/world.rs diff --git a/src/app.rs b/src/app.rs index 7566d91..61408a1 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, ObjectWindow, + InputWindow, ObjectWindow, WorldWindow, }, }; @@ -225,6 +225,10 @@ impl ApplicationHandler for Application { let objects = ObjectWindow::new(sim_id, &self.memory, &mut self.vram); self.open(event_loop, Box::new(objects)); } + UserEvent::OpenWorlds(sim_id) => { + let world = WorldWindow::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()); @@ -486,6 +490,7 @@ pub enum UserEvent { OpenCharacterData(SimId), OpenBgMap(SimId), OpenObjects(SimId), + OpenWorlds(SimId), OpenDebugger(SimId), OpenInput, OpenPlayer2, diff --git a/src/memory.rs b/src/memory.rs index 769326f..d679ad0 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -1,6 +1,7 @@ use std::{ collections::HashMap, fmt::Debug, + iter::FusedIterator, sync::{atomic::AtomicU64, Arc, Mutex, RwLock, RwLockReadGuard, TryLockError, Weak}, }; @@ -117,7 +118,6 @@ impl MemoryValue for [T; N] { pub struct MemoryIter<'a, T> { bytes: &'a [u8], - index: usize, _phantom: std::marker::PhantomData, } @@ -125,7 +125,6 @@ impl<'a, T> MemoryIter<'a, T> { fn new(bytes: &'a [u8]) -> Self { Self { bytes, - index: 0, _phantom: std::marker::PhantomData, } } @@ -136,15 +135,30 @@ impl Iterator for MemoryIter<'_, T> { #[inline] fn next(&mut self) -> Option { - if self.index >= self.bytes.len() { - return None; - } - let bytes = &self.bytes[self.index..self.index + std::mem::size_of::()]; - self.index += std::mem::size_of::(); + let (bytes, rest) = self.bytes.split_at_checked(std::mem::size_of::())?; + self.bytes = rest; + Some(T::from_bytes(bytes)) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let size = self.bytes.len() / std::mem::size_of::(); + (size, Some(size)) + } +} + +impl DoubleEndedIterator for MemoryIter<'_, T> { + fn next_back(&mut self) -> Option { + let mid = self.bytes.len().checked_sub(std::mem::size_of::())?; + // SAFETY: the checked_sub above is effectively a bounds check + let (rest, bytes) = unsafe { self.bytes.split_at_unchecked(mid) }; + self.bytes = rest; Some(T::from_bytes(bytes)) } } +impl FusedIterator for MemoryIter<'_, T> {} + impl MemoryRef<'_> { pub fn read(&self, index: usize) -> T { let from = index * size_of::(); diff --git a/src/vram.rs b/src/vram.rs index be6ee1d..88e5e89 100644 --- a/src/vram.rs +++ b/src/vram.rs @@ -39,6 +39,7 @@ impl VramProcessor { pub fn add + 'static>( &self, renderer: R, + params: R::Params, ) -> ([VramImageHandle; N], VramParams) { let states = renderer.sizes().map(VramRenderImageState::new); let handles = states.clone().map(|state| VramImageHandle { @@ -48,7 +49,7 @@ impl VramProcessor { let images = renderer .sizes() .map(|[width, height]| VramImage::new(width, height)); - let sink = Arc::new(Mutex::new(R::Params::default())); + let sink = Arc::new(Mutex::new(params.clone())); let _ = self.sender.send(Box::new(VramRendererWrapper { renderer, params: Arc::downgrade(&sink), @@ -56,7 +57,7 @@ impl VramProcessor { states, })); let params = VramParams { - value: R::Params::default(), + value: params, sink, }; (handles, params) @@ -100,32 +101,32 @@ impl VramProcessorWorker { } pub struct VramImage { - size: [usize; 2], - shades: Vec, + pub size: [usize; 2], + pub pixels: Vec, } impl VramImage { pub fn new(width: usize, height: usize) -> Self { Self { size: [width, height], - shades: vec![Color32::BLACK; width * height], + pixels: vec![Color32::BLACK; width * height], } } pub fn clear(&mut self) { - for shade in self.shades.iter_mut() { - *shade = Color32::BLACK; + for pixel in self.pixels.iter_mut() { + *pixel = Color32::BLACK; } } pub fn write(&mut self, coords: (usize, usize), pixel: Color32) { - self.shades[coords.1 * self.size[0] + coords.0] = pixel; + self.pixels[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( + let old = self.pixels[index]; + self.pixels[index] = Color32::from_rgb( old.r() + pixel.r(), old.g() + pixel.g(), old.b() + pixel.b(), @@ -133,11 +134,11 @@ impl VramImage { } pub fn changed(&self, image: &ColorImage) -> bool { - image.pixels.iter().zip(&self.shades).any(|(a, b)| a != b) + image.pixels.iter().zip(&self.pixels).any(|(a, b)| a != b) } pub fn read(&self, image: &mut ColorImage) { - image.pixels.copy_from_slice(&self.shades); + image.pixels.copy_from_slice(&self.pixels); } } @@ -176,7 +177,7 @@ impl VramParams { } pub trait VramRenderer: Send { - type Params: Clone + Default + Send; + type Params: Clone + Send; fn sizes(&self) -> [[usize; 2]; N]; fn render(&mut self, params: &Self::Params, images: &mut [VramImage; N]); } diff --git a/src/window.rs b/src/window.rs index fd7cfe1..80d5179 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}; +pub use vram::{BgMapWindow, CharacterDataWindow, ObjectWindow, WorldWindow}; use winit::event::KeyEvent; use crate::emulator::SimId; diff --git a/src/window/game.rs b/src/window/game.rs index 256fc78..36ac27f 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -150,6 +150,12 @@ impl GameWindow { .unwrap(); ui.close_menu(); } + if ui.button("Worlds").clicked() { + self.proxy + .send_event(UserEvent::OpenWorlds(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 ad93bef..f1acc3a 100644 --- a/src/window/vram.rs +++ b/src/window/vram.rs @@ -2,7 +2,9 @@ mod bgmap; mod chardata; mod object; mod utils; +mod world; pub use bgmap::*; pub use chardata::*; pub use object::*; +pub use world::*; diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index 8760347..47fa87e 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -33,7 +33,7 @@ pub struct BgMapWindow { impl BgMapWindow { pub fn new(sim_id: SimId, memory: &Arc, vram: &mut VramProcessor) -> Self { let renderer = BgMapRenderer::new(sim_id, memory); - let ([cell, bgmap], params) = vram.add(renderer); + let ([cell, bgmap], params) = vram.add(renderer, BgMapParams::default()); let loader = VramTextureLoader::new([("vram://cell".into(), cell), ("vram://bgmap".into(), bgmap)]); Self { @@ -41,8 +41,8 @@ impl BgMapWindow { loader: Arc::new(loader), memory: memory.clone(), bgmaps: memory.watch(sim_id, 0x00020000, 0x1d800), - cell_index: 0, - generic_palette: false, + cell_index: params.cell_index, + generic_palette: params.generic_palette, params, scale: 1.0, show_grid: false, @@ -243,7 +243,7 @@ impl BgMapRenderer { let colors = if generic_palette { [utils::generic_palette(Color32::RED); 4] } else { - [0, 2, 4, 6].map(|i| utils::parse_palette(palettes.read(i), &brts, Color32::RED)) + [0, 2, 4, 6].map(|i| utils::palette_colors(palettes.read(i), &brts, Color32::RED)) }; for (i, cell) in bgmaps.range::(bgmap_index * 4096, 4096).enumerate() { @@ -286,7 +286,7 @@ impl BgMapRenderer { let palette = if generic_palette { utils::generic_palette(Color32::RED) } else { - utils::parse_palette(palettes.read(palette_index * 2), &brts, Color32::RED) + utils::palette_colors(palettes.read(palette_index * 2), &brts, Color32::RED) }; for row in 0..8 { diff --git a/src/window/vram/chardata.rs b/src/window/vram/chardata.rs index 1cde8d4..10c14d7 100644 --- a/src/window/vram/chardata.rs +++ b/src/window/vram/chardata.rs @@ -94,7 +94,7 @@ pub struct CharacterDataWindow { impl CharacterDataWindow { pub fn new(sim_id: SimId, memory: &MemoryClient, vram: &mut VramProcessor) -> Self { let renderer = CharDataRenderer::new(sim_id, memory); - let ([char, chardata], params) = vram.add(renderer); + let ([char, chardata], params) = vram.add(renderer, CharDataParams::default()); let loader = VramTextureLoader::new([ ("vram://char".into(), char), ("vram://chardata".into(), chardata), @@ -222,7 +222,7 @@ impl CharacterDataWindow { let palette = self.palettes.borrow().read(offset); let brightnesses = self.brightness.borrow(); let brts = brightnesses.read(0); - utils::parse_palette(palette, &brts, Color32::RED) + utils::palette_colors(palette, &brts, Color32::RED) } fn show_chardata(&mut self, ui: &mut Ui) { @@ -351,6 +351,6 @@ impl CharDataRenderer { let palette = self.palettes.borrow().read(offset); let brightnesses = self.brightness.borrow(); let brts = brightnesses.read(0); - utils::parse_palette(palette, &brts, Color32::RED) + utils::palette_colors(palette, &brts, Color32::RED) } } diff --git a/src/window/vram/object.rs b/src/window/vram/object.rs index 8c2aae3..90c200e 100644 --- a/src/window/vram/object.rs +++ b/src/window/vram/object.rs @@ -31,8 +31,14 @@ pub struct ObjectWindow { impl ObjectWindow { pub fn new(sim_id: SimId, memory: &Arc, vram: &mut VramProcessor) -> Self { + let initial_params = ObjectParams { + index: 0, + generic_palette: false, + left_color: Color32::from_rgb(0xff, 0x00, 0x00), + right_color: Color32::from_rgb(0x00, 0xc6, 0xf0), + }; let renderer = ObjectRenderer::new(sim_id, memory); - let ([zoom, full], params) = vram.add(renderer); + let ([zoom, full], params) = vram.add(renderer, initial_params); let loader = VramTextureLoader::new([("vram://zoom".into(), zoom), ("vram://full".into(), full)]); Self { @@ -40,8 +46,8 @@ impl ObjectWindow { loader: Arc::new(loader), memory: memory.clone(), objects: memory.watch(sim_id, 0x0003e000, 0x2000), - index: 0, - generic_palette: false, + index: params.index, + generic_palette: params.generic_palette, params, scale: 1.0, } @@ -175,7 +181,7 @@ impl ObjectWindow { self.params.write(ObjectParams { index: self.index, generic_palette: self.generic_palette, - ..ObjectParams::default() + ..*self.params }); } @@ -233,17 +239,6 @@ struct ObjectParams { 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, @@ -302,7 +297,7 @@ impl ObjectRenderer { let palette = if params.generic_palette { utils::generic_palette(color) } else { - utils::parse_palette(palettes.read(8 + obj.data.palette_index * 2), &brts, color) + utils::palette_colors(palettes.read(8 + obj.data.palette_index * 2), &brts, color) }; for row in 0..8 { diff --git a/src/window/vram/utils.rs b/src/window/vram/utils.rs index d95319c..9cbfd4f 100644 --- a/src/window/vram/utils.rs +++ b/src/window/vram/utils.rs @@ -10,24 +10,29 @@ pub fn generic_palette(color: Color32) -> [Color32; 4] { GENERIC_PALETTE.map(|brt| shade(brt, color)) } -pub fn parse_palette(palette: u8, brts: &[u8; 8], color: Color32) -> [Color32; 4] { - let shades = [ - Color32::BLACK, - shade(brts[0], color), - shade(brts[2], color), - shade( - brts[0].saturating_add(brts[2]).saturating_add(brts[4]), - color, - ), - ]; +pub const fn parse_palette(palette: u8) -> [u8; 4] { [ - Color32::BLACK, - shades[(palette >> 2) as usize & 0x03], - shades[(palette >> 4) as usize & 0x03], - shades[(palette >> 6) as usize & 0x03], + 0, + (palette >> 2) & 0x03, + (palette >> 4) & 0x03, + (palette >> 6) & 0x03, ] } +pub const fn parse_shades(brts: &[u8; 8]) -> [u8; 4] { + [ + 0, + brts[0], + brts[2], + brts[0].saturating_add(brts[2]).saturating_add(brts[4]), + ] +} + +pub fn palette_colors(palette: u8, brts: &[u8; 8], color: Color32) -> [Color32; 4] { + let colors = parse_shades(brts).map(|s| shade(s, color)); + parse_palette(palette).map(|p| colors[p as usize]) +} + pub struct Object { pub x: i16, pub lon: bool, diff --git a/src/window/vram/world.rs b/src/window/vram/world.rs new file mode 100644 index 0000000..6bf78e6 --- /dev/null +++ b/src/window/vram/world.rs @@ -0,0 +1,409 @@ +use std::{fmt::Display, sync::Arc}; + +use egui::{ + CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextureOptions, + Ui, ViewportBuilder, ViewportId, +}; +use egui_extras::{Column, Size, StripBuilder, TableBuilder}; +use num_derive::{FromPrimitive, ToPrimitive}; +use num_traits::FromPrimitive; + +use crate::{ + emulator::SimId, + memory::{MemoryClient, MemoryView}, + vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, + window::{ + utils::{NumberEdit, UiExt as _}, + AppWindow, + }, +}; + +use super::utils::{self, shade, Object}; + +pub struct WorldWindow { + sim_id: SimId, + loader: Arc, + worlds: MemoryView, + index: usize, + generic_palette: bool, + params: VramParams, + scale: f32, +} + +impl WorldWindow { + pub fn new(sim_id: SimId, memory: &MemoryClient, vram: &mut VramProcessor) -> Self { + let initial_params = WorldParams { + index: 31, + generic_palette: false, + left_color: Color32::from_rgb(0xff, 0x00, 0x00), + right_color: Color32::from_rgb(0x00, 0xc6, 0xf0), + }; + let renderer = WorldRenderer::new(sim_id, memory); + let ([world], params) = vram.add(renderer, initial_params); + let loader = VramTextureLoader::new([("vram://world".into(), world)]); + Self { + sim_id, + loader: Arc::new(loader), + worlds: memory.watch(sim_id, 0x3d800, 0x400), + index: params.index, + generic_palette: params.generic_palette, + 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..32)); + }); + }); + }); + let data = { + let worlds = self.worlds.borrow(); + worlds.read(self.index) + }; + let mut world = World::parse(&data); + ui.section("Properties", |ui| { + TableBuilder::new(ui) + .column(Column::remainder()) + .column(Column::remainder()) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Mode"); + }); + row.col(|ui| { + ComboBox::from_id_salt("mode") + .selected_text(world.header.mode.to_string()) + .width(ui.available_width()) + .show_ui(ui, |ui| { + for mode in WorldMode::values() { + ui.selectable_value( + &mut world.header.mode, + mode, + mode.to_string(), + ); + } + }); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.add(Checkbox::new(&mut world.header.lon, "Left")); + }); + row.col(|ui| { + ui.add(Checkbox::new(&mut world.header.ron, "Right")); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.add(Checkbox::new(&mut world.header.end, "End")); + }); + row.col(|ui| { + ui.add(Checkbox::new(&mut world.header.over, "Overplane")); + }); + }); + }); + }); + 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(WorldParams { + index: self.index, + generic_palette: self.generic_palette, + ..*self.params + }); + } + + fn show_world(&mut self, ui: &mut Ui) { + let image = Image::new("vram://world") + .fit_to_original_size(self.scale) + .texture_options(TextureOptions::NEAREST); + ui.add(image); + } +} + +impl AppWindow for WorldWindow { + fn viewport_id(&self) -> ViewportId { + ViewportId::from_hash_of(format!("world-{}", self.sim_id)) + } + + fn sim_id(&self) -> SimId { + self.sim_id + } + + fn initial_viewport(&self) -> ViewportBuilder { + ViewportBuilder::default() + .with_title(format!("Worlds ({})", 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_world(ui)); + }); + }); + }); + }); + } +} + +#[derive(Clone, PartialEq, Eq)] +struct WorldParams { + index: usize, + generic_palette: bool, + left_color: Color32, + right_color: Color32, +} + +struct WorldRenderer { + chardata: MemoryView, + objects: MemoryView, + worlds: MemoryView, + brightness: MemoryView, + palettes: MemoryView, + scr: MemoryView, + // an object world could update the same pixel more than once, + // so we can't add the left/right eye color to the output buffer directly. + buffer: Box<[[u8; 2]; 384 * 224]>, +} + +impl WorldRenderer { + pub fn new(sim_id: SimId, memory: &MemoryClient) -> Self { + Self { + chardata: memory.watch(sim_id, 0x00078000, 0x8000), + objects: memory.watch(sim_id, 0x0003e000, 0x2000), + worlds: memory.watch(sim_id, 0x0003d800, 0x400), + brightness: memory.watch(sim_id, 0x0005f824, 8), + palettes: memory.watch(sim_id, 0x0005f860, 16), + scr: memory.watch(sim_id, 0x0005f848, 8), + + buffer: vec![[0, 0]; 384 * 224] + .into_boxed_slice() + .try_into() + .unwrap(), + } + } + + fn render_object_world(&mut self, group: usize, params: &WorldParams, image: &mut VramImage) { + for cell in self.buffer.iter_mut() { + *cell = [0, 0]; + } + + let palettes = { + let palettes = self.palettes.borrow().read::<[u8; 8]>(1); + [ + utils::parse_palette(palettes[0]), + utils::parse_palette(palettes[2]), + utils::parse_palette(palettes[4]), + utils::parse_palette(palettes[6]), + ] + }; + let chardata = self.chardata.borrow(); + let objects = self.objects.borrow(); + + let (first_range, second_range) = { + let scr = self.scr.borrow(); + let start = if group == 0 { + 0 + } else { + scr.read::(group - 1).wrapping_add(1) as usize & 0x03ff + }; + let end = scr.read::(group).wrapping_add(1) as usize & 0x03ff; + if start > end { + ((start, 1024 - start), (end != 0).then_some((0, end))) + } else { + ((start, end - start), None) + } + }; + + // Fill the buffer + for object in std::iter::once(first_range) + .chain(second_range) + .flat_map(|(start, count)| objects.range(start, count)) + .rev() + { + let obj = Object::parse(object); + if !obj.lon && !obj.ron { + continue; + } + + let char = chardata.read::<[u16; 8]>(obj.data.char_index); + let palette = &palettes[obj.data.palette_index]; + + for row in 0..8 { + let y = obj.y + row as i16; + if !(0..224).contains(&y) { + continue; + } + for (col, pixel) in + utils::read_char_row(&char, obj.data.hflip, obj.data.vflip, row).enumerate() + { + if pixel == 0 { + // transparent + continue; + } + let lx = obj.x + col as i16 - obj.parallax; + if obj.lon && (0..384).contains(&lx) { + let index = (y as usize) * 384 + lx as usize; + self.buffer[index][0] = palette[pixel as usize]; + } + let rx = obj.x + col as i16 + obj.parallax; + if obj.ron & (0..384).contains(&rx) { + let index = (y as usize) * 384 + rx as usize; + self.buffer[index][1] = palette[pixel as usize]; + } + } + } + } + + 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| shade(s, params.left_color)), + shades.map(|s| shade(s, params.right_color)), + ] + }; + + for (dst, shades) in image.pixels.iter_mut().zip(self.buffer.iter()) { + let left = colors[0][shades[0] as usize]; + let right = colors[1][shades[1] as usize]; + *dst = Color32::from_rgb( + left.r() + right.r(), + left.g() + right.g(), + left.b() + right.b(), + ) + } + } +} + +impl VramRenderer<1> for WorldRenderer { + type Params = WorldParams; + + 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 worlds = self.worlds.borrow(); + let header = WorldHeader::parse(worlds.read(params.index * 16)); + if header.end || (!header.lon && header.ron) { + image.clear(); + return; + } + + if header.mode == WorldMode::Object { + let mut group = 3usize; + for world in params.index + 1..32 { + let header = WorldHeader::parse(worlds.read(world * 16)); + if header.mode == WorldMode::Object && (header.lon || header.ron) { + group = group.checked_sub(1).unwrap_or(3); + } + } + drop(worlds); + self.render_object_world(group, params, image); + } + } +} + +struct World { + header: WorldHeader, +} + +impl World { + pub fn parse(data: &[u16; 16]) -> Self { + Self { + header: WorldHeader::parse(data[0]), + } + } +} + +struct WorldHeader { + lon: bool, + ron: bool, + mode: WorldMode, + over: bool, + end: bool, +} + +impl WorldHeader { + fn parse(data: u16) -> Self { + let lon = data & 0x8000 != 0; + let ron = data & 0x4000 != 0; + let mode = WorldMode::from_u16((data >> 12) & 0x3).unwrap(); + let over = data & 0x0080 != 0; + let end = data & 0x0040 != 0; + Self { + lon, + ron, + mode, + over, + end, + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] +enum WorldMode { + Normal = 0, + HBias = 1, + Affine = 2, + Object = 3, +} + +impl WorldMode { + fn values() -> [Self; 4] { + [Self::Normal, Self::HBias, Self::Affine, Self::Object] + } +} + +impl Display for WorldMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Normal => "Normal", + Self::HBias => "H-bias", + Self::Affine => "Affine", + Self::Object => "Object", + }) + } +} -- 2.40.1 From 2a4599756c2d25de13d3140093df510e11047c39 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 17 Feb 2025 00:20:12 -0500 Subject: [PATCH 20/34] View normal worlds --- src/window/vram/bgmap.rs | 6 +- src/window/vram/utils.rs | 6 + src/window/vram/world.rs | 343 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 347 insertions(+), 8 deletions(-) diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index 47fa87e..3b0f39f 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -40,7 +40,7 @@ impl BgMapWindow { sim_id, loader: Arc::new(loader), memory: memory.clone(), - bgmaps: memory.watch(sim_id, 0x00020000, 0x1d800), + bgmaps: memory.watch(sim_id, 0x00020000, 0x20000), cell_index: params.cell_index, generic_palette: params.generic_palette, params, @@ -62,7 +62,7 @@ impl BgMapWindow { }); row.col(|ui| { let mut bgmap_index = self.cell_index / 4096; - ui.add(NumberEdit::new(&mut bgmap_index).range(0..14)); + ui.add(NumberEdit::new(&mut bgmap_index).range(0..16)); if bgmap_index != self.cell_index / 4096 { self.cell_index = (bgmap_index * 4096) + (self.cell_index % 4096); } @@ -227,7 +227,7 @@ impl BgMapRenderer { pub fn new(sim_id: SimId, memory: &MemoryClient) -> Self { Self { chardata: memory.watch(sim_id, 0x00078000, 0x8000), - bgmaps: memory.watch(sim_id, 0x00020000, 0x1d800), + bgmaps: memory.watch(sim_id, 0x00020000, 0x20000), brightness: memory.watch(sim_id, 0x0005f824, 8), palettes: memory.watch(sim_id, 0x0005f860, 16), } diff --git a/src/window/vram/utils.rs b/src/window/vram/utils.rs index 9cbfd4f..98154da 100644 --- a/src/window/vram/utils.rs +++ b/src/window/vram/utils.rs @@ -134,6 +134,12 @@ pub fn read_char_row( }) } +pub fn read_char_pixel(char: &[u16; 8], hflip: bool, vflip: bool, row: usize, col: usize) -> u8 { + let pixels = if vflip { char[7 - row] } else { char[row] }; + let pixel = if hflip { 7 - col } else { col } << 1; + ((pixels >> pixel) & 0x3) as u8 +} + pub struct CharacterGrid<'a> { source: ImageSource<'a>, scale: f32, diff --git a/src/window/vram/world.rs b/src/window/vram/world.rs index 6bf78e6..18803cf 100644 --- a/src/window/vram/world.rs +++ b/src/window/vram/world.rs @@ -1,8 +1,8 @@ use std::{fmt::Display, sync::Arc}; use egui::{ - CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextureOptions, - Ui, ViewportBuilder, ViewportId, + Align, CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextEdit, + TextureOptions, Ui, ViewportBuilder, ViewportId, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use num_derive::{FromPrimitive, ToPrimitive}; @@ -10,7 +10,7 @@ use num_traits::FromPrimitive; use crate::{ emulator::SimId, - memory::{MemoryClient, MemoryView}, + memory::{MemoryClient, MemoryRef, MemoryView}, vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, window::{ utils::{NumberEdit, UiExt as _}, @@ -18,7 +18,7 @@ use crate::{ }, }; -use super::utils::{self, shade, Object}; +use super::utils::{self, shade, CellData, Object}; pub struct WorldWindow { sim_id: SimId, @@ -67,6 +67,19 @@ impl WorldWindow { ui.add(NumberEdit::new(&mut self.index).range(0..32)); }); }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Address"); + }); + row.col(|ui| { + let address = 0x3d800 + self.index * 32; + let mut address_str = format!("{address:08x}"); + ui.add_enabled( + false, + TextEdit::singleline(&mut address_str).horizontal_align(Align::Max), + ); + }); + }); }); let data = { let worlds = self.worlds.borrow(); @@ -78,6 +91,118 @@ impl WorldWindow { .column(Column::remainder()) .column(Column::remainder()) .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Map base"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut world.header.base).range(0..16)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("BG width"); + }); + row.col(|ui| { + let widths = ["1", "2", "4", "8"]; + ComboBox::from_id_salt("width") + .selected_text(widths[world.header.scx as usize]) + .width(ui.available_width()) + .show_ui(ui, |ui| { + for (value, label) in widths.into_iter().enumerate() { + ui.selectable_value( + &mut world.header.scx, + value as u32, + label, + ); + } + }); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("BG height"); + }); + row.col(|ui| { + let heights = ["1", "2", "4", "8"]; + ComboBox::from_id_salt("height") + .selected_text(heights[world.header.scy as usize]) + .width(ui.available_width()) + .show_ui(ui, |ui| { + for (value, label) in heights.into_iter().enumerate() { + ui.selectable_value( + &mut world.header.scy, + value as u32, + label, + ); + } + }); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Dest X"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut world.dst_x).range(-512..512)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Dest Y"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut world.dst_y)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Dest parallax"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut world.dst_parallax).range(-512..512)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Src X"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut world.src_x).range(-4096..4096)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Src Y"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut world.src_y).range(-16384..16384)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Src parallax"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut world.src_parallax).range(-4096..4096)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Width"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut world.width).range(-4096..4096)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Height"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut world.height)); + }); + }); body.row(row_height, |mut row| { row.col(|ui| { ui.label("Mode"); @@ -97,6 +222,28 @@ impl WorldWindow { }); }); }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Params"); + }); + row.col(|ui| { + let address = 0x00020000 + world.param_base * 2; + 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("Overplane"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut world.overplane).range(0..65536)); + }); + }); body.row(row_height, |mut row| { row.col(|ui| { ui.add(Checkbox::new(&mut world.header.lon, "Left")); @@ -154,7 +301,7 @@ impl AppWindow for WorldWindow { fn initial_viewport(&self) -> ViewportBuilder { ViewportBuilder::default() .with_title(format!("Worlds ({})", self.sim_id)) - .with_inner_size((640.0, 480.0)) + .with_inner_size((640.0, 500.0)) } fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) { @@ -190,6 +337,7 @@ struct WorldParams { struct WorldRenderer { chardata: MemoryView, + bgmaps: MemoryView, objects: MemoryView, worlds: MemoryView, brightness: MemoryView, @@ -204,6 +352,7 @@ impl WorldRenderer { pub fn new(sim_id: SimId, memory: &MemoryClient) -> Self { Self { chardata: memory.watch(sim_id, 0x00078000, 0x8000), + bgmaps: memory.watch(sim_id, 0x00020000, 0x20000), objects: memory.watch(sim_id, 0x0003e000, 0x2000), worlds: memory.watch(sim_id, 0x0003d800, 0x400), brightness: memory.watch(sim_id, 0x0005f824, 8), @@ -313,6 +462,86 @@ impl WorldRenderer { ) } } + + fn render_world(&mut self, world: World, params: &WorldParams, image: &mut VramImage) { + image.clear(); + + let height = if world.header.mode == WorldMode::Affine { + world.height.max(8) + } else { + world.height + }; + + let dx1 = world.dst_x; + let dx2 = dx1 + world.width; + if dx1 - world.dst_parallax > 384 || dx2 + world.dst_parallax < 0 { + return; + } + let dy1 = world.dst_y; + let dy2 = dy1 + height; + if dy1 > 224 || dy2 < 0 { + return; + } + + 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| shade(s, params.left_color)), + shades.map(|s| shade(s, params.right_color)), + ] + }; + + let mut chars = CharCache::new(self.chardata.borrow()); + let mut cells = CellCache::new(self.bgmaps.borrow()); + + for y in 0..height { + let dy = y + world.dst_y; + if !(0..224).contains(&dy) { + continue; + } + let sy = y + world.src_y; + + // left side + for x in 0..world.width { + let dx = x + world.dst_x - world.dst_parallax; + if !(0..384).contains(&dx) { + continue; + } + let sx = x + world.src_x - world.src_parallax; + + let cell_index = world.source_cell(sx, sy); + let cell = cells.get(cell_index); + let char = chars.get(cell.char_index); + let row = (sy & 0x7) as usize; + let col = (sx & 0x7) as usize; + let pixel = utils::read_char_pixel(char, cell.hflip, cell.vflip, row, col); + image.add((dx as usize, dy as usize), colors[0][pixel as usize]); + } + + // right side + for x in 0..world.width { + let dx = x + world.dst_x + world.dst_parallax; + if !(0..384).contains(&dx) { + continue; + } + let sx = x + world.src_x + world.src_parallax; + + let cell_index = world.source_cell(sx, sy); + let cell = cells.get(cell_index); + let char = chars.get(cell.char_index); + let row = (sy & 0x7) as usize; + let col = (sx & 0x7) as usize; + let pixel = utils::read_char_pixel(char, cell.hflip, cell.vflip, row, col); + image.add((dx as usize, dy as usize), colors[1][pixel as usize]); + } + } + } } impl VramRenderer<1> for WorldRenderer { @@ -342,28 +571,77 @@ impl VramRenderer<1> for WorldRenderer { } drop(worlds); self.render_object_world(group, params, image); + } else { + let world = World::parse(&worlds.read(params.index)); + drop(worlds); + self.render_world(world, params, image); } } } struct World { header: WorldHeader, + dst_x: i16, + dst_parallax: i16, + dst_y: i16, + src_x: i16, + src_parallax: i16, + src_y: i16, + width: i16, + height: i16, + param_base: usize, + overplane: usize, } impl World { pub fn parse(data: &[u16; 16]) -> Self { Self { header: WorldHeader::parse(data[0]), + dst_x: (data[1] as i16) << 6 >> 6, + dst_parallax: (data[2] as i16) << 6 >> 6, + dst_y: data[3] as i16, + src_x: (data[4] as i16) << 3 >> 3, + src_parallax: (data[5] as i16) << 1 >> 1, + src_y: (data[6] as i16) << 3 >> 3, + width: 1 + ((data[7] as i16) << 3 >> 3), + height: 1 + data[8] as i16, + param_base: data[9] as usize, + overplane: data[10] as usize, } } + + fn source_cell(&self, sx: i16, sy: i16) -> usize { + if self.header.over { + let bg_width = 1 << self.header.scx << 9; + let bg_height = 1 << self.header.scy << 9; + if !(0..bg_width).contains(&sx) || !(0..bg_height).contains(&sy) { + return self.overplane; + } + } + + let scx = 1 << self.header.scx.min(3 - self.header.scy); + let scy = 1 << self.header.scy; + let map_x = ((sx >> 9) & (scx - 1)) as usize; + let map_y = ((sy >> 9) & (scy - 1)) as usize; + let map_index = self.header.base + (map_y * scx as usize) + map_x; + + let cell_x = (sx >> 3) as usize & 0x3f; + let cell_y = (sy >> 3) as usize & 0x3f; + let cell_index = (cell_y * 64) + cell_x; + + (map_index << 12) + cell_index + } } struct WorldHeader { lon: bool, ron: bool, mode: WorldMode, + scx: u32, + scy: u32, over: bool, end: bool, + base: usize, } impl WorldHeader { @@ -371,14 +649,20 @@ impl WorldHeader { let lon = data & 0x8000 != 0; let ron = data & 0x4000 != 0; let mode = WorldMode::from_u16((data >> 12) & 0x3).unwrap(); + let scx = (data >> 10) as u32 & 0x03; + let scy = (data >> 10) as u32 & 0x03; let over = data & 0x0080 != 0; let end = data & 0x0040 != 0; + let base = (data & 0x000f) as usize; Self { lon, ron, mode, + scx, + scy, over, end, + base, } } } @@ -407,3 +691,52 @@ impl Display for WorldMode { }) } } + +struct CellCache<'a> { + bgmaps: MemoryRef<'a>, + index: usize, + cell: CellData, +} + +impl<'a> CellCache<'a> { + fn new(bgmaps: MemoryRef<'a>) -> Self { + Self { + bgmaps, + index: 0x10000, + cell: CellData::parse(0), + } + } + + fn get(&mut self, index: usize) -> &CellData { + if self.index != index { + let data = self.bgmaps.read(index); + self.cell = CellData::parse(data); + self.index = index; + } + &self.cell + } +} + +struct CharCache<'a> { + chardata: MemoryRef<'a>, + index: usize, + char: [u16; 8], +} + +impl<'a> CharCache<'a> { + fn new(chardata: MemoryRef<'a>) -> Self { + Self { + chardata, + index: 2048, + char: [0; 8], + } + } + + fn get(&mut self, index: usize) -> &[u16; 8] { + if self.index != index { + self.char = self.chardata.read(index); + self.index = index; + } + &self.char + } +} -- 2.40.1 From 3cdc0583a64e27b109bc5b34bff7947406cfd068 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 17 Feb 2025 13:52:04 -0500 Subject: [PATCH 21/34] View hbias worlds --- src/memory.rs | 3 + src/window/utils.rs | 13 ++- src/window/vram/world.rs | 198 ++++++++++++++++++++++++++++++++------- 3 files changed, 175 insertions(+), 39 deletions(-) diff --git a/src/memory.rs b/src/memory.rs index d679ad0..30b1f00 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -98,8 +98,11 @@ macro_rules! primitive_memory_value_impl { } primitive_memory_value_impl!(u8, 1); +primitive_memory_value_impl!(i8, 2); primitive_memory_value_impl!(u16, 2); +primitive_memory_value_impl!(i16, 2); primitive_memory_value_impl!(u32, 4); +primitive_memory_value_impl!(i32, 2); impl MemoryValue for [T; N] { #[inline] diff --git a/src/window/utils.rs b/src/window/utils.rs index ec3fe98..d53f6b4 100644 --- a/src/window/utils.rs +++ b/src/window/utils.rs @@ -34,10 +34,11 @@ pub trait UiExt { impl UiExt for Ui { fn section(&mut self, title: impl Into, add_contents: impl FnOnce(&mut Ui)) { + let title: String = title.into(); let mut frame = Frame::group(self.style()); frame.outer_margin.top += 10.0; frame.inner_margin.top += 2.0; - let res = frame.show(self, add_contents); + let res = self.push_id(&title, |ui| frame.show(ui, add_contents)); let text = RichText::new(title).background_color(self.style().visuals.panel_fill); let old_rect = res.response.rect; let mut text_rect = old_rect; @@ -133,12 +134,14 @@ impl<'a, T: Number> NumberEdit<'a, T> { impl Widget for NumberEdit<'_, T> { fn ui(self, ui: &mut Ui) -> Response { + let id = ui.id(); + let (last_value, mut str, focus) = ui.memory(|m| { let (lv, s) = m .data - .get_temp(ui.id()) + .get_temp(id) .unwrap_or((*self.value, self.value.to_string())); - let focus = m.has_focus(ui.id()); + let focus = m.has_focus(id); (lv, s, focus) }); let mut stale = false; @@ -174,7 +177,7 @@ impl Widget for NumberEdit<'_, T> { } let text = TextEdit::singleline(&mut str) .horizontal_align(Align::Max) - .id(ui.id()) + .id(id) .margin(Margin { left: 4.0, right: 20.0, @@ -242,7 +245,7 @@ impl Widget for NumberEdit<'_, T> { stale = true; } if stale { - ui.memory_mut(|m| m.data.insert_temp(ui.id(), (*self.value, str))); + ui.memory_mut(|m| m.data.insert_temp(id, (*self.value, str))); } res } diff --git a/src/window/vram/world.rs b/src/window/vram/world.rs index 18803cf..f9e9a61 100644 --- a/src/window/vram/world.rs +++ b/src/window/vram/world.rs @@ -24,7 +24,9 @@ pub struct WorldWindow { sim_id: SimId, loader: Arc, worlds: MemoryView, + bgmaps: MemoryView, index: usize, + param_index: usize, generic_palette: bool, params: VramParams, scale: f32, @@ -45,7 +47,9 @@ impl WorldWindow { sim_id, loader: Arc::new(loader), worlds: memory.watch(sim_id, 0x3d800, 0x400), + bgmaps: memory.watch(sim_id, 0x00020000, 0x20000), index: params.index, + param_index: 0, generic_palette: params.generic_palette, params, scale: 1.0, @@ -262,6 +266,57 @@ impl WorldWindow { }); }); }); + if world.header.mode == WorldMode::HBias { + ui.section("H-bias", |ui| { + TableBuilder::new(ui) + .column(Column::remainder()) + .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.param_index).range(0..32)); + }); + }); + let base = world.param_base + self.param_index * 2; + let mut param = HBiasParam::load(&self.bgmaps.borrow(), base); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Address"); + }); + row.col(|ui| { + let address = 0x00020000 + base * 2; + 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("Left"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut param.left).range(-4096..4096)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Right"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut param.right).range(-4096..4096)); + }); + }); + }); + }); + } else { + self.param_index = 0; + } ui.section("Display", |ui| { ui.horizontal(|ui| { ui.label("Scale"); @@ -497,48 +552,44 @@ impl WorldRenderer { ] }; - let mut chars = CharCache::new(self.chardata.borrow()); - let mut cells = CellCache::new(self.bgmaps.borrow()); + let chardata = self.chardata.borrow(); + let bgmaps = self.bgmaps.borrow(); + let mut chars = [CharCache::new(&chardata), CharCache::new(&chardata)]; + let mut cells = [CellCache::new(&bgmaps), CellCache::new(&bgmaps)]; + let mut source = SourceCoordCalculator::new(&bgmaps, &world); for y in 0..height { let dy = y + world.dst_y; if !(0..224).contains(&dy) { continue; } - let sy = y + world.src_y; - // left side for x in 0..world.width { let dx = x + world.dst_x - world.dst_parallax; - if !(0..384).contains(&dx) { - continue; + if world.header.lon && (0..384).contains(&dx) { + let (sx, sy) = source.left(x, y); + + let cell_index = world.source_cell(sx, sy); + let cell = cells[0].get(cell_index); + let char = chars[0].get(cell.char_index); + let row = (sy & 0x7) as usize; + let col = (sx & 0x7) as usize; + let pixel = utils::read_char_pixel(char, cell.hflip, cell.vflip, row, col); + image.add((dx as usize, dy as usize), colors[0][pixel as usize]); } - let sx = x + world.src_x - world.src_parallax; - let cell_index = world.source_cell(sx, sy); - let cell = cells.get(cell_index); - let char = chars.get(cell.char_index); - let row = (sy & 0x7) as usize; - let col = (sx & 0x7) as usize; - let pixel = utils::read_char_pixel(char, cell.hflip, cell.vflip, row, col); - image.add((dx as usize, dy as usize), colors[0][pixel as usize]); - } - - // right side - for x in 0..world.width { let dx = x + world.dst_x + world.dst_parallax; - if !(0..384).contains(&dx) { - continue; - } - let sx = x + world.src_x + world.src_parallax; + if world.header.ron && (0..384).contains(&dx) { + let (sx, sy) = source.right(x, y); - let cell_index = world.source_cell(sx, sy); - let cell = cells.get(cell_index); - let char = chars.get(cell.char_index); - let row = (sy & 0x7) as usize; - let col = (sx & 0x7) as usize; - let pixel = utils::read_char_pixel(char, cell.hflip, cell.vflip, row, col); - image.add((dx as usize, dy as usize), colors[1][pixel as usize]); + let cell_index = world.source_cell(sx, sy); + let cell = cells[1].get(cell_index); + let char = chars[1].get(cell.char_index); + let row = (sy & 0x7) as usize; + let col = (sx & 0x7) as usize; + let pixel = utils::read_char_pixel(char, cell.hflip, cell.vflip, row, col); + image.add((dx as usize, dy as usize), colors[1][pixel as usize]); + } } } } @@ -556,7 +607,7 @@ impl VramRenderer<1> for WorldRenderer { let worlds = self.worlds.borrow(); let header = WorldHeader::parse(worlds.read(params.index * 16)); - if header.end || (!header.lon && header.ron) { + if header.end || (!header.lon && !header.ron) { image.clear(); return; } @@ -693,13 +744,13 @@ impl Display for WorldMode { } struct CellCache<'a> { - bgmaps: MemoryRef<'a>, + bgmaps: &'a MemoryRef<'a>, index: usize, cell: CellData, } impl<'a> CellCache<'a> { - fn new(bgmaps: MemoryRef<'a>) -> Self { + fn new(bgmaps: &'a MemoryRef<'a>) -> Self { Self { bgmaps, index: 0x10000, @@ -718,13 +769,13 @@ impl<'a> CellCache<'a> { } struct CharCache<'a> { - chardata: MemoryRef<'a>, + chardata: &'a MemoryRef<'a>, index: usize, char: [u16; 8], } impl<'a> CharCache<'a> { - fn new(chardata: MemoryRef<'a>) -> Self { + fn new(chardata: &'a MemoryRef<'a>) -> Self { Self { chardata, index: 2048, @@ -740,3 +791,82 @@ impl<'a> CharCache<'a> { &self.char } } + +struct SourceCoordCalculator<'a> { + params: &'a MemoryRef<'a>, + world: &'a World, + y: i16, + param: SourceParam, +} + +impl<'a> SourceCoordCalculator<'a> { + fn new(params: &'a MemoryRef<'a>, world: &'a World) -> Self { + Self { + params, + world, + y: -1, + param: SourceParam::Normal, + } + } + + fn left(&mut self, x: i16, y: i16) -> (i16, i16) { + self.update_param(y); + match &self.param { + SourceParam::HBias(HBiasParam { left, .. }) => { + let sx = x + self.world.src_x - self.world.src_parallax + *left; + let sy = y + self.world.src_y; + (sx, sy) + } + SourceParam::Normal => { + let sx = x + self.world.src_x - self.world.src_parallax; + let sy = y + self.world.src_y; + (sx, sy) + } + } + } + + fn right(&mut self, x: i16, y: i16) -> (i16, i16) { + self.update_param(y); + match &self.param { + SourceParam::HBias(HBiasParam { right, .. }) => { + let sx = x + self.world.src_x + self.world.src_parallax + *right; + let sy = y + self.world.src_y; + (sx, sy) + } + SourceParam::Normal => { + let sx = x + self.world.src_x + self.world.src_parallax; + let sy = y + self.world.src_y; + (sx, sy) + } + } + } + + fn update_param(&mut self, y: i16) { + if self.y == y { + return; + } + if self.world.header.mode == WorldMode::HBias { + let base = self.world.param_base + (2 * y as usize); + self.param = SourceParam::HBias(HBiasParam::load(self.params, base)); + } + self.y = y; + } +} + +enum SourceParam { + Normal, + HBias(HBiasParam), +} + +struct HBiasParam { + left: i16, + right: i16, +} + +impl HBiasParam { + fn load(params: &MemoryRef, index: usize) -> Self { + let left = params.read::(index) << 3 >> 3; + let right = params.read::(index | 1) << 3 >> 3; + Self { left, right } + } +} -- 2.40.1 From 802da8f93e04bcc03812835f55a0f09506fe7228 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 17 Feb 2025 21:19:34 -0500 Subject: [PATCH 22/34] View affine worlds --- Cargo.lock | 55 +++++++++++++- Cargo.toml | 3 +- src/window/utils.rs | 26 +++++-- src/window/vram/world.rs | 151 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 218 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index afbb6ef..2e76f54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,6 +427,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + [[package]] name = "backtrace" version = "0.3.74" @@ -451,7 +457,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -834,6 +840,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "cursor-icon" version = "1.1.0" @@ -1166,6 +1178,19 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fixed" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c6e0b89bf864acd20590dbdbad56f69aeb898abfc9443008fd7bd48b2cc85a" +dependencies = [ + "az", + "bytemuck", + "half", + "num-traits", + "typenum", +] + [[package]] name = "flate2" version = "1.0.35" @@ -1443,6 +1468,16 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -1691,6 +1726,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.14" @@ -1779,10 +1823,11 @@ dependencies = [ "egui-wgpu", "egui-winit", "egui_extras", + "fixed", "gilrs", "hex", "image", - "itertools", + "itertools 0.14.0", "num-derive", "num-traits", "oneshot", @@ -3372,6 +3417,12 @@ dependencies = [ "rustc-hash", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "uds_windows" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index cd55b09..64eab6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,10 +21,11 @@ egui_extras = { version = "0.30", features = ["image"] } egui-toast = { git = "https://github.com/urholaukkarinen/egui-toast.git", rev = "d0bcf97" } egui-winit = "0.30" egui-wgpu = { version = "0.30", features = ["winit"] } +fixed = { version = "1.28", features = ["num-traits"] } gilrs = { version = "0.11", features = ["serde-serialize"] } hex = "0.4" image = { version = "0.25", default-features = false, features = ["png"] } -itertools = "0.13" +itertools = "0.14" num-derive = "0.4" num-traits = "0.2" oneshot = "0.1" diff --git a/src/window/utils.rs b/src/window/utils.rs index d53f6b4..7aab7b7 100644 --- a/src/window/utils.rs +++ b/src/window/utils.rs @@ -9,7 +9,7 @@ use egui::{ Response, RichText, Rounding, Sense, Shape, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, WidgetText, }; -use num_traits::PrimInt; +use num_traits::{CheckedAdd, CheckedSub, One}; pub trait UiExt { fn section(&mut self, title: impl Into, add_contents: impl FnOnce(&mut Ui)); @@ -97,12 +97,20 @@ enum Direction { Down, } -pub trait Number: PrimInt + Display + FromStr + Send + Sync + 'static {} -impl Number for T {} +pub trait Number: + Copy + One + CheckedAdd + CheckedSub + Eq + Ord + Display + FromStr + Send + Sync + 'static +{ +} +impl< + T: Copy + One + CheckedAdd + CheckedSub + Eq + Ord + Display + FromStr + Send + Sync + 'static, + > Number for T +{ +} pub struct NumberEdit<'a, T: Number> { value: &'a mut T, increment: T, + precision: usize, min: Option, max: Option, } @@ -112,11 +120,16 @@ impl<'a, T: Number> NumberEdit<'a, T> { Self { value, increment: T::one(), + precision: 3, min: None, max: None, } } + pub fn precision(self, precision: usize) -> Self { + Self { precision, ..self } + } + pub fn range(self, range: impl RangeBounds) -> Self { let min = match range.start_bound() { Bound::Unbounded => None, @@ -135,18 +148,19 @@ impl<'a, T: Number> NumberEdit<'a, T> { impl Widget for NumberEdit<'_, T> { fn ui(self, ui: &mut Ui) -> Response { let id = ui.id(); + let to_string = |val: &T| format!("{val:.0$}", self.precision); let (last_value, mut str, focus) = ui.memory(|m| { let (lv, s) = m .data .get_temp(id) - .unwrap_or((*self.value, self.value.to_string())); + .unwrap_or((*self.value, to_string(self.value))); let focus = m.has_focus(id); (lv, s, focus) }); let mut stale = false; if *self.value != last_value { - str = self.value.to_string(); + str = to_string(self.value); stale = true; } let valid = str.parse().is_ok_and(|v: T| v == *self.value); @@ -236,7 +250,7 @@ impl Widget for NumberEdit<'_, T> { if let Some(new_value) = value.filter(in_range) { *self.value = new_value; } - str = self.value.to_string(); + str = to_string(self.value); stale = true; } else if res.changed { if let Some(new_value) = str.parse().ok().filter(in_range) { diff --git a/src/window/vram/world.rs b/src/window/vram/world.rs index f9e9a61..4c0df76 100644 --- a/src/window/vram/world.rs +++ b/src/window/vram/world.rs @@ -5,6 +5,10 @@ use egui::{ TextureOptions, Ui, ViewportBuilder, ViewportId, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; +use fixed::{ + types::extra::{U3, U9}, + FixedI32, +}; use num_derive::{FromPrimitive, ToPrimitive}; use num_traits::FromPrimitive; @@ -277,10 +281,11 @@ impl WorldWindow { ui.label("Index"); }); row.col(|ui| { - ui.add(NumberEdit::new(&mut self.param_index).range(0..32)); + let max = world.height.max(8) as usize; + ui.add(NumberEdit::new(&mut self.param_index).range(0..max)); }); }); - let base = world.param_base + self.param_index * 2; + let base = (world.param_base + self.param_index * 2) & 0x1ffff; let mut param = HBiasParam::load(&self.bgmaps.borrow(), base); body.row(row_height, |mut row| { row.col(|ui| { @@ -314,6 +319,79 @@ impl WorldWindow { }); }); }); + } else if world.header.mode == WorldMode::Affine { + ui.section("Affine", |ui| { + TableBuilder::new(ui) + .column(Column::remainder()) + .column(Column::remainder()) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Index"); + }); + row.col(|ui| { + let max = world.height.max(1) as usize; + ui.add(NumberEdit::new(&mut self.param_index).range(0..max)); + }); + }); + let base = (world.param_base + self.param_index * 8) & 0x1ffff; + let mut param = AffineParam::load(&self.bgmaps.borrow(), base); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Address"); + }); + row.col(|ui| { + let address = 0x00020000 + base * 2; + 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("Src X"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut param.src_x).precision(3)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Src Y"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut param.src_y).precision(3)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Src parallax"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut param.src_parallax)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Delta X"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut param.dx).precision(9)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Delta Y"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut param.dy).precision(9)); + }); + }); + }); + }); } else { self.param_index = 0; } @@ -812,14 +890,19 @@ impl<'a> SourceCoordCalculator<'a> { fn left(&mut self, x: i16, y: i16) -> (i16, i16) { self.update_param(y); match &self.param { + SourceParam::Normal => { + let sx = x + self.world.src_x - self.world.src_parallax; + let sy = y + self.world.src_y; + (sx, sy) + } SourceParam::HBias(HBiasParam { left, .. }) => { let sx = x + self.world.src_x - self.world.src_parallax + *left; let sy = y + self.world.src_y; (sx, sy) } - SourceParam::Normal => { - let sx = x + self.world.src_x - self.world.src_parallax; - let sy = y + self.world.src_y; + SourceParam::Affine(affine) => { + let sx = affine_coord(affine.src_x, x, affine.dx, affine.src_parallax.min(0).abs()); + let sy = affine_coord(affine.src_y, x, affine.dy, affine.src_parallax.min(0).abs()); (sx, sy) } } @@ -828,14 +911,19 @@ impl<'a> SourceCoordCalculator<'a> { fn right(&mut self, x: i16, y: i16) -> (i16, i16) { self.update_param(y); match &self.param { + SourceParam::Normal => { + let sx = x + self.world.src_x + self.world.src_parallax; + let sy = y + self.world.src_y; + (sx, sy) + } SourceParam::HBias(HBiasParam { right, .. }) => { let sx = x + self.world.src_x + self.world.src_parallax + *right; let sy = y + self.world.src_y; (sx, sy) } - SourceParam::Normal => { - let sx = x + self.world.src_x + self.world.src_parallax; - let sy = y + self.world.src_y; + SourceParam::Affine(affine) => { + let sx = affine_coord(affine.src_x, x, affine.dx, -affine.src_parallax.max(0)); + let sy = affine_coord(affine.src_y, x, affine.dy, -affine.src_parallax.max(0)); (sx, sy) } } @@ -849,6 +937,10 @@ impl<'a> SourceCoordCalculator<'a> { let base = self.world.param_base + (2 * y as usize); self.param = SourceParam::HBias(HBiasParam::load(self.params, base)); } + if self.world.header.mode == WorldMode::Affine { + let base = self.world.param_base + (8 * y as usize); + self.param = SourceParam::Affine(AffineParam::load(self.params, base)); + } self.y = y; } } @@ -856,6 +948,7 @@ impl<'a> SourceCoordCalculator<'a> { enum SourceParam { Normal, HBias(HBiasParam), + Affine(AffineParam), } struct HBiasParam { @@ -870,3 +963,45 @@ impl HBiasParam { Self { left, right } } } + +struct AffineParam { + src_x: FixedI32, + src_parallax: i16, + src_y: FixedI32, + dx: FixedI32, + dy: FixedI32, +} + +impl AffineParam { + fn load(params: &MemoryRef, index: usize) -> Self { + let src_x = params.read::(index & 0x1ffff); + let src_x = FixedI32::from_bits(src_x as i32); + + let src_parallax = params.read::((index + 1) & 0x1ffff); + + let src_y = params.read::((index + 2) & 0x1ffff); + let src_y = FixedI32::from_bits(src_y as i32); + + let dx = params.read::((index + 3) & 0x1ffff); + let dx = FixedI32::from_bits(dx as i32); + + let dy = params.read::((index + 4) & 0x1ffff); + let dy = FixedI32::from_bits(dy as i32); + + AffineParam { + src_x, + src_parallax, + src_y, + dx, + dy, + } + } +} + +fn affine_coord(start: FixedI32, distance: i16, delta: FixedI32, parallax: i16) -> i16 { + let start = FixedI32::::from_num(start); + let distance = FixedI32::::from_num(distance); + let parallax = FixedI32::::from_num(parallax); + let coord = start + ((distance + parallax) * delta); + coord.to_num::() as i16 +} -- 2.40.1 From 2c71c20f204215ba0c3e218c33aa1d8772150f9e Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 17 Feb 2025 22:19:38 -0500 Subject: [PATCH 23/34] Edit worlds --- src/window/vram/world.rs | 169 ++++++++++++++++++++++++++++++++++----- 1 file changed, 147 insertions(+), 22 deletions(-) diff --git a/src/window/vram/world.rs b/src/window/vram/world.rs index 4c0df76..4596b1b 100644 --- a/src/window/vram/world.rs +++ b/src/window/vram/world.rs @@ -10,7 +10,7 @@ use fixed::{ FixedI32, }; use num_derive::{FromPrimitive, ToPrimitive}; -use num_traits::FromPrimitive; +use num_traits::{FromPrimitive, ToPrimitive}; use crate::{ emulator::SimId, @@ -27,6 +27,7 @@ use super::utils::{self, shade, CellData, Object}; pub struct WorldWindow { sim_id: SimId, loader: Arc, + memory: Arc, worlds: MemoryView, bgmaps: MemoryView, index: usize, @@ -37,7 +38,7 @@ pub struct WorldWindow { } impl WorldWindow { - pub fn new(sim_id: SimId, memory: &MemoryClient, vram: &mut VramProcessor) -> Self { + pub fn new(sim_id: SimId, memory: &Arc, vram: &mut VramProcessor) -> Self { let initial_params = WorldParams { index: 31, generic_palette: false, @@ -50,7 +51,8 @@ impl WorldWindow { Self { sim_id, loader: Arc::new(loader), - worlds: memory.watch(sim_id, 0x3d800, 0x400), + memory: memory.clone(), + worlds: memory.watch(sim_id, 0x0003d800, 0x400), bgmaps: memory.watch(sim_id, 0x00020000, 0x20000), index: params.index, param_index: 0, @@ -80,7 +82,7 @@ impl WorldWindow { ui.label("Address"); }); row.col(|ui| { - let address = 0x3d800 + self.index * 32; + let address = 0x0003d800 + self.index * 32; let mut address_str = format!("{address:08x}"); ui.add_enabled( false, @@ -89,7 +91,7 @@ impl WorldWindow { }); }); }); - let data = { + let mut data = { let worlds = self.worlds.borrow(); worlds.read(self.index) }; @@ -270,6 +272,10 @@ impl WorldWindow { }); }); }); + if world.update(&mut data) { + let address = 0x0003d800 + self.index * 32; + self.memory.write(self.sim_id, address as u32, &data); + } if world.header.mode == WorldMode::HBias { ui.section("H-bias", |ui| { TableBuilder::new(ui) @@ -317,6 +323,7 @@ impl WorldWindow { ui.add(NumberEdit::new(&mut param.right).range(-4096..4096)); }); }); + param.save(&self.memory, self.sim_id, base); }); }); } else if world.header.mode == WorldMode::Affine { @@ -390,6 +397,7 @@ impl WorldWindow { ui.add(NumberEdit::new(&mut param.dy).precision(9)); }); }); + param.save(&self.memory, self.sim_id, base); }); }); } else { @@ -599,6 +607,12 @@ impl WorldRenderer { fn render_world(&mut self, world: World, params: &WorldParams, image: &mut VramImage) { image.clear(); + let width = if world.header.mode == WorldMode::Affine { + world.width & 0x03ff + } else { + world.width + }; + let height = if world.header.mode == WorldMode::Affine { world.height.max(8) } else { @@ -606,7 +620,7 @@ impl WorldRenderer { }; let dx1 = world.dst_x; - let dx2 = dx1 + world.width; + let dx2 = dx1 + width; if dx1 - world.dst_parallax > 384 || dx2 + world.dst_parallax < 0 { return; } @@ -642,7 +656,7 @@ impl WorldRenderer { continue; } - for x in 0..world.width { + for x in 0..width { let dx = x + world.dst_x - world.dst_parallax; if world.header.lon && (0..384).contains(&dx) { let (sx, sy) = source.left(x, y); @@ -739,6 +753,52 @@ impl World { } } + fn update(&self, source: &mut [u16; 16]) -> bool { + let mut changed = self.header.update(&mut source[0]); + + let new_dst_x = (self.dst_x as u16 & 0x03ff) | (source[1] & 0xfc00); + changed |= source[1] != new_dst_x; + source[1] = new_dst_x; + + let new_dst_parallax = (self.dst_parallax as u16 & 0x03ff) | (source[2] & 0xfc00); + changed |= source[2] != new_dst_parallax; + source[2] = new_dst_parallax; + + let new_dst_y = self.dst_y as u16; + changed |= source[3] != new_dst_y; + source[3] = new_dst_y; + + let new_src_x = (self.src_x as u16 & 0x1fff) | (source[4] & 0xe000); + changed |= source[4] != new_src_x; + source[4] = new_src_x; + + let new_src_parallax = (self.src_parallax as u16 & 0x7fff) | (source[4] & 0x8000); + changed |= source[5] != new_src_parallax; + source[5] = new_src_parallax; + + let new_src_y = (self.src_y as u16 & 0x1fff) | (source[6] & 0xe000); + changed |= source[6] != new_src_y; + source[6] = new_src_y; + + let new_width = ((self.width - 1) as u16 & 0x1fff) | (source[7] & 0xe000); + changed |= source[7] != new_width; + source[7] = new_width; + + let new_height = (self.height - 1) as u16; + changed |= source[8] != new_height; + source[8] = new_height; + + let new_param_base = self.param_base as u16; + changed |= source[9] != new_param_base; + source[9] = new_param_base; + + let new_overplane = self.overplane as u16; + changed |= source[10] != new_overplane; + source[10] = new_overplane; + + changed + } + fn source_cell(&self, sx: i16, sy: i16) -> usize { if self.header.over { let bg_width = 1 << self.header.scx << 9; @@ -794,6 +854,21 @@ impl WorldHeader { base, } } + + fn update(&self, source: &mut u16) -> bool { + let new_value = (*source & 0x0030) + | if self.lon { 0x8000 } else { 0x0000 } + | if self.ron { 0x4000 } else { 0x0000 } + | self.mode.to_u16().unwrap() << 12 + | ((self.scx as u16) << 10) + | ((self.scy as u16) << 8) + | if self.over { 0x0080 } else { 0x0000 } + | if self.end { 0x0040 } else { 0x0000 } + | (self.base as u16 & 0x000f); + let changed = *source != new_value; + *source = new_value; + changed + } } #[derive(Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] @@ -954,13 +1029,29 @@ enum SourceParam { struct HBiasParam { left: i16, right: i16, + data: [u16; 2], } impl HBiasParam { fn load(params: &MemoryRef, index: usize) -> Self { - let left = params.read::(index) << 3 >> 3; - let right = params.read::(index | 1) << 3 >> 3; - Self { left, right } + let data = [params.read::(index), params.read::(index | 1)]; + let left = (data[0] as i16) << 3 >> 3; + let right = (data[1] as i16) << 3 >> 3; + Self { left, right, data } + } + + fn save(&self, memory: &MemoryClient, sim: SimId, index: usize) { + let new_left = (self.left as u16 & 0x1fff) | (self.data[0] & 0xe000); + if new_left != self.data[0] { + let address = 0x00020000 + (index * 2); + memory.write(sim, address as u32, &new_left); + } + + let new_right = (self.right as u16 & 0x1fff) | (self.data[1] & 0xe000); + if new_right != self.data[1] { + let address = 0x00020000 + ((index | 1) * 2); + memory.write(sim, address as u32, &new_right); + } } } @@ -970,23 +1061,24 @@ struct AffineParam { src_y: FixedI32, dx: FixedI32, dy: FixedI32, + data: [u16; 5], } impl AffineParam { fn load(params: &MemoryRef, index: usize) -> Self { - let src_x = params.read::(index & 0x1ffff); - let src_x = FixedI32::from_bits(src_x as i32); + let data = [ + params.read(index & 0xffff), + params.read((index + 1) & 0xffff), + params.read((index + 2) & 0xffff), + params.read((index + 3) & 0xffff), + params.read((index + 4) & 0xffff), + ]; - let src_parallax = params.read::((index + 1) & 0x1ffff); - - let src_y = params.read::((index + 2) & 0x1ffff); - let src_y = FixedI32::from_bits(src_y as i32); - - let dx = params.read::((index + 3) & 0x1ffff); - let dx = FixedI32::from_bits(dx as i32); - - let dy = params.read::((index + 4) & 0x1ffff); - let dy = FixedI32::from_bits(dy as i32); + let src_x = FixedI32::from_bits(data[0] as i16 as i32); + let src_parallax = data[1] as i16; + let src_y = FixedI32::from_bits(data[2] as i16 as i32); + let dx = FixedI32::from_bits(data[3] as i16 as i32); + let dy = FixedI32::from_bits(data[4] as i16 as i32); AffineParam { src_x, @@ -994,6 +1086,39 @@ impl AffineParam { src_y, dx, dy, + data, + } + } + + fn save(&self, memory: &MemoryClient, sim: SimId, index: usize) { + let new_src_x = self.src_x.to_bits() as u16; + if new_src_x != self.data[0] { + let address = 0x00020000 + 2 * (index & 0xffff); + memory.write(sim, address as u32, &new_src_x); + } + + let new_src_parallax = self.src_parallax as u16; + if new_src_parallax != self.data[1] { + let address = 0x00020000 + 2 * ((index + 1) & 0xffff); + memory.write(sim, address as u32, &new_src_parallax); + } + + let new_src_y = self.src_y.to_bits() as u16; + if new_src_y != self.data[2] { + let address = 0x00020000 + 2 * ((index + 2) & 0xffff); + memory.write(sim, address as u32, &new_src_y); + } + + let new_dx = self.dx.to_bits() as u16; + if new_dx != self.data[3] { + let address = 0x00020000 + 2 * ((index + 3) & 0xffff); + memory.write(sim, address as u32, &new_dx); + } + + let new_dy = self.dy.to_bits() as u16; + if new_dy != self.data[4] { + let address = 0x00020000 + 2 * ((index + 4) & 0xffff); + memory.write(sim, address as u32, &new_dy); } } } -- 2.40.1 From 6142179e31b79ab81befe41e0c0e0d4873e2e318 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 17 Feb 2025 22:32:42 -0500 Subject: [PATCH 24/34] Avoid unnecessary allocation --- src/memory.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/memory.rs b/src/memory.rs index 30b1f00..96fde14 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -238,11 +238,10 @@ impl MemoryRegion { } pub fn update(&self, data: &[u8]) { - let gens: Vec = self + let gens = self .gens - .iter() - .map(|i| i.load(std::sync::atomic::Ordering::Acquire)) - .collect(); + .each_ref() + .map(|i| i.load(std::sync::atomic::Ordering::Acquire)); let next_gen = gens.iter().max().unwrap() + 1; let indices = gens .into_iter() -- 2.40.1 From 2966a7c407928edcb867e9c77a6a0729baaa2d91 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 17 Feb 2025 23:21:16 -0500 Subject: [PATCH 25/34] View framebuffers --- src/app.rs | 9 +- src/window.rs | 2 +- src/window/game.rs | 6 + src/window/vram.rs | 2 + src/window/vram/framebuffer.rs | 267 +++++++++++++++++++++++++++++++++ 5 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 src/window/vram/framebuffer.rs 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); + } + } + } +} -- 2.40.1 From a737cd39b7a0f979bf94ea87ffd28d11b8bfe090 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Tue, 18 Feb 2025 23:59:25 -0500 Subject: [PATCH 26/34] Add register view with interrupts --- src/app.rs | 7 +- src/window.rs | 4 +- src/window/game.rs | 6 ++ src/window/vram.rs | 2 + src/window/vram/registers.rs | 195 +++++++++++++++++++++++++++++++++++ 5 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 src/window/vram/registers.rs diff --git a/src/app.rs b/src/app.rs index 3fa2fb4..2ceaf4f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -23,7 +23,7 @@ use crate::{ vram::VramProcessor, window::{ AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow, - GdbServerWindow, InputWindow, ObjectWindow, WorldWindow, + GdbServerWindow, InputWindow, ObjectWindow, RegisterWindow, WorldWindow, }, }; @@ -233,6 +233,10 @@ impl ApplicationHandler for Application { let world = FrameBufferWindow::new(sim_id, &self.memory, &mut self.vram); self.open(event_loop, Box::new(world)); } + UserEvent::OpenRegisters(sim_id) => { + let registers = RegisterWindow::new(sim_id, &self.memory); + self.open(event_loop, Box::new(registers)); + } UserEvent::OpenDebugger(sim_id) => { let debugger = GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone()); @@ -496,6 +500,7 @@ pub enum UserEvent { OpenObjects(SimId), OpenWorlds(SimId), OpenFrameBuffers(SimId), + OpenRegisters(SimId), OpenDebugger(SimId), OpenInput, OpenPlayer2, diff --git a/src/window.rs b/src/window.rs index 9353a64..2216004 100644 --- a/src/window.rs +++ b/src/window.rs @@ -3,7 +3,9 @@ use egui::{Context, ViewportBuilder, ViewportId}; pub use game::GameWindow; pub use gdb::GdbServerWindow; pub use input::InputWindow; -pub use vram::{BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, WorldWindow}; +pub use vram::{ + BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow, +}; use winit::event::KeyEvent; use crate::emulator::SimId; diff --git a/src/window/game.rs b/src/window/game.rs index e1072a9..8d91146 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -162,6 +162,12 @@ impl GameWindow { .unwrap(); ui.close_menu(); } + if ui.button("Registers").clicked() { + self.proxy + .send_event(UserEvent::OpenRegisters(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 4899486..595ba88 100644 --- a/src/window/vram.rs +++ b/src/window/vram.rs @@ -2,6 +2,7 @@ mod bgmap; mod chardata; mod framebuffer; mod object; +mod registers; mod utils; mod world; @@ -9,4 +10,5 @@ pub use bgmap::*; pub use chardata::*; pub use framebuffer::*; pub use object::*; +pub use registers::*; pub use world::*; diff --git a/src/window/vram/registers.rs b/src/window/vram/registers.rs new file mode 100644 index 0000000..e001dc4 --- /dev/null +++ b/src/window/vram/registers.rs @@ -0,0 +1,195 @@ +use std::sync::Arc; + +use egui::{ + Align, CentralPanel, Context, Label, Layout, ScrollArea, TextEdit, Ui, ViewportBuilder, + ViewportId, +}; +use egui_extras::{Column, Size, StripBuilder, TableBuilder}; + +use crate::{ + emulator::SimId, + memory::{MemoryClient, MemoryView}, + window::{utils::UiExt, AppWindow}, +}; + +pub struct RegisterWindow { + sim_id: SimId, + memory: Arc, + registers: MemoryView, +} + +impl RegisterWindow { + pub fn new(sim_id: SimId, memory: &Arc) -> Self { + Self { + sim_id, + memory: memory.clone(), + registers: memory.watch(sim_id, 0x0005f800, 0x70), + } + } + + fn show_interrupts(&mut self, ui: &mut Ui) { + let row_height = ui.spacing().interact_size.y; + let registers = self.registers.borrow(); + let [mut raw_intpnd, mut raw_intenb] = registers.read::<[u16; 2]>(0); + let mut intenb = InterruptReg::parse(raw_intenb); + let mut intpnd = InterruptReg::parse(raw_intpnd); + ui.section("Interrupt", |ui| { + ui.vertical(|ui| { + TableBuilder::new(ui) + .id_salt("raw_values") + .column(Column::auto()) + .column(Column::remainder()) + .cell_layout(Layout::left_to_right(Align::Max)) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("INTENB"); + }); + row.col(|ui| { + let mut text = format!("{raw_intenb:04x}"); + ui.add_enabled( + false, + TextEdit::singleline(&mut text).horizontal_align(Align::Max), + ); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("INTPND"); + }); + row.col(|ui| { + let mut text = format!("{raw_intpnd:04x}"); + ui.add_enabled( + false, + TextEdit::singleline(&mut text).horizontal_align(Align::Max), + ); + }); + }); + }); + ui.add_space(8.0); + TableBuilder::new(ui) + .id_salt("flags") + .column(Column::auto()) + .columns(Column::remainder(), 2) + .cell_layout(Layout::left_to_right(Align::Center).with_main_align(Align::RIGHT)) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|_ui| {}); + row.col(|ui| { + ui.add_sized([ui.available_width(), 0.0], Label::new("ENB")); + }); + row.col(|ui| { + ui.add_sized([ui.available_width(), 0.0], Label::new("PND")); + }); + }); + let mut add_row = |label: &str, enb: &mut bool, pnd: &mut bool| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label(label); + }); + row.col(|ui| { + let space = + (ui.available_width() - ui.spacing().icon_width) / 2.0; + ui.add_space(space); + ui.checkbox(enb, ""); + }); + row.col(|ui| { + let space = + (ui.available_width() - ui.spacing().icon_width) / 2.0; + ui.add_space(space); + ui.checkbox(pnd, ""); + }); + }); + }; + add_row("TIMEERR", &mut intenb.timeerr, &mut intpnd.timeerr); + add_row("XPEND", &mut intenb.xpend, &mut intpnd.xpend); + add_row("SBHIT", &mut intenb.sbhit, &mut intpnd.sbhit); + add_row("FRAMESTART", &mut intenb.framestart, &mut intpnd.framestart); + add_row("GAMESTART", &mut intenb.gamestart, &mut intpnd.gamestart); + add_row("RFBEND", &mut intenb.rfbend, &mut intpnd.rfbend); + add_row("LFBEND", &mut intenb.lfbend, &mut intpnd.lfbend); + add_row("SCANERR", &mut intenb.scanerr, &mut intpnd.scanerr); + }); + }); + }); + if intpnd.update(&mut raw_intpnd) { + self.memory.write(self.sim_id, 0x0005f800, &raw_intpnd); + } + if intenb.update(&mut raw_intenb) { + self.memory.write(self.sim_id, 0x0005f802, &raw_intenb); + } + } +} + +impl AppWindow for RegisterWindow { + fn viewport_id(&self) -> ViewportId { + ViewportId::from_hash_of(format!("registers-{}", self.sim_id)) + } + + fn sim_id(&self) -> SimId { + self.sim_id + } + + fn initial_viewport(&self) -> ViewportBuilder { + ViewportBuilder::default() + .with_title(format!("Registers ({})", self.sim_id)) + .with_inner_size((640.0, 480.0)) + } + + fn show(&mut self, ctx: &Context) { + CentralPanel::default().show(ctx, |ui| { + ScrollArea::vertical().show(ui, |ui| { + ui.horizontal_top(|ui| { + StripBuilder::new(ui) + .sizes(Size::remainder(), 4) + .horizontal(|mut strip| { + strip.cell(|ui| { + self.show_interrupts(ui); + }) + }); + }); + }); + }); + } +} + +struct InterruptReg { + timeerr: bool, + xpend: bool, + sbhit: bool, + framestart: bool, + gamestart: bool, + rfbend: bool, + lfbend: bool, + scanerr: bool, +} + +impl InterruptReg { + fn parse(value: u16) -> Self { + Self { + timeerr: value & 0x8000 != 0, + xpend: value & 0x4000 != 0, + sbhit: value & 0x2000 != 0, + framestart: value & 0x0010 != 0, + gamestart: value & 0x0008 != 0, + rfbend: value & 0x0004 != 0, + lfbend: value & 0x0002 != 0, + scanerr: value & 0x0001 != 0, + } + } + + fn update(&self, value: &mut u16) -> bool { + let new_value = (*value & 0x1fe0) + | if self.timeerr { 0x8000 } else { 0x0000 } + | if self.xpend { 0x4000 } else { 0x0000 } + | if self.sbhit { 0x2000 } else { 0x0000 } + | if self.framestart { 0x0010 } else { 0x0000 } + | if self.gamestart { 0x0008 } else { 0x0000 } + | if self.rfbend { 0x0004 } else { 0x0000 } + | if self.lfbend { 0x0002 } else { 0x0000 } + | if self.scanerr { 0x0001 } else { 0x0000 }; + let changed = *value != new_value; + *value = new_value; + changed + } +} -- 2.40.1 From 892d48e321eee2d424437cb98904b7c8574e3b3b Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Wed, 19 Feb 2025 19:52:31 -0500 Subject: [PATCH 27/34] Add dpctrl+xpctrl to register view --- src/window/vram/registers.rs | 296 ++++++++++++++++++++++++++++++++++- 1 file changed, 289 insertions(+), 7 deletions(-) diff --git a/src/window/vram/registers.rs b/src/window/vram/registers.rs index e001dc4..882b2b5 100644 --- a/src/window/vram/registers.rs +++ b/src/window/vram/registers.rs @@ -1,15 +1,18 @@ use std::sync::Arc; use egui::{ - Align, CentralPanel, Context, Label, Layout, ScrollArea, TextEdit, Ui, ViewportBuilder, - ViewportId, + Align, Button, CentralPanel, Checkbox, Context, Label, Layout, ScrollArea, TextEdit, Ui, + ViewportBuilder, ViewportId, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use crate::{ emulator::SimId, - memory::{MemoryClient, MemoryView}, - window::{utils::UiExt, AppWindow}, + memory::{MemoryClient, MemoryValue, MemoryView}, + window::{ + utils::{NumberEdit, UiExt}, + AppWindow, + }, }; pub struct RegisterWindow { @@ -29,8 +32,7 @@ impl RegisterWindow { fn show_interrupts(&mut self, ui: &mut Ui) { let row_height = ui.spacing().interact_size.y; - let registers = self.registers.borrow(); - let [mut raw_intpnd, mut raw_intenb] = registers.read::<[u16; 2]>(0); + let [mut raw_intpnd, mut raw_intenb] = self.read_address(0x0005f800); let mut intenb = InterruptReg::parse(raw_intenb); let mut intpnd = InterruptReg::parse(raw_intpnd); ui.section("Interrupt", |ui| { @@ -110,6 +112,7 @@ impl RegisterWindow { add_row("LFBEND", &mut intenb.lfbend, &mut intpnd.lfbend); add_row("SCANERR", &mut intenb.scanerr, &mut intpnd.scanerr); }); + ui.add_space(ui.available_height()); }); }); if intpnd.update(&mut raw_intpnd) { @@ -119,6 +122,187 @@ impl RegisterWindow { self.memory.write(self.sim_id, 0x0005f802, &raw_intenb); } } + + fn show_display_status(&mut self, ui: &mut Ui) { + let row_height = ui.spacing().interact_size.y; + let mut raw_dpstts = self.read_address(0x0005f820); + let mut dpstts = DisplayReg::parse(raw_dpstts); + ui.section("Display", |ui| { + TableBuilder::new(ui) + .column(Column::auto()) + .column(Column::remainder()) + .cell_layout(Layout::left_to_right(Align::Max)) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("DPSTTS"); + }); + row.col(|ui| { + let mut value_str = format!("{raw_dpstts:04x}"); + ui.add_enabled( + false, + TextEdit::singleline(&mut value_str).horizontal_align(Align::Max), + ); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.add_enabled(true, Checkbox::new(&mut dpstts.lock, "LOCK")); + }); + row.col(|ui| { + ui.add_enabled(false, Checkbox::new(&mut dpstts.r1bsy, "R1BSY")); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.add_enabled(true, Checkbox::new(&mut dpstts.synce, "SYNCE")); + }); + row.col(|ui| { + ui.add_enabled(false, Checkbox::new(&mut dpstts.l1bsy, "L1BSY")); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.add_enabled(true, Checkbox::new(&mut dpstts.re, "RE")); + }); + row.col(|ui| { + ui.add_enabled(false, Checkbox::new(&mut dpstts.r0bsy, "R0BSY")); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.add_enabled(false, Checkbox::new(&mut dpstts.fclk, "FCLK")); + }); + row.col(|ui| { + ui.add_enabled(false, Checkbox::new(&mut dpstts.l0bsy, "L0BSY")); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.add_enabled(false, Checkbox::new(&mut dpstts.scanrdy, "SCANRDY")); + }); + row.col(|ui| { + ui.add_enabled(true, Checkbox::new(&mut dpstts.disp, "DISP")); + }); + }); + body.row(row_height, |mut row| { + row.col(|_ui| {}); + row.col(|ui| { + if ui + .add(Button::new("DPRST").min_size(ui.available_size())) + .clicked() + { + dpstts.dprst = true; + } + }); + }); + }); + ui.vertical(|ui| ui.add_space((ui.available_height() - row_height).max(0.0))); + }); + if dpstts.update(&mut raw_dpstts) { + self.memory.write(self.sim_id, 0x0005f822, &raw_dpstts); + } + } + + fn show_drawing_status(&mut self, ui: &mut Ui) { + let row_height = ui.spacing().interact_size.y; + let [mut raw_xpstts, raw_xpctrl] = self.read_address(0x0005f840); + let mut xpstts = DrawingReg::parse(raw_xpstts); + ui.section("Drawing", |ui| { + TableBuilder::new(ui) + .column(Column::auto()) + .column(Column::remainder()) + .cell_layout(Layout::left_to_right(Align::Max)) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("XPCTRL"); + }); + row.col(|ui| { + let mut value_str = format!("{raw_xpctrl:04x}"); + ui.add_enabled( + false, + TextEdit::singleline(&mut value_str).horizontal_align(Align::Max), + ); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("XPSTTS"); + }); + row.col(|ui| { + let mut value_str = format!("{raw_xpstts:04x}"); + ui.add_enabled( + false, + TextEdit::singleline(&mut value_str).horizontal_align(Align::Max), + ); + }); + }); + /* + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("SBCMP"); + }); + row.col(|ui| { + let old_value = xpctrl.sbcmp; + ui.add_enabled(true, NumberEdit::new(&mut xpctrl.sbcmp).range(0..32)); + cmp_changed = xpctrl.sbcmp != old_value; + }); + }); + */ + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("SBCOUNT"); + }); + row.col(|ui| { + ui.add_enabled( + false, + NumberEdit::new(&mut xpstts.sbcount).range(0..32), + ); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.add_enabled(false, Checkbox::new(&mut xpstts.sbout, "SBOUT")); + }); + row.col(|ui| { + ui.add_enabled(false, Checkbox::new(&mut xpstts.f1bsy, "F1BSY")); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.add_enabled(false, Checkbox::new(&mut xpstts.f0bsy, "F0BSY")); + }); + row.col(|ui| { + ui.add_enabled(false, Checkbox::new(&mut xpstts.overtime, "OVERTIME")); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.add_enabled(true, Checkbox::new(&mut xpstts.xpen, "XPEN")); + }); + row.col(|ui| { + if ui + .add(Button::new("XPRST").min_size(ui.available_size())) + .clicked() + { + xpstts.xprst = true; + } + }); + }); + }); + ui.vertical(|ui| ui.add_space(ui.available_height())); + }); + if xpstts.update(&mut raw_xpstts) { + xpstts.update(&mut raw_xpstts); + self.memory.write(self.sim_id, 0x0005f842, &raw_xpstts); + } + } + + fn read_address(&self, address: usize) -> T { + let index = (address - 0x0005f800) / size_of::(); + self.registers.borrow().read(index) + } } impl AppWindow for RegisterWindow { @@ -145,7 +329,17 @@ impl AppWindow for RegisterWindow { .horizontal(|mut strip| { strip.cell(|ui| { self.show_interrupts(ui); - }) + }); + strip.strip(|nested| { + nested.sizes(Size::remainder(), 2).vertical(|mut strip| { + strip.cell(|ui| { + self.show_display_status(ui); + }); + strip.cell(|ui| { + self.show_drawing_status(ui); + }); + }); + }); }); }); }); @@ -193,3 +387,91 @@ impl InterruptReg { changed } } + +struct DisplayReg { + lock: bool, + synce: bool, + re: bool, + fclk: bool, + scanrdy: bool, + r1bsy: bool, + l1bsy: bool, + r0bsy: bool, + l0bsy: bool, + disp: bool, + dprst: bool, +} + +impl DisplayReg { + fn parse(value: u16) -> Self { + Self { + lock: value & 0x0400 != 0, + synce: value & 0x0200 != 0, + re: value & 0x0100 != 0, + fclk: value & 0x0080 != 0, + scanrdy: value & 0x0040 != 0, + r1bsy: value & 0x0020 != 0, + l1bsy: value & 0x0010 != 0, + r0bsy: value & 0x0008 != 0, + l0bsy: value & 0x0004 != 0, + disp: value & 0x0002 != 0, + dprst: value & 0x0001 != 0, + } + } + + fn update(&self, value: &mut u16) -> bool { + let new_value = (*value & 0xf800) + | if self.lock { 0x0400 } else { 0x0000 } + | if self.synce { 0x0200 } else { 0x0000 } + | if self.re { 0x0100 } else { 0x0000 } + | if self.fclk { 0x0080 } else { 0x0000 } + | if self.scanrdy { 0x0040 } else { 0x0000 } + | if self.r1bsy { 0x0020 } else { 0x0000 } + | if self.l1bsy { 0x0010 } else { 0x0000 } + | if self.r0bsy { 0x0008 } else { 0x0000 } + | if self.l0bsy { 0x0004 } else { 0x0000 } + | if self.disp { 0x0002 } else { 0x0000 } + | if self.dprst { 0x0001 } else { 0x0000 }; + let changed = *value != new_value; + *value = new_value; + changed + } +} + +struct DrawingReg { + sbout: bool, + sbcount: u8, + overtime: bool, + f1bsy: bool, + f0bsy: bool, + xpen: bool, + xprst: bool, +} + +impl DrawingReg { + fn parse(value: u16) -> Self { + Self { + sbout: value & 0x8000 != 0, + sbcount: (value >> 8) as u8 & 0x1f, + overtime: value & 0x0010 != 0, + f1bsy: value & 0x0008 != 0, + f0bsy: value & 0x0004 != 0, + xpen: value & 0x0002 != 0, + xprst: value & 0x0001 != 0, + } + } + + fn update(&self, value: &mut u16) -> bool { + let new_value = (*value & 0x60e0) + | if self.sbout { 0x8000 } else { 0x0000 } + | (((self.sbcount & 0x1f) as u16) << 8) + | if self.overtime { 0x0010 } else { 0x0000 } + | if self.f1bsy { 0x0008 } else { 0x0000 } + | if self.f0bsy { 0x0004 } else { 0x0000 } + | if self.xpen { 0x0002 } else { 0x0000 } + | if self.xprst { 0x0001 } else { 0x0000 }; + let changed = *value != new_value; + *value = new_value; + changed + } +} -- 2.40.1 From 4a6288147deed5b57ef28c7b64bb2460c929d3f1 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 23 Feb 2025 00:15:05 -0500 Subject: [PATCH 28/34] Add colors to register view --- src/window/utils.rs | 135 ++++++++++++++----- src/window/vram/registers.rs | 242 +++++++++++++++++++++++++++++++---- src/window/vram/utils.rs | 9 ++ 3 files changed, 328 insertions(+), 58 deletions(-) diff --git a/src/window/utils.rs b/src/window/utils.rs index 7aab7b7..d85d631 100644 --- a/src/window/utils.rs +++ b/src/window/utils.rs @@ -1,9 +1,10 @@ use std::{ - fmt::Display, + fmt::{Display, UpperHex}, ops::{Bound, RangeBounds}, str::FromStr, }; +use atoi::FromRadix16; use egui::{ ecolor::HexColor, Align, Color32, CursorIcon, Event, Frame, Key, Layout, Margin, Rect, Response, RichText, Rounding, Sense, Shape, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, @@ -38,7 +39,12 @@ impl UiExt for Ui { let mut frame = Frame::group(self.style()); frame.outer_margin.top += 10.0; frame.inner_margin.top += 2.0; - let res = self.push_id(&title, |ui| frame.show(ui, add_contents)); + let res = self.push_id(&title, |ui| { + frame.show(ui, |ui| { + ui.set_min_width(ui.available_width()); + add_contents(ui); + }) + }); let text = RichText::new(title).background_color(self.style().visuals.panel_fill); let old_rect = res.response.rect; let mut text_rect = old_rect; @@ -98,11 +104,35 @@ enum Direction { } pub trait Number: - Copy + One + CheckedAdd + CheckedSub + Eq + Ord + Display + FromStr + Send + Sync + 'static + Copy + + One + + CheckedAdd + + CheckedSub + + Eq + + Ord + + Display + + FromStr + + FromRadix16 + + UpperHex + + Send + + Sync + + 'static { } impl< - T: Copy + One + CheckedAdd + CheckedSub + Eq + Ord + Display + FromStr + Send + Sync + 'static, + T: Copy + + One + + CheckedAdd + + CheckedSub + + Eq + + Ord + + Display + + FromStr + + FromRadix16 + + UpperHex + + Send + + Sync + + 'static, > Number for T { } @@ -113,6 +143,8 @@ pub struct NumberEdit<'a, T: Number> { precision: usize, min: Option, max: Option, + arrows: bool, + hex: bool, } impl<'a, T: Number> NumberEdit<'a, T> { @@ -123,6 +155,8 @@ impl<'a, T: Number> NumberEdit<'a, T> { precision: 3, min: None, max: None, + arrows: true, + hex: false, } } @@ -143,12 +177,35 @@ impl<'a, T: Number> NumberEdit<'a, T> { }; Self { min, max, ..self } } + + pub fn arrows(self, arrows: bool) -> Self { + Self { arrows, ..self } + } + + pub fn hex(self, hex: bool) -> Self { + Self { hex, ..self } + } } impl Widget for NumberEdit<'_, T> { fn ui(self, ui: &mut Ui) -> Response { let id = ui.id(); - let to_string = |val: &T| format!("{val:.0$}", self.precision); + let to_string = |val: &T| { + if self.hex { + format!("{val:.0$X}", self.precision) + } else { + format!("{val:.0$}", self.precision) + } + }; + let from_string = |val: &str| { + if self.hex { + let bytes = val.as_bytes(); + let (result, consumed) = T::from_radix_16(bytes); + (consumed == bytes.len()).then_some(result) + } else { + val.parse::().ok() + } + }; let (last_value, mut str, focus) = ui.memory(|m| { let (lv, s) = m @@ -163,7 +220,7 @@ impl Widget for NumberEdit<'_, T> { str = to_string(self.value); stale = true; } - let valid = str.parse().is_ok_and(|v: T| v == *self.value); + let valid = from_string(&str).is_some_and(|v: T| v == *self.value); let mut up_pressed = false; let mut down_pressed = false; if focus { @@ -194,17 +251,25 @@ impl Widget for NumberEdit<'_, T> { .id(id) .margin(Margin { left: 4.0, - right: 20.0, + right: if self.arrows { 20.0 } else { 4.0 }, top: 2.0, bottom: 2.0, }); - let res = if valid { + let mut res = if valid { ui.add(text) } else { let message = match (self.min, self.max) { - (Some(min), Some(max)) => format!("Please enter a number between {min} and {max}."), - (Some(min), None) => format!("Please enter a number greater than {min}."), - (None, Some(max)) => format!("Please enter a number less than {max}."), + (Some(min), Some(max)) => format!( + "Please enter a number between {} and {}.", + to_string(&min), + to_string(&max) + ), + (Some(min), None) => { + format!("Please enter a number greater than {}.", to_string(&min)) + } + (None, Some(max)) => { + format!("Please enter a number less than {}.", to_string(&max)) + } (None, None) => "Please enter a number.".to_string(), }; ui.scope(|ui| { @@ -218,26 +283,28 @@ impl Widget for NumberEdit<'_, T> { .on_hover_text(message) }; - let arrow_left = res.rect.max.x + 4.0; - let arrow_right = res.rect.max.x + 20.0; - let arrow_top = res.rect.min.y - 2.0; - let arrow_middle = (res.rect.min.y + res.rect.max.y) / 2.0; - let arrow_bottom = res.rect.max.y + 2.0; - let mut delta = None; - let top_arrow_rect = Rect { - min: (arrow_left, arrow_top).into(), - max: (arrow_right, arrow_middle).into(), - }; - if draw_arrow(ui, top_arrow_rect, true).clicked_or_dragged() || up_pressed { - delta = Some(Direction::Up); - } - let bottom_arrow_rect = Rect { - min: (arrow_left, arrow_middle).into(), - max: (arrow_right, arrow_bottom).into(), - }; - if draw_arrow(ui, bottom_arrow_rect, false).clicked_or_dragged() || down_pressed { - delta = Some(Direction::Down); + if self.arrows { + let arrow_left = res.rect.max.x + 4.0; + let arrow_right = res.rect.max.x + 20.0; + let arrow_top = res.rect.min.y - 2.0; + let arrow_middle = (res.rect.min.y + res.rect.max.y) / 2.0; + let arrow_bottom = res.rect.max.y + 2.0; + + let top_arrow_rect = Rect { + min: (arrow_left, arrow_top).into(), + max: (arrow_right, arrow_middle).into(), + }; + if draw_arrow(ui, top_arrow_rect, true).clicked_or_dragged() || up_pressed { + delta = Some(Direction::Up); + } + let bottom_arrow_rect = Rect { + min: (arrow_left, arrow_middle).into(), + max: (arrow_right, arrow_bottom).into(), + }; + if draw_arrow(ui, bottom_arrow_rect, false).clicked_or_dragged() || down_pressed { + delta = Some(Direction::Down); + } } let in_range = @@ -248,12 +315,18 @@ impl Widget for NumberEdit<'_, T> { Direction::Down => self.value.checked_sub(&self.increment), }; if let Some(new_value) = value.filter(in_range) { + if *self.value != new_value { + res.mark_changed(); + } *self.value = new_value; } str = to_string(self.value); stale = true; } else if res.changed { - if let Some(new_value) = str.parse().ok().filter(in_range) { + if let Some(new_value) = from_string(&str).filter(in_range) { + if *self.value != new_value { + res.mark_changed(); + } *self.value = new_value; } stale = true; diff --git a/src/window/vram/registers.rs b/src/window/vram/registers.rs index 882b2b5..2301d40 100644 --- a/src/window/vram/registers.rs +++ b/src/window/vram/registers.rs @@ -1,20 +1,22 @@ use std::sync::Arc; use egui::{ - Align, Button, CentralPanel, Checkbox, Context, Label, Layout, ScrollArea, TextEdit, Ui, - ViewportBuilder, ViewportId, + Align, Button, CentralPanel, Checkbox, Color32, Context, Direction, Label, Layout, ScrollArea, + TextEdit, Ui, ViewportBuilder, ViewportId, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use crate::{ emulator::SimId, - memory::{MemoryClient, MemoryValue, MemoryView}, + memory::{MemoryClient, MemoryRef, MemoryValue, MemoryView}, window::{ utils::{NumberEdit, UiExt}, AppWindow, }, }; +use super::utils; + pub struct RegisterWindow { sim_id: SimId, memory: Arc, @@ -26,7 +28,7 @@ impl RegisterWindow { Self { sim_id, memory: memory.clone(), - registers: memory.watch(sim_id, 0x0005f800, 0x70), + registers: memory.watch(sim_id, 0x0005f800, 0x72), } } @@ -36,21 +38,21 @@ impl RegisterWindow { let mut intenb = InterruptReg::parse(raw_intenb); let mut intpnd = InterruptReg::parse(raw_intpnd); ui.section("Interrupt", |ui| { - ui.vertical(|ui| { + let width = ui.available_width(); + ui.with_layout(Layout::top_down_justified(Align::Center), |ui| { TableBuilder::new(ui) .id_salt("raw_values") - .column(Column::auto()) - .column(Column::remainder()) + .columns(Column::exact(width * 0.5), 2) .cell_layout(Layout::left_to_right(Align::Max)) .body(|mut body| { body.row(row_height, |mut row| { row.col(|ui| { - ui.label("INTENB"); + ui.add_sized(ui.available_size(), Label::new("INTENB")); }); row.col(|ui| { let mut text = format!("{raw_intenb:04x}"); - ui.add_enabled( - false, + ui.add_sized( + ui.available_size(), TextEdit::singleline(&mut text).horizontal_align(Align::Max), ); }); @@ -71,17 +73,17 @@ impl RegisterWindow { ui.add_space(8.0); TableBuilder::new(ui) .id_salt("flags") - .column(Column::auto()) - .columns(Column::remainder(), 2) + .column(Column::exact(width * 0.5)) + .columns(Column::exact(width * 0.25), 2) .cell_layout(Layout::left_to_right(Align::Center).with_main_align(Align::RIGHT)) .body(|mut body| { body.row(row_height, |mut row| { row.col(|_ui| {}); row.col(|ui| { - ui.add_sized([ui.available_width(), 0.0], Label::new("ENB")); + ui.add_sized(ui.available_size(), Label::new("ENB")); }); row.col(|ui| { - ui.add_sized([ui.available_width(), 0.0], Label::new("PND")); + ui.add_sized(ui.available_size(), Label::new("PND")); }); }); let mut add_row = |label: &str, enb: &mut bool, pnd: &mut bool| { @@ -112,7 +114,7 @@ impl RegisterWindow { add_row("LFBEND", &mut intenb.lfbend, &mut intpnd.lfbend); add_row("SCANERR", &mut intenb.scanerr, &mut intpnd.scanerr); }); - ui.add_space(ui.available_height()); + ui.allocate_space(ui.available_size()); }); }); if intpnd.update(&mut raw_intpnd) { @@ -128,9 +130,10 @@ impl RegisterWindow { let mut raw_dpstts = self.read_address(0x0005f820); let mut dpstts = DisplayReg::parse(raw_dpstts); ui.section("Display", |ui| { + let width = ui.available_width(); TableBuilder::new(ui) - .column(Column::auto()) - .column(Column::remainder()) + .column(Column::exact(width * 0.5)) + .column(Column::exact(width * 0.5)) .cell_layout(Layout::left_to_right(Align::Max)) .body(|mut body| { body.row(row_height, |mut row| { @@ -197,7 +200,7 @@ impl RegisterWindow { }); }); }); - ui.vertical(|ui| ui.add_space((ui.available_height() - row_height).max(0.0))); + ui.allocate_space(ui.available_size()); }); if dpstts.update(&mut raw_dpstts) { self.memory.write(self.sim_id, 0x0005f822, &raw_dpstts); @@ -209,9 +212,10 @@ impl RegisterWindow { let [mut raw_xpstts, raw_xpctrl] = self.read_address(0x0005f840); let mut xpstts = DrawingReg::parse(raw_xpstts); ui.section("Drawing", |ui| { + let width = ui.available_width(); TableBuilder::new(ui) - .column(Column::auto()) - .column(Column::remainder()) + .column(Column::exact(width * 0.5)) + .column(Column::exact(width * 0.5)) .cell_layout(Layout::left_to_right(Align::Max)) .body(|mut body| { body.row(row_height, |mut row| { @@ -257,7 +261,9 @@ impl RegisterWindow { row.col(|ui| { ui.add_enabled( false, - NumberEdit::new(&mut xpstts.sbcount).range(0..32), + NumberEdit::new(&mut xpstts.sbcount) + .arrows(false) + .range(0..32), ); }); }); @@ -291,7 +297,7 @@ impl RegisterWindow { }); }); }); - ui.vertical(|ui| ui.add_space(ui.available_height())); + ui.allocate_space(ui.available_size()); }); if xpstts.update(&mut raw_xpstts) { xpstts.update(&mut raw_xpstts); @@ -299,10 +305,185 @@ impl RegisterWindow { } } - fn read_address(&self, address: usize) -> T { - let index = (address - 0x0005f800) / size_of::(); - self.registers.borrow().read(index) + fn show_colors(&mut self, ui: &mut Ui) { + let row_height = ui.spacing().interact_size.y; + let registers = self.registers.borrow(); + ui.section("Colors", |ui| { + TableBuilder::new(ui) + .column(Column::auto()) + .columns(Column::remainder(), 4) + .cell_layout(Layout::left_to_right(Align::Max)) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|_ui| {}); + row.col(|_ui| {}); + row.col(|ui| { + ui.with_layout( + Layout::centered_and_justified(Direction::LeftToRight), + |ui| ui.label("1"), + ); + }); + row.col(|ui| { + ui.with_layout( + Layout::centered_and_justified(Direction::LeftToRight), + |ui| ui.label("2"), + ); + }); + row.col(|ui| { + ui.with_layout( + Layout::centered_and_justified(Direction::LeftToRight), + |ui| ui.label("3"), + ); + }); + }); + let mut brts: [u16; 3] = [ + read_address(®isters, 0x0005f824), + read_address(®isters, 0x0005f826), + read_address(®isters, 0x0005f828), + ]; + body.row(row_height, |mut row| { + let mut stale = false; + row.col(|ui| { + ui.label("BRT"); + }); + row.col(|_ui| {}); + for brt in brts.iter_mut() { + row.col(|ui| { + if ui + .add(NumberEdit::new(brt).range(0..256).arrows(false).hex(true)) + .changed() + { + stale = true; + } + }); + } + if stale { + self.memory.write(self.sim_id, 0x0005f824, &brts); + } + }); + body.row(row_height, |mut row| { + row.col(|_ui| {}); + for shade in utils::parse_brts(&brts, Color32::RED) { + row.col(|ui| { + ui.painter().rect_filled( + ui.available_rect_before_wrap(), + 0.0, + shade, + ); + }); + } + }); + let mut palettes = read_address::<[u16; 8]>(®isters, 0x0005f860); + let mut add_row = |name: &str, address: u32, value: &mut u16| { + let mut c1 = (*value >> 2) & 0x03; + let mut c2 = (*value >> 4) & 0x03; + let mut c3 = (*value >> 6) & 0x03; + let mut stale = false; + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label(name); + }); + row.col(|ui| { + if ui + .add( + NumberEdit::new(value) + .range(0..256) + .arrows(false) + .hex(true), + ) + .changed() + { + stale = true; + }; + }); + row.col(|ui| { + if ui + .add(NumberEdit::new(&mut c1).range(0..4).arrows(false)) + .changed() + { + *value = (*value & 0xfff3) | (c1 << 2); + stale = true; + } + }); + row.col(|ui| { + if ui + .add(NumberEdit::new(&mut c2).range(0..4).arrows(false)) + .changed() + { + *value = (*value & 0xffcf) | (c2 << 4); + stale = true; + } + }); + row.col(|ui| { + if ui + .add(NumberEdit::new(&mut c3).range(0..4).arrows(false)) + .changed() + { + *value = (*value & 0xff3f) | (c3 << 6); + stale = true; + } + }); + }); + if stale { + self.memory.write(self.sim_id, address, value); + } + }; + + add_row("GPLT0", 0x0005f860, &mut palettes[0]); + add_row("GPLT1", 0x0005f862, &mut palettes[1]); + add_row("GPLT2", 0x0005f864, &mut palettes[2]); + add_row("GPLT3", 0x0005f866, &mut palettes[3]); + add_row("JPLT0", 0x0005f868, &mut palettes[4]); + add_row("JPLT1", 0x0005f86a, &mut palettes[5]); + add_row("JPLT2", 0x0005f86c, &mut palettes[6]); + add_row("JPLT3", 0x0005f86e, &mut palettes[7]); + + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("BKCOL"); + }); + row.col(|ui| { + let mut bkcol: u16 = read_address(®isters, 0x0005f870); + if ui + .add(NumberEdit::new(&mut bkcol).range(0..4).arrows(false)) + .changed() + { + self.memory.write(self.sim_id, 0x0005f870, &bkcol); + } + }); + row.col(|_ui| {}); + row.col(|ui| { + ui.label("REST"); + }); + row.col(|ui| { + let mut rest: u16 = read_address(®isters, 0x0005f82a); + if ui + .add( + NumberEdit::new(&mut rest) + .range(0..256) + .arrows(false) + .hex(true), + ) + .changed() + { + self.memory.write(self.sim_id, 0x0005f82a, &rest); + } + }); + }); + }); + + ui.allocate_space(ui.available_size_before_wrap()); + }); } + + fn read_address(&self, address: usize) -> T { + read_address(&self.registers.borrow(), address) + } +} + +fn read_address(registers: &MemoryRef, address: usize) -> T { + let index = (address - 0x0005f800) / size_of::(); + registers.read(index) } impl AppWindow for RegisterWindow { @@ -317,15 +498,19 @@ impl AppWindow for RegisterWindow { fn initial_viewport(&self) -> ViewportBuilder { ViewportBuilder::default() .with_title(format!("Registers ({})", self.sim_id)) - .with_inner_size((640.0, 480.0)) + .with_inner_size((800.0, 480.0)) } fn show(&mut self, ctx: &Context) { CentralPanel::default().show(ctx, |ui| { ScrollArea::vertical().show(ui, |ui| { ui.horizontal_top(|ui| { + let width = ui.available_width() - (ui.spacing().item_spacing.x * 6.0); StripBuilder::new(ui) - .sizes(Size::remainder(), 4) + .size(Size::exact(width * 0.2)) + .size(Size::exact(width * 0.2)) + .size(Size::exact(width * 0.3)) + .size(Size::exact(width * 0.2)) .horizontal(|mut strip| { strip.cell(|ui| { self.show_interrupts(ui); @@ -340,6 +525,9 @@ impl AppWindow for RegisterWindow { }); }); }); + strip.cell(|ui| { + self.show_colors(ui); + }); }); }); }); diff --git a/src/window/vram/utils.rs b/src/window/vram/utils.rs index 98154da..726b18f 100644 --- a/src/window/vram/utils.rs +++ b/src/window/vram/utils.rs @@ -28,6 +28,15 @@ pub const fn parse_shades(brts: &[u8; 8]) -> [u8; 4] { ] } +pub fn parse_brts(brts: &[u16; 3], color: Color32) -> [Color32; 4] { + [ + Color32::BLACK, + shade(brts[0] as u8, color), + shade(brts[1] as u8, color), + shade((brts[0] + brts[1] + brts[2]) as u8, color), + ] +} + pub fn palette_colors(palette: u8, brts: &[u8; 8], color: Color32) -> [Color32; 4] { let colors = parse_shades(brts).map(|s| shade(s, color)); parse_palette(palette).map(|p| colors[p as usize]) -- 2.40.1 From d6957ecd09e481109705d03f034a007ebb8f4a1c Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 23 Feb 2025 01:02:08 -0500 Subject: [PATCH 29/34] Add other miscellanii to register view --- src/window/vram/registers.rs | 141 +++++++++++++++++++++++++++++++++-- 1 file changed, 136 insertions(+), 5 deletions(-) diff --git a/src/window/vram/registers.rs b/src/window/vram/registers.rs index 2301d40..b0e46f4 100644 --- a/src/window/vram/registers.rs +++ b/src/window/vram/registers.rs @@ -33,7 +33,7 @@ impl RegisterWindow { } fn show_interrupts(&mut self, ui: &mut Ui) { - let row_height = ui.spacing().interact_size.y; + let row_height = self.row_height(ui); let [mut raw_intpnd, mut raw_intenb] = self.read_address(0x0005f800); let mut intenb = InterruptReg::parse(raw_intenb); let mut intpnd = InterruptReg::parse(raw_intpnd); @@ -126,7 +126,7 @@ impl RegisterWindow { } fn show_display_status(&mut self, ui: &mut Ui) { - let row_height = ui.spacing().interact_size.y; + let row_height = self.row_height(ui); let mut raw_dpstts = self.read_address(0x0005f820); let mut dpstts = DisplayReg::parse(raw_dpstts); ui.section("Display", |ui| { @@ -208,7 +208,7 @@ impl RegisterWindow { } fn show_drawing_status(&mut self, ui: &mut Ui) { - let row_height = ui.spacing().interact_size.y; + let row_height = self.row_height(ui); let [mut raw_xpstts, raw_xpctrl] = self.read_address(0x0005f840); let mut xpstts = DrawingReg::parse(raw_xpstts); ui.section("Drawing", |ui| { @@ -306,7 +306,7 @@ impl RegisterWindow { } fn show_colors(&mut self, ui: &mut Ui) { - let row_height = ui.spacing().interact_size.y; + let row_height = self.row_height(ui); let registers = self.registers.borrow(); ui.section("Colors", |ui| { TableBuilder::new(ui) @@ -476,6 +476,127 @@ impl RegisterWindow { }); } + fn show_objects(&mut self, ui: &mut Ui) { + let row_height = self.row_height(ui); + ui.section("Objects", |ui| { + let width = ui.available_width(); + TableBuilder::new(ui) + .column(Column::exact(width * 0.25)) + .column(Column::exact(width * 0.25)) + .column(Column::exact(width * 0.5)) + .cell_layout(Layout::left_to_right(Align::Center).with_main_align(Align::RIGHT)) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|_ui| {}); + row.col(|ui| { + ui.add_sized(ui.available_size(), Label::new("Start")); + }); + row.col(|ui| { + ui.add_sized(ui.available_size(), Label::new("End")); + }); + }); + let mut spts = self.read_address::<[u16; 4]>(0x0005f848); + let prevs = std::iter::once(0u16).chain(spts.map(|i| (i + 1) & 0x03ff)); + let mut changed = false; + for (index, (spt, prev)) in spts.iter_mut().zip(prevs).enumerate() { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label(format!("SPT{index}")); + }); + row.col(|ui| { + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.label(prev.to_string()); + }); + }); + row.col(|ui| { + if ui.add(NumberEdit::new(spt).range(0..1024)).changed() { + changed = true; + } + }); + }); + } + if changed { + self.memory.write(self.sim_id, 0x0005f848, &spts); + } + }); + ui.allocate_space(ui.available_size_before_wrap()); + }); + } + + fn show_misc(&mut self, ui: &mut Ui) { + let row_height = self.row_height(ui); + let registers = self.registers.borrow(); + ui.section("Misc.", |ui| { + let width = ui.available_width(); + TableBuilder::new(ui) + .column(Column::exact(width * 0.5)) + .column(Column::exact(width * 0.5)) + .cell_layout(Layout::left_to_right(Align::Center).with_main_align(Align::RIGHT)) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("FRMCYC"); + }); + row.col(|ui| { + let mut frmcyc = read_address::(®isters, 0x0005f82e); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui.add(NumberEdit::new(&mut frmcyc).range(0..32)).changed() { + self.memory.write(self.sim_id, 0x0005f82e, &frmcyc); + } + }); + }); + }); + let mut cta = read_address::(®isters, 0x0005f830); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("CTA"); + }); + row.col(|ui| { + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.add_enabled( + false, + NumberEdit::new(&mut cta).arrows(false).hex(true), + ); + }); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("CTA_L"); + }); + row.col(|ui| { + let mut cta_l = cta & 0xff; + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.add_enabled( + false, + NumberEdit::new(&mut cta_l).arrows(false).hex(true), + ); + }); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("CTA_R"); + }); + row.col(|ui| { + let mut cta_r = (cta >> 8) & 0xff; + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.add_enabled( + false, + NumberEdit::new(&mut cta_r).arrows(false).hex(true), + ); + }); + }); + }); + }); + ui.allocate_space(ui.available_size_before_wrap()); + }); + } + + fn row_height(&self, ui: &mut Ui) -> f32 { + ui.spacing().interact_size.y + ui.style().visuals.selection.stroke.width + } + fn read_address(&self, address: usize) -> T { read_address(&self.registers.borrow(), address) } @@ -505,7 +626,7 @@ impl AppWindow for RegisterWindow { CentralPanel::default().show(ctx, |ui| { ScrollArea::vertical().show(ui, |ui| { ui.horizontal_top(|ui| { - let width = ui.available_width() - (ui.spacing().item_spacing.x * 6.0); + let width = ui.available_width() - (ui.spacing().item_spacing.x * 8.0); StripBuilder::new(ui) .size(Size::exact(width * 0.2)) .size(Size::exact(width * 0.2)) @@ -528,6 +649,16 @@ impl AppWindow for RegisterWindow { strip.cell(|ui| { self.show_colors(ui); }); + strip.strip(|nested| { + nested.sizes(Size::remainder(), 2).vertical(|mut strip| { + strip.cell(|ui| { + self.show_objects(ui); + }); + strip.cell(|ui| { + self.show_misc(ui); + }); + }); + }); }); }); }); -- 2.40.1 From f7408ac9b736f593e255534331f66dd785dc3ec2 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 23 Feb 2025 21:18:21 -0500 Subject: [PATCH 30/34] Fix layouts and colors --- src/window/utils.rs | 18 ++++++++- src/window/vram/bgmap.rs | 2 +- src/window/vram/chardata.rs | 2 +- src/window/vram/framebuffer.rs | 2 +- src/window/vram/object.rs | 2 +- src/window/vram/registers.rs | 70 ++++++++++++++++++++++++---------- src/window/vram/utils.rs | 9 ++++- src/window/vram/world.rs | 2 +- 8 files changed, 78 insertions(+), 29 deletions(-) diff --git a/src/window/utils.rs b/src/window/utils.rs index d85d631..d1afab6 100644 --- a/src/window/utils.rs +++ b/src/window/utils.rs @@ -41,7 +41,7 @@ impl UiExt for Ui { frame.inner_margin.top += 2.0; let res = self.push_id(&title, |ui| { frame.show(ui, |ui| { - ui.set_min_width(ui.available_width()); + ui.set_max_width(ui.available_width()); add_contents(ui); }) }); @@ -143,6 +143,7 @@ pub struct NumberEdit<'a, T: Number> { precision: usize, min: Option, max: Option, + desired_width: Option, arrows: bool, hex: bool, } @@ -155,6 +156,7 @@ impl<'a, T: Number> NumberEdit<'a, T> { precision: 3, min: None, max: None, + desired_width: None, arrows: true, hex: false, } @@ -178,6 +180,13 @@ impl<'a, T: Number> NumberEdit<'a, T> { Self { min, max, ..self } } + pub fn desired_width(self, desired_width: f32) -> Self { + Self { + desired_width: Some(desired_width), + ..self + } + } + pub fn arrows(self, arrows: bool) -> Self { Self { arrows, ..self } } @@ -246,9 +255,16 @@ impl Widget for NumberEdit<'_, T> { }) }); } + let mut desired_width = self + .desired_width + .unwrap_or_else(|| ui.spacing().text_edit_width); + if self.arrows { + desired_width -= 16.0; + } let text = TextEdit::singleline(&mut str) .horizontal_align(Align::Max) .id(id) + .desired_width(desired_width) .margin(Margin { left: 4.0, right: if self.arrows { 20.0 } else { 4.0 }, diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index 3b0f39f..25f7ca4 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -195,7 +195,7 @@ impl AppWindow for BgMapWindow { CentralPanel::default().show(ctx, |ui| { ui.horizontal_top(|ui| { StripBuilder::new(ui) - .size(Size::relative(0.3)) + .size(Size::relative(0.3).at_most(200.0)) .size(Size::remainder()) .horizontal(|mut strip| { strip.cell(|ui| { diff --git a/src/window/vram/chardata.rs b/src/window/vram/chardata.rs index 10c14d7..3a6d841 100644 --- a/src/window/vram/chardata.rs +++ b/src/window/vram/chardata.rs @@ -259,7 +259,7 @@ impl AppWindow for CharacterDataWindow { CentralPanel::default().show(ctx, |ui| { ui.horizontal_top(|ui| { StripBuilder::new(ui) - .size(Size::relative(0.3)) + .size(Size::relative(0.3).at_most(200.0)) .size(Size::remainder()) .horizontal(|mut strip| { strip.cell(|ui| { diff --git a/src/window/vram/framebuffer.rs b/src/window/vram/framebuffer.rs index b49a4bb..3a2e722 100644 --- a/src/window/vram/framebuffer.rs +++ b/src/window/vram/framebuffer.rs @@ -161,7 +161,7 @@ impl AppWindow for FrameBufferWindow { CentralPanel::default().show(ctx, |ui| { ui.horizontal_top(|ui| { StripBuilder::new(ui) - .size(Size::relative(0.3)) + .size(Size::relative(0.3).at_most(200.0)) .size(Size::remainder()) .horizontal(|mut strip| { strip.cell(|ui| { diff --git a/src/window/vram/object.rs b/src/window/vram/object.rs index 90c200e..656c2d1 100644 --- a/src/window/vram/object.rs +++ b/src/window/vram/object.rs @@ -216,7 +216,7 @@ impl AppWindow for ObjectWindow { CentralPanel::default().show(ctx, |ui| { ui.horizontal_top(|ui| { StripBuilder::new(ui) - .size(Size::relative(0.3)) + .size(Size::relative(0.3).at_most(200.0)) .size(Size::remainder()) .horizontal(|mut strip| { strip.cell(|ui| { diff --git a/src/window/vram/registers.rs b/src/window/vram/registers.rs index b0e46f4..997d5dc 100644 --- a/src/window/vram/registers.rs +++ b/src/window/vram/registers.rs @@ -39,15 +39,16 @@ impl RegisterWindow { let mut intpnd = InterruptReg::parse(raw_intpnd); ui.section("Interrupt", |ui| { let width = ui.available_width(); + let xspace = ui.spacing().item_spacing.x; ui.with_layout(Layout::top_down_justified(Align::Center), |ui| { TableBuilder::new(ui) .id_salt("raw_values") - .columns(Column::exact(width * 0.5), 2) + .columns(Column::exact(width * 0.5 - xspace), 2) .cell_layout(Layout::left_to_right(Align::Max)) .body(|mut body| { body.row(row_height, |mut row| { row.col(|ui| { - ui.add_sized(ui.available_size(), Label::new("INTENB")); + ui.label("INTENB"); }); row.col(|ui| { let mut text = format!("{raw_intenb:04x}"); @@ -73,8 +74,8 @@ impl RegisterWindow { ui.add_space(8.0); TableBuilder::new(ui) .id_salt("flags") - .column(Column::exact(width * 0.5)) - .columns(Column::exact(width * 0.25), 2) + .column(Column::exact(width * 0.5 - xspace)) + .columns(Column::exact(width * 0.25 - xspace), 2) .cell_layout(Layout::left_to_right(Align::Center).with_main_align(Align::RIGHT)) .body(|mut body| { body.row(row_height, |mut row| { @@ -114,8 +115,8 @@ impl RegisterWindow { add_row("LFBEND", &mut intenb.lfbend, &mut intpnd.lfbend); add_row("SCANERR", &mut intenb.scanerr, &mut intpnd.scanerr); }); - ui.allocate_space(ui.available_size()); }); + ui.allocate_space(ui.available_size()); }); if intpnd.update(&mut raw_intpnd) { self.memory.write(self.sim_id, 0x0005f800, &raw_intpnd); @@ -129,6 +130,7 @@ impl RegisterWindow { let row_height = self.row_height(ui); let mut raw_dpstts = self.read_address(0x0005f820); let mut dpstts = DisplayReg::parse(raw_dpstts); + ui.add_space(-ui.spacing().item_spacing.x); ui.section("Display", |ui| { let width = ui.available_width(); TableBuilder::new(ui) @@ -211,6 +213,7 @@ impl RegisterWindow { let row_height = self.row_height(ui); let [mut raw_xpstts, raw_xpctrl] = self.read_address(0x0005f840); let mut xpstts = DrawingReg::parse(raw_xpstts); + ui.add_space(-ui.spacing().item_spacing.x); ui.section("Drawing", |ui| { let width = ui.available_width(); TableBuilder::new(ui) @@ -308,10 +311,13 @@ impl RegisterWindow { fn show_colors(&mut self, ui: &mut Ui) { let row_height = self.row_height(ui); let registers = self.registers.borrow(); + ui.add_space(-ui.spacing().item_spacing.x); ui.section("Colors", |ui| { + let width = ui.available_width(); + let xspace = ui.spacing().item_spacing.x; TableBuilder::new(ui) - .column(Column::auto()) - .columns(Column::remainder(), 4) + .column(Column::exact(width * 0.2 - xspace)) + .columns(Column::exact(width * 0.20 - xspace), 4) .cell_layout(Layout::left_to_right(Align::Max)) .body(|mut body| { body.row(row_height, |mut row| { @@ -388,6 +394,7 @@ impl RegisterWindow { .add( NumberEdit::new(value) .range(0..256) + .desired_width(width * 0.2) .arrows(false) .hex(true), ) @@ -398,7 +405,11 @@ impl RegisterWindow { }); row.col(|ui| { if ui - .add(NumberEdit::new(&mut c1).range(0..4).arrows(false)) + .add( + NumberEdit::new(&mut c1) + .range(0..4) + .desired_width(ui.available_width() - xspace), + ) .changed() { *value = (*value & 0xfff3) | (c1 << 2); @@ -407,7 +418,11 @@ impl RegisterWindow { }); row.col(|ui| { if ui - .add(NumberEdit::new(&mut c2).range(0..4).arrows(false)) + .add( + NumberEdit::new(&mut c2) + .range(0..4) + .desired_width(ui.available_width() - xspace), + ) .changed() { *value = (*value & 0xffcf) | (c2 << 4); @@ -416,7 +431,11 @@ impl RegisterWindow { }); row.col(|ui| { if ui - .add(NumberEdit::new(&mut c3).range(0..4).arrows(false)) + .add( + NumberEdit::new(&mut c3) + .range(0..4) + .desired_width(ui.available_width() - xspace), + ) .changed() { *value = (*value & 0xff3f) | (c3 << 6); @@ -445,7 +464,11 @@ impl RegisterWindow { row.col(|ui| { let mut bkcol: u16 = read_address(®isters, 0x0005f870); if ui - .add(NumberEdit::new(&mut bkcol).range(0..4).arrows(false)) + .add( + NumberEdit::new(&mut bkcol) + .range(0..4) + .desired_width(ui.available_width() - xspace), + ) .changed() { self.memory.write(self.sim_id, 0x0005f870, &bkcol); @@ -478,12 +501,14 @@ impl RegisterWindow { fn show_objects(&mut self, ui: &mut Ui) { let row_height = self.row_height(ui); + ui.add_space(-ui.spacing().item_spacing.x); ui.section("Objects", |ui| { let width = ui.available_width(); + let xspace = ui.spacing().item_spacing.x; TableBuilder::new(ui) - .column(Column::exact(width * 0.25)) - .column(Column::exact(width * 0.25)) - .column(Column::exact(width * 0.5)) + .column(Column::exact(width * 0.25 - xspace)) + .column(Column::exact(width * 0.25 - xspace)) + .column(Column::exact(width * 0.5 - xspace)) .cell_layout(Layout::left_to_right(Align::Center).with_main_align(Align::RIGHT)) .body(|mut body| { body.row(row_height, |mut row| { @@ -526,11 +551,13 @@ impl RegisterWindow { fn show_misc(&mut self, ui: &mut Ui) { let row_height = self.row_height(ui); let registers = self.registers.borrow(); + ui.add_space(-ui.spacing().item_spacing.x); ui.section("Misc.", |ui| { let width = ui.available_width(); + let xspace = ui.spacing().item_spacing.x; TableBuilder::new(ui) - .column(Column::exact(width * 0.5)) - .column(Column::exact(width * 0.5)) + .column(Column::exact(width * 0.5 - xspace)) + .column(Column::exact(width * 0.5 - xspace)) .cell_layout(Layout::left_to_right(Align::Center).with_main_align(Align::RIGHT)) .body(|mut body| { body.row(row_height, |mut row| { @@ -626,12 +653,13 @@ impl AppWindow for RegisterWindow { CentralPanel::default().show(ctx, |ui| { ScrollArea::vertical().show(ui, |ui| { ui.horizontal_top(|ui| { - let width = ui.available_width() - (ui.spacing().item_spacing.x * 8.0); + let xspace = ui.spacing().item_spacing.x; + let width = ui.available_width() - (xspace * 5.0); StripBuilder::new(ui) - .size(Size::exact(width * 0.2)) - .size(Size::exact(width * 0.2)) - .size(Size::exact(width * 0.3)) - .size(Size::exact(width * 0.2)) + .size(Size::exact(width * 0.2 - xspace)) + .size(Size::exact(width * 0.2 - xspace)) + .size(Size::exact(width * 0.4 - xspace)) + .size(Size::exact(width * 0.2 - xspace)) .horizontal(|mut strip| { strip.cell(|ui| { self.show_interrupts(ui); diff --git a/src/window/vram/utils.rs b/src/window/vram/utils.rs index 726b18f..76df3bf 100644 --- a/src/window/vram/utils.rs +++ b/src/window/vram/utils.rs @@ -1,9 +1,14 @@ use egui::{Color32, Image, ImageSource, Response, Sense, TextureOptions, Ui, Widget}; -pub const GENERIC_PALETTE: [u8; 4] = [0, 64, 128, 255]; +pub const GENERIC_PALETTE: [u8; 4] = [0, 32, 64, 128]; pub fn shade(brt: u8, color: Color32) -> Color32 { - color.gamma_multiply(brt as f32 / 255.0) + let corrected = if brt & 0x80 != 0 { + 255 + } else { + (brt << 1) | (brt >> 6) + }; + color.gamma_multiply(corrected as f32 / 255.0) } pub fn generic_palette(color: Color32) -> [Color32; 4] { diff --git a/src/window/vram/world.rs b/src/window/vram/world.rs index 4596b1b..44258d0 100644 --- a/src/window/vram/world.rs +++ b/src/window/vram/world.rs @@ -453,7 +453,7 @@ impl AppWindow for WorldWindow { CentralPanel::default().show(ctx, |ui| { ui.horizontal_top(|ui| { StripBuilder::new(ui) - .size(Size::relative(0.3)) + .size(Size::relative(0.3).at_most(200.0)) .size(Size::remainder()) .horizontal(|mut strip| { strip.cell(|ui| { -- 2.40.1 From 0ae2a2f54f74cd94dccbd99c93c7e45db8bec9c7 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 23 Feb 2025 22:30:23 -0500 Subject: [PATCH 31/34] More visual fixes --- src/graphics.rs | 4 +--- src/window/utils.rs | 9 ++++----- src/window/vram/registers.rs | 19 +++++++------------ 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/graphics.rs b/src/graphics.rs index 4c0f75f..47f018d 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -64,9 +64,7 @@ impl TextureSink { sample_count: 1, dimension: wgpu::TextureDimension::D2, format: TextureFormat::Rg8Unorm, - usage: TextureUsages::COPY_SRC - | TextureUsages::COPY_DST - | TextureUsages::TEXTURE_BINDING, + usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, view_formats: &[TextureFormat::Rg8Unorm], }; device.create_texture(&desc) diff --git a/src/window/utils.rs b/src/window/utils.rs index d1afab6..cde57ab 100644 --- a/src/window/utils.rs +++ b/src/window/utils.rs @@ -49,11 +49,10 @@ impl UiExt for Ui { let old_rect = res.response.rect; let mut text_rect = old_rect; text_rect.min.x += 6.0; - let new_rect = self - .allocate_new_ui(UiBuilder::new().max_rect(text_rect), |ui| ui.label(text)) - .response - .rect; - self.allocate_space((old_rect.max - new_rect.max) - (old_rect.min - new_rect.min)); + self.allocate_new_ui(UiBuilder::new().max_rect(text_rect), |ui| ui.label(text)); + if old_rect.width() > 0.0 { + self.advance_cursor_after_rect(old_rect); + } } fn selectable_button(&mut self, selected: bool, text: impl Into) -> Response { diff --git a/src/window/vram/registers.rs b/src/window/vram/registers.rs index 997d5dc..1a1f303 100644 --- a/src/window/vram/registers.rs +++ b/src/window/vram/registers.rs @@ -130,7 +130,6 @@ impl RegisterWindow { let row_height = self.row_height(ui); let mut raw_dpstts = self.read_address(0x0005f820); let mut dpstts = DisplayReg::parse(raw_dpstts); - ui.add_space(-ui.spacing().item_spacing.x); ui.section("Display", |ui| { let width = ui.available_width(); TableBuilder::new(ui) @@ -213,7 +212,6 @@ impl RegisterWindow { let row_height = self.row_height(ui); let [mut raw_xpstts, raw_xpctrl] = self.read_address(0x0005f840); let mut xpstts = DrawingReg::parse(raw_xpstts); - ui.add_space(-ui.spacing().item_spacing.x); ui.section("Drawing", |ui| { let width = ui.available_width(); TableBuilder::new(ui) @@ -311,7 +309,6 @@ impl RegisterWindow { fn show_colors(&mut self, ui: &mut Ui) { let row_height = self.row_height(ui); let registers = self.registers.borrow(); - ui.add_space(-ui.spacing().item_spacing.x); ui.section("Colors", |ui| { let width = ui.available_width(); let xspace = ui.spacing().item_spacing.x; @@ -501,14 +498,13 @@ impl RegisterWindow { fn show_objects(&mut self, ui: &mut Ui) { let row_height = self.row_height(ui); - ui.add_space(-ui.spacing().item_spacing.x); ui.section("Objects", |ui| { let width = ui.available_width(); let xspace = ui.spacing().item_spacing.x; TableBuilder::new(ui) - .column(Column::exact(width * 0.25 - xspace)) - .column(Column::exact(width * 0.25 - xspace)) - .column(Column::exact(width * 0.5 - xspace)) + .column(Column::exact(width * 0.3 - xspace)) + .column(Column::exact(width * 0.3 - xspace)) + .column(Column::exact(width * 0.4 - xspace)) .cell_layout(Layout::left_to_right(Align::Center).with_main_align(Align::RIGHT)) .body(|mut body| { body.row(row_height, |mut row| { @@ -551,7 +547,6 @@ impl RegisterWindow { fn show_misc(&mut self, ui: &mut Ui) { let row_height = self.row_height(ui); let registers = self.registers.borrow(); - ui.add_space(-ui.spacing().item_spacing.x); ui.section("Misc.", |ui| { let width = ui.available_width(); let xspace = ui.spacing().item_spacing.x; @@ -653,12 +648,12 @@ impl AppWindow for RegisterWindow { CentralPanel::default().show(ctx, |ui| { ScrollArea::vertical().show(ui, |ui| { ui.horizontal_top(|ui| { + let width = ui.available_width(); let xspace = ui.spacing().item_spacing.x; - let width = ui.available_width() - (xspace * 5.0); StripBuilder::new(ui) - .size(Size::exact(width * 0.2 - xspace)) - .size(Size::exact(width * 0.2 - xspace)) - .size(Size::exact(width * 0.4 - xspace)) + .size(Size::exact(width * 0.25 - xspace)) + .size(Size::exact(width * 0.225 - xspace)) + .size(Size::exact(width * 0.325 - xspace)) .size(Size::exact(width * 0.2 - xspace)) .horizontal(|mut strip| { strip.cell(|ui| { -- 2.40.1 From 6ab9d2ee53d980304e0a42b069f834b955da0494 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 23 Feb 2025 22:44:11 -0500 Subject: [PATCH 32/34] Rename vram to vip internally --- src/app.rs | 20 +++---- src/{vram.rs => images.rs} | 76 ++++++++++++------------- src/main.rs | 2 +- src/memory.rs | 2 +- src/window.rs | 4 +- src/window/{vram.rs => vip.rs} | 0 src/window/{vram => vip}/bgmap.rs | 24 ++++---- src/window/{vram => vip}/chardata.rs | 48 ++++++++-------- src/window/{vram => vip}/framebuffer.rs | 18 +++--- src/window/{vram => vip}/object.rs | 28 +++++---- src/window/{vram => vip}/registers.rs | 0 src/window/{vram => vip}/utils.rs | 0 src/window/{vram => vip}/world.rs | 22 +++---- 13 files changed, 125 insertions(+), 119 deletions(-) rename src/{vram.rs => images.rs} (81%) rename src/window/{vram.rs => vip.rs} (100%) rename src/window/{vram => vip}/bgmap.rs (92%) rename src/window/{vram => vip}/chardata.rs (89%) rename src/window/{vram => vip}/framebuffer.rs (93%) rename src/window/{vram => vip}/object.rs (93%) rename src/window/{vram => vip}/registers.rs (100%) rename src/window/{vram => vip}/utils.rs (100%) rename src/window/{vram => vip}/world.rs (98%) diff --git a/src/app.rs b/src/app.rs index 2ceaf4f..8030986 100644 --- a/src/app.rs +++ b/src/app.rs @@ -17,10 +17,10 @@ use winit::{ use crate::{ controller::ControllerManager, emulator::{EmulatorClient, EmulatorCommand, SimId}, + images::ImageProcessor, input::MappingProvider, memory::MemoryClient, persistence::Persistence, - vram::VramProcessor, window::{ AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow, GdbServerWindow, InputWindow, ObjectWindow, RegisterWindow, WorldWindow, @@ -46,7 +46,7 @@ pub struct Application { mappings: MappingProvider, controllers: ControllerManager, memory: Arc, - vram: VramProcessor, + images: ImageProcessor, persistence: Persistence, viewports: HashMap, focused: Option, @@ -65,7 +65,7 @@ impl Application { let mappings = MappingProvider::new(persistence.clone()); let controllers = ControllerManager::new(client.clone(), &mappings); let memory = Arc::new(MemoryClient::new(client.clone())); - let vram = VramProcessor::new(); + let images = ImageProcessor::new(); { let mappings = mappings.clone(); let proxy = proxy.clone(); @@ -78,7 +78,7 @@ impl Application { proxy, mappings, memory, - vram, + images, controllers, persistence, viewports: HashMap::new(), @@ -214,23 +214,23 @@ impl ApplicationHandler for Application { self.open(event_loop, Box::new(about)); } UserEvent::OpenCharacterData(sim_id) => { - let vram = CharacterDataWindow::new(sim_id, &self.memory, &mut self.vram); - self.open(event_loop, Box::new(vram)); + let chardata = CharacterDataWindow::new(sim_id, &self.memory, &mut self.images); + self.open(event_loop, Box::new(chardata)); } UserEvent::OpenBgMap(sim_id) => { - let bgmap = BgMapWindow::new(sim_id, &self.memory, &mut self.vram); + let bgmap = BgMapWindow::new(sim_id, &self.memory, &mut self.images); self.open(event_loop, Box::new(bgmap)); } UserEvent::OpenObjects(sim_id) => { - let objects = ObjectWindow::new(sim_id, &self.memory, &mut self.vram); + let objects = ObjectWindow::new(sim_id, &self.memory, &mut self.images); self.open(event_loop, Box::new(objects)); } UserEvent::OpenWorlds(sim_id) => { - let world = WorldWindow::new(sim_id, &self.memory, &mut self.vram); + let world = WorldWindow::new(sim_id, &self.memory, &mut self.images); self.open(event_loop, Box::new(world)); } UserEvent::OpenFrameBuffers(sim_id) => { - let world = FrameBufferWindow::new(sim_id, &self.memory, &mut self.vram); + let world = FrameBufferWindow::new(sim_id, &self.memory, &mut self.images); self.open(event_loop, Box::new(world)); } UserEvent::OpenRegisters(sim_id) => { diff --git a/src/vram.rs b/src/images.rs similarity index 81% rename from src/vram.rs rename to src/images.rs index 88e5e89..1730aa8 100644 --- a/src/vram.rs +++ b/src/images.rs @@ -13,11 +13,11 @@ use egui::{ }; use tokio::{sync::mpsc, time::timeout}; -pub struct VramProcessor { - sender: mpsc::UnboundedSender>, +pub struct ImageProcessor { + sender: mpsc::UnboundedSender>, } -impl VramProcessor { +impl ImageProcessor { pub fn new() -> Self { let (sender, receiver) = mpsc::unbounded_channel(); thread::spawn(move || { @@ -26,7 +26,7 @@ impl VramProcessor { .build() .unwrap() .block_on(async move { - let mut worker = VramProcessorWorker { + let mut worker = ImageProcessorWorker { receiver, renderers: vec![], }; @@ -36,27 +36,27 @@ impl VramProcessor { Self { sender } } - pub fn add + 'static>( + pub fn add + 'static>( &self, renderer: R, params: R::Params, - ) -> ([VramImageHandle; N], VramParams) { - let states = renderer.sizes().map(VramRenderImageState::new); - let handles = states.clone().map(|state| VramImageHandle { + ) -> ([ImageHandle; N], ImageParams) { + let states = renderer.sizes().map(ImageState::new); + let handles = states.clone().map(|state| ImageHandle { size: state.size.map(|i| i as f32), data: state.sink, }); let images = renderer .sizes() - .map(|[width, height]| VramImage::new(width, height)); + .map(|[width, height]| ImageBuffer::new(width, height)); let sink = Arc::new(Mutex::new(params.clone())); - let _ = self.sender.send(Box::new(VramRendererWrapper { + let _ = self.sender.send(Box::new(ImageRendererWrapper { renderer, params: Arc::downgrade(&sink), images, states, })); - let params = VramParams { + let params = ImageParams { value: params, sink, }; @@ -64,12 +64,12 @@ impl VramProcessor { } } -struct VramProcessorWorker { - receiver: mpsc::UnboundedReceiver>, - renderers: Vec>, +struct ImageProcessorWorker { + receiver: mpsc::UnboundedReceiver>, + renderers: Vec>, } -impl VramProcessorWorker { +impl ImageProcessorWorker { async fn run(&mut self) { loop { if self.renderers.is_empty() { @@ -100,12 +100,12 @@ impl VramProcessorWorker { } } -pub struct VramImage { +pub struct ImageBuffer { pub size: [usize; 2], pub pixels: Vec, } -impl VramImage { +impl ImageBuffer { pub fn new(width: usize, height: usize) -> Self { Self { size: [width, height], @@ -143,23 +143,23 @@ impl VramImage { } #[derive(Clone)] -pub struct VramImageHandle { +pub struct ImageHandle { size: [f32; 2], data: Arc>>>, } -impl VramImageHandle { +impl ImageHandle { fn pull(&mut self) -> Option> { self.data.lock().unwrap().take() } } -pub struct VramParams { +pub struct ImageParams { value: T, sink: Arc>, } -impl Deref for VramParams { +impl Deref for ImageParams { type Target = T; fn deref(&self) -> &Self::Target { @@ -167,7 +167,7 @@ impl Deref for VramParams { } } -impl VramParams { +impl ImageParams { pub fn write(&mut self, value: T) { if self.value != value { self.value = value.clone(); @@ -176,21 +176,21 @@ impl VramParams { } } -pub trait VramRenderer: Send { +pub trait ImageRenderer: Send { type Params: Clone + Send; fn sizes(&self) -> [[usize; 2]; N]; - fn render(&mut self, params: &Self::Params, images: &mut [VramImage; N]); + fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; N]); } #[derive(Clone)] -struct VramRenderImageState { +struct ImageState { size: [usize; 2], buffers: [Arc; 2], last_buffer: usize, sink: Arc>>>, } -impl VramRenderImageState { +impl ImageState { fn new(size: [usize; 2]) -> Self { let buffers = [ Arc::new(ColorImage::new(size, Color32::BLACK)), @@ -204,7 +204,7 @@ impl VramRenderImageState { sink: Arc::new(Mutex::new(Some(sink))), } } - fn try_send_update(&mut self, image: &VramImage) { + fn try_send_update(&mut self, image: &ImageBuffer) { let last = &self.buffers[self.last_buffer]; if !image.changed(last) { return; @@ -218,18 +218,18 @@ impl VramRenderImageState { } } -struct VramRendererWrapper> { +struct ImageRendererWrapper> { renderer: R, params: Weak>, - images: [VramImage; N], - states: [VramRenderImageState; N], + images: [ImageBuffer; N], + states: [ImageState; N], } -trait VramRendererImpl: Send { +trait ImageRendererImpl: Send { fn try_update(&mut self) -> Result<(), ()>; } -impl + Send> VramRendererImpl for VramRendererWrapper { +impl + Send> ImageRendererImpl for ImageRendererWrapper { fn try_update(&mut self) -> Result<(), ()> { let params = match self.params.upgrade() { Some(params) => params.lock().unwrap().clone(), @@ -247,12 +247,12 @@ impl + Send> VramRendererImpl for VramRendere } } -pub struct VramTextureLoader { - cache: Mutex)>>, +pub struct ImageTextureLoader { + cache: Mutex)>>, } -impl VramTextureLoader { - pub fn new(renderers: impl IntoIterator) -> Self { +impl ImageTextureLoader { + pub fn new(renderers: impl IntoIterator) -> Self { let mut cache = HashMap::new(); for (key, image) in renderers { cache.insert(key, (image, None)); @@ -263,9 +263,9 @@ impl VramTextureLoader { } } -impl TextureLoader for VramTextureLoader { +impl TextureLoader for ImageTextureLoader { fn id(&self) -> &str { - concat!(module_path!(), "VramTextureLoader") + concat!(module_path!(), "ImageTextureLoader") } fn load( diff --git a/src/main.rs b/src/main.rs index 5b43893..eb160b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,10 +18,10 @@ mod controller; mod emulator; mod gdbserver; mod graphics; +mod images; mod input; mod memory; mod persistence; -mod vram; mod window; #[derive(Parser)] diff --git a/src/memory.rs b/src/memory.rs index 96fde14..92b10d5 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -263,7 +263,7 @@ impl MemoryRegion { /* * We have four buffers, and (at time of writing) only three threads interacting with memory: * - The UI thread, reading small regions of memory - * - The "vram renderer" thread, reading large regions of memory + * - The "image renderer" thread, reading large regions of memory * - The emulation thread, writing memory every so often * So it should be impossible for all four buffers to have a read lock at the same time, * and (because readers always read the newest buffer) at least one of the oldest three diff --git a/src/window.rs b/src/window.rs index 2216004..8ad5146 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::{ +pub use vip::{ BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow, }; use winit::event::KeyEvent; @@ -16,7 +16,7 @@ mod game_screen; mod gdb; mod input; mod utils; -mod vram; +mod vip; pub trait AppWindow { fn viewport_id(&self) -> ViewportId; diff --git a/src/window/vram.rs b/src/window/vip.rs similarity index 100% rename from src/window/vram.rs rename to src/window/vip.rs diff --git a/src/window/vram/bgmap.rs b/src/window/vip/bgmap.rs similarity index 92% rename from src/window/vram/bgmap.rs rename to src/window/vip/bgmap.rs index 25f7ca4..4a7a8c7 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vip/bgmap.rs @@ -8,8 +8,8 @@ use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use crate::{ emulator::SimId, + images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader}, memory::{MemoryClient, MemoryView}, - vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, window::{ utils::{NumberEdit, UiExt}, AppWindow, @@ -20,22 +20,22 @@ use super::utils::{self, CellData, CharacterGrid}; pub struct BgMapWindow { sim_id: SimId, - loader: Arc, + loader: Arc, memory: Arc, bgmaps: MemoryView, cell_index: usize, generic_palette: bool, - params: VramParams, + params: ImageParams, scale: f32, show_grid: bool, } impl BgMapWindow { - pub fn new(sim_id: SimId, memory: &Arc, vram: &mut VramProcessor) -> Self { + pub fn new(sim_id: SimId, memory: &Arc, images: &mut ImageProcessor) -> Self { let renderer = BgMapRenderer::new(sim_id, memory); - let ([cell, bgmap], params) = vram.add(renderer, BgMapParams::default()); + let ([cell, bgmap], params) = images.add(renderer, BgMapParams::default()); let loader = - VramTextureLoader::new([("vram://cell".into(), cell), ("vram://bgmap".into(), bgmap)]); + ImageTextureLoader::new([("vip://cell".into(), cell), ("vip://bgmap".into(), bgmap)]); Self { sim_id, loader: Arc::new(loader), @@ -90,7 +90,7 @@ impl BgMapWindow { }); }); }); - let image = Image::new("vram://cell") + let image = Image::new("vip://cell") .maintain_aspect_ratio(true) .texture_options(TextureOptions::NEAREST); ui.add(image); @@ -162,7 +162,7 @@ impl BgMapWindow { } fn show_bgmap(&mut self, ui: &mut Ui) { - let grid = CharacterGrid::new("vram://bgmap") + let grid = CharacterGrid::new("vip://bgmap") .with_scale(self.scale) .with_grid(self.show_grid) .with_selected(self.cell_index % 4096); @@ -233,7 +233,7 @@ impl BgMapRenderer { } } - fn render_bgmap(&self, image: &mut VramImage, bgmap_index: usize, generic_palette: bool) { + fn render_bgmap(&self, image: &mut ImageBuffer, bgmap_index: usize, generic_palette: bool) { let chardata = self.chardata.borrow(); let bgmaps = self.bgmaps.borrow(); let brightness = self.brightness.borrow(); @@ -266,7 +266,7 @@ impl BgMapRenderer { } } - fn render_bgmap_cell(&self, image: &mut VramImage, index: usize, generic_palette: bool) { + fn render_bgmap_cell(&self, image: &mut ImageBuffer, index: usize, generic_palette: bool) { let chardata = self.chardata.borrow(); let bgmaps = self.bgmaps.borrow(); let brightness = self.brightness.borrow(); @@ -297,14 +297,14 @@ impl BgMapRenderer { } } -impl VramRenderer<2> for BgMapRenderer { +impl ImageRenderer<2> for BgMapRenderer { type Params = BgMapParams; fn sizes(&self) -> [[usize; 2]; 2] { [[8, 8], [8 * 64, 8 * 64]] } - fn render(&mut self, params: &Self::Params, images: &mut [VramImage; 2]) { + fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 2]) { self.render_bgmap_cell(&mut images[0], params.cell_index, params.generic_palette); self.render_bgmap( &mut images[1], diff --git a/src/window/vram/chardata.rs b/src/window/vip/chardata.rs similarity index 89% rename from src/window/vram/chardata.rs rename to src/window/vip/chardata.rs index 3a6d841..3bddc7a 100644 --- a/src/window/vram/chardata.rs +++ b/src/window/vip/chardata.rs @@ -9,8 +9,8 @@ use serde::{Deserialize, Serialize}; use crate::{ emulator::SimId, + images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader}, memory::{MemoryClient, MemoryView}, - vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, window::{ utils::{NumberEdit, UiExt as _}, AppWindow, @@ -20,7 +20,7 @@ use crate::{ use super::utils::{self, CharacterGrid}; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum VramPalette { +pub enum Palette { #[default] Generic, Bg0, @@ -33,8 +33,8 @@ pub enum VramPalette { Obj3, } -impl VramPalette { - pub const fn values() -> [VramPalette; 9] { +impl Palette { + pub const fn values() -> [Palette; 9] { [ Self::Generic, Self::Bg0, @@ -63,7 +63,7 @@ impl VramPalette { } } -impl Display for VramPalette { +impl Display for Palette { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Generic => f.write_str("Generic"), @@ -81,23 +81,23 @@ impl Display for VramPalette { pub struct CharacterDataWindow { sim_id: SimId, - loader: Arc, + loader: Arc, brightness: MemoryView, palettes: MemoryView, - palette: VramPalette, + palette: Palette, index: usize, - params: VramParams, + params: ImageParams, scale: f32, show_grid: bool, } impl CharacterDataWindow { - pub fn new(sim_id: SimId, memory: &MemoryClient, vram: &mut VramProcessor) -> Self { + pub fn new(sim_id: SimId, memory: &MemoryClient, images: &mut ImageProcessor) -> Self { let renderer = CharDataRenderer::new(sim_id, memory); - let ([char, chardata], params) = vram.add(renderer, CharDataParams::default()); - let loader = VramTextureLoader::new([ - ("vram://char".into(), char), - ("vram://chardata".into(), chardata), + let ([char, chardata], params) = images.add(renderer, CharDataParams::default()); + let loader = ImageTextureLoader::new([ + ("vip://char".into(), char), + ("vip://chardata".into(), chardata), ]); Self { sim_id, @@ -160,7 +160,7 @@ impl CharacterDataWindow { }); }); }); - let image = Image::new("vram://char") + let image = Image::new("vip://char") .maintain_aspect_ratio(true) .texture_options(TextureOptions::NEAREST); ui.add(image); @@ -171,7 +171,7 @@ impl CharacterDataWindow { .selected_text(self.palette.to_string()) .width(ui.available_width()) .show_ui(ui, |ui| { - for palette in VramPalette::values() { + for palette in Palette::values() { ui.selectable_value( &mut self.palette, palette, @@ -226,7 +226,7 @@ impl CharacterDataWindow { } fn show_chardata(&mut self, ui: &mut Ui) { - let grid = CharacterGrid::new("vram://chardata") + let grid = CharacterGrid::new("vip://chardata") .with_scale(self.scale) .with_grid(self.show_grid) .with_selected(self.index); @@ -276,13 +276,13 @@ impl AppWindow for CharacterDataWindow { #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] enum CharDataResource { - Character { palette: VramPalette, index: usize }, - CharacterData { palette: VramPalette }, + Character { palette: Palette, index: usize }, + CharacterData { palette: Palette }, } #[derive(Clone, Default, PartialEq, Eq)] struct CharDataParams { - palette: VramPalette, + palette: Palette, index: usize, } @@ -292,14 +292,14 @@ struct CharDataRenderer { palettes: MemoryView, } -impl VramRenderer<2> for CharDataRenderer { +impl ImageRenderer<2> for CharDataRenderer { type Params = CharDataParams; fn sizes(&self) -> [[usize; 2]; 2] { [[8, 8], [16 * 8, 128 * 8]] } - fn render(&mut self, params: &Self::Params, image: &mut [VramImage; 2]) { + fn render(&mut self, params: &Self::Params, image: &mut [ImageBuffer; 2]) { self.render_character(&mut image[0], params.palette, params.index); self.render_character_data(&mut image[1], params.palette); } @@ -314,7 +314,7 @@ impl CharDataRenderer { } } - fn render_character(&self, image: &mut VramImage, palette: VramPalette, index: usize) { + fn render_character(&self, image: &mut ImageBuffer, palette: Palette, index: usize) { if index >= 2048 { return; } @@ -329,7 +329,7 @@ impl CharDataRenderer { } } - fn render_character_data(&self, image: &mut VramImage, palette: VramPalette) { + fn render_character_data(&self, image: &mut ImageBuffer, palette: Palette) { let palette = self.load_palette(palette); let chardata = self.chardata.borrow(); for (row, pixels) in chardata.range::(0, 8 * 2048).enumerate() { @@ -344,7 +344,7 @@ impl CharDataRenderer { } } - fn load_palette(&self, palette: VramPalette) -> [Color32; 4] { + fn load_palette(&self, palette: Palette) -> [Color32; 4] { let Some(offset) = palette.offset() else { return utils::GENERIC_PALETTE.map(|p| utils::shade(p, Color32::RED)); }; diff --git a/src/window/vram/framebuffer.rs b/src/window/vip/framebuffer.rs similarity index 93% rename from src/window/vram/framebuffer.rs rename to src/window/vip/framebuffer.rs index 3a2e722..ecd8e74 100644 --- a/src/window/vram/framebuffer.rs +++ b/src/window/vip/framebuffer.rs @@ -8,8 +8,8 @@ use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use crate::{ emulator::SimId, + images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader}, memory::{MemoryClient, MemoryView}, - vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, window::{ utils::{NumberEdit, UiExt as _}, AppWindow, @@ -20,17 +20,17 @@ use super::utils; pub struct FrameBufferWindow { sim_id: SimId, - loader: Arc, + loader: Arc, index: usize, left: bool, right: bool, generic_palette: bool, - params: VramParams, + params: ImageParams, scale: f32, } impl FrameBufferWindow { - pub fn new(sim_id: SimId, memory: &MemoryClient, vram: &mut VramProcessor) -> Self { + pub fn new(sim_id: SimId, memory: &MemoryClient, images: &mut ImageProcessor) -> Self { let initial_params = FrameBufferParams { index: 0, left: true, @@ -40,8 +40,8 @@ impl FrameBufferWindow { 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)]); + let ([buffer], params) = images.add(renderer, initial_params); + let loader = ImageTextureLoader::new([("vip://buffer".into(), buffer)]); Self { sim_id, loader: Arc::new(loader), @@ -131,7 +131,7 @@ impl FrameBufferWindow { } fn show_buffers(&mut self, ui: &mut Ui) { - let image = Image::new("vram://buffer") + let image = Image::new("vip://buffer") .fit_to_original_size(self.scale) .texture_options(TextureOptions::NEAREST); ui.add(image); @@ -205,14 +205,14 @@ impl FrameBufferRenderer { } } -impl VramRenderer<1> for FrameBufferRenderer { +impl ImageRenderer<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]) { + fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 1]) { let image = &mut images[0]; let left_buffer = self.buffers[params.index * 2].borrow(); diff --git a/src/window/vram/object.rs b/src/window/vip/object.rs similarity index 93% rename from src/window/vram/object.rs rename to src/window/vip/object.rs index 656c2d1..0899113 100644 --- a/src/window/vram/object.rs +++ b/src/window/vip/object.rs @@ -8,8 +8,8 @@ use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use crate::{ emulator::SimId, + images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader}, memory::{MemoryClient, MemoryView}, - vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, window::{ utils::{NumberEdit, UiExt as _}, AppWindow, @@ -20,17 +20,17 @@ use super::utils::{self, Object}; pub struct ObjectWindow { sim_id: SimId, - loader: Arc, + loader: Arc, memory: Arc, objects: MemoryView, index: usize, generic_palette: bool, - params: VramParams, + params: ImageParams, scale: f32, } impl ObjectWindow { - pub fn new(sim_id: SimId, memory: &Arc, vram: &mut VramProcessor) -> Self { + pub fn new(sim_id: SimId, memory: &Arc, images: &mut ImageProcessor) -> Self { let initial_params = ObjectParams { index: 0, generic_palette: false, @@ -38,9 +38,9 @@ impl ObjectWindow { right_color: Color32::from_rgb(0x00, 0xc6, 0xf0), }; let renderer = ObjectRenderer::new(sim_id, memory); - let ([zoom, full], params) = vram.add(renderer, initial_params); + let ([zoom, full], params) = images.add(renderer, initial_params); let loader = - VramTextureLoader::new([("vram://zoom".into(), zoom), ("vram://full".into(), full)]); + ImageTextureLoader::new([("vip://zoom".into(), zoom), ("vip://full".into(), full)]); Self { sim_id, loader: Arc::new(loader), @@ -82,7 +82,7 @@ impl ObjectWindow { }); }); }); - let image = Image::new("vram://zoom") + let image = Image::new("vip://zoom") .maintain_aspect_ratio(true) .texture_options(TextureOptions::NEAREST); ui.add(image); @@ -186,7 +186,7 @@ impl ObjectWindow { } fn show_object(&mut self, ui: &mut Ui) { - let image = Image::new("vram://full") + let image = Image::new("vip://full") .fit_to_original_size(self.scale) .texture_options(TextureOptions::NEAREST); ui.add(image); @@ -261,7 +261,13 @@ impl ObjectRenderer { } } - fn render_object(&self, image: &mut VramImage, params: &ObjectParams, use_pos: bool, eye: Eye) { + fn render_object( + &self, + image: &mut ImageBuffer, + params: &ObjectParams, + use_pos: bool, + eye: Eye, + ) { let chardata = self.chardata.borrow(); let objects = self.objects.borrow(); let brightness = self.brightness.borrow(); @@ -318,14 +324,14 @@ impl ObjectRenderer { } } -impl VramRenderer<2> for ObjectRenderer { +impl ImageRenderer<2> for ObjectRenderer { type Params = ObjectParams; fn sizes(&self) -> [[usize; 2]; 2] { [[8, 8], [384, 224]] } - fn render(&mut self, params: &Self::Params, images: &mut [VramImage; 2]) { + fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 2]) { images[0].clear(); self.render_object(&mut images[0], params, false, Eye::Left); self.render_object(&mut images[0], params, false, Eye::Right); diff --git a/src/window/vram/registers.rs b/src/window/vip/registers.rs similarity index 100% rename from src/window/vram/registers.rs rename to src/window/vip/registers.rs diff --git a/src/window/vram/utils.rs b/src/window/vip/utils.rs similarity index 100% rename from src/window/vram/utils.rs rename to src/window/vip/utils.rs diff --git a/src/window/vram/world.rs b/src/window/vip/world.rs similarity index 98% rename from src/window/vram/world.rs rename to src/window/vip/world.rs index 44258d0..c4c22a3 100644 --- a/src/window/vram/world.rs +++ b/src/window/vip/world.rs @@ -14,8 +14,8 @@ use num_traits::{FromPrimitive, ToPrimitive}; use crate::{ emulator::SimId, + images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader}, memory::{MemoryClient, MemoryRef, MemoryView}, - vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, window::{ utils::{NumberEdit, UiExt as _}, AppWindow, @@ -26,19 +26,19 @@ use super::utils::{self, shade, CellData, Object}; pub struct WorldWindow { sim_id: SimId, - loader: Arc, + loader: Arc, memory: Arc, worlds: MemoryView, bgmaps: MemoryView, index: usize, param_index: usize, generic_palette: bool, - params: VramParams, + params: ImageParams, scale: f32, } impl WorldWindow { - pub fn new(sim_id: SimId, memory: &Arc, vram: &mut VramProcessor) -> Self { + pub fn new(sim_id: SimId, memory: &Arc, images: &mut ImageProcessor) -> Self { let initial_params = WorldParams { index: 31, generic_palette: false, @@ -46,8 +46,8 @@ impl WorldWindow { right_color: Color32::from_rgb(0x00, 0xc6, 0xf0), }; let renderer = WorldRenderer::new(sim_id, memory); - let ([world], params) = vram.add(renderer, initial_params); - let loader = VramTextureLoader::new([("vram://world".into(), world)]); + let ([world], params) = images.add(renderer, initial_params); + let loader = ImageTextureLoader::new([("vip://world".into(), world)]); Self { sim_id, loader: Arc::new(loader), @@ -423,7 +423,7 @@ impl WorldWindow { } fn show_world(&mut self, ui: &mut Ui) { - let image = Image::new("vram://world") + let image = Image::new("vip://world") .fit_to_original_size(self.scale) .texture_options(TextureOptions::NEAREST); ui.add(image); @@ -507,7 +507,7 @@ impl WorldRenderer { } } - fn render_object_world(&mut self, group: usize, params: &WorldParams, image: &mut VramImage) { + fn render_object_world(&mut self, group: usize, params: &WorldParams, image: &mut ImageBuffer) { for cell in self.buffer.iter_mut() { *cell = [0, 0]; } @@ -604,7 +604,7 @@ impl WorldRenderer { } } - fn render_world(&mut self, world: World, params: &WorldParams, image: &mut VramImage) { + fn render_world(&mut self, world: World, params: &WorldParams, image: &mut ImageBuffer) { image.clear(); let width = if world.header.mode == WorldMode::Affine { @@ -687,14 +687,14 @@ impl WorldRenderer { } } -impl VramRenderer<1> for WorldRenderer { +impl ImageRenderer<1> for WorldRenderer { type Params = WorldParams; fn sizes(&self) -> [[usize; 2]; 1] { [[384, 224]] } - fn render(&mut self, params: &Self::Params, images: &mut [VramImage; 1]) { + fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 1]) { let image = &mut images[0]; let worlds = self.worlds.borrow(); -- 2.40.1 From 49343b6903cd22333d1c558ee2525535d3ef6f70 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 23 Feb 2025 22:48:30 -0500 Subject: [PATCH 33/34] Separate VIP tools from CPU --- src/window/game.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/window/game.rs b/src/window/game.rs index 8d91146..53d6484 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -132,6 +132,7 @@ impl GameWindow { .unwrap(); ui.close_menu(); } + ui.separator(); if ui.button("Character Data").clicked() { self.proxy .send_event(UserEvent::OpenCharacterData(self.sim_id)) -- 2.40.1 From ad994f885112eab06970b3e24a146147c4c4937d Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 23 Feb 2025 22:59:14 -0500 Subject: [PATCH 34/34] Fix typo in macro --- src/memory.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/memory.rs b/src/memory.rs index 92b10d5..2670bd5 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -98,11 +98,11 @@ macro_rules! primitive_memory_value_impl { } primitive_memory_value_impl!(u8, 1); -primitive_memory_value_impl!(i8, 2); +primitive_memory_value_impl!(i8, 1); primitive_memory_value_impl!(u16, 2); primitive_memory_value_impl!(i16, 2); primitive_memory_value_impl!(u32, 4); -primitive_memory_value_impl!(i32, 2); +primitive_memory_value_impl!(i32, 4); impl MemoryValue for [T; N] { #[inline] -- 2.40.1