From 82282ac6077be1c8687085c84e415a1d2f55604b Mon Sep 17 00:00:00 2001 From: kelpsyberry <138107494+kelpsyberry@users.noreply.github.com> Date: Thu, 18 Apr 2024 01:02:14 +0200 Subject: [PATCH] Allow exporting tilemaps and tilesets as PNG (#4) --- Cargo.lock | 42 + core/src/ds_slot/rom.rs | 2 +- frontend/desktop/Cargo.toml | 2 + .../desktop/src/debug_views/bg_maps_2d.rs | 1451 +++++++++++------ frontend/desktop/src/debug_views/fs.rs | 61 +- frontend/desktop/src/ui/config_editor.rs | 4 +- 6 files changed, 1063 insertions(+), 499 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9bb407..fb16d0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -785,6 +785,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.12" @@ -996,6 +1005,7 @@ dependencies = [ "objc", "opener", "parking_lot 0.12.1", + "png", "pollster", "proc-bitfield", "realfft", @@ -1214,6 +1224,25 @@ dependencies = [ "log", ] +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -2250,6 +2279,19 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.6.0" diff --git a/core/src/ds_slot/rom.rs b/core/src/ds_slot/rom.rs index 7e1dc96..a51dc45 100644 --- a/core/src/ds_slot/rom.rs +++ b/core/src/ds_slot/rom.rs @@ -49,7 +49,7 @@ impl Contents for BoxedByteSlice { fn as_any(&self) -> &dyn Any { self } - + fn as_any_mut(&mut self) -> &mut dyn Any { self } diff --git a/frontend/desktop/Cargo.toml b/frontend/desktop/Cargo.toml index 7de65ac..96b8493 100644 --- a/frontend/desktop/Cargo.toml +++ b/frontend/desktop/Cargo.toml @@ -10,6 +10,7 @@ log = ["logging", "dust-core/log"] debug-views = [ "imgui-memory-editor", "realfft", + "png", "dust-core/disasm", "dust-core/channel-audio-capture", ] @@ -62,6 +63,7 @@ crossbeam-channel = "0.5" parking_lot = "0.12" bitflags = "2.5" miniz_oxide = { version = "0.7", features = ["simd"] } +png = { version = "0.17", optional = true } fatfs = { version = "0.3", optional = true } tempfile = { version = "3.10", optional = true } proc-bitfield = { version = "0.4", features = ["nightly"] } diff --git a/frontend/desktop/src/debug_views/bg_maps_2d.rs b/frontend/desktop/src/debug_views/bg_maps_2d.rs index 0257f85..4a2aa5b 100644 --- a/frontend/desktop/src/debug_views/bg_maps_2d.rs +++ b/frontend/desktop/src/debug_views/bg_maps_2d.rs @@ -3,7 +3,7 @@ use super::{ InstanceableFrameViewEmuState, InstanceableView, }; use crate::ui::{ - utils::{add2, combo_value, scale_to_fit, sub2s}, + utils::{add2, combo_value, scale_to_fit, sub2, sub2s}, window::Window, }; use dust_core::{ @@ -16,7 +16,8 @@ use dust_core::{ utils::{mem_prelude::*, zeroed_box}, }; use imgui::{Image, MouseButton, SliderFlags, StyleColor, TextureId, WindowHoveredFlags}; -use std::slice; +use rfd::FileDialog; +use std::{fs::File, io, slice}; #[derive(Clone, Copy, PartialEq, Eq)] enum Engine2d { @@ -24,8 +25,17 @@ enum Engine2d { B, } +impl AsRef for Engine2d { + fn as_ref(&self) -> &str { + match self { + Engine2d::A => "Engine A", + Engine2d::B => "Engine B", + } + } +} + #[derive(Clone, Copy, PartialEq, Eq)] -enum BgDisplayMode { +enum BgMode { Text16, Text256, Affine, @@ -35,75 +45,306 @@ enum BgDisplayMode { LargeBitmap, } +impl AsRef for BgMode { + fn as_ref(&self) -> &str { + match self { + BgMode::Text16 => "Text, 16 colors", + BgMode::Text256 => "Text, 256 colors", + BgMode::Affine => "Affine, 256 colors", + BgMode::ExtendedMap => "Ext, 256 colors", + BgMode::ExtendedBitmap256 => "Ext bitmap, 256 colors", + BgMode::ExtendedBitmapDirect => "Ext bitmap, direct color", + BgMode::LargeBitmap => "Large bitmap, 256 colors", + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum BgResolvedFetchMode { + Text16, + Text256 { uses_ext_pal: bool }, + Affine, + ExtendedMap { uses_ext_pal: bool }, + ExtendedBitmap256, + ExtendedBitmapDirect, + LargeBitmap, +} + +impl BgResolvedFetchMode { + fn uses_ext_pal(self) -> Option { + match self { + BgResolvedFetchMode::Text256 { uses_ext_pal } + | BgResolvedFetchMode::ExtendedMap { uses_ext_pal } => Some(uses_ext_pal), + _ => None, + } + } + + fn palette_size(self) -> usize { + match self { + BgResolvedFetchMode::ExtendedBitmapDirect => 0, + BgResolvedFetchMode::Text256 { uses_ext_pal: true } + | BgResolvedFetchMode::ExtendedMap { uses_ext_pal: true } => 0x1000, + _ => 0x100, + } + } + + fn tiles_sqrt_len(self) -> usize { + match self { + BgResolvedFetchMode::Text16 + | BgResolvedFetchMode::Text256 { .. } + | BgResolvedFetchMode::ExtendedMap { .. } => 0x20, + BgResolvedFetchMode::Affine => 0x10, + _ => 0, + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum BgFetchMode { + Text16, + Text256 { uses_ext_pal: Option }, + Affine, + ExtendedMap { uses_ext_pal: Option }, + ExtendedBitmap256, + ExtendedBitmapDirect, + LargeBitmap, +} + +impl BgFetchMode { + fn resolve(self, default: BgResolvedFetchMode) -> BgResolvedFetchMode { + let default_uses_ext_pal = match default { + BgResolvedFetchMode::Text256 { uses_ext_pal } + | BgResolvedFetchMode::ExtendedMap { uses_ext_pal } => uses_ext_pal, + _ => false, + }; + match self { + BgFetchMode::Text16 => BgResolvedFetchMode::Text16, + BgFetchMode::Text256 { uses_ext_pal } => BgResolvedFetchMode::Text256 { + uses_ext_pal: uses_ext_pal.unwrap_or(default_uses_ext_pal), + }, + BgFetchMode::Affine => BgResolvedFetchMode::Affine, + BgFetchMode::ExtendedMap { uses_ext_pal } => BgResolvedFetchMode::ExtendedMap { + uses_ext_pal: uses_ext_pal.unwrap_or(default_uses_ext_pal), + }, + BgFetchMode::ExtendedBitmap256 => BgResolvedFetchMode::ExtendedBitmap256, + BgFetchMode::ExtendedBitmapDirect => BgResolvedFetchMode::ExtendedBitmapDirect, + BgFetchMode::LargeBitmap => BgResolvedFetchMode::LargeBitmap, + } + } + + fn allows_pal_index(self, default: BgResolvedFetchMode) -> bool { + match self { + BgFetchMode::Text16 => true, + BgFetchMode::Text256 { uses_ext_pal } => uses_ext_pal + .unwrap_or(default == BgResolvedFetchMode::Text256 { uses_ext_pal: true }), + BgFetchMode::ExtendedMap { uses_ext_pal } => uses_ext_pal + .unwrap_or(default == BgResolvedFetchMode::ExtendedMap { uses_ext_pal: true }), + _ => false, + } + } + + fn pal_index_changed(&mut self) { + match self { + BgFetchMode::Text256 { uses_ext_pal } | BgFetchMode::ExtendedMap { uses_ext_pal } => { + *uses_ext_pal = Some(true); + } + _ => {} + } + } + + fn uses_ext_pal_mut(&mut self) -> Option<&mut Option> { + match self { + BgFetchMode::Text256 { uses_ext_pal } | BgFetchMode::ExtendedMap { uses_ext_pal } => { + Some(uses_ext_pal) + } + _ => None, + } + } + + fn has_tiles(self) -> bool { + matches!( + self, + BgFetchMode::Text16 + | BgFetchMode::Text256 { .. } + | BgFetchMode::Affine + | BgFetchMode::ExtendedMap { .. } + ) + } +} + +impl From for BgMode { + fn from(value: BgResolvedFetchMode) -> Self { + match value { + BgResolvedFetchMode::Text16 => BgMode::Text16, + BgResolvedFetchMode::Text256 { .. } => BgMode::Text256, + BgResolvedFetchMode::Affine => BgMode::Affine, + BgResolvedFetchMode::ExtendedMap { .. } => BgMode::ExtendedMap, + BgResolvedFetchMode::ExtendedBitmap256 => BgMode::ExtendedBitmap256, + BgResolvedFetchMode::ExtendedBitmapDirect => BgMode::ExtendedBitmapDirect, + BgResolvedFetchMode::LargeBitmap => BgMode::LargeBitmap, + } + } +} + +impl From for BgMode { + fn from(value: BgFetchMode) -> Self { + match value { + BgFetchMode::Text16 => BgMode::Text16, + BgFetchMode::Text256 { .. } => BgMode::Text256, + BgFetchMode::Affine => BgMode::Affine, + BgFetchMode::ExtendedMap { .. } => BgMode::ExtendedMap, + BgFetchMode::ExtendedBitmap256 => BgMode::ExtendedBitmap256, + BgFetchMode::ExtendedBitmapDirect => BgMode::ExtendedBitmapDirect, + BgFetchMode::LargeBitmap => BgMode::LargeBitmap, + } + } +} + +impl From for BgFetchMode { + fn from(value: BgMode) -> Self { + match value { + BgMode::Text16 => BgFetchMode::Text16, + BgMode::Text256 => BgFetchMode::Text256 { uses_ext_pal: None }, + BgMode::Affine => BgFetchMode::Affine, + BgMode::ExtendedMap => BgFetchMode::ExtendedMap { uses_ext_pal: None }, + BgMode::ExtendedBitmap256 => BgFetchMode::ExtendedBitmap256, + BgMode::ExtendedBitmapDirect => BgFetchMode::ExtendedBitmapDirect, + BgMode::LargeBitmap => BgFetchMode::LargeBitmap, + } + } +} + +impl From for BgFetchMode { + fn from(value: BgResolvedFetchMode) -> Self { + match value { + BgResolvedFetchMode::Text16 => BgFetchMode::Text16, + BgResolvedFetchMode::Text256 { .. } => BgFetchMode::Text256 { uses_ext_pal: None }, + BgResolvedFetchMode::Affine => BgFetchMode::Affine, + BgResolvedFetchMode::ExtendedMap { .. } => { + BgFetchMode::ExtendedMap { uses_ext_pal: None } + } + BgResolvedFetchMode::ExtendedBitmap256 => BgFetchMode::ExtendedBitmap256, + BgResolvedFetchMode::ExtendedBitmapDirect => BgFetchMode::ExtendedBitmapDirect, + BgResolvedFetchMode::LargeBitmap => BgFetchMode::LargeBitmap, + } + } +} + #[derive(Clone, Copy, PartialEq, Eq)] pub struct Selection { engine: Engine2d, bg_index: BgIndex, - use_ext_palettes: Option, - display_mode: Option, +} + +impl Selection { + fn to_default_filename(self, mode: BgResolvedFetchMode, show_tiles: bool) -> String { + let engine = match self.engine { + Engine2d::A => "a", + Engine2d::B => "b", + }; + let bg_index = self.bg_index.get(); + match mode { + BgResolvedFetchMode::Text16 => format!( + "{}_{engine}_{bg_index}_text16", + if show_tiles { "tiles" } else { "bg" } + ), + BgResolvedFetchMode::Text256 { .. } => format!( + "{}_{engine}_{bg_index}_text256", + if show_tiles { "tiles" } else { "bg" } + ), + BgResolvedFetchMode::Affine => format!( + "{}_{engine}_{bg_index}_affine", + if show_tiles { "tiles" } else { "bg" } + ), + BgResolvedFetchMode::ExtendedMap { .. } => format!( + "{}_{engine}_{bg_index}_extmap", + if show_tiles { "tiles" } else { "bg" } + ), + BgResolvedFetchMode::ExtendedBitmap256 => { + format!("bitmap_{engine}_{bg_index}_extbitmap256") + } + BgResolvedFetchMode::ExtendedBitmapDirect => { + format!("bitmap_{engine}_{bg_index}_extbitmapdirect") + } + BgResolvedFetchMode::LargeBitmap => format!("bitmap_{engine}_{bg_index}_largebitmap"), + } + } } #[derive(Clone, Copy)] struct BgData { - display_mode: BgDisplayMode, - uses_ext_palettes: bool, + mode: BgResolvedFetchMode, size: [u16; 2], } -pub struct BgMapData { - bgs: [[BgData; 4]; 2], - selection: Option, - cur_bg: BgData, - tiles: Box>, - tile_bitmap_data: Box>, - palette: Box>, -} - -impl BgMapData { - fn palette_len(&self) -> usize { - if self.cur_bg.display_mode == BgDisplayMode::ExtendedBitmapDirect { - 0 - } else if self.cur_bg.uses_ext_palettes { - 0x1000 - } else { - 0x100 +impl BgData { + fn map_and_tiles_bitmap_size(&self) -> (usize, usize) { + let pixels_len = self.size[0] as usize * self.size[1] as usize; + match self.mode { + BgResolvedFetchMode::Text16 => (pixels_len / 32, 0x400 << 5), + BgResolvedFetchMode::Text256 { .. } | BgResolvedFetchMode::ExtendedMap { .. } => { + (pixels_len / 32, 0x400 << 6) + } + BgResolvedFetchMode::Affine => (pixels_len / 64, 0x100 << 6), + BgResolvedFetchMode::ExtendedBitmap256 | BgResolvedFetchMode::LargeBitmap => { + (0, pixels_len) + } + BgResolvedFetchMode::ExtendedBitmapDirect => (0, pixels_len << 1), } } +} - fn default_bgs() -> [[BgData; 4]; 2] { - [[BgData { - display_mode: BgDisplayMode::Text16, - uses_ext_palettes: false, - size: [128; 2], - }; 4]; 2] - } +#[derive(Clone)] +pub struct BgsData { + bg_defaults: [[BgData; 4]; 2], + + selection: (Selection, Option), + bg: BgData, + + map: Box>, + tiles_bitmap: Box>, + palette: Box>, +} + +impl BgsData { + const DEFAULT_BG_DEFAULT: BgData = BgData { + mode: BgResolvedFetchMode::Text16, + size: [128; 2], + }; } -impl Default for BgMapData { +impl Default for BgsData { fn default() -> Self { - BgMapData { - bgs: Self::default_bgs(), - selection: None, - cur_bg: BgData { - display_mode: BgDisplayMode::Text16, - uses_ext_palettes: false, + BgsData { + bg_defaults: [[Self::DEFAULT_BG_DEFAULT; 4]; 2], + + selection: ( + Selection { + engine: Engine2d::A, + bg_index: BgIndex::new(0), + }, + None, + ), + bg: BgData { + mode: BgResolvedFetchMode::Text16, size: [128; 2], }, - tiles: zeroed_box(), - tile_bitmap_data: zeroed_box(), + + map: zeroed_box(), + tiles_bitmap: zeroed_box(), palette: zeroed_box(), } } } pub struct EmuState { - selection: Selection, + selection: (Selection, Option), } impl super::FrameViewEmuState for EmuState { - type InitData = Selection; - type Message = Selection; - type FrameData = BgMapData; + type InitData = (Selection, Option); + type Message = (Selection, Option); + type FrameData = BgsData; fn new(selection: Self::InitData, _visible: bool, _emu: &mut Emu) -> Self { EmuState { selection } @@ -118,26 +359,27 @@ impl super::FrameViewEmuState for EmuState { emu: &mut Emu, frame_data: S, ) { - fn bg_size(bg: &engine_2d::Bg, display_mode: BgDisplayMode) -> [u16; 2] { - match display_mode { - BgDisplayMode::Text16 | BgDisplayMode::Text256 => match bg.control().size_key() { - 0 => [256, 256], - 1 => [512, 256], - 2 => [256, 512], - _ => [512, 512], - }, - BgDisplayMode::Affine | BgDisplayMode::ExtendedMap => { - [128 << bg.control().size_key(); 2] - } - BgDisplayMode::ExtendedBitmap256 | BgDisplayMode::ExtendedBitmapDirect => { + fn bg_size(bg: &engine_2d::Bg, mode: BgResolvedFetchMode) -> [u16; 2] { + match mode { + BgResolvedFetchMode::Text16 | BgResolvedFetchMode::Text256 { .. } => { match bg.control().size_key() { - 0 => [128, 128], - 1 => [256, 256], - 2 => [512, 256], - _ => [256, 512], + 0 => [256, 256], + 1 => [512, 256], + 2 => [256, 512], + _ => [512, 512], } } - BgDisplayMode::LargeBitmap => match bg.control().size_key() { + BgResolvedFetchMode::Affine | BgResolvedFetchMode::ExtendedMap { .. } => { + [128 << bg.control().size_key(); 2] + } + BgResolvedFetchMode::ExtendedBitmap256 + | BgResolvedFetchMode::ExtendedBitmapDirect => match bg.control().size_key() { + 0 => [128, 128], + 1 => [256, 256], + 2 => [512, 256], + _ => [256, 512], + }, + BgResolvedFetchMode::LargeBitmap => match bg.control().size_key() { 0 => [512, 1024], 1 => [1024, 512], 2 => [512, 256], @@ -146,79 +388,94 @@ impl super::FrameViewEmuState for EmuState { } } - fn get_bgs_data(engine: &engine_2d::Engine2d) -> [BgData; 4] { + fn get_bg_defaults(engine: &engine_2d::Engine2d) -> [BgData; 4] { [0, 1, 2, 3].map(|i| { let bg = &engine.bgs[i]; let text = if bg.control().use_256_colors() { - BgDisplayMode::Text256 + BgResolvedFetchMode::Text256 { + uses_ext_pal: engine.control().bg_ext_pal_enabled(), + } } else { - BgDisplayMode::Text16 + BgResolvedFetchMode::Text16 }; let extended = if bg.control().use_bitmap_extended_bg() { if bg.control().use_direct_color_extended_bg() { - BgDisplayMode::ExtendedBitmapDirect + BgResolvedFetchMode::ExtendedBitmapDirect } else { - BgDisplayMode::ExtendedBitmap256 + BgResolvedFetchMode::ExtendedBitmap256 } } else { - BgDisplayMode::ExtendedMap + BgResolvedFetchMode::ExtendedMap { + uses_ext_pal: engine.control().bg_ext_pal_enabled(), + } }; - let display_mode = match i { + let mode = match i { 0 => text, 1 => text, 2 => match engine.control().bg_mode() { 0..=1 | 3 | 7 => text, - 2 | 4 => BgDisplayMode::Affine, + 2 | 4 => BgResolvedFetchMode::Affine, 5 => extended, - _ => BgDisplayMode::LargeBitmap, + _ => BgResolvedFetchMode::LargeBitmap, }, _ => match engine.control().bg_mode() { 0 | 6..=7 => text, - 1..=2 => BgDisplayMode::Affine, + 1..=2 => BgResolvedFetchMode::Affine, _ => extended, }, }; BgData { - display_mode, - uses_ext_palettes: engine.control().bg_ext_pal_enabled() - && matches!( - display_mode, - BgDisplayMode::Text256 | BgDisplayMode::ExtendedMap - ), - size: bg_size(bg, display_mode), + mode, + size: bg_size(bg, mode), } }) } + fn read_bg_slice_wrapping(vram: &Vram, mut addr: u32, result: &mut [u8]) { + let mut dst_base = 0; + while dst_base != result.len() { + let len = ((R::BG_VRAM_MASK + 1 - addr) as usize).min(result.len() - dst_base); + unsafe { + (if R::IS_A { + Vram::read_a_bg_slice:: + } else { + Vram::read_b_bg_slice:: + })( + vram, + addr, + len, + result.as_mut_ptr().add(dst_base).cast::(), + ); + } + dst_base += len; + addr = 0; + } + } + fn copy_bg_render_data( engine: &engine_2d::Engine2d, vram: &Vram, - selection: Selection, - data: &mut BgMapData, + (selection, mode): (Selection, Option), + data: &mut BgsData, ) { + let all_bg_defaults = get_bg_defaults(engine); + let bg_defaults = all_bg_defaults[selection.bg_index.get() as usize]; let bg = &engine.bgs[selection.bg_index.get() as usize]; - data.cur_bg = { - let orig = data.bgs[selection.engine as usize][selection.bg_index.get() as usize]; - let (display_mode, size) = match selection.display_mode { - Some(display_mode) => (display_mode, bg_size(bg, display_mode)), - None => (orig.display_mode, orig.size), - }; - let uses_ext_palettes = selection - .use_ext_palettes - .unwrap_or_else(|| engine.control().bg_ext_pal_enabled()) - && matches!( - display_mode, - BgDisplayMode::Text256 | BgDisplayMode::ExtendedMap - ); - BgData { - display_mode, - size, - uses_ext_palettes, + + data.bg_defaults[selection.engine as usize] = all_bg_defaults; + data.bg = match mode { + Some(mode) => { + let mode = mode.resolve(bg_defaults.mode); + BgData { + mode, + size: bg_size(bg, mode), + } } + None => bg_defaults, }; let map_base = if R::IS_A { @@ -232,26 +489,11 @@ impl super::FrameViewEmuState for EmuState { } else { Vram::read_b_bg_slice:: }; + let read_bg_slice_wrapping = read_bg_slice_wrapping::; - let read_bg_slice_wrapping = |vram, mut addr, result: &mut [u8]| { - let mut dst_base = 0; - while dst_base != result.len() { - let len = ((R::BG_VRAM_MASK + 1 - addr) as usize).min(result.len() - dst_base); - unsafe { - read_bg_slice( - vram, - addr, - len, - result.as_mut_ptr().add(dst_base) as *mut usize, - ); - } - dst_base += len; - addr = 0; - } - }; - - match data.cur_bg.display_mode { - BgDisplayMode::Text16 | BgDisplayMode::Text256 => unsafe { + // Fetch tiles + match data.bg.mode { + BgResolvedFetchMode::Text16 | BgResolvedFetchMode::Text256 { .. } => unsafe { if bg.control().size_key() & 1 == 0 { let mut src_base = map_base; let mut dst_base = 0; @@ -259,11 +501,11 @@ impl super::FrameViewEmuState for EmuState { read_bg_slice( vram, src_base, - 2 * 32 * 32, - data.tiles.as_mut_ptr().add(dst_base) as *mut usize, + 0x800, + data.map.as_mut_ptr().add(dst_base).cast::(), ); src_base = (src_base + 0x800) & R::BG_VRAM_MASK; - dst_base += 2 * 32 * 32; + dst_base += 0x800; } } else { let mut src_base = map_base; @@ -273,36 +515,36 @@ impl super::FrameViewEmuState for EmuState { read_bg_slice( vram, src_base, - 2 * 32, - data.tiles.as_mut_ptr().add(dst_base) as *mut usize, + 0x40, + data.map.as_mut_ptr().add(dst_base).cast::(), ); read_bg_slice( vram, (src_base + 0x800) & R::BG_VRAM_MASK, - 2 * 32, - data.tiles.as_mut_ptr().add(dst_base + 2 * 32) as *mut usize, + 0x40, + data.map.as_mut_ptr().add(dst_base + 0x40).cast::(), ); - src_base += 2 * 32; - dst_base += 2 * 64; + src_base += 0x40; + dst_base += 0x80; } - src_base += 2 * 32 * 32; + src_base = (src_base + 0x800) & R::BG_VRAM_MASK; } } }, - BgDisplayMode::Affine | BgDisplayMode::ExtendedMap => { - let tiles_len = (data.cur_bg.size[0] as usize * data.cur_bg.size[1] as usize) - >> if data.cur_bg.display_mode == BgDisplayMode::Affine { + BgResolvedFetchMode::Affine | BgResolvedFetchMode::ExtendedMap { .. } => { + let map_size = (data.bg.size[0] as usize * data.bg.size[1] as usize) + >> if data.bg.mode == BgResolvedFetchMode::Affine { 6 } else { 5 }; - read_bg_slice_wrapping(vram, map_base, &mut data.tiles[..tiles_len]); + read_bg_slice_wrapping(vram, map_base, &mut data.map[..map_size]); } - BgDisplayMode::ExtendedBitmap256 - | BgDisplayMode::ExtendedBitmapDirect - | BgDisplayMode::LargeBitmap => {} + BgResolvedFetchMode::ExtendedBitmap256 + | BgResolvedFetchMode::ExtendedBitmapDirect + | BgResolvedFetchMode::LargeBitmap => {} } let tile_base = if R::IS_A { @@ -310,22 +552,26 @@ impl super::FrameViewEmuState for EmuState { } else { bg.control().tile_base() } & R::BG_VRAM_MASK; - let data_base = bg.control().map_base() << 3; - let pixels_len = data.cur_bg.size[0] as usize * data.cur_bg.size[1] as usize; - - let (base_addr, data_len) = match data.cur_bg.display_mode { - BgDisplayMode::Text16 => (tile_base, 0x400 << 5), - BgDisplayMode::Text256 | BgDisplayMode::ExtendedMap => (tile_base, 0x400 << 6), - BgDisplayMode::Affine => (tile_base, 0x100 << 6), - BgDisplayMode::ExtendedBitmap256 => (data_base, pixels_len), - BgDisplayMode::ExtendedBitmapDirect => (data_base, pixels_len * 2), - BgDisplayMode::LargeBitmap => (0, pixels_len), + let bitmap_base = bg.control().map_base() << 3; + let pixels_len = data.bg.size[0] as usize * data.bg.size[1] as usize; + + let (base_addr, tiles_bitmap_size) = match data.bg.mode { + BgResolvedFetchMode::Text16 => (tile_base, 0x400 << 5), + BgResolvedFetchMode::Text256 { .. } | BgResolvedFetchMode::ExtendedMap { .. } => { + (tile_base, 0x400 << 6) + } + BgResolvedFetchMode::Affine => (tile_base, 0x100 << 6), + BgResolvedFetchMode::ExtendedBitmap256 => (bitmap_base, pixels_len), + BgResolvedFetchMode::ExtendedBitmapDirect => (bitmap_base, pixels_len * 2), + BgResolvedFetchMode::LargeBitmap => (0, pixels_len), }; - read_bg_slice_wrapping(vram, base_addr, &mut data.tile_bitmap_data[..data_len]); + read_bg_slice_wrapping(vram, base_addr, &mut data.tiles_bitmap[..tiles_bitmap_size]); - if data.cur_bg.display_mode != BgDisplayMode::ExtendedBitmapDirect { - unsafe { - if data.cur_bg.uses_ext_palettes { + unsafe { + match data.bg.mode { + BgResolvedFetchMode::ExtendedBitmapDirect => {} + BgResolvedFetchMode::Text256 { uses_ext_pal: true } + | BgResolvedFetchMode::ExtendedMap { uses_ext_pal: true } => { let slot = selection.bg_index.get() | if selection.bg_index.get() < 2 { bg.control().bg01_ext_pal_slot() << 1 @@ -336,16 +582,17 @@ impl super::FrameViewEmuState for EmuState { vram.read_a_bg_ext_pal_slice( (slot as u32) << 13, 0x2000, - data.palette.as_mut_ptr() as *mut usize, + data.palette.as_mut_ptr().cast::(), ); } else { vram.read_b_bg_ext_pal_slice( (slot as u32) << 13, 0x2000, - data.palette.as_mut_ptr() as *mut usize, + data.palette.as_mut_ptr().cast::(), ); } - } else { + } + _ => { let pal_base = (!R::IS_A as usize) << 10; data.palette[..0x200] .copy_from_slice(&vram.palette.as_arr()[pal_base..pal_base + 0x200]); @@ -355,10 +602,8 @@ impl super::FrameViewEmuState for EmuState { } let frame_data = frame_data.get_or_insert_with(Default::default); - frame_data.bgs[0] = get_bgs_data(&emu.gpu.engine_2d_a); - frame_data.bgs[1] = get_bgs_data(&emu.gpu.engine_2d_b); - frame_data.selection = Some(self.selection); - match self.selection.engine { + frame_data.selection = self.selection; + match self.selection.0.engine { Engine2d::A => copy_bg_render_data( &emu.gpu.engine_2d_a, &emu.gpu.vram, @@ -377,20 +622,31 @@ impl super::FrameViewEmuState for EmuState { impl InstanceableFrameViewEmuState for EmuState {} +struct DisplayOptions { + show_tiles: bool, + pal_index: u8, +} + pub struct BgMaps2d { - cur_selection: Selection, tex_id: TextureId, + transparency_tex_id: TextureId, show_transparency_checkerboard: bool, - show_grid_lines: bool, + show_grid_lines_tiles: bool, + show_grid_lines_bitmap: bool, + palette_buffer: Box<[u32; 0x1000]>, pixel_buffer: Box<[u32; 1024 * 1024]>, - data: BgMapData, + + selection: (Selection, Option, DisplayOptions), + data: Option, } impl BaseView for BgMaps2d { const MENU_NAME: &'static str = "2D BG maps"; } +const BORDER_WIDTH: f32 = 1.0; + impl FrameView for BgMaps2d { type EmuState = EmuState; @@ -409,19 +665,66 @@ impl FrameView for BgMaps2d { ..Default::default() }, ); + + let transparency_tex_id = { + let tex = window.imgui_gfx.create_owned_texture( + Some("BG map transparency checkerboard".into()), + imgui_wgpu::TextureDescriptor { + width: 1024, + height: 1024, + format: wgpu::TextureFormat::Rgba8Unorm, + ..Default::default() + }, + imgui_wgpu::SamplerDescriptor { + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Linear, + ..Default::default() + }, + ); + + let transparency_colors = 0x0FFF_FFFF_u64 << 32 | 0x03FF_FFFF; + let mut data = Vec::with_capacity(1024 * 1024 * 4); + for y in 0..1024 { + for x in 0..1024 { + data.extend_from_slice( + &((transparency_colors >> ((x ^ y) << 3 & 32)) as u32).to_le_bytes(), + ); + } + } + tex.set_data( + window.gfx_device(), + window.gfx_queue(), + &data, + Default::default(), + ); + + window + .imgui_gfx + .add_texture(imgui_wgpu::Texture::Owned(tex)) + }; + BgMaps2d { - cur_selection: Selection { - engine: Engine2d::A, - bg_index: BgIndex::new(0), - use_ext_palettes: None, - display_mode: None, - }, tex_id, + transparency_tex_id, show_transparency_checkerboard: true, - show_grid_lines: true, + show_grid_lines_tiles: true, + show_grid_lines_bitmap: false, + palette_buffer: zeroed_box(), pixel_buffer: zeroed_box(), - data: BgMapData::default(), + + selection: ( + Selection { + engine: Engine2d::A, + bg_index: BgIndex::new(0), + }, + None, + DisplayOptions { + show_tiles: false, + pal_index: 0, + }, + ), + data: None, } } @@ -430,7 +733,7 @@ impl FrameView for BgMaps2d { } fn emu_state(&self) -> ::InitData { - self.cur_selection + (self.selection.0, self.selection.1.map(Into::into)) } fn update_from_frame_data( @@ -438,22 +741,20 @@ impl FrameView for BgMaps2d { frame_data: &::FrameData, _window: &mut Window, ) { - self.data.bgs = frame_data.bgs; - self.data.selection = frame_data.selection; - self.data.cur_bg = frame_data.cur_bg; - let pixels_len = frame_data.cur_bg.size[0] as usize * frame_data.cur_bg.size[1] as usize; - let (tiles_len, tile_bitmap_data_len) = match frame_data.cur_bg.display_mode { - BgDisplayMode::Text16 => (pixels_len / 32, 0x400 << 5), - BgDisplayMode::Text256 | BgDisplayMode::ExtendedMap => (pixels_len / 32, 0x400 << 6), - BgDisplayMode::Affine => (pixels_len / 64, 0x100 << 6), - BgDisplayMode::ExtendedBitmap256 | BgDisplayMode::LargeBitmap => (0, pixels_len), - BgDisplayMode::ExtendedBitmapDirect => (0, pixels_len * 2), - }; - self.data.tiles[..tiles_len].copy_from_slice(&frame_data.tiles[..tiles_len]); - self.data.tile_bitmap_data[..tile_bitmap_data_len] - .copy_from_slice(&frame_data.tile_bitmap_data[..tile_bitmap_data_len]); - let palette_len = frame_data.palette_len() << 1; - self.data.palette[..palette_len].copy_from_slice(&frame_data.palette[..palette_len]); + if let Some(data) = &mut self.data { + data.bg_defaults = frame_data.bg_defaults; + data.selection = frame_data.selection; + data.bg = frame_data.bg; + + let (map_size, tiles_bitmap_size) = frame_data.bg.map_and_tiles_bitmap_size(); + data.map[..map_size].copy_from_slice(&frame_data.map[..map_size]); + data.tiles_bitmap[..tiles_bitmap_size] + .copy_from_slice(&frame_data.tiles_bitmap[..tiles_bitmap_size]); + let palette_size = frame_data.bg.mode.palette_size() << 1; + data.palette[..palette_size].copy_from_slice(&frame_data.palette[..palette_size]); + } else { + self.data = Some(frame_data.clone()); + } } fn draw( @@ -468,129 +769,188 @@ impl FrameView for BgMaps2d { ui.open_popup("options"); } + let content_width = ui.content_region_avail()[0]; + let mut selection_updated = false; - let content_width = ui.content_region_avail()[0]; - let two_widgets_total_width = content_width - style!(ui, item_spacing)[0]; - - ui.set_next_item_width(two_widgets_total_width * (1.0 / 3.0)); - let mut cur_engine = self.cur_selection.engine as u8; - selection_updated |= ui - .slider_config("##engine", 0_u8, 1) - .display_format(match self.cur_selection.engine { - Engine2d::A => "Engine A", - Engine2d::B => "Engine B", + let three_width = content_width - 2.0 * style!(ui, item_spacing)[0]; + + { + ui.set_next_item_width(three_width * (1.0 / 3.0)); + let mut engine = self.selection.0.engine as u8; + selection_updated |= ui + .slider_config("##engine", 0_u8, 1) + .display_format(self.selection.0.engine.as_ref()) + .flags(SliderFlags::NO_INPUT) + .build(&mut engine); + self.selection.0.engine = match engine { + 0 => Engine2d::A, + _ => Engine2d::B, + }; + + ui.same_line(); + ui.set_next_item_width(three_width * (1.0 / 3.0)); + let mut bg_index = self.selection.0.bg_index.get(); + selection_updated |= ui + .slider_config("##bg_index", 0_u8, 3) + .display_format("BG%d") + .flags(SliderFlags::NO_INPUT) + .build(&mut bg_index); + self.selection.0.bg_index = BgIndex::new(bg_index); + } + + if selection_updated { + self.selection.1 = None; + } + + let default_bg_data = self + .data + .as_ref() + .map(|data| { + data.bg_defaults[self.selection.0.engine as usize] + [self.selection.0.bg_index.get() as usize] }) - .flags(SliderFlags::NO_INPUT) - .build(&mut cur_engine); - self.cur_selection.engine = match cur_engine { - 0 => Engine2d::A, - _ => Engine2d::B, - }; + .unwrap_or(BgsData::DEFAULT_BG_DEFAULT); ui.same_line(); - ui.set_next_item_width(two_widgets_total_width * (2.0 / 3.0)); - let mut cur_bg_index = self.cur_selection.bg_index.get(); - selection_updated |= ui - .slider_config("##bg_index", 0_u8, 3) - .display_format("BG%d") - .flags(SliderFlags::NO_INPUT) - .build(&mut cur_bg_index); - self.cur_selection.bg_index = BgIndex::new(cur_bg_index); + ui.set_next_item_width(three_width * (1.0 / 3.0)); + { + static BG_MODES: [Option; 8] = [ + None, + Some(BgMode::Text16), + Some(BgMode::Text256), + Some(BgMode::Affine), + Some(BgMode::ExtendedMap), + Some(BgMode::ExtendedBitmap256), + Some(BgMode::ExtendedBitmapDirect), + Some(BgMode::LargeBitmap), + ]; + + let mut mode = self.selection.1.map(Into::into); + if combo_value(ui, "##mode", &mut mode, &BG_MODES, |mode| match mode { + None => format!("Default ({})", BgMode::from(default_bg_data.mode).as_ref()).into(), + Some(mode) => mode.as_ref().into(), + }) { + self.selection.1 = mode.map(Into::into); + selection_updated = true; + } + } + + let mut fetch_mode = self + .selection + .1 + .unwrap_or_else(|| default_bg_data.mode.into()); + let has_tiles = fetch_mode.has_tiles(); if selection_updated { - self.cur_selection.use_ext_palettes = None; - self.cur_selection.display_mode = None; + self.selection.2.show_tiles = false; + self.selection.2.pal_index = 0; } - let default_bg_data = self.data.bgs[self.cur_selection.engine as usize] - [self.cur_selection.bg_index.get() as usize]; + 'display_mode_options: { + let uses_ext_pal = fetch_mode.uses_ext_pal_mut(); - ui.align_text_to_frame_padding(); - ui.text("Mode:"); - ui.same_line(); - ui.set_next_item_width( - ui.content_region_avail()[0] - - (two_widgets_total_width * 0.5 + style!(ui, item_spacing)[0]), - ); + let count = uses_ext_pal.is_some() as u8 + has_tiles as u8; + if count == 0 { + break 'display_mode_options; + } - static BG_DISPLAY_MODES: [Option; 8] = [ - None, - Some(BgDisplayMode::Text16), - Some(BgDisplayMode::Text256), - Some(BgDisplayMode::Affine), - Some(BgDisplayMode::ExtendedMap), - Some(BgDisplayMode::ExtendedBitmap256), - Some(BgDisplayMode::ExtendedBitmapDirect), - Some(BgDisplayMode::LargeBitmap), - ]; - - selection_updated |= combo_value( - ui, - "##display_mode", - &mut self.cur_selection.display_mode, - &BG_DISPLAY_MODES, - |display_mode: &Option| { - let label_display_mode = |display_mode| match display_mode { - BgDisplayMode::Text16 => "Text, 16 colors", - BgDisplayMode::Text256 => "Text, 256 colors", - BgDisplayMode::Affine => "Affine, 256 colors", - BgDisplayMode::ExtendedMap => "Extended, 256 colors", - BgDisplayMode::ExtendedBitmap256 => "Extended bitmap, 256 colors", - BgDisplayMode::ExtendedBitmapDirect => "Extended bitmap, direct color", - BgDisplayMode::LargeBitmap => "Large bitmap, 256 colors", - }; - match display_mode { - None => format!( - "Default ({})", - label_display_mode(default_bg_data.display_mode) - ) - .into(), - Some(display_mode) => label_display_mode(*display_mode).into(), - } - }, - ); + let select_width = + (content_width - (count - 1) as f32 * style!(ui, item_spacing)[0]) / count as f32; + + let mut mode_updated = false; + + if let Some(uses_ext_pal) = uses_ext_pal { + static EXT_PAL_SETTINGS: [Option; 3] = [None, Some(false), Some(true)]; + static LABELS: [&str; 2] = ["Standard palette", "Extended palette"]; + + ui.set_next_item_width(select_width); + mode_updated |= combo_value( + ui, + "##uses_ext_pals", + uses_ext_pal, + &EXT_PAL_SETTINGS, + |uses_ext_pals| match uses_ext_pals { + None => format!( + "Default ({})", + LABELS[default_bg_data.mode.uses_ext_pal().unwrap_or(false) as usize] + ) + .into(), + Some(value) => LABELS[*value as usize].into(), + }, + ); + ui.same_line(); + } - ui.same_line(); - ui.text("Use ext palettes:"); - ui.same_line(); - ui.set_next_item_width(ui.content_region_avail()[0]); - - static EXT_PALETTE_SETTINGS: [Option; 3] = [None, Some(true), Some(false)]; - - selection_updated |= combo_value( - ui, - "##ext_palettes", - &mut self.cur_selection.use_ext_palettes, - &EXT_PALETTE_SETTINGS, - |use_ext_palettes: &Option| match use_ext_palettes { - None => format!( - "Default ({})", - if default_bg_data.uses_ext_palettes { - "Yes" + if has_tiles { + ui.set_next_item_width(select_width); + let mut show_tiles = self.selection.2.show_tiles as u8; + mode_updated |= ui + .slider_config("##show_tiles", 0_u8, 1) + .display_format(if self.selection.2.show_tiles { + "Tileset" } else { - "No" - }, - ) - .into(), - Some(true) => "Yes".into(), - Some(false) => "No".into(), - }, - ); + "Tilemap" + }) + .flags(SliderFlags::NO_INPUT) + .build(&mut show_tiles); + self.selection.2.show_tiles = show_tiles != 0; + ui.same_line(); + } - if selection_updated { - messages.push(self.cur_selection); + if mode_updated { + self.selection.1 = Some(fetch_mode); + selection_updated = true; + } + + ui.new_line(); } - if self.data.selection == Some(self.cur_selection) { - ui.align_text_to_frame_padding(); - ui.text(format!( - "Size: {}x{}", - self.data.cur_bg.size[0], self.data.cur_bg.size[1] - )); - ui.same_line(); + let updated_data = self + .data + .as_ref() + .filter(|data| data.selection == (self.selection.0, self.selection.1)); + + let options_export_width = ui.calc_text_size("Options...")[0] + + style!(ui, item_spacing)[0] + + 4.0 * style!(ui, frame_padding)[0] + + ui.calc_text_size("Export...")[0]; + if let Some(data) = updated_data { + if self.selection.2.show_tiles { + if fetch_mode.allows_pal_index(data.bg.mode) { + ui.set_next_item_width( + ui.content_region_avail()[0] + - style!(ui, item_spacing)[0] + - options_export_width, + ); + if ui + .slider_config("##pal_index", 0_u8, 15) + .display_format("Palette %d") + .flags(SliderFlags::NO_INPUT) + .build(&mut self.selection.2.pal_index) + { + fetch_mode.pal_index_changed(); + self.selection.1 = Some(fetch_mode); + selection_updated = true; + } + ui.same_line(); + } + } else { + ui.align_text_to_frame_padding(); + ui.text(format!("Size: {}x{}", data.bg.size[0], data.bg.size[1])); + ui.same_line(); + } + } + + if selection_updated { + messages.push((self.selection.0, self.selection.1)); } + ui.set_cursor_pos([ + ui.content_region_max()[0] - options_export_width, + ui.cursor_pos()[1], + ]); + if ui.button("Options...") { ui.open_popup("options"); } @@ -601,249 +961,361 @@ impl FrameView for BgMaps2d { &mut self.show_transparency_checkerboard, ); - ui.checkbox("Show grid lines", &mut self.show_grid_lines); + ui.checkbox("Show tile grid lines", &mut self.show_grid_lines_tiles); + + ui.checkbox("Show bitmap grid lines", &mut self.show_grid_lines_bitmap); }); - if self.data.selection != Some(self.cur_selection) { + ui.same_line(); + let mut export_requested = false; + ui.enabled(updated_data.is_some(), || { + export_requested = ui.button("Export..."); + }); + + let Some(data) = updated_data else { + ui.text("Loading..."); return; - } + }; + + let tiles_per_row = data.bg.mode.tiles_sqrt_len(); + let image_pixels = if self.selection.2.show_tiles { + [tiles_per_row << 3; 2] + } else { + [data.bg.size[0] as usize, data.bg.size[1] as usize] + }; let (mut image_pos, image_size) = scale_to_fit( - self.data.cur_bg.size[0] as f32 / self.data.cur_bg.size[1] as f32, + image_pixels[0] as f32 / image_pixels[1] as f32, ui.content_region_avail(), ); image_pos[0] += style!(ui, window_padding)[0]; image_pos[1] += ui.cursor_pos()[1]; + + if self.show_transparency_checkerboard { + ui.set_cursor_pos(image_pos); + Image::new(self.transparency_tex_id, image_size) + .uv1(image_pixels.map(|size| size as f32 / 1024.0)) + .build(ui); + } ui.set_cursor_pos(image_pos); Image::new(self.tex_id, image_size) - .uv1(self.data.cur_bg.size.map(|size| size as f32 / 1024.0)) + .uv1(image_pixels.map(|size| size as f32 / 1024.0)) .build(ui); - if !matches!( - self.data.cur_bg.display_mode, - BgDisplayMode::ExtendedBitmap256 - | BgDisplayMode::ExtendedBitmapDirect - | BgDisplayMode::LargeBitmap - ) { - let window_abs_pos = ui.window_pos(); - let image_abs_pos = add2(window_abs_pos, image_pos); - let tiles = [ - self.data.cur_bg.size[0] as usize >> 3, - self.data.cur_bg.size[1] as usize >> 3, - ]; - let tile_size = [ - image_size[0] / tiles[0] as f32, - image_size[1] / tiles[1] as f32, + let show_grid_lines = if has_tiles { + self.show_grid_lines_tiles + } else { + self.show_grid_lines_bitmap + }; + let show_zoom_tooltip = ui.is_item_hovered(); + + if show_grid_lines || show_zoom_tooltip { + let window_screen_pos = ui.window_pos(); + let image_screen_pos = add2(window_screen_pos, image_pos); + + let grid_size = [image_pixels[0] >> 3, image_pixels[1] >> 3]; + let cell_size = [ + image_size[0] / grid_size[0] as f32, + image_size[1] / grid_size[1] as f32, ]; let border_color = ui.style_color(StyleColor::Border); - if self.show_grid_lines { - let line_thickness = 1.0; - let half_line_thickness = line_thickness * 0.5; + if show_grid_lines { + let start_screen_pos = sub2s(image_screen_pos, 0.5 * BORDER_WIDTH); + let end_screen_pos = add2(start_screen_pos, image_size); let draw_list = ui.get_window_draw_list(); - let image_abs_end_pos = add2(image_abs_pos, image_size); - for x in 0..=tiles[0] { - let x_pos = image_abs_pos[0] + x as f32 * tile_size[0]; + for x in 0..=grid_size[0] { + let x_pos = start_screen_pos[0] + x as f32 * cell_size[0]; draw_list .add_line( - sub2s([x_pos, image_abs_pos[1]], half_line_thickness), - sub2s([x_pos, image_abs_end_pos[1]], half_line_thickness), + [x_pos, start_screen_pos[1]], + [x_pos, end_screen_pos[1]], border_color, ) - .thickness(line_thickness) + .thickness(BORDER_WIDTH) .build(); } - for y in 0..=tiles[1] { - let y_pos = image_abs_pos[1] + y as f32 * tile_size[1]; + for y in 0..=grid_size[1] { + let y_pos = start_screen_pos[1] + y as f32 * cell_size[1]; draw_list .add_line( - sub2s([image_abs_pos[0], y_pos], half_line_thickness), - sub2s([image_abs_end_pos[0], y_pos], half_line_thickness), + [start_screen_pos[0], y_pos], + [end_screen_pos[0], y_pos], border_color, ) - .thickness(line_thickness) + .thickness(BORDER_WIDTH) .build(); } } - if ui.is_item_hovered() { + if show_zoom_tooltip { ui.tooltip(|| { - let font_size = ui.current_font_size(); - let mouse_abs_pos = ui.io().mouse_pos; - let tile_pos = [0, 1] - .map(|i| ((mouse_abs_pos[i] - image_abs_pos[i]) / tile_size[i]) as usize); - let image = Image::new(self.tex_id, [font_size * 4.0, font_size * 4.0]) - .border_col(border_color) - .uv0(tile_pos.map(|pos| pos as f32 / 128.0)) - .uv1(tile_pos.map(|pos| (pos + 1) as f32 / 128.0)); - let map_entry_index = tile_pos[1] * tiles[0] + tile_pos[0]; - if self.data.cur_bg.display_mode == BgDisplayMode::Affine { - ui.text(format!("Tile {:#04X}", self.data.tiles[map_entry_index])); - image.build(ui); - } else { - let tile = self.data.tiles.read_le::(map_entry_index << 1); - ui.text(format!("Tile {:#05X}", tile & 0x3FF)); - image.build(ui); - ui.align_text_to_frame_padding(); - ui.text("Flip: "); - ui.same_line(); - ui.checkbox("X", &mut (tile & 0x400 != 0)); - ui.same_line_with_spacing(0.0, style!(ui, item_spacing)[0] + 4.0); - ui.checkbox("Y", &mut (tile & 0x800 != 0)); - if self.data.cur_bg.display_mode == BgDisplayMode::Text16 - || self.data.cur_bg.uses_ext_palettes - { - ui.text(format!("Palette number: {:#03X}", tile >> 12)); + let tooltip_image_size = [ui.current_font_size() * 4.0; 2]; + let mouse_pos = sub2(ui.io().mouse_pos, image_screen_pos); + + if self.show_transparency_checkerboard { + let image_pos = ui.cursor_screen_pos(); + Image::new(self.transparency_tex_id, tooltip_image_size) + .uv1([1.0 / 128.0; 2]) + .build(ui); + ui.set_cursor_screen_pos(image_pos); + } + + if has_tiles { + let cell_pos = [ + (mouse_pos[0] / cell_size[0]) as usize, + (mouse_pos[1] / cell_size[1]) as usize, + ]; + + Image::new(self.tex_id, tooltip_image_size) + .border_col(border_color) + .uv0(cell_pos.map(|pos| pos as f32 / 128.0)) + .uv1(cell_pos.map(|pos| (pos + 1) as f32 / 128.0)) + .build(ui); + + let cell_index = cell_pos[1] * grid_size[0] + cell_pos[0]; + if self.selection.2.show_tiles { + ui.text(format!( + "Tile {cell_index:#0width$X}", + width = 4 + (data.bg.mode != BgResolvedFetchMode::Affine) as usize + )); + } else { + if data.bg.mode == BgResolvedFetchMode::Affine { + ui.text(format!("Tile {:#04X}", data.map[cell_index])); + } else { + let tile = data.map.read_le::(cell_index << 1); + ui.text(format!("Tile {:#05X}", tile & 0x3FF)); + ui.align_text_to_frame_padding(); + ui.text("Flip: "); + ui.same_line(); + ui.checkbox("X", &mut (tile & 0x400 != 0)); + ui.same_line_with_spacing(0.0, style!(ui, item_spacing)[0] + 4.0); + ui.checkbox("Y", &mut (tile & 0x800 != 0)); + if data.bg.mode == BgResolvedFetchMode::Text16 + || data.bg.mode.uses_ext_pal() == Some(true) + { + ui.text(format!("Palette number: {:#03X}", tile >> 12)); + } + } + ui.text(format!("X: {}, Y: {}", cell_pos[0] * 8, cell_pos[1] * 8)); } + } else { + let pixel_pos = [ + ((mouse_pos[0] / image_size[0] * image_pixels[0] as f32) as usize) + .saturating_sub(4) + .min(image_pixels[0] - 8), + ((mouse_pos[1] / image_size[1] * image_pixels[1] as f32) as usize) + .saturating_sub(4) + .min(image_pixels[1] - 8), + ]; + + Image::new(self.tex_id, tooltip_image_size) + .border_col(border_color) + .uv0(pixel_pos.map(|pos| pos as f32 / 1024.0)) + .uv1(pixel_pos.map(|pos| (pos + 8) as f32 / 1024.0)) + .build(ui); + + ui.text(format!("X: {}, Y: {}", pixel_pos[0], pixel_pos[1])); } - ui.text(format!("X: {}, Y: {}", tile_pos[0] * 8, tile_pos[1] * 8)); }); } } - for (i, color) in self.palette_buffer[..self.data.palette_len()] + for (i, color) in self.palette_buffer[..data.bg.mode.palette_size()] .iter_mut() .enumerate() { - let orig_color = self.data.palette.read_le::(i << 1); + let orig_color = data.palette.read_le::(i << 1); *color = rgb5_to_rgba8(orig_color); } - let pixels_len = self.data.cur_bg.size[0] as usize * self.data.cur_bg.size[1] as usize; - let x_shift = self.data.cur_bg.size[0].trailing_zeros(); - - let transparency_colors = if self.show_transparency_checkerboard { - 0x0FFF_FFFF_u64 << 32 | 0x03FF_FFFF - } else { - 0 - }; + let pixels_len = image_pixels[0] * image_pixels[1]; unsafe { - match self.data.cur_bg.display_mode { - BgDisplayMode::Text16 => { - let tile_x_shift = x_shift - 3; - let tile_i_x_mask = (1 << tile_x_shift) - 1; - for tile_i in 0..pixels_len / 64 { - let tile = self - .data - .tiles - .read_le_aligned_unchecked::(tile_i << 1) - as usize; - let src_base = (tile & 0x3FF) << 5; - let dst_base = - (tile_i >> tile_x_shift << 10 | (tile_i & tile_i_x_mask)) << 3; - let pal_base = tile >> 8 & 0xF0; - let src_x_xor_mask = if tile & 0x400 != 0 { 7 } else { 0 }; - let src_y_xor_mask = if tile & 0x800 != 0 { 7 } else { 0 }; - for y in 0..8 { - let src_base = src_base | (y ^ src_y_xor_mask) << 2; - let dst_base = dst_base | y << 10; - for x in 0..8 { - let src_x = x ^ src_x_xor_mask; - let color_index = *self - .data - .tile_bitmap_data - .get_unchecked(src_base | src_x >> 1) - >> ((src_x & 1) << 2) - & 0xF; - *self.pixel_buffer.get_unchecked_mut(dst_base | x) = - if color_index == 0 { - (transparency_colors >> ((x ^ y) << 3 & 32)) as u32 - } else { - self.palette_buffer[pal_base | color_index as usize] - }; + match data.bg.mode { + BgResolvedFetchMode::Text16 => { + if self.selection.2.show_tiles { + for tile in 0..0x400 { + let src_base = tile << 5; + let dst_base = + ((tile / tiles_per_row) << 10 | (tile % tiles_per_row)) << 3; + let pal_base = (self.selection.2.pal_index << 4) as usize; + for y in 0..8 { + let src_base = src_base | y << 2; + let dst_base = dst_base | y << 10; + let pixels = data.tiles_bitmap.read_le_unchecked::(src_base); + for x in 0..8 { + let color_index = pixels >> (x << 2) & 0xF; + *self.pixel_buffer.get_unchecked_mut(dst_base | x) = + if color_index == 0 { + 0 + } else { + self.palette_buffer[pal_base | color_index as usize] + }; + } + } + } + } else { + let x_shift = data.bg.size[0].trailing_zeros(); + let tile_x_shift = x_shift - 3; + let tile_i_x_mask = (1 << tile_x_shift) - 1; + for tile_i in 0..pixels_len / 64 { + let tile = data.map.read_le_unchecked::(tile_i << 1) as usize; + let src_base = (tile & 0x3FF) << 5; + let dst_base = + (tile_i >> tile_x_shift << 10 | (tile_i & tile_i_x_mask)) << 3; + let pal_base = tile >> 8 & 0xF0; + let src_x_xor_mask = if tile & 0x400 != 0 { 7 } else { 0 }; + let src_y_xor_mask = if tile & 0x800 != 0 { 7 } else { 0 }; + for y in 0..8 { + let src_base = src_base | (y ^ src_y_xor_mask) << 2; + let dst_base = dst_base | y << 10; + for x in 0..8 { + let src_x = x ^ src_x_xor_mask; + let color_index = + data.tiles_bitmap.read_unchecked(src_base | src_x >> 1) + >> ((src_x & 1) << 2) + & 0xF; + *self.pixel_buffer.get_unchecked_mut(dst_base | x) = + if color_index == 0 { + 0 + } else { + self.palette_buffer[pal_base | color_index as usize] + }; + } } } } } - BgDisplayMode::Text256 | BgDisplayMode::ExtendedMap => { - let tile_x_shift = x_shift - 3; - let tile_i_x_mask = (1 << tile_x_shift) - 1; - for tile_i in 0..pixels_len / 64 { - let tile = self - .data - .tiles - .read_le_aligned_unchecked::(tile_i << 1) - as usize; - let src_base = (tile & 0x3FF) << 6; - let dst_base = - (tile_i >> tile_x_shift << 10 | (tile_i & tile_i_x_mask)) << 3; - let pal_base = if self.data.cur_bg.uses_ext_palettes { - tile >> 4 & 0xF00 - } else { - 0 - }; - let src_x_xor_mask = if tile & 0x400 != 0 { 7 } else { 0 }; - let src_y_xor_mask = if tile & 0x800 != 0 { 7 } else { 0 }; - for y in 0..8 { - let src_base = src_base | (y ^ src_y_xor_mask) << 3; - let dst_base = dst_base | y << 10; - for x in 0..8 { - let color_index = *self - .data - .tile_bitmap_data - .get_unchecked(src_base | (x ^ src_x_xor_mask)); - *self.pixel_buffer.get_unchecked_mut(dst_base | x) = - if color_index == 0 { - (transparency_colors >> ((x ^ y) << 3 & 32)) as u32 + BgResolvedFetchMode::Text256 { uses_ext_pal } + | BgResolvedFetchMode::ExtendedMap { uses_ext_pal } => { + if self.selection.2.show_tiles { + for tile in 0..0x400 { + let src_base = tile << 6; + let dst_base = + ((tile / tiles_per_row) << 10 | (tile % tiles_per_row)) << 3; + let pal_base = if uses_ext_pal { + (self.selection.2.pal_index as usize & 0xF) << 8 + } else { + 0 + }; + for y in 0..8 { + let src_base = src_base | y << 3; + let dst_base = dst_base | y << 10; + for (dst_color, &color_index) in self + .pixel_buffer + .get_unchecked_mut(dst_base..dst_base + 8) + .iter_mut() + .zip(data.tiles_bitmap.get_unchecked(src_base..src_base + 8)) + { + *dst_color = if color_index == 0 { + 0 } else { self.palette_buffer[pal_base | color_index as usize] }; + } + } + } + } else { + let x_shift = data.bg.size[0].trailing_zeros(); + let tile_x_shift = x_shift - 3; + let tile_i_x_mask = (1 << tile_x_shift) - 1; + for tile_i in 0..pixels_len / 64 { + let tile = data.map.read_le_unchecked::(tile_i << 1) as usize; + let src_base = (tile & 0x3FF) << 6; + let dst_base = + (tile_i >> tile_x_shift << 10 | (tile_i & tile_i_x_mask)) << 3; + let pal_base = if uses_ext_pal { tile >> 4 & 0xF00 } else { 0 }; + let src_x_xor_mask = if tile & 0x400 != 0 { 7 } else { 0 }; + let src_y_xor_mask = if tile & 0x800 != 0 { 7 } else { 0 }; + for y in 0..8 { + let src_base = src_base | (y ^ src_y_xor_mask) << 3; + let dst_base = dst_base | y << 10; + for x in 0..8 { + let color_index = data + .tiles_bitmap + .read_unchecked(src_base | (x ^ src_x_xor_mask)); + *self.pixel_buffer.get_unchecked_mut(dst_base | x) = + if color_index == 0 { + 0 + } else { + self.palette_buffer[pal_base | color_index as usize] + }; + } } } } } - BgDisplayMode::Affine => { - let tile_x_shift = x_shift - 3; - let tile_i_x_mask = (1 << tile_x_shift) - 1; - for tile_i in 0..pixels_len / 64 { - let src_base = (self.data.tiles[tile_i] as usize) << 6; - let dst_base = - (tile_i >> tile_x_shift << 10 | (tile_i & tile_i_x_mask)) << 3; - for y in 0..8 { - let src_base = src_base | y << 3; - let dst_base = dst_base | y << 10; - for (x, (dst_color, &color_index)) in self - .pixel_buffer - .get_unchecked_mut(dst_base..dst_base + 8) - .iter_mut() - .zip( - self.data - .tile_bitmap_data - .get_unchecked(src_base..src_base + 8), - ) - .enumerate() - { - *dst_color = if color_index == 0 { - (transparency_colors >> ((x ^ y) << 3 & 32)) as u32 - } else { - self.palette_buffer[color_index as usize] - }; + BgResolvedFetchMode::Affine => { + if self.selection.2.show_tiles { + for tile in 0..0x100 { + let src_base = tile << 6; + let dst_base = + ((tile / tiles_per_row) << 10 | (tile % tiles_per_row)) << 3; + for y in 0..8 { + let src_base = src_base | y << 3; + let dst_base = dst_base | y << 10; + for (dst_color, &color_index) in self + .pixel_buffer + .get_unchecked_mut(dst_base..dst_base + 8) + .iter_mut() + .zip(data.tiles_bitmap.get_unchecked(src_base..src_base + 8)) + { + *dst_color = if color_index == 0 { + 0 + } else { + self.palette_buffer[color_index as usize] + }; + } + } + } + } else { + let x_shift = data.bg.size[0].trailing_zeros(); + let tile_x_shift = x_shift - 3; + let tile_i_x_mask = (1 << tile_x_shift) - 1; + for tile_i in 0..pixels_len / 64 { + let src_base = (data.map[tile_i] as usize) << 6; + let dst_base = + (tile_i >> tile_x_shift << 10 | (tile_i & tile_i_x_mask)) << 3; + for y in 0..8 { + let src_base = src_base | y << 3; + let dst_base = dst_base | y << 10; + for (dst_color, &color_index) in self + .pixel_buffer + .get_unchecked_mut(dst_base..dst_base + 8) + .iter_mut() + .zip(data.tiles_bitmap.get_unchecked(src_base..src_base + 8)) + { + *dst_color = if color_index == 0 { + 0 + } else { + self.palette_buffer[color_index as usize] + }; + } } } } } - BgDisplayMode::ExtendedBitmap256 | BgDisplayMode::LargeBitmap => { - for y in 0..self.data.cur_bg.size[1] as usize { + BgResolvedFetchMode::ExtendedBitmap256 | BgResolvedFetchMode::LargeBitmap => { + let x_shift = data.bg.size[0].trailing_zeros(); + for y in 0..data.bg.size[1] as usize { let src_base = y << x_shift; let dst_base = y << 10; - for (x, (dst_color, &color_index)) in self + for (dst_color, &color_index) in self .pixel_buffer - .get_unchecked_mut( - dst_base..dst_base + self.data.cur_bg.size[0] as usize, - ) + .get_unchecked_mut(dst_base..dst_base + data.bg.size[0] as usize) .iter_mut() - .zip(self.data.tile_bitmap_data.get_unchecked( - src_base..src_base + self.data.cur_bg.size[0] as usize, - )) - .enumerate() + .zip( + data.tiles_bitmap + .get_unchecked(src_base..src_base + data.bg.size[0] as usize), + ) { *dst_color = if color_index == 0 { - (transparency_colors >> ((x ^ y) << 3 & 32)) as u32 + 0 } else { self.palette_buffer[color_index as usize] }; @@ -851,18 +1323,18 @@ impl FrameView for BgMaps2d { } } - BgDisplayMode::ExtendedBitmapDirect => { - for y in 0..self.data.cur_bg.size[1] as usize { + BgResolvedFetchMode::ExtendedBitmapDirect => { + let x_shift = data.bg.size[0].trailing_zeros(); + for y in 0..data.bg.size[1] as usize { let src_base = y << (x_shift + 1); let dst_base = y << 10; - for x in 0..self.data.cur_bg.size[0] as usize { - let color = self - .data - .tile_bitmap_data - .read_le_aligned_unchecked::(src_base + (x << 1)); + for x in 0..data.bg.size[0] as usize { + let color = data + .tiles_bitmap + .read_le_unchecked::(src_base + (x << 1)); *self.pixel_buffer.get_unchecked_mut(dst_base + x) = if color & 0x8000 == 0 { - (transparency_colors >> ((x ^ y) << 3 & 32)) as u32 + 0 } else { rgb5_to_rgba8(color) }; @@ -883,11 +1355,52 @@ impl FrameView for BgMaps2d { slice::from_raw_parts(self.pixel_buffer.as_ptr() as *const u8, 1024 * 1024 * 4) }, imgui_wgpu::TextureSetRange { - width: Some(self.data.cur_bg.size[0] as u32), - height: Some(self.data.cur_bg.size[1] as u32), + width: Some(image_pixels[0] as u32), + height: Some(image_pixels[1] as u32), ..Default::default() }, ); + + if export_requested { + if let Some(dst_path) = FileDialog::new() + .add_filter("PNG image", &["png"]) + .set_file_name( + self.selection + .0 + .to_default_filename(data.bg.mode, self.selection.2.show_tiles), + ) + .save_file() + { + if let Err(err) = (|| -> io::Result<()> { + let [width, height] = image_pixels; + + let file = File::create(&dst_path)?; + let mut encoder = png::Encoder::new(file, width as u32, height as u32); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + encoder.set_srgb(png::SrgbRenderingIntent::Perceptual); + let mut writer = encoder.write_header()?; + + let mut data = Vec::with_capacity(4 * pixels_len); + for y in 0..height { + let y_base = y * 1024; + for pixel in &self.pixel_buffer[y_base..y_base + width] { + data.extend_from_slice(&pixel.to_le_bytes()) + } + } + writer.write_image_data(&data)?; + + writer.finish()?; + Ok(()) + })() { + error!( + "Export error", + "Couldn't complete export to `{}`: {err}", + dst_path.display() + ); + } + } + } } } diff --git a/frontend/desktop/src/debug_views/fs.rs b/frontend/desktop/src/debug_views/fs.rs index c267e35..1168f94 100644 --- a/frontend/desktop/src/debug_views/fs.rs +++ b/frontend/desktop/src/debug_views/fs.rs @@ -131,36 +131,43 @@ impl super::MessageViewEmuState for EmuState { .spawn(move || { const EXPORT_CHUNK_SIZE: usize = 0x100000; // 1 MB - if let Err(err) = (move || -> io::Result<()> { - use dust_core::ds_slot::rom::Contents; + let mut exported_size = 0; + let mut buffer = zeroed_box::>(); - let mut exported_size = 0; - let mut buffer = zeroed_box::>(); + if let Err((err, dst_path)) = + files.into_iter().try_for_each(|(dst_path, start, size)| { + (|| -> io::Result<()> { + use dust_core::ds_slot::rom::Contents; - for (dst_path, start, size) in files { - if let Some(parent) = dst_path.parent() { - fs::create_dir_all(parent)?; - } - let mut file = fs::File::create(dst_path)?; - - let mut addr = start; - let end = start + size; - while addr < end { - let len = buffer.len().min((end - addr) as usize); + if let Some(parent) = dst_path.parent() { + fs::create_dir_all(parent)?; + } + let mut file = fs::File::create(&dst_path)?; - let buffer = &mut buffer[..len]; - rom.read_slice_wrapping(addr, buffer); - file.write_all(buffer)?; + let mut addr = start; + let end = start + size; + while addr < end { + let len = buffer.len().min((end - addr) as usize); - addr += len as u32; - exported_size += len as u64; - exported_size_shared.store(exported_size, Ordering::Relaxed); - } - } + let buffer = &mut buffer[..len]; + rom.read_slice_wrapping(addr, buffer); + file.write_all(buffer)?; - Ok(()) - })() { - error!("Export error", "Couldn't complete export: {err}"); + addr += len as u32; + exported_size += len as u64; + exported_size_shared + .store(exported_size, Ordering::Relaxed); + } + Ok(()) + })() + .map_err(|e| (e, dst_path)) + }) + { + error!( + "Export error", + "Couldn't complete export at `{}`: {err}", + dst_path.display() + ); } }) .expect("couldn't spawn export thread"); @@ -548,7 +555,7 @@ impl MessageView for Fs { } ui.popup("export", || { - if ui.button("Export") { + if ui.button("Export...") { export_dir( if path.is_empty() { "/".to_owned() @@ -618,7 +625,7 @@ impl MessageView for Fs { } ui.popup("export", || { - if ui.button("Export") { + if ui.button("Export...") { export_file( path.clone(), &entry.name, diff --git a/frontend/desktop/src/ui/config_editor.rs b/frontend/desktop/src/ui/config_editor.rs index 140a729..208b58b 100644 --- a/frontend/desktop/src/ui/config_editor.rs +++ b/frontend/desktop/src/ui/config_editor.rs @@ -1189,7 +1189,7 @@ impl Editor { modify_configs!( ui, width bot_button_width, - icon_tooltip "\u{f56f}", "Import ", + icon_tooltip "\u{f56f}", "Import", "import", self.data.game_loaded, import_config!(deserialize_global), @@ -1216,7 +1216,7 @@ impl Editor { modify_configs!( ui, width bot_button_width, - icon_tooltip "\u{f56e}", "Export ", + icon_tooltip "\u{f56e}", "Export", "export", self.data.game_loaded, export_config!(serialize_global, "global_config.json"),