diff --git a/src/app.rs b/src/app.rs index f715617..40957da 100644 --- a/src/app.rs +++ b/src/app.rs @@ -24,7 +24,8 @@ use crate::{ persistence::Persistence, window::{ AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow, - GdbServerWindow, HotkeysWindow, InputWindow, ObjectWindow, RegisterWindow, WorldWindow, + GdbServerWindow, HotkeysWindow, InputWindow, ObjectWindow, RegisterWindow, TerminalWindow, + WorldWindow, }, }; @@ -243,6 +244,10 @@ impl ApplicationHandler for Application { let registers = RegisterWindow::new(sim_id, &self.memory); self.open(event_loop, Box::new(registers)); } + UserEvent::OpenTerminal(sim_id) => { + let terminal = TerminalWindow::new(sim_id, &self.client); + self.open(event_loop, Box::new(terminal)); + } UserEvent::OpenDebugger(sim_id) => { let debugger = GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone()); @@ -513,6 +518,7 @@ pub enum UserEvent { OpenWorlds(SimId), OpenFrameBuffers(SimId), OpenRegisters(SimId), + OpenTerminal(SimId), OpenDebugger(SimId), OpenInput, OpenHotkeys, diff --git a/src/emulator.rs b/src/emulator.rs index f2e83dd..b4d0ba8 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -43,6 +43,13 @@ impl SimId { Self::Player2 => 1, } } + pub const fn from_index(index: usize) -> Option { + match index { + 0 => Some(Self::Player1), + 1 => Some(Self::Player2), + _ => None, + } + } } impl Display for SimId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -169,6 +176,7 @@ pub struct Emulator { renderers: HashMap, messages: HashMap>, debuggers: HashMap, + stdouts: HashMap>, watched_regions: HashMap>, eye_contents: Vec, audio_samples: Vec, @@ -195,6 +203,7 @@ impl Emulator { renderers: HashMap::new(), messages: HashMap::new(), debuggers: HashMap::new(), + stdouts: HashMap::new(), watched_regions: HashMap::new(), eye_contents: vec![0u8; 384 * 224 * 2], audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE), @@ -228,8 +237,15 @@ impl Emulator { let index = sim_id.to_index(); while self.sims.len() <= index { + let new_index = self.sims.len(); self.sims.push(Sim::new()); - self.sim_state[index].store(SimState::NoGame, Ordering::Release); + if self + .stdouts + .contains_key(&SimId::from_index(new_index).unwrap()) + { + self.sims[new_index].watch_stdout(true); + } + self.sim_state[new_index].store(SimState::NoGame, Ordering::Release); } let sim = &mut self.sims[index]; sim.reset(); @@ -471,6 +487,20 @@ impl Emulator { self.state.store(EmulatorState::Paused, Ordering::Release); } + // stdout + self.stdouts.retain(|sim_id, stdout| { + let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { + return true; + }; + if let Some(text) = sim.take_stdout() { + if stdout.send(text).is_err() { + sim.watch_stdout(false); + return false; + } + } + true + }); + // Debug state if state == EmulatorState::Debugging { for sim_id in SimId::values() { @@ -645,6 +675,13 @@ impl Emulator { }; sim.remove_watchpoint(address, length, watch); } + EmulatorCommand::WatchStdout(sim_id, stdout_sink) => { + self.stdouts.insert(sim_id, stdout_sink); + let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { + return; + }; + sim.watch_stdout(true); + } 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); @@ -718,6 +755,7 @@ pub enum EmulatorCommand { RemoveBreakpoint(SimId, u32), AddWatchpoint(SimId, u32, usize, VBWatchpointType), RemoveWatchpoint(SimId, u32, usize, VBWatchpointType), + WatchStdout(SimId, mpsc::Sender), SetAudioEnabled(bool, bool), Link, Unlink, diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index cdfcdd5..772bdf8 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -230,7 +230,7 @@ extern "C" fn on_write( sim: *mut VB, address: u32, _type: VBDataType, - _value: *mut i32, + value: *mut i32, _cycles: *mut u32, _cancel: *mut c_int, ) -> c_int { @@ -238,6 +238,14 @@ 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 we're monitoring stdout, track this write + if let Some(stdout) = data.stdout.as_mut() { + let normalized_hw_address = address & 0x0700003f; + if normalized_hw_address == 0x02000030 { + stdout.push(unsafe { *value } as u8); + } + } + if let Some(start) = data.write_watchpoints.start_of_range_containing(address) { let watch = if data.read_watchpoints.contains(address) { VBWatchpointType::Access @@ -263,6 +271,7 @@ struct VBState { breakpoints: Vec, read_watchpoints: AddressSet, write_watchpoints: AddressSet, + stdout: Option>, } impl VBState { @@ -306,6 +315,7 @@ impl Sim { breakpoints: vec![], read_watchpoints: AddressSet::new(), write_watchpoints: AddressSet::new(), + stdout: None, }; unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) }; unsafe { vb_set_frame_callback(sim, Some(on_frame)) }; @@ -564,7 +574,9 @@ impl Sim { 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 state.stdout.is_none() { + unsafe { vb_set_write_callback(self.sim, None) }; + } if !needs_execute { unsafe { vb_set_execute_callback(self.sim, None) }; } @@ -586,11 +598,40 @@ impl Sim { data.breakpoints.clear(); data.read_watchpoints.clear(); data.write_watchpoints.clear(); + let needs_write = data.stdout.is_some(); unsafe { vb_set_read_callback(self.sim, None) }; - unsafe { vb_set_write_callback(self.sim, None) }; + if !needs_write { + unsafe { vb_set_write_callback(self.sim, None) }; + } unsafe { vb_set_execute_callback(self.sim, None) }; } + pub fn watch_stdout(&mut self, watch: bool) { + let data = self.get_state(); + if watch { + if data.stdout.is_none() { + data.stdout = Some(vec![]); + unsafe { vb_set_write_callback(self.sim, Some(on_write)) }; + } + } else { + data.stdout.take(); + if data.write_watchpoints.is_empty() { + unsafe { vb_set_write_callback(self.sim, None) }; + } + } + } + + pub fn take_stdout(&mut self) -> Option { + let data = self.get_state(); + let stdout = data.stdout.take()?; + let string = match String::from_utf8(stdout) { + Ok(str) => str, + Err(err) => String::from_utf8_lossy(err.as_bytes()).into_owned(), + }; + data.stdout = Some(vec![]); + Some(string) + } + pub fn stop_reason(&mut self) -> Option { let data = self.get_state(); let reason = data.stop_reason.take(); diff --git a/src/window.rs b/src/window.rs index 2573d82..502fae4 100644 --- a/src/window.rs +++ b/src/window.rs @@ -4,6 +4,7 @@ pub use game::GameWindow; pub use gdb::GdbServerWindow; pub use hotkeys::HotkeysWindow; pub use input::InputWindow; +pub use terminal::TerminalWindow; pub use vip::{ BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow, }; @@ -17,6 +18,7 @@ mod game_screen; mod gdb; mod hotkeys; mod input; +mod terminal; mod utils; mod vip; diff --git a/src/window/game.rs b/src/window/game.rs index 4ec3c25..f2d4631 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -204,6 +204,12 @@ impl GameWindow { } }); ui.menu_button("Tools", |ui| { + if ui.button("Terminal").clicked() { + self.proxy + .send_event(UserEvent::OpenTerminal(self.sim_id)) + .unwrap(); + ui.close_menu(); + } if ui.button("GDB Server").clicked() { self.proxy .send_event(UserEvent::OpenDebugger(self.sim_id)) diff --git a/src/window/terminal.rs b/src/window/terminal.rs new file mode 100644 index 0000000..fa04500 --- /dev/null +++ b/src/window/terminal.rs @@ -0,0 +1,79 @@ +use std::{collections::VecDeque, sync::mpsc}; + +use egui::{ + Align, CentralPanel, Context, FontFamily, Label, RichText, ScrollArea, ViewportBuilder, + ViewportId, +}; + +use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; + +use super::AppWindow; + +const SCROLLBACK: usize = 1000; + +pub struct TerminalWindow { + sim_id: SimId, + receiver: mpsc::Receiver, + lines: VecDeque, +} + +impl TerminalWindow { + pub fn new(sim_id: SimId, client: &EmulatorClient) -> Self { + let (sender, receiver) = mpsc::channel(); + client.send_command(EmulatorCommand::WatchStdout(sim_id, sender)); + let mut lines = VecDeque::new(); + lines.push_back(String::new()); + Self { + sim_id, + receiver, + lines, + } + } +} + +impl AppWindow for TerminalWindow { + fn viewport_id(&self) -> ViewportId { + ViewportId::from_hash_of(format!("terminal-{}", self.sim_id)) + } + + fn sim_id(&self) -> SimId { + self.sim_id + } + + fn initial_viewport(&self) -> ViewportBuilder { + ViewportBuilder::default() + .with_title(format!("Terminal ({})", self.sim_id)) + .with_inner_size((640.0, 480.0)) + } + + fn show(&mut self, ctx: &Context) { + if let Ok(text) = self.receiver.try_recv() { + let mut rest = text.as_str(); + while let Some(index) = rest.find('\n') { + let (line, lines) = rest.split_at(index); + let current = self.lines.back_mut().unwrap(); + current.push_str(line); + self.lines.push_back(String::new()); + if self.lines.len() > SCROLLBACK { + self.lines.pop_front(); + } + rest = &lines[1..]; + } + self.lines.back_mut().unwrap().push_str(rest); + } + CentralPanel::default().show(ctx, |ui| { + ScrollArea::vertical() + .stick_to_bottom(true) + .auto_shrink([false, false]) + .animated(false) + .show(ui, |ui| { + for line in &self.lines { + let label = Label::new(RichText::new(line).family(FontFamily::Monospace)) + .halign(Align::LEFT) + .wrap(); + ui.add(label); + } + }); + }); + } +}