657 lines
22 KiB
Rust
657 lines
22 KiB
Rust
use std::{
|
|
collections::hash_map::Entry,
|
|
num::NonZero,
|
|
sync::Arc,
|
|
thread,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use egui::{
|
|
Context, FontData, FontDefinitions, FontFamily, IconData, PlatformOutput, RawInput,
|
|
TextWrapMode, ViewportBuilder, ViewportCommand, ViewportEvent, ViewportId, ViewportIdMap,
|
|
ViewportIdSet, ViewportInfo, style::ScrollStyle,
|
|
};
|
|
use egui_wgpu::winit::Painter;
|
|
use egui_winit::EventResponse;
|
|
use gilrs::{EventType, Gilrs};
|
|
use tracing::{error, warn};
|
|
use winit::{
|
|
application::ApplicationHandler,
|
|
event::WindowEvent,
|
|
event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy},
|
|
window::{Window, WindowId},
|
|
};
|
|
|
|
use crate::{
|
|
config::CliArgs,
|
|
controller::ControllerManager,
|
|
emulator::{EmulatorClient, EmulatorCommand, SimId},
|
|
images::ImageTextureLoader,
|
|
input::{MappingProvider, ShortcutProvider},
|
|
memory::MemoryClient,
|
|
persistence::Persistence,
|
|
window::{AppWindow, ChildWindow, GameScreen, GameWindow, InitArgs},
|
|
};
|
|
|
|
const EGUI_FILENAME: &str = "egui";
|
|
|
|
fn load_icon() -> anyhow::Result<IconData> {
|
|
let bytes = include_bytes!("../assets/lemur-256x256.png");
|
|
let img = image::load_from_memory_with_format(bytes, image::ImageFormat::Png)?;
|
|
let rgba = img.into_rgba8();
|
|
Ok(IconData {
|
|
width: rgba.width(),
|
|
height: rgba.height(),
|
|
rgba: rgba.into_vec(),
|
|
})
|
|
}
|
|
|
|
struct SharedViewportState {
|
|
viewport_info: ViewportIdMap<ViewportInfo>,
|
|
painter: Painter,
|
|
resized_viewport: Option<ViewportId>,
|
|
}
|
|
|
|
pub struct Application {
|
|
client: EmulatorClient,
|
|
persistence: Persistence,
|
|
ctx: Context,
|
|
shared: SharedViewportState,
|
|
icon: Option<Arc<IconData>>,
|
|
app: GameWindow,
|
|
controllers: ControllerManager,
|
|
viewports: ViewportIdMap<ViewportManager>,
|
|
focused: Option<ViewportId>,
|
|
redraw_times: ViewportIdMap<Instant>,
|
|
initial_windows: Vec<ChildWindow>,
|
|
}
|
|
|
|
impl Application {
|
|
pub fn new(
|
|
client: EmulatorClient,
|
|
proxy: EventLoopProxy<UserEvent>,
|
|
persistence: Persistence,
|
|
args: CliArgs,
|
|
) -> Self {
|
|
let wgpu = WgpuState::new();
|
|
let icon = load_icon().ok().map(Arc::new);
|
|
let mappings = MappingProvider::new(persistence.clone(), args.player2_controller);
|
|
let shortcuts = ShortcutProvider::new(persistence.clone());
|
|
let controllers = ControllerManager::new(client.clone(), &mappings);
|
|
let memory = Arc::new(MemoryClient::new(client.clone()));
|
|
let images = Arc::new(ImageTextureLoader::new());
|
|
{
|
|
let mappings = mappings.clone();
|
|
let proxy = proxy.clone();
|
|
thread::spawn(|| process_gamepad_input(mappings, proxy));
|
|
}
|
|
let app = GameWindow::new(
|
|
ViewportId::ROOT,
|
|
client.clone(),
|
|
proxy.clone(),
|
|
persistence.clone(),
|
|
shortcuts.clone(),
|
|
&memory,
|
|
&images,
|
|
mappings.clone(),
|
|
SimId::Player1,
|
|
);
|
|
|
|
let ctx = Context::default();
|
|
let data = persistence.load_config(EGUI_FILENAME).unwrap_or_default();
|
|
ctx.data_mut(|d| *d = data);
|
|
let mut fonts = FontDefinitions::default();
|
|
fonts.font_data.insert(
|
|
"Selawik".into(),
|
|
Arc::new(FontData::from_static(include_bytes!(
|
|
"../assets/selawik.ttf"
|
|
))),
|
|
);
|
|
fonts
|
|
.families
|
|
.get_mut(&FontFamily::Proportional)
|
|
.unwrap()
|
|
.insert(0, "Selawik".into());
|
|
ctx.set_fonts(fonts);
|
|
ctx.global_style_mut(|s| {
|
|
s.wrap_mode = Some(TextWrapMode::Extend);
|
|
s.visuals.menu_corner_radius = Default::default();
|
|
s.spacing.scroll = ScrollStyle::thin();
|
|
});
|
|
egui_extras::install_image_loaders(&ctx);
|
|
ctx.add_texture_loader(images.clone());
|
|
ctx.set_embed_viewports(false);
|
|
{
|
|
let proxy = proxy.clone();
|
|
ctx.set_request_repaint_callback(move |info| {
|
|
let _ = proxy.send_event(UserEvent::RequestRedraw(
|
|
info.viewport_id,
|
|
Instant::now() + info.delay,
|
|
));
|
|
});
|
|
}
|
|
|
|
let wgpu_config = egui_wgpu::WgpuConfiguration {
|
|
present_mode: wgpu::PresentMode::AutoVsync,
|
|
wgpu_setup: egui_wgpu::WgpuSetup::Existing(egui_wgpu::WgpuSetupExisting {
|
|
instance: wgpu.instance.clone(),
|
|
adapter: wgpu.adapter.clone(),
|
|
device: wgpu.device.clone(),
|
|
queue: wgpu.queue.clone(),
|
|
}),
|
|
..egui_wgpu::WgpuConfiguration::default()
|
|
};
|
|
|
|
let options = egui_wgpu::RendererOptions::default();
|
|
let painter = pollster::block_on(Painter::new(ctx.clone(), wgpu_config, false, options));
|
|
|
|
let mut initial_windows = vec![];
|
|
if let Some(port) = args.debug_port {
|
|
initial_windows.push(ChildWindow::Debugger { port: Some(port) });
|
|
}
|
|
if args.profile {
|
|
initial_windows.push(ChildWindow::Profiler { launch: true });
|
|
}
|
|
if args.character_data {
|
|
initial_windows.push(ChildWindow::CharacterData);
|
|
}
|
|
if args.bgmap_data {
|
|
initial_windows.push(ChildWindow::BgMap);
|
|
}
|
|
if args.object_data {
|
|
initial_windows.push(ChildWindow::Objects);
|
|
}
|
|
if args.worlds {
|
|
initial_windows.push(ChildWindow::Worlds);
|
|
}
|
|
if args.frame_buffers {
|
|
initial_windows.push(ChildWindow::FrameBuffers);
|
|
}
|
|
if args.registers {
|
|
initial_windows.push(ChildWindow::Registers);
|
|
}
|
|
if args.terminal {
|
|
initial_windows.push(ChildWindow::Terminal);
|
|
}
|
|
if args.player2 {
|
|
client.send_command(EmulatorCommand::StartSecondSim(args.rom.clone()));
|
|
initial_windows.push(ChildWindow::Player2);
|
|
}
|
|
|
|
Self {
|
|
client,
|
|
persistence,
|
|
ctx,
|
|
shared: SharedViewportState {
|
|
viewport_info: ViewportIdMap::default(),
|
|
painter,
|
|
resized_viewport: None,
|
|
},
|
|
icon,
|
|
app,
|
|
controllers,
|
|
viewports: ViewportIdMap::default(),
|
|
focused: None,
|
|
redraw_times: ViewportIdMap::default(),
|
|
initial_windows,
|
|
}
|
|
}
|
|
|
|
fn check_repaint(&mut self, event_loop: &ActiveEventLoop) {
|
|
let now = Instant::now();
|
|
self.redraw_times.retain(|viewport_id, time| {
|
|
if *time > now {
|
|
return true;
|
|
}
|
|
if let Some(viewport) = self.viewports.get(viewport_id) {
|
|
viewport.window.request_redraw();
|
|
}
|
|
false
|
|
});
|
|
if let Some(next_repaint_time) = self.redraw_times.values().min().copied() {
|
|
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
|
|
}
|
|
}
|
|
|
|
fn 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 {
|
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
|
let mut viewport_builder = self.app.initial_viewport();
|
|
if let Some(icon) = &self.icon {
|
|
viewport_builder = viewport_builder.with_icon(icon.clone());
|
|
}
|
|
let manager = ViewportManager::new(
|
|
&self.ctx,
|
|
ViewportId::ROOT,
|
|
ViewportId::ROOT,
|
|
event_loop,
|
|
viewport_builder,
|
|
&mut self.shared,
|
|
);
|
|
let render_state = self.shared.painter.render_state().unwrap();
|
|
GameScreen::init_pipeline(&render_state);
|
|
self.app.handle_init(
|
|
ViewportId::ROOT,
|
|
InitArgs {
|
|
window: &manager.window,
|
|
render_state: &render_state,
|
|
},
|
|
);
|
|
self.viewports.insert(ViewportId::ROOT, manager);
|
|
for window in std::mem::take(&mut self.initial_windows) {
|
|
self.app.open(window);
|
|
}
|
|
}
|
|
|
|
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
|
if let Some(viewport) = self.viewports.get(&ViewportId::ROOT) {
|
|
viewport.window.request_redraw();
|
|
}
|
|
}
|
|
|
|
fn window_event(
|
|
&mut self,
|
|
event_loop: &ActiveEventLoop,
|
|
window_id: WindowId,
|
|
event: WindowEvent,
|
|
) {
|
|
let Some(viewport) = self
|
|
.viewports
|
|
.values_mut()
|
|
.find(|v| v.has_window_id(window_id))
|
|
else {
|
|
return;
|
|
};
|
|
let viewport_id = viewport.id;
|
|
let (response, close_requested) = viewport.on_window_event(&event, &mut self.shared);
|
|
if close_requested && viewport_id == ViewportId::ROOT {
|
|
event_loop.exit();
|
|
}
|
|
if !response.consumed {
|
|
match event {
|
|
WindowEvent::KeyboardInput { event, .. } => {
|
|
if !self.app.handle_key_event(viewport_id, &event) {
|
|
self.controllers.handle_key_event(&event);
|
|
}
|
|
}
|
|
WindowEvent::Focused(new_focused) => {
|
|
self.focused = new_focused.then_some(viewport_id);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
if response.repaint {
|
|
self.repaint_all(event_loop);
|
|
}
|
|
self.check_repaint(event_loop);
|
|
}
|
|
|
|
fn device_event(
|
|
&mut self,
|
|
_event_loop: &ActiveEventLoop,
|
|
_device_id: winit::event::DeviceId,
|
|
event: winit::event::DeviceEvent,
|
|
) {
|
|
if let winit::event::DeviceEvent::MouseMotion { delta } = event {
|
|
let Some(viewport) = self
|
|
.focused
|
|
.as_ref()
|
|
.and_then(|id| self.viewports.get_mut(id))
|
|
else {
|
|
return;
|
|
};
|
|
viewport.on_mouse_motion(delta);
|
|
}
|
|
}
|
|
|
|
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
|
|
match event {
|
|
UserEvent::GamepadEvent(event) => {
|
|
if !self
|
|
.focused
|
|
.is_some_and(|id| self.app.handle_gamepad_event(id, &event))
|
|
{
|
|
self.controllers.handle_gamepad_event(&event);
|
|
}
|
|
}
|
|
UserEvent::Quit(sim_id) => match sim_id {
|
|
SimId::Player1 => event_loop.exit(),
|
|
SimId::Player2 => self.app.close(ChildWindow::Player2),
|
|
},
|
|
UserEvent::RequestRedraw(viewport, when) => {
|
|
let scheduled = self.redraw_times.entry(viewport).or_insert(when);
|
|
if *scheduled > when {
|
|
*scheduled = when;
|
|
}
|
|
}
|
|
}
|
|
self.check_repaint(event_loop);
|
|
}
|
|
|
|
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
|
|
if let Err(error) = self
|
|
.ctx
|
|
.data(|d| self.persistence.save_config(EGUI_FILENAME, d))
|
|
{
|
|
error!(%error, "could not save egui state.");
|
|
}
|
|
let (sender, receiver) = oneshot::channel();
|
|
if self.client.send_command(EmulatorCommand::Exit(sender))
|
|
&& let Err(error) = receiver.recv_timeout(Duration::from_secs(5))
|
|
{
|
|
error!(%error, "could not gracefully exit.");
|
|
}
|
|
}
|
|
}
|
|
|
|
struct WgpuState {
|
|
instance: wgpu::Instance,
|
|
adapter: wgpu::Adapter,
|
|
device: wgpu::Device,
|
|
queue: wgpu::Queue,
|
|
}
|
|
|
|
impl WgpuState {
|
|
fn new() -> Self {
|
|
#[allow(unused_variables)]
|
|
let egui_wgpu::WgpuConfiguration {
|
|
wgpu_setup:
|
|
egui_wgpu::WgpuSetup::CreateNew(egui_wgpu::WgpuSetupCreateNew {
|
|
instance_descriptor: wgpu::InstanceDescriptor { backends, .. },
|
|
device_descriptor,
|
|
..
|
|
}),
|
|
..
|
|
} = egui_wgpu::WgpuConfiguration::default()
|
|
else {
|
|
panic!("required fields not found")
|
|
};
|
|
#[cfg(windows)]
|
|
let backends = wgpu::Backends::from_env()
|
|
.unwrap_or((wgpu::Backends::PRIMARY | wgpu::Backends::GL) - wgpu::Backends::VULKAN);
|
|
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
|
backends,
|
|
..wgpu::InstanceDescriptor::new_without_display_handle_from_env()
|
|
});
|
|
|
|
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
|
|
power_preference:
|
|
wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::LowPower),
|
|
compatible_surface: None,
|
|
force_fallback_adapter: false,
|
|
}))
|
|
.expect("could not create adapter");
|
|
|
|
let (device, queue) =
|
|
pollster::block_on(adapter.request_device(&(*device_descriptor)(&adapter)))
|
|
.expect("could not request device");
|
|
Self {
|
|
instance,
|
|
adapter,
|
|
device,
|
|
queue,
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ViewportManager {
|
|
id: ViewportId,
|
|
window: Arc<Window>,
|
|
state: egui_winit::State,
|
|
builder: ViewportBuilder,
|
|
}
|
|
impl ViewportManager {
|
|
fn new(
|
|
ctx: &Context,
|
|
viewport_id: ViewportId,
|
|
parent_id: ViewportId,
|
|
event_loop: &ActiveEventLoop,
|
|
builder: ViewportBuilder,
|
|
shared: &mut SharedViewportState,
|
|
) -> Self {
|
|
let window = Arc::new(egui_winit::create_window(ctx, event_loop, &builder).unwrap());
|
|
pollster::block_on(shared.painter.set_window(viewport_id, Some(window.clone()))).unwrap();
|
|
let state = egui_winit::State::new(
|
|
ctx.clone(),
|
|
viewport_id,
|
|
event_loop,
|
|
Some(window.scale_factor() as f32),
|
|
event_loop.system_theme(),
|
|
shared.painter.max_texture_side(),
|
|
);
|
|
|
|
let mut info = ViewportInfo {
|
|
parent: Some(parent_id),
|
|
..ViewportInfo::default()
|
|
};
|
|
egui_winit::update_viewport_info(&mut info, ctx, &window, true);
|
|
shared.viewport_info.insert(viewport_id, info);
|
|
|
|
Self {
|
|
id: viewport_id,
|
|
window,
|
|
state,
|
|
builder,
|
|
}
|
|
}
|
|
|
|
fn has_window_id(&self, window_id: WindowId) -> bool {
|
|
self.window.id() == window_id
|
|
}
|
|
|
|
fn on_mouse_motion(&mut self, delta: (f64, f64)) {
|
|
self.state.on_mouse_motion(delta);
|
|
}
|
|
|
|
fn on_window_event(
|
|
&mut self,
|
|
event: &WindowEvent,
|
|
shared: &mut SharedViewportState,
|
|
) -> (EventResponse, bool) {
|
|
if let WindowEvent::Resized(size) = event
|
|
&& let (Some(width), Some(height)) =
|
|
(NonZero::new(size.width), NonZero::new(size.height))
|
|
{
|
|
if shared.resized_viewport != Some(self.id) {
|
|
shared.resized_viewport = Some(self.id);
|
|
shared.painter.on_window_resize_state_change(self.id, true);
|
|
}
|
|
shared.painter.on_window_resized(self.id, width, height);
|
|
}
|
|
let response = self.state.on_window_event(&self.window, event);
|
|
let info = shared.viewport_info.get_mut(&self.id).unwrap();
|
|
if let WindowEvent::CloseRequested = event {
|
|
info.events.push(ViewportEvent::Close);
|
|
}
|
|
egui_winit::update_viewport_info(info, self.state.egui_ctx(), &self.window, false);
|
|
(response, info.close_requested())
|
|
}
|
|
|
|
fn take_egui_input(&mut self) -> RawInput {
|
|
self.state.take_egui_input(&self.window)
|
|
}
|
|
|
|
fn handle_platform_output(&mut self, platform_output: PlatformOutput) {
|
|
self.state
|
|
.handle_platform_output(&self.window, platform_output);
|
|
}
|
|
|
|
fn process_viewport_commands(
|
|
&self,
|
|
commands: Vec<ViewportCommand>,
|
|
shared: &mut SharedViewportState,
|
|
) {
|
|
let info = shared.viewport_info.get_mut(&self.id).unwrap();
|
|
egui_winit::process_viewport_commands(
|
|
self.state.egui_ctx(),
|
|
info,
|
|
commands,
|
|
&self.window,
|
|
&mut vec![],
|
|
);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum UserEvent {
|
|
GamepadEvent(gilrs::Event),
|
|
Quit(SimId),
|
|
RequestRedraw(ViewportId, Instant),
|
|
}
|
|
|
|
fn process_gamepad_input(mappings: MappingProvider, proxy: EventLoopProxy<UserEvent>) {
|
|
let mut gilrs = match Gilrs::new() {
|
|
Ok(gilrs) => gilrs,
|
|
Err(error) => {
|
|
warn!(%error, "could not connect gamepad listener");
|
|
return;
|
|
}
|
|
};
|
|
while let Some(event) = gilrs.next_event_blocking(None) {
|
|
if event.event == EventType::Connected {
|
|
let Some(gamepad) = gilrs.connected_gamepad(event.id) else {
|
|
continue;
|
|
};
|
|
mappings.handle_gamepad_connect(&gamepad);
|
|
}
|
|
if event.event == EventType::Disconnected {
|
|
mappings.handle_gamepad_disconnect(event.id);
|
|
}
|
|
if proxy.send_event(UserEvent::GamepadEvent(event)).is_err() {
|
|
// main thread has closed! we done
|
|
return;
|
|
}
|
|
}
|
|
}
|