Compare commits

..
16 Commits
34 changed files with 1886 additions and 937 deletions
Generated
+674 -618
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -4,24 +4,24 @@ 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.0" version = "0.15.2"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
atoi = "3" atoi = "3"
audioadapter-buffers = "4" audioadapter-buffers = "5"
atomic = "0.6" atomic = "0.6"
bitflags = { version = "2", features = ["serde"] } bitflags = { version = "2", features = ["serde"] }
bytemuck = { version = "1", features = ["derive"] } bytemuck = { version = "1", features = ["derive"] }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
cpal = "0.18" cpal = "0.18"
directories = "6" directories = "6"
egui = { version = "0.34", features = ["persistence", "serde"] } egui = { version = "0.36", features = ["persistence", "serde"] }
egui_extras = { version = "0.34", features = ["image"] } egui_extras = { version = "0.36", features = ["image"] }
egui-notify = "0.22" egui-notify = "0.23"
egui-winit = "0.34" egui-winit = "0.36"
egui-wgpu = { version = "0.34", features = ["winit"] } egui-wgpu = { version = "0.36", features = ["winit"] }
fxprof-processed-profile = "0.8" fxprof-processed-profile = "0.8"
fixed = { version = "1.28", features = ["num-traits"] } fixed = { version = "1.28", features = ["num-traits"] }
gilrs = { version = "0.11", features = ["serde-serialize"] } gilrs = { version = "0.11", features = ["serde-serialize"] }
@@ -31,22 +31,22 @@ image = { version = "0.25", default-features = false, features = ["png"] }
itertools = "0.15" itertools = "0.15"
normpath = "1" normpath = "1"
notify = "8" notify = "8"
num-derive = "0.4" num-derive = "0.5"
num-traits = "0.2" num-traits = "0.2"
object = "0.39" object = "0.40"
oneshot = { version = "0.2", features = ["async", "std"] } oneshot = { version = "0.2", features = ["async", "std"] }
pollster = "1" pollster = "1"
rand = "0.10" rand = "0.10"
rfd = "0.17" rfd = "0.17"
rtrb = "0.3" rtrb = "0.4"
rubato = "4" rubato = "5"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
thread-priority = "3" thread-priority = "3"
tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync", "time"] } tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync", "time"] }
tracing = { version = "0.1", features = ["release_max_level_debug"] } tracing = { version = "0.1", features = ["release_max_level_debug"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
wgpu = { version = "29", features = ["serde"] } wgpu = { version = "30", features = ["serde"] }
wholesym = "0.8" wholesym = "0.8"
winit = { version = "0.30", features = ["serde"] } winit = { version = "0.30", features = ["serde"] }
+13 -13
View File
@@ -10,30 +10,30 @@ RUN apt-get update && \
apt-get install -y ca-certificates apt-get install -y ca-certificates
COPY llvm.sources /etc/apt/sources.list.d/llvm.sources COPY llvm.sources /etc/apt/sources.list.d/llvm.sources
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y bash bzip2 clang-22 git lld-22 llvm-22 make patch xz-utils && \ apt-get install -y bash bzip2 clang-23 git lld-23 llvm-23 make patch xz-utils && \
ln -s $(which clang-22) /usr/bin/clang && \ ln -s $(which clang-23) /usr/bin/clang && \
ln -s $(which clang++-22) /usr/bin/clang++ && \ ln -s $(which clang++-23) /usr/bin/clang++ && \
ln -s $(which ld64.lld-22) /usr/bin/ld64.lld && \ ln -s $(which ld64.lld-23) /usr/bin/ld64.lld && \
SDK_VERSION=14.5 UNATTENDED=1 ENABLE_COMPILER_RT_INSTALL=1 TARGET_DIR=/osxcross ./build.sh SDK_VERSION=14.5 UNATTENDED=1 ENABLE_COMPILER_RT_INSTALL=1 TARGET_DIR=/osxcross ./build.sh
FROM rust:1.97-trixie FROM rust:1.99-trixie
ADD --chmod=644 "https://apt.llvm.org/llvm-snapshot.gpg.key" /etc/apt/trusted.gpg.d/apt.llvm.org.asc ADD --chmod=644 "https://apt.llvm.org/llvm-snapshot.gpg.key" /etc/apt/trusted.gpg.d/apt.llvm.org.asc
COPY llvm.sources /etc/apt/sources.list.d/llvm.sources COPY llvm.sources /etc/apt/sources.list.d/llvm.sources
RUN rustup target add x86_64-pc-windows-msvc && \ RUN rustup target add x86_64-pc-windows-msvc && \
rustup target add x86_64-apple-darwin && \ rustup target add x86_64-apple-darwin && \
rustup target add aarch64-apple-darwin && \ rustup target add aarch64-apple-darwin && \
apt-get update && \ apt-get update && \
apt-get install -y clang-22 lld-22 libc6-dev libasound2-dev libudev-dev genisoimage mingw-w64 && \ apt-get install -y clang-23 lld-23 libc6-dev libasound2-dev libudev-dev genisoimage mingw-w64 && \
cargo install cargo-bundle xwin && \ cargo install cargo-bundle xwin && \
xwin --accept-license splat --output xwin && \ xwin --accept-license splat --output xwin && \
rm -rf .xwin-cache && \ rm -rf .xwin-cache && \
ln -s $(which clang-22) /usr/bin/clang && \ ln -s $(which clang-23) /usr/bin/clang && \
ln -s $(which clang++-22) /usr/bin/clang++ ln -s $(which clang++-23) /usr/bin/clang++
COPY --from=osxcross /osxcross /osxcross COPY --from=osxcross /osxcross /osxcross
ENV PATH="/osxcross/bin:$PATH" \ ENV PATH="/osxcross/bin:$PATH" \
LD_LIBRARY_PATH="/osxcross/lib" \ LD_LIBRARY_PATH="/osxcross/lib" \
CC="clang-22" CXX="clang++-22" AR="llvm-ar-22" \ CC="clang-23" CXX="clang++-23" AR="llvm-ar-23" \
CC_x86_64-apple-darwin="o64-clang" \ CC_x86_64-apple-darwin="o64-clang" \
CXX_x86_64-apple-darwin="o64-clang++" \ CXX_x86_64-apple-darwin="o64-clang++" \
CC_aarch64-apple-darwin="oa64-clang" \ CC_aarch64-apple-darwin="oa64-clang" \
@@ -43,11 +43,11 @@ ENV PATH="/osxcross/bin:$PATH" \
SHROOMS_CFLAGS_x86_64-pc-windows-msvc="-flto" \ SHROOMS_CFLAGS_x86_64-pc-windows-msvc="-flto" \
CFLAGS_x86_64-pc-windows-msvc="-I/xwin/crt/include -I/xwin/sdk/include/ucrt -I/xwin/sdk/include/um -I/xwin/sdk/include/shared" \ CFLAGS_x86_64-pc-windows-msvc="-I/xwin/crt/include -I/xwin/sdk/include/ucrt -I/xwin/sdk/include/um -I/xwin/sdk/include/shared" \
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS="-Lnative=/xwin/crt/lib/x86_64 -Lnative=/xwin/sdk/lib/um/x86_64 -Lnative=/xwin/sdk/lib/ucrt/x86_64 -Clinker-plugin-lto -Clink-arg=-fuse-ld=lld" \ CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS="-Lnative=/xwin/crt/lib/x86_64 -Lnative=/xwin/sdk/lib/um/x86_64 -Lnative=/xwin/sdk/lib/ucrt/x86_64 -Clinker-plugin-lto -Clink-arg=-fuse-ld=lld" \
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link-22" \ CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link-23" \
CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER="o64-clang" \ CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER="o64-clang" \
CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-22" \ CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-23" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="oa64-clang" \ CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="oa64-clang" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-22" \ CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-23" \
RC_PATH="llvm-rc-22" \ RC_PATH="llvm-rc-23" \
MACOSX_DEPLOYMENT_TARGET="14.5" MACOSX_DEPLOYMENT_TARGET="14.5"
ENTRYPOINT ["bash"] ENTRYPOINT ["bash"]
+1 -1
View File
@@ -31,7 +31,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.define("VB_DIRECT_FRAME", "on_frame") .define("VB_DIRECT_FRAME", "on_frame")
.define("VB_DIRECT_READ", "on_read") .define("VB_DIRECT_READ", "on_read")
.define("VB_DIRECT_WRITE", "on_write") .define("VB_DIRECT_WRITE", "on_write")
.define("VBU_REALLOC", "vbu_realloc_shim") .define("VBU_MEMMGR", "VBU_MEMMGR")
.file(Path::new("shrooms-vb-core/core/vb.c")) .file(Path::new("shrooms-vb-core/core/vb.c"))
.file(Path::new("shrooms-vb-core/util/isx.c")) .file(Path::new("shrooms-vb-core/util/isx.c"))
.file(Path::new("shrooms-vb-core/util/vbu.c")) .file(Path::new("shrooms-vb-core/util/vbu.c"))
+1 -1
View File
@@ -1,4 +1,4 @@
Types: deb deb-src Types: deb deb-src
URIs: http://apt.llvm.org/trixie/ URIs: http://apt.llvm.org/trixie/
Suites: llvm-toolchain-trixie-22 Suites: llvm-toolchain-trixie-23
Components: main Components: main
+14 -8
View File
@@ -23,13 +23,13 @@ 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,
images::ImageTextureLoader, images::ImageTextureLoader,
input::{MappingProvider, ShortcutProvider}, input::{MappingProvider, ShortcutProvider},
memory::MemoryClient, memory::MemoryClient,
persistence::Persistence,
window::{AppWindow, ChildWindow, GameScreen, GameWindow, InitArgs}, window::{AppWindow, ChildWindow, GameScreen, GameWindow, InitArgs},
}; };
@@ -56,7 +56,7 @@ impl SharedViewportState {
fn init_painter(&mut self, ctx: &Context, window: &Window) -> &mut Painter { fn init_painter(&mut self, ctx: &Context, window: &Window) -> &mut Painter {
if self.painter.is_none() { if self.painter.is_none() {
let wgpu_config = egui_wgpu::WgpuConfiguration { let wgpu_config = egui_wgpu::WgpuConfiguration {
present_mode: wgpu::PresentMode::AutoVsync, surface: egui_wgpu::SurfaceConfig::HIGH_THROUGHPUT,
wgpu_setup: egui_wgpu::WgpuSetup::Existing(self.wgpu.init(window)), wgpu_setup: egui_wgpu::WgpuSetup::Existing(self.wgpu.init(window)),
..egui_wgpu::WgpuConfiguration::default() ..egui_wgpu::WgpuConfiguration::default()
}; };
@@ -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);
} }
@@ -257,7 +260,7 @@ impl Application {
continue; continue;
} }
input.viewports = self.shared.viewport_info.clone(); input.viewports = self.shared.viewport_info.clone();
let output = self.ctx.run_ui(input, |ui| { let mut output = self.ctx.run_ui(input, |ui| {
if viewport.id == ViewportId::ROOT { if viewport.id == ViewportId::ROOT {
self.app.show(ui); self.app.show(ui);
} else if let Some(cb) = ui.viewport(|v| v.viewport_ui_cb.clone()) { } else if let Some(cb) = ui.viewport(|v| v.viewport_ui_cb.clone()) {
@@ -304,8 +307,9 @@ impl Application {
output.pixels_per_point, output.pixels_per_point,
[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0],
&clipped_primitives, &clipped_primitives,
&output.textures_delta, &mut output.textures_delta,
vec![], vec![],
&viewport.window,
); );
} }
let mut active_viewports = ViewportIdSet::default(); let mut active_viewports = ViewportIdSet::default();
@@ -503,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, .. },
@@ -530,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:?}");
@@ -550,6 +555,7 @@ impl WgpuState {
power_preference, power_preference,
compatible_surface: Some(&surface), compatible_surface: Some(&surface),
force_fallback_adapter, force_fallback_adapter,
apply_limit_buckets: false,
})) }))
.expect("could not create adapter"); .expect("could not create adapter");
debug!("selected adapter: {:?}", adapter.get_info()); debug!("selected adapter: {:?}", adapter.get_info());
+43 -25
View File
@@ -1,9 +1,10 @@
use anyhow::Result; 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};
use crate::{emulator::SimId, persistence::Persistence, window::DisplayMode}; pub use crate::filesystem::Config;
use crate::{emulator::SimId, filesystem::Persistence, window::DisplayMode};
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Parser)] #[derive(Parser)]
@@ -35,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,
@@ -53,6 +57,9 @@ pub struct CliArgs {
/// Set a preference for whether to use a low-power or high-performance GPU adapter /// Set a preference for whether to use a low-power or high-performance GPU adapter
#[arg(long)] #[arg(long)]
pub wgpu_power_preference: Option<PowerPreferenceWrapper>, pub wgpu_power_preference: Option<PowerPreferenceWrapper>,
/// Choose whether save files are kept in the rom or the data directory
#[arg(long)]
pub save_data_location: Option<SaveDataLocation>,
} }
#[derive(ValueEnum, Clone, Copy)] #[derive(ValueEnum, Clone, Copy)]
@@ -90,40 +97,53 @@ const fn default_power_preference() -> wgpu::PowerPreference {
wgpu::PowerPreference::LowPower wgpu::PowerPreference::LowPower
} }
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, ValueEnum, Debug, NoUninit)]
#[repr(usize)]
pub enum SaveDataLocation {
/// The same directory as the ROM
RomDirectory,
/// This application's data directory
DataDirectory,
}
const fn default_save_data_location() -> SaveDataLocation {
SaveDataLocation::RomDirectory
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] #[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct AppConfig { pub struct AppConfig {
#[serde(default = "default_power_preference")] #[serde(default = "default_power_preference")]
pub power_preference: wgpu::PowerPreference, pub power_preference: wgpu::PowerPreference,
#[serde(default)] #[serde(default)]
pub force_fallback: bool, pub force_fallback: bool,
#[serde(default = "default_save_data_location")]
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(),
} }
} }
}
impl AppConfig {
pub fn load(persistence: &Persistence) -> Config<Self> {
persistence.config(APP_CONFIG_FILENAME)
}
pub fn save(&self, persistence: &Persistence) -> Result<()> { pub fn apply_updates(&mut self, args: &CliArgs) {
persistence.save_config(APP_CONFIG_FILENAME, &self)
}
pub fn update(&mut self, args: &CliArgs) -> bool {
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;
} }
updated if let Some(save_data_location) = args.save_data_location {
self.save_data_location = save_data_location;
}
} }
} }
@@ -147,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],
@@ -163,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))
} }
} }
+72 -44
View File
@@ -18,6 +18,7 @@ use tracing::{error, warn};
use crate::{ use crate::{
audio::Audio, audio::Audio,
config::SaveDataLocation,
graphics::TextureSink, graphics::TextureSink,
memory::{MemoryRange, MemoryRegion}, memory::{MemoryRange, MemoryRegion},
}; };
@@ -74,12 +75,13 @@ pub struct EmulatorBuilder {
state: Arc<Atomic<EmulatorState>>, state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>, audio_on: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>, linked: Arc<AtomicBool>,
save_data_location: Arc<Atomic<SaveDataLocation>>,
start_paused: bool, start_paused: bool,
watch_rom: Arc<[AtomicBool; 2]>, watch_rom: Arc<[AtomicBool; 2]>,
} }
impl EmulatorBuilder { impl EmulatorBuilder {
pub fn new() -> (Self, EmulatorClient) { pub fn new(save_data_location: SaveDataLocation) -> (Self, EmulatorClient) {
let (queue, commands) = mpsc::channel(); let (queue, commands) = mpsc::channel();
let builder = Self { let builder = Self {
rom: None, rom: None,
@@ -91,6 +93,7 @@ impl EmulatorBuilder {
state: Arc::new(Atomic::new(EmulatorState::Paused)), state: Arc::new(Atomic::new(EmulatorState::Paused)),
audio_on: Arc::new([AtomicBool::new(true), AtomicBool::new(true)]), audio_on: Arc::new([AtomicBool::new(true), AtomicBool::new(true)]),
linked: Arc::new(AtomicBool::new(false)), linked: Arc::new(AtomicBool::new(false)),
save_data_location: Arc::new(Atomic::new(save_data_location)),
start_paused: false, start_paused: false,
watch_rom: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]), watch_rom: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]),
}; };
@@ -98,8 +101,9 @@ impl EmulatorBuilder {
queue, queue,
sim_state: builder.sim_state.clone(), sim_state: builder.sim_state.clone(),
state: builder.state.clone(), state: builder.state.clone(),
watch_rom: builder.watch_rom.clone(),
linked: builder.linked.clone(), linked: builder.linked.clone(),
save_data_location: builder.save_data_location.clone(),
watch_rom: builder.watch_rom.clone(),
}; };
(builder, client) (builder, client)
} }
@@ -131,18 +135,42 @@ impl EmulatorBuilder {
} }
pub fn build(self) -> Result<Emulator> { pub fn build(self) -> Result<Emulator> {
let mut emulator = Emulator::new( let Self {
self.commands, rom,
self.sim_state, commands,
self.state, sim_state,
self.audio_on, state,
self.watch_rom, audio_on,
self.linked, watch_rom,
); linked,
if let Some(path) = self.rom { save_data_location,
start_paused,
} = self;
let mut emulator = Emulator {
sims: vec![],
carts: [None, None],
audio: Audio::init(),
commands,
sim_state,
state,
audio_on,
watch_rom,
linked,
save_data_location,
profilers: [None, None],
renderers: HashMap::new(),
messages: HashMap::new(),
debuggers: HashMap::new(),
stdouts: HashMap::new(),
watched_regions: HashMap::new(),
eye_contents: [vec![0u8; 384 * 224 * 2], vec![0u8; 384 * 224 * 2]],
audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE),
buffer: vec![],
};
if let Some(path) = rom {
emulator.load_cart(SimId::Player1, &path)?; emulator.load_cart(SimId::Player1, &path)?;
} }
if self.start_paused { if start_paused {
emulator.pause_sims()?; emulator.pause_sims()?;
} }
Ok(emulator) Ok(emulator)
@@ -159,6 +187,7 @@ pub struct Emulator {
audio_on: Arc<[AtomicBool; 2]>, audio_on: Arc<[AtomicBool; 2]>,
watch_rom: Arc<[AtomicBool; 2]>, watch_rom: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>, linked: Arc<AtomicBool>,
save_data_location: Arc<Atomic<SaveDataLocation>>,
profilers: [Option<ProfileSender>; 2], profilers: [Option<ProfileSender>; 2],
renderers: HashMap<SimId, TextureSink>, renderers: HashMap<SimId, TextureSink>,
messages: HashMap<SimId, mpsc::Sender<Toast>>, messages: HashMap<SimId, mpsc::Sender<Toast>>,
@@ -171,36 +200,6 @@ pub struct Emulator {
} }
impl Emulator { impl Emulator {
fn new(
commands: mpsc::Receiver<EmulatorCommand>,
sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>,
watch_rom: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
) -> Self {
Self {
sims: vec![],
carts: [None, None],
audio: Audio::init(),
commands,
sim_state,
state,
audio_on,
watch_rom,
linked,
profilers: [None, None],
renderers: HashMap::new(),
messages: HashMap::new(),
debuggers: HashMap::new(),
stdouts: HashMap::new(),
watched_regions: HashMap::new(),
eye_contents: [vec![0u8; 384 * 224 * 2], vec![0u8; 384 * 224 * 2]],
audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE),
buffer: vec![],
}
}
pub fn reload_cart(&mut self, sim_id: SimId) -> Result<()> { pub fn reload_cart(&mut self, sim_id: SimId) -> Result<()> {
let Some(cart) = &self.carts[sim_id.to_index()] else { let Some(cart) = &self.carts[sim_id.to_index()] else {
return Ok(()); return Ok(());
@@ -211,7 +210,12 @@ impl Emulator {
pub fn load_cart(&mut self, sim_id: SimId, path: &Path) -> Result<()> { pub fn load_cart(&mut self, sim_id: SimId, path: &Path) -> Result<()> {
let watch = self.watch_rom[sim_id.to_index()].load(Ordering::Acquire); let watch = self.watch_rom[sim_id.to_index()].load(Ordering::Acquire);
let cart = Cart::load(path, sim_id, watch)?; let cart = Cart::load(
path,
sim_id,
self.save_data_location.load(Ordering::Acquire),
watch,
)?;
self.try_reset_sim(sim_id, Some(cart))?; self.try_reset_sim(sim_id, Some(cart))?;
Ok(()) Ok(())
} }
@@ -224,7 +228,12 @@ impl Emulator {
}; };
let watch = self.watch_rom[SimId::Player2.to_index()].load(Ordering::Acquire); let watch = self.watch_rom[SimId::Player2.to_index()].load(Ordering::Acquire);
let cart = match file_path { let cart = match file_path {
Some(rom_path) => Some(Cart::load(&rom_path, SimId::Player2, watch)?), Some(rom_path) => Some(Cart::load(
&rom_path,
SimId::Player2,
self.save_data_location.load(Ordering::Acquire),
watch,
)?),
None => None, None => None,
}; };
self.try_reset_sim(SimId::Player2, cart)?; self.try_reset_sim(SimId::Player2, cart)?;
@@ -687,6 +696,20 @@ impl Emulator {
cart.stop_watching(); cart.stop_watching();
} }
} }
EmulatorCommand::SetSaveDataLocation(save_data_location) => {
self.save_data_location
.store(save_data_location, Ordering::Release);
for sim_id in SimId::values() {
if let Some(cart) = self.carts[sim_id.to_index()].as_mut()
&& let Err(error) = cart.change_save_data_location(save_data_location)
{
self.report_error(
sim_id,
format!("Error changing save data location: {error}"),
);
}
}
}
EmulatorCommand::StartSecondSim(path) => { EmulatorCommand::StartSecondSim(path) => {
if let Err(error) = self.start_second_sim(path) { if let Err(error) = self.start_second_sim(path) {
self.report_error( self.report_error(
@@ -854,6 +877,7 @@ pub enum EmulatorCommand {
LoadGame(SimId, PathBuf), LoadGame(SimId, PathBuf),
ReloadRom(SimId), ReloadRom(SimId),
WatchRom(SimId, bool), WatchRom(SimId, bool),
SetSaveDataLocation(SaveDataLocation),
StartSecondSim(Option<PathBuf>), StartSecondSim(Option<PathBuf>),
StopSecondSim, StopSecondSim,
Pause, Pause,
@@ -943,6 +967,7 @@ pub struct EmulatorClient {
sim_state: Arc<[Atomic<SimState>; 2]>, sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>, state: Arc<Atomic<EmulatorState>>,
linked: Arc<AtomicBool>, linked: Arc<AtomicBool>,
save_data_location: Arc<Atomic<SaveDataLocation>>,
watch_rom: Arc<[AtomicBool; 2]>, watch_rom: Arc<[AtomicBool; 2]>,
} }
@@ -959,6 +984,9 @@ impl EmulatorClient {
pub fn are_sims_linked(&self) -> bool { pub fn are_sims_linked(&self) -> bool {
self.linked.load(Ordering::Acquire) self.linked.load(Ordering::Acquire)
} }
pub fn save_data_location(&self) -> SaveDataLocation {
self.save_data_location.load(Ordering::Acquire)
}
pub fn is_rom_watched(&self, sim_id: SimId) -> bool { pub fn is_rom_watched(&self, sim_id: SimId) -> bool {
self.watch_rom[sim_id.to_index()].load(Ordering::Acquire) self.watch_rom[sim_id.to_index()].load(Ordering::Acquire)
} }
+39 -7
View File
@@ -8,10 +8,15 @@ use std::{
sync::{Arc, atomic::AtomicBool}, sync::{Arc, atomic::AtomicBool},
}; };
use crate::emulator::{SimId, game_info::GameInfo, shrooms_vb_util::rom_from_isx}; use crate::{
config::SaveDataLocation,
emulator::{SimId, game_info::GameInfo, shrooms_vb_util::rom_from_isx},
filesystem,
};
pub struct Cart { pub struct Cart {
pub file_path: PathBuf, pub file_path: PathBuf,
sim_id: SimId,
pub rom: Vec<u8>, pub rom: Vec<u8>,
sram_file: File, sram_file: File,
pub sram: Vec<u8>, pub sram: Vec<u8>,
@@ -21,7 +26,12 @@ pub struct Cart {
} }
impl Cart { impl Cart {
pub fn load(file_path: &Path, sim_id: SimId, watch: bool) -> Result<Self> { pub fn load(
file_path: &Path,
sim_id: SimId,
save_data_location: SaveDataLocation,
watch: bool,
) -> Result<Self> {
let (rom, info) = Self::read_rom(file_path)?; let (rom, info) = Self::read_rom(file_path)?;
let mut sram_file = File::options() let mut sram_file = File::options()
@@ -29,7 +39,7 @@ impl Cart {
.write(true) .write(true)
.create(true) .create(true)
.truncate(false) .truncate(false)
.open(sram_path(file_path, sim_id))?; .open(sram_path(file_path, sim_id, save_data_location))?;
let sram = if sram_file.metadata()?.len() == 0 { let sram = if sram_file.metadata()?.len() == 0 {
// new SRAM file, randomize the contents // new SRAM file, randomize the contents
@@ -54,6 +64,7 @@ impl Cart {
Ok(Cart { Ok(Cart {
file_path: file_path.to_path_buf(), file_path: file_path.to_path_buf(),
sim_id,
rom, rom,
sram_file, sram_file,
sram, sram,
@@ -73,6 +84,19 @@ impl Cart {
Ok(()) Ok(())
} }
pub fn change_save_data_location(
&mut self,
save_data_location: SaveDataLocation,
) -> Result<()> {
self.sram_file = File::options()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(sram_path(&self.file_path, self.sim_id, save_data_location))?;
self.save_sram()
}
pub fn save_sram(&mut self) -> Result<()> { pub fn save_sram(&mut self) -> Result<()> {
self.sram_file.seek(SeekFrom::Start(0))?; self.sram_file.seek(SeekFrom::Start(0))?;
self.sram_file.write_all(&self.sram)?; self.sram_file.write_all(&self.sram)?;
@@ -177,9 +201,17 @@ fn parse_elf_program<Elf: object::read::elf::FileHeader<Endian = object::Endiann
Some(bytes) Some(bytes)
} }
fn sram_path(file_path: &Path, sim_id: SimId) -> PathBuf { fn sram_path(file_path: &Path, sim_id: SimId, save_data_location: SaveDataLocation) -> PathBuf {
match sim_id { let extension = match sim_id {
SimId::Player1 => file_path.with_extension("p1.sram"), SimId::Player1 => "p1.sram",
SimId::Player2 => file_path.with_extension("p2.sram"), SimId::Player2 => "p2.sram",
};
if let SaveDataLocation::DataDirectory = save_data_location
&& let Some(data_dir) = filesystem::init_data_dir()
&& let Some(rom_name) = file_path.file_name()
{
data_dir.join(rom_name).with_extension(extension)
} else {
file_path.with_extension(extension)
} }
} }
+102 -11
View File
@@ -1,13 +1,104 @@
use std::ffi::c_void; use std::{
ffi::c_void,
sync::{LazyLock, Mutex},
};
#[derive(Default)]
struct Reallocator {
allocations: Vec<Allocation>,
}
unsafe impl Send for Reallocator {}
struct Allocation {
ptr: *mut u8,
size: usize,
}
impl Allocation {
fn new(size: usize) -> Self {
let layout = Self::layout(size);
// SAFETY: our layout is not zero, not too big, and properly aligned
let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
if ptr.is_null() {
std::alloc::handle_alloc_error(layout);
}
Self { ptr, size }
}
fn layout(size: usize) -> std::alloc::Layout {
assert!(size != 0);
std::alloc::Layout::from_size_align(size, 8).expect("far too large an allocation")
}
fn as_slice(&self) -> &[u8] {
// SAFETY: we allocated this ourselves, it's safe
unsafe { std::slice::from_raw_parts(self.ptr, self.size) }
}
fn as_mut_slice(&mut self) -> &mut [u8] {
// SAFETY: we allocated this ourselves, it's safe
unsafe { std::slice::from_raw_parts_mut(self.ptr, self.size) }
}
}
impl Drop for Allocation {
fn drop(&mut self) {
// SAFETY: we allocated this ourselves with this layout, and it hasn't been freed
unsafe { std::alloc::dealloc(self.ptr, Self::layout(self.size)) };
}
}
impl Reallocator {
fn realloc(&mut self, ptr: *mut c_void, new_size: usize) -> *mut c_void {
if let Some(old_data) = self.load_allocation(ptr) {
if new_size == 0 {
// free
std::ptr::null_mut()
} else {
// actually reallocate and copy the data over
let mut new_data = Allocation::new(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]);
self.store_allocation(new_data)
}
} else if new_size > 0 {
// fresh allocation
let allocation = Allocation::new(new_size);
self.store_allocation(allocation)
} else {
std::ptr::null_mut()
}
}
fn load_allocation(&mut self, ptr: *mut c_void) -> Option<Allocation> {
if ptr.is_null() {
return None;
}
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);
Some(allocation)
}
fn store_allocation(&mut self, allocation: Allocation) -> *mut c_void {
let ptr = allocation.ptr;
self.allocations.push(allocation);
ptr.cast()
}
}
static VBU_ALLOC: LazyLock<Mutex<Reallocator>> =
LazyLock::new(|| Mutex::new(Reallocator::default()));
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
unsafe fn vbu_realloc_shim(ptr: *mut c_void, new_size: usize) -> *mut c_void { #[allow(nonstandard_style)]
if !ptr.is_null() { unsafe fn VBU_MEMMGR(ptr: *mut c_void, new_size: usize) -> *mut c_void {
// not supporting proper realloc because it needs bookkeeping and it's unnecessary let mut allocator = VBU_ALLOC.lock().unwrap();
return std::ptr::null_mut(); allocator.realloc(ptr, new_size)
}
let allocation = vec![0u8; new_size].into_boxed_slice();
Box::into_raw(allocation).cast()
} }
#[link(name = "vb")] #[link(name = "vb")]
@@ -25,7 +116,7 @@ pub fn rom_from_isx(bytes: &[u8]) -> Option<Vec<u8>> {
if raw_rom.is_null() { if raw_rom.is_null() {
return None; return None;
} }
// SAFETY: the rom was allocated by vbu_realloc_shim, which created it from a Vec<u8>. let bytes = VBU_ALLOC.lock().unwrap().load_allocation(raw_rom)?;
let rom = unsafe { Vec::from_raw_parts(raw_rom.cast(), rom_length, rom_length) }; assert_eq!(bytes.size, rom_length);
Some(rom) Some(bytes.as_slice().to_vec())
} }
+167
View File
@@ -0,0 +1,167 @@
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 directories::ProjectDirs;
use serde::{Deserialize, Serialize};
#[derive(Clone)]
pub struct Persistence {
config_dir: Option<PathBuf>,
states: Arc<Mutex<HashMap<String, Arc<dyn Any + Send + Sync>>>>,
}
impl Persistence {
pub fn new() -> Self {
Self {
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,
}
}
pub fn save_config<T: Serialize>(&self, file: &str, data: &T) -> Result<()> {
if let Some(config_dir) = self.config_dir.as_ref() {
let bytes = serde_json::to_vec_pretty(data)?;
let filename = config_dir.join(file).with_extension("json");
fs::write(&filename, bytes)?;
}
Ok(())
}
pub fn load_config<T: for<'a> Deserialize<'a>>(&self, file: &str) -> Result<T> {
let Some(config_dir) = self.config_dir.as_ref() else {
bail!("config directory not found");
};
let filename = config_dir.join(file).with_extension("json");
let bytes = fs::read(filename)?;
Ok(serde_json::from_slice(&bytes)?)
}
}
fn init_config_dir() -> Option<PathBuf> {
let config_dir = project_dirs()?.config_dir().to_path_buf();
fs::create_dir_all(&config_dir).ok()?;
Some(config_dir)
}
pub fn init_data_dir() -> Option<PathBuf> {
let data_dir = project_dirs()?.data_dir().to_path_buf();
fs::create_dir_all(&data_dir).ok()?;
Some(data_dir)
}
fn project_dirs() -> Option<ProjectDirs> {
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 {
+1 -1
View File
@@ -14,7 +14,7 @@ use winit::keyboard::{KeyCode, PhysicalKey};
use crate::{ use crate::{
emulator::{SimId, VBKey}, emulator::{SimId, VBKey},
persistence::Persistence, filesystem::Persistence,
}; };
#[derive(Clone, PartialEq, Eq, Hash)] #[derive(Clone, PartialEq, Eq, Hash)]
+10 -15
View File
@@ -15,7 +15,7 @@ use winit::event_loop::{ControlFlow, EventLoop};
use crate::{ use crate::{
config::{AppConfig, CliArgs, SimConfig}, config::{AppConfig, CliArgs, SimConfig},
emulator::SimId, emulator::SimId,
persistence::Persistence, filesystem::Persistence,
}; };
mod app; mod app;
@@ -24,12 +24,12 @@ mod config;
mod controller; mod controller;
mod emulator; mod emulator;
mod filepicker; mod filepicker;
mod filesystem;
mod gdbserver; mod gdbserver;
mod graphics; mod graphics;
mod images; mod images;
mod input; mod input;
mod memory; mod memory;
mod persistence;
mod profiler; mod profiler;
mod window; mod window;
@@ -60,14 +60,9 @@ fn set_panic_handler() {
eprint!("{message}"); eprint!("{message}");
let Some(project_dirs) = directories::ProjectDirs::from("com", "virtual-boy", "Lemur") let Some(data_dir) = filesystem::init_data_dir() else {
else {
return; return;
}; };
let data_dir = project_dirs.data_dir();
if std::fs::create_dir_all(data_dir).is_err() {
return;
}
let timestamp = SystemTime::now() let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
.unwrap() .unwrap()
@@ -98,7 +93,13 @@ fn main() -> Result<()> {
let persistence = Persistence::new(); let persistence = Persistence::new();
let (mut builder, client) = EmulatorBuilder::new(); let mut config = AppConfig::load(&persistence);
config.update(|c| c.apply_updates(&args));
let p1 = SimConfig::load(&persistence, SimId::Player1);
let p2 = SimConfig::load(&persistence, SimId::Player2);
let (mut builder, client) = EmulatorBuilder::new(config.save_data_location);
if let Some(path) = &args.rom { if let Some(path) = &args.rom {
builder = builder.with_rom(path); builder = builder.with_rom(path);
} }
@@ -111,12 +112,6 @@ fn main() -> Result<()> {
if args.profile { if args.profile {
builder = builder.start_paused(true) builder = builder.start_paused(true)
} }
let mut config = AppConfig::load(&persistence);
if config.update(&args) {
let _ = config.save(&persistence);
}
let p1 = SimConfig::load(&persistence, SimId::Player1);
let p2 = SimConfig::load(&persistence, SimId::Player2);
let watch = args.watch; let watch = args.watch;
+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 {
-46
View File
@@ -1,46 +0,0 @@
use std::{fs, path::PathBuf};
use anyhow::{Result, bail};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
#[derive(Clone)]
pub struct Persistence {
dirs: Option<Dirs>,
}
impl Persistence {
pub fn new() -> Self {
Self { dirs: init_dirs() }
}
pub fn save_config<T: Serialize>(&self, file: &str, data: &T) -> Result<()> {
if let Some(dirs) = self.dirs.as_ref() {
let bytes = serde_json::to_vec_pretty(data)?;
let filename = dirs.config_dir.join(file).with_extension("json");
fs::write(&filename, bytes)?;
}
Ok(())
}
pub fn load_config<T: for<'a> Deserialize<'a>>(&self, file: &str) -> Result<T> {
let Some(dirs) = self.dirs.as_ref() else {
bail!("config directory not found");
};
let filename = dirs.config_dir.join(file).with_extension("json");
let bytes = fs::read(filename)?;
Ok(serde_json::from_slice(&bytes)?)
}
}
#[derive(Clone)]
struct Dirs {
config_dir: PathBuf,
}
fn init_dirs() -> Option<Dirs> {
let dirs = ProjectDirs::from("com", "virtual-boy", "Lemur")?;
let config_dir = dirs.config_dir().to_path_buf();
fs::create_dir_all(&config_dir).ok()?;
Some(Dirs { config_dir })
}
+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;
+1 -1
View File
@@ -12,7 +12,7 @@ impl AppWindow for AboutWindow {
} }
fn show(&mut self, ui: &mut Ui) { fn show(&mut self, ui: &mut Ui) {
CentralPanel::default().show_inside(ui, |ui| { CentralPanel::default().show(ui, |ui| {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.label("Lemur Virtual Boy Emulator"); ui.label("Lemur Virtual Boy Emulator");
ui.label(format!("Version {}", env!("CARGO_PKG_VERSION"))); ui.label(format!("Version {}", env!("CARGO_PKG_VERSION")));
+76 -37
View File
@@ -7,17 +7,17 @@ use std::{
use crate::{ use crate::{
app::UserEvent, app::UserEvent,
config::{COLOR_PRESETS, 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,
images::ImageTextureLoader, images::ImageTextureLoader,
input::{Command, MappingProvider, ShortcutProvider}, input::{Command, MappingProvider, ShortcutProvider},
memory::MemoryClient, memory::MemoryClient,
persistence::Persistence,
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,
@@ -604,6 +621,36 @@ impl GameWindow {
if ui.button("Hotkeys").clicked() { if ui.button("Hotkeys").clicked() {
self.open(ChildWindow::Hotkeys); self.open(ChildWindow::Hotkeys);
} }
ui.menu_button("Save Data Location", |ui| {
let save_data_location = self.client.save_data_location();
let update_save_data_location = |new_location: SaveDataLocation| {
if new_location == save_data_location {
return;
};
let mut app_config = AppConfig::load(&self.persistence);
app_config.update(|c| c.save_data_location = new_location);
self.client
.send_command(EmulatorCommand::SetSaveDataLocation(new_location));
};
if ui
.selectable_button(
save_data_location == SaveDataLocation::RomDirectory,
"Same Directory as ROM",
)
.clicked()
{
update_save_data_location(SaveDataLocation::RomDirectory);
}
if ui
.selectable_button(
save_data_location == SaveDataLocation::DataDirectory,
"App Data Directory",
)
.clicked()
{
update_save_data_location(SaveDataLocation::DataDirectory);
}
});
} }
fn show_color_picker(&mut self, ui: &mut Ui) { fn show_color_picker(&mut self, ui: &mut Ui) {
@@ -631,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) {
@@ -670,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)));
@@ -700,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;
}); });
@@ -708,9 +747,7 @@ impl AppWindow for GameWindow {
while let Ok(toast) = self.messages.try_recv() { while let Ok(toast) = self.messages.try_recv() {
self.toasts.add(toast); self.toasts.add(toast);
} }
Panel::top("menubar") Panel::top("menubar").exact_size(22.0).show(ui, |ui| {
.exact_size(22.0)
.show_inside(ui, |ui| {
MenuBar::new().ui(ui, |ui| { MenuBar::new().ui(ui, |ui| {
self.show_menu(ui); self.show_menu(ui);
}); });
@@ -725,7 +762,7 @@ impl AppWindow for GameWindow {
}); });
} }
let frame = Frame::central_panel(ui.style()).fill(Color32::BLACK); let frame = Frame::central_panel(ui.style()).fill(Color32::BLACK);
CentralPanel::default().frame(frame).show_inside(ui, |ui| { CentralPanel::default().frame(frame).show(ui, |ui| {
if let Some(screen) = self.screen.as_mut() { if let Some(screen) = self.screen.as_mut() {
screen.update(self.config.display_mode, self.config.colors); screen.update(self.config.display_mode, self.config.colors);
ui.add(screen); ui.add(screen);
@@ -845,6 +882,7 @@ impl DerefMut for AppWrapper {
pub enum ChildWindow { pub enum ChildWindow {
About, About,
CharacterData, CharacterData,
Memory,
BgMap, BgMap,
Objects, Objects,
Worlds, Worlds,
@@ -865,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",
+1 -1
View File
@@ -55,7 +55,7 @@ impl AppWindow for GdbServerWindow {
fn show(&mut self, ui: &mut egui::Ui) { fn show(&mut self, ui: &mut egui::Ui) {
let port_num: Option<u16> = self.port_str.parse().ok(); let port_num: Option<u16> = self.port_str.parse().ok();
let status = self.server.status(); let status = self.server.status();
CentralPanel::default().show_inside(ui, |ui| { CentralPanel::default().show(ui, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if port_num.is_none() { if port_num.is_none() {
let style = ui.style_mut(); let style = ui.style_mut();
+1 -1
View File
@@ -131,7 +131,7 @@ impl AppWindow for HotkeysWindow {
} }
fn show(&mut self, ui: &mut Ui) { fn show(&mut self, ui: &mut Ui) {
CentralPanel::default().show_inside(ui, |ui| { CentralPanel::default().show(ui, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("Use defaults").clicked() { if ui.button("Use defaults").clicked() {
self.shortcuts.reset(); self.shortcuts.reset();
+3 -3
View File
@@ -70,7 +70,7 @@ impl InputWindow {
.column(Column::remainder()) .column(Column::remainder())
.cell_layout(Layout::left_to_right(egui::Align::Center)) .cell_layout(Layout::left_to_right(egui::Align::Center))
.body(|mut body| { .body(|mut body| {
for keys in KEY_NAMES.chunks_exact(2) { for keys in KEY_NAMES.as_chunks::<2>().0 {
body.row(20.0, |mut row| { body.row(20.0, |mut row| {
for (key, name) in keys { for (key, name) in keys {
let binding = names.remove(key).map(|mut s| { let binding = names.remove(key).map(|mut s| {
@@ -167,7 +167,7 @@ impl AppWindow for InputWindow {
} }
fn show(&mut self, ui: &mut Ui) { fn show(&mut self, ui: &mut Ui) {
Panel::top("options").show_inside(ui, |ui| { Panel::top("options").show(ui, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let old_active_tab = self.active_tab; let old_active_tab = self.active_tab;
ui.selectable_value(&mut self.active_tab, InputTab::Player1, "Player 1"); ui.selectable_value(&mut self.active_tab, InputTab::Player1, "Player 1");
@@ -182,7 +182,7 @@ impl AppWindow for InputWindow {
} }
}); });
}); });
CentralPanel::default().show_inside(ui, |ui| { CentralPanel::default().show(ui, |ui| {
match self.active_tab { match self.active_tab {
InputTab::Player1 => self.show_key_bindings(ui, SimId::Player1), InputTab::Player1 => self.show_key_bindings(ui, SimId::Player1),
InputTab::Player2 => self.show_key_bindings(ui, SimId::Player2), InputTab::Player2 => self.show_key_bindings(ui, SimId::Player2),
+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);
}
}
+1 -1
View File
@@ -94,7 +94,7 @@ impl AppWindow for ProfileWindow {
fn show(&mut self, ui: &mut egui::Ui) { fn show(&mut self, ui: &mut egui::Ui) {
let status = self.profiler.status(); let status = self.profiler.status();
let recording = matches!(status, ProfilerStatus::Recording); let recording = matches!(status, ProfilerStatus::Recording);
CentralPanel::default().show_inside(ui, |ui| { CentralPanel::default().show(ui, |ui| {
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 0.0; ui.spacing_mut().item_spacing.x = 0.0;
ui.add( ui.add(
+1 -1
View File
@@ -54,7 +54,7 @@ impl AppWindow for TerminalWindow {
} }
self.lines.back_mut().unwrap().push_str(rest); self.lines.back_mut().unwrap().push_str(rest);
} }
CentralPanel::default().show_inside(ui, |ui| { CentralPanel::default().show(ui, |ui| {
ScrollArea::vertical() ScrollArea::vertical()
.stick_to_bottom(true) .stick_to_bottom(true)
.auto_shrink([false, false]) .auto_shrink([false, false])
+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) {
+24 -10
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,
@@ -203,7 +210,7 @@ impl AppWindow for BgMapWindow {
fn show(&mut self, ui: &mut Ui) { fn show(&mut self, ui: &mut Ui) {
self.state.load(ui); self.state.load(ui);
CentralPanel::default().show_inside(ui, |ui| { CentralPanel::default().show(ui, |ui| {
ui.horizontal_top(|ui| { ui.horizontal_top(|ui| {
StripBuilder::new(ui) StripBuilder::new(ui)
.size(Size::relative(0.3).at_most(200.0)) .size(Size::relative(0.3).at_most(200.0))
@@ -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],
+34 -14
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,8 +275,9 @@ 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_inside(ui, |ui| { CentralPanel::default().show(ui, |ui| {
ui.horizontal_top(|ui| { ui.horizontal_top(|ui| {
StripBuilder::new(ui) StripBuilder::new(ui)
.size(Size::relative(0.3).at_most(200.0)) .size(Size::relative(0.3).at_most(200.0))
@@ -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)
} }
} }
+19 -18
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
}); });
} }
@@ -163,7 +167,7 @@ impl AppWindow for FrameBufferWindow {
fn show(&mut self, ui: &mut Ui) { fn show(&mut self, ui: &mut Ui) {
self.state.load(ui); self.state.load(ui);
CentralPanel::default().show_inside(ui, |ui| { CentralPanel::default().show(ui, |ui| {
ui.horizontal_top(|ui| { ui.horizontal_top(|ui| {
StripBuilder::new(ui) StripBuilder::new(ui)
.size(Size::relative(0.3).at_most(200.0)) .size(Size::relative(0.3).at_most(200.0))
@@ -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() {
+18 -13
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
}); });
} }
@@ -219,7 +223,7 @@ impl AppWindow for ObjectWindow {
fn show(&mut self, ui: &mut Ui) { fn show(&mut self, ui: &mut Ui) {
self.state.load(ui); self.state.load(ui);
CentralPanel::default().show_inside(ui, |ui| { CentralPanel::default().show(ui, |ui| {
ui.horizontal_top(|ui| { ui.horizontal_top(|ui| {
StripBuilder::new(ui) StripBuilder::new(ui)
.size(Size::relative(0.3).at_most(200.0)) .size(Size::relative(0.3).at_most(200.0))
@@ -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);
+10 -5
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,7 +645,8 @@ impl AppWindow for RegisterWindow {
} }
fn show(&mut self, ui: &mut Ui) { fn show(&mut self, ui: &mut Ui) {
CentralPanel::default().show_inside(ui, |ui| { self.config.refresh();
CentralPanel::default().show(ui, |ui| {
ScrollArea::vertical().show(ui, |ui| { ScrollArea::vertical().show(ui, |ui| {
ui.horizontal_top(|ui| { ui.horizontal_top(|ui| {
let width = ui.available_width(); let width = ui.available_width();
+4 -1
View File
@@ -1,6 +1,7 @@
use egui::{Color32, Image, ImageSource, Response, Sense, TextureOptions, Ui, Widget}; use egui::{Color32, Image, ImageSource, Response, Sense, TextureOptions, Ui, Widget};
pub const GENERIC_PALETTE: [u8; 4] = [0, 32, 64, 128]; pub const GENERIC_PALETTE: [u8; 4] = [0, 32, 64, 128];
const COLOR_TRANSPARENT: Color32 = Color32::from_rgb(0x25, 0x25, 0x25);
pub fn shade(brt: u8, color: Color32) -> Color32 { pub fn shade(brt: u8, color: Color32) -> Color32 {
let corrected = if brt > 132 { let corrected = if brt > 132 {
@@ -44,7 +45,9 @@ pub fn parse_brts(brts: &[u16; 3], color: Color32) -> [Color32; 4] {
pub fn palette_colors(palette: u8, brts: &[u8; 8], color: Color32) -> [Color32; 4] { pub fn palette_colors(palette: u8, brts: &[u8; 8], color: Color32) -> [Color32; 4] {
let colors = parse_shades(brts).map(|s| shade(s, color)); let colors = parse_shades(brts).map(|s| shade(s, color));
parse_palette(palette).map(|p| colors[p as usize]) let mut palette_colors = parse_palette(palette).map(|p| colors[p as usize]);
palette_colors[0] = COLOR_TRANSPARENT;
palette_colors
} }
pub struct Object { pub struct Object {
+23 -16
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,8 +543,14 @@ 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_inside(ui, |ui| { CentralPanel::default().show(ui, |ui| {
ui.horizontal_top(|ui| { ui.horizontal_top(|ui| {
StripBuilder::new(ui) StripBuilder::new(ui)
.size(Size::relative(0.3).at_most(200.0)) .size(Size::relative(0.3).at_most(200.0))
@@ -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(),
)
} }
} }
@@ -944,7 +951,7 @@ impl WorldHeader {
let ron = data & 0x4000 != 0; let ron = data & 0x4000 != 0;
let mode = WorldMode::from_u16((data >> 12) & 0x3).unwrap(); let mode = WorldMode::from_u16((data >> 12) & 0x3).unwrap();
let scx = (data >> 10) as u32 & 0x03; let scx = (data >> 10) as u32 & 0x03;
let scy = (data >> 10) as u32 & 0x03; let scy = (data >> 8) as u32 & 0x03;
let over = data & 0x0080 != 0; let over = data & 0x0080 != 0;
let end = data & 0x0040 != 0; let end = data & 0x0040 != 0;
let base = (data & 0x000f) as usize; let base = (data & 0x000f) as usize;