From 47a05968fbcba726f44a70dbcc7f577e79000394 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 30 Dec 2024 21:55:30 -0500 Subject: [PATCH 01/32] Create tcp server --- Cargo.lock | 94 +++++++++++++++++++++++++++ Cargo.toml | 1 + src/app.rs | 7 ++- src/emulator.rs | 9 +++ src/gdbserver.rs | 154 +++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + src/window.rs | 2 + src/window/game.rs | 8 +++ src/window/gdb.rs | 80 +++++++++++++++++++++++ 9 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 src/gdbserver.rs create mode 100644 src/window/gdb.rs diff --git a/Cargo.lock b/Cargo.lock index 8eeb29e..e7401f9 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" @@ -400,6 +409,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 +1330,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" @@ -1736,6 +1766,7 @@ dependencies = [ "serde", "serde_json", "thread-priority", + "tokio", "wgpu", "windows 0.58.0", "winit", @@ -1902,6 +1933,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" @@ -2276,6 +2318,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" @@ -2696,6 +2747,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" @@ -2918,6 +2975,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" @@ -3070,6 +3137,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" diff --git a/Cargo.toml b/Cargo.toml index 54757fc..e5ea2be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ rubato = "0.16" serde = { version = "1", features = ["derive"] } serde_json = "1" thread-priority = "1" +tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync"] } wgpu = "23" winit = { version = "0.30", features = ["serde"] } diff --git a/src/app.rs b/src/app.rs index 8c83a07..ec9fddc 100644 --- a/src/app.rs +++ b/src/app.rs @@ -18,7 +18,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 { @@ -183,6 +183,10 @@ 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.open(event_loop, Box::new(debugger)); + } UserEvent::OpenInput => { let input = InputWindow::new(self.mappings.clone()); self.open(event_loop, Box::new(input)); @@ -374,6 +378,7 @@ impl Drop for Viewport { pub enum UserEvent { GamepadEvent(gilrs::Event), OpenAbout, + OpenDebugger(SimId), OpenInput, OpenPlayer2, } diff --git a/src/emulator.rs b/src/emulator.rs index 814149b..8748a10 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + fmt::Display, fs::{self, File}, io::{Read, Seek, SeekFrom, Write}, path::{Path, PathBuf}, @@ -45,6 +46,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, diff --git a/src/gdbserver.rs b/src/gdbserver.rs new file mode 100644 index 0000000..e763d1a --- /dev/null +++ b/src/gdbserver.rs @@ -0,0 +1,154 @@ +use std::{ + error::Error, + sync::{Arc, Mutex}, + thread, +}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt as _, BufReader, BufWriter}, + net::{ + tcp::{OwnedReadHalf, OwnedWriteHalf}, + TcpListener, TcpStream, + }, + select, + sync::oneshot, +}; + +use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; + +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) { + 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, port, &status) => {} + _ = rx => { + *status.lock().unwrap() = GdbServerStatus::Stopped; + } + } + }) + }); + } + + pub fn stop(&mut self) { + if let Some(killer) = self.killer.take() { + let _ = killer.send(()); + } + } +} + +async fn run_server( + sim_id: SimId, + client: EmulatorClient, + port: u16, + status: &Mutex, +) { + client.send_command(EmulatorCommand::Pause); + let Some(stream) = try_connect(port, status).await else { + return; + }; + let connection = GdbConnection::new(sim_id, client, stream); + match connection.run().await { + Ok(()) => { + *status.lock().unwrap() = GdbServerStatus::Stopped; + } + Err(error) => { + *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) => { + *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) => { + *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, + stream_in: BufReader, + stream_out: BufWriter, +} + +impl GdbConnection { + fn new(sim_id: SimId, client: EmulatorClient, stream: TcpStream) -> Self { + let (rx, tx) = stream.into_split(); + Self { + sim_id, + client, + stream_in: BufReader::new(rx), + stream_out: BufWriter::new(tx), + } + } + async fn run(mut self) -> Result<(), Box> { + println!("Connected for {}", self.sim_id); + self.client.send_command(EmulatorCommand::Resume); + loop { + let byte = self.read_byte().await?; + println!("{byte}"); + self.stream_out.write_u8(byte).await?; + } + } + + async fn read_byte(&mut self) -> std::io::Result { + self.stream_in.read_u8().await + } +} diff --git a/src/main.rs b/src/main.rs index 642f9c5..e9abf8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ mod app; mod audio; mod controller; mod emulator; +mod gdbserver; mod graphics; mod input; mod persistence; diff --git a/src/window.rs b/src/window.rs index fd53acc..b60e9c3 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,12 +1,14 @@ 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; mod about; mod game; mod game_screen; +mod gdb; mod input; pub trait AppWindow { diff --git a/src/window/game.rs b/src/window/game.rs index 2570d07..ced5d1d 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -121,6 +121,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(); diff --git a/src/window/gdb.rs b/src/window/gdb.rs new file mode 100644 index 0000000..c96c169 --- /dev/null +++ b/src/window/gdb.rs @@ -0,0 +1,80 @@ +use egui::{Button, CentralPanel, TextEdit, ViewportBuilder, ViewportId}; + +use crate::{ + emulator::{EmulatorClient, SimId}, + gdbserver::{GdbServer, GdbServerStatus}, +}; + +use super::AppWindow; + +pub struct GdbServerWindow { + sim_id: SimId, + port_str: String, + server: GdbServer, +} + +impl GdbServerWindow { + pub fn new(sim_id: SimId, client: EmulatorClient) -> Self { + Self { + sim_id, + port_str: (8080 + sim_id.to_index()).to_string(), + server: GdbServer::new(sim_id, client), + } + } +} + +impl AppWindow for GdbServerWindow { + fn viewport_id(&self) -> ViewportId { + ViewportId::from_hash_of(format!("Debugger-{}", 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); + }); + + if !status.running() { + let start_button = Button::new("Start"); + if ui.add_enabled(port_num.is_some(), start_button).clicked() { + let port = port_num.unwrap(); + self.server.start(port); + } + } 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); + } + } + }); + } +} -- 2.40.1 From 0a5e223ba7eed7de87269e4c7a6fdcd48364b3a6 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 30 Dec 2024 23:38:28 -0500 Subject: [PATCH 02/32] Handle one message --- src/gdbserver.rs | 135 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 130 insertions(+), 5 deletions(-) diff --git a/src/gdbserver.rs b/src/gdbserver.rs index e763d1a..a449fe8 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -1,5 +1,5 @@ +use anyhow::{bail, Result}; use std::{ - error::Error, sync::{Arc, Mutex}, thread, }; @@ -126,6 +126,7 @@ struct GdbConnection { client: EmulatorClient, stream_in: BufReader, stream_out: BufWriter, + ack_messages: bool, } impl GdbConnection { @@ -136,19 +137,143 @@ impl GdbConnection { client, stream_in: BufReader::new(rx), stream_out: BufWriter::new(tx), + ack_messages: true, } } - async fn run(mut self) -> Result<(), Box> { + async fn run(mut self) -> Result<()> { println!("Connected for {}", self.sim_id); self.client.send_command(EmulatorCommand::Resume); loop { - let byte = self.read_byte().await?; - println!("{byte}"); - self.stream_out.write_u8(byte).await?; + let message = self.read_message().await?; + println!("received {:?}", message); + + let mut res = ResponseWriter::new(&mut self.stream_out); + res.init(self.ack_messages).await?; + + let body = match &message { + Message::String(str) => str.as_str(), + Message::Signal => { + // TODO: handle this + res.send().await?; + continue; + } + }; + + if body == "QStartNoAckMode" { + self.ack_messages = false; + res.send_ok().await?; + } else { + // unrecognized command + res.send().await?; + } } } + async fn read_message(&mut self) -> Result { + let mut char = self.read_byte().await?; + while char == b'+' { + // just ignore positive acks + char = self.read_byte().await?; + } + if char == b'-' { + bail!("no support for negative acks"); + } + if char == 0x03 { + // This is how the client "cancels an in-flight request" + return Ok(Message::Signal); + } + if char != b'$' { + // Messages are supposed to start with a dollar sign + bail!("malformed message"); + } + + // now read the body + let mut checksum = 0u8; + let mut body = vec![]; + char = self.read_byte().await?; + while char != b'#' { + if char == b'}' { + // escape character + checksum = checksum.wrapping_add(char); + char = self.read_byte().await?; + checksum = checksum.wrapping_add(char); + body.push(char ^ 0x20); + } else { + checksum = checksum.wrapping_add(char); + body.push(char); + } + char = self.read_byte().await?; + } + + let mut checksum_bytes = [b'0'; 2]; + self.stream_in.read_exact(&mut checksum_bytes).await?; + let checksum_str = std::str::from_utf8(&checksum_bytes)?; + let real_checksum = u8::from_str_radix(checksum_str, 16)?; + if checksum != real_checksum { + bail!("invalid checksum"); + } + + let string = String::from_utf8(body)?; + Ok(Message::String(string)) + } + async fn read_byte(&mut self) -> std::io::Result { self.stream_in.read_u8().await } } + +struct ResponseWriter<'a> { + inner: &'a mut BufWriter, + checksum: u8, +} +impl<'a> ResponseWriter<'a> { + fn new(inner: &'a mut BufWriter) -> Self { + Self { inner, checksum: 0 } + } + + async fn init(&mut self, ack: bool) -> std::io::Result<()> { + if ack { + self.inner.write_u8(b'+').await?; + } + self.inner.write_u8(b'$').await + } + + async fn write_str(&mut self, str: &str) -> std::io::Result<()> { + for byte in str.bytes() { + self.checksum = self.checksum.wrapping_add(byte); + } + self.inner.write_all(str.as_bytes()).await + } + + async fn write_hex_u8(&mut self, value: u8) -> std::io::Result<()> { + for digit in [(value >> 4), (value & 0xf)] { + let char = if digit > 9 { + b'a' + digit - 10 + } else { + b'0' + digit + }; + self.checksum = self.checksum.wrapping_add(char); + self.inner.write_u8(char).await?; + } + Ok(()) + } + + async fn send_ok(mut self) -> std::io::Result<()> { + self.write_str("OK").await?; + self.send().await + } + + async fn send(mut self) -> std::io::Result<()> { + let final_checksum = self.checksum; + self.inner.write_u8(b'#').await?; + self.write_hex_u8(final_checksum).await?; + println!("{:?}", std::str::from_utf8(self.inner.buffer())); + self.inner.flush().await + } +} + +#[derive(Debug)] +enum Message { + String(String), + Signal, +} -- 2.40.1 From 014b9ebbfa1846003a9d40f200e86f3acbcf5527 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 30 Dec 2024 23:44:06 -0500 Subject: [PATCH 03/32] Handle the kill packet --- src/gdbserver.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gdbserver.rs b/src/gdbserver.rs index a449fe8..ae01a40 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -162,6 +162,8 @@ impl GdbConnection { if body == "QStartNoAckMode" { self.ack_messages = false; res.send_ok().await?; + } else if body == "k" { + return Ok(()); } else { // unrecognized command res.send().await?; -- 2.40.1 From b94ae4d5869dec715a84b9bbd9ac1e8020a47dba Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Tue, 31 Dec 2024 00:08:45 -0500 Subject: [PATCH 04/32] Add support for moar commands --- Cargo.lock | 1 + Cargo.toml | 1 + src/gdbserver.rs | 13 +++++++++++++ 3 files changed, 15 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index e7401f9..3c80d98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1754,6 +1754,7 @@ dependencies = [ "egui-winit", "egui_extras", "gilrs", + "hex", "image", "itertools", "num-derive", diff --git a/Cargo.toml b/Cargo.toml index e5ea2be..f7e7434 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,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" diff --git a/src/gdbserver.rs b/src/gdbserver.rs index ae01a40..2d90c11 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -162,6 +162,19 @@ impl GdbConnection { if body == "QStartNoAckMode" { self.ack_messages = false; res.send_ok().await?; + } else if body.starts_with("qSupported:") { + res.write_str("multiprocess+;swbreak+;vContSupported+") + .await?; + res.send().await?; + } else if body == "QThreadSuffixSupported" || body == "QListThreadsInStopReply" { + res.send_ok().await?; + } else if body == "qHostInfo" || body == "qProcessInfo" { + res.write_str(&format!( + "triple:{};endian:little;ptrsize:4;", + hex::encode("v810-unknown-vb") + )) + .await?; + res.send().await?; } else if body == "k" { return Ok(()); } else { -- 2.40.1 From 2c5084d31733fc59a8fd4423cef28a0e6f0d2728 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Wed, 1 Jan 2025 13:13:01 -0500 Subject: [PATCH 05/32] CLI argument to start the server at startup --- src/app.rs | 17 +++++++++++++---- src/main.rs | 6 +++++- src/window/gdb.rs | 6 ++++++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/app.rs b/src/app.rs index ec9fddc..569a798 100644 --- a/src/app.rs +++ b/src/app.rs @@ -41,10 +41,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 +68,7 @@ impl Application { persistence, viewports: HashMap::new(), focused: None, + init_debug_port: debug_port, } } @@ -86,9 +92,12 @@ impl ApplicationHandler for Application { 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)); + if let Some(port) = self.init_debug_port { + let mut server = GdbServerWindow::new(SimId::Player1, self.client.clone()); + server.start(port); + self.open(event_loop, Box::new(server)); + } } fn window_event( diff --git a/src/main.rs b/src/main.rs index e9abf8e..0d97e7d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,11 @@ 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 set_panic_handler() { @@ -101,6 +105,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/gdb.rs b/src/window/gdb.rs index c96c169..9d6fa0f 100644 --- a/src/window/gdb.rs +++ b/src/window/gdb.rs @@ -21,6 +21,12 @@ impl GdbServerWindow { server: GdbServer::new(sim_id, client), } } + + pub fn start(&mut self, port: u16) { + self.server.stop(); + self.port_str = port.to_string(); + self.server.start(port); + } } impl AppWindow for GdbServerWindow { -- 2.40.1 From 24487b21b7df00bd35c24889b9e85dc4d90128aa Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Wed, 1 Jan 2025 14:56:54 -0500 Subject: [PATCH 06/32] Support pausing and resuming --- src/gdbserver.rs | 66 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/src/gdbserver.rs b/src/gdbserver.rs index 2d90c11..4f0a78d 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -71,7 +71,6 @@ async fn run_server( port: u16, status: &Mutex, ) { - client.send_command(EmulatorCommand::Pause); let Some(stream) = try_connect(port, status).await else { return; }; @@ -142,43 +141,88 @@ impl GdbConnection { } async fn run(mut self) -> Result<()> { println!("Connected for {}", self.sim_id); - self.client.send_command(EmulatorCommand::Resume); + self.client.send_command(EmulatorCommand::Pause); loop { let message = self.read_message().await?; println!("received {:?}", message); - let mut res = ResponseWriter::new(&mut self.stream_out); - res.init(self.ack_messages).await?; + //let mut res = ResponseWriter::new(&mut self.stream_out); + //res.init(self.ack_messages).await?; let body = match &message { Message::String(str) => str.as_str(), Message::Signal => { - // TODO: handle this + self.client.send_command(EmulatorCommand::Pause); + let mut res = self.respond().await?; + res.write_str("T05;thread:p1.t1;threads:p1.t1;reason:trap;") + .await?; res.send().await?; continue; } }; if body == "QStartNoAckMode" { + let mut res = ResponseWriter::new(&mut self.stream_out); + res.init(self.ack_messages).await?; self.ack_messages = false; res.send_ok().await?; } else if body.starts_with("qSupported:") { + let mut res = self.respond().await?; res.write_str("multiprocess+;swbreak+;vContSupported+") .await?; res.send().await?; - } else if body == "QThreadSuffixSupported" || body == "QListThreadsInStopReply" { + } else if body == "QThreadSuffixSupported" + || body == "QListThreadsInStopReply" + || body == "QEnableErrorStrings" + { + let res = self.respond().await?; res.send_ok().await?; - } else if body == "qHostInfo" || body == "qProcessInfo" { + } else if body == "qHostInfo" { + let mut res = self.respond().await?; res.write_str(&format!( "triple:{};endian:little;ptrsize:4;", hex::encode("v810-unknown-vb") )) .await?; res.send().await?; + } else if body == "qProcessInfo" { + let mut res = self.respond().await?; + res.write_str(&format!( + "pid:1;triple:{};endian:little;ptrsize:4;", + hex::encode("v810-unknown-vb") + )) + .await?; + res.send().await?; + } else if body == "vCont?" { + let mut res = self.respond().await?; + res.write_str("vCont;c;").await?; + res.send().await?; + } else if body == "qC" { + // The v810 has no threads, so report that the "current thread" is 1. + let mut res = self.respond().await?; + res.write_str("QCp1.t1").await?; + res.send().await?; + } else if body == "qfThreadInfo" { + let mut res = self.respond().await?; + res.write_str("mp1.t1").await?; + res.send().await?; + } else if body == "qsThreadInfo" { + let mut res = self.respond().await?; + res.write_str("l").await?; + res.send().await?; } else if body == "k" { - return Ok(()); + bail!("debug process was killed"); + } else if body == "?" { + let mut res = self.respond().await?; + res.write_str("T00;thread:p1.t1;threads:p1.t1;").await?; + res.send().await?; + } else if body == "c" || body.starts_with("vCont;c:") { + // Continue running the game until we're interrupted again + self.client.send_command(EmulatorCommand::Resume); + continue; } else { // unrecognized command + let res = self.respond().await?; res.send().await?; } } @@ -235,6 +279,12 @@ impl GdbConnection { async fn read_byte(&mut self) -> std::io::Result { self.stream_in.read_u8().await } + + async fn respond(&mut self) -> std::io::Result> { + let mut res = ResponseWriter::new(&mut self.stream_out); + res.init(self.ack_messages).await?; + Ok(res) + } } struct ResponseWriter<'a> { -- 2.40.1 From 9519897711ec7bf2d5ff48c93dfa0656806cd5f6 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Wed, 1 Jan 2025 17:02:14 -0500 Subject: [PATCH 07/32] Separate GDB request and response into structs --- src/gdbserver.rs | 251 +++++++++++--------------------------- src/gdbserver/request.rs | 84 +++++++++++++ src/gdbserver/response.rs | 45 +++++++ 3 files changed, 201 insertions(+), 179 deletions(-) create mode 100644 src/gdbserver/request.rs create mode 100644 src/gdbserver/response.rs diff --git a/src/gdbserver.rs b/src/gdbserver.rs index 4f0a78d..97171e0 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -1,10 +1,12 @@ use anyhow::{bail, Result}; +use request::{Request, RequestKind}; +use response::Response; use std::{ sync::{Arc, Mutex}, thread, }; use tokio::{ - io::{AsyncReadExt, AsyncWriteExt as _, BufReader, BufWriter}, + io::{AsyncWriteExt as _, BufReader, BufWriter}, net::{ tcp::{OwnedReadHalf, OwnedWriteHalf}, TcpListener, TcpStream, @@ -15,6 +17,9 @@ use tokio::{ use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; +mod request; +mod response; + pub struct GdbServer { sim_id: SimId, client: EmulatorClient, @@ -126,6 +131,8 @@ struct GdbConnection { stream_in: BufReader, stream_out: BufWriter, ack_messages: bool, + request_buf: Vec, + response_buf: Option>, } impl GdbConnection { @@ -137,208 +144,94 @@ impl GdbConnection { stream_in: BufReader::new(rx), stream_out: BufWriter::new(tx), ack_messages: true, + request_buf: vec![], + response_buf: None, } } async fn run(mut self) -> Result<()> { println!("Connected for {}", self.sim_id); self.client.send_command(EmulatorCommand::Pause); loop { - let message = self.read_message().await?; - println!("received {:?}", message); + let mut req = Request::read(&mut self.stream_in, &mut self.request_buf).await?; + println!("received {:?}", req); - //let mut res = ResponseWriter::new(&mut self.stream_out); - //res.init(self.ack_messages).await?; + if req.kind == RequestKind::Signal { + self.client.send_command(EmulatorCommand::Pause); + let res = self + .response() + .write_str("T05;thread:p1.t1;threads:p1.t1;reason:trap;"); + self.send(res).await?; + continue; + } - let body = match &message { - Message::String(str) => str.as_str(), - Message::Signal => { - self.client.send_command(EmulatorCommand::Pause); - let mut res = self.respond().await?; - res.write_str("T05;thread:p1.t1;threads:p1.t1;reason:trap;") - .await?; - res.send().await?; - continue; - } - }; - - if body == "QStartNoAckMode" { - let mut res = ResponseWriter::new(&mut self.stream_out); - res.init(self.ack_messages).await?; + if req.match_str("QStartNoAckMode") { + let res = self.response().write_str("OK"); + self.send(res).await?; self.ack_messages = false; - res.send_ok().await?; - } else if body.starts_with("qSupported:") { - let mut res = self.respond().await?; - res.write_str("multiprocess+;swbreak+;vContSupported+") - .await?; - res.send().await?; - } else if body == "QThreadSuffixSupported" - || body == "QListThreadsInStopReply" - || body == "QEnableErrorStrings" + } else if req.match_str("qSupported:") { + let res = self + .response() + .write_str("multiprocess+;swbreak+;vContSupported+"); + self.send(res).await?; + } else if req.match_str("QThreadSuffixSupported") + || req.match_str("QListThreadsInStopReply") + || req.match_str("QEnableErrorStrings") { - let res = self.respond().await?; - res.send_ok().await?; - } else if body == "qHostInfo" { - let mut res = self.respond().await?; - res.write_str(&format!( + let res = self.response().write_str("OK"); + self.send(res).await?; + } else if req.match_str("qHostInfo") { + let res = self.response().write_str(&format!( "triple:{};endian:little;ptrsize:4;", hex::encode("v810-unknown-vb") - )) - .await?; - res.send().await?; - } else if body == "qProcessInfo" { - let mut res = self.respond().await?; - res.write_str(&format!( + )); + self.send(res).await?; + } else if req.match_str("qProcessInfo") { + let res = self.response().write_str(&format!( "pid:1;triple:{};endian:little;ptrsize:4;", hex::encode("v810-unknown-vb") - )) - .await?; - res.send().await?; - } else if body == "vCont?" { - let mut res = self.respond().await?; - res.write_str("vCont;c;").await?; - res.send().await?; - } else if body == "qC" { + )); + self.send(res).await?; + } else if req.match_str("vCont?") { + let res = self.response().write_str("vCont;c;"); + self.send(res).await?; + } else if req.match_str("qC") { // The v810 has no threads, so report that the "current thread" is 1. - let mut res = self.respond().await?; - res.write_str("QCp1.t1").await?; - res.send().await?; - } else if body == "qfThreadInfo" { - let mut res = self.respond().await?; - res.write_str("mp1.t1").await?; - res.send().await?; - } else if body == "qsThreadInfo" { - let mut res = self.respond().await?; - res.write_str("l").await?; - res.send().await?; - } else if body == "k" { + let res = self.response().write_str("QCp1.t1"); + self.send(res).await?; + } else if req.match_str("qfThreadInfo") { + let res = self.response().write_str("mp1.t1"); + self.send(res).await?; + } else if req.match_str("qsThreadInfo") { + let res = self.response().write_str("l"); + self.send(res).await?; + } else if req.match_str("k") { bail!("debug process was killed"); - } else if body == "?" { - let mut res = self.respond().await?; - res.write_str("T00;thread:p1.t1;threads:p1.t1;").await?; - res.send().await?; - } else if body == "c" || body.starts_with("vCont;c:") { - // Continue running the game until we're interrupted again + } else if req.match_str("?") { + let res = self.response().write_str("T00;thread:p1.t1;threads:p1.t1;"); + self.send(res).await?; + } else if req.match_str("c") || req.match_str("vCont;c:") { self.client.send_command(EmulatorCommand::Resume); - continue; + // Don't send a response until we hit a breakpoint or get interrupted } else { // unrecognized command - let res = self.respond().await?; - res.send().await?; + let res = self.response(); + self.send(res).await?; } } } - async fn read_message(&mut self) -> Result { - let mut char = self.read_byte().await?; - while char == b'+' { - // just ignore positive acks - char = self.read_byte().await?; - } - if char == b'-' { - bail!("no support for negative acks"); - } - if char == 0x03 { - // This is how the client "cancels an in-flight request" - return Ok(Message::Signal); - } - if char != b'$' { - // Messages are supposed to start with a dollar sign - bail!("malformed message"); - } - - // now read the body - let mut checksum = 0u8; - let mut body = vec![]; - char = self.read_byte().await?; - while char != b'#' { - if char == b'}' { - // escape character - checksum = checksum.wrapping_add(char); - char = self.read_byte().await?; - checksum = checksum.wrapping_add(char); - body.push(char ^ 0x20); - } else { - checksum = checksum.wrapping_add(char); - body.push(char); - } - char = self.read_byte().await?; - } - - let mut checksum_bytes = [b'0'; 2]; - self.stream_in.read_exact(&mut checksum_bytes).await?; - let checksum_str = std::str::from_utf8(&checksum_bytes)?; - let real_checksum = u8::from_str_radix(checksum_str, 16)?; - if checksum != real_checksum { - bail!("invalid checksum"); - } - - let string = String::from_utf8(body)?; - Ok(Message::String(string)) + fn response(&mut self) -> Response { + Response::new( + self.response_buf.take().unwrap_or_default(), + self.ack_messages, + ) } - async fn read_byte(&mut self) -> std::io::Result { - self.stream_in.read_u8().await - } - - async fn respond(&mut self) -> std::io::Result> { - let mut res = ResponseWriter::new(&mut self.stream_out); - res.init(self.ack_messages).await?; - Ok(res) + async fn send(&mut self, res: Response) -> std::io::Result<()> { + let buffer = res.finish(); + println!("{:?}", std::str::from_utf8(&buffer)); + self.stream_out.write_all(&buffer).await?; + self.response_buf = Some(buffer); + self.stream_out.flush().await } } - -struct ResponseWriter<'a> { - inner: &'a mut BufWriter, - checksum: u8, -} -impl<'a> ResponseWriter<'a> { - fn new(inner: &'a mut BufWriter) -> Self { - Self { inner, checksum: 0 } - } - - async fn init(&mut self, ack: bool) -> std::io::Result<()> { - if ack { - self.inner.write_u8(b'+').await?; - } - self.inner.write_u8(b'$').await - } - - async fn write_str(&mut self, str: &str) -> std::io::Result<()> { - for byte in str.bytes() { - self.checksum = self.checksum.wrapping_add(byte); - } - self.inner.write_all(str.as_bytes()).await - } - - async fn write_hex_u8(&mut self, value: u8) -> std::io::Result<()> { - for digit in [(value >> 4), (value & 0xf)] { - let char = if digit > 9 { - b'a' + digit - 10 - } else { - b'0' + digit - }; - self.checksum = self.checksum.wrapping_add(char); - self.inner.write_u8(char).await?; - } - Ok(()) - } - - async fn send_ok(mut self) -> std::io::Result<()> { - self.write_str("OK").await?; - self.send().await - } - - async fn send(mut self) -> std::io::Result<()> { - let final_checksum = self.checksum; - self.inner.write_u8(b'#').await?; - self.write_hex_u8(final_checksum).await?; - println!("{:?}", std::str::from_utf8(self.inner.buffer())); - self.inner.flush().await - } -} - -#[derive(Debug)] -enum Message { - String(String), - Signal, -} diff --git a/src/gdbserver/request.rs b/src/gdbserver/request.rs new file mode 100644 index 0000000..63f66e8 --- /dev/null +++ b/src/gdbserver/request.rs @@ -0,0 +1,84 @@ +use anyhow::{bail, Result}; +use tokio::io::{AsyncRead, AsyncReadExt as _}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RequestKind { + Signal, + Command, +} + +#[derive(Debug)] +pub struct Request<'a> { + pub kind: RequestKind, + body: &'a str, +} + +impl<'a> Request<'a> { + pub async fn read( + reader: &mut R, + buffer: &'a mut Vec, + ) -> Result { + buffer.clear(); + + let mut char = reader.read_u8().await?; + while char == b'+' { + // just ignore positive acks + char = 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" + buffer.push(char); + let body = std::str::from_utf8(buffer)?; + return Ok(Self { + kind: RequestKind::Signal, + body, + }); + } + if char != b'$' { + // Messages are supposed to start with a dollar sign + bail!("malformed message"); + } + + // now read the body + let mut checksum = 0u8; + char = reader.read_u8().await?; + while char != b'#' { + if char == b'}' { + // escape character + checksum = checksum.wrapping_add(char); + char = reader.read_u8().await?; + checksum = checksum.wrapping_add(char); + buffer.push(char ^ 0x20); + } else { + checksum = checksum.wrapping_add(char); + buffer.push(char); + } + char = reader.read_u8().await?; + } + + let mut checksum_bytes = [b'0'; 2]; + reader.read_exact(&mut checksum_bytes).await?; + let checksum_str = std::str::from_utf8(&checksum_bytes)?; + let real_checksum = u8::from_str_radix(checksum_str, 16)?; + if checksum != real_checksum { + bail!("invalid checksum"); + } + + let body = std::str::from_utf8(buffer)?; + Ok(Self { + kind: RequestKind::Command, + body, + }) + } + + pub fn match_str(&mut self, prefix: &str) -> bool { + if let Some(new_body) = self.body.strip_prefix(prefix) { + self.body = new_body; + return true; + } + false + } +} diff --git a/src/gdbserver/response.rs b/src/gdbserver/response.rs new file mode 100644 index 0000000..fdd9220 --- /dev/null +++ b/src/gdbserver/response.rs @@ -0,0 +1,45 @@ +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(mut self, str: &str) -> Self { + for char in str.as_bytes() { + self.buffer.push(*char); + self.checksum = self.checksum.wrapping_add(*char); + } + self + } + + pub fn write_hex_u8(mut self, value: u8) -> Self { + for digit in [(value >> 4), (value & 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_u8(checksum).buffer + } +} -- 2.40.1 From d01653840807ec1ee3b173944fc20adc35a0b535 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Wed, 1 Jan 2025 18:15:41 -0500 Subject: [PATCH 08/32] Stub out reading registers and memory --- Cargo.lock | 10 ++++++++ Cargo.toml | 1 + src/gdbserver.rs | 27 ++++++++++++++++++++ src/gdbserver/request.rs | 53 +++++++++++++++++++++++++++++++-------- src/gdbserver/response.rs | 30 ++++++++++++---------- 5 files changed, 97 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c80d98..4b3eb56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -397,6 +397,15 @@ 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-waker" version = "1.1.2" @@ -1742,6 +1751,7 @@ name = "lemur" version = "0.2.5" dependencies = [ "anyhow", + "atoi", "bitflags 2.6.0", "bytemuck", "cc", diff --git a/Cargo.toml b/Cargo.toml index f7e7434..0e1b3fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" [dependencies] anyhow = "1" +atoi = "2" bitflags = { version = "2", features = ["serde"] } bytemuck = { version = "1", features = ["derive"] } clap = { version = "4", features = ["derive"] } diff --git a/src/gdbserver.rs b/src/gdbserver.rs index 97171e0..ae11619 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -212,6 +212,33 @@ impl GdbConnection { } else if req.match_str("c") || req.match_str("vCont;c:") { self.client.send_command(EmulatorCommand::Resume); // Don't send a response until we hit a breakpoint or get interrupted + } else if req.match_str("p") { + let Some(register) = req.match_hex::() else { + let res = self.response(); + self.send(res).await?; + continue; + }; + let res = self.response().write_hex(register); + self.send(res).await?; + } else if req.match_str("m") { + let mut read_params = || { + let start = req.match_hex::()?; + if !req.match_str(",") { + return None; + }; + let size = req.match_hex::()?; + Some((start, size)) + }; + let Some((start, size)) = read_params() else { + let res = self.response(); + self.send(res).await?; + continue; + }; + let mut res = self.response(); + for i in 0..size { + res = res.write_hex((start + i) as u8); + } + self.send(res).await?; } else { // unrecognized command let res = self.response(); diff --git a/src/gdbserver/request.rs b/src/gdbserver/request.rs index 63f66e8..6648794 100644 --- a/src/gdbserver/request.rs +++ b/src/gdbserver/request.rs @@ -1,4 +1,5 @@ use anyhow::{bail, Result}; +use atoi::FromRadix16; use tokio::io::{AsyncRead, AsyncReadExt as _}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -6,11 +7,32 @@ pub enum RequestKind { Signal, Command, } +impl RequestKind { + fn name(self) -> &'static str { + match self { + Self::Signal => "Signal", + Self::Command => "Command", + } + } +} -#[derive(Debug)] pub struct Request<'a> { pub kind: RequestKind, - body: &'a str, + 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<'a> Request<'a> { @@ -31,10 +53,9 @@ impl<'a> Request<'a> { if char == 0x03 { // This is how the client "cancels an in-flight request" buffer.push(char); - let body = std::str::from_utf8(buffer)?; return Ok(Self { kind: RequestKind::Signal, - body, + buffer, }); } if char != b'$' { @@ -61,24 +82,34 @@ impl<'a> Request<'a> { let mut checksum_bytes = [b'0'; 2]; reader.read_exact(&mut checksum_bytes).await?; - let checksum_str = std::str::from_utf8(&checksum_bytes)?; - let real_checksum = u8::from_str_radix(checksum_str, 16)?; - if checksum != real_checksum { + let (real_checksum, 2) = u8::from_radix_16(&checksum_bytes) else { bail!("invalid checksum"); + }; + if checksum != real_checksum { + bail!("mismatched checksum"); } - let body = std::str::from_utf8(buffer)?; Ok(Self { kind: RequestKind::Command, - body, + buffer, }) } pub fn match_str(&mut self, prefix: &str) -> bool { - if let Some(new_body) = self.body.strip_prefix(prefix) { - self.body = new_body; + if let Some(new_buffer) = self.buffer.strip_prefix(prefix.as_bytes()) { + self.buffer = new_buffer; return true; } false } + + 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) + } + } + } } diff --git a/src/gdbserver/response.rs b/src/gdbserver/response.rs index fdd9220..b7c684e 100644 --- a/src/gdbserver/response.rs +++ b/src/gdbserver/response.rs @@ -1,3 +1,5 @@ +use num_traits::ToBytes; + pub struct Response { buffer: Vec, checksum: u8, @@ -17,22 +19,24 @@ impl Response { } pub fn write_str(mut self, str: &str) -> Self { - for char in str.as_bytes() { - self.buffer.push(*char); - self.checksum = self.checksum.wrapping_add(*char); + for byte in str.as_bytes() { + self.buffer.push(*byte); + self.checksum = self.checksum.wrapping_add(*byte); } self } - pub fn write_hex_u8(mut self, value: u8) -> Self { - for digit in [(value >> 4), (value & 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); + pub fn write_hex(mut self, value: T) -> Self { + for byte in value.to_be_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 } @@ -40,6 +44,6 @@ impl Response { pub fn finish(mut self) -> Vec { let checksum = self.checksum; self.buffer.push(b'#'); - self.write_hex_u8(checksum).buffer + self.write_hex(checksum).buffer } } -- 2.40.1 From 8707a5196bd9a8be7dd7e98750d182bd1fc1d4d6 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Wed, 1 Jan 2025 20:57:20 -0500 Subject: [PATCH 09/32] Return register info --- src/gdbserver.rs | 14 +++++ src/gdbserver/registers.rs | 117 +++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 src/gdbserver/registers.rs diff --git a/src/gdbserver.rs b/src/gdbserver.rs index ae11619..70ef3a9 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -1,4 +1,5 @@ use anyhow::{bail, Result}; +use registers::REGISTERS; use request::{Request, RequestKind}; use response::Response; use std::{ @@ -17,6 +18,7 @@ use tokio::{ use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; +mod registers; mod request; mod response; @@ -191,6 +193,18 @@ impl GdbConnection { hex::encode("v810-unknown-vb") )); self.send(res).await?; + } else if req.match_str("qRegisterInfo") { + let mut get_reg_info = || { + let register = req.match_hex::()?; + REGISTERS.get(register) + }; + let Some(reg_info) = get_reg_info() else { + let res = self.response(); + self.send(res).await?; + continue; + }; + let res = self.response().write_str(®_info.to_description()); + self.send(res).await?; } else if req.match_str("vCont?") { let res = self.response().write_str("vCont;c;"); self.send(res).await?; diff --git a/src/gdbserver/registers.rs b/src/gdbserver/registers.rs new file mode 100644 index 0000000..ad5f5f4 --- /dev/null +++ b/src/gdbserver/registers.rs @@ -0,0 +1,117 @@ +pub struct RegisterInfo { + index: usize, + 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.index * 4, + self.set, + self.index + )); + if let Some(generic) = self.generic { + string.push_str(&format!(";generic:{}", generic)); + } + string + } +} + +macro_rules! register { + ($set:expr, $index:expr, $name:expr) => { + RegisterInfo { + index: $index, + name: $name, + set: $set, + alt_name: None, + generic: None, + } + }; + ($set:expr, $index:expr, $name:expr, alt: $alt:expr) => { + RegisterInfo { + index: $index, + name: $name, + set: $set, + alt_name: Some($alt), + generic: None, + } + }; + ($set:expr, $index:expr, $name:expr, generic: $generic:expr) => { + RegisterInfo { + index: $index, + name: $name, + set: $set, + alt_name: None, + generic: Some($generic), + } + }; + ($set:expr, $index:expr, $name:expr, alt: $alt:expr, generic: $generic:expr) => { + RegisterInfo { + index: $index, + 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"), +]; -- 2.40.1 From 82c3104ab9be7570fe389172b8c6644c08a5f756 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Wed, 1 Jan 2025 21:48:33 -0500 Subject: [PATCH 10/32] Read actual register values --- src/emulator.rs | 10 +++++++++- src/emulator/shrooms_vb_core.rs | 23 ++++++++++++++++++++++ src/gdbserver.rs | 15 ++++++++++++-- src/gdbserver/registers.rs | 35 ++++++++++++++++++++++----------- 4 files changed, 69 insertions(+), 14 deletions(-) diff --git a/src/emulator.rs b/src/emulator.rs index 8748a10..4f84103 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -15,8 +15,8 @@ use anyhow::Result; use egui_toast::{Toast, ToastKind, ToastOptions}; use crate::{audio::Audio, graphics::TextureSink}; -pub use shrooms_vb_core::VBKey; use shrooms_vb_core::{Sim, EXPECTED_FRAME_SIZE}; +pub use shrooms_vb_core::{VBKey, VBRegister}; mod shrooms_vb_core; @@ -390,6 +390,13 @@ impl Emulator { } } } + 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::SetAudioEnabled(p1, p2) => { self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release); self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release); @@ -447,6 +454,7 @@ pub enum EmulatorCommand { StopSecondSim, Pause, Resume, + ReadRegister(SimId, VBRegister, oneshot::Sender), SetAudioEnabled(bool, bool), Link, Unlink, diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index 2c9510a..507de03 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -55,6 +55,13 @@ bitflags! { } } +#[derive(Clone, Copy, Debug)] +pub enum VBRegister { + Program(u32), + System(u32), + PC, +} + type OnFrame = extern "C" fn(sim: *mut VB) -> c_int; #[link(name = "vb")] @@ -77,6 +84,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,6 +95,8 @@ 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"] @@ -291,6 +304,16 @@ 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) }, + } + } } impl Drop for Sim { diff --git a/src/gdbserver.rs b/src/gdbserver.rs index 70ef3a9..056ae08 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -227,12 +227,23 @@ impl GdbConnection { self.client.send_command(EmulatorCommand::Resume); // Don't send a response until we hit a breakpoint or get interrupted } else if req.match_str("p") { - let Some(register) = req.match_hex::() else { + 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() + }; + let Some(value) = read_register() else { let res = self.response(); self.send(res).await?; continue; }; - let res = self.response().write_hex(register); + let res = self.response().write_hex(value); self.send(res).await?; } else if req.match_str("m") { let mut read_params = || { diff --git a/src/gdbserver/registers.rs b/src/gdbserver/registers.rs index ad5f5f4..370e52e 100644 --- a/src/gdbserver/registers.rs +++ b/src/gdbserver/registers.rs @@ -1,5 +1,7 @@ +use crate::emulator::VBRegister; + pub struct RegisterInfo { - index: usize, + dwarf: u32, name: &'static str, set: &'static str, alt_name: Option<&'static str>, @@ -14,48 +16,59 @@ impl RegisterInfo { } string.push_str(&format!( ";bitsize:32;offset:{};encoding:uint;format:hex;set:{};dwarf:{}", - self.index * 4, + self.dwarf * 4, self.set, - self.index + 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, $index:expr, $name:expr) => { + ($set:expr, $dwarf:expr, $name:expr) => { RegisterInfo { - index: $index, + dwarf: $dwarf, name: $name, set: $set, alt_name: None, generic: None, } }; - ($set:expr, $index:expr, $name:expr, alt: $alt:expr) => { + ($set:expr, $dwarf:expr, $name:expr, alt: $alt:expr) => { RegisterInfo { - index: $index, + dwarf: $dwarf, name: $name, set: $set, alt_name: Some($alt), generic: None, } }; - ($set:expr, $index:expr, $name:expr, generic: $generic:expr) => { + ($set:expr, $dwarf:expr, $name:expr, generic: $generic:expr) => { RegisterInfo { - index: $index, + dwarf: $dwarf, name: $name, set: $set, alt_name: None, generic: Some($generic), } }; - ($set:expr, $index:expr, $name:expr, alt: $alt:expr, generic: $generic:expr) => { + ($set:expr, $dwarf:expr, $name:expr, alt: $alt:expr, generic: $generic:expr) => { RegisterInfo { - index: $index, + dwarf: $dwarf, name: $name, set: $set, alt_name: Some($alt), -- 2.40.1 From be8cdd958aab70c8940861da8636978a55a0dc2e Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Thu, 2 Jan 2025 01:10:19 -0500 Subject: [PATCH 11/32] Implement reading from real memory --- src/emulator.rs | 9 ++++ src/emulator/shrooms_vb_core.rs | 11 ++++- src/gdbserver.rs | 80 ++++++++++++++++++++++++++------- src/gdbserver/response.rs | 22 +++++++-- 4 files changed, 101 insertions(+), 21 deletions(-) diff --git a/src/emulator.rs b/src/emulator.rs index 4f84103..0250cf4 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -3,6 +3,7 @@ use std::{ fmt::Display, fs::{self, File}, io::{Read, Seek, SeekFrom, Write}, + ops::Range, path::{Path, PathBuf}, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, @@ -397,6 +398,13 @@ impl Emulator { let value = sim.read_register(register); let _ = done.send(value); } + EmulatorCommand::ReadMemory(sim_id, addresses, mut buffer, done) => { + let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { + return; + }; + sim.read_memory(addresses, &mut buffer); + let _ = done.send(buffer); + } EmulatorCommand::SetAudioEnabled(p1, p2) => { self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release); self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release); @@ -455,6 +463,7 @@ pub enum EmulatorCommand { Pause, Resume, ReadRegister(SimId, VBRegister, oneshot::Sender), + ReadMemory(SimId, Range, Vec, oneshot::Sender>), SetAudioEnabled(bool, bool), Link, Unlink, diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index 507de03..0fffe62 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -1,4 +1,4 @@ -use std::{ffi::c_void, ptr, slice}; +use std::{ffi::c_void, ops::Range, ptr, slice}; use anyhow::{anyhow, Result}; use bitflags::bitflags; @@ -101,6 +101,8 @@ extern "C" { 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"] @@ -314,6 +316,13 @@ impl Sim { VBRegister::PC => unsafe { vb_get_program_counter(self.sim) }, } } + + pub fn read_memory(&mut self, addresses: Range, into: &mut Vec) { + for address in addresses { + let byte = unsafe { vb_read(self.sim, address, VBDataType::U8) }; + into.push(byte as u8); + } + } } impl Drop for Sim { diff --git a/src/gdbserver.rs b/src/gdbserver.rs index 056ae08..09d0dbe 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -135,6 +135,7 @@ struct GdbConnection { ack_messages: bool, request_buf: Vec, response_buf: Option>, + memory_buf: Option>, } impl GdbConnection { @@ -148,6 +149,7 @@ impl GdbConnection { ack_messages: true, request_buf: vec![], response_buf: None, + memory_buf: None, } } async fn run(mut self) -> Result<()> { @@ -155,7 +157,7 @@ impl GdbConnection { self.client.send_command(EmulatorCommand::Pause); loop { let mut req = Request::read(&mut self.stream_in, &mut self.request_buf).await?; - println!("received {:?}", req); + println!("received {:02x?}", req); if req.kind == RequestKind::Signal { self.client.send_command(EmulatorCommand::Pause); @@ -173,7 +175,7 @@ impl GdbConnection { } else if req.match_str("qSupported:") { let res = self .response() - .write_str("multiprocess+;swbreak+;vContSupported+"); + .write_str("multiprocess+;swbreak+;vContSupported+;PacketSize=10000"); self.send(res).await?; } else if req.match_str("QThreadSuffixSupported") || req.match_str("QListThreadsInStopReply") @@ -199,8 +201,7 @@ impl GdbConnection { REGISTERS.get(register) }; let Some(reg_info) = get_reg_info() else { - let res = self.response(); - self.send(res).await?; + self.send_empty().await?; continue; }; let res = self.response().write_str(®_info.to_description()); @@ -239,39 +240,83 @@ impl GdbConnection { rx.recv().ok() }; let Some(value) = read_register() else { - let res = self.response(); - self.send(res).await?; + self.send_empty().await?; continue; }; let res = self.response().write_hex(value); self.send(res).await?; } else if req.match_str("m") { - let mut read_params = || { + let mut read_memory = || { let start = req.match_hex::()?; if !req.match_str(",") { return None; }; let size = req.match_hex::()?; - Some((start, size)) + let mut buf = self.memory_buf.take().unwrap_or_default(); + buf.clear(); + let (tx, rx) = ::oneshot::channel(); + self.client.send_command(EmulatorCommand::ReadMemory( + self.sim_id, + start..(start + size), + buf, + tx, + )); + rx.recv().ok() }; - let Some((start, size)) = read_params() else { - let res = self.response(); - self.send(res).await?; + let Some(memory) = read_memory() else { + self.send_empty().await?; continue; }; let mut res = self.response(); - for i in 0..size { - res = res.write_hex((start + i) as u8); + for byte in &memory { + res = res.write_hex(*byte); } + self.memory_buf = Some(memory); + self.send(res).await?; + } else if req.match_str("x") { + let mut read_memory = || { + let start = req.match_hex::()?; + if !req.match_str(",") { + return None; + }; + let size = req.match_hex::()?; + let mut buf = self.memory_buf.take().unwrap_or_default(); + buf.clear(); + let (tx, rx) = ::oneshot::channel(); + self.client.send_command(EmulatorCommand::ReadMemory( + self.sim_id, + start..(start + size), + buf, + tx, + )); + rx.recv().ok() + }; + let Some(memory) = read_memory() else { + self.send_empty().await?; + continue; + }; + let mut res = self.response(); + if memory.is_empty() { + res = res.write_str("OK"); + } else { + for byte in &memory { + res = res.write_byte(*byte); + } + } + self.memory_buf = Some(memory); self.send(res).await?; } else { // unrecognized command - let res = self.response(); - self.send(res).await?; + self.send_empty().await?; } } } + async fn send_empty(&mut self) -> std::io::Result<()> { + let res = self.response(); + self.send(res).await + } + fn response(&mut self) -> Response { Response::new( self.response_buf.take().unwrap_or_default(), @@ -281,7 +326,10 @@ impl GdbConnection { async fn send(&mut self, res: Response) -> std::io::Result<()> { let buffer = res.finish(); - println!("{:?}", std::str::from_utf8(&buffer)); + match std::str::from_utf8(&buffer) { + Ok(text) => println!("response: {text}"), + Err(_) => println!("response: {buffer:02x?}"), + } self.stream_out.write_all(&buffer).await?; self.response_buf = Some(buffer); self.stream_out.flush().await diff --git a/src/gdbserver/response.rs b/src/gdbserver/response.rs index b7c684e..e35bcc7 100644 --- a/src/gdbserver/response.rs +++ b/src/gdbserver/response.rs @@ -18,16 +18,30 @@ impl Response { } } - pub fn write_str(mut self, str: &str) -> Self { + pub fn write_str(self, str: &str) -> Self { + let mut me = self; for byte in str.as_bytes() { - self.buffer.push(*byte); - self.checksum = self.checksum.wrapping_add(*byte); + 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_be_bytes().as_ref() { + 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 -- 2.40.1 From a5b5f8e80f01048c9272e67885c7358609572a23 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Thu, 2 Jan 2025 22:02:51 -0500 Subject: [PATCH 12/32] Use explicit states for sims --- Cargo.lock | 10 +++ Cargo.toml | 1 + src/emulator.rs | 155 ++++++++++++++++++++++++++------------------- src/window/game.rs | 25 +++++--- 4 files changed, 116 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b3eb56..8a1298f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -406,6 +406,15 @@ 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" @@ -1752,6 +1761,7 @@ version = "0.2.5" dependencies = [ "anyhow", "atoi", + "atomic", "bitflags 2.6.0", "bytemuck", "cc", diff --git a/Cargo.toml b/Cargo.toml index 0e1b3fb..c757995 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ 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"] } diff --git a/src/emulator.rs b/src/emulator.rs index 0250cf4..2cc7b61 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -6,13 +6,15 @@ use std::{ ops::Range, 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 crate::{audio::Audio, graphics::TextureSink}; @@ -21,16 +23,6 @@ pub use shrooms_vb_core::{VBKey, VBRegister}; 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, @@ -93,23 +85,33 @@ 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, +} + 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)), }; 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(), }; @@ -126,9 +128,8 @@ impl EmulatorBuilder { 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, )?; @@ -144,9 +145,8 @@ 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, @@ -158,9 +158,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 { @@ -169,9 +168,8 @@ impl Emulator { carts: [None, None], audio: Audio::init()?, commands, - sim_count, - running, - has_game, + sim_state, + state, audio_on, linked, renderers: HashMap::new(), @@ -208,17 +206,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(()) } @@ -243,9 +241,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<()> { @@ -263,9 +283,7 @@ 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.linked.store(false, Ordering::Release); Ok(()) } @@ -299,9 +317,12 @@ 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); + let p1_running = state == EmulatorState::Running && p1_state == SimState::Ready; + let p2_running = state == EmulatorState::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 { @@ -377,19 +398,12 @@ 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::ReadRegister(sim_id, register, done) => { let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { @@ -472,32 +486,43 @@ 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, +} + #[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, diff --git a/src/window/game.rs b/src/window/game.rs index ced5d1d..df3cdd6 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,7 +73,7 @@ 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(); } @@ -82,17 +82,21 @@ impl GameWindow { } }); 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 can_interact = self.client.sim_state(self.sim_id) == SimState::Ready; + if state == EmulatorState::Running { + if ui.add_enabled(can_interact, 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_interact, 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(can_interact, Button::new("Reset")).clicked() { self.client .send_command(EmulatorCommand::Reset(self.sim_id)); ui.close_menu(); @@ -100,8 +104,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 +114,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); @@ -207,7 +212,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); } -- 2.40.1 From 11df670ff4f8248068a36aea799c88950fc884fd Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 4 Jan 2025 01:04:21 -0500 Subject: [PATCH 13/32] Let emulator tell debugger to stop --- src/emulator.rs | 88 +++++++++- src/gdbserver.rs | 354 +++++++++++++++++++-------------------- src/gdbserver/request.rs | 184 +++++++++++++------- src/window/game.rs | 12 +- 4 files changed, 392 insertions(+), 246 deletions(-) diff --git a/src/emulator.rs b/src/emulator.rs index 2cc7b61..43486d2 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -151,6 +151,7 @@ pub struct Emulator { linked: Arc, renderers: HashMap, messages: HashMap>, + debuggers: HashMap, eye_contents: Vec, audio_samples: Vec, } @@ -174,6 +175,7 @@ impl Emulator { 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], }) @@ -288,6 +290,48 @@ impl Emulator { Ok(()) } + fn start_debugging(&mut self, sim_id: SimId, sender: DebugSender) { + let debug = DebugInfo { + sender, + stop_reason: Some(DebugStopReason::Trapped), + }; + self.debuggers.insert(sim_id, debug); + self.state + .store(EmulatorState::Debugging, Ordering::Release); + } + + fn stop_debugging(&mut self, sim_id: SimId) { + self.debuggers.remove(&sim_id); + if self.debuggers.is_empty() { + self.state.store(EmulatorState::Running, Ordering::Release); + } + } + + fn debug_interrupt(&mut self, sim_id: SimId) { + let Some(debugger) = self.debuggers.get_mut(&sim_id) else { + self.stop_debugging(sim_id); + return; + }; + if !matches!(debugger.stop_reason, Some(DebugStopReason::Trapped)) { + debugger.stop_reason = Some(DebugStopReason::Trapped); + if debugger + .sender + .send(DebugEvent::Stopped(DebugStopReason::Trapped)) + .is_err() + { + self.stop_debugging(sim_id); + } + } + } + + fn debug_continue(&mut self, sim_id: SimId) { + let Some(debugger) = self.debuggers.get_mut(&sim_id) else { + self.stop_debugging(sim_id); + return; + }; + debugger.stop_reason = None; + } + pub fn run(&mut self) { loop { let idle = self.tick(); @@ -320,8 +364,14 @@ impl Emulator { 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); - let p1_running = state == EmulatorState::Running && p1_state == SimState::Ready; - let p2_running = state == EmulatorState::Running && p2_state == SimState::Ready; + // 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); @@ -405,6 +455,18 @@ impl Emulator { EmulatorCommand::Resume => { 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::ReadRegister(sim_id, register, done) => { let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { return; @@ -476,6 +538,10 @@ pub enum EmulatorCommand { StopSecondSim, Pause, Resume, + StartDebugging(SimId, DebugSender), + StopDebugging(SimId), + DebugInterrupt(SimId), + DebugContinue(SimId), ReadRegister(SimId, VBRegister, oneshot::Sender), ReadMemory(SimId, Range, Vec, oneshot::Sender>), SetAudioEnabled(bool, bool), @@ -499,6 +565,24 @@ pub enum SimState { pub enum EmulatorState { Paused, Running, + Debugging, +} + +type DebugSender = tokio::sync::mpsc::UnboundedSender; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DebugStopReason { + // The debugger told us to pause + Trapped, +} + +struct DebugInfo { + sender: DebugSender, + stop_reason: Option, +} + +pub enum DebugEvent { + Stopped(DebugStopReason), } #[derive(Clone)] diff --git a/src/gdbserver.rs b/src/gdbserver.rs index 09d0dbe..ffeaf13 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -1,22 +1,19 @@ use anyhow::{bail, Result}; use registers::REGISTERS; -use request::{Request, RequestKind}; +use request::{Request, RequestKind, RequestSource}; use response::Response; use std::{ sync::{Arc, Mutex}, thread, }; use tokio::{ - io::{AsyncWriteExt as _, BufReader, BufWriter}, - net::{ - tcp::{OwnedReadHalf, OwnedWriteHalf}, - TcpListener, TcpStream, - }, + io::{AsyncWriteExt as _, BufReader}, + net::{TcpListener, TcpStream}, select, - sync::oneshot, + sync::{mpsc, oneshot}, }; -use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; +use crate::emulator::{DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId}; mod registers; mod request; @@ -81,8 +78,8 @@ async fn run_server( let Some(stream) = try_connect(port, status).await else { return; }; - let connection = GdbConnection::new(sim_id, client, stream); - match connection.run().await { + let mut connection = GdbConnection::new(sim_id, client); + match connection.run(stream).await { Ok(()) => { *status.lock().unwrap() = GdbServerStatus::Stopped; } @@ -130,191 +127,191 @@ impl GdbServerStatus { struct GdbConnection { sim_id: SimId, client: EmulatorClient, - stream_in: BufReader, - stream_out: BufWriter, ack_messages: bool, - request_buf: Vec, + stop_reason: Option, response_buf: Option>, memory_buf: Option>, } impl GdbConnection { - fn new(sim_id: SimId, client: EmulatorClient, stream: TcpStream) -> Self { - let (rx, tx) = stream.into_split(); + fn new(sim_id: SimId, client: EmulatorClient) -> Self { Self { sim_id, client, - stream_in: BufReader::new(rx), - stream_out: BufWriter::new(tx), ack_messages: true, - request_buf: vec![], + stop_reason: None, response_buf: None, memory_buf: None, } } - async fn run(mut self) -> Result<()> { - println!("Connected for {}", self.sim_id); - self.client.send_command(EmulatorCommand::Pause); + async fn run(&mut self, stream: TcpStream) -> Result<()> { + let (debug_sink, mut debug_source) = mpsc::unbounded_channel(); + let (rx, mut tx) = stream.into_split(); + let mut request_source = RequestSource::new(BufReader::new(rx)); + self.client + .send_command(EmulatorCommand::StartDebugging(self.sim_id, debug_sink)); loop { - let mut req = Request::read(&mut self.stream_in, &mut self.request_buf).await?; - println!("received {:02x?}", req); - - if req.kind == RequestKind::Signal { - self.client.send_command(EmulatorCommand::Pause); - let res = self - .response() - .write_str("T05;thread:p1.t1;threads:p1.t1;reason:trap;"); - self.send(res).await?; - continue; - } - - if req.match_str("QStartNoAckMode") { - let res = self.response().write_str("OK"); - self.send(res).await?; - self.ack_messages = false; - } else if req.match_str("qSupported:") { - let res = self - .response() - .write_str("multiprocess+;swbreak+;vContSupported+;PacketSize=10000"); - self.send(res).await?; - } else if req.match_str("QThreadSuffixSupported") - || req.match_str("QListThreadsInStopReply") - || req.match_str("QEnableErrorStrings") - { - let res = self.response().write_str("OK"); - self.send(res).await?; - } else if req.match_str("qHostInfo") { - let res = self.response().write_str(&format!( - "triple:{};endian:little;ptrsize:4;", - hex::encode("v810-unknown-vb") - )); - self.send(res).await?; - } else if req.match_str("qProcessInfo") { - let res = self.response().write_str(&format!( - "pid:1;triple:{};endian:little;ptrsize:4;", - hex::encode("v810-unknown-vb") - )); - self.send(res).await?; - } else if req.match_str("qRegisterInfo") { - let mut get_reg_info = || { - let register = req.match_hex::()?; - REGISTERS.get(register) - }; - let Some(reg_info) = get_reg_info() else { - self.send_empty().await?; - continue; - }; - let res = self.response().write_str(®_info.to_description()); - self.send(res).await?; - } else if req.match_str("vCont?") { - let res = self.response().write_str("vCont;c;"); - self.send(res).await?; - } else if req.match_str("qC") { - // The v810 has no threads, so report that the "current thread" is 1. - let res = self.response().write_str("QCp1.t1"); - self.send(res).await?; - } else if req.match_str("qfThreadInfo") { - let res = self.response().write_str("mp1.t1"); - self.send(res).await?; - } else if req.match_str("qsThreadInfo") { - let res = self.response().write_str("l"); - self.send(res).await?; - } else if req.match_str("k") { - bail!("debug process was killed"); - } else if req.match_str("?") { - let res = self.response().write_str("T00;thread:p1.t1;threads:p1.t1;"); - self.send(res).await?; - } else if req.match_str("c") || req.match_str("vCont;c:") { - self.client.send_command(EmulatorCommand::Resume); - // Don't send a response until we hit a breakpoint or get interrupted - } 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() - }; - let Some(value) = read_register() else { - self.send_empty().await?; - continue; - }; - let res = self.response().write_hex(value); - self.send(res).await?; - } else if req.match_str("m") { - let mut read_memory = || { - let start = req.match_hex::()?; - if !req.match_str(",") { - return None; + let response = select! { + maybe_event = debug_source.recv() => { + let Some(event) = maybe_event else { + // debugger has stopped running + break; }; - let size = req.match_hex::()?; - let mut buf = self.memory_buf.take().unwrap_or_default(); - buf.clear(); - let (tx, rx) = ::oneshot::channel(); - self.client.send_command(EmulatorCommand::ReadMemory( - self.sim_id, - start..(start + size), - buf, - tx, - )); - rx.recv().ok() - }; - let Some(memory) = read_memory() else { - self.send_empty().await?; - continue; - }; - let mut res = self.response(); - for byte in &memory { - res = res.write_hex(*byte); + self.handle_event(event) } - self.memory_buf = Some(memory); - self.send(res).await?; - } else if req.match_str("x") { - let mut read_memory = || { - let start = req.match_hex::()?; - if !req.match_str(",") { - return None; - }; - let size = req.match_hex::()?; - let mut buf = self.memory_buf.take().unwrap_or_default(); - buf.clear(); - let (tx, rx) = ::oneshot::channel(); - self.client.send_command(EmulatorCommand::ReadMemory( - self.sim_id, - start..(start + size), - buf, - tx, - )); - rx.recv().ok() - }; - let Some(memory) = read_memory() else { - self.send_empty().await?; - continue; + maybe_request = request_source.recv() => { + let req = maybe_request?; + self.handle_request(req)? + } + }; + if let Some(res) = response { + let buffer = res.finish(); + match std::str::from_utf8(&buffer) { + Ok(text) => println!("response: {text}"), + Err(_) => println!("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> { + println!("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") + } 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;") + } 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_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 let Some(op) = req.match_some_str(["m", "x"]) { + let mut read_memory = || { + let start = req.match_hex::()?; + if !req.match_str(",") { + return None; }; + let size = req.match_hex::()?; + let mut buf = self.memory_buf.take().unwrap_or_default(); + buf.clear(); + let (tx, rx) = ::oneshot::channel(); + self.client.send_command(EmulatorCommand::ReadMemory( + self.sim_id, + start..(start + size), + 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"); + 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); - self.send(res).await?; + res } else { - // unrecognized command - self.send_empty().await?; + self.response() } - } - } - - async fn send_empty(&mut self) -> std::io::Result<()> { - let res = self.response(); - self.send(res).await + } else { + // unrecognized command + self.response() + }; + Ok(Some(res)) } fn response(&mut self) -> Response { @@ -323,15 +320,18 @@ impl GdbConnection { self.ack_messages, ) } +} - async fn send(&mut self, res: Response) -> std::io::Result<()> { - let buffer = res.finish(); - match std::str::from_utf8(&buffer) { - Ok(text) => println!("response: {text}"), - Err(_) => println!("response: {buffer:02x?}"), - } - self.stream_out.write_all(&buffer).await?; - self.response_buf = Some(buffer); - self.stream_out.flush().await +impl Drop for GdbConnection { + fn drop(&mut self) { + self.client + .send_command(EmulatorCommand::StopDebugging(self.sim_id)); + } +} + +fn debug_stop_reason_string(reason: Option) -> &'static str { + match reason { + Some(DebugStopReason::Trapped) => "T05;thread:p1.t1;threads:p1.t1;reason:trap;", + None => "T00;thread:p1.t1;threads:p1.t1;", } } diff --git a/src/gdbserver/request.rs b/src/gdbserver/request.rs index 6648794..b3be216 100644 --- a/src/gdbserver/request.rs +++ b/src/gdbserver/request.rs @@ -35,66 +35,7 @@ impl std::fmt::Debug for Request<'_> { } } -impl<'a> Request<'a> { - pub async fn read( - reader: &mut R, - buffer: &'a mut Vec, - ) -> Result { - buffer.clear(); - - let mut char = reader.read_u8().await?; - while char == b'+' { - // just ignore positive acks - char = 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" - buffer.push(char); - return Ok(Self { - kind: RequestKind::Signal, - buffer, - }); - } - if char != b'$' { - // Messages are supposed to start with a dollar sign - bail!("malformed message"); - } - - // now read the body - let mut checksum = 0u8; - char = reader.read_u8().await?; - while char != b'#' { - if char == b'}' { - // escape character - checksum = checksum.wrapping_add(char); - char = reader.read_u8().await?; - checksum = checksum.wrapping_add(char); - buffer.push(char ^ 0x20); - } else { - checksum = checksum.wrapping_add(char); - buffer.push(char); - } - char = reader.read_u8().await?; - } - - let mut checksum_bytes = [b'0'; 2]; - reader.read_exact(&mut checksum_bytes).await?; - let (real_checksum, 2) = u8::from_radix_16(&checksum_bytes) else { - bail!("invalid checksum"); - }; - if checksum != real_checksum { - bail!("mismatched checksum"); - } - - Ok(Self { - kind: RequestKind::Command, - buffer, - }) - } - +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; @@ -103,6 +44,13 @@ impl<'a> Request<'a> { 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, @@ -113,3 +61,119 @@ impl<'a> Request<'a> { } } } + +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/window/game.rs b/src/window/game.rs index df3cdd6..a1ecebd 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -83,20 +83,18 @@ impl GameWindow { }); ui.menu_button("Emulation", |ui| { let state = self.client.emulator_state(); - let can_interact = self.client.sim_state(self.sim_id) == SimState::Ready; + let is_ready = self.client.sim_state(self.sim_id) == SimState::Ready; + let can_pause = is_ready && state == EmulatorState::Running; if state == EmulatorState::Running { - if ui.add_enabled(can_interact, Button::new("Pause")).clicked() { + if ui.add_enabled(is_ready, Button::new("Pause")).clicked() { self.client.send_command(EmulatorCommand::Pause); ui.close_menu(); } - } else if ui - .add_enabled(can_interact, Button::new("Resume")) - .clicked() - { + } else if ui.add_enabled(can_pause, Button::new("Resume")).clicked() { self.client.send_command(EmulatorCommand::Resume); ui.close_menu(); } - if ui.add_enabled(can_interact, 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(); -- 2.40.1 From 856ee00999fb65ec8defaac9f90dbf5a264cb430 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 4 Jan 2025 12:17:56 -0500 Subject: [PATCH 14/32] Fix bugs around debugging lifecycle --- src/emulator.rs | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/emulator.rs b/src/emulator.rs index 43486d2..3de6a38 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -177,7 +177,7 @@ impl Emulator { 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), }) } @@ -286,6 +286,7 @@ impl Emulator { self.renderers.remove(&SimId::Player2); self.sims.truncate(1); self.sim_state[SimId::Player2.to_index()].store(SimState::Uninitialized, Ordering::Release); + self.stop_debugging(SimId::Player2); self.linked.store(false, Ordering::Release); Ok(()) } @@ -303,7 +304,12 @@ impl Emulator { fn stop_debugging(&mut self, sim_id: SimId) { self.debuggers.remove(&sim_id); if self.debuggers.is_empty() { - self.state.store(EmulatorState::Running, Ordering::Release); + let _ = self.state.compare_exchange( + EmulatorState::Debugging, + EmulatorState::Running, + Ordering::AcqRel, + Ordering::Relaxed, + ); } } @@ -364,6 +370,8 @@ impl Emulator { 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, @@ -381,6 +389,7 @@ impl Emulator { self.sims[SimId::Player2.to_index()].emulate(); } + // Video for sim_id in SimId::values() { let Some(renderer) = self.renderers.get_mut(&sim_id) else { continue; @@ -395,24 +404,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); -- 2.40.1 From d6eb8ec7efd75d50976fd83e85ee9078d0291455 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 5 Jan 2025 00:47:58 -0500 Subject: [PATCH 15/32] Implement execute breakpoints --- src/emulator.rs | 50 +++++++++++++++++---- src/emulator/shrooms_vb_core.rs | 78 +++++++++++++++++++++++++++++---- src/gdbserver.rs | 19 +++++++- 3 files changed, 129 insertions(+), 18 deletions(-) diff --git a/src/emulator.rs b/src/emulator.rs index 3de6a38..9bc37b3 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -18,7 +18,7 @@ use bytemuck::NoUninit; use egui_toast::{Toast, ToastKind, ToastOptions}; use crate::{audio::Audio, graphics::TextureSink}; -use shrooms_vb_core::{Sim, EXPECTED_FRAME_SIZE}; +use shrooms_vb_core::{Sim, StopReason, EXPECTED_FRAME_SIZE}; pub use shrooms_vb_core::{VBKey, VBRegister}; mod shrooms_vb_core; @@ -302,6 +302,9 @@ impl Emulator { } fn stop_debugging(&mut self, sim_id: SimId) { + if let Some(sim) = self.sims.get_mut(sim_id.to_index()) { + sim.clear_breakpoints(); + } self.debuggers.remove(&sim_id); if self.debuggers.is_empty() { let _ = self.state.compare_exchange( @@ -314,17 +317,17 @@ impl Emulator { } fn debug_interrupt(&mut self, sim_id: SimId) { + self.debug_stop(sim_id, DebugStopReason::Trapped); + } + + 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 !matches!(debugger.stop_reason, Some(DebugStopReason::Trapped)) { - debugger.stop_reason = Some(DebugStopReason::Trapped); - if debugger - .sender - .send(DebugEvent::Stopped(DebugStopReason::Trapped)) - .is_err() - { + 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); } } @@ -389,6 +392,21 @@ 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::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 { @@ -493,6 +511,18 @@ impl Emulator { sim.read_memory(addresses, &mut 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::SetAudioEnabled(p1, p2) => { self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release); self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release); @@ -556,6 +586,8 @@ pub enum EmulatorCommand { DebugContinue(SimId), ReadRegister(SimId, VBRegister, oneshot::Sender), ReadMemory(SimId, Range, Vec, oneshot::Sender>), + AddBreakpoint(SimId, u32), + RemoveBreakpoint(SimId, u32), SetAudioEnabled(bool, bool), Link, Unlink, @@ -584,6 +616,8 @@ type DebugSender = tokio::sync::mpsc::UnboundedSender; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum DebugStopReason { + // We hit a breakpoint + Breakpoint, // The debugger told us to pause Trapped, } diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index 0fffe62..e027869 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -63,6 +63,8 @@ pub enum VBRegister { } type OnFrame = extern "C" fn(sim: *mut VB) -> c_int; +type OnExecute = + extern "C" fn(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> c_int; #[link(name = "vb")] extern "C" { @@ -109,8 +111,10 @@ extern "C" { 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); #[link_name = "vbSetFrameCallback"] - fn vb_set_frame_callback(sim: *mut VB, on_frame: OnFrame); + fn vb_set_frame_callback(sim: *mut VB, callback: Option); #[link_name = "vbSetKeys"] fn vb_set_keys(sim: *mut VB, keys: u16) -> u16; #[link_name = "vbSetOption"] @@ -130,7 +134,7 @@ extern "C" { fn vb_size_of() -> usize; } -extern "C" fn on_frame(sim: *mut VB) -> i32 { +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() }; @@ -138,12 +142,27 @@ extern "C" fn on_frame(sim: *mut VB) -> i32 { 1 } +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() }; + + if data.breakpoints.binary_search(&address).is_err() { + return 0; + } + + data.stop_reason = Some(StopReason::Breakpoint); + 1 +} + 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, + breakpoints: Vec, } #[repr(transparent)] @@ -151,8 +170,9 @@ pub struct Sim { sim: *mut VB, } -// SAFETY: the memory pointed to by sim is valid -unsafe impl Send for Sim {} +pub enum StopReason { + Breakpoint, +} impl Sim { pub fn new() -> Self { @@ -166,9 +186,13 @@ impl Sim { unsafe { vb_reset(sim) }; // set up userdata - let state = VBState { frame_seen: false }; + let state = VBState { + frame_seen: false, + stop_reason: None, + breakpoints: vec![], + }; 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]; @@ -257,9 +281,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; } @@ -323,6 +345,44 @@ impl Sim { into.push(byte as u8); } } + + 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.breakpoints.is_empty() { + unsafe { vb_set_execute_callback(self.sim, None) }; + } + } + } + + pub fn clear_breakpoints(&mut self) { + let data = self.get_state(); + data.breakpoints.clear(); + unsafe { vb_set_execute_callback(self.sim, None) }; + } + + pub fn stop_reason(&mut self) -> Option { + let data = self.get_state(); + data.stop_reason.take() + } + + 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 index ffeaf13..0223fcc 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -307,6 +307,22 @@ impl GdbConnection { } else { self.response() } + } else if req.match_str("Z0,") { + if let Some(address) = req.match_hex() { + self.client + .send_command(EmulatorCommand::AddBreakpoint(self.sim_id, address)); + self.response().write_str("OK") + } else { + self.response() + } + } else if req.match_str("z0,") { + if let Some(address) = req.match_hex() { + self.client + .send_command(EmulatorCommand::RemoveBreakpoint(self.sim_id, address)); + self.response().write_str("OK") + } else { + self.response() + } } else { // unrecognized command self.response() @@ -331,7 +347,8 @@ impl Drop for GdbConnection { fn debug_stop_reason_string(reason: Option) -> &'static str { match reason { - Some(DebugStopReason::Trapped) => "T05;thread:p1.t1;threads:p1.t1;reason:trap;", + Some(DebugStopReason::Breakpoint) => "T05;thread:p1.t1;threads;p1.t1;reason:breakpoint", + Some(DebugStopReason::Trapped) => "T05;swbreak;thread:p1.t1;threads:p1.t1;reason:trap;", None => "T00;thread:p1.t1;threads:p1.t1;", } } -- 2.40.1 From 6fd8d8f5cff3e7178d023c784689d97142d2de17 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 5 Jan 2025 11:24:59 -0500 Subject: [PATCH 16/32] Improve flow of command-line debugging --- src/app.rs | 18 ++++++++++++------ src/emulator.rs | 12 ++++++++++++ src/gdbserver.rs | 1 + src/main.rs | 3 +++ src/window/gdb.rs | 14 ++++++++++++-- 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/app.rs b/src/app.rs index 569a798..a821c07 100644 --- a/src/app.rs +++ b/src/app.rs @@ -86,6 +86,12 @@ 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(), @@ -93,11 +99,6 @@ impl ApplicationHandler for Application { SimId::Player1, ); self.open(event_loop, Box::new(app)); - if let Some(port) = self.init_debug_port { - let mut server = GdbServerWindow::new(SimId::Player1, self.client.clone()); - server.start(port); - self.open(event_loop, Box::new(server)); - } } fn window_event( @@ -193,7 +194,8 @@ impl ApplicationHandler for Application { self.open(event_loop, Box::new(about)); } UserEvent::OpenDebugger(sim_id) => { - let debugger = GdbServerWindow::new(sim_id, self.client.clone()); + let debugger = + GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone()); self.open(event_loop, Box::new(debugger)); } UserEvent::OpenInput => { @@ -209,6 +211,9 @@ impl ApplicationHandler for Application { ); self.open(event_loop, Box::new(p2)); } + UserEvent::Quit => { + event_loop.exit(); + } } } @@ -390,6 +395,7 @@ pub enum UserEvent { OpenDebugger(SimId), OpenInput, OpenPlayer2, + Quit, } pub enum Action { diff --git a/src/emulator.rs b/src/emulator.rs index 9bc37b3..b4d5ab8 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -92,6 +92,7 @@ pub struct EmulatorBuilder { state: Arc>, audio_on: Arc<[AtomicBool; 2]>, linked: Arc, + start_paused: bool, } impl EmulatorBuilder { @@ -107,6 +108,7 @@ impl EmulatorBuilder { 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, @@ -125,6 +127,13 @@ 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, @@ -136,6 +145,9 @@ impl EmulatorBuilder { if let Some(path) = self.rom { emulator.load_cart(SimId::Player1, &path)?; } + if self.start_paused { + emulator.pause_sims()?; + } Ok(emulator) } } diff --git a/src/gdbserver.rs b/src/gdbserver.rs index 0223fcc..fd95938 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -41,6 +41,7 @@ impl GdbServer { } 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(); diff --git a/src/main.rs b/src/main.rs index 0d97e7d..1943ef5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -87,6 +87,9 @@ fn main() -> Result<()> { if let Some(path) = args.rom { builder = builder.with_rom(&path); } + if args.debug_port.is_some() { + builder = builder.start_paused(true); + } ThreadBuilder::default() .name("Emulator".to_owned()) diff --git a/src/window/gdb.rs b/src/window/gdb.rs index 9d6fa0f..e384eb2 100644 --- a/src/window/gdb.rs +++ b/src/window/gdb.rs @@ -1,6 +1,8 @@ use egui::{Button, CentralPanel, TextEdit, ViewportBuilder, ViewportId}; +use winit::event_loop::EventLoopProxy; use crate::{ + app::UserEvent, emulator::{EmulatorClient, SimId}, gdbserver::{GdbServer, GdbServerStatus}, }; @@ -10,21 +12,26 @@ use super::AppWindow; pub struct GdbServerWindow { sim_id: SimId, port_str: String, + launched: bool, server: GdbServer, + proxy: EventLoopProxy, } impl GdbServerWindow { - pub fn new(sim_id: SimId, client: EmulatorClient) -> Self { + pub fn new(sim_id: SimId, client: EmulatorClient, proxy: EventLoopProxy) -> Self { Self { sim_id, port_str: (8080 + sim_id.to_index()).to_string(), + launched: false, server: GdbServer::new(sim_id, client), + proxy, } } - pub fn start(&mut self, port: u16) { + pub fn launch(&mut self, port: u16) { self.server.stop(); self.port_str = port.to_string(); + self.launched = true; self.server.start(port); } } @@ -57,6 +64,9 @@ impl AppWindow for GdbServerWindow { }); if !status.running() { + if self.launched { + self.proxy.send_event(UserEvent::Quit).unwrap(); + } let start_button = Button::new("Start"); if ui.add_enabled(port_num.is_some(), start_button).clicked() { let port = port_num.unwrap(); -- 2.40.1 From 84f2cf7ecea35fddce7d1848ba2547c31b7b23bf Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 5 Jan 2025 13:44:59 -0500 Subject: [PATCH 17/32] Implement stepping --- src/emulator.rs | 22 ++++++++++++++++--- src/emulator/shrooms_vb_core.rs | 38 +++++++++++++++++++++++++++------ src/gdbserver.rs | 9 +++++++- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/src/emulator.rs b/src/emulator.rs index b4d5ab8..b52cc75 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -315,7 +315,7 @@ impl Emulator { fn stop_debugging(&mut self, sim_id: SimId) { if let Some(sim) = self.sims.get_mut(sim_id.to_index()) { - sim.clear_breakpoints(); + sim.clear_debug_state(); } self.debuggers.remove(&sim_id); if self.debuggers.is_empty() { @@ -345,12 +345,21 @@ impl Emulator { } } - fn debug_continue(&mut self, sim_id: SimId) { + 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; + 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) { @@ -412,6 +421,7 @@ impl Emulator { }; if let Some(reason) = sim.stop_reason() { let stop_reason = match reason { + StopReason::Stepped => DebugStopReason::Trace, StopReason::Breakpoint => DebugStopReason::Breakpoint, }; self.debug_stop(sim_id, stop_reason); @@ -509,6 +519,9 @@ impl Emulator { 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; @@ -596,6 +609,7 @@ pub enum EmulatorCommand { StopDebugging(SimId), DebugInterrupt(SimId), DebugContinue(SimId), + DebugStep(SimId), ReadRegister(SimId, VBRegister, oneshot::Sender), ReadMemory(SimId, Range, Vec, oneshot::Sender>), AddBreakpoint(SimId, u32), @@ -628,6 +642,8 @@ type DebugSender = tokio::sync::mpsc::UnboundedSender; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum DebugStopReason { + // We are stepping + Trace, // We hit a breakpoint Breakpoint, // The debugger told us to pause diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index e027869..5ce58de 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -147,12 +147,18 @@ extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: // 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 data.breakpoints.binary_search(&address).is_err() { - return 0; + let mut stopped = 0; + if data.step_from.is_some_and(|s| s != address) { + data.step_from = None; + data.stop_reason = Some(StopReason::Stepped); + stopped = 1; + } + if data.breakpoints.binary_search(&address).is_ok() { + data.stop_reason = Some(StopReason::Breakpoint); + stopped = 1; } - data.stop_reason = Some(StopReason::Breakpoint); - 1 + stopped } const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4; @@ -162,6 +168,7 @@ pub const EXPECTED_FRAME_SIZE: usize = 834 * 2; struct VBState { frame_seen: bool, stop_reason: Option, + step_from: Option, breakpoints: Vec, } @@ -172,6 +179,7 @@ pub struct Sim { pub enum StopReason { Breakpoint, + Stepped, } impl Sim { @@ -183,12 +191,14 @@ impl Sim { let sim: *mut VB = Box::into_raw(memory.into_boxed_slice()).cast(); unsafe { vb_init(sim) }; unsafe { vb_set_option(sim, VBOption::PseudoHalt, 1) }; + unsafe { vb_set_keys(sim, VBKey::SGN.bits()) }; unsafe { vb_reset(sim) }; // set up userdata let state = VBState { frame_seen: false, stop_reason: None, + step_from: None, breakpoints: vec![], }; unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) }; @@ -361,21 +371,35 @@ impl Sim { let data = self.get_state(); if let Ok(index) = data.breakpoints.binary_search(&address) { data.breakpoints.remove(index); - if data.breakpoints.is_empty() { + if data.step_from.is_none() && data.breakpoints.is_empty() { unsafe { vb_set_execute_callback(self.sim, None) }; } } } - pub fn clear_breakpoints(&mut self) { + 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(); unsafe { vb_set_execute_callback(self.sim, None) }; } pub fn stop_reason(&mut self) -> Option { let data = self.get_state(); - data.stop_reason.take() + let reason = data.stop_reason.take(); + if data.step_from.is_none() && data.breakpoints.is_empty() { + unsafe { vb_set_execute_callback(self.sim, None) }; + } + reason } fn get_state(&mut self) -> &mut VBState { diff --git a/src/gdbserver.rs b/src/gdbserver.rs index fd95938..7e2b5af 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -258,6 +258,12 @@ impl GdbConnection { 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("s") { + 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::()?; @@ -348,7 +354,8 @@ impl Drop for GdbConnection { fn debug_stop_reason_string(reason: Option) -> &'static str { match reason { - Some(DebugStopReason::Breakpoint) => "T05;thread:p1.t1;threads;p1.t1;reason:breakpoint", + Some(DebugStopReason::Trace) => "T05;thread:p1.t1;threads:p1.t1;reason:trace;", + Some(DebugStopReason::Breakpoint) => "T05;thread:p1.t1;threads:p1.t1;reason:breakpoint;", Some(DebugStopReason::Trapped) => "T05;swbreak;thread:p1.t1;threads:p1.t1;reason:trap;", None => "T00;thread:p1.t1;threads:p1.t1;", } -- 2.40.1 From 2cf99dbc21150ff8b0736f725f9d9cd9bbb2be0d Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 5 Jan 2025 13:54:53 -0500 Subject: [PATCH 18/32] Fix wrapping memory reads for 64-bit addresses --- src/emulator.rs | 7 +++---- src/emulator/shrooms_vb_core.rs | 8 +++++--- src/gdbserver.rs | 7 ++++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/emulator.rs b/src/emulator.rs index b52cc75..5ddb560 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -3,7 +3,6 @@ use std::{ fmt::Display, fs::{self, File}, io::{Read, Seek, SeekFrom, Write}, - ops::Range, path::{Path, PathBuf}, sync::{ atomic::{AtomicBool, Ordering}, @@ -529,11 +528,11 @@ impl Emulator { let value = sim.read_register(register); let _ = done.send(value); } - EmulatorCommand::ReadMemory(sim_id, addresses, mut buffer, done) => { + 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(addresses, &mut buffer); + sim.read_memory(start, length, &mut buffer); let _ = done.send(buffer); } EmulatorCommand::AddBreakpoint(sim_id, address) => { @@ -611,7 +610,7 @@ pub enum EmulatorCommand { DebugContinue(SimId), DebugStep(SimId), ReadRegister(SimId, VBRegister, oneshot::Sender), - ReadMemory(SimId, Range, Vec, oneshot::Sender>), + ReadMemory(SimId, u32, usize, Vec, oneshot::Sender>), AddBreakpoint(SimId, u32), RemoveBreakpoint(SimId, u32), SetAudioEnabled(bool, bool), diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index 5ce58de..26aa0cd 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -1,4 +1,4 @@ -use std::{ffi::c_void, ops::Range, ptr, slice}; +use std::{ffi::c_void, ptr, slice}; use anyhow::{anyhow, Result}; use bitflags::bitflags; @@ -349,10 +349,12 @@ impl Sim { } } - pub fn read_memory(&mut self, addresses: Range, into: &mut Vec) { - for address in addresses { + 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); } } diff --git a/src/gdbserver.rs b/src/gdbserver.rs index 7e2b5af..b25767a 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -280,17 +280,18 @@ impl GdbConnection { } } else if let Some(op) = req.match_some_str(["m", "x"]) { let mut read_memory = || { - let start = req.match_hex::()?; + let start = req.match_hex::()? as u32; if !req.match_str(",") { return None; }; - let size = req.match_hex::()?; + let length = req.match_hex::()?; let mut buf = self.memory_buf.take().unwrap_or_default(); buf.clear(); let (tx, rx) = ::oneshot::channel(); self.client.send_command(EmulatorCommand::ReadMemory( self.sim_id, - start..(start + size), + start, + length, buf, tx, )); -- 2.40.1 From fda738fb93778c091a40a14852f4fc8862563694 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 5 Jan 2025 15:03:59 -0500 Subject: [PATCH 19/32] Fix some debug protocol issues --- src/gdbserver.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/gdbserver.rs b/src/gdbserver.rs index b25767a..ce49814 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -239,7 +239,7 @@ impl GdbConnection { self.response() } } else if req.match_str("vCont?") { - self.response().write_str("vCont;c;") + 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") @@ -258,7 +258,7 @@ impl GdbConnection { 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("s") { + } else if req.match_some_str(["s", "vCont;s:"]).is_some() { self.client .send_command(EmulatorCommand::DebugStep(self.sim_id)); self.stop_reason = None; @@ -280,13 +280,25 @@ impl GdbConnection { } } else if let Some(op) = req.match_some_str(["m", "x"]) { let mut read_memory = || { - let start = req.match_hex::()? as u32; + let start = req.match_hex::()?; if !req.match_str(",") { return None; }; let length = req.match_hex::()?; + let mut buf = self.memory_buf.take().unwrap_or_default(); 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 { + return Some(buf); + } let (tx, rx) = ::oneshot::channel(); self.client.send_command(EmulatorCommand::ReadMemory( self.sim_id, @@ -299,7 +311,9 @@ impl GdbConnection { }; if let Some(memory) = read_memory() { let mut res = self.response(); - if op == "m" { + 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); -- 2.40.1 From fd7298d24e937c5228e086f35e519fc0dc8634f8 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Thu, 9 Jan 2025 23:00:46 -0500 Subject: [PATCH 20/32] Add a checkbox to quit on disconnect --- src/app.rs | 10 +++++++--- src/window.rs | 5 +++++ src/window/game.rs | 12 +++++++++--- src/window/gdb.rs | 21 ++++++++++++++++----- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/app.rs b/src/app.rs index a421b5e..a326620 100644 --- a/src/app.rs +++ b/src/app.rs @@ -211,8 +211,12 @@ impl ApplicationHandler for Application { ); self.open(event_loop, Box::new(p2)); } - UserEvent::Quit => { - event_loop.exit(); + UserEvent::Quit(sim_id) => { + self.viewports + .retain(|_, viewport| viewport.app.sim_id() != sim_id); + if !self.viewports.contains_key(&ViewportId::ROOT) { + event_loop.exit(); + } } } } @@ -401,7 +405,7 @@ pub enum UserEvent { OpenDebugger(SimId), OpenInput, OpenPlayer2, - Quit, + Quit(SimId), } pub enum Action { diff --git a/src/window.rs b/src/window.rs index b60e9c3..85048ff 100644 --- a/src/window.rs +++ b/src/window.rs @@ -5,6 +5,8 @@ pub use gdb::GdbServerWindow; pub use input::InputWindow; use winit::event::KeyEvent; +use crate::emulator::SimId; + mod about; mod game; mod game_screen; @@ -13,6 +15,9 @@ 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 a1ecebd..dd83dc5 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -78,19 +78,20 @@ impl GameWindow { 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 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(is_ready, Button::new("Pause")).clicked() { + if ui.add_enabled(can_pause, Button::new("Pause")).clicked() { self.client.send_command(EmulatorCommand::Pause); ui.close_menu(); } - } else if ui.add_enabled(can_pause, Button::new("Resume")).clicked() { + } else if ui.add_enabled(can_resume, Button::new("Resume")).clicked() { self.client.send_command(EmulatorCommand::Resume); ui.close_menu(); } @@ -317,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") @@ -380,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 index e384eb2..50d1910 100644 --- a/src/window/gdb.rs +++ b/src/window/gdb.rs @@ -12,7 +12,8 @@ use super::AppWindow; pub struct GdbServerWindow { sim_id: SimId, port_str: String, - launched: bool, + connected: bool, + quit_on_disconnect: bool, server: GdbServer, proxy: EventLoopProxy, } @@ -22,7 +23,8 @@ impl GdbServerWindow { Self { sim_id, port_str: (8080 + sim_id.to_index()).to_string(), - launched: false, + connected: false, + quit_on_disconnect: false, server: GdbServer::new(sim_id, client), proxy, } @@ -31,8 +33,9 @@ impl GdbServerWindow { pub fn launch(&mut self, port: u16) { self.server.stop(); self.port_str = port.to_string(); - self.launched = true; + self.quit_on_disconnect = true; self.server.start(port); + self.connected = true; } } @@ -41,6 +44,10 @@ impl AppWindow for GdbServerWindow { 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)) @@ -61,16 +68,20 @@ impl AppWindow for GdbServerWindow { 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.launched { - self.proxy.send_event(UserEvent::Quit).unwrap(); + 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"); if ui.add_enabled(port_num.is_some(), start_button).clicked() { let port = port_num.unwrap(); self.server.start(port); + self.connected = true; } } else { let stop_button = Button::new("Stop"); -- 2.40.1 From af9a4ae8ee3749bb411a3ad476565dfb0b234e99 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 13 Jan 2025 00:30:47 -0500 Subject: [PATCH 21/32] Support watchpoints --- src/emulator.rs | 22 ++- src/emulator/address_set.rs | 231 ++++++++++++++++++++++++++++++++ src/emulator/shrooms_vb_core.rs | 193 ++++++++++++++++++++++++-- src/gdbserver.rs | 113 +++++++++++++--- 4 files changed, 527 insertions(+), 32 deletions(-) create mode 100644 src/emulator/address_set.rs diff --git a/src/emulator.rs b/src/emulator.rs index 5ddb560..9ec0380 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -18,8 +18,9 @@ use egui_toast::{Toast, ToastKind, ToastOptions}; use crate::{audio::Audio, graphics::TextureSink}; 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; #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] @@ -421,6 +422,9 @@ impl Emulator { 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); @@ -547,6 +551,18 @@ impl Emulator { }; 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); self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release); @@ -613,6 +629,8 @@ pub enum EmulatorCommand { ReadMemory(SimId, u32, usize, Vec, oneshot::Sender>), AddBreakpoint(SimId, u32), RemoveBreakpoint(SimId, u32), + AddWatchpoint(SimId, u32, usize, VBWatchpointType), + RemoveWatchpoint(SimId, u32, usize, VBWatchpointType), SetAudioEnabled(bool, bool), Link, Unlink, @@ -645,6 +663,8 @@ pub enum DebugStopReason { Trace, // We hit a breakpoint Breakpoint, + // We hit a watchpoint + Watchpoint(VBWatchpointType, u32), // The debugger told us to pause Trapped, } diff --git a/src/emulator/address_set.rs b/src/emulator/address_set.rs new file mode 100644 index 0000000..fe8e5be --- /dev/null +++ b/src/emulator/address_set.rs @@ -0,0 +1,231 @@ +use std::{ + collections::{BTreeMap, HashSet}, + ops::Bound, +}; + +#[derive(Debug, Default)] +pub struct AddressSet { + ranges: HashSet<(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) + } +} + +#[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)); + } + + #[test] + fn should_include_addresses_when_full() { + let mut set = AddressSet::new(); + set.add(0x00000000, 0x100000000); + assert!(set.contains(0x13374200)); + } + + #[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)); + } + } +} diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index 147992a..208f86a 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], @@ -62,9 +64,31 @@ pub enum VBRegister { 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 = 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" { @@ -112,15 +136,17 @@ extern "C" { #[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); + fn vb_set_execute_callback(sim: *mut VB, callback: Option) -> Option; #[link_name = "vbSetFrameCallback"] - fn vb_set_frame_callback(sim: *mut VB, callback: Option); + 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 = "vbSetReadCallback"] + fn vb_set_read_callback(sim: *mut VB, callback: Option) -> Option; #[link_name = "vbSetSamples"] fn vb_set_samples( sim: *mut VB, @@ -130,6 +156,8 @@ extern "C" { ) -> c_int; #[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; } @@ -147,18 +175,73 @@ extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: // 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 = 0; + 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 = 1; + stopped = true; } if data.breakpoints.binary_search(&address).is_ok() { data.stop_reason = Some(StopReason::Breakpoint); - stopped = 1; + stopped = true; } - stopped + if stopped { + 1 + } else { + 0 + } +} + +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 data.read_watchpoints.contains(address) { + let watch = if data.write_watchpoints.contains(address) { + VBWatchpointType::Access + } else { + VBWatchpointType::Read + }; + data.stop_reason = Some(StopReason::Watchpoint(watch, address)); + } + + // Don't stop here, the debugger expects us to break after the memory access. + // We'll stop in on_execute instead. + 0 +} + +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 data.write_watchpoints.contains(address) { + let watch = if data.read_watchpoints.contains(address) { + VBWatchpointType::Access + } else { + VBWatchpointType::Write + }; + data.stop_reason = Some(StopReason::Watchpoint(watch, address)); + } + + // 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; @@ -170,6 +253,23 @@ struct VBState { 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)] @@ -177,11 +277,6 @@ pub struct Sim { sim: *mut VB, } -pub enum StopReason { - Breakpoint, - Stepped, -} - impl Sim { pub fn new() -> Self { // init the VB instance itself @@ -201,6 +296,8 @@ impl Sim { 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, Some(on_frame)) }; @@ -374,7 +471,71 @@ impl Sim { let data = self.get_state(); if let Ok(index) = data.breakpoints.binary_search(&address) { 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) }; } } @@ -393,13 +554,17 @@ impl Sim { 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.step_from.is_none() && data.breakpoints.is_empty() { + if !data.needs_execute_callback() { unsafe { vb_set_execute_callback(self.sim, None) }; } reason diff --git a/src/gdbserver.rs b/src/gdbserver.rs index ce49814..0c0393f 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -13,7 +13,9 @@ use tokio::{ sync::{mpsc, oneshot}, }; -use crate::emulator::{DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId}; +use crate::emulator::{ + DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId, VBWatchpointType, +}; mod registers; mod request; @@ -187,7 +189,7 @@ impl GdbConnection { } self.stop_reason = Some(reason); self.response() - .write_str(debug_stop_reason_string(self.stop_reason)) + .write_str(&debug_stop_reason_string(self.stop_reason)) } }; Some(res) @@ -251,7 +253,7 @@ impl GdbConnection { bail!("debug process was killed"); } else if req.match_str("?") { 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() { self.client .send_command(EmulatorCommand::DebugContinue(self.sim_id)); @@ -329,18 +331,68 @@ impl GdbConnection { } else { self.response() } - } else if req.match_str("Z0,") { - if let Some(address) = req.match_hex() { - self.client - .send_command(EmulatorCommand::AddBreakpoint(self.sim_id, address)); + } 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("z0,") { - if let Some(address) = req.match_hex() { - self.client - .send_command(EmulatorCommand::RemoveBreakpoint(self.sim_id, address)); + } 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() @@ -367,11 +419,38 @@ impl Drop for GdbConnection { } } -fn debug_stop_reason_string(reason: Option) -> &'static str { - match reason { - Some(DebugStopReason::Trace) => "T05;thread:p1.t1;threads:p1.t1;reason:trace;", - Some(DebugStopReason::Breakpoint) => "T05;thread:p1.t1;threads:p1.t1;reason:breakpoint;", - Some(DebugStopReason::Trapped) => "T05;swbreak;thread:p1.t1;threads:p1.t1;reason:trap;", - None => "T00;thread:p1.t1;threads:p1.t1;", +fn debug_stop_reason_string(reason: Option) -> String { + let mut result = String::new(); + result += if reason.is_some() { "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::Trapped => "trap;", + }; + } + + if let Some(DebugStopReason::Watchpoint(_, address)) = reason { + result += "description:"; + result += &hex::encode(address.to_string()); + result += ";"; + } + + result } -- 2.40.1 From f1c5571aa6cc2ee5a966471472e1f783947443fa Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Tue, 14 Jan 2025 21:38:44 -0500 Subject: [PATCH 22/32] Tell LLDB to use big watch regions --- src/gdbserver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gdbserver.rs b/src/gdbserver.rs index 0c0393f..7034f8b 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -210,7 +210,7 @@ impl GdbConnection { res } else if req.match_str("qSupported:") { 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 .match_some_str([ "QThreadSuffixSupported", -- 2.40.1 From 102aff15808c98b4204be15e9fbcf35cf42b0892 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Tue, 14 Jan 2025 22:25:27 -0500 Subject: [PATCH 23/32] Turn LTO on when cutting releases --- build.Dockerfile | 1 + build.rs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/build.Dockerfile b/build.Dockerfile index 2521d83..21ca4cc 100644 --- a/build.Dockerfile +++ b/build.Dockerfile @@ -29,6 +29,7 @@ ENV PATH="/osxcross/bin:$PATH" \ CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="oa64-clang" \ CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-19" \ CROSS_COMPILE="setting-this-to-silence-a-warning-" \ + SHROOMS_CFLAGS="-flto" \ 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..c478485 100644 --- a/build.rs +++ b/build.rs @@ -8,9 +8,17 @@ 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) -- 2.40.1 From f031bb17b25013379bcc3fc67d267710e164f87e Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Tue, 14 Jan 2025 23:33:30 -0500 Subject: [PATCH 24/32] Use logging --- Cargo.lock | 109 +++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 2 + src/app.rs | 14 +++--- src/audio.rs | 3 +- src/emulator.rs | 5 ++- src/gdbserver.rs | 17 ++++++-- src/main.rs | 15 ++++++- 7 files changed, 148 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fcf8366..73ed9c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1755,6 +1755,12 @@ 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" @@ -1788,6 +1794,8 @@ dependencies = [ "serde_json", "thread-priority", "tokio", + "tracing", + "tracing-subscriber", "wgpu", "windows 0.58.0", "winit", @@ -1883,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" @@ -2068,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" @@ -2408,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" @@ -2701,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]] @@ -2713,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" @@ -2906,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" @@ -3123,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" @@ -3248,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]] @@ -3358,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 06a79ea..9ad9959 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,8 @@ 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/src/app.rs b/src/app.rs index a326620..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, @@ -230,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."); } } } @@ -434,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 9ec0380..117d5fa 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -15,6 +15,7 @@ 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}; use shrooms_vb_core::{Sim, StopReason, EXPECTED_FRAME_SIZE}; @@ -608,7 +609,7 @@ impl Emulator { return; } } - eprintln!("{}", message); + error!("{}", message); } } @@ -704,7 +705,7 @@ impl EmulatorClient { 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/gdbserver.rs b/src/gdbserver.rs index 7034f8b..09a9eb8 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -12,6 +12,7 @@ use tokio::{ select, sync::{mpsc, oneshot}, }; +use tracing::{debug, enabled, error, info, Level}; use crate::emulator::{ DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId, VBWatchpointType, @@ -78,15 +79,19 @@ async fn run_server( port: u16, status: &Mutex, ) { + info!("Connecting to debugger on port {port}..."); let Some(stream) = try_connect(port, status).await else { return; }; + info!("Connected!"); let mut connection = GdbConnection::new(sim_id, client); match connection.run(stream).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()); } } @@ -97,6 +102,7 @@ async fn try_connect(port: u16, status: &Mutex) -> Option l, Err(err) => { + error!(%err, "Could not open port."); *status.lock().unwrap() = GdbServerStatus::Error(err.to_string()); return None; } @@ -107,6 +113,7 @@ async fn try_connect(port: u16, status: &Mutex) -> Option { + error!(%err, "Could not connect to debugger."); *status.lock().unwrap() = GdbServerStatus::Error(err.to_string()); None } @@ -169,9 +176,11 @@ impl GdbConnection { }; if let Some(res) = response { let buffer = res.finish(); - match std::str::from_utf8(&buffer) { - Ok(text) => println!("response: {text}"), - Err(_) => println!("response: {buffer:02x?}"), + 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); @@ -196,7 +205,7 @@ impl GdbConnection { } fn handle_request(&mut self, mut req: Request<'_>) -> Result> { - println!("received {:02x?}", req); + debug!("received {:02x?}", req); if req.kind == RequestKind::Signal { self.client diff --git a/src/main.rs b/src/main.rs index 1943ef5..4ee7949 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,8 @@ 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; @@ -29,6 +31,13 @@ struct Args { 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() { std::panic::set_hook(Box::new(|info| { let mut message = String::new(); @@ -76,6 +85,8 @@ fn set_process_priority_to_high() -> Result<()> { } fn main() -> Result<()> { + init_logger(); + set_panic_handler(); #[cfg(windows)] @@ -97,8 +108,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); } }; -- 2.40.1 From 17d38111245c4dbe761877a1931251b84af03893 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Fri, 17 Jan 2025 22:00:46 -0500 Subject: [PATCH 25/32] No debugging unless the game is running --- src/emulator.rs | 4 ++++ src/gdbserver.rs | 47 ++++++++++++++++++++++++++++++++++++++--------- src/main.rs | 9 ++++++--- src/window/gdb.rs | 8 ++++++-- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/emulator.rs b/src/emulator.rs index 117d5fa..aa065b0 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -305,6 +305,10 @@ impl Emulator { } 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::Trapped), diff --git a/src/gdbserver.rs b/src/gdbserver.rs index 09a9eb8..e5a6231 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -9,7 +9,7 @@ use std::{ use tokio::{ io::{AsyncWriteExt as _, BufReader}, net::{TcpListener, TcpStream}, - select, + pin, select, sync::{mpsc, oneshot}, }; use tracing::{debug, enabled, error, info, Level}; @@ -57,8 +57,9 @@ impl GdbServer { .unwrap() .block_on(async move { select! { - _ = run_server(sim_id, client, port, &status) => {} + _ = run_server(sim_id, client.clone(), port, &status) => {} _ = rx => { + client.send_command(EmulatorCommand::StopDebugging(sim_id)); *status.lock().unwrap() = GdbServerStatus::Stopped; } } @@ -73,19 +74,46 @@ impl GdbServer { } } +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 Some(stream) = try_connect(port, status).await else { - return; + 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).await { + match connection.run(stream, debug_source).await { Ok(()) => { info!("Finished debugging."); *status.lock().unwrap() = GdbServerStatus::Stopped; @@ -154,12 +182,13 @@ impl GdbConnection { memory_buf: None, } } - async fn run(&mut self, stream: TcpStream) -> Result<()> { - let (debug_sink, mut debug_source) = mpsc::unbounded_channel(); + 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)); - self.client - .send_command(EmulatorCommand::StartDebugging(self.sim_id, debug_sink)); loop { let response = select! { maybe_event = debug_source.recv() => { diff --git a/src/main.rs b/src/main.rs index 4ee7949..33ae747 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use std::{path::PathBuf, process, time::SystemTime}; -use anyhow::Result; +use anyhow::{bail, Result}; use app::Application; use clap::Parser; use emulator::EmulatorBuilder; @@ -95,10 +95,13 @@ 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); } diff --git a/src/window/gdb.rs b/src/window/gdb.rs index 50d1910..e89de02 100644 --- a/src/window/gdb.rs +++ b/src/window/gdb.rs @@ -3,7 +3,7 @@ use winit::event_loop::EventLoopProxy; use crate::{ app::UserEvent, - emulator::{EmulatorClient, SimId}, + emulator::{EmulatorClient, SimId, SimState}, gdbserver::{GdbServer, GdbServerStatus}, }; @@ -11,6 +11,7 @@ use super::AppWindow; pub struct GdbServerWindow { sim_id: SimId, + client: EmulatorClient, port_str: String, connected: bool, quit_on_disconnect: bool, @@ -22,6 +23,7 @@ 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, @@ -78,7 +80,9 @@ impl AppWindow for GdbServerWindow { self.connected = false; } 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(); self.server.start(port); self.connected = true; -- 2.40.1 From 0fff4d427f7551bf14dafe948b552a94b385495a Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 18 Jan 2025 00:20:57 -0500 Subject: [PATCH 26/32] Implement writing registers --- src/emulator.rs | 7 +++++++ src/emulator/shrooms_vb_core.rs | 20 ++++++++++++++++++++ src/gdbserver.rs | 26 ++++++++++++++++++++++++++ src/gdbserver/request.rs | 14 ++++++++++++++ 4 files changed, 67 insertions(+) diff --git a/src/emulator.rs b/src/emulator.rs index aa065b0..b0d3d61 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -537,6 +537,12 @@ impl Emulator { 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; @@ -631,6 +637,7 @@ pub enum EmulatorCommand { DebugContinue(SimId), DebugStep(SimId), ReadRegister(SimId, VBRegister, oneshot::Sender), + WriteRegister(SimId, VBRegister, u32), ReadMemory(SimId, u32, usize, Vec, oneshot::Sender>), AddBreakpoint(SimId, u32), RemoveBreakpoint(SimId, u32), diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index 208f86a..773114f 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -145,6 +145,10 @@ extern "C" { 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"] @@ -154,6 +158,8 @@ 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"] @@ -447,6 +453,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) { let mut address = start; for _ in 0..length { diff --git a/src/gdbserver.rs b/src/gdbserver.rs index e5a6231..35ca275 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -318,6 +318,32 @@ impl GdbConnection { } 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 = req.match_hex::()?; diff --git a/src/gdbserver/request.rs b/src/gdbserver/request.rs index b3be216..d0459b3 100644 --- a/src/gdbserver/request.rs +++ b/src/gdbserver/request.rs @@ -60,6 +60,20 @@ 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 struct RequestSource { -- 2.40.1 From 5ce39fb70eb9f14336ce6f785423cde042fd5553 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 18 Jan 2025 01:03:22 -0500 Subject: [PATCH 27/32] Implement writing memory --- src/emulator.rs | 8 +++++ src/emulator/shrooms_vb_core.rs | 10 ++++++ src/gdbserver.rs | 64 +++++++++++++++++++++++++-------- src/gdbserver/request.rs | 9 +++++ 4 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/emulator.rs b/src/emulator.rs index b0d3d61..3dadfe1 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -550,6 +550,13 @@ impl Emulator { 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; @@ -639,6 +646,7 @@ pub enum EmulatorCommand { 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), diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index 773114f..c6952cd 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -166,6 +166,8 @@ extern "C" { 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) -> c_int { @@ -476,6 +478,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) { let data = self.get_state(); if let Err(index) = data.breakpoints.binary_search(&address) { diff --git a/src/gdbserver.rs b/src/gdbserver.rs index 35ca275..b646990 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -346,22 +346,9 @@ impl GdbConnection { } } else if let Some(op) = req.match_some_str(["m", "x"]) { let mut read_memory = || { - let start = req.match_hex::()?; - if !req.match_str(",") { - return None; - }; - let length = req.match_hex::()?; - + let (start, length) = parse_memory_range(&mut req)?; let mut buf = self.memory_buf.take().unwrap_or_default(); 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 { return Some(buf); } @@ -395,6 +382,38 @@ impl GdbConnection { } 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::()?; @@ -483,6 +502,23 @@ impl Drop for GdbConnection { } } +// 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() { "T05;" } else { "T00;" }; diff --git a/src/gdbserver/request.rs b/src/gdbserver/request.rs index d0459b3..86bf600 100644 --- a/src/gdbserver/request.rs +++ b/src/gdbserver/request.rs @@ -74,6 +74,15 @@ impl Request<'_> { 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 { -- 2.40.1 From 9f7895a457d1710d32664a2b76f5c4b3e2b5a3e1 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 18 Jan 2025 01:21:45 -0500 Subject: [PATCH 28/32] Statically compile callbacks --- build.rs | 3 +++ src/emulator/shrooms_vb_core.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/build.rs b/build.rs index c478485..44fd098 100644 --- a/build.rs +++ b/build.rs @@ -23,6 +23,9 @@ fn main() -> Result<(), Box> { .define("VB_LITTLE_ENDIAN", None) .define("VB_SIGNED_PROPAGATE", None) .define("VB_DIV_GENERIC", None) + .define("VB_DIRECT_EXECUTE", "on_execute") + .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/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index c6952cd..b27b9d8 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -178,6 +178,7 @@ extern "C" fn on_frame(sim: *mut VB) -> c_int { 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. @@ -201,6 +202,7 @@ extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: } } +#[no_mangle] extern "C" fn on_read( sim: *mut VB, address: u32, @@ -226,6 +228,7 @@ extern "C" fn on_read( 0 } +#[no_mangle] extern "C" fn on_write( sim: *mut VB, address: u32, -- 2.40.1 From e1a8006af704ab8220a27b821fefb01403501a08 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 18 Jan 2025 13:57:19 -0500 Subject: [PATCH 29/32] Statically compile that callback too --- build.rs | 1 + src/emulator/shrooms_vb_core.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/build.rs b/build.rs index 44fd098..aeb1a79 100644 --- a/build.rs +++ b/build.rs @@ -24,6 +24,7 @@ fn main() -> Result<(), Box> { .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")) diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index b27b9d8..53c99de 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -170,6 +170,7 @@ extern "C" { 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 { // SAFETY: the *mut VB owns its userdata. // There is no way for the userdata to be null or otherwise invalid. -- 2.40.1 From 9d37d22a221939829a228977c6ecfe3e275a49c2 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 18 Jan 2025 17:01:17 -0500 Subject: [PATCH 30/32] Fix LTO for windows/linux, disable for OSX --- README.md | 2 +- build.Dockerfile | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) 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 21ca4cc..dca7dbc 100644 --- a/build.Dockerfile +++ b/build.Dockerfile @@ -23,13 +23,15 @@ 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" \ CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="oa64-clang" \ CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-19" \ CROSS_COMPILE="setting-this-to-silence-a-warning-" \ - SHROOMS_CFLAGS="-flto" \ 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" -- 2.40.1 From d59da1009a4a4b3ecaec67a725aed4cc38cf4db8 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 18 Jan 2025 18:51:25 -0500 Subject: [PATCH 31/32] Fix watchpoint reporting --- src/emulator/address_set.rs | 43 +++++++++++++++++++++++++++++++-- src/emulator/shrooms_vb_core.rs | 8 +++--- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/emulator/address_set.rs b/src/emulator/address_set.rs index fe8e5be..4e8ee17 100644 --- a/src/emulator/address_set.rs +++ b/src/emulator/address_set.rs @@ -1,11 +1,11 @@ use std::{ - collections::{BTreeMap, HashSet}, + collections::{BTreeMap, BTreeSet}, ops::Bound, }; #[derive(Debug, Default)] pub struct AddressSet { - ranges: HashSet<(u32, usize)>, + ranges: BTreeSet<(u32, usize)>, bounds: BTreeMap, } @@ -109,6 +109,22 @@ impl AddressSet { .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)] @@ -120,6 +136,7 @@ mod tests { let set = AddressSet::new(); assert!(set.is_empty()); assert!(!set.contains(0x13374200)); + assert_eq!(set.start_of_range_containing(0x13374200), None); } #[test] @@ -127,6 +144,7 @@ mod tests { let mut set = AddressSet::new(); set.add(0x00000000, 0x100000000); assert!(set.contains(0x13374200)); + assert_eq!(set.start_of_range_containing(0x13374200), Some(0x00000000)); } #[test] @@ -228,4 +246,25 @@ mod tests { 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 53c99de..e15ca2c 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -215,13 +215,13 @@ extern "C" fn on_read( // 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 data.read_watchpoints.contains(address) { + 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, address)); + data.stop_reason = Some(StopReason::Watchpoint(watch, start)); } // Don't stop here, the debugger expects us to break after the memory access. @@ -242,13 +242,13 @@ extern "C" fn on_write( // 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 data.write_watchpoints.contains(address) { + 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, address)); + data.stop_reason = Some(StopReason::Watchpoint(watch, start)); } // Don't stop here, the debugger expects us to break after the memory access. -- 2.40.1 From 16a2aaee92bf2ea491ffca9e6938fe32869b12bd Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 18 Jan 2025 19:10:55 -0500 Subject: [PATCH 32/32] Fix status returned on interrupt --- src/emulator.rs | 6 +++--- src/gdbserver.rs | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/emulator.rs b/src/emulator.rs index 3dadfe1..96df37d 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -311,7 +311,7 @@ impl Emulator { } let debug = DebugInfo { sender, - stop_reason: Some(DebugStopReason::Trapped), + stop_reason: Some(DebugStopReason::Paused), }; self.debuggers.insert(sim_id, debug); self.state @@ -334,7 +334,7 @@ impl Emulator { } 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) { @@ -686,7 +686,7 @@ pub enum DebugStopReason { // We hit a watchpoint Watchpoint(VBWatchpointType, u32), // The debugger told us to pause - Trapped, + Paused, } struct DebugInfo { diff --git a/src/gdbserver.rs b/src/gdbserver.rs index b646990..6c59ccd 100644 --- a/src/gdbserver.rs +++ b/src/gdbserver.rs @@ -521,7 +521,11 @@ fn parse_memory_range(req: &mut Request<'_>) -> Option<(u32, usize)> { fn debug_stop_reason_string(reason: Option) -> String { let mut result = String::new(); - result += if reason.is_some() { "T05;" } else { "T00;" }; + result += if reason.is_some_and(|r| r != DebugStopReason::Paused) { + "T05;" + } else { + "T00;" + }; if let Some(DebugStopReason::Breakpoint) = reason { result += "swbreak;"; } @@ -542,7 +546,7 @@ fn debug_stop_reason_string(reason: Option) -> String { DebugStopReason::Trace => "trace;", DebugStopReason::Breakpoint => "breakpoint;", DebugStopReason::Watchpoint(_, _) => "watchpoint;", - DebugStopReason::Trapped => "trap;", + DebugStopReason::Paused => "trap;", }; } -- 2.40.1