Compare commits

..

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

44 changed files with 2788 additions and 6857 deletions

4376
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,54 +4,46 @@ description = "An emulator for the Virtual Boy."
repository = "https://git.virtual-boy.com/PVB/lemur"
publish = false
license = "MIT"
version = "0.12.3"
version = "0.4.8"
edition = "2024"
[dependencies]
anyhow = "1"
atoi = "2"
audioadapter-buffers = "3"
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"
egui = { version = "0.34", features = ["persistence", "serde"] }
egui_extras = { version = "0.34", features = ["image"] }
egui-notify = "0.22"
egui-winit = "0.34"
egui-wgpu = { version = "0.34", features = ["winit"] }
fxprof-processed-profile = "0.8"
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"] }
gimli = "0.33"
hex = "0.4"
image = { version = "0.25", default-features = false, features = ["png"] }
itertools = "0.14"
normpath = "1"
notify = "8"
num-derive = "0.4"
num-traits = "0.2"
object = "0.39"
oneshot = { version = "0.2", features = ["async", "std"] }
oneshot = "0.1"
pollster = "0.4"
rand = "0.10"
rfd = "0.17"
rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "async-std"]}
rtrb = "0.3"
rubato = "2"
rubato = "0.16"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thread-priority = "3"
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 = "29"
wholesym = "0.8"
wgpu = "23"
winit = { version = "0.30", features = ["serde"] }
[target.'cfg(windows)'.dependencies]
windows = { version = "0.62", features = ["Win32_System_Threading"] }
windows = { version = "0.59", features = ["Win32_System_Threading"] }
[build-dependencies]
cc = "1"

View File

@ -1,43 +1,24 @@
# This Dockerfile produces a base image for builds.
# It includes all dependencies necessary to cross-compile for Windows/MacOS/Linux.
FROM debian:bookworm AS osxcross
ADD --chmod=644 "https://apt.llvm.org/llvm-snapshot.gpg.key" /etc/apt/trusted.gpg.d/apt.llvm.org.asc
WORKDIR /build/osxcross
ADD "https://github.com/tpoechtrager/osxcross.git#47936a512273bb3b414b5a2e83043c92eabc7ae7" .
ADD "https://github.com/joseluisq/macosx-sdks/releases/download/14.5/MacOSX14.5.sdk.tar.xz" ./tarballs/MacOSX14.5.sdk.tar.xz
RUN apt-get update && \
apt-get install -y ca-certificates
COPY llvm.sources /etc/apt/sources.list.d/llvm.sources
COPY install-llvm.sh .
RUN apt-get update && \
./install-llvm.sh && \
apt-get install -y bash bzip2 git make patch xz-utils && \
ln -s $(which clang-22) /usr/bin/clang && \
ln -s $(which clang++-22) /usr/bin/clang++ && \
ln -s $(which ld64.lld-22) /usr/bin/ld64.lld && \
SDK_VERSION=14.5 UNATTENDED=1 ENABLE_COMPILER_RT_INSTALL=1 TARGET_DIR=/osxcross ./build.sh
FROM crazymax/osxcross:latest-ubuntu AS osxcross
FROM rust:1.95-bookworm
ADD --chmod=644 "https://apt.llvm.org/llvm-snapshot.gpg.key" /etc/apt/trusted.gpg.d/apt.llvm.org.asc
COPY llvm.sources /etc/apt/sources.list.d/llvm.sources
COPY install-llvm.sh .
FROM rust:latest
RUN rustup target add x86_64-pc-windows-msvc && \
rustup target add x86_64-apple-darwin && \
rustup target add aarch64-apple-darwin && \
apt-get update && \
./install-llvm.sh && \
apt-get install -y libc6-dev libasound2-dev libudev-dev genisoimage mingw-w64 && \
apt-get install -y clang-19 lld-19 libc6-dev libasound2-dev libudev-dev genisoimage mingw-w64 && \
cargo install cargo-bundle xwin && \
xwin --accept-license splat --output xwin && \
rm -rf .xwin-cache && \
ln -s $(which clang-22) /usr/bin/clang && \
ln -s $(which clang++-22) /usr/bin/clang++
ln -s $(which clang-19) /usr/bin/clang && \
ln -s $(which clang++-19) /usr/bin/clang++
COPY --from=osxcross /osxcross /osxcross
ENV PATH="/osxcross/bin:$PATH" \
LD_LIBRARY_PATH="/osxcross/lib" \
CC="clang-22" CXX="clang++-22" AR="llvm-ar-22" \
CC="clang-19" CXX="clang++-19" AR="llvm-ar-19" \
CC_x86_64-apple-darwin="o64-clang" \
CXX_x86_64-apple-darwin="o64-clang++" \
CC_aarch64-apple-darwin="oa64-clang" \
@ -45,13 +26,11 @@ ENV PATH="/osxcross/bin:$PATH" \
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" \
CFLAGS_x86_64-pc-windows-msvc="-I/xwin/crt/include -I/xwin/sdk/include/ucrt -I/xwin/sdk/include/um -I/xwin/sdk/include/shared" \
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-22" \
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-22" \
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-22" \
RC_PATH="llvm-rc-22" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-19" \
RC_PATH="llvm-rc-19" \
MACOSX_DEPLOYMENT_TARGET="14.5"
ENTRYPOINT ["bash"]

View File

@ -18,23 +18,16 @@ fn main() -> Result<(), Box<dyn Error>> {
};
builder
.include(Path::new("shrooms-vb-core/core"))
.include(Path::new("shrooms-vb-core/util"))
.opt_level(opt_level)
.flag_if_supported("-fno-strict-aliasing")
.define("_CRT_SECURE_NO_WARNINGS", None)
.define("VB_LITTLE_ENDIAN", None)
.define("VB_SIGNED_PROPAGATE", None)
.define("VB_DIV_GENERIC", None)
.define("VB_DIRECT_EXCEPTION", "on_exception")
.define("VB_DIRECT_EXECUTE", "on_execute")
.define("VB_DIRECT_FETCH", "on_fetch")
.define("VB_DIRECT_FRAME", "on_frame")
.define("VB_DIRECT_READ", "on_read")
.define("VB_DIRECT_WRITE", "on_write")
.define("VBU_REALLOC", "vbu_realloc_shim")
.file(Path::new("shrooms-vb-core/core/vb.c"))
.file(Path::new("shrooms-vb-core/util/isx.c"))
.file(Path::new("shrooms-vb-core/util/vbu.c"))
.compile("vb");
Ok(())

View File

@ -1,19 +0,0 @@
#!/usr/bin/env bash
# Hopefully temporary script to manually install working llvm packages.
# The apt index for these is broken in bookworm, and upgrading to trixie
# would make them depend on too new of a libc version.
# https://apt.llvm.org/bookworm/pool/main/l/llvm-toolchain-22/clang-22_22.1.0~%2B%2B20260223093050%2Bcd5897175d0d-1~exp1~20260223093246.28_amd64.deb
PACKAGES=('clang-22' 'clang-tools-22' 'libclang-common-22-dev' 'libclang-cpp22' 'libclang-rt-22-dev' 'libclang1-22' 'libllvm22' 'lld-22' 'llvm-22' 'llvm-22-dev' 'llvm-22-linker-tools' 'llvm-22-runtime' 'llvm-22-tools')
FILES=()
URL='https://apt.llvm.org/bookworm/pool/main/l/llvm-toolchain-22'
VERSION='22.1.0~%2B%2B20260223093050%2Bcd5897175d0d-1~exp1~20260223093246.28_amd64.deb'
apt-get install -y curl python3
for package in "${PACKAGES[@]}"; do
curl -O -L "$URL/${package}_$VERSION"
FILES+=("./${package}_$VERSION")
done
apt-get install -y "${FILES[@]}"

View File

@ -1,4 +0,0 @@
Types: deb deb-src
URIs: http://apt.llvm.org/bookworm/
Suites: llvm-toolchain-bookworm-22
Components: main

View File

@ -1,4 +0,0 @@
set -e
docker build --pull -f build.Dockerfile -t lemur-build .
MSYS_NO_PATHCONV=1 docker run -it --rm -v .:/app -w /app lemur-build /app/scripts/do-bundle.sh

View File

@ -24,7 +24,8 @@ if ! command -v docker 2>&1 >/dev/null; then
exit 1
fi
./scripts/bundle.sh
docker build -f build.Dockerfile -t lemur-build .
MSYS_NO_PATHCONV=1 docker run -it --rm -v .:/app -w /app --entrypoint bash lemur-build /app/scripts/do-bundle.sh
body=$(cat <<EOF
## How to install

@ -1 +1 @@
Subproject commit 29ade46a0a58e885a9a913f738cdb30d54e0a9c5
Subproject commit ecbd103917315e3aa24fd2a682208f5548ec5d1b

View File

@ -1,40 +1,33 @@
use std::{
collections::hash_map::Entry,
num::NonZero,
sync::Arc,
thread,
time::{Duration, Instant},
};
use std::{collections::HashSet, num::NonZero, sync::Arc, thread, time::Duration};
use egui::{
Context, FontData, FontDefinitions, FontFamily, IconData, PlatformOutput, RawInput,
TextWrapMode, ViewportBuilder, ViewportCommand, ViewportEvent, ViewportId, ViewportIdMap,
ViewportIdSet, ViewportInfo, style::ScrollStyle,
Context, FontData, FontDefinitions, FontFamily, IconData, TextWrapMode, ViewportBuilder,
ViewportCommand, ViewportId, ViewportInfo,
ahash::{HashMap, HashMapExt},
style::ScrollStyle,
};
use egui_wgpu::winit::Painter;
use egui_winit::EventResponse;
use gilrs::{EventType, Gilrs};
use tracing::{error, warn};
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy},
window::{Window, WindowId},
event_loop::{ActiveEventLoop, EventLoopProxy},
window::Window,
};
use crate::{
config::CliArgs,
controller::ControllerManager,
emulator::{EmulatorClient, EmulatorCommand, SimId},
images::ImageTextureLoader,
images::ImageProcessor,
input::{MappingProvider, ShortcutProvider},
memory::MemoryClient,
persistence::Persistence,
window::{AppWindow, ChildWindow, GameScreen, GameWindow, InitArgs},
window::{
AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow,
GdbServerWindow, InputWindow, ObjectWindow, RegisterWindow, ShortcutsWindow, WorldWindow,
},
};
const EGUI_FILENAME: &str = "egui";
fn load_icon() -> anyhow::Result<IconData> {
let bytes = include_bytes!("../assets/lemur-256x256.png");
let img = image::load_from_memory_with_format(bytes, image::ImageFormat::Png)?;
@ -46,378 +39,148 @@ fn load_icon() -> anyhow::Result<IconData> {
})
}
struct SharedViewportState {
viewport_info: ViewportIdMap<ViewportInfo>,
painter: Painter,
resized_viewport: Option<ViewportId>,
}
pub struct Application {
client: EmulatorClient,
persistence: Persistence,
ctx: Context,
shared: SharedViewportState,
icon: Option<Arc<IconData>>,
app: GameWindow,
wgpu: WgpuState,
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
mappings: MappingProvider,
shortcuts: ShortcutProvider,
controllers: ControllerManager,
viewports: ViewportIdMap<ViewportManager>,
memory: Arc<MemoryClient>,
images: ImageProcessor,
persistence: Persistence,
viewports: HashMap<ViewportId, Viewport>,
focused: Option<ViewportId>,
redraw_times: ViewportIdMap<Instant>,
initial_windows: Vec<ChildWindow>,
init_debug_port: Option<u16>,
}
impl Application {
pub fn new(
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
args: CliArgs,
debug_port: Option<u16>,
) -> Self {
let wgpu = WgpuState::new();
let icon = load_icon().ok().map(Arc::new);
let mappings = MappingProvider::new(persistence.clone(), args.player2_controller);
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 = Arc::new(ImageTextureLoader::new());
let images = ImageProcessor::new();
{
let mappings = mappings.clone();
let proxy = proxy.clone();
thread::spawn(|| process_gamepad_input(mappings, proxy));
}
let app = GameWindow::new(
ViewportId::ROOT,
client.clone(),
proxy.clone(),
persistence.clone(),
shortcuts.clone(),
&memory,
&images,
mappings.clone(),
SimId::Player1,
);
let ctx = Context::default();
let data = persistence.load_config(EGUI_FILENAME).unwrap_or_default();
ctx.data_mut(|d| *d = data);
let mut fonts = FontDefinitions::default();
fonts.font_data.insert(
"Selawik".into(),
Arc::new(FontData::from_static(include_bytes!(
"../assets/selawik.ttf"
))),
);
fonts
.families
.get_mut(&FontFamily::Proportional)
.unwrap()
.insert(0, "Selawik".into());
ctx.set_fonts(fonts);
ctx.global_style_mut(|s| {
s.wrap_mode = Some(TextWrapMode::Extend);
s.visuals.menu_corner_radius = Default::default();
s.spacing.scroll = ScrollStyle::thin();
});
egui_extras::install_image_loaders(&ctx);
ctx.add_texture_loader(images.clone());
ctx.set_embed_viewports(false);
{
let proxy = proxy.clone();
ctx.set_request_repaint_callback(move |info| {
let _ = proxy.send_event(UserEvent::RequestRedraw(
info.viewport_id,
Instant::now() + info.delay,
));
});
}
let wgpu_config = egui_wgpu::WgpuConfiguration {
present_mode: wgpu::PresentMode::AutoVsync,
wgpu_setup: egui_wgpu::WgpuSetup::Existing(egui_wgpu::WgpuSetupExisting {
instance: wgpu.instance.clone(),
adapter: wgpu.adapter.clone(),
device: wgpu.device.clone(),
queue: wgpu.queue.clone(),
}),
..egui_wgpu::WgpuConfiguration::default()
};
let options = egui_wgpu::RendererOptions::default();
let painter = pollster::block_on(Painter::new(ctx.clone(), wgpu_config, false, options));
let mut initial_windows = vec![];
if let Some(port) = args.debug_port {
initial_windows.push(ChildWindow::Debugger { port: Some(port) });
}
if args.profile {
initial_windows.push(ChildWindow::Profiler { launch: true });
}
if args.character_data {
initial_windows.push(ChildWindow::CharacterData);
}
if args.bgmap_data {
initial_windows.push(ChildWindow::BgMap);
}
if args.object_data {
initial_windows.push(ChildWindow::Objects);
}
if args.worlds {
initial_windows.push(ChildWindow::Worlds);
}
if args.frame_buffers {
initial_windows.push(ChildWindow::FrameBuffers);
}
if args.registers {
initial_windows.push(ChildWindow::Registers);
}
if args.terminal {
initial_windows.push(ChildWindow::Terminal);
}
if args.player2 {
client.send_command(EmulatorCommand::StartSecondSim(args.rom.clone()));
initial_windows.push(ChildWindow::Player2);
}
Self {
client,
persistence,
ctx,
shared: SharedViewportState {
viewport_info: ViewportIdMap::default(),
painter,
resized_viewport: None,
},
icon,
app,
wgpu,
client,
proxy,
mappings,
shortcuts,
memory,
images,
controllers,
viewports: ViewportIdMap::default(),
persistence,
viewports: HashMap::new(),
focused: None,
redraw_times: ViewportIdMap::default(),
initial_windows,
init_debug_port: debug_port,
}
}
fn check_repaint(&mut self, event_loop: &ActiveEventLoop) {
let now = Instant::now();
self.redraw_times.retain(|viewport_id, time| {
if *time > now {
return true;
}
if let Some(viewport) = self.viewports.get(viewport_id) {
viewport.window.request_redraw();
}
false
});
if let Some(next_repaint_time) = self.redraw_times.values().min().copied() {
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
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();
return;
}
}
fn repaint_all(&mut self, event_loop: &ActiveEventLoop) {
enum ViewportUpdate {
Keep {
recreate: bool,
builder: ViewportBuilder,
parent: ViewportId,
commands: Vec<ViewportCommand>,
},
Remove,
}
let mut updates = ViewportIdMap::<ViewportUpdate>::default();
for viewport in self.viewports.values() {
updates.insert(
viewport.id,
ViewportUpdate::Keep {
recreate: false,
builder: viewport.builder.clone(),
parent: self
.shared
.viewport_info
.get(&viewport.id)
.and_then(|vp| vp.parent)
.unwrap_or(ViewportId::ROOT),
commands: vec![],
},
);
}
for viewport in self.viewports.values_mut() {
let mut input = viewport.take_egui_input();
if !self.shared.viewport_info.contains_key(&viewport.id) {
continue;
}
input.viewports = self.shared.viewport_info.clone();
let output = self.ctx.run_ui(input, |ui| {
if viewport.id == ViewportId::ROOT {
self.app.show(ui);
} else if let Some(cb) = ui.viewport(|v| v.viewport_ui_cb.clone()) {
cb(ui);
}
});
viewport.handle_platform_output(output.platform_output);
for (id, update) in updates.iter_mut() {
if !output.viewport_output.contains_key(id) {
self.shared.viewport_info.remove(id);
*update = ViewportUpdate::Remove;
}
}
for (id, mut vp) in output.viewport_output {
match updates.entry(id) {
Entry::Vacant(e) => {
e.insert(ViewportUpdate::Keep {
recreate: true,
builder: vp.builder,
parent: vp.parent,
commands: vp.commands,
});
}
Entry::Occupied(e) => {
let ViewportUpdate::Keep {
recreate,
builder,
commands,
..
} = e.into_mut()
else {
continue;
};
let (_, should_recreate) = builder.patch(vp.builder);
*recreate |= should_recreate;
commands.append(&mut vp.commands);
}
}
}
let clipped_primitives = self.ctx.tessellate(output.shapes, output.pixels_per_point);
viewport.window.pre_present_notify();
self.shared.painter.paint_and_update_textures(
viewport.id,
output.pixels_per_point,
[0.0, 0.0, 0.0, 0.0],
&clipped_primitives,
&output.textures_delta,
vec![],
);
}
let mut active_viewports = ViewportIdSet::default();
for (id, update) in updates {
match update {
ViewportUpdate::Keep {
recreate,
builder,
parent,
commands,
} => {
active_viewports.insert(id);
let manager = self
.viewports
.remove(&id)
.filter(|_| !recreate)
.unwrap_or_else(|| {
let manager = ViewportManager::new(
&self.ctx,
id,
parent,
event_loop,
builder,
&mut self.shared,
);
self.app.handle_init(
id,
InitArgs {
window: &manager.window,
render_state: &self.shared.painter.render_state().unwrap(),
},
);
manager
});
manager.process_viewport_commands(commands, &mut self.shared);
self.viewports.insert(id, manager);
}
ViewportUpdate::Remove => {
self.viewports.remove(&id);
}
}
}
if !active_viewports.contains(&ViewportId::ROOT) {
event_loop.exit();
}
self.shared.painter.gc_viewports(&active_viewports);
self.viewports.insert(
viewport_id,
Viewport::new(event_loop, &self.wgpu, self.icon.clone(), window),
);
}
}
impl ApplicationHandler<UserEvent> for Application {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let mut viewport_builder = self.app.initial_viewport();
if let Some(icon) = &self.icon {
viewport_builder = viewport_builder.with_icon(icon.clone());
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 manager = ViewportManager::new(
&self.ctx,
ViewportId::ROOT,
ViewportId::ROOT,
event_loop,
viewport_builder,
&mut self.shared,
let app = GameWindow::new(
self.client.clone(),
self.proxy.clone(),
self.persistence.clone(),
self.shortcuts.clone(),
SimId::Player1,
);
let render_state = self.shared.painter.render_state().unwrap();
GameScreen::init_pipeline(&render_state);
self.app.handle_init(
ViewportId::ROOT,
InitArgs {
window: &manager.window,
render_state: &render_state,
},
);
self.viewports.insert(ViewportId::ROOT, manager);
for window in std::mem::take(&mut self.initial_windows) {
self.app.open(window);
}
}
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
if let Some(viewport) = self.viewports.get(&ViewportId::ROOT) {
viewport.window.request_redraw();
}
self.open(event_loop, Box::new(app));
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
window_id: winit::window::WindowId,
event: WindowEvent,
) {
let Some(viewport) = self
.viewports
.values_mut()
.find(|v| v.has_window_id(window_id))
.find(|v| v.window.id() == window_id)
else {
return;
};
let viewport_id = viewport.id;
let (response, close_requested) = viewport.on_window_event(&event, &mut self.shared);
if close_requested && viewport_id == ViewportId::ROOT {
event_loop.exit();
}
if response.repaint {
viewport.window.request_redraw();
}
if !response.consumed {
let viewport_id = 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 !self.app.handle_key_event(viewport_id, &event) =>
{
self.controllers.handle_key_event(&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);
}
WindowEvent::RedrawRequested => {
self.repaint_all(event_loop);
}
_ => {}
}
}
self.check_repaint(event_loop);
match action {
Some(Action::Redraw) => {
for viewport in self.viewports.values_mut() {
match viewport.redraw(event_loop) {
Some(Action::Redraw) => {
queue_redraw = true;
}
Some(Action::Close) => {
inactive_viewports.insert(viewport.id());
}
None => {}
}
}
}
Some(Action::Close) => {
inactive_viewports.insert(viewport_id);
}
None => {}
}
self.viewports
.retain(|k, _| !inactive_viewports.contains(k));
match self.viewports.get(&ViewportId::ROOT) {
Some(viewport) => {
if queue_redraw {
viewport.window.request_redraw();
}
}
None => event_loop.exit(),
}
}
fn device_event(
@ -434,55 +197,106 @@ impl ApplicationHandler<UserEvent> for Application {
else {
return;
};
viewport.on_mouse_motion(delta);
viewport.state.on_mouse_motion(delta);
}
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
match event {
UserEvent::GamepadEvent(event) => {
if !self
if let Some(viewport) = self
.focused
.is_some_and(|id| self.app.handle_gamepad_event(id, &event))
.as_ref()
.and_then(|id| self.viewports.get_mut(id))
{
self.controllers.handle_gamepad_event(&event);
if viewport.app.handle_gamepad_event(&event) {
return;
}
}
self.controllers.handle_gamepad_event(&event);
}
UserEvent::Quit(sim_id) => match sim_id {
SimId::Player1 => event_loop.exit(),
SimId::Player2 => self.app.close(ChildWindow::Player2),
},
UserEvent::RequestRedraw(viewport, when) => {
let scheduled = self.redraw_times.entry(viewport).or_insert(when);
if *scheduled > when {
*scheduled = when;
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::OpenShortcuts => {
let shortcuts = ShortcutsWindow::new(self.shortcuts.clone());
self.open(event_loop, Box::new(shortcuts));
}
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();
}
}
}
self.check_repaint(event_loop);
}
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
if let Some(viewport) = self.viewports.get(&ViewportId::ROOT) {
viewport.window.request_redraw();
}
}
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
if let Err(error) = self
.ctx
.data(|d| self.persistence.save_config(EGUI_FILENAME, d))
{
error!(%error, "could not save egui state.");
}
let (sender, receiver) = oneshot::channel();
if self.client.send_command(EmulatorCommand::Exit(sender))
&& let Err(error) = receiver.recv_timeout(Duration::from_secs(5))
{
error!(%error, "could not gracefully exit.");
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.");
}
}
}
}
struct WgpuState {
instance: wgpu::Instance,
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
instance: Arc<wgpu::Instance>,
adapter: Arc<wgpu::Adapter>,
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
}
impl WgpuState {
@ -490,147 +304,245 @@ impl WgpuState {
#[allow(unused_variables)]
let egui_wgpu::WgpuConfiguration {
wgpu_setup:
egui_wgpu::WgpuSetup::CreateNew(egui_wgpu::WgpuSetupCreateNew {
instance_descriptor: wgpu::InstanceDescriptor { backends, .. },
egui_wgpu::WgpuSetup::CreateNew {
supported_backends,
device_descriptor,
..
}),
},
..
} = egui_wgpu::WgpuConfiguration::default()
else {
panic!("required fields not found")
};
#[cfg(windows)]
let backends = wgpu::Backends::from_env()
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,
..wgpu::InstanceDescriptor::new_without_display_handle_from_env()
backends: supported_backends,
..wgpu::InstanceDescriptor::default()
});
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference:
wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::LowPower),
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: None,
force_fallback_adapter: false,
}))
.expect("could not create adapter");
let (device, queue) =
pollster::block_on(adapter.request_device(&(*device_descriptor)(&adapter)))
.expect("could not request device");
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,
adapter,
device,
queue,
instance: Arc::new(instance),
adapter: Arc::new(adapter),
device: Arc::new(device),
queue: Arc::new(queue),
}
}
}
struct ViewportManager {
id: ViewportId,
struct Viewport {
painter: egui_wgpu::winit::Painter,
ctx: Context,
info: ViewportInfo,
commands: Vec<ViewportCommand>,
builder: ViewportBuilder,
window: Arc<Window>,
state: egui_winit::State,
builder: ViewportBuilder,
app: Box<dyn AppWindow>,
}
impl ViewportManager {
fn new(
ctx: &Context,
viewport_id: ViewportId,
parent_id: ViewportId,
impl Viewport {
pub fn new(
event_loop: &ActiveEventLoop,
builder: ViewportBuilder,
shared: &mut SharedViewportState,
wgpu: &WgpuState,
icon: Option<Arc<IconData>>,
mut app: Box<dyn AppWindow>,
) -> Self {
let window = Arc::new(egui_winit::create_window(ctx, event_loop, &builder).unwrap());
pollster::block_on(shared.painter.set_window(viewport_id, Some(window.clone()))).unwrap();
let state = egui_winit::State::new(
ctx.clone(),
viewport_id,
event_loop,
Some(window.scale_factor() as f32),
event_loop.system_theme(),
shared.painter.max_texture_side(),
let ctx = Context::default();
let mut fonts = FontDefinitions::default();
fonts.font_data.insert(
"Selawik".into(),
Arc::new(FontData::from_static(include_bytes!(
"../assets/selawik.ttf"
))),
);
fonts
.families
.get_mut(&FontFamily::Proportional)
.unwrap()
.insert(0, "Selawik".into());
ctx.set_fonts(fonts);
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 mut info = ViewportInfo {
parent: Some(parent_id),
..ViewportInfo::default()
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(),
},
..egui_wgpu::WgpuConfiguration::default()
};
egui_winit::update_viewport_info(&mut info, ctx, &window, true);
shared.viewport_info.insert(viewport_id, info);
let mut painter =
egui_wgpu::winit::Painter::new(ctx.clone(), wgpu_config, 1, None, false, true);
let mut info = ViewportInfo::default();
let mut builder = app.initial_viewport();
if let Some(icon) = icon {
builder = builder.with_icon(icon);
}
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());
Self {
id: viewport_id,
painter,
ctx,
info,
commands: vec![],
builder,
window,
state,
builder,
app,
}
}
fn has_window_id(&self, window_id: WindowId) -> bool {
self.window.id() == window_id
pub fn id(&self) -> ViewportId {
self.app.viewport_id()
}
fn on_mouse_motion(&mut self, delta: (f64, f64)) {
self.state.on_mouse_motion(delta);
}
fn on_window_event(
&mut self,
event: &WindowEvent,
shared: &mut SharedViewportState,
) -> (EventResponse, bool) {
if let WindowEvent::Resized(size) = event
&& let (Some(width), Some(height)) =
(NonZero::new(size.width), NonZero::new(size.height))
{
if shared.resized_viewport != Some(self.id) {
shared.resized_viewport = Some(self.id);
shared.painter.on_window_resize_state_change(self.id, true);
}
shared.painter.on_window_resized(self.id, width, height);
}
pub fn on_window_event(&mut self, event: &WindowEvent) -> (bool, Option<Action>) {
let response = self.state.on_window_event(&self.window, event);
let info = shared.viewport_info.get_mut(&self.id).unwrap();
if let WindowEvent::CloseRequested = event {
info.events.push(ViewportEvent::Close);
}
egui_winit::update_viewport_info(info, self.state.egui_ctx(), &self.window, false);
(response, info.close_requested())
}
fn take_egui_input(&mut self) -> RawInput {
self.state.take_egui_input(&self.window)
}
fn handle_platform_output(&mut self, platform_output: PlatformOutput) {
self.state
.handle_platform_output(&self.window, platform_output);
}
fn process_viewport_commands(
&self,
commands: Vec<ViewportCommand>,
shared: &mut SharedViewportState,
) {
let info = shared.viewport_info.get_mut(&self.id).unwrap();
egui_winit::process_viewport_commands(
egui_winit::update_viewport_info(
&mut self.info,
self.state.egui_ctx(),
info,
commands,
&self.window,
&mut vec![],
false,
);
let action = match event {
WindowEvent::RedrawRequested => Some(Action::Redraw),
WindowEvent::CloseRequested => Some(Action::Close),
WindowEvent::Resized(size) => {
if let (Some(width), Some(height)) =
(NonZero::new(size.width), NonZero::new(size.height))
{
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> {
let mut input = self.state.take_egui_input(&self.window);
input.viewports = std::iter::once((ViewportId::ROOT, self.info.clone())).collect();
let mut output = self.ctx.run(input, |ctx| {
self.app.show(ctx);
});
let clipped_primitives = self.ctx.tessellate(output.shapes, output.pixels_per_point);
self.painter.paint_and_update_textures(
ViewportId::ROOT,
output.pixels_per_point,
[0.0, 0.0, 0.0, 0.0],
&clipped_primitives,
&output.textures_delta,
vec![],
);
self.state
.handle_platform_output(&self.window, output.platform_output);
let Some(mut viewport_output) = output.viewport_output.remove(&ViewportId::ROOT) else {
return Some(Action::Close);
};
let (mut deferred_commands, recreate) = self.builder.patch(viewport_output.builder);
if recreate {
let (window, state) =
create_window_and_state(&self.ctx, event_loop, &self.builder, &mut self.painter);
egui_winit::update_viewport_info(&mut self.info, &self.ctx, &window, true);
self.window = window;
self.state = state;
}
self.commands.append(&mut deferred_commands);
self.commands.append(&mut viewport_output.commands);
egui_winit::process_viewport_commands(
&self.ctx,
&mut self.info,
std::mem::take(&mut self.commands),
&self.window,
&mut HashSet::default(),
);
if self.info.close_requested() {
Some(Action::Close)
} else {
Some(Action::Redraw)
}
}
}
impl Drop for Viewport {
fn drop(&mut self) {
self.app.on_destroy();
}
}
#[derive(Debug)]
pub enum UserEvent {
GamepadEvent(gilrs::Event),
OpenAbout,
OpenCharacterData(SimId),
OpenBgMap(SimId),
OpenObjects(SimId),
OpenWorlds(SimId),
OpenFrameBuffers(SimId),
OpenRegisters(SimId),
OpenDebugger(SimId),
OpenInput,
OpenShortcuts,
OpenPlayer2,
Quit(SimId),
RequestRedraw(ViewportId, Instant),
}
pub enum Action {
Redraw,
Close,
}
fn create_window_and_state(
ctx: &Context,
event_loop: &ActiveEventLoop,
builder: &ViewportBuilder,
painter: &mut egui_wgpu::winit::Painter,
) -> (Arc<Window>, egui_winit::State) {
pollster::block_on(painter.set_window(ViewportId::ROOT, None)).unwrap();
let window = Arc::new(egui_winit::create_window(ctx, event_loop, builder).unwrap());
pollster::block_on(painter.set_window(ViewportId::ROOT, Some(window.clone()))).unwrap();
let state = egui_winit::State::new(
ctx.clone(),
ViewportId::ROOT,
event_loop,
Some(window.scale_factor() as f32),
event_loop.system_theme(),
painter.max_texture_side(),
);
(window, state)
}
fn process_gamepad_input(mappings: MappingProvider, proxy: EventLoopProxy<UserEvent>) {

View File

@ -1,22 +1,20 @@
use std::time::Duration;
use anyhow::{Result, bail};
use audioadapter_buffers::direct::InterleavedSlice;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use rubato::Resampler;
use itertools::Itertools;
use rubato::{FftFixedInOut, Resampler};
use tracing::error;
pub struct Audio {
#[allow(unused)]
stream: cpal::Stream,
sampler: rubato::Async<f32>,
input_buffer: Vec<f32>,
output_buffer: Vec<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,20 +28,11 @@ 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 = rubato::Async::new_poly(
resample_ratio,
64.0,
rubato::PolynomialDegree::Cubic,
chunk_size,
2,
rubato::FixedAsync::Output,
)?;
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 = Vec::with_capacity(sampler.nbr_channels() * sampler.input_frames_max());
let output_buffer = vec![0.0; sampler.nbr_channels() * sampler.output_frames_max()];
let input_buffer = sampler.input_buffer_allocate(true);
let output_buffer = sampler.output_buffer_allocate(true);
let (sample_sink, mut sample_source) =
rtrb::RingBuffer::new(sampler.output_frames_max() * 4);
@ -79,47 +68,37 @@ impl Audio {
})
}
pub fn update(&mut self, mut samples: &[f32]) {
while self.input_buffer.len() + samples.len() >= self.sampler.input_frames_next() * 2 {
let samples_needed =
(self.sampler.input_frames_next() * 2).saturating_sub(self.input_buffer.len());
let (current_samples, future_samples) = samples.split_at(samples_needed);
self.input_buffer.extend_from_slice(current_samples);
samples = future_samples;
let buffer_in =
InterleavedSlice::new(&self.input_buffer, 2, self.sampler.input_frames_next())
pub fn update(&mut self, samples: &[f32]) {
for sample in samples.chunks_exact(2) {
for (channel, value) in self.input_buffer.iter_mut().zip(sample) {
channel.push(*value);
}
if self.input_buffer[0].len() >= self.sampler.input_frames_next() {
let (_, output_samples) = self
.sampler
.process_into_buffer(&self.input_buffer, &mut self.output_buffer, None)
.unwrap();
let mut buffer_out = InterleavedSlice::new_mut(
&mut self.output_buffer,
2,
self.sampler.output_frames_next(),
)
.unwrap();
let (_, output_samples) = self
.sampler
.process_into_buffer(&buffer_in, &mut buffer_out, None)
.unwrap();
let chunk = match self.sample_sink.write_chunk_uninit(output_samples * 2) {
Ok(c) => c,
Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => {
self.sample_sink.write_chunk_uninit(n).unwrap()
}
};
chunk.fill_from_iter(self.output_buffer[..output_samples * 2].iter().copied());
self.input_buffer.clear();
let chunk = match self.sample_sink.write_chunk_uninit(output_samples * 2) {
Ok(c) => c,
Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => {
self.sample_sink.write_chunk_uninit(n).unwrap()
}
};
let interleaved = self.output_buffer[0]
.iter()
.interleave(self.output_buffer[1].iter())
.cloned();
chunk.fill_from_iter(interleaved);
for channel in &mut self.input_buffer {
channel.clear();
}
}
}
self.input_buffer.extend_from_slice(samples);
while self.sample_sink.slots() < self.sampler.output_frames_max() * 2 {
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,106 +0,0 @@
use anyhow::Result;
use clap::Parser;
use egui::{Color32, Pos2, Vec2};
use serde::{Deserialize, Serialize};
use crate::{emulator::SimId, persistence::Persistence, window::DisplayMode};
use std::path::PathBuf;
#[derive(Parser)]
#[command(version, long_version = env!("CARGO_PKG_VERSION"))]
pub struct CliArgs {
/// The path to a virtual boy ROM to run.
pub rom: Option<PathBuf>,
/// Start a GDB/LLDB debug server on this port.
#[arg(short, long)]
pub debug_port: Option<u16>,
/// Enable profiling a game
#[arg(short, long)]
pub profile: bool,
/// Open character data window
#[arg(short, long)]
pub character_data: bool,
/// Open bgmap data window
#[arg(short, long)]
pub bgmap_data: bool,
/// Open object data window
#[arg(short, long)]
pub object_data: bool,
/// Open worlds window
#[arg(long)]
pub worlds: bool,
/// Open frame buffers window
#[arg(short, long)]
pub frame_buffers: bool,
/// Open registers window
#[arg(short, long)]
pub registers: bool,
/// Open terminal
#[arg(short, long)]
pub terminal: bool,
/// Watch ROM files for changes, automatically reload
#[arg(short, long)]
pub watch: bool,
/// Automatically open Player 2 for multiplayer
#[arg(long)]
pub player2: bool,
/// Map the first connected controller to Player 2
#[arg(long)]
pub player2_controller: bool,
}
pub const COLOR_PRESETS: [[Color32; 2]; 3] = [
[
Color32::from_rgb(0xff, 0x00, 0x00),
Color32::from_rgb(0x00, 0xc6, 0xf0),
],
[
Color32::from_rgb(0x00, 0xb4, 0x00),
Color32::from_rgb(0xc8, 0x00, 0xff),
],
[
Color32::from_rgb(0xb4, 0x9b, 0x00),
Color32::from_rgb(0x00, 0x00, 0xff),
],
];
const fn default_audio_enabled() -> bool {
true
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct SimConfig {
pub display_mode: DisplayMode,
pub colors: [Color32; 2],
pub dimensions: Vec2,
#[serde(default = "default_audio_enabled")]
pub audio_enabled: bool,
#[serde(default)]
pub position: Option<Pos2>,
}
impl SimConfig {
pub fn load(persistence: &Persistence, sim_id: SimId) -> Self {
if let Ok(config) = persistence.load_config(config_filename(sim_id)) {
return config;
}
Self {
display_mode: DisplayMode::Anaglyph,
colors: COLOR_PRESETS[0],
dimensions: DisplayMode::Anaglyph.proportions() + Vec2::new(0.0, 22.0),
audio_enabled: true,
position: None,
}
}
pub fn save(&self, persistence: &Persistence, sim_id: SimId) -> Result<()> {
persistence.save_config(config_filename(sim_id), self)
}
}
fn config_filename(sim_id: SimId) -> &'static str {
match sim_id {
SimId::Player1 => "config_p1",
SimId::Player2 => "config_p2",
}
}

View File

@ -1,19 +1,20 @@
use std::{
collections::HashMap,
fmt::Display,
fs::{self, File},
io::{Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
sync::{
Arc, Weak,
atomic::{AtomicBool, Ordering},
mpsc::{self, RecvTimeoutError, TryRecvError},
mpsc::{self, RecvError, TryRecvError},
},
time::Duration,
};
use anyhow::Result;
use atomic::Atomic;
use bytemuck::NoUninit;
use egui_notify::Toast;
use egui_toast::{Toast, ToastKind, ToastOptions};
use tracing::{error, warn};
use crate::{
@ -21,19 +22,11 @@ use crate::{
graphics::TextureSink,
memory::{MemoryRange, MemoryRegion},
};
use cart::Cart;
pub use game_info::GameInfo;
pub use inline_stack_map::InlineStack;
use inline_stack_map::InlineStackMap;
use shrooms_vb_core::{EXPECTED_FRAME_SIZE, Sim, StopReason};
pub use shrooms_vb_core::{SimEvent, VBKey, VBRegister, VBWatchpointType};
pub use shrooms_vb_core::{VBKey, VBRegister, VBWatchpointType};
mod address_set;
mod cart;
mod game_info;
mod inline_stack_map;
mod shrooms_vb_core;
mod shrooms_vb_util;
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub enum SimId {
@ -50,13 +43,6 @@ impl SimId {
Self::Player2 => 1,
}
}
pub const fn from_index(index: usize) -> Option<Self> {
match index {
0 => Some(Self::Player1),
1 => Some(Self::Player2),
_ => None,
}
}
}
impl Display for SimId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@ -67,6 +53,43 @@ impl Display for SimId {
}
}
struct Cart {
rom_path: PathBuf,
rom: Vec<u8>,
sram_file: File,
sram: Vec<u8>,
}
impl Cart {
fn load(rom_path: &Path, sim_id: SimId) -> Result<Self> {
let rom = fs::read(rom_path)?;
let mut sram_file = File::options()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(sram_path(rom_path, sim_id))?;
sram_file.set_len(8 * 1024)?;
let mut sram = vec![];
sram_file.read_to_end(&mut sram)?;
Ok(Cart {
rom_path: rom_path.to_path_buf(),
rom,
sram_file,
sram,
})
}
}
fn sram_path(rom_path: &Path, sim_id: SimId) -> PathBuf {
match sim_id {
SimId::Player1 => rom_path.with_extension("p1.sram"),
SimId::Player2 => rom_path.with_extension("p2.sram"),
}
}
pub struct EmulatorBuilder {
rom: Option<PathBuf>,
commands: mpsc::Receiver<EmulatorCommand>,
@ -75,7 +98,6 @@ pub struct EmulatorBuilder {
audio_on: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
start_paused: bool,
watch_rom: Arc<[AtomicBool; 2]>,
}
impl EmulatorBuilder {
@ -92,13 +114,12 @@ impl EmulatorBuilder {
audio_on: Arc::new([AtomicBool::new(true), AtomicBool::new(true)]),
linked: Arc::new(AtomicBool::new(false)),
start_paused: false,
watch_rom: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]),
};
let client = EmulatorClient {
queue,
sim_state: builder.sim_state.clone(),
state: builder.state.clone(),
watch_rom: builder.watch_rom.clone(),
audio_on: builder.audio_on.clone(),
linked: builder.linked.clone(),
};
(builder, client)
@ -118,25 +139,12 @@ impl EmulatorBuilder {
}
}
pub fn with_audio_on(self, p1: bool, p2: bool) -> Self {
self.audio_on[0].store(p1, Ordering::Relaxed);
self.audio_on[1].store(p2, Ordering::Relaxed);
self
}
pub fn with_watch_rom(self, p1: bool, p2: bool) -> Self {
self.watch_rom[0].store(p1, Ordering::Relaxed);
self.watch_rom[1].store(p2, Ordering::Relaxed);
self
}
pub fn build(self) -> Result<Emulator> {
let mut emulator = Emulator::new(
self.commands,
self.sim_state,
self.state,
self.audio_on,
self.watch_rom,
self.linked,
)?;
if let Some(path) = self.rom {
@ -157,15 +165,12 @@ pub struct Emulator {
sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>,
watch_rom: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
profilers: [Option<ProfileSender>; 2],
renderers: HashMap<SimId, TextureSink>,
messages: HashMap<SimId, mpsc::Sender<Toast>>,
debuggers: HashMap<SimId, DebugInfo>,
stdouts: HashMap<SimId, mpsc::Sender<String>>,
watched_regions: HashMap<MemoryRange, Weak<MemoryRegion>>,
eye_contents: [Vec<u8>; 2],
eye_contents: Vec<u8>,
audio_samples: Vec<f32>,
buffer: Vec<u8>,
}
@ -176,7 +181,6 @@ impl Emulator {
sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>,
watch_rom: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
) -> Result<Self> {
Ok(Self {
@ -187,74 +191,45 @@ impl Emulator {
sim_state,
state,
audio_on,
watch_rom,
linked,
profilers: [None, None],
renderers: HashMap::new(),
messages: HashMap::new(),
debuggers: HashMap::new(),
stdouts: HashMap::new(),
watched_regions: HashMap::new(),
eye_contents: [vec![0u8; 384 * 224 * 2], vec![0u8; 384 * 224 * 2]],
eye_contents: vec![0u8; 384 * 224 * 2],
audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE),
buffer: vec![],
})
}
pub fn reload_cart(&mut self, sim_id: SimId) -> Result<()> {
let Some(cart) = &self.carts[sim_id.to_index()] else {
return Ok(());
};
let path = cart.file_path.clone();
self.load_cart(sim_id, &path)
}
pub fn load_cart(&mut self, sim_id: SimId, path: &Path) -> Result<()> {
let watch = self.watch_rom[sim_id.to_index()].load(Ordering::Acquire);
let cart = Cart::load(path, sim_id, watch)?;
self.try_reset_sim(sim_id, Some(cart))?;
let cart = Cart::load(path, sim_id)?;
self.reset_sim(sim_id, Some(cart))?;
Ok(())
}
pub fn start_second_sim(&mut self, rom: Option<PathBuf>) -> Result<()> {
let file_path = if let Some(path) = rom {
let rom_path = if let Some(path) = rom {
Some(path)
} else {
self.carts[0].as_ref().map(|c| c.file_path.clone())
self.carts[0].as_ref().map(|c| c.rom_path.clone())
};
let watch = self.watch_rom[SimId::Player2.to_index()].load(Ordering::Acquire);
let cart = match file_path {
Some(rom_path) => Some(Cart::load(&rom_path, SimId::Player2, watch)?),
let cart = match rom_path {
Some(rom_path) => Some(Cart::load(&rom_path, SimId::Player2)?),
None => None,
};
self.try_reset_sim(SimId::Player2, cart)?;
self.reset_sim(SimId::Player2, cart)?;
self.link_sims();
Ok(())
}
fn reset_sim(&mut self, sim_id: SimId, new_cart: Option<Cart>) -> bool {
if let Err(error) = self.try_reset_sim(sim_id, new_cart) {
self.report_error(sim_id, format!("Error resetting sim: {error}"));
false
} else {
true
}
}
fn try_reset_sim(&mut self, sim_id: SimId, new_cart: Option<Cart>) -> Result<()> {
fn reset_sim(&mut self, sim_id: SimId, new_cart: Option<Cart>) -> Result<()> {
self.save_sram(sim_id)?;
let index = sim_id.to_index();
while self.sims.len() <= index {
let new_index = self.sims.len();
self.sims.push(Sim::new());
if self
.stdouts
.contains_key(&SimId::from_index(new_index).unwrap())
{
self.sims[new_index].watch_stdout(true);
}
self.sim_state[new_index].store(SimState::NoGame, Ordering::Release);
self.sim_state[index].store(SimState::NoGame, Ordering::Release);
}
let sim = &mut self.sims[index];
sim.reset();
@ -262,27 +237,7 @@ impl Emulator {
sim.load_cart(cart.rom.clone(), cart.sram.clone())?;
self.carts[index] = Some(cart);
self.sim_state[index].store(SimState::Ready, Ordering::Release);
} else if let Some(cart) = self.carts[index].as_mut()
&& cart.is_watching()
{
cart.restart_watching();
}
let mut profiling = false;
if let Some(profiler) = self.profilers[sim_id.to_index()].as_ref()
&& let Some(cart) = self.carts[index].as_ref()
&& profiler
.send(ProfileEvent::Start {
info: cart.info.clone(),
})
.is_ok()
{
sim.monitor_events(true, cart.info.inline_stack_map().clone());
profiling = true;
}
if !profiling {
sim.monitor_events(false, InlineStackMap::empty());
}
if self.sim_state[index].load(Ordering::Acquire) == SimState::Ready {
self.resume_sims();
}
@ -356,16 +311,13 @@ impl Emulator {
}
}
fn set_speed(&mut self, speed: f64) -> Result<()> {
self.audio.set_speed(speed)
}
fn save_sram(&mut self, sim_id: SimId) -> Result<()> {
let sim = self.sims.get_mut(sim_id.to_index());
let cart = self.carts[sim_id.to_index()].as_mut();
if let (Some(sim), Some(cart)) = (sim, cart) {
sim.read_sram(&mut cart.sram);
cart.save_sram()?;
cart.sram_file.seek(SeekFrom::Start(0))?;
cart.sram_file.write_all(&cart.sram)?;
}
Ok(())
}
@ -380,11 +332,6 @@ impl Emulator {
Ok(())
}
fn start_profiling(&mut self, sim_id: SimId, sender: ProfileSender) -> Result<()> {
self.profilers[sim_id.to_index()] = Some(sender);
self.try_reset_sim(sim_id, None)
}
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
@ -458,30 +405,11 @@ impl Emulator {
let idle = self.tick();
if idle {
// The game is paused, and we have output all the video/audio we have.
// Block the thread until a new command comes in, or a ROM changes.
loop {
match self.commands.recv_timeout(Duration::from_millis(250)) {
Ok(command) => {
self.handle_command(command);
break;
}
Err(RecvTimeoutError::Timeout) => {
let mut changed = false;
for sim_id in SimId::values() {
let Some(cart) = self.carts[sim_id.to_index()].as_ref() else {
continue;
};
if cart.changed() {
changed |= self.reset_sim(sim_id, None);
}
}
if changed {
break;
}
}
Err(RecvTimeoutError::Disconnected) => {
return;
}
// Block the thread until a new command comes in.
match self.commands.recv() {
Ok(command) => self.handle_command(command),
Err(RecvError) => {
return;
}
}
}
@ -496,14 +424,6 @@ impl Emulator {
}
}
}
for sim_id in SimId::values() {
let Some(cart) = self.carts[sim_id.to_index()].as_ref() else {
continue;
};
if cart.changed() {
self.reset_sim(sim_id, None);
}
}
self.watched_regions.retain(|range, region| {
let Some(region) = region.upgrade() else {
return false;
@ -535,59 +455,25 @@ impl Emulator {
let p1_running = running && p1_state == SimState::Ready;
let p2_running = running && p2_state == SimState::Ready;
let mut idle = !p1_running && !p2_running;
let cycles = self.emulate(p1_running, p2_running);
// if we're profiling, track events
for ((sim, profiler), running) in self
.sims
.iter_mut()
.zip(self.profilers.iter_mut())
.zip([p1_running, p2_running])
{
if !running {
continue;
}
if let Some(p) = profiler {
let (event, inline_stack) = sim.take_profiler_updates();
if p.send(ProfileEvent::Update {
cycles,
event,
inline_stack,
})
.is_err()
{
sim.monitor_events(false, InlineStackMap::empty());
*profiler = None;
}
}
if p1_running && p2_running {
Sim::emulate_many(&mut self.sims);
} else if p1_running {
self.sims[SimId::Player1.to_index()].emulate();
} else if p2_running {
self.sims[SimId::Player2.to_index()].emulate();
}
if state == EmulatorState::Stepping {
self.state.store(EmulatorState::Paused, Ordering::Release);
}
// stdout
self.stdouts.retain(|sim_id, stdout| {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return true;
};
if let Some(text) = sim.take_stdout()
&& stdout.send(text).is_err()
{
sim.watch_stdout(false);
return false;
}
true
});
// 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.take_stop_reason() {
if let Some(reason) = sim.stop_reason() {
let stop_reason = match reason {
StopReason::Stepped => DebugStopReason::Trace,
StopReason::Watchpoint(watch, address) => {
@ -608,12 +494,9 @@ impl Emulator {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
continue;
};
if sim.read_pixels(&mut self.eye_contents[sim_id.to_index()]) {
if sim.read_pixels(&mut self.eye_contents) {
idle = false;
if renderer
.queue_render(&self.eye_contents[sim_id.to_index()])
.is_err()
{
if renderer.queue_render(&self.eye_contents).is_err() {
self.renderers.remove(&sim_id);
}
}
@ -646,19 +529,6 @@ impl Emulator {
idle
}
fn emulate(&mut self, p1_running: bool, p2_running: bool) -> u32 {
const MAX_CYCLES: u32 = 20_000_000;
let mut cycles = MAX_CYCLES;
if p1_running && p2_running {
Sim::emulate_many(&mut self.sims, &mut cycles);
} else if p1_running {
self.sims[SimId::Player1.to_index()].emulate(&mut cycles);
} else if p2_running {
self.sims[SimId::Player2.to_index()].emulate(&mut cycles);
}
MAX_CYCLES - cycles
}
fn handle_command(&mut self, command: EmulatorCommand) {
match command {
EmulatorCommand::ConnectToSim(sim_id, renderer, messages) => {
@ -670,22 +540,6 @@ impl Emulator {
self.report_error(sim_id, format!("Error loading rom: {error}"));
}
}
EmulatorCommand::ReloadRom(sim_id) => {
if let Err(error) = self.reload_cart(sim_id) {
self.report_error(sim_id, format!("Error loading rom: {error}"));
}
}
EmulatorCommand::WatchRom(sim_id, watch) => {
self.watch_rom[sim_id.to_index()].store(watch, Ordering::Release);
let Some(cart) = self.carts[sim_id.to_index()].as_mut() else {
return;
};
if watch {
cart.restart_watching();
} else {
cart.stop_watching();
}
}
EmulatorCommand::StartSecondSim(path) => {
if let Err(error) = self.start_second_sim(path) {
self.report_error(
@ -713,16 +567,6 @@ impl Emulator {
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}"));
}
}
EmulatorCommand::StartProfiling(sim_id, profiler) => {
if let Err(error) = self.start_profiling(sim_id, profiler) {
self.report_error(SimId::Player1, format!("Error enaling profiler: {error}"));
}
}
EmulatorCommand::StartDebugging(sim_id, debugger) => {
self.start_debugging(sim_id, debugger);
}
@ -792,15 +636,9 @@ impl Emulator {
};
sim.remove_watchpoint(address, length, watch);
}
EmulatorCommand::WatchStdout(sim_id, stdout_sink) => {
self.stdouts.insert(sim_id, stdout_sink);
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.watch_stdout(true);
}
EmulatorCommand::SetAudioEnabled(sim_id, enabled) => {
self.audio_on[sim_id.to_index()].store(enabled, Ordering::Release);
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);
}
EmulatorCommand::Link => {
self.link_sims();
@ -809,17 +647,15 @@ impl Emulator {
self.unlink_sims();
}
EmulatorCommand::Reset(sim_id) => {
self.reset_sim(sim_id, None);
if let Err(error) = self.reset_sim(sim_id, None) {
self.report_error(sim_id, format!("Error resetting sim: {error}"));
}
}
EmulatorCommand::SetKeys(sim_id, keys) => {
if let Some(sim) = self.sims.get_mut(sim_id.to_index()) {
sim.set_keys(keys);
}
}
EmulatorCommand::Screenshot(sim_id, sender) => {
let contents = self.eye_contents[sim_id.to_index()].clone();
let _ = sender.send(contents);
}
EmulatorCommand::Exit(done) => {
for sim_id in SimId::values() {
if let Err(error) = self.save_sram(sim_id) {
@ -837,8 +673,10 @@ impl Emulator {
.get(&sim_id)
.or_else(|| self.messages.get(&SimId::Player1));
if let Some(msg) = messages {
let mut toast = Toast::error(&message);
toast.duration(Some(Duration::from_secs(5)));
let toast = Toast::new()
.kind(ToastKind::Error)
.options(ToastOptions::default().duration_in_seconds(5.0))
.text(&message);
if msg.send(toast).is_ok() {
return;
}
@ -851,15 +689,11 @@ impl Emulator {
pub enum EmulatorCommand {
ConnectToSim(SimId, TextureSink, mpsc::Sender<Toast>),
LoadGame(SimId, PathBuf),
ReloadRom(SimId),
WatchRom(SimId, bool),
StartSecondSim(Option<PathBuf>),
StopSecondSim,
Pause,
Resume,
FrameAdvance,
SetSpeed(f64),
StartProfiling(SimId, ProfileSender),
StartDebugging(SimId, DebugSender),
StopDebugging(SimId),
DebugInterrupt(SimId),
@ -874,13 +708,11 @@ pub enum EmulatorCommand {
RemoveBreakpoint(SimId, u32),
AddWatchpoint(SimId, u32, usize, VBWatchpointType),
RemoveWatchpoint(SimId, u32, usize, VBWatchpointType),
WatchStdout(SimId, mpsc::Sender<String>),
SetAudioEnabled(SimId, bool),
SetAudioEnabled(bool, bool),
Link,
Unlink,
Reset(SimId),
SetKeys(SimId, VBKey),
Screenshot(SimId, oneshot::Sender<Vec<u8>>),
Exit(oneshot::Sender<()>),
}
@ -901,7 +733,6 @@ pub enum EmulatorState {
Debugging,
}
type ProfileSender = tokio::sync::mpsc::UnboundedSender<ProfileEvent>;
type DebugSender = tokio::sync::mpsc::UnboundedSender<DebugEvent>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@ -925,24 +756,13 @@ pub enum DebugEvent {
Stopped(DebugStopReason),
}
pub enum ProfileEvent {
Start {
info: Arc<GameInfo>,
},
Update {
cycles: u32,
event: Option<SimEvent>,
inline_stack: Option<InlineStack>,
},
}
#[derive(Clone)]
pub struct EmulatorClient {
queue: mpsc::Sender<EmulatorCommand>,
sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>,
watch_rom: Arc<[AtomicBool; 2]>,
}
impl EmulatorClient {
@ -952,12 +772,12 @@ impl EmulatorClient {
pub fn emulator_state(&self) -> EmulatorState {
self.state.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 are_sims_linked(&self) -> bool {
self.linked.load(Ordering::Acquire)
}
pub fn is_rom_watched(&self, sim_id: SimId) -> bool {
self.watch_rom[sim_id.to_index()].load(Ordering::Acquire)
}
pub fn send_command(&self, command: EmulatorCommand) -> bool {
match self.queue.send(command) {
Ok(()) => true,

View File

@ -1,170 +0,0 @@
use anyhow::Result;
use notify::Watcher;
use rand::RngExt;
use std::{
fs::{self, File},
io::{Read, Seek as _, SeekFrom, Write as _},
path::{Path, PathBuf},
sync::{Arc, atomic::AtomicBool},
};
use crate::emulator::{SimId, game_info::GameInfo, shrooms_vb_util::rom_from_isx};
pub struct Cart {
pub file_path: PathBuf,
pub rom: Vec<u8>,
sram_file: File,
pub sram: Vec<u8>,
pub info: Arc<GameInfo>,
watcher: Option<notify::RecommendedWatcher>,
changed: Arc<AtomicBool>,
}
impl Cart {
pub fn load(file_path: &Path, sim_id: SimId, watch: bool) -> Result<Self> {
let rom = fs::read(file_path)?;
let (rom, info) = try_parse_isx(file_path, &rom)
.or_else(|| try_parse_elf(file_path, &rom))
.unwrap_or_else(|| (rom, GameInfo::empty(file_path)));
let mut sram_file = File::options()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(sram_path(file_path, sim_id))?;
let sram = if sram_file.metadata()?.len() == 0 {
// new SRAM file, randomize the contents
let mut sram = vec![0; 16 * 1024];
let mut rng = rand::rng();
for dst in sram.iter_mut().step_by(2) {
*dst = rng.random();
}
sram
} else {
let mut sram = Vec::with_capacity(16 * 1024);
sram_file.read_to_end(&mut sram)?;
sram
};
let changed = Arc::new(AtomicBool::new(false));
let watcher = if watch {
build_watcher(file_path, changed.clone())
} else {
None
};
Ok(Cart {
file_path: file_path.to_path_buf(),
rom,
sram_file,
sram,
info: Arc::new(info),
watcher,
changed,
})
}
pub fn save_sram(&mut self) -> Result<()> {
self.sram_file.seek(SeekFrom::Start(0))?;
self.sram_file.write_all(&self.sram)?;
Ok(())
}
pub fn changed(&self) -> bool {
self.changed.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn is_watching(&self) -> bool {
self.watcher.is_some()
}
pub fn restart_watching(&mut self) {
self.changed = Arc::new(AtomicBool::new(false));
if let Some(mut watcher) = self.watcher.take() {
let _ = watcher.unwatch(&self.file_path);
};
self.watcher = build_watcher(&self.file_path, self.changed.clone());
}
pub fn stop_watching(&mut self) {
self.changed = Arc::new(AtomicBool::new(false));
self.watcher = None;
}
}
fn build_watcher(file_path: &Path, changed: Arc<AtomicBool>) -> Option<notify::RecommendedWatcher> {
let file_path = file_path.to_path_buf();
let mut watcher =
notify::recommended_watcher(move |event: Result<notify::Event, notify::Error>| {
let Ok(e) = event else {
return;
};
let modified = !matches!(
e.kind,
notify::EventKind::Access(_)
| notify::EventKind::Modify(notify::event::ModifyKind::Metadata(_))
);
if modified {
changed.store(true, std::sync::atomic::Ordering::Relaxed);
}
})
.ok()?;
watcher
.watch(&file_path, notify::RecursiveMode::NonRecursive)
.ok()?;
Some(watcher)
}
fn try_parse_isx(file_path: &Path, data: &[u8]) -> Option<(Vec<u8>, GameInfo)> {
let rom = rom_from_isx(data)?;
let info = GameInfo::from_isx(file_path, data);
Some((rom, info))
}
fn try_parse_elf(file_path: &Path, data: &[u8]) -> Option<(Vec<u8>, GameInfo)> {
use object::read::elf::FileHeader;
let program = match object::FileKind::parse(data).ok()? {
object::FileKind::Elf32 => {
let header = object::elf::FileHeader32::parse(data).ok()?;
parse_elf_program(header, data)?
}
object::FileKind::Elf64 => {
let header = object::elf::FileHeader64::parse(data).ok()?;
parse_elf_program(header, data)?
}
_ => return None,
};
let info = GameInfo::from_elf(file_path, data).unwrap_or_else(|_| GameInfo::empty(file_path));
Some((program, info))
}
fn parse_elf_program<Elf: object::read::elf::FileHeader<Endian = object::Endianness>>(
header: &Elf,
data: &[u8],
) -> Option<Vec<u8>> {
use object::read::elf::ProgramHeader;
let endian = header.endian().ok()?;
let mut bytes = vec![];
let mut pstart = None;
for phdr in header.program_headers(endian, data).ok()? {
let pma = phdr.p_paddr(endian).into();
if pma < 0x07000000 || phdr.p_filesz(endian).into() == 0 {
continue;
}
let start = pstart.unwrap_or(pma);
pstart = Some(start);
bytes.resize((pma - start) as usize, 0);
let data = phdr.data(endian, data).ok()?;
bytes.extend_from_slice(data);
}
Some(bytes)
}
fn sram_path(file_path: &Path, sim_id: SimId) -> PathBuf {
match sim_id {
SimId::Player1 => file_path.with_extension("p1.sram"),
SimId::Player2 => file_path.with_extension("p2.sram"),
}
}

View File

@ -1,284 +0,0 @@
use std::{borrow::Cow, path::Path, sync::Arc};
use anyhow::{Result, bail};
use fxprof_processed_profile::{LibraryInfo, Symbol, SymbolTable, debugid::DebugId};
use object::{Object, ObjectSection, ObjectSymbol};
use wholesym::samply_symbols::{DebugIdExt, demangle_any};
use crate::emulator::inline_stack_map::{InlineStackMap, InlineStackMapBuilder};
#[derive(Debug)]
pub struct GameInfo {
library_info: LibraryInfo,
inline_stack_map: InlineStackMap,
}
impl GameInfo {
pub fn from_elf(file_path: &Path, input: &[u8]) -> Result<Self> {
let file = object::File::parse(input)?;
let (name, path) = name_and_path(file_path);
let debug_id = file
.build_id()?
.map(|id| DebugId::from_identifier(id, true))
.unwrap_or_default();
let code_id = file.build_id()?.map(hex::encode);
let mut symbols = vec![];
for sym in file.symbols() {
symbols.push(Symbol {
address: sym.address() as u32,
size: Some(sym.size() as u32),
name: demangle_any(sym.name()?),
});
}
let inline_stack_map =
build_inline_stack_map(file).unwrap_or_else(|_| InlineStackMap::empty());
let library_info = LibraryInfo {
name: name.clone(),
debug_name: name,
path: path.clone(),
debug_path: path,
debug_id,
code_id,
arch: None,
symbol_table: Some(Arc::new(SymbolTable::new(symbols))),
};
Ok(Self {
library_info,
inline_stack_map,
})
}
pub fn from_isx(file_path: &Path, input: &[u8]) -> Self {
let (name, path) = name_and_path(file_path);
let symbols = extract_isx_symbols(input);
let library_info = LibraryInfo {
name: name.clone(),
debug_name: name,
path: path.clone(),
debug_path: path,
debug_id: DebugId::default(),
code_id: None,
arch: None,
symbol_table: symbols.map(|syms| Arc::new(SymbolTable::new(syms))),
};
let inline_stack_map = InlineStackMap::empty();
Self {
library_info,
inline_stack_map,
}
}
pub fn empty(file_path: &Path) -> Self {
let (name, path) = name_and_path(file_path);
let library_info = LibraryInfo {
name: name.clone(),
debug_name: name,
path: path.clone(),
debug_path: path,
debug_id: DebugId::default(),
code_id: None,
arch: None,
symbol_table: None,
};
let inline_stack_map = InlineStackMap::empty();
Self {
library_info,
inline_stack_map,
}
}
pub fn name(&self) -> &str {
&self.library_info.name
}
pub fn library_info(&self) -> &LibraryInfo {
&self.library_info
}
pub fn inline_stack_map(&self) -> &InlineStackMap {
&self.inline_stack_map
}
}
fn build_inline_stack_map(file: object::File) -> Result<InlineStackMap> {
let endian = if file.is_little_endian() {
gimli::RunTimeEndian::Little
} else {
gimli::RunTimeEndian::Big
};
fn load_section<'a>(file: &'a object::File, id: gimli::SectionId) -> Result<Cow<'a, [u8]>> {
let input = match file.section_by_name(id.name()) {
Some(section) => section.uncompressed_data()?,
None => Cow::Owned(vec![]),
};
Ok(input)
}
let dorf = gimli::DwarfSections::load(|id| load_section(&file, id))?;
let dorf = dorf.borrow(|sec| gimli::EndianSlice::new(sec, endian));
let mut units = dorf.units();
let mut frames = InlineStackMap::builder();
while let Some(header) = units.next()? {
let unit = dorf.unit(header)?;
let mut entree = unit.entries_tree(None)?;
let root = entree.root()?;
let mut ctx = ParseContext {
dorf: &dorf,
unit: &unit,
frames: &mut frames,
};
parse_inline(&mut ctx, root)?;
}
Ok(frames.build())
}
fn extract_isx_symbols(input: &[u8]) -> Option<Vec<Symbol>> {
let mut syms = vec![];
let (_, mut buf) = input.split_at_checked(32)?;
while !buf.is_empty() {
let typ;
(typ, buf) = buf.split_first()?;
match typ {
0x11 => {
// Code (Virtual Boy)
(_, buf) = buf.split_at_checked(4)?;
let len_bytes;
(len_bytes, buf) = buf.split_first_chunk()?;
let len = u32::from_le_bytes(*len_bytes);
(_, buf) = buf.split_at_checked(len as usize)?;
}
0x13 => {
// Range (Virtual Boy)
let count_bytes;
(count_bytes, buf) = buf.split_first_chunk()?;
let count = u16::from_le_bytes(*count_bytes);
(_, buf) = buf.split_at_checked(count as usize * 9)?;
}
0x14 => {
// Symbol (Virtual Boy)
let count_bytes;
(count_bytes, buf) = buf.split_first_chunk()?;
let count = u16::from_le_bytes(*count_bytes);
for _ in 0..count {
let name_len;
(name_len, buf) = buf.split_first()?;
let name_bytes;
(name_bytes, buf) = buf.split_at_checked(*name_len as usize)?;
(_, buf) = buf.split_at_checked(2)?;
let address_bytes;
(address_bytes, buf) = buf.split_first_chunk()?;
let name_str = String::from_utf8_lossy(name_bytes);
let address = u32::from_le_bytes(*address_bytes);
syms.push(Symbol {
address: address & 0x07ffffff,
size: Some(4),
name: demangle_any(&name_str),
});
}
}
0x20..=0x22 => {
// System (undocumented)
let length_bytes;
(length_bytes, buf) = buf.split_first_chunk()?;
let length = u32::from_le_bytes(*length_bytes);
(_, buf) = buf.split_at_checked(length as usize)?;
}
_ => {
return None;
}
}
}
Some(syms)
}
type Reader<'a> = gimli::EndianSlice<'a, gimli::RunTimeEndian>;
struct ParseContext<'a> {
dorf: &'a gimli::Dwarf<Reader<'a>>,
unit: &'a gimli::Unit<Reader<'a>>,
frames: &'a mut InlineStackMapBuilder,
}
impl ParseContext<'_> {
fn name_attr(&self, attr: gimli::AttributeValue<Reader>) -> Result<Option<String>> {
match attr {
gimli::AttributeValue::DebugInfoRef(offset) => {
let mut units = self.dorf.units();
while let Some(header) = units.next()? {
if let Some(offset) = offset.to_unit_offset(&header) {
let unit = self.dorf.unit(header)?;
return self.name_entry(&unit, offset);
}
}
Ok(None)
}
gimli::AttributeValue::UnitRef(offset) => self.name_entry(self.unit, offset),
other => {
bail!("unrecognized attr {other:?}");
}
}
}
fn name_entry(
&self,
unit: &gimli::Unit<Reader>,
offset: gimli::UnitOffset,
) -> Result<Option<String>> {
let abbreviations = self.dorf.abbreviations(&unit.header)?;
let mut entries = unit.header.entries_raw(&abbreviations, Some(offset))?;
let Some(abbrev) = entries.read_abbreviation()? else {
return Ok(None);
};
let mut name = None;
for spec in abbrev.attributes() {
let attr = entries.read_attribute(*spec)?;
if attr.name() == gimli::DW_AT_linkage_name
|| (attr.name() == gimli::DW_AT_name && name.is_none())
{
name = Some(self.dorf.attr_string(unit, attr.value())?)
}
}
Ok(name.map(|n| demangle_any(&String::from_utf8_lossy(&n))))
}
}
fn parse_inline<'a>(
ctx: &mut ParseContext<'a>,
node: gimli::EntriesTreeNode<'a, '_, Reader<'a>>,
) -> Result<()> {
if node.entry().tag() == gimli::DW_TAG_inlined_subroutine
&& let Some(attr) = node.entry().attr_value(gimli::DW_AT_abstract_origin)
&& let Some(name) = ctx.name_attr(attr)?
{
let name = Arc::new(name);
let mut ranges = ctx.dorf.die_ranges(ctx.unit, node.entry())?;
while let Some(range) = ranges.next()? {
let start = range.begin as u32 & 0x07ffffff;
let end = range.end as u32 & 0x07ffffff;
ctx.frames.add(start, end, name.clone());
}
}
let mut children = node.children();
while let Some(child) = children.next()? {
parse_inline(ctx, child)?;
}
Ok(())
}
fn name_and_path(file_path: &Path) -> (String, String) {
let normalized = normpath::PathExt::normalize(file_path);
let path = normalized
.as_ref()
.map(|n| n.as_path())
.unwrap_or(file_path);
let name = match path.file_stem() {
Some(s) => s.to_string_lossy().into_owned(),
None => "game".to_string(),
};
let path = path.to_string_lossy().into_owned();
(name, path)
}

View File

@ -1,87 +0,0 @@
use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
};
pub type InlineStack = Arc<Vec<Arc<String>>>;
#[derive(Debug, Clone)]
pub struct InlineStackMap {
entries: Vec<(u32, InlineStack)>,
empty: InlineStack,
}
impl InlineStackMap {
pub fn empty() -> Self {
Self {
entries: vec![],
empty: Arc::new(vec![]),
}
}
pub fn builder() -> InlineStackMapBuilder {
InlineStackMapBuilder {
events: BTreeMap::new(),
}
}
pub fn get(&self, address: u32) -> &InlineStack {
match self.entries.binary_search_by_key(&address, |(a, _)| *a) {
Ok(index) => self.entries.get(index),
Err(after) => after.checked_sub(1).and_then(|i| self.entries.get(i)),
}
.map(|(_, s)| s)
.unwrap_or(&self.empty)
}
}
#[derive(Default)]
struct Event {
end: usize,
start: Vec<Arc<String>>,
}
pub struct InlineStackMapBuilder {
events: BTreeMap<u32, Event>,
}
impl InlineStackMapBuilder {
pub fn add(&mut self, start: u32, end: u32, name: Arc<String>) {
self.events.entry(start).or_default().start.push(name);
self.events.entry(end).or_default().end += 1;
}
pub fn build(self) -> InlineStackMap {
let empty = Arc::new(vec![]);
let mut entries = vec![];
let mut stack_indexes = vec![];
let mut stack = vec![];
let mut string_dedup = HashMap::new();
let mut stack_dedup = BTreeMap::new();
stack_dedup.insert(vec![], empty.clone());
for (address, event) in self.events {
for _ in 0..event.end {
stack.pop();
stack_indexes.pop();
}
for call in event.start {
if let Some(index) = string_dedup.get(&call) {
stack.push(call);
stack_indexes.push(*index);
} else {
let index = string_dedup.len();
string_dedup.insert(call.clone(), index);
stack.push(call);
stack_indexes.push(index);
}
}
if let Some(stack) = stack_dedup.get(&stack_indexes) {
entries.push((address, stack.clone()));
} else {
let stack = Arc::new(stack.clone());
stack_dedup.insert(stack_indexes.clone(), stack.clone());
entries.push((address, stack));
}
}
InlineStackMap { entries, empty }
}
}

View File

@ -1,12 +1,10 @@
use std::{borrow::Cow, ffi::c_void, ptr, slice, sync::Arc};
use std::{ffi::c_void, ptr, slice};
use anyhow::{Result, anyhow};
use bitflags::bitflags;
use num_derive::{FromPrimitive, ToPrimitive};
use serde::{Deserialize, Serialize};
use crate::emulator::inline_stack_map::{InlineStack, InlineStackMap};
use super::address_set::AddressSet;
#[repr(C)]
@ -73,16 +71,8 @@ pub enum VBWatchpointType {
Access,
}
type OnException = extern "C" fn(sim: *mut VB, cause: *mut u16) -> c_int;
type OnExecute =
extern "C" fn(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> c_int;
type OnFetch = extern "C" fn(
sim: *mut VB,
fetch: c_int,
address: u32,
value: *mut i32,
cycles: *mut u32,
) -> c_int;
type OnFrame = extern "C" fn(sim: *mut VB) -> c_int;
type OnRead = extern "C" fn(
sim: *mut VB,
@ -145,15 +135,8 @@ unsafe extern "C" {
fn vb_set_cart_ram(sim: *mut VB, sram: *mut c_void, size: u32) -> c_int;
#[link_name = "vbSetCartROM"]
fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int;
#[link_name = "vbSetExceptionCallback"]
fn vb_set_exception_callback(
sim: *mut VB,
callback: Option<OnException>,
) -> Option<OnException>;
#[link_name = "vbSetExecuteCallback"]
fn vb_set_execute_callback(sim: *mut VB, callback: Option<OnExecute>) -> Option<OnExecute>;
#[link_name = "vbSetFetchCallback"]
fn vb_set_fetch_callback(sim: *mut VB, callback: Option<OnFetch>) -> Option<OnFetch>;
#[link_name = "vbSetFrameCallback"]
fn vb_set_frame_callback(sim: *mut VB, callback: Option<OnFrame>) -> Option<OnFrame>;
#[link_name = "vbSetKeys"]
@ -193,27 +176,16 @@ extern "C" fn on_frame(sim: *mut VB) -> c_int {
// There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
data.frame_seen = true;
if data.monitor.enabled {
data.monitor.event = Some(SimEvent::Marker(Cow::Borrowed("Frame Drawn")));
}
1
}
#[unsafe(no_mangle)]
extern "C" fn on_execute(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> c_int {
extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: c_int) -> c_int {
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
if data.monitor.enabled {
// SAFETY: length is the length of code, in elements
let code = unsafe { slice::from_raw_parts(code, length as usize) };
data.monitor.detect_event(sim, address, code);
// Something interesting will happen after this instruction is run.
// We'll react in the on_fetch callback it does.
}
let mut stopped = data.stop_reason.is_some() || data.monitor.event.is_some();
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);
@ -227,49 +199,6 @@ extern "C" fn on_execute(sim: *mut VB, address: u32, code: *const u16, length: c
if stopped { 1 } else { 0 }
}
#[unsafe(no_mangle)]
extern "C" fn on_fetch(
sim: *mut VB,
_fetch: c_int,
address: u32,
_value: *mut i32,
_cycles: *mut u32,
) -> c_int {
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
data.monitor.event = data.monitor.queued_event.take();
data.monitor.new_inline_stack = data.monitor.detect_new_inline_stack(address);
unsafe { vb_set_exception_callback(sim, Some(on_exception)) };
if data.monitor.event.is_some() || data.monitor.new_inline_stack.is_some() {
1
} else {
0
}
}
#[unsafe(no_mangle)]
extern "C" fn on_exception(sim: *mut VB, cause: *mut u16) -> c_int {
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
let cause = unsafe { *cause };
let pc = if cause == 0xff70 {
0xffffff60
} else {
(cause & 0xfff0) as u32 | 0xffff0000
};
data.monitor.event = data.monitor.queued_event.take();
data.monitor.new_inline_stack = data.monitor.detect_new_inline_stack(pc);
data.monitor.queued_event = Some(SimEvent::Interrupt(cause, pc & 0x07ffffff));
unsafe { vb_set_exception_callback(sim, None) };
if data.monitor.event.is_some() || data.monitor.new_inline_stack.is_some() {
1
} else {
0
}
}
#[unsafe(no_mangle)]
extern "C" fn on_read(
sim: *mut VB,
@ -300,8 +229,8 @@ extern "C" fn on_read(
extern "C" fn on_write(
sim: *mut VB,
address: u32,
typ_: VBDataType,
value: *mut i32,
_type: VBDataType,
_value: *mut i32,
_cycles: *mut u32,
_cancel: *mut c_int,
) -> c_int {
@ -309,37 +238,6 @@ extern "C" fn on_write(
// There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
// If we're monitoring stdout, track this write
if let Some(stdout) = data.stdout.as_mut() {
let normalized_hw_address = address & 0x0700003f;
if normalized_hw_address == 0x02000030 {
stdout.push(unsafe { *value } as u8);
}
}
// If we have profiling enabled, track custom markers
if data.monitor.enabled {
let normalized_hw_address = address & 0x0700003f;
if normalized_hw_address == 0x02000038 && matches!(typ_, VBDataType::S32) {
assert!(data.monitor.queued_event.is_none());
// The game has written the address of a null-terminated string
// (whose length is at most 64 bytes). Read that string.
let str_address = unsafe { *value } as u32;
let mut bytes = [0u8; 64];
let mut len = 0;
for (dst, src_address) in bytes.iter_mut().zip(str_address..str_address + 64) {
let char = unsafe { vb_read(sim, src_address, VBDataType::U8) } as u8;
if char == 0 {
break;
}
*dst = char;
len += 1;
}
let name = String::from_utf8_lossy(&bytes[..len]).into_owned();
data.monitor.queued_event = Some(SimEvent::Marker(Cow::Owned(name)));
}
}
if let Some(start) = data.write_watchpoints.start_of_range_containing(address) {
let watch = if data.read_watchpoints.contains(address) {
VBWatchpointType::Access
@ -354,119 +252,6 @@ extern "C" fn on_write(
0
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum SimEvent {
Call(u32),
Return,
Halt,
Interrupt(u16, u32),
Reti,
Marker(Cow<'static, str>),
}
struct EventMonitor {
enabled: bool,
event: Option<SimEvent>,
queued_event: Option<SimEvent>,
just_halted: bool,
inline_stack_map: InlineStackMap,
new_inline_stack: Option<InlineStack>,
last_inline_stack: InlineStack,
}
impl EventMonitor {
fn new() -> Self {
let inline_stack_map = InlineStackMap::empty();
let last_inline_stack = inline_stack_map.get(0).clone();
Self {
enabled: false,
event: None,
queued_event: None,
just_halted: false,
inline_stack_map,
new_inline_stack: None,
last_inline_stack,
}
}
fn detect_new_inline_stack(&mut self, address: u32) -> Option<InlineStack> {
let stack = self.inline_stack_map.get(address);
if Arc::ptr_eq(stack, &self.last_inline_stack) {
return None;
}
self.last_inline_stack = stack.clone();
Some(stack.clone())
}
fn detect_event(&mut self, sim: *mut VB, address: u32, code: &[u16]) -> bool {
self.queued_event = self.do_detect_event(sim, address, code);
self.queued_event.is_some()
}
fn do_detect_event(&mut self, sim: *mut VB, address: u32, code: &[u16]) -> Option<SimEvent> {
const HALT_OPCODE: u16 = 0b011010;
const JAL_OPCODE: u16 = 0b101011;
const JMP_OPCODE: u16 = 0b000110;
const RETI_OPCODE: u16 = 0b011001;
const fn format_i_reg_1(code: &[u16]) -> u8 {
(code[0] & 0x1f) as u8
}
const fn format_iv_disp(code: &[u16]) -> i32 {
let value = ((code[0] & 0x3ff) as i32) << 16 | (code[1] as i32);
value << 6 >> 6
}
let opcode = code[0] >> 10;
if opcode == HALT_OPCODE {
if !self.just_halted {
self.just_halted = true;
self.event = Some(SimEvent::Halt);
} else {
self.just_halted = false;
}
// Don't _return_ an event, we want to emit this right away.
// If the CPU is halting, no other callbacks will run for a long time.
return None;
}
if opcode == JAL_OPCODE {
let disp = format_iv_disp(code);
if disp != 4 {
// JAL .+4 is how programs get r31 to a known value for indirect calls
// (which we detect later.)
// Any other JAL is a function call.
return Some(SimEvent::Call(
address.wrapping_add_signed(disp) & 0x07ffffff,
));
}
}
if opcode == JMP_OPCODE {
let jmp_reg = format_i_reg_1(code);
if jmp_reg == 31 {
// JMP[r31] is a return
return Some(SimEvent::Return);
}
let r31 = unsafe { vb_get_program_register(sim, 31) };
if r31 as u32 == address.wrapping_add(2) {
// JMP anywhere else, if r31 points to after the JMP, is an indirect call
let target = unsafe { vb_get_program_register(sim, jmp_reg as u32) };
return Some(SimEvent::Call(target as u32 & 0x07ffffff));
}
}
if opcode == RETI_OPCODE {
return Some(SimEvent::Reti);
}
None
}
}
const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4;
const AUDIO_CAPACITY_FLOATS: usize = AUDIO_CAPACITY_SAMPLES * 2;
pub const EXPECTED_FRAME_SIZE: usize = 834 * 2;
@ -474,26 +259,19 @@ pub const EXPECTED_FRAME_SIZE: usize = 834 * 2;
struct VBState {
frame_seen: bool,
stop_reason: Option<StopReason>,
monitor: EventMonitor,
step_from: Option<u32>,
breakpoints: Vec<u32>,
read_watchpoints: AddressSet,
write_watchpoints: AddressSet,
stdout: Option<Vec<u8>>,
}
impl VBState {
fn needs_execute_callback(&self) -> bool {
self.step_from.is_some()
|| self.monitor.enabled
|| !self.breakpoints.is_empty()
|| !self.read_watchpoints.is_empty()
|| !self.write_watchpoints.is_empty()
}
fn needs_write_callback(&self) -> bool {
self.stdout.is_some() || self.monitor.enabled || !self.write_watchpoints.is_empty()
}
}
pub enum StopReason {
@ -524,12 +302,10 @@ impl Sim {
let state = VBState {
frame_seen: false,
stop_reason: None,
monitor: EventMonitor::new(),
step_from: None,
breakpoints: vec![],
read_watchpoints: AddressSet::new(),
write_watchpoints: AddressSet::new(),
stdout: None,
};
unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) };
unsafe { vb_set_frame_callback(sim, Some(on_frame)) };
@ -546,33 +322,6 @@ impl Sim {
unsafe { vb_reset(self.sim) };
}
pub fn monitor_events(&mut self, enabled: bool, inline_stack_map: InlineStackMap) {
let state = self.get_state();
state.monitor.enabled = enabled;
state.monitor.event = None;
state.monitor.queued_event = None;
state.monitor.new_inline_stack = None;
state.monitor.last_inline_stack = inline_stack_map.get(0).clone();
state.monitor.inline_stack_map = inline_stack_map;
if enabled {
unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) };
unsafe { vb_set_exception_callback(self.sim, Some(on_exception)) };
unsafe { vb_set_fetch_callback(self.sim, Some(on_fetch)) };
unsafe { vb_set_write_callback(self.sim, Some(on_write)) };
} else {
let needs_execute = state.needs_execute_callback();
let needs_write = state.needs_write_callback();
if !needs_execute {
unsafe { vb_set_execute_callback(self.sim, None) };
}
unsafe { vb_set_exception_callback(self.sim, None) };
unsafe { vb_set_fetch_callback(self.sim, None) };
if !needs_write {
unsafe { vb_set_write_callback(self.sim, None) };
}
}
}
pub fn load_cart(&mut self, mut rom: Vec<u8>, mut sram: Vec<u8>) -> Result<()> {
self.unload_cart();
@ -635,14 +384,16 @@ impl Sim {
unsafe { vb_set_peer(self.sim, ptr::null_mut()) };
}
pub fn emulate(&mut self, cycles: &mut u32) {
unsafe { vb_emulate(self.sim, cycles) };
pub fn emulate(&mut self) {
let mut cycles = 20_000_000;
unsafe { vb_emulate(self.sim, &mut cycles) };
}
pub fn emulate_many(sims: &mut [Sim], cycles: &mut u32) {
pub fn emulate_many(sims: &mut [Sim]) {
let mut cycles = 20_000_000;
let count = sims.len() as c_uint;
let sims = sims.as_mut_ptr().cast();
unsafe { vb_emulate_ex(sims, count, cycles) };
unsafe { vb_emulate_ex(sims, count, &mut cycles) };
}
pub fn read_pixels(&mut self, buffers: &mut [u8]) -> bool {
@ -811,12 +562,9 @@ impl Sim {
fn remove_write_watchpoint(&mut self, address: u32, length: usize) {
let state = self.get_state();
state.write_watchpoints.remove(address, length);
let needs_write = state.needs_write_callback();
let needs_execute = state.needs_execute_callback();
if state.write_watchpoints.is_empty() {
if !needs_write {
unsafe { vb_set_write_callback(self.sim, None) };
}
unsafe { vb_set_write_callback(self.sim, None) };
if !needs_execute {
unsafe { vb_set_execute_callback(self.sim, None) };
}
@ -838,44 +586,12 @@ impl Sim {
data.breakpoints.clear();
data.read_watchpoints.clear();
data.write_watchpoints.clear();
let needs_write = data.needs_write_callback();
let needs_execute = data.needs_execute_callback();
unsafe { vb_set_read_callback(self.sim, None) };
if !needs_write {
unsafe { vb_set_write_callback(self.sim, None) };
}
if !needs_execute {
unsafe { vb_set_execute_callback(self.sim, None) };
}
unsafe { vb_set_write_callback(self.sim, None) };
unsafe { vb_set_execute_callback(self.sim, None) };
}
pub fn watch_stdout(&mut self, watch: bool) {
let data = self.get_state();
if watch {
if data.stdout.is_none() {
data.stdout = Some(vec![]);
unsafe { vb_set_write_callback(self.sim, Some(on_write)) };
}
} else {
data.stdout.take();
if !data.needs_write_callback() {
unsafe { vb_set_write_callback(self.sim, None) };
}
}
}
pub fn take_stdout(&mut self) -> Option<String> {
let data = self.get_state();
let stdout = data.stdout.take()?;
let string = match String::from_utf8(stdout) {
Ok(str) => str,
Err(err) => String::from_utf8_lossy(err.as_bytes()).into_owned(),
};
data.stdout = Some(vec![]);
Some(string)
}
pub fn take_stop_reason(&mut self) -> Option<StopReason> {
pub fn stop_reason(&mut self) -> Option<StopReason> {
let data = self.get_state();
let reason = data.stop_reason.take();
if !data.needs_execute_callback() {
@ -884,13 +600,6 @@ impl Sim {
reason
}
pub fn take_profiler_updates(&mut self) -> (Option<SimEvent>, Option<InlineStack>) {
let data = self.get_state();
let event = data.monitor.event.take();
let inline_stack = data.monitor.new_inline_stack.take();
(event, inline_stack)
}
fn get_state(&mut self) -> &mut VBState {
// SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid.

View File

@ -1,31 +0,0 @@
use std::ffi::c_void;
#[unsafe(no_mangle)]
unsafe fn vbu_realloc_shim(ptr: *mut c_void, new_size: usize) -> *mut c_void {
if !ptr.is_null() {
// not supporting proper realloc because it needs bookkeeping and it's unnecessary
return std::ptr::null_mut();
}
let allocation = vec![0u8; new_size].into_boxed_slice();
Box::into_raw(allocation).cast()
}
#[link(name = "vb")]
unsafe extern "C" {
#[link_name = "vbuFromISX"]
fn vbu_from_isx(bytes: *const c_void, length: usize, rom_length: *mut usize) -> *mut c_void;
}
pub fn rom_from_isx(bytes: &[u8]) -> Option<Vec<u8>> {
if !bytes.starts_with(b"ISX") {
return None;
}
let mut rom_length = 0;
let raw_rom = unsafe { vbu_from_isx(bytes.as_ptr().cast(), bytes.len(), &mut rom_length) };
if raw_rom.is_null() {
return None;
}
// SAFETY: the rom was allocated by vbu_realloc_shim, which created it from a Vec<u8>.
let rom = unsafe { Vec::from_raw_parts(raw_rom.cast(), rom_length, rom_length) };
Some(rom)
}

View File

@ -12,7 +12,7 @@ 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!(";alt-name:{}", alt));
}
string.push_str(&format!(
";bitsize:32;offset:{};encoding:uint;format:hex;set:{};dwarf:{}",
@ -21,7 +21,7 @@ impl RegisterInfo {
self.dwarf
));
if let Some(generic) = self.generic {
string.push_str(&format!(";generic:{generic}"));
string.push_str(&format!(";generic:{}", generic));
}
string
}

View File

@ -10,7 +10,7 @@ use std::{
use anyhow::{Result, bail};
use itertools::Itertools as _;
use wgpu::{
Device, Extent3d, Origin3d, Queue, TexelCopyBufferLayout, TexelCopyTextureInfo, Texture,
Device, Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, Queue, Texture,
TextureDescriptor, TextureFormat, TextureUsages, TextureView, TextureViewDescriptor,
};
@ -21,7 +21,7 @@ pub struct TextureSink {
}
impl TextureSink {
pub fn new(device: &Device, queue: Queue) -> (Self, TextureView) {
pub fn new(device: &Device, queue: Arc<Queue>) -> (Self, TextureView) {
let texture = Self::create_texture(device);
let view = texture.create_view(&TextureViewDescriptor::default());
let buffers = Arc::new(BufferPool::new());
@ -72,7 +72,7 @@ impl TextureSink {
}
fn write_texture(queue: &Queue, texture: &Texture, bytes: &[u8]) {
let texture = TexelCopyTextureInfo {
let texture = ImageCopyTexture {
texture,
mip_level: 0,
origin: Origin3d::ZERO,
@ -83,7 +83,7 @@ impl TextureSink {
height: 224,
depth_or_array_layers: 1,
};
let data_layout = TexelCopyBufferLayout {
let data_layout = ImageDataLayout {
offset: 0,
bytes_per_row: Some(384 * 2),
rows_per_image: Some(224),

View File

@ -1,5 +1,5 @@
use std::{
collections::{HashMap, hash_map::Entry},
collections::HashMap,
ops::Deref,
sync::{Arc, Mutex, Weak},
thread,
@ -7,21 +7,17 @@ use std::{
};
use egui::{
Color32, ColorImage, TextureHandle,
Color32, ColorImage, TextureHandle, TextureOptions,
epaint::ImageDelta,
generate_loader_id,
load::{LoadError, SizedTexture, TextureLoader, TexturePoll},
};
use tokio::{sync::mpsc, time::timeout};
use crate::emulator::SimId;
pub struct ImageTextureLoader {
cache: Mutex<HashMap<String, ImageEntry>>,
pub struct ImageProcessor {
sender: mpsc::UnboundedSender<Box<dyn ImageRendererImpl>>,
}
impl ImageTextureLoader {
impl ImageProcessor {
pub fn new() -> Self {
let (sender, receiver) = mpsc::unbounded_channel();
thread::spawn(move || {
@ -30,52 +26,29 @@ impl ImageTextureLoader {
.build()
.unwrap()
.block_on(async move {
let mut worker = ImageTextureLoaderWorker {
let mut worker = ImageProcessorWorker {
receiver,
renderers: vec![],
};
worker.run().await
})
});
Self {
cache: Mutex::new(HashMap::new()),
sender,
}
Self { sender }
}
pub fn add<const N: usize, R: ImageRenderer<N> + 'static>(
&self,
sim_id: SimId,
renderer: R,
params: R::Params,
) -> ImageParams<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 mut cache = self.cache.lock().unwrap();
for (name, state) in renderer.names().into_iter().zip(&states) {
let url = format!("vip://{sim_id}/{name}");
let entry = ImageEntry {
size: state.size.map(|i| i as f32),
data: Arc::downgrade(&state.sink),
texture: None,
};
match cache.entry(url) {
Entry::Vacant(e) => {
e.insert(entry);
}
Entry::Occupied(mut e) => {
// Only overwrite an old entry if that entry is dead.
// Otherwise, we clear out the reference to a texture actually in use.
if e.get().data.strong_count() == 0 {
e.insert(entry);
}
}
}
}
}
let sink = Arc::new(Mutex::new(params.clone()));
let _ = self.sender.send(Box::new(ImageRendererWrapper {
renderer,
@ -83,87 +56,20 @@ impl ImageTextureLoader {
images,
states,
}));
ImageParams {
let params = ImageParams {
value: params,
sink,
}
};
(handles, params)
}
}
impl TextureLoader for ImageTextureLoader {
fn id(&self) -> &str {
generate_loader_id!(ImageTextureLoader2)
}
fn load(
&self,
ctx: &egui::Context,
uri: &str,
texture_options: egui::TextureOptions,
_size_hint: egui::SizeHint,
) -> Result<TexturePoll, LoadError> {
let mut cache = self.cache.lock().unwrap();
let Some(entry) = cache.get_mut(uri) else {
return Err(LoadError::NotSupported);
};
if texture_options != egui::TextureOptions::NEAREST {
return Err(LoadError::Loading(
"Only TextureOptions::NEAREST are supported".into(),
));
}
let Some(data) = entry.data.upgrade() else {
cache.remove(uri);
return Err(LoadError::Loading("Backing loader no longer exists".into()));
};
match (data.lock().unwrap().take(), entry.texture.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, entry.size);
Ok(TexturePoll::Ready { texture })
}
(Some(update), None) => {
let handle = ctx.load_texture(uri, update, texture_options);
let texture = SizedTexture::new(&handle, entry.size);
entry.texture.replace(handle);
Ok(TexturePoll::Ready { texture })
}
(None, Some(handle)) => {
let texture = SizedTexture::new(handle, entry.size);
Ok(TexturePoll::Ready { texture })
}
(None, None) => {
let size = entry.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(|entry| {
let [width, height] = entry.size;
width as usize * height as usize * 4
})
.sum()
}
}
struct ImageTextureLoaderWorker {
struct ImageProcessorWorker {
receiver: mpsc::UnboundedReceiver<Box<dyn ImageRendererImpl>>,
renderers: Vec<Box<dyn ImageRendererImpl>>,
}
impl ImageTextureLoaderWorker {
impl ImageProcessorWorker {
async fn run(&mut self) {
loop {
if self.renderers.is_empty() {
@ -236,10 +142,16 @@ impl ImageBuffer {
}
}
struct ImageEntry {
#[derive(Clone)]
pub struct ImageHandle {
size: [f32; 2],
data: Weak<Mutex<Option<Arc<ColorImage>>>>,
texture: Option<TextureHandle>,
data: Arc<Mutex<Option<Arc<ColorImage>>>>,
}
impl ImageHandle {
fn pull(&mut self) -> Option<Arc<ColorImage>> {
self.data.lock().unwrap().take()
}
}
pub struct ImageParams<T> {
@ -266,7 +178,6 @@ impl<T: Clone + Eq> ImageParams<T> {
pub trait ImageRenderer<const N: usize>: Send {
type Params: Clone + Send;
fn names(&self) -> [&str; N];
fn sizes(&self) -> [[usize; 2]; N];
fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; N]);
}
@ -282,8 +193,8 @@ struct ImageState {
impl ImageState {
fn new(size: [usize; 2]) -> Self {
let buffers = [
Arc::new(ColorImage::filled(size, Color32::BLACK)),
Arc::new(ColorImage::filled(size, Color32::BLACK)),
Arc::new(ColorImage::new(size, Color32::BLACK)),
Arc::new(ColorImage::new(size, Color32::BLACK)),
];
let sink = buffers[0].clone();
Self {
@ -335,3 +246,83 @@ impl<const N: usize, R: ImageRenderer<N> + Send> ImageRendererImpl for ImageRend
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,13 +1,13 @@
use std::{
cmp::Ordering,
collections::{HashMap, hash_map::Entry},
collections::{HashMap, HashSet, hash_map::Entry},
fmt::Display,
str::FromStr,
sync::{Arc, Mutex, RwLock},
};
use anyhow::anyhow;
use egui::{Event, Key, KeyboardShortcut, Modifiers};
use egui::{Key, KeyboardShortcut, Modifiers};
use gilrs::{Axis, Button, Gamepad, GamepadId, ev::Code};
use serde::{Deserialize, Serialize};
use winit::keyboard::{KeyCode, PhysicalKey};
@ -227,7 +227,7 @@ impl Mappings for InputMapping {
for (keyboard_key, keys) in &self.keys {
let name = match keyboard_key {
PhysicalKey::Code(code) => format!("{code:?}"),
k => format!("{k:?}"),
k => format!("{:?}", k),
};
for key in keys.iter() {
results.entry(key).or_default().push(name.clone());
@ -272,14 +272,13 @@ impl Mappings for InputMapping {
#[derive(Clone)]
pub struct MappingProvider {
persistence: Persistence,
first_gamepad_is_p2: bool,
device_mappings: Arc<RwLock<HashMap<DeviceId, Arc<RwLock<GamepadMapping>>>>>,
sim_mappings: HashMap<SimId, Arc<RwLock<InputMapping>>>,
gamepad_info: Arc<RwLock<HashMap<GamepadId, GamepadInfo>>>,
}
impl MappingProvider {
pub fn new(persistence: Persistence, first_gamepad_is_p2: bool) -> Self {
pub fn new(persistence: Persistence) -> Self {
let mut sim_mappings = HashMap::new();
let mut device_mappings = HashMap::new();
@ -308,7 +307,6 @@ impl MappingProvider {
device_mappings: Arc::new(RwLock::new(device_mappings)),
gamepad_info: Arc::new(RwLock::new(HashMap::new())),
sim_mappings,
first_gamepad_is_p2,
}
}
@ -340,12 +338,7 @@ impl MappingProvider {
.clone();
drop(lock);
let mut lock = self.gamepad_info.write().unwrap();
let players = if self.first_gamepad_is_p2 {
vec![SimId::Player2, SimId::Player1]
} else {
vec![SimId::Player1, SimId::Player2]
};
let bound_to = players
let bound_to = SimId::values()
.into_iter()
.find(|sim_id| lock.values().all(|info| info.bound_to != Some(*sim_id)));
if let Entry::Vacant(entry) = lock.entry(gamepad.id()) {
@ -464,43 +457,40 @@ struct PersistedGamepadMapping {
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,
ReloadRom,
Quit,
FrameAdvance,
FastForward(u32),
Reset,
PauseResume,
Screenshot,
// if you update this, update Command::all and add a default
}
impl Command {
pub fn all() -> [Self; 8] {
pub fn all() -> [Self; 5] {
[
Self::OpenRom,
Self::ReloadRom,
Self::Quit,
Self::PauseResume,
Self::Reset,
Self::FrameAdvance,
Self::FastForward(0),
Self::Screenshot,
]
}
pub fn name(self) -> &'static str {
match self {
Self::OpenRom => "Open ROM",
Self::ReloadRom => "Reload ROM",
Self::Quit => "Exit",
Self::PauseResume => "Pause/Resume",
Self::Reset => "Reset",
Self::FrameAdvance => "Frame Advance",
Self::FastForward(_) => "Fast Forward",
Self::Screenshot => "Screenshot",
}
}
}
@ -526,10 +516,6 @@ impl Default for Shortcuts {
Command::OpenRom,
KeyboardShortcut::new(Modifiers::COMMAND, Key::O),
);
shortcuts.set(
Command::ReloadRom,
KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, Key::F5),
);
shortcuts.set(
Command::Quit,
KeyboardShortcut::new(Modifiers::COMMAND, Key::Q),
@ -546,14 +532,6 @@ impl Default for Shortcuts {
Command::FrameAdvance,
KeyboardShortcut::new(Modifiers::NONE, Key::F6),
);
shortcuts.set(
Command::FastForward(0),
KeyboardShortcut::new(Modifiers::NONE, Key::Space),
);
shortcuts.set(
Command::Screenshot,
KeyboardShortcut::new(Modifiers::NONE, Key::F12),
);
shortcuts
}
}
@ -579,11 +557,13 @@ impl Shortcuts {
}
}
fn save(&self, saved: &mut PersistedSettings) {
fn save(&self) -> PersistedShortcuts {
let mut shortcuts = PersistedShortcuts { shortcuts: vec![] };
for command in Command::all() {
let shortcut = self.by_command.get(&command).copied();
saved.shortcuts.push((command, shortcut));
shortcuts.shortcuts.push((command, shortcut));
}
shortcuts
}
}
@ -609,123 +589,52 @@ fn specificity(modifiers: egui::Modifiers) -> usize {
mods
}
#[derive(Serialize, Deserialize, Default)]
struct PersistedSettings {
#[derive(Serialize, Deserialize)]
struct PersistedShortcuts {
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>>,
shortcuts: Arc<Mutex<Shortcuts>>,
}
impl ShortcutProvider {
pub fn new(persistence: Persistence) -> Self {
let mut settings = Settings::default();
if let Ok(saved) = persistence.load_config::<PersistedSettings>("shortcuts") {
let mut shortcuts = Shortcuts::default();
if let Ok(saved) = persistence.load_config::<PersistedShortcuts>("shortcuts") {
for (command, shortcut) in saved.shortcuts {
if let Some(shortcut) = shortcut {
settings.shortcuts.set(command, shortcut);
shortcuts.set(command, shortcut);
} else {
settings.shortcuts.unset(command);
shortcuts.unset(command);
}
}
settings.ff_settings = saved.ff_settings;
};
}
Self {
persistence,
settings: Arc::new(Mutex::new(settings)),
shortcuts: Arc::new(Mutex::new(shortcuts)),
}
}
pub fn shortcut_for(&self, command: Command) -> Option<KeyboardShortcut> {
let lock = self.settings.lock().unwrap();
lock.shortcuts.by_command.get(&command).copied()
let lock = self.shortcuts.lock().unwrap();
lock.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 *key != shortcut.logical_key || !modifiers.contains(shortcut.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 consume_all(&self, input: &mut egui::InputState) -> HashSet<Command> {
let lock = self.shortcuts.lock().unwrap();
lock.all
.iter()
.filter_map(|(command, shortcut)| input.consume_shortcut(shortcut).then_some(*command))
.collect()
}
pub fn set(&self, command: Command, shortcut: KeyboardShortcut) {
let updated = {
let mut lock = self.settings.lock().unwrap();
lock.shortcuts.set(command, shortcut);
let mut lock = self.shortcuts.lock().unwrap();
lock.set(command, shortcut);
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
@ -733,20 +642,8 @@ impl ShortcutProvider {
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;
}
let mut lock = self.shortcuts.lock().unwrap();
lock.unset(command);
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
@ -754,25 +651,10 @@ impl ShortcutProvider {
pub fn reset(&self) {
let updated = {
let mut lock = self.settings.lock().unwrap();
*lock = Settings::default();
let mut lock = self.shortcuts.lock().unwrap();
*lock = Shortcuts::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

@ -1,7 +1,7 @@
// hide console in release mode
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::{process, time::SystemTime};
use std::{path::PathBuf, process, time::SystemTime};
use anyhow::{Result, bail};
use app::Application;
@ -12,15 +12,8 @@ use tracing::error;
use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt};
use winit::event_loop::{ControlFlow, EventLoop};
use crate::{
config::{CliArgs, SimConfig},
emulator::SimId,
persistence::Persistence,
};
mod app;
mod audio;
mod config;
mod controller;
mod emulator;
mod gdbserver;
@ -29,9 +22,17 @@ mod images;
mod input;
mod memory;
mod persistence;
mod profiler;
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);
@ -43,9 +44,9 @@ fn set_panic_handler() {
std::panic::set_hook(Box::new(|info| {
let mut message = String::new();
if let Some(msg) = info.payload().downcast_ref::<&str>() {
message += &format!("{msg}\n");
message += &format!("{}\n", msg);
} else if let Some(msg) = info.payload().downcast_ref::<String>() {
message += &format!("{msg}\n");
message += &format!("{}\n", msg);
}
if let Some(location) = info.location() {
message += &format!(
@ -55,9 +56,9 @@ fn set_panic_handler() {
);
}
let backtrace = std::backtrace::Backtrace::force_capture();
message += &format!("stack trace:\n{backtrace:#}\n");
message += &format!("stack trace:\n{:#}\n", backtrace);
eprint!("{message}");
eprint!("{}", message);
let Some(project_dirs) = directories::ProjectDirs::from("com", "virtual-boy", "Lemur")
else {
@ -71,7 +72,7 @@ fn set_panic_handler() {
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis();
let logfile_name = format!("crash-{timestamp}.txt");
let logfile_name = format!("crash-{}.txt", timestamp);
let _ = std::fs::write(data_dir.join(logfile_name), message);
}));
}
@ -93,9 +94,7 @@ fn main() -> Result<()> {
#[cfg(windows)]
set_process_priority_to_high()?;
let args = CliArgs::parse();
let persistence = Persistence::new();
let args = Args::parse();
let (mut builder, client) = EmulatorBuilder::new();
if let Some(path) = &args.rom {
@ -107,17 +106,6 @@ fn main() -> Result<()> {
}
builder = builder.start_paused(true);
}
if args.profile {
builder = builder.start_paused(true)
}
let p1 = SimConfig::load(&persistence, SimId::Player1);
let p2 = SimConfig::load(&persistence, SimId::Player2);
let watch = args.watch;
builder = builder
.with_audio_on(p1.audio_enabled, p2.audio_enabled)
.with_watch_rom(watch, watch);
ThreadBuilder::default()
.name("Emulator".to_owned())
@ -136,6 +124,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, persistence, args))?;
event_loop.run_app(&mut Application::new(client, proxy, args.debug_port))?;
Ok(())
}

View File

@ -50,11 +50,11 @@ impl MemoryClient {
}
fn aligned_memory(start: u32, length: usize) -> BoxBytes {
if start.is_multiple_of(4) && length.is_multiple_of(4) {
if start % 4 == 0 && length % 4 == 0 {
let memory = vec![0u32; length / 4].into_boxed_slice();
return bytemuck::box_bytes_of(memory);
}
if start.is_multiple_of(2) && length.is_multiple_of(2) {
if start % 2 == 0 && length % 2 == 0 {
let memory = vec![0u16; length / 2].into_boxed_slice();
return bytemuck::box_bytes_of(memory);
}
@ -169,7 +169,7 @@ impl MemoryRef<'_> {
T::from_bytes(&self.inner[from..to])
}
pub fn range<T: MemoryValue>(&self, start: usize, count: usize) -> MemoryIter<'_, T> {
pub fn range<T: MemoryValue>(&self, start: usize, count: usize) -> MemoryIter<T> {
let from = start * size_of::<T>();
let to = from + (count * size_of::<T>());
MemoryIter::new(&self.inner[from..to])

View File

@ -1,264 +0,0 @@
use std::{
sync::{Arc, Mutex},
thread,
};
use anyhow::Result;
use tokio::{select, sync::mpsc};
use crate::emulator::{
EmulatorClient, EmulatorCommand, GameInfo, InlineStack, ProfileEvent, SimEvent, SimId,
};
use recording::Recording;
use state::ProgramState;
mod recording;
mod state;
pub struct Profiler {
sim_id: SimId,
client: EmulatorClient,
status: Arc<Mutex<ProfilerStatus>>,
action: Option<mpsc::UnboundedSender<RecordingAction>>,
killer: Option<oneshot::Sender<()>>,
}
impl Profiler {
pub fn new(sim_id: SimId, client: EmulatorClient) -> Self {
Self {
sim_id,
client,
status: Arc::new(Mutex::new(ProfilerStatus::Disabled)),
action: None,
killer: None,
}
}
pub fn status(&self) -> ProfilerStatus {
self.status.lock().unwrap().clone()
}
pub fn enable(&mut self) {
let sim_id = self.sim_id;
let client = self.client.clone();
let status = self.status.clone();
let (action_tx, action_rx) = mpsc::unbounded_channel();
self.action = Some(action_tx);
let (killer_tx, killer_rx) = oneshot::channel();
self.killer = Some(killer_tx);
thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async move {
select! {
_ = run_profile(sim_id, client, status.clone(), action_rx) => {}
_ = killer_rx => {
*status.lock().unwrap() = ProfilerStatus::Disabled;
}
}
})
});
}
pub fn disable(&mut self) {
if let Some(killer) = self.killer.take() {
let _ = killer.send(());
}
}
pub fn start_recording(&mut self) {
if let Some(action) = &self.action {
let _ = action.send(RecordingAction::Start);
}
}
pub fn finish_recording(&mut self) -> oneshot::Receiver<Vec<u8>> {
let (tx, rx) = oneshot::channel();
if let Some(action) = &self.action {
let _ = action.send(RecordingAction::Finish(tx));
}
rx
}
pub fn cancel_recording(&mut self) {
if let Some(action) = &self.action {
let _ = action.send(RecordingAction::Cancel);
}
}
}
impl Drop for Profiler {
fn drop(&mut self) {
self.disable();
}
}
async fn run_profile(
sim_id: SimId,
client: EmulatorClient,
status: Arc<Mutex<ProfilerStatus>>,
mut action_source: mpsc::UnboundedReceiver<RecordingAction>,
) {
let (profile_sync, mut profile_source) = mpsc::unbounded_channel();
client.send_command(EmulatorCommand::StartProfiling(sim_id, profile_sync));
*status.lock().unwrap() = ProfilerStatus::Enabled;
let mut session = ProfilerSession::new();
loop {
select! {
maybe_event = profile_source.recv() => {
let Some(event) = maybe_event else {
break; // emulator thread disconnected
};
if let Err(error) = handle_event(event, &mut session).await {
*status.lock().unwrap() = ProfilerStatus::Error(error.to_string());
return;
}
}
maybe_action = action_source.recv() => {
let Some(action) = maybe_action else {
break; // ui thread disconnected
};
handle_action(action, &mut session, &status);
}
}
}
*status.lock().unwrap() = ProfilerStatus::Disabled;
}
async fn handle_event(event: ProfileEvent, session: &mut ProfilerSession) -> Result<()> {
match event {
ProfileEvent::Start { info } => session.start_profiling(info).await,
ProfileEvent::Update {
cycles,
event,
inline_stack,
} => {
session.track_elapsed_cycles(cycles);
if let Some(event) = event {
session.track_event(event)?;
}
if let Some(stack) = inline_stack {
session.track_inline_stack(stack);
}
}
}
Ok(())
}
fn handle_action(
action: RecordingAction,
session: &mut ProfilerSession,
status: &Mutex<ProfilerStatus>,
) {
match action {
RecordingAction::Start => {
session.start_recording();
*status.lock().unwrap() = ProfilerStatus::Recording;
}
RecordingAction::Finish(rx) => {
if let Some(bytes) = session.finish_recording() {
let _ = rx.send(bytes);
}
*status.lock().unwrap() = ProfilerStatus::Enabled;
}
RecordingAction::Cancel => {
session.cancel_recording();
*status.lock().unwrap() = ProfilerStatus::Enabled;
}
}
}
#[derive(Clone)]
pub enum ProfilerStatus {
Disabled,
Enabled,
Recording,
Error(String),
}
impl ProfilerStatus {
pub fn enabled(&self) -> bool {
matches!(self, Self::Enabled | Self::Recording)
}
}
enum RecordingAction {
Start,
Finish(oneshot::Sender<Vec<u8>>),
Cancel,
}
struct ProfilerSession {
program: Option<ProgramState>,
recording: Option<Recording>,
}
impl ProfilerSession {
fn new() -> Self {
Self {
program: None,
recording: None,
}
}
async fn start_profiling(&mut self, info: Arc<GameInfo>) {
let program = ProgramState::new(info).await;
let recording = if self.recording.is_some() {
Some(Recording::new(&program))
} else {
None
};
self.program = Some(program);
self.recording = recording;
}
fn track_elapsed_cycles(&mut self, cycles: u32) {
if let (Some(state), Some(recording)) = (&self.program, &mut self.recording) {
recording.track_elapsed_cycles(state, cycles);
}
}
fn track_event(&mut self, event: SimEvent) -> Result<()> {
let Some(program) = &mut self.program else {
return Ok(());
};
match event {
SimEvent::Call(address) => program.track_call(address),
SimEvent::Return => program.track_return(),
SimEvent::Halt => program.track_halt(),
SimEvent::Interrupt(code, address) => program.track_interrupt(code, address),
SimEvent::Reti => program.track_reti(),
SimEvent::Marker(name) => {
if let Some(recording) = &mut self.recording {
recording.track_marker(name);
};
Ok(())
}
}
}
fn track_inline_stack(&mut self, inline_stack: InlineStack) {
if let Some(program) = &mut self.program {
program.track_inline_stack(inline_stack);
}
}
fn start_recording(&mut self) {
if let Some(program) = &self.program {
self.recording = Some(Recording::new(program));
}
}
fn finish_recording(&mut self) -> Option<Vec<u8>> {
self.recording.take().map(|r| r.finish())
}
fn cancel_recording(&mut self) {
self.recording.take();
}
}

View File

@ -1,152 +0,0 @@
use std::{borrow::Cow, collections::HashMap};
use fxprof_processed_profile::{
CategoryHandle, CpuDelta, Frame, FrameFlags, FrameInfo, MarkerTiming, ProcessHandle, Profile,
ReferenceTimestamp, SamplingInterval, StackHandle, StaticSchemaMarker, StringHandle,
ThreadHandle, Timestamp,
};
use crate::profiler::state::{ProgramState, RESET_CODE, StackFrame};
pub struct Recording {
profile: Profile,
process: ProcessHandle,
threads: HashMap<u16, ThreadHandle>,
now: u64,
}
impl Recording {
pub fn new(state: &ProgramState) -> Self {
let reference_timestamp = ReferenceTimestamp::from_millis_since_unix_epoch(0.0);
let interval = SamplingInterval::from_hz(20_000_000.0);
let mut profile = Profile::new(state.name(), reference_timestamp, interval);
let process =
profile.add_process(state.name(), 1, Timestamp::from_nanos_since_reference(0));
let lib = profile.add_lib(state.library_info().clone());
profile.add_lib_mapping(process, lib, 0x00000000, 0xffffffff, 0);
let mut me = Self {
profile,
process,
threads: HashMap::new(),
now: 0,
};
me.track_elapsed_cycles(state, 0);
me
}
pub fn track_elapsed_cycles(&mut self, state: &ProgramState, cycles: u32) {
self.now += cycles as u64;
let timestamp = Timestamp::from_nanos_since_reference(self.now * 50);
let weight = 1;
let active_code = if let Some((code, frames)) = state.current_stack() {
let thread = *self.threads.entry(code).or_insert_with(|| {
let process = self.process;
let tid = code as u32;
let start_time = Timestamp::from_nanos_since_reference(self.now * 50);
let is_main = code == RESET_CODE;
let thread = self.profile.add_thread(process, tid, start_time, is_main);
self.profile
.set_thread_name(thread, &thread_name_for_code(code));
thread
});
let stack = self.handle_for_stack(thread, frames);
let cpu_delta = CpuDelta::from_nanos((self.now - cycles as u64) * 50);
self.profile
.add_sample(thread, timestamp, stack, cpu_delta, weight);
Some(code)
} else {
None
};
for (code, thread) in &self.threads {
if active_code == Some(*code) {
continue;
}
self.profile
.add_sample_same_stack_zero_cpu(*thread, timestamp, weight);
}
}
pub fn track_marker(&mut self, name: Cow<'static, str>) {
let Some(thread) = self.threads.get(&RESET_CODE) else {
return;
};
let timing = MarkerTiming::Instant(Timestamp::from_nanos_since_reference(self.now * 50));
let marker = SimpleMarker(name);
self.profile.add_marker(*thread, timing, marker);
}
pub fn finish(self) -> Vec<u8> {
serde_json::to_vec(&self.profile).expect("could not serialize profile")
}
fn handle_for_stack(
&mut self,
thread: ThreadHandle,
frames: &[StackFrame],
) -> Option<StackHandle> {
let frames = frames
.iter()
.map(|f| {
let frame = match f {
StackFrame::Address(address) => Frame::InstructionPointer(*address as u64),
StackFrame::Label(label) => Frame::Label(self.profile.intern_string(label)),
};
FrameInfo {
frame,
category_pair: CategoryHandle::OTHER.into(),
flags: FrameFlags::empty(),
}
})
.collect::<Vec<_>>();
self.profile.intern_stack_frames(thread, frames.into_iter())
}
}
struct SimpleMarker(Cow<'static, str>);
impl StaticSchemaMarker for SimpleMarker {
const UNIQUE_MARKER_TYPE_NAME: &'static str = "Simple";
const FIELDS: &'static [fxprof_processed_profile::StaticSchemaMarkerField] = &[];
fn name(&self, profile: &mut Profile) -> StringHandle {
profile.intern_string(&self.0)
}
fn category(&self, _profile: &mut Profile) -> CategoryHandle {
CategoryHandle::OTHER
}
fn string_field_value(&self, _field_index: u32) -> StringHandle {
unreachable!()
}
fn number_field_value(&self, _field_index: u32) -> f64 {
unreachable!()
}
}
fn thread_name_for_code(code: u16) -> std::borrow::Cow<'static, str> {
match code {
RESET_CODE => "Main".into(),
0xffd0 => "Duplexed exception".into(),
0xfe40 => "VIP interrupt".into(),
0xfe30 => "Communication interrupt".into(),
0xfe20 => "Game pak interrupt".into(),
0xfe10 => "Timer interrupt".into(),
0xfe00 => "Game pad interrupt".into(),
0xffc0 => "Address trap".into(),
0xffa0..0xffc0 => format!("Trap (vector {})", code - 0xffa0).into(),
0xff90 => "Illegal opcode exception".into(),
0xff80 => "Zero division exception".into(),
0xff60 => "Floating-point reserved operand exception".into(),
0xff70 => "Floating-point invalid operation exception".into(),
0xff68 => "Floating-point zero division exception".into(),
0xff64 => "Floating-point overflow exception".into(),
other => format!("Unrecognized handler (0x{other:04x})").into(),
}
}

View File

@ -1,129 +0,0 @@
use std::{collections::HashMap, sync::Arc};
use anyhow::{Result, bail};
use fxprof_processed_profile::LibraryInfo;
use crate::emulator::{GameInfo, InlineStack};
pub struct ProgramState {
info: Arc<GameInfo>,
call_stacks: HashMap<u16, Vec<StackFrame>>,
context_stack: Vec<u16>,
}
pub enum StackFrame {
Address(u32),
Label(Arc<String>),
}
pub const RESET_CODE: u16 = 0xfff0;
impl ProgramState {
pub async fn new(info: Arc<GameInfo>) -> Self {
let mut call_stacks = HashMap::new();
call_stacks.insert(RESET_CODE, vec![StackFrame::Address(0xfffffff0)]);
Self {
info,
call_stacks,
context_stack: vec![RESET_CODE],
}
}
pub fn name(&self) -> &str {
self.info.name()
}
pub fn library_info(&self) -> &LibraryInfo {
self.info.library_info()
}
pub fn current_stack(&self) -> Option<(u16, &[StackFrame])> {
let code = self.context_stack.last()?;
let call_stack = self.call_stacks.get(code)?;
Some((*code, call_stack))
}
pub fn track_call(&mut self, address: u32) -> Result<()> {
let Some(code) = self.context_stack.last() else {
bail!("How did we call anything when we're halted?");
};
let Some(stack) = self.call_stacks.get_mut(code) else {
bail!("missing stack {code:04x}");
};
stack.push(StackFrame::Address(address));
Ok(())
}
pub fn track_return(&mut self) -> Result<()> {
let Some(code) = self.context_stack.last() else {
bail!("how did we return when we're halted?");
};
let Some(stack) = self.call_stacks.get_mut(code) else {
bail!("missing stack {code:04x}");
};
// just popping the inline frames first
while stack
.pop_if(|f| matches!(f, StackFrame::Label(_)))
.is_some()
{}
if stack.pop().is_none() {
bail!("returned from {code:04x} but stack was empty");
}
if stack.is_empty() {
bail!("returned to oblivion");
}
Ok(())
}
pub fn track_halt(&mut self) -> Result<()> {
let Some(RESET_CODE) = self.context_stack.pop() else {
bail!("halted when not in an interrupt");
};
Ok(())
}
pub fn track_interrupt(&mut self, code: u16, address: u32) -> Result<()> {
// if the CPU was halted before, wake it up now
if self.context_stack.is_empty() {
self.context_stack.push(RESET_CODE);
}
self.context_stack.push(code);
if self
.call_stacks
.insert(code, vec![StackFrame::Address(address)])
.is_some()
{
bail!("{code:04x} fired twice");
}
Ok(())
}
pub fn track_reti(&mut self) -> Result<()> {
let Some(code) = self.context_stack.pop() else {
bail!("RETI when halted");
};
if code == RESET_CODE {
bail!("RETI when not in interrupt");
}
if self.call_stacks.remove(&code).is_none() {
bail!("{code:04x} popped but never called");
}
Ok(())
}
pub fn track_inline_stack(&mut self, inline_stack: InlineStack) {
let Some(code) = self.context_stack.last() else {
return;
};
let Some(call_stack) = self.call_stacks.get_mut(code) else {
return;
};
while call_stack
.pop_if(|f| matches!(f, StackFrame::Label(_)))
.is_some()
{}
for label in inline_stack.iter() {
call_stack.push(StackFrame::Label(label.clone()));
}
}
}

View File

@ -1,18 +1,13 @@
use std::sync::Arc;
pub use about::AboutWindow;
use egui::{Ui, ViewportBuilder};
pub use game::{ChildWindow, GameWindow};
pub use game_screen::{DisplayMode, GameScreen};
use egui::{Context, ViewportBuilder, ViewportId};
pub use game::GameWindow;
pub use gdb::GdbServerWindow;
pub use hotkeys::HotkeysWindow;
pub use input::InputWindow;
pub use profile::ProfileWindow;
pub use terminal::TerminalWindow;
pub use shortcuts::ShortcutsWindow;
pub use vip::{
BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow,
};
use winit::{event::KeyEvent, window::Window};
use winit::event::KeyEvent;
use crate::emulator::SimId;
@ -20,25 +15,23 @@ mod about;
mod game;
mod game_screen;
mod gdb;
mod hotkeys;
mod input;
mod profile;
mod terminal;
mod shortcuts;
mod utils;
mod vip;
pub trait AppWindow {
fn viewport_id(&self) -> ViewportId;
fn sim_id(&self) -> SimId {
SimId::Player1
}
fn image_url(&self, name: &str) -> String {
format!("vip://{}/{name}", self.sim_id())
}
fn initial_viewport(&self) -> ViewportBuilder;
fn show(&mut self, ui: &mut Ui);
fn on_init(&mut self, args: InitArgs) {
let _ = args;
fn show(&mut self, ctx: &Context);
fn on_init(&mut self, ctx: &Context, render_state: &egui_wgpu::RenderState) {
let _ = ctx;
let _ = render_state;
}
fn on_destroy(&mut self) {}
fn handle_key_event(&mut self, event: &KeyEvent) -> bool {
let _ = event;
false
@ -48,8 +41,3 @@ pub trait AppWindow {
false
}
}
pub struct InitArgs<'a> {
pub window: &'a Arc<Window>,
pub render_state: &'a egui_wgpu::RenderState,
}

View File

@ -1,18 +1,22 @@
use egui::{CentralPanel, Image, Ui, ViewportBuilder};
use egui::{CentralPanel, Context, Image, ViewportBuilder, ViewportId};
use super::AppWindow;
pub struct AboutWindow;
impl AppWindow for AboutWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of("About")
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("About Lemur")
.with_inner_size((300.0, 200.0))
}
fn show(&mut self, ui: &mut Ui) {
CentralPanel::default().show_inside(ui, |ui| {
fn show(&mut self, ctx: &Context) {
CentralPanel::default().show(ctx, |ui| {
ui.vertical_centered(|ui| {
ui.label("Lemur Virtual Boy Emulator");
ui.label(format!("Version {}", env!("CARGO_PKG_VERSION")));

View File

@ -1,31 +1,18 @@
use std::{
ops::{Deref, DerefMut},
sync::{Arc, Mutex, atomic::AtomicBool, mpsc},
time::Duration,
};
use std::sync::mpsc;
use crate::{
app::UserEvent,
config::{COLOR_PRESETS, SimConfig},
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
images::ImageTextureLoader,
input::{Command, MappingProvider, ShortcutProvider},
memory::MemoryClient,
input::{Command, ShortcutProvider},
persistence::Persistence,
window::{
AboutWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GdbServerWindow,
HotkeysWindow, InitArgs, InputWindow, ObjectWindow, ProfileWindow, RegisterWindow,
TerminalWindow, WorldWindow, utils::UiData,
},
};
use anyhow::Context as _;
use egui::{
Align2, Button, CentralPanel, Color32, Context, Frame, MenuBar, Panel, Pos2, Ui, Vec2,
ViewportBuilder, ViewportCommand, ViewportId, ViewportIdMap, Window,
Align2, Button, CentralPanel, Color32, Context, Direction, Frame, TopBottomPanel, Ui, Vec2,
ViewportBuilder, ViewportCommand, ViewportId, Window, menu,
};
use egui_notify::{Anchor, Toast, Toasts};
use egui_toast::{Toast, Toasts};
use serde::{Deserialize, Serialize};
use winit::{event::KeyEvent, event_loop::EventLoopProxy};
use winit::event_loop::EventLoopProxy;
use super::{
AppWindow,
@ -33,202 +20,56 @@ use super::{
utils::UiExt as _,
};
const COLOR_PRESETS: [[Color32; 2]; 3] = [
[
Color32::from_rgb(0xff, 0x00, 0x00),
Color32::from_rgb(0x00, 0xc6, 0xf0),
],
[
Color32::from_rgb(0x00, 0xb4, 0x00),
Color32::from_rgb(0xc8, 0x00, 0xff),
],
[
Color32::from_rgb(0xb4, 0x9b, 0x00),
Color32::from_rgb(0x00, 0x00, 0xff),
],
];
pub struct GameWindow {
viewport_id: ViewportId,
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
shortcuts: ShortcutProvider,
sim_id: SimId,
config: SimConfig,
toasts: Toasts,
config: GameConfig,
screen: Option<GameScreen>,
messages: mpsc::Receiver<Toast>,
message_sink: mpsc::Sender<Toast>,
messages: Option<mpsc::Receiver<Toast>>,
color_picker: Option<ColorPickerState>,
window: Option<Arc<winit::window::Window>>,
memory: Arc<MemoryClient>,
images: Arc<ImageTextureLoader>,
mappings: MappingProvider,
children: ViewportIdMap<ChildWindowWrapper>,
child_states: UiData<ViewportIdMap<ChildState>>,
queued_children: Vec<ChildWindow>,
}
impl GameWindow {
#[expect(clippy::too_many_arguments)]
pub fn new(
viewport_id: ViewportId,
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
shortcuts: ShortcutProvider,
memory: &Arc<MemoryClient>,
images: &Arc<ImageTextureLoader>,
mappings: MappingProvider,
sim_id: SimId,
) -> Self {
let config = SimConfig::load(&persistence, sim_id);
let toasts = Toasts::new()
.with_anchor(Anchor::BottomLeft)
.with_margin((10.0, 10.0).into())
.reverse(true);
let (message_sink, messages) = mpsc::channel();
let config = load_config(&persistence, sim_id);
Self {
viewport_id,
client,
proxy,
persistence,
shortcuts,
sim_id,
config,
toasts,
screen: None,
messages,
message_sink,
messages: None,
color_picker: None,
window: None,
memory: memory.clone(),
images: images.clone(),
mappings: mappings.clone(),
children: ViewportIdMap::default(),
child_states: UiData::new(),
queued_children: vec![],
}
}
pub fn open(&mut self, window: ChildWindow) {
// queue opening this child until our next render call,
// so that we get a chance to look up its size
self.queued_children.push(window);
}
pub fn close(&mut self, window: ChildWindow) {
let id = window.viewport_id(self.sim_id);
self.children.remove(&id);
}
fn do_open(&mut self, window: ChildWindow) {
let viewport_id = window.viewport_id(self.sim_id);
if self.children.contains_key(&viewport_id) {
return;
}
let child = match window {
ChildWindow::About => AppWrapper::of_dyn(AboutWindow),
ChildWindow::CharacterData => AppWrapper::of_dyn(CharacterDataWindow::new(
self.sim_id,
&self.memory,
&self.images,
)),
ChildWindow::BgMap => {
AppWrapper::of_dyn(BgMapWindow::new(self.sim_id, &self.memory, &self.images))
}
ChildWindow::Objects => {
AppWrapper::of_dyn(ObjectWindow::new(self.sim_id, &self.memory, &self.images))
}
ChildWindow::Worlds => {
AppWrapper::of_dyn(WorldWindow::new(self.sim_id, &self.memory, &self.images))
}
ChildWindow::FrameBuffers => AppWrapper::of_dyn(FrameBufferWindow::new(
self.sim_id,
&self.memory,
&self.images,
)),
ChildWindow::Registers => {
AppWrapper::of_dyn(RegisterWindow::new(self.sim_id, &self.memory))
}
ChildWindow::Terminal => {
AppWrapper::of_dyn(TerminalWindow::new(self.sim_id, &self.client))
}
ChildWindow::Profiler { launch } => {
let mut profile = ProfileWindow::new(self.sim_id, self.client.clone());
if launch {
profile.launch();
}
AppWrapper::of_dyn(profile)
}
ChildWindow::Debugger { port } => {
let mut debugger =
GdbServerWindow::new(self.sim_id, self.client.clone(), self.proxy.clone());
if let Some(port) = port {
debugger.launch(port);
}
AppWrapper::of_dyn(debugger)
}
ChildWindow::Input => AppWrapper::of_dyn(InputWindow::new(self.mappings.clone())),
ChildWindow::Hotkeys => AppWrapper::of_dyn(HotkeysWindow::new(self.shortcuts.clone())),
ChildWindow::Player2 => {
if self.sim_id == SimId::Player2 {
return;
}
AppWrapper::of_game(GameWindow::new(
viewport_id,
self.client.clone(),
self.proxy.clone(),
self.persistence.clone(),
self.shortcuts.clone(),
&self.memory,
&self.images,
self.mappings.clone(),
SimId::Player2,
))
}
};
let mut viewport = child.initial_viewport();
if let Some(state) = self.child_states.get(&viewport_id) {
viewport.position = Some(state.position);
viewport.inner_size = Some(state.size);
}
self.children.insert(
viewport_id,
ChildWindowWrapper {
app: Arc::new(Mutex::new(child)),
updates: Some(viewport),
close_requested: Arc::new(AtomicBool::new(false)),
},
);
}
pub fn handle_init(&mut self, viewport_id: ViewportId, args: InitArgs) {
self.handle_event(viewport_id, |window| window.on_init(args))
}
pub fn handle_key_event(&mut self, viewport_id: ViewportId, event: &KeyEvent) -> bool {
self.handle_event(viewport_id, |window| window.handle_key_event(event))
}
pub fn handle_gamepad_event(&mut self, viewport_id: ViewportId, event: &gilrs::Event) -> bool {
self.handle_event(viewport_id, |window| window.handle_gamepad_event(event))
}
fn handle_event<F, R>(&mut self, viewport_id: ViewportId, cb: F) -> R
where
F: FnOnce(&mut dyn AppWindow) -> R,
R: Default,
{
let p2_viewport_id = ChildWindow::Player2.viewport_id(SimId::Player1);
if self.viewport_id == viewport_id {
cb(self)
} else if let Some(child) = self.children.get_mut(&viewport_id) {
cb(child.app.lock().unwrap().deref_mut().deref_mut())
} else if let Some(mut p2) = self
.children
.get_mut(&p2_viewport_id)
.map(|w| w.app.lock().unwrap())
{
if let AppWrapper::Game(g) = p2.deref_mut() {
g.handle_event(viewport_id, cb)
} else {
R::default()
}
} else {
R::default()
}
}
fn show_menu(&mut self, ui: &mut Ui) {
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;
@ -239,17 +80,13 @@ impl GameWindow {
match command {
Command::OpenRom => {
let rom = rfd::FileDialog::new()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom", "elf", "isx"])
.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::ReloadRom => {
self.client
.send_command(EmulatorCommand::ReloadRom(self.sim_id));
}
Command::Quit => {
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
}
@ -272,20 +109,6 @@ impl GameWindow {
self.client.send_command(EmulatorCommand::FrameAdvance);
}
}
Command::FastForward(speed) => {
self.client
.send_command(EmulatorCommand::SetSpeed(speed as f64));
}
Command::Screenshot => {
let autopause = state == EmulatorState::Running && can_pause;
if autopause {
self.client.send_command(EmulatorCommand::Pause);
}
pollster::block_on(self.take_screenshot());
if autopause {
self.client.send_command(EmulatorCommand::Resume);
}
}
}
}
@ -295,24 +118,13 @@ impl GameWindow {
.clicked()
{
let rom = rfd::FileDialog::new()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom", "elf", "isx"])
.add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
.pick_file();
if let Some(path) = rom {
self.client
.send_command(EmulatorCommand::LoadGame(self.sim_id, path));
}
}
if ui
.add(self.button_for(ui.ctx(), "Reload ROM", Command::ReloadRom))
.clicked()
{
self.client
.send_command(EmulatorCommand::ReloadRom(self.sim_id));
}
let watch_rom = self.client.is_rom_watched(self.sim_id);
if ui.selectable_button(watch_rom, "Watch ROM").clicked() {
self.client
.send_command(EmulatorCommand::WatchRom(self.sim_id, !watch_rom));
ui.close_menu();
}
if ui
.add(self.button_for(ui.ctx(), "Quit", Command::Quit))
@ -331,6 +143,7 @@ impl GameWindow {
.clicked()
{
self.client.send_command(EmulatorCommand::Pause);
ui.close_menu();
}
} else if ui
.add_enabled(
@ -340,6 +153,7 @@ impl GameWindow {
.clicked()
{
self.client.send_command(EmulatorCommand::Resume);
ui.close_menu();
}
if ui
.add_enabled(is_ready, self.button_for(ui.ctx(), "Reset", Command::Reset))
@ -347,6 +161,7 @@ impl GameWindow {
{
self.client
.send_command(EmulatorCommand::Reset(self.sim_id));
ui.close_menu();
}
ui.separator();
if ui
@ -357,19 +172,10 @@ impl GameWindow {
.clicked()
{
self.client.send_command(EmulatorCommand::FrameAdvance);
}
ui.separator();
if ui
.add_enabled(
is_ready,
self.button_for(ui.ctx(), "Screenshot", Command::Screenshot),
)
.clicked()
{
pollster::block_on(self.take_screenshot());
ui.close_menu();
}
});
ui.menu_button("Options", |ui| self.show_options_menu(ui));
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
@ -378,100 +184,75 @@ impl GameWindow {
{
self.client
.send_command(EmulatorCommand::StartSecondSim(None));
self.open(ChildWindow::Player2);
self.proxy.send_event(UserEvent::OpenPlayer2).unwrap();
ui.close_menu();
}
if has_player_2 {
let linked = self.client.are_sims_linked();
if linked && ui.button("Unlink").clicked() {
self.client.send_command(EmulatorCommand::Unlink);
ui.close_menu();
}
if !linked && ui.button("Link").clicked() {
self.client.send_command(EmulatorCommand::Link);
ui.close_menu();
}
}
});
ui.menu_button("Tools", |ui| {
if ui.button("Terminal").clicked() {
self.open(ChildWindow::Terminal);
}
if ui.button("Profiler").clicked() {
self.open(ChildWindow::Profiler { launch: false });
}
if ui.button("GDB Server").clicked() {
self.open(ChildWindow::Debugger { port: None });
self.proxy
.send_event(UserEvent::OpenDebugger(self.sim_id))
.unwrap();
ui.close_menu();
}
ui.separator();
if ui.button("Character Data").clicked() {
self.open(ChildWindow::CharacterData);
self.proxy
.send_event(UserEvent::OpenCharacterData(self.sim_id))
.unwrap();
ui.close_menu();
}
if ui.button("Background Maps").clicked() {
self.open(ChildWindow::BgMap);
self.proxy
.send_event(UserEvent::OpenBgMap(self.sim_id))
.unwrap();
ui.close_menu();
}
if ui.button("Objects").clicked() {
self.open(ChildWindow::Objects);
self.proxy
.send_event(UserEvent::OpenObjects(self.sim_id))
.unwrap();
ui.close_menu();
}
if ui.button("Worlds").clicked() {
self.open(ChildWindow::Worlds);
self.proxy
.send_event(UserEvent::OpenWorlds(self.sim_id))
.unwrap();
ui.close_menu();
}
if ui.button("Frame Buffers").clicked() {
self.open(ChildWindow::FrameBuffers);
self.proxy
.send_event(UserEvent::OpenFrameBuffers(self.sim_id))
.unwrap();
ui.close_menu();
}
if ui.button("Registers").clicked() {
self.open(ChildWindow::Registers);
self.proxy
.send_event(UserEvent::OpenRegisters(self.sim_id))
.unwrap();
ui.close_menu();
}
});
ui.menu_button("Help", |ui| {
if ui.button("About").clicked() {
self.open(ChildWindow::About);
self.proxy.send_event(UserEvent::OpenAbout).unwrap();
ui.close_menu();
}
});
}
async fn take_screenshot(&mut self) {
match self.try_take_screenshot().await {
Ok(Some(path)) => {
let mut toast = Toast::info(format!("Saved to {path}"));
toast.duration(Some(Duration::from_secs(5)));
self.toasts.add(toast);
}
Ok(None) => {}
Err(error) => {
let mut toast = Toast::error(format!("{error:#}"));
toast.duration(Some(Duration::from_secs(5)));
self.toasts.add(toast);
}
}
}
async fn try_take_screenshot(&self) -> anyhow::Result<Option<String>> {
let (tx, rx) = oneshot::channel();
self.client
.send_command(EmulatorCommand::Screenshot(self.sim_id, tx));
let bytes = rx.await.context("Could not take screenshot")?;
let mut file_dialog = rfd::FileDialog::new()
.add_filter("PNG images", &["png"])
.set_file_name("screenshot.png");
if let Some(window) = self.window.as_ref() {
file_dialog = file_dialog.set_parent(window);
}
let file = file_dialog.save_file();
let Some(path) = file else {
return Ok(None);
};
if bytes.len() != 384 * 224 * 2 {
anyhow::bail!("Unexpected screenshot size");
}
let mut screencap = image::GrayImage::new(384 * 2, 224);
for (index, pixel) in bytes.into_iter().enumerate() {
let x = (index / 2) % 384 + ((index % 2) * 384);
let y = (index / 2) / 384;
screencap.put_pixel(x as u32, y as u32, image::Luma([pixel]));
}
screencap.save(&path).context("Could not save screenshot")?;
Ok(Some(path.display().to_string()))
}
fn show_options_menu(&mut self, ui: &mut Ui) {
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;
@ -487,7 +268,8 @@ impl GameWindow {
.selectable_button((current_dims - dims).length() < 1.0, label)
.clicked()
{
ui.send_viewport_cmd(ViewportCommand::InnerSize(dims));
ctx.send_viewport_cmd(ViewportCommand::InnerSize(dims));
ui.close_menu();
}
}
});
@ -517,18 +299,20 @@ impl GameWindow {
let new_proportions = display_mode.proportions();
let scale = new_proportions / old_proportions;
if scale != Vec2::new(1.0, 1.0) {
ui.send_viewport_cmd(ViewportCommand::InnerSize(current_dims * scale));
ctx.send_viewport_cmd(ViewportCommand::InnerSize(current_dims * scale));
}
self.update_config(|c| {
c.display_mode = display_mode;
c.dimensions = current_dims * scale;
});
ui.close_menu();
});
ui.menu_button("Colors", |ui| {
for preset in COLOR_PRESETS {
if ui.color_pair_button(preset[0], preset[1]).clicked() {
self.update_config(|c| c.colors = preset);
ui.close_menu();
}
}
ui.with_layout(ui.layout().with_cross_align(egui::Align::Center), |ui| {
@ -549,29 +333,34 @@ impl GameWindow {
just_opened: true,
unpause_on_close: is_running,
});
ui.close_menu();
}
});
});
});
ui.menu_button("Audio", |ui| {
if ui
.selectable_button(self.config.audio_enabled, "Enabled")
.clicked()
{
self.update_config(|c| c.audio_enabled = !c.audio_enabled);
self.client.send_command(EmulatorCommand::SetAudioEnabled(
self.sim_id,
self.config.audio_enabled,
));
let p1_enabled = self.client.is_audio_enabled(SimId::Player1);
let p2_enabled = self.client.is_audio_enabled(SimId::Player2);
if ui.selectable_button(p1_enabled, "Player 1").clicked() {
self.client
.send_command(EmulatorCommand::SetAudioEnabled(!p1_enabled, p2_enabled));
ui.close_menu();
}
if ui.selectable_button(p2_enabled, "Player 2").clicked() {
self.client
.send_command(EmulatorCommand::SetAudioEnabled(p1_enabled, !p2_enabled));
ui.close_menu();
}
});
ui.menu_button("Input", |ui| {
if ui.button("Bind Inputs").clicked() {
self.open(ChildWindow::Input);
self.proxy.send_event(UserEvent::OpenInput).unwrap();
ui.close_menu();
}
});
if ui.button("Hotkeys").clicked() {
self.open(ChildWindow::Hotkeys);
if ui.button("Key Shortcuts").clicked() {
self.proxy.send_event(UserEvent::OpenShortcuts).unwrap();
ui.close_menu();
}
}
@ -604,16 +393,18 @@ impl GameWindow {
}
}
fn update_config(&mut self, update: impl FnOnce(&mut SimConfig)) {
fn update_config(&mut self, update: impl FnOnce(&mut GameConfig)) {
let mut new_config = self.config.clone();
update(&mut new_config);
if self.config != new_config {
let _ = new_config.save(&self.persistence, self.sim_id);
let _ = self
.persistence
.save_config(config_filename(self.sim_id), &new_config);
}
self.config = new_config;
}
fn button_for(&self, ctx: &Context, text: &str, command: Command) -> Button<'_> {
fn button_for(&self, ctx: &Context, text: &str, command: Command) -> Button {
let button = Button::new(text);
match self.shortcuts.shortcut_for(command) {
Some(shortcut) => button.shortcut_text(ctx.format_shortcut(&shortcut)),
@ -622,42 +413,62 @@ impl GameWindow {
}
}
fn config_filename(sim_id: SimId) -> &'static str {
match sim_id {
SimId::Player1 => "config_p1",
SimId::Player2 => "config_p2",
}
}
fn load_config(persistence: &Persistence, sim_id: SimId) -> GameConfig {
if let Ok(config) = persistence.load_config(config_filename(sim_id)) {
return config;
}
GameConfig {
display_mode: DisplayMode::Anaglyph,
colors: COLOR_PRESETS[0],
dimensions: DisplayMode::Anaglyph.proportions() + Vec2::new(0.0, 22.0),
}
}
impl AppWindow for GameWindow {
fn viewport_id(&self) -> ViewportId {
match self.sim_id {
SimId::Player1 => ViewportId::ROOT,
SimId::Player2 => ViewportId::from_hash_of("Player2"),
}
}
fn sim_id(&self) -> SimId {
self.sim_id
}
fn initial_viewport(&self) -> ViewportBuilder {
let builder = ViewportBuilder::default()
ViewportBuilder::default()
.with_title("Lemur")
.with_inner_size(self.config.dimensions);
if let Some(position) = self.config.position {
builder.with_position(position)
} else {
builder
}
.with_inner_size(self.config.dimensions)
}
fn show(&mut self, ui: &mut Ui) {
self.child_states.load(ui);
fn show(&mut self, ctx: &Context) {
let dimensions = {
let bounds = ui.content_rect();
let bounds = ctx.available_rect();
bounds.max - bounds.min
};
let position = ui.input(|i| i.viewport().outer_rect.map(|r| r.min));
self.update_config(|c| {
c.dimensions = dimensions;
c.position = position;
});
self.update_config(|c| c.dimensions = dimensions);
while let Ok(toast) = self.messages.try_recv() {
self.toasts.add(toast);
let mut toasts = Toasts::new()
.anchor(Align2::LEFT_BOTTOM, (10.0, 10.0))
.direction(Direction::BottomUp);
if let Some(messages) = self.messages.as_mut() {
while let Ok(toast) = messages.try_recv() {
toasts.add(toast);
}
}
Panel::top("menubar")
.exact_size(22.0)
.show_inside(ui, |ui| {
MenuBar::new().ui(ui, |ui| {
self.show_menu(ui);
TopBottomPanel::top("menubar")
.exact_height(22.0)
.show(ctx, |ui| {
menu::bar(ui, |ui| {
self.show_menu(ctx, ui);
});
});
if self.color_picker.is_some() {
@ -665,68 +476,33 @@ impl AppWindow for GameWindow {
.title_bar(false)
.resizable(false)
.anchor(Align2::CENTER_CENTER, Vec2::ZERO)
.show(ui, |ui| {
.show(ctx, |ui| {
self.show_color_picker(ui);
});
}
let frame = Frame::central_panel(ui.style()).fill(Color32::BLACK);
CentralPanel::default().frame(frame).show_inside(ui, |ui| {
let frame = Frame::central_panel(&ctx.style()).fill(Color32::BLACK);
CentralPanel::default().frame(frame).show(ctx, |ui| {
if let Some(screen) = self.screen.as_mut() {
screen.update(self.config.display_mode, self.config.colors);
ui.add(screen);
}
});
self.toasts.show(ui);
for window in std::mem::take(&mut self.queued_children) {
self.do_open(window);
}
self.children.retain(|id, child| {
if child
.close_requested
.load(std::sync::atomic::Ordering::Relaxed)
{
return false;
}
let app = child.app.clone();
let viewport_builder = child.updates.take().unwrap_or_default();
let close_requested = child.close_requested.clone();
if let Some(rect) = ui.input_for(*id, |inp| inp.viewport().outer_rect) {
self.child_states.insert(
*id,
ChildState {
position: rect.min,
size: rect.size(),
},
);
}
ui.show_viewport_deferred(*id, viewport_builder, move |ui, _| {
app.lock().unwrap().show(ui);
if ui.input(|s| s.viewport().close_requested()) {
close_requested.store(true, std::sync::atomic::Ordering::Relaxed);
}
});
true
});
self.child_states.save(ui);
ui.request_repaint_after(Duration::from_millis(10));
toasts.show(ctx);
}
fn on_init(&mut self, args: InitArgs) {
if self.screen.is_none() {
let (screen, sink) = GameScreen::init(args.render_state);
self.client.send_command(EmulatorCommand::ConnectToSim(
self.sim_id,
sink,
self.message_sink.clone(),
));
self.screen = Some(screen);
}
self.window = Some(args.window.clone());
fn on_init(&mut self, _ctx: &Context, 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(
self.sim_id,
sink,
message_sink,
));
self.screen = Some(screen);
self.messages = Some(message_source);
}
}
impl Drop for GameWindow {
fn drop(&mut self) {
fn on_destroy(&mut self) {
if self.sim_id == SimId::Player2 {
self.client.send_command(EmulatorCommand::StopSecondSim);
}
@ -740,82 +516,9 @@ struct ColorPickerState {
unpause_on_close: bool,
}
struct ChildWindowWrapper {
app: Arc<Mutex<AppWrapper>>,
updates: Option<ViewportBuilder>,
close_requested: Arc<AtomicBool>,
}
enum AppWrapper {
Game(Box<GameWindow>),
Dyn(Box<dyn AppWindow + Send + 'static>),
}
impl AppWrapper {
fn of_game(game: GameWindow) -> Self {
Self::Game(Box::new(game))
}
fn of_dyn<T: AppWindow + Send + 'static>(inner: T) -> Self {
Self::Dyn(Box::new(inner))
}
}
impl Deref for AppWrapper {
type Target = dyn AppWindow + Send + 'static;
fn deref(&self) -> &Self::Target {
match self {
Self::Dyn(inner) => inner.as_ref(),
Self::Game(inner) => inner.as_ref(),
}
}
}
impl DerefMut for AppWrapper {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
Self::Dyn(inner) => inner.as_mut(),
Self::Game(inner) => inner.as_mut(),
}
}
}
#[derive(Debug)]
pub enum ChildWindow {
About,
CharacterData,
BgMap,
Objects,
Worlds,
FrameBuffers,
Registers,
Terminal,
Profiler { launch: bool },
Debugger { port: Option<u16> },
Input,
Hotkeys,
Player2,
}
impl ChildWindow {
fn viewport_id(&self, sim_id: SimId) -> ViewportId {
ViewportId::from_hash_of(format!("{sim_id:?}{}", self.name()))
}
fn name(&self) -> &'static str {
match self {
Self::About => "About",
Self::CharacterData => "CharacterData",
Self::BgMap => "BgMap",
Self::Objects => "Objects",
Self::Worlds => "Worlds",
Self::FrameBuffers => "FrameBuffers",
Self::Registers => "Registers",
Self::Terminal => "Terminal",
Self::Profiler { .. } => "Profiler",
Self::Debugger { .. } => "Debugger",
Self::Input => "Input",
Self::Hotkeys => "Hotkeys",
Self::Player2 => "Player2",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
struct ChildState {
position: Pos2,
size: Vec2,
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
struct GameConfig {
display_mode: DisplayMode,
colors: [Color32; 2],
dimensions: Vec2,
}

View File

@ -14,7 +14,7 @@ pub struct GameScreen {
}
impl GameScreen {
pub fn init_pipeline(render_state: &egui_wgpu::RenderState) {
fn init_pipeline(render_state: &egui_wgpu::RenderState) {
let device = &render_state.device;
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
@ -53,8 +53,8 @@ impl GameScreen {
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("render pipeline layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let create_render_pipeline = |entry_point: &str| {
@ -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,
})],
@ -92,7 +92,7 @@ impl GameScreen {
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
multiview: None,
cache: None,
})
};
@ -117,6 +117,8 @@ impl GameScreen {
}
pub fn init(render_state: &egui_wgpu::RenderState) -> (Self, TextureSink) {
Self::init_pipeline(render_state);
let device = &render_state.device;
let queue = &render_state.queue;

View File

@ -1,4 +1,4 @@
use egui::{Button, CentralPanel, TextEdit, ViewportBuilder};
use egui::{Button, CentralPanel, TextEdit, ViewportBuilder, ViewportId};
use winit::event_loop::EventLoopProxy;
use crate::{
@ -42,6 +42,10 @@ impl GdbServerWindow {
}
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
}
@ -52,10 +56,10 @@ impl AppWindow for GdbServerWindow {
.with_inner_size((300.0, 200.0))
}
fn show(&mut self, ui: &mut egui::Ui) {
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_inside(ui, |ui| {
CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
if port_num.is_none() {
let style = ui.style_mut();

View File

@ -1,145 +0,0 @@
use egui::{
Button, CentralPanel, Event, KeyboardShortcut, Label, Layout, Slider, Ui, ViewportBuilder,
};
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
&& 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 initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Keyboard Shortcuts")
.with_inner_size((400.0, 400.0))
}
fn show(&mut self, ui: &mut Ui) {
CentralPanel::default().show_inside(ui, |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

@ -1,4 +1,6 @@
use egui::{Button, CentralPanel, Label, Layout, Panel, Ui, ViewportBuilder};
use egui::{
Button, CentralPanel, Context, Label, Layout, TopBottomPanel, Ui, ViewportBuilder, ViewportId,
};
use egui_extras::{Column, TableBuilder};
use gilrs::{EventType, GamepadId};
use std::sync::RwLock;
@ -160,14 +162,18 @@ impl InputWindow {
}
impl AppWindow for InputWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of("input")
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Bind Inputs")
.with_inner_size((600.0, 400.0))
}
fn show(&mut self, ui: &mut Ui) {
Panel::top("options").show_inside(ui, |ui| {
fn show(&mut self, ctx: &Context) {
TopBottomPanel::top("options").show(ctx, |ui| {
ui.horizontal(|ui| {
let old_active_tab = self.active_tab;
ui.selectable_value(&mut self.active_tab, InputTab::Player1, "Player 1");
@ -182,7 +188,7 @@ impl AppWindow for InputWindow {
}
});
});
CentralPanel::default().show_inside(ui, |ui| {
CentralPanel::default().show(ctx, |ui| {
match self.active_tab {
InputTab::Player1 => self.show_key_bindings(ui, SimId::Player1),
InputTab::Player2 => self.show_key_bindings(ui, SimId::Player2),

View File

@ -1,169 +0,0 @@
use std::{fs, sync::Arc, time::Duration};
use anyhow::Result;
use egui::{Button, CentralPanel, Checkbox, Label, ViewportBuilder};
use egui_notify::{Anchor, Toast, Toasts};
use winit::window::Window;
use crate::{
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId},
profiler::{Profiler, ProfilerStatus},
window::{AppWindow, InitArgs},
};
pub struct ProfileWindow {
sim_id: SimId,
client: EmulatorClient,
profiler: Profiler,
toasts: Toasts,
window: Option<Arc<Window>>,
}
impl ProfileWindow {
pub fn new(sim_id: SimId, client: EmulatorClient) -> Self {
Self {
sim_id,
client: client.clone(),
profiler: Profiler::new(sim_id, client),
toasts: Toasts::new()
.with_anchor(Anchor::BottomLeft)
.with_margin((10.0, 10.0).into())
.reverse(true),
window: None,
}
}
pub fn launch(&mut self) {
self.profiler.enable();
}
fn finish_recording(&mut self) {
let pause = matches!(self.client.emulator_state(), EmulatorState::Running);
if pause {
self.client.send_command(EmulatorCommand::Pause);
}
match self.try_finish_recording() {
Ok(Some(path)) => {
let mut toast = Toast::info(format!("Saved to {path}"));
toast.duration(Some(Duration::from_secs(5)));
self.toasts.add(toast);
}
Ok(None) => {}
Err(error) => {
let mut toast = Toast::error(format!("{error:#}"));
toast.duration(Some(Duration::from_secs(5)));
self.toasts.add(toast);
}
}
if pause {
self.client.send_command(EmulatorCommand::Resume);
}
}
fn try_finish_recording(&mut self) -> Result<Option<String>> {
let bytes_receiver = self.profiler.finish_recording();
let mut file_dialog = rfd::FileDialog::new()
.add_filter("Profiler files", &["json"])
.set_file_name("profile.json");
if let Some(window) = self.window.as_ref() {
file_dialog = file_dialog.set_parent(window);
}
let file = file_dialog.save_file();
if let Some(path) = file {
let bytes = pollster::block_on(bytes_receiver)?;
let _ = fs::remove_file(&path);
fs::write(&path, bytes)?;
Ok(Some(path.display().to_string()))
} else {
self.profiler.cancel_recording();
Ok(None)
}
}
}
impl AppWindow for ProfileWindow {
fn sim_id(&self) -> SimId {
self.sim_id
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("Profiler ({})", self.sim_id))
.with_inner_size((300.0, 200.0))
}
fn show(&mut self, ui: &mut egui::Ui) {
let status = self.profiler.status();
let recording = matches!(status, ProfilerStatus::Recording);
CentralPanel::default().show_inside(ui, |ui| {
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.add(
Label::new(
"Use this tool to record performance profiles of your game, for use in ",
)
.wrap_mode(egui::TextWrapMode::Wrap),
);
ui.hyperlink("https://profiler.firefox.com");
ui.label(".");
});
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.add(
Label::new("For more instructions, see ").wrap_mode(egui::TextWrapMode::Wrap),
);
ui.hyperlink_to(
"the Lemur wiki",
"https://git.virtual-boy.com/PVB/lemur/wiki/Profiling-with-Lemur",
);
ui.label(".");
});
ui.separator();
let mut enabled = status.enabled();
let enabled_checkbox = Checkbox::new(&mut enabled, "Enable profiling");
if ui.add_enabled(!recording, enabled_checkbox).changed() {
if enabled {
self.profiler.enable();
} else {
self.profiler.disable();
}
}
if !enabled {
ui.label("Enabling profiling will restart your current game.");
} else {
ui.horizontal(|ui| {
if !recording {
let record_button = Button::new("Record");
let can_record = matches!(status, ProfilerStatus::Enabled);
if ui.add_enabled(can_record, record_button).clicked() {
self.profiler.start_recording();
}
} else {
if ui.button("Finish recording").clicked() {
self.finish_recording();
}
if ui.button("Cancel recording").clicked() {
self.profiler.cancel_recording();
}
}
});
}
match &status {
ProfilerStatus::Recording => {
ui.label("Recording...");
}
ProfilerStatus::Error(message) => {
ui.label(message);
}
_ => {}
}
});
self.toasts.show(ui);
}
fn on_init(&mut self, args: InitArgs) {
self.window = Some(args.window.clone());
}
}

103
src/window/shortcuts.rs Normal file
View File

@ -0,0 +1,103 @@
use egui::{
Button, CentralPanel, Context, Event, KeyboardShortcut, Label, Layout, Ui, ViewportBuilder,
ViewportId,
};
use egui_extras::{Column, TableBuilder};
use crate::input::{Command, ShortcutProvider};
use super::AppWindow;
pub struct ShortcutsWindow {
shortcuts: ShortcutProvider,
now_binding: Option<Command>,
}
impl ShortcutsWindow {
pub fn new(shortcuts: ShortcutProvider) -> Self {
Self {
shortcuts,
now_binding: None,
}
}
fn show_shortcuts(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| {
if ui.button("Use defaults").clicked() {
self.shortcuts.reset();
}
});
ui.separator();
let row_height = ui.spacing().interact_size.y;
let width = ui.available_width() - 20.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;
}
}
}
}
impl AppWindow for ShortcutsWindow {
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| {
self.show_shortcuts(ui);
});
}
}

View File

@ -1,72 +0,0 @@
use std::{collections::VecDeque, sync::mpsc};
use egui::{Align, CentralPanel, FontFamily, Label, RichText, ScrollArea, Ui, ViewportBuilder};
use crate::emulator::{EmulatorClient, EmulatorCommand, SimId};
use super::AppWindow;
const SCROLLBACK: usize = 1000;
pub struct TerminalWindow {
sim_id: SimId,
receiver: mpsc::Receiver<String>,
lines: VecDeque<String>,
}
impl TerminalWindow {
pub fn new(sim_id: SimId, client: &EmulatorClient) -> Self {
let (sender, receiver) = mpsc::channel();
client.send_command(EmulatorCommand::WatchStdout(sim_id, sender));
let mut lines = VecDeque::new();
lines.push_back(String::new());
Self {
sim_id,
receiver,
lines,
}
}
}
impl AppWindow for TerminalWindow {
fn sim_id(&self) -> SimId {
self.sim_id
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("Terminal ({})", self.sim_id))
.with_inner_size((640.0, 480.0))
}
fn show(&mut self, ui: &mut Ui) {
if let Ok(text) = self.receiver.try_recv() {
let mut rest = text.as_str();
while let Some(index) = rest.find('\n') {
let (line, lines) = rest.split_at(index);
let current = self.lines.back_mut().unwrap();
current.push_str(line);
self.lines.push_back(String::new());
if self.lines.len() > SCROLLBACK {
self.lines.pop_front();
}
rest = &lines[1..];
}
self.lines.back_mut().unwrap().push_str(rest);
}
CentralPanel::default().show_inside(ui, |ui| {
ScrollArea::vertical()
.stick_to_bottom(true)
.auto_shrink([false, false])
.animated(false)
.show(ui, |ui| {
for line in &self.lines {
let label = Label::new(RichText::new(line).family(FontFamily::Monospace))
.halign(Align::LEFT)
.wrap();
ui.add(label);
}
});
});
}
}

View File

@ -1,14 +1,14 @@
use std::{
fmt::{Display, UpperHex},
ops::{Bound, Deref, DerefMut, RangeBounds},
ops::{Bound, RangeBounds},
str::FromStr,
};
use atoi::FromRadix16;
use egui::{
Align, Color32, CornerRadius, CursorIcon, Event, Frame, Key, Layout, Margin, Rect, Response,
RichText, Sense, Shape, Stroke, StrokeKind, TextEdit, Ui, UiBuilder, Vec2, Widget, WidgetText,
ecolor::HexColor, util::id_type_map::SerializableAny,
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};
@ -37,8 +37,8 @@ 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;
frame.inner_margin.top += 2;
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());
@ -49,7 +49,7 @@ impl UiExt for Ui {
let old_rect = res.response.rect;
let mut text_rect = old_rect;
text_rect.min.x += 6.0;
self.scope_builder(UiBuilder::new().max_rect(text_rect), |ui| ui.label(text));
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);
}
@ -73,8 +73,7 @@ impl UiExt for Ui {
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, StrokeKind::Inside);
self.painter().rect_stroke(rect, 0.0, style.fg_stroke);
response
}
@ -86,10 +85,10 @@ impl UiExt for 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()
&& let Ok(new_color) = HexColor::from_str_without_hash(hex)
{
*color = new_color.color();
if resp.changed() {
if let Ok(new_color) = HexColor::from_str_without_hash(hex) {
*color = new_color.color();
}
}
resp
},
@ -266,10 +265,10 @@ impl<T: Number> Widget for NumberEdit<'_, T> {
.id(id)
.desired_width(desired_width)
.margin(Margin {
left: 4,
right: if self.arrows { 20 } else { 4 },
top: 2,
bottom: 2,
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)
@ -301,11 +300,11 @@ impl<T: Number> Widget for NumberEdit<'_, T> {
let mut delta = None;
if self.arrows {
let arrow_left = res.rect.max.x - 16.0;
let arrow_right = res.rect.max.x;
let arrow_top = res.rect.min.y;
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;
let arrow_bottom = res.rect.max.y + 2.0;
let top_arrow_rect = Rect {
min: (arrow_left, arrow_top).into(),
@ -338,7 +337,7 @@ impl<T: Number> Widget for NumberEdit<'_, T> {
}
str = to_string(self.value);
stale = true;
} else if res.changed() {
} else if res.changed {
if let Some(new_value) = from_string(&str).filter(in_range) {
if *self.value != new_value {
res.mark_changed();
@ -356,20 +355,27 @@ impl<T: Number> Widget for NumberEdit<'_, T> {
fn draw_arrow(ui: &mut Ui, rect: Rect, up: bool) -> Response {
let arrow_res = ui
.allocate_rect(rect, Sense::all())
.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 {
CornerRadius {
ne: 2,
..CornerRadius::ZERO
Rounding {
ne: 2.0,
..Rounding::ZERO
}
} else {
CornerRadius {
se: 2,
..CornerRadius::ZERO
Rounding {
se: 2.0,
..Rounding::ZERO
}
};
painter.rect_filled(arrow_res.rect, rounding, visuals.bg_fill);
@ -409,51 +415,3 @@ impl ResponseExt for Response {
self.clicked() || self.dragged()
}
}
pub struct UiData<T> {
current: T,
prev: T,
loaded: bool,
}
impl<T> UiData<T>
where
T: Default + PartialEq + SerializableAny,
{
pub fn new() -> Self {
Self {
current: T::default(),
prev: T::default(),
loaded: false,
}
}
pub fn load(&mut self, ui: &Ui) {
if !self.loaded {
self.current = ui
.data_mut(|d| d.get_persisted(ui.id()))
.unwrap_or_default();
self.loaded = true;
}
}
pub fn save(&mut self, ui: &Ui) {
if self.current != self.prev {
ui.data_mut(|d| d.insert_persisted(ui.id(), self.current.clone()));
self.prev = self.current.clone();
}
}
}
impl<T> Deref for UiData<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.current
}
}
impl<T> DerefMut for UiData<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.current
}
}

View File

@ -1,68 +1,51 @@
use std::sync::Arc;
use egui::{
Align, CentralPanel, Checkbox, Color32, ComboBox, Image, ScrollArea, Slider, TextEdit,
TextureOptions, Ui, ViewportBuilder,
Align, CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextEdit,
TextureOptions, Ui, ViewportBuilder, ViewportId,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use serde::{Deserialize, Serialize};
use crate::{
emulator::SimId,
images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader},
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView},
window::{
AppWindow,
utils::{NumberEdit, UiData, UiExt},
utils::{NumberEdit, UiExt},
},
};
use super::utils::{self, CellData, CharacterGrid};
#[derive(Clone, PartialEq, Serialize, Deserialize)]
struct State {
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 Default for State {
fn default() -> Self {
Self {
cell_index: 0,
generic_palette: false,
scale: 1.0,
show_grid: false,
}
}
}
pub struct BgMapWindow {
sim_id: SimId,
memory: Arc<MemoryClient>,
bgmaps: MemoryView,
params: ImageParams<BgMapParams>,
state: UiData<State>,
}
impl BgMapWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &ImageTextureLoader) -> Self {
let state: UiData<State> = UiData::new();
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &mut ImageProcessor) -> Self {
let renderer = BgMapRenderer::new(sim_id, memory);
let params = images.add(
sim_id,
renderer,
BgMapParams {
cell_index: state.cell_index,
generic_palette: state.generic_palette,
},
);
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,
state,
scale: 1.0,
show_grid: false,
}
}
@ -78,11 +61,10 @@ impl BgMapWindow {
ui.label("Map");
});
row.col(|ui| {
let mut bgmap_index = self.state.cell_index / 4096;
let mut bgmap_index = self.cell_index / 4096;
ui.add(NumberEdit::new(&mut bgmap_index).range(0..16));
if bgmap_index != self.state.cell_index / 4096 {
self.state.cell_index =
(bgmap_index * 4096) + (self.state.cell_index % 4096);
if bgmap_index != self.cell_index / 4096 {
self.cell_index = (bgmap_index * 4096) + (self.cell_index % 4096);
}
});
});
@ -91,7 +73,7 @@ impl BgMapWindow {
ui.label("Cell");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut self.state.cell_index).range(0..16 * 4096));
ui.add(NumberEdit::new(&mut self.cell_index).range(0..16 * 4096));
});
});
body.row(row_height, |mut row| {
@ -99,7 +81,7 @@ impl BgMapWindow {
ui.label("Address");
});
row.col(|ui| {
let address = 0x00020000 + (self.state.cell_index * 2);
let address = 0x00020000 + (self.cell_index * 2);
let mut address_str = format!("{address:08x}");
ui.add_enabled(
false,
@ -108,12 +90,12 @@ impl BgMapWindow {
});
});
});
let image = Image::new(self.image_url("bgmap-cell"))
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.state.cell_index);
let mut data = self.bgmaps.borrow().read::<u16>(self.cell_index);
let mut cell = CellData::parse(data);
TableBuilder::new(ui)
.column(Column::remainder())
@ -156,7 +138,7 @@ impl BgMapWindow {
});
});
if cell.update(&mut data) {
let address = 0x00020000 + (self.state.cell_index * 2);
let address = 0x00020000 + (self.cell_index * 2);
self.memory.write(self.sim_id, address as u32, &data);
}
});
@ -164,33 +146,37 @@ impl BgMapWindow {
ui.horizontal(|ui| {
ui.label("Scale");
ui.spacing_mut().slider_width = ui.available_width();
let slider = Slider::new(&mut self.state.scale, 1.0..=10.0)
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.state.show_grid, "Show grid");
ui.checkbox(&mut self.state.generic_palette, "Generic palette");
ui.checkbox(&mut self.show_grid, "Show grid");
ui.checkbox(&mut self.generic_palette, "Generic palette");
});
});
self.params.write(BgMapParams {
cell_index: self.state.cell_index,
generic_palette: self.state.generic_palette,
cell_index: self.cell_index,
generic_palette: self.generic_palette,
});
}
fn show_bgmap(&mut self, ui: &mut Ui) {
let grid = CharacterGrid::new(self.image_url("bgmap"))
.with_scale(self.state.scale)
.with_grid(self.state.show_grid)
.with_selected(self.state.cell_index % 4096);
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.state.cell_index = (self.state.cell_index / 4096 * 4096) + selected;
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
}
@ -201,9 +187,12 @@ impl AppWindow for BgMapWindow {
.with_inner_size((640.0, 480.0))
}
fn show(&mut self, ui: &mut Ui) {
self.state.load(ui);
CentralPanel::default().show_inside(ui, |ui| {
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))
@ -218,7 +207,6 @@ impl AppWindow for BgMapWindow {
})
});
});
self.state.save(ui);
}
}
@ -312,10 +300,6 @@ impl BgMapRenderer {
impl ImageRenderer<2> for BgMapRenderer {
type Params = BgMapParams;
fn names(&self) -> [&str; 2] {
["bgmap-cell", "bgmap"]
}
fn sizes(&self) -> [[usize; 2]; 2] {
[[8, 8], [8 * 64, 8 * 64]]
}

View File

@ -1,19 +1,19 @@
use std::fmt::Display;
use std::{fmt::Display, sync::Arc};
use egui::{
Align, CentralPanel, Color32, ComboBox, Image, ScrollArea, Slider, TextEdit, TextureOptions,
Ui, Vec2, ViewportBuilder,
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, ImageRenderer, ImageTextureLoader},
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView},
window::{
AppWindow,
utils::{NumberEdit, UiData, UiExt as _},
utils::{NumberEdit, UiExt as _},
},
};
@ -79,50 +79,36 @@ impl Display for Palette {
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
struct State {
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 Default for State {
fn default() -> Self {
Self {
palette: Palette::default(),
index: 0,
scale: 4.0,
show_grid: true,
}
}
}
pub struct CharacterDataWindow {
sim_id: SimId,
brightness: MemoryView,
palettes: MemoryView,
params: ImageParams<CharDataParams>,
state: UiData<State>,
}
impl CharacterDataWindow {
pub fn new(sim_id: SimId, memory: &MemoryClient, images: &ImageTextureLoader) -> Self {
let state: UiData<State> = UiData::new();
pub fn new(sim_id: SimId, memory: &MemoryClient, images: &mut ImageProcessor) -> Self {
let renderer = CharDataRenderer::new(sim_id, memory);
let params = images.add(
sim_id,
renderer,
CharDataParams {
palette: state.palette,
index: state.index,
},
);
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,
state,
scale: 4.0,
show_grid: true,
}
}
@ -138,7 +124,7 @@ impl CharacterDataWindow {
ui.label("Index");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut self.state.index).range(0..2048));
ui.add(NumberEdit::new(&mut self.index).range(0..2048));
});
});
body.row(row_height, |mut row| {
@ -146,11 +132,11 @@ impl CharacterDataWindow {
ui.label("Address");
});
row.col(|ui| {
let address = match self.state.index {
0x000..0x200 => 0x00060000 + self.state.index * 16,
0x200..0x400 => 0x000e0000 + (self.state.index - 0x200) * 16,
0x400..0x600 => 0x00160000 + (self.state.index - 0x400) * 16,
0x600..0x800 => 0x001e0000 + (self.state.index - 0x600) * 16,
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}");
@ -165,7 +151,7 @@ impl CharacterDataWindow {
ui.label("Mirror");
});
row.col(|ui| {
let mirror = 0x00078000 + (self.state.index * 16);
let mirror = 0x00078000 + (self.index * 16);
let mut mirror_str = format!("{mirror:08x}");
ui.add_enabled(
false,
@ -174,7 +160,7 @@ impl CharacterDataWindow {
});
});
});
let image = Image::new(self.image_url("chardata-char"))
let image = Image::new("vip://char")
.maintain_aspect_ratio(true)
.texture_options(TextureOptions::NEAREST);
ui.add(image);
@ -182,12 +168,12 @@ impl CharacterDataWindow {
ui.horizontal(|ui| {
ui.label("Palette");
ComboBox::from_id_salt("palette")
.selected_text(self.state.palette.to_string())
.selected_text(self.palette.to_string())
.width(ui.available_width())
.show_ui(ui, |ui| {
for palette in Palette::values() {
ui.selectable_value(
&mut self.state.palette,
&mut self.palette,
palette,
palette.to_string(),
);
@ -214,23 +200,23 @@ impl CharacterDataWindow {
ui.horizontal(|ui| {
ui.label("Scale");
ui.spacing_mut().slider_width = ui.available_width();
let slider = Slider::new(&mut self.state.scale, 1.0..=10.0)
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.state.show_grid, "Show grid");
ui.checkbox(&mut self.show_grid, "Show grid");
});
});
self.params.write(CharDataParams {
palette: self.state.palette,
index: self.state.index,
palette: self.palette,
index: self.index,
});
}
fn load_palette_colors(&self) -> [Color32; 4] {
let Some(offset) = self.state.palette.offset() else {
let Some(offset) = self.palette.offset() else {
return utils::generic_palette(Color32::RED);
};
let palette = self.palettes.borrow().read(offset);
@ -240,17 +226,21 @@ impl CharacterDataWindow {
}
fn show_chardata(&mut self, ui: &mut Ui) {
let grid = CharacterGrid::new(self.image_url("chardata"))
.with_scale(self.state.scale)
.with_grid(self.state.show_grid)
.with_selected(self.state.index);
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.state.index = selected;
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
}
@ -261,9 +251,12 @@ impl AppWindow for CharacterDataWindow {
.with_inner_size((640.0, 480.0))
}
fn show(&mut self, ui: &mut Ui) {
self.state.load(ui);
CentralPanel::default().show_inside(ui, |ui| {
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))
@ -278,11 +271,16 @@ impl AppWindow for CharacterDataWindow {
});
});
});
self.state.save(ui);
}
}
#[derive(Clone, PartialEq, Eq)]
#[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,
@ -297,10 +295,6 @@ struct CharDataRenderer {
impl ImageRenderer<2> for CharDataRenderer {
type Params = CharDataParams;
fn names(&self) -> [&str; 2] {
["chardata-char", "chardata"]
}
fn sizes(&self) -> [[usize; 2]; 2] {
[[8, 8], [16 * 8, 128 * 8]]
}

View File

@ -1,68 +1,56 @@
use std::sync::Arc;
use egui::{
Align, CentralPanel, Color32, Image, ScrollArea, Slider, TextEdit, TextureOptions, Ui,
ViewportBuilder,
Align, CentralPanel, Color32, Context, Image, ScrollArea, Slider, TextEdit, TextureOptions, Ui,
ViewportBuilder, ViewportId,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use serde::{Deserialize, Serialize};
use crate::{
emulator::SimId,
images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader},
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView},
window::{
AppWindow,
utils::{NumberEdit, UiData, UiExt as _},
utils::{NumberEdit, UiExt as _},
},
};
use super::utils;
#[derive(Clone, PartialEq, Serialize, Deserialize)]
struct State {
pub struct FrameBufferWindow {
sim_id: SimId,
loader: Arc<ImageTextureLoader>,
index: usize,
left: bool,
right: bool,
generic_palette: bool,
params: ImageParams<FrameBufferParams>,
scale: f32,
}
impl Default for State {
fn default() -> Self {
Self {
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,
scale: 2.0,
}
}
}
pub struct FrameBufferWindow {
sim_id: SimId,
params: ImageParams<FrameBufferParams>,
state: UiData<State>,
}
impl FrameBufferWindow {
pub fn new(sim_id: SimId, memory: &MemoryClient, images: &ImageTextureLoader) -> Self {
let state: UiData<State> = UiData::new();
left_color: Color32::from_rgb(0xff, 0x00, 0x00),
right_color: Color32::from_rgb(0x00, 0xc6, 0xf0),
};
let renderer = FrameBufferRenderer::new(sim_id, memory);
let params = images.add(
sim_id,
renderer,
FrameBufferParams {
index: state.index,
left: state.left,
right: state.right,
generic_palette: state.generic_palette,
left_color: Color32::from_rgb(0xff, 0x00, 0x00),
right_color: Color32::from_rgb(0x00, 0xc6, 0xf0),
},
);
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,
state,
scale: 2.0,
}
}
@ -78,7 +66,7 @@ impl FrameBufferWindow {
ui.label("Index");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut self.state.index).range(0..2));
ui.add(NumberEdit::new(&mut self.index).range(0..2));
});
});
body.row(row_height, |mut row| {
@ -86,7 +74,7 @@ impl FrameBufferWindow {
ui.label("Left");
});
row.col(|ui| {
let address = self.state.index * 0x00008000;
let address = self.index * 0x00008000;
let mut address_str = format!("{address:08x}");
ui.add_enabled(
false,
@ -99,7 +87,7 @@ impl FrameBufferWindow {
ui.label("Right");
});
row.col(|ui| {
let address = self.state.index * 0x00008000 + 0x00010000;
let address = self.index * 0x00008000 + 0x00010000;
let mut address_str = format!("{address:08x}");
ui.add_enabled(
false,
@ -112,7 +100,7 @@ impl FrameBufferWindow {
ui.horizontal(|ui| {
ui.label("Scale");
ui.spacing_mut().slider_width = ui.available_width();
let slider = Slider::new(&mut self.state.scale, 1.0..=10.0)
let slider = Slider::new(&mut self.scale, 1.0..=10.0)
.step_by(1.0)
.show_value(false);
ui.add(slider);
@ -122,35 +110,39 @@ impl FrameBufferWindow {
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.checkbox(&mut self.state.left, "Left");
ui.checkbox(&mut self.left, "Left");
});
row.col(|ui| {
ui.checkbox(&mut self.state.right, "Right");
ui.checkbox(&mut self.right, "Right");
});
});
});
ui.checkbox(&mut self.state.generic_palette, "Generic colors");
ui.checkbox(&mut self.generic_palette, "Generic colors");
});
});
self.params.write(FrameBufferParams {
index: self.state.index,
left: self.state.left,
right: self.state.right,
generic_palette: self.state.generic_palette,
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(self.image_url("buffer"))
.fit_to_original_size(self.state.scale)
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
}
@ -161,9 +153,12 @@ impl AppWindow for FrameBufferWindow {
.with_inner_size((640.0, 480.0))
}
fn show(&mut self, ui: &mut Ui) {
self.state.load(ui);
CentralPanel::default().show_inside(ui, |ui| {
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))
@ -178,7 +173,6 @@ impl AppWindow for FrameBufferWindow {
});
});
});
self.state.save(ui);
}
}
@ -214,10 +208,6 @@ impl FrameBufferRenderer {
impl ImageRenderer<1> for FrameBufferRenderer {
type Params = FrameBufferParams;
fn names(&self) -> [&str; 1] {
["buffer"]
}
fn sizes(&self) -> [[usize; 2]; 1] {
[[384, 224]]
}

View File

@ -1,68 +1,55 @@
use std::sync::Arc;
use egui::{
Align, CentralPanel, Checkbox, Color32, ComboBox, Image, ScrollArea, Slider, TextEdit,
TextureOptions, Ui, ViewportBuilder,
Align, CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextEdit,
TextureOptions, Ui, ViewportBuilder, ViewportId,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use serde::{Deserialize, Serialize};
use crate::{
emulator::SimId,
images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader},
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView},
window::{
AppWindow,
utils::{NumberEdit, UiData, UiExt as _},
utils::{NumberEdit, UiExt as _},
},
};
use super::utils::{self, Object};
#[derive(Clone, PartialEq, Serialize, Deserialize)]
struct State {
index: usize,
generic_palette: bool,
scale: f32,
}
impl Default for State {
fn default() -> Self {
Self {
index: 0,
generic_palette: false,
scale: 1.0,
}
}
}
pub struct ObjectWindow {
sim_id: SimId,
loader: Arc<ImageTextureLoader>,
memory: Arc<MemoryClient>,
objects: MemoryView,
index: usize,
generic_palette: bool,
params: ImageParams<ObjectParams>,
state: UiData<State>,
scale: f32,
}
impl ObjectWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &ImageTextureLoader) -> Self {
let state: UiData<State> = UiData::new();
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 params = images.add(
sim_id,
renderer,
ObjectParams {
index: state.index,
generic_palette: state.generic_palette,
left_color: Color32::from_rgb(0xff, 0x00, 0x00),
right_color: Color32::from_rgb(0x00, 0xc6, 0xf0),
},
);
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,
state,
scale: 1.0,
}
}
@ -78,7 +65,7 @@ impl ObjectWindow {
ui.label("Index");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut self.state.index).range(0..1024));
ui.add(NumberEdit::new(&mut self.index).range(0..1024));
});
});
body.row(row_height, |mut row| {
@ -86,7 +73,7 @@ impl ObjectWindow {
ui.label("Address");
});
row.col(|ui| {
let address = 0x3e000 + self.state.index * 8;
let address = 0x3e000 + self.index * 8;
let mut address_str = format!("{address:08x}");
ui.add_enabled(
false,
@ -95,12 +82,12 @@ impl ObjectWindow {
});
});
});
let image = Image::new(self.image_url("object-zoom"))
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.state.index);
let mut object = self.objects.borrow().read::<[u16; 4]>(self.index);
let mut obj = Object::parse(object);
TableBuilder::new(ui)
.column(Column::remainder())
@ -175,7 +162,7 @@ impl ObjectWindow {
});
});
if obj.update(&mut object) {
let address = 0x3e000 + self.state.index * 8;
let address = 0x3e000 + self.index * 8;
self.memory.write(self.sim_id, address as u32, &object);
}
});
@ -183,30 +170,34 @@ impl ObjectWindow {
ui.horizontal(|ui| {
ui.label("Scale");
ui.spacing_mut().slider_width = ui.available_width();
let slider = Slider::new(&mut self.state.scale, 1.0..=10.0)
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.state.generic_palette, "Generic palette");
ui.checkbox(&mut self.generic_palette, "Generic palette");
});
});
self.params.write(ObjectParams {
index: self.state.index,
generic_palette: self.state.generic_palette,
index: self.index,
generic_palette: self.generic_palette,
..*self.params
});
}
fn show_object(&mut self, ui: &mut Ui) {
let image = Image::new(self.image_url("object-full"))
.fit_to_original_size(self.state.scale)
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
}
@ -217,9 +208,12 @@ impl AppWindow for ObjectWindow {
.with_inner_size((640.0, 500.0))
}
fn show(&mut self, ui: &mut Ui) {
self.state.load(ui);
CentralPanel::default().show_inside(ui, |ui| {
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))
@ -234,7 +228,6 @@ impl AppWindow for ObjectWindow {
});
});
});
self.state.save(ui);
}
}
@ -334,10 +327,6 @@ impl ObjectRenderer {
impl ImageRenderer<2> for ObjectRenderer {
type Params = ObjectParams;
fn names(&self) -> [&str; 2] {
["object-zoom", "object-full"]
}
fn sizes(&self) -> [[usize; 2]; 2] {
[[8, 8], [384, 224]]
}

View File

@ -1,8 +1,8 @@
use std::sync::Arc;
use egui::{
Align, Button, CentralPanel, Checkbox, Color32, Direction, Label, Layout, ScrollArea, TextEdit,
Ui, ViewportBuilder,
Align, Button, CentralPanel, Checkbox, Color32, Context, Direction, Label, Layout, ScrollArea,
TextEdit, Ui, ViewportBuilder, ViewportId,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
@ -630,6 +630,10 @@ fn read_address<T: MemoryValue>(registers: &MemoryRef, address: usize) -> T {
}
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
}
@ -640,8 +644,8 @@ impl AppWindow for RegisterWindow {
.with_inner_size((800.0, 480.0))
}
fn show(&mut self, ui: &mut Ui) {
CentralPanel::default().show_inside(ui, |ui| {
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();

View File

@ -1,8 +1,8 @@
use std::{fmt::Display, sync::Arc};
use egui::{
Align, CentralPanel, Checkbox, Color32, ComboBox, Image, ScrollArea, Slider, TextEdit,
TextureOptions, Ui, ViewportBuilder,
Align, CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextEdit,
TextureOptions, Ui, ViewportBuilder, ViewportId,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use fixed::{
@ -11,70 +11,56 @@ use fixed::{
};
use num_derive::{FromPrimitive, ToPrimitive};
use num_traits::{FromPrimitive, ToPrimitive};
use serde::{Deserialize, Serialize};
use crate::{
emulator::SimId,
images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader},
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryRef, MemoryView},
window::{
AppWindow,
utils::{NumberEdit, UiData, UiExt as _},
utils::{NumberEdit, UiExt as _},
},
};
use super::utils::{self, CellData, Object, shade};
#[derive(Clone, PartialEq, Serialize, Deserialize)]
struct State {
pub struct WorldWindow {
sim_id: SimId,
loader: Arc<ImageTextureLoader>,
memory: Arc<MemoryClient>,
worlds: MemoryView,
bgmaps: MemoryView,
index: usize,
param_index: usize,
generic_palette: bool,
show_extents: bool,
scale: f32,
}
impl Default for State {
fn default() -> Self {
Self {
index: 31,
param_index: 0,
generic_palette: false,
show_extents: false,
scale: 1.0,
}
}
}
pub struct WorldWindow {
sim_id: SimId,
memory: Arc<MemoryClient>,
worlds: MemoryView,
bgmaps: MemoryView,
params: ImageParams<WorldParams>,
state: UiData<State>,
scale: f32,
}
impl WorldWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &ImageTextureLoader) -> Self {
let state: UiData<State> = UiData::new();
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &mut ImageProcessor) -> Self {
let initial_params = WorldParams {
index: 31,
generic_palette: false,
left_color: Color32::from_rgb(0xff, 0x00, 0x00),
right_color: Color32::from_rgb(0x00, 0xc6, 0xf0),
};
let renderer = WorldRenderer::new(sim_id, memory);
let params = images.add(
sim_id,
renderer,
WorldParams {
index: state.index,
generic_palette: state.generic_palette,
left_color: Color32::from_rgb(0xff, 0x00, 0x00),
right_color: Color32::from_rgb(0x00, 0xc6, 0xf0),
},
);
let ([world], params) = images.add(renderer, initial_params);
let loader = ImageTextureLoader::new([("vip://world".into(), world)]);
Self {
sim_id,
loader: Arc::new(loader),
memory: memory.clone(),
worlds: memory.watch(sim_id, 0x0003d800, 0x400),
bgmaps: memory.watch(sim_id, 0x00020000, 0x20000),
index: params.index,
param_index: 0,
generic_palette: params.generic_palette,
show_extents: false,
params,
state,
scale: 1.0,
}
}
@ -90,7 +76,7 @@ impl WorldWindow {
ui.label("Index");
});
row.col(|ui| {
ui.add(NumberEdit::new(&mut self.state.index).range(0..32));
ui.add(NumberEdit::new(&mut self.index).range(0..32));
});
});
body.row(row_height, |mut row| {
@ -98,7 +84,7 @@ impl WorldWindow {
ui.label("Address");
});
row.col(|ui| {
let address = 0x0003d800 + self.state.index * 32;
let address = 0x0003d800 + self.index * 32;
let mut address_str = format!("{address:08x}");
ui.add_enabled(
false,
@ -109,7 +95,7 @@ impl WorldWindow {
});
let mut data = {
let worlds = self.worlds.borrow();
worlds.read(self.state.index)
worlds.read(self.index)
};
let mut world = World::parse(&data);
ui.section("Properties", |ui| {
@ -289,7 +275,7 @@ impl WorldWindow {
});
});
if world.update(&mut data) {
let address = 0x0003d800 + self.state.index * 32;
let address = 0x0003d800 + self.index * 32;
self.memory.write(self.sim_id, address as u32, &data);
}
if world.header.mode == WorldMode::HBias {
@ -304,12 +290,10 @@ impl WorldWindow {
});
row.col(|ui| {
let max = world.height.max(8) as usize;
ui.add(
NumberEdit::new(&mut self.state.param_index).range(0..max),
);
ui.add(NumberEdit::new(&mut self.param_index).range(0..max));
});
});
let base = (world.param_base + self.state.param_index * 2) & 0x1ffff;
let base = (world.param_base + self.param_index * 2) & 0x1ffff;
let mut param = HBiasParam::load(&self.bgmaps.borrow(), base);
body.row(row_height, |mut row| {
row.col(|ui| {
@ -356,12 +340,10 @@ impl WorldWindow {
});
row.col(|ui| {
let max = world.height.max(1) as usize;
ui.add(
NumberEdit::new(&mut self.state.param_index).range(0..max),
);
ui.add(NumberEdit::new(&mut self.param_index).range(0..max));
});
});
let base = (world.param_base + self.state.param_index * 8) & 0x1ffff;
let base = (world.param_base + self.param_index * 8) & 0x1ffff;
let mut param = AffineParam::load(&self.bgmaps.borrow(), base);
body.row(row_height, |mut row| {
row.col(|ui| {
@ -421,49 +403,49 @@ impl WorldWindow {
});
});
} else {
self.state.param_index = 0;
self.param_index = 0;
}
ui.section("Display", |ui| {
ui.horizontal(|ui| {
ui.label("Scale");
ui.spacing_mut().slider_width = ui.available_width();
let slider = Slider::new(&mut self.state.scale, 1.0..=10.0)
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.state.generic_palette, "Generic palette");
ui.checkbox(&mut self.state.show_extents, "Show extents");
ui.checkbox(&mut self.generic_palette, "Generic palette");
ui.checkbox(&mut self.show_extents, "Show extents");
});
});
self.params.write(WorldParams {
index: self.state.index,
generic_palette: self.state.generic_palette,
index: self.index,
generic_palette: self.generic_palette,
..*self.params
});
}
fn show_world(&mut self, ui: &mut Ui) {
let image = Image::new(self.image_url("world"))
.fit_to_original_size(self.state.scale)
let image = Image::new("vip://world")
.fit_to_original_size(self.scale)
.texture_options(TextureOptions::NEAREST);
let res = ui.add(image);
if self.state.show_extents {
if self.show_extents {
let world = {
let worlds = self.worlds.borrow();
let data = worlds.read(self.state.index);
let data = worlds.read(self.index);
World::parse(&data)
};
if world.header.mode == WorldMode::Object {
return;
}
let lx1 = (world.dst_x - world.dst_parallax) as f32 * self.state.scale;
let lx2 = lx1 + world.width as f32 * self.state.scale;
let rx1 = (world.dst_x + world.dst_parallax) as f32 * self.state.scale;
let rx2 = rx1 + world.width as f32 * self.state.scale;
let y1 = world.dst_y as f32 * self.state.scale;
let y2 = y1 + world.height as f32 * self.state.scale;
let lx1 = (world.dst_x - world.dst_parallax) as f32 * self.scale;
let lx2 = lx1 + world.width as f32 * self.scale;
let rx1 = (world.dst_x + world.dst_parallax) as f32 * self.scale;
let rx2 = rx1 + world.width as f32 * self.scale;
let y1 = world.dst_y as f32 * self.scale;
let y2 = y1 + world.height as f32 * self.scale;
let left_color = self.params.left_color;
let right_color = self.params.right_color;
@ -527,6 +509,10 @@ impl WorldWindow {
}
impl AppWindow for WorldWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of(format!("world-{}", self.sim_id))
}
fn sim_id(&self) -> SimId {
self.sim_id
}
@ -537,9 +523,12 @@ impl AppWindow for WorldWindow {
.with_inner_size((640.0, 520.0))
}
fn show(&mut self, ui: &mut Ui) {
self.state.load(ui);
CentralPanel::default().show_inside(ui, |ui| {
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))
@ -554,7 +543,6 @@ impl AppWindow for WorldWindow {
});
});
});
self.state.save(ui);
}
}
@ -791,10 +779,6 @@ impl WorldRenderer {
impl ImageRenderer<1> for WorldRenderer {
type Params = WorldParams;
fn names(&self) -> [&str; 1] {
["world"]
}
fn sizes(&self) -> [[usize; 2]; 1] {
[[384, 224]]
}