Implement GDB/LLDB compatible server #3

Merged
SonicSwordcane merged 33 commits from debugger into main 2025-01-19 00:13:43 +00:00
4 changed files with 392 additions and 246 deletions
Showing only changes of commit 11df670ff4 - Show all commits

View File

@ -151,6 +151,7 @@ pub struct Emulator {
linked: Arc<AtomicBool>, linked: Arc<AtomicBool>,
renderers: HashMap<SimId, TextureSink>, renderers: HashMap<SimId, TextureSink>,
messages: HashMap<SimId, mpsc::Sender<Toast>>, messages: HashMap<SimId, mpsc::Sender<Toast>>,
debuggers: HashMap<SimId, DebugInfo>,
eye_contents: Vec<u8>, eye_contents: Vec<u8>,
audio_samples: Vec<f32>, audio_samples: Vec<f32>,
} }
@ -174,6 +175,7 @@ impl Emulator {
linked, linked,
renderers: HashMap::new(), renderers: HashMap::new(),
messages: HashMap::new(), messages: HashMap::new(),
debuggers: HashMap::new(),
eye_contents: vec![0u8; 384 * 224 * 2], eye_contents: vec![0u8; 384 * 224 * 2],
audio_samples: vec![0.0; EXPECTED_FRAME_SIZE], audio_samples: vec![0.0; EXPECTED_FRAME_SIZE],
}) })
@ -288,6 +290,48 @@ impl Emulator {
Ok(()) 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) { pub fn run(&mut self) {
loop { loop {
let idle = self.tick(); let idle = self.tick();
@ -320,8 +364,14 @@ impl Emulator {
let p1_state = self.sim_state[SimId::Player1.to_index()].load(Ordering::Acquire); 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 p2_state = self.sim_state[SimId::Player2.to_index()].load(Ordering::Acquire);
let state = self.state.load(Ordering::Acquire); let state = self.state.load(Ordering::Acquire);
let p1_running = state == EmulatorState::Running && p1_state == SimState::Ready; // Don't emulate if the state is "paused", or if any sim is paused in the debugger
let p2_running = state == EmulatorState::Running && p2_state == SimState::Ready; 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; let mut idle = !p1_running && !p2_running;
if p1_running && p2_running { if p1_running && p2_running {
Sim::emulate_many(&mut self.sims); Sim::emulate_many(&mut self.sims);
@ -405,6 +455,18 @@ impl Emulator {
EmulatorCommand::Resume => { EmulatorCommand::Resume => {
self.resume_sims(); 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) => { EmulatorCommand::ReadRegister(sim_id, register, done) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return; return;
@ -476,6 +538,10 @@ pub enum EmulatorCommand {
StopSecondSim, StopSecondSim,
Pause, Pause,
Resume, Resume,
StartDebugging(SimId, DebugSender),
StopDebugging(SimId),
DebugInterrupt(SimId),
DebugContinue(SimId),
ReadRegister(SimId, VBRegister, oneshot::Sender<u32>), ReadRegister(SimId, VBRegister, oneshot::Sender<u32>),
ReadMemory(SimId, Range<u32>, Vec<u8>, oneshot::Sender<Vec<u8>>), ReadMemory(SimId, Range<u32>, Vec<u8>, oneshot::Sender<Vec<u8>>),
SetAudioEnabled(bool, bool), SetAudioEnabled(bool, bool),
@ -499,6 +565,24 @@ pub enum SimState {
pub enum EmulatorState { pub enum EmulatorState {
Paused, Paused,
Running, Running,
Debugging,
}
type DebugSender = tokio::sync::mpsc::UnboundedSender<DebugEvent>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DebugStopReason {
// The debugger told us to pause
Trapped,
}
struct DebugInfo {
sender: DebugSender,
stop_reason: Option<DebugStopReason>,
}
pub enum DebugEvent {
Stopped(DebugStopReason),
} }
#[derive(Clone)] #[derive(Clone)]

View File

@ -1,22 +1,19 @@
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use registers::REGISTERS; use registers::REGISTERS;
use request::{Request, RequestKind}; use request::{Request, RequestKind, RequestSource};
use response::Response; use response::Response;
use std::{ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
thread, thread,
}; };
use tokio::{ use tokio::{
io::{AsyncWriteExt as _, BufReader, BufWriter}, io::{AsyncWriteExt as _, BufReader},
net::{ net::{TcpListener, TcpStream},
tcp::{OwnedReadHalf, OwnedWriteHalf},
TcpListener, TcpStream,
},
select, select,
sync::oneshot, sync::{mpsc, oneshot},
}; };
use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; use crate::emulator::{DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId};
mod registers; mod registers;
mod request; mod request;
@ -81,8 +78,8 @@ async fn run_server(
let Some(stream) = try_connect(port, status).await else { let Some(stream) = try_connect(port, status).await else {
return; return;
}; };
let connection = GdbConnection::new(sim_id, client, stream); let mut connection = GdbConnection::new(sim_id, client);
match connection.run().await { match connection.run(stream).await {
Ok(()) => { Ok(()) => {
*status.lock().unwrap() = GdbServerStatus::Stopped; *status.lock().unwrap() = GdbServerStatus::Stopped;
} }
@ -130,191 +127,191 @@ impl GdbServerStatus {
struct GdbConnection { struct GdbConnection {
sim_id: SimId, sim_id: SimId,
client: EmulatorClient, client: EmulatorClient,
stream_in: BufReader<OwnedReadHalf>,
stream_out: BufWriter<OwnedWriteHalf>,
ack_messages: bool, ack_messages: bool,
request_buf: Vec<u8>, stop_reason: Option<DebugStopReason>,
response_buf: Option<Vec<u8>>, response_buf: Option<Vec<u8>>,
memory_buf: Option<Vec<u8>>, memory_buf: Option<Vec<u8>>,
} }
impl GdbConnection { impl GdbConnection {
fn new(sim_id: SimId, client: EmulatorClient, stream: TcpStream) -> Self { fn new(sim_id: SimId, client: EmulatorClient) -> Self {
let (rx, tx) = stream.into_split();
Self { Self {
sim_id, sim_id,
client, client,
stream_in: BufReader::new(rx),
stream_out: BufWriter::new(tx),
ack_messages: true, ack_messages: true,
request_buf: vec![], stop_reason: None,
response_buf: None, response_buf: None,
memory_buf: None, memory_buf: None,
} }
} }
async fn run(mut self) -> Result<()> { async fn run(&mut self, stream: TcpStream) -> Result<()> {
println!("Connected for {}", self.sim_id); let (debug_sink, mut debug_source) = mpsc::unbounded_channel();
self.client.send_command(EmulatorCommand::Pause); 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 { loop {
let mut req = Request::read(&mut self.stream_in, &mut self.request_buf).await?; let response = select! {
println!("received {:02x?}", req); maybe_event = debug_source.recv() => {
let Some(event) = maybe_event else {
if req.kind == RequestKind::Signal { // debugger has stopped running
self.client.send_command(EmulatorCommand::Pause); break;
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::<usize>()?;
REGISTERS.get(register)
};
let Some(reg_info) = get_reg_info() else {
self.send_empty().await?;
continue;
};
let res = self.response().write_str(&reg_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::<usize>()?;
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::<u32>()?;
if !req.match_str(",") {
return None;
}; };
let size = req.match_hex::<u32>()?; self.handle_event(event)
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.memory_buf = Some(memory); maybe_request = request_source.recv() => {
self.send(res).await?; let req = maybe_request?;
} else if req.match_str("x") { self.handle_request(req)?
let mut read_memory = || { }
let start = req.match_hex::<u32>()?; };
if !req.match_str(",") { if let Some(res) = response {
return None; let buffer = res.finish();
}; match std::str::from_utf8(&buffer) {
let size = req.match_hex::<u32>()?; Ok(text) => println!("response: {text}"),
let mut buf = self.memory_buf.take().unwrap_or_default(); Err(_) => println!("response: {buffer:02x?}"),
buf.clear(); }
let (tx, rx) = ::oneshot::channel(); tx.write_all(&buffer).await?;
self.client.send_command(EmulatorCommand::ReadMemory( self.response_buf = Some(buffer);
self.sim_id, tx.flush().await?;
start..(start + size), }
buf, }
tx, Ok(())
)); }
rx.recv().ok()
}; fn handle_event(&mut self, event: DebugEvent) -> Option<Response> {
let Some(memory) = read_memory() else { let res = match event {
self.send_empty().await?; DebugEvent::Stopped(reason) => {
continue; 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<Option<Response>> {
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::<usize>()?;
REGISTERS.get(register)
};
if let Some(reg_info) = get_reg_info() {
self.response().write_str(&reg_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::<usize>()?;
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::<u32>()?;
if !req.match_str(",") {
return None;
}; };
let size = req.match_hex::<u32>()?;
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(); let mut res = self.response();
if memory.is_empty() { if op == "m" {
res = res.write_str("OK"); // send the hex-encoded byte stream
for byte in &memory {
res = res.write_hex(*byte);
}
} else { } else {
// send the raw byte stream
for byte in &memory { for byte in &memory {
res = res.write_byte(*byte); res = res.write_byte(*byte);
} }
} }
self.memory_buf = Some(memory); self.memory_buf = Some(memory);
self.send(res).await?; res
} else { } else {
// unrecognized command self.response()
self.send_empty().await?;
} }
} } else {
} // unrecognized command
self.response()
async fn send_empty(&mut self) -> std::io::Result<()> { };
let res = self.response(); Ok(Some(res))
self.send(res).await
} }
fn response(&mut self) -> Response { fn response(&mut self) -> Response {
@ -323,15 +320,18 @@ impl GdbConnection {
self.ack_messages, self.ack_messages,
) )
} }
}
async fn send(&mut self, res: Response) -> std::io::Result<()> { impl Drop for GdbConnection {
let buffer = res.finish(); fn drop(&mut self) {
match std::str::from_utf8(&buffer) { self.client
Ok(text) => println!("response: {text}"), .send_command(EmulatorCommand::StopDebugging(self.sim_id));
Err(_) => println!("response: {buffer:02x?}"), }
} }
self.stream_out.write_all(&buffer).await?;
self.response_buf = Some(buffer); fn debug_stop_reason_string(reason: Option<DebugStopReason>) -> &'static str {
self.stream_out.flush().await match reason {
Some(DebugStopReason::Trapped) => "T05;thread:p1.t1;threads:p1.t1;reason:trap;",
None => "T00;thread:p1.t1;threads:p1.t1;",
} }
} }

View File

@ -35,66 +35,7 @@ impl std::fmt::Debug for Request<'_> {
} }
} }
impl<'a> Request<'a> { impl Request<'_> {
pub async fn read<R: AsyncRead + Unpin>(
reader: &mut R,
buffer: &'a mut Vec<u8>,
) -> Result<Self> {
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,
})
}
pub fn match_str(&mut self, prefix: &str) -> bool { pub fn match_str(&mut self, prefix: &str) -> bool {
if let Some(new_buffer) = self.buffer.strip_prefix(prefix.as_bytes()) { if let Some(new_buffer) = self.buffer.strip_prefix(prefix.as_bytes()) {
self.buffer = new_buffer; self.buffer = new_buffer;
@ -103,6 +44,13 @@ impl<'a> Request<'a> {
false false
} }
pub fn match_some_str<'a, I: IntoIterator<Item = &'a str>>(
&mut self,
prefixes: I,
) -> Option<&'a str> {
prefixes.into_iter().find(|&prefix| self.match_str(prefix))
}
pub fn match_hex<I: FromRadix16>(&mut self) -> Option<I> { pub fn match_hex<I: FromRadix16>(&mut self) -> Option<I> {
match I::from_radix_16(self.buffer) { match I::from_radix_16(self.buffer) {
(_, 0) => None, (_, 0) => None,
@ -113,3 +61,119 @@ impl<'a> Request<'a> {
} }
} }
} }
pub struct RequestSource<R> {
reader: R,
buffer: Vec<u8>,
state: RequestReadState,
}
impl<R: AsyncRead + Unpin> RequestSource<R> {
pub fn new(reader: R) -> Self {
Self {
reader,
buffer: vec![],
state: RequestReadState::Header,
}
}
pub async fn recv(&mut self) -> Result<Request<'_>> {
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,
},
}

View File

@ -83,20 +83,18 @@ impl GameWindow {
}); });
ui.menu_button("Emulation", |ui| { ui.menu_button("Emulation", |ui| {
let state = self.client.emulator_state(); 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 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); self.client.send_command(EmulatorCommand::Pause);
ui.close_menu(); ui.close_menu();
} }
} else if ui } else if ui.add_enabled(can_pause, Button::new("Resume")).clicked() {
.add_enabled(can_interact, Button::new("Resume"))
.clicked()
{
self.client.send_command(EmulatorCommand::Resume); self.client.send_command(EmulatorCommand::Resume);
ui.close_menu(); 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 self.client
.send_command(EmulatorCommand::Reset(self.sim_id)); .send_command(EmulatorCommand::Reset(self.sim_id));
ui.close_menu(); ui.close_menu();