1222 lines
45 KiB
Rust
1222 lines
45 KiB
Rust
use std::{fmt::Display, sync::Arc};
|
|
|
|
use egui::{
|
|
Align, CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextEdit,
|
|
TextureOptions, Ui, ViewportBuilder, ViewportId,
|
|
};
|
|
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
|
|
use fixed::{
|
|
types::extra::{U3, U9},
|
|
FixedI32,
|
|
};
|
|
use num_derive::{FromPrimitive, ToPrimitive};
|
|
use num_traits::{FromPrimitive, ToPrimitive};
|
|
|
|
use crate::{
|
|
emulator::SimId,
|
|
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
|
|
memory::{MemoryClient, MemoryRef, MemoryView},
|
|
window::{
|
|
utils::{NumberEdit, UiExt as _},
|
|
AppWindow,
|
|
},
|
|
};
|
|
|
|
use super::utils::{self, shade, CellData, Object};
|
|
|
|
pub struct WorldWindow {
|
|
sim_id: SimId,
|
|
loader: Arc<ImageTextureLoader>,
|
|
memory: Arc<MemoryClient>,
|
|
worlds: MemoryView,
|
|
bgmaps: MemoryView,
|
|
index: usize,
|
|
param_index: usize,
|
|
generic_palette: bool,
|
|
show_extents: bool,
|
|
params: ImageParams<WorldParams>,
|
|
scale: f32,
|
|
}
|
|
|
|
impl WorldWindow {
|
|
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &mut ImageProcessor) -> 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) = images.add(renderer, initial_params);
|
|
let loader = ImageTextureLoader::new([("vip://world".into(), world)]);
|
|
Self {
|
|
sim_id,
|
|
loader: Arc::new(loader),
|
|
memory: memory.clone(),
|
|
worlds: memory.watch(sim_id, 0x0003d800, 0x400),
|
|
bgmaps: memory.watch(sim_id, 0x00020000, 0x20000),
|
|
index: params.index,
|
|
param_index: 0,
|
|
generic_palette: params.generic_palette,
|
|
show_extents: 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..32));
|
|
});
|
|
});
|
|
body.row(row_height, |mut row| {
|
|
row.col(|ui| {
|
|
ui.label("Address");
|
|
});
|
|
row.col(|ui| {
|
|
let address = 0x0003d800 + self.index * 32;
|
|
let mut address_str = format!("{address:08x}");
|
|
ui.add_enabled(
|
|
false,
|
|
TextEdit::singleline(&mut address_str).horizontal_align(Align::Max),
|
|
);
|
|
});
|
|
});
|
|
});
|
|
let mut 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("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");
|
|
});
|
|
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.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"));
|
|
});
|
|
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"));
|
|
});
|
|
});
|
|
});
|
|
});
|
|
if world.update(&mut data) {
|
|
let address = 0x0003d800 + self.index * 32;
|
|
self.memory.write(self.sim_id, address as u32, &data);
|
|
}
|
|
if world.header.mode == WorldMode::HBias {
|
|
ui.section("H-bias", |ui| {
|
|
TableBuilder::new(ui)
|
|
.column(Column::remainder())
|
|
.column(Column::remainder())
|
|
.body(|mut body| {
|
|
body.row(row_height, |mut row| {
|
|
row.col(|ui| {
|
|
ui.label("Index");
|
|
});
|
|
row.col(|ui| {
|
|
let max = world.height.max(8) as usize;
|
|
ui.add(NumberEdit::new(&mut self.param_index).range(0..max));
|
|
});
|
|
});
|
|
let base = (world.param_base + self.param_index * 2) & 0x1ffff;
|
|
let mut param = HBiasParam::load(&self.bgmaps.borrow(), base);
|
|
body.row(row_height, |mut row| {
|
|
row.col(|ui| {
|
|
ui.label("Address");
|
|
});
|
|
row.col(|ui| {
|
|
let address = 0x00020000 + 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("Left");
|
|
});
|
|
row.col(|ui| {
|
|
ui.add(NumberEdit::new(&mut param.left).range(-4096..4096));
|
|
});
|
|
});
|
|
body.row(row_height, |mut row| {
|
|
row.col(|ui| {
|
|
ui.label("Right");
|
|
});
|
|
row.col(|ui| {
|
|
ui.add(NumberEdit::new(&mut param.right).range(-4096..4096));
|
|
});
|
|
});
|
|
param.save(&self.memory, self.sim_id, base);
|
|
});
|
|
});
|
|
} else if world.header.mode == WorldMode::Affine {
|
|
ui.section("Affine", |ui| {
|
|
TableBuilder::new(ui)
|
|
.column(Column::remainder())
|
|
.column(Column::remainder())
|
|
.body(|mut body| {
|
|
body.row(row_height, |mut row| {
|
|
row.col(|ui| {
|
|
ui.label("Index");
|
|
});
|
|
row.col(|ui| {
|
|
let max = world.height.max(1) as usize;
|
|
ui.add(NumberEdit::new(&mut self.param_index).range(0..max));
|
|
});
|
|
});
|
|
let base = (world.param_base + self.param_index * 8) & 0x1ffff;
|
|
let mut param = AffineParam::load(&self.bgmaps.borrow(), base);
|
|
body.row(row_height, |mut row| {
|
|
row.col(|ui| {
|
|
ui.label("Address");
|
|
});
|
|
row.col(|ui| {
|
|
let address = 0x00020000 + 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("Src X");
|
|
});
|
|
row.col(|ui| {
|
|
ui.add(NumberEdit::new(&mut param.src_x).precision(3));
|
|
});
|
|
});
|
|
body.row(row_height, |mut row| {
|
|
row.col(|ui| {
|
|
ui.label("Src Y");
|
|
});
|
|
row.col(|ui| {
|
|
ui.add(NumberEdit::new(&mut param.src_y).precision(3));
|
|
});
|
|
});
|
|
body.row(row_height, |mut row| {
|
|
row.col(|ui| {
|
|
ui.label("Src parallax");
|
|
});
|
|
row.col(|ui| {
|
|
ui.add(NumberEdit::new(&mut param.src_parallax));
|
|
});
|
|
});
|
|
body.row(row_height, |mut row| {
|
|
row.col(|ui| {
|
|
ui.label("Delta X");
|
|
});
|
|
row.col(|ui| {
|
|
ui.add(NumberEdit::new(&mut param.dx).precision(9));
|
|
});
|
|
});
|
|
body.row(row_height, |mut row| {
|
|
row.col(|ui| {
|
|
ui.label("Delta Y");
|
|
});
|
|
row.col(|ui| {
|
|
ui.add(NumberEdit::new(&mut param.dy).precision(9));
|
|
});
|
|
});
|
|
param.save(&self.memory, self.sim_id, base);
|
|
});
|
|
});
|
|
} else {
|
|
self.param_index = 0;
|
|
}
|
|
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");
|
|
ui.checkbox(&mut self.show_extents, "Show extents");
|
|
});
|
|
});
|
|
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("vip://world")
|
|
.fit_to_original_size(self.scale)
|
|
.texture_options(TextureOptions::NEAREST);
|
|
let res = ui.add(image);
|
|
if self.show_extents {
|
|
let world = {
|
|
let worlds = self.worlds.borrow();
|
|
let data = worlds.read(self.index);
|
|
World::parse(&data)
|
|
};
|
|
if world.header.mode == WorldMode::Object {
|
|
return;
|
|
}
|
|
|
|
let lx1 = (world.dst_x - world.dst_parallax) as f32 * self.scale;
|
|
let lx2 = lx1 + world.width as f32 * self.scale;
|
|
let rx1 = (world.dst_x + world.dst_parallax) as f32 * self.scale;
|
|
let rx2 = rx1 + world.width as f32 * self.scale;
|
|
let y1 = world.dst_y as f32 * self.scale;
|
|
let y2 = y1 + world.height as f32 * self.scale;
|
|
|
|
let left_color = self.params.left_color;
|
|
let right_color = self.params.right_color;
|
|
let both_color = Color32::from_rgb(
|
|
left_color.r() + right_color.r(),
|
|
left_color.g() + right_color.g(),
|
|
left_color.b() + right_color.b(),
|
|
);
|
|
|
|
let painter = ui.painter();
|
|
let draw_rect = |x1: f32, x2: f32, color: Color32| {
|
|
painter.line(
|
|
vec![
|
|
res.rect.min + (x1, y1).into(),
|
|
res.rect.min + (x2, y1).into(),
|
|
res.rect.min + (x2, y2).into(),
|
|
res.rect.min + (x1, y2).into(),
|
|
res.rect.min + (x1, y1).into(),
|
|
],
|
|
(2.0, color),
|
|
)
|
|
};
|
|
|
|
match (world.header.lon, world.header.ron) {
|
|
(false, false) => {}
|
|
(true, false) => {
|
|
draw_rect(lx1, lx2, left_color);
|
|
}
|
|
(false, true) => {
|
|
draw_rect(rx1, rx2, right_color);
|
|
}
|
|
(true, true) if world.dst_parallax == 0 => {
|
|
draw_rect(lx1, lx2, both_color);
|
|
}
|
|
(true, true) => {
|
|
draw_rect(lx1, lx2, left_color);
|
|
draw_rect(rx1, rx2, right_color);
|
|
let (x1, x2) = if world.dst_parallax < 0 {
|
|
(lx1, rx2)
|
|
} else {
|
|
(rx1, lx2)
|
|
};
|
|
painter.line_segment(
|
|
[
|
|
res.rect.min + (x1, y1).into(),
|
|
res.rect.min + (x2 + 1.0, y1).into(),
|
|
],
|
|
(2.0, both_color),
|
|
);
|
|
painter.line_segment(
|
|
[
|
|
res.rect.min + (x1, y2).into(),
|
|
res.rect.min + (x2 + 1.0, y2).into(),
|
|
],
|
|
(2.0, both_color),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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, 520.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).at_most(200.0))
|
|
.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,
|
|
bgmaps: 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),
|
|
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),
|
|
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 ImageBuffer) {
|
|
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(),
|
|
)
|
|
}
|
|
}
|
|
|
|
fn render_world(&mut self, world: World, params: &WorldParams, image: &mut ImageBuffer) {
|
|
image.clear();
|
|
|
|
let width = if world.header.mode == WorldMode::Affine {
|
|
world.width & 0x03ff
|
|
} else {
|
|
world.width
|
|
};
|
|
|
|
let height = if world.header.mode == WorldMode::Affine {
|
|
world.height.max(8)
|
|
} else {
|
|
world.height
|
|
};
|
|
|
|
let dx1 = world.dst_x;
|
|
let dx2 = dx1 + 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 palettes = {
|
|
let palettes = self.palettes.borrow().read::<[u8; 8]>(0);
|
|
[
|
|
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 bgmaps = self.bgmaps.borrow();
|
|
let mut chars = [CharCache::new(&chardata), CharCache::new(&chardata)];
|
|
let mut cells = [CellCache::new(&bgmaps), CellCache::new(&bgmaps)];
|
|
let mut source = SourceCoordCalculator::new(&bgmaps, &world);
|
|
|
|
for y in 0..height {
|
|
let dy = y + world.dst_y;
|
|
if !(0..224).contains(&dy) {
|
|
continue;
|
|
}
|
|
|
|
for x in 0..width {
|
|
let dx = x + world.dst_x - world.dst_parallax;
|
|
if world.header.lon && (0..384).contains(&dx) {
|
|
let (sx, sy) = source.left(x, y);
|
|
|
|
let cell_index = world.source_cell(sx, sy);
|
|
let cell = cells[0].get(cell_index);
|
|
let char = chars[0].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);
|
|
let shade = palettes[cell.palette_index][pixel as usize];
|
|
image.add((dx as usize, dy as usize), colors[0][shade as usize]);
|
|
}
|
|
|
|
let dx = x + world.dst_x + world.dst_parallax;
|
|
if world.header.ron && (0..384).contains(&dx) {
|
|
let (sx, sy) = source.right(x, y);
|
|
|
|
let cell_index = world.source_cell(sx, sy);
|
|
let cell = cells[1].get(cell_index);
|
|
let char = chars[1].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);
|
|
let shade = palettes[cell.palette_index][pixel as usize];
|
|
image.add((dx as usize, dy as usize), colors[1][shade as usize]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ImageRenderer<1> for WorldRenderer {
|
|
type Params = WorldParams;
|
|
|
|
fn sizes(&self) -> [[usize; 2]; 1] {
|
|
[[384, 224]]
|
|
}
|
|
|
|
fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 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);
|
|
} 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 update(&self, source: &mut [u16; 16]) -> bool {
|
|
let mut changed = self.header.update(&mut source[0]);
|
|
|
|
let new_dst_x = (self.dst_x as u16 & 0x03ff) | (source[1] & 0xfc00);
|
|
changed |= source[1] != new_dst_x;
|
|
source[1] = new_dst_x;
|
|
|
|
let new_dst_parallax = (self.dst_parallax as u16 & 0x03ff) | (source[2] & 0xfc00);
|
|
changed |= source[2] != new_dst_parallax;
|
|
source[2] = new_dst_parallax;
|
|
|
|
let new_dst_y = self.dst_y as u16;
|
|
changed |= source[3] != new_dst_y;
|
|
source[3] = new_dst_y;
|
|
|
|
let new_src_x = (self.src_x as u16 & 0x1fff) | (source[4] & 0xe000);
|
|
changed |= source[4] != new_src_x;
|
|
source[4] = new_src_x;
|
|
|
|
let new_src_parallax = (self.src_parallax as u16 & 0x7fff) | (source[4] & 0x8000);
|
|
changed |= source[5] != new_src_parallax;
|
|
source[5] = new_src_parallax;
|
|
|
|
let new_src_y = (self.src_y as u16 & 0x1fff) | (source[6] & 0xe000);
|
|
changed |= source[6] != new_src_y;
|
|
source[6] = new_src_y;
|
|
|
|
let new_width = ((self.width - 1) as u16 & 0x1fff) | (source[7] & 0xe000);
|
|
changed |= source[7] != new_width;
|
|
source[7] = new_width;
|
|
|
|
let new_height = (self.height - 1) as u16;
|
|
changed |= source[8] != new_height;
|
|
source[8] = new_height;
|
|
|
|
let new_param_base = self.param_base as u16;
|
|
changed |= source[9] != new_param_base;
|
|
source[9] = new_param_base;
|
|
|
|
let new_overplane = self.overplane as u16;
|
|
changed |= source[10] != new_overplane;
|
|
source[10] = new_overplane;
|
|
|
|
changed
|
|
}
|
|
|
|
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 {
|
|
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 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,
|
|
}
|
|
}
|
|
|
|
fn update(&self, source: &mut u16) -> bool {
|
|
let new_value = (*source & 0x0030)
|
|
| if self.lon { 0x8000 } else { 0x0000 }
|
|
| if self.ron { 0x4000 } else { 0x0000 }
|
|
| self.mode.to_u16().unwrap() << 12
|
|
| ((self.scx as u16) << 10)
|
|
| ((self.scy as u16) << 8)
|
|
| if self.over { 0x0080 } else { 0x0000 }
|
|
| if self.end { 0x0040 } else { 0x0000 }
|
|
| (self.base as u16 & 0x000f);
|
|
let changed = *source != new_value;
|
|
*source = new_value;
|
|
changed
|
|
}
|
|
}
|
|
|
|
#[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",
|
|
})
|
|
}
|
|
}
|
|
|
|
struct CellCache<'a> {
|
|
bgmaps: &'a MemoryRef<'a>,
|
|
index: usize,
|
|
cell: CellData,
|
|
}
|
|
|
|
impl<'a> CellCache<'a> {
|
|
fn new(bgmaps: &'a 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: &'a MemoryRef<'a>,
|
|
index: usize,
|
|
char: [u16; 8],
|
|
}
|
|
|
|
impl<'a> CharCache<'a> {
|
|
fn new(chardata: &'a 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
|
|
}
|
|
}
|
|
|
|
struct SourceCoordCalculator<'a> {
|
|
params: &'a MemoryRef<'a>,
|
|
world: &'a World,
|
|
y: i16,
|
|
param: SourceParam,
|
|
}
|
|
|
|
impl<'a> SourceCoordCalculator<'a> {
|
|
fn new(params: &'a MemoryRef<'a>, world: &'a World) -> Self {
|
|
Self {
|
|
params,
|
|
world,
|
|
y: -1,
|
|
param: SourceParam::Normal,
|
|
}
|
|
}
|
|
|
|
fn left(&mut self, x: i16, y: i16) -> (i16, i16) {
|
|
self.update_param(y);
|
|
match &self.param {
|
|
SourceParam::Normal => {
|
|
let sx = x + self.world.src_x - self.world.src_parallax;
|
|
let sy = y + self.world.src_y;
|
|
(sx, sy)
|
|
}
|
|
SourceParam::HBias(HBiasParam { left, .. }) => {
|
|
let sx = x + self.world.src_x - self.world.src_parallax + *left;
|
|
let sy = y + self.world.src_y;
|
|
(sx, sy)
|
|
}
|
|
SourceParam::Affine(affine) => {
|
|
let sx = affine_coord(affine.src_x, x, affine.dx, affine.src_parallax.min(0));
|
|
let sy = affine_coord(affine.src_y, x, affine.dy, affine.src_parallax.min(0));
|
|
(sx, sy)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn right(&mut self, x: i16, y: i16) -> (i16, i16) {
|
|
self.update_param(y);
|
|
match &self.param {
|
|
SourceParam::Normal => {
|
|
let sx = x + self.world.src_x + self.world.src_parallax;
|
|
let sy = y + self.world.src_y;
|
|
(sx, sy)
|
|
}
|
|
SourceParam::HBias(HBiasParam { right, .. }) => {
|
|
let sx = x + self.world.src_x + self.world.src_parallax + *right;
|
|
let sy = y + self.world.src_y;
|
|
(sx, sy)
|
|
}
|
|
SourceParam::Affine(affine) => {
|
|
let sx = affine_coord(affine.src_x, x, affine.dx, affine.src_parallax.max(0));
|
|
let sy = affine_coord(affine.src_y, x, affine.dy, affine.src_parallax.max(0));
|
|
(sx, sy)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_param(&mut self, y: i16) {
|
|
if self.y == y {
|
|
return;
|
|
}
|
|
if self.world.header.mode == WorldMode::HBias {
|
|
let base = self.world.param_base + (2 * y as usize);
|
|
self.param = SourceParam::HBias(HBiasParam::load(self.params, base));
|
|
}
|
|
if self.world.header.mode == WorldMode::Affine {
|
|
let base = self.world.param_base + (8 * y as usize);
|
|
self.param = SourceParam::Affine(AffineParam::load(self.params, base));
|
|
}
|
|
self.y = y;
|
|
}
|
|
}
|
|
|
|
enum SourceParam {
|
|
Normal,
|
|
HBias(HBiasParam),
|
|
Affine(AffineParam),
|
|
}
|
|
|
|
struct HBiasParam {
|
|
left: i16,
|
|
right: i16,
|
|
data: [u16; 2],
|
|
}
|
|
|
|
impl HBiasParam {
|
|
fn load(params: &MemoryRef, index: usize) -> Self {
|
|
let data = [params.read::<u16>(index), params.read::<u16>(index | 1)];
|
|
let left = (data[0] as i16) << 3 >> 3;
|
|
let right = (data[1] as i16) << 3 >> 3;
|
|
Self { left, right, data }
|
|
}
|
|
|
|
fn save(&self, memory: &MemoryClient, sim: SimId, index: usize) {
|
|
let new_left = (self.left as u16 & 0x1fff) | (self.data[0] & 0xe000);
|
|
if new_left != self.data[0] {
|
|
let address = 0x00020000 + (index * 2);
|
|
memory.write(sim, address as u32, &new_left);
|
|
}
|
|
|
|
let new_right = (self.right as u16 & 0x1fff) | (self.data[1] & 0xe000);
|
|
if new_right != self.data[1] {
|
|
let address = 0x00020000 + ((index | 1) * 2);
|
|
memory.write(sim, address as u32, &new_right);
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AffineParam {
|
|
src_x: FixedI32<U3>,
|
|
src_parallax: i16,
|
|
src_y: FixedI32<U3>,
|
|
dx: FixedI32<U9>,
|
|
dy: FixedI32<U9>,
|
|
data: [u16; 5],
|
|
}
|
|
|
|
impl AffineParam {
|
|
fn load(params: &MemoryRef, index: usize) -> Self {
|
|
let data = [
|
|
params.read(index & 0xffff),
|
|
params.read((index + 1) & 0xffff),
|
|
params.read((index + 2) & 0xffff),
|
|
params.read((index + 3) & 0xffff),
|
|
params.read((index + 4) & 0xffff),
|
|
];
|
|
|
|
let src_x = FixedI32::from_bits(data[0] as i16 as i32);
|
|
let src_parallax = data[1] as i16;
|
|
let src_y = FixedI32::from_bits(data[2] as i16 as i32);
|
|
let dx = FixedI32::from_bits(data[3] as i16 as i32);
|
|
let dy = FixedI32::from_bits(data[4] as i16 as i32);
|
|
|
|
AffineParam {
|
|
src_x,
|
|
src_parallax,
|
|
src_y,
|
|
dx,
|
|
dy,
|
|
data,
|
|
}
|
|
}
|
|
|
|
fn save(&self, memory: &MemoryClient, sim: SimId, index: usize) {
|
|
let new_src_x = self.src_x.to_bits() as u16;
|
|
if new_src_x != self.data[0] {
|
|
let address = 0x00020000 + 2 * (index & 0xffff);
|
|
memory.write(sim, address as u32, &new_src_x);
|
|
}
|
|
|
|
let new_src_parallax = self.src_parallax as u16;
|
|
if new_src_parallax != self.data[1] {
|
|
let address = 0x00020000 + 2 * ((index + 1) & 0xffff);
|
|
memory.write(sim, address as u32, &new_src_parallax);
|
|
}
|
|
|
|
let new_src_y = self.src_y.to_bits() as u16;
|
|
if new_src_y != self.data[2] {
|
|
let address = 0x00020000 + 2 * ((index + 2) & 0xffff);
|
|
memory.write(sim, address as u32, &new_src_y);
|
|
}
|
|
|
|
let new_dx = self.dx.to_bits() as u16;
|
|
if new_dx != self.data[3] {
|
|
let address = 0x00020000 + 2 * ((index + 3) & 0xffff);
|
|
memory.write(sim, address as u32, &new_dx);
|
|
}
|
|
|
|
let new_dy = self.dy.to_bits() as u16;
|
|
if new_dy != self.data[4] {
|
|
let address = 0x00020000 + 2 * ((index + 4) & 0xffff);
|
|
memory.write(sim, address as u32, &new_dy);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn affine_coord(start: FixedI32<U3>, distance: i16, delta: FixedI32<U9>, parallax: i16) -> i16 {
|
|
let start = FixedI32::<U9>::from_num(start);
|
|
let distance = FixedI32::<U9>::from_num(distance);
|
|
let parallax = FixedI32::<U9>::from_num(parallax);
|
|
let coord = start + ((distance + parallax) * delta);
|
|
coord.to_num::<i32>() as i16
|
|
}
|