Compare commits

...
22 Commits
Author SHA1 Message Date
SonicSwordcane db36332307 chore: Release lemur version 0.3.2 2025-02-15 16:56:20 -05:00
SonicSwordcane 8df05d923d Share one wgpu instance across all windows 2025-02-15 16:55:21 -05:00
SonicSwordcane 0086b9c9ce chore: Release lemur version 0.3.1 2025-02-14 16:57:10 -05:00
SonicSwordcane a87fce102d Avoid egui feature unsupported on wayland 2025-02-14 16:56:36 -05:00
SonicSwordcane 1cae0c8e54 chore: Release lemur version 0.3.0 2025-01-18 19:14:25 -05:00
SonicSwordcane 707dd3a200 Merge pull request 'Implement GDB/LLDB compatible server' (#3) from debugger into main
Reviewed-on: #3
2025-01-19 00:13:22 +00:00
SonicSwordcane 16a2aaee92 Fix status returned on interrupt 2025-01-18 19:10:55 -05:00
SonicSwordcane d59da1009a Fix watchpoint reporting 2025-01-18 18:51:25 -05:00
SonicSwordcane 9d37d22a22 Fix LTO for windows/linux, disable for OSX 2025-01-18 17:01:17 -05:00
SonicSwordcane e1a8006af7 Statically compile that callback too 2025-01-18 13:57:19 -05:00
SonicSwordcane 9f7895a457 Statically compile callbacks 2025-01-18 01:21:45 -05:00
SonicSwordcane 5ce39fb70e Implement writing memory 2025-01-18 01:03:22 -05:00
SonicSwordcane 0fff4d427f Implement writing registers 2025-01-18 00:20:57 -05:00
SonicSwordcane 17d3811124 No debugging unless the game is running 2025-01-17 22:00:46 -05:00
SonicSwordcane b79f35ac61 chore: Release lemur version 0.2.9 2025-01-17 10:49:48 -05:00
SonicSwordcane 89128b8207 Update shrooms-vb-core to fix drawing logic 2025-01-17 10:49:09 -05:00
SonicSwordcane b0404a5da2 chore: Release lemur version 0.2.8 2025-01-15 21:08:42 -05:00
SonicSwordcane 16cf54e5cf Update shrooms-vb-core to fix sign-extended hardware reads 2025-01-15 20:59:43 -05:00
SonicSwordcane f031bb17b2 Use logging 2025-01-14 23:33:30 -05:00
SonicSwordcane 102aff1580 Turn LTO on when cutting releases 2025-01-14 22:25:27 -05:00
SonicSwordcane f1c5571aa6 Tell LLDB to use big watch regions 2025-01-14 21:38:44 -05:00
SonicSwordcane af9a4ae8ee Support watchpoints 2025-01-13 00:30:47 -05:00
16 changed files with 1012 additions and 106 deletions
Generated
+107 -4
View File
@@ -1755,9 +1755,15 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "lemur" name = "lemur"
version = "0.2.7" version = "0.3.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"atoi", "atoi",
@@ -1788,6 +1794,8 @@ dependencies = [
"serde_json", "serde_json",
"thread-priority", "thread-priority",
"tokio", "tokio",
"tracing",
"tracing-subscriber",
"wgpu", "wgpu",
"windows 0.58.0", "windows 0.58.0",
"winit", "winit",
@@ -1883,6 +1891,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.4"
@@ -2068,6 +2085,16 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]] [[package]]
name = "num-complex" name = "num-complex"
version = "0.4.6" version = "0.4.6"
@@ -2408,6 +2435,12 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "owned_ttf_parser" name = "owned_ttf_parser"
version = "0.25.0" version = "0.25.0"
@@ -2701,8 +2734,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-automata", "regex-automata 0.4.9",
"regex-syntax", "regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
] ]
[[package]] [[package]]
@@ -2713,9 +2755,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-syntax", "regex-syntax 0.8.5",
] ]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.5" version = "0.8.5"
@@ -2906,6 +2954,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@@ -3123,6 +3180,16 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]] [[package]]
name = "tiny-skia" name = "tiny-skia"
version = "0.11.4" version = "0.11.4"
@@ -3248,6 +3315,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
] ]
[[package]] [[package]]
@@ -3358,6 +3455,12 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]] [[package]]
name = "vec_map" name = "vec_map"
version = "0.8.2" version = "0.8.2"
+3 -1
View File
@@ -4,7 +4,7 @@ description = "An emulator for the Virtual Boy."
repository = "https://git.virtual-boy.com/PVB/lemur" repository = "https://git.virtual-boy.com/PVB/lemur"
publish = false publish = false
license = "MIT" license = "MIT"
version = "0.2.7" version = "0.3.2"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@@ -36,6 +36,8 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
thread-priority = "1" thread-priority = "1"
tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync"] } tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync"] }
tracing = { version = "0.1", features = ["release_max_level_info"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
wgpu = "23" wgpu = "23"
winit = { version = "0.30", features = ["serde"] } winit = { version = "0.30", features = ["serde"] }
+1 -1
View File
@@ -5,7 +5,7 @@ A Virtual Boy emulator built around the shrooms-vb core. Written in Rust, using
## Setup ## Setup
Install the following dependencies: Install the following dependencies:
- `cargo` - `cargo` (via [rustup](https://rustup.rs/), the version from your package manager is too old)
Run Run
```sh ```sh
+4 -1
View File
@@ -23,6 +23,10 @@ ENV PATH="/osxcross/bin:$PATH" \
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" \
CXX_aarch64-apple-darwin="o6a4-clang++" \ CXX_aarch64-apple-darwin="o6a4-clang++" \
SHROOMS_CFLAGS_x86_64-unknown-linux-gnu="-flto" \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" \
SHROOMS_CFLAGS_x86_64-pc-windows-msvc="-flto" \
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-19" \ CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link-19" \
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-19" \ CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-19" \
@@ -30,5 +34,4 @@ ENV PATH="/osxcross/bin:$PATH" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-19" \ CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-19" \
CROSS_COMPILE="setting-this-to-silence-a-warning-" \ CROSS_COMPILE="setting-this-to-silence-a-warning-" \
RC_PATH="llvm-rc-19" \ RC_PATH="llvm-rc-19" \
RUSTFLAGS="-Lnative=/xwin/crt/lib/x86_64 -Lnative=/xwin/sdk/lib/um/x86_64 -Lnative=/xwin/sdk/lib/ucrt/x86_64" \
MACOSX_DEPLOYMENT_TARGET="14.5" MACOSX_DEPLOYMENT_TARGET="14.5"
+14 -2
View File
@@ -8,13 +8,25 @@ fn main() -> Result<(), Box<dyn Error>> {
} }
println!("cargo::rerun-if-changed=shrooms-vb-core"); println!("cargo::rerun-if-changed=shrooms-vb-core");
cc::Build::new()
let mut builder = cc::Build::new();
let _ = builder.try_flags_from_environment("SHROOMS_CFLAGS");
let opt_level = if builder.get_compiler().is_like_msvc() {
2
} else {
3
};
builder
.include(Path::new("shrooms-vb-core/core")) .include(Path::new("shrooms-vb-core/core"))
.opt_level(2) .opt_level(opt_level)
.flag_if_supported("-fno-strict-aliasing") .flag_if_supported("-fno-strict-aliasing")
.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)
.define("VB_DIRECT_EXECUTE", "on_execute")
.define("VB_DIRECT_FRAME", "on_frame")
.define("VB_DIRECT_READ", "on_read")
.define("VB_DIRECT_WRITE", "on_write")
.file(Path::new("shrooms-vb-core/core/vb.c")) .file(Path::new("shrooms-vb-core/core/vb.c"))
.compile("vb"); .compile("vb");
+73 -17
View File
@@ -6,6 +6,7 @@ use egui::{
ViewportCommand, ViewportId, ViewportInfo, ViewportCommand, ViewportId, ViewportInfo,
}; };
use gilrs::{EventType, Gilrs}; use gilrs::{EventType, Gilrs};
use tracing::{error, warn};
use winit::{ use winit::{
application::ApplicationHandler, application::ApplicationHandler,
event::WindowEvent, event::WindowEvent,
@@ -34,6 +35,7 @@ fn load_icon() -> anyhow::Result<IconData> {
pub struct Application { pub struct Application {
icon: Option<Arc<IconData>>, icon: Option<Arc<IconData>>,
wgpu: WgpuState,
client: EmulatorClient, client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
mappings: MappingProvider, mappings: MappingProvider,
@@ -50,6 +52,7 @@ impl Application {
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
debug_port: Option<u16>, debug_port: Option<u16>,
) -> Self { ) -> Self {
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 persistence = Persistence::new();
let mappings = MappingProvider::new(persistence.clone()); let mappings = MappingProvider::new(persistence.clone());
@@ -61,6 +64,7 @@ impl Application {
} }
Self { Self {
icon, icon,
wgpu,
client, client,
proxy, proxy,
mappings, mappings,
@@ -79,7 +83,7 @@ impl Application {
} }
self.viewports.insert( self.viewports.insert(
viewport_id, viewport_id,
Viewport::new(event_loop, self.icon.clone(), window), Viewport::new(event_loop, &self.wgpu, self.icon.clone(), window),
); );
} }
} }
@@ -230,13 +234,65 @@ impl ApplicationHandler<UserEvent> for Application {
fn exiting(&mut self, _event_loop: &ActiveEventLoop) { fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
let (sender, receiver) = oneshot::channel(); let (sender, receiver) = oneshot::channel();
if self.client.send_command(EmulatorCommand::Exit(sender)) { if self.client.send_command(EmulatorCommand::Exit(sender)) {
if let Err(err) = receiver.recv_timeout(Duration::from_secs(5)) { if let Err(error) = receiver.recv_timeout(Duration::from_secs(5)) {
eprintln!("could not gracefully exit: {}", err); error!(%error, "could not gracefully exit.");
} }
} }
} }
} }
struct WgpuState {
instance: Arc<wgpu::Instance>,
adapter: Arc<wgpu::Adapter>,
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
}
impl WgpuState {
fn new() -> Self {
let egui_wgpu::WgpuConfiguration {
wgpu_setup:
egui_wgpu::WgpuSetup::CreateNew {
#[allow(unused)]
supported_backends,
device_descriptor,
..
},
..
} = egui_wgpu::WgpuConfiguration::default()
else {
panic!("required fields not found")
};
#[cfg(windows)]
let supported_backends = wgpu::util::backend_bits_from_env()
.unwrap_or((wgpu::Backends::PRIMARY | wgpu::Backends::GL) - wgpu::Backends::VULKAN);
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: supported_backends,
..wgpu::InstanceDescriptor::default()
});
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: None,
force_fallback_adapter: false,
}))
.expect("could not create adapter");
let trace_path = std::env::var("WGPU_TRACE");
let (device, queue) = pollster::block_on(adapter.request_device(
&(*device_descriptor)(&adapter),
trace_path.ok().as_ref().map(std::path::Path::new),
))
.expect("could not request device");
Self {
instance: Arc::new(instance),
adapter: Arc::new(adapter),
device: Arc::new(device),
queue: Arc::new(queue),
}
}
}
struct Viewport { struct Viewport {
painter: egui_wgpu::winit::Painter, painter: egui_wgpu::winit::Painter,
ctx: Context, ctx: Context,
@@ -250,6 +306,7 @@ struct Viewport {
impl Viewport { impl Viewport {
pub fn new( pub fn new(
event_loop: &ActiveEventLoop, event_loop: &ActiveEventLoop,
wgpu: &WgpuState,
icon: Option<Arc<IconData>>, icon: Option<Arc<IconData>>,
mut app: Box<dyn AppWindow>, mut app: Box<dyn AppWindow>,
) -> Self { ) -> Self {
@@ -273,20 +330,16 @@ impl Viewport {
}); });
egui_extras::install_image_loaders(&ctx); egui_extras::install_image_loaders(&ctx);
#[allow(unused_mut)] let wgpu_config = egui_wgpu::WgpuConfiguration {
let mut wgpu_config = egui_wgpu::WgpuConfiguration {
present_mode: wgpu::PresentMode::AutoNoVsync, present_mode: wgpu::PresentMode::AutoNoVsync,
wgpu_setup: egui_wgpu::WgpuSetup::Existing {
instance: wgpu.instance.clone(),
adapter: wgpu.adapter.clone(),
device: wgpu.device.clone(),
queue: wgpu.queue.clone(),
},
..egui_wgpu::WgpuConfiguration::default() ..egui_wgpu::WgpuConfiguration::default()
}; };
#[cfg(windows)]
{
if let egui_wgpu::WgpuSetup::CreateNew {
supported_backends, ..
} = &mut wgpu_config.wgpu_setup
{
*supported_backends -= wgpu::Backends::VULKAN;
}
}
let mut painter = let mut painter =
egui_wgpu::winit::Painter::new(ctx.clone(), wgpu_config, 1, None, false, true); egui_wgpu::winit::Painter::new(ctx.clone(), wgpu_config, 1, None, false, true);
@@ -434,9 +487,12 @@ fn create_window_and_state(
} }
fn process_gamepad_input(mappings: MappingProvider, proxy: EventLoopProxy<UserEvent>) { fn process_gamepad_input(mappings: MappingProvider, proxy: EventLoopProxy<UserEvent>) {
let Ok(mut gilrs) = Gilrs::new() else { let mut gilrs = match Gilrs::new() {
eprintln!("could not connect gamepad listener"); Ok(gilrs) => gilrs,
return; Err(error) => {
warn!(%error, "could not connect gamepad listener");
return;
}
}; };
while let Some(event) = gilrs.next_event_blocking(None) { while let Some(event) = gilrs.next_event_blocking(None) {
if event.event == EventType::Connected { if event.event == EventType::Connected {
+2 -1
View File
@@ -4,6 +4,7 @@ use anyhow::{bail, Result};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use itertools::Itertools; use itertools::Itertools;
use rubato::{FftFixedInOut, Resampler}; use rubato::{FftFixedInOut, Resampler};
use tracing::error;
pub struct Audio { pub struct Audio {
#[allow(unused)] #[allow(unused)]
@@ -54,7 +55,7 @@ impl Audio {
} }
chunk.commit_all(); chunk.commit_all();
}, },
move |err| eprintln!("stream error: {err}"), move |error| error!(%error, "stream error"),
None, None,
)?; )?;
stream.play()?; stream.play()?;
+46 -6
View File
@@ -15,11 +15,13 @@ use anyhow::Result;
use atomic::Atomic; use atomic::Atomic;
use bytemuck::NoUninit; use bytemuck::NoUninit;
use egui_toast::{Toast, ToastKind, ToastOptions}; use egui_toast::{Toast, ToastKind, ToastOptions};
use tracing::{error, warn};
use crate::{audio::Audio, graphics::TextureSink}; use crate::{audio::Audio, graphics::TextureSink};
use shrooms_vb_core::{Sim, StopReason, EXPECTED_FRAME_SIZE}; use shrooms_vb_core::{Sim, StopReason, EXPECTED_FRAME_SIZE};
pub use shrooms_vb_core::{VBKey, VBRegister}; pub use shrooms_vb_core::{VBKey, VBRegister, VBWatchpointType};
mod address_set;
mod shrooms_vb_core; mod shrooms_vb_core;
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
@@ -303,9 +305,13 @@ impl Emulator {
} }
fn start_debugging(&mut self, sim_id: SimId, sender: DebugSender) { fn start_debugging(&mut self, sim_id: SimId, sender: DebugSender) {
if self.sim_state[sim_id.to_index()].load(Ordering::Acquire) != SimState::Ready {
// Can't debug unless a game is connected
return;
}
let debug = DebugInfo { let debug = DebugInfo {
sender, sender,
stop_reason: Some(DebugStopReason::Trapped), stop_reason: Some(DebugStopReason::Paused),
}; };
self.debuggers.insert(sim_id, debug); self.debuggers.insert(sim_id, debug);
self.state self.state
@@ -328,7 +334,7 @@ impl Emulator {
} }
fn debug_interrupt(&mut self, sim_id: SimId) { fn debug_interrupt(&mut self, sim_id: SimId) {
self.debug_stop(sim_id, DebugStopReason::Trapped); self.debug_stop(sim_id, DebugStopReason::Paused);
} }
fn debug_stop(&mut self, sim_id: SimId, reason: DebugStopReason) { fn debug_stop(&mut self, sim_id: SimId, reason: DebugStopReason) {
@@ -421,6 +427,9 @@ impl Emulator {
if let Some(reason) = sim.stop_reason() { if let Some(reason) = sim.stop_reason() {
let stop_reason = match reason { let stop_reason = match reason {
StopReason::Stepped => DebugStopReason::Trace, StopReason::Stepped => DebugStopReason::Trace,
StopReason::Watchpoint(watch, address) => {
DebugStopReason::Watchpoint(watch, address)
}
StopReason::Breakpoint => DebugStopReason::Breakpoint, StopReason::Breakpoint => DebugStopReason::Breakpoint,
}; };
self.debug_stop(sim_id, stop_reason); self.debug_stop(sim_id, stop_reason);
@@ -528,6 +537,12 @@ impl Emulator {
let value = sim.read_register(register); let value = sim.read_register(register);
let _ = done.send(value); let _ = done.send(value);
} }
EmulatorCommand::WriteRegister(sim_id, register, value) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.write_register(register, value);
}
EmulatorCommand::ReadMemory(sim_id, start, length, mut buffer, done) => { EmulatorCommand::ReadMemory(sim_id, start, length, mut buffer, done) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return; return;
@@ -535,6 +550,13 @@ impl Emulator {
sim.read_memory(start, length, &mut buffer); sim.read_memory(start, length, &mut buffer);
let _ = done.send(buffer); let _ = done.send(buffer);
} }
EmulatorCommand::WriteMemory(sim_id, start, buffer, done) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.write_memory(start, &buffer);
let _ = done.send(buffer);
}
EmulatorCommand::AddBreakpoint(sim_id, address) => { EmulatorCommand::AddBreakpoint(sim_id, address) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return; return;
@@ -547,6 +569,18 @@ impl Emulator {
}; };
sim.remove_breakpoint(address); sim.remove_breakpoint(address);
} }
EmulatorCommand::AddWatchpoint(sim_id, address, length, watch) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.add_watchpoint(address, length, watch);
}
EmulatorCommand::RemoveWatchpoint(sim_id, address, length, watch) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.remove_watchpoint(address, length, watch);
}
EmulatorCommand::SetAudioEnabled(p1, p2) => { EmulatorCommand::SetAudioEnabled(p1, p2) => {
self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release); self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release);
self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release); self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release);
@@ -592,7 +626,7 @@ impl Emulator {
return; return;
} }
} }
eprintln!("{}", message); error!("{}", message);
} }
} }
@@ -610,9 +644,13 @@ pub enum EmulatorCommand {
DebugContinue(SimId), DebugContinue(SimId),
DebugStep(SimId), DebugStep(SimId),
ReadRegister(SimId, VBRegister, oneshot::Sender<u32>), ReadRegister(SimId, VBRegister, oneshot::Sender<u32>),
WriteRegister(SimId, VBRegister, u32),
ReadMemory(SimId, u32, usize, Vec<u8>, oneshot::Sender<Vec<u8>>), ReadMemory(SimId, u32, usize, Vec<u8>, oneshot::Sender<Vec<u8>>),
WriteMemory(SimId, u32, Vec<u8>, oneshot::Sender<Vec<u8>>),
AddBreakpoint(SimId, u32), AddBreakpoint(SimId, u32),
RemoveBreakpoint(SimId, u32), RemoveBreakpoint(SimId, u32),
AddWatchpoint(SimId, u32, usize, VBWatchpointType),
RemoveWatchpoint(SimId, u32, usize, VBWatchpointType),
SetAudioEnabled(bool, bool), SetAudioEnabled(bool, bool),
Link, Link,
Unlink, Unlink,
@@ -645,8 +683,10 @@ pub enum DebugStopReason {
Trace, Trace,
// We hit a breakpoint // We hit a breakpoint
Breakpoint, Breakpoint,
// We hit a watchpoint
Watchpoint(VBWatchpointType, u32),
// The debugger told us to pause // The debugger told us to pause
Trapped, Paused,
} }
struct DebugInfo { struct DebugInfo {
@@ -684,7 +724,7 @@ impl EmulatorClient {
match self.queue.send(command) { match self.queue.send(command) {
Ok(()) => true, Ok(()) => true,
Err(err) => { Err(err) => {
eprintln!( warn!(
"could not send command {:?} as emulator is shut down", "could not send command {:?} as emulator is shut down",
err.0 err.0
); );
+270
View File
@@ -0,0 +1,270 @@
use std::{
collections::{BTreeMap, BTreeSet},
ops::Bound,
};
#[derive(Debug, Default)]
pub struct AddressSet {
ranges: BTreeSet<(u32, usize)>,
bounds: BTreeMap<u32, usize>,
}
impl AddressSet {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, address: u32, length: usize) {
if length == 0 || !self.ranges.insert((address, length)) {
return;
}
let end = (address as usize)
.checked_add(length)
.and_then(|e| u32::try_from(e).ok());
if let Some(end) = end {
let val_before = self.bounds.range(..=end).next_back().map_or(0, |(_, &v)| v);
self.bounds.insert(end, val_before);
}
let val_before = self
.bounds
.range(..address)
.next_back()
.map_or(0, |(_, &v)| v);
if let Some(&val_at) = self.bounds.get(&address) {
if val_before == val_at + 1 {
self.bounds.remove(&address);
}
} else {
self.bounds.insert(address, val_before);
}
let start_bound = Bound::Included(address);
let end_bound = match end {
Some(e) => Bound::Excluded(e),
None => Bound::Unbounded,
};
for (_, val) in self.bounds.range_mut((start_bound, end_bound)) {
*val += 1;
}
}
pub fn remove(&mut self, address: u32, length: usize) {
if !self.ranges.remove(&(address, length)) {
return;
}
let end = (address as usize)
.checked_add(length)
.and_then(|e| u32::try_from(e).ok());
if let Some(end) = end {
let val_before = self.bounds.range(..end).next_back().map_or(0, |(_, &v)| v);
if let Some(&val_at) = self.bounds.get(&end) {
if val_at + 1 == val_before {
self.bounds.remove(&end);
}
} else {
self.bounds.insert(end, val_before);
}
}
let val_before = self
.bounds
.range(..address)
.next_back()
.map_or(0, |(_, &v)| v);
if let Some(&val_at) = self.bounds.get(&address) {
if val_before + 1 == val_at {
self.bounds.remove(&address);
}
} else {
self.bounds.insert(address, val_before);
}
let start_bound = Bound::Included(address);
let end_bound = match end {
Some(e) => Bound::Excluded(e),
None => Bound::Unbounded,
};
for (_, val) in self.bounds.range_mut((start_bound, end_bound)) {
*val -= 1;
}
}
pub fn clear(&mut self) {
self.ranges.clear();
self.bounds.clear();
}
pub fn is_empty(&self) -> bool {
self.bounds.is_empty()
}
pub fn contains(&self, address: u32) -> bool {
self.bounds
.range(..=address)
.next_back()
.is_some_and(|(_, &val)| val > 0)
}
pub fn start_of_range_containing(&self, address: u32) -> Option<u32> {
if !self.contains(address) {
return None;
}
self.ranges
.range(..=(address, usize::MAX))
.rev()
.find_map(|&(start, length)| {
let contains = start <= address
&& (start as usize)
.checked_add(length)
.is_none_or(|end| end > address as usize);
contains.then_some(start)
})
}
}
#[cfg(test)]
mod tests {
use super::AddressSet;
#[test]
fn should_not_include_addresses_when_empty() {
let set = AddressSet::new();
assert!(set.is_empty());
assert!(!set.contains(0x13374200));
assert_eq!(set.start_of_range_containing(0x13374200), None);
}
#[test]
fn should_include_addresses_when_full() {
let mut set = AddressSet::new();
set.add(0x00000000, 0x100000000);
assert!(set.contains(0x13374200));
assert_eq!(set.start_of_range_containing(0x13374200), Some(0x00000000));
}
#[test]
fn should_ignore_empty_address_ranges() {
let mut set = AddressSet::new();
set.add(0x13374200, 0);
assert!(set.is_empty());
assert!(!set.contains(0x13374200));
}
#[test]
fn should_add_addresses_idempotently() {
let mut set = AddressSet::new();
set.add(0x13374200, 1);
set.add(0x13374200, 1);
set.remove(0x13374200, 1);
assert!(set.is_empty());
assert!(!set.contains(0x13374200));
}
#[test]
fn should_remove_addresses_idempotently() {
let mut set = AddressSet::new();
set.add(0x13374200, 1);
set.remove(0x13374200, 1);
set.remove(0x13374200, 1);
assert!(set.is_empty());
assert!(!set.contains(0x13374200));
}
#[test]
fn should_report_address_in_range() {
let mut set = AddressSet::new();
set.add(0x13374200, 4);
assert!(!set.contains(0x133741ff));
for address in 0x13374200..0x13374204 {
assert!(set.contains(address));
}
assert!(!set.contains(0x13374204));
}
#[test]
fn should_allow_overlapping_addresses() {
let mut set = AddressSet::new();
set.add(0x13374200, 4);
set.add(0x13374201, 1);
set.add(0x13374202, 2);
assert!(!set.contains(0x133741ff));
for address in 0x13374200..0x13374204 {
assert!(set.contains(address));
}
assert!(!set.contains(0x13374204));
set.remove(0x13374200, 4);
assert!(!set.contains(0x13374200));
for address in 0x13374201..0x13374204 {
assert!(set.contains(address));
}
assert!(!set.contains(0x13374204));
}
#[test]
fn should_allow_removing_overlapped_address_ranges() {
let mut set = AddressSet::new();
set.add(0x13374200, 8);
set.add(0x13374204, 8);
set.remove(0x13374204, 8);
for address in 0x13374200..0x13374208 {
assert!(set.contains(address));
}
for address in 0x13374208..0x1337420c {
assert!(!set.contains(address));
}
}
#[test]
fn should_merge_adjacent_ranges() {
let mut set = AddressSet::new();
set.add(0x13374200, 4);
set.add(0x13374204, 4);
set.add(0x13374208, 4);
set.add(0x1337420c, 4);
assert!(!set.contains(0x133741ff));
for address in 0x13374200..0x13374210 {
assert!(set.contains(address));
}
assert!(!set.contains(0x13374210));
set.remove(0x13374200, 4);
set.remove(0x13374204, 4);
set.remove(0x13374208, 4);
set.remove(0x1337420c, 4);
assert!(set.is_empty());
for address in 0x133741ff..=0x13374210 {
assert!(!set.contains(address));
}
}
#[test]
fn should_find_start_of_range() {
let mut set = AddressSet::new();
set.add(0x13374200, 4);
assert_eq!(set.start_of_range_containing(0x133741ff), None);
for address in 0x13374200..0x13374204 {
assert_eq!(set.start_of_range_containing(address), Some(0x13374200));
}
assert_eq!(set.start_of_range_containing(0x13374204), None);
}
#[test]
fn should_ignore_ranges_not_containing_address() {
let mut set = AddressSet::new();
set.add(0x10000000, 1024);
set.add(0x30000000, 1024);
assert!(!set.contains(0x20000000));
assert_eq!(set.start_of_range_containing(0x20000000), None);
}
}
+213 -14
View File
@@ -5,6 +5,8 @@ use bitflags::bitflags;
use num_derive::{FromPrimitive, ToPrimitive}; use num_derive::{FromPrimitive, ToPrimitive};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::address_set::AddressSet;
#[repr(C)] #[repr(C)]
struct VB { struct VB {
_data: [u8; 0], _data: [u8; 0],
@@ -62,9 +64,31 @@ pub enum VBRegister {
PC, PC,
} }
type OnFrame = extern "C" fn(sim: *mut VB) -> c_int; #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VBWatchpointType {
Read,
Write,
Access,
}
type OnExecute = type OnExecute =
extern "C" fn(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> c_int; extern "C" fn(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> c_int;
type OnFrame = extern "C" fn(sim: *mut VB) -> c_int;
type OnRead = extern "C" fn(
sim: *mut VB,
address: u32,
type_: VBDataType,
value: *mut i32,
cycles: *mut u32,
) -> c_int;
type OnWrite = extern "C" fn(
sim: *mut VB,
address: u32,
type_: VBDataType,
value: *mut i32,
cycles: *mut u32,
cancel: *mut c_int,
) -> c_int;
#[link(name = "vb")] #[link(name = "vb")]
extern "C" { extern "C" {
@@ -112,15 +136,21 @@ extern "C" {
#[link_name = "vbSetCartROM"] #[link_name = "vbSetCartROM"]
fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int; fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int;
#[link_name = "vbSetExecuteCallback"] #[link_name = "vbSetExecuteCallback"]
fn vb_set_execute_callback(sim: *mut VB, callback: Option<OnExecute>); fn vb_set_execute_callback(sim: *mut VB, callback: Option<OnExecute>) -> Option<OnExecute>;
#[link_name = "vbSetFrameCallback"] #[link_name = "vbSetFrameCallback"]
fn vb_set_frame_callback(sim: *mut VB, callback: Option<OnFrame>); fn vb_set_frame_callback(sim: *mut VB, callback: Option<OnFrame>) -> Option<OnFrame>;
#[link_name = "vbSetKeys"] #[link_name = "vbSetKeys"]
fn vb_set_keys(sim: *mut VB, keys: u16) -> u16; fn vb_set_keys(sim: *mut VB, keys: u16) -> u16;
#[link_name = "vbSetOption"] #[link_name = "vbSetOption"]
fn vb_set_option(sim: *mut VB, key: VBOption, value: c_int); fn vb_set_option(sim: *mut VB, key: VBOption, value: c_int);
#[link_name = "vbSetPeer"] #[link_name = "vbSetPeer"]
fn vb_set_peer(sim: *mut VB, peer: *mut VB); fn vb_set_peer(sim: *mut VB, peer: *mut VB);
#[link_name = "vbSetProgramCounter"]
fn vb_set_program_counter(sim: *mut VB, value: u32) -> u32;
#[link_name = "vbSetProgramRegister"]
fn vb_set_program_register(sim: *mut VB, index: c_uint, value: i32) -> i32;
#[link_name = "vbSetReadCallback"]
fn vb_set_read_callback(sim: *mut VB, callback: Option<OnRead>) -> Option<OnRead>;
#[link_name = "vbSetSamples"] #[link_name = "vbSetSamples"]
fn vb_set_samples( fn vb_set_samples(
sim: *mut VB, sim: *mut VB,
@@ -128,12 +158,19 @@ extern "C" {
typ_: VBDataType, typ_: VBDataType,
capacity: c_uint, capacity: c_uint,
) -> c_int; ) -> c_int;
#[link_name = "vbSetSystemRegister"]
fn vb_set_system_register(sim: *mut VB, index: c_uint, value: u32) -> u32;
#[link_name = "vbSetUserData"] #[link_name = "vbSetUserData"]
fn vb_set_user_data(sim: *mut VB, tag: *mut c_void); fn vb_set_user_data(sim: *mut VB, tag: *mut c_void);
#[link_name = "vbSetWriteCallback"]
fn vb_set_write_callback(sim: *mut VB, callback: Option<OnWrite>) -> Option<OnWrite>;
#[link_name = "vbSizeOf"] #[link_name = "vbSizeOf"]
fn vb_size_of() -> usize; fn vb_size_of() -> usize;
#[link_name = "vbWrite"]
fn vb_write(sim: *mut VB, address: u32, _type: VBDataType, value: i32) -> i32;
} }
#[no_mangle]
extern "C" fn on_frame(sim: *mut VB) -> c_int { extern "C" fn on_frame(sim: *mut VB) -> c_int {
// SAFETY: the *mut VB owns its userdata. // SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid. // There is no way for the userdata to be null or otherwise invalid.
@@ -142,23 +179,81 @@ extern "C" fn on_frame(sim: *mut VB) -> c_int {
1 1
} }
#[no_mangle]
extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: c_int) -> c_int { extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: c_int) -> c_int {
// SAFETY: the *mut VB owns its userdata. // SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid. // There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() }; let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
let mut stopped = 0; let mut stopped = data.stop_reason.is_some();
if data.step_from.is_some_and(|s| s != address) { if data.step_from.is_some_and(|s| s != address) {
data.step_from = None; data.step_from = None;
data.stop_reason = Some(StopReason::Stepped); data.stop_reason = Some(StopReason::Stepped);
stopped = 1; stopped = true;
} }
if data.breakpoints.binary_search(&address).is_ok() { if data.breakpoints.binary_search(&address).is_ok() {
data.stop_reason = Some(StopReason::Breakpoint); data.stop_reason = Some(StopReason::Breakpoint);
stopped = 1; stopped = true;
} }
stopped if stopped {
1
} else {
0
}
}
#[no_mangle]
extern "C" fn on_read(
sim: *mut VB,
address: u32,
_type: VBDataType,
_value: *mut i32,
_cycles: *mut u32,
) -> c_int {
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
if let Some(start) = data.read_watchpoints.start_of_range_containing(address) {
let watch = if data.write_watchpoints.contains(address) {
VBWatchpointType::Access
} else {
VBWatchpointType::Read
};
data.stop_reason = Some(StopReason::Watchpoint(watch, start));
}
// Don't stop here, the debugger expects us to break after the memory access.
// We'll stop in on_execute instead.
0
}
#[no_mangle]
extern "C" fn on_write(
sim: *mut VB,
address: u32,
_type: VBDataType,
_value: *mut i32,
_cycles: *mut u32,
_cancel: *mut c_int,
) -> c_int {
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
if let Some(start) = data.write_watchpoints.start_of_range_containing(address) {
let watch = if data.read_watchpoints.contains(address) {
VBWatchpointType::Access
} else {
VBWatchpointType::Write
};
data.stop_reason = Some(StopReason::Watchpoint(watch, start));
}
// Don't stop here, the debugger expects us to break after the memory access.
// We'll stop in on_execute instead.
0
} }
const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4; const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4;
@@ -170,6 +265,23 @@ struct VBState {
stop_reason: Option<StopReason>, stop_reason: Option<StopReason>,
step_from: Option<u32>, step_from: Option<u32>,
breakpoints: Vec<u32>, breakpoints: Vec<u32>,
read_watchpoints: AddressSet,
write_watchpoints: AddressSet,
}
impl VBState {
fn needs_execute_callback(&self) -> bool {
self.step_from.is_some()
|| !self.breakpoints.is_empty()
|| !self.read_watchpoints.is_empty()
|| !self.write_watchpoints.is_empty()
}
}
pub enum StopReason {
Breakpoint,
Watchpoint(VBWatchpointType, u32),
Stepped,
} }
#[repr(transparent)] #[repr(transparent)]
@@ -177,11 +289,6 @@ pub struct Sim {
sim: *mut VB, sim: *mut VB,
} }
pub enum StopReason {
Breakpoint,
Stepped,
}
impl Sim { impl Sim {
pub fn new() -> Self { pub fn new() -> Self {
// init the VB instance itself // init the VB instance itself
@@ -201,6 +308,8 @@ impl Sim {
stop_reason: None, stop_reason: None,
step_from: None, step_from: None,
breakpoints: vec![], breakpoints: vec![],
read_watchpoints: AddressSet::new(),
write_watchpoints: AddressSet::new(),
}; };
unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) }; unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) };
unsafe { vb_set_frame_callback(sim, Some(on_frame)) }; unsafe { vb_set_frame_callback(sim, Some(on_frame)) };
@@ -350,6 +459,20 @@ impl Sim {
} }
} }
pub fn write_register(&mut self, register: VBRegister, value: u32) {
match register {
VBRegister::Program(index) => unsafe {
vb_set_program_register(self.sim, index, value as i32);
},
VBRegister::System(index) => unsafe {
vb_set_system_register(self.sim, index, value);
},
VBRegister::PC => unsafe {
vb_set_program_counter(self.sim, value);
},
}
}
pub fn read_memory(&mut self, start: u32, length: usize, into: &mut Vec<u8>) { pub fn read_memory(&mut self, start: u32, length: usize, into: &mut Vec<u8>) {
let mut address = start; let mut address = start;
for _ in 0..length { for _ in 0..length {
@@ -359,6 +482,14 @@ impl Sim {
} }
} }
pub fn write_memory(&mut self, start: u32, buffer: &[u8]) {
let mut address = start;
for byte in buffer {
unsafe { vb_write(self.sim, address, VBDataType::U8, *byte as i32) };
address = address.wrapping_add(1);
}
}
pub fn add_breakpoint(&mut self, address: u32) { pub fn add_breakpoint(&mut self, address: u32) {
let data = self.get_state(); let data = self.get_state();
if let Err(index) = data.breakpoints.binary_search(&address) { if let Err(index) = data.breakpoints.binary_search(&address) {
@@ -374,7 +505,71 @@ impl Sim {
let data = self.get_state(); let data = self.get_state();
if let Ok(index) = data.breakpoints.binary_search(&address) { if let Ok(index) = data.breakpoints.binary_search(&address) {
data.breakpoints.remove(index); data.breakpoints.remove(index);
if data.step_from.is_none() && data.breakpoints.is_empty() { if !data.needs_execute_callback() {
unsafe { vb_set_execute_callback(self.sim, None) };
}
}
}
pub fn add_watchpoint(&mut self, address: u32, length: usize, watch: VBWatchpointType) {
match watch {
VBWatchpointType::Read => self.add_read_watchpoint(address, length),
VBWatchpointType::Write => self.add_write_watchpoint(address, length),
VBWatchpointType::Access => {
self.add_read_watchpoint(address, length);
self.add_write_watchpoint(address, length);
}
}
}
fn add_read_watchpoint(&mut self, address: u32, length: usize) {
let state = self.get_state();
state.read_watchpoints.add(address, length);
if !state.read_watchpoints.is_empty() {
unsafe { vb_set_read_callback(self.sim, Some(on_read)) };
unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) };
}
}
fn add_write_watchpoint(&mut self, address: u32, length: usize) {
let state = self.get_state();
state.write_watchpoints.add(address, length);
if !state.write_watchpoints.is_empty() {
unsafe { vb_set_write_callback(self.sim, Some(on_write)) };
unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) };
}
}
pub fn remove_watchpoint(&mut self, address: u32, length: usize, watch: VBWatchpointType) {
match watch {
VBWatchpointType::Read => self.remove_read_watchpoint(address, length),
VBWatchpointType::Write => self.remove_write_watchpoint(address, length),
VBWatchpointType::Access => {
self.remove_read_watchpoint(address, length);
self.remove_write_watchpoint(address, length);
}
}
}
fn remove_read_watchpoint(&mut self, address: u32, length: usize) {
let state = self.get_state();
state.read_watchpoints.remove(address, length);
let needs_execute = state.needs_execute_callback();
if state.read_watchpoints.is_empty() {
unsafe { vb_set_read_callback(self.sim, None) };
if !needs_execute {
unsafe { vb_set_execute_callback(self.sim, None) };
}
}
}
fn remove_write_watchpoint(&mut self, address: u32, length: usize) {
let state = self.get_state();
state.write_watchpoints.remove(address, length);
let needs_execute = state.needs_execute_callback();
if state.write_watchpoints.is_empty() {
unsafe { vb_set_write_callback(self.sim, None) };
if !needs_execute {
unsafe { vb_set_execute_callback(self.sim, None) }; unsafe { vb_set_execute_callback(self.sim, None) };
} }
} }
@@ -393,13 +588,17 @@ impl Sim {
let data = self.get_state(); let data = self.get_state();
data.step_from = None; data.step_from = None;
data.breakpoints.clear(); data.breakpoints.clear();
data.read_watchpoints.clear();
data.write_watchpoints.clear();
unsafe { vb_set_read_callback(self.sim, None) };
unsafe { vb_set_write_callback(self.sim, None) };
unsafe { vb_set_execute_callback(self.sim, None) }; unsafe { vb_set_execute_callback(self.sim, None) };
} }
pub fn stop_reason(&mut self) -> Option<StopReason> { pub fn stop_reason(&mut self) -> Option<StopReason> {
let data = self.get_state(); let data = self.get_state();
let reason = data.stop_reason.take(); let reason = data.stop_reason.take();
if data.step_from.is_none() && data.breakpoints.is_empty() { if !data.needs_execute_callback() {
unsafe { vb_set_execute_callback(self.sim, None) }; unsafe { vb_set_execute_callback(self.sim, None) };
} }
reason reason
+227 -44
View File
@@ -9,11 +9,14 @@ use std::{
use tokio::{ use tokio::{
io::{AsyncWriteExt as _, BufReader}, io::{AsyncWriteExt as _, BufReader},
net::{TcpListener, TcpStream}, net::{TcpListener, TcpStream},
select, pin, select,
sync::{mpsc, oneshot}, sync::{mpsc, oneshot},
}; };
use tracing::{debug, enabled, error, info, Level};
use crate::emulator::{DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId}; use crate::emulator::{
DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId, VBWatchpointType,
};
mod registers; mod registers;
mod request; mod request;
@@ -54,8 +57,9 @@ impl GdbServer {
.unwrap() .unwrap()
.block_on(async move { .block_on(async move {
select! { select! {
_ = run_server(sim_id, client, port, &status) => {} _ = run_server(sim_id, client.clone(), port, &status) => {}
_ = rx => { _ = rx => {
client.send_command(EmulatorCommand::StopDebugging(sim_id));
*status.lock().unwrap() = GdbServerStatus::Stopped; *status.lock().unwrap() = GdbServerStatus::Stopped;
} }
} }
@@ -70,21 +74,52 @@ impl GdbServer {
} }
} }
impl Drop for GdbServer {
fn drop(&mut self) {
self.stop();
}
}
async fn run_server( async fn run_server(
sim_id: SimId, sim_id: SimId,
client: EmulatorClient, client: EmulatorClient,
port: u16, port: u16,
status: &Mutex<GdbServerStatus>, status: &Mutex<GdbServerStatus>,
) { ) {
let Some(stream) = try_connect(port, status).await else { let (debug_sink, mut debug_source) = mpsc::unbounded_channel();
return; client.send_command(EmulatorCommand::StartDebugging(sim_id, debug_sink));
info!("Connecting to debugger on port {port}...");
let connect_future = try_connect(port, status);
pin!(connect_future);
let stream = loop {
select! {
stream = &mut connect_future => {
if let Some(stream) = stream {
break stream;
} else {
return;
}
}
event = debug_source.recv() => {
if event.is_none() {
// The sim has stopped (or was never started)
*status.lock().unwrap() = GdbServerStatus::Stopped;
return;
}
}
}
}; };
info!("Connected!");
let mut connection = GdbConnection::new(sim_id, client); let mut connection = GdbConnection::new(sim_id, client);
match connection.run(stream).await { match connection.run(stream, debug_source).await {
Ok(()) => { Ok(()) => {
info!("Finished debugging.");
*status.lock().unwrap() = GdbServerStatus::Stopped; *status.lock().unwrap() = GdbServerStatus::Stopped;
} }
Err(error) => { Err(error) => {
error!(%error, "Error from debugger.");
*status.lock().unwrap() = GdbServerStatus::Error(error.to_string()); *status.lock().unwrap() = GdbServerStatus::Error(error.to_string());
} }
} }
@@ -95,6 +130,7 @@ async fn try_connect(port: u16, status: &Mutex<GdbServerStatus>) -> Option<TcpSt
let listener = match TcpListener::bind(("127.0.0.1", port)).await { let listener = match TcpListener::bind(("127.0.0.1", port)).await {
Ok(l) => l, Ok(l) => l,
Err(err) => { Err(err) => {
error!(%err, "Could not open port.");
*status.lock().unwrap() = GdbServerStatus::Error(err.to_string()); *status.lock().unwrap() = GdbServerStatus::Error(err.to_string());
return None; return None;
} }
@@ -105,6 +141,7 @@ async fn try_connect(port: u16, status: &Mutex<GdbServerStatus>) -> Option<TcpSt
Some(stream) Some(stream)
} }
Err(err) => { Err(err) => {
error!(%err, "Could not connect to debugger.");
*status.lock().unwrap() = GdbServerStatus::Error(err.to_string()); *status.lock().unwrap() = GdbServerStatus::Error(err.to_string());
None None
} }
@@ -145,12 +182,13 @@ impl GdbConnection {
memory_buf: None, memory_buf: None,
} }
} }
async fn run(&mut self, stream: TcpStream) -> Result<()> { async fn run(
let (debug_sink, mut debug_source) = mpsc::unbounded_channel(); &mut self,
stream: TcpStream,
mut debug_source: mpsc::UnboundedReceiver<DebugEvent>,
) -> Result<()> {
let (rx, mut tx) = stream.into_split(); let (rx, mut tx) = stream.into_split();
let mut request_source = RequestSource::new(BufReader::new(rx)); let mut request_source = RequestSource::new(BufReader::new(rx));
self.client
.send_command(EmulatorCommand::StartDebugging(self.sim_id, debug_sink));
loop { loop {
let response = select! { let response = select! {
maybe_event = debug_source.recv() => { maybe_event = debug_source.recv() => {
@@ -167,9 +205,11 @@ impl GdbConnection {
}; };
if let Some(res) = response { if let Some(res) = response {
let buffer = res.finish(); let buffer = res.finish();
match std::str::from_utf8(&buffer) { if enabled!(Level::DEBUG) {
Ok(text) => println!("response: {text}"), match std::str::from_utf8(&buffer) {
Err(_) => println!("response: {buffer:02x?}"), Ok(text) => debug!("response: {text}"),
Err(_) => debug!("response: {buffer:02x?}"),
}
} }
tx.write_all(&buffer).await?; tx.write_all(&buffer).await?;
self.response_buf = Some(buffer); self.response_buf = Some(buffer);
@@ -187,14 +227,14 @@ impl GdbConnection {
} }
self.stop_reason = Some(reason); self.stop_reason = Some(reason);
self.response() self.response()
.write_str(debug_stop_reason_string(self.stop_reason)) .write_str(&debug_stop_reason_string(self.stop_reason))
} }
}; };
Some(res) Some(res)
} }
fn handle_request(&mut self, mut req: Request<'_>) -> Result<Option<Response>> { fn handle_request(&mut self, mut req: Request<'_>) -> Result<Option<Response>> {
println!("received {:02x?}", req); debug!("received {:02x?}", req);
if req.kind == RequestKind::Signal { if req.kind == RequestKind::Signal {
self.client self.client
@@ -208,7 +248,7 @@ impl GdbConnection {
res res
} else if req.match_str("qSupported:") { } else if req.match_str("qSupported:") {
self.response() self.response()
.write_str("multiprocess+;swbreak+;vContSupported+;PacketSize=10000") .write_str("multiprocess+;swbreak+;vContSupported+;PacketSize=10000;SupportedWatchpointTypes=x86_64,aarch64-bas,aarch64-mask")
} else if req } else if req
.match_some_str([ .match_some_str([
"QThreadSuffixSupported", "QThreadSuffixSupported",
@@ -251,7 +291,7 @@ impl GdbConnection {
bail!("debug process was killed"); bail!("debug process was killed");
} else if req.match_str("?") { } else if req.match_str("?") {
self.response() self.response()
.write_str(debug_stop_reason_string(self.stop_reason)) .write_str(&debug_stop_reason_string(self.stop_reason))
} else if req.match_some_str(["c", "vCont;c:"]).is_some() { } else if req.match_some_str(["c", "vCont;c:"]).is_some() {
self.client self.client
.send_command(EmulatorCommand::DebugContinue(self.sim_id)); .send_command(EmulatorCommand::DebugContinue(self.sim_id));
@@ -278,24 +318,37 @@ impl GdbConnection {
} else { } else {
self.response() self.response()
} }
} else if req.match_str("P") {
let mut write_register = || {
let register_index = req.match_hex::<usize>()?;
let register = REGISTERS.get(register_index)?.to_vb_register();
if !req.match_str("=") {
return None;
}
let value = {
let mut buffer = [0; 4];
if !req.match_hex_bytes(&mut buffer) {
return None;
}
u32::from_le_bytes(buffer)
};
self.client.send_command(EmulatorCommand::WriteRegister(
self.sim_id,
register,
value,
));
Some(())
};
if let Some(()) = write_register() {
self.response().write_str("OK")
} else {
self.response()
}
} else if let Some(op) = req.match_some_str(["m", "x"]) { } else if let Some(op) = req.match_some_str(["m", "x"]) {
let mut read_memory = || { let mut read_memory = || {
let start = req.match_hex::<u64>()?; let (start, length) = parse_memory_range(&mut req)?;
if !req.match_str(",") {
return None;
};
let length = req.match_hex::<usize>()?;
let mut buf = self.memory_buf.take().unwrap_or_default(); let mut buf = self.memory_buf.take().unwrap_or_default();
buf.clear(); buf.clear();
// The v810 has a 32-bit address space.
// Addresses wrap within that space, but we don't need to implement that for 64-bit addresses.
// Just refuse to return any info for addresses above 0xffffffff.
let Ok(start) = u32::try_from(start) else {
return Some(buf);
};
let length = length.min((u32::MAX - start) as usize + 1);
if length == 0 { if length == 0 {
return Some(buf); return Some(buf);
} }
@@ -329,18 +382,100 @@ impl GdbConnection {
} else { } else {
self.response() self.response()
} }
} else if req.match_str("Z0,") { } else if let Some(op) = req.match_some_str(["M", "X"]) {
if let Some(address) = req.match_hex() { let mut write_memory = || {
let (start, length) = parse_memory_range(&mut req)?;
if length == 0 {
return Some(());
}
if !req.match_str(":") {
return None;
}
let mut buf = self.memory_buf.take().unwrap_or_default();
buf.resize(length, 0);
let successful_read = if op == "M" {
req.match_hex_bytes(&mut buf)
} else {
req.match_bytes(&mut buf)
};
if !successful_read {
self.memory_buf = Some(buf);
return None;
}
let (tx, rx) = ::oneshot::channel();
self.client self.client
.send_command(EmulatorCommand::AddBreakpoint(self.sim_id, address)); .send_command(EmulatorCommand::WriteMemory(self.sim_id, start, buf, tx));
let buf = rx.recv().ok()?;
self.memory_buf = Some(buf);
Some(())
};
if write_memory().is_some() {
self.response().write_str("OK") self.response().write_str("OK")
} else { } else {
self.response() self.response()
} }
} else if req.match_str("z0,") { } else if req.match_str("Z") {
if let Some(address) = req.match_hex() { let mut parse_request = || {
self.client let type_ = req.match_hex::<u8>()?;
.send_command(EmulatorCommand::RemoveBreakpoint(self.sim_id, address)); if !req.match_str(",") {
return None;
}
let address = req.match_hex()?;
if type_ == 0 || type_ == 1 {
return Some(EmulatorCommand::AddBreakpoint(self.sim_id, address));
}
if !req.match_str(",") {
return None;
}
let length = req.match_hex()?;
let watch = match type_ {
2 => VBWatchpointType::Write,
3 => VBWatchpointType::Read,
4 => VBWatchpointType::Access,
_ => return None,
};
Some(EmulatorCommand::AddWatchpoint(
self.sim_id,
address,
length,
watch,
))
};
if let Some(command) = parse_request() {
self.client.send_command(command);
self.response().write_str("OK")
} else {
self.response()
}
} else if req.match_str("z") {
let mut parse_request = || {
let type_ = req.match_hex::<u8>()?;
if !req.match_str(",") {
return None;
}
let address = req.match_hex()?;
if type_ == 0 || type_ == 1 {
return Some(EmulatorCommand::RemoveBreakpoint(self.sim_id, address));
}
if !req.match_str(",") {
return None;
}
let length = req.match_hex()?;
let watch = match type_ {
2 => VBWatchpointType::Write,
3 => VBWatchpointType::Read,
4 => VBWatchpointType::Access,
_ => return None,
};
Some(EmulatorCommand::RemoveWatchpoint(
self.sim_id,
address,
length,
watch,
))
};
if let Some(command) = parse_request() {
self.client.send_command(command);
self.response().write_str("OK") self.response().write_str("OK")
} else { } else {
self.response() self.response()
@@ -367,11 +502,59 @@ impl Drop for GdbConnection {
} }
} }
fn debug_stop_reason_string(reason: Option<DebugStopReason>) -> &'static str { // parse a memory range into a start and a length.
match reason { fn parse_memory_range(req: &mut Request<'_>) -> Option<(u32, usize)> {
Some(DebugStopReason::Trace) => "T05;thread:p1.t1;threads:p1.t1;reason:trace;", let start = req.match_hex::<u64>()?;
Some(DebugStopReason::Breakpoint) => "T05;thread:p1.t1;threads:p1.t1;reason:breakpoint;", if !req.match_str(",") {
Some(DebugStopReason::Trapped) => "T05;swbreak;thread:p1.t1;threads:p1.t1;reason:trap;", return None;
None => "T00;thread:p1.t1;threads:p1.t1;", };
let length = req.match_hex::<usize>()?;
let Ok(start) = u32::try_from(start) else {
// The v810 has a 32-bit address space.
// Addresses wrap within that space, but we don't need to implement that for 64-bit addresses.
// Just refuse to return any info for addresses above 0xffffffff.
return Some((0, 0));
};
let length = length.min((u32::MAX - start) as usize + 1);
Some((start, length))
}
fn debug_stop_reason_string(reason: Option<DebugStopReason>) -> String {
let mut result = String::new();
result += if reason.is_some_and(|r| r != DebugStopReason::Paused) {
"T05;"
} else {
"T00;"
};
if let Some(DebugStopReason::Breakpoint) = reason {
result += "swbreak;";
} }
if let Some(DebugStopReason::Watchpoint(watch, address)) = reason {
result += match watch {
VBWatchpointType::Write => "watch:",
VBWatchpointType::Read => "rwatch:",
VBWatchpointType::Access => "awatch:",
};
result += &format!("{address:08x};");
}
result += "thread:p1.t1;threads:p1.t1;";
if let Some(reason) = reason {
result += "reason:";
result += match reason {
DebugStopReason::Trace => "trace;",
DebugStopReason::Breakpoint => "breakpoint;",
DebugStopReason::Watchpoint(_, _) => "watchpoint;",
DebugStopReason::Paused => "trap;",
};
}
if let Some(DebugStopReason::Watchpoint(_, address)) = reason {
result += "description:";
result += &hex::encode(address.to_string());
result += ";";
}
result
} }
+23
View File
@@ -60,6 +60,29 @@ impl Request<'_> {
} }
} }
} }
pub fn match_hex_bytes(&mut self, buffer: &mut [u8]) -> bool {
if self.buffer.len() < buffer.len() * 2 {
return false;
}
for (i, item) in buffer.iter_mut().enumerate() {
match u8::from_radix_16(&self.buffer[(i * 2)..(i * 2) + 2]) {
(byte, 2) => *item = byte,
_ => return false,
};
}
self.buffer = self.buffer.split_at(buffer.len()).1;
true
}
pub fn match_bytes(&mut self, buffer: &mut [u8]) -> bool {
if self.buffer.len() < buffer.len() {
return false;
}
buffer.copy_from_slice(&self.buffer[0..buffer.len()]);
self.buffer = self.buffer.split_at(buffer.len()).1;
true
}
} }
pub struct RequestSource<R> { pub struct RequestSource<R> {
+19 -5
View File
@@ -3,11 +3,13 @@
use std::{path::PathBuf, process, time::SystemTime}; use std::{path::PathBuf, process, time::SystemTime};
use anyhow::Result; use anyhow::{bail, Result};
use app::Application; use app::Application;
use clap::Parser; use clap::Parser;
use emulator::EmulatorBuilder; use emulator::EmulatorBuilder;
use thread_priority::{ThreadBuilder, ThreadPriority}; use thread_priority::{ThreadBuilder, ThreadPriority};
use tracing::error;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
use winit::event_loop::{ControlFlow, EventLoop}; use winit::event_loop::{ControlFlow, EventLoop};
mod app; mod app;
@@ -29,6 +31,13 @@ struct Args {
debug_port: Option<u16>, debug_port: Option<u16>,
} }
fn init_logger() {
let directives = std::env::var("RUST_LOG").unwrap_or("error,lemur=info".into());
let filter = EnvFilter::builder().parse_lossy(directives);
let layer = tracing_subscriber::fmt::layer().with_filter(filter);
tracing_subscriber::registry().with(layer).init();
}
fn set_panic_handler() { fn set_panic_handler() {
std::panic::set_hook(Box::new(|info| { std::panic::set_hook(Box::new(|info| {
let mut message = String::new(); let mut message = String::new();
@@ -76,6 +85,8 @@ fn set_process_priority_to_high() -> Result<()> {
} }
fn main() -> Result<()> { fn main() -> Result<()> {
init_logger();
set_panic_handler(); set_panic_handler();
#[cfg(windows)] #[cfg(windows)]
@@ -84,10 +95,13 @@ fn main() -> Result<()> {
let args = Args::parse(); let args = Args::parse();
let (mut builder, client) = EmulatorBuilder::new(); let (mut builder, client) = EmulatorBuilder::new();
if let Some(path) = args.rom { if let Some(path) = &args.rom {
builder = builder.with_rom(&path); builder = builder.with_rom(path);
} }
if args.debug_port.is_some() { if args.debug_port.is_some() {
if args.rom.is_none() {
bail!("to start debugging, please select a game.");
}
builder = builder.start_paused(true); builder = builder.start_paused(true);
} }
@@ -97,8 +111,8 @@ fn main() -> Result<()> {
.spawn_careless(move || { .spawn_careless(move || {
let mut emulator = match builder.build() { let mut emulator = match builder.build() {
Ok(e) => e, Ok(e) => e,
Err(err) => { Err(error) => {
eprintln!("Error initializing emulator: {err}"); error!(%error, "Error initializing emulator");
process::exit(1); process::exit(1);
} }
}; };
+3 -7
View File
@@ -142,8 +142,7 @@ impl GameWindow {
fn show_options_menu(&mut self, ctx: &Context, ui: &mut Ui) { fn show_options_menu(&mut self, ctx: &Context, ui: &mut Ui) {
ui.menu_button("Video", |ui| { ui.menu_button("Video", |ui| {
ui.menu_button("Screen Size", |ui| { ui.menu_button("Screen Size", |ui| {
let current_dims = ctx.input(|i| i.viewport().inner_rect.unwrap()); let current_dims = self.config.dimensions;
let current_dims = current_dims.max - current_dims.min;
for scale in 1..=4 { for scale in 1..=4 {
let label = format!("x{scale}"); let label = format!("x{scale}");
@@ -183,10 +182,7 @@ impl GameWindow {
return; return;
} }
let current_dims = { let current_dims = self.config.dimensions;
let viewport = ctx.input(|i| i.viewport().inner_rect.unwrap());
viewport.max - viewport.min
};
let new_proportions = display_mode.proportions(); let new_proportions = display_mode.proportions();
let scale = new_proportions / old_proportions; let scale = new_proportions / old_proportions;
if scale != Vec2::new(1.0, 1.0) { if scale != Vec2::new(1.0, 1.0) {
@@ -330,7 +326,7 @@ impl AppWindow for GameWindow {
fn show(&mut self, ctx: &Context) { fn show(&mut self, ctx: &Context) {
let dimensions = { let dimensions = {
let bounds = ctx.input(|i| i.viewport().inner_rect.unwrap()); let bounds = ctx.available_rect();
bounds.max - bounds.min bounds.max - bounds.min
}; };
self.update_config(|c| c.dimensions = dimensions); self.update_config(|c| c.dimensions = dimensions);
+6 -2
View File
@@ -3,7 +3,7 @@ use winit::event_loop::EventLoopProxy;
use crate::{ use crate::{
app::UserEvent, app::UserEvent,
emulator::{EmulatorClient, SimId}, emulator::{EmulatorClient, SimId, SimState},
gdbserver::{GdbServer, GdbServerStatus}, gdbserver::{GdbServer, GdbServerStatus},
}; };
@@ -11,6 +11,7 @@ use super::AppWindow;
pub struct GdbServerWindow { pub struct GdbServerWindow {
sim_id: SimId, sim_id: SimId,
client: EmulatorClient,
port_str: String, port_str: String,
connected: bool, connected: bool,
quit_on_disconnect: bool, quit_on_disconnect: bool,
@@ -22,6 +23,7 @@ impl GdbServerWindow {
pub fn new(sim_id: SimId, client: EmulatorClient, proxy: EventLoopProxy<UserEvent>) -> Self { pub fn new(sim_id: SimId, client: EmulatorClient, proxy: EventLoopProxy<UserEvent>) -> Self {
Self { Self {
sim_id, sim_id,
client: client.clone(),
port_str: (8080 + sim_id.to_index()).to_string(), port_str: (8080 + sim_id.to_index()).to_string(),
connected: false, connected: false,
quit_on_disconnect: false, quit_on_disconnect: false,
@@ -78,7 +80,9 @@ impl AppWindow for GdbServerWindow {
self.connected = false; self.connected = false;
} }
let start_button = Button::new("Start"); let start_button = Button::new("Start");
if ui.add_enabled(port_num.is_some(), start_button).clicked() { let can_start =
port_num.is_some() && self.client.sim_state(self.sim_id) == SimState::Ready;
if ui.add_enabled(can_start, start_button).clicked() {
let port = port_num.unwrap(); let port = port_num.unwrap();
self.server.start(port); self.server.start(port);
self.connected = true; self.connected = true;