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

View File

@ -16,7 +16,7 @@ RUN apt-get update && \
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
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
COPY llvm.sources /etc/apt/sources.list.d/llvm.sources
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"))
.opt_level(opt_level)
.flag_if_supported("-fno-strict-aliasing")
.define("_CRT_SECURE_NO_WARNINGS", None)
.define("VB_LITTLE_ENDIAN", None)
.define("VB_SIGNED_PROPAGATE", None)
.define("VB_DIV_GENERIC", None)

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

View File

@ -16,7 +16,6 @@ use winit::{
};
use crate::{
config::CliArgs,
controller::ControllerManager,
emulator::{EmulatorClient, EmulatorCommand, SimId},
images::ImageProcessor,
@ -56,24 +55,18 @@ pub struct Application {
focused: Option<ViewportId>,
init_debug_port: Option<u16>,
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 {
pub fn new(
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
args: CliArgs,
debug_port: Option<u16>,
profiling: bool,
) -> Self {
let wgpu = WgpuState::new();
let icon = load_icon().ok().map(Arc::new);
let persistence = Persistence::new();
let mappings = MappingProvider::new(persistence.clone());
let shortcuts = ShortcutProvider::new(persistence.clone());
let controllers = ControllerManager::new(client.clone(), &mappings);
@ -97,15 +90,8 @@ impl Application {
persistence,
viewports: HashMap::new(),
focused: None,
init_debug_port: args.debug_port,
init_profiling: args.profile,
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,
init_debug_port: debug_port,
init_profiling: profiling,
}
}
@ -138,40 +124,11 @@ impl ApplicationHandler<UserEvent> for Application {
SimId::Player1,
);
self.open(event_loop, Box::new(app));
let sim_id = SimId::Player1;
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();
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(
@ -287,8 +244,8 @@ impl ApplicationHandler<UserEvent> for Application {
self.open(event_loop, Box::new(world));
}
UserEvent::OpenFrameBuffers(sim_id) => {
let fb = FrameBufferWindow::new(sim_id, &self.memory, &mut self.images);
self.open(event_loop, Box::new(fb));
let world = FrameBufferWindow::new(sim_id, &self.memory, &mut self.images);
self.open(event_loop, Box::new(world));
}
UserEvent::OpenRegisters(sim_id) => {
let registers = RegisterWindow::new(sim_id, &self.memory);
@ -449,12 +406,13 @@ impl Viewport {
..egui_wgpu::WgpuConfiguration::default()
};
let options = egui_wgpu::RendererOptions::default();
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
ctx.clone(),
wgpu_config,
1,
None,
false,
options,
true,
));
let mut info = ViewportInfo::default();
@ -553,7 +511,7 @@ impl Viewport {
&mut self.info,
std::mem::take(&mut self.commands),
&self.window,
&mut vec![],
&mut HashSet::default(),
);
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::{
Arc, Weak,
atomic::{AtomicBool, Ordering},
mpsc::{self, RecvTimeoutError, TryRecvError},
mpsc::{self, RecvError, TryRecvError},
},
time::Duration,
};
@ -75,7 +75,6 @@ pub struct EmulatorBuilder {
audio_on: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
start_paused: bool,
watch_rom: Arc<[AtomicBool; 2]>,
}
impl EmulatorBuilder {
@ -92,13 +91,12 @@ impl EmulatorBuilder {
audio_on: Arc::new([AtomicBool::new(true), AtomicBool::new(true)]),
linked: Arc::new(AtomicBool::new(false)),
start_paused: false,
watch_rom: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]),
};
let client = EmulatorClient {
queue,
sim_state: builder.sim_state.clone(),
state: builder.state.clone(),
watch_rom: builder.watch_rom.clone(),
audio_on: builder.audio_on.clone(),
linked: builder.linked.clone(),
};
(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> {
let mut emulator = Emulator::new(
self.commands,
self.sim_state,
self.state,
self.audio_on,
self.watch_rom,
self.linked,
)?;
if let Some(path) = self.rom {
@ -157,7 +142,6 @@ pub struct Emulator {
sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>,
watch_rom: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
profilers: [Option<ProfileSender>; 2],
renderers: HashMap<SimId, TextureSink>,
@ -176,7 +160,6 @@ impl Emulator {
sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>,
watch_rom: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
) -> Result<Self> {
Ok(Self {
@ -187,7 +170,6 @@ impl Emulator {
sim_state,
state,
audio_on,
watch_rom,
linked,
profilers: [None, None],
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<()> {
let watch = self.watch_rom[sim_id.to_index()].load(Ordering::Acquire);
let cart = Cart::load(path, sim_id, watch)?;
self.try_reset_sim(sim_id, Some(cart))?;
let cart = Cart::load(path, sim_id)?;
self.reset_sim(sim_id, Some(cart))?;
Ok(())
}
@ -222,26 +195,16 @@ impl Emulator {
} else {
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 {
Some(rom_path) => Some(Cart::load(&rom_path, SimId::Player2, watch)?),
Some(rom_path) => Some(Cart::load(&rom_path, SimId::Player2)?),
None => None,
};
self.try_reset_sim(SimId::Player2, cart)?;
self.reset_sim(SimId::Player2, cart)?;
self.link_sims();
Ok(())
}
fn reset_sim(&mut self, sim_id: SimId, new_cart: Option<Cart>) -> bool {
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<()> {
fn reset_sim(&mut self, sim_id: SimId, new_cart: Option<Cart>) -> Result<()> {
self.save_sram(sim_id)?;
let index = sim_id.to_index();
@ -262,10 +225,6 @@ impl Emulator {
sim.load_cart(cart.rom.clone(), cart.sram.clone())?;
self.carts[index] = Some(cart);
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;
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<()> {
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) {
@ -458,33 +417,14 @@ impl Emulator {
let idle = self.tick();
if idle {
// 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.
loop {
match self.commands.recv_timeout(Duration::from_millis(250)) {
Ok(command) => {
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) => {
// Block the thread until a new command comes in.
match self.commands.recv() {
Ok(command) => self.handle_command(command),
Err(RecvError) => {
return;
}
}
}
}
loop {
match self.commands.try_recv() {
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| {
let Some(region) = region.upgrade() else {
return false;
@ -670,22 +602,6 @@ impl Emulator {
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) => {
if let Err(error) = self.start_second_sim(path) {
self.report_error(
@ -799,8 +715,9 @@ impl Emulator {
};
sim.watch_stdout(true);
}
EmulatorCommand::SetAudioEnabled(sim_id, enabled) => {
self.audio_on[sim_id.to_index()].store(enabled, Ordering::Release);
EmulatorCommand::SetAudioEnabled(p1, p2) => {
self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release);
self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release);
}
EmulatorCommand::Link => {
self.link_sims();
@ -809,7 +726,9 @@ impl Emulator {
self.unlink_sims();
}
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) => {
if let Some(sim) = self.sims.get_mut(sim_id.to_index()) {
@ -851,8 +770,6 @@ impl Emulator {
pub enum EmulatorCommand {
ConnectToSim(SimId, TextureSink, mpsc::Sender<Toast>),
LoadGame(SimId, PathBuf),
ReloadRom(SimId),
WatchRom(SimId, bool),
StartSecondSim(Option<PathBuf>),
StopSecondSim,
Pause,
@ -875,7 +792,7 @@ pub enum EmulatorCommand {
AddWatchpoint(SimId, u32, usize, VBWatchpointType),
RemoveWatchpoint(SimId, u32, usize, VBWatchpointType),
WatchStdout(SimId, mpsc::Sender<String>),
SetAudioEnabled(SimId, bool),
SetAudioEnabled(bool, bool),
Link,
Unlink,
Reset(SimId),
@ -941,8 +858,8 @@ pub struct EmulatorClient {
queue: mpsc::Sender<EmulatorCommand>,
sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
watch_rom: Arc<[AtomicBool; 2]>,
}
impl EmulatorClient {
@ -952,12 +869,12 @@ impl EmulatorClient {
pub fn emulator_state(&self) -> EmulatorState {
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 {
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 {
match self.queue.send(command) {
Ok(()) => true,

View File

@ -1,11 +1,10 @@
use anyhow::Result;
use notify::Watcher;
use rand::Rng;
use std::{
fs::{self, File},
io::{Read, Seek as _, SeekFrom, Write as _},
path::{Path, PathBuf},
sync::{Arc, atomic::AtomicBool},
sync::Arc,
};
use crate::emulator::{SimId, game_info::GameInfo, shrooms_vb_util::rom_from_isx};
@ -16,12 +15,10 @@ pub struct Cart {
sram_file: File,
pub sram: Vec<u8>,
pub info: Arc<GameInfo>,
watcher: Option<notify::RecommendedWatcher>,
changed: Arc<AtomicBool>,
}
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, info) = try_parse_isx(file_path, &rom)
.or_else(|| try_parse_elf(file_path, &rom))
@ -48,21 +45,12 @@ impl Cart {
sram
};
let changed = Arc::new(AtomicBool::new(false));
let watcher = if watch {
build_watcher(file_path, changed.clone())
} else {
None
};
Ok(Cart {
file_path: file_path.to_path_buf(),
rom,
sram_file,
sram,
info: Arc::new(info),
watcher,
changed,
})
}
@ -71,50 +59,6 @@ impl Cart {
self.sram_file.write_all(&self.sram)?;
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)> {

View File

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

View File

@ -1,7 +1,7 @@
// hide console in release mode
#![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 app::Application;
@ -12,15 +12,8 @@ use tracing::error;
use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt};
use winit::event_loop::{ControlFlow, EventLoop};
use crate::{
config::{CliArgs, SimConfig},
emulator::SimId,
persistence::Persistence,
};
mod app;
mod audio;
mod config;
mod controller;
mod emulator;
mod gdbserver;
@ -32,6 +25,18 @@ mod persistence;
mod profiler;
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() {
let directives = std::env::var("RUST_LOG").unwrap_or("error,lemur=info".into());
let filter = EnvFilter::builder().parse_lossy(directives);
@ -93,9 +98,7 @@ fn main() -> Result<()> {
#[cfg(windows)]
set_process_priority_to_high()?;
let args = CliArgs::parse();
let persistence = Persistence::new();
let args = Args::parse();
let (mut builder, client) = EmulatorBuilder::new();
if let Some(path) = &args.rom {
@ -110,14 +113,6 @@ fn main() -> Result<()> {
if args.profile {
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()
.name("Emulator".to_owned())
@ -136,6 +131,11 @@ fn main() -> Result<()> {
let event_loop = EventLoop::with_user_event().build().unwrap();
event_loop.set_control_flow(ControlFlow::Poll);
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(())
}

View File

@ -50,11 +50,11 @@ impl MemoryClient {
}
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();
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();
return bytemuck::box_bytes_of(memory);
}

View File

@ -60,11 +60,6 @@ impl ProgramState {
let Some(stack) = self.call_stacks.get_mut(code) else {
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() {
bail!("returned from {code:04x} but stack was empty");
}

View File

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

View File

@ -5,7 +5,6 @@ use std::{
use crate::{
app::UserEvent,
config::{COLOR_PRESETS, SimConfig},
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
input::{Command, ShortcutProvider},
persistence::Persistence,
@ -17,6 +16,7 @@ use egui::{
ViewportBuilder, ViewportCommand, ViewportId, Window,
};
use egui_notify::{Anchor, Toast, Toasts};
use serde::{Deserialize, Serialize};
use winit::event_loop::EventLoopProxy;
use super::{
@ -25,13 +25,28 @@ use super::{
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 {
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
shortcuts: ShortcutProvider,
sim_id: SimId,
config: SimConfig,
config: GameConfig,
toasts: Toasts,
screen: Option<GameScreen>,
messages: Option<mpsc::Receiver<Toast>>,
@ -47,7 +62,7 @@ impl GameWindow {
shortcuts: ShortcutProvider,
sim_id: SimId,
) -> Self {
let config = SimConfig::load(&persistence, sim_id);
let config = load_config(&persistence, sim_id);
let toasts = Toasts::new()
.with_anchor(Anchor::BottomLeft)
.with_margin((10.0, 10.0).into())
@ -85,10 +100,6 @@ impl GameWindow {
.send_command(EmulatorCommand::LoadGame(self.sim_id, path));
}
}
Command::ReloadRom => {
self.client
.send_command(EmulatorCommand::ReloadRom(self.sim_id));
}
Command::Quit => {
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
}
@ -141,18 +152,6 @@ impl GameWindow {
.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
.add(self.button_for(ui.ctx(), "Quit", Command::Quit))
.clicked()
@ -411,15 +410,15 @@ impl GameWindow {
});
});
ui.menu_button("Audio", |ui| {
if ui
.selectable_button(self.config.audio_enabled, "Enabled")
.clicked()
{
self.update_config(|c| c.audio_enabled = !c.audio_enabled);
self.client.send_command(EmulatorCommand::SetAudioEnabled(
self.sim_id,
self.config.audio_enabled,
));
let p1_enabled = self.client.is_audio_enabled(SimId::Player1);
let p2_enabled = self.client.is_audio_enabled(SimId::Player2);
if ui.selectable_button(p1_enabled, "Player 1").clicked() {
self.client
.send_command(EmulatorCommand::SetAudioEnabled(!p1_enabled, p2_enabled));
}
if ui.selectable_button(p2_enabled, "Player 2").clicked() {
self.client
.send_command(EmulatorCommand::SetAudioEnabled(p1_enabled, !p2_enabled));
}
});
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();
update(&mut 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;
}
@ -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 {
fn viewport_id(&self) -> ViewportId {
match self.sim_id {
@ -561,3 +580,10 @@ struct ColorPickerState {
just_opened: 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)]
struct CharDataParams {
palette: Palette,