Compare commits

...
8 Commits
19 changed files with 842 additions and 151 deletions
Generated
+1 -1
View File
@@ -2040,7 +2040,7 @@ checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
[[package]] [[package]]
name = "lemur" name = "lemur"
version = "0.15.1" version = "0.15.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"atoi", "atoi",
+1 -1
View File
@@ -4,7 +4,7 @@ description = "An emulator for the Virtual Boy."
repository = "https://git.virtual-boy.com/PVB/lemur" repository = "https://git.virtual-boy.com/PVB/lemur"
publish = false publish = false
license = "MIT" license = "MIT"
version = "0.15.1" version = "0.15.2"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
+8 -4
View File
@@ -23,7 +23,7 @@ use winit::{
}; };
use crate::{ use crate::{
config::{AppConfig, CliArgs}, config::{AppConfig, CliArgs, Config},
controller::ControllerManager, controller::ControllerManager,
emulator::{EmulatorClient, EmulatorCommand, SimId}, emulator::{EmulatorClient, EmulatorCommand, SimId},
filesystem::Persistence, filesystem::Persistence,
@@ -94,7 +94,7 @@ impl Application {
handle: OwnedDisplayHandle, handle: OwnedDisplayHandle,
persistence: Persistence, persistence: Persistence,
args: CliArgs, args: CliArgs,
config: AppConfig, config: Config<AppConfig>,
) -> Self { ) -> Self {
let wgpu = WgpuState::new(handle, config); let wgpu = WgpuState::new(handle, config);
let icon = load_icon().ok().map(Arc::new); let icon = load_icon().ok().map(Arc::new);
@@ -179,6 +179,9 @@ impl Application {
if args.registers { if args.registers {
initial_windows.push(ChildWindow::Registers); initial_windows.push(ChildWindow::Registers);
} }
if args.memory {
initial_windows.push(ChildWindow::Memory);
}
if args.terminal { if args.terminal {
initial_windows.push(ChildWindow::Terminal); initial_windows.push(ChildWindow::Terminal);
} }
@@ -504,11 +507,11 @@ impl ApplicationHandler<UserEvent> for Application {
struct WgpuState { struct WgpuState {
instance: wgpu::Instance, instance: wgpu::Instance,
device_descriptor: Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>, device_descriptor: Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
config: AppConfig, config: Config<AppConfig>,
} }
impl WgpuState { impl WgpuState {
fn new(handle: OwnedDisplayHandle, config: AppConfig) -> Self { fn new(handle: OwnedDisplayHandle, config: Config<AppConfig>) -> Self {
#[allow(unused_variables)] #[allow(unused_variables)]
let egui_wgpu::WgpuSetupCreateNew { let egui_wgpu::WgpuSetupCreateNew {
instance_descriptor: wgpu::InstanceDescriptor { backends, .. }, instance_descriptor: wgpu::InstanceDescriptor { backends, .. },
@@ -531,6 +534,7 @@ impl WgpuState {
} }
fn init(&mut self, window: &Window) -> egui_wgpu::WgpuSetupExisting { fn init(&mut self, window: &Window) -> egui_wgpu::WgpuSetupExisting {
self.config.refresh();
let power_preference = let power_preference =
wgpu::PowerPreference::from_env().unwrap_or(self.config.power_preference); wgpu::PowerPreference::from_env().unwrap_or(self.config.power_preference);
debug!("power preference: {power_preference:?}"); debug!("power preference: {power_preference:?}");
+17 -23
View File
@@ -1,9 +1,9 @@
use anyhow::Result;
use bytemuck::NoUninit; use bytemuck::NoUninit;
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use egui::{Color32, Pos2, Vec2}; use egui::{Color32, Pos2, Vec2};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub use crate::filesystem::Config;
use crate::{emulator::SimId, filesystem::Persistence, window::DisplayMode}; use crate::{emulator::SimId, filesystem::Persistence, window::DisplayMode};
use std::path::PathBuf; use std::path::PathBuf;
@@ -36,6 +36,9 @@ pub struct CliArgs {
/// Open registers window /// Open registers window
#[arg(short, long)] #[arg(short, long)]
pub registers: bool, pub registers: bool,
/// Open memory window
#[arg(short, long)]
pub memory: bool,
/// Open terminal /// Open terminal
#[arg(short, long)] #[arg(short, long)]
pub terminal: bool, pub terminal: bool,
@@ -116,38 +119,31 @@ pub struct AppConfig {
#[serde(default = "default_save_data_location")] #[serde(default = "default_save_data_location")]
pub save_data_location: SaveDataLocation, pub save_data_location: SaveDataLocation,
} }
impl Default for AppConfig {
impl AppConfig { fn default() -> Self {
pub fn load(persistence: &Persistence) -> Self {
if let Ok(config) = persistence.load_config(APP_CONFIG_FILENAME) {
return config;
}
Self { Self {
power_preference: default_power_preference(), power_preference: default_power_preference(),
force_fallback: false, force_fallback: false,
save_data_location: default_save_data_location(), save_data_location: default_save_data_location(),
} }
} }
}
pub fn save(&self, persistence: &Persistence) -> Result<()> { impl AppConfig {
persistence.save_config(APP_CONFIG_FILENAME, &self) pub fn load(persistence: &Persistence) -> Config<Self> {
persistence.config(APP_CONFIG_FILENAME)
} }
pub fn update(&mut self, args: &CliArgs) -> bool { pub fn apply_updates(&mut self, args: &CliArgs) {
let mut updated = false;
if let Some(pref) = args.wgpu_power_preference { if let Some(pref) = args.wgpu_power_preference {
self.power_preference = pref.into(); self.power_preference = pref.into();
updated = true;
} }
if let Some(force_fallback) = args.wgpu_force_fallback { if let Some(force_fallback) = args.wgpu_force_fallback {
self.force_fallback = force_fallback; self.force_fallback = force_fallback;
updated = true;
} }
if let Some(save_data_location) = args.save_data_location { if let Some(save_data_location) = args.save_data_location {
self.save_data_location = save_data_location; self.save_data_location = save_data_location;
updated = true;
} }
updated
} }
} }
@@ -171,12 +167,8 @@ pub struct SimConfig {
#[serde(default)] #[serde(default)]
pub position: Option<Pos2>, pub position: Option<Pos2>,
} }
impl Default for SimConfig {
impl SimConfig { fn default() -> Self {
pub fn load(persistence: &Persistence, sim_id: SimId) -> Self {
if let Ok(config) = persistence.load_config(sim_config_filename(sim_id)) {
return config;
}
Self { Self {
display_mode: DisplayMode::Anaglyph, display_mode: DisplayMode::Anaglyph,
colors: COLOR_PRESETS[0], colors: COLOR_PRESETS[0],
@@ -187,9 +179,11 @@ impl SimConfig {
position: None, position: None,
} }
} }
}
pub fn save(&self, persistence: &Persistence, sim_id: SimId) -> Result<()> { impl SimConfig {
persistence.save_config(sim_config_filename(sim_id), self) pub fn load(persistence: &Persistence, sim_id: SimId) -> Config<Self> {
persistence.config(sim_config_filename(sim_id))
} }
} }
+13 -4
View File
@@ -1,4 +1,7 @@
use std::{ffi::c_void, sync::{LazyLock, Mutex}}; use std::{
ffi::c_void,
sync::{LazyLock, Mutex},
};
#[derive(Default)] #[derive(Default)]
struct Reallocator { struct Reallocator {
@@ -55,7 +58,8 @@ impl Reallocator {
// actually reallocate and copy the data over // actually reallocate and copy the data over
let mut new_data = Allocation::new(new_size); let mut new_data = Allocation::new(new_size);
let bytes_to_copy = old_data.size.min(new_size); let bytes_to_copy = old_data.size.min(new_size);
new_data.as_mut_slice()[..bytes_to_copy].copy_from_slice(&old_data.as_slice()[..bytes_to_copy]); new_data.as_mut_slice()[..bytes_to_copy]
.copy_from_slice(&old_data.as_slice()[..bytes_to_copy]);
self.store_allocation(new_data) self.store_allocation(new_data)
} }
} else if new_size > 0 { } else if new_size > 0 {
@@ -71,7 +75,11 @@ impl Reallocator {
if ptr.is_null() { if ptr.is_null() {
return None; return None;
} }
let index = self.allocations.iter().enumerate().find_map(|(i, alloc)| std::ptr::addr_eq(alloc.ptr, ptr).then_some(i))?; let index = self
.allocations
.iter()
.enumerate()
.find_map(|(i, alloc)| std::ptr::addr_eq(alloc.ptr, ptr).then_some(i))?;
let allocation = self.allocations.remove(index); let allocation = self.allocations.remove(index);
Some(allocation) Some(allocation)
} }
@@ -83,7 +91,8 @@ impl Reallocator {
} }
} }
static VBU_ALLOC: LazyLock<Mutex<Reallocator>> = LazyLock::new(|| Mutex::new(Reallocator::default())); static VBU_ALLOC: LazyLock<Mutex<Reallocator>> =
LazyLock::new(|| Mutex::new(Reallocator::default()));
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
#[allow(nonstandard_style)] #[allow(nonstandard_style)]
+116 -1
View File
@@ -1,4 +1,14 @@
use std::{fs, path::PathBuf}; use std::{
any::Any,
collections::{HashMap, hash_map::Entry},
fs,
ops::Deref,
path::PathBuf,
sync::{
Arc, Mutex, RwLock,
atomic::{AtomicU64, Ordering},
},
};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use directories::ProjectDirs; use directories::ProjectDirs;
@@ -7,12 +17,56 @@ use serde::{Deserialize, Serialize};
#[derive(Clone)] #[derive(Clone)]
pub struct Persistence { pub struct Persistence {
config_dir: Option<PathBuf>, config_dir: Option<PathBuf>,
states: Arc<Mutex<HashMap<String, Arc<dyn Any + Send + Sync>>>>,
} }
impl Persistence { impl Persistence {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
config_dir: init_config_dir(), config_dir: init_config_dir(),
states: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn config<T: ConfigCompatible>(&self, file: &str) -> Config<T> {
let state = {
let mut lock = self.states.lock().unwrap();
match lock.entry(file.to_string()) {
Entry::Vacant(e) => {
let path = self
.config_dir
.as_ref()
.map(|p| p.join(file).with_extension("json"));
let value = if let Some(p) = path.as_ref()
&& let Ok(bytes) = fs::read(p)
&& let Ok(v) = serde_json::from_slice(&bytes)
{
v
} else {
T::default()
};
let state = Arc::new(ConfigState {
current: RwLock::new(value),
version: AtomicU64::new(0),
path,
});
e.insert(state.clone());
state
}
Entry::Occupied(e) => e
.get()
.clone()
.downcast()
.expect("same config file accessed with two types"),
}
};
let value = state.current.read().unwrap().clone();
let version = state.version.load(Ordering::Relaxed);
Config {
value,
version,
state,
} }
} }
@@ -50,3 +104,64 @@ pub fn init_data_dir() -> Option<PathBuf> {
fn project_dirs() -> Option<ProjectDirs> { fn project_dirs() -> Option<ProjectDirs> {
ProjectDirs::from("com", "virtual-boy", "Lemur") ProjectDirs::from("com", "virtual-boy", "Lemur")
} }
pub trait ConfigCompatible:
Send + Sync + 'static + Clone + Default + Eq + Serialize + for<'a> Deserialize<'a>
{
}
impl<T: Send + Sync + 'static + Clone + Default + Eq + Serialize + for<'a> Deserialize<'a>>
ConfigCompatible for T
{
}
struct ConfigState<T> {
current: RwLock<T>,
version: AtomicU64,
path: Option<PathBuf>,
}
// A wrapper for a config struct, loaded from disk and referenced across the codebase.
// Cheap to read, and lets you sync changes to all copies.
pub struct Config<T> {
value: T,
version: u64,
state: Arc<ConfigState<T>>,
}
impl<T> Deref for Config<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T: ConfigCompatible> Config<T> {
pub fn refresh(&mut self) {
let version = self.state.version.load(Ordering::Relaxed);
if self.version == version {
return;
}
self.value = self.state.current.read().unwrap().clone();
self.version = version;
}
pub fn update(&mut self, mut update: impl FnMut(&mut T)) {
let mut new_value = self.value.clone();
update(&mut new_value);
if self.value == new_value {
return;
}
let mut current = self.state.current.write().unwrap();
update(&mut current);
if let Some(path) = &self.state.path
&& let Ok(bytes) = serde_json::to_vec(&*current)
{
let _ = fs::write(path, bytes);
}
let version = self.state.version.fetch_add(1, Ordering::Relaxed) + 1;
self.value = current.clone();
self.version = version;
}
}
+1 -5
View File
@@ -220,11 +220,7 @@ impl ImageBuffer {
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.pixels[index]; let old = self.pixels[index];
self.pixels[index] = Color32::from_rgb( self.pixels[index] = old + pixel;
old.r() + pixel.r(),
old.g() + pixel.g(),
old.b() + pixel.b(),
);
} }
pub fn changed(&self, image: &ColorImage) -> bool { pub fn changed(&self, image: &ColorImage) -> bool {
+2 -3
View File
@@ -94,9 +94,8 @@ fn main() -> Result<()> {
let persistence = Persistence::new(); let persistence = Persistence::new();
let mut config = AppConfig::load(&persistence); let mut config = AppConfig::load(&persistence);
if config.update(&args) { config.update(|c| c.apply_updates(&args));
let _ = config.save(&persistence);
}
let p1 = SimConfig::load(&persistence, SimId::Player1); let p1 = SimConfig::load(&persistence, SimId::Player1);
let p2 = SimConfig::load(&persistence, SimId::Player2); let p2 = SimConfig::load(&persistence, SimId::Player2);
+66 -5
View File
@@ -37,7 +37,53 @@ impl MemoryClient {
.send_command(EmulatorCommand::WatchMemory(range, Arc::downgrade(&region))); .send_command(EmulatorCommand::WatchMemory(range, Arc::downgrade(&region)));
region region
}); });
MemoryView { region } MemoryView {
region,
sim,
start,
length,
}
}
// Update an existing view to point to a new region of memory.
// If the old and new regions overlap, it'll retain any old values.
pub fn retarget(&self, view: &mut MemoryView, sim: SimId, start: u32, length: usize) {
let range = MemoryRange { sim, start, length };
let mut regions = self.regions.lock().unwrap_or_else(|e| e.into_inner());
let region = regions
.get(&range)
.and_then(|r| r.upgrade())
.unwrap_or_else(|| {
let region = Arc::new(MemoryRegion::new(start, length));
if view.range().overlaps(range) {
let (old_index, new_index) = if start < view.start {
(0, view.start as usize - start as usize)
} else {
(start as usize - view.start as usize, 0)
};
let len = (view.length - old_index).min(length - new_index);
let mut new_data = vec![0u8; length];
let old_data = view.borrow();
for (dst, src) in new_data[new_index..(new_index + len)]
.iter_mut()
.zip(old_data.range::<u8>(old_index, len))
{
*dst = src;
}
drop(old_data);
region.update(&new_data);
}
regions.insert(range, Arc::downgrade(&region));
self.client
.send_command(EmulatorCommand::WatchMemory(range, Arc::downgrade(&region)));
region
});
*view = MemoryView {
region,
sim,
start,
length,
}
} }
pub fn write<T: MemoryValue>(&self, sim: SimId, address: u32, data: &T) { pub fn write<T: MemoryValue>(&self, sim: SimId, address: u32, data: &T) {
@@ -64,12 +110,23 @@ fn aligned_memory(start: u32, length: usize) -> BoxBytes {
pub struct MemoryView { pub struct MemoryView {
region: Arc<MemoryRegion>, region: Arc<MemoryRegion>,
sim: SimId,
start: u32,
length: usize,
} }
impl MemoryView { impl MemoryView {
pub fn borrow(&self) -> MemoryRef<'_> { pub fn borrow(&self) -> MemoryRef<'_> {
self.region.borrow() self.region.borrow()
} }
pub fn range(&self) -> MemoryRange {
MemoryRange {
sim: self.sim,
start: self.start,
length: self.length,
}
}
} }
pub struct MemoryRef<'a> { pub struct MemoryRef<'a> {
@@ -183,6 +240,14 @@ pub struct MemoryRange {
pub length: usize, pub length: usize,
} }
impl MemoryRange {
pub fn overlaps(&self, other: MemoryRange) -> bool {
self.sim == other.sim
&& (self.start as usize) < other.start as usize + other.length
&& (other.start as usize) < self.start as usize + self.length
}
}
const BUFFERS: usize = 4; const BUFFERS: usize = 4;
pub struct MemoryRegion { pub struct MemoryRegion {
gens: [AtomicU64; BUFFERS], gens: [AtomicU64; BUFFERS],
@@ -195,10 +260,6 @@ impl Debug for MemoryRegion {
.finish_non_exhaustive() .finish_non_exhaustive()
} }
} }
// SAFETY: BoxBytes is meant to be Send+Sync, will be in a future version
unsafe impl Send for MemoryRegion {}
// SAFETY: BoxBytes is meant to be Send+Sync, will be in a future version
unsafe impl Sync for MemoryRegion {}
impl MemoryRegion { impl MemoryRegion {
fn new(start: u32, length: usize) -> Self { fn new(start: u32, length: usize) -> Self {
+2
View File
@@ -7,6 +7,7 @@ pub use game_screen::{DisplayMode, GameScreen};
pub use gdb::GdbServerWindow; pub use gdb::GdbServerWindow;
pub use hotkeys::HotkeysWindow; pub use hotkeys::HotkeysWindow;
pub use input::InputWindow; pub use input::InputWindow;
pub use memory::MemoryWindow;
pub use profile::ProfileWindow; pub use profile::ProfileWindow;
pub use terminal::TerminalWindow; pub use terminal::TerminalWindow;
pub use vip::{ pub use vip::{
@@ -22,6 +23,7 @@ mod game_screen;
mod gdb; mod gdb;
mod hotkeys; mod hotkeys;
mod input; mod input;
mod memory;
mod profile; mod profile;
mod terminal; mod terminal;
mod utils; mod utils;
+44 -34
View File
@@ -7,7 +7,7 @@ use std::{
use crate::{ use crate::{
app::UserEvent, app::UserEvent,
config::{AppConfig, COLOR_PRESETS, SaveDataLocation, SimConfig}, config::{AppConfig, COLOR_PRESETS, Config, SaveDataLocation, SimConfig},
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState}, emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
filepicker::FilePicker, filepicker::FilePicker,
filesystem::Persistence, filesystem::Persistence,
@@ -16,8 +16,8 @@ use crate::{
memory::MemoryClient, memory::MemoryClient,
window::{ window::{
AboutWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GdbServerWindow, AboutWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GdbServerWindow,
HotkeysWindow, InitArgs, InputWindow, ObjectWindow, ProfileWindow, RegisterWindow, HotkeysWindow, InitArgs, InputWindow, MemoryWindow, ObjectWindow, ProfileWindow,
TerminalWindow, WorldWindow, utils::UiData, RegisterWindow, TerminalWindow, WorldWindow, utils::UiData,
}, },
}; };
use anyhow::Context as _; use anyhow::Context as _;
@@ -42,7 +42,7 @@ pub struct GameWindow {
persistence: Persistence, persistence: Persistence,
shortcuts: ShortcutProvider, shortcuts: ShortcutProvider,
sim_id: SimId, sim_id: SimId,
config: SimConfig, config: Config<SimConfig>,
toasts: Toasts, toasts: Toasts,
screen: Option<GameScreen>, screen: Option<GameScreen>,
messages: mpsc::Receiver<Toast>, messages: mpsc::Receiver<Toast>,
@@ -124,25 +124,39 @@ impl GameWindow {
ChildWindow::CharacterData => AppWrapper::of_dyn(CharacterDataWindow::new( ChildWindow::CharacterData => AppWrapper::of_dyn(CharacterDataWindow::new(
self.sim_id, self.sim_id,
&self.memory, &self.memory,
&self.persistence,
&self.images,
)),
ChildWindow::BgMap => AppWrapper::of_dyn(BgMapWindow::new(
self.sim_id,
&self.memory,
&self.persistence,
&self.images,
)),
ChildWindow::Objects => AppWrapper::of_dyn(ObjectWindow::new(
self.sim_id,
&self.memory,
&self.persistence,
&self.images,
)),
ChildWindow::Worlds => AppWrapper::of_dyn(WorldWindow::new(
self.sim_id,
&self.memory,
&self.persistence,
&self.images, &self.images,
)), )),
ChildWindow::BgMap => {
AppWrapper::of_dyn(BgMapWindow::new(self.sim_id, &self.memory, &self.images))
}
ChildWindow::Objects => {
AppWrapper::of_dyn(ObjectWindow::new(self.sim_id, &self.memory, &self.images))
}
ChildWindow::Worlds => {
AppWrapper::of_dyn(WorldWindow::new(self.sim_id, &self.memory, &self.images))
}
ChildWindow::FrameBuffers => AppWrapper::of_dyn(FrameBufferWindow::new( ChildWindow::FrameBuffers => AppWrapper::of_dyn(FrameBufferWindow::new(
self.sim_id, self.sim_id,
&self.memory, &self.memory,
&self.persistence,
&self.images, &self.images,
)), )),
ChildWindow::Registers => { ChildWindow::Registers => AppWrapper::of_dyn(RegisterWindow::new(
AppWrapper::of_dyn(RegisterWindow::new(self.sim_id, &self.memory)) self.sim_id,
} &self.memory,
&self.persistence,
)),
ChildWindow::Memory => AppWrapper::of_dyn(MemoryWindow::new(self.sim_id, &self.memory)),
ChildWindow::Terminal => { ChildWindow::Terminal => {
AppWrapper::of_dyn(TerminalWindow::new(self.sim_id, &self.client)) AppWrapper::of_dyn(TerminalWindow::new(self.sim_id, &self.client))
} }
@@ -334,7 +348,7 @@ impl GameWindow {
} }
let watch_rom = self.client.is_rom_watched(self.sim_id); let watch_rom = self.client.is_rom_watched(self.sim_id);
if ui.selectable_button(watch_rom, "Watch ROM").clicked() { if ui.selectable_button(watch_rom, "Watch ROM").clicked() {
self.update_config(|c| c.watch_rom = !watch_rom); self.config.update(|c| c.watch_rom = !watch_rom);
self.client self.client
.send_command(EmulatorCommand::WatchRom(self.sim_id, !watch_rom)); .send_command(EmulatorCommand::WatchRom(self.sim_id, !watch_rom));
} }
@@ -395,7 +409,7 @@ impl GameWindow {
ui.separator(); ui.separator();
let low_battery = self.config.low_battery; let low_battery = self.config.low_battery;
if ui.selectable_button(low_battery, "Low Battery").clicked() { if ui.selectable_button(low_battery, "Low Battery").clicked() {
self.update_config(|c| c.low_battery = !low_battery); self.config.update(|c| c.low_battery = !low_battery);
let _ = self let _ = self
.proxy .proxy
.send_event(UserEvent::LowBattery(self.sim_id, !low_battery)); .send_event(UserEvent::LowBattery(self.sim_id, !low_battery));
@@ -432,6 +446,9 @@ impl GameWindow {
if ui.button("GDB Server").clicked() { if ui.button("GDB Server").clicked() {
self.open(ChildWindow::Debugger { port: None }); self.open(ChildWindow::Debugger { port: None });
} }
if ui.button("Memory").clicked() {
self.open(ChildWindow::Memory);
}
ui.separator(); ui.separator();
if ui.button("Character Data").clicked() { if ui.button("Character Data").clicked() {
self.open(ChildWindow::CharacterData); self.open(ChildWindow::CharacterData);
@@ -551,7 +568,7 @@ impl GameWindow {
ui.send_viewport_cmd(ViewportCommand::InnerSize(current_dims * scale)); ui.send_viewport_cmd(ViewportCommand::InnerSize(current_dims * scale));
} }
self.update_config(|c| { self.config.update(|c| {
c.display_mode = display_mode; c.display_mode = display_mode;
c.dimensions = current_dims * scale; c.dimensions = current_dims * scale;
}); });
@@ -559,7 +576,7 @@ impl GameWindow {
ui.menu_button("Colors", |ui| { ui.menu_button("Colors", |ui| {
for preset in COLOR_PRESETS { for preset in COLOR_PRESETS {
if ui.color_pair_button(preset[0], preset[1]).clicked() { if ui.color_pair_button(preset[0], preset[1]).clicked() {
self.update_config(|c| c.colors = preset); self.config.update(|c| c.colors = preset);
} }
} }
ui.with_layout(ui.layout().with_cross_align(egui::Align::Center), |ui| { ui.with_layout(ui.layout().with_cross_align(egui::Align::Center), |ui| {
@@ -589,7 +606,7 @@ impl GameWindow {
.selectable_button(self.config.audio_enabled, "Enabled") .selectable_button(self.config.audio_enabled, "Enabled")
.clicked() .clicked()
{ {
self.update_config(|c| c.audio_enabled = !c.audio_enabled); self.config.update(|c| c.audio_enabled = !c.audio_enabled);
self.client.send_command(EmulatorCommand::SetAudioEnabled( self.client.send_command(EmulatorCommand::SetAudioEnabled(
self.sim_id, self.sim_id,
self.config.audio_enabled, self.config.audio_enabled,
@@ -611,8 +628,7 @@ impl GameWindow {
return; return;
}; };
let mut app_config = AppConfig::load(&self.persistence); let mut app_config = AppConfig::load(&self.persistence);
app_config.save_data_location = new_location; app_config.update(|c| c.save_data_location = new_location);
let _ = app_config.save(&self.persistence);
self.client self.client
.send_command(EmulatorCommand::SetSaveDataLocation(new_location)); .send_command(EmulatorCommand::SetSaveDataLocation(new_location));
}; };
@@ -662,19 +678,10 @@ impl GameWindow {
self.color_picker = None; self.color_picker = None;
} }
if updated { if updated {
self.update_config(|c| c.colors = colors); self.config.update(|c| c.colors = colors);
} }
} }
fn update_config(&mut self, update: impl FnOnce(&mut SimConfig)) {
let mut new_config = self.config.clone();
update(&mut new_config);
if self.config != new_config {
let _ = new_config.save(&self.persistence, self.sim_id);
}
self.config = new_config;
}
fn button_for(&self, ctx: &Context, text: &str, command: Command) -> Button<'_> { fn button_for(&self, ctx: &Context, text: &str, command: Command) -> Button<'_> {
let button = Button::new(text); let button = Button::new(text);
match self.shortcuts.shortcut_for(command) { match self.shortcuts.shortcut_for(command) {
@@ -701,6 +708,7 @@ impl AppWindow for GameWindow {
} }
fn show(&mut self, ui: &mut Ui) { fn show(&mut self, ui: &mut Ui) {
self.config.refresh();
let has_player_2 = self.client.has_player_2(); let has_player_2 = self.client.has_player_2();
if !self.show_player_name && has_player_2 { if !self.show_player_name && has_player_2 {
ui.send_viewport_cmd(ViewportCommand::Title(format!("Lemur ({})", self.sim_id))); ui.send_viewport_cmd(ViewportCommand::Title(format!("Lemur ({})", self.sim_id)));
@@ -731,7 +739,7 @@ impl AppWindow for GameWindow {
bounds.max - bounds.min bounds.max - bounds.min
}; };
let position = ui.input(|i| i.viewport().outer_rect.map(|r| r.min)); let position = ui.input(|i| i.viewport().outer_rect.map(|r| r.min));
self.update_config(|c| { self.config.update(|c| {
c.dimensions = dimensions; c.dimensions = dimensions;
c.position = position; c.position = position;
}); });
@@ -874,6 +882,7 @@ impl DerefMut for AppWrapper {
pub enum ChildWindow { pub enum ChildWindow {
About, About,
CharacterData, CharacterData,
Memory,
BgMap, BgMap,
Objects, Objects,
Worlds, Worlds,
@@ -894,6 +903,7 @@ impl ChildWindow {
match self { match self {
Self::About => "About", Self::About => "About",
Self::CharacterData => "CharacterData", Self::CharacterData => "CharacterData",
Self::Memory => "Memory",
Self::BgMap => "BgMap", Self::BgMap => "BgMap",
Self::Objects => "Objects", Self::Objects => "Objects",
Self::Worlds => "Worlds", Self::Worlds => "Worlds",
+447
View File
@@ -0,0 +1,447 @@
use std::{fmt::LowerHex, sync::Arc};
use egui::{
Align, CentralPanel, FontSelection, Label, Layout, Response, RichText, Separator, TextEdit, Ui,
ViewportBuilder, Widget, text::LayoutJob,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use crate::{
emulator::SimId,
memory::{MemoryClient, MemoryRef, MemoryValue, MemoryView},
window::{AppWindow, utils::UiData},
};
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
enum ValueSize {
Byte,
Halfword,
Word,
}
impl ValueSize {
fn size(self) -> usize {
match self {
Self::Byte => 1,
Self::Halfword => 2,
Self::Word => 4,
}
}
}
struct HexEditResponse {
submitted: bool,
response: Response,
}
struct HexEdit<'a> {
text: &'a mut String,
button_label: &'static str,
}
impl<'a> HexEdit<'a> {
#[must_use]
fn new(text: &'a mut String, button_label: &'static str) -> Self {
Self { text, button_label }
}
fn show(self, ui: &mut Ui) -> HexEditResponse {
const HEX_WORD_WIDTH: f32 = 80.0;
let text_response = TextEdit::singleline(self.text)
.desired_width(HEX_WORD_WIDTH)
.ui(ui);
let button_response = ui.button(self.button_label);
let submitted = (text_response.lost_focus()
&& text_response.ctx.input(|i| i.key_pressed(egui::Key::Enter)))
|| button_response.clicked();
let response = text_response.union(button_response);
HexEditResponse {
submitted,
response,
}
}
}
impl Widget for HexEdit<'_> {
fn ui(self, ui: &mut Ui) -> egui::Response {
self.show(ui).response
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
struct State {
address: u32,
value_size: ValueSize,
}
impl Default for State {
fn default() -> Self {
Self {
address: 0x05000000,
value_size: ValueSize::Byte,
}
}
}
pub struct MemoryWindow {
sim_id: SimId,
memory: Arc<MemoryClient>,
view: MemoryView,
scroll_to: Option<u32>,
jump_to: String,
editing_buffer: Vec<u8>,
state: UiData<State>,
}
impl MemoryWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>) -> Self {
let state: UiData<State> = UiData::new();
Self {
sim_id,
memory: memory.clone(),
view: memory.watch(sim_id, 0x0, 0),
scroll_to: None,
jump_to: String::new(),
editing_buffer: vec![],
state,
}
}
fn show_top_bar(&mut self, ui: &mut Ui) {
if ui
.radio_value(&mut self.state.value_size, ValueSize::Byte, "8-bit")
.clicked()
{
self.editing_buffer.clear();
}
if ui
.radio_value(&mut self.state.value_size, ValueSize::Halfword, "16-bit")
.clicked()
{
self.editing_buffer.clear();
}
if ui
.radio_value(&mut self.state.value_size, ValueSize::Word, "32-bit")
.clicked()
{
self.editing_buffer.clear();
}
self.state.address &= match self.state.value_size {
ValueSize::Byte => 0x07ffffff,
ValueSize::Halfword => 0x07fffffe,
ValueSize::Word => 0x07fffffc,
};
if HexEdit::new(&mut self.jump_to, "Jump to address")
.show(ui)
.submitted
{
let address_str = std::mem::take(&mut self.jump_to);
if let Ok(address) = u64::from_str_radix(&address_str, 16) {
let address = (address & 0x07ffffff) as u32;
self.state.address = address;
self.editing_buffer.clear();
self.scroll_to = Some(address);
}
}
}
fn show_memory(&mut self, ui: &mut Ui) {
let row_height_sans_spacing = ui.text_style_height(&egui::TextStyle::Monospace);
let max_height = ui.max_rect().height();
let something_has_focus = ui.memory(|m| m.focused().is_some());
let mut selected_changed = false;
ui.input(|i| {
if !i.focused || something_has_focus {
return;
}
let arrow_keys: [(egui::Key, i32); 4] = [
(egui::Key::ArrowUp, -0x10),
(egui::Key::ArrowDown, 0x10),
(egui::Key::ArrowLeft, -(self.state.value_size.size() as i32)),
(egui::Key::ArrowRight, self.state.value_size.size() as i32),
];
for (key, delta) in arrow_keys {
if !selected_changed && i.key_pressed(key) {
self.state.address = self.state.address.wrapping_add_signed(delta) & 0x07ffffff;
self.editing_buffer.clear();
selected_changed = true;
}
}
if !self.editing_buffer.is_empty() && i.key_pressed(egui::Key::Enter) {
let full_size = self.state.value_size.size() * 2;
while self.editing_buffer.len() < full_size {
self.editing_buffer.insert(0, 0);
}
}
});
let mut table = TableBuilder::new(ui)
.column(Column::auto())
.column(Column::initial(0.0))
.column(Column::auto())
.column(Column::initial(0.0))
.column(Column::remainder())
.max_scroll_height(max_height)
.animate_scrolling(false);
if let Some(scroll_to) = self.scroll_to.take() {
table = table.scroll_to_row(
(scroll_to as usize / 0x10).saturating_sub(2),
Some(Align::Min),
);
}
table.body(|body| {
let total_rows = 0x00800000; // whole 27-bit non-mirrored address space, with 16 bytes per row
let range = self.view.range();
let current_start = range.start as usize;
let current_length = range.length;
let rows = self.view.borrow();
let mut new_start = None;
let mut new_length = 0;
body.rows(row_height_sans_spacing, total_rows, |mut row| {
let address = row.index() * 0x10;
let offset = address
.checked_sub(current_start)
.filter(|offset| offset + 0x10 <= current_length);
if new_start.is_none() {
new_start = Some(address);
}
new_length += 0x10;
row.col(|ui| {
let label = Label::new(RichText::new(format!("{address:08x}")).monospace());
if ui.add(label).clicked() {
self.state.address = address as u32;
self.editing_buffer.clear();
selected_changed = true;
};
});
row.col(|ui| {
ui.separator();
});
row.col(|ui| {
if let Some(offset) = offset {
fn show_memory<T: MemoryValue + LowerHex>(
ui: &mut Ui,
rows: &MemoryRef,
(start, offset): (usize, usize),
selected: u32,
memory: &MemoryClient,
sim: SimId,
buffer: &mut Vec<u8>,
) -> Option<u32> {
let mut new_selected = None;
for (i, value) in rows
.range::<T>(offset / size_of::<T>(), 0x10 / size_of::<T>())
.enumerate()
{
let address = (start + offset + i * size_of::<T>()) as u32;
let mut job = LayoutJob::default();
let rich_text = |value: &str| {
let text = RichText::new(value).monospace();
if address == selected {
text.background_color(ui.visuals().selection.bg_fill)
.color(ui.visuals().selection.stroke.color)
} else {
text
}
};
let edit_len = if address == selected { buffer.len() } else { 0 };
if edit_len > 0 {
let buffer_str =
buffer.iter().map(|d| format!("{d:x}")).join("");
let text = rich_text(&buffer_str).underline();
text.append_to(
&mut job,
ui.style(),
FontSelection::default(),
Align::Center,
);
}
let val_str = format!("{value:08x}")
.split_off(8 - size_of::<T>() * 2 + edit_len);
let text = rich_text(&val_str);
text.append_to(
&mut job,
ui.style(),
FontSelection::default(),
Align::Center,
);
let response = ui.label(job);
if response.clicked() {
new_selected = Some(address);
}
if ui.memory(|m| m.focused().is_none())
&& ui.input(|i| i.focused)
&& address == selected
{
const KEYS: [egui::Key; 16] = [
egui::Key::Num0,
egui::Key::Num1,
egui::Key::Num2,
egui::Key::Num3,
egui::Key::Num4,
egui::Key::Num5,
egui::Key::Num6,
egui::Key::Num7,
egui::Key::Num8,
egui::Key::Num9,
egui::Key::A,
egui::Key::B,
egui::Key::C,
egui::Key::D,
egui::Key::E,
egui::Key::F,
];
let pressed = ui.input(|i| {
for (index, key) in KEYS.into_iter().enumerate() {
if i.key_pressed(key) {
return Some(index as u8);
}
}
None
});
if let Some(value) = pressed {
buffer.push(value);
}
if buffer.len() == size_of::<T>() * 2 {
let bytes = buffer
.chunks(2)
.map(|letters| (letters[0] << 4) | letters[1])
.rev()
.collect::<Vec<u8>>();
let data = T::from_bytes(&bytes);
memory.write(sim, address, &data);
new_selected = Some(
address.wrapping_add(size_of::<T>() as u32)
& 0x07ffffff,
);
}
}
}
new_selected
}
let selected = match self.state.value_size {
ValueSize::Byte => show_memory::<u8>(
ui,
&rows,
(current_start, offset),
self.state.address,
&self.memory,
self.sim_id,
&mut self.editing_buffer,
),
ValueSize::Halfword => show_memory::<u16>(
ui,
&rows,
(current_start, offset),
self.state.address,
&self.memory,
self.sim_id,
&mut self.editing_buffer,
),
ValueSize::Word => show_memory::<u32>(
ui,
&rows,
(current_start, offset),
self.state.address,
&self.memory,
self.sim_id,
&mut self.editing_buffer,
),
};
if let Some(selected) = selected {
self.state.address = selected;
selected_changed = true;
}
};
if selected_changed {
self.editing_buffer.clear();
}
});
row.col(|ui| {
ui.separator();
});
row.col(|ui| {
if let Some(offset) = offset {
let printable: String = rows
.range::<u8>(offset, 0x10)
.map(|c| {
if (32..128).contains(&c) {
c as char
} else {
'.'
}
})
.collect();
ui.label(RichText::new(printable).monospace());
}
});
});
drop(rows);
if let Some(start) = new_start
&& (start != current_start || new_length != current_length)
{
self.memory
.retarget(&mut self.view, self.sim_id, start as u32, new_length);
}
});
if selected_changed && self.scroll_to.is_none() {
let range = self.view.range();
if self.state.address < range.start {
self.scroll_to = Some(self.state.address);
} else if let Some(delta) = self
.state
.address
.checked_sub(range.start + range.length as u32 - 0x20)
{
self.scroll_to = Some(range.start + delta + 0x40);
}
}
}
}
impl AppWindow for MemoryWindow {
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Memory")
.with_inner_size((640.0, 480.0))
}
fn show(&mut self, ui: &mut Ui) {
let row_height =
ui.text_style_height(&egui::TextStyle::Monospace) + ui.spacing().item_spacing.y;
let table_height = ui.available_height() - (row_height * 2.5);
if self.state.load(ui) {
self.scroll_to = Some(self.state.address);
}
CentralPanel::default().show(ui, |ui| {
StripBuilder::new(ui)
.size(Size::exact(row_height))
.size(Size::initial(0.0))
.size(Size::exact(table_height))
.cell_layout(Layout::left_to_right(Align::Center))
.vertical(|mut strip| {
strip.cell(|ui| self.show_top_bar(ui));
strip.cell(|ui| {
Separator::default().horizontal().ui(ui);
});
strip.cell(|ui| self.show_memory(ui));
});
});
self.state.save(ui);
}
}
+3 -1
View File
@@ -432,13 +432,15 @@ where
} }
} }
pub fn load(&mut self, ui: &Ui) { pub fn load(&mut self, ui: &Ui) -> bool {
let just_loaded = !self.loaded;
if !self.loaded { if !self.loaded {
self.current = ui self.current = ui
.data_mut(|d| d.get_persisted(ui.id())) .data_mut(|d| d.get_persisted(ui.id()))
.unwrap_or_default(); .unwrap_or_default();
self.loaded = true; self.loaded = true;
} }
just_loaded
} }
pub fn save(&mut self, ui: &Ui) { pub fn save(&mut self, ui: &Ui) {
+23 -9
View File
@@ -1,14 +1,16 @@
use std::sync::Arc; use std::sync::Arc;
use egui::{ use egui::{
Align, CentralPanel, Checkbox, Color32, ComboBox, Image, ScrollArea, Slider, TextEdit, Align, CentralPanel, Checkbox, ComboBox, Image, ScrollArea, Slider, TextEdit, TextureOptions,
TextureOptions, Ui, ViewportBuilder, Ui, ViewportBuilder,
}; };
use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
config::{Config, SimConfig},
emulator::SimId, emulator::SimId,
filesystem::Persistence,
images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader}, images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView}, memory::{MemoryClient, MemoryView},
window::{ window::{
@@ -46,9 +48,14 @@ pub struct BgMapWindow {
} }
impl BgMapWindow { impl BgMapWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &ImageTextureLoader) -> Self { pub fn new(
sim_id: SimId,
memory: &Arc<MemoryClient>,
persistence: &Persistence,
images: &ImageTextureLoader,
) -> Self {
let state: UiData<State> = UiData::new(); let state: UiData<State> = UiData::new();
let renderer = BgMapRenderer::new(sim_id, memory); let renderer = BgMapRenderer::new(sim_id, persistence, memory);
let params = images.add( let params = images.add(
sim_id, sim_id,
renderer, renderer,
@@ -229,6 +236,7 @@ struct BgMapParams {
} }
struct BgMapRenderer { struct BgMapRenderer {
config: Config<SimConfig>,
chardata: MemoryView, chardata: MemoryView,
bgmaps: MemoryView, bgmaps: MemoryView,
brightness: MemoryView, brightness: MemoryView,
@@ -236,8 +244,9 @@ struct BgMapRenderer {
} }
impl BgMapRenderer { impl BgMapRenderer {
pub fn new(sim_id: SimId, memory: &MemoryClient) -> Self { pub fn new(sim_id: SimId, persistence: &Persistence, memory: &MemoryClient) -> Self {
Self { Self {
config: SimConfig::load(persistence, sim_id),
chardata: memory.watch(sim_id, 0x00078000, 0x8000), chardata: memory.watch(sim_id, 0x00078000, 0x8000),
bgmaps: memory.watch(sim_id, 0x00020000, 0x20000), bgmaps: memory.watch(sim_id, 0x00020000, 0x20000),
brightness: memory.watch(sim_id, 0x0005f824, 8), brightness: memory.watch(sim_id, 0x0005f824, 8),
@@ -246,6 +255,8 @@ impl BgMapRenderer {
} }
fn render_bgmap(&self, image: &mut ImageBuffer, bgmap_index: usize, generic_palette: bool) { fn render_bgmap(&self, image: &mut ImageBuffer, bgmap_index: usize, generic_palette: bool) {
let color = self.config.colors[0];
let chardata = self.chardata.borrow(); let chardata = self.chardata.borrow();
let bgmaps = self.bgmaps.borrow(); let bgmaps = self.bgmaps.borrow();
let brightness = self.brightness.borrow(); let brightness = self.brightness.borrow();
@@ -253,9 +264,9 @@ impl BgMapRenderer {
let brts = brightness.read::<[u8; 8]>(0); let brts = brightness.read::<[u8; 8]>(0);
let colors = if generic_palette { let colors = if generic_palette {
[utils::generic_palette(Color32::RED); 4] [utils::generic_palette(color); 4]
} else { } else {
[0, 2, 4, 6].map(|i| utils::palette_colors(palettes.read(i), &brts, Color32::RED)) [0, 2, 4, 6].map(|i| utils::palette_colors(palettes.read(i), &brts, color))
}; };
for (i, cell) in bgmaps.range::<u16>(bgmap_index * 4096, 4096).enumerate() { for (i, cell) in bgmaps.range::<u16>(bgmap_index * 4096, 4096).enumerate() {
@@ -279,6 +290,8 @@ impl BgMapRenderer {
} }
fn render_bgmap_cell(&self, image: &mut ImageBuffer, index: usize, generic_palette: bool) { fn render_bgmap_cell(&self, image: &mut ImageBuffer, index: usize, generic_palette: bool) {
let color = self.config.colors[0];
let chardata = self.chardata.borrow(); let chardata = self.chardata.borrow();
let bgmaps = self.bgmaps.borrow(); let bgmaps = self.bgmaps.borrow();
let brightness = self.brightness.borrow(); let brightness = self.brightness.borrow();
@@ -296,9 +309,9 @@ impl BgMapRenderer {
} = CellData::parse(cell); } = CellData::parse(cell);
let char = chardata.read::<[u16; 8]>(char_index); let char = chardata.read::<[u16; 8]>(char_index);
let palette = if generic_palette { let palette = if generic_palette {
utils::generic_palette(Color32::RED) utils::generic_palette(color)
} else { } else {
utils::palette_colors(palettes.read(palette_index * 2), &brts, Color32::RED) utils::palette_colors(palettes.read(palette_index * 2), &brts, color)
}; };
for row in 0..8 { for row in 0..8 {
@@ -321,6 +334,7 @@ impl ImageRenderer<2> for BgMapRenderer {
} }
fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 2]) { fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 2]) {
self.config.refresh();
self.render_bgmap_cell(&mut images[0], params.cell_index, params.generic_palette); self.render_bgmap_cell(&mut images[0], params.cell_index, params.generic_palette);
self.render_bgmap( self.render_bgmap(
&mut images[1], &mut images[1],
+33 -13
View File
@@ -8,7 +8,9 @@ use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
config::{Config, SimConfig},
emulator::SimId, emulator::SimId,
filesystem::Persistence,
images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader}, images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView}, memory::{MemoryClient, MemoryView},
window::{ window::{
@@ -99,6 +101,7 @@ impl Default for State {
pub struct CharacterDataWindow { pub struct CharacterDataWindow {
sim_id: SimId, sim_id: SimId,
config: Config<SimConfig>,
brightness: MemoryView, brightness: MemoryView,
palettes: MemoryView, palettes: MemoryView,
params: ImageParams<CharDataParams>, params: ImageParams<CharDataParams>,
@@ -106,7 +109,13 @@ pub struct CharacterDataWindow {
} }
impl CharacterDataWindow { impl CharacterDataWindow {
pub fn new(sim_id: SimId, memory: &MemoryClient, images: &ImageTextureLoader) -> Self { pub fn new(
sim_id: SimId,
memory: &MemoryClient,
persistence: &Persistence,
images: &ImageTextureLoader,
) -> Self {
let config = SimConfig::load(persistence, sim_id);
let state: UiData<State> = UiData::new(); let state: UiData<State> = UiData::new();
let renderer = CharDataRenderer::new(sim_id, memory); let renderer = CharDataRenderer::new(sim_id, memory);
let params = images.add( let params = images.add(
@@ -115,10 +124,12 @@ impl CharacterDataWindow {
CharDataParams { CharDataParams {
palette: state.palette, palette: state.palette,
index: state.index, index: state.index,
color: config.colors[0],
}, },
); );
Self { Self {
sim_id, sim_id,
config,
brightness: memory.watch(sim_id, 0x0005f824, 8), brightness: memory.watch(sim_id, 0x0005f824, 8),
palettes: memory.watch(sim_id, 0x0005f860, 16), palettes: memory.watch(sim_id, 0x0005f860, 16),
params, params,
@@ -226,17 +237,19 @@ impl CharacterDataWindow {
self.params.write(CharDataParams { self.params.write(CharDataParams {
palette: self.state.palette, palette: self.state.palette,
index: self.state.index, index: self.state.index,
color: self.config.colors[0],
}); });
} }
fn load_palette_colors(&self) -> [Color32; 4] { fn load_palette_colors(&self) -> [Color32; 4] {
let color = self.config.colors[0];
let Some(offset) = self.state.palette.offset() else { let Some(offset) = self.state.palette.offset() else {
return utils::generic_palette(Color32::RED); return utils::generic_palette(color);
}; };
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::palette_colors(palette, &brts, Color32::RED) utils::palette_colors(palette, &brts, color)
} }
fn show_chardata(&mut self, ui: &mut Ui) { fn show_chardata(&mut self, ui: &mut Ui) {
@@ -262,6 +275,7 @@ impl AppWindow for CharacterDataWindow {
} }
fn show(&mut self, ui: &mut Ui) { fn show(&mut self, ui: &mut Ui) {
self.config.refresh();
self.state.load(ui); self.state.load(ui);
CentralPanel::default().show(ui, |ui| { CentralPanel::default().show(ui, |ui| {
ui.horizontal_top(|ui| { ui.horizontal_top(|ui| {
@@ -286,8 +300,8 @@ impl AppWindow for CharacterDataWindow {
struct CharDataParams { struct CharDataParams {
palette: Palette, palette: Palette,
index: usize, index: usize,
color: Color32,
} }
struct CharDataRenderer { struct CharDataRenderer {
chardata: MemoryView, chardata: MemoryView,
brightness: MemoryView, brightness: MemoryView,
@@ -306,8 +320,8 @@ impl ImageRenderer<2> for CharDataRenderer {
} }
fn render(&mut self, params: &Self::Params, image: &mut [ImageBuffer; 2]) { fn render(&mut self, params: &Self::Params, image: &mut [ImageBuffer; 2]) {
self.render_character(&mut image[0], params.palette, params.index); self.render_character(&mut image[0], params.palette, params.index, params.color);
self.render_character_data(&mut image[1], params.palette); self.render_character_data(&mut image[1], params.palette, params.color);
} }
} }
@@ -320,11 +334,17 @@ impl CharDataRenderer {
} }
} }
fn render_character(&self, image: &mut ImageBuffer, palette: Palette, index: usize) { fn render_character(
&self,
image: &mut ImageBuffer,
palette: Palette,
index: usize,
color: Color32,
) {
if index >= 2048 { if index >= 2048 {
return; return;
} }
let palette = self.load_palette(palette); let palette = self.load_palette(palette, color);
let chardata = self.chardata.borrow(); let chardata = self.chardata.borrow();
let character = chardata.range::<u16>(index * 8, 8); let character = chardata.range::<u16>(index * 8, 8);
for (row, pixels) in character.enumerate() { for (row, pixels) in character.enumerate() {
@@ -335,8 +355,8 @@ impl CharDataRenderer {
} }
} }
fn render_character_data(&self, image: &mut ImageBuffer, palette: Palette) { fn render_character_data(&self, image: &mut ImageBuffer, palette: Palette, color: Color32) {
let palette = self.load_palette(palette); let palette = self.load_palette(palette, color);
let chardata = self.chardata.borrow(); let chardata = self.chardata.borrow();
for (row, pixels) in chardata.range::<u16>(0, 8 * 2048).enumerate() { for (row, pixels) in chardata.range::<u16>(0, 8 * 2048).enumerate() {
let char_index = row / 8; let char_index = row / 8;
@@ -350,13 +370,13 @@ impl CharDataRenderer {
} }
} }
fn load_palette(&self, palette: Palette) -> [Color32; 4] { fn load_palette(&self, palette: Palette, color: Color32) -> [Color32; 4] {
let Some(offset) = palette.offset() else { let Some(offset) = palette.offset() else {
return utils::GENERIC_PALETTE.map(|p| utils::shade(p, Color32::RED)); return utils::GENERIC_PALETTE.map(|p| utils::shade(p, color));
}; };
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::palette_colors(palette, &brts, Color32::RED) utils::palette_colors(palette, &brts, color)
} }
} }
+18 -17
View File
@@ -6,7 +6,9 @@ use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
config::{Config, SimConfig},
emulator::SimId, emulator::SimId,
filesystem::Persistence,
images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader}, images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView}, memory::{MemoryClient, MemoryView},
window::{ window::{
@@ -44,9 +46,14 @@ pub struct FrameBufferWindow {
} }
impl FrameBufferWindow { impl FrameBufferWindow {
pub fn new(sim_id: SimId, memory: &MemoryClient, images: &ImageTextureLoader) -> Self { pub fn new(
sim_id: SimId,
memory: &MemoryClient,
persistence: &Persistence,
images: &ImageTextureLoader,
) -> Self {
let state: UiData<State> = UiData::new(); let state: UiData<State> = UiData::new();
let renderer = FrameBufferRenderer::new(sim_id, memory); let renderer = FrameBufferRenderer::new(sim_id, memory, persistence);
let params = images.add( let params = images.add(
sim_id, sim_id,
renderer, renderer,
@@ -55,8 +62,6 @@ impl FrameBufferWindow {
left: state.left, left: state.left,
right: state.right, right: state.right,
generic_palette: state.generic_palette, generic_palette: state.generic_palette,
left_color: Color32::from_rgb(0xff, 0x00, 0x00),
right_color: Color32::from_rgb(0x00, 0xc6, 0xf0),
}, },
); );
Self { Self {
@@ -138,7 +143,6 @@ impl FrameBufferWindow {
left: self.state.left, left: self.state.left,
right: self.state.right, right: self.state.right,
generic_palette: self.state.generic_palette, generic_palette: self.state.generic_palette,
..*self.params
}); });
} }
@@ -188,18 +192,18 @@ struct FrameBufferParams {
left: bool, left: bool,
right: bool, right: bool,
generic_palette: bool, generic_palette: bool,
left_color: Color32,
right_color: Color32,
} }
struct FrameBufferRenderer { struct FrameBufferRenderer {
config: Config<SimConfig>,
buffers: [MemoryView; 4], buffers: [MemoryView; 4],
brightness: MemoryView, brightness: MemoryView,
} }
impl FrameBufferRenderer { impl FrameBufferRenderer {
fn new(sim_id: SimId, memory: &MemoryClient) -> Self { fn new(sim_id: SimId, memory: &MemoryClient, persistence: &Persistence) -> Self {
Self { Self {
config: SimConfig::load(persistence, sim_id),
buffers: [ buffers: [
memory.watch(sim_id, 0x00000000, 0x6000), memory.watch(sim_id, 0x00000000, 0x6000),
memory.watch(sim_id, 0x00008000, 0x6000), memory.watch(sim_id, 0x00008000, 0x6000),
@@ -223,6 +227,7 @@ impl ImageRenderer<1> for FrameBufferRenderer {
} }
fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 1]) { fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 1]) {
self.config.refresh();
let image = &mut images[0]; let image = &mut images[0];
let left_buffer = self.buffers[params.index].borrow(); let left_buffer = self.buffers[params.index].borrow();
@@ -230,15 +235,15 @@ impl ImageRenderer<1> for FrameBufferRenderer {
let colors = if params.generic_palette { let colors = if params.generic_palette {
[ [
utils::generic_palette(params.left_color), utils::generic_palette(self.config.colors[0]),
utils::generic_palette(params.right_color), utils::generic_palette(self.config.colors[1]),
] ]
} else { } else {
let brts = self.brightness.borrow().read::<[u8; 8]>(0); let brts = self.brightness.borrow().read::<[u8; 8]>(0);
let shades = utils::parse_shades(&brts); let shades = utils::parse_shades(&brts);
[ [
shades.map(|s| utils::shade(s, params.left_color)), shades.map(|s| utils::shade(s, self.config.colors[0])),
shades.map(|s| utils::shade(s, params.right_color)), shades.map(|s| utils::shade(s, self.config.colors[1])),
] ]
}; };
@@ -261,11 +266,7 @@ impl ImageRenderer<1> for FrameBufferRenderer {
} else { } else {
Color32::BLACK Color32::BLACK
}; };
Color32::from_rgb( left + right
left.r() + right.r(),
left.g() + right.g(),
left.b() + right.b(),
)
}); });
let x = index / 64; let x = index / 64;
for (i, pixel) in pixels.into_iter().enumerate() { for (i, pixel) in pixels.into_iter().enumerate() {
+17 -12
View File
@@ -1,14 +1,16 @@
use std::sync::Arc; use std::sync::Arc;
use egui::{ use egui::{
Align, CentralPanel, Checkbox, Color32, ComboBox, Image, ScrollArea, Slider, TextEdit, Align, CentralPanel, Checkbox, ComboBox, Image, ScrollArea, Slider, TextEdit, TextureOptions,
TextureOptions, Ui, ViewportBuilder, Ui, ViewportBuilder,
}; };
use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
config::{Config, SimConfig},
emulator::SimId, emulator::SimId,
filesystem::Persistence,
images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader}, images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView}, memory::{MemoryClient, MemoryView},
window::{ window::{
@@ -44,17 +46,20 @@ pub struct ObjectWindow {
} }
impl ObjectWindow { impl ObjectWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &ImageTextureLoader) -> Self { pub fn new(
sim_id: SimId,
memory: &Arc<MemoryClient>,
persistence: &Persistence,
images: &ImageTextureLoader,
) -> Self {
let state: UiData<State> = UiData::new(); let state: UiData<State> = UiData::new();
let renderer = ObjectRenderer::new(sim_id, memory); let renderer = ObjectRenderer::new(sim_id, memory, persistence);
let params = images.add( let params = images.add(
sim_id, sim_id,
renderer, renderer,
ObjectParams { ObjectParams {
index: state.index, index: state.index,
generic_palette: state.generic_palette, generic_palette: state.generic_palette,
left_color: Color32::from_rgb(0xff, 0x00, 0x00),
right_color: Color32::from_rgb(0x00, 0xc6, 0xf0),
}, },
); );
Self { Self {
@@ -194,7 +199,6 @@ impl ObjectWindow {
self.params.write(ObjectParams { self.params.write(ObjectParams {
index: self.state.index, index: self.state.index,
generic_palette: self.state.generic_palette, generic_palette: self.state.generic_palette,
..*self.params
}); });
} }
@@ -242,8 +246,6 @@ impl AppWindow for ObjectWindow {
struct ObjectParams { struct ObjectParams {
index: usize, index: usize,
generic_palette: bool, generic_palette: bool,
left_color: Color32,
right_color: Color32,
} }
enum Eye { enum Eye {
@@ -252,6 +254,7 @@ enum Eye {
} }
struct ObjectRenderer { struct ObjectRenderer {
config: Config<SimConfig>,
chardata: MemoryView, chardata: MemoryView,
objects: MemoryView, objects: MemoryView,
brightness: MemoryView, brightness: MemoryView,
@@ -259,8 +262,9 @@ struct ObjectRenderer {
} }
impl ObjectRenderer { impl ObjectRenderer {
pub fn new(sim_id: SimId, memory: &MemoryClient) -> Self { pub fn new(sim_id: SimId, memory: &MemoryClient, persistence: &Persistence) -> Self {
Self { Self {
config: SimConfig::load(persistence, sim_id),
chardata: memory.watch(sim_id, 0x00078000, 0x8000), chardata: memory.watch(sim_id, 0x00078000, 0x8000),
objects: memory.watch(sim_id, 0x0003e000, 0x2000), objects: memory.watch(sim_id, 0x0003e000, 0x2000),
brightness: memory.watch(sim_id, 0x0005f824, 8), brightness: memory.watch(sim_id, 0x0005f824, 8),
@@ -302,8 +306,8 @@ impl ObjectRenderer {
}; };
let color = match eye { let color = match eye {
Eye::Left => params.left_color, Eye::Left => self.config.colors[0],
Eye::Right => params.right_color, Eye::Right => self.config.colors[1],
}; };
let char = chardata.read::<[u16; 8]>(obj.data.char_index); let char = chardata.read::<[u16; 8]>(obj.data.char_index);
@@ -343,6 +347,7 @@ impl ImageRenderer<2> for ObjectRenderer {
} }
fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 2]) { fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 2]) {
self.config.refresh();
images[0].clear(); images[0].clear();
self.render_object(&mut images[0], params, false, Eye::Left); self.render_object(&mut images[0], params, false, Eye::Left);
self.render_object(&mut images[0], params, false, Eye::Right); self.render_object(&mut images[0], params, false, Eye::Right);
+9 -4
View File
@@ -1,13 +1,15 @@
use std::sync::Arc; use std::sync::Arc;
use egui::{ use egui::{
Align, Button, CentralPanel, Checkbox, Color32, Direction, Label, Layout, ScrollArea, TextEdit, Align, Button, CentralPanel, Checkbox, Direction, Label, Layout, ScrollArea, TextEdit, Ui,
Ui, ViewportBuilder, ViewportBuilder,
}; };
use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use crate::{ use crate::{
config::{Config, SimConfig},
emulator::SimId, emulator::SimId,
filesystem::Persistence,
memory::{MemoryClient, MemoryRef, MemoryValue, MemoryView}, memory::{MemoryClient, MemoryRef, MemoryValue, MemoryView},
window::{ window::{
AppWindow, AppWindow,
@@ -19,14 +21,16 @@ use super::utils;
pub struct RegisterWindow { pub struct RegisterWindow {
sim_id: SimId, sim_id: SimId,
config: Config<SimConfig>,
memory: Arc<MemoryClient>, memory: Arc<MemoryClient>,
registers: MemoryView, registers: MemoryView,
} }
impl RegisterWindow { impl RegisterWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>) -> Self { pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, persistence: &Persistence) -> Self {
Self { Self {
sim_id, sim_id,
config: SimConfig::load(persistence, sim_id),
memory: memory.clone(), memory: memory.clone(),
registers: memory.watch(sim_id, 0x0005f800, 0x72), registers: memory.watch(sim_id, 0x0005f800, 0x72),
} }
@@ -366,7 +370,7 @@ impl RegisterWindow {
}); });
body.row(row_height, |mut row| { body.row(row_height, |mut row| {
row.col(|_ui| {}); row.col(|_ui| {});
for shade in utils::parse_brts(&brts, Color32::RED) { for shade in utils::parse_brts(&brts, self.config.colors[0]) {
row.col(|ui| { row.col(|ui| {
ui.painter().rect_filled( ui.painter().rect_filled(
ui.available_rect_before_wrap(), ui.available_rect_before_wrap(),
@@ -641,6 +645,7 @@ impl AppWindow for RegisterWindow {
} }
fn show(&mut self, ui: &mut Ui) { fn show(&mut self, ui: &mut Ui) {
self.config.refresh();
CentralPanel::default().show(ui, |ui| { CentralPanel::default().show(ui, |ui| {
ScrollArea::vertical().show(ui, |ui| { ScrollArea::vertical().show(ui, |ui| {
ui.horizontal_top(|ui| { ui.horizontal_top(|ui| {
+21 -14
View File
@@ -14,7 +14,9 @@ use num_traits::{FromPrimitive, ToPrimitive};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
config::{Config, SimConfig},
emulator::SimId, emulator::SimId,
filesystem::Persistence,
images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader}, images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryRef, MemoryView}, memory::{MemoryClient, MemoryRef, MemoryView},
window::{ window::{
@@ -47,6 +49,7 @@ impl Default for State {
pub struct WorldWindow { pub struct WorldWindow {
sim_id: SimId, sim_id: SimId,
config: Config<SimConfig>,
memory: Arc<MemoryClient>, memory: Arc<MemoryClient>,
worlds: MemoryView, worlds: MemoryView,
bgmaps: MemoryView, bgmaps: MemoryView,
@@ -55,7 +58,13 @@ pub struct WorldWindow {
} }
impl WorldWindow { impl WorldWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &ImageTextureLoader) -> Self { pub fn new(
sim_id: SimId,
memory: &Arc<MemoryClient>,
persistence: &Persistence,
images: &ImageTextureLoader,
) -> Self {
let config = SimConfig::load(persistence, sim_id);
let state: UiData<State> = UiData::new(); let state: UiData<State> = UiData::new();
let renderer = WorldRenderer::new(sim_id, memory); let renderer = WorldRenderer::new(sim_id, memory);
let params = images.add( let params = images.add(
@@ -64,12 +73,13 @@ impl WorldWindow {
WorldParams { WorldParams {
index: state.index, index: state.index,
generic_palette: state.generic_palette, generic_palette: state.generic_palette,
left_color: Color32::from_rgb(0xff, 0x00, 0x00), left_color: config.colors[0],
right_color: Color32::from_rgb(0x00, 0xc6, 0xf0), right_color: config.colors[1],
}, },
); );
Self { Self {
sim_id, sim_id,
config,
memory: memory.clone(), memory: memory.clone(),
worlds: memory.watch(sim_id, 0x0003d800, 0x400), worlds: memory.watch(sim_id, 0x0003d800, 0x400),
bgmaps: memory.watch(sim_id, 0x00020000, 0x20000), bgmaps: memory.watch(sim_id, 0x00020000, 0x20000),
@@ -467,12 +477,7 @@ impl WorldWindow {
let left_color = self.params.left_color; let left_color = self.params.left_color;
let right_color = self.params.right_color; let right_color = self.params.right_color;
let both_color = Color32::from_rgb( let both_color = left_color + right_color;
left_color.r() + right_color.r(),
left_color.g() + right_color.g(),
left_color.b() + right_color.b(),
);
let painter = ui.painter(); let painter = ui.painter();
let draw_rect = |x1: f32, x2: f32, color: Color32| { let draw_rect = |x1: f32, x2: f32, color: Color32| {
painter.line( painter.line(
@@ -538,6 +543,12 @@ impl AppWindow for WorldWindow {
} }
fn show(&mut self, ui: &mut Ui) { fn show(&mut self, ui: &mut Ui) {
self.config.refresh();
self.params.write(WorldParams {
left_color: self.config.colors[0],
right_color: self.config.colors[1],
..*self.params
});
self.state.load(ui); self.state.load(ui);
CentralPanel::default().show(ui, |ui| { CentralPanel::default().show(ui, |ui| {
ui.horizontal_top(|ui| { ui.horizontal_top(|ui| {
@@ -686,11 +697,7 @@ impl WorldRenderer {
for (dst, shades) in image.pixels.iter_mut().zip(self.buffer.iter()) { for (dst, shades) in image.pixels.iter_mut().zip(self.buffer.iter()) {
let left = colors[0][shades[0] as usize]; let left = colors[0][shades[0] as usize];
let right = colors[1][shades[1] as usize]; let right = colors[1][shades[1] as usize];
*dst = Color32::from_rgb( *dst = left + right;
left.r() + right.r(),
left.g() + right.g(),
left.b() + right.b(),
)
} }
} }