Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc072cc2ba | ||
|
|
3ac13d0cf2 | ||
|
|
422fe23cf2 | ||
|
|
065f68e9a8 | ||
|
|
b6b0a8c22b | ||
|
|
f1658619ad | ||
|
|
cf1e8a7c9e | ||
|
|
05081a7662 | ||
|
|
68a91c4af1 | ||
|
|
a2a5884a2a | ||
|
|
2d18aeaba2 | ||
|
|
8be866656a | ||
|
|
b58746fee4 |
Generated
+1
-1
@@ -1832,7 +1832,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lemur"
|
name = "lemur"
|
||||||
version = "0.5.0"
|
version = "0.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"atoi",
|
"atoi",
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ description = "An emulator for the Virtual Boy."
|
|||||||
repository = "https://git.virtual-boy.com/PVB/lemur"
|
repository = "https://git.virtual-boy.com/PVB/lemur"
|
||||||
publish = false
|
publish = false
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
version = "0.5.0"
|
version = "0.7.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
+7
-1
@@ -24,7 +24,8 @@ use crate::{
|
|||||||
persistence::Persistence,
|
persistence::Persistence,
|
||||||
window::{
|
window::{
|
||||||
AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow,
|
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);
|
let registers = RegisterWindow::new(sim_id, &self.memory);
|
||||||
self.open(event_loop, Box::new(registers));
|
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) => {
|
UserEvent::OpenDebugger(sim_id) => {
|
||||||
let debugger =
|
let debugger =
|
||||||
GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone());
|
GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone());
|
||||||
@@ -513,6 +518,7 @@ pub enum UserEvent {
|
|||||||
OpenWorlds(SimId),
|
OpenWorlds(SimId),
|
||||||
OpenFrameBuffers(SimId),
|
OpenFrameBuffers(SimId),
|
||||||
OpenRegisters(SimId),
|
OpenRegisters(SimId),
|
||||||
|
OpenTerminal(SimId),
|
||||||
OpenDebugger(SimId),
|
OpenDebugger(SimId),
|
||||||
OpenInput,
|
OpenInput,
|
||||||
OpenHotkeys,
|
OpenHotkeys,
|
||||||
|
|||||||
+51
-5
@@ -43,6 +43,13 @@ impl SimId {
|
|||||||
Self::Player2 => 1,
|
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 {
|
impl Display for SimId {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
@@ -169,8 +176,9 @@ pub struct Emulator {
|
|||||||
renderers: HashMap<SimId, TextureSink>,
|
renderers: HashMap<SimId, TextureSink>,
|
||||||
messages: HashMap<SimId, mpsc::Sender<Toast>>,
|
messages: HashMap<SimId, mpsc::Sender<Toast>>,
|
||||||
debuggers: HashMap<SimId, DebugInfo>,
|
debuggers: HashMap<SimId, DebugInfo>,
|
||||||
|
stdouts: HashMap<SimId, mpsc::Sender<String>>,
|
||||||
watched_regions: HashMap<MemoryRange, Weak<MemoryRegion>>,
|
watched_regions: HashMap<MemoryRange, Weak<MemoryRegion>>,
|
||||||
eye_contents: Vec<u8>,
|
eye_contents: [Vec<u8>; 2],
|
||||||
audio_samples: Vec<f32>,
|
audio_samples: Vec<f32>,
|
||||||
buffer: Vec<u8>,
|
buffer: Vec<u8>,
|
||||||
}
|
}
|
||||||
@@ -195,8 +203,9 @@ impl Emulator {
|
|||||||
renderers: HashMap::new(),
|
renderers: HashMap::new(),
|
||||||
messages: HashMap::new(),
|
messages: HashMap::new(),
|
||||||
debuggers: HashMap::new(),
|
debuggers: HashMap::new(),
|
||||||
|
stdouts: HashMap::new(),
|
||||||
watched_regions: HashMap::new(),
|
watched_regions: HashMap::new(),
|
||||||
eye_contents: vec![0u8; 384 * 224 * 2],
|
eye_contents: [vec![0u8; 384 * 224 * 2], vec![0u8; 384 * 224 * 2]],
|
||||||
audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE),
|
audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE),
|
||||||
buffer: vec![],
|
buffer: vec![],
|
||||||
})
|
})
|
||||||
@@ -228,8 +237,15 @@ impl Emulator {
|
|||||||
|
|
||||||
let index = sim_id.to_index();
|
let index = sim_id.to_index();
|
||||||
while self.sims.len() <= index {
|
while self.sims.len() <= index {
|
||||||
|
let new_index = self.sims.len();
|
||||||
self.sims.push(Sim::new());
|
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];
|
let sim = &mut self.sims[index];
|
||||||
sim.reset();
|
sim.reset();
|
||||||
@@ -471,6 +487,20 @@ impl Emulator {
|
|||||||
self.state.store(EmulatorState::Paused, Ordering::Release);
|
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
|
// Debug state
|
||||||
if state == EmulatorState::Debugging {
|
if state == EmulatorState::Debugging {
|
||||||
for sim_id in SimId::values() {
|
for sim_id in SimId::values() {
|
||||||
@@ -498,9 +528,12 @@ impl Emulator {
|
|||||||
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
|
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if sim.read_pixels(&mut self.eye_contents) {
|
if sim.read_pixels(&mut self.eye_contents[sim_id.to_index()]) {
|
||||||
idle = false;
|
idle = false;
|
||||||
if renderer.queue_render(&self.eye_contents).is_err() {
|
if renderer
|
||||||
|
.queue_render(&self.eye_contents[sim_id.to_index()])
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
self.renderers.remove(&sim_id);
|
self.renderers.remove(&sim_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -645,6 +678,13 @@ impl Emulator {
|
|||||||
};
|
};
|
||||||
sim.remove_watchpoint(address, length, watch);
|
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) => {
|
EmulatorCommand::SetAudioEnabled(p1, p2) => {
|
||||||
self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release);
|
self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release);
|
||||||
self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release);
|
self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release);
|
||||||
@@ -665,6 +705,10 @@ impl Emulator {
|
|||||||
sim.set_keys(keys);
|
sim.set_keys(keys);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
EmulatorCommand::Screenshot(sim_id, sender) => {
|
||||||
|
let contents = self.eye_contents[sim_id.to_index()].clone();
|
||||||
|
let _ = sender.send(contents);
|
||||||
|
}
|
||||||
EmulatorCommand::Exit(done) => {
|
EmulatorCommand::Exit(done) => {
|
||||||
for sim_id in SimId::values() {
|
for sim_id in SimId::values() {
|
||||||
if let Err(error) = self.save_sram(sim_id) {
|
if let Err(error) = self.save_sram(sim_id) {
|
||||||
@@ -718,11 +762,13 @@ pub enum EmulatorCommand {
|
|||||||
RemoveBreakpoint(SimId, u32),
|
RemoveBreakpoint(SimId, u32),
|
||||||
AddWatchpoint(SimId, u32, usize, VBWatchpointType),
|
AddWatchpoint(SimId, u32, usize, VBWatchpointType),
|
||||||
RemoveWatchpoint(SimId, u32, usize, VBWatchpointType),
|
RemoveWatchpoint(SimId, u32, usize, VBWatchpointType),
|
||||||
|
WatchStdout(SimId, mpsc::Sender<String>),
|
||||||
SetAudioEnabled(bool, bool),
|
SetAudioEnabled(bool, bool),
|
||||||
Link,
|
Link,
|
||||||
Unlink,
|
Unlink,
|
||||||
Reset(SimId),
|
Reset(SimId),
|
||||||
SetKeys(SimId, VBKey),
|
SetKeys(SimId, VBKey),
|
||||||
|
Screenshot(SimId, oneshot::Sender<Vec<u8>>),
|
||||||
Exit(oneshot::Sender<()>),
|
Exit(oneshot::Sender<()>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ extern "C" fn on_write(
|
|||||||
sim: *mut VB,
|
sim: *mut VB,
|
||||||
address: u32,
|
address: u32,
|
||||||
_type: VBDataType,
|
_type: VBDataType,
|
||||||
_value: *mut i32,
|
value: *mut i32,
|
||||||
_cycles: *mut u32,
|
_cycles: *mut u32,
|
||||||
_cancel: *mut c_int,
|
_cancel: *mut c_int,
|
||||||
) -> 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.
|
// 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() };
|
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) {
|
if let Some(start) = data.write_watchpoints.start_of_range_containing(address) {
|
||||||
let watch = if data.read_watchpoints.contains(address) {
|
let watch = if data.read_watchpoints.contains(address) {
|
||||||
VBWatchpointType::Access
|
VBWatchpointType::Access
|
||||||
@@ -263,6 +271,7 @@ struct VBState {
|
|||||||
breakpoints: Vec<u32>,
|
breakpoints: Vec<u32>,
|
||||||
read_watchpoints: AddressSet,
|
read_watchpoints: AddressSet,
|
||||||
write_watchpoints: AddressSet,
|
write_watchpoints: AddressSet,
|
||||||
|
stdout: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VBState {
|
impl VBState {
|
||||||
@@ -306,6 +315,7 @@ impl Sim {
|
|||||||
breakpoints: vec![],
|
breakpoints: vec![],
|
||||||
read_watchpoints: AddressSet::new(),
|
read_watchpoints: AddressSet::new(),
|
||||||
write_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_user_data(sim, Box::into_raw(Box::new(state)).cast()) };
|
||||||
unsafe { vb_set_frame_callback(sim, Some(on_frame)) };
|
unsafe { vb_set_frame_callback(sim, Some(on_frame)) };
|
||||||
@@ -564,7 +574,9 @@ impl Sim {
|
|||||||
state.write_watchpoints.remove(address, length);
|
state.write_watchpoints.remove(address, length);
|
||||||
let needs_execute = state.needs_execute_callback();
|
let needs_execute = state.needs_execute_callback();
|
||||||
if state.write_watchpoints.is_empty() {
|
if state.write_watchpoints.is_empty() {
|
||||||
|
if state.stdout.is_none() {
|
||||||
unsafe { vb_set_write_callback(self.sim, None) };
|
unsafe { vb_set_write_callback(self.sim, None) };
|
||||||
|
}
|
||||||
if !needs_execute {
|
if !needs_execute {
|
||||||
unsafe { vb_set_execute_callback(self.sim, None) };
|
unsafe { vb_set_execute_callback(self.sim, None) };
|
||||||
}
|
}
|
||||||
@@ -586,11 +598,40 @@ impl Sim {
|
|||||||
data.breakpoints.clear();
|
data.breakpoints.clear();
|
||||||
data.read_watchpoints.clear();
|
data.read_watchpoints.clear();
|
||||||
data.write_watchpoints.clear();
|
data.write_watchpoints.clear();
|
||||||
|
let needs_write = data.stdout.is_some();
|
||||||
unsafe { vb_set_read_callback(self.sim, None) };
|
unsafe { vb_set_read_callback(self.sim, None) };
|
||||||
|
if !needs_write {
|
||||||
unsafe { vb_set_write_callback(self.sim, None) };
|
unsafe { vb_set_write_callback(self.sim, None) };
|
||||||
|
}
|
||||||
unsafe { vb_set_execute_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> {
|
pub fn stop_reason(&mut self) -> Option<StopReason> {
|
||||||
let data = self.get_state();
|
let data = self.get_state();
|
||||||
let reason = data.stop_reason.take();
|
let reason = data.stop_reason.take();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ impl RegisterInfo {
|
|||||||
pub fn to_description(&self) -> String {
|
pub fn to_description(&self) -> String {
|
||||||
let mut string = format!("name:{}", self.name);
|
let mut string = format!("name:{}", self.name);
|
||||||
if let Some(alt) = self.alt_name {
|
if let Some(alt) = self.alt_name {
|
||||||
string.push_str(&format!(";alt-name:{}", alt));
|
string.push_str(&format!(";alt-name:{alt}"));
|
||||||
}
|
}
|
||||||
string.push_str(&format!(
|
string.push_str(&format!(
|
||||||
";bitsize:32;offset:{};encoding:uint;format:hex;set:{};dwarf:{}",
|
";bitsize:32;offset:{};encoding:uint;format:hex;set:{};dwarf:{}",
|
||||||
@@ -21,7 +21,7 @@ impl RegisterInfo {
|
|||||||
self.dwarf
|
self.dwarf
|
||||||
));
|
));
|
||||||
if let Some(generic) = self.generic {
|
if let Some(generic) = self.generic {
|
||||||
string.push_str(&format!(";generic:{}", generic));
|
string.push_str(&format!(";generic:{generic}"));
|
||||||
}
|
}
|
||||||
string
|
string
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -227,7 +227,7 @@ impl Mappings for InputMapping {
|
|||||||
for (keyboard_key, keys) in &self.keys {
|
for (keyboard_key, keys) in &self.keys {
|
||||||
let name = match keyboard_key {
|
let name = match keyboard_key {
|
||||||
PhysicalKey::Code(code) => format!("{code:?}"),
|
PhysicalKey::Code(code) => format!("{code:?}"),
|
||||||
k => format!("{:?}", k),
|
k => format!("{k:?}"),
|
||||||
};
|
};
|
||||||
for key in keys.iter() {
|
for key in keys.iter() {
|
||||||
results.entry(key).or_default().push(name.clone());
|
results.entry(key).or_default().push(name.clone());
|
||||||
@@ -471,11 +471,12 @@ pub enum Command {
|
|||||||
FastForward(u32),
|
FastForward(u32),
|
||||||
Reset,
|
Reset,
|
||||||
PauseResume,
|
PauseResume,
|
||||||
|
Screenshot,
|
||||||
// if you update this, update Command::all and add a default
|
// if you update this, update Command::all and add a default
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
pub fn all() -> [Self; 6] {
|
pub fn all() -> [Self; 7] {
|
||||||
[
|
[
|
||||||
Self::OpenRom,
|
Self::OpenRom,
|
||||||
Self::Quit,
|
Self::Quit,
|
||||||
@@ -483,6 +484,7 @@ impl Command {
|
|||||||
Self::Reset,
|
Self::Reset,
|
||||||
Self::FrameAdvance,
|
Self::FrameAdvance,
|
||||||
Self::FastForward(0),
|
Self::FastForward(0),
|
||||||
|
Self::Screenshot,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,6 +496,7 @@ impl Command {
|
|||||||
Self::Reset => "Reset",
|
Self::Reset => "Reset",
|
||||||
Self::FrameAdvance => "Frame Advance",
|
Self::FrameAdvance => "Frame Advance",
|
||||||
Self::FastForward(_) => "Fast Forward",
|
Self::FastForward(_) => "Fast Forward",
|
||||||
|
Self::Screenshot => "Screenshot",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -539,6 +542,10 @@ impl Default for Shortcuts {
|
|||||||
Command::FastForward(0),
|
Command::FastForward(0),
|
||||||
KeyboardShortcut::new(Modifiers::NONE, Key::Space),
|
KeyboardShortcut::new(Modifiers::NONE, Key::Space),
|
||||||
);
|
);
|
||||||
|
shortcuts.set(
|
||||||
|
Command::Screenshot,
|
||||||
|
KeyboardShortcut::new(Modifiers::NONE, Key::F12),
|
||||||
|
);
|
||||||
shortcuts
|
shortcuts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -44,9 +44,9 @@ fn set_panic_handler() {
|
|||||||
std::panic::set_hook(Box::new(|info| {
|
std::panic::set_hook(Box::new(|info| {
|
||||||
let mut message = String::new();
|
let mut message = String::new();
|
||||||
if let Some(msg) = info.payload().downcast_ref::<&str>() {
|
if let Some(msg) = info.payload().downcast_ref::<&str>() {
|
||||||
message += &format!("{}\n", msg);
|
message += &format!("{msg}\n");
|
||||||
} else if let Some(msg) = info.payload().downcast_ref::<String>() {
|
} else if let Some(msg) = info.payload().downcast_ref::<String>() {
|
||||||
message += &format!("{}\n", msg);
|
message += &format!("{msg}\n");
|
||||||
}
|
}
|
||||||
if let Some(location) = info.location() {
|
if let Some(location) = info.location() {
|
||||||
message += &format!(
|
message += &format!(
|
||||||
@@ -56,9 +56,9 @@ fn set_panic_handler() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let backtrace = std::backtrace::Backtrace::force_capture();
|
let backtrace = std::backtrace::Backtrace::force_capture();
|
||||||
message += &format!("stack trace:\n{:#}\n", backtrace);
|
message += &format!("stack trace:\n{backtrace:#}\n");
|
||||||
|
|
||||||
eprint!("{}", message);
|
eprint!("{message}");
|
||||||
|
|
||||||
let Some(project_dirs) = directories::ProjectDirs::from("com", "virtual-boy", "Lemur")
|
let Some(project_dirs) = directories::ProjectDirs::from("com", "virtual-boy", "Lemur")
|
||||||
else {
|
else {
|
||||||
@@ -72,7 +72,7 @@ fn set_panic_handler() {
|
|||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_millis();
|
.as_millis();
|
||||||
let logfile_name = format!("crash-{}.txt", timestamp);
|
let logfile_name = format!("crash-{timestamp}.txt");
|
||||||
let _ = std::fs::write(data_dir.join(logfile_name), message);
|
let _ = std::fs::write(data_dir.join(logfile_name), message);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pub use game::GameWindow;
|
|||||||
pub use gdb::GdbServerWindow;
|
pub use gdb::GdbServerWindow;
|
||||||
pub use hotkeys::HotkeysWindow;
|
pub use hotkeys::HotkeysWindow;
|
||||||
pub use input::InputWindow;
|
pub use input::InputWindow;
|
||||||
|
pub use terminal::TerminalWindow;
|
||||||
pub use vip::{
|
pub use vip::{
|
||||||
BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow,
|
BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow,
|
||||||
};
|
};
|
||||||
@@ -17,6 +18,7 @@ mod game_screen;
|
|||||||
mod gdb;
|
mod gdb;
|
||||||
mod hotkeys;
|
mod hotkeys;
|
||||||
mod input;
|
mod input;
|
||||||
|
mod terminal;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod vip;
|
mod vip;
|
||||||
|
|
||||||
|
|||||||
+78
-3
@@ -6,11 +6,12 @@ use crate::{
|
|||||||
input::{Command, ShortcutProvider},
|
input::{Command, ShortcutProvider},
|
||||||
persistence::Persistence,
|
persistence::Persistence,
|
||||||
};
|
};
|
||||||
|
use anyhow::Context as _;
|
||||||
use egui::{
|
use egui::{
|
||||||
Align2, Button, CentralPanel, Color32, Context, Direction, Frame, TopBottomPanel, Ui, Vec2,
|
Align2, Button, CentralPanel, Color32, Context, Direction, Frame, TopBottomPanel, Ui, Vec2,
|
||||||
ViewportBuilder, ViewportCommand, ViewportId, Window, menu,
|
ViewportBuilder, ViewportCommand, ViewportId, Window, menu,
|
||||||
};
|
};
|
||||||
use egui_toast::{Toast, Toasts};
|
use egui_toast::{Toast, ToastKind, ToastOptions, Toasts};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use winit::event_loop::EventLoopProxy;
|
use winit::event_loop::EventLoopProxy;
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ impl GameWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_menu(&mut self, ctx: &Context, ui: &mut Ui) {
|
fn show_menu(&mut self, ctx: &Context, ui: &mut Ui, toasts: &mut Toasts) {
|
||||||
let state = self.client.emulator_state();
|
let state = self.client.emulator_state();
|
||||||
let is_ready = 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;
|
let can_pause = is_ready && state == EmulatorState::Running;
|
||||||
@@ -113,6 +114,16 @@ impl GameWindow {
|
|||||||
self.client
|
self.client
|
||||||
.send_command(EmulatorCommand::SetSpeed(speed as f64));
|
.send_command(EmulatorCommand::SetSpeed(speed as f64));
|
||||||
}
|
}
|
||||||
|
Command::Screenshot => {
|
||||||
|
let autopause = state == EmulatorState::Running && can_pause;
|
||||||
|
if autopause {
|
||||||
|
self.client.send_command(EmulatorCommand::Pause);
|
||||||
|
}
|
||||||
|
pollster::block_on(self.take_screenshot(toasts));
|
||||||
|
if autopause {
|
||||||
|
self.client.send_command(EmulatorCommand::Resume);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +189,17 @@ impl GameWindow {
|
|||||||
self.client.send_command(EmulatorCommand::FrameAdvance);
|
self.client.send_command(EmulatorCommand::FrameAdvance);
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
|
ui.separator();
|
||||||
|
if ui
|
||||||
|
.add_enabled(
|
||||||
|
is_ready,
|
||||||
|
self.button_for(ui.ctx(), "Screenshot", Command::Screenshot),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
pollster::block_on(self.take_screenshot(toasts));
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
ui.menu_button("Options", |ui| self.show_options_menu(ctx, ui));
|
ui.menu_button("Options", |ui| self.show_options_menu(ctx, ui));
|
||||||
ui.menu_button("Multiplayer", |ui| {
|
ui.menu_button("Multiplayer", |ui| {
|
||||||
@@ -204,6 +226,12 @@ impl GameWindow {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.menu_button("Tools", |ui| {
|
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() {
|
if ui.button("GDB Server").clicked() {
|
||||||
self.proxy
|
self.proxy
|
||||||
.send_event(UserEvent::OpenDebugger(self.sim_id))
|
.send_event(UserEvent::OpenDebugger(self.sim_id))
|
||||||
@@ -256,6 +284,53 @@ impl GameWindow {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn take_screenshot(&self, toasts: &mut Toasts) {
|
||||||
|
match self.try_take_screenshot().await {
|
||||||
|
Ok(Some(path)) => {
|
||||||
|
toasts.add(
|
||||||
|
Toast::new()
|
||||||
|
.kind(ToastKind::Info)
|
||||||
|
.options(ToastOptions::default().duration_in_seconds(5.0))
|
||||||
|
.text(format!("Saved to {path}")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(error) => {
|
||||||
|
toasts.add(
|
||||||
|
Toast::new()
|
||||||
|
.kind(ToastKind::Error)
|
||||||
|
.options(ToastOptions::default().duration_in_seconds(5.0))
|
||||||
|
.text(format!("{error:#}")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn try_take_screenshot(&self) -> anyhow::Result<Option<String>> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
self.client
|
||||||
|
.send_command(EmulatorCommand::Screenshot(self.sim_id, tx));
|
||||||
|
let bytes = rx.await.context("Could not take screenshot")?;
|
||||||
|
let file = rfd::FileDialog::new()
|
||||||
|
.add_filter("PNG images", &["png"])
|
||||||
|
.set_file_name("screenshot.png")
|
||||||
|
.save_file();
|
||||||
|
let Some(path) = file else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
if bytes.len() != 384 * 224 * 2 {
|
||||||
|
anyhow::bail!("Unexpected screenshot size");
|
||||||
|
}
|
||||||
|
let mut screencap = image::GrayImage::new(384 * 2, 224);
|
||||||
|
for (index, pixel) in bytes.into_iter().enumerate() {
|
||||||
|
let x = (index / 2) % 384 + ((index % 2) * 384);
|
||||||
|
let y = (index / 2) / 384;
|
||||||
|
screencap.put_pixel(x as u32, y as u32, image::Luma([pixel]));
|
||||||
|
}
|
||||||
|
screencap.save(&path).context("Could not save screenshot")?;
|
||||||
|
Ok(Some(path.display().to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
fn show_options_menu(&mut self, ctx: &Context, ui: &mut Ui) {
|
fn show_options_menu(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||||
ui.menu_button("Video", |ui| {
|
ui.menu_button("Video", |ui| {
|
||||||
ui.menu_button("Screen Size", |ui| {
|
ui.menu_button("Screen Size", |ui| {
|
||||||
@@ -472,7 +547,7 @@ impl AppWindow for GameWindow {
|
|||||||
.exact_height(22.0)
|
.exact_height(22.0)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
menu::bar(ui, |ui| {
|
menu::bar(ui, |ui| {
|
||||||
self.show_menu(ctx, ui);
|
self.show_menu(ctx, ui, &mut toasts);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if self.color_picker.is_some() {
|
if self.color_picker.is_some() {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ impl GameScreen {
|
|||||||
module: &shader,
|
module: &shader,
|
||||||
entry_point: Some(entry_point),
|
entry_point: Some(entry_point),
|
||||||
targets: &[Some(wgpu::ColorTargetState {
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
format: wgpu::TextureFormat::Bgra8Unorm,
|
format: render_state.target_format,
|
||||||
blend: Some(wgpu::BlendState::REPLACE),
|
blend: Some(wgpu::BlendState::REPLACE),
|
||||||
write_mask: wgpu::ColorWrites::ALL,
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
})],
|
})],
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user