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 854b0c3..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" @@ -35,7 +36,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 2cfa0a7..8030986 100644 --- a/src/app.rs +++ b/src/app.rs @@ -17,9 +17,14 @@ use winit::{ use crate::{ controller::ControllerManager, emulator::{EmulatorClient, EmulatorCommand, SimId}, + images::ImageProcessor, input::MappingProvider, + memory::MemoryClient, persistence::Persistence, - window::{AboutWindow, AppWindow, GameWindow, GdbServerWindow, InputWindow}, + window::{ + AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow, + GdbServerWindow, InputWindow, ObjectWindow, RegisterWindow, WorldWindow, + }, }; fn load_icon() -> anyhow::Result { @@ -40,6 +45,8 @@ pub struct Application { proxy: EventLoopProxy, mappings: MappingProvider, controllers: ControllerManager, + memory: Arc, + images: ImageProcessor, persistence: Persistence, viewports: HashMap, focused: Option, @@ -57,6 +64,8 @@ impl Application { let persistence = Persistence::new(); let mappings = MappingProvider::new(persistence.clone()); let controllers = ControllerManager::new(client.clone(), &mappings); + let memory = Arc::new(MemoryClient::new(client.clone())); + let images = ImageProcessor::new(); { let mappings = mappings.clone(); let proxy = proxy.clone(); @@ -68,6 +77,8 @@ impl Application { client, proxy, mappings, + memory, + images, controllers, persistence, viewports: HashMap::new(), @@ -119,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) { @@ -183,20 +198,45 @@ 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; self.open(event_loop, Box::new(about)); } + UserEvent::OpenCharacterData(sim_id) => { + 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.images); + self.open(event_loop, Box::new(bgmap)); + } + UserEvent::OpenObjects(sim_id) => { + 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.images); + self.open(event_loop, Box::new(world)); + } + UserEvent::OpenFrameBuffers(sim_id) => { + let world = FrameBufferWindow::new(sim_id, &self.memory, &mut self.images); + 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()); @@ -352,7 +392,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, @@ -369,8 +409,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(), @@ -378,22 +418,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 { @@ -455,6 +495,12 @@ impl Drop for Viewport { pub enum UserEvent { GamepadEvent(gilrs::Event), OpenAbout, + OpenCharacterData(SimId), + OpenBgMap(SimId), + OpenObjects(SimId), + OpenWorlds(SimId), + OpenFrameBuffers(SimId), + OpenRegisters(SimId), OpenDebugger(SimId), OpenInput, OpenPlayer2, 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/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/images.rs b/src/images.rs new file mode 100644 index 0000000..1730aa8 --- /dev/null +++ b/src/images.rs @@ -0,0 +1,328 @@ +use std::{ + collections::HashMap, + ops::Deref, + sync::{Arc, Mutex, Weak}, + thread, + time::Duration, +}; + +use egui::{ + epaint::ImageDelta, + load::{LoadError, SizedTexture, TextureLoader, TexturePoll}, + Color32, ColorImage, TextureHandle, TextureOptions, +}; +use tokio::{sync::mpsc, time::timeout}; + +pub struct ImageProcessor { + sender: mpsc::UnboundedSender>, +} + +impl ImageProcessor { + 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 = ImageProcessorWorker { + receiver, + renderers: vec![], + }; + worker.run().await + }) + }); + Self { sender } + } + + pub fn add + 'static>( + &self, + renderer: R, + params: R::Params, + ) -> ([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]| ImageBuffer::new(width, height)); + let sink = Arc::new(Mutex::new(params.clone())); + let _ = self.sender.send(Box::new(ImageRendererWrapper { + renderer, + params: Arc::downgrade(&sink), + images, + states, + })); + let params = ImageParams { + value: params, + sink, + }; + (handles, params) + } +} + +struct ImageProcessorWorker { + receiver: mpsc::UnboundedReceiver>, + renderers: Vec>, +} + +impl ImageProcessorWorker { + 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); + } + } + } + } +} + +pub struct ImageBuffer { + pub size: [usize; 2], + pub pixels: Vec, +} + +impl ImageBuffer { + pub fn new(width: usize, height: usize) -> Self { + Self { + size: [width, height], + pixels: vec![Color32::BLACK; width * height], + } + } + + pub fn clear(&mut self) { + for pixel in self.pixels.iter_mut() { + *pixel = Color32::BLACK; + } + } + + pub fn write(&mut self, coords: (usize, usize), pixel: Color32) { + 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.pixels[index]; + self.pixels[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().zip(&self.pixels).any(|(a, b)| a != b) + } + + pub fn read(&self, image: &mut ColorImage) { + image.pixels.copy_from_slice(&self.pixels); + } +} + +#[derive(Clone)] +pub struct ImageHandle { + size: [f32; 2], + data: Arc>>>, +} + +impl ImageHandle { + fn pull(&mut self) -> Option> { + self.data.lock().unwrap().take() + } +} + +pub struct ImageParams { + value: T, + sink: Arc>, +} + +impl Deref for ImageParams { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl ImageParams { + pub fn write(&mut self, value: T) { + if self.value != value { + self.value = value.clone(); + *self.sink.lock().unwrap() = value; + } + } +} + +pub trait ImageRenderer: Send { + type Params: Clone + Send; + fn sizes(&self) -> [[usize; 2]; N]; + fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; N]); +} + +#[derive(Clone)] +struct ImageState { + size: [usize; 2], + buffers: [Arc; 2], + last_buffer: usize, + sink: Arc>>>, +} + +impl ImageState { + 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: &ImageBuffer) { + 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 ImageRendererWrapper> { + renderer: R, + params: Weak>, + images: [ImageBuffer; N], + states: [ImageState; N], +} + +trait ImageRendererImpl: Send { + fn try_update(&mut self) -> Result<(), ()>; +} + +impl + Send> ImageRendererImpl for ImageRendererWrapper { + 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 (state, image) in self.states.iter_mut().zip(&self.images) { + state.try_send_update(image); + } + Ok(()) + } +} + +pub struct ImageTextureLoader { + cache: Mutex)>>, +} + +impl ImageTextureLoader { + 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 ImageTextureLoader { + fn id(&self) -> &str { + concat!(module_path!(), "ImageTextureLoader") + } + + fn load( + &self, + ctx: &egui::Context, + uri: &str, + texture_options: TextureOptions, + _size_hint: egui::SizeHint, + ) -> Result { + 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 { + return Err(LoadError::Loading( + "Only TextureOptions::NEAREST are supported".into(), + )); + } + 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); + let texture = SizedTexture::new(handle, image.size); + Ok(TexturePoll::Ready { texture }) + } + (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) { + let _ = uri; + } + + fn forget_all(&self) {} + + fn byte_size(&self) -> usize { + self.cache + .lock() + .unwrap() + .values() + .map(|(image, _)| { + let [width, height] = image.size; + width as usize * height as usize * 4 + }) + .sum() + } +} diff --git a/src/main.rs b/src/main.rs index 33ae747..eb160b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,9 @@ mod controller; mod emulator; mod gdbserver; mod graphics; +mod images; mod input; +mod memory; mod persistence; mod window; diff --git a/src/memory.rs b/src/memory.rs new file mode 100644 index 0000000..2670bd5 --- /dev/null +++ b/src/memory.rs @@ -0,0 +1,276 @@ +use std::{ + collections::HashMap, + fmt::Debug, + iter::FusedIterator, + sync::{atomic::AtomicU64, Arc, Mutex, RwLock, RwLockReadGuard, TryLockError, Weak}, +}; + +use bytemuck::BoxBytes; +use itertools::Itertools; +use tracing::warn; + +use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; + +pub struct MemoryClient { + client: EmulatorClient, + regions: Mutex>>, +} + +impl MemoryClient { + pub fn new(client: EmulatorClient) -> Self { + Self { + client, + regions: Mutex::new(HashMap::new()), + } + } + + pub fn watch(&self, sim: SimId, start: u32, length: usize) -> MemoryView { + let range = MemoryRange { sim, start, length }; + 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)); + 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 mut buffer = vec![]; + data.to_bytes(&mut buffer); + let (tx, _) = oneshot::channel(); + self.client + .send_command(EmulatorCommand::WriteMemory(sim, address, buffer, tx)); + } +} + +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 { + region: Arc, +} + +impl MemoryView { + pub fn borrow(&self) -> MemoryRef<'_> { + self.region.borrow() + } +} + +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!(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, 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], + _phantom: std::marker::PhantomData, +} + +impl<'a, T> MemoryIter<'a, T> { + fn new(bytes: &'a [u8]) -> Self { + Self { + bytes, + _phantom: std::marker::PhantomData, + } + } +} + +impl Iterator for MemoryIter<'_, T> { + type Item = T; + + #[inline] + fn next(&mut self) -> Option { + 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::(); + let to = from + size_of::(); + T::from_bytes(&self.inner[from..to]) + } + + pub fn range(&self, start: usize, count: usize) -> MemoryIter { + let from = start * size_of::(); + let to = from + (count * size_of::()); + MemoryIter::new(&self.inner[from..to]) + } +} + +#[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 = self + .gens + .each_ref() + .map(|i| i.load(std::sync::atomic::Ordering::Acquire)); + 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 "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 + * 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.rs b/src/window.rs index 85048ff..8ad5146 100644 --- a/src/window.rs +++ b/src/window.rs @@ -3,6 +3,9 @@ use egui::{Context, ViewportBuilder, ViewportId}; pub use game::GameWindow; pub use gdb::GdbServerWindow; pub use input::InputWindow; +pub use vip::{ + BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow, +}; use winit::event::KeyEvent; use crate::emulator::SimId; @@ -12,6 +15,8 @@ mod game; mod game_screen; mod gdb; mod input; +mod utils; +mod vip; pub trait AppWindow { fn viewport_id(&self) -> ViewportId; @@ -20,14 +25,17 @@ 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) {} - 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/game.rs b/src/window/game.rs index b3406bc..53d6484 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, }; @@ -132,10 +132,49 @@ impl GameWindow { .unwrap(); ui.close_menu(); } + ui.separator(); + if ui.button("Character Data").clicked() { + self.proxy + .send_event(UserEvent::OpenCharacterData(self.sim_id)) + .unwrap(); + ui.close_menu(); + } + if ui.button("Background Maps").clicked() { + self.proxy + .send_event(UserEvent::OpenBgMap(self.sim_id)) + .unwrap(); + ui.close_menu(); + } + if ui.button("Objects").clicked() { + self.proxy + .send_event(UserEvent::OpenObjects(self.sim_id)) + .unwrap(); + ui.close_menu(); + } + if ui.button("Worlds").clicked() { + self.proxy + .send_event(UserEvent::OpenWorlds(self.sim_id)) + .unwrap(); + ui.close_menu(); + } + if ui.button("Frame Buffers").clicked() { + self.proxy + .send_event(UserEvent::OpenFrameBuffers(self.sim_id)) + .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("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(); + } }); } @@ -365,7 +404,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( @@ -385,69 +424,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/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, } } } diff --git a/src/window/utils.rs b/src/window/utils.rs new file mode 100644 index 0000000..cde57ab --- /dev/null +++ b/src/window/utils.rs @@ -0,0 +1,417 @@ +use std::{ + 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, + WidgetText, +}; +use num_traits::{CheckedAdd, CheckedSub, One}; + +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; +} + +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 = self.push_id(&title, |ui| { + frame.show(ui, |ui| { + ui.set_max_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; + text_rect.min.x += 6.0; + 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 { + 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 + } +} + +enum Direction { + Up, + Down, +} + +pub trait Number: + Copy + + One + + CheckedAdd + + CheckedSub + + Eq + + Ord + + Display + + FromStr + + FromRadix16 + + UpperHex + + Send + + Sync + + 'static +{ +} +impl< + T: Copy + + One + + CheckedAdd + + CheckedSub + + Eq + + Ord + + Display + + FromStr + + FromRadix16 + + UpperHex + + Send + + Sync + + 'static, + > Number for T +{ +} + +pub struct NumberEdit<'a, T: Number> { + value: &'a mut T, + increment: T, + precision: usize, + min: Option, + max: Option, + desired_width: Option, + arrows: bool, + hex: bool, +} + +impl<'a, T: Number> NumberEdit<'a, T> { + pub fn new(value: &'a mut T) -> Self { + Self { + value, + increment: T::one(), + precision: 3, + min: None, + max: None, + desired_width: None, + arrows: true, + hex: false, + } + } + + 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, + Bound::Included(t) => Some(*t), + 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(&self.increment), + }; + 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 } + } + + 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| { + 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 + .data + .get_temp(id) + .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 = to_string(self.value); + stale = true; + } + 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 { + 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 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 }, + top: 2.0, + bottom: 2.0, + }); + 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 {} 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| { + 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 mut delta = None; + 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 = + |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) { + 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) = from_string(&str).filter(in_range) { + if *self.value != new_value { + res.mark_changed(); + } + *self.value = new_value; + } + stale = true; + } + if stale { + ui.memory_mut(|m| m.data.insert_temp(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: 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); + + 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/vip.rs b/src/window/vip.rs new file mode 100644 index 0000000..595ba88 --- /dev/null +++ b/src/window/vip.rs @@ -0,0 +1,14 @@ +mod bgmap; +mod chardata; +mod framebuffer; +mod object; +mod registers; +mod utils; +mod world; + +pub use bgmap::*; +pub use chardata::*; +pub use framebuffer::*; +pub use object::*; +pub use registers::*; +pub use world::*; diff --git a/src/window/vip/bgmap.rs b/src/window/vip/bgmap.rs new file mode 100644 index 0000000..4a7a8c7 --- /dev/null +++ b/src/window/vip/bgmap.rs @@ -0,0 +1,315 @@ +use std::sync::Arc; + +use egui::{ + 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, + images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader}, + memory::{MemoryClient, MemoryView}, + window::{ + utils::{NumberEdit, UiExt}, + AppWindow, + }, +}; + +use super::utils::{self, CellData, CharacterGrid}; + +pub struct BgMapWindow { + sim_id: SimId, + loader: Arc, + memory: Arc, + bgmaps: MemoryView, + cell_index: usize, + generic_palette: bool, + params: ImageParams, + scale: f32, + show_grid: bool, +} + +impl BgMapWindow { + pub fn new(sim_id: SimId, memory: &Arc, images: &mut ImageProcessor) -> Self { + let renderer = BgMapRenderer::new(sim_id, memory); + let ([cell, bgmap], params) = images.add(renderer, BgMapParams::default()); + let loader = + ImageTextureLoader::new([("vip://cell".into(), cell), ("vip://bgmap".into(), bgmap)]); + Self { + sim_id, + loader: Arc::new(loader), + memory: memory.clone(), + bgmaps: memory.watch(sim_id, 0x00020000, 0x20000), + cell_index: params.cell_index, + generic_palette: params.generic_palette, + params, + scale: 1.0, + show_grid: 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; + 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); + } + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Cell"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut self.cell_index).range(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 image = Image::new("vip://cell") + .maintain_aspect_ratio(true) + .texture_options(TextureOptions::NEAREST); + ui.add(image); + ui.section("Cell", |ui| { + 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()) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Character"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut cell.char_index).range(0..2048)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Palette"); + }); + row.col(|ui| { + 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| { + ui.add(Checkbox::new(&mut cell.hflip, "H-flip")); + }); + row.col(|ui| { + 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| { + 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 { + cell_index: self.cell_index, + generic_palette: self.generic_palette, + }); + } + + fn show_bgmap(&mut self, ui: &mut Ui) { + let grid = CharacterGrid::new("vip://bgmap") + .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; + } + } +} + +impl AppWindow for BgMapWindow { + fn viewport_id(&self) -> ViewportId { + 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) { + 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).at_most(200.0)) + .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)); + }); + }) + }); + }); + } +} + +#[derive(Default, Clone, PartialEq, Eq)] +struct BgMapParams { + cell_index: usize, + generic_palette: bool, +} + +struct BgMapRenderer { + chardata: MemoryView, + bgmaps: MemoryView, + brightness: MemoryView, + palettes: MemoryView, +} + +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, 0x20000), + brightness: memory.watch(sim_id, 0x0005f824, 8), + palettes: memory.watch(sim_id, 0x0005f860, 16), + } + } + + 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(); + let palettes = self.palettes.borrow(); + + 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::palette_colors(palettes.read(i), &brts, Color32::RED)) + }; + + 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.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() { + let x = col + (i % 64) * 8; + image.write((x, y), palette[pixel as usize]); + } + } + } + } + + 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(); + let palettes = self.palettes.borrow(); + + let brts = brightness.read::<[u8; 8]>(0); + + let cell = bgmaps.read::(index); + + let CellData { + char_index, + vflip, + hflip, + palette_index, + } = CellData::parse(cell); + let char = chardata.read::<[u16; 8]>(char_index); + let palette = if generic_palette { + utils::generic_palette(Color32::RED) + } else { + utils::palette_colors(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() { + image.write((col, row), palette[pixel as usize]); + } + } + } +} + +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 [ImageBuffer; 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/vip/chardata.rs b/src/window/vip/chardata.rs new file mode 100644 index 0000000..3bddc7a --- /dev/null +++ b/src/window/vip/chardata.rs @@ -0,0 +1,356 @@ +use std::{fmt::Display, sync::Arc}; + +use egui::{ + 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}; + +use crate::{ + emulator::SimId, + images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader}, + memory::{MemoryClient, MemoryView}, + window::{ + utils::{NumberEdit, UiExt as _}, + AppWindow, + }, +}; + +use super::utils::{self, CharacterGrid}; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum Palette { + #[default] + Generic, + Bg0, + Bg1, + Bg2, + Bg3, + Obj0, + Obj1, + Obj2, + Obj3, +} + +impl Palette { + pub const fn values() -> [Palette; 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 Palette { + 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: Arc, + brightness: MemoryView, + palettes: MemoryView, + palette: Palette, + index: usize, + params: ImageParams, + scale: f32, + show_grid: bool, +} + +impl CharacterDataWindow { + pub fn new(sim_id: SimId, memory: &MemoryClient, images: &mut ImageProcessor) -> Self { + let renderer = CharDataRenderer::new(sim_id, memory); + let ([char, chardata], params) = images.add(renderer, CharDataParams::default()); + let loader = ImageTextureLoader::new([ + ("vip://char".into(), char), + ("vip://chardata".into(), chardata), + ]); + Self { + sim_id, + loader: Arc::new(loader), + brightness: memory.watch(sim_id, 0x0005f824, 8), + palettes: memory.watch(sim_id, 0x0005f860, 16), + palette: params.palette, + index: params.index, + params, + 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| { + ui.add(NumberEdit::new(&mut self.index).range(0..2048)); + }); + }); + 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 * 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}"); + 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 image = Image::new("vip://char") + .maintain_aspect_ratio(true) + .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 Palette::values() { + ui.selectable_value( + &mut self.palette, + palette, + palette.to_string(), + ); + } + }); + }); + TableBuilder::new(ui) + .columns(Column::remainder(), 4) + .body(|mut body| { + let palette = self.load_palette_colors(); + body.row(30.0, |mut row| { + 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)); + ui.painter().rect_filled(rect, 0.0, color); + }); + } + }); + }); + }); + 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"); + }); + }); + + self.params.write(CharDataParams { + palette: self.palette, + index: self.index, + }); + } + + fn load_palette_colors(&self) -> [Color32; 4] { + let Some(offset) = self.palette.offset() else { + return utils::generic_palette(Color32::RED); + }; + let palette = self.palettes.borrow().read(offset); + let brightnesses = self.brightness.borrow(); + let brts = brightnesses.read(0); + utils::palette_colors(palette, &brts, Color32::RED) + } + + fn show_chardata(&mut self, ui: &mut Ui) { + let grid = CharacterGrid::new("vip://chardata") + .with_scale(self.scale) + .with_grid(self.show_grid) + .with_selected(self.index); + if let Some(selected) = grid.show(ui) { + self.index = selected; + } + } +} + +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 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).at_most(200.0)) + .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)); + }); + }); + }); + }); + } +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +enum CharDataResource { + Character { palette: Palette, index: usize }, + CharacterData { palette: Palette }, +} + +#[derive(Clone, Default, PartialEq, Eq)] +struct CharDataParams { + palette: Palette, + index: usize, +} + +struct CharDataRenderer { + chardata: MemoryView, + brightness: MemoryView, + palettes: MemoryView, +} + +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 [ImageBuffer; 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: &MemoryClient) -> Self { + Self { + chardata: memory.watch(sim_id, 0x00078000, 0x8000), + brightness: memory.watch(sim_id, 0x0005f824, 8), + palettes: memory.watch(sim_id, 0x0005f860, 16), + } + } + + fn render_character(&self, image: &mut ImageBuffer, palette: Palette, 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.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 ImageBuffer, palette: Palette) { + let palette = self.load_palette(palette); + let chardata = self.chardata.borrow(); + 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; + 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: Palette) -> [Color32; 4] { + let Some(offset) = palette.offset() else { + 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.read(0); + utils::palette_colors(palette, &brts, Color32::RED) + } +} diff --git a/src/window/vip/framebuffer.rs b/src/window/vip/framebuffer.rs new file mode 100644 index 0000000..ecd8e74 --- /dev/null +++ b/src/window/vip/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, + images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader}, + memory::{MemoryClient, MemoryView}, + 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: ImageParams, + scale: f32, +} + +impl FrameBufferWindow { + pub fn new(sim_id: SimId, memory: &MemoryClient, images: &mut ImageProcessor) -> 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) = images.add(renderer, initial_params); + let loader = ImageTextureLoader::new([("vip://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("vip://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).at_most(200.0)) + .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 ImageRenderer<1> for FrameBufferRenderer { + type Params = FrameBufferParams; + + fn sizes(&self) -> [[usize; 2]; 1] { + [[384, 224]] + } + + 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(); + 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); + } + } + } +} diff --git a/src/window/vip/object.rs b/src/window/vip/object.rs new file mode 100644 index 0000000..0899113 --- /dev/null +++ b/src/window/vip/object.rs @@ -0,0 +1,342 @@ +use std::sync::Arc; + +use egui::{ + 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, + images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader}, + memory::{MemoryClient, MemoryView}, + window::{ + utils::{NumberEdit, UiExt as _}, + AppWindow, + }, +}; + +use super::utils::{self, Object}; + +pub struct ObjectWindow { + sim_id: SimId, + loader: Arc, + memory: Arc, + objects: MemoryView, + index: usize, + generic_palette: bool, + params: ImageParams, + scale: f32, +} + +impl ObjectWindow { + pub fn new(sim_id: SimId, memory: &Arc, images: &mut ImageProcessor) -> 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) = images.add(renderer, initial_params); + let loader = + ImageTextureLoader::new([("vip://zoom".into(), zoom), ("vip://full".into(), full)]); + Self { + sim_id, + loader: Arc::new(loader), + memory: memory.clone(), + objects: memory.watch(sim_id, 0x0003e000, 0x2000), + 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..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("vip://zoom") + .maintain_aspect_ratio(true) + .texture_options(TextureOptions::NEAREST); + ui.add(image); + ui.section("Properties", |ui| { + let mut object = self.objects.borrow().read::<[u16; 4]>(self.index); + let mut obj = Object::parse(object); + 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(NumberEdit::new(&mut obj.data.char_index).range(0..2048)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Palette"); + }); + row.col(|ui| { + 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| { + row.col(|ui| { + ui.label("X"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut obj.x).range(-512..512)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Y"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut obj.y).range(-8..=224)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Parallax"); + }); + row.col(|ui| { + ui.add(NumberEdit::new(&mut obj.parallax).range(-512..512)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.add(Checkbox::new(&mut obj.data.hflip, "H-flip")); + }); + row.col(|ui| { + ui.add(Checkbox::new(&mut obj.data.vflip, "V-flip")); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.add(Checkbox::new(&mut obj.lon, "Left")); + }); + row.col(|ui| { + 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| { + 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, + ..*self.params + }); + } + + fn show_object(&mut self, ui: &mut Ui) { + let image = Image::new("vip://full") + .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, 500.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).at_most(200.0)) + .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, +} + +enum Eye { + Left, + Right, +} + +struct ObjectRenderer { + chardata: MemoryView, + objects: MemoryView, + brightness: MemoryView, + palettes: MemoryView, +} + +impl ObjectRenderer { + pub fn new(sim_id: SimId, memory: &MemoryClient) -> Self { + Self { + 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), + } + } + + 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(); + let palettes = self.palettes.borrow(); + + let object: [u16; 4] = objects.read(params.index); + let obj = Object::parse(object); + + if match eye { + Eye::Left => !obj.lon, + Eye::Right => !obj.ron, + } { + return; + } + + let brts = brightness.read::<[u8; 8]>(0); + 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 color = match eye { + Eye::Left => params.left_color, + Eye::Right => params.right_color, + }; + + let char = chardata.read::<[u16; 8]>(obj.data.char_index); + let palette = if params.generic_palette { + utils::generic_palette(color) + } else { + utils::palette_colors(palettes.read(8 + obj.data.palette_index * 2), &brts, color) + }; + + for row in 0..8 { + let real_y = y + row as i16; + if !(0..224).contains(&real_y) { + continue; + } + 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..384).contains(&real_x) { + continue; + } + image.add((real_x as usize, real_y as usize), palette[pixel as usize]); + } + } + } +} + +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 [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); + 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/vip/registers.rs b/src/window/vip/registers.rs new file mode 100644 index 0000000..1a1f303 --- /dev/null +++ b/src/window/vip/registers.rs @@ -0,0 +1,819 @@ +use std::sync::Arc; + +use egui::{ + 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, MemoryRef, MemoryValue, MemoryView}, + window::{ + utils::{NumberEdit, UiExt}, + AppWindow, + }, +}; + +use super::utils; + +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, 0x72), + } + } + + fn show_interrupts(&mut self, ui: &mut Ui) { + 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); + 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 - xspace), 2) + .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_sized( + ui.available_size(), + 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::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| { + row.col(|_ui| {}); + row.col(|ui| { + ui.add_sized(ui.available_size(), Label::new("ENB")); + }); + row.col(|ui| { + ui.add_sized(ui.available_size(), 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); + }); + }); + ui.allocate_space(ui.available_size()); + }); + 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); + } + } + + fn show_display_status(&mut self, ui: &mut Ui) { + 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| { + 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::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.allocate_space(ui.available_size()); + }); + 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 = 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| { + 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::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) + .arrows(false) + .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.allocate_space(ui.available_size()); + }); + if xpstts.update(&mut raw_xpstts) { + xpstts.update(&mut raw_xpstts); + self.memory.write(self.sim_id, 0x0005f842, &raw_xpstts); + } + } + + fn show_colors(&mut self, ui: &mut Ui) { + let row_height = self.row_height(ui); + let registers = self.registers.borrow(); + ui.section("Colors", |ui| { + let width = ui.available_width(); + let xspace = ui.spacing().item_spacing.x; + TableBuilder::new(ui) + .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| { + 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) + .desired_width(width * 0.2) + .arrows(false) + .hex(true), + ) + .changed() + { + stale = true; + }; + }); + row.col(|ui| { + if ui + .add( + NumberEdit::new(&mut c1) + .range(0..4) + .desired_width(ui.available_width() - xspace), + ) + .changed() + { + *value = (*value & 0xfff3) | (c1 << 2); + stale = true; + } + }); + row.col(|ui| { + if ui + .add( + NumberEdit::new(&mut c2) + .range(0..4) + .desired_width(ui.available_width() - xspace), + ) + .changed() + { + *value = (*value & 0xffcf) | (c2 << 4); + stale = true; + } + }); + row.col(|ui| { + if ui + .add( + NumberEdit::new(&mut c3) + .range(0..4) + .desired_width(ui.available_width() - xspace), + ) + .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) + .desired_width(ui.available_width() - xspace), + ) + .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 show_objects(&mut self, ui: &mut Ui) { + let row_height = self.row_height(ui); + ui.section("Objects", |ui| { + let width = ui.available_width(); + let xspace = ui.spacing().item_spacing.x; + TableBuilder::new(ui) + .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| { + 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(); + let xspace = ui.spacing().item_spacing.x; + TableBuilder::new(ui) + .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| { + 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) + } +} + +fn read_address(registers: &MemoryRef, address: usize) -> T { + let index = (address - 0x0005f800) / size_of::(); + registers.read(index) +} + +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((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(); + let xspace = ui.spacing().item_spacing.x; + StripBuilder::new(ui) + .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| { + 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); + }); + }); + }); + 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); + }); + }); + }); + }); + }); + }); + }); + } +} + +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 + } +} + +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 + } +} diff --git a/src/window/vip/utils.rs b/src/window/vip/utils.rs new file mode 100644 index 0000000..76df3bf --- /dev/null +++ b/src/window/vip/utils.rs @@ -0,0 +1,251 @@ +use egui::{Color32, Image, ImageSource, Response, Sense, TextureOptions, Ui, Widget}; + +pub const GENERIC_PALETTE: [u8; 4] = [0, 32, 64, 128]; + +pub fn shade(brt: u8, color: Color32) -> Color32 { + 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] { + GENERIC_PALETTE.map(|brt| shade(brt, color)) +} + +pub const fn parse_palette(palette: u8) -> [u8; 4] { + [ + 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 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]) +} + +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] & 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] & 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 }; + let data = CellData::parse(object[3]); + Self { + x, + lon, + ron, + parallax, + y, + 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 { + 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 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( + char: &[u16; 8], + 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 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, + 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) + .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 + } +} diff --git a/src/window/vip/world.rs b/src/window/vip/world.rs new file mode 100644 index 0000000..c4c22a3 --- /dev/null +++ b/src/window/vip/world.rs @@ -0,0 +1,1132 @@ +use std::{fmt::Display, sync::Arc}; + +use egui::{ + Align, CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextEdit, + 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, ToPrimitive}; + +use crate::{ + emulator::SimId, + images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader}, + memory::{MemoryClient, MemoryRef, MemoryView}, + window::{ + utils::{NumberEdit, UiExt as _}, + AppWindow, + }, +}; + +use super::utils::{self, shade, CellData, Object}; + +pub struct WorldWindow { + sim_id: SimId, + loader: Arc, + memory: Arc, + worlds: MemoryView, + bgmaps: MemoryView, + index: usize, + param_index: usize, + generic_palette: bool, + params: ImageParams, + scale: f32, +} + +impl WorldWindow { + pub fn new(sim_id: SimId, memory: &Arc, images: &mut ImageProcessor) -> 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) = images.add(renderer, initial_params); + let loader = ImageTextureLoader::new([("vip://world".into(), world)]); + Self { + sim_id, + loader: Arc::new(loader), + memory: memory.clone(), + worlds: memory.watch(sim_id, 0x0003d800, 0x400), + bgmaps: memory.watch(sim_id, 0x00020000, 0x20000), + index: params.index, + param_index: 0, + 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)); + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Address"); + }); + row.col(|ui| { + let address = 0x0003d800 + self.index * 32; + let mut address_str = format!("{address:08x}"); + ui.add_enabled( + false, + TextEdit::singleline(&mut address_str).horizontal_align(Align::Max), + ); + }); + }); + }); + let mut 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("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"); + }); + 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.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")); + }); + 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")); + }); + }); + }); + }); + 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) + .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(8) as usize; + ui.add(NumberEdit::new(&mut self.param_index).range(0..max)); + }); + }); + 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| { + 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)); + }); + }); + param.save(&self.memory, self.sim_id, base); + }); + }); + } 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)); + }); + }); + param.save(&self.memory, self.sim_id, base); + }); + }); + } else { + self.param_index = 0; + } + 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("vip://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, 500.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).at_most(200.0)) + .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, + bgmaps: 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), + 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), + 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 ImageBuffer) { + 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(), + ) + } + } + + fn render_world(&mut self, world: World, params: &WorldParams, image: &mut ImageBuffer) { + 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 { + world.height + }; + + let dx1 = world.dst_x; + let dx2 = dx1 + 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 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; + } + + 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); + + 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 dx = x + world.dst_x + world.dst_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[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]); + } + } + } + } +} + +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 [ImageBuffer; 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); + } 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 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; + 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 { + 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 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, + } + } + + 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)] +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", + }) + } +} + +struct CellCache<'a> { + bgmaps: &'a MemoryRef<'a>, + index: usize, + cell: CellData, +} + +impl<'a> CellCache<'a> { + fn new(bgmaps: &'a 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: &'a MemoryRef<'a>, + index: usize, + char: [u16; 8], +} + +impl<'a> CharCache<'a> { + fn new(chardata: &'a 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 + } +} + +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::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::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) + } + } + } + + 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::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) + } + } + } + + 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)); + } + 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; + } +} + +enum SourceParam { + Normal, + HBias(HBiasParam), + Affine(AffineParam), +} + +struct HBiasParam { + left: i16, + right: i16, + data: [u16; 2], +} + +impl HBiasParam { + fn load(params: &MemoryRef, index: usize) -> Self { + 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); + } + } +} + +struct AffineParam { + src_x: FixedI32, + src_parallax: i16, + src_y: FixedI32, + dx: FixedI32, + dy: FixedI32, + data: [u16; 5], +} + +impl AffineParam { + fn load(params: &MemoryRef, index: usize) -> Self { + 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_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, + src_parallax, + 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); + } + } +} + +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 +}