Compare commits

...
14 Commits
12 changed files with 150 additions and 38 deletions
Generated
+1 -1
View File
@@ -1763,7 +1763,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "lemur" name = "lemur"
version = "0.2.7" version = "0.3.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"atoi", "atoi",
+1 -1
View File
@@ -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.2.7" version = "0.3.1"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
+1 -1
View File
@@ -5,7 +5,7 @@ A Virtual Boy emulator built around the shrooms-vb core. Written in Rust, using
## Setup ## Setup
Install the following dependencies: Install the following dependencies:
- `cargo` - `cargo` (via [rustup](https://rustup.rs/), the version from your package manager is too old)
Run Run
```sh ```sh
+4 -2
View File
@@ -23,13 +23,15 @@ ENV PATH="/osxcross/bin:$PATH" \
CXX_x86_64-apple-darwin="o64-clang++" \ CXX_x86_64-apple-darwin="o64-clang++" \
CC_aarch64-apple-darwin="oa64-clang" \ CC_aarch64-apple-darwin="oa64-clang" \
CXX_aarch64-apple-darwin="o6a4-clang++" \ CXX_aarch64-apple-darwin="o6a4-clang++" \
SHROOMS_CFLAGS_x86_64-unknown-linux-gnu="-flto" \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" \
SHROOMS_CFLAGS_x86_64-pc-windows-msvc="-flto" \
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS="-Lnative=/xwin/crt/lib/x86_64 -Lnative=/xwin/sdk/lib/um/x86_64 -Lnative=/xwin/sdk/lib/ucrt/x86_64 -Clinker-plugin-lto -Clink-arg=-fuse-ld=lld" \
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link-19" \ CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link-19" \
CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER="o64-clang" \ CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER="o64-clang" \
CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-19" \ CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-19" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="oa64-clang" \ CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="oa64-clang" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-19" \ CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-19" \
CROSS_COMPILE="setting-this-to-silence-a-warning-" \ CROSS_COMPILE="setting-this-to-silence-a-warning-" \
SHROOMS_CFLAGS="-flto" \
RC_PATH="llvm-rc-19" \ RC_PATH="llvm-rc-19" \
RUSTFLAGS="-Lnative=/xwin/crt/lib/x86_64 -Lnative=/xwin/sdk/lib/um/x86_64 -Lnative=/xwin/sdk/lib/ucrt/x86_64" \
MACOSX_DEPLOYMENT_TARGET="14.5" MACOSX_DEPLOYMENT_TARGET="14.5"
+4
View File
@@ -23,6 +23,10 @@ fn main() -> Result<(), Box<dyn Error>> {
.define("VB_LITTLE_ENDIAN", None) .define("VB_LITTLE_ENDIAN", None)
.define("VB_SIGNED_PROPAGATE", None) .define("VB_SIGNED_PROPAGATE", None)
.define("VB_DIV_GENERIC", None) .define("VB_DIV_GENERIC", None)
.define("VB_DIRECT_EXECUTE", "on_execute")
.define("VB_DIRECT_FRAME", "on_frame")
.define("VB_DIRECT_READ", "on_read")
.define("VB_DIRECT_WRITE", "on_write")
.file(Path::new("shrooms-vb-core/core/vb.c")) .file(Path::new("shrooms-vb-core/core/vb.c"))
.compile("vb"); .compile("vb");
+11 -3
View File
@@ -311,7 +311,7 @@ impl Emulator {
} }
let debug = DebugInfo { let debug = DebugInfo {
sender, sender,
stop_reason: Some(DebugStopReason::Trapped), stop_reason: Some(DebugStopReason::Paused),
}; };
self.debuggers.insert(sim_id, debug); self.debuggers.insert(sim_id, debug);
self.state self.state
@@ -334,7 +334,7 @@ impl Emulator {
} }
fn debug_interrupt(&mut self, sim_id: SimId) { fn debug_interrupt(&mut self, sim_id: SimId) {
self.debug_stop(sim_id, DebugStopReason::Trapped); self.debug_stop(sim_id, DebugStopReason::Paused);
} }
fn debug_stop(&mut self, sim_id: SimId, reason: DebugStopReason) { fn debug_stop(&mut self, sim_id: SimId, reason: DebugStopReason) {
@@ -550,6 +550,13 @@ impl Emulator {
sim.read_memory(start, length, &mut buffer); sim.read_memory(start, length, &mut buffer);
let _ = done.send(buffer); let _ = done.send(buffer);
} }
EmulatorCommand::WriteMemory(sim_id, start, buffer, done) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.write_memory(start, &buffer);
let _ = done.send(buffer);
}
EmulatorCommand::AddBreakpoint(sim_id, address) => { EmulatorCommand::AddBreakpoint(sim_id, address) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return; return;
@@ -639,6 +646,7 @@ pub enum EmulatorCommand {
ReadRegister(SimId, VBRegister, oneshot::Sender<u32>), ReadRegister(SimId, VBRegister, oneshot::Sender<u32>),
WriteRegister(SimId, VBRegister, u32), WriteRegister(SimId, VBRegister, u32),
ReadMemory(SimId, u32, usize, Vec<u8>, oneshot::Sender<Vec<u8>>), ReadMemory(SimId, u32, usize, Vec<u8>, oneshot::Sender<Vec<u8>>),
WriteMemory(SimId, u32, Vec<u8>, oneshot::Sender<Vec<u8>>),
AddBreakpoint(SimId, u32), AddBreakpoint(SimId, u32),
RemoveBreakpoint(SimId, u32), RemoveBreakpoint(SimId, u32),
AddWatchpoint(SimId, u32, usize, VBWatchpointType), AddWatchpoint(SimId, u32, usize, VBWatchpointType),
@@ -678,7 +686,7 @@ pub enum DebugStopReason {
// We hit a watchpoint // We hit a watchpoint
Watchpoint(VBWatchpointType, u32), Watchpoint(VBWatchpointType, u32),
// The debugger told us to pause // The debugger told us to pause
Trapped, Paused,
} }
struct DebugInfo { struct DebugInfo {
+41 -2
View File
@@ -1,11 +1,11 @@
use std::{ use std::{
collections::{BTreeMap, HashSet}, collections::{BTreeMap, BTreeSet},
ops::Bound, ops::Bound,
}; };
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct AddressSet { pub struct AddressSet {
ranges: HashSet<(u32, usize)>, ranges: BTreeSet<(u32, usize)>,
bounds: BTreeMap<u32, usize>, bounds: BTreeMap<u32, usize>,
} }
@@ -109,6 +109,22 @@ impl AddressSet {
.next_back() .next_back()
.is_some_and(|(_, &val)| val > 0) .is_some_and(|(_, &val)| val > 0)
} }
pub fn start_of_range_containing(&self, address: u32) -> Option<u32> {
if !self.contains(address) {
return None;
}
self.ranges
.range(..=(address, usize::MAX))
.rev()
.find_map(|&(start, length)| {
let contains = start <= address
&& (start as usize)
.checked_add(length)
.is_none_or(|end| end > address as usize);
contains.then_some(start)
})
}
} }
#[cfg(test)] #[cfg(test)]
@@ -120,6 +136,7 @@ mod tests {
let set = AddressSet::new(); let set = AddressSet::new();
assert!(set.is_empty()); assert!(set.is_empty());
assert!(!set.contains(0x13374200)); assert!(!set.contains(0x13374200));
assert_eq!(set.start_of_range_containing(0x13374200), None);
} }
#[test] #[test]
@@ -127,6 +144,7 @@ mod tests {
let mut set = AddressSet::new(); let mut set = AddressSet::new();
set.add(0x00000000, 0x100000000); set.add(0x00000000, 0x100000000);
assert!(set.contains(0x13374200)); assert!(set.contains(0x13374200));
assert_eq!(set.start_of_range_containing(0x13374200), Some(0x00000000));
} }
#[test] #[test]
@@ -228,4 +246,25 @@ mod tests {
assert!(!set.contains(address)); assert!(!set.contains(address));
} }
} }
#[test]
fn should_find_start_of_range() {
let mut set = AddressSet::new();
set.add(0x13374200, 4);
assert_eq!(set.start_of_range_containing(0x133741ff), None);
for address in 0x13374200..0x13374204 {
assert_eq!(set.start_of_range_containing(address), Some(0x13374200));
}
assert_eq!(set.start_of_range_containing(0x13374204), None);
}
#[test]
fn should_ignore_ranges_not_containing_address() {
let mut set = AddressSet::new();
set.add(0x10000000, 1024);
set.add(0x30000000, 1024);
assert!(!set.contains(0x20000000));
assert_eq!(set.start_of_range_containing(0x20000000), None);
}
} }
+18 -4
View File
@@ -166,8 +166,11 @@ extern "C" {
fn vb_set_write_callback(sim: *mut VB, callback: Option<OnWrite>) -> Option<OnWrite>; fn vb_set_write_callback(sim: *mut VB, callback: Option<OnWrite>) -> Option<OnWrite>;
#[link_name = "vbSizeOf"] #[link_name = "vbSizeOf"]
fn vb_size_of() -> usize; fn vb_size_of() -> usize;
#[link_name = "vbWrite"]
fn vb_write(sim: *mut VB, address: u32, _type: VBDataType, value: i32) -> i32;
} }
#[no_mangle]
extern "C" fn on_frame(sim: *mut VB) -> c_int { extern "C" fn on_frame(sim: *mut VB) -> c_int {
// SAFETY: the *mut VB owns its userdata. // SAFETY: the *mut VB owns its userdata.
// 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.
@@ -176,6 +179,7 @@ extern "C" fn on_frame(sim: *mut VB) -> c_int {
1 1
} }
#[no_mangle]
extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: c_int) -> c_int { extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: c_int) -> c_int {
// SAFETY: the *mut VB owns its userdata. // SAFETY: the *mut VB owns its userdata.
// 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.
@@ -199,6 +203,7 @@ extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length:
} }
} }
#[no_mangle]
extern "C" fn on_read( extern "C" fn on_read(
sim: *mut VB, sim: *mut VB,
address: u32, address: u32,
@@ -210,13 +215,13 @@ extern "C" fn on_read(
// 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 data.read_watchpoints.contains(address) { if let Some(start) = data.read_watchpoints.start_of_range_containing(address) {
let watch = if data.write_watchpoints.contains(address) { let watch = if data.write_watchpoints.contains(address) {
VBWatchpointType::Access VBWatchpointType::Access
} else { } else {
VBWatchpointType::Read VBWatchpointType::Read
}; };
data.stop_reason = Some(StopReason::Watchpoint(watch, address)); data.stop_reason = Some(StopReason::Watchpoint(watch, start));
} }
// Don't stop here, the debugger expects us to break after the memory access. // Don't stop here, the debugger expects us to break after the memory access.
@@ -224,6 +229,7 @@ extern "C" fn on_read(
0 0
} }
#[no_mangle]
extern "C" fn on_write( extern "C" fn on_write(
sim: *mut VB, sim: *mut VB,
address: u32, address: u32,
@@ -236,13 +242,13 @@ 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 data.write_watchpoints.contains(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
} else { } else {
VBWatchpointType::Write VBWatchpointType::Write
}; };
data.stop_reason = Some(StopReason::Watchpoint(watch, address)); data.stop_reason = Some(StopReason::Watchpoint(watch, start));
} }
// Don't stop here, the debugger expects us to break after the memory access. // Don't stop here, the debugger expects us to break after the memory access.
@@ -476,6 +482,14 @@ impl Sim {
} }
} }
pub fn write_memory(&mut self, start: u32, buffer: &[u8]) {
let mut address = start;
for byte in buffer {
unsafe { vb_write(self.sim, address, VBDataType::U8, *byte as i32) };
address = address.wrapping_add(1);
}
}
pub fn add_breakpoint(&mut self, address: u32) { pub fn add_breakpoint(&mut self, address: u32) {
let data = self.get_state(); let data = self.get_state();
if let Err(index) = data.breakpoints.binary_search(&address) { if let Err(index) = data.breakpoints.binary_search(&address) {
+56 -16
View File
@@ -346,22 +346,9 @@ impl GdbConnection {
} }
} else if let Some(op) = req.match_some_str(["m", "x"]) { } else if let Some(op) = req.match_some_str(["m", "x"]) {
let mut read_memory = || { let mut read_memory = || {
let start = req.match_hex::<u64>()?; let (start, length) = parse_memory_range(&mut req)?;
if !req.match_str(",") {
return None;
};
let length = req.match_hex::<usize>()?;
let mut buf = self.memory_buf.take().unwrap_or_default(); let mut buf = self.memory_buf.take().unwrap_or_default();
buf.clear(); buf.clear();
// The v810 has a 32-bit address space.
// Addresses wrap within that space, but we don't need to implement that for 64-bit addresses.
// Just refuse to return any info for addresses above 0xffffffff.
let Ok(start) = u32::try_from(start) else {
return Some(buf);
};
let length = length.min((u32::MAX - start) as usize + 1);
if length == 0 { if length == 0 {
return Some(buf); return Some(buf);
} }
@@ -395,6 +382,38 @@ impl GdbConnection {
} else { } else {
self.response() self.response()
} }
} else if let Some(op) = req.match_some_str(["M", "X"]) {
let mut write_memory = || {
let (start, length) = parse_memory_range(&mut req)?;
if length == 0 {
return Some(());
}
if !req.match_str(":") {
return None;
}
let mut buf = self.memory_buf.take().unwrap_or_default();
buf.resize(length, 0);
let successful_read = if op == "M" {
req.match_hex_bytes(&mut buf)
} else {
req.match_bytes(&mut buf)
};
if !successful_read {
self.memory_buf = Some(buf);
return None;
}
let (tx, rx) = ::oneshot::channel();
self.client
.send_command(EmulatorCommand::WriteMemory(self.sim_id, start, buf, tx));
let buf = rx.recv().ok()?;
self.memory_buf = Some(buf);
Some(())
};
if write_memory().is_some() {
self.response().write_str("OK")
} else {
self.response()
}
} else if req.match_str("Z") { } else if req.match_str("Z") {
let mut parse_request = || { let mut parse_request = || {
let type_ = req.match_hex::<u8>()?; let type_ = req.match_hex::<u8>()?;
@@ -483,9 +502,30 @@ impl Drop for GdbConnection {
} }
} }
// parse a memory range into a start and a length.
fn parse_memory_range(req: &mut Request<'_>) -> Option<(u32, usize)> {
let start = req.match_hex::<u64>()?;
if !req.match_str(",") {
return None;
};
let length = req.match_hex::<usize>()?;
let Ok(start) = u32::try_from(start) else {
// The v810 has a 32-bit address space.
// Addresses wrap within that space, but we don't need to implement that for 64-bit addresses.
// Just refuse to return any info for addresses above 0xffffffff.
return Some((0, 0));
};
let length = length.min((u32::MAX - start) as usize + 1);
Some((start, length))
}
fn debug_stop_reason_string(reason: Option<DebugStopReason>) -> String { fn debug_stop_reason_string(reason: Option<DebugStopReason>) -> String {
let mut result = String::new(); let mut result = String::new();
result += if reason.is_some() { "T05;" } else { "T00;" }; result += if reason.is_some_and(|r| r != DebugStopReason::Paused) {
"T05;"
} else {
"T00;"
};
if let Some(DebugStopReason::Breakpoint) = reason { if let Some(DebugStopReason::Breakpoint) = reason {
result += "swbreak;"; result += "swbreak;";
} }
@@ -506,7 +546,7 @@ fn debug_stop_reason_string(reason: Option<DebugStopReason>) -> String {
DebugStopReason::Trace => "trace;", DebugStopReason::Trace => "trace;",
DebugStopReason::Breakpoint => "breakpoint;", DebugStopReason::Breakpoint => "breakpoint;",
DebugStopReason::Watchpoint(_, _) => "watchpoint;", DebugStopReason::Watchpoint(_, _) => "watchpoint;",
DebugStopReason::Trapped => "trap;", DebugStopReason::Paused => "trap;",
}; };
} }
+9
View File
@@ -74,6 +74,15 @@ impl Request<'_> {
self.buffer = self.buffer.split_at(buffer.len()).1; self.buffer = self.buffer.split_at(buffer.len()).1;
true true
} }
pub fn match_bytes(&mut self, buffer: &mut [u8]) -> bool {
if self.buffer.len() < buffer.len() {
return false;
}
buffer.copy_from_slice(&self.buffer[0..buffer.len()]);
self.buffer = self.buffer.split_at(buffer.len()).1;
true
}
} }
pub struct RequestSource<R> { pub struct RequestSource<R> {
+3 -7
View File
@@ -142,8 +142,7 @@ impl GameWindow {
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| {
let current_dims = ctx.input(|i| i.viewport().inner_rect.unwrap()); let current_dims = self.config.dimensions;
let current_dims = current_dims.max - current_dims.min;
for scale in 1..=4 { for scale in 1..=4 {
let label = format!("x{scale}"); let label = format!("x{scale}");
@@ -183,10 +182,7 @@ impl GameWindow {
return; return;
} }
let current_dims = { let current_dims = self.config.dimensions;
let viewport = ctx.input(|i| i.viewport().inner_rect.unwrap());
viewport.max - viewport.min
};
let new_proportions = display_mode.proportions(); let new_proportions = display_mode.proportions();
let scale = new_proportions / old_proportions; let scale = new_proportions / old_proportions;
if scale != Vec2::new(1.0, 1.0) { if scale != Vec2::new(1.0, 1.0) {
@@ -330,7 +326,7 @@ impl AppWindow for GameWindow {
fn show(&mut self, ctx: &Context) { fn show(&mut self, ctx: &Context) {
let dimensions = { let dimensions = {
let bounds = ctx.input(|i| i.viewport().inner_rect.unwrap()); let bounds = ctx.available_rect();
bounds.max - bounds.min bounds.max - bounds.min
}; };
self.update_config(|c| c.dimensions = dimensions); self.update_config(|c| c.dimensions = dimensions);