Compare commits

..

No commits in common. "16613fb98241404c0329fc4ee9b8a7b82867b732" and "0a8da6bff05ce0e7890b5511c9d9e57bf16898fd" have entirely different histories.

16 changed files with 904 additions and 1350 deletions

1720
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,11 +16,11 @@ bytemuck = { version = "1", features = ["derive"] }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
cpal = { git = "https://github.com/sidit77/cpal.git", rev = "66ed6be" } cpal = { git = "https://github.com/sidit77/cpal.git", rev = "66ed6be" }
directories = "6" directories = "6"
egui = { version = "0.33", features = ["serde"] } egui = { version = "0.32", features = ["serde"] }
egui_extras = { version = "0.33", features = ["image"] } egui_extras = { version = "0.32", features = ["image"] }
egui-notify = "0.21" egui-notify = "0.20"
egui-winit = "0.33" egui-winit = "0.32"
egui-wgpu = { version = "0.33", features = ["winit"] } egui-wgpu = { version = "0.32", 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"] }
@ -29,10 +29,9 @@ hex = "0.4"
image = { version = "0.25", default-features = false, features = ["png"] } image = { version = "0.25", default-features = false, features = ["png"] }
itertools = "0.14" itertools = "0.14"
normpath = "1" normpath = "1"
notify = "8"
num-derive = "0.4" num-derive = "0.4"
num-traits = "0.2" num-traits = "0.2"
object = "0.38" object = "0.37"
oneshot = "0.1" oneshot = "0.1"
pollster = "0.4" pollster = "0.4"
rand = "0.9" rand = "0.9"
@ -41,16 +40,16 @@ rtrb = "0.3"
rubato = "0.16" rubato = "0.16"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
thread-priority = "3" thread-priority = "2"
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_info"] } tracing = { version = "0.1", features = ["release_max_level_info"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
wgpu = "27" wgpu = "25"
wholesym = "0.8" wholesym = "0.8"
winit = { version = "0.30", features = ["serde"] } winit = { version = "0.30", features = ["serde"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows = { version = "0.62", features = ["Win32_System_Threading"] } windows = { version = "0.61", features = ["Win32_System_Threading"] }
[build-dependencies] [build-dependencies]
cc = "1" cc = "1"

View File

@ -16,7 +16,7 @@ RUN apt-get update && \
ln -s $(which ld64.lld-20) /usr/bin/ld64.lld && \ ln -s $(which ld64.lld-20) /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.91-bookworm FROM rust:1.89-bookworm
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 && \

View File

@ -21,7 +21,6 @@ fn main() -> Result<(), Box<dyn Error>> {
.include(Path::new("shrooms-vb-core/util")) .include(Path::new("shrooms-vb-core/util"))
.opt_level(opt_level) .opt_level(opt_level)
.flag_if_supported("-fno-strict-aliasing") .flag_if_supported("-fno-strict-aliasing")
.define("_CRT_SECURE_NO_WARNINGS", None)
.define("VB_LITTLE_ENDIAN", None) .define("VB_LITTLE_ENDIAN", None)
.define("VB_SIGNED_PROPAGATE", None) .define("VB_SIGNED_PROPAGATE", None)
.define("VB_DIV_GENERIC", None) .define("VB_DIV_GENERIC", None)

@ -1 +1 @@
Subproject commit 534ce852f46f1deba9014971b1bae29e974984d7 Subproject commit 4ed3b7299507b8ea0079a0965f33b0c8a6886572

View File

@ -16,7 +16,6 @@ use winit::{
}; };
use crate::{ use crate::{
config::CliArgs,
controller::ControllerManager, controller::ControllerManager,
emulator::{EmulatorClient, EmulatorCommand, SimId}, emulator::{EmulatorClient, EmulatorCommand, SimId},
images::ImageProcessor, images::ImageProcessor,
@ -56,24 +55,18 @@ pub struct Application {
focused: Option<ViewportId>, focused: Option<ViewportId>,
init_debug_port: Option<u16>, init_debug_port: Option<u16>,
init_profiling: bool, init_profiling: bool,
init_bgmap: bool,
init_chardata: bool,
init_objects: bool,
init_worlds: bool,
init_framebuffers: bool,
init_registers: bool,
init_terminal: bool,
} }
impl Application { impl Application {
pub fn new( pub fn new(
client: EmulatorClient, client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
persistence: Persistence, debug_port: Option<u16>,
args: CliArgs, profiling: bool,
) -> Self { ) -> Self {
let wgpu = WgpuState::new(); let wgpu = WgpuState::new();
let icon = load_icon().ok().map(Arc::new); let icon = load_icon().ok().map(Arc::new);
let persistence = Persistence::new();
let mappings = MappingProvider::new(persistence.clone()); let mappings = MappingProvider::new(persistence.clone());
let shortcuts = ShortcutProvider::new(persistence.clone()); let shortcuts = ShortcutProvider::new(persistence.clone());
let controllers = ControllerManager::new(client.clone(), &mappings); let controllers = ControllerManager::new(client.clone(), &mappings);
@ -97,15 +90,8 @@ impl Application {
persistence, persistence,
viewports: HashMap::new(), viewports: HashMap::new(),
focused: None, focused: None,
init_debug_port: args.debug_port, init_debug_port: debug_port,
init_profiling: args.profile, init_profiling: profiling,
init_bgmap: args.bgmap_data,
init_chardata: args.character_data,
init_objects: args.object_data,
init_worlds: args.worlds,
init_framebuffers: args.frame_buffers,
init_registers: args.registers,
init_terminal: args.terminal,
} }
} }
@ -138,40 +124,11 @@ impl ApplicationHandler<UserEvent> for Application {
SimId::Player1, SimId::Player1,
); );
self.open(event_loop, Box::new(app)); self.open(event_loop, Box::new(app));
let sim_id = SimId::Player1;
if self.init_profiling { if self.init_profiling {
let mut profiler = ProfileWindow::new(sim_id, self.client.clone()); let mut profiler = ProfileWindow::new(SimId::Player1, self.client.clone());
profiler.launch(); profiler.launch();
self.open(event_loop, Box::new(profiler)); self.open(event_loop, Box::new(profiler));
} }
if self.init_chardata {
let chardata = CharacterDataWindow::new(sim_id, &self.memory, &mut self.images);
self.open(event_loop, Box::new(chardata));
}
if self.init_bgmap {
let bgmap = BgMapWindow::new(sim_id, &self.memory, &mut self.images);
self.open(event_loop, Box::new(bgmap));
}
if self.init_objects {
let objects = ObjectWindow::new(sim_id, &self.memory, &mut self.images);
self.open(event_loop, Box::new(objects));
}
if self.init_worlds {
let world = WorldWindow::new(sim_id, &self.memory, &mut self.images);
self.open(event_loop, Box::new(world));
}
if self.init_framebuffers {
let fb = FrameBufferWindow::new(sim_id, &self.memory, &mut self.images);
self.open(event_loop, Box::new(fb));
}
if self.init_registers {
let registers = RegisterWindow::new(sim_id, &self.memory);
self.open(event_loop, Box::new(registers));
}
if self.init_terminal {
let terminal = TerminalWindow::new(sim_id, &self.client);
self.open(event_loop, Box::new(terminal));
}
} }
fn window_event( fn window_event(
@ -287,8 +244,8 @@ impl ApplicationHandler<UserEvent> for Application {
self.open(event_loop, Box::new(world)); self.open(event_loop, Box::new(world));
} }
UserEvent::OpenFrameBuffers(sim_id) => { UserEvent::OpenFrameBuffers(sim_id) => {
let fb = FrameBufferWindow::new(sim_id, &self.memory, &mut self.images); let world = FrameBufferWindow::new(sim_id, &self.memory, &mut self.images);
self.open(event_loop, Box::new(fb)); self.open(event_loop, Box::new(world));
} }
UserEvent::OpenRegisters(sim_id) => { UserEvent::OpenRegisters(sim_id) => {
let registers = RegisterWindow::new(sim_id, &self.memory); let registers = RegisterWindow::new(sim_id, &self.memory);
@ -449,12 +406,13 @@ impl Viewport {
..egui_wgpu::WgpuConfiguration::default() ..egui_wgpu::WgpuConfiguration::default()
}; };
let options = egui_wgpu::RendererOptions::default();
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new( let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
ctx.clone(), ctx.clone(),
wgpu_config, wgpu_config,
1,
None,
false, false,
options, true,
)); ));
let mut info = ViewportInfo::default(); let mut info = ViewportInfo::default();
@ -553,7 +511,7 @@ impl Viewport {
&mut self.info, &mut self.info,
std::mem::take(&mut self.commands), std::mem::take(&mut self.commands),
&self.window, &self.window,
&mut vec![], &mut HashSet::default(),
); );
if self.info.close_requested() { if self.info.close_requested() {

View File

@ -1,96 +0,0 @@
use anyhow::Result;
use clap::Parser;
use egui::{Color32, Vec2};
use serde::{Deserialize, Serialize};
use crate::{emulator::SimId, persistence::Persistence, window::DisplayMode};
use std::path::PathBuf;
#[derive(Parser)]
pub struct CliArgs {
/// The path to a virtual boy ROM to run.
pub rom: Option<PathBuf>,
/// Start a GDB/LLDB debug server on this port.
#[arg(short, long)]
pub debug_port: Option<u16>,
/// Enable profiling a game
#[arg(short, long)]
pub profile: bool,
/// Open character data window
#[arg(short, long)]
pub character_data: bool,
/// Open bgmap data window
#[arg(short, long)]
pub bgmap_data: bool,
/// Open object data window
#[arg(short, long)]
pub object_data: bool,
/// Open worlds window
#[arg(long)]
pub worlds: bool,
/// Open frame buffers window
#[arg(short, long)]
pub frame_buffers: bool,
/// Open registers window
#[arg(short, long)]
pub registers: bool,
/// Open terminal
#[arg(short, long)]
pub terminal: bool,
/// Watch ROM files for changes, automatically reload
#[arg(short, long)]
pub watch: bool,
}
pub const COLOR_PRESETS: [[Color32; 2]; 3] = [
[
Color32::from_rgb(0xff, 0x00, 0x00),
Color32::from_rgb(0x00, 0xc6, 0xf0),
],
[
Color32::from_rgb(0x00, 0xb4, 0x00),
Color32::from_rgb(0xc8, 0x00, 0xff),
],
[
Color32::from_rgb(0xb4, 0x9b, 0x00),
Color32::from_rgb(0x00, 0x00, 0xff),
],
];
const fn default_audio_enabled() -> bool {
true
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct SimConfig {
pub display_mode: DisplayMode,
pub colors: [Color32; 2],
pub dimensions: Vec2,
#[serde(default = "default_audio_enabled")]
pub audio_enabled: bool,
}
impl SimConfig {
pub fn load(persistence: &Persistence, sim_id: SimId) -> Self {
if let Ok(config) = persistence.load_config(config_filename(sim_id)) {
return config;
}
Self {
display_mode: DisplayMode::Anaglyph,
colors: COLOR_PRESETS[0],
dimensions: DisplayMode::Anaglyph.proportions() + Vec2::new(0.0, 22.0),
audio_enabled: true,
}
}
pub fn save(&self, persistence: &Persistence, sim_id: SimId) -> Result<()> {
persistence.save_config(config_filename(sim_id), self)
}
}
fn config_filename(sim_id: SimId) -> &'static str {
match sim_id {
SimId::Player1 => "config_p1",
SimId::Player2 => "config_p2",
}
}

View File

@ -5,7 +5,7 @@ use std::{
sync::{ sync::{
Arc, Weak, Arc, Weak,
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
mpsc::{self, RecvTimeoutError, TryRecvError}, mpsc::{self, RecvError, TryRecvError},
}, },
time::Duration, time::Duration,
}; };
@ -75,7 +75,6 @@ pub struct EmulatorBuilder {
audio_on: Arc<[AtomicBool; 2]>, audio_on: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>, linked: Arc<AtomicBool>,
start_paused: bool, start_paused: bool,
watch_rom: Arc<[AtomicBool; 2]>,
} }
impl EmulatorBuilder { impl EmulatorBuilder {
@ -92,13 +91,12 @@ impl EmulatorBuilder {
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)),
start_paused: false, start_paused: false,
watch_rom: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]),
}; };
let client = EmulatorClient { let client = EmulatorClient {
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(), audio_on: builder.audio_on.clone(),
linked: builder.linked.clone(), linked: builder.linked.clone(),
}; };
(builder, client) (builder, client)
@ -118,25 +116,12 @@ impl EmulatorBuilder {
} }
} }
pub fn with_audio_on(self, p1: bool, p2: bool) -> Self {
self.audio_on[0].store(p1, Ordering::Relaxed);
self.audio_on[1].store(p2, Ordering::Relaxed);
self
}
pub fn with_watch_rom(self, p1: bool, p2: bool) -> Self {
self.watch_rom[0].store(p1, Ordering::Relaxed);
self.watch_rom[1].store(p2, Ordering::Relaxed);
self
}
pub fn build(self) -> Result<Emulator> { pub fn build(self) -> Result<Emulator> {
let mut emulator = Emulator::new( let mut emulator = Emulator::new(
self.commands, self.commands,
self.sim_state, self.sim_state,
self.state, self.state,
self.audio_on, self.audio_on,
self.watch_rom,
self.linked, self.linked,
)?; )?;
if let Some(path) = self.rom { if let Some(path) = self.rom {
@ -157,7 +142,6 @@ pub struct Emulator {
sim_state: Arc<[Atomic<SimState>; 2]>, sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>, state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>, audio_on: Arc<[AtomicBool; 2]>,
watch_rom: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>, linked: Arc<AtomicBool>,
profilers: [Option<ProfileSender>; 2], profilers: [Option<ProfileSender>; 2],
renderers: HashMap<SimId, TextureSink>, renderers: HashMap<SimId, TextureSink>,
@ -176,7 +160,6 @@ impl Emulator {
sim_state: Arc<[Atomic<SimState>; 2]>, sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>, state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>, audio_on: Arc<[AtomicBool; 2]>,
watch_rom: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>, linked: Arc<AtomicBool>,
) -> Result<Self> { ) -> Result<Self> {
Ok(Self { Ok(Self {
@ -187,7 +170,6 @@ impl Emulator {
sim_state, sim_state,
state, state,
audio_on, audio_on,
watch_rom,
linked, linked,
profilers: [None, None], profilers: [None, None],
renderers: HashMap::new(), renderers: HashMap::new(),
@ -201,18 +183,9 @@ impl Emulator {
}) })
} }
pub fn reload_cart(&mut self, sim_id: SimId) -> Result<()> {
let Some(cart) = &self.carts[sim_id.to_index()] else {
return Ok(());
};
let path = cart.file_path.clone();
self.load_cart(sim_id, &path)
}
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 cart = Cart::load(path, sim_id)?;
let cart = Cart::load(path, sim_id, watch)?; self.reset_sim(sim_id, Some(cart))?;
self.try_reset_sim(sim_id, Some(cart))?;
Ok(()) Ok(())
} }
@ -222,26 +195,16 @@ impl Emulator {
} else { } else {
self.carts[0].as_ref().map(|c| c.file_path.clone()) self.carts[0].as_ref().map(|c| c.file_path.clone())
}; };
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)?),
None => None, None => None,
}; };
self.try_reset_sim(SimId::Player2, cart)?; self.reset_sim(SimId::Player2, cart)?;
self.link_sims(); self.link_sims();
Ok(()) Ok(())
} }
fn reset_sim(&mut self, sim_id: SimId, new_cart: Option<Cart>) -> bool { fn reset_sim(&mut self, sim_id: SimId, new_cart: Option<Cart>) -> Result<()> {
if let Err(error) = self.try_reset_sim(sim_id, new_cart) {
self.report_error(sim_id, format!("Error resetting sim: {error}"));
false
} else {
true
}
}
fn try_reset_sim(&mut self, sim_id: SimId, new_cart: Option<Cart>) -> Result<()> {
self.save_sram(sim_id)?; self.save_sram(sim_id)?;
let index = sim_id.to_index(); let index = sim_id.to_index();
@ -262,10 +225,6 @@ impl Emulator {
sim.load_cart(cart.rom.clone(), cart.sram.clone())?; sim.load_cart(cart.rom.clone(), cart.sram.clone())?;
self.carts[index] = Some(cart); self.carts[index] = Some(cart);
self.sim_state[index].store(SimState::Ready, Ordering::Release); self.sim_state[index].store(SimState::Ready, Ordering::Release);
} else if let Some(cart) = self.carts[index].as_mut()
&& cart.is_watching()
{
cart.restart_watching();
} }
let mut profiling = false; let mut profiling = false;
if let Some(profiler) = self.profilers[sim_id.to_index()].as_ref() if let Some(profiler) = self.profilers[sim_id.to_index()].as_ref()
@ -382,7 +341,7 @@ impl Emulator {
fn start_profiling(&mut self, sim_id: SimId, sender: ProfileSender) -> Result<()> { fn start_profiling(&mut self, sim_id: SimId, sender: ProfileSender) -> Result<()> {
self.profilers[sim_id.to_index()] = Some(sender); self.profilers[sim_id.to_index()] = Some(sender);
self.try_reset_sim(sim_id, None) self.reset_sim(sim_id, None)
} }
fn start_debugging(&mut self, sim_id: SimId, sender: DebugSender) { fn start_debugging(&mut self, sim_id: SimId, sender: DebugSender) {
@ -458,33 +417,14 @@ impl Emulator {
let idle = self.tick(); let idle = self.tick();
if idle { if idle {
// The game is paused, and we have output all the video/audio we have. // The game is paused, and we have output all the video/audio we have.
// Block the thread until a new command comes in, or a ROM changes. // Block the thread until a new command comes in.
loop { match self.commands.recv() {
match self.commands.recv_timeout(Duration::from_millis(250)) { Ok(command) => self.handle_command(command),
Ok(command) => { Err(RecvError) => {
self.handle_command(command);
break;
}
Err(RecvTimeoutError::Timeout) => {
let mut changed = false;
for sim_id in SimId::values() {
let Some(cart) = self.carts[sim_id.to_index()].as_ref() else {
continue;
};
if cart.changed() {
changed |= self.reset_sim(sim_id, None);
}
}
if changed {
break;
}
}
Err(RecvTimeoutError::Disconnected) => {
return; return;
} }
} }
} }
}
loop { loop {
match self.commands.try_recv() { match self.commands.try_recv() {
Ok(command) => self.handle_command(command), Ok(command) => self.handle_command(command),
@ -496,14 +436,6 @@ impl Emulator {
} }
} }
} }
for sim_id in SimId::values() {
let Some(cart) = self.carts[sim_id.to_index()].as_ref() else {
continue;
};
if cart.changed() {
self.reset_sim(sim_id, None);
}
}
self.watched_regions.retain(|range, region| { self.watched_regions.retain(|range, region| {
let Some(region) = region.upgrade() else { let Some(region) = region.upgrade() else {
return false; return false;
@ -670,22 +602,6 @@ impl Emulator {
self.report_error(sim_id, format!("Error loading rom: {error}")); self.report_error(sim_id, format!("Error loading rom: {error}"));
} }
} }
EmulatorCommand::ReloadRom(sim_id) => {
if let Err(error) = self.reload_cart(sim_id) {
self.report_error(sim_id, format!("Error loading rom: {error}"));
}
}
EmulatorCommand::WatchRom(sim_id, watch) => {
self.watch_rom[sim_id.to_index()].store(watch, Ordering::Release);
let Some(cart) = self.carts[sim_id.to_index()].as_mut() else {
return;
};
if watch {
cart.restart_watching();
} else {
cart.stop_watching();
}
}
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(
@ -799,8 +715,9 @@ impl Emulator {
}; };
sim.watch_stdout(true); sim.watch_stdout(true);
} }
EmulatorCommand::SetAudioEnabled(sim_id, enabled) => { EmulatorCommand::SetAudioEnabled(p1, p2) => {
self.audio_on[sim_id.to_index()].store(enabled, Ordering::Release); self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release);
self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release);
} }
EmulatorCommand::Link => { EmulatorCommand::Link => {
self.link_sims(); self.link_sims();
@ -809,7 +726,9 @@ impl Emulator {
self.unlink_sims(); self.unlink_sims();
} }
EmulatorCommand::Reset(sim_id) => { EmulatorCommand::Reset(sim_id) => {
self.reset_sim(sim_id, None); if let Err(error) = self.reset_sim(sim_id, None) {
self.report_error(sim_id, format!("Error resetting sim: {error}"));
}
} }
EmulatorCommand::SetKeys(sim_id, keys) => { EmulatorCommand::SetKeys(sim_id, keys) => {
if let Some(sim) = self.sims.get_mut(sim_id.to_index()) { if let Some(sim) = self.sims.get_mut(sim_id.to_index()) {
@ -851,8 +770,6 @@ impl Emulator {
pub enum EmulatorCommand { pub enum EmulatorCommand {
ConnectToSim(SimId, TextureSink, mpsc::Sender<Toast>), ConnectToSim(SimId, TextureSink, mpsc::Sender<Toast>),
LoadGame(SimId, PathBuf), LoadGame(SimId, PathBuf),
ReloadRom(SimId),
WatchRom(SimId, bool),
StartSecondSim(Option<PathBuf>), StartSecondSim(Option<PathBuf>),
StopSecondSim, StopSecondSim,
Pause, Pause,
@ -875,7 +792,7 @@ pub enum EmulatorCommand {
AddWatchpoint(SimId, u32, usize, VBWatchpointType), AddWatchpoint(SimId, u32, usize, VBWatchpointType),
RemoveWatchpoint(SimId, u32, usize, VBWatchpointType), RemoveWatchpoint(SimId, u32, usize, VBWatchpointType),
WatchStdout(SimId, mpsc::Sender<String>), WatchStdout(SimId, mpsc::Sender<String>),
SetAudioEnabled(SimId, bool), SetAudioEnabled(bool, bool),
Link, Link,
Unlink, Unlink,
Reset(SimId), Reset(SimId),
@ -941,8 +858,8 @@ pub struct EmulatorClient {
queue: mpsc::Sender<EmulatorCommand>, queue: mpsc::Sender<EmulatorCommand>,
sim_state: Arc<[Atomic<SimState>; 2]>, sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>, state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>, linked: Arc<AtomicBool>,
watch_rom: Arc<[AtomicBool; 2]>,
} }
impl EmulatorClient { impl EmulatorClient {
@ -952,12 +869,12 @@ impl EmulatorClient {
pub fn emulator_state(&self) -> EmulatorState { pub fn emulator_state(&self) -> EmulatorState {
self.state.load(Ordering::Acquire) self.state.load(Ordering::Acquire)
} }
pub fn is_audio_enabled(&self, sim_id: SimId) -> bool {
self.audio_on[sim_id.to_index()].load(Ordering::Acquire)
}
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 is_rom_watched(&self, sim_id: SimId) -> bool {
self.watch_rom[sim_id.to_index()].load(Ordering::Acquire)
}
pub fn send_command(&self, command: EmulatorCommand) -> bool { pub fn send_command(&self, command: EmulatorCommand) -> bool {
match self.queue.send(command) { match self.queue.send(command) {
Ok(()) => true, Ok(()) => true,

View File

@ -1,11 +1,10 @@
use anyhow::Result; use anyhow::Result;
use notify::Watcher;
use rand::Rng; use rand::Rng;
use std::{ use std::{
fs::{self, File}, fs::{self, File},
io::{Read, Seek as _, SeekFrom, Write as _}, io::{Read, Seek as _, SeekFrom, Write as _},
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, atomic::AtomicBool}, sync::Arc,
}; };
use crate::emulator::{SimId, game_info::GameInfo, shrooms_vb_util::rom_from_isx}; use crate::emulator::{SimId, game_info::GameInfo, shrooms_vb_util::rom_from_isx};
@ -16,12 +15,10 @@ pub struct Cart {
sram_file: File, sram_file: File,
pub sram: Vec<u8>, pub sram: Vec<u8>,
pub info: Arc<GameInfo>, pub info: Arc<GameInfo>,
watcher: Option<notify::RecommendedWatcher>,
changed: Arc<AtomicBool>,
} }
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) -> Result<Self> {
let rom = fs::read(file_path)?; let rom = fs::read(file_path)?;
let (rom, info) = try_parse_isx(file_path, &rom) let (rom, info) = try_parse_isx(file_path, &rom)
.or_else(|| try_parse_elf(file_path, &rom)) .or_else(|| try_parse_elf(file_path, &rom))
@ -48,21 +45,12 @@ impl Cart {
sram sram
}; };
let changed = Arc::new(AtomicBool::new(false));
let watcher = if watch {
build_watcher(file_path, changed.clone())
} else {
None
};
Ok(Cart { Ok(Cart {
file_path: file_path.to_path_buf(), file_path: file_path.to_path_buf(),
rom, rom,
sram_file, sram_file,
sram, sram,
info: Arc::new(info), info: Arc::new(info),
watcher,
changed,
}) })
} }
@ -71,50 +59,6 @@ impl Cart {
self.sram_file.write_all(&self.sram)?; self.sram_file.write_all(&self.sram)?;
Ok(()) Ok(())
} }
pub fn changed(&self) -> bool {
self.changed.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn is_watching(&self) -> bool {
self.watcher.is_some()
}
pub fn restart_watching(&mut self) {
self.changed = Arc::new(AtomicBool::new(false));
if let Some(mut watcher) = self.watcher.take() {
let _ = watcher.unwatch(&self.file_path);
};
self.watcher = build_watcher(&self.file_path, self.changed.clone());
}
pub fn stop_watching(&mut self) {
self.changed = Arc::new(AtomicBool::new(false));
self.watcher = None;
}
}
fn build_watcher(file_path: &Path, changed: Arc<AtomicBool>) -> Option<notify::RecommendedWatcher> {
let file_path = file_path.to_path_buf();
let mut watcher =
notify::recommended_watcher(move |event: Result<notify::Event, notify::Error>| {
let Ok(e) = event else {
return;
};
let modified = !matches!(
e.kind,
notify::EventKind::Access(_)
| notify::EventKind::Modify(notify::event::ModifyKind::Metadata(_))
);
if modified {
changed.store(true, std::sync::atomic::Ordering::Relaxed);
}
})
.ok()?;
watcher
.watch(&file_path, notify::RecursiveMode::NonRecursive)
.ok()?;
Some(watcher)
} }
fn try_parse_isx(file_path: &Path, data: &[u8]) -> Option<(Vec<u8>, GameInfo)> { fn try_parse_isx(file_path: &Path, data: &[u8]) -> Option<(Vec<u8>, GameInfo)> {

View File

@ -457,10 +457,15 @@ struct PersistedGamepadMapping {
default_axes: Vec<(Code, (VBKey, VBKey))>, default_axes: Vec<(Code, (VBKey, VBKey))>,
} }
#[derive(Serialize, Deserialize)]
pub struct Shortcut {
pub shortcut: KeyboardShortcut,
pub command: Command,
}
#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum Command { pub enum Command {
OpenRom, OpenRom,
ReloadRom,
Quit, Quit,
FrameAdvance, FrameAdvance,
FastForward(u32), FastForward(u32),
@ -471,10 +476,9 @@ pub enum Command {
} }
impl Command { impl Command {
pub fn all() -> [Self; 8] { pub fn all() -> [Self; 7] {
[ [
Self::OpenRom, Self::OpenRom,
Self::ReloadRom,
Self::Quit, Self::Quit,
Self::PauseResume, Self::PauseResume,
Self::Reset, Self::Reset,
@ -487,7 +491,6 @@ impl Command {
pub fn name(self) -> &'static str { pub fn name(self) -> &'static str {
match self { match self {
Self::OpenRom => "Open ROM", Self::OpenRom => "Open ROM",
Self::ReloadRom => "Reload ROM",
Self::Quit => "Exit", Self::Quit => "Exit",
Self::PauseResume => "Pause/Resume", Self::PauseResume => "Pause/Resume",
Self::Reset => "Reset", Self::Reset => "Reset",
@ -519,10 +522,6 @@ impl Default for Shortcuts {
Command::OpenRom, Command::OpenRom,
KeyboardShortcut::new(Modifiers::COMMAND, Key::O), KeyboardShortcut::new(Modifiers::COMMAND, Key::O),
); );
shortcuts.set(
Command::ReloadRom,
KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, Key::F5),
);
shortcuts.set( shortcuts.set(
Command::Quit, Command::Quit,
KeyboardShortcut::new(Modifiers::COMMAND, Key::Q), KeyboardShortcut::new(Modifiers::COMMAND, Key::Q),

View File

@ -1,7 +1,7 @@
// hide console in release mode // hide console in release mode
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::{process, time::SystemTime}; use std::{path::PathBuf, process, time::SystemTime};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use app::Application; use app::Application;
@ -12,15 +12,8 @@ use tracing::error;
use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt};
use winit::event_loop::{ControlFlow, EventLoop}; use winit::event_loop::{ControlFlow, EventLoop};
use crate::{
config::{CliArgs, SimConfig},
emulator::SimId,
persistence::Persistence,
};
mod app; mod app;
mod audio; mod audio;
mod config;
mod controller; mod controller;
mod emulator; mod emulator;
mod gdbserver; mod gdbserver;
@ -32,6 +25,18 @@ mod persistence;
mod profiler; mod profiler;
mod window; mod window;
#[derive(Parser)]
struct Args {
/// The path to a virtual boy ROM to run.
rom: Option<PathBuf>,
/// Start a GDB/LLDB debug server on this port.
#[arg(short, long)]
debug_port: Option<u16>,
/// Enable profiling a game
#[arg(short, long)]
profile: bool,
}
fn init_logger() { fn init_logger() {
let directives = std::env::var("RUST_LOG").unwrap_or("error,lemur=info".into()); let directives = std::env::var("RUST_LOG").unwrap_or("error,lemur=info".into());
let filter = EnvFilter::builder().parse_lossy(directives); let filter = EnvFilter::builder().parse_lossy(directives);
@ -93,9 +98,7 @@ fn main() -> Result<()> {
#[cfg(windows)] #[cfg(windows)]
set_process_priority_to_high()?; set_process_priority_to_high()?;
let args = CliArgs::parse(); let args = Args::parse();
let persistence = Persistence::new();
let (mut builder, client) = EmulatorBuilder::new(); let (mut builder, client) = EmulatorBuilder::new();
if let Some(path) = &args.rom { if let Some(path) = &args.rom {
@ -110,14 +113,6 @@ fn main() -> Result<()> {
if args.profile { if args.profile {
builder = builder.start_paused(true) builder = builder.start_paused(true)
} }
let p1 = SimConfig::load(&persistence, SimId::Player1);
let p2 = SimConfig::load(&persistence, SimId::Player2);
let watch = args.watch;
builder = builder
.with_audio_on(p1.audio_enabled, p2.audio_enabled)
.with_watch_rom(watch, watch);
ThreadBuilder::default() ThreadBuilder::default()
.name("Emulator".to_owned()) .name("Emulator".to_owned())
@ -136,6 +131,11 @@ fn main() -> Result<()> {
let event_loop = EventLoop::with_user_event().build().unwrap(); let event_loop = EventLoop::with_user_event().build().unwrap();
event_loop.set_control_flow(ControlFlow::Poll); event_loop.set_control_flow(ControlFlow::Poll);
let proxy = event_loop.create_proxy(); let proxy = event_loop.create_proxy();
event_loop.run_app(&mut Application::new(client, proxy, persistence, args))?; event_loop.run_app(&mut Application::new(
client,
proxy,
args.debug_port,
args.profile,
))?;
Ok(()) Ok(())
} }

View File

@ -50,11 +50,11 @@ impl MemoryClient {
} }
fn aligned_memory(start: u32, length: usize) -> BoxBytes { fn aligned_memory(start: u32, length: usize) -> BoxBytes {
if start.is_multiple_of(4) && length.is_multiple_of(4) { if start % 4 == 0 && length % 4 == 0 {
let memory = vec![0u32; length / 4].into_boxed_slice(); let memory = vec![0u32; length / 4].into_boxed_slice();
return bytemuck::box_bytes_of(memory); return bytemuck::box_bytes_of(memory);
} }
if start.is_multiple_of(2) && length.is_multiple_of(2) { if start % 2 == 0 && length % 2 == 0 {
let memory = vec![0u16; length / 2].into_boxed_slice(); let memory = vec![0u16; length / 2].into_boxed_slice();
return bytemuck::box_bytes_of(memory); return bytemuck::box_bytes_of(memory);
} }

View File

@ -60,11 +60,6 @@ impl ProgramState {
let Some(stack) = self.call_stacks.get_mut(code) else { let Some(stack) = self.call_stacks.get_mut(code) else {
bail!("missing stack {code:04x}"); bail!("missing stack {code:04x}");
}; };
// just popping the inline frames first
while stack
.pop_if(|f| matches!(f, StackFrame::Label(_)))
.is_some()
{}
if stack.pop().is_none() { if stack.pop().is_none() {
bail!("returned from {code:04x} but stack was empty"); bail!("returned from {code:04x} but stack was empty");
} }

View File

@ -3,7 +3,6 @@ use std::sync::Arc;
pub use about::AboutWindow; pub use about::AboutWindow;
use egui::{Context, ViewportBuilder, ViewportId}; use egui::{Context, ViewportBuilder, ViewportId};
pub use game::GameWindow; pub use game::GameWindow;
pub use game_screen::DisplayMode;
pub use gdb::GdbServerWindow; pub use gdb::GdbServerWindow;
pub use hotkeys::HotkeysWindow; pub use hotkeys::HotkeysWindow;
pub use input::InputWindow; pub use input::InputWindow;

View File

@ -5,7 +5,6 @@ use std::{
use crate::{ use crate::{
app::UserEvent, app::UserEvent,
config::{COLOR_PRESETS, SimConfig},
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState}, emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
input::{Command, ShortcutProvider}, input::{Command, ShortcutProvider},
persistence::Persistence, persistence::Persistence,
@ -17,6 +16,7 @@ use egui::{
ViewportBuilder, ViewportCommand, ViewportId, Window, ViewportBuilder, ViewportCommand, ViewportId, Window,
}; };
use egui_notify::{Anchor, Toast, Toasts}; use egui_notify::{Anchor, Toast, Toasts};
use serde::{Deserialize, Serialize};
use winit::event_loop::EventLoopProxy; use winit::event_loop::EventLoopProxy;
use super::{ use super::{
@ -25,13 +25,28 @@ use super::{
utils::UiExt as _, utils::UiExt as _,
}; };
const COLOR_PRESETS: [[Color32; 2]; 3] = [
[
Color32::from_rgb(0xff, 0x00, 0x00),
Color32::from_rgb(0x00, 0xc6, 0xf0),
],
[
Color32::from_rgb(0x00, 0xb4, 0x00),
Color32::from_rgb(0xc8, 0x00, 0xff),
],
[
Color32::from_rgb(0xb4, 0x9b, 0x00),
Color32::from_rgb(0x00, 0x00, 0xff),
],
];
pub struct GameWindow { pub struct GameWindow {
client: EmulatorClient, client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
persistence: Persistence, persistence: Persistence,
shortcuts: ShortcutProvider, shortcuts: ShortcutProvider,
sim_id: SimId, sim_id: SimId,
config: SimConfig, config: GameConfig,
toasts: Toasts, toasts: Toasts,
screen: Option<GameScreen>, screen: Option<GameScreen>,
messages: Option<mpsc::Receiver<Toast>>, messages: Option<mpsc::Receiver<Toast>>,
@ -47,7 +62,7 @@ impl GameWindow {
shortcuts: ShortcutProvider, shortcuts: ShortcutProvider,
sim_id: SimId, sim_id: SimId,
) -> Self { ) -> Self {
let config = SimConfig::load(&persistence, sim_id); let config = load_config(&persistence, sim_id);
let toasts = Toasts::new() let toasts = Toasts::new()
.with_anchor(Anchor::BottomLeft) .with_anchor(Anchor::BottomLeft)
.with_margin((10.0, 10.0).into()) .with_margin((10.0, 10.0).into())
@ -85,10 +100,6 @@ impl GameWindow {
.send_command(EmulatorCommand::LoadGame(self.sim_id, path)); .send_command(EmulatorCommand::LoadGame(self.sim_id, path));
} }
} }
Command::ReloadRom => {
self.client
.send_command(EmulatorCommand::ReloadRom(self.sim_id));
}
Command::Quit => { Command::Quit => {
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id)); let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
} }
@ -141,18 +152,6 @@ impl GameWindow {
.send_command(EmulatorCommand::LoadGame(self.sim_id, path)); .send_command(EmulatorCommand::LoadGame(self.sim_id, path));
} }
} }
if ui
.add(self.button_for(ui.ctx(), "Reload ROM", Command::ReloadRom))
.clicked()
{
self.client
.send_command(EmulatorCommand::ReloadRom(self.sim_id));
}
let watch_rom = self.client.is_rom_watched(self.sim_id);
if ui.selectable_button(watch_rom, "Watch ROM").clicked() {
self.client
.send_command(EmulatorCommand::WatchRom(self.sim_id, !watch_rom));
}
if ui if ui
.add(self.button_for(ui.ctx(), "Quit", Command::Quit)) .add(self.button_for(ui.ctx(), "Quit", Command::Quit))
.clicked() .clicked()
@ -411,15 +410,15 @@ impl GameWindow {
}); });
}); });
ui.menu_button("Audio", |ui| { ui.menu_button("Audio", |ui| {
if ui let p1_enabled = self.client.is_audio_enabled(SimId::Player1);
.selectable_button(self.config.audio_enabled, "Enabled") let p2_enabled = self.client.is_audio_enabled(SimId::Player2);
.clicked() if ui.selectable_button(p1_enabled, "Player 1").clicked() {
{ self.client
self.update_config(|c| c.audio_enabled = !c.audio_enabled); .send_command(EmulatorCommand::SetAudioEnabled(!p1_enabled, p2_enabled));
self.client.send_command(EmulatorCommand::SetAudioEnabled( }
self.sim_id, if ui.selectable_button(p2_enabled, "Player 2").clicked() {
self.config.audio_enabled, self.client
)); .send_command(EmulatorCommand::SetAudioEnabled(p1_enabled, !p2_enabled));
} }
}); });
ui.menu_button("Input", |ui| { ui.menu_button("Input", |ui| {
@ -461,11 +460,13 @@ impl GameWindow {
} }
} }
fn update_config(&mut self, update: impl FnOnce(&mut SimConfig)) { fn update_config(&mut self, update: impl FnOnce(&mut GameConfig)) {
let mut new_config = self.config.clone(); let mut new_config = self.config.clone();
update(&mut new_config); update(&mut new_config);
if self.config != new_config { if self.config != new_config {
let _ = new_config.save(&self.persistence, self.sim_id); let _ = self
.persistence
.save_config(config_filename(self.sim_id), &new_config);
} }
self.config = new_config; self.config = new_config;
} }
@ -479,6 +480,24 @@ impl GameWindow {
} }
} }
fn config_filename(sim_id: SimId) -> &'static str {
match sim_id {
SimId::Player1 => "config_p1",
SimId::Player2 => "config_p2",
}
}
fn load_config(persistence: &Persistence, sim_id: SimId) -> GameConfig {
if let Ok(config) = persistence.load_config(config_filename(sim_id)) {
return config;
}
GameConfig {
display_mode: DisplayMode::Anaglyph,
colors: COLOR_PRESETS[0],
dimensions: DisplayMode::Anaglyph.proportions() + Vec2::new(0.0, 22.0),
}
}
impl AppWindow for GameWindow { impl AppWindow for GameWindow {
fn viewport_id(&self) -> ViewportId { fn viewport_id(&self) -> ViewportId {
match self.sim_id { match self.sim_id {
@ -561,3 +580,10 @@ struct ColorPickerState {
just_opened: bool, just_opened: bool,
unpause_on_close: bool, unpause_on_close: bool,
} }
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
struct GameConfig {
display_mode: DisplayMode,
colors: [Color32; 2],
dimensions: Vec2,
}

View File

@ -274,6 +274,12 @@ impl AppWindow for CharacterDataWindow {
} }
} }
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
enum CharDataResource {
Character { palette: Palette, index: usize },
CharacterData { palette: Palette },
}
#[derive(Clone, Default, PartialEq, Eq)] #[derive(Clone, Default, PartialEq, Eq)]
struct CharDataParams { struct CharDataParams {
palette: Palette, palette: Palette,