diff --git a/src/window/vram/bgmap.rs b/src/window/vram/bgmap.rs index 47fa87e..3b0f39f 100644 --- a/src/window/vram/bgmap.rs +++ b/src/window/vram/bgmap.rs @@ -40,7 +40,7 @@ impl BgMapWindow { sim_id, loader: Arc::new(loader), memory: memory.clone(), - bgmaps: memory.watch(sim_id, 0x00020000, 0x1d800), + bgmaps: memory.watch(sim_id, 0x00020000, 0x20000), cell_index: params.cell_index, generic_palette: params.generic_palette, params, @@ -62,7 +62,7 @@ impl BgMapWindow { }); row.col(|ui| { let mut bgmap_index = self.cell_index / 4096; - ui.add(NumberEdit::new(&mut bgmap_index).range(0..14)); + 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); } @@ -227,7 +227,7 @@ 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, 0x1d800), + bgmaps: memory.watch(sim_id, 0x00020000, 0x20000), brightness: memory.watch(sim_id, 0x0005f824, 8), palettes: memory.watch(sim_id, 0x0005f860, 16), } diff --git a/src/window/vram/utils.rs b/src/window/vram/utils.rs index 9cbfd4f..98154da 100644 --- a/src/window/vram/utils.rs +++ b/src/window/vram/utils.rs @@ -134,6 +134,12 @@ pub fn read_char_row( }) } +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, diff --git a/src/window/vram/world.rs b/src/window/vram/world.rs index 6bf78e6..18803cf 100644 --- a/src/window/vram/world.rs +++ b/src/window/vram/world.rs @@ -1,8 +1,8 @@ use std::{fmt::Display, sync::Arc}; use egui::{ - CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextureOptions, - Ui, ViewportBuilder, ViewportId, + Align, CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextEdit, + TextureOptions, Ui, ViewportBuilder, ViewportId, }; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use num_derive::{FromPrimitive, ToPrimitive}; @@ -10,7 +10,7 @@ use num_traits::FromPrimitive; use crate::{ emulator::SimId, - memory::{MemoryClient, MemoryView}, + memory::{MemoryClient, MemoryRef, MemoryView}, vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader}, window::{ utils::{NumberEdit, UiExt as _}, @@ -18,7 +18,7 @@ use crate::{ }, }; -use super::utils::{self, shade, Object}; +use super::utils::{self, shade, CellData, Object}; pub struct WorldWindow { sim_id: SimId, @@ -67,6 +67,19 @@ impl WorldWindow { 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 = 0x3d800 + self.index * 32; + let mut address_str = format!("{address:08x}"); + ui.add_enabled( + false, + TextEdit::singleline(&mut address_str).horizontal_align(Align::Max), + ); + }); + }); }); let data = { let worlds = self.worlds.borrow(); @@ -78,6 +91,118 @@ impl WorldWindow { .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"); @@ -97,6 +222,28 @@ impl WorldWindow { }); }); }); + 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")); @@ -154,7 +301,7 @@ impl AppWindow for WorldWindow { fn initial_viewport(&self) -> ViewportBuilder { ViewportBuilder::default() .with_title(format!("Worlds ({})", self.sim_id)) - .with_inner_size((640.0, 480.0)) + .with_inner_size((640.0, 500.0)) } fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) { @@ -190,6 +337,7 @@ struct WorldParams { struct WorldRenderer { chardata: MemoryView, + bgmaps: MemoryView, objects: MemoryView, worlds: MemoryView, brightness: MemoryView, @@ -204,6 +352,7 @@ 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), @@ -313,6 +462,86 @@ impl WorldRenderer { ) } } + + fn render_world(&mut self, world: World, params: &WorldParams, image: &mut VramImage) { + image.clear(); + + let height = if world.header.mode == WorldMode::Affine { + world.height.max(8) + } else { + world.height + }; + + let dx1 = world.dst_x; + let dx2 = dx1 + world.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 mut chars = CharCache::new(self.chardata.borrow()); + let mut cells = CellCache::new(self.bgmaps.borrow()); + + for y in 0..height { + let dy = y + world.dst_y; + if !(0..224).contains(&dy) { + continue; + } + let sy = y + world.src_y; + + // left side + for x in 0..world.width { + let dx = x + world.dst_x - world.dst_parallax; + if !(0..384).contains(&dx) { + continue; + } + let sx = x + world.src_x - world.src_parallax; + + let cell_index = world.source_cell(sx, sy); + let cell = cells.get(cell_index); + let char = chars.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]); + } + + // right side + for x in 0..world.width { + let dx = x + world.dst_x + world.dst_parallax; + if !(0..384).contains(&dx) { + continue; + } + let sx = x + world.src_x + world.src_parallax; + + let cell_index = world.source_cell(sx, sy); + let cell = cells.get(cell_index); + let char = chars.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 VramRenderer<1> for WorldRenderer { @@ -342,28 +571,77 @@ impl VramRenderer<1> for WorldRenderer { } 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 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 { @@ -371,14 +649,20 @@ impl WorldHeader { 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, } } } @@ -407,3 +691,52 @@ impl Display for WorldMode { }) } } + +struct CellCache<'a> { + bgmaps: MemoryRef<'a>, + index: usize, + cell: CellData, +} + +impl<'a> CellCache<'a> { + fn new(bgmaps: 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: MemoryRef<'a>, + index: usize, + char: [u16; 8], +} + +impl<'a> CharCache<'a> { + fn new(chardata: 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 + } +}