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();