926 lines
31 KiB
Rust
926 lines
31 KiB
Rust
use std::{borrow::Cow, ffi::c_void, ptr, slice, sync::Arc};
|
|
|
|
use anyhow::{Result, anyhow};
|
|
use bitflags::bitflags;
|
|
use num_derive::{FromPrimitive, ToPrimitive};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::emulator::inline_stack_map::{InlineStack, InlineStackMap};
|
|
|
|
use super::address_set::AddressSet;
|
|
|
|
#[repr(C)]
|
|
struct VB {
|
|
_data: [u8; 0],
|
|
}
|
|
|
|
#[allow(non_camel_case_types)]
|
|
type c_int = i32;
|
|
#[allow(non_camel_case_types)]
|
|
type c_uint = u32;
|
|
|
|
#[repr(u32)]
|
|
#[derive(FromPrimitive, ToPrimitive)]
|
|
enum VBDataType {
|
|
S8 = 0,
|
|
U8 = 1,
|
|
S16 = 2,
|
|
U16 = 3,
|
|
S32 = 4,
|
|
F32 = 5,
|
|
}
|
|
|
|
#[repr(i32)]
|
|
#[derive(FromPrimitive, ToPrimitive)]
|
|
enum VBOption {
|
|
PseudoHalt = 0,
|
|
}
|
|
|
|
bitflags! {
|
|
#[repr(transparent)]
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub struct VBKey: u16 {
|
|
const PWR = 0x0001;
|
|
const SGN = 0x0002;
|
|
const A = 0x0004;
|
|
const B = 0x0008;
|
|
const RT = 0x0010;
|
|
const LT = 0x0020;
|
|
const RU = 0x0040;
|
|
const RR = 0x0080;
|
|
const LR = 0x0100;
|
|
const LL = 0x0200;
|
|
const LD = 0x0400;
|
|
const LU = 0x0800;
|
|
const STA = 0x1000;
|
|
const SEL = 0x2000;
|
|
const RL = 0x4000;
|
|
const RD = 0x8000;
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub enum VBRegister {
|
|
Program(u32),
|
|
System(u32),
|
|
PC,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum VBWatchpointType {
|
|
Read,
|
|
Write,
|
|
Access,
|
|
}
|
|
|
|
type OnException = extern "C" fn(sim: *mut VB, cause: *mut u16) -> c_int;
|
|
type OnExecute =
|
|
extern "C" fn(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> c_int;
|
|
type OnFetch = extern "C" fn(
|
|
sim: *mut VB,
|
|
fetch: c_int,
|
|
address: u32,
|
|
value: *mut i32,
|
|
cycles: *mut u32,
|
|
) -> c_int;
|
|
type OnFrame = extern "C" fn(sim: *mut VB) -> c_int;
|
|
type OnRead = extern "C" fn(
|
|
sim: *mut VB,
|
|
address: u32,
|
|
type_: VBDataType,
|
|
value: *mut i32,
|
|
cycles: *mut u32,
|
|
) -> c_int;
|
|
type OnWrite = extern "C" fn(
|
|
sim: *mut VB,
|
|
address: u32,
|
|
type_: VBDataType,
|
|
value: *mut i32,
|
|
cycles: *mut u32,
|
|
cancel: *mut c_int,
|
|
) -> c_int;
|
|
|
|
#[link(name = "vb")]
|
|
unsafe extern "C" {
|
|
#[link_name = "vbEmulate"]
|
|
fn vb_emulate(sim: *mut VB, cycles: *mut u32) -> c_int;
|
|
#[link_name = "vbEmulateEx"]
|
|
fn vb_emulate_ex(sims: *mut *mut VB, count: c_uint, cycles: *mut u32) -> c_int;
|
|
#[link_name = "vbGetCartRAM"]
|
|
fn vb_get_cart_ram(sim: *mut VB, size: *mut u32) -> *mut c_void;
|
|
#[link_name = "vbGetCartROM"]
|
|
fn vb_get_cart_rom(sim: *mut VB, size: *mut u32) -> *mut c_void;
|
|
#[link_name = "vbGetPixels"]
|
|
fn vb_get_pixels(
|
|
sim: *mut VB,
|
|
left: *mut c_void,
|
|
left_stride_x: c_int,
|
|
left_stride_y: c_int,
|
|
right: *mut c_void,
|
|
right_stride_x: c_int,
|
|
right_stride_y: c_int,
|
|
);
|
|
#[link_name = "vbGetProgramCounter"]
|
|
fn vb_get_program_counter(sim: *mut VB) -> u32;
|
|
#[link_name = "vbGetProgramRegister"]
|
|
fn vb_get_program_register(sim: *mut VB, index: c_uint) -> i32;
|
|
#[link_name = "vbGetSamples"]
|
|
fn vb_get_samples(
|
|
sim: *mut VB,
|
|
typ_: *mut VBDataType,
|
|
capacity: *mut c_uint,
|
|
position: *mut c_uint,
|
|
) -> *mut c_void;
|
|
#[link_name = "vbGetSystemRegister"]
|
|
fn vb_get_system_register(sim: *mut VB, index: c_uint) -> i32;
|
|
#[link_name = "vbGetUserData"]
|
|
fn vb_get_user_data(sim: *mut VB) -> *mut c_void;
|
|
#[link_name = "vbInit"]
|
|
fn vb_init(sim: *mut VB) -> *mut VB;
|
|
#[link_name = "vbRead"]
|
|
fn vb_read(sim: *mut VB, address: u32, typ_: VBDataType) -> i32;
|
|
#[link_name = "vbReset"]
|
|
fn vb_reset(sim: *mut VB);
|
|
#[link_name = "vbSetCartRAM"]
|
|
fn vb_set_cart_ram(sim: *mut VB, sram: *mut c_void, size: u32) -> c_int;
|
|
#[link_name = "vbSetCartROM"]
|
|
fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int;
|
|
#[link_name = "vbSetExceptionCallback"]
|
|
fn vb_set_exception_callback(
|
|
sim: *mut VB,
|
|
callback: Option<OnException>,
|
|
) -> Option<OnException>;
|
|
#[link_name = "vbSetExecuteCallback"]
|
|
fn vb_set_execute_callback(sim: *mut VB, callback: Option<OnExecute>) -> Option<OnExecute>;
|
|
#[link_name = "vbSetFetchCallback"]
|
|
fn vb_set_fetch_callback(sim: *mut VB, callback: Option<OnFetch>) -> Option<OnFetch>;
|
|
#[link_name = "vbSetFrameCallback"]
|
|
fn vb_set_frame_callback(sim: *mut VB, callback: Option<OnFrame>) -> Option<OnFrame>;
|
|
#[link_name = "vbSetKeys"]
|
|
fn vb_set_keys(sim: *mut VB, keys: u16) -> u16;
|
|
#[link_name = "vbSetOption"]
|
|
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"]
|
|
fn vb_set_samples(
|
|
sim: *mut VB,
|
|
samples: *mut c_void,
|
|
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;
|
|
}
|
|
|
|
#[unsafe(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.
|
|
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
|
|
data.frame_seen = true;
|
|
if data.monitor.enabled {
|
|
data.monitor.event = Some(SimEvent::Marker(Cow::Borrowed("Frame Drawn")));
|
|
}
|
|
1
|
|
}
|
|
|
|
#[unsafe(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.
|
|
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
|
|
|
|
if data.monitor.enabled {
|
|
// SAFETY: length is the length of code, in elements
|
|
let code = unsafe { slice::from_raw_parts(code, length as usize) };
|
|
data.monitor.detect_event(sim, address, code);
|
|
// Something interesting will happen after this instruction is run.
|
|
// We'll react in the on_fetch callback it does.
|
|
}
|
|
|
|
let mut stopped = data.stop_reason.is_some() || data.monitor.event.is_some();
|
|
if data.step_from.is_some_and(|s| s != address) {
|
|
data.step_from = None;
|
|
data.stop_reason = Some(StopReason::Stepped);
|
|
stopped = true;
|
|
}
|
|
if data.breakpoints.binary_search(&address).is_ok() {
|
|
data.stop_reason = Some(StopReason::Breakpoint);
|
|
stopped = true;
|
|
}
|
|
|
|
if stopped { 1 } else { 0 }
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
extern "C" fn on_fetch(
|
|
sim: *mut VB,
|
|
_fetch: c_int,
|
|
address: u32,
|
|
_value: *mut i32,
|
|
_cycles: *mut u32,
|
|
) -> c_int {
|
|
// SAFETY: the *mut VB owns its userdata.
|
|
// 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() };
|
|
data.monitor.event = data.monitor.queued_event.take();
|
|
data.monitor.new_inline_stack = data.monitor.detect_new_inline_stack(address);
|
|
unsafe { vb_set_exception_callback(sim, Some(on_exception)) };
|
|
if data.monitor.event.is_some() || data.monitor.new_inline_stack.is_some() {
|
|
1
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
extern "C" fn on_exception(sim: *mut VB, cause: *mut u16) -> c_int {
|
|
// SAFETY: the *mut VB owns its userdata.
|
|
// 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 cause = unsafe { *cause };
|
|
let pc = if cause == 0xff70 {
|
|
0xffffff60
|
|
} else {
|
|
(cause & 0xfff0) as u32 | 0xffff0000
|
|
};
|
|
data.monitor.event = data.monitor.queued_event.take();
|
|
data.monitor.new_inline_stack = data.monitor.detect_new_inline_stack(pc);
|
|
data.monitor.queued_event = Some(SimEvent::Interrupt(cause, pc & 0x07ffffff));
|
|
unsafe { vb_set_exception_callback(sim, None) };
|
|
if data.monitor.event.is_some() || data.monitor.new_inline_stack.is_some() {
|
|
1
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
extern "C" fn on_read(
|
|
sim: *mut VB,
|
|
address: u32,
|
|
_type: VBDataType,
|
|
_value: *mut i32,
|
|
_cycles: *mut u32,
|
|
) -> c_int {
|
|
// SAFETY: the *mut VB owns its userdata.
|
|
// 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 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, start));
|
|
}
|
|
|
|
// Don't stop here, the debugger expects us to break after the memory access.
|
|
// We'll stop in on_execute instead.
|
|
0
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
extern "C" fn on_write(
|
|
sim: *mut VB,
|
|
address: u32,
|
|
typ_: VBDataType,
|
|
value: *mut i32,
|
|
_cycles: *mut u32,
|
|
_cancel: *mut c_int,
|
|
) -> c_int {
|
|
// SAFETY: the *mut VB owns its userdata.
|
|
// 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 we have profiling enabled, track custom markers
|
|
if data.monitor.enabled {
|
|
let normalized_hw_address = address & 0x0700003f;
|
|
if normalized_hw_address == 0x02000038 && matches!(typ_, VBDataType::S32) {
|
|
assert!(data.monitor.queued_event.is_none());
|
|
// The game has written the address of a null-terminated string
|
|
// (whose length is at most 64 bytes). Read that string.
|
|
let str_address = unsafe { *value } as u32;
|
|
let mut bytes = [0u8; 64];
|
|
let mut len = 0;
|
|
for (dst, src_address) in bytes.iter_mut().zip(str_address..str_address + 64) {
|
|
let char = unsafe { vb_read(sim, src_address, VBDataType::U8) } as u8;
|
|
if char == 0 {
|
|
break;
|
|
}
|
|
*dst = char;
|
|
len += 1;
|
|
}
|
|
let name = String::from_utf8_lossy(&bytes[..len]).into_owned();
|
|
data.monitor.queued_event = Some(SimEvent::Marker(Cow::Owned(name)));
|
|
}
|
|
}
|
|
|
|
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, start));
|
|
}
|
|
|
|
// Don't stop here, the debugger expects us to break after the memory access.
|
|
// We'll stop in on_execute instead.
|
|
0
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Debug)]
|
|
pub enum SimEvent {
|
|
Call(u32),
|
|
Return,
|
|
Halt,
|
|
Interrupt(u16, u32),
|
|
Reti,
|
|
Marker(Cow<'static, str>),
|
|
}
|
|
|
|
struct EventMonitor {
|
|
enabled: bool,
|
|
event: Option<SimEvent>,
|
|
queued_event: Option<SimEvent>,
|
|
just_halted: bool,
|
|
inline_stack_map: InlineStackMap,
|
|
new_inline_stack: Option<InlineStack>,
|
|
last_inline_stack: InlineStack,
|
|
}
|
|
|
|
impl EventMonitor {
|
|
fn new() -> Self {
|
|
let inline_stack_map = InlineStackMap::empty();
|
|
let last_inline_stack = inline_stack_map.get(0).clone();
|
|
Self {
|
|
enabled: false,
|
|
event: None,
|
|
queued_event: None,
|
|
just_halted: false,
|
|
inline_stack_map,
|
|
new_inline_stack: None,
|
|
last_inline_stack,
|
|
}
|
|
}
|
|
|
|
fn detect_new_inline_stack(&mut self, address: u32) -> Option<InlineStack> {
|
|
let stack = self.inline_stack_map.get(address);
|
|
if Arc::ptr_eq(stack, &self.last_inline_stack) {
|
|
return None;
|
|
}
|
|
self.last_inline_stack = stack.clone();
|
|
Some(stack.clone())
|
|
}
|
|
|
|
fn detect_event(&mut self, sim: *mut VB, address: u32, code: &[u16]) -> bool {
|
|
self.queued_event = self.do_detect_event(sim, address, code);
|
|
self.queued_event.is_some()
|
|
}
|
|
|
|
fn do_detect_event(&mut self, sim: *mut VB, address: u32, code: &[u16]) -> Option<SimEvent> {
|
|
const HALT_OPCODE: u16 = 0b011010;
|
|
const JAL_OPCODE: u16 = 0b101011;
|
|
const JMP_OPCODE: u16 = 0b000110;
|
|
const RETI_OPCODE: u16 = 0b011001;
|
|
|
|
const fn format_i_reg_1(code: &[u16]) -> u8 {
|
|
(code[0] & 0x1f) as u8
|
|
}
|
|
|
|
const fn format_iv_disp(code: &[u16]) -> i32 {
|
|
let value = ((code[0] & 0x3ff) as i32) << 16 | (code[1] as i32);
|
|
value << 6 >> 6
|
|
}
|
|
|
|
let opcode = code[0] >> 10;
|
|
|
|
if opcode == HALT_OPCODE {
|
|
if !self.just_halted {
|
|
self.just_halted = true;
|
|
self.event = Some(SimEvent::Halt);
|
|
} else {
|
|
self.just_halted = false;
|
|
}
|
|
// Don't _return_ an event, we want to emit this right away.
|
|
// If the CPU is halting, no other callbacks will run for a long time.
|
|
return None;
|
|
}
|
|
|
|
if opcode == JAL_OPCODE {
|
|
let disp = format_iv_disp(code);
|
|
if disp != 4 {
|
|
// JAL .+4 is how programs get r31 to a known value for indirect calls
|
|
// (which we detect later.)
|
|
// Any other JAL is a function call.
|
|
return Some(SimEvent::Call(
|
|
address.wrapping_add_signed(disp) & 0x07ffffff,
|
|
));
|
|
}
|
|
}
|
|
|
|
if opcode == JMP_OPCODE {
|
|
let jmp_reg = format_i_reg_1(code);
|
|
if jmp_reg == 31 {
|
|
// JMP[r31] is a return
|
|
return Some(SimEvent::Return);
|
|
}
|
|
let r31 = unsafe { vb_get_program_register(sim, 31) };
|
|
if r31 as u32 == address.wrapping_add(2) {
|
|
// JMP anywhere else, if r31 points to after the JMP, is an indirect call
|
|
let target = unsafe { vb_get_program_register(sim, jmp_reg as u32) };
|
|
return Some(SimEvent::Call(target as u32 & 0x07ffffff));
|
|
}
|
|
}
|
|
|
|
if opcode == RETI_OPCODE {
|
|
return Some(SimEvent::Reti);
|
|
}
|
|
|
|
None
|
|
}
|
|
}
|
|
|
|
const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4;
|
|
const AUDIO_CAPACITY_FLOATS: usize = AUDIO_CAPACITY_SAMPLES * 2;
|
|
pub const EXPECTED_FRAME_SIZE: usize = 834 * 2;
|
|
|
|
struct VBState {
|
|
frame_seen: bool,
|
|
stop_reason: Option<StopReason>,
|
|
monitor: EventMonitor,
|
|
step_from: Option<u32>,
|
|
breakpoints: Vec<u32>,
|
|
read_watchpoints: AddressSet,
|
|
write_watchpoints: AddressSet,
|
|
stdout: Option<Vec<u8>>,
|
|
}
|
|
|
|
impl VBState {
|
|
fn needs_execute_callback(&self) -> bool {
|
|
self.step_from.is_some()
|
|
|| self.monitor.enabled
|
|
|| !self.breakpoints.is_empty()
|
|
|| !self.read_watchpoints.is_empty()
|
|
|| !self.write_watchpoints.is_empty()
|
|
}
|
|
|
|
fn needs_write_callback(&self) -> bool {
|
|
self.stdout.is_some() || self.monitor.enabled || !self.write_watchpoints.is_empty()
|
|
}
|
|
}
|
|
|
|
pub enum StopReason {
|
|
Breakpoint,
|
|
Watchpoint(VBWatchpointType, u32),
|
|
Stepped,
|
|
}
|
|
|
|
#[repr(transparent)]
|
|
pub struct Sim {
|
|
sim: *mut VB,
|
|
}
|
|
|
|
impl Sim {
|
|
pub fn new() -> Self {
|
|
// init the VB instance itself
|
|
let size = unsafe { vb_size_of() };
|
|
// allocate a vec of u64 so that this memory is 8-byte aligned
|
|
let memory = vec![0u64; size.div_ceil(4)];
|
|
let sim: *mut VB = Box::into_raw(memory.into_boxed_slice()).cast();
|
|
unsafe { vb_init(sim) };
|
|
// pseudohalt is disabled due to breaking red alarm
|
|
unsafe { vb_set_option(sim, VBOption::PseudoHalt, 0) };
|
|
unsafe { vb_set_keys(sim, VBKey::SGN.bits()) };
|
|
unsafe { vb_reset(sim) };
|
|
|
|
// set up userdata
|
|
let state = VBState {
|
|
frame_seen: false,
|
|
stop_reason: None,
|
|
monitor: EventMonitor::new(),
|
|
step_from: None,
|
|
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)) };
|
|
|
|
// set up audio buffer
|
|
let audio_buffer = vec![0.0f32; AUDIO_CAPACITY_FLOATS];
|
|
let samples: *mut c_void = Box::into_raw(audio_buffer.into_boxed_slice()).cast();
|
|
unsafe { vb_set_samples(sim, samples, VBDataType::F32, AUDIO_CAPACITY_SAMPLES as u32) };
|
|
|
|
Sim { sim }
|
|
}
|
|
|
|
pub fn reset(&mut self) {
|
|
unsafe { vb_reset(self.sim) };
|
|
}
|
|
|
|
pub fn monitor_events(&mut self, enabled: bool, inline_stack_map: InlineStackMap) {
|
|
let state = self.get_state();
|
|
state.monitor.enabled = enabled;
|
|
state.monitor.event = None;
|
|
state.monitor.queued_event = None;
|
|
state.monitor.new_inline_stack = None;
|
|
state.monitor.last_inline_stack = inline_stack_map.get(0).clone();
|
|
state.monitor.inline_stack_map = inline_stack_map;
|
|
if enabled {
|
|
unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) };
|
|
unsafe { vb_set_exception_callback(self.sim, Some(on_exception)) };
|
|
unsafe { vb_set_fetch_callback(self.sim, Some(on_fetch)) };
|
|
unsafe { vb_set_write_callback(self.sim, Some(on_write)) };
|
|
} else {
|
|
let needs_execute = state.needs_execute_callback();
|
|
let needs_write = state.needs_write_callback();
|
|
if !needs_execute {
|
|
unsafe { vb_set_execute_callback(self.sim, None) };
|
|
}
|
|
unsafe { vb_set_exception_callback(self.sim, None) };
|
|
unsafe { vb_set_fetch_callback(self.sim, None) };
|
|
if !needs_write {
|
|
unsafe { vb_set_write_callback(self.sim, None) };
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn load_cart(&mut self, mut rom: Vec<u8>, mut sram: Vec<u8>) -> Result<()> {
|
|
self.unload_cart();
|
|
|
|
rom.shrink_to_fit();
|
|
sram.shrink_to_fit();
|
|
|
|
let size = rom.len() as u32;
|
|
let rom = Box::into_raw(rom.into_boxed_slice()).cast();
|
|
let status = unsafe { vb_set_cart_rom(self.sim, rom, size) };
|
|
if status != 0 {
|
|
let _: Vec<u8> =
|
|
unsafe { Vec::from_raw_parts(rom.cast(), size as usize, size as usize) };
|
|
return Err(anyhow!("Invalid ROM size of {} bytes", size));
|
|
}
|
|
|
|
let size = sram.len() as u32;
|
|
let sram = Box::into_raw(sram.into_boxed_slice()).cast();
|
|
let status = unsafe { vb_set_cart_ram(self.sim, sram, size) };
|
|
if status != 0 {
|
|
let _: Vec<u8> =
|
|
unsafe { Vec::from_raw_parts(sram.cast(), size as usize, size as usize) };
|
|
return Err(anyhow!("Invalid SRAM size of {} bytes", size));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn unload_cart(&mut self) {
|
|
let mut size = 0;
|
|
let rom = unsafe { vb_get_cart_rom(self.sim, &mut size) };
|
|
unsafe { vb_set_cart_rom(self.sim, ptr::null_mut(), 0) };
|
|
if !rom.is_null() {
|
|
let _: Vec<u8> =
|
|
unsafe { Vec::from_raw_parts(rom.cast(), size as usize, size as usize) };
|
|
}
|
|
|
|
let sram = unsafe { vb_get_cart_ram(self.sim, &mut size) };
|
|
unsafe { vb_set_cart_ram(self.sim, ptr::null_mut(), 0) };
|
|
if !sram.is_null() {
|
|
let _: Vec<u8> =
|
|
unsafe { Vec::from_raw_parts(sram.cast(), size as usize, size as usize) };
|
|
}
|
|
}
|
|
|
|
pub fn read_sram(&mut self, buffer: &mut [u8]) {
|
|
let mut size = 0;
|
|
let sram = unsafe { vb_get_cart_ram(self.sim, &mut size) };
|
|
if sram.is_null() {
|
|
return;
|
|
}
|
|
let bytes = unsafe { slice::from_raw_parts(sram.cast(), size as usize) };
|
|
buffer.copy_from_slice(bytes);
|
|
}
|
|
|
|
pub fn link(&mut self, peer: &mut Sim) {
|
|
unsafe { vb_set_peer(self.sim, peer.sim) };
|
|
}
|
|
|
|
pub fn unlink(&mut self) {
|
|
unsafe { vb_set_peer(self.sim, ptr::null_mut()) };
|
|
}
|
|
|
|
pub fn emulate(&mut self, cycles: &mut u32) {
|
|
unsafe { vb_emulate(self.sim, cycles) };
|
|
}
|
|
|
|
pub fn emulate_many(sims: &mut [Sim], cycles: &mut u32) {
|
|
let count = sims.len() as c_uint;
|
|
let sims = sims.as_mut_ptr().cast();
|
|
unsafe { vb_emulate_ex(sims, count, cycles) };
|
|
}
|
|
|
|
pub fn read_pixels(&mut self, buffers: &mut [u8]) -> bool {
|
|
let data = self.get_state();
|
|
if !data.frame_seen {
|
|
return false;
|
|
}
|
|
data.frame_seen = false;
|
|
|
|
// the buffer must be big enough for our data
|
|
assert!(buffers.len() >= 384 * 224 * 2);
|
|
unsafe {
|
|
vb_get_pixels(
|
|
self.sim,
|
|
buffers.as_mut_ptr().cast(),
|
|
2,
|
|
384 * 2,
|
|
buffers.as_mut_ptr().offset(1).cast(),
|
|
2,
|
|
384 * 2,
|
|
);
|
|
};
|
|
true
|
|
}
|
|
|
|
pub fn read_samples(&mut self, samples: &mut Vec<f32>, weight: f32) {
|
|
let mut position = 0;
|
|
let ptr =
|
|
unsafe { vb_get_samples(self.sim, ptr::null_mut(), ptr::null_mut(), &mut position) };
|
|
// SAFETY: position is an offset in a buffer of (f32, f32). so, position * 2 is an offset in a buffer of f32.
|
|
let read_samples: &[f32] =
|
|
unsafe { slice::from_raw_parts(ptr.cast(), position as usize * 2) };
|
|
samples.resize(read_samples.len(), 0.0);
|
|
for (index, sample) in read_samples.iter().enumerate() {
|
|
samples[index] += sample * weight;
|
|
}
|
|
|
|
unsafe {
|
|
vb_set_samples(
|
|
self.sim,
|
|
ptr,
|
|
VBDataType::F32,
|
|
AUDIO_CAPACITY_SAMPLES as u32,
|
|
)
|
|
};
|
|
}
|
|
|
|
pub fn set_keys(&mut self, keys: VBKey) {
|
|
unsafe { vb_set_keys(self.sim, keys.bits()) };
|
|
}
|
|
|
|
pub fn read_register(&mut self, register: VBRegister) -> u32 {
|
|
match register {
|
|
VBRegister::Program(index) => unsafe {
|
|
vb_get_program_register(self.sim, index) as u32
|
|
},
|
|
VBRegister::System(index) => unsafe { vb_get_system_register(self.sim, index) as u32 },
|
|
VBRegister::PC => unsafe { vb_get_program_counter(self.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 {
|
|
let byte = unsafe { vb_read(self.sim, address, VBDataType::U8) };
|
|
into.push(byte as u8);
|
|
address = address.wrapping_add(1);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
data.breakpoints.insert(index, address);
|
|
}
|
|
|
|
unsafe {
|
|
vb_set_execute_callback(self.sim, Some(on_execute));
|
|
}
|
|
}
|
|
|
|
pub fn remove_breakpoint(&mut self, address: u32) {
|
|
let data = self.get_state();
|
|
if let Ok(index) = data.breakpoints.binary_search(&address) {
|
|
data.breakpoints.remove(index);
|
|
if !data.needs_execute_callback() {
|
|
unsafe { vb_set_execute_callback(self.sim, None) };
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn add_watchpoint(&mut self, address: u32, length: usize, watch: VBWatchpointType) {
|
|
match watch {
|
|
VBWatchpointType::Read => self.add_read_watchpoint(address, length),
|
|
VBWatchpointType::Write => self.add_write_watchpoint(address, length),
|
|
VBWatchpointType::Access => {
|
|
self.add_read_watchpoint(address, length);
|
|
self.add_write_watchpoint(address, length);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn add_read_watchpoint(&mut self, address: u32, length: usize) {
|
|
let state = self.get_state();
|
|
state.read_watchpoints.add(address, length);
|
|
if !state.read_watchpoints.is_empty() {
|
|
unsafe { vb_set_read_callback(self.sim, Some(on_read)) };
|
|
unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) };
|
|
}
|
|
}
|
|
|
|
fn add_write_watchpoint(&mut self, address: u32, length: usize) {
|
|
let state = self.get_state();
|
|
state.write_watchpoints.add(address, length);
|
|
if !state.write_watchpoints.is_empty() {
|
|
unsafe { vb_set_write_callback(self.sim, Some(on_write)) };
|
|
unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) };
|
|
}
|
|
}
|
|
|
|
pub fn remove_watchpoint(&mut self, address: u32, length: usize, watch: VBWatchpointType) {
|
|
match watch {
|
|
VBWatchpointType::Read => self.remove_read_watchpoint(address, length),
|
|
VBWatchpointType::Write => self.remove_write_watchpoint(address, length),
|
|
VBWatchpointType::Access => {
|
|
self.remove_read_watchpoint(address, length);
|
|
self.remove_write_watchpoint(address, length);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn remove_read_watchpoint(&mut self, address: u32, length: usize) {
|
|
let state = self.get_state();
|
|
state.read_watchpoints.remove(address, length);
|
|
let needs_execute = state.needs_execute_callback();
|
|
if state.read_watchpoints.is_empty() {
|
|
unsafe { vb_set_read_callback(self.sim, None) };
|
|
if !needs_execute {
|
|
unsafe { vb_set_execute_callback(self.sim, None) };
|
|
}
|
|
}
|
|
}
|
|
|
|
fn remove_write_watchpoint(&mut self, address: u32, length: usize) {
|
|
let state = self.get_state();
|
|
state.write_watchpoints.remove(address, length);
|
|
let needs_write = state.needs_write_callback();
|
|
let needs_execute = state.needs_execute_callback();
|
|
if state.write_watchpoints.is_empty() {
|
|
if !needs_write {
|
|
unsafe { vb_set_write_callback(self.sim, None) };
|
|
}
|
|
if !needs_execute {
|
|
unsafe { vb_set_execute_callback(self.sim, None) };
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn step(&mut self) {
|
|
let current_pc = unsafe { vb_get_program_counter(self.sim) };
|
|
let data = self.get_state();
|
|
data.step_from = Some(current_pc);
|
|
unsafe {
|
|
vb_set_execute_callback(self.sim, Some(on_execute));
|
|
}
|
|
}
|
|
|
|
pub fn clear_debug_state(&mut self) {
|
|
let data = self.get_state();
|
|
data.step_from = None;
|
|
data.breakpoints.clear();
|
|
data.read_watchpoints.clear();
|
|
data.write_watchpoints.clear();
|
|
let needs_write = data.needs_write_callback();
|
|
let needs_execute = data.needs_execute_callback();
|
|
unsafe { vb_set_read_callback(self.sim, None) };
|
|
if !needs_write {
|
|
unsafe { vb_set_write_callback(self.sim, None) };
|
|
}
|
|
if !needs_execute {
|
|
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.needs_write_callback() {
|
|
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 take_stop_reason(&mut self) -> Option<StopReason> {
|
|
let data = self.get_state();
|
|
let reason = data.stop_reason.take();
|
|
if !data.needs_execute_callback() {
|
|
unsafe { vb_set_execute_callback(self.sim, None) };
|
|
}
|
|
reason
|
|
}
|
|
|
|
pub fn take_profiler_updates(&mut self) -> (Option<SimEvent>, Option<InlineStack>) {
|
|
let data = self.get_state();
|
|
let event = data.monitor.event.take();
|
|
let inline_stack = data.monitor.new_inline_stack.take();
|
|
(event, inline_stack)
|
|
}
|
|
|
|
fn get_state(&mut self) -> &mut VBState {
|
|
// SAFETY: the *mut VB owns its userdata.
|
|
// There is no way for the userdata to be null or otherwise invalid.
|
|
unsafe { &mut *vb_get_user_data(self.sim).cast() }
|
|
}
|
|
}
|
|
|
|
impl Drop for Sim {
|
|
fn drop(&mut self) {
|
|
let ptr =
|
|
unsafe { vb_get_samples(self.sim, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()) };
|
|
// SAFETY: the audio buffer originally came from a Vec<u32>
|
|
let floats: Vec<f32> = unsafe {
|
|
Vec::from_raw_parts(ptr.cast(), AUDIO_CAPACITY_FLOATS, AUDIO_CAPACITY_FLOATS)
|
|
};
|
|
drop(floats);
|
|
|
|
// SAFETY: the *mut VB owns its userdata.
|
|
// There is no way for the userdata to be null or otherwise invalid.
|
|
let ptr: *mut VBState = unsafe { vb_get_user_data(self.sim).cast() };
|
|
// SAFETY: we made this pointer ourselves, we can for sure free it
|
|
unsafe { drop(Box::from_raw(ptr)) };
|
|
|
|
// If we're linked to another sim, unlink from them.
|
|
unsafe { vb_set_peer(self.sim, ptr::null_mut()) };
|
|
|
|
let len = unsafe { vb_size_of() }.div_ceil(4);
|
|
// SAFETY: the sim's memory originally came from a Vec<u64>
|
|
let bytes: Vec<u64> = unsafe { Vec::from_raw_parts(self.sim.cast(), len, len) };
|
|
drop(bytes);
|
|
}
|
|
}
|