Merge pull request 'Terminal' (#5) from terminal into main
Reviewed-on: #5
This commit is contained in:
commit
cf1e8a7c9e
|
@ -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<UserEvent> 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,
|
||||
|
|
|
@ -43,6 +43,13 @@ impl SimId {
|
|||
Self::Player2 => 1,
|
||||
}
|
||||
}
|
||||
pub const fn from_index(index: usize) -> Option<Self> {
|
||||
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<SimId, TextureSink>,
|
||||
messages: HashMap<SimId, mpsc::Sender<Toast>>,
|
||||
debuggers: HashMap<SimId, DebugInfo>,
|
||||
stdouts: HashMap<SimId, mpsc::Sender<String>>,
|
||||
watched_regions: HashMap<MemoryRange, Weak<MemoryRegion>>,
|
||||
eye_contents: Vec<u8>,
|
||||
audio_samples: Vec<f32>,
|
||||
|
@ -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<String>),
|
||||
SetAudioEnabled(bool, bool),
|
||||
Link,
|
||||
Unlink,
|
||||
|
|
|
@ -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<u32>,
|
||||
read_watchpoints: AddressSet,
|
||||
write_watchpoints: AddressSet,
|
||||
stdout: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
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<String> {
|
||||
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<StopReason> {
|
||||
let data = self.get_state();
|
||||
let reason = data.stop_reason.take();
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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<String>,
|
||||
lines: VecDeque<String>,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue