VIP inspection tooling #4

Merged
SonicSwordcane merged 34 commits from vram into main 2025-02-24 04:01:18 +00:00
11 changed files with 497 additions and 60 deletions
Showing only changes of commit cfc08032e6 - Show all commits

View File

@ -23,7 +23,7 @@ use crate::{
vram::VramProcessor,
window::{
AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, GameWindow, GdbServerWindow,
InputWindow, ObjectWindow,
InputWindow, ObjectWindow, WorldWindow,
},
};
@ -225,6 +225,10 @@ impl ApplicationHandler<UserEvent> for Application {
let objects = ObjectWindow::new(sim_id, &self.memory, &mut self.vram);
self.open(event_loop, Box::new(objects));
}
UserEvent::OpenWorlds(sim_id) => {
let world = WorldWindow::new(sim_id, &self.memory, &mut self.vram);
self.open(event_loop, Box::new(world));
}
UserEvent::OpenDebugger(sim_id) => {
let debugger =
GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone());
@ -486,6 +490,7 @@ pub enum UserEvent {
OpenCharacterData(SimId),
OpenBgMap(SimId),
OpenObjects(SimId),
OpenWorlds(SimId),
OpenDebugger(SimId),
OpenInput,
OpenPlayer2,

View File

@ -1,6 +1,7 @@
use std::{
collections::HashMap,
fmt::Debug,
iter::FusedIterator,
sync::{atomic::AtomicU64, Arc, Mutex, RwLock, RwLockReadGuard, TryLockError, Weak},
};
@ -117,7 +118,6 @@ impl<const N: usize, T: MemoryValue> MemoryValue for [T; N] {
pub struct MemoryIter<'a, T> {
bytes: &'a [u8],
index: usize,
_phantom: std::marker::PhantomData<T>,
}
@ -125,7 +125,6 @@ impl<'a, T> MemoryIter<'a, T> {
fn new(bytes: &'a [u8]) -> Self {
Self {
bytes,
index: 0,
_phantom: std::marker::PhantomData,
}
}
@ -136,15 +135,30 @@ impl<T: MemoryValue> Iterator for MemoryIter<'_, T> {
#[inline]
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.bytes.len() {
return None;
}
let bytes = &self.bytes[self.index..self.index + std::mem::size_of::<T>()];
self.index += std::mem::size_of::<T>();
let (bytes, rest) = self.bytes.split_at_checked(std::mem::size_of::<T>())?;
self.bytes = rest;
Some(T::from_bytes(bytes))
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
let size = self.bytes.len() / std::mem::size_of::<T>();
(size, Some(size))
}
}
impl<T: MemoryValue> DoubleEndedIterator for MemoryIter<'_, T> {
fn next_back(&mut self) -> Option<Self::Item> {
let mid = self.bytes.len().checked_sub(std::mem::size_of::<T>())?;
// 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<T: MemoryValue> FusedIterator for MemoryIter<'_, T> {}
impl MemoryRef<'_> {
pub fn read<T: MemoryValue>(&self, index: usize) -> T {
let from = index * size_of::<T>();

View File

@ -39,6 +39,7 @@ impl VramProcessor {
pub fn add<const N: usize, R: VramRenderer<N> + 'static>(
&self,
renderer: R,
params: R::Params,
) -> ([VramImageHandle; N], VramParams<R::Params>) {
let states = renderer.sizes().map(VramRenderImageState::new);
let handles = states.clone().map(|state| VramImageHandle {
@ -48,7 +49,7 @@ impl VramProcessor {
let images = renderer
.sizes()
.map(|[width, height]| VramImage::new(width, height));
let sink = Arc::new(Mutex::new(R::Params::default()));
let sink = Arc::new(Mutex::new(params.clone()));
let _ = self.sender.send(Box::new(VramRendererWrapper {
renderer,
params: Arc::downgrade(&sink),
@ -56,7 +57,7 @@ impl VramProcessor {
states,
}));
let params = VramParams {
value: R::Params::default(),
value: params,
sink,
};
(handles, params)
@ -100,32 +101,32 @@ impl VramProcessorWorker {
}
pub struct VramImage {
size: [usize; 2],
shades: Vec<Color32>,
pub size: [usize; 2],
pub pixels: Vec<Color32>,
}
impl VramImage {
pub fn new(width: usize, height: usize) -> Self {
Self {
size: [width, height],
shades: vec![Color32::BLACK; width * height],
pixels: vec![Color32::BLACK; width * height],
}
}
pub fn clear(&mut self) {
for shade in self.shades.iter_mut() {
*shade = Color32::BLACK;
for pixel in self.pixels.iter_mut() {
*pixel = Color32::BLACK;
}
}
pub fn write(&mut self, coords: (usize, usize), pixel: Color32) {
self.shades[coords.1 * self.size[0] + coords.0] = pixel;
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.shades[index];
self.shades[index] = Color32::from_rgb(
let old = self.pixels[index];
self.pixels[index] = Color32::from_rgb(
old.r() + pixel.r(),
old.g() + pixel.g(),
old.b() + pixel.b(),
@ -133,11 +134,11 @@ impl VramImage {
}
pub fn changed(&self, image: &ColorImage) -> bool {
image.pixels.iter().zip(&self.shades).any(|(a, b)| a != b)
image.pixels.iter().zip(&self.pixels).any(|(a, b)| a != b)
}
pub fn read(&self, image: &mut ColorImage) {
image.pixels.copy_from_slice(&self.shades);
image.pixels.copy_from_slice(&self.pixels);
}
}
@ -176,7 +177,7 @@ impl<T: Clone + Eq> VramParams<T> {
}
pub trait VramRenderer<const N: usize>: Send {
type Params: Clone + Default + Send;
type Params: Clone + Send;
fn sizes(&self) -> [[usize; 2]; N];
fn render(&mut self, params: &Self::Params, images: &mut [VramImage; N]);
}

View File

@ -3,7 +3,7 @@ use egui::{Context, ViewportBuilder, ViewportId};
pub use game::GameWindow;
pub use gdb::GdbServerWindow;
pub use input::InputWindow;
pub use vram::{BgMapWindow, CharacterDataWindow, ObjectWindow};
pub use vram::{BgMapWindow, CharacterDataWindow, ObjectWindow, WorldWindow};
use winit::event::KeyEvent;
use crate::emulator::SimId;

View File

@ -150,6 +150,12 @@ impl GameWindow {
.unwrap();
ui.close_menu();
}
if ui.button("Worlds").clicked() {
self.proxy
.send_event(UserEvent::OpenWorlds(self.sim_id))
.unwrap();
ui.close_menu();
}
});
ui.menu_button("Help", |ui| {
if ui.button("About").clicked() {

View File

@ -2,7 +2,9 @@ mod bgmap;
mod chardata;
mod object;
mod utils;
mod world;
pub use bgmap::*;
pub use chardata::*;
pub use object::*;
pub use world::*;

View File

@ -33,7 +33,7 @@ pub struct BgMapWindow {
impl BgMapWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, vram: &mut VramProcessor) -> Self {
let renderer = BgMapRenderer::new(sim_id, memory);
let ([cell, bgmap], params) = vram.add(renderer);
let ([cell, bgmap], params) = vram.add(renderer, BgMapParams::default());
let loader =
VramTextureLoader::new([("vram://cell".into(), cell), ("vram://bgmap".into(), bgmap)]);
Self {
@ -41,8 +41,8 @@ impl BgMapWindow {
loader: Arc::new(loader),
memory: memory.clone(),
bgmaps: memory.watch(sim_id, 0x00020000, 0x1d800),
cell_index: 0,
generic_palette: false,
cell_index: params.cell_index,
generic_palette: params.generic_palette,
params,
scale: 1.0,
show_grid: false,
@ -243,7 +243,7 @@ impl BgMapRenderer {
let colors = if generic_palette {
[utils::generic_palette(Color32::RED); 4]
} else {
[0, 2, 4, 6].map(|i| utils::parse_palette(palettes.read(i), &brts, Color32::RED))
[0, 2, 4, 6].map(|i| utils::palette_colors(palettes.read(i), &brts, Color32::RED))
};
for (i, cell) in bgmaps.range::<u16>(bgmap_index * 4096, 4096).enumerate() {
@ -286,7 +286,7 @@ impl BgMapRenderer {
let palette = if generic_palette {
utils::generic_palette(Color32::RED)
} else {
utils::parse_palette(palettes.read(palette_index * 2), &brts, Color32::RED)
utils::palette_colors(palettes.read(palette_index * 2), &brts, Color32::RED)
};
for row in 0..8 {

View File

@ -94,7 +94,7 @@ pub struct CharacterDataWindow {
impl CharacterDataWindow {
pub fn new(sim_id: SimId, memory: &MemoryClient, vram: &mut VramProcessor) -> Self {
let renderer = CharDataRenderer::new(sim_id, memory);
let ([char, chardata], params) = vram.add(renderer);
let ([char, chardata], params) = vram.add(renderer, CharDataParams::default());
let loader = VramTextureLoader::new([
("vram://char".into(), char),
("vram://chardata".into(), chardata),
@ -222,7 +222,7 @@ impl CharacterDataWindow {
let palette = self.palettes.borrow().read(offset);
let brightnesses = self.brightness.borrow();
let brts = brightnesses.read(0);
utils::parse_palette(palette, &brts, Color32::RED)
utils::palette_colors(palette, &brts, Color32::RED)
}
fn show_chardata(&mut self, ui: &mut Ui) {
@ -351,6 +351,6 @@ impl CharDataRenderer {
let palette = self.palettes.borrow().read(offset);
let brightnesses = self.brightness.borrow();
let brts = brightnesses.read(0);
utils::parse_palette(palette, &brts, Color32::RED)
utils::palette_colors(palette, &brts, Color32::RED)
}
}

View File

@ -31,8 +31,14 @@ pub struct ObjectWindow {
impl ObjectWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, vram: &mut VramProcessor) -> 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) = vram.add(renderer);
let ([zoom, full], params) = vram.add(renderer, initial_params);
let loader =
VramTextureLoader::new([("vram://zoom".into(), zoom), ("vram://full".into(), full)]);
Self {
@ -40,8 +46,8 @@ impl ObjectWindow {
loader: Arc::new(loader),
memory: memory.clone(),
objects: memory.watch(sim_id, 0x0003e000, 0x2000),
index: 0,
generic_palette: false,
index: params.index,
generic_palette: params.generic_palette,
params,
scale: 1.0,
}
@ -175,7 +181,7 @@ impl ObjectWindow {
self.params.write(ObjectParams {
index: self.index,
generic_palette: self.generic_palette,
..ObjectParams::default()
..*self.params
});
}
@ -233,17 +239,6 @@ struct ObjectParams {
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,
@ -302,7 +297,7 @@ impl ObjectRenderer {
let palette = if params.generic_palette {
utils::generic_palette(color)
} else {
utils::parse_palette(palettes.read(8 + obj.data.palette_index * 2), &brts, color)
utils::palette_colors(palettes.read(8 + obj.data.palette_index * 2), &brts, color)
};
for row in 0..8 {

View File

@ -10,24 +10,29 @@ pub fn generic_palette(color: Color32) -> [Color32; 4] {
GENERIC_PALETTE.map(|brt| shade(brt, color))
}
pub fn parse_palette(palette: u8, brts: &[u8; 8], color: Color32) -> [Color32; 4] {
let shades = [
Color32::BLACK,
shade(brts[0], color),
shade(brts[2], color),
shade(
brts[0].saturating_add(brts[2]).saturating_add(brts[4]),
color,
),
];
pub const fn parse_palette(palette: u8) -> [u8; 4] {
[
Color32::BLACK,
shades[(palette >> 2) as usize & 0x03],
shades[(palette >> 4) as usize & 0x03],
shades[(palette >> 6) as usize & 0x03],
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 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,

409
src/window/vram/world.rs Normal file
View File

@ -0,0 +1,409 @@
use std::{fmt::Display, sync::Arc};
use egui::{
CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextureOptions,
Ui, ViewportBuilder, ViewportId,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use num_derive::{FromPrimitive, ToPrimitive};
use num_traits::FromPrimitive;
use crate::{
emulator::SimId,
memory::{MemoryClient, MemoryView},
vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader},
window::{
utils::{NumberEdit, UiExt as _},
AppWindow,
},
};
use super::utils::{self, shade, Object};
pub struct WorldWindow {
sim_id: SimId,
loader: Arc<VramTextureLoader>,
worlds: MemoryView,
index: usize,
generic_palette: bool,
params: VramParams<WorldParams>,
scale: f32,
}
impl WorldWindow {
pub fn new(sim_id: SimId, memory: &MemoryClient, vram: &mut VramProcessor) -> 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) = vram.add(renderer, initial_params);
let loader = VramTextureLoader::new([("vram://world".into(), world)]);
Self {
sim_id,
loader: Arc::new(loader),
worlds: memory.watch(sim_id, 0x3d800, 0x400),
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..32));
});
});
});
let 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("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.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"));
});
});
});
});
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("vram://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, 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_world(ui));
});
});
});
});
}
}
#[derive(Clone, PartialEq, Eq)]
struct WorldParams {
index: usize,
generic_palette: bool,
left_color: Color32,
right_color: Color32,
}
struct WorldRenderer {
chardata: 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),
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 VramImage) {
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::<u16>(group - 1).wrapping_add(1) as usize & 0x03ff
};
let end = scr.read::<u16>(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(),
)
}
}
}
impl VramRenderer<1> for WorldRenderer {
type Params = WorldParams;
fn sizes(&self) -> [[usize; 2]; 1] {
[[384, 224]]
}
fn render(&mut self, params: &Self::Params, images: &mut [VramImage; 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);
}
}
}
struct World {
header: WorldHeader,
}
impl World {
pub fn parse(data: &[u16; 16]) -> Self {
Self {
header: WorldHeader::parse(data[0]),
}
}
}
struct WorldHeader {
lon: bool,
ron: bool,
mode: WorldMode,
over: bool,
end: bool,
}
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 over = data & 0x0080 != 0;
let end = data & 0x0040 != 0;
Self {
lon,
ron,
mode,
over,
end,
}
}
}
#[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",
})
}
}