diff --git a/src/app.rs b/src/app.rs index 50661ca..0df022f 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, @@ -39,11 +39,11 @@ fn load_icon() -> anyhow::Result { pub struct Application { icon: Option>, - vram: Arc, client: EmulatorClient, proxy: EventLoopProxy, mappings: MappingProvider, controllers: ControllerManager, + memory: MemoryMonitor, persistence: Persistence, viewports: HashMap, focused: Option, @@ -60,6 +60,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(); @@ -67,10 +68,10 @@ impl Application { } Self { icon, - vram: Arc::new(VramLoader::new(client.clone())), client, proxy, mappings, + memory, controllers, persistence, viewports: HashMap::new(), @@ -86,7 +87,7 @@ impl Application { } self.viewports.insert( viewport_id, - Viewport::new(event_loop, self.icon.clone(), self.vram.clone(), window), + Viewport::new(event_loop, self.icon.clone(), window), ); } } @@ -201,11 +202,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) => { @@ -266,7 +267,6 @@ impl Viewport { pub fn new( event_loop: &ActiveEventLoop, icon: Option>, - vram: Arc, mut app: Box, ) -> Self { let ctx = Context::default(); @@ -288,7 +288,6 @@ impl Viewport { s.visuals.menu_rounding = Default::default(); }); egui_extras::install_image_loaders(&ctx); - ctx.add_image_loader(vram); #[allow(unused_mut)] let mut wgpu_config = egui_wgpu::WgpuConfiguration { @@ -316,7 +315,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 6c50736..ec7b687 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -381,7 +381,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], + ] +}