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); + } + } + }); + } +}