Compare commits

..

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

38 changed files with 657 additions and 8593 deletions

1330
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,46 +4,39 @@ description = "An emulator for the Virtual Boy."
repository = "https://git.virtual-boy.com/PVB/lemur"
publish = false
license = "MIT"
version = "0.5.1"
edition = "2024"
version = "0.2.4"
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"] }
cpal = { git = "https://github.com/sidit77/cpal.git", rev = "66ed6be" }
directories = "6"
directories = "5"
egui = { version = "0.30", features = ["serde"] }
egui_extras = { version = "0.30", features = ["image"] }
egui-toast = { git = "https://github.com/urholaukkarinen/egui-toast.git", rev = "d0bcf97" }
egui-winit = "0.30"
egui-wgpu = { version = "0.30", features = ["winit"] }
fixed = { version = "1.28", features = ["num-traits"] }
gilrs = { version = "0.11", features = ["serde-serialize"] }
hex = "0.4"
image = { version = "0.25", default-features = false, features = ["png"] }
itertools = "0.14"
itertools = "0.13"
num-derive = "0.4"
num-traits = "0.2"
oneshot = "0.1"
pollster = "0.4"
rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "async-std"]}
rfd = "0.15"
rtrb = "0.3"
rubato = "0.16"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thread-priority = "1"
tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync", "time"] }
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"] }
[target.'cfg(windows)'.dependencies]
windows = { version = "0.59", features = ["Win32_System_Threading"] }
windows = { version = "0.58", features = ["Win32_System_Threading"] }
[build-dependencies]
cc = "1"

View File

@ -5,9 +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)
- a C compiler (any will do, [the build script](https://docs.rs/cc/latest/cc/#compile-time-requirements) will find it automatically)
- (on linux) `libasound2-dev` and `libudev-dev`
- `cargo`
Run
```sh

View File

@ -23,14 +23,12 @@ ENV PATH="/osxcross/bin:$PATH" \
CXX_x86_64-apple-darwin="o64-clang++" \
CC_aarch64-apple-darwin="oa64-clang" \
CXX_aarch64-apple-darwin="o6a4-clang++" \
SHROOMS_CFLAGS_x86_64-unknown-linux-gnu="-flto" \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" \
SHROOMS_CFLAGS_x86_64-pc-windows-msvc="-flto" \
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS="-Lnative=/xwin/crt/lib/x86_64 -Lnative=/xwin/sdk/lib/um/x86_64 -Lnative=/xwin/sdk/lib/ucrt/x86_64 -Clinker-plugin-lto -Clink-arg=-fuse-ld=lld" \
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link-19" \
CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER="o64-clang" \
CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-19" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="oa64-clang" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-19" \
CROSS_COMPILE="setting-this-to-silence-a-warning-" \
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");

View File

@ -27,7 +27,7 @@ fi
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
read -r -d EOF 'body' <<EOF
## How to install
The emulator can be found in the "Downloads" section of this release.
@ -48,9 +48,8 @@ If you're not sure which to choose, use [this guide](https://support.apple.com/e
You can either download and run \`lemur-linux\`, or download and install the attached .deb file.
EOF
)
payload=$(cat <<EOF
read -r -d EOF 'payload' <<EOF
{
"body": $(echo "$body" | jq -Rsa .),
"draft": false,
@ -59,7 +58,6 @@ payload=$(cat <<EOF
"tag_name": "v${version}"
}
EOF
)
echo "Creating release..."
response=$(curl -s --json "$payload" "https://git.virtual-boy.com/api/v1/repos/PVB/lemur/releases?token=$RELEASE_TOKEN")

@ -1 +1 @@
Subproject commit ecbd103917315e3aa24fd2a682208f5548ec5d1b
Subproject commit 06849b54ba7f7fd8bba09e1cf8a8cc9107ec269b

View File

@ -1,13 +1,11 @@
use std::{collections::HashSet, num::NonZero, sync::Arc, thread, time::Duration};
use egui::{
ahash::{HashMap, HashMapExt},
Context, FontData, FontDefinitions, FontFamily, IconData, TextWrapMode, ViewportBuilder,
ViewportCommand, ViewportId, ViewportInfo,
ahash::{HashMap, HashMapExt},
style::ScrollStyle,
};
use gilrs::{EventType, Gilrs};
use tracing::{error, warn};
use winit::{
application::ApplicationHandler,
event::WindowEvent,
@ -18,14 +16,9 @@ use winit::{
use crate::{
controller::ControllerManager,
emulator::{EmulatorClient, EmulatorCommand, SimId},
images::ImageProcessor,
input::{MappingProvider, ShortcutProvider},
memory::MemoryClient,
input::MappingProvider,
persistence::Persistence,
window::{
AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow,
GdbServerWindow, HotkeysWindow, InputWindow, ObjectWindow, RegisterWindow, WorldWindow,
},
window::{AboutWindow, AppWindow, GameWindow, InputWindow},
};
fn load_icon() -> anyhow::Result<IconData> {
@ -41,34 +34,21 @@ fn load_icon() -> anyhow::Result<IconData> {
pub struct Application {
icon: Option<Arc<IconData>>,
wgpu: WgpuState,
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
mappings: MappingProvider,
shortcuts: ShortcutProvider,
controllers: ControllerManager,
memory: Arc<MemoryClient>,
images: ImageProcessor,
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 {
let wgpu = WgpuState::new();
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());
let shortcuts = ShortcutProvider::new(persistence.clone());
let controllers = ControllerManager::new(client.clone(), &mappings);
let memory = Arc::new(MemoryClient::new(client.clone()));
let images = ImageProcessor::new();
{
let mappings = mappings.clone();
let proxy = proxy.clone();
@ -76,50 +56,39 @@ impl Application {
}
Self {
icon,
wgpu,
client,
proxy,
mappings,
shortcuts,
memory,
images,
controllers,
persistence,
viewports: HashMap::new(),
focused: None,
init_debug_port: debug_port,
}
}
fn open(&mut self, event_loop: &ActiveEventLoop, window: Box<dyn AppWindow>) {
let viewport_id = window.viewport_id();
if let Some(viewport) = self.viewports.get(&viewport_id) {
viewport.window.focus_window();
if self.viewports.contains_key(&viewport_id) {
return;
}
self.viewports.insert(
viewport_id,
Viewport::new(event_loop, &self.wgpu, self.icon.clone(), window),
Viewport::new(event_loop, self.icon.clone(), window),
);
}
}
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(),
self.shortcuts.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(
@ -136,23 +105,19 @@ impl ApplicationHandler<UserEvent> for Application {
return;
};
let viewport_id = viewport.id();
match &event {
WindowEvent::KeyboardInput { event, .. } => {
self.controllers.handle_key_event(event);
viewport.app.handle_key_event(event);
}
WindowEvent::Focused(new_focused) => {
self.focused = new_focused.then_some(viewport_id);
}
_ => {}
}
let mut queue_redraw = false;
let mut inactive_viewports = HashSet::new();
let (consumed, action) = viewport.on_window_event(&event);
if !consumed {
match event {
WindowEvent::KeyboardInput { event, .. } => {
if !viewport.app.handle_key_event(&event) {
self.controllers.handle_key_event(&event);
}
}
WindowEvent::Focused(new_focused) => {
self.focused = new_focused.then_some(viewport_id);
}
_ => {}
}
}
match action {
match viewport.on_window_event(event) {
Some(Action::Redraw) => {
for viewport in self.viewports.values_mut() {
match viewport.redraw(event_loop) {
@ -204,75 +169,33 @@ impl ApplicationHandler<UserEvent> for Application {
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
match event {
UserEvent::GamepadEvent(event) => {
if let Some(viewport) = self
self.controllers.handle_gamepad_event(&event);
let Some(viewport) = self
.focused
.as_ref()
.and_then(|id| self.viewports.get_mut(id))
{
if viewport.app.handle_gamepad_event(&event) {
return;
}
}
self.controllers.handle_gamepad_event(&event);
else {
return;
};
viewport.app.handle_gamepad_event(&event);
}
UserEvent::OpenAbout => {
let about = AboutWindow;
self.open(event_loop, Box::new(about));
}
UserEvent::OpenCharacterData(sim_id) => {
let chardata = CharacterDataWindow::new(sim_id, &self.memory, &mut self.images);
self.open(event_loop, Box::new(chardata));
}
UserEvent::OpenBgMap(sim_id) => {
let bgmap = BgMapWindow::new(sim_id, &self.memory, &mut self.images);
self.open(event_loop, Box::new(bgmap));
}
UserEvent::OpenObjects(sim_id) => {
let objects = ObjectWindow::new(sim_id, &self.memory, &mut self.images);
self.open(event_loop, Box::new(objects));
}
UserEvent::OpenWorlds(sim_id) => {
let world = WorldWindow::new(sim_id, &self.memory, &mut self.images);
self.open(event_loop, Box::new(world));
}
UserEvent::OpenFrameBuffers(sim_id) => {
let world = FrameBufferWindow::new(sim_id, &self.memory, &mut self.images);
self.open(event_loop, Box::new(world));
}
UserEvent::OpenRegisters(sim_id) => {
let registers = RegisterWindow::new(sim_id, &self.memory);
self.open(event_loop, Box::new(registers));
}
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));
}
UserEvent::OpenHotkeys => {
let hotkeys = HotkeysWindow::new(self.shortcuts.clone());
self.open(event_loop, Box::new(hotkeys));
}
UserEvent::OpenPlayer2 => {
let p2 = GameWindow::new(
self.client.clone(),
self.proxy.clone(),
self.persistence.clone(),
self.shortcuts.clone(),
SimId::Player2,
);
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();
}
}
}
}
@ -285,65 +208,13 @@ 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);
}
}
}
}
struct WgpuState {
instance: Arc<wgpu::Instance>,
adapter: Arc<wgpu::Adapter>,
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
}
impl WgpuState {
fn new() -> Self {
#[allow(unused_variables)]
let egui_wgpu::WgpuConfiguration {
wgpu_setup:
egui_wgpu::WgpuSetup::CreateNew {
supported_backends,
device_descriptor,
..
},
..
} = egui_wgpu::WgpuConfiguration::default()
else {
panic!("required fields not found")
};
#[cfg(windows)]
let supported_backends = wgpu::util::backend_bits_from_env()
.unwrap_or((wgpu::Backends::PRIMARY | wgpu::Backends::GL) - wgpu::Backends::VULKAN);
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: supported_backends,
..wgpu::InstanceDescriptor::default()
});
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: None,
force_fallback_adapter: false,
}))
.expect("could not create adapter");
let trace_path = std::env::var("WGPU_TRACE");
let (device, queue) = pollster::block_on(adapter.request_device(
&(*device_descriptor)(&adapter),
trace_path.ok().as_ref().map(std::path::Path::new),
))
.expect("could not request device");
Self {
instance: Arc::new(instance),
adapter: Arc::new(adapter),
device: Arc::new(device),
queue: Arc::new(queue),
}
}
}
struct Viewport {
painter: egui_wgpu::winit::Painter,
ctx: Context,
@ -357,7 +228,6 @@ struct Viewport {
impl Viewport {
pub fn new(
event_loop: &ActiveEventLoop,
wgpu: &WgpuState,
icon: Option<Arc<IconData>>,
mut app: Box<dyn AppWindow>,
) -> Self {
@ -378,23 +248,20 @@ impl Viewport {
ctx.style_mut(|s| {
s.wrap_mode = Some(TextWrapMode::Extend);
s.visuals.menu_rounding = Default::default();
s.spacing.scroll = ScrollStyle::thin();
});
egui_extras::install_image_loaders(&ctx);
let wgpu_config = egui_wgpu::WgpuConfiguration {
present_mode: wgpu::PresentMode::AutoNoVsync,
wgpu_setup: egui_wgpu::WgpuSetup::Existing {
instance: wgpu.instance.clone(),
adapter: wgpu.adapter.clone(),
device: wgpu.device.clone(),
queue: wgpu.queue.clone(),
let mut painter = egui_wgpu::winit::Painter::new(
ctx.clone(),
egui_wgpu::WgpuConfiguration {
present_mode: wgpu::PresentMode::AutoNoVsync,
..egui_wgpu::WgpuConfiguration::default()
},
..egui_wgpu::WgpuConfiguration::default()
};
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();
@ -404,7 +271,7 @@ impl Viewport {
let (window, state) = create_window_and_state(&ctx, event_loop, &builder, &mut painter);
egui_winit::update_viewport_info(&mut info, &ctx, &window, true);
app.on_init(&ctx, painter.render_state().as_ref().unwrap());
app.on_init(painter.render_state().as_ref().unwrap());
Self {
painter,
ctx,
@ -421,8 +288,8 @@ impl Viewport {
self.app.viewport_id()
}
pub fn on_window_event(&mut self, event: &WindowEvent) -> (bool, Option<Action>) {
let response = self.state.on_window_event(&self.window, event);
pub fn on_window_event(&mut self, event: WindowEvent) -> Option<Action> {
let response = self.state.on_window_event(&self.window, &event);
egui_winit::update_viewport_info(
&mut self.info,
self.state.egui_ctx(),
@ -430,22 +297,22 @@ impl Viewport {
false,
);
let action = match event {
match event {
WindowEvent::RedrawRequested => Some(Action::Redraw),
WindowEvent::CloseRequested => Some(Action::Close),
WindowEvent::Resized(size) => {
if let (Some(width), Some(height)) =
let (Some(width), Some(height)) =
(NonZero::new(size.width), NonZero::new(size.height))
{
self.painter
.on_window_resized(ViewportId::ROOT, width, height);
}
else {
return None;
};
self.painter
.on_window_resized(ViewportId::ROOT, width, height);
None
}
_ if response.repaint => Some(Action::Redraw),
_ => None,
};
(response.consumed, action)
}
}
fn redraw(&mut self, event_loop: &ActiveEventLoop) -> Option<Action> {
@ -507,17 +374,8 @@ impl Drop for Viewport {
pub enum UserEvent {
GamepadEvent(gilrs::Event),
OpenAbout,
OpenCharacterData(SimId),
OpenBgMap(SimId),
OpenObjects(SimId),
OpenWorlds(SimId),
OpenFrameBuffers(SimId),
OpenRegisters(SimId),
OpenDebugger(SimId),
OpenInput,
OpenHotkeys,
OpenPlayer2,
Quit(SimId),
}
pub enum Action {
@ -546,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");
return;
}
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

@ -1,22 +1,19 @@
use std::time::Duration;
use anyhow::{Result, bail};
use anyhow::{bail, Result};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use itertools::Itertools;
use rubato::{FastFixedOut, Resampler};
use tracing::error;
use rubato::{FftFixedInOut, Resampler};
pub struct Audio {
#[allow(unused)]
stream: cpal::Stream,
sampler: FastFixedOut<f32>,
sampler: FftFixedInOut<f32>,
input_buffer: Vec<Vec<f32>>,
output_buffer: Vec<Vec<f32>>,
sample_sink: rtrb::Producer<f32>,
}
const VB_FREQUENCY: usize = 41700;
impl Audio {
pub fn init() -> Result<Self> {
let host = cpal::default_host();
@ -30,15 +27,7 @@ impl Audio {
bail!("No suitable output config available");
};
let mut config = config.with_max_sample_rate().config();
let resample_ratio = config.sample_rate.0 as f64 / VB_FREQUENCY as f64;
let chunk_size = (834.0 * resample_ratio) as usize;
let sampler = FastFixedOut::new(
resample_ratio,
64.0,
rubato::PolynomialDegree::Cubic,
chunk_size,
2,
)?;
let sampler = FftFixedInOut::new(41700, config.sample_rate.0 as usize, 834, 2)?;
config.buffer_size = cpal::BufferSize::Fixed(sampler.output_frames_max() as u32);
let input_buffer = sampler.input_buffer_allocate(true);
@ -65,7 +54,7 @@ impl Audio {
}
chunk.commit_all();
},
move |error| error!(%error, "stream error"),
move |err| eprintln!("stream error: {err}"),
None,
)?;
stream.play()?;
@ -111,10 +100,4 @@ impl Audio {
std::thread::sleep(Duration::from_micros(500));
}
}
pub fn set_speed(&mut self, speed: f64) -> Result<()> {
self.sampler
.set_resample_ratio_relative(1.0 / speed, false)?;
Ok(())
}
}

View File

@ -1,6 +1,6 @@
use std::sync::{Arc, RwLock};
use gilrs::{Event as GamepadEvent, EventType, GamepadId, ev::Code};
use gilrs::{ev::Code, Event as GamepadEvent, EventType, GamepadId};
use winit::{
event::{ElementState, KeyEvent},
keyboard::PhysicalKey,

View File

@ -1,33 +1,34 @@
use std::{
collections::HashMap,
fmt::Display,
fs::{self, File},
io::{Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
sync::{
Arc, Weak,
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,
memory::{MemoryRange, MemoryRegion},
};
use shrooms_vb_core::{EXPECTED_FRAME_SIZE, Sim, StopReason};
pub use shrooms_vb_core::{VBKey, VBRegister, VBWatchpointType};
use crate::{audio::Audio, graphics::TextureSink};
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,
@ -44,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,
@ -90,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(),
};
@ -132,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)
}
}
@ -162,24 +134,23 @@ 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>,
watched_regions: HashMap<MemoryRange, Weak<MemoryRegion>>,
eye_contents: Vec<u8>,
audio_samples: Vec<f32>,
buffer: Vec<u8>,
}
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> {
@ -188,17 +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(),
watched_regions: HashMap::new(),
eye_contents: vec![0u8; 384 * 224 * 2],
audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE),
buffer: vec![],
audio_samples: vec![0.0; EXPECTED_FRAME_SIZE],
})
}
@ -229,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(())
}
@ -264,55 +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,
);
}
fn frame_advance(&mut self) {
if self
.state
.compare_exchange(
EmulatorState::Paused,
EmulatorState::Stepping,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_err_and(|s| s == EmulatorState::Running)
{
let _ = self.state.compare_exchange(
EmulatorState::Running,
EmulatorState::Stepping,
Ordering::AcqRel,
Ordering::Relaxed,
);
}
}
fn set_speed(&mut self, speed: f64) -> Result<()> {
self.audio.set_speed(speed)
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<()> {
@ -330,80 +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();
}
}
fn watch_memory(&mut self, range: MemoryRange, region: Weak<MemoryRegion>) {
self.watched_regions.insert(range, region);
}
pub fn run(&mut self) {
loop {
let idle = self.tick();
@ -428,37 +284,14 @@ impl Emulator {
}
}
}
self.watched_regions.retain(|range, region| {
let Some(region) = region.upgrade() else {
return false;
};
let Some(sim) = self.sims.get_mut(range.sim.to_index()) else {
return false;
};
self.buffer.clear();
sim.read_memory(range.start, range.length, &mut self.buffer);
region.update(&self.buffer);
true
});
}
}
// 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 | EmulatorState::Stepping => 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 {
@ -467,30 +300,6 @@ impl Emulator {
self.sims[SimId::Player2.to_index()].emulate();
}
if state == EmulatorState::Stepping {
self.state.store(EmulatorState::Paused, Ordering::Release);
}
// 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;
@ -505,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),
};
if let Some(sim) = self.sims.get_mut(SimId::Player1.to_index()) {
sim.read_samples(&mut self.audio_samples, p1_weight);
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, weight);
}
}
if let Some(sim) = self.sims.get_mut(SimId::Player2.to_index()) {
sim.read_samples(&mut self.audio_samples, p2_weight);
if p2_audio {
if let Some(sim) = self.sims.get_mut(SimId::Player2.to_index()) {
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);
@ -561,90 +367,20 @@ 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();
}
EmulatorCommand::FrameAdvance => {
self.frame_advance();
}
EmulatorCommand::SetSpeed(speed) => {
if let Err(error) = self.set_speed(speed) {
self.report_error(SimId::Player1, format!("Error setting speed: {error}"));
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::WatchMemory(range, region) => {
self.watch_memory(range, region);
}
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);
self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release);
@ -690,7 +426,7 @@ impl Emulator {
return;
}
}
error!("{}", message);
eprintln!("{}", message);
}
}
@ -702,22 +438,6 @@ pub enum EmulatorCommand {
StopSecondSim,
Pause,
Resume,
FrameAdvance,
SetSpeed(f64),
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>>),
WatchMemory(MemoryRange, Weak<MemoryRegion>),
AddBreakpoint(SimId, u32),
RemoveBreakpoint(SimId, u32),
AddWatchpoint(SimId, u32, usize, VBWatchpointType),
RemoveWatchpoint(SimId, u32, usize, VBWatchpointType),
SetAudioEnabled(bool, bool),
Link,
Unlink,
@ -726,73 +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,
Stepping,
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

@ -1,12 +1,10 @@
use std::{ffi::c_void, ptr, slice};
use anyhow::{Result, anyhow};
use anyhow::{anyhow, Result};
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,41 +55,10 @@ 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")]
unsafe extern "C" {
extern "C" {
#[link_name = "vbEmulate"]
fn vb_emulate(sim: *mut VB, cycles: *mut u32) -> c_int;
#[link_name = "vbEmulateEx"]
@ -110,10 +77,6 @@ unsafe 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 @@ unsafe 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 @@ unsafe 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;
}
#[unsafe(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,105 +123,12 @@ extern "C" fn on_frame(sim: *mut VB) -> c_int {
1
}
#[unsafe(no_mangle)]
extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: c_int) -> c_int {
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
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 }
}
#[unsafe(no_mangle)]
extern "C" fn on_read(
sim: *mut VB,
address: u32,
_type: VBDataType,
_value: *mut i32,
_cycles: *mut u32,
) -> c_int {
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
if let Some(start) = data.read_watchpoints.start_of_range_containing(address) {
let watch = if data.write_watchpoints.contains(address) {
VBWatchpointType::Access
} else {
VBWatchpointType::Read
};
data.stop_reason = Some(StopReason::Watchpoint(watch, start));
}
// Don't stop here, the debugger expects us to break after the memory access.
// We'll stop in on_execute instead.
0
}
#[unsafe(no_mangle)]
extern "C" fn on_write(
sim: *mut VB,
address: u32,
_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)]
@ -285,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
@ -293,22 +147,13 @@ impl Sim {
let memory = vec![0u64; size.div_ceil(4)];
let sim: *mut VB = Box::into_raw(memory.into_boxed_slice()).cast();
unsafe { vb_init(sim) };
// pseudohalt is disabled due to breaking red alarm
unsafe { vb_set_option(sim, VBOption::PseudoHalt, 0) };
unsafe { vb_set_keys(sim, VBKey::SGN.bits()) };
unsafe { vb_set_option(sim, VBOption::PseudoHalt, 1) };
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];
@ -397,7 +242,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;
}
@ -444,167 +291,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::{Result, bail};
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::{Level, debug, enabled, error, info};
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::{Result, bail};
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

@ -1,13 +1,12 @@
use std::{
sync::{
Arc, Mutex, MutexGuard,
atomic::{AtomicU64, Ordering},
mpsc,
mpsc, Arc, Mutex, MutexGuard,
},
thread,
};
use anyhow::{Result, bail};
use anyhow::{bail, Result};
use itertools::Itertools as _;
use wgpu::{
Device, Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, Queue, Texture,
@ -65,7 +64,9 @@ impl TextureSink {
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TextureFormat::Rg8Unorm,
usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING,
usage: TextureUsages::COPY_SRC
| TextureUsages::COPY_DST
| TextureUsages::TEXTURE_BINDING,
view_formats: &[TextureFormat::Rg8Unorm],
};
device.create_texture(&desc)

View File

@ -1,328 +0,0 @@
use std::{
collections::HashMap,
ops::Deref,
sync::{Arc, Mutex, Weak},
thread,
time::Duration,
};
use egui::{
Color32, ColorImage, TextureHandle, TextureOptions,
epaint::ImageDelta,
load::{LoadError, SizedTexture, TextureLoader, TexturePoll},
};
use tokio::{sync::mpsc, time::timeout};
pub struct ImageProcessor {
sender: mpsc::UnboundedSender<Box<dyn ImageRendererImpl>>,
}
impl ImageProcessor {
pub fn new() -> Self {
let (sender, receiver) = mpsc::unbounded_channel();
thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async move {
let mut worker = ImageProcessorWorker {
receiver,
renderers: vec![],
};
worker.run().await
})
});
Self { sender }
}
pub fn add<const N: usize, R: ImageRenderer<N> + 'static>(
&self,
renderer: R,
params: R::Params,
) -> ([ImageHandle; N], ImageParams<R::Params>) {
let states = renderer.sizes().map(ImageState::new);
let handles = states.clone().map(|state| ImageHandle {
size: state.size.map(|i| i as f32),
data: state.sink,
});
let images = renderer
.sizes()
.map(|[width, height]| ImageBuffer::new(width, height));
let sink = Arc::new(Mutex::new(params.clone()));
let _ = self.sender.send(Box::new(ImageRendererWrapper {
renderer,
params: Arc::downgrade(&sink),
images,
states,
}));
let params = ImageParams {
value: params,
sink,
};
(handles, params)
}
}
struct ImageProcessorWorker {
receiver: mpsc::UnboundedReceiver<Box<dyn ImageRendererImpl>>,
renderers: Vec<Box<dyn ImageRendererImpl>>,
}
impl ImageProcessorWorker {
async fn run(&mut self) {
loop {
if self.renderers.is_empty() {
// if we have nothing to do, block until we have something to do
if self.receiver.recv_many(&mut self.renderers, 64).await == 0 {
// shutdown
return;
}
while let Ok(renderer) = self.receiver.try_recv() {
self.renderers.push(renderer);
}
}
self.renderers
.retain_mut(|renderer| renderer.try_update().is_ok());
// wait up to 10 ms for more renderers
if timeout(
Duration::from_millis(10),
self.receiver.recv_many(&mut self.renderers, 64),
)
.await
.is_ok()
{
while let Ok(renderer) = self.receiver.try_recv() {
self.renderers.push(renderer);
}
}
}
}
}
pub struct ImageBuffer {
pub size: [usize; 2],
pub pixels: Vec<Color32>,
}
impl ImageBuffer {
pub fn new(width: usize, height: usize) -> Self {
Self {
size: [width, height],
pixels: vec![Color32::BLACK; width * height],
}
}
pub fn clear(&mut self) {
for pixel in self.pixels.iter_mut() {
*pixel = Color32::BLACK;
}
}
pub fn write(&mut self, coords: (usize, usize), pixel: Color32) {
self.pixels[coords.1 * self.size[0] + coords.0] = pixel;
}
pub fn add(&mut self, coords: (usize, usize), pixel: Color32) {
let index = coords.1 * self.size[0] + coords.0;
let old = self.pixels[index];
self.pixels[index] = Color32::from_rgb(
old.r() + pixel.r(),
old.g() + pixel.g(),
old.b() + pixel.b(),
);
}
pub fn changed(&self, image: &ColorImage) -> bool {
image.pixels.iter().zip(&self.pixels).any(|(a, b)| a != b)
}
pub fn read(&self, image: &mut ColorImage) {
image.pixels.copy_from_slice(&self.pixels);
}
}
#[derive(Clone)]
pub struct ImageHandle {
size: [f32; 2],
data: Arc<Mutex<Option<Arc<ColorImage>>>>,
}
impl ImageHandle {
fn pull(&mut self) -> Option<Arc<ColorImage>> {
self.data.lock().unwrap().take()
}
}
pub struct ImageParams<T> {
value: T,
sink: Arc<Mutex<T>>,
}
impl<T> Deref for ImageParams<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T: Clone + Eq> ImageParams<T> {
pub fn write(&mut self, value: T) {
if self.value != value {
self.value = value.clone();
*self.sink.lock().unwrap() = value;
}
}
}
pub trait ImageRenderer<const N: usize>: Send {
type Params: Clone + Send;
fn sizes(&self) -> [[usize; 2]; N];
fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; N]);
}
#[derive(Clone)]
struct ImageState {
size: [usize; 2],
buffers: [Arc<ColorImage>; 2],
last_buffer: usize,
sink: Arc<Mutex<Option<Arc<ColorImage>>>>,
}
impl ImageState {
fn new(size: [usize; 2]) -> Self {
let buffers = [
Arc::new(ColorImage::new(size, Color32::BLACK)),
Arc::new(ColorImage::new(size, Color32::BLACK)),
];
let sink = buffers[0].clone();
Self {
size,
buffers,
last_buffer: 0,
sink: Arc::new(Mutex::new(Some(sink))),
}
}
fn try_send_update(&mut self, image: &ImageBuffer) {
let last = &self.buffers[self.last_buffer];
if !image.changed(last) {
return;
}
let next_buffer = (self.last_buffer + 1) % self.buffers.len();
let next = &mut self.buffers[next_buffer];
image.read(Arc::make_mut(next));
self.last_buffer = next_buffer;
self.sink.lock().unwrap().replace(next.clone());
}
}
struct ImageRendererWrapper<const N: usize, R: ImageRenderer<N>> {
renderer: R,
params: Weak<Mutex<R::Params>>,
images: [ImageBuffer; N],
states: [ImageState; N],
}
trait ImageRendererImpl: Send {
fn try_update(&mut self) -> Result<(), ()>;
}
impl<const N: usize, R: ImageRenderer<N> + Send> ImageRendererImpl for ImageRendererWrapper<N, R> {
fn try_update(&mut self) -> Result<(), ()> {
let params = match self.params.upgrade() {
Some(params) => params.lock().unwrap().clone(),
None => {
// the UI isn't using this anymore
return Err(());
}
};
self.renderer.render(&params, &mut self.images);
for (state, image) in self.states.iter_mut().zip(&self.images) {
state.try_send_update(image);
}
Ok(())
}
}
pub struct ImageTextureLoader {
cache: Mutex<HashMap<String, (ImageHandle, Option<TextureHandle>)>>,
}
impl ImageTextureLoader {
pub fn new(renderers: impl IntoIterator<Item = (String, ImageHandle)>) -> Self {
let mut cache = HashMap::new();
for (key, image) in renderers {
cache.insert(key, (image, None));
}
Self {
cache: Mutex::new(cache),
}
}
}
impl TextureLoader for ImageTextureLoader {
fn id(&self) -> &str {
concat!(module_path!(), "ImageTextureLoader")
}
fn load(
&self,
ctx: &egui::Context,
uri: &str,
texture_options: TextureOptions,
_size_hint: egui::SizeHint,
) -> Result<TexturePoll, LoadError> {
let mut cache = self.cache.lock().unwrap();
let Some((image, maybe_handle)) = cache.get_mut(uri) else {
return Err(LoadError::NotSupported);
};
if texture_options != TextureOptions::NEAREST {
return Err(LoadError::Loading(
"Only TextureOptions::NEAREST are supported".into(),
));
}
match (image.pull(), maybe_handle.as_ref()) {
(Some(update), Some(handle)) => {
let delta = ImageDelta::full(update, texture_options);
ctx.tex_manager().write().set(handle.id(), delta);
let texture = SizedTexture::new(handle, image.size);
Ok(TexturePoll::Ready { texture })
}
(Some(update), None) => {
let handle = ctx.load_texture(uri, update, texture_options);
let texture = SizedTexture::new(&handle, image.size);
maybe_handle.replace(handle);
Ok(TexturePoll::Ready { texture })
}
(None, Some(handle)) => {
let texture = SizedTexture::new(handle, image.size);
Ok(TexturePoll::Ready { texture })
}
(None, None) => {
let size = image.size.into();
Ok(TexturePoll::Pending { size: Some(size) })
}
}
}
fn forget(&self, uri: &str) {
let _ = uri;
}
fn forget_all(&self) {}
fn byte_size(&self) -> usize {
self.cache
.lock()
.unwrap()
.values()
.map(|(image, _)| {
let [width, height] = image.size;
width as usize * height as usize * 4
})
.sum()
}
}

View File

@ -1,14 +1,12 @@
use std::{
cmp::Ordering,
collections::{HashMap, hash_map::Entry},
collections::{hash_map::Entry, HashMap},
fmt::Display,
str::FromStr,
sync::{Arc, Mutex, RwLock},
sync::{Arc, RwLock},
};
use anyhow::anyhow;
use egui::{Event, Key, KeyboardShortcut, Modifiers};
use gilrs::{Axis, Button, Gamepad, GamepadId, ev::Code};
use gilrs::{ev::Code, Axis, Button, Gamepad, GamepadId};
use serde::{Deserialize, Serialize};
use winit::keyboard::{KeyCode, PhysicalKey};
@ -456,308 +454,3 @@ struct PersistedGamepadMapping {
default_buttons: Vec<(Code, VBKey)>,
default_axes: Vec<(Code, (VBKey, VBKey))>,
}
#[derive(Serialize, Deserialize)]
pub struct Shortcut {
pub shortcut: KeyboardShortcut,
pub command: Command,
}
#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum Command {
OpenRom,
Quit,
FrameAdvance,
FastForward(u32),
Reset,
PauseResume,
// if you update this, update Command::all and add a default
}
impl Command {
pub fn all() -> [Self; 6] {
[
Self::OpenRom,
Self::Quit,
Self::PauseResume,
Self::Reset,
Self::FrameAdvance,
Self::FastForward(0),
]
}
pub fn name(self) -> &'static str {
match self {
Self::OpenRom => "Open ROM",
Self::Quit => "Exit",
Self::PauseResume => "Pause/Resume",
Self::Reset => "Reset",
Self::FrameAdvance => "Frame Advance",
Self::FastForward(_) => "Fast Forward",
}
}
}
impl Display for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
struct Shortcuts {
all: Vec<(Command, KeyboardShortcut)>,
by_command: HashMap<Command, KeyboardShortcut>,
}
impl Default for Shortcuts {
fn default() -> Self {
let mut shortcuts = Shortcuts {
all: vec![],
by_command: HashMap::new(),
};
shortcuts.set(
Command::OpenRom,
KeyboardShortcut::new(Modifiers::COMMAND, Key::O),
);
shortcuts.set(
Command::Quit,
KeyboardShortcut::new(Modifiers::COMMAND, Key::Q),
);
shortcuts.set(
Command::PauseResume,
KeyboardShortcut::new(Modifiers::NONE, Key::F5),
);
shortcuts.set(
Command::Reset,
KeyboardShortcut::new(Modifiers::SHIFT, Key::F5),
);
shortcuts.set(
Command::FrameAdvance,
KeyboardShortcut::new(Modifiers::NONE, Key::F6),
);
shortcuts.set(
Command::FastForward(0),
KeyboardShortcut::new(Modifiers::NONE, Key::Space),
);
shortcuts
}
}
impl Shortcuts {
fn set(&mut self, command: Command, shortcut: KeyboardShortcut) {
if self.by_command.insert(command, shortcut).is_some() {
for (cmd, sht) in &mut self.all {
if *cmd == command {
*sht = shortcut;
break;
}
}
} else {
self.all.push((command, shortcut));
}
self.all.sort_by(|l, r| order_shortcut(l.1, r.1));
}
fn unset(&mut self, command: Command) {
if self.by_command.remove(&command).is_some() {
self.all.retain(|(c, _)| *c != command);
}
}
fn save(&self, saved: &mut PersistedSettings) {
for command in Command::all() {
let shortcut = self.by_command.get(&command).copied();
saved.shortcuts.push((command, shortcut));
}
}
}
fn order_shortcut(left: KeyboardShortcut, right: KeyboardShortcut) -> Ordering {
left.logical_key.cmp(&right.logical_key).then_with(|| {
specificity(left.modifiers)
.cmp(&specificity(right.modifiers))
.reverse()
})
}
fn specificity(modifiers: egui::Modifiers) -> usize {
let mut mods = 0;
if modifiers.alt {
mods += 1;
}
if modifiers.command || modifiers.ctrl {
mods += 1;
}
if modifiers.shift {
mods += 1;
}
mods
}
#[derive(Serialize, Deserialize, Default)]
struct PersistedSettings {
shortcuts: Vec<(Command, Option<KeyboardShortcut>)>,
#[serde(default)]
ff_settings: FastForwardSettings,
}
#[derive(Default, Clone)]
struct ShortcutState {
ff_toggled: bool,
}
#[derive(Default)]
struct Settings {
shortcuts: Shortcuts,
ff_settings: FastForwardSettings,
state: ShortcutState,
}
impl Settings {
fn save(&self) -> PersistedSettings {
let mut saved = PersistedSettings {
shortcuts: vec![],
ff_settings: self.ff_settings.clone(),
};
self.shortcuts.save(&mut saved);
saved
}
}
#[derive(Clone)]
pub struct ShortcutProvider {
persistence: Persistence,
settings: Arc<Mutex<Settings>>,
}
impl ShortcutProvider {
pub fn new(persistence: Persistence) -> Self {
let mut settings = Settings::default();
if let Ok(saved) = persistence.load_config::<PersistedSettings>("shortcuts") {
for (command, shortcut) in saved.shortcuts {
if let Some(shortcut) = shortcut {
settings.shortcuts.set(command, shortcut);
} else {
settings.shortcuts.unset(command);
}
}
settings.ff_settings = saved.ff_settings;
};
Self {
persistence,
settings: Arc::new(Mutex::new(settings)),
}
}
pub fn shortcut_for(&self, command: Command) -> Option<KeyboardShortcut> {
let lock = self.settings.lock().unwrap();
lock.shortcuts.by_command.get(&command).copied()
}
pub fn ff_settings(&self) -> FastForwardSettings {
let lock = self.settings.lock().unwrap();
lock.ff_settings.clone()
}
pub fn consume_all(&self, input: &mut egui::InputState) -> Vec<Command> {
let mut lock = self.settings.lock().unwrap();
let mut state = lock.state.clone();
let mut consumed = vec![];
for (command, shortcut) in &lock.shortcuts.all {
input.events.retain(|event| {
let Event::Key {
key,
pressed,
repeat,
modifiers,
..
} = event
else {
return true;
};
if shortcut.logical_key != *key || !shortcut.modifiers.contains(*modifiers) {
return true;
}
if matches!(command, Command::FastForward(_)) {
if *repeat {
return true;
}
let sped_up = if lock.ff_settings.toggle {
if !*pressed {
return true;
}
state.ff_toggled = !state.ff_toggled;
state.ff_toggled
} else {
*pressed
};
let speed = if sped_up { lock.ff_settings.speed } else { 1 };
consumed.push(Command::FastForward(speed));
false
} else {
if !*pressed {
return true;
}
consumed.push(*command);
false
}
});
}
lock.state = state;
consumed
}
pub fn set(&self, command: Command, shortcut: KeyboardShortcut) {
let updated = {
let mut lock = self.settings.lock().unwrap();
lock.shortcuts.set(command, shortcut);
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
}
pub fn unset(&self, command: Command) {
let updated = {
let mut lock = self.settings.lock().unwrap();
lock.shortcuts.unset(command);
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
}
pub fn update_ff_settings(&self, ff_settings: FastForwardSettings) {
let updated = {
let mut lock = self.settings.lock().unwrap();
lock.ff_settings = ff_settings;
if !lock.ff_settings.toggle {
lock.state.ff_toggled = false;
}
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
}
pub fn reset(&self) {
let updated = {
let mut lock = self.settings.lock().unwrap();
*lock = Settings::default();
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct FastForwardSettings {
pub toggle: bool,
pub speed: u32,
}
impl Default for FastForwardSettings {
fn default() -> Self {
Self {
toggle: false,
speed: 10,
}
}
}

View File

@ -3,41 +3,25 @@
use std::{path::PathBuf, process, time::SystemTime};
use anyhow::{Result, bail};
use anyhow::Result;
use app::Application;
use clap::Parser;
use emulator::EmulatorBuilder;
use thread_priority::{ThreadBuilder, ThreadPriority};
use tracing::error;
use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt};
use winit::event_loop::{ControlFlow, EventLoop};
mod app;
mod audio;
mod controller;
mod emulator;
mod gdbserver;
mod graphics;
mod images;
mod input;
mod memory;
mod persistence;
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() {
@ -87,8 +71,6 @@ fn set_process_priority_to_high() -> Result<()> {
}
fn main() -> Result<()> {
init_logger();
set_panic_handler();
#[cfg(windows)]
@ -97,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()
@ -113,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);
}
};
@ -124,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,276 +0,0 @@
use std::{
collections::HashMap,
fmt::Debug,
iter::FusedIterator,
sync::{Arc, Mutex, RwLock, RwLockReadGuard, TryLockError, Weak, atomic::AtomicU64},
};
use bytemuck::BoxBytes;
use itertools::Itertools;
use tracing::warn;
use crate::emulator::{EmulatorClient, EmulatorCommand, SimId};
pub struct MemoryClient {
client: EmulatorClient,
regions: Mutex<HashMap<MemoryRange, Weak<MemoryRegion>>>,
}
impl MemoryClient {
pub fn new(client: EmulatorClient) -> Self {
Self {
client,
regions: Mutex::new(HashMap::new()),
}
}
pub fn watch(&self, sim: SimId, start: u32, length: usize) -> MemoryView {
let range = MemoryRange { sim, start, length };
let mut regions = self.regions.lock().unwrap_or_else(|e| e.into_inner());
let region = regions
.get(&range)
.and_then(|r| r.upgrade())
.unwrap_or_else(|| {
let region = Arc::new(MemoryRegion::new(start, length));
regions.insert(range, Arc::downgrade(&region));
self.client
.send_command(EmulatorCommand::WatchMemory(range, Arc::downgrade(&region)));
region
});
MemoryView { region }
}
pub fn write<T: MemoryValue>(&self, sim: SimId, address: u32, data: &T) {
let mut buffer = vec![];
data.to_bytes(&mut buffer);
let (tx, _) = oneshot::channel();
self.client
.send_command(EmulatorCommand::WriteMemory(sim, address, buffer, tx));
}
}
fn aligned_memory(start: u32, length: usize) -> BoxBytes {
if start % 4 == 0 && length % 4 == 0 {
let memory = vec![0u32; length / 4].into_boxed_slice();
return bytemuck::box_bytes_of(memory);
}
if start % 2 == 0 && length % 2 == 0 {
let memory = vec![0u16; length / 2].into_boxed_slice();
return bytemuck::box_bytes_of(memory);
}
let memory = vec![0u8; length].into_boxed_slice();
bytemuck::box_bytes_of(memory)
}
pub struct MemoryView {
region: Arc<MemoryRegion>,
}
impl MemoryView {
pub fn borrow(&self) -> MemoryRef<'_> {
self.region.borrow()
}
}
pub struct MemoryRef<'a> {
inner: RwLockReadGuard<'a, BoxBytes>,
}
pub trait MemoryValue {
fn from_bytes(bytes: &[u8]) -> Self;
fn to_bytes(&self, buffer: &mut Vec<u8>);
}
macro_rules! primitive_memory_value_impl {
($T:ty, $L: expr) => {
impl MemoryValue for $T {
#[inline]
fn from_bytes(bytes: &[u8]) -> Self {
let bytes: [u8; std::mem::size_of::<$T>()] = std::array::from_fn(|i| bytes[i]);
<$T>::from_le_bytes(bytes)
}
#[inline]
fn to_bytes(&self, buffer: &mut Vec<u8>) {
buffer.extend_from_slice(&self.to_le_bytes())
}
}
};
}
primitive_memory_value_impl!(u8, 1);
primitive_memory_value_impl!(i8, 1);
primitive_memory_value_impl!(u16, 2);
primitive_memory_value_impl!(i16, 2);
primitive_memory_value_impl!(u32, 4);
primitive_memory_value_impl!(i32, 4);
impl<const N: usize, T: MemoryValue> MemoryValue for [T; N] {
#[inline]
fn from_bytes(bytes: &[u8]) -> Self {
std::array::from_fn(|i| {
T::from_bytes(&bytes[i * std::mem::size_of::<T>()..(i + 1) * std::mem::size_of::<T>()])
})
}
#[inline]
fn to_bytes(&self, buffer: &mut Vec<u8>) {
for item in self {
item.to_bytes(buffer);
}
}
}
pub struct MemoryIter<'a, T> {
bytes: &'a [u8],
_phantom: std::marker::PhantomData<T>,
}
impl<'a, T> MemoryIter<'a, T> {
fn new(bytes: &'a [u8]) -> Self {
Self {
bytes,
_phantom: std::marker::PhantomData,
}
}
}
impl<T: MemoryValue> Iterator for MemoryIter<'_, T> {
type Item = T;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let (bytes, rest) = self.bytes.split_at_checked(std::mem::size_of::<T>())?;
self.bytes = rest;
Some(T::from_bytes(bytes))
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
let size = self.bytes.len() / std::mem::size_of::<T>();
(size, Some(size))
}
}
impl<T: MemoryValue> DoubleEndedIterator for MemoryIter<'_, T> {
fn next_back(&mut self) -> Option<Self::Item> {
let mid = self.bytes.len().checked_sub(std::mem::size_of::<T>())?;
// SAFETY: the checked_sub above is effectively a bounds check
let (rest, bytes) = unsafe { self.bytes.split_at_unchecked(mid) };
self.bytes = rest;
Some(T::from_bytes(bytes))
}
}
impl<T: MemoryValue> FusedIterator for MemoryIter<'_, T> {}
impl MemoryRef<'_> {
pub fn read<T: MemoryValue>(&self, index: usize) -> T {
let from = index * size_of::<T>();
let to = from + size_of::<T>();
T::from_bytes(&self.inner[from..to])
}
pub fn range<T: MemoryValue>(&self, start: usize, count: usize) -> MemoryIter<T> {
let from = start * size_of::<T>();
let to = from + (count * size_of::<T>());
MemoryIter::new(&self.inner[from..to])
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct MemoryRange {
pub sim: SimId,
pub start: u32,
pub length: usize,
}
const BUFFERS: usize = 4;
pub struct MemoryRegion {
gens: [AtomicU64; BUFFERS],
bufs: [RwLock<BoxBytes>; BUFFERS],
}
impl Debug for MemoryRegion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MemoryRegion")
.field("gens", &self.gens)
.finish_non_exhaustive()
}
}
// SAFETY: BoxBytes is meant to be Send+Sync, will be in a future version
unsafe impl Send for MemoryRegion {}
// SAFETY: BoxBytes is meant to be Send+Sync, will be in a future version
unsafe impl Sync for MemoryRegion {}
impl MemoryRegion {
fn new(start: u32, length: usize) -> Self {
Self {
gens: std::array::from_fn(|i| AtomicU64::new(i as u64)),
bufs: std::array::from_fn(|_| RwLock::new(aligned_memory(start, length))),
}
}
pub fn borrow(&self) -> MemoryRef<'_> {
/*
* When reading memory, a thread will grab the newest buffer (with the highest gen)
* It will only fail to grab the lock if the writer already has it,
* but the writer prioritizes older buffers (with lower gens).
* So this method will only block if the writer produces three full buffers
* in the time it takes the reader to do four atomic reads and grab a lock.
* In the unlikely event this happens... just try again.
*/
loop {
let newest_index = self
.gens
.iter()
.map(|i| i.load(std::sync::atomic::Ordering::Acquire))
.enumerate()
.max_by_key(|(_, g)| *g)
.map(|(i, _)| i)
.unwrap();
let inner = match self.bufs[newest_index].try_read() {
Ok(inner) => inner,
Err(TryLockError::Poisoned(e)) => e.into_inner(),
Err(TryLockError::WouldBlock) => {
continue;
}
};
break MemoryRef { inner };
}
}
pub fn update(&self, data: &[u8]) {
let gens = self
.gens
.each_ref()
.map(|i| i.load(std::sync::atomic::Ordering::Acquire));
let next_gen = gens.iter().max().unwrap() + 1;
let indices = gens
.into_iter()
.enumerate()
.sorted_by_key(|(_, val)| *val)
.map(|(i, _)| i);
for index in indices {
let mut lock = match self.bufs[index].try_write() {
Ok(inner) => inner,
Err(TryLockError::Poisoned(e)) => e.into_inner(),
Err(TryLockError::WouldBlock) => {
continue;
}
};
lock.copy_from_slice(data);
self.gens[index].store(next_gen, std::sync::atomic::Ordering::Release);
return;
}
/*
* We have four buffers, and (at time of writing) only three threads interacting with memory:
* - The UI thread, reading small regions of memory
* - The "image renderer" thread, reading large regions of memory
* - The emulation thread, writing memory every so often
* So it should be impossible for all four buffers to have a read lock at the same time,
* and (because readers always read the newest buffer) at least one of the oldest three
* buffers will be free the entire time we're in this method.
* TL;DR this should never happen.
* But if it does, do nothing. This isn't medical software, better to show stale data than crash.
*/
warn!("all buffers were locked by a reader at the same time")
}
}

View File

@ -1,6 +1,6 @@
use std::{fs, path::PathBuf};
use anyhow::{Result, bail};
use anyhow::{bail, Result};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};

View File

@ -1,43 +1,26 @@
pub use about::AboutWindow;
use egui::{Context, ViewportBuilder, ViewportId};
pub use game::GameWindow;
pub use gdb::GdbServerWindow;
pub use hotkeys::HotkeysWindow;
pub use input::InputWindow;
pub use vip::{
BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow,
};
use winit::event::KeyEvent;
use crate::emulator::SimId;
mod about;
mod game;
mod game_screen;
mod gdb;
mod hotkeys;
mod input;
mod utils;
mod vip;
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, ctx: &Context, render_state: &egui_wgpu::RenderState) {
let _ = ctx;
fn on_init(&mut self, render_state: &egui_wgpu::RenderState) {
let _ = render_state;
}
fn on_destroy(&mut self) {}
fn handle_key_event(&mut self, event: &KeyEvent) -> bool {
fn handle_key_event(&mut self, event: &KeyEvent) {
let _ = event;
false
}
fn handle_gamepad_event(&mut self, event: &gilrs::Event) -> bool {
fn handle_gamepad_event(&mut self, event: &gilrs::Event) {
let _ = event;
false
}
}

View File

@ -2,22 +2,21 @@ use std::sync::mpsc;
use crate::{
app::UserEvent,
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
input::{Command, ShortcutProvider},
emulator::{EmulatorClient, EmulatorCommand, SimId},
persistence::Persistence,
};
use egui::{
Align2, Button, CentralPanel, Color32, Context, Direction, Frame, TopBottomPanel, Ui, Vec2,
ViewportBuilder, ViewportCommand, ViewportId, Window, menu,
ecolor::HexColor, menu, Align2, Button, CentralPanel, Color32, Context, Direction, Frame,
Layout, Response, Sense, TopBottomPanel, Ui, Vec2, ViewportBuilder, ViewportCommand,
ViewportId, WidgetText, Window,
};
use egui_toast::{Toast, Toasts};
use serde::{Deserialize, Serialize};
use winit::event_loop::EventLoopProxy;
use super::{
AppWindow,
game_screen::{DisplayMode, GameScreen},
utils::UiExt as _,
AppWindow,
};
const COLOR_PRESETS: [[Color32; 2]; 3] = [
@ -39,7 +38,6 @@ pub struct GameWindow {
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
shortcuts: ShortcutProvider,
sim_id: SimId,
config: GameConfig,
screen: Option<GameScreen>,
@ -52,7 +50,6 @@ impl GameWindow {
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
shortcuts: ShortcutProvider,
sim_id: SimId,
) -> Self {
let config = load_config(&persistence, sim_id);
@ -60,7 +57,6 @@ impl GameWindow {
client,
proxy,
persistence,
shortcuts,
sim_id,
config,
screen: None,
@ -70,120 +66,42 @@ impl GameWindow {
}
fn show_menu(&mut self, ctx: &Context, ui: &mut 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;
let can_frame_advance = is_ready && state != EmulatorState::Debugging;
for command in ui.input_mut(|input| self.shortcuts.consume_all(input)) {
match command {
Command::OpenRom => {
let rom = rfd::FileDialog::new()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
.pick_file();
if let Some(path) = rom {
self.client
.send_command(EmulatorCommand::LoadGame(self.sim_id, path));
}
}
Command::Quit => {
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
}
Command::PauseResume => {
if state == EmulatorState::Paused && can_resume {
self.client.send_command(EmulatorCommand::Resume);
}
if state == EmulatorState::Running && can_pause {
self.client.send_command(EmulatorCommand::Pause);
}
}
Command::Reset => {
if is_ready {
self.client
.send_command(EmulatorCommand::Reset(self.sim_id));
}
}
Command::FrameAdvance => {
if can_frame_advance {
self.client.send_command(EmulatorCommand::FrameAdvance);
}
}
Command::FastForward(speed) => {
self.client
.send_command(EmulatorCommand::SetSpeed(speed as f64));
}
}
}
ui.menu_button("ROM", |ui| {
if ui
.add(self.button_for(ui.ctx(), "Open ROM", Command::OpenRom))
.clicked()
{
if ui.button("Open ROM").clicked() {
let rom = rfd::FileDialog::new()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
.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
.add(self.button_for(ui.ctx(), "Quit", Command::Quit))
.clicked()
{
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
if ui.button("Quit").clicked() {
ctx.send_viewport_cmd(ViewportCommand::Close);
}
});
ui.menu_button("Emulation", |ui| {
if state == EmulatorState::Running {
if ui
.add_enabled(
can_pause,
self.button_for(ui.ctx(), "Pause", Command::PauseResume),
)
.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,
self.button_for(ui.ctx(), "Resume", Command::PauseResume),
)
.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, self.button_for(ui.ctx(), "Reset", Command::Reset))
.clicked()
{
if ui.add_enabled(has_game, Button::new("Reset")).clicked() {
self.client
.send_command(EmulatorCommand::Reset(self.sim_id));
ui.close_menu();
}
ui.separator();
if ui
.add_enabled(
can_frame_advance,
self.button_for(ui.ctx(), "Frame Advance", Command::FrameAdvance),
)
.clicked()
{
self.client.send_command(EmulatorCommand::FrameAdvance);
ui.close_menu();
}
});
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
@ -191,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);
@ -203,63 +121,17 @@ 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.separator();
if ui.button("Character Data").clicked() {
self.proxy
.send_event(UserEvent::OpenCharacterData(self.sim_id))
.unwrap();
ui.close_menu();
}
if ui.button("Background Maps").clicked() {
self.proxy
.send_event(UserEvent::OpenBgMap(self.sim_id))
.unwrap();
ui.close_menu();
}
if ui.button("Objects").clicked() {
self.proxy
.send_event(UserEvent::OpenObjects(self.sim_id))
.unwrap();
ui.close_menu();
}
if ui.button("Worlds").clicked() {
self.proxy
.send_event(UserEvent::OpenWorlds(self.sim_id))
.unwrap();
ui.close_menu();
}
if ui.button("Frame Buffers").clicked() {
self.proxy
.send_event(UserEvent::OpenFrameBuffers(self.sim_id))
.unwrap();
ui.close_menu();
}
if ui.button("Registers").clicked() {
self.proxy
.send_event(UserEvent::OpenRegisters(self.sim_id))
.unwrap();
ui.close_menu();
}
});
ui.menu_button("Help", |ui| {
if ui.button("About").clicked() {
self.proxy.send_event(UserEvent::OpenAbout).unwrap();
ui.close_menu();
}
ui.menu_button("About", |ui| {
self.proxy.send_event(UserEvent::OpenAbout).unwrap();
ui.close_menu();
});
}
fn show_options_menu(&mut self, ctx: &Context, ui: &mut Ui) {
ui.menu_button("Video", |ui| {
ui.menu_button("Screen Size", |ui| {
let current_dims = self.config.dimensions;
let current_dims = ctx.input(|i| i.viewport().inner_rect.unwrap());
let current_dims = current_dims.max - current_dims.min;
for scale in 1..=4 {
let label = format!("x{scale}");
@ -299,7 +171,10 @@ impl GameWindow {
return;
}
let current_dims = self.config.dimensions;
let current_dims = {
let viewport = ctx.input(|i| i.viewport().inner_rect.unwrap());
viewport.max - viewport.min
};
let new_proportions = display_mode.proportions();
let scale = new_proportions / old_proportions;
if scale != Vec2::new(1.0, 1.0) {
@ -324,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);
}
@ -362,10 +237,6 @@ impl GameWindow {
ui.close_menu();
}
});
if ui.button("Hotkeys").clicked() {
self.proxy.send_event(UserEvent::OpenHotkeys).unwrap();
ui.close_menu();
}
}
fn show_color_picker(&mut self, ui: &mut Ui) {
@ -407,14 +278,6 @@ impl GameWindow {
}
self.config = new_config;
}
fn button_for(&self, ctx: &Context, text: &str, command: Command) -> Button {
let button = Button::new(text);
match self.shortcuts.shortcut_for(command) {
Some(shortcut) => button.shortcut_text(ctx.format_shortcut(&shortcut)),
None => button,
}
}
}
fn config_filename(sim_id: SimId) -> &'static str {
@ -443,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")
@ -455,7 +314,7 @@ impl AppWindow for GameWindow {
fn show(&mut self, ctx: &Context) {
let dimensions = {
let bounds = ctx.available_rect();
let bounds = ctx.input(|i| i.viewport().inner_rect.unwrap());
bounds.max - bounds.min
};
self.update_config(|c| c.dimensions = dimensions);
@ -494,7 +353,7 @@ impl AppWindow for GameWindow {
toasts.show(ctx);
}
fn on_init(&mut self, _ctx: &Context, render_state: &egui_wgpu::RenderState) {
fn on_init(&mut self, render_state: &egui_wgpu::RenderState) {
let (screen, sink) = GameScreen::init(render_state);
let (message_sink, message_source) = mpsc::channel();
self.client.send_command(EmulatorCommand::ConnectToSim(
@ -510,7 +369,69 @@ 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));
}
}
trait UiExt {
fn selectable_button(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response;
fn selectable_option<T: Eq>(
&mut self,
current_value: &mut T,
selected_value: T,
text: impl Into<WidgetText>,
) -> Response {
let response = self.selectable_button(*current_value == selected_value, text);
if response.clicked() {
*current_value = selected_value;
}
response
}
fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response;
fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response;
}
impl UiExt for Ui {
fn selectable_button(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response {
self.style_mut().visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
self.style_mut().visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT;
self.style_mut().visuals.widgets.active.bg_fill = Color32::TRANSPARENT;
let mut selected = selected;
self.checkbox(&mut selected, text)
}
fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response {
let button_size = Vec2::new(60.0, 20.0);
let (rect, response) = self.allocate_at_least(button_size, Sense::click());
let center_x = rect.center().x;
let left_rect = rect.with_max_x(center_x);
self.painter().rect_filled(left_rect, 0.0, left);
let right_rect = rect.with_min_x(center_x);
self.painter().rect_filled(right_rect, 0.0, right);
let style = self.style().interact(&response);
self.painter().rect_stroke(rect, 0.0, style.fg_stroke);
response
}
fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response {
self.allocate_ui_with_layout(
Vec2::new(100.0, 130.0),
Layout::top_down_justified(egui::Align::Center),
|ui| {
let (rect, _) = ui.allocate_at_least(Vec2::new(100.0, 100.0), Sense::hover());
ui.painter().rect_filled(rect, 0.0, *color);
let resp = ui.text_edit_singleline(hex);
if resp.changed() {
if let Ok(new_color) = HexColor::from_str_without_hash(hex) {
*color = new_color.color();
}
}
resp
},
)
.inner
}
}

View File

@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc};
use egui::{Color32, Rgba, Vec2, Widget};
use serde::{Deserialize, Serialize};
use wgpu::{BindGroup, BindGroupLayout, Buffer, RenderPipeline, util::DeviceExt as _};
use wgpu::{util::DeviceExt as _, BindGroup, BindGroupLayout, Buffer, RenderPipeline};
use crate::graphics::TextureSink;
@ -71,7 +71,7 @@ impl GameScreen {
module: &shader,
entry_point: Some(entry_point),
targets: &[Some(wgpu::ColorTargetState {
format: render_state.target_format,
format: wgpu::TextureFormat::Bgra8Unorm,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],

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);
}
}
});
}
}

View File

@ -1,150 +0,0 @@
use egui::{
Button, CentralPanel, Context, Event, KeyboardShortcut, Label, Layout, Slider, Ui,
ViewportBuilder, ViewportId,
};
use egui_extras::{Column, TableBuilder};
use crate::input::{Command, ShortcutProvider};
use super::{AppWindow, utils::UiExt};
pub struct HotkeysWindow {
shortcuts: ShortcutProvider,
now_binding: Option<Command>,
}
impl HotkeysWindow {
pub fn new(shortcuts: ShortcutProvider) -> Self {
Self {
shortcuts,
now_binding: None,
}
}
fn show_shortcuts(&mut self, ui: &mut Ui) {
let row_height = ui.spacing().interact_size.y;
ui.section("Shortcuts", |ui| {
let width = ui.available_width() - 16.0;
TableBuilder::new(ui)
.column(Column::exact(width * 0.3))
.column(Column::exact(width * 0.5))
.column(Column::exact(width * 0.2))
.cell_layout(Layout::left_to_right(egui::Align::Center))
.body(|mut body| {
for command in Command::all() {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_sized(ui.available_size(), Label::new(command.name()));
});
row.col(|ui| {
let button = if self.now_binding == Some(command) {
Button::new("Binding...")
} else if let Some(shortcut) = self.shortcuts.shortcut_for(command)
{
Button::new(ui.ctx().format_shortcut(&shortcut))
} else {
Button::new("")
};
if ui.add_sized(ui.available_size(), button).clicked() {
self.now_binding = Some(command);
}
});
row.col(|ui| {
if ui
.add_sized(ui.available_size(), Button::new("Clear"))
.clicked()
{
self.shortcuts.unset(command);
self.now_binding = None;
}
});
});
}
});
});
if let Some(command) = self.now_binding {
if let Some(shortcut) = ui.input_mut(|i| {
i.events.iter().find_map(|event| match event {
Event::Key {
key,
pressed: true,
modifiers,
..
} => Some(KeyboardShortcut::new(*modifiers, *key)),
_ => None,
})
}) {
self.shortcuts.set(command, shortcut);
self.now_binding = None;
}
}
}
fn show_ff_settings(&mut self, ui: &mut Ui) {
let row_height = ui.spacing().interact_size.y;
ui.section("Fast Forward", |ui| {
let width = ui.available_width() - 8.0;
let mut ff_settings = self.shortcuts.ff_settings();
let mut updated = false;
TableBuilder::new(ui)
.column(Column::exact(width * 0.5))
.column(Column::exact(width * 0.5))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Treat button as toggle");
});
row.col(|ui| {
if ui.checkbox(&mut ff_settings.toggle, "").changed() {
updated = true;
}
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Speed multiplier");
});
row.col(|ui| {
if ui
.add_sized(
ui.available_size(),
Slider::new(&mut ff_settings.speed, 1..=15),
)
.changed()
{
updated = true;
}
});
});
});
if updated {
self.shortcuts.update_ff_settings(ff_settings);
}
});
}
}
impl AppWindow for HotkeysWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of("shortcuts")
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Keyboard Shortcuts")
.with_inner_size((400.0, 400.0))
}
fn show(&mut self, ctx: &Context) {
CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
if ui.button("Use defaults").clicked() {
self.shortcuts.reset();
}
});
ui.separator();
self.show_shortcuts(ui);
self.show_ff_settings(ui);
});
}
}

View File

@ -198,63 +198,58 @@ impl AppWindow for InputWindow {
});
}
fn handle_key_event(&mut self, event: &winit::event::KeyEvent) -> bool {
fn handle_key_event(&mut self, event: &winit::event::KeyEvent) {
if !event.state.is_pressed() {
return false;
return;
}
let sim_id = match self.active_tab {
InputTab::Player1 => SimId::Player1,
InputTab::Player2 => SimId::Player2,
_ => {
return false;
return;
}
};
let Some(vb) = self.now_binding.take() else {
return false;
return;
};
let mut mappings = self.mappings.for_sim(sim_id).write().unwrap();
mappings.add_keyboard_mapping(vb, event.physical_key);
drop(mappings);
self.mappings.save();
true
}
fn handle_gamepad_event(&mut self, event: &gilrs::Event) -> bool {
fn handle_gamepad_event(&mut self, event: &gilrs::Event) {
let InputTab::RebindGamepad(gamepad_id) = self.active_tab else {
return false;
return;
};
if gamepad_id != event.id {
return false;
return;
}
let Some(mappings) = self.mappings.for_gamepad(gamepad_id) else {
return false;
return;
};
let Some(vb) = self.now_binding else {
return false;
return;
};
match event.event {
EventType::ButtonPressed(_, code) => {
let mut mapping = mappings.write().unwrap();
mapping.add_button_mapping(vb, code);
self.now_binding.take();
true
}
EventType::AxisChanged(_, value, code) => {
if value < -0.75 {
let mut mapping = mappings.write().unwrap();
mapping.add_axis_neg_mapping(vb, code);
self.now_binding.take();
true
} else if value > 0.75 {
}
if value > 0.75 {
let mut mapping = mappings.write().unwrap();
mapping.add_axis_pos_mapping(vb, code);
self.now_binding.take();
true
} else {
false
}
}
_ => false,
_ => {}
}
}
}

View File

@ -1,417 +0,0 @@
use std::{
fmt::{Display, UpperHex},
ops::{Bound, RangeBounds},
str::FromStr,
};
use atoi::FromRadix16;
use egui::{
Align, Color32, CursorIcon, Event, Frame, Key, Layout, Margin, Rect, Response, RichText,
Rounding, Sense, Shape, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, WidgetText,
ecolor::HexColor,
};
use num_traits::{CheckedAdd, CheckedSub, One};
pub trait UiExt {
fn section(&mut self, title: impl Into<String>, add_contents: impl FnOnce(&mut Ui));
fn selectable_button(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response;
fn selectable_option<T: Eq>(
&mut self,
current_value: &mut T,
selected_value: T,
text: impl Into<WidgetText>,
) -> Response {
let response = self.selectable_button(*current_value == selected_value, text);
if response.clicked() {
*current_value = selected_value;
}
response
}
fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response;
fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response;
}
impl UiExt for Ui {
fn section(&mut self, title: impl Into<String>, add_contents: impl FnOnce(&mut Ui)) {
let title: String = title.into();
let mut frame = Frame::group(self.style());
frame.outer_margin.top += 10.0;
frame.inner_margin.top += 2.0;
let res = self.push_id(&title, |ui| {
frame.show(ui, |ui| {
ui.set_max_width(ui.available_width());
add_contents(ui);
})
});
let text = RichText::new(title).background_color(self.style().visuals.panel_fill);
let old_rect = res.response.rect;
let mut text_rect = old_rect;
text_rect.min.x += 6.0;
self.allocate_new_ui(UiBuilder::new().max_rect(text_rect), |ui| ui.label(text));
if old_rect.width() > 0.0 {
self.advance_cursor_after_rect(old_rect);
}
}
fn selectable_button(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response {
self.style_mut().visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
self.style_mut().visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT;
self.style_mut().visuals.widgets.active.bg_fill = Color32::TRANSPARENT;
let mut selected = selected;
self.checkbox(&mut selected, text)
}
fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response {
let button_size = Vec2::new(60.0, 20.0);
let (rect, response) = self.allocate_at_least(button_size, Sense::click());
let center_x = rect.center().x;
let left_rect = rect.with_max_x(center_x);
self.painter().rect_filled(left_rect, 0.0, left);
let right_rect = rect.with_min_x(center_x);
self.painter().rect_filled(right_rect, 0.0, right);
let style = self.style().interact(&response);
self.painter().rect_stroke(rect, 0.0, style.fg_stroke);
response
}
fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response {
self.allocate_ui_with_layout(
Vec2::new(100.0, 130.0),
Layout::top_down_justified(egui::Align::Center),
|ui| {
let (rect, _) = ui.allocate_at_least(Vec2::new(100.0, 100.0), Sense::hover());
ui.painter().rect_filled(rect, 0.0, *color);
let resp = ui.text_edit_singleline(hex);
if resp.changed() {
if let Ok(new_color) = HexColor::from_str_without_hash(hex) {
*color = new_color.color();
}
}
resp
},
)
.inner
}
}
enum Direction {
Up,
Down,
}
pub trait Number:
Copy
+ One
+ CheckedAdd
+ CheckedSub
+ Eq
+ Ord
+ Display
+ FromStr
+ FromRadix16
+ UpperHex
+ Send
+ Sync
+ 'static
{
}
impl<
T: Copy
+ One
+ CheckedAdd
+ CheckedSub
+ Eq
+ Ord
+ Display
+ FromStr
+ FromRadix16
+ UpperHex
+ Send
+ Sync
+ 'static,
> Number for T
{
}
pub struct NumberEdit<'a, T: Number> {
value: &'a mut T,
increment: T,
precision: usize,
min: Option<T>,
max: Option<T>,
desired_width: Option<f32>,
arrows: bool,
hex: bool,
}
impl<'a, T: Number> NumberEdit<'a, T> {
pub fn new(value: &'a mut T) -> Self {
Self {
value,
increment: T::one(),
precision: 3,
min: None,
max: None,
desired_width: None,
arrows: true,
hex: false,
}
}
pub fn precision(self, precision: usize) -> Self {
Self { precision, ..self }
}
pub fn range(self, range: impl RangeBounds<T>) -> Self {
let min = match range.start_bound() {
Bound::Unbounded => None,
Bound::Included(t) => Some(*t),
Bound::Excluded(t) => t.checked_add(&self.increment),
};
let max = match range.end_bound() {
Bound::Unbounded => None,
Bound::Included(t) => Some(*t),
Bound::Excluded(t) => t.checked_sub(&self.increment),
};
Self { min, max, ..self }
}
pub fn desired_width(self, desired_width: f32) -> Self {
Self {
desired_width: Some(desired_width),
..self
}
}
pub fn arrows(self, arrows: bool) -> Self {
Self { arrows, ..self }
}
pub fn hex(self, hex: bool) -> Self {
Self { hex, ..self }
}
}
impl<T: Number> Widget for NumberEdit<'_, T> {
fn ui(self, ui: &mut Ui) -> Response {
let id = ui.id();
let to_string = |val: &T| {
if self.hex {
format!("{val:.0$X}", self.precision)
} else {
format!("{val:.0$}", self.precision)
}
};
let from_string = |val: &str| {
if self.hex {
let bytes = val.as_bytes();
let (result, consumed) = T::from_radix_16(bytes);
(consumed == bytes.len()).then_some(result)
} else {
val.parse::<T>().ok()
}
};
let (last_value, mut str, focus) = ui.memory(|m| {
let (lv, s) = m
.data
.get_temp(id)
.unwrap_or((*self.value, to_string(self.value)));
let focus = m.has_focus(id);
(lv, s, focus)
});
let mut stale = false;
if *self.value != last_value {
str = to_string(self.value);
stale = true;
}
let valid = from_string(&str).is_some_and(|v: T| v == *self.value);
let mut up_pressed = false;
let mut down_pressed = false;
if focus {
ui.input_mut(|i| {
i.events.retain(|e| match e {
Event::Key {
key: Key::ArrowUp,
pressed: true,
..
} => {
up_pressed = true;
false
}
Event::Key {
key: Key::ArrowDown,
pressed: true,
..
} => {
down_pressed = true;
false
}
_ => true,
})
});
}
let mut desired_width = self
.desired_width
.unwrap_or_else(|| ui.spacing().text_edit_width);
if self.arrows {
desired_width -= 16.0;
}
let text = TextEdit::singleline(&mut str)
.horizontal_align(Align::Max)
.id(id)
.desired_width(desired_width)
.margin(Margin {
left: 4.0,
right: if self.arrows { 20.0 } else { 4.0 },
top: 2.0,
bottom: 2.0,
});
let mut res = if valid {
ui.add(text)
} else {
let message = match (self.min, self.max) {
(Some(min), Some(max)) => format!(
"Please enter a number between {} and {}.",
to_string(&min),
to_string(&max)
),
(Some(min), None) => {
format!("Please enter a number greater than {}.", to_string(&min))
}
(None, Some(max)) => {
format!("Please enter a number less than {}.", to_string(&max))
}
(None, None) => "Please enter a number.".to_string(),
};
ui.scope(|ui| {
let style = ui.style_mut();
style.visuals.selection.stroke.color = style.visuals.error_fg_color;
style.visuals.widgets.hovered.bg_stroke.color =
style.visuals.error_fg_color.gamma_multiply(0.60);
ui.add(text)
})
.inner
.on_hover_text(message)
};
let mut delta = None;
if self.arrows {
let arrow_left = res.rect.max.x + 4.0;
let arrow_right = res.rect.max.x + 20.0;
let arrow_top = res.rect.min.y - 2.0;
let arrow_middle = (res.rect.min.y + res.rect.max.y) / 2.0;
let arrow_bottom = res.rect.max.y + 2.0;
let top_arrow_rect = Rect {
min: (arrow_left, arrow_top).into(),
max: (arrow_right, arrow_middle).into(),
};
if draw_arrow(ui, top_arrow_rect, true).clicked_or_dragged() || up_pressed {
delta = Some(Direction::Up);
}
let bottom_arrow_rect = Rect {
min: (arrow_left, arrow_middle).into(),
max: (arrow_right, arrow_bottom).into(),
};
if draw_arrow(ui, bottom_arrow_rect, false).clicked_or_dragged() || down_pressed {
delta = Some(Direction::Down);
}
}
let in_range =
|val: &T| self.min.is_none_or(|m| &m <= val) && self.max.is_none_or(|m| &m >= val);
if let Some(dir) = delta {
let value = match dir {
Direction::Up => self.value.checked_add(&self.increment),
Direction::Down => self.value.checked_sub(&self.increment),
};
if let Some(new_value) = value.filter(in_range) {
if *self.value != new_value {
res.mark_changed();
}
*self.value = new_value;
}
str = to_string(self.value);
stale = true;
} else if res.changed {
if let Some(new_value) = from_string(&str).filter(in_range) {
if *self.value != new_value {
res.mark_changed();
}
*self.value = new_value;
}
stale = true;
}
if stale {
ui.memory_mut(|m| m.data.insert_temp(id, (*self.value, str)));
}
res
}
}
fn draw_arrow(ui: &mut Ui, rect: Rect, up: bool) -> Response {
let arrow_res = ui
.allocate_rect(
rect,
Sense {
click: true,
drag: true,
focusable: false,
},
)
.on_hover_cursor(CursorIcon::Default);
let visuals = ui.style().visuals.widgets.style(&arrow_res);
let painter = ui.painter_at(arrow_res.rect);
let rounding = if up {
Rounding {
ne: 2.0,
..Rounding::ZERO
}
} else {
Rounding {
se: 2.0,
..Rounding::ZERO
}
};
painter.rect_filled(arrow_res.rect, rounding, visuals.bg_fill);
let left = rect.left() + 4.0;
let center = (rect.left() + rect.right()) / 2.0;
let right = rect.right() - 4.0;
let top = rect.top() + 3.0;
let bottom = rect.bottom() - 3.0;
let points = if up {
vec![
(left, bottom).into(),
(center, top).into(),
(right, bottom).into(),
]
} else {
vec![
(right, top).into(),
(center, bottom).into(),
(left, top).into(),
]
};
painter.add(Shape::convex_polygon(
points,
visuals.fg_stroke.color,
Stroke::NONE,
));
arrow_res
}
trait ResponseExt {
fn clicked_or_dragged(&self) -> bool;
}
impl ResponseExt for Response {
fn clicked_or_dragged(&self) -> bool {
self.clicked() || self.dragged()
}
}

View File

@ -1,14 +0,0 @@
mod bgmap;
mod chardata;
mod framebuffer;
mod object;
mod registers;
mod utils;
mod world;
pub use bgmap::*;
pub use chardata::*;
pub use framebuffer::*;
pub use object::*;
pub use registers::*;
pub use world::*;

View File

@ -1,315 +0,0 @@
use std::sync::Arc;
use egui::{
Align, CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextEdit,
TextureOptions, Ui, ViewportBuilder, ViewportId,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use crate::{
emulator::SimId,
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView},
window::{
AppWindow,
utils::{NumberEdit, UiExt},
},
};
use super::utils::{self, CellData, CharacterGrid};
pub struct BgMapWindow {
sim_id: SimId,
loader: Arc<ImageTextureLoader>,
memory: Arc<MemoryClient>,
bgmaps: MemoryView,
cell_index: usize,
generic_palette: bool,
params: ImageParams<BgMapParams>,
scale: f32,
show_grid: bool,
}
impl BgMapWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &mut ImageProcessor) -> Self {
let renderer = BgMapRenderer::new(sim_id, memory);
let ([cell, bgmap], params) = images.add(renderer, BgMapParams::default());
let loader =
ImageTextureLoader::new([("vip://cell".into(), cell), ("vip://bgmap".into(), bgmap)]);
Self {
sim_id,
loader: Arc::new(loader),
memory: memory.clone(),
bgmaps: memory.watch(sim_id, 0x00020000, 0x20000),
cell_index: params.cell_index,
generic_palette: params.generic_palette,
params,
scale: 1.0,
show_grid: false,
}
}
fn show_form(&mut self, ui: &mut Ui) {
let row_height = ui.spacing().interact_size.y;
ui.vertical(|ui| {
TableBuilder::new(ui)
.column(Column::auto())
.column(Column::remainder())
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Map");
});
row.col(|ui| {
let mut bgmap_index = self.cell_index / 4096;
ui.add(NumberEdit::new(&mut bgmap_index).range(0..16));
if bgmap_index != self.cell_index / 4096 {
self.cell_index = (bgmap_index * 4096) + (self.cell_index % 4096);
}
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Cell");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut self.cell_index).range(0..16 * 4096));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Address");
});
row.col(|ui| {
let address = 0x00020000 + (self.cell_index * 2);
let mut address_str = format!("{address:08x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut address_str).horizontal_align(Align::Max),
);
});
});
});
let image = Image::new("vip://cell")
.maintain_aspect_ratio(true)
.texture_options(TextureOptions::NEAREST);
ui.add(image);
ui.section("Cell", |ui| {
let mut data = self.bgmaps.borrow().read::<u16>(self.cell_index);
let mut cell = CellData::parse(data);
TableBuilder::new(ui)
.column(Column::remainder())
.column(Column::remainder())
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Character");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut cell.char_index).range(0..2048));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Palette");
});
row.col(|ui| {
ComboBox::from_id_salt("palette")
.selected_text(format!("BG {}", cell.palette_index))
.width(ui.available_width())
.show_ui(ui, |ui| {
for palette in 0..4 {
ui.selectable_value(
&mut cell.palette_index,
palette,
format!("BG {palette}"),
);
}
});
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add(Checkbox::new(&mut cell.hflip, "H-flip"));
});
row.col(|ui| {
ui.add(Checkbox::new(&mut cell.vflip, "V-flip"));
});
});
});
if cell.update(&mut data) {
let address = 0x00020000 + (self.cell_index * 2);
self.memory.write(self.sim_id, address as u32, &data);
}
});
ui.section("Display", |ui| {
ui.horizontal(|ui| {
ui.label("Scale");
ui.spacing_mut().slider_width = ui.available_width();
let slider = Slider::new(&mut self.scale, 1.0..=10.0)
.step_by(1.0)
.show_value(false);
ui.add(slider);
});
ui.checkbox(&mut self.show_grid, "Show grid");
ui.checkbox(&mut self.generic_palette, "Generic palette");
});
});
self.params.write(BgMapParams {
cell_index: self.cell_index,
generic_palette: self.generic_palette,
});
}
fn show_bgmap(&mut self, ui: &mut Ui) {
let grid = CharacterGrid::new("vip://bgmap")
.with_scale(self.scale)
.with_grid(self.show_grid)
.with_selected(self.cell_index % 4096);
if let Some(selected) = grid.show(ui) {
self.cell_index = (self.cell_index / 4096 * 4096) + selected;
}
}
}
impl AppWindow for BgMapWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of(format!("bgmap-{}", self.sim_id))
}
fn sim_id(&self) -> SimId {
self.sim_id
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("BG Map Data ({})", self.sim_id))
.with_inner_size((640.0, 480.0))
}
fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) {
ctx.add_texture_loader(self.loader.clone());
}
fn show(&mut self, ctx: &Context) {
CentralPanel::default().show(ctx, |ui| {
ui.horizontal_top(|ui| {
StripBuilder::new(ui)
.size(Size::relative(0.3).at_most(200.0))
.size(Size::remainder())
.horizontal(|mut strip| {
strip.cell(|ui| {
ScrollArea::vertical().show(ui, |ui| self.show_form(ui));
});
strip.cell(|ui| {
ScrollArea::both().show(ui, |ui| self.show_bgmap(ui));
});
})
});
});
}
}
#[derive(Default, Clone, PartialEq, Eq)]
struct BgMapParams {
cell_index: usize,
generic_palette: bool,
}
struct BgMapRenderer {
chardata: MemoryView,
bgmaps: MemoryView,
brightness: MemoryView,
palettes: MemoryView,
}
impl BgMapRenderer {
pub fn new(sim_id: SimId, memory: &MemoryClient) -> Self {
Self {
chardata: memory.watch(sim_id, 0x00078000, 0x8000),
bgmaps: memory.watch(sim_id, 0x00020000, 0x20000),
brightness: memory.watch(sim_id, 0x0005f824, 8),
palettes: memory.watch(sim_id, 0x0005f860, 16),
}
}
fn render_bgmap(&self, image: &mut ImageBuffer, bgmap_index: usize, generic_palette: bool) {
let chardata = self.chardata.borrow();
let bgmaps = self.bgmaps.borrow();
let brightness = self.brightness.borrow();
let palettes = self.palettes.borrow();
let brts = brightness.read::<[u8; 8]>(0);
let colors = if generic_palette {
[utils::generic_palette(Color32::RED); 4]
} else {
[0, 2, 4, 6].map(|i| utils::palette_colors(palettes.read(i), &brts, Color32::RED))
};
for (i, cell) in bgmaps.range::<u16>(bgmap_index * 4096, 4096).enumerate() {
let CellData {
char_index,
vflip,
hflip,
palette_index,
} = CellData::parse(cell);
let char = chardata.read::<[u16; 8]>(char_index);
let palette = &colors[palette_index];
for row in 0..8 {
let y = row + (i / 64) * 8;
for (col, pixel) in utils::read_char_row(&char, hflip, vflip, row).enumerate() {
let x = col + (i % 64) * 8;
image.write((x, y), palette[pixel as usize]);
}
}
}
}
fn render_bgmap_cell(&self, image: &mut ImageBuffer, index: usize, generic_palette: bool) {
let chardata = self.chardata.borrow();
let bgmaps = self.bgmaps.borrow();
let brightness = self.brightness.borrow();
let palettes = self.palettes.borrow();
let brts = brightness.read::<[u8; 8]>(0);
let cell = bgmaps.read::<u16>(index);
let CellData {
char_index,
vflip,
hflip,
palette_index,
} = CellData::parse(cell);
let char = chardata.read::<[u16; 8]>(char_index);
let palette = if generic_palette {
utils::generic_palette(Color32::RED)
} else {
utils::palette_colors(palettes.read(palette_index * 2), &brts, Color32::RED)
};
for row in 0..8 {
for (col, pixel) in utils::read_char_row(&char, hflip, vflip, row).enumerate() {
image.write((col, row), palette[pixel as usize]);
}
}
}
}
impl ImageRenderer<2> for BgMapRenderer {
type Params = BgMapParams;
fn sizes(&self) -> [[usize; 2]; 2] {
[[8, 8], [8 * 64, 8 * 64]]
}
fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 2]) {
self.render_bgmap_cell(&mut images[0], params.cell_index, params.generic_palette);
self.render_bgmap(
&mut images[1],
params.cell_index / 4096,
params.generic_palette,
);
}
}

View File

@ -1,356 +0,0 @@
use std::{fmt::Display, sync::Arc};
use egui::{
Align, CentralPanel, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextEdit,
TextureOptions, Ui, Vec2, ViewportBuilder, ViewportId,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use serde::{Deserialize, Serialize};
use crate::{
emulator::SimId,
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView},
window::{
AppWindow,
utils::{NumberEdit, UiExt as _},
},
};
use super::utils::{self, CharacterGrid};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Palette {
#[default]
Generic,
Bg0,
Bg1,
Bg2,
Bg3,
Obj0,
Obj1,
Obj2,
Obj3,
}
impl Palette {
pub const fn values() -> [Palette; 9] {
[
Self::Generic,
Self::Bg0,
Self::Bg1,
Self::Bg2,
Self::Bg3,
Self::Obj0,
Self::Obj1,
Self::Obj2,
Self::Obj3,
]
}
pub const fn offset(self) -> Option<usize> {
match self {
Self::Generic => None,
Self::Bg0 => Some(0),
Self::Bg1 => Some(2),
Self::Bg2 => Some(4),
Self::Bg3 => Some(6),
Self::Obj0 => Some(8),
Self::Obj1 => Some(10),
Self::Obj2 => Some(12),
Self::Obj3 => Some(14),
}
}
}
impl Display for Palette {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Generic => f.write_str("Generic"),
Self::Bg0 => f.write_str("BG 0"),
Self::Bg1 => f.write_str("BG 1"),
Self::Bg2 => f.write_str("BG 2"),
Self::Bg3 => f.write_str("BG 3"),
Self::Obj0 => f.write_str("OBJ 0"),
Self::Obj1 => f.write_str("OBJ 1"),
Self::Obj2 => f.write_str("OBJ 2"),
Self::Obj3 => f.write_str("OBJ 3"),
}
}
}
pub struct CharacterDataWindow {
sim_id: SimId,
loader: Arc<ImageTextureLoader>,
brightness: MemoryView,
palettes: MemoryView,
palette: Palette,
index: usize,
params: ImageParams<CharDataParams>,
scale: f32,
show_grid: bool,
}
impl CharacterDataWindow {
pub fn new(sim_id: SimId, memory: &MemoryClient, images: &mut ImageProcessor) -> Self {
let renderer = CharDataRenderer::new(sim_id, memory);
let ([char, chardata], params) = images.add(renderer, CharDataParams::default());
let loader = ImageTextureLoader::new([
("vip://char".into(), char),
("vip://chardata".into(), chardata),
]);
Self {
sim_id,
loader: Arc::new(loader),
brightness: memory.watch(sim_id, 0x0005f824, 8),
palettes: memory.watch(sim_id, 0x0005f860, 16),
palette: params.palette,
index: params.index,
params,
scale: 4.0,
show_grid: true,
}
}
fn show_form(&mut self, ui: &mut Ui) {
let row_height = ui.spacing().interact_size.y;
ui.vertical(|ui| {
TableBuilder::new(ui)
.column(Column::auto())
.column(Column::remainder())
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Index");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut self.index).range(0..2048));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Address");
});
row.col(|ui| {
let address = match self.index {
0x000..0x200 => 0x00060000 + self.index * 16,
0x200..0x400 => 0x000e0000 + (self.index - 0x200) * 16,
0x400..0x600 => 0x00160000 + (self.index - 0x400) * 16,
0x600..0x800 => 0x001e0000 + (self.index - 0x600) * 16,
_ => unreachable!("can't happen"),
};
let mut address_str = format!("{address:08x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut address_str).horizontal_align(Align::Max),
);
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Mirror");
});
row.col(|ui| {
let mirror = 0x00078000 + (self.index * 16);
let mut mirror_str = format!("{mirror:08x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut mirror_str).horizontal_align(Align::Max),
);
});
});
});
let image = Image::new("vip://char")
.maintain_aspect_ratio(true)
.texture_options(TextureOptions::NEAREST);
ui.add(image);
ui.section("Colors", |ui| {
ui.horizontal(|ui| {
ui.label("Palette");
ComboBox::from_id_salt("palette")
.selected_text(self.palette.to_string())
.width(ui.available_width())
.show_ui(ui, |ui| {
for palette in Palette::values() {
ui.selectable_value(
&mut self.palette,
palette,
palette.to_string(),
);
}
});
});
TableBuilder::new(ui)
.columns(Column::remainder(), 4)
.body(|mut body| {
let palette = self.load_palette_colors();
body.row(30.0, |mut row| {
for color in palette {
row.col(|ui| {
let rect = ui.available_rect_before_wrap();
let scale = rect.height() / rect.width();
let rect = rect.scale_from_center2(Vec2::new(scale, 1.0));
ui.painter().rect_filled(rect, 0.0, color);
});
}
});
});
});
ui.section("Display", |ui| {
ui.horizontal(|ui| {
ui.label("Scale");
ui.spacing_mut().slider_width = ui.available_width();
let slider = Slider::new(&mut self.scale, 1.0..=10.0)
.step_by(1.0)
.show_value(false);
ui.add(slider);
});
ui.checkbox(&mut self.show_grid, "Show grid");
});
});
self.params.write(CharDataParams {
palette: self.palette,
index: self.index,
});
}
fn load_palette_colors(&self) -> [Color32; 4] {
let Some(offset) = self.palette.offset() else {
return utils::generic_palette(Color32::RED);
};
let palette = self.palettes.borrow().read(offset);
let brightnesses = self.brightness.borrow();
let brts = brightnesses.read(0);
utils::palette_colors(palette, &brts, Color32::RED)
}
fn show_chardata(&mut self, ui: &mut Ui) {
let grid = CharacterGrid::new("vip://chardata")
.with_scale(self.scale)
.with_grid(self.show_grid)
.with_selected(self.index);
if let Some(selected) = grid.show(ui) {
self.index = selected;
}
}
}
impl AppWindow for CharacterDataWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of(format!("chardata-{}", self.sim_id))
}
fn sim_id(&self) -> SimId {
self.sim_id
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("Character Data ({})", self.sim_id))
.with_inner_size((640.0, 480.0))
}
fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) {
ctx.add_texture_loader(self.loader.clone());
}
fn show(&mut self, ctx: &Context) {
CentralPanel::default().show(ctx, |ui| {
ui.horizontal_top(|ui| {
StripBuilder::new(ui)
.size(Size::relative(0.3).at_most(200.0))
.size(Size::remainder())
.horizontal(|mut strip| {
strip.cell(|ui| {
ScrollArea::vertical().show(ui, |ui| self.show_form(ui));
});
strip.cell(|ui| {
ScrollArea::both().show(ui, |ui| self.show_chardata(ui));
});
});
});
});
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
enum CharDataResource {
Character { palette: Palette, index: usize },
CharacterData { palette: Palette },
}
#[derive(Clone, Default, PartialEq, Eq)]
struct CharDataParams {
palette: Palette,
index: usize,
}
struct CharDataRenderer {
chardata: MemoryView,
brightness: MemoryView,
palettes: MemoryView,
}
impl ImageRenderer<2> for CharDataRenderer {
type Params = CharDataParams;
fn sizes(&self) -> [[usize; 2]; 2] {
[[8, 8], [16 * 8, 128 * 8]]
}
fn render(&mut self, params: &Self::Params, image: &mut [ImageBuffer; 2]) {
self.render_character(&mut image[0], params.palette, params.index);
self.render_character_data(&mut image[1], params.palette);
}
}
impl CharDataRenderer {
pub fn new(sim_id: SimId, memory: &MemoryClient) -> Self {
Self {
chardata: memory.watch(sim_id, 0x00078000, 0x8000),
brightness: memory.watch(sim_id, 0x0005f824, 8),
palettes: memory.watch(sim_id, 0x0005f860, 16),
}
}
fn render_character(&self, image: &mut ImageBuffer, palette: Palette, index: usize) {
if index >= 2048 {
return;
}
let palette = self.load_palette(palette);
let chardata = self.chardata.borrow();
let character = chardata.range::<u16>(index * 8, 8);
for (row, pixels) in character.enumerate() {
for col in 0..8 {
let char = (pixels >> (col * 2)) & 0x03;
image.write((col, row), palette[char as usize]);
}
}
}
fn render_character_data(&self, image: &mut ImageBuffer, palette: Palette) {
let palette = self.load_palette(palette);
let chardata = self.chardata.borrow();
for (row, pixels) in chardata.range::<u16>(0, 8 * 2048).enumerate() {
let char_index = row / 8;
let row_index = row % 8;
let x = (char_index % 16) * 8;
let y = (char_index / 16) * 8 + row_index;
for col in 0..8 {
let char = (pixels >> (col * 2)) & 0x03;
image.write((x + col, y), palette[char as usize]);
}
}
}
fn load_palette(&self, palette: Palette) -> [Color32; 4] {
let Some(offset) = palette.offset() else {
return utils::GENERIC_PALETTE.map(|p| utils::shade(p, Color32::RED));
};
let palette = self.palettes.borrow().read(offset);
let brightnesses = self.brightness.borrow();
let brts = brightnesses.read(0);
utils::palette_colors(palette, &brts, Color32::RED)
}
}

View File

@ -1,267 +0,0 @@
use std::sync::Arc;
use egui::{
Align, CentralPanel, Color32, Context, Image, ScrollArea, Slider, TextEdit, TextureOptions, Ui,
ViewportBuilder, ViewportId,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use crate::{
emulator::SimId,
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView},
window::{
AppWindow,
utils::{NumberEdit, UiExt as _},
},
};
use super::utils;
pub struct FrameBufferWindow {
sim_id: SimId,
loader: Arc<ImageTextureLoader>,
index: usize,
left: bool,
right: bool,
generic_palette: bool,
params: ImageParams<FrameBufferParams>,
scale: f32,
}
impl FrameBufferWindow {
pub fn new(sim_id: SimId, memory: &MemoryClient, images: &mut ImageProcessor) -> Self {
let initial_params = FrameBufferParams {
index: 0,
left: true,
right: true,
generic_palette: false,
left_color: Color32::from_rgb(0xff, 0x00, 0x00),
right_color: Color32::from_rgb(0x00, 0xc6, 0xf0),
};
let renderer = FrameBufferRenderer::new(sim_id, memory);
let ([buffer], params) = images.add(renderer, initial_params);
let loader = ImageTextureLoader::new([("vip://buffer".into(), buffer)]);
Self {
sim_id,
loader: Arc::new(loader),
index: params.index,
left: params.left,
right: params.right,
generic_palette: params.generic_palette,
params,
scale: 2.0,
}
}
fn show_form(&mut self, ui: &mut Ui) {
let row_height = ui.spacing().interact_size.y;
ui.vertical(|ui| {
TableBuilder::new(ui)
.column(Column::auto())
.column(Column::remainder())
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Index");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut self.index).range(0..2));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Left");
});
row.col(|ui| {
let address = self.index * 0x00008000;
let mut address_str = format!("{address:08x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut address_str).horizontal_align(Align::Max),
);
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Right");
});
row.col(|ui| {
let address = self.index * 0x00008000 + 0x00010000;
let mut address_str = format!("{address:08x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut address_str).horizontal_align(Align::Max),
);
});
});
});
ui.section("Display", |ui| {
ui.horizontal(|ui| {
ui.label("Scale");
ui.spacing_mut().slider_width = ui.available_width();
let slider = Slider::new(&mut self.scale, 1.0..=10.0)
.step_by(1.0)
.show_value(false);
ui.add(slider);
});
TableBuilder::new(ui)
.columns(Column::remainder(), 2)
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.checkbox(&mut self.left, "Left");
});
row.col(|ui| {
ui.checkbox(&mut self.right, "Right");
});
});
});
ui.checkbox(&mut self.generic_palette, "Generic colors");
});
});
self.params.write(FrameBufferParams {
index: self.index,
left: self.left,
right: self.right,
generic_palette: self.generic_palette,
..*self.params
});
}
fn show_buffers(&mut self, ui: &mut Ui) {
let image = Image::new("vip://buffer")
.fit_to_original_size(self.scale)
.texture_options(TextureOptions::NEAREST);
ui.add(image);
}
}
impl AppWindow for FrameBufferWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of(format!("framebuffer-{}", self.sim_id))
}
fn sim_id(&self) -> SimId {
self.sim_id
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("Frame Buffers ({})", self.sim_id))
.with_inner_size((640.0, 480.0))
}
fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) {
ctx.add_texture_loader(self.loader.clone());
}
fn show(&mut self, ctx: &Context) {
CentralPanel::default().show(ctx, |ui| {
ui.horizontal_top(|ui| {
StripBuilder::new(ui)
.size(Size::relative(0.3).at_most(200.0))
.size(Size::remainder())
.horizontal(|mut strip| {
strip.cell(|ui| {
ScrollArea::vertical().show(ui, |ui| self.show_form(ui));
});
strip.cell(|ui| {
ScrollArea::both().show(ui, |ui| self.show_buffers(ui));
});
});
});
});
}
}
#[derive(Clone, PartialEq, Eq)]
struct FrameBufferParams {
index: usize,
left: bool,
right: bool,
generic_palette: bool,
left_color: Color32,
right_color: Color32,
}
struct FrameBufferRenderer {
buffers: [MemoryView; 4],
brightness: MemoryView,
}
impl FrameBufferRenderer {
fn new(sim_id: SimId, memory: &MemoryClient) -> Self {
Self {
buffers: [
memory.watch(sim_id, 0x00000000, 0x6000),
memory.watch(sim_id, 0x00008000, 0x6000),
memory.watch(sim_id, 0x00010000, 0x6000),
memory.watch(sim_id, 0x00018000, 0x6000),
],
brightness: memory.watch(sim_id, 0x0005f824, 8),
}
}
}
impl ImageRenderer<1> for FrameBufferRenderer {
type Params = FrameBufferParams;
fn sizes(&self) -> [[usize; 2]; 1] {
[[384, 224]]
}
fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 1]) {
let image = &mut images[0];
let left_buffer = self.buffers[params.index].borrow();
let right_buffer = self.buffers[2 + params.index].borrow();
let colors = if params.generic_palette {
[
utils::generic_palette(params.left_color),
utils::generic_palette(params.right_color),
]
} else {
let brts = self.brightness.borrow().read::<[u8; 8]>(0);
let shades = utils::parse_shades(&brts);
[
shades.map(|s| utils::shade(s, params.left_color)),
shades.map(|s| utils::shade(s, params.right_color)),
]
};
let left_cols = left_buffer.range::<u8>(0, 0x6000);
let right_cols = right_buffer.range::<u8>(0, 0x6000);
for (index, (left, right)) in left_cols.zip(right_cols).enumerate() {
let top = (index % 64) * 4;
if top >= 224 {
continue;
}
let pixels = [0, 2, 4, 6].map(|i| {
let left = if params.left {
colors[0][(left >> i) as usize & 0x3]
} else {
Color32::BLACK
};
let right = if params.right {
colors[1][(right >> i) as usize & 0x3]
} else {
Color32::BLACK
};
Color32::from_rgb(
left.r() + right.r(),
left.g() + right.g(),
left.b() + right.b(),
)
});
let x = index / 64;
for (i, pixel) in pixels.into_iter().enumerate() {
let y = top + i;
image.write((x, y), pixel);
}
}
}
}

View File

@ -1,342 +0,0 @@
use std::sync::Arc;
use egui::{
Align, CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextEdit,
TextureOptions, Ui, ViewportBuilder, ViewportId,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use crate::{
emulator::SimId,
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView},
window::{
AppWindow,
utils::{NumberEdit, UiExt as _},
},
};
use super::utils::{self, Object};
pub struct ObjectWindow {
sim_id: SimId,
loader: Arc<ImageTextureLoader>,
memory: Arc<MemoryClient>,
objects: MemoryView,
index: usize,
generic_palette: bool,
params: ImageParams<ObjectParams>,
scale: f32,
}
impl ObjectWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &mut ImageProcessor) -> Self {
let initial_params = ObjectParams {
index: 0,
generic_palette: false,
left_color: Color32::from_rgb(0xff, 0x00, 0x00),
right_color: Color32::from_rgb(0x00, 0xc6, 0xf0),
};
let renderer = ObjectRenderer::new(sim_id, memory);
let ([zoom, full], params) = images.add(renderer, initial_params);
let loader =
ImageTextureLoader::new([("vip://zoom".into(), zoom), ("vip://full".into(), full)]);
Self {
sim_id,
loader: Arc::new(loader),
memory: memory.clone(),
objects: memory.watch(sim_id, 0x0003e000, 0x2000),
index: params.index,
generic_palette: params.generic_palette,
params,
scale: 1.0,
}
}
fn show_form(&mut self, ui: &mut Ui) {
let row_height = ui.spacing().interact_size.y;
ui.vertical(|ui| {
TableBuilder::new(ui)
.column(Column::auto())
.column(Column::remainder())
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Index");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut self.index).range(0..1024));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Address");
});
row.col(|ui| {
let address = 0x3e000 + self.index * 8;
let mut address_str = format!("{address:08x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut address_str).horizontal_align(Align::Max),
);
});
});
});
let image = Image::new("vip://zoom")
.maintain_aspect_ratio(true)
.texture_options(TextureOptions::NEAREST);
ui.add(image);
ui.section("Properties", |ui| {
let mut object = self.objects.borrow().read::<[u16; 4]>(self.index);
let mut obj = Object::parse(object);
TableBuilder::new(ui)
.column(Column::remainder())
.column(Column::remainder())
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Character");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut obj.data.char_index).range(0..2048));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Palette");
});
row.col(|ui| {
ComboBox::from_id_salt("palette")
.selected_text(format!("OBJ {}", obj.data.palette_index))
.width(ui.available_width())
.show_ui(ui, |ui| {
for palette in 0..4 {
ui.selectable_value(
&mut obj.data.palette_index,
palette,
format!("OBJ {palette}"),
);
}
});
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("X");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut obj.x).range(-512..512));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Y");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut obj.y).range(-8..=224));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Parallax");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut obj.parallax).range(-512..512));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add(Checkbox::new(&mut obj.data.hflip, "H-flip"));
});
row.col(|ui| {
ui.add(Checkbox::new(&mut obj.data.vflip, "V-flip"));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add(Checkbox::new(&mut obj.lon, "Left"));
});
row.col(|ui| {
ui.add(Checkbox::new(&mut obj.ron, "Right"));
});
});
});
if obj.update(&mut object) {
let address = 0x3e000 + self.index * 8;
self.memory.write(self.sim_id, address as u32, &object);
}
});
ui.section("Display", |ui| {
ui.horizontal(|ui| {
ui.label("Scale");
ui.spacing_mut().slider_width = ui.available_width();
let slider = Slider::new(&mut self.scale, 1.0..=10.0)
.step_by(1.0)
.show_value(false);
ui.add(slider);
});
ui.checkbox(&mut self.generic_palette, "Generic palette");
});
});
self.params.write(ObjectParams {
index: self.index,
generic_palette: self.generic_palette,
..*self.params
});
}
fn show_object(&mut self, ui: &mut Ui) {
let image = Image::new("vip://full")
.fit_to_original_size(self.scale)
.texture_options(TextureOptions::NEAREST);
ui.add(image);
}
}
impl AppWindow for ObjectWindow {
fn viewport_id(&self) -> egui::ViewportId {
ViewportId::from_hash_of(format!("object-{}", self.sim_id))
}
fn sim_id(&self) -> SimId {
self.sim_id
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("Object Data ({})", self.sim_id))
.with_inner_size((640.0, 500.0))
}
fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) {
ctx.add_texture_loader(self.loader.clone());
}
fn show(&mut self, ctx: &Context) {
CentralPanel::default().show(ctx, |ui| {
ui.horizontal_top(|ui| {
StripBuilder::new(ui)
.size(Size::relative(0.3).at_most(200.0))
.size(Size::remainder())
.horizontal(|mut strip| {
strip.cell(|ui| {
ScrollArea::vertical().show(ui, |ui| self.show_form(ui));
});
strip.cell(|ui| {
ScrollArea::both().show(ui, |ui| self.show_object(ui));
});
});
});
});
}
}
#[derive(Clone, PartialEq, Eq)]
struct ObjectParams {
index: usize,
generic_palette: bool,
left_color: Color32,
right_color: Color32,
}
enum Eye {
Left,
Right,
}
struct ObjectRenderer {
chardata: MemoryView,
objects: MemoryView,
brightness: MemoryView,
palettes: MemoryView,
}
impl ObjectRenderer {
pub fn new(sim_id: SimId, memory: &MemoryClient) -> Self {
Self {
chardata: memory.watch(sim_id, 0x00078000, 0x8000),
objects: memory.watch(sim_id, 0x0003e000, 0x2000),
brightness: memory.watch(sim_id, 0x0005f824, 8),
palettes: memory.watch(sim_id, 0x0005f860, 16),
}
}
fn render_object(
&self,
image: &mut ImageBuffer,
params: &ObjectParams,
use_pos: bool,
eye: Eye,
) {
let chardata = self.chardata.borrow();
let objects = self.objects.borrow();
let brightness = self.brightness.borrow();
let palettes = self.palettes.borrow();
let object: [u16; 4] = objects.read(params.index);
let obj = Object::parse(object);
if match eye {
Eye::Left => !obj.lon,
Eye::Right => !obj.ron,
} {
return;
}
let brts = brightness.read::<[u8; 8]>(0);
let (x, y) = if use_pos {
let x = match eye {
Eye::Left => obj.x - obj.parallax,
Eye::Right => obj.x + obj.parallax,
};
(x, obj.y)
} else {
(0, 0)
};
let color = match eye {
Eye::Left => params.left_color,
Eye::Right => params.right_color,
};
let char = chardata.read::<[u16; 8]>(obj.data.char_index);
let palette = if params.generic_palette {
utils::generic_palette(color)
} else {
utils::palette_colors(palettes.read(8 + obj.data.palette_index * 2), &brts, color)
};
for row in 0..8 {
let real_y = y + row as i16;
if !(0..224).contains(&real_y) {
continue;
}
for (col, pixel) in
utils::read_char_row(&char, obj.data.hflip, obj.data.vflip, row).enumerate()
{
let real_x = x + col as i16;
if !(0..384).contains(&real_x) {
continue;
}
image.add((real_x as usize, real_y as usize), palette[pixel as usize]);
}
}
}
}
impl ImageRenderer<2> for ObjectRenderer {
type Params = ObjectParams;
fn sizes(&self) -> [[usize; 2]; 2] {
[[8, 8], [384, 224]]
}
fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; 2]) {
images[0].clear();
self.render_object(&mut images[0], params, false, Eye::Left);
self.render_object(&mut images[0], params, false, Eye::Right);
images[1].clear();
self.render_object(&mut images[1], params, true, Eye::Left);
self.render_object(&mut images[1], params, true, Eye::Right);
}
}

View File

@ -1,819 +0,0 @@
use std::sync::Arc;
use egui::{
Align, Button, CentralPanel, Checkbox, Color32, Context, Direction, Label, Layout, ScrollArea,
TextEdit, Ui, ViewportBuilder, ViewportId,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use crate::{
emulator::SimId,
memory::{MemoryClient, MemoryRef, MemoryValue, MemoryView},
window::{
AppWindow,
utils::{NumberEdit, UiExt},
},
};
use super::utils;
pub struct RegisterWindow {
sim_id: SimId,
memory: Arc<MemoryClient>,
registers: MemoryView,
}
impl RegisterWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>) -> Self {
Self {
sim_id,
memory: memory.clone(),
registers: memory.watch(sim_id, 0x0005f800, 0x72),
}
}
fn show_interrupts(&mut self, ui: &mut Ui) {
let row_height = self.row_height(ui);
let [mut raw_intpnd, mut raw_intenb] = self.read_address(0x0005f800);
let mut intenb = InterruptReg::parse(raw_intenb);
let mut intpnd = InterruptReg::parse(raw_intpnd);
ui.section("Interrupt", |ui| {
let width = ui.available_width();
let xspace = ui.spacing().item_spacing.x;
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
TableBuilder::new(ui)
.id_salt("raw_values")
.columns(Column::exact(width * 0.5 - xspace), 2)
.cell_layout(Layout::left_to_right(Align::Max))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("INTENB");
});
row.col(|ui| {
let mut text = format!("{raw_intenb:04x}");
ui.add_sized(
ui.available_size(),
TextEdit::singleline(&mut text).horizontal_align(Align::Max),
);
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("INTPND");
});
row.col(|ui| {
let mut text = format!("{raw_intpnd:04x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut text).horizontal_align(Align::Max),
);
});
});
});
ui.add_space(8.0);
TableBuilder::new(ui)
.id_salt("flags")
.column(Column::exact(width * 0.5 - xspace))
.columns(Column::exact(width * 0.25 - xspace), 2)
.cell_layout(Layout::left_to_right(Align::Center).with_main_align(Align::RIGHT))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|_ui| {});
row.col(|ui| {
ui.add_sized(ui.available_size(), Label::new("ENB"));
});
row.col(|ui| {
ui.add_sized(ui.available_size(), Label::new("PND"));
});
});
let mut add_row = |label: &str, enb: &mut bool, pnd: &mut bool| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label(label);
});
row.col(|ui| {
let space =
(ui.available_width() - ui.spacing().icon_width) / 2.0;
ui.add_space(space);
ui.checkbox(enb, "");
});
row.col(|ui| {
let space =
(ui.available_width() - ui.spacing().icon_width) / 2.0;
ui.add_space(space);
ui.checkbox(pnd, "");
});
});
};
add_row("TIMEERR", &mut intenb.timeerr, &mut intpnd.timeerr);
add_row("XPEND", &mut intenb.xpend, &mut intpnd.xpend);
add_row("SBHIT", &mut intenb.sbhit, &mut intpnd.sbhit);
add_row("FRAMESTART", &mut intenb.framestart, &mut intpnd.framestart);
add_row("GAMESTART", &mut intenb.gamestart, &mut intpnd.gamestart);
add_row("RFBEND", &mut intenb.rfbend, &mut intpnd.rfbend);
add_row("LFBEND", &mut intenb.lfbend, &mut intpnd.lfbend);
add_row("SCANERR", &mut intenb.scanerr, &mut intpnd.scanerr);
});
});
ui.allocate_space(ui.available_size());
});
if intpnd.update(&mut raw_intpnd) {
self.memory.write(self.sim_id, 0x0005f800, &raw_intpnd);
}
if intenb.update(&mut raw_intenb) {
self.memory.write(self.sim_id, 0x0005f802, &raw_intenb);
}
}
fn show_display_status(&mut self, ui: &mut Ui) {
let row_height = self.row_height(ui);
let mut raw_dpstts = self.read_address(0x0005f820);
let mut dpstts = DisplayReg::parse(raw_dpstts);
ui.section("Display", |ui| {
let width = ui.available_width();
TableBuilder::new(ui)
.column(Column::exact(width * 0.5))
.column(Column::exact(width * 0.5))
.cell_layout(Layout::left_to_right(Align::Max))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("DPSTTS");
});
row.col(|ui| {
let mut value_str = format!("{raw_dpstts:04x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut value_str).horizontal_align(Align::Max),
);
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_enabled(true, Checkbox::new(&mut dpstts.lock, "LOCK"));
});
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut dpstts.r1bsy, "R1BSY"));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_enabled(true, Checkbox::new(&mut dpstts.synce, "SYNCE"));
});
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut dpstts.l1bsy, "L1BSY"));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_enabled(true, Checkbox::new(&mut dpstts.re, "RE"));
});
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut dpstts.r0bsy, "R0BSY"));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut dpstts.fclk, "FCLK"));
});
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut dpstts.l0bsy, "L0BSY"));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut dpstts.scanrdy, "SCANRDY"));
});
row.col(|ui| {
ui.add_enabled(true, Checkbox::new(&mut dpstts.disp, "DISP"));
});
});
body.row(row_height, |mut row| {
row.col(|_ui| {});
row.col(|ui| {
if ui
.add(Button::new("DPRST").min_size(ui.available_size()))
.clicked()
{
dpstts.dprst = true;
}
});
});
});
ui.allocate_space(ui.available_size());
});
if dpstts.update(&mut raw_dpstts) {
self.memory.write(self.sim_id, 0x0005f822, &raw_dpstts);
}
}
fn show_drawing_status(&mut self, ui: &mut Ui) {
let row_height = self.row_height(ui);
let [mut raw_xpstts, raw_xpctrl] = self.read_address(0x0005f840);
let mut xpstts = DrawingReg::parse(raw_xpstts);
ui.section("Drawing", |ui| {
let width = ui.available_width();
TableBuilder::new(ui)
.column(Column::exact(width * 0.5))
.column(Column::exact(width * 0.5))
.cell_layout(Layout::left_to_right(Align::Max))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("XPCTRL");
});
row.col(|ui| {
let mut value_str = format!("{raw_xpctrl:04x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut value_str).horizontal_align(Align::Max),
);
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("XPSTTS");
});
row.col(|ui| {
let mut value_str = format!("{raw_xpstts:04x}");
ui.add_enabled(
false,
TextEdit::singleline(&mut value_str).horizontal_align(Align::Max),
);
});
});
/*
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("SBCMP");
});
row.col(|ui| {
let old_value = xpctrl.sbcmp;
ui.add_enabled(true, NumberEdit::new(&mut xpctrl.sbcmp).range(0..32));
cmp_changed = xpctrl.sbcmp != old_value;
});
});
*/
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("SBCOUNT");
});
row.col(|ui| {
ui.add_enabled(
false,
NumberEdit::new(&mut xpstts.sbcount)
.arrows(false)
.range(0..32),
);
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut xpstts.sbout, "SBOUT"));
});
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut xpstts.f1bsy, "F1BSY"));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut xpstts.f0bsy, "F0BSY"));
});
row.col(|ui| {
ui.add_enabled(false, Checkbox::new(&mut xpstts.overtime, "OVERTIME"));
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_enabled(true, Checkbox::new(&mut xpstts.xpen, "XPEN"));
});
row.col(|ui| {
if ui
.add(Button::new("XPRST").min_size(ui.available_size()))
.clicked()
{
xpstts.xprst = true;
}
});
});
});
ui.allocate_space(ui.available_size());
});
if xpstts.update(&mut raw_xpstts) {
xpstts.update(&mut raw_xpstts);
self.memory.write(self.sim_id, 0x0005f842, &raw_xpstts);
}
}
fn show_colors(&mut self, ui: &mut Ui) {
let row_height = self.row_height(ui);
let registers = self.registers.borrow();
ui.section("Colors", |ui| {
let width = ui.available_width();
let xspace = ui.spacing().item_spacing.x;
TableBuilder::new(ui)
.column(Column::exact(width * 0.2 - xspace))
.columns(Column::exact(width * 0.20 - xspace), 4)
.cell_layout(Layout::left_to_right(Align::Max))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|_ui| {});
row.col(|_ui| {});
row.col(|ui| {
ui.with_layout(
Layout::centered_and_justified(Direction::LeftToRight),
|ui| ui.label("1"),
);
});
row.col(|ui| {
ui.with_layout(
Layout::centered_and_justified(Direction::LeftToRight),
|ui| ui.label("2"),
);
});
row.col(|ui| {
ui.with_layout(
Layout::centered_and_justified(Direction::LeftToRight),
|ui| ui.label("3"),
);
});
});
let mut brts: [u16; 3] = [
read_address(&registers, 0x0005f824),
read_address(&registers, 0x0005f826),
read_address(&registers, 0x0005f828),
];
body.row(row_height, |mut row| {
let mut stale = false;
row.col(|ui| {
ui.label("BRT");
});
row.col(|_ui| {});
for brt in brts.iter_mut() {
row.col(|ui| {
if ui
.add(NumberEdit::new(brt).range(0..256).arrows(false).hex(true))
.changed()
{
stale = true;
}
});
}
if stale {
self.memory.write(self.sim_id, 0x0005f824, &brts);
}
});
body.row(row_height, |mut row| {
row.col(|_ui| {});
for shade in utils::parse_brts(&brts, Color32::RED) {
row.col(|ui| {
ui.painter().rect_filled(
ui.available_rect_before_wrap(),
0.0,
shade,
);
});
}
});
let mut palettes = read_address::<[u16; 8]>(&registers, 0x0005f860);
let mut add_row = |name: &str, address: u32, value: &mut u16| {
let mut c1 = (*value >> 2) & 0x03;
let mut c2 = (*value >> 4) & 0x03;
let mut c3 = (*value >> 6) & 0x03;
let mut stale = false;
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label(name);
});
row.col(|ui| {
if ui
.add(
NumberEdit::new(value)
.range(0..256)
.desired_width(width * 0.2)
.arrows(false)
.hex(true),
)
.changed()
{
stale = true;
};
});
row.col(|ui| {
if ui
.add(
NumberEdit::new(&mut c1)
.range(0..4)
.desired_width(ui.available_width() - xspace),
)
.changed()
{
*value = (*value & 0xfff3) | (c1 << 2);
stale = true;
}
});
row.col(|ui| {
if ui
.add(
NumberEdit::new(&mut c2)
.range(0..4)
.desired_width(ui.available_width() - xspace),
)
.changed()
{
*value = (*value & 0xffcf) | (c2 << 4);
stale = true;
}
});
row.col(|ui| {
if ui
.add(
NumberEdit::new(&mut c3)
.range(0..4)
.desired_width(ui.available_width() - xspace),
)
.changed()
{
*value = (*value & 0xff3f) | (c3 << 6);
stale = true;
}
});
});
if stale {
self.memory.write(self.sim_id, address, value);
}
};
add_row("GPLT0", 0x0005f860, &mut palettes[0]);
add_row("GPLT1", 0x0005f862, &mut palettes[1]);
add_row("GPLT2", 0x0005f864, &mut palettes[2]);
add_row("GPLT3", 0x0005f866, &mut palettes[3]);
add_row("JPLT0", 0x0005f868, &mut palettes[4]);
add_row("JPLT1", 0x0005f86a, &mut palettes[5]);
add_row("JPLT2", 0x0005f86c, &mut palettes[6]);
add_row("JPLT3", 0x0005f86e, &mut palettes[7]);
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("BKCOL");
});
row.col(|ui| {
let mut bkcol: u16 = read_address(&registers, 0x0005f870);
if ui
.add(
NumberEdit::new(&mut bkcol)
.range(0..4)
.desired_width(ui.available_width() - xspace),
)
.changed()
{
self.memory.write(self.sim_id, 0x0005f870, &bkcol);
}
});
row.col(|_ui| {});
row.col(|ui| {
ui.label("REST");
});
row.col(|ui| {
let mut rest: u16 = read_address(&registers, 0x0005f82a);
if ui
.add(
NumberEdit::new(&mut rest)
.range(0..256)
.arrows(false)
.hex(true),
)
.changed()
{
self.memory.write(self.sim_id, 0x0005f82a, &rest);
}
});
});
});
ui.allocate_space(ui.available_size_before_wrap());
});
}
fn show_objects(&mut self, ui: &mut Ui) {
let row_height = self.row_height(ui);
ui.section("Objects", |ui| {
let width = ui.available_width();
let xspace = ui.spacing().item_spacing.x;
TableBuilder::new(ui)
.column(Column::exact(width * 0.3 - xspace))
.column(Column::exact(width * 0.3 - xspace))
.column(Column::exact(width * 0.4 - xspace))
.cell_layout(Layout::left_to_right(Align::Center).with_main_align(Align::RIGHT))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|_ui| {});
row.col(|ui| {
ui.add_sized(ui.available_size(), Label::new("Start"));
});
row.col(|ui| {
ui.add_sized(ui.available_size(), Label::new("End"));
});
});
let mut spts = self.read_address::<[u16; 4]>(0x0005f848);
let prevs = std::iter::once(0u16).chain(spts.map(|i| (i + 1) & 0x03ff));
let mut changed = false;
for (index, (spt, prev)) in spts.iter_mut().zip(prevs).enumerate() {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label(format!("SPT{index}"));
});
row.col(|ui| {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
ui.label(prev.to_string());
});
});
row.col(|ui| {
if ui.add(NumberEdit::new(spt).range(0..1024)).changed() {
changed = true;
}
});
});
}
if changed {
self.memory.write(self.sim_id, 0x0005f848, &spts);
}
});
ui.allocate_space(ui.available_size_before_wrap());
});
}
fn show_misc(&mut self, ui: &mut Ui) {
let row_height = self.row_height(ui);
let registers = self.registers.borrow();
ui.section("Misc.", |ui| {
let width = ui.available_width();
let xspace = ui.spacing().item_spacing.x;
TableBuilder::new(ui)
.column(Column::exact(width * 0.5 - xspace))
.column(Column::exact(width * 0.5 - xspace))
.cell_layout(Layout::left_to_right(Align::Center).with_main_align(Align::RIGHT))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("FRMCYC");
});
row.col(|ui| {
let mut frmcyc = read_address::<u16>(&registers, 0x0005f82e);
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if ui.add(NumberEdit::new(&mut frmcyc).range(0..32)).changed() {
self.memory.write(self.sim_id, 0x0005f82e, &frmcyc);
}
});
});
});
let mut cta = read_address::<u16>(&registers, 0x0005f830);
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("CTA");
});
row.col(|ui| {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
ui.add_enabled(
false,
NumberEdit::new(&mut cta).arrows(false).hex(true),
);
});
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("CTA_L");
});
row.col(|ui| {
let mut cta_l = cta & 0xff;
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
ui.add_enabled(
false,
NumberEdit::new(&mut cta_l).arrows(false).hex(true),
);
});
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("CTA_R");
});
row.col(|ui| {
let mut cta_r = (cta >> 8) & 0xff;
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
ui.add_enabled(
false,
NumberEdit::new(&mut cta_r).arrows(false).hex(true),
);
});
});
});
});
ui.allocate_space(ui.available_size_before_wrap());
});
}
fn row_height(&self, ui: &mut Ui) -> f32 {
ui.spacing().interact_size.y + ui.style().visuals.selection.stroke.width
}
fn read_address<T: MemoryValue>(&self, address: usize) -> T {
read_address(&self.registers.borrow(), address)
}
}
fn read_address<T: MemoryValue>(registers: &MemoryRef, address: usize) -> T {
let index = (address - 0x0005f800) / size_of::<T>();
registers.read(index)
}
impl AppWindow for RegisterWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of(format!("registers-{}", self.sim_id))
}
fn sim_id(&self) -> SimId {
self.sim_id
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("Registers ({})", self.sim_id))
.with_inner_size((800.0, 480.0))
}
fn show(&mut self, ctx: &Context) {
CentralPanel::default().show(ctx, |ui| {
ScrollArea::vertical().show(ui, |ui| {
ui.horizontal_top(|ui| {
let width = ui.available_width();
let xspace = ui.spacing().item_spacing.x;
StripBuilder::new(ui)
.size(Size::exact(width * 0.25 - xspace))
.size(Size::exact(width * 0.225 - xspace))
.size(Size::exact(width * 0.325 - xspace))
.size(Size::exact(width * 0.2 - xspace))
.horizontal(|mut strip| {
strip.cell(|ui| {
self.show_interrupts(ui);
});
strip.strip(|nested| {
nested.sizes(Size::remainder(), 2).vertical(|mut strip| {
strip.cell(|ui| {
self.show_display_status(ui);
});
strip.cell(|ui| {
self.show_drawing_status(ui);
});
});
});
strip.cell(|ui| {
self.show_colors(ui);
});
strip.strip(|nested| {
nested.sizes(Size::remainder(), 2).vertical(|mut strip| {
strip.cell(|ui| {
self.show_objects(ui);
});
strip.cell(|ui| {
self.show_misc(ui);
});
});
});
});
});
});
});
}
}
struct InterruptReg {
timeerr: bool,
xpend: bool,
sbhit: bool,
framestart: bool,
gamestart: bool,
rfbend: bool,
lfbend: bool,
scanerr: bool,
}
impl InterruptReg {
fn parse(value: u16) -> Self {
Self {
timeerr: value & 0x8000 != 0,
xpend: value & 0x4000 != 0,
sbhit: value & 0x2000 != 0,
framestart: value & 0x0010 != 0,
gamestart: value & 0x0008 != 0,
rfbend: value & 0x0004 != 0,
lfbend: value & 0x0002 != 0,
scanerr: value & 0x0001 != 0,
}
}
fn update(&self, value: &mut u16) -> bool {
let new_value = (*value & 0x1fe0)
| if self.timeerr { 0x8000 } else { 0x0000 }
| if self.xpend { 0x4000 } else { 0x0000 }
| if self.sbhit { 0x2000 } else { 0x0000 }
| if self.framestart { 0x0010 } else { 0x0000 }
| if self.gamestart { 0x0008 } else { 0x0000 }
| if self.rfbend { 0x0004 } else { 0x0000 }
| if self.lfbend { 0x0002 } else { 0x0000 }
| if self.scanerr { 0x0001 } else { 0x0000 };
let changed = *value != new_value;
*value = new_value;
changed
}
}
struct DisplayReg {
lock: bool,
synce: bool,
re: bool,
fclk: bool,
scanrdy: bool,
r1bsy: bool,
l1bsy: bool,
r0bsy: bool,
l0bsy: bool,
disp: bool,
dprst: bool,
}
impl DisplayReg {
fn parse(value: u16) -> Self {
Self {
lock: value & 0x0400 != 0,
synce: value & 0x0200 != 0,
re: value & 0x0100 != 0,
fclk: value & 0x0080 != 0,
scanrdy: value & 0x0040 != 0,
r1bsy: value & 0x0020 != 0,
l1bsy: value & 0x0010 != 0,
r0bsy: value & 0x0008 != 0,
l0bsy: value & 0x0004 != 0,
disp: value & 0x0002 != 0,
dprst: value & 0x0001 != 0,
}
}
fn update(&self, value: &mut u16) -> bool {
let new_value = (*value & 0xf800)
| if self.lock { 0x0400 } else { 0x0000 }
| if self.synce { 0x0200 } else { 0x0000 }
| if self.re { 0x0100 } else { 0x0000 }
| if self.fclk { 0x0080 } else { 0x0000 }
| if self.scanrdy { 0x0040 } else { 0x0000 }
| if self.r1bsy { 0x0020 } else { 0x0000 }
| if self.l1bsy { 0x0010 } else { 0x0000 }
| if self.r0bsy { 0x0008 } else { 0x0000 }
| if self.l0bsy { 0x0004 } else { 0x0000 }
| if self.disp { 0x0002 } else { 0x0000 }
| if self.dprst { 0x0001 } else { 0x0000 };
let changed = *value != new_value;
*value = new_value;
changed
}
}
struct DrawingReg {
sbout: bool,
sbcount: u8,
overtime: bool,
f1bsy: bool,
f0bsy: bool,
xpen: bool,
xprst: bool,
}
impl DrawingReg {
fn parse(value: u16) -> Self {
Self {
sbout: value & 0x8000 != 0,
sbcount: (value >> 8) as u8 & 0x1f,
overtime: value & 0x0010 != 0,
f1bsy: value & 0x0008 != 0,
f0bsy: value & 0x0004 != 0,
xpen: value & 0x0002 != 0,
xprst: value & 0x0001 != 0,
}
}
fn update(&self, value: &mut u16) -> bool {
let new_value = (*value & 0x60e0)
| if self.sbout { 0x8000 } else { 0x0000 }
| (((self.sbcount & 0x1f) as u16) << 8)
| if self.overtime { 0x0010 } else { 0x0000 }
| if self.f1bsy { 0x0008 } else { 0x0000 }
| if self.f0bsy { 0x0004 } else { 0x0000 }
| if self.xpen { 0x0002 } else { 0x0000 }
| if self.xprst { 0x0001 } else { 0x0000 };
let changed = *value != new_value;
*value = new_value;
changed
}
}

View File

@ -1,251 +0,0 @@
use egui::{Color32, Image, ImageSource, Response, Sense, TextureOptions, Ui, Widget};
pub const GENERIC_PALETTE: [u8; 4] = [0, 32, 64, 128];
pub fn shade(brt: u8, color: Color32) -> Color32 {
let corrected = if brt > 132 {
255.0
} else {
(brt as f32 * 255.0 / 133.0).round()
};
color.gamma_multiply(corrected / 255.0)
}
pub fn generic_palette(color: Color32) -> [Color32; 4] {
GENERIC_PALETTE.map(|brt| shade(brt, color))
}
pub const fn parse_palette(palette: u8) -> [u8; 4] {
[
0,
(palette >> 2) & 0x03,
(palette >> 4) & 0x03,
(palette >> 6) & 0x03,
]
}
pub const fn parse_shades(brts: &[u8; 8]) -> [u8; 4] {
[
0,
brts[0],
brts[2],
brts[0].saturating_add(brts[2]).saturating_add(brts[4]),
]
}
pub fn parse_brts(brts: &[u16; 3], color: Color32) -> [Color32; 4] {
[
Color32::BLACK,
shade(brts[0] as u8, color),
shade(brts[1] as u8, color),
shade((brts[0] + brts[1] + brts[2]) as u8, color),
]
}
pub fn palette_colors(palette: u8, brts: &[u8; 8], color: Color32) -> [Color32; 4] {
let colors = parse_shades(brts).map(|s| shade(s, color));
parse_palette(palette).map(|p| colors[p as usize])
}
pub struct Object {
pub x: i16,
pub lon: bool,
pub ron: bool,
pub parallax: i16,
pub y: i16,
pub data: CellData,
}
impl Object {
pub fn parse(object: [u16; 4]) -> Self {
let x = ((object[0] & 0x03ff) << 6 >> 6) as i16;
let parallax = ((object[1] & 0x03ff) << 6 >> 6) as i16;
let lon = object[1] & 0x8000 != 0;
let ron = object[1] & 0x4000 != 0;
let y = (object[2] & 0x00ff) as i16;
// Y is stored as the bottom 8 bits of an i16,
// so only sign extend if it's out of range.
let y = if y > 224 { y << 8 >> 8 } else { y };
let data = CellData::parse(object[3]);
Self {
x,
lon,
ron,
parallax,
y,
data,
}
}
pub fn update(&self, source: &mut [u16; 4]) -> bool {
let mut changed = false;
let new_x = (self.x as u16 & 0x03ff) | (source[0] & 0xfc00);
changed |= source[0] != new_x;
source[0] = new_x;
let new_p = if self.lon { 0x8000 } else { 0x0000 }
| if self.ron { 0x4000 } else { 0x0000 }
| (self.parallax as u16 & 0x3ff)
| (source[1] & 0x3c00);
changed |= source[1] != new_p;
source[1] = new_p;
let new_y = (self.y as u16 & 0x00ff) | (source[2] & 0xff00);
changed |= source[2] != new_y;
source[2] = new_y;
if self.data.update(&mut source[3]) {
changed = true;
}
changed
}
}
pub struct CellData {
pub palette_index: usize,
pub hflip: bool,
pub vflip: bool,
pub char_index: usize,
}
impl CellData {
pub fn parse(cell: u16) -> Self {
let char_index = (cell & 0x7ff) as usize;
let vflip = cell & 0x1000 != 0;
let hflip = cell & 0x2000 != 0;
let palette_index = (cell >> 14) as usize;
Self {
char_index,
vflip,
hflip,
palette_index,
}
}
pub fn update(&self, source: &mut u16) -> bool {
let new_value = ((self.palette_index as u16) << 14)
| if self.hflip { 0x2000 } else { 0x0000 }
| if self.vflip { 0x1000 } else { 0x0000 }
| (self.char_index as u16 & 0x07ff)
| (*source & 0x0800);
let changed = *source != new_value;
*source = new_value;
changed
}
}
pub fn read_char_row(
char: &[u16; 8],
hflip: bool,
vflip: bool,
row: usize,
) -> impl Iterator<Item = u8> {
let pixels = if vflip { char[7 - row] } else { char[row] };
(0..16).step_by(2).map(move |i| {
let pixel = if hflip { 14 - i } else { i };
((pixels >> pixel) & 0x3) as u8
})
}
pub fn read_char_pixel(char: &[u16; 8], hflip: bool, vflip: bool, row: usize, col: usize) -> u8 {
let pixels = if vflip { char[7 - row] } else { char[row] };
let pixel = if hflip { 7 - col } else { col } << 1;
((pixels >> pixel) & 0x3) as u8
}
pub struct CharacterGrid<'a> {
source: ImageSource<'a>,
scale: f32,
show_grid: bool,
selected: Option<usize>,
}
impl<'a> CharacterGrid<'a> {
pub fn new(source: impl Into<ImageSource<'a>>) -> Self {
Self {
source: source.into(),
scale: 1.0,
show_grid: false,
selected: None,
}
}
pub fn with_scale(self, scale: f32) -> Self {
Self { scale, ..self }
}
pub fn with_grid(self, show_grid: bool) -> Self {
Self { show_grid, ..self }
}
pub fn with_selected(self, selected: usize) -> Self {
Self {
selected: Some(selected),
..self
}
}
pub fn show(self, ui: &mut Ui) -> Option<usize> {
let start_pos = ui.cursor().min;
let cell_size = 8.0 * self.scale;
let res = self.ui(ui);
let grid_width_cells = ((res.rect.max.x - res.rect.min.x) / cell_size).round() as usize;
if res.clicked() {
let click_pos = res.interact_pointer_pos()?;
let grid_pos = (click_pos - start_pos) / cell_size;
Some((grid_pos.y as usize * grid_width_cells) + grid_pos.x as usize)
} else {
None
}
}
}
impl Widget for CharacterGrid<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let image = Image::new(self.source)
.fit_to_original_size(self.scale)
.texture_options(TextureOptions::NEAREST)
.sense(Sense::click());
let res = ui.add(image);
let cell_size = 8.0 * self.scale;
let grid_width_cells = ((res.rect.max.x - res.rect.min.x) / cell_size).round() as usize;
let grid_height_cells = ((res.rect.max.y - res.rect.min.y) / cell_size).round() as usize;
let painter = ui.painter_at(res.rect);
if self.show_grid {
let stroke = ui.style().visuals.widgets.noninteractive.fg_stroke;
for x in (1..grid_width_cells).map(|i| (i as f32) * cell_size) {
let p1 = (res.rect.min.x + x, res.rect.min.y).into();
let p2 = (res.rect.min.x + x, res.rect.max.y).into();
painter.line_segment([p1, p2], stroke);
}
for y in (1..grid_height_cells).map(|i| (i as f32) * cell_size) {
let p1 = (res.rect.min.x, res.rect.min.y + y).into();
let p2 = (res.rect.max.x, res.rect.min.y + y).into();
painter.line_segment([p1, p2], stroke);
}
}
if let Some(selected) = self.selected {
let x1 = (selected % grid_width_cells) as f32 * cell_size;
let x2 = x1 + cell_size;
let y1 = (selected / grid_width_cells) as f32 * cell_size;
let y2 = y1 + cell_size;
painter.line(
vec![
(res.rect.min + (x1, y1).into()),
(res.rect.min + (x2, y1).into()),
(res.rect.min + (x2, y2).into()),
(res.rect.min + (x1, y2).into()),
(res.rect.min + (x1, y1).into()),
],
ui.style().visuals.widgets.active.fg_stroke,
);
}
res
}
}

File diff suppressed because it is too large Load Diff