From 2d18aeaba2f6ef1282b57cbe7eb006ed4462381f Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Fri, 30 May 2025 22:25:08 -0400 Subject: [PATCH 1/4] Add UI for a terminal --- src/app.rs | 8 +++++- src/emulator.rs | 5 ++++ src/window.rs | 2 ++ src/window/game.rs | 6 +++++ src/window/terminal.rs | 59 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/window/terminal.rs 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..f2e330e 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -645,6 +645,10 @@ impl Emulator { }; sim.remove_watchpoint(address, length, watch); } + EmulatorCommand::ConnectTerminal(sim_id, terminal_sink) => { + // TODO + let _ = (sim_id, terminal_sink); + } 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 +722,7 @@ pub enum EmulatorCommand { RemoveBreakpoint(SimId, u32), AddWatchpoint(SimId, u32, usize, VBWatchpointType), RemoveWatchpoint(SimId, u32, usize, VBWatchpointType), + ConnectTerminal(SimId, mpsc::Sender), SetAudioEnabled(bool, bool), Link, Unlink, 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..957e042 --- /dev/null +++ b/src/window/terminal.rs @@ -0,0 +1,59 @@ +use std::sync::mpsc; + +use egui::{ + CentralPanel, Context, FontFamily, Label, RichText, ScrollArea, ViewportBuilder, ViewportId, +}; + +use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; + +use super::AppWindow; + +pub struct TerminalWindow { + sim_id: SimId, + receiver: mpsc::Receiver, + text: String, +} + +const TEXT: &str = " +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam various, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst."; + +impl TerminalWindow { + pub fn new(sim_id: SimId, client: &EmulatorClient) -> Self { + let (sender, receiver) = mpsc::channel(); + client.send_command(EmulatorCommand::ConnectTerminal(sim_id, sender)); + Self { + sim_id, + receiver, + text: TEXT.to_string(), + } + } +} + +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() { + self.text.push_str(&text); + } + CentralPanel::default().show(ctx, |ui| { + ScrollArea::vertical().show(ui, |ui| { + ui.add(Label::new(RichText::new(&self.text).family(FontFamily::Monospace)).wrap()); + }); + }); + } +} -- 2.40.1 From a2a5884a2aa40e431e215b66271f124528af3cfe Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Fri, 30 May 2025 23:52:16 -0400 Subject: [PATCH 2/4] Implement fake stdout --- src/emulator.rs | 27 ++++++++++++++++--- src/emulator/shrooms_vb_core.rs | 47 ++++++++++++++++++++++++++++++--- src/window/terminal.rs | 24 +++++++++-------- 3 files changed, 80 insertions(+), 18 deletions(-) diff --git a/src/emulator.rs b/src/emulator.rs index f2e330e..4c9dfe9 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -169,6 +169,7 @@ pub struct Emulator { renderers: HashMap, messages: HashMap>, debuggers: HashMap, + stdouts: HashMap>, watched_regions: HashMap>, eye_contents: Vec, audio_samples: Vec, @@ -195,6 +196,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), @@ -471,6 +473,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 false; + }; + 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,9 +661,12 @@ impl Emulator { }; sim.remove_watchpoint(address, length, watch); } - EmulatorCommand::ConnectTerminal(sim_id, terminal_sink) => { - // TODO - let _ = (sim_id, terminal_sink); + 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); @@ -722,7 +741,7 @@ pub enum EmulatorCommand { RemoveBreakpoint(SimId, u32), AddWatchpoint(SimId, u32, usize, VBWatchpointType), RemoveWatchpoint(SimId, u32, usize, VBWatchpointType), - ConnectTerminal(SimId, mpsc::Sender), + 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/terminal.rs b/src/window/terminal.rs index 957e042..36f7323 100644 --- a/src/window/terminal.rs +++ b/src/window/terminal.rs @@ -1,7 +1,8 @@ use std::sync::mpsc; use egui::{ - CentralPanel, Context, FontFamily, Label, RichText, ScrollArea, ViewportBuilder, ViewportId, + Align, CentralPanel, Context, FontFamily, Label, RichText, ScrollArea, ViewportBuilder, + ViewportId, }; use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; @@ -14,19 +15,14 @@ pub struct TerminalWindow { text: String, } -const TEXT: &str = " -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - -Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam various, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst."; - impl TerminalWindow { pub fn new(sim_id: SimId, client: &EmulatorClient) -> Self { let (sender, receiver) = mpsc::channel(); - client.send_command(EmulatorCommand::ConnectTerminal(sim_id, sender)); + client.send_command(EmulatorCommand::WatchStdout(sim_id, sender)); Self { sim_id, receiver, - text: TEXT.to_string(), + text: String::new(), } } } @@ -51,9 +47,15 @@ impl AppWindow for TerminalWindow { self.text.push_str(&text); } CentralPanel::default().show(ctx, |ui| { - ScrollArea::vertical().show(ui, |ui| { - ui.add(Label::new(RichText::new(&self.text).family(FontFamily::Monospace)).wrap()); - }); + ScrollArea::vertical() + .stick_to_bottom(true) + .auto_shrink([false, false]) + .show(ui, |ui| { + let label = Label::new(RichText::new(&self.text).family(FontFamily::Monospace)) + .halign(Align::LEFT) + .wrap(); + ui.add(label); + }); }); } } -- 2.40.1 From 68a91c4af1cc71f57b31548f330283b2810ce70c Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 31 May 2025 00:09:33 -0400 Subject: [PATCH 3/4] Allow terminal to be open even if game is not --- src/emulator.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/emulator.rs b/src/emulator.rs index 4c9dfe9..5d4cf13 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 { @@ -230,8 +237,12 @@ 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(); @@ -476,7 +487,7 @@ impl Emulator { // stdout self.stdouts.retain(|sim_id, stdout| { let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { - return false; + return true; }; if let Some(text) = sim.take_stdout() { if stdout.send(text).is_err() { -- 2.40.1 From 05081a76628fc6d418168727eda0679841c2ea80 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 31 May 2025 00:37:07 -0400 Subject: [PATCH 4/4] Only show 1000 lines of history in terminal --- src/emulator.rs | 7 +++++-- src/window/terminal.rs | 34 ++++++++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/emulator.rs b/src/emulator.rs index 5d4cf13..b4d0ba8 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -47,7 +47,7 @@ impl SimId { match index { 0 => Some(Self::Player1), 1 => Some(Self::Player2), - _ => None + _ => None, } } } @@ -239,7 +239,10 @@ impl Emulator { while self.sims.len() <= index { let new_index = self.sims.len(); self.sims.push(Sim::new()); - if self.stdouts.contains_key(&SimId::from_index(new_index).unwrap()) { + 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); diff --git a/src/window/terminal.rs b/src/window/terminal.rs index 36f7323..fa04500 100644 --- a/src/window/terminal.rs +++ b/src/window/terminal.rs @@ -1,4 +1,4 @@ -use std::sync::mpsc; +use std::{collections::VecDeque, sync::mpsc}; use egui::{ Align, CentralPanel, Context, FontFamily, Label, RichText, ScrollArea, ViewportBuilder, @@ -9,20 +9,24 @@ use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; use super::AppWindow; +const SCROLLBACK: usize = 1000; + pub struct TerminalWindow { sim_id: SimId, receiver: mpsc::Receiver, - text: String, + 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, - text: String::new(), + lines, } } } @@ -44,17 +48,31 @@ impl AppWindow for TerminalWindow { fn show(&mut self, ctx: &Context) { if let Ok(text) = self.receiver.try_recv() { - self.text.push_str(&text); + 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| { - let label = Label::new(RichText::new(&self.text).family(FontFamily::Monospace)) - .halign(Align::LEFT) - .wrap(); - ui.add(label); + for line in &self.lines { + let label = Label::new(RichText::new(line).family(FontFamily::Monospace)) + .halign(Align::LEFT) + .wrap(); + ui.add(label); + } }); }); } -- 2.40.1