Implement world viewer with support for OBJ worlds
This commit is contained in:
parent
f7cf960b62
commit
cfc08032e6
|
@ -23,7 +23,7 @@ use crate::{
|
||||||
vram::VramProcessor,
|
vram::VramProcessor,
|
||||||
window::{
|
window::{
|
||||||
AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, GameWindow, GdbServerWindow,
|
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);
|
let objects = ObjectWindow::new(sim_id, &self.memory, &mut self.vram);
|
||||||
self.open(event_loop, Box::new(objects));
|
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) => {
|
UserEvent::OpenDebugger(sim_id) => {
|
||||||
let debugger =
|
let debugger =
|
||||||
GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone());
|
GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone());
|
||||||
|
@ -486,6 +490,7 @@ pub enum UserEvent {
|
||||||
OpenCharacterData(SimId),
|
OpenCharacterData(SimId),
|
||||||
OpenBgMap(SimId),
|
OpenBgMap(SimId),
|
||||||
OpenObjects(SimId),
|
OpenObjects(SimId),
|
||||||
|
OpenWorlds(SimId),
|
||||||
OpenDebugger(SimId),
|
OpenDebugger(SimId),
|
||||||
OpenInput,
|
OpenInput,
|
||||||
OpenPlayer2,
|
OpenPlayer2,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
|
iter::FusedIterator,
|
||||||
sync::{atomic::AtomicU64, Arc, Mutex, RwLock, RwLockReadGuard, TryLockError, Weak},
|
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> {
|
pub struct MemoryIter<'a, T> {
|
||||||
bytes: &'a [u8],
|
bytes: &'a [u8],
|
||||||
index: usize,
|
|
||||||
_phantom: std::marker::PhantomData<T>,
|
_phantom: std::marker::PhantomData<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +125,6 @@ impl<'a, T> MemoryIter<'a, T> {
|
||||||
fn new(bytes: &'a [u8]) -> Self {
|
fn new(bytes: &'a [u8]) -> Self {
|
||||||
Self {
|
Self {
|
||||||
bytes,
|
bytes,
|
||||||
index: 0,
|
|
||||||
_phantom: std::marker::PhantomData,
|
_phantom: std::marker::PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,15 +135,30 @@ impl<T: MemoryValue> Iterator for MemoryIter<'_, T> {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
if self.index >= self.bytes.len() {
|
let (bytes, rest) = self.bytes.split_at_checked(std::mem::size_of::<T>())?;
|
||||||
return None;
|
self.bytes = rest;
|
||||||
}
|
Some(T::from_bytes(bytes))
|
||||||
let bytes = &self.bytes[self.index..self.index + std::mem::size_of::<T>()];
|
}
|
||||||
self.index += std::mem::size_of::<T>();
|
|
||||||
|
#[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))
|
Some(T::from_bytes(bytes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: MemoryValue> FusedIterator for MemoryIter<'_, T> {}
|
||||||
|
|
||||||
impl MemoryRef<'_> {
|
impl MemoryRef<'_> {
|
||||||
pub fn read<T: MemoryValue>(&self, index: usize) -> T {
|
pub fn read<T: MemoryValue>(&self, index: usize) -> T {
|
||||||
let from = index * size_of::<T>();
|
let from = index * size_of::<T>();
|
||||||
|
|
27
src/vram.rs
27
src/vram.rs
|
@ -39,6 +39,7 @@ impl VramProcessor {
|
||||||
pub fn add<const N: usize, R: VramRenderer<N> + 'static>(
|
pub fn add<const N: usize, R: VramRenderer<N> + 'static>(
|
||||||
&self,
|
&self,
|
||||||
renderer: R,
|
renderer: R,
|
||||||
|
params: R::Params,
|
||||||
) -> ([VramImageHandle; N], VramParams<R::Params>) {
|
) -> ([VramImageHandle; N], VramParams<R::Params>) {
|
||||||
let states = renderer.sizes().map(VramRenderImageState::new);
|
let states = renderer.sizes().map(VramRenderImageState::new);
|
||||||
let handles = states.clone().map(|state| VramImageHandle {
|
let handles = states.clone().map(|state| VramImageHandle {
|
||||||
|
@ -48,7 +49,7 @@ impl VramProcessor {
|
||||||
let images = renderer
|
let images = renderer
|
||||||
.sizes()
|
.sizes()
|
||||||
.map(|[width, height]| VramImage::new(width, height));
|
.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 {
|
let _ = self.sender.send(Box::new(VramRendererWrapper {
|
||||||
renderer,
|
renderer,
|
||||||
params: Arc::downgrade(&sink),
|
params: Arc::downgrade(&sink),
|
||||||
|
@ -56,7 +57,7 @@ impl VramProcessor {
|
||||||
states,
|
states,
|
||||||
}));
|
}));
|
||||||
let params = VramParams {
|
let params = VramParams {
|
||||||
value: R::Params::default(),
|
value: params,
|
||||||
sink,
|
sink,
|
||||||
};
|
};
|
||||||
(handles, params)
|
(handles, params)
|
||||||
|
@ -100,32 +101,32 @@ impl VramProcessorWorker {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct VramImage {
|
pub struct VramImage {
|
||||||
size: [usize; 2],
|
pub size: [usize; 2],
|
||||||
shades: Vec<Color32>,
|
pub pixels: Vec<Color32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VramImage {
|
impl VramImage {
|
||||||
pub fn new(width: usize, height: usize) -> Self {
|
pub fn new(width: usize, height: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
size: [width, height],
|
size: [width, height],
|
||||||
shades: vec![Color32::BLACK; width * height],
|
pixels: vec![Color32::BLACK; width * height],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
for shade in self.shades.iter_mut() {
|
for pixel in self.pixels.iter_mut() {
|
||||||
*shade = Color32::BLACK;
|
*pixel = Color32::BLACK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(&mut self, coords: (usize, usize), pixel: Color32) {
|
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) {
|
pub fn add(&mut self, coords: (usize, usize), pixel: Color32) {
|
||||||
let index = coords.1 * self.size[0] + coords.0;
|
let index = coords.1 * self.size[0] + coords.0;
|
||||||
let old = self.shades[index];
|
let old = self.pixels[index];
|
||||||
self.shades[index] = Color32::from_rgb(
|
self.pixels[index] = Color32::from_rgb(
|
||||||
old.r() + pixel.r(),
|
old.r() + pixel.r(),
|
||||||
old.g() + pixel.g(),
|
old.g() + pixel.g(),
|
||||||
old.b() + pixel.b(),
|
old.b() + pixel.b(),
|
||||||
|
@ -133,11 +134,11 @@ impl VramImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn changed(&self, image: &ColorImage) -> bool {
|
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) {
|
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 {
|
pub trait VramRenderer<const N: usize>: Send {
|
||||||
type Params: Clone + Default + Send;
|
type Params: Clone + Send;
|
||||||
fn sizes(&self) -> [[usize; 2]; N];
|
fn sizes(&self) -> [[usize; 2]; N];
|
||||||
fn render(&mut self, params: &Self::Params, images: &mut [VramImage; N]);
|
fn render(&mut self, params: &Self::Params, images: &mut [VramImage; N]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use egui::{Context, ViewportBuilder, ViewportId};
|
||||||
pub use game::GameWindow;
|
pub use game::GameWindow;
|
||||||
pub use gdb::GdbServerWindow;
|
pub use gdb::GdbServerWindow;
|
||||||
pub use input::InputWindow;
|
pub use input::InputWindow;
|
||||||
pub use vram::{BgMapWindow, CharacterDataWindow, ObjectWindow};
|
pub use vram::{BgMapWindow, CharacterDataWindow, ObjectWindow, WorldWindow};
|
||||||
use winit::event::KeyEvent;
|
use winit::event::KeyEvent;
|
||||||
|
|
||||||
use crate::emulator::SimId;
|
use crate::emulator::SimId;
|
||||||
|
|
|
@ -150,6 +150,12 @@ impl GameWindow {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
ui.close_menu();
|
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| {
|
ui.menu_button("Help", |ui| {
|
||||||
if ui.button("About").clicked() {
|
if ui.button("About").clicked() {
|
||||||
|
|
|
@ -2,7 +2,9 @@ mod bgmap;
|
||||||
mod chardata;
|
mod chardata;
|
||||||
mod object;
|
mod object;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
mod world;
|
||||||
|
|
||||||
pub use bgmap::*;
|
pub use bgmap::*;
|
||||||
pub use chardata::*;
|
pub use chardata::*;
|
||||||
pub use object::*;
|
pub use object::*;
|
||||||
|
pub use world::*;
|
||||||
|
|
|
@ -33,7 +33,7 @@ pub struct BgMapWindow {
|
||||||
impl BgMapWindow {
|
impl BgMapWindow {
|
||||||
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, vram: &mut VramProcessor) -> Self {
|
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, vram: &mut VramProcessor) -> Self {
|
||||||
let renderer = BgMapRenderer::new(sim_id, memory);
|
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 =
|
let loader =
|
||||||
VramTextureLoader::new([("vram://cell".into(), cell), ("vram://bgmap".into(), bgmap)]);
|
VramTextureLoader::new([("vram://cell".into(), cell), ("vram://bgmap".into(), bgmap)]);
|
||||||
Self {
|
Self {
|
||||||
|
@ -41,8 +41,8 @@ impl BgMapWindow {
|
||||||
loader: Arc::new(loader),
|
loader: Arc::new(loader),
|
||||||
memory: memory.clone(),
|
memory: memory.clone(),
|
||||||
bgmaps: memory.watch(sim_id, 0x00020000, 0x1d800),
|
bgmaps: memory.watch(sim_id, 0x00020000, 0x1d800),
|
||||||
cell_index: 0,
|
cell_index: params.cell_index,
|
||||||
generic_palette: false,
|
generic_palette: params.generic_palette,
|
||||||
params,
|
params,
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
show_grid: false,
|
show_grid: false,
|
||||||
|
@ -243,7 +243,7 @@ impl BgMapRenderer {
|
||||||
let colors = if generic_palette {
|
let colors = if generic_palette {
|
||||||
[utils::generic_palette(Color32::RED); 4]
|
[utils::generic_palette(Color32::RED); 4]
|
||||||
} else {
|
} 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() {
|
for (i, cell) in bgmaps.range::<u16>(bgmap_index * 4096, 4096).enumerate() {
|
||||||
|
@ -286,7 +286,7 @@ impl BgMapRenderer {
|
||||||
let palette = if generic_palette {
|
let palette = if generic_palette {
|
||||||
utils::generic_palette(Color32::RED)
|
utils::generic_palette(Color32::RED)
|
||||||
} else {
|
} 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 {
|
for row in 0..8 {
|
||||||
|
|
|
@ -94,7 +94,7 @@ pub struct CharacterDataWindow {
|
||||||
impl CharacterDataWindow {
|
impl CharacterDataWindow {
|
||||||
pub fn new(sim_id: SimId, memory: &MemoryClient, vram: &mut VramProcessor) -> Self {
|
pub fn new(sim_id: SimId, memory: &MemoryClient, vram: &mut VramProcessor) -> Self {
|
||||||
let renderer = CharDataRenderer::new(sim_id, memory);
|
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([
|
let loader = VramTextureLoader::new([
|
||||||
("vram://char".into(), char),
|
("vram://char".into(), char),
|
||||||
("vram://chardata".into(), chardata),
|
("vram://chardata".into(), chardata),
|
||||||
|
@ -222,7 +222,7 @@ impl CharacterDataWindow {
|
||||||
let palette = self.palettes.borrow().read(offset);
|
let palette = self.palettes.borrow().read(offset);
|
||||||
let brightnesses = self.brightness.borrow();
|
let brightnesses = self.brightness.borrow();
|
||||||
let brts = brightnesses.read(0);
|
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) {
|
fn show_chardata(&mut self, ui: &mut Ui) {
|
||||||
|
@ -351,6 +351,6 @@ impl CharDataRenderer {
|
||||||
let palette = self.palettes.borrow().read(offset);
|
let palette = self.palettes.borrow().read(offset);
|
||||||
let brightnesses = self.brightness.borrow();
|
let brightnesses = self.brightness.borrow();
|
||||||
let brts = brightnesses.read(0);
|
let brts = brightnesses.read(0);
|
||||||
utils::parse_palette(palette, &brts, Color32::RED)
|
utils::palette_colors(palette, &brts, Color32::RED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,14 @@ pub struct ObjectWindow {
|
||||||
|
|
||||||
impl ObjectWindow {
|
impl ObjectWindow {
|
||||||
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, vram: &mut VramProcessor) -> Self {
|
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 renderer = ObjectRenderer::new(sim_id, memory);
|
||||||
let ([zoom, full], params) = vram.add(renderer);
|
let ([zoom, full], params) = vram.add(renderer, initial_params);
|
||||||
let loader =
|
let loader =
|
||||||
VramTextureLoader::new([("vram://zoom".into(), zoom), ("vram://full".into(), full)]);
|
VramTextureLoader::new([("vram://zoom".into(), zoom), ("vram://full".into(), full)]);
|
||||||
Self {
|
Self {
|
||||||
|
@ -40,8 +46,8 @@ impl ObjectWindow {
|
||||||
loader: Arc::new(loader),
|
loader: Arc::new(loader),
|
||||||
memory: memory.clone(),
|
memory: memory.clone(),
|
||||||
objects: memory.watch(sim_id, 0x0003e000, 0x2000),
|
objects: memory.watch(sim_id, 0x0003e000, 0x2000),
|
||||||
index: 0,
|
index: params.index,
|
||||||
generic_palette: false,
|
generic_palette: params.generic_palette,
|
||||||
params,
|
params,
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
}
|
}
|
||||||
|
@ -175,7 +181,7 @@ impl ObjectWindow {
|
||||||
self.params.write(ObjectParams {
|
self.params.write(ObjectParams {
|
||||||
index: self.index,
|
index: self.index,
|
||||||
generic_palette: self.generic_palette,
|
generic_palette: self.generic_palette,
|
||||||
..ObjectParams::default()
|
..*self.params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,17 +239,6 @@ struct ObjectParams {
|
||||||
right_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 {
|
enum Eye {
|
||||||
Left,
|
Left,
|
||||||
Right,
|
Right,
|
||||||
|
@ -302,7 +297,7 @@ impl ObjectRenderer {
|
||||||
let palette = if params.generic_palette {
|
let palette = if params.generic_palette {
|
||||||
utils::generic_palette(color)
|
utils::generic_palette(color)
|
||||||
} else {
|
} 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 {
|
for row in 0..8 {
|
||||||
|
|
|
@ -10,24 +10,29 @@ pub fn generic_palette(color: Color32) -> [Color32; 4] {
|
||||||
GENERIC_PALETTE.map(|brt| shade(brt, color))
|
GENERIC_PALETTE.map(|brt| shade(brt, color))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_palette(palette: u8, brts: &[u8; 8], color: Color32) -> [Color32; 4] {
|
pub const fn parse_palette(palette: u8) -> [u8; 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,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
[
|
[
|
||||||
Color32::BLACK,
|
0,
|
||||||
shades[(palette >> 2) as usize & 0x03],
|
(palette >> 2) & 0x03,
|
||||||
shades[(palette >> 4) as usize & 0x03],
|
(palette >> 4) & 0x03,
|
||||||
shades[(palette >> 6) as usize & 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 struct Object {
|
||||||
pub x: i16,
|
pub x: i16,
|
||||||
pub lon: bool,
|
pub lon: bool,
|
||||||
|
|
|
@ -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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue