Compare commits

..

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

36 changed files with 2618 additions and 3634 deletions

3270
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,54 +4,52 @@ description = "An emulator for the Virtual Boy."
repository = "https://git.virtual-boy.com/PVB/lemur" repository = "https://git.virtual-boy.com/PVB/lemur"
publish = false publish = false
license = "MIT" license = "MIT"
version = "0.12.2" version = "0.7.3"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
atoi = "2" atoi = "2"
audioadapter-buffers = "3"
atomic = "0.6" atomic = "0.6"
bitflags = { version = "2", features = ["serde"] } bitflags = { version = "2", features = ["serde"] }
bytemuck = { version = "1", features = ["derive"] } bytemuck = { version = "1", features = ["derive"] }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
cpal = { git = "https://github.com/sidit77/cpal.git", rev = "66ed6be" } cpal = { git = "https://github.com/sidit77/cpal.git", rev = "66ed6be" }
directories = "6" directories = "6"
egui = { version = "0.34", features = ["persistence", "serde"] } egui = { version = "0.32", features = ["serde"] }
egui_extras = { version = "0.34", features = ["image"] } egui_extras = { version = "0.32", features = ["image"] }
egui-notify = "0.22" egui-notify = "0.20"
egui-winit = "0.34" egui-winit = "0.32"
egui-wgpu = { version = "0.34", features = ["winit"] } egui-wgpu = { version = "0.32", features = ["winit"] }
fxprof-processed-profile = "0.8" fxprof-processed-profile = "0.8"
fixed = { version = "1.28", features = ["num-traits"] } fixed = { version = "1.28", features = ["num-traits"] }
gilrs = { version = "0.11", features = ["serde-serialize"] } gilrs = { version = "0.11", features = ["serde-serialize"] }
gimli = "0.33" gimli = "0.32"
hex = "0.4" hex = "0.4"
image = { version = "0.25", default-features = false, features = ["png"] } image = { version = "0.25", default-features = false, features = ["png"] }
itertools = "0.14" itertools = "0.14"
normpath = "1" normpath = "1"
notify = "8"
num-derive = "0.4" num-derive = "0.4"
num-traits = "0.2" num-traits = "0.2"
object = "0.39" object = "0.37"
oneshot = { version = "0.2", features = ["async", "std"] } oneshot = "0.1"
pollster = "0.4" pollster = "0.4"
rand = "0.10" rand = "0.9"
rfd = "0.17" rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "async-std"]}
rtrb = "0.3" rtrb = "0.3"
rubato = "2" rubato = "0.16"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
thread-priority = "3" thread-priority = "2"
tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync", "time"] } tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync", "time"] }
tracing = { version = "0.1", features = ["release_max_level_info"] } tracing = { version = "0.1", features = ["release_max_level_info"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
wgpu = "29" wgpu = "25"
wholesym = "0.8" wholesym = "0.8"
winit = { version = "0.30", features = ["serde"] } winit = { version = "0.30", features = ["serde"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows = { version = "0.62", features = ["Win32_System_Threading"] } windows = { version = "0.61", features = ["Win32_System_Threading"] }
[build-dependencies] [build-dependencies]
cc = "1" cc = "1"

View File

@ -9,35 +9,31 @@ ADD "https://github.com/joseluisq/macosx-sdks/releases/download/14.5/MacOSX14.5.
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y ca-certificates apt-get install -y ca-certificates
COPY llvm.sources /etc/apt/sources.list.d/llvm.sources COPY llvm.sources /etc/apt/sources.list.d/llvm.sources
COPY install-llvm.sh .
RUN apt-get update && \ RUN apt-get update && \
./install-llvm.sh && \ apt-get install -y bash bzip2 clang-20 git lld-20 llvm-20 make patch xz-utils && \
apt-get install -y bash bzip2 git make patch xz-utils && \ ln -s $(which clang-20) /usr/bin/clang && \
ln -s $(which clang-21) /usr/bin/clang && \ ln -s $(which clang++-20) /usr/bin/clang++ && \
ln -s $(which clang++-21) /usr/bin/clang++ && \ ln -s $(which ld64.lld-20) /usr/bin/ld64.lld && \
ln -s $(which ld64.lld-21) /usr/bin/ld64.lld && \
SDK_VERSION=14.5 UNATTENDED=1 ENABLE_COMPILER_RT_INSTALL=1 TARGET_DIR=/osxcross ./build.sh SDK_VERSION=14.5 UNATTENDED=1 ENABLE_COMPILER_RT_INSTALL=1 TARGET_DIR=/osxcross ./build.sh
FROM rust:1.95-bookworm FROM rust:1.89-bookworm
ADD --chmod=644 "https://apt.llvm.org/llvm-snapshot.gpg.key" /etc/apt/trusted.gpg.d/apt.llvm.org.asc 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 llvm.sources /etc/apt/sources.list.d/llvm.sources
COPY install-llvm.sh .
RUN rustup target add x86_64-pc-windows-msvc && \ RUN rustup target add x86_64-pc-windows-msvc && \
rustup target add x86_64-apple-darwin && \ rustup target add x86_64-apple-darwin && \
rustup target add aarch64-apple-darwin && \ rustup target add aarch64-apple-darwin && \
apt-get update && \ apt-get update && \
./install-llvm.sh && \ apt-get install -y clang-20 lld-20 libc6-dev libasound2-dev libudev-dev genisoimage mingw-w64 && \
apt-get install -y libc6-dev libasound2-dev libudev-dev genisoimage mingw-w64 && \
cargo install cargo-bundle xwin && \ cargo install cargo-bundle xwin && \
xwin --accept-license splat --output xwin && \ xwin --accept-license splat --output xwin && \
rm -rf .xwin-cache && \ rm -rf .xwin-cache && \
ln -s $(which clang-21) /usr/bin/clang && \ ln -s $(which clang-20) /usr/bin/clang && \
ln -s $(which clang++-21) /usr/bin/clang++ ln -s $(which clang++-20) /usr/bin/clang++
COPY --from=osxcross /osxcross /osxcross COPY --from=osxcross /osxcross /osxcross
ENV PATH="/osxcross/bin:$PATH" \ ENV PATH="/osxcross/bin:$PATH" \
LD_LIBRARY_PATH="/osxcross/lib" \ LD_LIBRARY_PATH="/osxcross/lib" \
CC="clang-21" CXX="clang++-21" AR="llvm-ar-21" \ CC="clang-20" CXX="clang++-20" AR="llvm-ar-20" \
CC_x86_64-apple-darwin="o64-clang" \ CC_x86_64-apple-darwin="o64-clang" \
CXX_x86_64-apple-darwin="o64-clang++" \ CXX_x86_64-apple-darwin="o64-clang++" \
CC_aarch64-apple-darwin="oa64-clang" \ CC_aarch64-apple-darwin="oa64-clang" \
@ -45,13 +41,12 @@ ENV PATH="/osxcross/bin:$PATH" \
SHROOMS_CFLAGS_x86_64-unknown-linux-gnu="-flto" \ 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" \ CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" \
SHROOMS_CFLAGS_x86_64-pc-windows-msvc="-flto" \ SHROOMS_CFLAGS_x86_64-pc-windows-msvc="-flto" \
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_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-21" \ CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link-20" \
CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER="o64-clang" \ CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER="o64-clang" \
CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-21" \ CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-20" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="oa64-clang" \ CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="oa64-clang" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-21" \ CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-20" \
RC_PATH="llvm-rc-21" \ RC_PATH="llvm-rc-20" \
MACOSX_DEPLOYMENT_TARGET="14.5" MACOSX_DEPLOYMENT_TARGET="14.5"
ENTRYPOINT ["bash"] ENTRYPOINT ["bash"]

View File

@ -18,10 +18,8 @@ fn main() -> Result<(), Box<dyn Error>> {
}; };
builder builder
.include(Path::new("shrooms-vb-core/core")) .include(Path::new("shrooms-vb-core/core"))
.include(Path::new("shrooms-vb-core/util"))
.opt_level(opt_level) .opt_level(opt_level)
.flag_if_supported("-fno-strict-aliasing") .flag_if_supported("-fno-strict-aliasing")
.define("_CRT_SECURE_NO_WARNINGS", None)
.define("VB_LITTLE_ENDIAN", None) .define("VB_LITTLE_ENDIAN", None)
.define("VB_SIGNED_PROPAGATE", None) .define("VB_SIGNED_PROPAGATE", None)
.define("VB_DIV_GENERIC", None) .define("VB_DIV_GENERIC", None)
@ -31,10 +29,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.define("VB_DIRECT_FRAME", "on_frame") .define("VB_DIRECT_FRAME", "on_frame")
.define("VB_DIRECT_READ", "on_read") .define("VB_DIRECT_READ", "on_read")
.define("VB_DIRECT_WRITE", "on_write") .define("VB_DIRECT_WRITE", "on_write")
.define("VBU_REALLOC", "vbu_realloc_shim")
.file(Path::new("shrooms-vb-core/core/vb.c")) .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"); .compile("vb");
Ok(()) 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-21/clang-21_21.1.5~%2B%2B20251023083151%2B45afac62e373-1~exp1~20251023083333.51_amd64.deb
PACKAGES=('clang-21' 'clang-tools-21' 'libclang-common-21-dev' 'libclang-cpp21' 'libclang-rt-21-dev' 'libclang1-21' 'libllvm21' 'lld-21' 'llvm-21' 'llvm-21-dev' 'llvm-21-linker-tools' 'llvm-21-runtime' 'llvm-21-tools')
FILES=()
URL='https://apt.llvm.org/bookworm/pool/main/l/llvm-toolchain-21'
VERSION='21.1.5~%2B%2B20251023083151%2B45afac62e373-1~exp1~20251023083333.51_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 +1,4 @@
Types: deb deb-src Types: deb deb-src
URIs: http://apt.llvm.org/bookworm/ URIs: http://apt.llvm.org/bookworm/
Suites: llvm-toolchain-bookworm-21 Suites: llvm-toolchain-bookworm-20
Components: main Components: main

@ -1 +1 @@
Subproject commit 29ade46a0a58e885a9a913f738cdb30d54e0a9c5 Subproject commit 8598eab087cced12b92a411f6c7f2eb9de51310f

View File

@ -1,40 +1,34 @@
use std::{ use std::{collections::HashSet, num::NonZero, sync::Arc, thread, time::Duration};
collections::hash_map::Entry,
num::NonZero,
sync::Arc,
thread,
time::{Duration, Instant},
};
use egui::{ use egui::{
Context, FontData, FontDefinitions, FontFamily, IconData, PlatformOutput, RawInput, Context, FontData, FontDefinitions, FontFamily, IconData, TextWrapMode, ViewportBuilder,
TextWrapMode, ViewportBuilder, ViewportCommand, ViewportEvent, ViewportId, ViewportIdMap, ViewportCommand, ViewportId, ViewportInfo,
ViewportIdSet, ViewportInfo, style::ScrollStyle, ahash::{HashMap, HashMapExt},
style::ScrollStyle,
}; };
use egui_wgpu::winit::Painter;
use egui_winit::EventResponse;
use gilrs::{EventType, Gilrs}; use gilrs::{EventType, Gilrs};
use tracing::{error, warn}; use tracing::{error, warn};
use winit::{ use winit::{
application::ApplicationHandler, application::ApplicationHandler,
event::WindowEvent, event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy}, event_loop::{ActiveEventLoop, EventLoopProxy},
window::{Window, WindowId}, window::Window,
}; };
use crate::{ use crate::{
config::CliArgs,
controller::ControllerManager, controller::ControllerManager,
emulator::{EmulatorClient, EmulatorCommand, SimId}, emulator::{EmulatorClient, EmulatorCommand, SimId},
images::ImageTextureLoader, images::ImageProcessor,
input::{MappingProvider, ShortcutProvider}, input::{MappingProvider, ShortcutProvider},
memory::MemoryClient, memory::MemoryClient,
persistence::Persistence, persistence::Persistence,
window::{AppWindow, ChildWindow, GameScreen, GameWindow, InitArgs}, window::{
AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow,
GdbServerWindow, HotkeysWindow, InputWindow, ObjectWindow, ProfileWindow, RegisterWindow,
TerminalWindow, WorldWindow,
},
}; };
const EGUI_FILENAME: &str = "egui";
fn load_icon() -> anyhow::Result<IconData> { fn load_icon() -> anyhow::Result<IconData> {
let bytes = include_bytes!("../assets/lemur-256x256.png"); let bytes = include_bytes!("../assets/lemur-256x256.png");
let img = image::load_from_memory_with_format(bytes, image::ImageFormat::Png)?; let img = image::load_from_memory_with_format(bytes, image::ImageFormat::Png)?;
@ -46,375 +40,156 @@ fn load_icon() -> anyhow::Result<IconData> {
}) })
} }
struct SharedViewportState {
viewport_info: ViewportIdMap<ViewportInfo>,
painter: Painter,
resized_viewport: Option<ViewportId>,
}
pub struct Application { pub struct Application {
client: EmulatorClient,
persistence: Persistence,
ctx: Context,
shared: SharedViewportState,
icon: Option<Arc<IconData>>, icon: Option<Arc<IconData>>,
app: GameWindow, wgpu: WgpuState,
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
mappings: MappingProvider,
shortcuts: ShortcutProvider,
controllers: ControllerManager, controllers: ControllerManager,
viewports: ViewportIdMap<ViewportManager>, memory: Arc<MemoryClient>,
images: ImageProcessor,
persistence: Persistence,
viewports: HashMap<ViewportId, Viewport>,
focused: Option<ViewportId>, focused: Option<ViewportId>,
redraw_times: ViewportIdMap<Instant>, init_debug_port: Option<u16>,
initial_windows: Vec<ChildWindow>, init_profiling: bool,
} }
impl Application { impl Application {
pub fn new( pub fn new(
client: EmulatorClient, client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
persistence: Persistence, debug_port: Option<u16>,
args: CliArgs, profiling: bool,
) -> Self { ) -> Self {
let wgpu = WgpuState::new(); let wgpu = WgpuState::new();
let icon = load_icon().ok().map(Arc::new); let icon = load_icon().ok().map(Arc::new);
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 shortcuts = ShortcutProvider::new(persistence.clone());
let controllers = ControllerManager::new(client.clone(), &mappings); let controllers = ControllerManager::new(client.clone(), &mappings);
let memory = Arc::new(MemoryClient::new(client.clone())); let memory = Arc::new(MemoryClient::new(client.clone()));
let images = Arc::new(ImageTextureLoader::new()); let images = ImageProcessor::new();
{ {
let mappings = mappings.clone(); let mappings = mappings.clone();
let proxy = proxy.clone(); let proxy = proxy.clone();
thread::spawn(|| process_gamepad_input(mappings, proxy)); 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 { Self {
client,
persistence,
ctx,
shared: SharedViewportState {
viewport_info: ViewportIdMap::default(),
painter,
resized_viewport: None,
},
icon, icon,
app, wgpu,
client,
proxy,
mappings,
shortcuts,
memory,
images,
controllers, controllers,
viewports: ViewportIdMap::default(), persistence,
viewports: HashMap::new(),
focused: None, focused: None,
redraw_times: ViewportIdMap::default(), init_debug_port: debug_port,
initial_windows, init_profiling: profiling,
} }
} }
fn check_repaint(&mut self, event_loop: &ActiveEventLoop) { fn open(&mut self, event_loop: &ActiveEventLoop, window: Box<dyn AppWindow>) {
let now = Instant::now(); let viewport_id = window.viewport_id();
self.redraw_times.retain(|viewport_id, time| { if let Some(viewport) = self.viewports.get(&viewport_id) {
if *time > now { viewport.window.focus_window();
return true; return;
} }
if let Some(viewport) = self.viewports.get(viewport_id) { self.viewports.insert(
viewport.window.request_redraw(); viewport_id,
} Viewport::new(event_loop, &self.wgpu, self.icon.clone(), window),
false
});
if let Some(next_repaint_time) = self.redraw_times.values().min().copied() {
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
}
}
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);
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![],
);
viewport.window.pre_present_notify();
}
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);
}
} }
impl ApplicationHandler<UserEvent> for Application { impl ApplicationHandler<UserEvent> for Application {
fn resumed(&mut self, event_loop: &ActiveEventLoop) { fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let mut viewport_builder = self.app.initial_viewport(); if let Some(port) = self.init_debug_port {
if let Some(icon) = &self.icon { let mut server =
viewport_builder = viewport_builder.with_icon(icon.clone()); GdbServerWindow::new(SimId::Player1, self.client.clone(), self.proxy.clone());
server.launch(port);
self.open(event_loop, Box::new(server));
} }
let manager = ViewportManager::new( let app = GameWindow::new(
&self.ctx, self.client.clone(),
ViewportId::ROOT, self.proxy.clone(),
ViewportId::ROOT, self.persistence.clone(),
event_loop, self.shortcuts.clone(),
viewport_builder, SimId::Player1,
&mut self.shared,
); );
let render_state = self.shared.painter.render_state().unwrap(); self.open(event_loop, Box::new(app));
GameScreen::init_pipeline(&render_state); if self.init_profiling {
self.app.handle_init( let mut profiler = ProfileWindow::new(SimId::Player1, self.client.clone());
ViewportId::ROOT, profiler.launch();
InitArgs { self.open(event_loop, Box::new(profiler));
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();
} }
} }
fn window_event( fn window_event(
&mut self, &mut self,
event_loop: &ActiveEventLoop, event_loop: &ActiveEventLoop,
window_id: WindowId, window_id: winit::window::WindowId,
event: WindowEvent, event: WindowEvent,
) { ) {
let Some(viewport) = self let Some(viewport) = self
.viewports .viewports
.values_mut() .values_mut()
.find(|v| v.has_window_id(window_id)) .find(|v| v.window.id() == window_id)
else { else {
return; return;
}; };
let viewport_id = viewport.id; let viewport_id = viewport.id();
let (response, close_requested) = viewport.on_window_event(&event, &mut self.shared); let mut queue_redraw = false;
if close_requested && viewport_id == ViewportId::ROOT { let mut inactive_viewports = HashSet::new();
event_loop.exit(); let (consumed, action) = viewport.on_window_event(&event);
} if !consumed {
if !response.consumed {
match event { match event {
WindowEvent::KeyboardInput { event, .. } WindowEvent::KeyboardInput { event, .. } => {
if !self.app.handle_key_event(viewport_id, &event) => if !viewport.app.handle_key_event(&event) {
{
self.controllers.handle_key_event(&event); self.controllers.handle_key_event(&event);
} }
}
WindowEvent::Focused(new_focused) => { WindowEvent::Focused(new_focused) => {
self.focused = new_focused.then_some(viewport_id); 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( fn device_event(
@ -431,41 +206,99 @@ impl ApplicationHandler<UserEvent> for Application {
else { else {
return; return;
}; };
viewport.on_mouse_motion(delta); viewport.state.on_mouse_motion(delta);
} }
} }
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
match event { match event {
UserEvent::GamepadEvent(event) => { UserEvent::GamepadEvent(event) => {
if !self if let Some(viewport) = self
.focused .focused
.is_some_and(|id| self.app.handle_gamepad_event(id, &event)) .as_ref()
.and_then(|id| self.viewports.get_mut(id))
&& viewport.app.handle_gamepad_event(&event)
{ {
return;
}
self.controllers.handle_gamepad_event(&event); self.controllers.handle_gamepad_event(&event);
} }
UserEvent::OpenAbout => {
let about = AboutWindow;
self.open(event_loop, Box::new(about));
} }
UserEvent::Quit(sim_id) => match sim_id { UserEvent::OpenCharacterData(sim_id) => {
SimId::Player1 => event_loop.exit(), let chardata = CharacterDataWindow::new(sim_id, &self.memory, &mut self.images);
SimId::Player2 => self.app.close(ChildWindow::Player2), self.open(event_loop, Box::new(chardata));
}, }
UserEvent::RequestRedraw(viewport, when) => { UserEvent::OpenBgMap(sim_id) => {
let scheduled = self.redraw_times.entry(viewport).or_insert(when); let bgmap = BgMapWindow::new(sim_id, &self.memory, &mut self.images);
if *scheduled > when { self.open(event_loop, Box::new(bgmap));
*scheduled = when; }
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::OpenTerminal(sim_id) => {
let terminal = TerminalWindow::new(sim_id, &self.client);
self.open(event_loop, Box::new(terminal));
}
UserEvent::OpenProfiler(sim_id) => {
let profile = ProfileWindow::new(sim_id, self.client.clone());
self.open(event_loop, Box::new(profile));
}
UserEvent::OpenDebugger(sim_id) => {
let debugger =
GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone());
self.open(event_loop, Box::new(debugger));
}
UserEvent::OpenInput => {
let input = InputWindow::new(self.mappings.clone());
self.open(event_loop, Box::new(input));
}
UserEvent::OpenHotkeys => {
let hotkeys = HotkeysWindow::new(self.shortcuts.clone());
self.open(event_loop, Box::new(hotkeys));
}
UserEvent::OpenPlayer2 => {
let p2 = GameWindow::new(
self.client.clone(),
self.proxy.clone(),
self.persistence.clone(),
self.shortcuts.clone(),
SimId::Player2,
);
self.open(event_loop, Box::new(p2));
}
UserEvent::Quit(sim_id) => {
self.viewports
.retain(|_, viewport| viewport.app.sim_id() != sim_id);
if !self.viewports.contains_key(&ViewportId::ROOT) {
event_loop.exit();
} }
} }
} }
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) { 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(); let (sender, receiver) = oneshot::channel();
if self.client.send_command(EmulatorCommand::Exit(sender)) if self.client.send_command(EmulatorCommand::Exit(sender))
&& let Err(error) = receiver.recv_timeout(Duration::from_secs(5)) && let Err(error) = receiver.recv_timeout(Duration::from_secs(5))
@ -500,14 +333,13 @@ impl WgpuState {
#[cfg(windows)] #[cfg(windows)]
let backends = wgpu::Backends::from_env() let backends = wgpu::Backends::from_env()
.unwrap_or((wgpu::Backends::PRIMARY | wgpu::Backends::GL) - wgpu::Backends::VULKAN); .unwrap_or((wgpu::Backends::PRIMARY | wgpu::Backends::GL) - wgpu::Backends::VULKAN);
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends, backends,
..wgpu::InstanceDescriptor::new_without_display_handle_from_env() ..wgpu::InstanceDescriptor::default()
}); });
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: power_preference: wgpu::PowerPreference::HighPerformance,
wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::LowPower),
compatible_surface: None, compatible_surface: None,
force_fallback_adapter: false, force_fallback_adapter: false,
})) }))
@ -525,109 +357,213 @@ impl WgpuState {
} }
} }
struct ViewportManager { struct Viewport {
id: ViewportId, painter: egui_wgpu::winit::Painter,
ctx: Context,
info: ViewportInfo,
commands: Vec<ViewportCommand>,
builder: ViewportBuilder,
window: Arc<Window>, window: Arc<Window>,
state: egui_winit::State, state: egui_winit::State,
builder: ViewportBuilder, app: Box<dyn AppWindow>,
} }
impl ViewportManager { impl Viewport {
fn new( pub fn new(
ctx: &Context,
viewport_id: ViewportId,
parent_id: ViewportId,
event_loop: &ActiveEventLoop, event_loop: &ActiveEventLoop,
builder: ViewportBuilder, wgpu: &WgpuState,
shared: &mut SharedViewportState, icon: Option<Arc<IconData>>,
mut app: Box<dyn AppWindow>,
) -> Self { ) -> Self {
let window = Arc::new(egui_winit::create_window(ctx, event_loop, &builder).unwrap()); let ctx = Context::default();
pollster::block_on(shared.painter.set_window(viewport_id, Some(window.clone()))).unwrap(); let mut fonts = FontDefinitions::default();
let state = egui_winit::State::new( fonts.font_data.insert(
ctx.clone(), "Selawik".into(),
viewport_id, Arc::new(FontData::from_static(include_bytes!(
event_loop, "../assets/selawik.ttf"
Some(window.scale_factor() as f32), ))),
event_loop.system_theme(),
shared.painter.max_texture_side(),
); );
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_corner_radius = Default::default();
s.spacing.scroll = ScrollStyle::thin();
});
egui_extras::install_image_loaders(&ctx);
let mut info = ViewportInfo { let wgpu_config = egui_wgpu::WgpuConfiguration {
parent: Some(parent_id), present_mode: wgpu::PresentMode::AutoNoVsync,
..ViewportInfo::default() 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()
}; };
egui_winit::update_viewport_info(&mut info, ctx, &window, true);
shared.viewport_info.insert(viewport_id, info);
let mut painter = pollster::block_on(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 { Self {
id: viewport_id, painter,
ctx,
info,
commands: vec![],
builder,
window, window,
state, state,
builder, app,
} }
} }
fn has_window_id(&self, window_id: WindowId) -> bool { pub fn id(&self) -> ViewportId {
self.window.id() == window_id self.app.viewport_id()
} }
fn on_mouse_motion(&mut self, delta: (f64, f64)) { pub fn on_window_event(&mut self, event: &WindowEvent) -> (bool, Option<Action>) {
self.state.on_mouse_motion(delta); let response = self.state.on_window_event(&self.window, event);
} egui_winit::update_viewport_info(
&mut self.info,
self.state.egui_ctx(),
&self.window,
false,
);
fn on_window_event( let action = match event {
&mut self, WindowEvent::RedrawRequested => Some(Action::Redraw),
event: &WindowEvent, WindowEvent::CloseRequested => Some(Action::Close),
shared: &mut SharedViewportState, WindowEvent::Resized(size) => {
) -> (EventResponse, bool) { if let (Some(width), Some(height)) =
if let WindowEvent::Resized(size) = event
&& let (Some(width), Some(height)) =
(NonZero::new(size.width), NonZero::new(size.height)) (NonZero::new(size.width), NonZero::new(size.height))
{ {
if shared.resized_viewport != Some(self.id) { self.painter
shared.resized_viewport = Some(self.id); .on_window_resized(ViewportId::ROOT, width, height);
shared.painter.on_window_resize_state_change(self.id, true);
} }
shared.painter.on_window_resized(self.id, width, height); None
} }
let response = self.state.on_window_event(&self.window, event); _ if response.repaint => Some(Action::Redraw),
let info = shared.viewport_info.get_mut(&self.id).unwrap(); _ => None,
if let WindowEvent::CloseRequested = event { };
info.events.push(ViewportEvent::Close); (response.consumed, action)
}
egui_winit::update_viewport_info(info, self.state.egui_ctx(), &self.window, false);
(response, info.close_requested())
} }
fn take_egui_input(&mut self) -> RawInput { fn redraw(&mut self, event_loop: &ActiveEventLoop) -> Option<Action> {
self.state.take_egui_input(&self.window) 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| {
fn handle_platform_output(&mut self, platform_output: PlatformOutput) { self.app.show(ctx);
self.state });
.handle_platform_output(&self.window, platform_output); let clipped_primitives = self.ctx.tessellate(output.shapes, output.pixels_per_point);
} self.painter.paint_and_update_textures(
ViewportId::ROOT,
fn process_viewport_commands( output.pixels_per_point,
&self, [0.0, 0.0, 0.0, 0.0],
commands: Vec<ViewportCommand>, &clipped_primitives,
shared: &mut SharedViewportState, &output.textures_delta,
) { vec![],
let info = shared.viewport_info.get_mut(&self.id).unwrap();
egui_winit::process_viewport_commands(
self.state.egui_ctx(),
info,
commands,
&self.window,
&mut 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)] #[derive(Debug)]
pub enum UserEvent { pub enum UserEvent {
GamepadEvent(gilrs::Event), GamepadEvent(gilrs::Event),
OpenAbout,
OpenCharacterData(SimId),
OpenBgMap(SimId),
OpenObjects(SimId),
OpenWorlds(SimId),
OpenFrameBuffers(SimId),
OpenRegisters(SimId),
OpenTerminal(SimId),
OpenProfiler(SimId),
OpenDebugger(SimId),
OpenInput,
OpenHotkeys,
OpenPlayer2,
Quit(SimId), 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>) { fn process_gamepad_input(mappings: MappingProvider, proxy: EventLoopProxy<UserEvent>) {

View File

@ -1,17 +1,17 @@
use std::time::Duration; use std::time::Duration;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use audioadapter_buffers::direct::InterleavedSlice;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use rubato::Resampler; use itertools::Itertools;
use rubato::{FastFixedOut, Resampler};
use tracing::error; use tracing::error;
pub struct Audio { pub struct Audio {
#[allow(unused)] #[allow(unused)]
stream: cpal::Stream, stream: cpal::Stream,
sampler: rubato::Async<f32>, sampler: FastFixedOut<f32>,
input_buffer: Vec<f32>, input_buffer: Vec<Vec<f32>>,
output_buffer: Vec<f32>, output_buffer: Vec<Vec<f32>>,
sample_sink: rtrb::Producer<f32>, sample_sink: rtrb::Producer<f32>,
} }
@ -32,18 +32,17 @@ impl Audio {
let mut config = config.with_max_sample_rate().config(); let mut config = config.with_max_sample_rate().config();
let resample_ratio = config.sample_rate.0 as f64 / VB_FREQUENCY as f64; let resample_ratio = config.sample_rate.0 as f64 / VB_FREQUENCY as f64;
let chunk_size = (834.0 * resample_ratio) as usize; let chunk_size = (834.0 * resample_ratio) as usize;
let sampler = rubato::Async::new_poly( let sampler = FastFixedOut::new(
resample_ratio, resample_ratio,
64.0, 64.0,
rubato::PolynomialDegree::Cubic, rubato::PolynomialDegree::Cubic,
chunk_size, chunk_size,
2, 2,
rubato::FixedAsync::Output,
)?; )?;
config.buffer_size = cpal::BufferSize::Fixed(sampler.output_frames_max() as u32); 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 input_buffer = sampler.input_buffer_allocate(true);
let output_buffer = vec![0.0; sampler.nbr_channels() * sampler.output_frames_max()]; let output_buffer = sampler.output_buffer_allocate(true);
let (sample_sink, mut sample_source) = let (sample_sink, mut sample_source) =
rtrb::RingBuffer::new(sampler.output_frames_max() * 4); rtrb::RingBuffer::new(sampler.output_frames_max() * 4);
@ -79,38 +78,34 @@ impl Audio {
}) })
} }
pub fn update(&mut self, mut samples: &[f32]) { pub fn update(&mut self, samples: &[f32]) {
while self.input_buffer.len() + samples.len() >= self.sampler.input_frames_next() * 2 { for sample in samples.chunks_exact(2) {
let samples_needed = for (channel, value) in self.input_buffer.iter_mut().zip(sample) {
(self.sampler.input_frames_next() * 2).saturating_sub(self.input_buffer.len()); channel.push(*value);
let (current_samples, future_samples) = samples.split_at(samples_needed); }
self.input_buffer.extend_from_slice(current_samples); if self.input_buffer[0].len() >= self.sampler.input_frames_next() {
samples = future_samples;
let buffer_in =
InterleavedSlice::new(&self.input_buffer, 2, self.sampler.input_frames_next())
.unwrap();
let mut buffer_out = InterleavedSlice::new_mut(
&mut self.output_buffer,
2,
self.sampler.output_frames_next(),
)
.unwrap();
let (_, output_samples) = self let (_, output_samples) = self
.sampler .sampler
.process_into_buffer(&buffer_in, &mut buffer_out, None) .process_into_buffer(&self.input_buffer, &mut self.output_buffer, None)
.unwrap(); .unwrap();
let chunk = match self.sample_sink.write_chunk_uninit(output_samples * 2) { let chunk = match self.sample_sink.write_chunk_uninit(output_samples * 2) {
Ok(c) => c, Ok(c) => c,
Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => { Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => {
self.sample_sink.write_chunk_uninit(n).unwrap() self.sample_sink.write_chunk_uninit(n).unwrap()
} }
}; };
chunk.fill_from_iter(self.output_buffer[..output_samples * 2].iter().copied()); let interleaved = self.output_buffer[0]
.iter()
.interleave(self.output_buffer[1].iter())
.cloned();
chunk.fill_from_iter(interleaved);
self.input_buffer.clear(); 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 { while self.sample_sink.slots() < self.sampler.output_frames_max() * 2 {
std::thread::sleep(Duration::from_micros(500)); std::thread::sleep(Duration::from_micros(500));

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

@ -5,7 +5,7 @@ use std::{
sync::{ sync::{
Arc, Weak, Arc, Weak,
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
mpsc::{self, RecvTimeoutError, TryRecvError}, mpsc::{self, RecvError, TryRecvError},
}, },
time::Duration, time::Duration,
}; };
@ -33,7 +33,6 @@ mod cart;
mod game_info; mod game_info;
mod inline_stack_map; mod inline_stack_map;
mod shrooms_vb_core; mod shrooms_vb_core;
mod shrooms_vb_util;
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub enum SimId { pub enum SimId {
@ -75,7 +74,6 @@ pub struct EmulatorBuilder {
audio_on: Arc<[AtomicBool; 2]>, audio_on: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>, linked: Arc<AtomicBool>,
start_paused: bool, start_paused: bool,
watch_rom: Arc<[AtomicBool; 2]>,
} }
impl EmulatorBuilder { impl EmulatorBuilder {
@ -92,13 +90,12 @@ impl EmulatorBuilder {
audio_on: Arc::new([AtomicBool::new(true), AtomicBool::new(true)]), audio_on: Arc::new([AtomicBool::new(true), AtomicBool::new(true)]),
linked: Arc::new(AtomicBool::new(false)), linked: Arc::new(AtomicBool::new(false)),
start_paused: false, start_paused: false,
watch_rom: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]),
}; };
let client = EmulatorClient { let client = EmulatorClient {
queue, queue,
sim_state: builder.sim_state.clone(), sim_state: builder.sim_state.clone(),
state: builder.state.clone(), state: builder.state.clone(),
watch_rom: builder.watch_rom.clone(), audio_on: builder.audio_on.clone(),
linked: builder.linked.clone(), linked: builder.linked.clone(),
}; };
(builder, client) (builder, client)
@ -118,25 +115,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> { pub fn build(self) -> Result<Emulator> {
let mut emulator = Emulator::new( let mut emulator = Emulator::new(
self.commands, self.commands,
self.sim_state, self.sim_state,
self.state, self.state,
self.audio_on, self.audio_on,
self.watch_rom,
self.linked, self.linked,
)?; )?;
if let Some(path) = self.rom { if let Some(path) = self.rom {
@ -157,7 +141,6 @@ pub struct Emulator {
sim_state: Arc<[Atomic<SimState>; 2]>, sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>, state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>, audio_on: Arc<[AtomicBool; 2]>,
watch_rom: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>, linked: Arc<AtomicBool>,
profilers: [Option<ProfileSender>; 2], profilers: [Option<ProfileSender>; 2],
renderers: HashMap<SimId, TextureSink>, renderers: HashMap<SimId, TextureSink>,
@ -176,7 +159,6 @@ impl Emulator {
sim_state: Arc<[Atomic<SimState>; 2]>, sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>, state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>, audio_on: Arc<[AtomicBool; 2]>,
watch_rom: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>, linked: Arc<AtomicBool>,
) -> Result<Self> { ) -> Result<Self> {
Ok(Self { Ok(Self {
@ -187,7 +169,6 @@ impl Emulator {
sim_state, sim_state,
state, state,
audio_on, audio_on,
watch_rom,
linked, linked,
profilers: [None, None], profilers: [None, None],
renderers: HashMap::new(), renderers: HashMap::new(),
@ -201,18 +182,9 @@ impl Emulator {
}) })
} }
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<()> { 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)?;
let cart = Cart::load(path, sim_id, watch)?; self.reset_sim(sim_id, Some(cart))?;
self.try_reset_sim(sim_id, Some(cart))?;
Ok(()) Ok(())
} }
@ -222,26 +194,16 @@ impl Emulator {
} else { } else {
self.carts[0].as_ref().map(|c| c.file_path.clone()) self.carts[0].as_ref().map(|c| c.file_path.clone())
}; };
let watch = self.watch_rom[SimId::Player2.to_index()].load(Ordering::Acquire);
let cart = match file_path { let cart = match file_path {
Some(rom_path) => Some(Cart::load(&rom_path, SimId::Player2, watch)?), Some(rom_path) => Some(Cart::load(&rom_path, SimId::Player2)?),
None => None, None => None,
}; };
self.try_reset_sim(SimId::Player2, cart)?; self.reset_sim(SimId::Player2, cart)?;
self.link_sims(); self.link_sims();
Ok(()) Ok(())
} }
fn reset_sim(&mut self, sim_id: SimId, new_cart: Option<Cart>) -> bool { fn reset_sim(&mut self, sim_id: SimId, new_cart: Option<Cart>) -> Result<()> {
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<()> {
self.save_sram(sim_id)?; self.save_sram(sim_id)?;
let index = sim_id.to_index(); let index = sim_id.to_index();
@ -262,10 +224,6 @@ impl Emulator {
sim.load_cart(cart.rom.clone(), cart.sram.clone())?; sim.load_cart(cart.rom.clone(), cart.sram.clone())?;
self.carts[index] = Some(cart); self.carts[index] = Some(cart);
self.sim_state[index].store(SimState::Ready, Ordering::Release); self.sim_state[index].store(SimState::Ready, Ordering::Release);
} else if let Some(cart) = self.carts[index].as_mut()
&& cart.is_watching()
{
cart.restart_watching();
} }
let mut profiling = false; let mut profiling = false;
if let Some(profiler) = self.profilers[sim_id.to_index()].as_ref() if let Some(profiler) = self.profilers[sim_id.to_index()].as_ref()
@ -382,7 +340,7 @@ impl Emulator {
fn start_profiling(&mut self, sim_id: SimId, sender: ProfileSender) -> Result<()> { fn start_profiling(&mut self, sim_id: SimId, sender: ProfileSender) -> Result<()> {
self.profilers[sim_id.to_index()] = Some(sender); self.profilers[sim_id.to_index()] = Some(sender);
self.try_reset_sim(sim_id, None) self.reset_sim(sim_id, None)
} }
fn start_debugging(&mut self, sim_id: SimId, sender: DebugSender) { fn start_debugging(&mut self, sim_id: SimId, sender: DebugSender) {
@ -458,33 +416,14 @@ impl Emulator {
let idle = self.tick(); let idle = self.tick();
if idle { if idle {
// The game is paused, and we have output all the video/audio we have. // 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. // Block the thread until a new command comes in.
loop { match self.commands.recv() {
match self.commands.recv_timeout(Duration::from_millis(250)) { Ok(command) => self.handle_command(command),
Ok(command) => { Err(RecvError) => {
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; return;
} }
} }
} }
}
loop { loop {
match self.commands.try_recv() { match self.commands.try_recv() {
Ok(command) => self.handle_command(command), Ok(command) => self.handle_command(command),
@ -496,14 +435,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| { self.watched_regions.retain(|range, region| {
let Some(region) = region.upgrade() else { let Some(region) = region.upgrade() else {
return false; return false;
@ -670,22 +601,6 @@ impl Emulator {
self.report_error(sim_id, format!("Error loading rom: {error}")); 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) => { EmulatorCommand::StartSecondSim(path) => {
if let Err(error) = self.start_second_sim(path) { if let Err(error) = self.start_second_sim(path) {
self.report_error( self.report_error(
@ -799,8 +714,9 @@ impl Emulator {
}; };
sim.watch_stdout(true); sim.watch_stdout(true);
} }
EmulatorCommand::SetAudioEnabled(sim_id, enabled) => { EmulatorCommand::SetAudioEnabled(p1, p2) => {
self.audio_on[sim_id.to_index()].store(enabled, Ordering::Release); self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release);
self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release);
} }
EmulatorCommand::Link => { EmulatorCommand::Link => {
self.link_sims(); self.link_sims();
@ -809,7 +725,9 @@ impl Emulator {
self.unlink_sims(); self.unlink_sims();
} }
EmulatorCommand::Reset(sim_id) => { 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) => { EmulatorCommand::SetKeys(sim_id, keys) => {
if let Some(sim) = self.sims.get_mut(sim_id.to_index()) { if let Some(sim) = self.sims.get_mut(sim_id.to_index()) {
@ -851,8 +769,6 @@ impl Emulator {
pub enum EmulatorCommand { pub enum EmulatorCommand {
ConnectToSim(SimId, TextureSink, mpsc::Sender<Toast>), ConnectToSim(SimId, TextureSink, mpsc::Sender<Toast>),
LoadGame(SimId, PathBuf), LoadGame(SimId, PathBuf),
ReloadRom(SimId),
WatchRom(SimId, bool),
StartSecondSim(Option<PathBuf>), StartSecondSim(Option<PathBuf>),
StopSecondSim, StopSecondSim,
Pause, Pause,
@ -875,7 +791,7 @@ pub enum EmulatorCommand {
AddWatchpoint(SimId, u32, usize, VBWatchpointType), AddWatchpoint(SimId, u32, usize, VBWatchpointType),
RemoveWatchpoint(SimId, u32, usize, VBWatchpointType), RemoveWatchpoint(SimId, u32, usize, VBWatchpointType),
WatchStdout(SimId, mpsc::Sender<String>), WatchStdout(SimId, mpsc::Sender<String>),
SetAudioEnabled(SimId, bool), SetAudioEnabled(bool, bool),
Link, Link,
Unlink, Unlink,
Reset(SimId), Reset(SimId),
@ -941,8 +857,8 @@ pub struct EmulatorClient {
queue: mpsc::Sender<EmulatorCommand>, queue: mpsc::Sender<EmulatorCommand>,
sim_state: Arc<[Atomic<SimState>; 2]>, sim_state: Arc<[Atomic<SimState>; 2]>,
state: Arc<Atomic<EmulatorState>>, state: Arc<Atomic<EmulatorState>>,
audio_on: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>, linked: Arc<AtomicBool>,
watch_rom: Arc<[AtomicBool; 2]>,
} }
impl EmulatorClient { impl EmulatorClient {
@ -952,12 +868,12 @@ impl EmulatorClient {
pub fn emulator_state(&self) -> EmulatorState { pub fn emulator_state(&self) -> EmulatorState {
self.state.load(Ordering::Acquire) 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 { pub fn are_sims_linked(&self) -> bool {
self.linked.load(Ordering::Acquire) 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 { pub fn send_command(&self, command: EmulatorCommand) -> bool {
match self.queue.send(command) { match self.queue.send(command) {
Ok(()) => true, Ok(()) => true,

View File

@ -1,14 +1,13 @@
use anyhow::Result; use anyhow::Result;
use notify::Watcher; use rand::Rng;
use rand::RngExt;
use std::{ use std::{
fs::{self, File}, fs::{self, File},
io::{Read, Seek as _, SeekFrom, Write as _}, io::{Read, Seek as _, SeekFrom, Write as _},
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, atomic::AtomicBool}, sync::Arc,
}; };
use crate::emulator::{SimId, game_info::GameInfo, shrooms_vb_util::rom_from_isx}; use crate::emulator::{SimId, game_info::GameInfo};
pub struct Cart { pub struct Cart {
pub file_path: PathBuf, pub file_path: PathBuf,
@ -16,16 +15,13 @@ pub struct Cart {
sram_file: File, sram_file: File,
pub sram: Vec<u8>, pub sram: Vec<u8>,
pub info: Arc<GameInfo>, pub info: Arc<GameInfo>,
watcher: Option<notify::RecommendedWatcher>,
changed: Arc<AtomicBool>,
} }
impl Cart { impl Cart {
pub fn load(file_path: &Path, sim_id: SimId, watch: bool) -> Result<Self> { pub fn load(file_path: &Path, sim_id: SimId) -> Result<Self> {
let rom = fs::read(file_path)?; let rom = fs::read(file_path)?;
let (rom, info) = try_parse_isx(file_path, &rom) let (rom, info) =
.or_else(|| try_parse_elf(file_path, &rom)) try_parse_elf(file_path, &rom).unwrap_or_else(|| (rom, GameInfo::empty(file_path)));
.unwrap_or_else(|| (rom, GameInfo::empty(file_path)));
let mut sram_file = File::options() let mut sram_file = File::options()
.read(true) .read(true)
@ -48,21 +44,12 @@ impl Cart {
sram sram
}; };
let changed = Arc::new(AtomicBool::new(false));
let watcher = if watch {
build_watcher(file_path, changed.clone())
} else {
None
};
Ok(Cart { Ok(Cart {
file_path: file_path.to_path_buf(), file_path: file_path.to_path_buf(),
rom, rom,
sram_file, sram_file,
sram, sram,
info: Arc::new(info), info: Arc::new(info),
watcher,
changed,
}) })
} }
@ -71,56 +58,6 @@ impl Cart {
self.sram_file.write_all(&self.sram)?; self.sram_file.write_all(&self.sram)?;
Ok(()) 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)> { fn try_parse_elf(file_path: &Path, data: &[u8]) -> Option<(Vec<u8>, GameInfo)> {
@ -136,7 +73,7 @@ fn try_parse_elf(file_path: &Path, data: &[u8]) -> Option<(Vec<u8>, GameInfo)> {
} }
_ => return None, _ => return None,
}; };
let info = GameInfo::from_elf(file_path, data).unwrap_or_else(|_| GameInfo::empty(file_path)); let info = GameInfo::new(file_path, data).unwrap_or_else(|_| GameInfo::empty(file_path));
Some((program, info)) Some((program, info))
} }
@ -149,13 +86,12 @@ fn parse_elf_program<Elf: object::read::elf::FileHeader<Endian = object::Endiann
let mut bytes = vec![]; let mut bytes = vec![];
let mut pstart = None; let mut pstart = None;
for phdr in header.program_headers(endian, data).ok()? { for phdr in header.program_headers(endian, data).ok()? {
let pma = phdr.p_paddr(endian).into(); if phdr.p_filesz(endian).into() == 0 {
if pma < 0x07000000 || phdr.p_filesz(endian).into() == 0 {
continue; continue;
} }
let start = pstart.unwrap_or(pma); let start = pstart.unwrap_or(phdr.p_paddr(endian).into());
pstart = Some(start); pstart = Some(start);
bytes.resize((pma - start) as usize, 0); bytes.resize((phdr.p_paddr(endian).into() - start) as usize, 0);
let data = phdr.data(endian, data).ok()?; let data = phdr.data(endian, data).ok()?;
bytes.extend_from_slice(data); bytes.extend_from_slice(data);
} }

View File

@ -14,7 +14,7 @@ pub struct GameInfo {
} }
impl GameInfo { impl GameInfo {
pub fn from_elf(file_path: &Path, input: &[u8]) -> Result<Self> { pub fn new(file_path: &Path, input: &[u8]) -> Result<Self> {
let file = object::File::parse(input)?; let file = object::File::parse(input)?;
let (name, path) = name_and_path(file_path); let (name, path) = name_and_path(file_path);
@ -52,26 +52,6 @@ impl GameInfo {
}) })
} }
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 { pub fn empty(file_path: &Path) -> Self {
let (name, path) = name_and_path(file_path); let (name, path) = name_and_path(file_path);
let library_info = LibraryInfo { let library_info = LibraryInfo {
@ -135,66 +115,6 @@ fn build_inline_stack_map(file: object::File) -> Result<InlineStackMap> {
Ok(frames.build()) 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>; type Reader<'a> = gimli::EndianSlice<'a, gimli::RunTimeEndian>;
struct ParseContext<'a> { struct ParseContext<'a> {
@ -245,19 +165,16 @@ impl ParseContext<'_> {
} }
} }
fn parse_inline<'a>( fn parse_inline(ctx: &mut ParseContext, node: gimli::EntriesTreeNode<Reader>) -> Result<()> {
ctx: &mut ParseContext<'a>,
node: gimli::EntriesTreeNode<'a, '_, Reader<'a>>,
) -> Result<()> {
if node.entry().tag() == gimli::DW_TAG_inlined_subroutine if node.entry().tag() == gimli::DW_TAG_inlined_subroutine
&& let Some(attr) = node.entry().attr_value(gimli::DW_AT_abstract_origin) && let Some(attr) = node.entry().attr_value(gimli::DW_AT_abstract_origin)?
&& let Some(name) = ctx.name_attr(attr)? && let Some(name) = ctx.name_attr(attr)?
{ {
let name = Arc::new(name); let name = Arc::new(name);
let mut ranges = ctx.dorf.die_ranges(ctx.unit, node.entry())?; let mut ranges = ctx.dorf.die_ranges(ctx.unit, node.entry())?;
while let Some(range) = ranges.next()? { while let Some(range) = ranges.next()? {
let start = range.begin as u32 & 0x07ffffff; let start = range.begin as u32;
let end = range.end as u32 & 0x07ffffff; let end = range.end as u32;
ctx.frames.add(start, end, name.clone()); ctx.frames.add(start, end, name.clone());
} }
} }

View File

@ -261,7 +261,7 @@ extern "C" fn on_exception(sim: *mut VB, cause: *mut u16) -> c_int {
}; };
data.monitor.event = data.monitor.queued_event.take(); data.monitor.event = data.monitor.queued_event.take();
data.monitor.new_inline_stack = data.monitor.detect_new_inline_stack(pc); data.monitor.new_inline_stack = data.monitor.detect_new_inline_stack(pc);
data.monitor.queued_event = Some(SimEvent::Interrupt(cause, pc & 0x07ffffff)); data.monitor.queued_event = Some(SimEvent::Interrupt(cause, pc));
unsafe { vb_set_exception_callback(sim, None) }; unsafe { vb_set_exception_callback(sim, None) };
if data.monitor.event.is_some() || data.monitor.new_inline_stack.is_some() { if data.monitor.event.is_some() || data.monitor.new_inline_stack.is_some() {
1 1
@ -439,9 +439,7 @@ impl EventMonitor {
// JAL .+4 is how programs get r31 to a known value for indirect calls // JAL .+4 is how programs get r31 to a known value for indirect calls
// (which we detect later.) // (which we detect later.)
// Any other JAL is a function call. // Any other JAL is a function call.
return Some(SimEvent::Call( return Some(SimEvent::Call(address.wrapping_add_signed(disp)));
address.wrapping_add_signed(disp) & 0x07ffffff,
));
} }
} }
@ -455,7 +453,7 @@ impl EventMonitor {
if r31 as u32 == address.wrapping_add(2) { if r31 as u32 == address.wrapping_add(2) {
// JMP anywhere else, if r31 points to after the JMP, is an indirect call // 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) }; let target = unsafe { vb_get_program_register(sim, jmp_reg as u32) };
return Some(SimEvent::Call(target as u32 & 0x07ffffff)); return Some(SimEvent::Call(target as u32));
} }
} }

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

@ -1,5 +1,5 @@
use std::{ use std::{
collections::{HashMap, hash_map::Entry}, collections::HashMap,
ops::Deref, ops::Deref,
sync::{Arc, Mutex, Weak}, sync::{Arc, Mutex, Weak},
thread, thread,
@ -7,21 +7,17 @@ use std::{
}; };
use egui::{ use egui::{
Color32, ColorImage, TextureHandle, Color32, ColorImage, TextureHandle, TextureOptions,
epaint::ImageDelta, epaint::ImageDelta,
generate_loader_id,
load::{LoadError, SizedTexture, TextureLoader, TexturePoll}, load::{LoadError, SizedTexture, TextureLoader, TexturePoll},
}; };
use tokio::{sync::mpsc, time::timeout}; use tokio::{sync::mpsc, time::timeout};
use crate::emulator::SimId; pub struct ImageProcessor {
pub struct ImageTextureLoader {
cache: Mutex<HashMap<String, ImageEntry>>,
sender: mpsc::UnboundedSender<Box<dyn ImageRendererImpl>>, sender: mpsc::UnboundedSender<Box<dyn ImageRendererImpl>>,
} }
impl ImageTextureLoader { impl ImageProcessor {
pub fn new() -> Self { pub fn new() -> Self {
let (sender, receiver) = mpsc::unbounded_channel(); let (sender, receiver) = mpsc::unbounded_channel();
thread::spawn(move || { thread::spawn(move || {
@ -30,52 +26,29 @@ impl ImageTextureLoader {
.build() .build()
.unwrap() .unwrap()
.block_on(async move { .block_on(async move {
let mut worker = ImageTextureLoaderWorker { let mut worker = ImageProcessorWorker {
receiver, receiver,
renderers: vec![], renderers: vec![],
}; };
worker.run().await worker.run().await
}) })
}); });
Self { Self { sender }
cache: Mutex::new(HashMap::new()),
sender,
}
} }
pub fn add<const N: usize, R: ImageRenderer<N> + 'static>( pub fn add<const N: usize, R: ImageRenderer<N> + 'static>(
&self, &self,
sim_id: SimId,
renderer: R, renderer: R,
params: R::Params, params: R::Params,
) -> ImageParams<R::Params> { ) -> ([ImageHandle; N], ImageParams<R::Params>) {
let states = renderer.sizes().map(ImageState::new); 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 let images = renderer
.sizes() .sizes()
.map(|[width, height]| ImageBuffer::new(width, height)); .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 sink = Arc::new(Mutex::new(params.clone()));
let _ = self.sender.send(Box::new(ImageRendererWrapper { let _ = self.sender.send(Box::new(ImageRendererWrapper {
renderer, renderer,
@ -83,87 +56,20 @@ impl ImageTextureLoader {
images, images,
states, states,
})); }));
ImageParams { let params = ImageParams {
value: params, value: params,
sink, sink,
}
}
}
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 { (handles, params)
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>>, receiver: mpsc::UnboundedReceiver<Box<dyn ImageRendererImpl>>,
renderers: Vec<Box<dyn ImageRendererImpl>>, renderers: Vec<Box<dyn ImageRendererImpl>>,
} }
impl ImageTextureLoaderWorker { impl ImageProcessorWorker {
async fn run(&mut self) { async fn run(&mut self) {
loop { loop {
if self.renderers.is_empty() { if self.renderers.is_empty() {
@ -236,10 +142,16 @@ impl ImageBuffer {
} }
} }
struct ImageEntry { #[derive(Clone)]
pub struct ImageHandle {
size: [f32; 2], size: [f32; 2],
data: Weak<Mutex<Option<Arc<ColorImage>>>>, data: Arc<Mutex<Option<Arc<ColorImage>>>>,
texture: Option<TextureHandle>, }
impl ImageHandle {
fn pull(&mut self) -> Option<Arc<ColorImage>> {
self.data.lock().unwrap().take()
}
} }
pub struct ImageParams<T> { pub struct ImageParams<T> {
@ -266,7 +178,6 @@ impl<T: Clone + Eq> ImageParams<T> {
pub trait ImageRenderer<const N: usize>: Send { pub trait ImageRenderer<const N: usize>: Send {
type Params: Clone + Send; type Params: Clone + Send;
fn names(&self) -> [&str; N];
fn sizes(&self) -> [[usize; 2]; N]; fn sizes(&self) -> [[usize; 2]; N];
fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; N]); fn render(&mut self, params: &Self::Params, images: &mut [ImageBuffer; N]);
} }
@ -335,3 +246,83 @@ impl<const N: usize, R: ImageRenderer<N> + Send> ImageRendererImpl for ImageRend
Ok(()) 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

@ -272,14 +272,13 @@ impl Mappings for InputMapping {
#[derive(Clone)] #[derive(Clone)]
pub struct MappingProvider { pub struct MappingProvider {
persistence: Persistence, persistence: Persistence,
first_gamepad_is_p2: bool,
device_mappings: Arc<RwLock<HashMap<DeviceId, Arc<RwLock<GamepadMapping>>>>>, device_mappings: Arc<RwLock<HashMap<DeviceId, Arc<RwLock<GamepadMapping>>>>>,
sim_mappings: HashMap<SimId, Arc<RwLock<InputMapping>>>, sim_mappings: HashMap<SimId, Arc<RwLock<InputMapping>>>,
gamepad_info: Arc<RwLock<HashMap<GamepadId, GamepadInfo>>>, gamepad_info: Arc<RwLock<HashMap<GamepadId, GamepadInfo>>>,
} }
impl MappingProvider { 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 sim_mappings = HashMap::new();
let mut device_mappings = HashMap::new(); let mut device_mappings = HashMap::new();
@ -308,7 +307,6 @@ impl MappingProvider {
device_mappings: Arc::new(RwLock::new(device_mappings)), device_mappings: Arc::new(RwLock::new(device_mappings)),
gamepad_info: Arc::new(RwLock::new(HashMap::new())), gamepad_info: Arc::new(RwLock::new(HashMap::new())),
sim_mappings, sim_mappings,
first_gamepad_is_p2,
} }
} }
@ -340,12 +338,7 @@ impl MappingProvider {
.clone(); .clone();
drop(lock); drop(lock);
let mut lock = self.gamepad_info.write().unwrap(); let mut lock = self.gamepad_info.write().unwrap();
let players = if self.first_gamepad_is_p2 { let bound_to = SimId::values()
vec![SimId::Player2, SimId::Player1]
} else {
vec![SimId::Player1, SimId::Player2]
};
let bound_to = players
.into_iter() .into_iter()
.find(|sim_id| lock.values().all(|info| info.bound_to != Some(*sim_id))); .find(|sim_id| lock.values().all(|info| info.bound_to != Some(*sim_id)));
if let Entry::Vacant(entry) = lock.entry(gamepad.id()) { if let Entry::Vacant(entry) = lock.entry(gamepad.id()) {
@ -464,10 +457,15 @@ struct PersistedGamepadMapping {
default_axes: Vec<(Code, (VBKey, VBKey))>, default_axes: Vec<(Code, (VBKey, VBKey))>,
} }
#[derive(Serialize, Deserialize)]
pub struct Shortcut {
pub shortcut: KeyboardShortcut,
pub command: Command,
}
#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum Command { pub enum Command {
OpenRom, OpenRom,
ReloadRom,
Quit, Quit,
FrameAdvance, FrameAdvance,
FastForward(u32), FastForward(u32),
@ -478,10 +476,9 @@ pub enum Command {
} }
impl Command { impl Command {
pub fn all() -> [Self; 8] { pub fn all() -> [Self; 7] {
[ [
Self::OpenRom, Self::OpenRom,
Self::ReloadRom,
Self::Quit, Self::Quit,
Self::PauseResume, Self::PauseResume,
Self::Reset, Self::Reset,
@ -494,7 +491,6 @@ impl Command {
pub fn name(self) -> &'static str { pub fn name(self) -> &'static str {
match self { match self {
Self::OpenRom => "Open ROM", Self::OpenRom => "Open ROM",
Self::ReloadRom => "Reload ROM",
Self::Quit => "Exit", Self::Quit => "Exit",
Self::PauseResume => "Pause/Resume", Self::PauseResume => "Pause/Resume",
Self::Reset => "Reset", Self::Reset => "Reset",
@ -526,10 +522,6 @@ impl Default for Shortcuts {
Command::OpenRom, Command::OpenRom,
KeyboardShortcut::new(Modifiers::COMMAND, Key::O), KeyboardShortcut::new(Modifiers::COMMAND, Key::O),
); );
shortcuts.set(
Command::ReloadRom,
KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, Key::F5),
);
shortcuts.set( shortcuts.set(
Command::Quit, Command::Quit,
KeyboardShortcut::new(Modifiers::COMMAND, Key::Q), KeyboardShortcut::new(Modifiers::COMMAND, Key::Q),
@ -690,7 +682,7 @@ impl ShortcutProvider {
else { else {
return true; return true;
}; };
if *key != shortcut.logical_key || !modifiers.contains(shortcut.modifiers) { if shortcut.logical_key != *key || !shortcut.modifiers.contains(*modifiers) {
return true; return true;
} }
if matches!(command, Command::FastForward(_)) { if matches!(command, Command::FastForward(_)) {

View File

@ -1,7 +1,7 @@
// hide console in release mode // hide console in release mode
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![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 anyhow::{Result, bail};
use app::Application; use app::Application;
@ -12,15 +12,8 @@ use tracing::error;
use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt};
use winit::event_loop::{ControlFlow, EventLoop}; use winit::event_loop::{ControlFlow, EventLoop};
use crate::{
config::{CliArgs, SimConfig},
emulator::SimId,
persistence::Persistence,
};
mod app; mod app;
mod audio; mod audio;
mod config;
mod controller; mod controller;
mod emulator; mod emulator;
mod gdbserver; mod gdbserver;
@ -32,6 +25,18 @@ mod persistence;
mod profiler; mod profiler;
mod window; 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>,
/// Enable profiling a game
#[arg(short, long)]
profile: bool,
}
fn init_logger() { fn init_logger() {
let directives = std::env::var("RUST_LOG").unwrap_or("error,lemur=info".into()); let directives = std::env::var("RUST_LOG").unwrap_or("error,lemur=info".into());
let filter = EnvFilter::builder().parse_lossy(directives); let filter = EnvFilter::builder().parse_lossy(directives);
@ -93,9 +98,7 @@ fn main() -> Result<()> {
#[cfg(windows)] #[cfg(windows)]
set_process_priority_to_high()?; set_process_priority_to_high()?;
let args = CliArgs::parse(); let args = Args::parse();
let persistence = Persistence::new();
let (mut builder, client) = EmulatorBuilder::new(); let (mut builder, client) = EmulatorBuilder::new();
if let Some(path) = &args.rom { if let Some(path) = &args.rom {
@ -110,14 +113,6 @@ fn main() -> Result<()> {
if args.profile { if args.profile {
builder = builder.start_paused(true) 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() ThreadBuilder::default()
.name("Emulator".to_owned()) .name("Emulator".to_owned())
@ -136,6 +131,11 @@ fn main() -> Result<()> {
let event_loop = EventLoop::with_user_event().build().unwrap(); let event_loop = EventLoop::with_user_event().build().unwrap();
event_loop.set_control_flow(ControlFlow::Poll); event_loop.set_control_flow(ControlFlow::Poll);
let proxy = event_loop.create_proxy(); let proxy = event_loop.create_proxy();
event_loop.run_app(&mut Application::new(client, proxy, persistence, args))?; event_loop.run_app(&mut Application::new(
client,
proxy,
args.debug_port,
args.profile,
))?;
Ok(()) Ok(())
} }

View File

@ -50,11 +50,11 @@ impl MemoryClient {
} }
fn aligned_memory(start: u32, length: usize) -> BoxBytes { 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(); let memory = vec![0u32; length / 4].into_boxed_slice();
return bytemuck::box_bytes_of(memory); 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(); let memory = vec![0u16; length / 2].into_boxed_slice();
return bytemuck::box_bytes_of(memory); return bytemuck::box_bytes_of(memory);
} }

View File

@ -60,11 +60,6 @@ impl ProgramState {
let Some(stack) = self.call_stacks.get_mut(code) else { let Some(stack) = self.call_stacks.get_mut(code) else {
bail!("missing stack {code:04x}"); 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() { if stack.pop().is_none() {
bail!("returned from {code:04x} but stack was empty"); bail!("returned from {code:04x} but stack was empty");
} }

View File

@ -1,9 +1,6 @@
use std::sync::Arc;
pub use about::AboutWindow; pub use about::AboutWindow;
use egui::{Ui, ViewportBuilder}; use egui::{Context, ViewportBuilder, ViewportId};
pub use game::{ChildWindow, GameWindow}; pub use game::GameWindow;
pub use game_screen::{DisplayMode, GameScreen};
pub use gdb::GdbServerWindow; pub use gdb::GdbServerWindow;
pub use hotkeys::HotkeysWindow; pub use hotkeys::HotkeysWindow;
pub use input::InputWindow; pub use input::InputWindow;
@ -12,7 +9,7 @@ pub use terminal::TerminalWindow;
pub use vip::{ pub use vip::{
BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow,
}; };
use winit::{event::KeyEvent, window::Window}; use winit::event::KeyEvent;
use crate::emulator::SimId; use crate::emulator::SimId;
@ -28,17 +25,17 @@ mod utils;
mod vip; mod vip;
pub trait AppWindow { pub trait AppWindow {
fn viewport_id(&self) -> ViewportId;
fn sim_id(&self) -> SimId { fn sim_id(&self) -> SimId {
SimId::Player1 SimId::Player1
} }
fn image_url(&self, name: &str) -> String {
format!("vip://{}/{name}", self.sim_id())
}
fn initial_viewport(&self) -> ViewportBuilder; fn initial_viewport(&self) -> ViewportBuilder;
fn show(&mut self, ui: &mut Ui); fn show(&mut self, ctx: &Context);
fn on_init(&mut self, args: InitArgs) { fn on_init(&mut self, ctx: &Context, render_state: &egui_wgpu::RenderState) {
let _ = args; let _ = ctx;
let _ = render_state;
} }
fn on_destroy(&mut self) {}
fn handle_key_event(&mut self, event: &KeyEvent) -> bool { fn handle_key_event(&mut self, event: &KeyEvent) -> bool {
let _ = event; let _ = event;
false false
@ -48,8 +45,3 @@ pub trait AppWindow {
false 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; use super::AppWindow;
pub struct AboutWindow; pub struct AboutWindow;
impl AppWindow for AboutWindow { impl AppWindow for AboutWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of("About")
}
fn initial_viewport(&self) -> ViewportBuilder { fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default() ViewportBuilder::default()
.with_title("About Lemur") .with_title("About Lemur")
.with_inner_size((300.0, 200.0)) .with_inner_size((300.0, 200.0))
} }
fn show(&mut self, ui: &mut Ui) { fn show(&mut self, ctx: &Context) {
CentralPanel::default().show_inside(ui, |ui| { CentralPanel::default().show(ctx, |ui| {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.label("Lemur Virtual Boy Emulator"); ui.label("Lemur Virtual Boy Emulator");
ui.label(format!("Version {}", env!("CARGO_PKG_VERSION"))); ui.label(format!("Version {}", env!("CARGO_PKG_VERSION")));

View File

@ -1,31 +1,19 @@
use std::{ use std::{sync::mpsc, time::Duration};
ops::{Deref, DerefMut},
sync::{Arc, Mutex, atomic::AtomicBool, mpsc},
time::Duration,
};
use crate::{ use crate::{
app::UserEvent, app::UserEvent,
config::{COLOR_PRESETS, SimConfig},
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState}, emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
images::ImageTextureLoader, input::{Command, ShortcutProvider},
input::{Command, MappingProvider, ShortcutProvider},
memory::MemoryClient,
persistence::Persistence, persistence::Persistence,
window::{
AboutWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GdbServerWindow,
HotkeysWindow, InitArgs, InputWindow, ObjectWindow, ProfileWindow, RegisterWindow,
TerminalWindow, WorldWindow, utils::UiData,
},
}; };
use anyhow::Context as _; use anyhow::Context as _;
use egui::{ use egui::{
Align2, Button, CentralPanel, Color32, Context, Frame, MenuBar, Panel, Pos2, Ui, Vec2, Align2, Button, CentralPanel, Color32, Context, Frame, MenuBar, TopBottomPanel, Ui, Vec2,
ViewportBuilder, ViewportCommand, ViewportId, ViewportIdMap, Window, ViewportBuilder, ViewportCommand, ViewportId, Window,
}; };
use egui_notify::{Anchor, Toast, Toasts}; use egui_notify::{Anchor, Toast, Toasts};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use winit::{event::KeyEvent, event_loop::EventLoopProxy}; use winit::event_loop::EventLoopProxy;
use super::{ use super::{
AppWindow, AppWindow,
@ -33,49 +21,48 @@ use super::{
utils::UiExt as _, 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 { pub struct GameWindow {
viewport_id: ViewportId,
client: EmulatorClient, client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
persistence: Persistence, persistence: Persistence,
shortcuts: ShortcutProvider, shortcuts: ShortcutProvider,
sim_id: SimId, sim_id: SimId,
config: SimConfig, config: GameConfig,
toasts: Toasts, toasts: Toasts,
screen: Option<GameScreen>, screen: Option<GameScreen>,
messages: mpsc::Receiver<Toast>, messages: Option<mpsc::Receiver<Toast>>,
message_sink: mpsc::Sender<Toast>,
color_picker: Option<ColorPickerState>, 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 { impl GameWindow {
#[expect(clippy::too_many_arguments)]
pub fn new( pub fn new(
viewport_id: ViewportId,
client: EmulatorClient, client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
persistence: Persistence, persistence: Persistence,
shortcuts: ShortcutProvider, shortcuts: ShortcutProvider,
memory: &Arc<MemoryClient>,
images: &Arc<ImageTextureLoader>,
mappings: MappingProvider,
sim_id: SimId, sim_id: SimId,
) -> Self { ) -> Self {
let config = SimConfig::load(&persistence, sim_id); let config = load_config(&persistence, sim_id);
let toasts = Toasts::new() let toasts = Toasts::new()
.with_anchor(Anchor::BottomLeft) .with_anchor(Anchor::BottomLeft)
.with_margin((10.0, 10.0).into()) .with_margin((10.0, 10.0).into())
.reverse(true); .reverse(true);
let (message_sink, messages) = mpsc::channel();
Self { Self {
viewport_id,
client, client,
proxy, proxy,
persistence, persistence,
@ -84,151 +71,12 @@ impl GameWindow {
config, config,
toasts, toasts,
screen: None, screen: None,
messages, messages: None,
message_sink,
color_picker: 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) { fn show_menu(&mut self, ctx: &Context, ui: &mut Ui) {
// 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) {
let state = self.client.emulator_state(); let state = self.client.emulator_state();
let is_ready = self.client.sim_state(self.sim_id) == SimState::Ready; let is_ready = self.client.sim_state(self.sim_id) == SimState::Ready;
let can_pause = is_ready && state == EmulatorState::Running; let can_pause = is_ready && state == EmulatorState::Running;
@ -239,17 +87,13 @@ impl GameWindow {
match command { match command {
Command::OpenRom => { Command::OpenRom => {
let rom = rfd::FileDialog::new() let rom = rfd::FileDialog::new()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom", "elf", "isx"]) .add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
.pick_file(); .pick_file();
if let Some(path) = rom { if let Some(path) = rom {
self.client self.client
.send_command(EmulatorCommand::LoadGame(self.sim_id, path)); .send_command(EmulatorCommand::LoadGame(self.sim_id, path));
} }
} }
Command::ReloadRom => {
self.client
.send_command(EmulatorCommand::ReloadRom(self.sim_id));
}
Command::Quit => { Command::Quit => {
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id)); let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
} }
@ -295,25 +139,13 @@ impl GameWindow {
.clicked() .clicked()
{ {
let rom = rfd::FileDialog::new() let rom = rfd::FileDialog::new()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom", "elf", "isx"]) .add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
.pick_file(); .pick_file();
if let Some(path) = rom { if let Some(path) = rom {
self.client self.client
.send_command(EmulatorCommand::LoadGame(self.sim_id, path)); .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));
}
if ui if ui
.add(self.button_for(ui.ctx(), "Quit", Command::Quit)) .add(self.button_for(ui.ctx(), "Quit", Command::Quit))
.clicked() .clicked()
@ -369,7 +201,7 @@ impl GameWindow {
pollster::block_on(self.take_screenshot()); pollster::block_on(self.take_screenshot());
} }
}); });
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| { ui.menu_button("Multiplayer", |ui| {
let has_player_2 = self.client.sim_state(SimId::Player2) != SimState::Uninitialized; let has_player_2 = self.client.sim_state(SimId::Player2) != SimState::Uninitialized;
if self.sim_id == SimId::Player1 if self.sim_id == SimId::Player1
@ -378,7 +210,7 @@ impl GameWindow {
{ {
self.client self.client
.send_command(EmulatorCommand::StartSecondSim(None)); .send_command(EmulatorCommand::StartSecondSim(None));
self.open(ChildWindow::Player2); self.proxy.send_event(UserEvent::OpenPlayer2).unwrap();
} }
if has_player_2 { if has_player_2 {
let linked = self.client.are_sims_linked(); let linked = self.client.are_sims_linked();
@ -392,37 +224,55 @@ impl GameWindow {
}); });
ui.menu_button("Tools", |ui| { ui.menu_button("Tools", |ui| {
if ui.button("Terminal").clicked() { if ui.button("Terminal").clicked() {
self.open(ChildWindow::Terminal); self.proxy
.send_event(UserEvent::OpenTerminal(self.sim_id))
.unwrap();
} }
if ui.button("Profiler").clicked() { if ui.button("Profiler").clicked() {
self.open(ChildWindow::Profiler { launch: false }); self.proxy
.send_event(UserEvent::OpenProfiler(self.sim_id))
.unwrap();
} }
if ui.button("GDB Server").clicked() { if ui.button("GDB Server").clicked() {
self.open(ChildWindow::Debugger { port: None }); self.proxy
.send_event(UserEvent::OpenDebugger(self.sim_id))
.unwrap();
} }
ui.separator(); ui.separator();
if ui.button("Character Data").clicked() { if ui.button("Character Data").clicked() {
self.open(ChildWindow::CharacterData); self.proxy
.send_event(UserEvent::OpenCharacterData(self.sim_id))
.unwrap();
} }
if ui.button("Background Maps").clicked() { if ui.button("Background Maps").clicked() {
self.open(ChildWindow::BgMap); self.proxy
.send_event(UserEvent::OpenBgMap(self.sim_id))
.unwrap();
} }
if ui.button("Objects").clicked() { if ui.button("Objects").clicked() {
self.open(ChildWindow::Objects); self.proxy
.send_event(UserEvent::OpenObjects(self.sim_id))
.unwrap();
} }
if ui.button("Worlds").clicked() { if ui.button("Worlds").clicked() {
self.open(ChildWindow::Worlds); self.proxy
.send_event(UserEvent::OpenWorlds(self.sim_id))
.unwrap();
} }
if ui.button("Frame Buffers").clicked() { if ui.button("Frame Buffers").clicked() {
self.open(ChildWindow::FrameBuffers); self.proxy
.send_event(UserEvent::OpenFrameBuffers(self.sim_id))
.unwrap();
} }
if ui.button("Registers").clicked() { if ui.button("Registers").clicked() {
self.open(ChildWindow::Registers); self.proxy
.send_event(UserEvent::OpenRegisters(self.sim_id))
.unwrap();
} }
}); });
ui.menu_button("Help", |ui| { ui.menu_button("Help", |ui| {
if ui.button("About").clicked() { if ui.button("About").clicked() {
self.open(ChildWindow::About); self.proxy.send_event(UserEvent::OpenAbout).unwrap();
} }
}); });
} }
@ -448,13 +298,10 @@ impl GameWindow {
self.client self.client
.send_command(EmulatorCommand::Screenshot(self.sim_id, tx)); .send_command(EmulatorCommand::Screenshot(self.sim_id, tx));
let bytes = rx.await.context("Could not take screenshot")?; let bytes = rx.await.context("Could not take screenshot")?;
let mut file_dialog = rfd::FileDialog::new() let file = rfd::FileDialog::new()
.add_filter("PNG images", &["png"]) .add_filter("PNG images", &["png"])
.set_file_name("screenshot.png"); .set_file_name("screenshot.png")
if let Some(window) = self.window.as_ref() { .save_file();
file_dialog = file_dialog.set_parent(window);
}
let file = file_dialog.save_file();
let Some(path) = file else { let Some(path) = file else {
return Ok(None); return Ok(None);
}; };
@ -471,7 +318,7 @@ impl GameWindow {
Ok(Some(path.display().to_string())) 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("Video", |ui| {
ui.menu_button("Screen Size", |ui| { ui.menu_button("Screen Size", |ui| {
let current_dims = self.config.dimensions; let current_dims = self.config.dimensions;
@ -487,7 +334,7 @@ impl GameWindow {
.selectable_button((current_dims - dims).length() < 1.0, label) .selectable_button((current_dims - dims).length() < 1.0, label)
.clicked() .clicked()
{ {
ui.send_viewport_cmd(ViewportCommand::InnerSize(dims)); ctx.send_viewport_cmd(ViewportCommand::InnerSize(dims));
} }
} }
}); });
@ -517,7 +364,7 @@ impl GameWindow {
let new_proportions = display_mode.proportions(); let new_proportions = display_mode.proportions();
let scale = new_proportions / old_proportions; let scale = new_proportions / old_proportions;
if scale != Vec2::new(1.0, 1.0) { 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| { self.update_config(|c| {
@ -554,24 +401,24 @@ impl GameWindow {
}); });
}); });
ui.menu_button("Audio", |ui| { ui.menu_button("Audio", |ui| {
if ui let p1_enabled = self.client.is_audio_enabled(SimId::Player1);
.selectable_button(self.config.audio_enabled, "Enabled") let p2_enabled = self.client.is_audio_enabled(SimId::Player2);
.clicked() if ui.selectable_button(p1_enabled, "Player 1").clicked() {
{ self.client
self.update_config(|c| c.audio_enabled = !c.audio_enabled); .send_command(EmulatorCommand::SetAudioEnabled(!p1_enabled, p2_enabled));
self.client.send_command(EmulatorCommand::SetAudioEnabled( }
self.sim_id, if ui.selectable_button(p2_enabled, "Player 2").clicked() {
self.config.audio_enabled, self.client
)); .send_command(EmulatorCommand::SetAudioEnabled(p1_enabled, !p2_enabled));
} }
}); });
ui.menu_button("Input", |ui| { ui.menu_button("Input", |ui| {
if ui.button("Bind Inputs").clicked() { if ui.button("Bind Inputs").clicked() {
self.open(ChildWindow::Input); self.proxy.send_event(UserEvent::OpenInput).unwrap();
} }
}); });
if ui.button("Hotkeys").clicked() { if ui.button("Hotkeys").clicked() {
self.open(ChildWindow::Hotkeys); self.proxy.send_event(UserEvent::OpenHotkeys).unwrap();
} }
} }
@ -604,11 +451,13 @@ 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(); let mut new_config = self.config.clone();
update(&mut new_config); update(&mut new_config);
if self.config != 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; self.config = new_config;
} }
@ -622,42 +471,59 @@ 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 { 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 { fn sim_id(&self) -> SimId {
self.sim_id self.sim_id
} }
fn initial_viewport(&self) -> ViewportBuilder { fn initial_viewport(&self) -> ViewportBuilder {
let builder = ViewportBuilder::default() ViewportBuilder::default()
.with_title("Lemur") .with_title("Lemur")
.with_inner_size(self.config.dimensions); .with_inner_size(self.config.dimensions)
if let Some(position) = self.config.position {
builder.with_position(position)
} else {
builder
}
} }
fn show(&mut self, ui: &mut Ui) { fn show(&mut self, ctx: &Context) {
self.child_states.load(ui);
let dimensions = { let dimensions = {
let bounds = ui.content_rect(); let bounds = ctx.available_rect();
bounds.max - bounds.min bounds.max - bounds.min
}; };
let position = ui.input(|i| i.viewport().outer_rect.map(|r| r.min)); self.update_config(|c| c.dimensions = dimensions);
self.update_config(|c| {
c.dimensions = dimensions;
c.position = position;
});
while let Ok(toast) = self.messages.try_recv() { if let Some(messages) = self.messages.as_mut() {
while let Ok(toast) = messages.try_recv() {
self.toasts.add(toast); self.toasts.add(toast);
} }
Panel::top("menubar") }
.exact_size(22.0) TopBottomPanel::top("menubar")
.show_inside(ui, |ui| { .exact_height(22.0)
.show(ctx, |ui| {
MenuBar::new().ui(ui, |ui| { MenuBar::new().ui(ui, |ui| {
self.show_menu(ui); self.show_menu(ctx, ui);
}); });
}); });
if self.color_picker.is_some() { if self.color_picker.is_some() {
@ -665,68 +531,33 @@ impl AppWindow for GameWindow {
.title_bar(false) .title_bar(false)
.resizable(false) .resizable(false)
.anchor(Align2::CENTER_CENTER, Vec2::ZERO) .anchor(Align2::CENTER_CENTER, Vec2::ZERO)
.show(ui, |ui| { .show(ctx, |ui| {
self.show_color_picker(ui); self.show_color_picker(ui);
}); });
} }
let frame = Frame::central_panel(ui.style()).fill(Color32::BLACK); let frame = Frame::central_panel(&ctx.style()).fill(Color32::BLACK);
CentralPanel::default().frame(frame).show_inside(ui, |ui| { CentralPanel::default().frame(frame).show(ctx, |ui| {
if let Some(screen) = self.screen.as_mut() { if let Some(screen) = self.screen.as_mut() {
screen.update(self.config.display_mode, self.config.colors); screen.update(self.config.display_mode, self.config.colors);
ui.add(screen); ui.add(screen);
} }
}); });
self.toasts.show(ui); self.toasts.show(ctx);
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));
} }
fn on_init(&mut self, args: InitArgs) { fn on_init(&mut self, _ctx: &Context, render_state: &egui_wgpu::RenderState) {
if self.screen.is_none() { let (screen, sink) = GameScreen::init(render_state);
let (screen, sink) = GameScreen::init(args.render_state); let (message_sink, message_source) = mpsc::channel();
self.client.send_command(EmulatorCommand::ConnectToSim( self.client.send_command(EmulatorCommand::ConnectToSim(
self.sim_id, self.sim_id,
sink, sink,
self.message_sink.clone(), message_sink,
)); ));
self.screen = Some(screen); self.screen = Some(screen);
} self.messages = Some(message_source);
self.window = Some(args.window.clone());
}
} }
impl Drop for GameWindow { fn on_destroy(&mut self) {
fn drop(&mut self) {
if self.sim_id == SimId::Player2 { if self.sim_id == SimId::Player2 {
self.client.send_command(EmulatorCommand::StopSecondSim); self.client.send_command(EmulatorCommand::StopSecondSim);
} }
@ -740,82 +571,9 @@ struct ColorPickerState {
unpause_on_close: bool, unpause_on_close: bool,
} }
struct ChildWindowWrapper { #[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
app: Arc<Mutex<AppWrapper>>, struct GameConfig {
updates: Option<ViewportBuilder>, display_mode: DisplayMode,
close_requested: Arc<AtomicBool>, colors: [Color32; 2],
} dimensions: Vec2,
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,
} }

View File

@ -14,7 +14,7 @@ pub struct GameScreen {
} }
impl 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 device = &render_state.device;
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
@ -53,8 +53,8 @@ impl GameScreen {
let render_pipeline_layout = let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("render pipeline layout"), label: Some("render pipeline layout"),
bind_group_layouts: &[Some(&bind_group_layout)], bind_group_layouts: &[&bind_group_layout],
immediate_size: 0, push_constant_ranges: &[],
}); });
let create_render_pipeline = |entry_point: &str| { let create_render_pipeline = |entry_point: &str| {
@ -92,7 +92,7 @@ impl GameScreen {
mask: !0, mask: !0,
alpha_to_coverage_enabled: false, alpha_to_coverage_enabled: false,
}, },
multiview_mask: None, multiview: None,
cache: None, cache: None,
}) })
}; };
@ -117,6 +117,8 @@ impl GameScreen {
} }
pub fn init(render_state: &egui_wgpu::RenderState) -> (Self, TextureSink) { pub fn init(render_state: &egui_wgpu::RenderState) -> (Self, TextureSink) {
Self::init_pipeline(render_state);
let device = &render_state.device; let device = &render_state.device;
let queue = &render_state.queue; 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 winit::event_loop::EventLoopProxy;
use crate::{ use crate::{
@ -42,6 +42,10 @@ impl GdbServerWindow {
} }
impl AppWindow for GdbServerWindow { impl AppWindow for GdbServerWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of(format!("Debugger-{}", self.sim_id))
}
fn sim_id(&self) -> SimId { fn sim_id(&self) -> SimId {
self.sim_id self.sim_id
} }
@ -52,10 +56,10 @@ impl AppWindow for GdbServerWindow {
.with_inner_size((300.0, 200.0)) .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 port_num: Option<u16> = self.port_str.parse().ok();
let status = self.server.status(); let status = self.server.status();
CentralPanel::default().show_inside(ui, |ui| { CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if port_num.is_none() { if port_num.is_none() {
let style = ui.style_mut(); let style = ui.style_mut();

View File

@ -1,5 +1,6 @@
use egui::{ use egui::{
Button, CentralPanel, Event, KeyboardShortcut, Label, Layout, Slider, Ui, ViewportBuilder, Button, CentralPanel, Context, Event, KeyboardShortcut, Label, Layout, Slider, Ui,
ViewportBuilder, ViewportId,
}; };
use egui_extras::{Column, TableBuilder}; use egui_extras::{Column, TableBuilder};
@ -124,14 +125,18 @@ impl HotkeysWindow {
} }
impl AppWindow for HotkeysWindow { impl AppWindow for HotkeysWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of("shortcuts")
}
fn initial_viewport(&self) -> ViewportBuilder { fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default() ViewportBuilder::default()
.with_title("Keyboard Shortcuts") .with_title("Keyboard Shortcuts")
.with_inner_size((400.0, 400.0)) .with_inner_size((400.0, 400.0))
} }
fn show(&mut self, ui: &mut Ui) { fn show(&mut self, ctx: &Context) {
CentralPanel::default().show_inside(ui, |ui| { CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("Use defaults").clicked() { if ui.button("Use defaults").clicked() {
self.shortcuts.reset(); self.shortcuts.reset();

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

View File

@ -1,14 +1,13 @@
use std::{fs, sync::Arc, time::Duration}; use std::{fs, time::Duration};
use anyhow::Result; use anyhow::Result;
use egui::{Button, CentralPanel, Checkbox, Label, ViewportBuilder}; use egui::{Button, CentralPanel, Checkbox, Label, ViewportBuilder, ViewportId};
use egui_notify::{Anchor, Toast, Toasts}; use egui_notify::{Anchor, Toast, Toasts};
use winit::window::Window;
use crate::{ use crate::{
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId}, emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId},
profiler::{Profiler, ProfilerStatus}, profiler::{Profiler, ProfilerStatus},
window::{AppWindow, InitArgs}, window::AppWindow,
}; };
pub struct ProfileWindow { pub struct ProfileWindow {
@ -16,7 +15,6 @@ pub struct ProfileWindow {
client: EmulatorClient, client: EmulatorClient,
profiler: Profiler, profiler: Profiler,
toasts: Toasts, toasts: Toasts,
window: Option<Arc<Window>>,
} }
impl ProfileWindow { impl ProfileWindow {
@ -29,7 +27,6 @@ impl ProfileWindow {
.with_anchor(Anchor::BottomLeft) .with_anchor(Anchor::BottomLeft)
.with_margin((10.0, 10.0).into()) .with_margin((10.0, 10.0).into())
.reverse(true), .reverse(true),
window: None,
} }
} }
@ -62,16 +59,12 @@ impl ProfileWindow {
fn try_finish_recording(&mut self) -> Result<Option<String>> { fn try_finish_recording(&mut self) -> Result<Option<String>> {
let bytes_receiver = self.profiler.finish_recording(); let bytes_receiver = self.profiler.finish_recording();
let mut file_dialog = rfd::FileDialog::new() let file = rfd::FileDialog::new()
.add_filter("Profiler files", &["json"]) .add_filter("Profiler files", &["json"])
.set_file_name("profile.json"); .set_file_name("profile.json")
if let Some(window) = self.window.as_ref() { .save_file();
file_dialog = file_dialog.set_parent(window);
}
let file = file_dialog.save_file();
if let Some(path) = file { if let Some(path) = file {
let bytes = pollster::block_on(bytes_receiver)?; let bytes = pollster::block_on(bytes_receiver)?;
let _ = fs::remove_file(&path);
fs::write(&path, bytes)?; fs::write(&path, bytes)?;
Ok(Some(path.display().to_string())) Ok(Some(path.display().to_string()))
} else { } else {
@ -82,6 +75,10 @@ impl ProfileWindow {
} }
impl AppWindow for ProfileWindow { impl AppWindow for ProfileWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of(format!("Profile-{}", self.sim_id))
}
fn sim_id(&self) -> SimId { fn sim_id(&self) -> SimId {
self.sim_id self.sim_id
} }
@ -92,10 +89,10 @@ impl AppWindow for ProfileWindow {
.with_inner_size((300.0, 200.0)) .with_inner_size((300.0, 200.0))
} }
fn show(&mut self, ui: &mut egui::Ui) { fn show(&mut self, ctx: &egui::Context) {
let status = self.profiler.status(); let status = self.profiler.status();
let recording = matches!(status, ProfilerStatus::Recording); let recording = matches!(status, ProfilerStatus::Recording);
CentralPanel::default().show_inside(ui, |ui| { CentralPanel::default().show(ctx, |ui| {
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 0.0; ui.spacing_mut().item_spacing.x = 0.0;
ui.add( ui.add(
@ -160,10 +157,6 @@ impl AppWindow for ProfileWindow {
_ => {} _ => {}
} }
}); });
self.toasts.show(ui); self.toasts.show(ctx);
}
fn on_init(&mut self, args: InitArgs) {
self.window = Some(args.window.clone());
} }
} }

View File

@ -1,6 +1,9 @@
use std::{collections::VecDeque, sync::mpsc}; use std::{collections::VecDeque, sync::mpsc};
use egui::{Align, CentralPanel, FontFamily, Label, RichText, ScrollArea, Ui, ViewportBuilder}; use egui::{
Align, CentralPanel, Context, FontFamily, Label, RichText, ScrollArea, ViewportBuilder,
ViewportId,
};
use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; use crate::emulator::{EmulatorClient, EmulatorCommand, SimId};
@ -29,6 +32,10 @@ impl TerminalWindow {
} }
impl AppWindow for TerminalWindow { impl AppWindow for TerminalWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of(format!("terminal-{}", self.sim_id))
}
fn sim_id(&self) -> SimId { fn sim_id(&self) -> SimId {
self.sim_id self.sim_id
} }
@ -39,7 +46,7 @@ impl AppWindow for TerminalWindow {
.with_inner_size((640.0, 480.0)) .with_inner_size((640.0, 480.0))
} }
fn show(&mut self, ui: &mut Ui) { fn show(&mut self, ctx: &Context) {
if let Ok(text) = self.receiver.try_recv() { if let Ok(text) = self.receiver.try_recv() {
let mut rest = text.as_str(); let mut rest = text.as_str();
while let Some(index) = rest.find('\n') { while let Some(index) = rest.find('\n') {
@ -54,7 +61,7 @@ impl AppWindow for TerminalWindow {
} }
self.lines.back_mut().unwrap().push_str(rest); self.lines.back_mut().unwrap().push_str(rest);
} }
CentralPanel::default().show_inside(ui, |ui| { CentralPanel::default().show(ctx, |ui| {
ScrollArea::vertical() ScrollArea::vertical()
.stick_to_bottom(true) .stick_to_bottom(true)
.auto_shrink([false, false]) .auto_shrink([false, false])

View File

@ -1,6 +1,6 @@
use std::{ use std::{
fmt::{Display, UpperHex}, fmt::{Display, UpperHex},
ops::{Bound, Deref, DerefMut, RangeBounds}, ops::{Bound, RangeBounds},
str::FromStr, str::FromStr,
}; };
@ -8,7 +8,7 @@ use atoi::FromRadix16;
use egui::{ use egui::{
Align, Color32, CornerRadius, CursorIcon, Event, Frame, Key, Layout, Margin, Rect, Response, Align, Color32, CornerRadius, CursorIcon, Event, Frame, Key, Layout, Margin, Rect, Response,
RichText, Sense, Shape, Stroke, StrokeKind, TextEdit, Ui, UiBuilder, Vec2, Widget, WidgetText, RichText, Sense, Shape, Stroke, StrokeKind, TextEdit, Ui, UiBuilder, Vec2, Widget, WidgetText,
ecolor::HexColor, util::id_type_map::SerializableAny, ecolor::HexColor,
}; };
use num_traits::{CheckedAdd, CheckedSub, One}; use num_traits::{CheckedAdd, CheckedSub, One};
@ -409,51 +409,3 @@ impl ResponseExt for Response {
self.clicked() || self.dragged() 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 std::sync::Arc;
use egui::{ use egui::{
Align, CentralPanel, Checkbox, Color32, ComboBox, Image, ScrollArea, Slider, TextEdit, Align, CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextEdit,
TextureOptions, Ui, ViewportBuilder, TextureOptions, Ui, ViewportBuilder, ViewportId,
}; };
use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
emulator::SimId, emulator::SimId,
images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader}, images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView}, memory::{MemoryClient, MemoryView},
window::{ window::{
AppWindow, AppWindow,
utils::{NumberEdit, UiData, UiExt}, utils::{NumberEdit, UiExt},
}, },
}; };
use super::utils::{self, CellData, CharacterGrid}; use super::utils::{self, CellData, CharacterGrid};
#[derive(Clone, PartialEq, Serialize, Deserialize)] pub struct BgMapWindow {
struct State { sim_id: SimId,
loader: Arc<ImageTextureLoader>,
memory: Arc<MemoryClient>,
bgmaps: MemoryView,
cell_index: usize, cell_index: usize,
generic_palette: bool, generic_palette: bool,
params: ImageParams<BgMapParams>,
scale: f32, scale: f32,
show_grid: bool, 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 { impl BgMapWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &ImageTextureLoader) -> Self { pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &mut ImageProcessor) -> Self {
let state: UiData<State> = UiData::new();
let renderer = BgMapRenderer::new(sim_id, memory); let renderer = BgMapRenderer::new(sim_id, memory);
let params = images.add( let ([cell, bgmap], params) = images.add(renderer, BgMapParams::default());
sim_id, let loader =
renderer, ImageTextureLoader::new([("vip://cell".into(), cell), ("vip://bgmap".into(), bgmap)]);
BgMapParams {
cell_index: state.cell_index,
generic_palette: state.generic_palette,
},
);
Self { Self {
sim_id, sim_id,
loader: Arc::new(loader),
memory: memory.clone(), memory: memory.clone(),
bgmaps: memory.watch(sim_id, 0x00020000, 0x20000), bgmaps: memory.watch(sim_id, 0x00020000, 0x20000),
cell_index: params.cell_index,
generic_palette: params.generic_palette,
params, params,
state, scale: 1.0,
show_grid: false,
} }
} }
@ -78,11 +61,10 @@ impl BgMapWindow {
ui.label("Map"); ui.label("Map");
}); });
row.col(|ui| { 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)); ui.add(NumberEdit::new(&mut bgmap_index).range(0..16));
if bgmap_index != self.state.cell_index / 4096 { if bgmap_index != self.cell_index / 4096 {
self.state.cell_index = self.cell_index = (bgmap_index * 4096) + (self.cell_index % 4096);
(bgmap_index * 4096) + (self.state.cell_index % 4096);
} }
}); });
}); });
@ -91,7 +73,7 @@ impl BgMapWindow {
ui.label("Cell"); ui.label("Cell");
}); });
row.col(|ui| { 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| { body.row(row_height, |mut row| {
@ -99,7 +81,7 @@ impl BgMapWindow {
ui.label("Address"); ui.label("Address");
}); });
row.col(|ui| { 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}"); let mut address_str = format!("{address:08x}");
ui.add_enabled( ui.add_enabled(
false, 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) .maintain_aspect_ratio(true)
.texture_options(TextureOptions::NEAREST); .texture_options(TextureOptions::NEAREST);
ui.add(image); ui.add(image);
ui.section("Cell", |ui| { 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); let mut cell = CellData::parse(data);
TableBuilder::new(ui) TableBuilder::new(ui)
.column(Column::remainder()) .column(Column::remainder())
@ -156,7 +138,7 @@ impl BgMapWindow {
}); });
}); });
if cell.update(&mut data) { 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); self.memory.write(self.sim_id, address as u32, &data);
} }
}); });
@ -164,33 +146,37 @@ impl BgMapWindow {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Scale"); ui.label("Scale");
ui.spacing_mut().slider_width = ui.available_width(); 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) .step_by(1.0)
.show_value(false); .show_value(false);
ui.add(slider); ui.add(slider);
}); });
ui.checkbox(&mut self.state.show_grid, "Show grid"); ui.checkbox(&mut self.show_grid, "Show grid");
ui.checkbox(&mut self.state.generic_palette, "Generic palette"); ui.checkbox(&mut self.generic_palette, "Generic palette");
}); });
}); });
self.params.write(BgMapParams { self.params.write(BgMapParams {
cell_index: self.state.cell_index, cell_index: self.cell_index,
generic_palette: self.state.generic_palette, generic_palette: self.generic_palette,
}); });
} }
fn show_bgmap(&mut self, ui: &mut Ui) { fn show_bgmap(&mut self, ui: &mut Ui) {
let grid = CharacterGrid::new(self.image_url("bgmap")) let grid = CharacterGrid::new("vip://bgmap")
.with_scale(self.state.scale) .with_scale(self.scale)
.with_grid(self.state.show_grid) .with_grid(self.show_grid)
.with_selected(self.state.cell_index % 4096); .with_selected(self.cell_index % 4096);
if let Some(selected) = grid.show(ui) { 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 { impl AppWindow for BgMapWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of(format!("bgmap-{}", self.sim_id))
}
fn sim_id(&self) -> SimId { fn sim_id(&self) -> SimId {
self.sim_id self.sim_id
} }
@ -201,9 +187,12 @@ impl AppWindow for BgMapWindow {
.with_inner_size((640.0, 480.0)) .with_inner_size((640.0, 480.0))
} }
fn show(&mut self, ui: &mut Ui) { fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) {
self.state.load(ui); ctx.add_texture_loader(self.loader.clone());
CentralPanel::default().show_inside(ui, |ui| { }
fn show(&mut self, ctx: &Context) {
CentralPanel::default().show(ctx, |ui| {
ui.horizontal_top(|ui| { ui.horizontal_top(|ui| {
StripBuilder::new(ui) StripBuilder::new(ui)
.size(Size::relative(0.3).at_most(200.0)) .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 { impl ImageRenderer<2> for BgMapRenderer {
type Params = BgMapParams; type Params = BgMapParams;
fn names(&self) -> [&str; 2] {
["bgmap-cell", "bgmap"]
}
fn sizes(&self) -> [[usize; 2]; 2] { fn sizes(&self) -> [[usize; 2]; 2] {
[[8, 8], [8 * 64, 8 * 64]] [[8, 8], [8 * 64, 8 * 64]]
} }

View File

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

View File

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

View File

@ -1,68 +1,55 @@
use std::sync::Arc; use std::sync::Arc;
use egui::{ use egui::{
Align, CentralPanel, Checkbox, Color32, ComboBox, Image, ScrollArea, Slider, TextEdit, Align, CentralPanel, Checkbox, Color32, ComboBox, Context, Image, ScrollArea, Slider, TextEdit,
TextureOptions, Ui, ViewportBuilder, TextureOptions, Ui, ViewportBuilder, ViewportId,
}; };
use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
emulator::SimId, emulator::SimId,
images::{ImageBuffer, ImageParams, ImageRenderer, ImageTextureLoader}, images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView}, memory::{MemoryClient, MemoryView},
window::{ window::{
AppWindow, AppWindow,
utils::{NumberEdit, UiData, UiExt as _}, utils::{NumberEdit, UiExt as _},
}, },
}; };
use super::utils::{self, Object}; 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 { pub struct ObjectWindow {
sim_id: SimId, sim_id: SimId,
loader: Arc<ImageTextureLoader>,
memory: Arc<MemoryClient>, memory: Arc<MemoryClient>,
objects: MemoryView, objects: MemoryView,
index: usize,
generic_palette: bool,
params: ImageParams<ObjectParams>, params: ImageParams<ObjectParams>,
state: UiData<State>, scale: f32,
} }
impl ObjectWindow { impl ObjectWindow {
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &ImageTextureLoader) -> Self { pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>, images: &mut ImageProcessor) -> Self {
let state: UiData<State> = UiData::new(); let initial_params = ObjectParams {
let renderer = ObjectRenderer::new(sim_id, memory); index: 0,
let params = images.add( generic_palette: false,
sim_id,
renderer,
ObjectParams {
index: state.index,
generic_palette: state.generic_palette,
left_color: Color32::from_rgb(0xff, 0x00, 0x00), left_color: Color32::from_rgb(0xff, 0x00, 0x00),
right_color: Color32::from_rgb(0x00, 0xc6, 0xf0), right_color: Color32::from_rgb(0x00, 0xc6, 0xf0),
}, };
); let renderer = ObjectRenderer::new(sim_id, memory);
let ([zoom, full], params) = images.add(renderer, initial_params);
let loader =
ImageTextureLoader::new([("vip://zoom".into(), zoom), ("vip://full".into(), full)]);
Self { Self {
sim_id, sim_id,
loader: Arc::new(loader),
memory: memory.clone(), memory: memory.clone(),
objects: memory.watch(sim_id, 0x0003e000, 0x2000), objects: memory.watch(sim_id, 0x0003e000, 0x2000),
index: params.index,
generic_palette: params.generic_palette,
params, params,
state, scale: 1.0,
} }
} }
@ -78,7 +65,7 @@ impl ObjectWindow {
ui.label("Index"); ui.label("Index");
}); });
row.col(|ui| { 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| { body.row(row_height, |mut row| {
@ -86,7 +73,7 @@ impl ObjectWindow {
ui.label("Address"); ui.label("Address");
}); });
row.col(|ui| { row.col(|ui| {
let address = 0x3e000 + self.state.index * 8; let address = 0x3e000 + self.index * 8;
let mut address_str = format!("{address:08x}"); let mut address_str = format!("{address:08x}");
ui.add_enabled( ui.add_enabled(
false, 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) .maintain_aspect_ratio(true)
.texture_options(TextureOptions::NEAREST); .texture_options(TextureOptions::NEAREST);
ui.add(image); ui.add(image);
ui.section("Properties", |ui| { 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); let mut obj = Object::parse(object);
TableBuilder::new(ui) TableBuilder::new(ui)
.column(Column::remainder()) .column(Column::remainder())
@ -175,7 +162,7 @@ impl ObjectWindow {
}); });
}); });
if obj.update(&mut object) { 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); self.memory.write(self.sim_id, address as u32, &object);
} }
}); });
@ -183,30 +170,34 @@ impl ObjectWindow {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Scale"); ui.label("Scale");
ui.spacing_mut().slider_width = ui.available_width(); 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) .step_by(1.0)
.show_value(false); .show_value(false);
ui.add(slider); ui.add(slider);
}); });
ui.checkbox(&mut self.state.generic_palette, "Generic palette"); ui.checkbox(&mut self.generic_palette, "Generic palette");
}); });
}); });
self.params.write(ObjectParams { self.params.write(ObjectParams {
index: self.state.index, index: self.index,
generic_palette: self.state.generic_palette, generic_palette: self.generic_palette,
..*self.params ..*self.params
}); });
} }
fn show_object(&mut self, ui: &mut Ui) { fn show_object(&mut self, ui: &mut Ui) {
let image = Image::new(self.image_url("object-full")) let image = Image::new("vip://full")
.fit_to_original_size(self.state.scale) .fit_to_original_size(self.scale)
.texture_options(TextureOptions::NEAREST); .texture_options(TextureOptions::NEAREST);
ui.add(image); ui.add(image);
} }
} }
impl AppWindow for ObjectWindow { impl AppWindow for ObjectWindow {
fn viewport_id(&self) -> egui::ViewportId {
ViewportId::from_hash_of(format!("object-{}", self.sim_id))
}
fn sim_id(&self) -> SimId { fn sim_id(&self) -> SimId {
self.sim_id self.sim_id
} }
@ -217,9 +208,12 @@ impl AppWindow for ObjectWindow {
.with_inner_size((640.0, 500.0)) .with_inner_size((640.0, 500.0))
} }
fn show(&mut self, ui: &mut Ui) { fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) {
self.state.load(ui); ctx.add_texture_loader(self.loader.clone());
CentralPanel::default().show_inside(ui, |ui| { }
fn show(&mut self, ctx: &Context) {
CentralPanel::default().show(ctx, |ui| {
ui.horizontal_top(|ui| { ui.horizontal_top(|ui| {
StripBuilder::new(ui) StripBuilder::new(ui)
.size(Size::relative(0.3).at_most(200.0)) .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 { impl ImageRenderer<2> for ObjectRenderer {
type Params = ObjectParams; type Params = ObjectParams;
fn names(&self) -> [&str; 2] {
["object-zoom", "object-full"]
}
fn sizes(&self) -> [[usize; 2]; 2] { fn sizes(&self) -> [[usize; 2]; 2] {
[[8, 8], [384, 224]] [[8, 8], [384, 224]]
} }

View File

@ -1,8 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use egui::{ use egui::{
Align, Button, CentralPanel, Checkbox, Color32, Direction, Label, Layout, ScrollArea, TextEdit, Align, Button, CentralPanel, Checkbox, Color32, Context, Direction, Label, Layout, ScrollArea,
Ui, ViewportBuilder, TextEdit, Ui, ViewportBuilder, ViewportId,
}; };
use egui_extras::{Column, Size, StripBuilder, TableBuilder}; 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 { impl AppWindow for RegisterWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of(format!("registers-{}", self.sim_id))
}
fn sim_id(&self) -> SimId { fn sim_id(&self) -> SimId {
self.sim_id self.sim_id
} }
@ -640,8 +644,8 @@ impl AppWindow for RegisterWindow {
.with_inner_size((800.0, 480.0)) .with_inner_size((800.0, 480.0))
} }
fn show(&mut self, ui: &mut Ui) { fn show(&mut self, ctx: &Context) {
CentralPanel::default().show_inside(ui, |ui| { CentralPanel::default().show(ctx, |ui| {
ScrollArea::vertical().show(ui, |ui| { ScrollArea::vertical().show(ui, |ui| {
ui.horizontal_top(|ui| { ui.horizontal_top(|ui| {
let width = ui.available_width(); let width = ui.available_width();

View File

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