Compare commits

...
4 Commits
Author SHA1 Message Date
SonicSwordcane a2a5884a2a Implement fake stdout 2025-05-30 23:52:16 -04:00
SonicSwordcane 2d18aeaba2 Add UI for a terminal 2025-05-30 22:25:08 -04:00
SonicSwordcane 8be866656a chore: Release lemur version 0.5.1 2025-03-29 10:24:43 -04:00
SonicSwordcane b58746fee4 Use target format from render state 2025-03-29 10:23:03 -04:00
9 changed files with 147 additions and 7 deletions
Generated
+1 -1
View File
@@ -1832,7 +1832,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lemur"
version = "0.5.0"
version = "0.5.1"
dependencies = [
"anyhow",
"atoi",
+1 -1
View File
@@ -4,7 +4,7 @@ description = "An emulator for the Virtual Boy."
repository = "https://git.virtual-boy.com/PVB/lemur"
publish = false
license = "MIT"
version = "0.5.0"
version = "0.5.1"
edition = "2024"
[dependencies]
+7 -1
View File
@@ -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,
+24
View File
@@ -169,6 +169,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 +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,6 +661,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 +741,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,
+44 -3
View File
@@ -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();
+2
View File
@@ -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;
+6
View File
@@ -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))
+1 -1
View File
@@ -71,7 +71,7 @@ impl GameScreen {
module: &shader,
entry_point: Some(entry_point),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Bgra8Unorm,
format: render_state.target_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
+61
View File
@@ -0,0 +1,61 @@
use std::sync::mpsc;
use egui::{
Align, 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<String>,
text: 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));
Self {
sim_id,
receiver,
text: String::new(),
}
}
}
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()
.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);
});
});
}
}