Compare commits

..
3 Commits
Author SHA1 Message Date
SonicSwordcane 0c6c9ec4c3 Start profiler UI/thread 2025-08-09 23:45:34 -04:00
SonicSwordcane 6349402458 Run elf files 2025-08-09 22:28:45 -04:00
SonicSwordcane 80bab28442 Start implementing profiling 2025-08-07 23:49:29 -04:00
20 changed files with 652 additions and 254 deletions
Generated
+178 -171
View File
File diff suppressed because it is too large Load Diff
+2 -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.7.3" version = "0.7.1"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
@@ -21,6 +21,7 @@ egui_extras = { version = "0.32", features = ["image"] }
egui-notify = "0.20" egui-notify = "0.20"
egui-winit = "0.32" egui-winit = "0.32"
egui-wgpu = { version = "0.32", features = ["winit"] } egui-wgpu = { version = "0.32", features = ["winit"] }
elf = "0.8"
fixed = { version = "1.28", features = ["num-traits"] } fixed = { version = "1.28", features = ["num-traits"] }
gilrs = { version = "0.11", features = ["serde-serialize"] } gilrs = { version = "0.11", features = ["serde-serialize"] }
hex = "0.4" hex = "0.4"
+10 -26
View File
@@ -1,39 +1,24 @@
# This Dockerfile produces a base image for builds. # This Dockerfile produces a base image for builds.
# It includes all dependencies necessary to cross-compile for Windows/MacOS/Linux. # It includes all dependencies necessary to cross-compile for Windows/MacOS/Linux.
FROM debian:bookworm AS osxcross FROM crazymax/osxcross:latest-ubuntu AS osxcross
ADD --chmod=644 "https://apt.llvm.org/llvm-snapshot.gpg.key" /etc/apt/trusted.gpg.d/apt.llvm.org.asc
WORKDIR /build/osxcross
ADD "https://github.com/tpoechtrager/osxcross.git#47936a512273bb3b414b5a2e83043c92eabc7ae7" .
ADD "https://github.com/joseluisq/macosx-sdks/releases/download/14.5/MacOSX14.5.sdk.tar.xz" ./tarballs/MacOSX14.5.sdk.tar.xz
RUN apt-get update && \
apt-get install -y ca-certificates
COPY llvm.sources /etc/apt/sources.list.d/llvm.sources
RUN apt-get update && \
apt-get install -y bash bzip2 clang-20 git lld-20 llvm-20 make patch xz-utils && \
ln -s $(which clang-20) /usr/bin/clang && \
ln -s $(which clang++-20) /usr/bin/clang++ && \
ln -s $(which ld64.lld-20) /usr/bin/ld64.lld && \
SDK_VERSION=14.5 UNATTENDED=1 ENABLE_COMPILER_RT_INSTALL=1 TARGET_DIR=/osxcross ./build.sh
FROM rust:1.89-bookworm FROM rust:latest
ADD --chmod=644 "https://apt.llvm.org/llvm-snapshot.gpg.key" /etc/apt/trusted.gpg.d/apt.llvm.org.asc
COPY llvm.sources /etc/apt/sources.list.d/llvm.sources
RUN rustup target add x86_64-pc-windows-msvc && \ RUN rustup target add x86_64-pc-windows-msvc && \
rustup target add x86_64-apple-darwin && \ rustup target add x86_64-apple-darwin && \
rustup target add aarch64-apple-darwin && \ rustup target add aarch64-apple-darwin && \
apt-get update && \ apt-get update && \
apt-get install -y clang-20 lld-20 libc6-dev libasound2-dev libudev-dev genisoimage mingw-w64 && \ apt-get install -y clang-19 lld-19 libc6-dev libasound2-dev libudev-dev genisoimage mingw-w64 && \
cargo install cargo-bundle xwin && \ cargo install cargo-bundle xwin && \
xwin --accept-license splat --output xwin && \ xwin --accept-license splat --output xwin && \
rm -rf .xwin-cache && \ rm -rf .xwin-cache && \
ln -s $(which clang-20) /usr/bin/clang && \ ln -s $(which clang-19) /usr/bin/clang && \
ln -s $(which clang++-20) /usr/bin/clang++ ln -s $(which clang++-19) /usr/bin/clang++
COPY --from=osxcross /osxcross /osxcross COPY --from=osxcross /osxcross /osxcross
ENV PATH="/osxcross/bin:$PATH" \ ENV PATH="/osxcross/bin:$PATH" \
LD_LIBRARY_PATH="/osxcross/lib" \ LD_LIBRARY_PATH="/osxcross/lib" \
CC="clang-20" CXX="clang++-20" AR="llvm-ar-20" \ CC="clang-19" CXX="clang++-19" AR="llvm-ar-19" \
CC_x86_64-apple-darwin="o64-clang" \ CC_x86_64-apple-darwin="o64-clang" \
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" \
@@ -42,11 +27,10 @@ ENV PATH="/osxcross/bin:$PATH" \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" \ 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" \ 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_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-20" \ 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-20" \ 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-20" \ CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-19" \
RC_PATH="llvm-rc-20" \ RC_PATH="llvm-rc-19" \
MACOSX_DEPLOYMENT_TARGET="14.5" MACOSX_DEPLOYMENT_TARGET="14.5"
ENTRYPOINT ["bash"]
+2
View File
@@ -23,7 +23,9 @@ 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_EXCEPTION", "on_exception")
.define("VB_DIRECT_EXECUTE", "on_execute") .define("VB_DIRECT_EXECUTE", "on_execute")
.define("VB_DIRECT_FETCH", "on_fetch")
.define("VB_DIRECT_FRAME", "on_frame") .define("VB_DIRECT_FRAME", "on_frame")
.define("VB_DIRECT_READ", "on_read") .define("VB_DIRECT_READ", "on_read")
.define("VB_DIRECT_WRITE", "on_write") .define("VB_DIRECT_WRITE", "on_write")
-4
View File
@@ -1,4 +0,0 @@
Types: deb deb-src
URIs: http://apt.llvm.org/bookworm/
Suites: llvm-toolchain-bookworm-20
Components: main
-4
View File
@@ -1,4 +0,0 @@
set -e
docker build --pull -f build.Dockerfile -t lemur-build .
MSYS_NO_PATHCONV=1 docker run -it --rm -v .:/app -w /app lemur-build /app/scripts/do-bundle.sh
+2 -1
View File
@@ -24,7 +24,8 @@ if ! command -v docker 2>&1 >/dev/null; then
exit 1 exit 1
fi fi
./scripts/bundle.sh docker build -f build.Dockerfile -t lemur-build .
MSYS_NO_PATHCONV=1 docker run -it --rm -v .:/app -w /app --entrypoint bash lemur-build /app/scripts/do-bundle.sh
body=$(cat <<EOF body=$(cat <<EOF
## How to install ## How to install
+20 -6
View File
@@ -24,8 +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, TerminalWindow, GdbServerWindow, HotkeysWindow, InputWindow, ObjectWindow, ProfileWindow, RegisterWindow,
WorldWindow, TerminalWindow, WorldWindow,
}, },
}; };
@@ -54,6 +54,7 @@ pub struct Application {
viewports: HashMap<ViewportId, Viewport>, viewports: HashMap<ViewportId, Viewport>,
focused: Option<ViewportId>, focused: Option<ViewportId>,
init_debug_port: Option<u16>, init_debug_port: Option<u16>,
init_profiling: bool,
} }
impl Application { impl Application {
@@ -61,6 +62,7 @@ impl Application {
client: EmulatorClient, client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
debug_port: Option<u16>, debug_port: Option<u16>,
profiling: bool,
) -> Self { ) -> Self {
let wgpu = WgpuState::new(); let wgpu = WgpuState::new();
let icon = load_icon().ok().map(Arc::new); let icon = load_icon().ok().map(Arc::new);
@@ -89,6 +91,7 @@ impl Application {
viewports: HashMap::new(), viewports: HashMap::new(),
focused: None, focused: None,
init_debug_port: debug_port, init_debug_port: debug_port,
init_profiling: profiling,
} }
} }
@@ -113,6 +116,11 @@ impl ApplicationHandler<UserEvent> for Application {
server.launch(port); server.launch(port);
self.open(event_loop, Box::new(server)); self.open(event_loop, Box::new(server));
} }
if self.init_profiling {
let mut profiler = ProfileWindow::new(SimId::Player1, self.client.clone());
profiler.launch();
self.open(event_loop, Box::new(profiler));
}
let app = GameWindow::new( let app = GameWindow::new(
self.client.clone(), self.client.clone(),
self.proxy.clone(), self.proxy.clone(),
@@ -209,10 +217,11 @@ impl ApplicationHandler<UserEvent> for Application {
.focused .focused
.as_ref() .as_ref()
.and_then(|id| self.viewports.get_mut(id)) .and_then(|id| self.viewports.get_mut(id))
&& viewport.app.handle_gamepad_event(&event)
{ {
if viewport.app.handle_gamepad_event(&event) {
return; return;
} }
}
self.controllers.handle_gamepad_event(&event); self.controllers.handle_gamepad_event(&event);
} }
UserEvent::OpenAbout => { UserEvent::OpenAbout => {
@@ -247,6 +256,10 @@ impl ApplicationHandler<UserEvent> for Application {
let terminal = TerminalWindow::new(sim_id, &self.client); let terminal = TerminalWindow::new(sim_id, &self.client);
self.open(event_loop, Box::new(terminal)); self.open(event_loop, Box::new(terminal));
} }
UserEvent::OpenProfiler(sim_id) => {
let profile = ProfileWindow::new(sim_id, self.client.clone());
self.open(event_loop, Box::new(profile));
}
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());
@@ -288,12 +301,12 @@ impl ApplicationHandler<UserEvent> for Application {
fn exiting(&mut self, _event_loop: &ActiveEventLoop) { fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
let (sender, receiver) = oneshot::channel(); let (sender, receiver) = oneshot::channel();
if self.client.send_command(EmulatorCommand::Exit(sender)) if self.client.send_command(EmulatorCommand::Exit(sender)) {
&& let Err(error) = receiver.recv_timeout(Duration::from_secs(5)) if let Err(error) = receiver.recv_timeout(Duration::from_secs(5)) {
{
error!(%error, "could not gracefully exit."); error!(%error, "could not gracefully exit.");
} }
} }
}
} }
struct WgpuState { struct WgpuState {
@@ -521,6 +534,7 @@ pub enum UserEvent {
OpenFrameBuffers(SimId), OpenFrameBuffers(SimId),
OpenRegisters(SimId), OpenRegisters(SimId),
OpenTerminal(SimId), OpenTerminal(SimId),
OpenProfiler(SimId),
OpenDebugger(SimId), OpenDebugger(SimId),
OpenInput, OpenInput,
OpenHotkeys, OpenHotkeys,
+84 -11
View File
@@ -18,7 +18,7 @@ use tracing::{error, warn};
use crate::{ use crate::{
audio::Audio, audio::Audio,
emulator::cart::Cart, emulator::{cart::Cart, shrooms_vb_core::SimEvent},
graphics::TextureSink, graphics::TextureSink,
memory::{MemoryRange, MemoryRegion}, memory::{MemoryRange, MemoryRegion},
}; };
@@ -137,6 +137,7 @@ pub struct Emulator {
state: Arc<Atomic<EmulatorState>>, state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>, audio_on: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>, linked: Arc<AtomicBool>,
profilers: [Option<ProfileSender>; 2],
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>,
@@ -164,6 +165,7 @@ impl Emulator {
state, state,
audio_on, audio_on,
linked, linked,
profilers: [None, None],
renderers: HashMap::new(), renderers: HashMap::new(),
messages: HashMap::new(), messages: HashMap::new(),
debuggers: HashMap::new(), debuggers: HashMap::new(),
@@ -218,6 +220,24 @@ impl Emulator {
self.carts[index] = Some(cart); self.carts[index] = Some(cart);
self.sim_state[index].store(SimState::Ready, Ordering::Release); self.sim_state[index].store(SimState::Ready, Ordering::Release);
} }
let mut profiling = false;
if let Some(profiler) = self.profilers[sim_id.to_index()].as_ref() {
if let Some(cart) = self.carts[index].as_ref() {
if profiler
.send(ProfileEvent::Start {
file_path: cart.file_path.clone(),
})
.is_ok()
{
sim.monitor_events(true);
profiling = true;
}
}
}
if !profiling {
sim.monitor_events(false);
}
if self.sim_state[index].load(Ordering::Acquire) == SimState::Ready { if self.sim_state[index].load(Ordering::Acquire) == SimState::Ready {
self.resume_sims(); self.resume_sims();
} }
@@ -315,6 +335,11 @@ impl Emulator {
Ok(()) Ok(())
} }
fn start_profiling(&mut self, sim_id: SimId, sender: ProfileSender) -> Result<()> {
self.profilers[sim_id.to_index()] = Some(sender);
self.reset_sim(sim_id, None)
}
fn start_debugging(&mut self, sim_id: SimId, sender: DebugSender) { fn start_debugging(&mut self, sim_id: SimId, sender: DebugSender) {
if self.sim_state[sim_id.to_index()].load(Ordering::Acquire) != SimState::Ready { if self.sim_state[sim_id.to_index()].load(Ordering::Acquire) != SimState::Ready {
// Can't debug unless a game is connected // Can't debug unless a game is connected
@@ -438,12 +463,30 @@ impl Emulator {
let p1_running = running && p1_state == SimState::Ready; let p1_running = running && p1_state == SimState::Ready;
let p2_running = running && p2_state == SimState::Ready; let p2_running = running && p2_state == SimState::Ready;
let mut idle = !p1_running && !p2_running; let mut idle = !p1_running && !p2_running;
if p1_running && p2_running {
Sim::emulate_many(&mut self.sims); let cycles = self.emulate(p1_running, p2_running);
} else if p1_running {
self.sims[SimId::Player1.to_index()].emulate(); // if we're profiling, track events
} else if p2_running { for ((sim, profiler), running) in self
self.sims[SimId::Player2.to_index()].emulate(); .sims
.iter_mut()
.zip(self.profilers.iter_mut())
.zip([p1_running, p2_running])
{
if !running {
continue;
}
if let Some(p) = profiler {
if p.send(ProfileEvent::Update {
cycles,
event: sim.take_sim_event(),
})
.is_err()
{
sim.monitor_events(false);
*profiler = None;
}
}
} }
if state == EmulatorState::Stepping { if state == EmulatorState::Stepping {
@@ -455,12 +498,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 {
return true; return true;
}; };
if let Some(text) = sim.take_stdout() if let Some(text) = sim.take_stdout() {
&& stdout.send(text).is_err() if stdout.send(text).is_err() {
{
sim.watch_stdout(false); sim.watch_stdout(false);
return false; return false;
} }
}
true true
}); });
@@ -470,7 +513,7 @@ 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 let Some(reason) = sim.stop_reason() { if let Some(reason) = sim.take_stop_reason() {
let stop_reason = match reason { let stop_reason = match reason {
StopReason::Stepped => DebugStopReason::Trace, StopReason::Stepped => DebugStopReason::Trace,
StopReason::Watchpoint(watch, address) => { StopReason::Watchpoint(watch, address) => {
@@ -529,6 +572,19 @@ impl Emulator {
idle idle
} }
fn emulate(&mut self, p1_running: bool, p2_running: bool) -> u32 {
const MAX_CYCLES: u32 = 20_000_000;
let mut cycles = MAX_CYCLES;
if p1_running && p2_running {
Sim::emulate_many(&mut self.sims, &mut cycles);
} else if p1_running {
self.sims[SimId::Player1.to_index()].emulate(&mut cycles);
} else if p2_running {
self.sims[SimId::Player2.to_index()].emulate(&mut cycles);
}
MAX_CYCLES - cycles
}
fn handle_command(&mut self, command: EmulatorCommand) { fn handle_command(&mut self, command: EmulatorCommand) {
match command { match command {
EmulatorCommand::ConnectToSim(sim_id, renderer, messages) => { EmulatorCommand::ConnectToSim(sim_id, renderer, messages) => {
@@ -572,6 +628,11 @@ impl Emulator {
self.report_error(SimId::Player1, format!("Error setting speed: {error}")); self.report_error(SimId::Player1, format!("Error setting speed: {error}"));
} }
} }
EmulatorCommand::StartProfiling(sim_id, profiler) => {
if let Err(error) = self.start_profiling(sim_id, profiler) {
self.report_error(SimId::Player1, format!("Error enaling profiler: {error}"));
}
}
EmulatorCommand::StartDebugging(sim_id, debugger) => { EmulatorCommand::StartDebugging(sim_id, debugger) => {
self.start_debugging(sim_id, debugger); self.start_debugging(sim_id, debugger);
} }
@@ -709,6 +770,7 @@ pub enum EmulatorCommand {
Resume, Resume,
FrameAdvance, FrameAdvance,
SetSpeed(f64), SetSpeed(f64),
StartProfiling(SimId, ProfileSender),
StartDebugging(SimId, DebugSender), StartDebugging(SimId, DebugSender),
StopDebugging(SimId), StopDebugging(SimId),
DebugInterrupt(SimId), DebugInterrupt(SimId),
@@ -750,6 +812,7 @@ pub enum EmulatorState {
Debugging, Debugging,
} }
type ProfileSender = tokio::sync::mpsc::UnboundedSender<ProfileEvent>;
type DebugSender = tokio::sync::mpsc::UnboundedSender<DebugEvent>; type DebugSender = tokio::sync::mpsc::UnboundedSender<DebugEvent>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -773,6 +836,16 @@ pub enum DebugEvent {
Stopped(DebugStopReason), Stopped(DebugStopReason),
} }
pub enum ProfileEvent {
Start {
file_path: PathBuf,
},
Update {
cycles: u32,
event: Option<SimEvent>,
},
}
#[derive(Clone)] #[derive(Clone)]
pub struct EmulatorClient { pub struct EmulatorClient {
queue: mpsc::Sender<EmulatorCommand>, queue: mpsc::Sender<EmulatorCommand>,
+18
View File
@@ -18,6 +18,7 @@ pub struct Cart {
impl Cart { impl Cart {
pub fn load(file_path: &Path, sim_id: SimId) -> Result<Self> { pub fn load(file_path: &Path, sim_id: SimId) -> Result<Self> {
let rom = fs::read(file_path)?; let rom = fs::read(file_path)?;
let rom = try_parse_elf(&rom).unwrap_or(rom);
let mut sram_file = File::options() let mut sram_file = File::options()
.read(true) .read(true)
@@ -55,6 +56,23 @@ impl Cart {
} }
} }
fn try_parse_elf(data: &[u8]) -> Option<Vec<u8>> {
let parsed = elf::ElfBytes::<elf::endian::AnyEndian>::minimal_parse(data).ok()?;
let mut bytes = vec![];
let mut pstart = None;
for phdr in parsed.segments()? {
if phdr.p_filesz == 0 {
continue;
}
let start = pstart.unwrap_or(phdr.p_paddr);
pstart = Some(start);
bytes.resize((phdr.p_paddr - start) as usize, 0);
let data = parsed.segment_data(&phdr).ok()?;
bytes.extend_from_slice(data);
}
Some(bytes)
}
fn sram_path(file_path: &Path, sim_id: SimId) -> PathBuf { fn sram_path(file_path: &Path, sim_id: SimId) -> PathBuf {
match sim_id { match sim_id {
SimId::Player1 => file_path.with_extension("p1.sram"), SimId::Player1 => file_path.with_extension("p1.sram"),
+162 -8
View File
@@ -71,8 +71,16 @@ pub enum VBWatchpointType {
Access, Access,
} }
type OnException = extern "C" fn(sim: *mut VB, cause: *mut u16) -> c_int;
type OnExecute = type OnExecute =
extern "C" fn(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> c_int; 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 OnFrame = extern "C" fn(sim: *mut VB) -> c_int;
type OnRead = extern "C" fn( type OnRead = extern "C" fn(
sim: *mut VB, sim: *mut VB,
@@ -135,8 +143,15 @@ unsafe extern "C" {
fn vb_set_cart_ram(sim: *mut VB, sram: *mut c_void, size: u32) -> c_int; fn vb_set_cart_ram(sim: *mut VB, sram: *mut c_void, size: u32) -> c_int;
#[link_name = "vbSetCartROM"] #[link_name = "vbSetCartROM"]
fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int; 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"] #[link_name = "vbSetExecuteCallback"]
fn vb_set_execute_callback(sim: *mut VB, callback: Option<OnExecute>) -> Option<OnExecute>; 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"] #[link_name = "vbSetFrameCallback"]
fn vb_set_frame_callback(sim: *mut VB, callback: Option<OnFrame>) -> Option<OnFrame>; fn vb_set_frame_callback(sim: *mut VB, callback: Option<OnFrame>) -> Option<OnFrame>;
#[link_name = "vbSetKeys"] #[link_name = "vbSetKeys"]
@@ -180,11 +195,21 @@ extern "C" fn on_frame(sim: *mut VB) -> c_int {
} }
#[unsafe(no_mangle)] #[unsafe(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.
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.monitor.enabled {
// SAFETY: length is the length of code, in elements
let code = unsafe { slice::from_raw_parts(code, length as usize) };
if data.monitor.detect_event(sim, address, code) {
// Something interesting will happen after this instruction is run.
// The on_fetch callback will fire when it does.
unsafe { vb_set_fetch_callback(sim, Some(on_fetch)) };
}
}
let mut stopped = data.stop_reason.is_some(); let mut stopped = data.stop_reason.is_some();
if data.step_from.is_some_and(|s| s != address) { if data.step_from.is_some_and(|s| s != address) {
data.step_from = None; data.step_from = None;
@@ -199,6 +224,35 @@ extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length:
if stopped { 1 } else { 0 } 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();
unsafe { vb_set_exception_callback(sim, Some(on_exception)) };
unsafe { vb_set_fetch_callback(sim, None) };
1
}
#[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() };
data.monitor.event = data.monitor.queued_event.take();
data.monitor.queued_event = Some(SimEvent::Interrupt(unsafe { *cause }));
unsafe { vb_set_exception_callback(sim, None) };
unsafe { vb_set_fetch_callback(sim, Some(on_fetch)) };
if data.monitor.event.is_some() { 1 } else { 0 }
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
extern "C" fn on_read( extern "C" fn on_read(
sim: *mut VB, sim: *mut VB,
@@ -260,6 +314,83 @@ extern "C" fn on_write(
0 0
} }
#[allow(dead_code)]
#[derive(Debug)]
pub enum SimEvent {
Call(u32),
Return,
Interrupt(u16),
Reti,
}
struct EventMonitor {
enabled: bool,
event: Option<SimEvent>,
queued_event: Option<SimEvent>,
}
impl EventMonitor {
fn new() -> Self {
Self {
enabled: false,
event: None,
queued_event: None,
}
}
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(&self, sim: *mut VB, address: u32, code: &[u16]) -> Option<SimEvent> {
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 == 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)));
}
}
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));
}
}
if opcode == RETI_OPCODE {
return Some(SimEvent::Reti);
}
None
}
}
const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4; const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4;
const AUDIO_CAPACITY_FLOATS: usize = AUDIO_CAPACITY_SAMPLES * 2; const AUDIO_CAPACITY_FLOATS: usize = AUDIO_CAPACITY_SAMPLES * 2;
pub const EXPECTED_FRAME_SIZE: usize = 834 * 2; pub const EXPECTED_FRAME_SIZE: usize = 834 * 2;
@@ -267,6 +398,7 @@ pub const EXPECTED_FRAME_SIZE: usize = 834 * 2;
struct VBState { struct VBState {
frame_seen: bool, frame_seen: bool,
stop_reason: Option<StopReason>, stop_reason: Option<StopReason>,
monitor: EventMonitor,
step_from: Option<u32>, step_from: Option<u32>,
breakpoints: Vec<u32>, breakpoints: Vec<u32>,
read_watchpoints: AddressSet, read_watchpoints: AddressSet,
@@ -277,6 +409,7 @@ struct VBState {
impl VBState { impl VBState {
fn needs_execute_callback(&self) -> bool { fn needs_execute_callback(&self) -> bool {
self.step_from.is_some() self.step_from.is_some()
|| self.monitor.enabled
|| !self.breakpoints.is_empty() || !self.breakpoints.is_empty()
|| !self.read_watchpoints.is_empty() || !self.read_watchpoints.is_empty()
|| !self.write_watchpoints.is_empty() || !self.write_watchpoints.is_empty()
@@ -311,6 +444,7 @@ impl Sim {
let state = VBState { let state = VBState {
frame_seen: false, frame_seen: false,
stop_reason: None, stop_reason: None,
monitor: EventMonitor::new(),
step_from: None, step_from: None,
breakpoints: vec![], breakpoints: vec![],
read_watchpoints: AddressSet::new(), read_watchpoints: AddressSet::new(),
@@ -332,6 +466,23 @@ impl Sim {
unsafe { vb_reset(self.sim) }; unsafe { vb_reset(self.sim) };
} }
pub fn monitor_events(&mut self, enabled: bool) {
let state = self.get_state();
state.monitor.enabled = enabled;
state.monitor.event = None;
state.monitor.queued_event = None;
if enabled {
unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) };
unsafe { vb_set_exception_callback(self.sim, Some(on_exception)) };
} else {
if !state.needs_execute_callback() {
unsafe { vb_set_execute_callback(self.sim, None) };
}
unsafe { vb_set_exception_callback(self.sim, None) };
unsafe { vb_set_fetch_callback(self.sim, None) };
}
}
pub fn load_cart(&mut self, mut rom: Vec<u8>, mut sram: Vec<u8>) -> Result<()> { pub fn load_cart(&mut self, mut rom: Vec<u8>, mut sram: Vec<u8>) -> Result<()> {
self.unload_cart(); self.unload_cart();
@@ -394,16 +545,14 @@ impl Sim {
unsafe { vb_set_peer(self.sim, ptr::null_mut()) }; unsafe { vb_set_peer(self.sim, ptr::null_mut()) };
} }
pub fn emulate(&mut self) { pub fn emulate(&mut self, cycles: &mut u32) {
let mut cycles = 20_000_000; unsafe { vb_emulate(self.sim, cycles) };
unsafe { vb_emulate(self.sim, &mut cycles) };
} }
pub fn emulate_many(sims: &mut [Sim]) { pub fn emulate_many(sims: &mut [Sim], cycles: &mut u32) {
let mut cycles = 20_000_000;
let count = sims.len() as c_uint; let count = sims.len() as c_uint;
let sims = sims.as_mut_ptr().cast(); let sims = sims.as_mut_ptr().cast();
unsafe { vb_emulate_ex(sims, count, &mut cycles) }; unsafe { vb_emulate_ex(sims, count, cycles) };
} }
pub fn read_pixels(&mut self, buffers: &mut [u8]) -> bool { pub fn read_pixels(&mut self, buffers: &mut [u8]) -> bool {
@@ -632,7 +781,7 @@ impl Sim {
Some(string) Some(string)
} }
pub fn stop_reason(&mut self) -> Option<StopReason> { pub fn take_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();
if !data.needs_execute_callback() { if !data.needs_execute_callback() {
@@ -641,6 +790,11 @@ impl Sim {
reason reason
} }
pub fn take_sim_event(&mut self) -> Option<SimEvent> {
let data = self.get_state();
data.monitor.event.take()
}
fn get_state(&mut self) -> &mut VBState { fn get_state(&mut self) -> &mut VBState {
// 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.
+13 -1
View File
@@ -22,6 +22,7 @@ mod images;
mod input; mod input;
mod memory; mod memory;
mod persistence; mod persistence;
mod profiler;
mod window; mod window;
#[derive(Parser)] #[derive(Parser)]
@@ -31,6 +32,9 @@ struct Args {
/// Start a GDB/LLDB debug server on this port. /// Start a GDB/LLDB debug server on this port.
#[arg(short, long)] #[arg(short, long)]
debug_port: Option<u16>, debug_port: Option<u16>,
/// Enable profiling a game
#[arg(short, long)]
profile: bool,
} }
fn init_logger() { fn init_logger() {
@@ -106,6 +110,9 @@ fn main() -> Result<()> {
} }
builder = builder.start_paused(true); builder = builder.start_paused(true);
} }
if args.profile {
builder = builder.start_paused(true)
}
ThreadBuilder::default() ThreadBuilder::default()
.name("Emulator".to_owned()) .name("Emulator".to_owned())
@@ -124,6 +131,11 @@ fn main() -> Result<()> {
let event_loop = EventLoop::with_user_event().build().unwrap(); let event_loop = EventLoop::with_user_event().build().unwrap();
event_loop.set_control_flow(ControlFlow::Poll); event_loop.set_control_flow(ControlFlow::Poll);
let proxy = event_loop.create_proxy(); let proxy = event_loop.create_proxy();
event_loop.run_app(&mut Application::new(client, proxy, args.debug_port))?; event_loop.run_app(&mut Application::new(
client,
proxy,
args.debug_port,
args.profile,
))?;
Ok(()) Ok(())
} }
+1 -1
View File
@@ -169,7 +169,7 @@ impl MemoryRef<'_> {
T::from_bytes(&self.inner[from..to]) T::from_bytes(&self.inner[from..to])
} }
pub fn range<T: MemoryValue>(&self, start: usize, count: usize) -> MemoryIter<'_, T> { pub fn range<T: MemoryValue>(&self, start: usize, count: usize) -> MemoryIter<T> {
let from = start * size_of::<T>(); let from = start * size_of::<T>();
let to = from + (count * size_of::<T>()); let to = from + (count * size_of::<T>());
MemoryIter::new(&self.inner[from..to]) MemoryIter::new(&self.inner[from..to])
+79
View File
@@ -0,0 +1,79 @@
use std::{
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
thread,
};
use tokio::{select, sync::mpsc};
use crate::emulator::{EmulatorClient, EmulatorCommand, ProfileEvent, SimId};
pub struct Profiler {
sim_id: SimId,
client: EmulatorClient,
running: Arc<AtomicBool>,
killer: Option<oneshot::Sender<()>>,
}
impl Profiler {
pub fn new(sim_id: SimId, client: EmulatorClient) -> Self {
Self {
sim_id,
client,
running: Arc::new(AtomicBool::new(false)),
killer: None,
}
}
pub fn started(&self) -> bool {
self.running.load(Ordering::Relaxed)
}
pub fn start(&mut self) {
let sim_id = self.sim_id;
let client = self.client.clone();
let running = self.running.clone();
let (tx, rx) = oneshot::channel();
self.killer = Some(tx);
thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async move {
select! {
_ = run_profile(sim_id, client, running.clone()) => {}
_ = rx => {
running.store(false, Ordering::Relaxed);
}
}
})
});
}
pub fn stop(&mut self) {
if let Some(killer) = self.killer.take() {
let _ = killer.send(());
}
}
}
async fn run_profile(sim_id: SimId, client: EmulatorClient, running: Arc<AtomicBool>) {
let (profile_sync, mut profile_source) = mpsc::unbounded_channel();
client.send_command(EmulatorCommand::StartProfiling(sim_id, profile_sync));
running.store(true, Ordering::Relaxed);
while let Some(event) = profile_source.recv().await {
match event {
ProfileEvent::Start { file_path } => {
println!("profiling {}", file_path.display());
}
ProfileEvent::Update { cycles, event } => {
println!("update {cycles} {event:#x?}");
}
}
}
running.store(false, Ordering::Release);
}
+2
View File
@@ -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 profile::ProfileWindow;
pub use terminal::TerminalWindow; pub use terminal::TerminalWindow;
pub use vip::{ pub use vip::{
BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow,
@@ -18,6 +19,7 @@ mod game_screen;
mod gdb; mod gdb;
mod hotkeys; mod hotkeys;
mod input; mod input;
mod profile;
mod terminal; mod terminal;
mod utils; mod utils;
mod vip; mod vip;
+6 -1
View File
@@ -228,6 +228,11 @@ impl GameWindow {
.send_event(UserEvent::OpenTerminal(self.sim_id)) .send_event(UserEvent::OpenTerminal(self.sim_id))
.unwrap(); .unwrap();
} }
if ui.button("Profiler").clicked() {
self.proxy
.send_event(UserEvent::OpenProfiler(self.sim_id))
.unwrap();
}
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))
@@ -457,7 +462,7 @@ impl GameWindow {
self.config = new_config; self.config = new_config;
} }
fn button_for(&self, ctx: &Context, text: &str, command: Command) -> Button<'_> { fn button_for(&self, ctx: &Context, text: &str, command: Command) -> Button {
let button = Button::new(text); let button = Button::new(text);
match self.shortcuts.shortcut_for(command) { match self.shortcuts.shortcut_for(command) {
Some(shortcut) => button.shortcut_text(ctx.format_shortcut(&shortcut)), Some(shortcut) => button.shortcut_text(ctx.format_shortcut(&shortcut)),
+4 -4
View File
@@ -62,8 +62,8 @@ impl HotkeysWindow {
} }
}); });
}); });
if let Some(command) = self.now_binding if let Some(command) = self.now_binding {
&& let Some(shortcut) = ui.input_mut(|i| { if let Some(shortcut) = ui.input_mut(|i| {
i.events.iter().find_map(|event| match event { i.events.iter().find_map(|event| match event {
Event::Key { Event::Key {
key, key,
@@ -73,12 +73,12 @@ impl HotkeysWindow {
} => Some(KeyboardShortcut::new(*modifiers, *key)), } => Some(KeyboardShortcut::new(*modifiers, *key)),
_ => None, _ => None,
}) })
}) }) {
{
self.shortcuts.set(command, shortcut); self.shortcuts.set(command, shortcut);
self.now_binding = None; self.now_binding = None;
} }
} }
}
fn show_ff_settings(&mut self, ui: &mut Ui) { fn show_ff_settings(&mut self, ui: &mut Ui) {
let row_height = ui.spacing().interact_size.y; let row_height = ui.spacing().interact_size.y;
+54
View File
@@ -0,0 +1,54 @@
use egui::{CentralPanel, ViewportBuilder, ViewportId};
use crate::{
emulator::{EmulatorClient, SimId},
profiler::Profiler,
window::AppWindow,
};
pub struct ProfileWindow {
sim_id: SimId,
profiler: Profiler,
}
impl ProfileWindow {
pub fn new(sim_id: SimId, client: EmulatorClient) -> Self {
Self {
sim_id,
profiler: Profiler::new(sim_id, client),
}
}
pub fn launch(&mut self) {
self.profiler.start();
}
}
impl AppWindow for ProfileWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of(format!("Profile-{}", self.sim_id))
}
fn sim_id(&self) -> SimId {
self.sim_id
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("Profiler ({})", self.sim_id))
.with_inner_size((300.0, 200.0))
}
fn show(&mut self, ctx: &egui::Context) {
CentralPanel::default().show(ctx, |ui| {
let mut started = self.profiler.started();
if ui.checkbox(&mut started, "Profiling enabled?").changed() {
if started {
self.profiler.start();
} else {
self.profiler.stop();
}
}
});
}
}
+7 -7
View File
@@ -86,11 +86,11 @@ impl UiExt for Ui {
let (rect, _) = ui.allocate_at_least(Vec2::new(100.0, 100.0), Sense::hover()); let (rect, _) = ui.allocate_at_least(Vec2::new(100.0, 100.0), Sense::hover());
ui.painter().rect_filled(rect, 0.0, *color); ui.painter().rect_filled(rect, 0.0, *color);
let resp = ui.text_edit_singleline(hex); let resp = ui.text_edit_singleline(hex);
if resp.changed() if resp.changed() {
&& let Ok(new_color) = HexColor::from_str_without_hash(hex) if let Ok(new_color) = HexColor::from_str_without_hash(hex) {
{
*color = new_color.color(); *color = new_color.color();
} }
}
resp resp
}, },
) )
@@ -301,11 +301,11 @@ impl<T: Number> Widget for NumberEdit<'_, T> {
let mut delta = None; let mut delta = None;
if self.arrows { if self.arrows {
let arrow_left = res.rect.max.x - 16.0; let arrow_left = res.rect.max.x + 4.0;
let arrow_right = res.rect.max.x; let arrow_right = res.rect.max.x + 20.0;
let arrow_top = res.rect.min.y; let arrow_top = res.rect.min.y - 2.0;
let arrow_middle = (res.rect.min.y + res.rect.max.y) / 2.0; let arrow_middle = (res.rect.min.y + res.rect.max.y) / 2.0;
let arrow_bottom = res.rect.max.y; let arrow_bottom = res.rect.max.y + 2.0;
let top_arrow_rect = Rect { let top_arrow_rect = Rect {
min: (arrow_left, arrow_top).into(), min: (arrow_left, arrow_top).into(),