use std::sync::Arc; use egui::{ Align, CentralPanel, Checkbox, Color32, Context, Image, ScrollArea, Slider, TextEdit, TextureOptions, Ui, ViewportBuilder, ViewportId, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use crate::{ emulator::SimId, memory::{MemoryMonitor, MemoryView}, vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, window::{ utils::{NumberEdit, UiExt as _}, AppWindow, }, }; use super::utils::{self, Object}; pub struct ObjectWindow { sim_id: SimId, loader: Arc, objects: MemoryView, index: usize, generic_palette: bool, params: VramParams, scale: f32, } impl ObjectWindow { pub fn new(sim_id: SimId, memory: &mut MemoryMonitor, vram: &mut VramProcessor) -> Self { let renderer = ObjectRenderer::new(sim_id, memory); let ([zoom, full], params) = vram.add(renderer); let loader = VramTextureLoader::new([("vram://zoom".into(), zoom), ("vram://full".into(), full)]); Self { sim_id, loader: Arc::new(loader), objects: memory.view(sim_id, 0x0003e000, 0x2000), index: 0, generic_palette: false, params, scale: 1.0, } } fn show_form(&mut self, ui: &mut Ui) { let row_height = ui.spacing().interact_size.y; ui.vertical(|ui| { TableBuilder::new(ui) .column(Column::auto()) .column(Column::remainder()) .body(|mut body| { body.row(row_height, |mut row| { row.col(|ui| { ui.label("Index"); }); row.col(|ui| { ui.add(NumberEdit::new(&mut self.index).range(0..1024)); }); }); body.row(row_height, |mut row| { row.col(|ui| { ui.label("Address"); }); row.col(|ui| { let address = 0x3e000 + self.index * 8; let mut address_str = format!("{address:08x}"); ui.add_enabled( false, TextEdit::singleline(&mut address_str).horizontal_align(Align::Max), ); }); }); }); let image = Image::new("vram://zoom") .maintain_aspect_ratio(true) .texture_options(TextureOptions::NEAREST); ui.add(image); ui.section("Properties", |ui| { let object = self.objects.borrow().read::<[u16; 4]>(self.index); let mut 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_enabled( false, 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| { let mut palette = format!("OBJ {}", obj.data.palette_index); ui.add_enabled( false, TextEdit::singleline(&mut palette).horizontal_align(Align::Max), ); }); }); body.row(row_height, |mut row| { row.col(|ui| { ui.label("X"); }); row.col(|ui| { ui.add_enabled(false, 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_enabled(false, 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_enabled( false, NumberEdit::new(&mut obj.parallax).range(-512..512), ); }); }); body.row(row_height, |mut row| { row.col(|ui| { let checkbox = Checkbox::new(&mut obj.data.hflip, "H-flip"); ui.add_enabled(false, checkbox); }); row.col(|ui| { let checkbox = Checkbox::new(&mut obj.data.vflip, "V-flip"); ui.add_enabled(false, checkbox); }); }); body.row(row_height, |mut row| { row.col(|ui| { let checkbox = Checkbox::new(&mut obj.lon, "Left"); ui.add_enabled(false, checkbox); }); row.col(|ui| { let checkbox = Checkbox::new(&mut obj.ron, "Right"); ui.add_enabled(false, checkbox); }); }); }); }); ui.section("Display", |ui| { ui.horizontal(|ui| { ui.label("Scale"); ui.spacing_mut().slider_width = ui.available_width(); let slider = Slider::new(&mut self.scale, 1.0..=10.0) .step_by(1.0) .show_value(false); ui.add(slider); }); ui.checkbox(&mut self.generic_palette, "Generic palette"); }); }); self.params.write(ObjectParams { index: self.index, generic_palette: self.generic_palette, ..ObjectParams::default() }); } fn show_object(&mut self, ui: &mut Ui) { let image = Image::new("vram://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)) .size(Size::remainder()) .horizontal(|mut strip| { strip.cell(|ui| { ScrollArea::vertical().show(ui, |ui| self.show_form(ui)); }); strip.cell(|ui| { ScrollArea::both().show(ui, |ui| self.show_object(ui)); }); }); }); }); } } #[derive(Clone, PartialEq, Eq)] struct ObjectParams { index: usize, generic_palette: bool, left_color: Color32, right_color: Color32, } impl Default for ObjectParams { fn default() -> Self { Self { index: 0, generic_palette: false, left_color: Color32::from_rgb(0xff, 0x00, 0x00), right_color: Color32::from_rgb(0x00, 0xc6, 0xf0), } } } enum Eye { Left, Right, } struct ObjectRenderer { chardata: MemoryView, objects: MemoryView, brightness: MemoryView, palettes: MemoryView, } impl ObjectRenderer { pub fn new(sim_id: SimId, memory: &mut MemoryMonitor) -> Self { Self { chardata: memory.view(sim_id, 0x00078000, 0x8000), objects: memory.view(sim_id, 0x0003e000, 0x2000), brightness: memory.view(sim_id, 0x0005f824, 8), palettes: memory.view(sim_id, 0x0005f860, 16), } } fn render_object(&self, image: &mut VramImage, params: &ObjectParams, 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.range::(0, 8); 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.range::(obj.data.char_index * 8, 8); let palette = if params.generic_palette { utils::generic_palette(color) } else { utils::parse_palette(palettes.read(8 + obj.data.palette_index * 2), brts, color) }; for row in 0..8 { let real_y = y + row as i16; if !(0..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 VramRenderer<2> for ObjectRenderer { type Params = ObjectParams; fn sizes(&self) -> [[usize; 2]; 2] { [[8, 8], [384, 224]] } fn render(&mut self, params: &Self::Params, images: &mut [VramImage; 2]) { 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); } }