Compare commits

..
8 Commits
10 changed files with 265 additions and 42 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ A Virtual Boy emulator built around the shrooms-vb core. Written in Rust, using
## Setup
Install the following dependencies:
- `cargo`
- `cargo` (via [rustup](https://rustup.rs/), the version from your package manager is too old)
Run
```sh
+4 -2
View File
@@ -23,13 +23,15 @@ ENV PATH="/osxcross/bin:$PATH" \
CXX_x86_64-apple-darwin="o64-clang++" \
CC_aarch64-apple-darwin="oa64-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_APPLE_DARWIN_LINKER="o64-clang" \
CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-19" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="oa64-clang" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-19" \
CROSS_COMPILE="setting-this-to-silence-a-warning-" \
SHROOMS_CFLAGS="-flto" \
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"
+4
View File
@@ -23,6 +23,10 @@ fn main() -> Result<(), Box<dyn Error>> {
.define("VB_LITTLE_ENDIAN", None)
.define("VB_SIGNED_PROPAGATE", 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"))
.compile("vb");
+22 -3
View File
@@ -305,9 +305,13 @@ impl Emulator {
}
fn start_debugging(&mut self, sim_id: SimId, sender: DebugSender) {
if self.sim_state[sim_id.to_index()].load(Ordering::Acquire) != SimState::Ready {
// Can't debug unless a game is connected
return;
}
let debug = DebugInfo {
sender,
stop_reason: Some(DebugStopReason::Trapped),
stop_reason: Some(DebugStopReason::Paused),
};
self.debuggers.insert(sim_id, debug);
self.state
@@ -330,7 +334,7 @@ impl Emulator {
}
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) {
@@ -533,6 +537,12 @@ impl Emulator {
let value = sim.read_register(register);
let _ = done.send(value);
}
EmulatorCommand::WriteRegister(sim_id, register, value) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.write_register(register, value);
}
EmulatorCommand::ReadMemory(sim_id, start, length, mut buffer, done) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
@@ -540,6 +550,13 @@ impl Emulator {
sim.read_memory(start, length, &mut 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) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
@@ -627,7 +644,9 @@ pub enum EmulatorCommand {
DebugContinue(SimId),
DebugStep(SimId),
ReadRegister(SimId, VBRegister, oneshot::Sender<u32>),
WriteRegister(SimId, VBRegister, u32),
ReadMemory(SimId, u32, usize, Vec<u8>, oneshot::Sender<Vec<u8>>),
WriteMemory(SimId, u32, Vec<u8>, oneshot::Sender<Vec<u8>>),
AddBreakpoint(SimId, u32),
RemoveBreakpoint(SimId, u32),
AddWatchpoint(SimId, u32, usize, VBWatchpointType),
@@ -667,7 +686,7 @@ pub enum DebugStopReason {
// We hit a watchpoint
Watchpoint(VBWatchpointType, u32),
// The debugger told us to pause
Trapped,
Paused,
}
struct DebugInfo {
+41 -2
View File
@@ -1,11 +1,11 @@
use std::{
collections::{BTreeMap, HashSet},
collections::{BTreeMap, BTreeSet},
ops::Bound,
};
#[derive(Debug, Default)]
pub struct AddressSet {
ranges: HashSet<(u32, usize)>,
ranges: BTreeSet<(u32, usize)>,
bounds: BTreeMap<u32, usize>,
}
@@ -109,6 +109,22 @@ impl AddressSet {
.next_back()
.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)]
@@ -120,6 +136,7 @@ mod tests {
let set = AddressSet::new();
assert!(set.is_empty());
assert!(!set.contains(0x13374200));
assert_eq!(set.start_of_range_containing(0x13374200), None);
}
#[test]
@@ -127,6 +144,7 @@ mod tests {
let mut set = AddressSet::new();
set.add(0x00000000, 0x100000000);
assert!(set.contains(0x13374200));
assert_eq!(set.start_of_range_containing(0x13374200), Some(0x00000000));
}
#[test]
@@ -228,4 +246,25 @@ mod tests {
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);
}
}
+38 -4
View File
@@ -145,6 +145,10 @@ extern "C" {
fn vb_set_option(sim: *mut VB, key: VBOption, value: c_int);
#[link_name = "vbSetPeer"]
fn vb_set_peer(sim: *mut VB, peer: *mut VB);
#[link_name = "vbSetProgramCounter"]
fn vb_set_program_counter(sim: *mut VB, value: u32) -> u32;
#[link_name = "vbSetProgramRegister"]
fn vb_set_program_register(sim: *mut VB, index: c_uint, value: i32) -> i32;
#[link_name = "vbSetReadCallback"]
fn vb_set_read_callback(sim: *mut VB, callback: Option<OnRead>) -> Option<OnRead>;
#[link_name = "vbSetSamples"]
@@ -154,14 +158,19 @@ extern "C" {
typ_: VBDataType,
capacity: c_uint,
) -> c_int;
#[link_name = "vbSetSystemRegister"]
fn vb_set_system_register(sim: *mut VB, index: c_uint, value: u32) -> u32;
#[link_name = "vbSetUserData"]
fn vb_set_user_data(sim: *mut VB, tag: *mut c_void);
#[link_name = "vbSetWriteCallback"]
fn vb_set_write_callback(sim: *mut VB, callback: Option<OnWrite>) -> Option<OnWrite>;
#[link_name = "vbSizeOf"]
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 {
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.
@@ -170,6 +179,7 @@ extern "C" fn on_frame(sim: *mut VB) -> c_int {
1
}
#[no_mangle]
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.
// There is no way for the userdata to be null or otherwise invalid.
@@ -193,6 +203,7 @@ extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length:
}
}
#[no_mangle]
extern "C" fn on_read(
sim: *mut VB,
address: u32,
@@ -204,13 +215,13 @@ extern "C" fn on_read(
// 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 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) {
VBWatchpointType::Access
} else {
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.
@@ -218,6 +229,7 @@ extern "C" fn on_read(
0
}
#[no_mangle]
extern "C" fn on_write(
sim: *mut VB,
address: u32,
@@ -230,13 +242,13 @@ 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 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) {
VBWatchpointType::Access
} else {
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.
@@ -447,6 +459,20 @@ impl Sim {
}
}
pub fn write_register(&mut self, register: VBRegister, value: u32) {
match register {
VBRegister::Program(index) => unsafe {
vb_set_program_register(self.sim, index, value as i32);
},
VBRegister::System(index) => unsafe {
vb_set_system_register(self.sim, index, value);
},
VBRegister::PC => unsafe {
vb_set_program_counter(self.sim, value);
},
}
}
pub fn read_memory(&mut self, start: u32, length: usize, into: &mut Vec<u8>) {
let mut address = start;
for _ in 0..length {
@@ -456,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) {
let data = self.get_state();
if let Err(index) = data.breakpoints.binary_search(&address) {
+120 -25
View File
@@ -9,7 +9,7 @@ use std::{
use tokio::{
io::{AsyncWriteExt as _, BufReader},
net::{TcpListener, TcpStream},
select,
pin, select,
sync::{mpsc, oneshot},
};
use tracing::{debug, enabled, error, info, Level};
@@ -57,8 +57,9 @@ impl GdbServer {
.unwrap()
.block_on(async move {
select! {
_ = run_server(sim_id, client, port, &status) => {}
_ = run_server(sim_id, client.clone(), port, &status) => {}
_ = rx => {
client.send_command(EmulatorCommand::StopDebugging(sim_id));
*status.lock().unwrap() = GdbServerStatus::Stopped;
}
}
@@ -73,19 +74,46 @@ impl GdbServer {
}
}
impl Drop for GdbServer {
fn drop(&mut self) {
self.stop();
}
}
async fn run_server(
sim_id: SimId,
client: EmulatorClient,
port: u16,
status: &Mutex<GdbServerStatus>,
) {
let (debug_sink, mut debug_source) = mpsc::unbounded_channel();
client.send_command(EmulatorCommand::StartDebugging(sim_id, debug_sink));
info!("Connecting to debugger on port {port}...");
let Some(stream) = try_connect(port, status).await else {
return;
let connect_future = try_connect(port, status);
pin!(connect_future);
let stream = loop {
select! {
stream = &mut connect_future => {
if let Some(stream) = stream {
break stream;
} else {
return;
}
}
event = debug_source.recv() => {
if event.is_none() {
// The sim has stopped (or was never started)
*status.lock().unwrap() = GdbServerStatus::Stopped;
return;
}
}
}
};
info!("Connected!");
let mut connection = GdbConnection::new(sim_id, client);
match connection.run(stream).await {
match connection.run(stream, debug_source).await {
Ok(()) => {
info!("Finished debugging.");
*status.lock().unwrap() = GdbServerStatus::Stopped;
@@ -154,12 +182,13 @@ impl GdbConnection {
memory_buf: None,
}
}
async fn run(&mut self, stream: TcpStream) -> Result<()> {
let (debug_sink, mut debug_source) = mpsc::unbounded_channel();
async fn run(
&mut self,
stream: TcpStream,
mut debug_source: mpsc::UnboundedReceiver<DebugEvent>,
) -> Result<()> {
let (rx, mut tx) = stream.into_split();
let mut request_source = RequestSource::new(BufReader::new(rx));
self.client
.send_command(EmulatorCommand::StartDebugging(self.sim_id, debug_sink));
loop {
let response = select! {
maybe_event = debug_source.recv() => {
@@ -289,24 +318,37 @@ impl GdbConnection {
} else {
self.response()
}
} else if req.match_str("P") {
let mut write_register = || {
let register_index = req.match_hex::<usize>()?;
let register = REGISTERS.get(register_index)?.to_vb_register();
if !req.match_str("=") {
return None;
}
let value = {
let mut buffer = [0; 4];
if !req.match_hex_bytes(&mut buffer) {
return None;
}
u32::from_le_bytes(buffer)
};
self.client.send_command(EmulatorCommand::WriteRegister(
self.sim_id,
register,
value,
));
Some(())
};
if let Some(()) = write_register() {
self.response().write_str("OK")
} else {
self.response()
}
} else if let Some(op) = req.match_some_str(["m", "x"]) {
let mut read_memory = || {
let start = req.match_hex::<u64>()?;
if !req.match_str(",") {
return None;
};
let length = req.match_hex::<usize>()?;
let (start, length) = parse_memory_range(&mut req)?;
let mut buf = self.memory_buf.take().unwrap_or_default();
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 {
return Some(buf);
}
@@ -340,6 +382,38 @@ impl GdbConnection {
} else {
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") {
let mut parse_request = || {
let type_ = req.match_hex::<u8>()?;
@@ -428,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 {
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 {
result += "swbreak;";
}
@@ -451,7 +546,7 @@ fn debug_stop_reason_string(reason: Option<DebugStopReason>) -> String {
DebugStopReason::Trace => "trace;",
DebugStopReason::Breakpoint => "breakpoint;",
DebugStopReason::Watchpoint(_, _) => "watchpoint;",
DebugStopReason::Trapped => "trap;",
DebugStopReason::Paused => "trap;",
};
}
+23
View File
@@ -60,6 +60,29 @@ impl Request<'_> {
}
}
}
pub fn match_hex_bytes(&mut self, buffer: &mut [u8]) -> bool {
if self.buffer.len() < buffer.len() * 2 {
return false;
}
for (i, item) in buffer.iter_mut().enumerate() {
match u8::from_radix_16(&self.buffer[(i * 2)..(i * 2) + 2]) {
(byte, 2) => *item = byte,
_ => return false,
};
}
self.buffer = self.buffer.split_at(buffer.len()).1;
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> {
+6 -3
View File
@@ -3,7 +3,7 @@
use std::{path::PathBuf, process, time::SystemTime};
use anyhow::Result;
use anyhow::{bail, Result};
use app::Application;
use clap::Parser;
use emulator::EmulatorBuilder;
@@ -95,10 +95,13 @@ fn main() -> Result<()> {
let args = Args::parse();
let (mut builder, client) = EmulatorBuilder::new();
if let Some(path) = args.rom {
builder = builder.with_rom(&path);
if let Some(path) = &args.rom {
builder = builder.with_rom(path);
}
if args.debug_port.is_some() {
if args.rom.is_none() {
bail!("to start debugging, please select a game.");
}
builder = builder.start_paused(true);
}
+6 -2
View File
@@ -3,7 +3,7 @@ use winit::event_loop::EventLoopProxy;
use crate::{
app::UserEvent,
emulator::{EmulatorClient, SimId},
emulator::{EmulatorClient, SimId, SimState},
gdbserver::{GdbServer, GdbServerStatus},
};
@@ -11,6 +11,7 @@ use super::AppWindow;
pub struct GdbServerWindow {
sim_id: SimId,
client: EmulatorClient,
port_str: String,
connected: bool,
quit_on_disconnect: bool,
@@ -22,6 +23,7 @@ impl GdbServerWindow {
pub fn new(sim_id: SimId, client: EmulatorClient, proxy: EventLoopProxy<UserEvent>) -> Self {
Self {
sim_id,
client: client.clone(),
port_str: (8080 + sim_id.to_index()).to_string(),
connected: false,
quit_on_disconnect: false,
@@ -78,7 +80,9 @@ impl AppWindow for GdbServerWindow {
self.connected = false;
}
let start_button = Button::new("Start");
if ui.add_enabled(port_num.is_some(), start_button).clicked() {
let can_start =
port_num.is_some() && self.client.sim_state(self.sim_id) == SimState::Ready;
if ui.add_enabled(can_start, start_button).clicked() {
let port = port_num.unwrap();
self.server.start(port);
self.connected = true;