Compare commits

..

No commits in common. "main" and "v0.2.6" have entirely different histories.
main ... v0.2.6

19 changed files with 140 additions and 2361 deletions

226
Cargo.lock generated
View File

@ -28,15 +28,6 @@ dependencies = [
"serde",
]
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
@ -397,24 +388,6 @@ dependencies = [
"syn",
]
[[package]]
name = "atoi"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
dependencies = [
"num-traits",
]
[[package]]
name = "atomic"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994"
dependencies = [
"bytemuck",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@ -427,21 +400,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "backtrace"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets 0.52.6",
]
[[package]]
name = "bindgen"
version = "0.70.1"
@ -1348,12 +1306,6 @@ dependencies = [
"windows 0.58.0",
]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "gl_generator"
version = "0.14.0"
@ -1755,19 +1707,11 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lemur"
version = "0.3.0"
version = "0.2.6"
dependencies = [
"anyhow",
"atoi",
"atomic",
"bitflags 2.6.0",
"bytemuck",
"cc",
@ -1780,7 +1724,6 @@ dependencies = [
"egui-winit",
"egui_extras",
"gilrs",
"hex",
"image",
"itertools",
"num-derive",
@ -1793,9 +1736,6 @@ dependencies = [
"serde",
"serde_json",
"thread-priority",
"tokio",
"tracing",
"tracing-subscriber",
"wgpu",
"windows 0.58.0",
"winit",
@ -1891,15 +1831,6 @@ dependencies = [
"libc",
]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]]
name = "memchr"
version = "2.7.4"
@ -1971,17 +1902,6 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "mio"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"wasi",
"windows-sys 0.52.0",
]
[[package]]
name = "naga"
version = "23.1.0"
@ -2085,16 +2005,6 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-complex"
version = "0.4.6"
@ -2366,15 +2276,6 @@ dependencies = [
"objc2-foundation",
]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]]
name = "oboe"
version = "0.6.1"
@ -2435,12 +2336,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "owned_ttf_parser"
version = "0.25.0"
@ -2734,17 +2629,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
"regex-automata",
"regex-syntax",
]
[[package]]
@ -2755,15 +2641,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.5",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
@ -2816,12 +2696,6 @@ dependencies = [
"realfft",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "1.1.0"
@ -2954,15 +2828,6 @@ dependencies = [
"serde",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
@ -3053,16 +2918,6 @@ dependencies = [
"serde",
]
[[package]]
name = "socket2"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "spirv"
version = "0.3.0+sdk-1.3.268.0"
@ -3180,16 +3035,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "tiny-skia"
version = "0.11.4"
@ -3225,33 +3070,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "tokio"
version = "1.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"pin-project-lite",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-macros"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "toml"
version = "0.8.19"
@ -3315,36 +3133,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
@ -3455,12 +3243,6 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vec_map"
version = "0.8.2"

View File

@ -4,13 +4,11 @@ description = "An emulator for the Virtual Boy."
repository = "https://git.virtual-boy.com/PVB/lemur"
publish = false
license = "MIT"
version = "0.3.0"
version = "0.2.6"
edition = "2021"
[dependencies]
anyhow = "1"
atoi = "2"
atomic = "0.6"
bitflags = { version = "2", features = ["serde"] }
bytemuck = { version = "1", features = ["derive"] }
clap = { version = "4", features = ["derive"] }
@ -22,7 +20,6 @@ egui-toast = { git = "https://github.com/urholaukkarinen/egui-toast.git", rev =
egui-winit = "0.30"
egui-wgpu = { version = "0.30", features = ["winit"] }
gilrs = { version = "0.11", features = ["serde-serialize"] }
hex = "0.4"
image = { version = "0.25", default-features = false, features = ["png"] }
itertools = "0.13"
num-derive = "0.4"
@ -35,9 +32,6 @@ rubato = "0.16"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thread-priority = "1"
tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync"] }
tracing = { version = "0.1", features = ["release_max_level_info"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
wgpu = "23"
winit = { version = "0.30", features = ["serde"] }

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` (via [rustup](https://rustup.rs/), the version from your package manager is too old)
- `cargo`
Run
```sh

View File

@ -23,10 +23,6 @@ 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" \
@ -34,4 +30,5 @@ ENV PATH="/osxcross/bin:$PATH" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-19" \
CROSS_COMPILE="setting-this-to-silence-a-warning-" \
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"

View File

@ -8,25 +8,13 @@ fn main() -> Result<(), Box<dyn Error>> {
}
println!("cargo::rerun-if-changed=shrooms-vb-core");
let mut builder = cc::Build::new();
let _ = builder.try_flags_from_environment("SHROOMS_CFLAGS");
let opt_level = if builder.get_compiler().is_like_msvc() {
2
} else {
3
};
builder
cc::Build::new()
.include(Path::new("shrooms-vb-core/core"))
.opt_level(opt_level)
.opt_level(2)
.flag_if_supported("-fno-strict-aliasing")
.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");

@ -1 +1 @@
Subproject commit 155a3aa678ee0c65ed8703bccc48d36f81da1db5
Subproject commit 57dcd8370a885541ae6a1de0f35b25675728b226

View File

@ -6,7 +6,6 @@ use egui::{
ViewportCommand, ViewportId, ViewportInfo,
};
use gilrs::{EventType, Gilrs};
use tracing::{error, warn};
use winit::{
application::ApplicationHandler,
event::WindowEvent,
@ -19,7 +18,7 @@ use crate::{
emulator::{EmulatorClient, EmulatorCommand, SimId},
input::MappingProvider,
persistence::Persistence,
window::{AboutWindow, AppWindow, GameWindow, GdbServerWindow, InputWindow},
window::{AboutWindow, AppWindow, GameWindow, InputWindow},
};
fn load_icon() -> anyhow::Result<IconData> {
@ -42,15 +41,10 @@ pub struct Application {
persistence: Persistence,
viewports: HashMap<ViewportId, Viewport>,
focused: Option<ViewportId>,
init_debug_port: Option<u16>,
}
impl Application {
pub fn new(
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
debug_port: Option<u16>,
) -> Self {
pub fn new(client: EmulatorClient, proxy: EventLoopProxy<UserEvent>) -> Self {
let icon = load_icon().ok().map(Arc::new);
let persistence = Persistence::new();
let mappings = MappingProvider::new(persistence.clone());
@ -69,7 +63,6 @@ impl Application {
persistence,
viewports: HashMap::new(),
focused: None,
init_debug_port: debug_port,
}
}
@ -87,19 +80,15 @@ impl Application {
impl ApplicationHandler<UserEvent> for Application {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if let Some(port) = self.init_debug_port {
let mut server =
GdbServerWindow::new(SimId::Player1, self.client.clone(), self.proxy.clone());
server.launch(port);
self.open(event_loop, Box::new(server));
}
let app = GameWindow::new(
self.client.clone(),
self.proxy.clone(),
self.persistence.clone(),
SimId::Player1,
);
self.open(event_loop, Box::new(app));
let wrapper = Viewport::new(event_loop, self.icon.clone(), Box::new(app));
self.focused = Some(wrapper.id());
self.viewports.insert(wrapper.id(), wrapper);
}
fn window_event(
@ -194,11 +183,6 @@ impl ApplicationHandler<UserEvent> for Application {
let about = AboutWindow;
self.open(event_loop, Box::new(about));
}
UserEvent::OpenDebugger(sim_id) => {
let debugger =
GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone());
self.open(event_loop, Box::new(debugger));
}
UserEvent::OpenInput => {
let input = InputWindow::new(self.mappings.clone());
self.open(event_loop, Box::new(input));
@ -212,13 +196,6 @@ impl ApplicationHandler<UserEvent> for Application {
);
self.open(event_loop, Box::new(p2));
}
UserEvent::Quit(sim_id) => {
self.viewports
.retain(|_, viewport| viewport.app.sim_id() != sim_id);
if !self.viewports.contains_key(&ViewportId::ROOT) {
event_loop.exit();
}
}
}
}
@ -231,8 +208,8 @@ impl ApplicationHandler<UserEvent> for Application {
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
let (sender, receiver) = oneshot::channel();
if self.client.send_command(EmulatorCommand::Exit(sender)) {
if let Err(error) = receiver.recv_timeout(Duration::from_secs(5)) {
error!(%error, "could not gracefully exit.");
if let Err(err) = receiver.recv_timeout(Duration::from_secs(5)) {
eprintln!("could not gracefully exit: {}", err);
}
}
}
@ -274,23 +251,17 @@ impl Viewport {
});
egui_extras::install_image_loaders(&ctx);
#[allow(unused_mut)]
let mut wgpu_config = egui_wgpu::WgpuConfiguration {
let mut painter = egui_wgpu::winit::Painter::new(
ctx.clone(),
egui_wgpu::WgpuConfiguration {
present_mode: wgpu::PresentMode::AutoNoVsync,
..egui_wgpu::WgpuConfiguration::default()
};
#[cfg(windows)]
{
if let egui_wgpu::WgpuSetup::CreateNew {
supported_backends, ..
} = &mut wgpu_config.wgpu_setup
{
*supported_backends -= wgpu::Backends::VULKAN;
}
}
let mut painter =
egui_wgpu::winit::Painter::new(ctx.clone(), wgpu_config, 1, None, false, true);
},
1,
None,
false,
true,
);
let mut info = ViewportInfo::default();
let mut builder = app.initial_viewport();
@ -403,10 +374,8 @@ impl Drop for Viewport {
pub enum UserEvent {
GamepadEvent(gilrs::Event),
OpenAbout,
OpenDebugger(SimId),
OpenInput,
OpenPlayer2,
Quit(SimId),
}
pub enum Action {
@ -435,12 +404,9 @@ fn create_window_and_state(
}
fn process_gamepad_input(mappings: MappingProvider, proxy: EventLoopProxy<UserEvent>) {
let mut gilrs = match Gilrs::new() {
Ok(gilrs) => gilrs,
Err(error) => {
warn!(%error, "could not connect gamepad listener");
let Ok(mut gilrs) = Gilrs::new() else {
eprintln!("could not connect gamepad listener");
return;
}
};
while let Some(event) = gilrs.next_event_blocking(None) {
if event.event == EventType::Connected {

View File

@ -4,7 +4,6 @@ use anyhow::{bail, Result};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use itertools::Itertools;
use rubato::{FftFixedInOut, Resampler};
use tracing::error;
pub struct Audio {
#[allow(unused)]
@ -55,7 +54,7 @@ impl Audio {
}
chunk.commit_all();
},
move |error| error!(%error, "stream error"),
move |err| eprintln!("stream error: {err}"),
None,
)?;
stream.play()?;

View File

@ -1,29 +1,34 @@
use std::{
collections::HashMap,
fmt::Display,
fs::{self, File},
io::{Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
atomic::{AtomicBool, AtomicUsize, Ordering},
mpsc::{self, RecvError, TryRecvError},
Arc,
},
};
use anyhow::Result;
use atomic::Atomic;
use bytemuck::NoUninit;
use egui_toast::{Toast, ToastKind, ToastOptions};
use tracing::{error, warn};
use crate::{audio::Audio, graphics::TextureSink};
use shrooms_vb_core::{Sim, StopReason, EXPECTED_FRAME_SIZE};
pub use shrooms_vb_core::{VBKey, VBRegister, VBWatchpointType};
pub use shrooms_vb_core::VBKey;
use shrooms_vb_core::{Sim, EXPECTED_FRAME_SIZE};
mod address_set;
mod shrooms_vb_core;
pub struct EmulatorBuilder {
rom: Option<PathBuf>,
commands: mpsc::Receiver<EmulatorCommand>,
sim_count: Arc<AtomicUsize>,
running: Arc<[AtomicBool; 2]>,
has_game: Arc<[AtomicBool; 2]>,
audio_on: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
}
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub enum SimId {
Player1,
@ -40,14 +45,6 @@ impl SimId {
}
}
}
impl Display for SimId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Player1 => "Player 1",
Self::Player2 => "Player 2",
})
}
}
struct Cart {
rom_path: PathBuf,
@ -86,35 +83,23 @@ fn sram_path(rom_path: &Path, sim_id: SimId) -> PathBuf {
}
}
pub struct EmulatorBuilder {
rom: Option<PathBuf>,
commands: mpsc::Receiver<EmulatorCommand>,
sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
start_paused: bool,
}
impl EmulatorBuilder {
pub fn new() -> (Self, EmulatorClient) {
let (queue, commands) = mpsc::channel();
let builder = Self {
rom: None,
commands,
sim_state: Arc::new([
Atomic::new(SimState::Uninitialized),
Atomic::new(SimState::Uninitialized),
]),
state: Arc::new(Atomic::new(EmulatorState::Paused)),
sim_count: Arc::new(AtomicUsize::new(0)),
running: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]),
has_game: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]),
audio_on: Arc::new([AtomicBool::new(true), AtomicBool::new(true)]),
linked: Arc::new(AtomicBool::new(false)),
start_paused: false,
};
let client = EmulatorClient {
queue,
sim_state: builder.sim_state.clone(),
state: builder.state.clone(),
sim_count: builder.sim_count.clone(),
running: builder.running.clone(),
has_game: builder.has_game.clone(),
audio_on: builder.audio_on.clone(),
linked: builder.linked.clone(),
};
@ -128,27 +113,18 @@ impl EmulatorBuilder {
}
}
pub fn start_paused(self, paused: bool) -> Self {
Self {
start_paused: paused,
..self
}
}
pub fn build(self) -> Result<Emulator> {
let mut emulator = Emulator::new(
self.commands,
self.sim_state,
self.state,
self.sim_count,
self.running,
self.has_game,
self.audio_on,
self.linked,
)?;
if let Some(path) = self.rom {
emulator.load_cart(SimId::Player1, &path)?;
}
if self.start_paused {
emulator.pause_sims()?;
}
Ok(emulator)
}
}
@ -158,13 +134,13 @@ pub struct Emulator {
carts: [Option<Cart>; 2],
audio: Audio,
commands: mpsc::Receiver<EmulatorCommand>,
sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>,
sim_count: Arc<AtomicUsize>,
running: Arc<[AtomicBool; 2]>,
has_game: Arc<[AtomicBool; 2]>,
audio_on: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
renderers: HashMap<SimId, TextureSink>,
messages: HashMap<SimId, mpsc::Sender<Toast>>,
debuggers: HashMap<SimId, DebugInfo>,
eye_contents: Vec<u8>,
audio_samples: Vec<f32>,
}
@ -172,8 +148,9 @@ pub struct Emulator {
impl Emulator {
fn new(
commands: mpsc::Receiver<EmulatorCommand>,
sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>,
sim_count: Arc<AtomicUsize>,
running: Arc<[AtomicBool; 2]>,
has_game: Arc<[AtomicBool; 2]>,
audio_on: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
) -> Result<Self> {
@ -182,15 +159,15 @@ impl Emulator {
carts: [None, None],
audio: Audio::init()?,
commands,
sim_state,
state,
sim_count,
running,
has_game,
audio_on,
linked,
renderers: HashMap::new(),
messages: HashMap::new(),
debuggers: HashMap::new(),
eye_contents: vec![0u8; 384 * 224 * 2],
audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE),
audio_samples: vec![0.0; EXPECTED_FRAME_SIZE],
})
}
@ -221,17 +198,17 @@ impl Emulator {
let index = sim_id.to_index();
while self.sims.len() <= index {
self.sims.push(Sim::new());
self.sim_state[index].store(SimState::NoGame, Ordering::Release);
}
self.sim_count.store(self.sims.len(), Ordering::Relaxed);
let sim = &mut self.sims[index];
sim.reset();
if let Some(cart) = new_cart {
sim.load_cart(cart.rom.clone(), cart.sram.clone())?;
self.carts[index] = Some(cart);
self.sim_state[index].store(SimState::Ready, Ordering::Release);
self.has_game[index].store(true, Ordering::Release);
}
if self.sim_state[index].load(Ordering::Acquire) == SimState::Ready {
self.resume_sims();
if self.has_game[index].load(Ordering::Acquire) {
self.running[index].store(true, Ordering::Release);
}
Ok(())
}
@ -256,31 +233,9 @@ impl Emulator {
self.linked.store(false, Ordering::Release);
}
fn pause_sims(&mut self) -> Result<()> {
if self
.state
.compare_exchange(
EmulatorState::Running,
EmulatorState::Paused,
Ordering::AcqRel,
Ordering::Relaxed,
)
.is_ok()
{
for sim_id in SimId::values() {
self.save_sram(sim_id)?;
}
}
Ok(())
}
fn resume_sims(&mut self) {
let _ = self.state.compare_exchange(
EmulatorState::Paused,
EmulatorState::Running,
Ordering::AcqRel,
Ordering::Relaxed,
);
pub fn pause_sim(&mut self, sim_id: SimId) -> Result<()> {
self.running[sim_id.to_index()].store(false, Ordering::Release);
self.save_sram(sim_id)
}
fn save_sram(&mut self, sim_id: SimId) -> Result<()> {
@ -298,75 +253,13 @@ impl Emulator {
self.save_sram(SimId::Player2)?;
self.renderers.remove(&SimId::Player2);
self.sims.truncate(1);
self.sim_state[SimId::Player2.to_index()].store(SimState::Uninitialized, Ordering::Release);
self.stop_debugging(SimId::Player2);
self.sim_count.store(self.sims.len(), Ordering::Relaxed);
self.running[SimId::Player2.to_index()].store(false, Ordering::Release);
self.has_game[SimId::Player2.to_index()].store(false, Ordering::Release);
self.linked.store(false, Ordering::Release);
Ok(())
}
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::Paused),
};
self.debuggers.insert(sim_id, debug);
self.state
.store(EmulatorState::Debugging, Ordering::Release);
}
fn stop_debugging(&mut self, sim_id: SimId) {
if let Some(sim) = self.sims.get_mut(sim_id.to_index()) {
sim.clear_debug_state();
}
self.debuggers.remove(&sim_id);
if self.debuggers.is_empty() {
let _ = self.state.compare_exchange(
EmulatorState::Debugging,
EmulatorState::Running,
Ordering::AcqRel,
Ordering::Relaxed,
);
}
}
fn debug_interrupt(&mut self, sim_id: SimId) {
self.debug_stop(sim_id, DebugStopReason::Paused);
}
fn debug_stop(&mut self, sim_id: SimId, reason: DebugStopReason) {
let Some(debugger) = self.debuggers.get_mut(&sim_id) else {
self.stop_debugging(sim_id);
return;
};
if debugger.stop_reason != Some(reason) {
debugger.stop_reason = Some(reason);
if debugger.sender.send(DebugEvent::Stopped(reason)).is_err() {
self.stop_debugging(sim_id);
}
}
}
fn debug_continue(&mut self, sim_id: SimId) -> bool {
let Some(debugger) = self.debuggers.get_mut(&sim_id) else {
self.stop_debugging(sim_id);
return false;
};
debugger.stop_reason = None;
true
}
fn debug_step(&mut self, sim_id: SimId) {
if self.debug_continue(sim_id) {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.step();
}
}
pub fn run(&mut self) {
loop {
let idle = self.tick();
@ -396,20 +289,9 @@ impl Emulator {
// returns true if the emulator is "idle" (i.e. this didn't output anything)
pub fn tick(&mut self) -> bool {
let p1_state = self.sim_state[SimId::Player1.to_index()].load(Ordering::Acquire);
let p2_state = self.sim_state[SimId::Player2.to_index()].load(Ordering::Acquire);
let state = self.state.load(Ordering::Acquire);
// Emulation
// Don't emulate if the state is "paused", or if any sim is paused in the debugger
let running = match state {
EmulatorState::Paused => false,
EmulatorState::Running => true,
EmulatorState::Debugging => self.debuggers.values().all(|d| d.stop_reason.is_none()),
};
let p1_running = running && p1_state == SimState::Ready;
let p2_running = running && p2_state == SimState::Ready;
let mut idle = !p1_running && !p2_running;
let p1_running = self.running[SimId::Player1.to_index()].load(Ordering::Acquire);
let p2_running = self.running[SimId::Player2.to_index()].load(Ordering::Acquire);
let mut idle = p1_running || p2_running;
if p1_running && p2_running {
Sim::emulate_many(&mut self.sims);
} else if p1_running {
@ -418,26 +300,6 @@ impl Emulator {
self.sims[SimId::Player2.to_index()].emulate();
}
// Debug state
if state == EmulatorState::Debugging {
for sim_id in SimId::values() {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
continue;
};
if let Some(reason) = sim.stop_reason() {
let stop_reason = match reason {
StopReason::Stepped => DebugStopReason::Trace,
StopReason::Watchpoint(watch, address) => {
DebugStopReason::Watchpoint(watch, address)
}
StopReason::Breakpoint => DebugStopReason::Breakpoint,
};
self.debug_stop(sim_id, stop_reason);
}
}
}
// Video
for sim_id in SimId::values() {
let Some(renderer) = self.renderers.get_mut(&sim_id) else {
continue;
@ -452,27 +314,24 @@ impl Emulator {
}
}
}
// Audio
// Audio playback speed is how we keep the emulator running in real time.
// Even if we're muted, call `read_samples` to know how many frames of silence to play.
let p1_audio =
p1_running && self.audio_on[SimId::Player1.to_index()].load(Ordering::Acquire);
let p2_audio =
p2_running && self.audio_on[SimId::Player2.to_index()].load(Ordering::Acquire);
let (p1_weight, p2_weight) = match (p1_audio, p2_audio) {
(true, true) => (0.5, 0.5),
(true, false) => (1.0, 0.0),
(false, true) => (0.0, 1.0),
(false, false) => (0.0, 0.0),
};
let weight = if p1_audio && p2_audio { 0.5 } else { 1.0 };
if p1_audio {
if let Some(sim) = self.sims.get_mut(SimId::Player1.to_index()) {
sim.read_samples(&mut self.audio_samples, p1_weight);
sim.read_samples(&mut self.audio_samples, weight);
}
}
if p2_audio {
if let Some(sim) = self.sims.get_mut(SimId::Player2.to_index()) {
sim.read_samples(&mut self.audio_samples, p2_weight);
sim.read_samples(&mut self.audio_samples, weight);
}
if !self.audio_samples.is_empty() {
}
if self.audio_samples.is_empty() {
self.audio_samples.resize(EXPECTED_FRAME_SIZE, 0.0);
} else {
idle = false;
}
self.audio.update(&self.audio_samples);
@ -508,78 +367,19 @@ impl Emulator {
}
}
EmulatorCommand::Pause => {
if let Err(error) = self.pause_sims() {
self.report_error(SimId::Player1, format!("Error pausing: {error}"));
for sim_id in SimId::values() {
if let Err(error) = self.pause_sim(sim_id) {
self.report_error(sim_id, format!("Error pausing: {error}"));
}
}
}
EmulatorCommand::Resume => {
self.resume_sims();
for sim_id in SimId::values() {
let index = sim_id.to_index();
if self.has_game[index].load(Ordering::Acquire) {
self.running[index].store(true, Ordering::Relaxed);
}
EmulatorCommand::StartDebugging(sim_id, debugger) => {
self.start_debugging(sim_id, debugger);
}
EmulatorCommand::StopDebugging(sim_id) => {
self.stop_debugging(sim_id);
}
EmulatorCommand::DebugInterrupt(sim_id) => {
self.debug_interrupt(sim_id);
}
EmulatorCommand::DebugContinue(sim_id) => {
self.debug_continue(sim_id);
}
EmulatorCommand::DebugStep(sim_id) => {
self.debug_step(sim_id);
}
EmulatorCommand::ReadRegister(sim_id, register, done) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
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;
};
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;
};
sim.add_breakpoint(address);
}
EmulatorCommand::RemoveBreakpoint(sim_id, address) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.remove_breakpoint(address);
}
EmulatorCommand::AddWatchpoint(sim_id, address, length, watch) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.add_watchpoint(address, length, watch);
}
EmulatorCommand::RemoveWatchpoint(sim_id, address, length, watch) => {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.remove_watchpoint(address, length, watch);
}
EmulatorCommand::SetAudioEnabled(p1, p2) => {
self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release);
@ -626,7 +426,7 @@ impl Emulator {
return;
}
}
error!("{}", message);
eprintln!("{}", message);
}
}
@ -638,19 +438,6 @@ pub enum EmulatorCommand {
StopSecondSim,
Pause,
Resume,
StartDebugging(SimId, DebugSender),
StopDebugging(SimId),
DebugInterrupt(SimId),
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),
RemoveWatchpoint(SimId, u32, usize, VBWatchpointType),
SetAudioEnabled(bool, bool),
Link,
Unlink,
@ -659,72 +446,37 @@ pub enum EmulatorCommand {
Exit(oneshot::Sender<()>),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, NoUninit)]
#[repr(usize)]
pub enum SimState {
Uninitialized,
NoGame,
Ready,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, NoUninit)]
#[repr(usize)]
pub enum EmulatorState {
Paused,
Running,
Debugging,
}
type DebugSender = tokio::sync::mpsc::UnboundedSender<DebugEvent>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DebugStopReason {
// We are stepping
Trace,
// We hit a breakpoint
Breakpoint,
// We hit a watchpoint
Watchpoint(VBWatchpointType, u32),
// The debugger told us to pause
Paused,
}
struct DebugInfo {
sender: DebugSender,
stop_reason: Option<DebugStopReason>,
}
pub enum DebugEvent {
Stopped(DebugStopReason),
}
#[derive(Clone)]
pub struct EmulatorClient {
queue: mpsc::Sender<EmulatorCommand>,
sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>,
sim_count: Arc<AtomicUsize>,
running: Arc<[AtomicBool; 2]>,
has_game: Arc<[AtomicBool; 2]>,
audio_on: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
}
impl EmulatorClient {
pub fn sim_state(&self, sim_id: SimId) -> SimState {
self.sim_state[sim_id.to_index()].load(Ordering::Acquire)
pub fn has_player_2(&self) -> bool {
self.sim_count.load(Ordering::Acquire) == 2
}
pub fn emulator_state(&self) -> EmulatorState {
self.state.load(Ordering::Acquire)
pub fn is_running(&self, sim_id: SimId) -> bool {
self.running[sim_id.to_index()].load(Ordering::Acquire)
}
pub fn is_audio_enabled(&self, sim_id: SimId) -> bool {
self.audio_on[sim_id.to_index()].load(Ordering::Acquire)
pub fn has_game(&self, sim_id: SimId) -> bool {
self.has_game[sim_id.to_index()].load(Ordering::Acquire)
}
pub fn are_sims_linked(&self) -> bool {
self.linked.load(Ordering::Acquire)
}
pub fn is_audio_enabled(&self, sim_id: SimId) -> bool {
self.audio_on[sim_id.to_index()].load(Ordering::Acquire)
}
pub fn send_command(&self, command: EmulatorCommand) -> bool {
match self.queue.send(command) {
Ok(()) => true,
Err(err) => {
warn!(
eprintln!(
"could not send command {:?} as emulator is shut down",
err.0
);

View File

@ -1,270 +0,0 @@
use std::{
collections::{BTreeMap, BTreeSet},
ops::Bound,
};
#[derive(Debug, Default)]
pub struct AddressSet {
ranges: BTreeSet<(u32, usize)>,
bounds: BTreeMap<u32, usize>,
}
impl AddressSet {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, address: u32, length: usize) {
if length == 0 || !self.ranges.insert((address, length)) {
return;
}
let end = (address as usize)
.checked_add(length)
.and_then(|e| u32::try_from(e).ok());
if let Some(end) = end {
let val_before = self.bounds.range(..=end).next_back().map_or(0, |(_, &v)| v);
self.bounds.insert(end, val_before);
}
let val_before = self
.bounds
.range(..address)
.next_back()
.map_or(0, |(_, &v)| v);
if let Some(&val_at) = self.bounds.get(&address) {
if val_before == val_at + 1 {
self.bounds.remove(&address);
}
} else {
self.bounds.insert(address, val_before);
}
let start_bound = Bound::Included(address);
let end_bound = match end {
Some(e) => Bound::Excluded(e),
None => Bound::Unbounded,
};
for (_, val) in self.bounds.range_mut((start_bound, end_bound)) {
*val += 1;
}
}
pub fn remove(&mut self, address: u32, length: usize) {
if !self.ranges.remove(&(address, length)) {
return;
}
let end = (address as usize)
.checked_add(length)
.and_then(|e| u32::try_from(e).ok());
if let Some(end) = end {
let val_before = self.bounds.range(..end).next_back().map_or(0, |(_, &v)| v);
if let Some(&val_at) = self.bounds.get(&end) {
if val_at + 1 == val_before {
self.bounds.remove(&end);
}
} else {
self.bounds.insert(end, val_before);
}
}
let val_before = self
.bounds
.range(..address)
.next_back()
.map_or(0, |(_, &v)| v);
if let Some(&val_at) = self.bounds.get(&address) {
if val_before + 1 == val_at {
self.bounds.remove(&address);
}
} else {
self.bounds.insert(address, val_before);
}
let start_bound = Bound::Included(address);
let end_bound = match end {
Some(e) => Bound::Excluded(e),
None => Bound::Unbounded,
};
for (_, val) in self.bounds.range_mut((start_bound, end_bound)) {
*val -= 1;
}
}
pub fn clear(&mut self) {
self.ranges.clear();
self.bounds.clear();
}
pub fn is_empty(&self) -> bool {
self.bounds.is_empty()
}
pub fn contains(&self, address: u32) -> bool {
self.bounds
.range(..=address)
.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)]
mod tests {
use super::AddressSet;
#[test]
fn should_not_include_addresses_when_empty() {
let set = AddressSet::new();
assert!(set.is_empty());
assert!(!set.contains(0x13374200));
assert_eq!(set.start_of_range_containing(0x13374200), None);
}
#[test]
fn should_include_addresses_when_full() {
let mut set = AddressSet::new();
set.add(0x00000000, 0x100000000);
assert!(set.contains(0x13374200));
assert_eq!(set.start_of_range_containing(0x13374200), Some(0x00000000));
}
#[test]
fn should_ignore_empty_address_ranges() {
let mut set = AddressSet::new();
set.add(0x13374200, 0);
assert!(set.is_empty());
assert!(!set.contains(0x13374200));
}
#[test]
fn should_add_addresses_idempotently() {
let mut set = AddressSet::new();
set.add(0x13374200, 1);
set.add(0x13374200, 1);
set.remove(0x13374200, 1);
assert!(set.is_empty());
assert!(!set.contains(0x13374200));
}
#[test]
fn should_remove_addresses_idempotently() {
let mut set = AddressSet::new();
set.add(0x13374200, 1);
set.remove(0x13374200, 1);
set.remove(0x13374200, 1);
assert!(set.is_empty());
assert!(!set.contains(0x13374200));
}
#[test]
fn should_report_address_in_range() {
let mut set = AddressSet::new();
set.add(0x13374200, 4);
assert!(!set.contains(0x133741ff));
for address in 0x13374200..0x13374204 {
assert!(set.contains(address));
}
assert!(!set.contains(0x13374204));
}
#[test]
fn should_allow_overlapping_addresses() {
let mut set = AddressSet::new();
set.add(0x13374200, 4);
set.add(0x13374201, 1);
set.add(0x13374202, 2);
assert!(!set.contains(0x133741ff));
for address in 0x13374200..0x13374204 {
assert!(set.contains(address));
}
assert!(!set.contains(0x13374204));
set.remove(0x13374200, 4);
assert!(!set.contains(0x13374200));
for address in 0x13374201..0x13374204 {
assert!(set.contains(address));
}
assert!(!set.contains(0x13374204));
}
#[test]
fn should_allow_removing_overlapped_address_ranges() {
let mut set = AddressSet::new();
set.add(0x13374200, 8);
set.add(0x13374204, 8);
set.remove(0x13374204, 8);
for address in 0x13374200..0x13374208 {
assert!(set.contains(address));
}
for address in 0x13374208..0x1337420c {
assert!(!set.contains(address));
}
}
#[test]
fn should_merge_adjacent_ranges() {
let mut set = AddressSet::new();
set.add(0x13374200, 4);
set.add(0x13374204, 4);
set.add(0x13374208, 4);
set.add(0x1337420c, 4);
assert!(!set.contains(0x133741ff));
for address in 0x13374200..0x13374210 {
assert!(set.contains(address));
}
assert!(!set.contains(0x13374210));
set.remove(0x13374200, 4);
set.remove(0x13374204, 4);
set.remove(0x13374208, 4);
set.remove(0x1337420c, 4);
assert!(set.is_empty());
for address in 0x133741ff..=0x13374210 {
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);
}
}

View File

@ -5,8 +5,6 @@ use bitflags::bitflags;
use num_derive::{FromPrimitive, ToPrimitive};
use serde::{Deserialize, Serialize};
use super::address_set::AddressSet;
#[repr(C)]
struct VB {
_data: [u8; 0],
@ -57,38 +55,7 @@ bitflags! {
}
}
#[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 OnExecute =
extern "C" fn(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> 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")]
extern "C" {
@ -110,10 +77,6 @@ extern "C" {
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,
@ -121,36 +84,24 @@ extern "C" {
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 = "vbSetExecuteCallback"]
fn vb_set_execute_callback(sim: *mut VB, callback: Option<OnExecute>) -> Option<OnExecute>;
#[link_name = "vbSetFrameCallback"]
fn vb_set_frame_callback(sim: *mut VB, callback: Option<OnFrame>) -> Option<OnFrame>;
fn vb_set_frame_callback(sim: *mut VB, on_frame: 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,
@ -158,20 +109,13 @@ 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 {
extern "C" fn on_frame(sim: *mut VB) -> i32 {
// 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() };
@ -179,109 +123,12 @@ 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.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
let mut stopped = data.stop_reason.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
}
}
#[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
}
#[no_mangle]
extern "C" fn on_write(
sim: *mut VB,
address: u32,
_type: 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 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
}
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>,
step_from: Option<u32>,
breakpoints: Vec<u32>,
read_watchpoints: AddressSet,
write_watchpoints: AddressSet,
}
impl VBState {
fn needs_execute_callback(&self) -> bool {
self.step_from.is_some()
|| !self.breakpoints.is_empty()
|| !self.read_watchpoints.is_empty()
|| !self.write_watchpoints.is_empty()
}
}
pub enum StopReason {
Breakpoint,
Watchpoint(VBWatchpointType, u32),
Stepped,
}
#[repr(transparent)]
@ -289,6 +136,9 @@ pub struct Sim {
sim: *mut VB,
}
// SAFETY: the memory pointed to by sim is valid
unsafe impl Send for Sim {}
impl Sim {
pub fn new() -> Self {
// init the VB instance itself
@ -299,20 +149,12 @@ impl Sim {
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,
step_from: None,
breakpoints: vec![],
read_watchpoints: AddressSet::new(),
write_watchpoints: AddressSet::new(),
};
let state = VBState { frame_seen: false };
unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) };
unsafe { vb_set_frame_callback(sim, Some(on_frame)) };
unsafe { vb_set_frame_callback(sim, on_frame) };
// set up audio buffer
let audio_buffer = vec![0.0f32; AUDIO_CAPACITY_FLOATS];
@ -401,7 +243,9 @@ impl Sim {
}
pub fn read_pixels(&mut self, buffers: &mut [u8]) -> bool {
let data = self.get_state();
// 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(self.sim).cast() };
if !data.frame_seen {
return false;
}
@ -448,167 +292,6 @@ impl Sim {
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_execute = state.needs_execute_callback();
if state.write_watchpoints.is_empty() {
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();
unsafe { vb_set_read_callback(self.sim, None) };
unsafe { vb_set_write_callback(self.sim, None) };
unsafe { vb_set_execute_callback(self.sim, None) };
}
pub fn 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
}
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 {

View File

@ -1,560 +0,0 @@
use anyhow::{bail, Result};
use registers::REGISTERS;
use request::{Request, RequestKind, RequestSource};
use response::Response;
use std::{
sync::{Arc, Mutex},
thread,
};
use tokio::{
io::{AsyncWriteExt as _, BufReader},
net::{TcpListener, TcpStream},
pin, select,
sync::{mpsc, oneshot},
};
use tracing::{debug, enabled, error, info, Level};
use crate::emulator::{
DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId, VBWatchpointType,
};
mod registers;
mod request;
mod response;
pub struct GdbServer {
sim_id: SimId,
client: EmulatorClient,
status: Arc<Mutex<GdbServerStatus>>,
killer: Option<oneshot::Sender<()>>,
}
impl GdbServer {
pub fn new(sim_id: SimId, client: EmulatorClient) -> Self {
Self {
sim_id,
client,
status: Arc::new(Mutex::new(GdbServerStatus::Stopped)),
killer: None,
}
}
pub fn status(&self) -> GdbServerStatus {
self.status.lock().unwrap().clone()
}
pub fn start(&mut self, port: u16) {
*self.status.lock().unwrap() = GdbServerStatus::Connecting;
let sim_id = self.sim_id;
let client = self.client.clone();
let status = self.status.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_server(sim_id, client.clone(), port, &status) => {}
_ = rx => {
client.send_command(EmulatorCommand::StopDebugging(sim_id));
*status.lock().unwrap() = GdbServerStatus::Stopped;
}
}
})
});
}
pub fn stop(&mut self) {
if let Some(killer) = self.killer.take() {
let _ = killer.send(());
}
}
}
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 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, debug_source).await {
Ok(()) => {
info!("Finished debugging.");
*status.lock().unwrap() = GdbServerStatus::Stopped;
}
Err(error) => {
error!(%error, "Error from debugger.");
*status.lock().unwrap() = GdbServerStatus::Error(error.to_string());
}
}
}
async fn try_connect(port: u16, status: &Mutex<GdbServerStatus>) -> Option<TcpStream> {
*status.lock().unwrap() = GdbServerStatus::Connecting;
let listener = match TcpListener::bind(("127.0.0.1", port)).await {
Ok(l) => l,
Err(err) => {
error!(%err, "Could not open port.");
*status.lock().unwrap() = GdbServerStatus::Error(err.to_string());
return None;
}
};
match listener.accept().await {
Ok((stream, _)) => {
*status.lock().unwrap() = GdbServerStatus::Running;
Some(stream)
}
Err(err) => {
error!(%err, "Could not connect to debugger.");
*status.lock().unwrap() = GdbServerStatus::Error(err.to_string());
None
}
}
}
#[derive(Clone)]
pub enum GdbServerStatus {
Stopped,
Connecting,
Running,
Error(String),
}
impl GdbServerStatus {
pub fn running(&self) -> bool {
matches!(self, Self::Connecting | Self::Running)
}
}
struct GdbConnection {
sim_id: SimId,
client: EmulatorClient,
ack_messages: bool,
stop_reason: Option<DebugStopReason>,
response_buf: Option<Vec<u8>>,
memory_buf: Option<Vec<u8>>,
}
impl GdbConnection {
fn new(sim_id: SimId, client: EmulatorClient) -> Self {
Self {
sim_id,
client,
ack_messages: true,
stop_reason: None,
response_buf: None,
memory_buf: None,
}
}
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));
loop {
let response = select! {
maybe_event = debug_source.recv() => {
let Some(event) = maybe_event else {
// debugger has stopped running
break;
};
self.handle_event(event)
}
maybe_request = request_source.recv() => {
let req = maybe_request?;
self.handle_request(req)?
}
};
if let Some(res) = response {
let buffer = res.finish();
if enabled!(Level::DEBUG) {
match std::str::from_utf8(&buffer) {
Ok(text) => debug!("response: {text}"),
Err(_) => debug!("response: {buffer:02x?}"),
}
}
tx.write_all(&buffer).await?;
self.response_buf = Some(buffer);
tx.flush().await?;
}
}
Ok(())
}
fn handle_event(&mut self, event: DebugEvent) -> Option<Response> {
let res = match event {
DebugEvent::Stopped(reason) => {
if self.stop_reason.is_some_and(|r| r == reason) {
return None;
}
self.stop_reason = Some(reason);
self.response()
.write_str(&debug_stop_reason_string(self.stop_reason))
}
};
Some(res)
}
fn handle_request(&mut self, mut req: Request<'_>) -> Result<Option<Response>> {
debug!("received {:02x?}", req);
if req.kind == RequestKind::Signal {
self.client
.send_command(EmulatorCommand::DebugInterrupt(self.sim_id));
return Ok(None); // we'll send a message when the emulator reports it has stopped
}
let res = if req.match_str("QStartNoAckMode") {
let res = self.response().write_str("OK");
self.ack_messages = false;
res
} else if req.match_str("qSupported:") {
self.response()
.write_str("multiprocess+;swbreak+;vContSupported+;PacketSize=10000;SupportedWatchpointTypes=x86_64,aarch64-bas,aarch64-mask")
} else if req
.match_some_str([
"QThreadSuffixSupported",
"QListThreadsInStopReply",
"QEnableErrorStrings",
])
.is_some()
{
self.response().write_str("OK")
} else if req.match_str("qHostInfo") {
self.response().write_str(&format!(
"triple:{};endian:little;ptrsize:4;",
hex::encode("v810-unknown-vb")
))
} else if req.match_str("qProcessInfo") {
self.response().write_str(&format!(
"pid:1;triple:{};endian:little;ptrsize:4;",
hex::encode("v810-unknown-vb")
))
} else if req.match_str("qRegisterInfo") {
let mut get_reg_info = || {
let register = req.match_hex::<usize>()?;
REGISTERS.get(register)
};
if let Some(reg_info) = get_reg_info() {
self.response().write_str(&reg_info.to_description())
} else {
self.response()
}
} else if req.match_str("vCont?") {
self.response().write_str("vCont;c;s;")
} else if req.match_str("qC") {
// The v810 has no threads, so report that the "current thread" is 1.
self.response().write_str("QCp1.t1")
} else if req.match_str("qfThreadInfo") {
self.response().write_str("mp1.t1")
} else if req.match_str("qsThreadInfo") {
self.response().write_str("l")
} else if req.match_str("k") {
bail!("debug process was killed");
} else if req.match_str("?") {
self.response()
.write_str(&debug_stop_reason_string(self.stop_reason))
} else if req.match_some_str(["c", "vCont;c:"]).is_some() {
self.client
.send_command(EmulatorCommand::DebugContinue(self.sim_id));
self.stop_reason = None;
// Don't send a response until we hit a breakpoint or get interrupted
return Ok(None);
} else if req.match_some_str(["s", "vCont;s:"]).is_some() {
self.client
.send_command(EmulatorCommand::DebugStep(self.sim_id));
self.stop_reason = None;
// Don't send a response until we hit a breakpoint or get interrupted
return Ok(None);
} else if req.match_str("p") {
let mut read_register = || {
let register_index = req.match_hex::<usize>()?;
let register = REGISTERS.get(register_index)?.to_vb_register();
let (tx, rx) = ::oneshot::channel();
self.client
.send_command(EmulatorCommand::ReadRegister(self.sim_id, register, tx));
rx.recv().ok()
};
if let Some(value) = read_register() {
self.response().write_hex(value)
} 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, length) = parse_memory_range(&mut req)?;
let mut buf = self.memory_buf.take().unwrap_or_default();
buf.clear();
if length == 0 {
return Some(buf);
}
let (tx, rx) = ::oneshot::channel();
self.client.send_command(EmulatorCommand::ReadMemory(
self.sim_id,
start,
length,
buf,
tx,
));
rx.recv().ok()
};
if let Some(memory) = read_memory() {
let mut res = self.response();
if memory.is_empty() {
res = res.write_str("OK");
} else if op == "m" {
// send the hex-encoded byte stream
for byte in &memory {
res = res.write_hex(*byte);
}
} else {
// send the raw byte stream
for byte in &memory {
res = res.write_byte(*byte);
}
}
self.memory_buf = Some(memory);
res
} 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>()?;
if !req.match_str(",") {
return None;
}
let address = req.match_hex()?;
if type_ == 0 || type_ == 1 {
return Some(EmulatorCommand::AddBreakpoint(self.sim_id, address));
}
if !req.match_str(",") {
return None;
}
let length = req.match_hex()?;
let watch = match type_ {
2 => VBWatchpointType::Write,
3 => VBWatchpointType::Read,
4 => VBWatchpointType::Access,
_ => return None,
};
Some(EmulatorCommand::AddWatchpoint(
self.sim_id,
address,
length,
watch,
))
};
if let Some(command) = parse_request() {
self.client.send_command(command);
self.response().write_str("OK")
} else {
self.response()
}
} else if req.match_str("z") {
let mut parse_request = || {
let type_ = req.match_hex::<u8>()?;
if !req.match_str(",") {
return None;
}
let address = req.match_hex()?;
if type_ == 0 || type_ == 1 {
return Some(EmulatorCommand::RemoveBreakpoint(self.sim_id, address));
}
if !req.match_str(",") {
return None;
}
let length = req.match_hex()?;
let watch = match type_ {
2 => VBWatchpointType::Write,
3 => VBWatchpointType::Read,
4 => VBWatchpointType::Access,
_ => return None,
};
Some(EmulatorCommand::RemoveWatchpoint(
self.sim_id,
address,
length,
watch,
))
};
if let Some(command) = parse_request() {
self.client.send_command(command);
self.response().write_str("OK")
} else {
self.response()
}
} else {
// unrecognized command
self.response()
};
Ok(Some(res))
}
fn response(&mut self) -> Response {
Response::new(
self.response_buf.take().unwrap_or_default(),
self.ack_messages,
)
}
}
impl Drop for GdbConnection {
fn drop(&mut self) {
self.client
.send_command(EmulatorCommand::StopDebugging(self.sim_id));
}
}
// 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_and(|r| r != DebugStopReason::Paused) {
"T05;"
} else {
"T00;"
};
if let Some(DebugStopReason::Breakpoint) = reason {
result += "swbreak;";
}
if let Some(DebugStopReason::Watchpoint(watch, address)) = reason {
result += match watch {
VBWatchpointType::Write => "watch:",
VBWatchpointType::Read => "rwatch:",
VBWatchpointType::Access => "awatch:",
};
result += &format!("{address:08x};");
}
result += "thread:p1.t1;threads:p1.t1;";
if let Some(reason) = reason {
result += "reason:";
result += match reason {
DebugStopReason::Trace => "trace;",
DebugStopReason::Breakpoint => "breakpoint;",
DebugStopReason::Watchpoint(_, _) => "watchpoint;",
DebugStopReason::Paused => "trap;",
};
}
if let Some(DebugStopReason::Watchpoint(_, address)) = reason {
result += "description:";
result += &hex::encode(address.to_string());
result += ";";
}
result
}

View File

@ -1,130 +0,0 @@
use crate::emulator::VBRegister;
pub struct RegisterInfo {
dwarf: u32,
name: &'static str,
set: &'static str,
alt_name: Option<&'static str>,
generic: Option<&'static str>,
}
impl RegisterInfo {
pub fn to_description(&self) -> String {
let mut string = format!("name:{}", self.name);
if let Some(alt) = self.alt_name {
string.push_str(&format!(";alt-name:{}", alt));
}
string.push_str(&format!(
";bitsize:32;offset:{};encoding:uint;format:hex;set:{};dwarf:{}",
self.dwarf * 4,
self.set,
self.dwarf
));
if let Some(generic) = self.generic {
string.push_str(&format!(";generic:{}", generic));
}
string
}
pub fn to_vb_register(&self) -> VBRegister {
match self.dwarf {
0..32 => VBRegister::Program(self.dwarf),
32..40 => VBRegister::System(self.dwarf - 32),
40..42 => VBRegister::System(self.dwarf - 16),
42..45 => VBRegister::System(self.dwarf - 13),
45 => VBRegister::PC,
other => panic!("unexpected DWARF register {other}"),
}
}
}
macro_rules! register {
($set:expr, $dwarf:expr, $name:expr) => {
RegisterInfo {
dwarf: $dwarf,
name: $name,
set: $set,
alt_name: None,
generic: None,
}
};
($set:expr, $dwarf:expr, $name:expr, alt: $alt:expr) => {
RegisterInfo {
dwarf: $dwarf,
name: $name,
set: $set,
alt_name: Some($alt),
generic: None,
}
};
($set:expr, $dwarf:expr, $name:expr, generic: $generic:expr) => {
RegisterInfo {
dwarf: $dwarf,
name: $name,
set: $set,
alt_name: None,
generic: Some($generic),
}
};
($set:expr, $dwarf:expr, $name:expr, alt: $alt:expr, generic: $generic:expr) => {
RegisterInfo {
dwarf: $dwarf,
name: $name,
set: $set,
alt_name: Some($alt),
generic: Some($generic),
}
};
}
const GENERAL: &str = "General Purpose Registers";
const SPECIAL: &str = "Special Registers";
pub const REGISTERS: [RegisterInfo; 46] = [
register!(GENERAL, 0, "r0"),
register!(GENERAL, 1, "r1"),
register!(GENERAL, 2, "fp", alt: "r2", generic: "fp"),
register!(GENERAL, 3, "sp", alt: "r3", generic: "sp"),
register!(GENERAL, 4, "gp", alt: "r4"),
register!(GENERAL, 5, "tp", alt: "r5"),
register!(GENERAL, 6, "r6", generic: "arg1"),
register!(GENERAL, 7, "r7", generic: "arg2"),
register!(GENERAL, 8, "r8", generic: "arg3"),
register!(GENERAL, 9, "r9", generic: "arg4"),
register!(GENERAL, 10, "r10"),
register!(GENERAL, 11, "r11"),
register!(GENERAL, 12, "r12"),
register!(GENERAL, 13, "r13"),
register!(GENERAL, 14, "r14"),
register!(GENERAL, 15, "r15"),
register!(GENERAL, 16, "r16"),
register!(GENERAL, 17, "r17"),
register!(GENERAL, 18, "r18"),
register!(GENERAL, 19, "r19"),
register!(GENERAL, 20, "r20"),
register!(GENERAL, 21, "r21"),
register!(GENERAL, 22, "r22"),
register!(GENERAL, 23, "r23"),
register!(GENERAL, 24, "r24"),
register!(GENERAL, 25, "r25"),
register!(GENERAL, 26, "r26"),
register!(GENERAL, 27, "r27"),
register!(GENERAL, 28, "r28"),
register!(GENERAL, 29, "r29"),
register!(GENERAL, 30, "r30"),
register!(GENERAL, 31, "lp", alt: "r31", generic: "ra"),
register!(SPECIAL, 32, "eipc", alt: "sr0"),
register!(SPECIAL, 33, "eipsw", alt: "sr1"),
register!(SPECIAL, 34, "fepc", alt: "sr2"),
register!(SPECIAL, 35, "fepsw", alt: "sr3"),
register!(SPECIAL, 36, "ecr", alt: "sr4"),
register!(SPECIAL, 37, "psw", alt: "sr5", generic: "flags"),
register!(SPECIAL, 38, "pir", alt: "sr6"),
register!(SPECIAL, 39, "tkcw", alt: "sr7"),
register!(SPECIAL, 40, "chcw", alt: "sr24"),
register!(SPECIAL, 41, "adtre", alt: "sr25"),
register!(SPECIAL, 42, "sr29"),
register!(SPECIAL, 43, "sr30"),
register!(SPECIAL, 44, "sr31"),
register!(SPECIAL, 45, "pc", generic: "pc"),
];

View File

@ -1,202 +0,0 @@
use anyhow::{bail, Result};
use atoi::FromRadix16;
use tokio::io::{AsyncRead, AsyncReadExt as _};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RequestKind {
Signal,
Command,
}
impl RequestKind {
fn name(self) -> &'static str {
match self {
Self::Signal => "Signal",
Self::Command => "Command",
}
}
}
pub struct Request<'a> {
pub kind: RequestKind,
buffer: &'a [u8],
}
impl std::fmt::Debug for Request<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut ds = f.debug_tuple(self.kind.name());
match self.kind {
RequestKind::Signal => ds.field(&self.buffer),
RequestKind::Command => match std::str::from_utf8(self.buffer) {
Ok(str) => ds.field(&str),
Err(_) => ds.field(&self.buffer),
},
};
ds.finish()
}
}
impl Request<'_> {
pub fn match_str(&mut self, prefix: &str) -> bool {
if let Some(new_buffer) = self.buffer.strip_prefix(prefix.as_bytes()) {
self.buffer = new_buffer;
return true;
}
false
}
pub fn match_some_str<'a, I: IntoIterator<Item = &'a str>>(
&mut self,
prefixes: I,
) -> Option<&'a str> {
prefixes.into_iter().find(|&prefix| self.match_str(prefix))
}
pub fn match_hex<I: FromRadix16>(&mut self) -> Option<I> {
match I::from_radix_16(self.buffer) {
(_, 0) => None,
(val, used) => {
self.buffer = self.buffer.split_at(used).1;
Some(val)
}
}
}
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> {
reader: R,
buffer: Vec<u8>,
state: RequestReadState,
}
impl<R: AsyncRead + Unpin> RequestSource<R> {
pub fn new(reader: R) -> Self {
Self {
reader,
buffer: vec![],
state: RequestReadState::Header,
}
}
pub async fn recv(&mut self) -> Result<Request<'_>> {
let mut char = self.reader.read_u8().await?;
if matches!(self.state, RequestReadState::Start) {
self.buffer.clear();
self.state = RequestReadState::Header;
}
if matches!(self.state, RequestReadState::Header) {
// Just ignore positive acks
while char == b'+' {
char = self.reader.read_u8().await?;
}
if char == b'-' {
bail!("no support for negative acks");
}
if char == 0x03 {
// This is how the client "cancels an in-flight request"
self.buffer.push(char);
self.state = RequestReadState::Start;
return Ok(Request {
kind: RequestKind::Signal,
buffer: &self.buffer,
});
}
if char != b'$' {
// Messages are supposed to start with a dollar sign
bail!("malformed message");
}
self.state = RequestReadState::Body {
checksum: 0,
escaping: false,
};
char = self.reader.read_u8().await?;
}
while let RequestReadState::Body { checksum, escaping } = &mut self.state {
if char == b'#' && !*escaping {
self.state = RequestReadState::Checksum {
expected: *checksum,
actual: 0,
digits: 0,
};
char = self.reader.read_u8().await?;
break;
}
*checksum = checksum.wrapping_add(char);
if *escaping {
// escaped character
self.buffer.push(char ^ 0x20);
*escaping = false;
} else if char == b'}' {
// next character will be escaped
*escaping = true;
} else {
self.buffer.push(char);
}
char = self.reader.read_u8().await?;
}
while let RequestReadState::Checksum {
expected,
actual,
digits,
} = &mut self.state
{
let digit = match char {
b'0'..=b'9' => char - b'0',
b'a'..=b'f' => char - b'a' + 10,
b'A'..=b'F' => char - b'A' + 10,
_ => bail!("invalid checksum"),
};
*actual = (*actual << 4) + digit;
*digits += 1;
if *digits == 2 {
if *expected != *actual {
bail!("mismatched checksum");
}
self.state = RequestReadState::Start;
return Ok(Request {
kind: RequestKind::Command,
buffer: &self.buffer,
});
}
char = self.reader.read_u8().await?;
}
unreachable!();
}
}
enum RequestReadState {
Start,
Header,
Body {
checksum: u8,
escaping: bool,
},
Checksum {
expected: u8,
actual: u8,
digits: u8,
},
}

View File

@ -1,63 +0,0 @@
use num_traits::ToBytes;
pub struct Response {
buffer: Vec<u8>,
checksum: u8,
}
impl Response {
pub fn new(mut buffer: Vec<u8>, ack: bool) -> Self {
buffer.clear();
if ack {
buffer.push(b'+');
}
buffer.push(b'$');
Self {
buffer,
checksum: 0,
}
}
pub fn write_str(self, str: &str) -> Self {
let mut me = self;
for byte in str.as_bytes() {
me = me.write_byte(*byte);
}
me
}
pub fn write_byte(mut self, byte: u8) -> Self {
if byte == b'}' || byte == b'#' || byte == b'$' || byte == b'*' {
self.buffer.push(b'}');
self.checksum = self.checksum.wrapping_add(b'}');
let escaped = byte ^ 0x20;
self.buffer.push(escaped);
self.checksum = self.checksum.wrapping_add(escaped);
} else {
self.buffer.push(byte);
self.checksum = self.checksum.wrapping_add(byte);
}
self
}
pub fn write_hex<T: ToBytes>(mut self, value: T) -> Self {
for byte in value.to_le_bytes().as_ref() {
for digit in [(byte >> 4), (byte & 0xf)] {
let char = if digit > 9 {
b'a' + digit - 10
} else {
b'0' + digit
};
self.buffer.push(char);
self.checksum = self.checksum.wrapping_add(char);
}
}
self
}
pub fn finish(mut self) -> Vec<u8> {
let checksum = self.checksum;
self.buffer.push(b'#');
self.write_hex(checksum).buffer
}
}

View File

@ -3,20 +3,17 @@
use std::{path::PathBuf, process, time::SystemTime};
use anyhow::{bail, Result};
use anyhow::Result;
use app::Application;
use clap::Parser;
use emulator::EmulatorBuilder;
use thread_priority::{ThreadBuilder, ThreadPriority};
use tracing::error;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
use winit::event_loop::{ControlFlow, EventLoop};
mod app;
mod audio;
mod controller;
mod emulator;
mod gdbserver;
mod graphics;
mod input;
mod persistence;
@ -24,18 +21,7 @@ mod window;
#[derive(Parser)]
struct Args {
/// The path to a virtual boy ROM to run.
rom: Option<PathBuf>,
/// Start a GDB/LLDB debug server on this port.
#[arg(short, long)]
debug_port: Option<u16>,
}
fn init_logger() {
let directives = std::env::var("RUST_LOG").unwrap_or("error,lemur=info".into());
let filter = EnvFilter::builder().parse_lossy(directives);
let layer = tracing_subscriber::fmt::layer().with_filter(filter);
tracing_subscriber::registry().with(layer).init();
}
fn set_panic_handler() {
@ -85,8 +71,6 @@ fn set_process_priority_to_high() -> Result<()> {
}
fn main() -> Result<()> {
init_logger();
set_panic_handler();
#[cfg(windows)]
@ -95,14 +79,8 @@ 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 args.debug_port.is_some() {
if args.rom.is_none() {
bail!("to start debugging, please select a game.");
}
builder = builder.start_paused(true);
if let Some(path) = args.rom {
builder = builder.with_rom(&path);
}
ThreadBuilder::default()
@ -111,8 +89,8 @@ fn main() -> Result<()> {
.spawn_careless(move || {
let mut emulator = match builder.build() {
Ok(e) => e,
Err(error) => {
error!(%error, "Error initializing emulator");
Err(err) => {
eprintln!("Error initializing emulator: {err}");
process::exit(1);
}
};
@ -122,6 +100,6 @@ fn main() -> Result<()> {
let event_loop = EventLoop::with_user_event().build().unwrap();
event_loop.set_control_flow(ControlFlow::Poll);
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))?;
Ok(())
}

View File

@ -1,23 +1,16 @@
pub use about::AboutWindow;
use egui::{Context, ViewportBuilder, ViewportId};
pub use game::GameWindow;
pub use gdb::GdbServerWindow;
pub use input::InputWindow;
use winit::event::KeyEvent;
use crate::emulator::SimId;
mod about;
mod game;
mod game_screen;
mod gdb;
mod input;
pub trait AppWindow {
fn viewport_id(&self) -> ViewportId;
fn sim_id(&self) -> SimId {
SimId::Player1
}
fn initial_viewport(&self) -> ViewportBuilder;
fn show(&mut self, ctx: &Context);
fn on_init(&mut self, render_state: &egui_wgpu::RenderState) {

View File

@ -2,7 +2,7 @@ use std::sync::mpsc;
use crate::{
app::UserEvent,
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
emulator::{EmulatorClient, EmulatorCommand, SimId},
persistence::Persistence,
};
use egui::{
@ -73,29 +73,26 @@ impl GameWindow {
.pick_file();
if let Some(path) = rom {
self.client
.send_command(EmulatorCommand::LoadGame(self.sim_id, path));
.send_command(EmulatorCommand::LoadGame(SimId::Player1, path));
}
ui.close_menu();
}
if ui.button("Quit").clicked() {
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
ctx.send_viewport_cmd(ViewportCommand::Close);
}
});
ui.menu_button("Emulation", |ui| {
let state = self.client.emulator_state();
let is_ready = self.client.sim_state(self.sim_id) == SimState::Ready;
let can_pause = is_ready && state == EmulatorState::Running;
let can_resume = is_ready && state == EmulatorState::Paused;
if state == EmulatorState::Running {
if ui.add_enabled(can_pause, Button::new("Pause")).clicked() {
let has_game = self.client.has_game(self.sim_id);
if self.client.is_running(self.sim_id) {
if ui.add_enabled(has_game, Button::new("Pause")).clicked() {
self.client.send_command(EmulatorCommand::Pause);
ui.close_menu();
}
} else if ui.add_enabled(can_resume, Button::new("Resume")).clicked() {
} else if ui.add_enabled(has_game, Button::new("Resume")).clicked() {
self.client.send_command(EmulatorCommand::Resume);
ui.close_menu();
}
if ui.add_enabled(is_ready, Button::new("Reset")).clicked() {
if ui.add_enabled(has_game, Button::new("Reset")).clicked() {
self.client
.send_command(EmulatorCommand::Reset(self.sim_id));
ui.close_menu();
@ -103,9 +100,8 @@ impl GameWindow {
});
ui.menu_button("Options", |ui| self.show_options_menu(ctx, ui));
ui.menu_button("Multiplayer", |ui| {
let has_player_2 = self.client.sim_state(SimId::Player2) != SimState::Uninitialized;
if self.sim_id == SimId::Player1
&& !has_player_2
&& !self.client.has_player_2()
&& ui.button("Open Player 2").clicked()
{
self.client
@ -113,7 +109,7 @@ impl GameWindow {
self.proxy.send_event(UserEvent::OpenPlayer2).unwrap();
ui.close_menu();
}
if has_player_2 {
if self.client.has_player_2() {
let linked = self.client.are_sims_linked();
if linked && ui.button("Unlink").clicked() {
self.client.send_command(EmulatorCommand::Unlink);
@ -125,14 +121,6 @@ impl GameWindow {
}
}
});
ui.menu_button("Tools", |ui| {
if ui.button("GDB Server").clicked() {
self.proxy
.send_event(UserEvent::OpenDebugger(self.sim_id))
.unwrap();
ui.close_menu();
}
});
ui.menu_button("About", |ui| {
self.proxy.send_event(UserEvent::OpenAbout).unwrap();
ui.close_menu();
@ -211,7 +199,7 @@ impl GameWindow {
let color_str = |color: Color32| {
format!("{:02x}{:02x}{:02x}", color.r(), color.g(), color.b())
};
let is_running = self.client.emulator_state() == EmulatorState::Running;
let is_running = self.client.is_running(self.sim_id);
if is_running {
self.client.send_command(EmulatorCommand::Pause);
}
@ -318,10 +306,6 @@ impl AppWindow for GameWindow {
}
}
fn sim_id(&self) -> SimId {
self.sim_id
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Lemur")
@ -385,7 +369,6 @@ impl AppWindow for GameWindow {
if self.sim_id == SimId::Player2 {
self.client.send_command(EmulatorCommand::StopSecondSim);
}
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
}
}

View File

@ -1,111 +0,0 @@
use egui::{Button, CentralPanel, TextEdit, ViewportBuilder, ViewportId};
use winit::event_loop::EventLoopProxy;
use crate::{
app::UserEvent,
emulator::{EmulatorClient, SimId, SimState},
gdbserver::{GdbServer, GdbServerStatus},
};
use super::AppWindow;
pub struct GdbServerWindow {
sim_id: SimId,
client: EmulatorClient,
port_str: String,
connected: bool,
quit_on_disconnect: bool,
server: GdbServer,
proxy: EventLoopProxy<UserEvent>,
}
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,
server: GdbServer::new(sim_id, client),
proxy,
}
}
pub fn launch(&mut self, port: u16) {
self.server.stop();
self.port_str = port.to_string();
self.quit_on_disconnect = true;
self.server.start(port);
self.connected = true;
}
}
impl AppWindow for GdbServerWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of(format!("Debugger-{}", self.sim_id))
}
fn sim_id(&self) -> SimId {
self.sim_id
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("GDB Server ({})", self.sim_id))
.with_inner_size((300.0, 200.0))
}
fn show(&mut self, ctx: &egui::Context) {
let port_num: Option<u16> = self.port_str.parse().ok();
let status = self.server.status();
CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
if port_num.is_none() {
let style = ui.style_mut();
let error = style.visuals.error_fg_color;
style.visuals.widgets.active.bg_stroke.color = error;
style.visuals.widgets.hovered.bg_stroke.color = error;
}
ui.label("Port");
let port_editor = TextEdit::singleline(&mut self.port_str).desired_width(100.0);
ui.add_enabled(!status.running(), port_editor);
ui.checkbox(&mut self.quit_on_disconnect, "Quit on disconnect");
});
if !status.running() {
if self.connected && self.quit_on_disconnect {
self.proxy.send_event(UserEvent::Quit(self.sim_id)).unwrap();
} else {
self.connected = false;
}
let start_button = Button::new("Start");
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;
}
} else {
let stop_button = Button::new("Stop");
if ui.add(stop_button).clicked() {
self.server.stop();
}
}
match &status {
GdbServerStatus::Stopped => {}
GdbServerStatus::Connecting => {
ui.label("Connecting...");
}
GdbServerStatus::Running => {
ui.label("Running");
}
GdbServerStatus::Error(message) => {
ui.label(message);
}
}
});
}
}