diff --git a/Cargo.lock b/Cargo.lock index 42fd9f8..73ed9c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,15 @@ dependencies = [ "serde", ] +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.0" @@ -388,6 +397,24 @@ dependencies = [ "syn", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -400,6 +427,21 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "bindgen" version = "0.70.1" @@ -1306,6 +1348,12 @@ dependencies = [ "windows 0.58.0", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "gl_generator" version = "0.14.0" @@ -1707,11 +1755,19 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "lemur" version = "0.2.7" dependencies = [ "anyhow", + "atoi", + "atomic", "bitflags 2.6.0", "bytemuck", "cc", @@ -1724,6 +1780,7 @@ dependencies = [ "egui-winit", "egui_extras", "gilrs", + "hex", "image", "itertools", "num-derive", @@ -1736,6 +1793,9 @@ dependencies = [ "serde", "serde_json", "thread-priority", + "tokio", + "tracing", + "tracing-subscriber", "wgpu", "windows 0.58.0", "winit", @@ -1831,6 +1891,15 @@ dependencies = [ "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]] name = "memchr" version = "2.7.4" @@ -1902,6 +1971,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "naga" version = "23.1.0" @@ -2005,6 +2085,16 @@ dependencies = [ "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]] name = "num-complex" version = "0.4.6" @@ -2276,6 +2366,15 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "oboe" version = "0.6.1" @@ -2336,6 +2435,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "owned_ttf_parser" version = "0.25.0" @@ -2629,8 +2734,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "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]] @@ -2641,9 +2755,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "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]] name = "regex-syntax" version = "0.8.5" @@ -2696,6 +2816,12 @@ dependencies = [ "realfft", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2828,6 +2954,15 @@ dependencies = [ "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]] name = "shlex" version = "1.3.0" @@ -2918,6 +3053,16 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -3035,6 +3180,16 @@ dependencies = [ "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]] name = "tiny-skia" version = "0.11.4" @@ -3070,6 +3225,33 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokio" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.8.19" @@ -3133,6 +3315,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "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]] @@ -3243,6 +3455,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vec_map" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index d5889af..9ad9959 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,8 @@ edition = "2021" [dependencies] anyhow = "1" +atoi = "2" +atomic = "0.6" bitflags = { version = "2", features = ["serde"] } bytemuck = { version = "1", features = ["derive"] } clap = { version = "4", features = ["derive"] } @@ -20,6 +22,7 @@ egui-toast = { git = "https://github.com/urholaukkarinen/egui-toast.git", rev = egui-winit = "0.30" egui-wgpu = { version = "0.30", features = ["winit"] } gilrs = { version = "0.11", features = ["serde-serialize"] } +hex = "0.4" image = { version = "0.25", default-features = false, features = ["png"] } itertools = "0.13" num-derive = "0.4" @@ -32,6 +35,9 @@ rubato = "0.16" serde = { version = "1", features = ["derive"] } serde_json = "1" thread-priority = "1" +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" winit = { version = "0.30", features = ["serde"] } diff --git a/README.md b/README.md index f134e02..de29880 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A Virtual Boy emulator built around the shrooms-vb core. Written in Rust, using ## Setup Install the following dependencies: - - `cargo` + - `cargo` (via [rustup](https://rustup.rs/), the version from your package manager is too old) Run ```sh diff --git a/build.Dockerfile b/build.Dockerfile index 2521d83..dca7dbc 100644 --- a/build.Dockerfile +++ b/build.Dockerfile @@ -23,6 +23,10 @@ ENV PATH="/osxcross/bin:$PATH" \ CXX_x86_64-apple-darwin="o64-clang++" \ CC_aarch64-apple-darwin="oa64-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_APPLE_DARWIN_LINKER="o64-clang" \ 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" \ CROSS_COMPILE="setting-this-to-silence-a-warning-" \ 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" diff --git a/build.rs b/build.rs index 6335468..aeb1a79 100644 --- a/build.rs +++ b/build.rs @@ -8,13 +8,25 @@ fn main() -> Result<(), Box> { } 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")) - .opt_level(2) + .opt_level(opt_level) .flag_if_supported("-fno-strict-aliasing") .define("VB_LITTLE_ENDIAN", None) .define("VB_SIGNED_PROPAGATE", 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")) .compile("vb"); diff --git a/src/app.rs b/src/app.rs index 886127e..00e358f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,6 +6,7 @@ use egui::{ ViewportCommand, ViewportId, ViewportInfo, }; use gilrs::{EventType, Gilrs}; +use tracing::{error, warn}; use winit::{ application::ApplicationHandler, event::WindowEvent, @@ -18,7 +19,7 @@ use crate::{ emulator::{EmulatorClient, EmulatorCommand, SimId}, input::MappingProvider, persistence::Persistence, - window::{AboutWindow, AppWindow, GameWindow, InputWindow}, + window::{AboutWindow, AppWindow, GameWindow, GdbServerWindow, InputWindow}, }; fn load_icon() -> anyhow::Result { @@ -41,10 +42,15 @@ pub struct Application { persistence: Persistence, viewports: HashMap, focused: Option, + init_debug_port: Option, } impl Application { - pub fn new(client: EmulatorClient, proxy: EventLoopProxy) -> Self { + pub fn new( + client: EmulatorClient, + proxy: EventLoopProxy, + debug_port: Option, + ) -> Self { let icon = load_icon().ok().map(Arc::new); let persistence = Persistence::new(); let mappings = MappingProvider::new(persistence.clone()); @@ -63,6 +69,7 @@ impl Application { persistence, viewports: HashMap::new(), focused: None, + init_debug_port: debug_port, } } @@ -80,15 +87,19 @@ impl Application { impl ApplicationHandler for Application { fn resumed(&mut self, event_loop: &ActiveEventLoop) { + if let Some(port) = self.init_debug_port { + let mut server = + GdbServerWindow::new(SimId::Player1, self.client.clone(), self.proxy.clone()); + server.launch(port); + self.open(event_loop, Box::new(server)); + } let app = GameWindow::new( self.client.clone(), self.proxy.clone(), self.persistence.clone(), SimId::Player1, ); - let wrapper = Viewport::new(event_loop, self.icon.clone(), Box::new(app)); - self.focused = Some(wrapper.id()); - self.viewports.insert(wrapper.id(), wrapper); + self.open(event_loop, Box::new(app)); } fn window_event( @@ -183,6 +194,11 @@ impl ApplicationHandler for Application { let about = AboutWindow; self.open(event_loop, Box::new(about)); } + UserEvent::OpenDebugger(sim_id) => { + let debugger = + GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone()); + self.open(event_loop, Box::new(debugger)); + } UserEvent::OpenInput => { let input = InputWindow::new(self.mappings.clone()); self.open(event_loop, Box::new(input)); @@ -196,6 +212,13 @@ impl ApplicationHandler for Application { ); self.open(event_loop, Box::new(p2)); } + UserEvent::Quit(sim_id) => { + self.viewports + .retain(|_, viewport| viewport.app.sim_id() != sim_id); + if !self.viewports.contains_key(&ViewportId::ROOT) { + event_loop.exit(); + } + } } } @@ -208,8 +231,8 @@ impl ApplicationHandler for Application { fn exiting(&mut self, _event_loop: &ActiveEventLoop) { let (sender, receiver) = oneshot::channel(); if self.client.send_command(EmulatorCommand::Exit(sender)) { - if let Err(err) = receiver.recv_timeout(Duration::from_secs(5)) { - eprintln!("could not gracefully exit: {}", err); + if let Err(error) = receiver.recv_timeout(Duration::from_secs(5)) { + error!(%error, "could not gracefully exit."); } } } @@ -380,8 +403,10 @@ impl Drop for Viewport { pub enum UserEvent { GamepadEvent(gilrs::Event), OpenAbout, + OpenDebugger(SimId), OpenInput, OpenPlayer2, + Quit(SimId), } pub enum Action { @@ -410,9 +435,12 @@ fn create_window_and_state( } fn process_gamepad_input(mappings: MappingProvider, proxy: EventLoopProxy) { - let Ok(mut gilrs) = Gilrs::new() else { - eprintln!("could not connect gamepad listener"); - return; + let mut gilrs = match Gilrs::new() { + Ok(gilrs) => gilrs, + Err(error) => { + warn!(%error, "could not connect gamepad listener"); + return; + } }; while let Some(event) = gilrs.next_event_blocking(None) { if event.event == EventType::Connected { diff --git a/src/audio.rs b/src/audio.rs index 2d97949..480c758 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -4,6 +4,7 @@ use anyhow::{bail, Result}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use itertools::Itertools; use rubato::{FftFixedInOut, Resampler}; +use tracing::error; pub struct Audio { #[allow(unused)] @@ -54,7 +55,7 @@ impl Audio { } chunk.commit_all(); }, - move |err| eprintln!("stream error: {err}"), + move |error| error!(%error, "stream error"), None, )?; stream.play()?; diff --git a/src/emulator.rs b/src/emulator.rs index 814149b..96df37d 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -1,34 +1,29 @@ use std::{ collections::HashMap, + fmt::Display, fs::{self, File}, io::{Read, Seek, SeekFrom, Write}, path::{Path, PathBuf}, sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, + atomic::{AtomicBool, Ordering}, mpsc::{self, RecvError, TryRecvError}, Arc, }, }; use anyhow::Result; +use atomic::Atomic; +use bytemuck::NoUninit; use egui_toast::{Toast, ToastKind, ToastOptions}; +use tracing::{error, warn}; use crate::{audio::Audio, graphics::TextureSink}; -pub use shrooms_vb_core::VBKey; -use shrooms_vb_core::{Sim, EXPECTED_FRAME_SIZE}; +use shrooms_vb_core::{Sim, StopReason, EXPECTED_FRAME_SIZE}; +pub use shrooms_vb_core::{VBKey, VBRegister, VBWatchpointType}; +mod address_set; mod shrooms_vb_core; -pub struct EmulatorBuilder { - rom: Option, - commands: mpsc::Receiver, - sim_count: Arc, - running: Arc<[AtomicBool; 2]>, - has_game: Arc<[AtomicBool; 2]>, - audio_on: Arc<[AtomicBool; 2]>, - linked: Arc, -} - #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] pub enum SimId { Player1, @@ -45,6 +40,14 @@ impl SimId { } } } +impl Display for SimId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Player1 => "Player 1", + Self::Player2 => "Player 2", + }) + } +} struct Cart { rom_path: PathBuf, @@ -83,23 +86,35 @@ fn sram_path(rom_path: &Path, sim_id: SimId) -> PathBuf { } } +pub struct EmulatorBuilder { + rom: Option, + commands: mpsc::Receiver, + sim_state: Arc<[Atomic; 2]>, + state: Arc>, + audio_on: Arc<[AtomicBool; 2]>, + linked: Arc, + start_paused: bool, +} + impl EmulatorBuilder { pub fn new() -> (Self, EmulatorClient) { let (queue, commands) = mpsc::channel(); let builder = Self { rom: None, commands, - sim_count: Arc::new(AtomicUsize::new(0)), - running: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]), - has_game: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]), + sim_state: Arc::new([ + Atomic::new(SimState::Uninitialized), + Atomic::new(SimState::Uninitialized), + ]), + state: Arc::new(Atomic::new(EmulatorState::Paused)), audio_on: Arc::new([AtomicBool::new(true), AtomicBool::new(true)]), linked: Arc::new(AtomicBool::new(false)), + start_paused: false, }; let client = EmulatorClient { queue, - sim_count: builder.sim_count.clone(), - running: builder.running.clone(), - has_game: builder.has_game.clone(), + sim_state: builder.sim_state.clone(), + state: builder.state.clone(), audio_on: builder.audio_on.clone(), linked: builder.linked.clone(), }; @@ -113,18 +128,27 @@ impl EmulatorBuilder { } } + pub fn start_paused(self, paused: bool) -> Self { + Self { + start_paused: paused, + ..self + } + } + pub fn build(self) -> Result { let mut emulator = Emulator::new( self.commands, - self.sim_count, - self.running, - self.has_game, + self.sim_state, + self.state, self.audio_on, self.linked, )?; if let Some(path) = self.rom { emulator.load_cart(SimId::Player1, &path)?; } + if self.start_paused { + emulator.pause_sims()?; + } Ok(emulator) } } @@ -134,13 +158,13 @@ pub struct Emulator { carts: [Option; 2], audio: Audio, commands: mpsc::Receiver, - sim_count: Arc, - running: Arc<[AtomicBool; 2]>, - has_game: Arc<[AtomicBool; 2]>, + sim_state: Arc<[Atomic; 2]>, + state: Arc>, audio_on: Arc<[AtomicBool; 2]>, linked: Arc, renderers: HashMap, messages: HashMap>, + debuggers: HashMap, eye_contents: Vec, audio_samples: Vec, } @@ -148,9 +172,8 @@ pub struct Emulator { impl Emulator { fn new( commands: mpsc::Receiver, - sim_count: Arc, - running: Arc<[AtomicBool; 2]>, - has_game: Arc<[AtomicBool; 2]>, + sim_state: Arc<[Atomic; 2]>, + state: Arc>, audio_on: Arc<[AtomicBool; 2]>, linked: Arc, ) -> Result { @@ -159,15 +182,15 @@ impl Emulator { carts: [None, None], audio: Audio::init()?, commands, - sim_count, - running, - has_game, + sim_state, + state, audio_on, linked, renderers: HashMap::new(), messages: HashMap::new(), + debuggers: HashMap::new(), eye_contents: vec![0u8; 384 * 224 * 2], - audio_samples: vec![0.0; EXPECTED_FRAME_SIZE], + audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE), }) } @@ -198,17 +221,17 @@ impl Emulator { let index = sim_id.to_index(); while self.sims.len() <= index { self.sims.push(Sim::new()); + self.sim_state[index].store(SimState::NoGame, Ordering::Release); } - self.sim_count.store(self.sims.len(), Ordering::Relaxed); let sim = &mut self.sims[index]; sim.reset(); if let Some(cart) = new_cart { sim.load_cart(cart.rom.clone(), cart.sram.clone())?; self.carts[index] = Some(cart); - self.has_game[index].store(true, Ordering::Release); + self.sim_state[index].store(SimState::Ready, Ordering::Release); } - if self.has_game[index].load(Ordering::Acquire) { - self.running[index].store(true, Ordering::Release); + if self.sim_state[index].load(Ordering::Acquire) == SimState::Ready { + self.resume_sims(); } Ok(()) } @@ -233,9 +256,31 @@ impl Emulator { self.linked.store(false, Ordering::Release); } - pub fn pause_sim(&mut self, sim_id: SimId) -> Result<()> { - self.running[sim_id.to_index()].store(false, Ordering::Release); - self.save_sram(sim_id) + fn pause_sims(&mut self) -> Result<()> { + if self + .state + .compare_exchange( + EmulatorState::Running, + EmulatorState::Paused, + Ordering::AcqRel, + Ordering::Relaxed, + ) + .is_ok() + { + for sim_id in SimId::values() { + self.save_sram(sim_id)?; + } + } + Ok(()) + } + + fn resume_sims(&mut self) { + let _ = self.state.compare_exchange( + EmulatorState::Paused, + EmulatorState::Running, + Ordering::AcqRel, + Ordering::Relaxed, + ); } fn save_sram(&mut self, sim_id: SimId) -> Result<()> { @@ -253,13 +298,75 @@ impl Emulator { self.save_sram(SimId::Player2)?; self.renderers.remove(&SimId::Player2); self.sims.truncate(1); - self.sim_count.store(self.sims.len(), Ordering::Relaxed); - self.running[SimId::Player2.to_index()].store(false, Ordering::Release); - self.has_game[SimId::Player2.to_index()].store(false, Ordering::Release); + self.sim_state[SimId::Player2.to_index()].store(SimState::Uninitialized, Ordering::Release); + self.stop_debugging(SimId::Player2); self.linked.store(false, Ordering::Release); Ok(()) } + 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 { + sender, + stop_reason: Some(DebugStopReason::Paused), + }; + self.debuggers.insert(sim_id, debug); + self.state + .store(EmulatorState::Debugging, Ordering::Release); + } + + fn stop_debugging(&mut self, sim_id: SimId) { + if let Some(sim) = self.sims.get_mut(sim_id.to_index()) { + sim.clear_debug_state(); + } + self.debuggers.remove(&sim_id); + if self.debuggers.is_empty() { + let _ = self.state.compare_exchange( + EmulatorState::Debugging, + EmulatorState::Running, + Ordering::AcqRel, + Ordering::Relaxed, + ); + } + } + + fn debug_interrupt(&mut self, sim_id: SimId) { + self.debug_stop(sim_id, DebugStopReason::Paused); + } + + fn debug_stop(&mut self, sim_id: SimId, reason: DebugStopReason) { + let Some(debugger) = self.debuggers.get_mut(&sim_id) else { + self.stop_debugging(sim_id); + return; + }; + if debugger.stop_reason != Some(reason) { + debugger.stop_reason = Some(reason); + if debugger.sender.send(DebugEvent::Stopped(reason)).is_err() { + self.stop_debugging(sim_id); + } + } + } + + fn debug_continue(&mut self, sim_id: SimId) -> bool { + let Some(debugger) = self.debuggers.get_mut(&sim_id) else { + self.stop_debugging(sim_id); + return false; + }; + debugger.stop_reason = None; + true + } + fn debug_step(&mut self, sim_id: SimId) { + if self.debug_continue(sim_id) { + let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { + return; + }; + sim.step(); + } + } + pub fn run(&mut self) { loop { let idle = self.tick(); @@ -289,9 +396,20 @@ impl Emulator { // returns true if the emulator is "idle" (i.e. this didn't output anything) pub fn tick(&mut self) -> bool { - let p1_running = self.running[SimId::Player1.to_index()].load(Ordering::Acquire); - let p2_running = self.running[SimId::Player2.to_index()].load(Ordering::Acquire); - let mut idle = p1_running || p2_running; + let p1_state = self.sim_state[SimId::Player1.to_index()].load(Ordering::Acquire); + let p2_state = self.sim_state[SimId::Player2.to_index()].load(Ordering::Acquire); + let state = self.state.load(Ordering::Acquire); + + // Emulation + // Don't emulate if the state is "paused", or if any sim is paused in the debugger + let running = match state { + EmulatorState::Paused => false, + EmulatorState::Running => true, + EmulatorState::Debugging => self.debuggers.values().all(|d| d.stop_reason.is_none()), + }; + let p1_running = running && p1_state == SimState::Ready; + let p2_running = running && p2_state == SimState::Ready; + let mut idle = !p1_running && !p2_running; if p1_running && p2_running { Sim::emulate_many(&mut self.sims); } else if p1_running { @@ -300,6 +418,26 @@ impl Emulator { self.sims[SimId::Player2.to_index()].emulate(); } + // Debug state + if state == EmulatorState::Debugging { + for sim_id in SimId::values() { + let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { + continue; + }; + if let Some(reason) = sim.stop_reason() { + let stop_reason = match reason { + StopReason::Stepped => DebugStopReason::Trace, + StopReason::Watchpoint(watch, address) => { + DebugStopReason::Watchpoint(watch, address) + } + StopReason::Breakpoint => DebugStopReason::Breakpoint, + }; + self.debug_stop(sim_id, stop_reason); + } + } + } + + // Video for sim_id in SimId::values() { let Some(renderer) = self.renderers.get_mut(&sim_id) else { continue; @@ -314,24 +452,27 @@ impl Emulator { } } } + + // Audio + // Audio playback speed is how we keep the emulator running in real time. + // Even if we're muted, call `read_samples` to know how many frames of silence to play. let p1_audio = p1_running && self.audio_on[SimId::Player1.to_index()].load(Ordering::Acquire); let p2_audio = p2_running && self.audio_on[SimId::Player2.to_index()].load(Ordering::Acquire); - let weight = if p1_audio && p2_audio { 0.5 } else { 1.0 }; - if p1_audio { - if let Some(sim) = self.sims.get_mut(SimId::Player1.to_index()) { - sim.read_samples(&mut self.audio_samples, weight); - } + let (p1_weight, p2_weight) = match (p1_audio, p2_audio) { + (true, true) => (0.5, 0.5), + (true, false) => (1.0, 0.0), + (false, true) => (0.0, 1.0), + (false, false) => (0.0, 0.0), + }; + if let Some(sim) = self.sims.get_mut(SimId::Player1.to_index()) { + sim.read_samples(&mut self.audio_samples, p1_weight); } - if p2_audio { - if let Some(sim) = self.sims.get_mut(SimId::Player2.to_index()) { - sim.read_samples(&mut self.audio_samples, weight); - } + if let Some(sim) = self.sims.get_mut(SimId::Player2.to_index()) { + sim.read_samples(&mut self.audio_samples, p2_weight); } - if self.audio_samples.is_empty() { - self.audio_samples.resize(EXPECTED_FRAME_SIZE, 0.0); - } else { + if !self.audio_samples.is_empty() { idle = false; } self.audio.update(&self.audio_samples); @@ -367,19 +508,78 @@ impl Emulator { } } EmulatorCommand::Pause => { - for sim_id in SimId::values() { - if let Err(error) = self.pause_sim(sim_id) { - self.report_error(sim_id, format!("Error pausing: {error}")); - } + if let Err(error) = self.pause_sims() { + self.report_error(SimId::Player1, format!("Error pausing: {error}")); } } EmulatorCommand::Resume => { - for sim_id in SimId::values() { - let index = sim_id.to_index(); - if self.has_game[index].load(Ordering::Acquire) { - self.running[index].store(true, Ordering::Relaxed); - } - } + self.resume_sims(); + } + EmulatorCommand::StartDebugging(sim_id, debugger) => { + self.start_debugging(sim_id, debugger); + } + EmulatorCommand::StopDebugging(sim_id) => { + self.stop_debugging(sim_id); + } + EmulatorCommand::DebugInterrupt(sim_id) => { + self.debug_interrupt(sim_id); + } + EmulatorCommand::DebugContinue(sim_id) => { + self.debug_continue(sim_id); + } + EmulatorCommand::DebugStep(sim_id) => { + self.debug_step(sim_id); + } + EmulatorCommand::ReadRegister(sim_id, register, done) => { + let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { + return; + }; + let value = sim.read_register(register); + 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) => { + let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { + return; + }; + sim.read_memory(start, length, &mut 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) => { + let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { + return; + }; + sim.add_breakpoint(address); + } + EmulatorCommand::RemoveBreakpoint(sim_id, address) => { + let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { + return; + }; + 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) => { self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release); @@ -426,7 +626,7 @@ impl Emulator { return; } } - eprintln!("{}", message); + error!("{}", message); } } @@ -438,6 +638,19 @@ pub enum EmulatorCommand { StopSecondSim, Pause, Resume, + StartDebugging(SimId, DebugSender), + StopDebugging(SimId), + DebugInterrupt(SimId), + DebugContinue(SimId), + DebugStep(SimId), + ReadRegister(SimId, VBRegister, oneshot::Sender), + WriteRegister(SimId, VBRegister, u32), + ReadMemory(SimId, u32, usize, Vec, oneshot::Sender>), + WriteMemory(SimId, u32, Vec, oneshot::Sender>), + AddBreakpoint(SimId, u32), + RemoveBreakpoint(SimId, u32), + AddWatchpoint(SimId, u32, usize, VBWatchpointType), + RemoveWatchpoint(SimId, u32, usize, VBWatchpointType), SetAudioEnabled(bool, bool), Link, Unlink, @@ -446,37 +659,72 @@ pub enum EmulatorCommand { Exit(oneshot::Sender<()>), } +#[derive(Clone, Copy, Debug, PartialEq, Eq, NoUninit)] +#[repr(usize)] +pub enum SimState { + Uninitialized, + NoGame, + Ready, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, NoUninit)] +#[repr(usize)] +pub enum EmulatorState { + Paused, + Running, + Debugging, +} + +type DebugSender = tokio::sync::mpsc::UnboundedSender; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DebugStopReason { + // We are stepping + Trace, + // We hit a breakpoint + Breakpoint, + // We hit a watchpoint + Watchpoint(VBWatchpointType, u32), + // The debugger told us to pause + Paused, +} + +struct DebugInfo { + sender: DebugSender, + stop_reason: Option, +} + +pub enum DebugEvent { + Stopped(DebugStopReason), +} + #[derive(Clone)] pub struct EmulatorClient { queue: mpsc::Sender, - sim_count: Arc, - running: Arc<[AtomicBool; 2]>, - has_game: Arc<[AtomicBool; 2]>, + sim_state: Arc<[Atomic; 2]>, + state: Arc>, audio_on: Arc<[AtomicBool; 2]>, linked: Arc, } impl EmulatorClient { - pub fn has_player_2(&self) -> bool { - self.sim_count.load(Ordering::Acquire) == 2 + pub fn sim_state(&self, sim_id: SimId) -> SimState { + self.sim_state[sim_id.to_index()].load(Ordering::Acquire) } - pub fn is_running(&self, sim_id: SimId) -> bool { - self.running[sim_id.to_index()].load(Ordering::Acquire) - } - pub fn has_game(&self, sim_id: SimId) -> bool { - self.has_game[sim_id.to_index()].load(Ordering::Acquire) - } - pub fn are_sims_linked(&self) -> bool { - self.linked.load(Ordering::Acquire) + 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 send_command(&self, command: EmulatorCommand) -> bool { match self.queue.send(command) { Ok(()) => true, Err(err) => { - eprintln!( + warn!( "could not send command {:?} as emulator is shut down", err.0 ); diff --git a/src/emulator/address_set.rs b/src/emulator/address_set.rs new file mode 100644 index 0000000..4e8ee17 --- /dev/null +++ b/src/emulator/address_set.rs @@ -0,0 +1,270 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + ops::Bound, +}; + +#[derive(Debug, Default)] +pub struct AddressSet { + ranges: BTreeSet<(u32, usize)>, + bounds: BTreeMap, +} + +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 { + 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); + } +} diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index bc30433..e15ca2c 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -5,6 +5,8 @@ use bitflags::bitflags; use num_derive::{FromPrimitive, ToPrimitive}; use serde::{Deserialize, Serialize}; +use super::address_set::AddressSet; + #[repr(C)] struct VB { _data: [u8; 0], @@ -55,7 +57,38 @@ bitflags! { } } +#[derive(Clone, Copy, Debug)] +pub enum VBRegister { + Program(u32), + System(u32), + PC, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum VBWatchpointType { + Read, + Write, + Access, +} + +type OnExecute = + 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")] extern "C" { @@ -77,6 +110,10 @@ extern "C" { right_stride_x: c_int, right_stride_y: c_int, ); + #[link_name = "vbGetProgramCounter"] + fn vb_get_program_counter(sim: *mut VB) -> u32; + #[link_name = "vbGetProgramRegister"] + fn vb_get_program_register(sim: *mut VB, index: c_uint) -> i32; #[link_name = "vbGetSamples"] fn vb_get_samples( sim: *mut VB, @@ -84,24 +121,36 @@ extern "C" { capacity: *mut c_uint, position: *mut c_uint, ) -> *mut c_void; + #[link_name = "vbGetSystemRegister"] + fn vb_get_system_register(sim: *mut VB, index: c_uint) -> i32; #[link_name = "vbGetUserData"] fn vb_get_user_data(sim: *mut VB) -> *mut c_void; #[link_name = "vbInit"] fn vb_init(sim: *mut VB) -> *mut VB; + #[link_name = "vbRead"] + fn vb_read(sim: *mut VB, address: u32, typ_: VBDataType) -> i32; #[link_name = "vbReset"] fn vb_reset(sim: *mut VB); #[link_name = "vbSetCartRAM"] fn vb_set_cart_ram(sim: *mut VB, sram: *mut c_void, size: u32) -> c_int; #[link_name = "vbSetCartROM"] fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int; + #[link_name = "vbSetExecuteCallback"] + fn vb_set_execute_callback(sim: *mut VB, callback: Option) -> Option; #[link_name = "vbSetFrameCallback"] - fn vb_set_frame_callback(sim: *mut VB, on_frame: OnFrame); + fn vb_set_frame_callback(sim: *mut VB, callback: Option) -> Option; #[link_name = "vbSetKeys"] fn vb_set_keys(sim: *mut VB, keys: u16) -> u16; #[link_name = "vbSetOption"] fn vb_set_option(sim: *mut VB, key: VBOption, value: c_int); #[link_name = "vbSetPeer"] 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) -> Option; #[link_name = "vbSetSamples"] fn vb_set_samples( sim: *mut VB, @@ -109,13 +158,20 @@ extern "C" { typ_: VBDataType, capacity: c_uint, ) -> c_int; + #[link_name = "vbSetSystemRegister"] + fn vb_set_system_register(sim: *mut VB, index: c_uint, value: u32) -> u32; #[link_name = "vbSetUserData"] 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) -> Option; #[link_name = "vbSizeOf"] fn vb_size_of() -> usize; + #[link_name = "vbWrite"] + fn vb_write(sim: *mut VB, address: u32, _type: VBDataType, value: i32) -> i32; } -extern "C" fn on_frame(sim: *mut VB) -> i32 { +#[no_mangle] +extern "C" fn on_frame(sim: *mut VB) -> 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() }; @@ -123,12 +179,109 @@ extern "C" fn on_frame(sim: *mut VB) -> i32 { 1 } +#[no_mangle] +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. + // 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 mut stopped = data.stop_reason.is_some(); + if data.step_from.is_some_and(|s| s != address) { + data.step_from = None; + data.stop_reason = Some(StopReason::Stepped); + stopped = true; + } + if data.breakpoints.binary_search(&address).is_ok() { + data.stop_reason = Some(StopReason::Breakpoint); + stopped = true; + } + + 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_FLOATS: usize = AUDIO_CAPACITY_SAMPLES * 2; pub const EXPECTED_FRAME_SIZE: usize = 834 * 2; struct VBState { frame_seen: bool, + stop_reason: Option, + step_from: Option, + breakpoints: Vec, + 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)] @@ -136,9 +289,6 @@ pub struct Sim { sim: *mut VB, } -// SAFETY: the memory pointed to by sim is valid -unsafe impl Send for Sim {} - impl Sim { pub fn new() -> Self { // init the VB instance itself @@ -149,12 +299,20 @@ impl Sim { unsafe { vb_init(sim) }; // pseudohalt is disabled due to breaking red alarm unsafe { vb_set_option(sim, VBOption::PseudoHalt, 0) }; + unsafe { vb_set_keys(sim, VBKey::SGN.bits()) }; unsafe { vb_reset(sim) }; // set up userdata - let state = VBState { frame_seen: false }; + let state = VBState { + frame_seen: false, + stop_reason: None, + step_from: None, + 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_frame_callback(sim, on_frame) }; + unsafe { vb_set_frame_callback(sim, Some(on_frame)) }; // set up audio buffer let audio_buffer = vec![0.0f32; AUDIO_CAPACITY_FLOATS]; @@ -243,9 +401,7 @@ impl Sim { } pub fn read_pixels(&mut self, buffers: &mut [u8]) -> bool { - // 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(self.sim).cast() }; + let data = self.get_state(); if !data.frame_seen { return false; } @@ -292,6 +448,167 @@ impl Sim { pub fn set_keys(&mut self, keys: VBKey) { unsafe { vb_set_keys(self.sim, keys.bits()) }; } + + pub fn read_register(&mut self, register: VBRegister) -> u32 { + match register { + VBRegister::Program(index) => unsafe { + vb_get_program_register(self.sim, index) as u32 + }, + VBRegister::System(index) => unsafe { vb_get_system_register(self.sim, index) as u32 }, + VBRegister::PC => unsafe { vb_get_program_counter(self.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) { + let mut address = start; + for _ in 0..length { + let byte = unsafe { vb_read(self.sim, address, VBDataType::U8) }; + into.push(byte as u8); + address = address.wrapping_add(1); + } + } + + 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) { + let data = self.get_state(); + if let Err(index) = data.breakpoints.binary_search(&address) { + data.breakpoints.insert(index, address); + } + + unsafe { + vb_set_execute_callback(self.sim, Some(on_execute)); + } + } + + pub fn remove_breakpoint(&mut self, address: u32) { + let data = self.get_state(); + if let Ok(index) = data.breakpoints.binary_search(&address) { + data.breakpoints.remove(index); + 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) }; + } + } + } + + pub fn step(&mut self) { + let current_pc = unsafe { vb_get_program_counter(self.sim) }; + let data = self.get_state(); + data.step_from = Some(current_pc); + unsafe { + vb_set_execute_callback(self.sim, Some(on_execute)); + } + } + + pub fn clear_debug_state(&mut self) { + let data = self.get_state(); + data.step_from = None; + 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) }; + } + + pub fn stop_reason(&mut self) -> Option { + let data = self.get_state(); + let reason = data.stop_reason.take(); + if !data.needs_execute_callback() { + unsafe { vb_set_execute_callback(self.sim, None) }; + } + reason + } + + fn get_state(&mut self) -> &mut VBState { + // SAFETY: the *mut VB owns its userdata. + // There is no way for the userdata to be null or otherwise invalid. + unsafe { &mut *vb_get_user_data(self.sim).cast() } + } } impl Drop for Sim { diff --git a/src/gdbserver.rs b/src/gdbserver.rs new file mode 100644 index 0000000..6c59ccd --- /dev/null +++ b/src/gdbserver.rs @@ -0,0 +1,560 @@ +use anyhow::{bail, Result}; +use registers::REGISTERS; +use request::{Request, RequestKind, RequestSource}; +use response::Response; +use std::{ + sync::{Arc, Mutex}, + thread, +}; +use tokio::{ + io::{AsyncWriteExt as _, BufReader}, + net::{TcpListener, TcpStream}, + pin, select, + sync::{mpsc, oneshot}, +}; +use tracing::{debug, enabled, error, info, Level}; + +use crate::emulator::{ + DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId, VBWatchpointType, +}; + +mod registers; +mod request; +mod response; + +pub struct GdbServer { + sim_id: SimId, + client: EmulatorClient, + status: Arc>, + killer: Option>, +} + +impl GdbServer { + pub fn new(sim_id: SimId, client: EmulatorClient) -> Self { + Self { + sim_id, + client, + status: Arc::new(Mutex::new(GdbServerStatus::Stopped)), + killer: None, + } + } + + pub fn status(&self) -> GdbServerStatus { + self.status.lock().unwrap().clone() + } + + pub fn start(&mut self, port: u16) { + *self.status.lock().unwrap() = GdbServerStatus::Connecting; + let sim_id = self.sim_id; + let client = self.client.clone(); + let status = self.status.clone(); + let (tx, rx) = oneshot::channel(); + self.killer = Some(tx); + thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + select! { + _ = run_server(sim_id, client.clone(), port, &status) => {} + _ = rx => { + client.send_command(EmulatorCommand::StopDebugging(sim_id)); + *status.lock().unwrap() = GdbServerStatus::Stopped; + } + } + }) + }); + } + + pub fn stop(&mut self) { + if let Some(killer) = self.killer.take() { + let _ = killer.send(()); + } + } +} + +impl Drop for GdbServer { + fn drop(&mut self) { + self.stop(); + } +} + +async fn run_server( + sim_id: SimId, + client: EmulatorClient, + port: u16, + status: &Mutex, +) { + let (debug_sink, mut debug_source) = mpsc::unbounded_channel(); + 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); + match connection.run(stream, debug_source).await { + Ok(()) => { + info!("Finished debugging."); + *status.lock().unwrap() = GdbServerStatus::Stopped; + } + Err(error) => { + error!(%error, "Error from debugger."); + *status.lock().unwrap() = GdbServerStatus::Error(error.to_string()); + } + } +} + +async fn try_connect(port: u16, status: &Mutex) -> Option { + *status.lock().unwrap() = GdbServerStatus::Connecting; + let listener = match TcpListener::bind(("127.0.0.1", port)).await { + Ok(l) => l, + Err(err) => { + error!(%err, "Could not open port."); + *status.lock().unwrap() = GdbServerStatus::Error(err.to_string()); + return None; + } + }; + match listener.accept().await { + Ok((stream, _)) => { + *status.lock().unwrap() = GdbServerStatus::Running; + Some(stream) + } + Err(err) => { + error!(%err, "Could not connect to debugger."); + *status.lock().unwrap() = GdbServerStatus::Error(err.to_string()); + None + } + } +} + +#[derive(Clone)] +pub enum GdbServerStatus { + Stopped, + Connecting, + Running, + Error(String), +} + +impl GdbServerStatus { + pub fn running(&self) -> bool { + matches!(self, Self::Connecting | Self::Running) + } +} + +struct GdbConnection { + sim_id: SimId, + client: EmulatorClient, + ack_messages: bool, + stop_reason: Option, + response_buf: Option>, + memory_buf: Option>, +} + +impl GdbConnection { + fn new(sim_id: SimId, client: EmulatorClient) -> Self { + Self { + sim_id, + client, + ack_messages: true, + stop_reason: None, + response_buf: None, + memory_buf: None, + } + } + async fn run( + &mut self, + stream: TcpStream, + mut debug_source: mpsc::UnboundedReceiver, + ) -> Result<()> { + let (rx, mut tx) = stream.into_split(); + let mut request_source = RequestSource::new(BufReader::new(rx)); + loop { + let response = select! { + maybe_event = debug_source.recv() => { + let Some(event) = maybe_event else { + // debugger has stopped running + break; + }; + self.handle_event(event) + } + maybe_request = request_source.recv() => { + let req = maybe_request?; + self.handle_request(req)? + } + }; + if let Some(res) = response { + let buffer = res.finish(); + if enabled!(Level::DEBUG) { + match std::str::from_utf8(&buffer) { + Ok(text) => debug!("response: {text}"), + Err(_) => debug!("response: {buffer:02x?}"), + } + } + tx.write_all(&buffer).await?; + self.response_buf = Some(buffer); + tx.flush().await?; + } + } + Ok(()) + } + + fn handle_event(&mut self, event: DebugEvent) -> Option { + let res = match event { + DebugEvent::Stopped(reason) => { + if self.stop_reason.is_some_and(|r| r == reason) { + return None; + } + self.stop_reason = Some(reason); + self.response() + .write_str(&debug_stop_reason_string(self.stop_reason)) + } + }; + Some(res) + } + + fn handle_request(&mut self, mut req: Request<'_>) -> Result> { + debug!("received {:02x?}", req); + + if req.kind == RequestKind::Signal { + self.client + .send_command(EmulatorCommand::DebugInterrupt(self.sim_id)); + return Ok(None); // we'll send a message when the emulator reports it has stopped + } + + let res = if req.match_str("QStartNoAckMode") { + let res = self.response().write_str("OK"); + self.ack_messages = false; + res + } else if req.match_str("qSupported:") { + self.response() + .write_str("multiprocess+;swbreak+;vContSupported+;PacketSize=10000;SupportedWatchpointTypes=x86_64,aarch64-bas,aarch64-mask") + } else if req + .match_some_str([ + "QThreadSuffixSupported", + "QListThreadsInStopReply", + "QEnableErrorStrings", + ]) + .is_some() + { + self.response().write_str("OK") + } else if req.match_str("qHostInfo") { + self.response().write_str(&format!( + "triple:{};endian:little;ptrsize:4;", + hex::encode("v810-unknown-vb") + )) + } else if req.match_str("qProcessInfo") { + self.response().write_str(&format!( + "pid:1;triple:{};endian:little;ptrsize:4;", + hex::encode("v810-unknown-vb") + )) + } else if req.match_str("qRegisterInfo") { + let mut get_reg_info = || { + let register = req.match_hex::()?; + REGISTERS.get(register) + }; + if let Some(reg_info) = get_reg_info() { + self.response().write_str(®_info.to_description()) + } else { + self.response() + } + } else if req.match_str("vCont?") { + self.response().write_str("vCont;c;s;") + } else if req.match_str("qC") { + // The v810 has no threads, so report that the "current thread" is 1. + self.response().write_str("QCp1.t1") + } else if req.match_str("qfThreadInfo") { + self.response().write_str("mp1.t1") + } else if req.match_str("qsThreadInfo") { + self.response().write_str("l") + } else if req.match_str("k") { + bail!("debug process was killed"); + } else if req.match_str("?") { + self.response() + .write_str(&debug_stop_reason_string(self.stop_reason)) + } else if req.match_some_str(["c", "vCont;c:"]).is_some() { + self.client + .send_command(EmulatorCommand::DebugContinue(self.sim_id)); + self.stop_reason = None; + // Don't send a response until we hit a breakpoint or get interrupted + return Ok(None); + } else if req.match_some_str(["s", "vCont;s:"]).is_some() { + self.client + .send_command(EmulatorCommand::DebugStep(self.sim_id)); + self.stop_reason = None; + // Don't send a response until we hit a breakpoint or get interrupted + return Ok(None); + } else if req.match_str("p") { + let mut read_register = || { + let register_index = req.match_hex::()?; + let register = REGISTERS.get(register_index)?.to_vb_register(); + let (tx, rx) = ::oneshot::channel(); + self.client + .send_command(EmulatorCommand::ReadRegister(self.sim_id, register, tx)); + rx.recv().ok() + }; + if let Some(value) = read_register() { + self.response().write_hex(value) + } else { + self.response() + } + } else if req.match_str("P") { + let mut write_register = || { + let register_index = req.match_hex::()?; + 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"]) { + let mut read_memory = || { + let (start, length) = parse_memory_range(&mut req)?; + let mut buf = self.memory_buf.take().unwrap_or_default(); + buf.clear(); + if length == 0 { + return Some(buf); + } + let (tx, rx) = ::oneshot::channel(); + self.client.send_command(EmulatorCommand::ReadMemory( + self.sim_id, + start, + length, + buf, + tx, + )); + rx.recv().ok() + }; + if let Some(memory) = read_memory() { + let mut res = self.response(); + if memory.is_empty() { + res = res.write_str("OK"); + } else if op == "m" { + // send the hex-encoded byte stream + for byte in &memory { + res = res.write_hex(*byte); + } + } else { + // send the raw byte stream + for byte in &memory { + res = res.write_byte(*byte); + } + } + self.memory_buf = Some(memory); + res + } else { + self.response() + } + } else if let Some(op) = req.match_some_str(["M", "X"]) { + 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 + .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") + } else { + self.response() + } + } else if req.match_str("Z") { + let mut parse_request = || { + let type_ = req.match_hex::()?; + 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::()?; + 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") + } else { + self.response() + } + } else { + // unrecognized command + self.response() + }; + Ok(Some(res)) + } + + fn response(&mut self) -> Response { + Response::new( + self.response_buf.take().unwrap_or_default(), + self.ack_messages, + ) + } +} + +impl Drop for GdbConnection { + fn drop(&mut self) { + self.client + .send_command(EmulatorCommand::StopDebugging(self.sim_id)); + } +} + +// parse a memory range into a start and a length. +fn parse_memory_range(req: &mut Request<'_>) -> Option<(u32, usize)> { + let start = req.match_hex::()?; + if !req.match_str(",") { + return None; + }; + let length = req.match_hex::()?; + 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) -> 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 +} diff --git a/src/gdbserver/registers.rs b/src/gdbserver/registers.rs new file mode 100644 index 0000000..370e52e --- /dev/null +++ b/src/gdbserver/registers.rs @@ -0,0 +1,130 @@ +use crate::emulator::VBRegister; + +pub struct RegisterInfo { + dwarf: u32, + name: &'static str, + set: &'static str, + alt_name: Option<&'static str>, + generic: Option<&'static str>, +} + +impl RegisterInfo { + pub fn to_description(&self) -> String { + let mut string = format!("name:{}", self.name); + if let Some(alt) = self.alt_name { + string.push_str(&format!(";alt-name:{}", alt)); + } + string.push_str(&format!( + ";bitsize:32;offset:{};encoding:uint;format:hex;set:{};dwarf:{}", + self.dwarf * 4, + self.set, + self.dwarf + )); + if let Some(generic) = self.generic { + string.push_str(&format!(";generic:{}", generic)); + } + string + } + + pub fn to_vb_register(&self) -> VBRegister { + match self.dwarf { + 0..32 => VBRegister::Program(self.dwarf), + 32..40 => VBRegister::System(self.dwarf - 32), + 40..42 => VBRegister::System(self.dwarf - 16), + 42..45 => VBRegister::System(self.dwarf - 13), + 45 => VBRegister::PC, + other => panic!("unexpected DWARF register {other}"), + } + } +} + +macro_rules! register { + ($set:expr, $dwarf:expr, $name:expr) => { + RegisterInfo { + dwarf: $dwarf, + name: $name, + set: $set, + alt_name: None, + generic: None, + } + }; + ($set:expr, $dwarf:expr, $name:expr, alt: $alt:expr) => { + RegisterInfo { + dwarf: $dwarf, + name: $name, + set: $set, + alt_name: Some($alt), + generic: None, + } + }; + ($set:expr, $dwarf:expr, $name:expr, generic: $generic:expr) => { + RegisterInfo { + dwarf: $dwarf, + name: $name, + set: $set, + alt_name: None, + generic: Some($generic), + } + }; + ($set:expr, $dwarf:expr, $name:expr, alt: $alt:expr, generic: $generic:expr) => { + RegisterInfo { + dwarf: $dwarf, + name: $name, + set: $set, + alt_name: Some($alt), + generic: Some($generic), + } + }; +} + +const GENERAL: &str = "General Purpose Registers"; +const SPECIAL: &str = "Special Registers"; + +pub const REGISTERS: [RegisterInfo; 46] = [ + register!(GENERAL, 0, "r0"), + register!(GENERAL, 1, "r1"), + register!(GENERAL, 2, "fp", alt: "r2", generic: "fp"), + register!(GENERAL, 3, "sp", alt: "r3", generic: "sp"), + register!(GENERAL, 4, "gp", alt: "r4"), + register!(GENERAL, 5, "tp", alt: "r5"), + register!(GENERAL, 6, "r6", generic: "arg1"), + register!(GENERAL, 7, "r7", generic: "arg2"), + register!(GENERAL, 8, "r8", generic: "arg3"), + register!(GENERAL, 9, "r9", generic: "arg4"), + register!(GENERAL, 10, "r10"), + register!(GENERAL, 11, "r11"), + register!(GENERAL, 12, "r12"), + register!(GENERAL, 13, "r13"), + register!(GENERAL, 14, "r14"), + register!(GENERAL, 15, "r15"), + register!(GENERAL, 16, "r16"), + register!(GENERAL, 17, "r17"), + register!(GENERAL, 18, "r18"), + register!(GENERAL, 19, "r19"), + register!(GENERAL, 20, "r20"), + register!(GENERAL, 21, "r21"), + register!(GENERAL, 22, "r22"), + register!(GENERAL, 23, "r23"), + register!(GENERAL, 24, "r24"), + register!(GENERAL, 25, "r25"), + register!(GENERAL, 26, "r26"), + register!(GENERAL, 27, "r27"), + register!(GENERAL, 28, "r28"), + register!(GENERAL, 29, "r29"), + register!(GENERAL, 30, "r30"), + register!(GENERAL, 31, "lp", alt: "r31", generic: "ra"), + register!(SPECIAL, 32, "eipc", alt: "sr0"), + register!(SPECIAL, 33, "eipsw", alt: "sr1"), + register!(SPECIAL, 34, "fepc", alt: "sr2"), + register!(SPECIAL, 35, "fepsw", alt: "sr3"), + register!(SPECIAL, 36, "ecr", alt: "sr4"), + register!(SPECIAL, 37, "psw", alt: "sr5", generic: "flags"), + register!(SPECIAL, 38, "pir", alt: "sr6"), + register!(SPECIAL, 39, "tkcw", alt: "sr7"), + register!(SPECIAL, 40, "chcw", alt: "sr24"), + register!(SPECIAL, 41, "adtre", alt: "sr25"), + register!(SPECIAL, 42, "sr29"), + register!(SPECIAL, 43, "sr30"), + register!(SPECIAL, 44, "sr31"), + register!(SPECIAL, 45, "pc", generic: "pc"), +]; diff --git a/src/gdbserver/request.rs b/src/gdbserver/request.rs new file mode 100644 index 0000000..86bf600 --- /dev/null +++ b/src/gdbserver/request.rs @@ -0,0 +1,202 @@ +use anyhow::{bail, Result}; +use atoi::FromRadix16; +use tokio::io::{AsyncRead, AsyncReadExt as _}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RequestKind { + Signal, + Command, +} +impl RequestKind { + fn name(self) -> &'static str { + match self { + Self::Signal => "Signal", + Self::Command => "Command", + } + } +} + +pub struct Request<'a> { + pub kind: RequestKind, + buffer: &'a [u8], +} + +impl std::fmt::Debug for Request<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut ds = f.debug_tuple(self.kind.name()); + match self.kind { + RequestKind::Signal => ds.field(&self.buffer), + RequestKind::Command => match std::str::from_utf8(self.buffer) { + Ok(str) => ds.field(&str), + Err(_) => ds.field(&self.buffer), + }, + }; + ds.finish() + } +} + +impl Request<'_> { + pub fn match_str(&mut self, prefix: &str) -> bool { + if let Some(new_buffer) = self.buffer.strip_prefix(prefix.as_bytes()) { + self.buffer = new_buffer; + return true; + } + false + } + + pub fn match_some_str<'a, I: IntoIterator>( + &mut self, + prefixes: I, + ) -> Option<&'a str> { + prefixes.into_iter().find(|&prefix| self.match_str(prefix)) + } + + pub fn match_hex(&mut self) -> Option { + match I::from_radix_16(self.buffer) { + (_, 0) => None, + (val, used) => { + self.buffer = self.buffer.split_at(used).1; + Some(val) + } + } + } + + 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 { + reader: R, + buffer: Vec, + state: RequestReadState, +} + +impl RequestSource { + pub fn new(reader: R) -> Self { + Self { + reader, + buffer: vec![], + state: RequestReadState::Header, + } + } + + pub async fn recv(&mut self) -> Result> { + let mut char = self.reader.read_u8().await?; + if matches!(self.state, RequestReadState::Start) { + self.buffer.clear(); + self.state = RequestReadState::Header; + } + if matches!(self.state, RequestReadState::Header) { + // Just ignore positive acks + while char == b'+' { + char = self.reader.read_u8().await?; + } + if char == b'-' { + bail!("no support for negative acks"); + } + if char == 0x03 { + // This is how the client "cancels an in-flight request" + self.buffer.push(char); + self.state = RequestReadState::Start; + return Ok(Request { + kind: RequestKind::Signal, + buffer: &self.buffer, + }); + } + if char != b'$' { + // Messages are supposed to start with a dollar sign + bail!("malformed message"); + } + self.state = RequestReadState::Body { + checksum: 0, + escaping: false, + }; + char = self.reader.read_u8().await?; + } + while let RequestReadState::Body { checksum, escaping } = &mut self.state { + if char == b'#' && !*escaping { + self.state = RequestReadState::Checksum { + expected: *checksum, + actual: 0, + digits: 0, + }; + char = self.reader.read_u8().await?; + break; + } + *checksum = checksum.wrapping_add(char); + + if *escaping { + // escaped character + self.buffer.push(char ^ 0x20); + *escaping = false; + } else if char == b'}' { + // next character will be escaped + *escaping = true; + } else { + self.buffer.push(char); + } + char = self.reader.read_u8().await?; + } + while let RequestReadState::Checksum { + expected, + actual, + digits, + } = &mut self.state + { + let digit = match char { + b'0'..=b'9' => char - b'0', + b'a'..=b'f' => char - b'a' + 10, + b'A'..=b'F' => char - b'A' + 10, + _ => bail!("invalid checksum"), + }; + *actual = (*actual << 4) + digit; + *digits += 1; + if *digits == 2 { + if *expected != *actual { + bail!("mismatched checksum"); + } + self.state = RequestReadState::Start; + return Ok(Request { + kind: RequestKind::Command, + buffer: &self.buffer, + }); + } + char = self.reader.read_u8().await?; + } + unreachable!(); + } +} + +enum RequestReadState { + Start, + Header, + Body { + checksum: u8, + escaping: bool, + }, + Checksum { + expected: u8, + actual: u8, + digits: u8, + }, +} diff --git a/src/gdbserver/response.rs b/src/gdbserver/response.rs new file mode 100644 index 0000000..e35bcc7 --- /dev/null +++ b/src/gdbserver/response.rs @@ -0,0 +1,63 @@ +use num_traits::ToBytes; + +pub struct Response { + buffer: Vec, + checksum: u8, +} + +impl Response { + pub fn new(mut buffer: Vec, ack: bool) -> Self { + buffer.clear(); + if ack { + buffer.push(b'+'); + } + buffer.push(b'$'); + Self { + buffer, + checksum: 0, + } + } + + pub fn write_str(self, str: &str) -> Self { + let mut me = self; + for byte in str.as_bytes() { + me = me.write_byte(*byte); + } + me + } + + pub fn write_byte(mut self, byte: u8) -> Self { + if byte == b'}' || byte == b'#' || byte == b'$' || byte == b'*' { + self.buffer.push(b'}'); + self.checksum = self.checksum.wrapping_add(b'}'); + let escaped = byte ^ 0x20; + self.buffer.push(escaped); + self.checksum = self.checksum.wrapping_add(escaped); + } else { + self.buffer.push(byte); + self.checksum = self.checksum.wrapping_add(byte); + } + self + } + + pub fn write_hex(mut self, value: T) -> Self { + for byte in value.to_le_bytes().as_ref() { + for digit in [(byte >> 4), (byte & 0xf)] { + let char = if digit > 9 { + b'a' + digit - 10 + } else { + b'0' + digit + }; + self.buffer.push(char); + self.checksum = self.checksum.wrapping_add(char); + } + } + self + } + + pub fn finish(mut self) -> Vec { + let checksum = self.checksum; + self.buffer.push(b'#'); + self.write_hex(checksum).buffer + } +} diff --git a/src/main.rs b/src/main.rs index 642f9c5..33ae747 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,17 +3,20 @@ use std::{path::PathBuf, process, time::SystemTime}; -use anyhow::Result; +use anyhow::{bail, Result}; use app::Application; use clap::Parser; use emulator::EmulatorBuilder; use thread_priority::{ThreadBuilder, ThreadPriority}; +use tracing::error; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; use winit::event_loop::{ControlFlow, EventLoop}; mod app; mod audio; mod controller; mod emulator; +mod gdbserver; mod graphics; mod input; mod persistence; @@ -21,7 +24,18 @@ mod window; #[derive(Parser)] struct Args { + /// The path to a virtual boy ROM to run. rom: Option, + /// Start a GDB/LLDB debug server on this port. + #[arg(short, long)] + debug_port: Option, +} + +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() { @@ -71,6 +85,8 @@ fn set_process_priority_to_high() -> Result<()> { } fn main() -> Result<()> { + init_logger(); + set_panic_handler(); #[cfg(windows)] @@ -79,8 +95,14 @@ fn main() -> Result<()> { let args = Args::parse(); let (mut builder, client) = EmulatorBuilder::new(); - if let Some(path) = args.rom { - builder = builder.with_rom(&path); + if let Some(path) = &args.rom { + builder = builder.with_rom(path); + } + if args.debug_port.is_some() { + if args.rom.is_none() { + bail!("to start debugging, please select a game."); + } + builder = builder.start_paused(true); } ThreadBuilder::default() @@ -89,8 +111,8 @@ fn main() -> Result<()> { .spawn_careless(move || { let mut emulator = match builder.build() { Ok(e) => e, - Err(err) => { - eprintln!("Error initializing emulator: {err}"); + Err(error) => { + error!(%error, "Error initializing emulator"); process::exit(1); } }; @@ -100,6 +122,6 @@ 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))?; + event_loop.run_app(&mut Application::new(client, proxy, args.debug_port))?; Ok(()) } diff --git a/src/window.rs b/src/window.rs index fd53acc..85048ff 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,16 +1,23 @@ pub use about::AboutWindow; use egui::{Context, ViewportBuilder, ViewportId}; pub use game::GameWindow; +pub use gdb::GdbServerWindow; pub use input::InputWindow; use winit::event::KeyEvent; +use crate::emulator::SimId; + mod about; mod game; mod game_screen; +mod gdb; mod input; pub trait AppWindow { fn viewport_id(&self) -> ViewportId; + fn sim_id(&self) -> SimId { + SimId::Player1 + } fn initial_viewport(&self) -> ViewportBuilder; fn show(&mut self, ctx: &Context); fn on_init(&mut self, render_state: &egui_wgpu::RenderState) { diff --git a/src/window/game.rs b/src/window/game.rs index 2570d07..dd83dc5 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -2,7 +2,7 @@ use std::sync::mpsc; use crate::{ app::UserEvent, - emulator::{EmulatorClient, EmulatorCommand, SimId}, + emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState}, persistence::Persistence, }; use egui::{ @@ -73,26 +73,29 @@ impl GameWindow { .pick_file(); if let Some(path) = rom { self.client - .send_command(EmulatorCommand::LoadGame(SimId::Player1, path)); + .send_command(EmulatorCommand::LoadGame(self.sim_id, path)); } ui.close_menu(); } if ui.button("Quit").clicked() { - ctx.send_viewport_cmd(ViewportCommand::Close); + let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id)); } }); ui.menu_button("Emulation", |ui| { - let has_game = self.client.has_game(self.sim_id); - if self.client.is_running(self.sim_id) { - if ui.add_enabled(has_game, Button::new("Pause")).clicked() { + let state = self.client.emulator_state(); + let is_ready = self.client.sim_state(self.sim_id) == SimState::Ready; + let can_pause = is_ready && state == EmulatorState::Running; + let can_resume = is_ready && state == EmulatorState::Paused; + if state == EmulatorState::Running { + if ui.add_enabled(can_pause, Button::new("Pause")).clicked() { self.client.send_command(EmulatorCommand::Pause); ui.close_menu(); } - } else if ui.add_enabled(has_game, Button::new("Resume")).clicked() { + } else if ui.add_enabled(can_resume, Button::new("Resume")).clicked() { self.client.send_command(EmulatorCommand::Resume); ui.close_menu(); } - if ui.add_enabled(has_game, Button::new("Reset")).clicked() { + if ui.add_enabled(is_ready, Button::new("Reset")).clicked() { self.client .send_command(EmulatorCommand::Reset(self.sim_id)); ui.close_menu(); @@ -100,8 +103,9 @@ impl GameWindow { }); ui.menu_button("Options", |ui| self.show_options_menu(ctx, ui)); ui.menu_button("Multiplayer", |ui| { + let has_player_2 = self.client.sim_state(SimId::Player2) != SimState::Uninitialized; if self.sim_id == SimId::Player1 - && !self.client.has_player_2() + && !has_player_2 && ui.button("Open Player 2").clicked() { self.client @@ -109,7 +113,7 @@ impl GameWindow { self.proxy.send_event(UserEvent::OpenPlayer2).unwrap(); ui.close_menu(); } - if self.client.has_player_2() { + if has_player_2 { let linked = self.client.are_sims_linked(); if linked && ui.button("Unlink").clicked() { self.client.send_command(EmulatorCommand::Unlink); @@ -121,6 +125,14 @@ impl GameWindow { } } }); + ui.menu_button("Tools", |ui| { + if ui.button("GDB Server").clicked() { + self.proxy + .send_event(UserEvent::OpenDebugger(self.sim_id)) + .unwrap(); + ui.close_menu(); + } + }); ui.menu_button("About", |ui| { self.proxy.send_event(UserEvent::OpenAbout).unwrap(); ui.close_menu(); @@ -199,7 +211,7 @@ impl GameWindow { let color_str = |color: Color32| { format!("{:02x}{:02x}{:02x}", color.r(), color.g(), color.b()) }; - let is_running = self.client.is_running(self.sim_id); + let is_running = self.client.emulator_state() == EmulatorState::Running; if is_running { self.client.send_command(EmulatorCommand::Pause); } @@ -306,6 +318,10 @@ impl AppWindow for GameWindow { } } + fn sim_id(&self) -> SimId { + self.sim_id + } + fn initial_viewport(&self) -> ViewportBuilder { ViewportBuilder::default() .with_title("Lemur") @@ -369,6 +385,7 @@ impl AppWindow for GameWindow { if self.sim_id == SimId::Player2 { self.client.send_command(EmulatorCommand::StopSecondSim); } + let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id)); } } diff --git a/src/window/gdb.rs b/src/window/gdb.rs new file mode 100644 index 0000000..e89de02 --- /dev/null +++ b/src/window/gdb.rs @@ -0,0 +1,111 @@ +use egui::{Button, CentralPanel, TextEdit, ViewportBuilder, ViewportId}; +use winit::event_loop::EventLoopProxy; + +use crate::{ + app::UserEvent, + emulator::{EmulatorClient, SimId, SimState}, + gdbserver::{GdbServer, GdbServerStatus}, +}; + +use super::AppWindow; + +pub struct GdbServerWindow { + sim_id: SimId, + client: EmulatorClient, + port_str: String, + connected: bool, + quit_on_disconnect: bool, + server: GdbServer, + proxy: EventLoopProxy, +} + +impl GdbServerWindow { + pub fn new(sim_id: SimId, client: EmulatorClient, proxy: EventLoopProxy) -> Self { + Self { + sim_id, + client: client.clone(), + port_str: (8080 + sim_id.to_index()).to_string(), + connected: false, + quit_on_disconnect: false, + server: GdbServer::new(sim_id, client), + proxy, + } + } + + pub fn launch(&mut self, port: u16) { + self.server.stop(); + self.port_str = port.to_string(); + self.quit_on_disconnect = true; + self.server.start(port); + self.connected = true; + } +} + +impl AppWindow for GdbServerWindow { + fn viewport_id(&self) -> ViewportId { + ViewportId::from_hash_of(format!("Debugger-{}", self.sim_id)) + } + + fn sim_id(&self) -> SimId { + self.sim_id + } + + fn initial_viewport(&self) -> ViewportBuilder { + ViewportBuilder::default() + .with_title(format!("GDB Server ({})", self.sim_id)) + .with_inner_size((300.0, 200.0)) + } + + fn show(&mut self, ctx: &egui::Context) { + let port_num: Option = self.port_str.parse().ok(); + let status = self.server.status(); + CentralPanel::default().show(ctx, |ui| { + ui.horizontal(|ui| { + if port_num.is_none() { + let style = ui.style_mut(); + let error = style.visuals.error_fg_color; + style.visuals.widgets.active.bg_stroke.color = error; + style.visuals.widgets.hovered.bg_stroke.color = error; + } + ui.label("Port"); + let port_editor = TextEdit::singleline(&mut self.port_str).desired_width(100.0); + ui.add_enabled(!status.running(), port_editor); + ui.checkbox(&mut self.quit_on_disconnect, "Quit on disconnect"); + }); + + if !status.running() { + if self.connected && self.quit_on_disconnect { + self.proxy.send_event(UserEvent::Quit(self.sim_id)).unwrap(); + } else { + self.connected = false; + } + let start_button = Button::new("Start"); + 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(); + self.server.start(port); + self.connected = true; + } + } else { + let stop_button = Button::new("Stop"); + if ui.add(stop_button).clicked() { + self.server.stop(); + } + } + + match &status { + GdbServerStatus::Stopped => {} + GdbServerStatus::Connecting => { + ui.label("Connecting..."); + } + GdbServerStatus::Running => { + ui.label("Running"); + } + GdbServerStatus::Error(message) => { + ui.label(message); + } + } + }); + } +}