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}, AppWindow, }, }; use super::utils::{parse_palette, CharacterGrid, GENERIC_PALETTE}; pub struct BgMapWindow { sim_id: SimId, loader: Arc, bgmaps: MemoryView, cell_index: usize, generic_palette: bool, params: VramParams, scale: f32, show_grid: bool, } impl BgMapWindow { pub fn new(sim_id: SimId, memory: &mut MemoryMonitor, vram: &mut VramProcessor) -> Self { let renderer = BgMapRenderer::new(sim_id, memory); let ([cell, bgmap], params) = vram.add(renderer); let loader = VramTextureLoader::new([("vram://cell".into(), cell), ("vram://bgmap".into(), bgmap)]); Self { sim_id, loader: Arc::new(loader), bgmaps: memory.view(sim_id, 0x00020000, 0x1d800), cell_index: 0, generic_palette: false, 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..14)); 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("vram://cell") .maintain_aspect_ratio(true) .tint(Color32::RED) .texture_options(TextureOptions::NEAREST); ui.add(image); ui.section("Cell", |ui| { let cell = self.bgmaps.borrow().read::(self.cell_index); let (char_index, mut vflip, mut hflip, palette_index) = parse_cell(cell); TableBuilder::new(ui) .column(Column::remainder()) .column(Column::remainder()) .body(|mut body| { body.row(row_height, |mut row| { row.col(|ui| { ui.label("Character"); }); row.col(|ui| { let mut character_str = char_index.to_string(); ui.add_enabled( false, TextEdit::singleline(&mut character_str) .horizontal_align(Align::Max), ); }); }); body.row(row_height, |mut row| { row.col(|ui| { ui.label("Palette"); }); row.col(|ui| { let mut palette = format!("BG {}", palette_index); ui.add_enabled( false, TextEdit::singleline(&mut palette).horizontal_align(Align::Max), ); }); }); body.row(row_height, |mut row| { row.col(|ui| { let checkbox = Checkbox::new(&mut hflip, "H-flip"); ui.add_enabled(false, checkbox); }); row.col(|ui| { let checkbox = Checkbox::new(&mut vflip, "V-flip"); ui.add_enabled(false, checkbox); }); }); }); ui.section("Display", |ui| { ui.horizontal(|ui| { ui.label("Scale"); ui.spacing_mut().slider_width = ui.available_width(); let slider = Slider::new(&mut self.scale, 1.0..=10.0) .step_by(1.0) .show_value(false); ui.add(slider); }); ui.checkbox(&mut self.show_grid, "Show grid"); ui.checkbox(&mut self.generic_palette, "Generic palette"); }); }); }); 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("vram://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)) .size(Size::remainder()) .horizontal(|mut strip| { strip.cell(|ui| { ScrollArea::vertical().show(ui, |ui| self.show_form(ui)); }); strip.cell(|ui| { ScrollArea::both().show(ui, |ui| self.show_bgmap(ui)); }); }) }); }); } } fn parse_cell(cell: u16) -> (usize, bool, bool, usize) { let char_index = (cell & 0x7ff) as usize; let vflip = cell & 0x1000 != 0; let hflip = cell & 0x2000 != 0; let palette_index = (cell >> 14) as usize; (char_index, vflip, hflip, palette_index) } #[derive(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: &mut MemoryMonitor) -> Self { Self { chardata: memory.view(sim_id, 0x00078000, 0x8000), bgmaps: memory.view(sim_id, 0x00020000, 0x1d800), brightness: memory.view(sim_id, 0x0005f824, 8), palettes: memory.view(sim_id, 0x0005f860, 16), } } fn render_bgmap(&self, image: &mut VramImage, bgmap_index: usize, generic_palette: bool) { let chardata = self.chardata.borrow(); let bgmaps = self.bgmaps.borrow(); let brightness = self.brightness.borrow(); let palettes = self.palettes.borrow(); let brts = brightness.range::(0, 8); let colors = if generic_palette { [ GENERIC_PALETTE, GENERIC_PALETTE, GENERIC_PALETTE, GENERIC_PALETTE, ] } else { [ parse_palette(palettes.read(0), brts), parse_palette(palettes.read(2), brts), parse_palette(palettes.read(4), brts), parse_palette(palettes.read(6), brts), ] }; for (i, cell) in bgmaps .range::(bgmap_index * 4096, 4096) .iter() .enumerate() { let (char_index, vflip, hflip, palette_index) = parse_cell(*cell); let char = chardata.range::(char_index * 8, 8); let palette = &colors[palette_index]; for row in 0..8 { let y = row + (i / 64) * 8; for (col, pixel) in self.read_char_row(char, hflip, vflip, row).enumerate() { let x = col + (i % 64) * 8; image.write((x, y), palette[pixel as usize]); } } } } fn render_bgmap_cell(&self, image: &mut VramImage, index: usize, generic_palette: bool) { let chardata = self.chardata.borrow(); let bgmaps = self.bgmaps.borrow(); let brightness = self.brightness.borrow(); let palettes = self.palettes.borrow(); let brts = brightness.range::(0, 8); let cell = bgmaps.read::(index); let (char_index, vflip, hflip, palette_index) = parse_cell(cell); let char = chardata.range::(char_index * 8, 8); let palette = if generic_palette { GENERIC_PALETTE } else { parse_palette(palettes.read(palette_index * 2), brts) }; for row in 0..8 { for (col, pixel) in self.read_char_row(char, hflip, vflip, row).enumerate() { image.write((col, row), palette[pixel as usize]); } } } fn read_char_row( &self, char: &[u16], hflip: bool, vflip: bool, row: usize, ) -> impl Iterator { let pixels = if vflip { char[7 - row] } else { char[row] }; (0..16).step_by(2).map(move |i| { let pixel = if hflip { 14 - i } else { i }; ((pixels >> pixel) & 0x3) as u8 }) } } impl VramRenderer<2> for BgMapRenderer { type Params = BgMapParams; fn sizes(&self) -> [[usize; 2]; 2] { [[8, 8], [8 * 64, 8 * 64]] } fn render(&mut self, params: &Self::Params, images: &mut [VramImage; 2]) { 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, ); } }