From a461faf89d474899f0c40ea3cb19350a35bdec4c Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 9 Feb 2025 16:17:08 -0500 Subject: [PATCH] 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 { } } } + */