Implement multiplayer #2
			
				
			
		
		
		
	
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										15
									
								
								Cargo.toml
								
								
								
								
							
							
						
						
									
										15
									
								
								Cargo.toml
								
								
								
								
							| 
						 | 
				
			
			@ -8,21 +8,26 @@ anyhow = "1"
 | 
			
		|||
bitflags = "2"
 | 
			
		||||
bytemuck = { version = "1", features = ["derive"] }
 | 
			
		||||
clap = { version = "4", features = ["derive"] }
 | 
			
		||||
cpal = "0.15"
 | 
			
		||||
imgui = { version = "0.12", features = ["tables-api"] }
 | 
			
		||||
imgui-wgpu = { git = "https://github.com/Yatekii/imgui-wgpu-rs", rev = "2edd348" }
 | 
			
		||||
imgui-winit-support = "0.13"
 | 
			
		||||
cpal = { git = "https://github.com/sidit77/cpal.git", rev = "66ed6be" }
 | 
			
		||||
egui = "0.29"
 | 
			
		||||
egui_extras = "0.29"
 | 
			
		||||
egui-winit = "0.29"
 | 
			
		||||
egui-wgpu = { version = "0.29", features = ["winit"] }
 | 
			
		||||
gilrs = "0.11"
 | 
			
		||||
itertools = "0.13"
 | 
			
		||||
native-dialog = "0.7"
 | 
			
		||||
num-derive = "0.4"
 | 
			
		||||
num-traits = "0.2"
 | 
			
		||||
pollster = "0.4"
 | 
			
		||||
rfd = "0.15"
 | 
			
		||||
rtrb = "0.3"
 | 
			
		||||
rubato = "0.16"
 | 
			
		||||
thread-priority = "1"
 | 
			
		||||
wgpu = "22.1"
 | 
			
		||||
winit = "0.30"
 | 
			
		||||
 | 
			
		||||
[target.'cfg(windows)'.dependencies]
 | 
			
		||||
windows = { version = "0.58", features = ["Win32_System_Threading"] }
 | 
			
		||||
 | 
			
		||||
[build-dependencies]
 | 
			
		||||
cc = "1"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										4
									
								
								build.rs
								
								
								
								
							
							
						
						
									
										4
									
								
								build.rs
								
								
								
								
							| 
						 | 
				
			
			@ -5,8 +5,10 @@ fn main() {
 | 
			
		|||
    cc::Build::new()
 | 
			
		||||
        .include(Path::new("shrooms-vb-core/core"))
 | 
			
		||||
        .opt_level(2)
 | 
			
		||||
        .flag_if_supported("-flto")
 | 
			
		||||
        .flag_if_supported("-fno-strict-aliasing")
 | 
			
		||||
        .define("VB_LITTLE_ENDIAN", None)
 | 
			
		||||
        .define("VB_SIGNED_PROPAGATE", None)
 | 
			
		||||
        .define("VB_DIV_GENERIC", None)
 | 
			
		||||
        .file(Path::new("shrooms-vb-core/core/vb.c"))
 | 
			
		||||
        .compile("vb");
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
Subproject commit ae22c95dbee3d0b338168bfdf98143e6eddc6c70
 | 
			
		||||
Subproject commit 18b2c589e6cacec5a0bd0f450cedf2f8fe3a2bc8
 | 
			
		||||
							
								
								
									
										391
									
								
								src/app.rs
								
								
								
								
							
							
						
						
									
										391
									
								
								src/app.rs
								
								
								
								
							| 
						 | 
				
			
			@ -1,131 +1,368 @@
 | 
			
		|||
use std::{
 | 
			
		||||
    collections::HashMap,
 | 
			
		||||
    fmt::Debug,
 | 
			
		||||
    sync::{Arc, RwLock},
 | 
			
		||||
};
 | 
			
		||||
use std::{collections::HashSet, num::NonZero, sync::Arc, thread};
 | 
			
		||||
 | 
			
		||||
use game::GameWindow;
 | 
			
		||||
use egui::{
 | 
			
		||||
    ahash::{HashMap, HashMapExt},
 | 
			
		||||
    Context, FontData, FontDefinitions, FontFamily, TextWrapMode, ViewportBuilder, ViewportCommand,
 | 
			
		||||
    ViewportId, ViewportInfo,
 | 
			
		||||
};
 | 
			
		||||
use gilrs::{EventType, Gilrs};
 | 
			
		||||
use winit::{
 | 
			
		||||
    application::ApplicationHandler,
 | 
			
		||||
    event::{Event, WindowEvent},
 | 
			
		||||
    event::WindowEvent,
 | 
			
		||||
    event_loop::{ActiveEventLoop, EventLoopProxy},
 | 
			
		||||
    window::WindowId,
 | 
			
		||||
    window::Window,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    controller::ControllerState,
 | 
			
		||||
    emulator::{EmulatorClient, EmulatorCommand},
 | 
			
		||||
    input::InputMapper,
 | 
			
		||||
    controller::ControllerManager,
 | 
			
		||||
    emulator::{EmulatorClient, SimId},
 | 
			
		||||
    input::MappingProvider,
 | 
			
		||||
    window::{AppWindow, GameWindow, InputWindow},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
mod common;
 | 
			
		||||
mod game;
 | 
			
		||||
mod input;
 | 
			
		||||
 | 
			
		||||
pub struct App {
 | 
			
		||||
    windows: HashMap<WindowId, Box<dyn AppWindow>>,
 | 
			
		||||
pub struct Application {
 | 
			
		||||
    client: EmulatorClient,
 | 
			
		||||
    input_mapper: Arc<RwLock<InputMapper>>,
 | 
			
		||||
    controller: ControllerState,
 | 
			
		||||
    proxy: EventLoopProxy<UserEvent>,
 | 
			
		||||
    mappings: MappingProvider,
 | 
			
		||||
    controllers: ControllerManager,
 | 
			
		||||
    viewports: HashMap<ViewportId, Viewport>,
 | 
			
		||||
    focused: Option<ViewportId>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl App {
 | 
			
		||||
impl Application {
 | 
			
		||||
    pub fn new(client: EmulatorClient, proxy: EventLoopProxy<UserEvent>) -> Self {
 | 
			
		||||
        let input_mapper = Arc::new(RwLock::new(InputMapper::new()));
 | 
			
		||||
        let controller = ControllerState::new(input_mapper.clone());
 | 
			
		||||
        Self {
 | 
			
		||||
            windows: HashMap::new(),
 | 
			
		||||
            client,
 | 
			
		||||
            input_mapper,
 | 
			
		||||
            controller,
 | 
			
		||||
            proxy,
 | 
			
		||||
        let mappings = MappingProvider::new();
 | 
			
		||||
        let controllers = ControllerManager::new(client.clone(), &mappings);
 | 
			
		||||
        {
 | 
			
		||||
            let mappings = mappings.clone();
 | 
			
		||||
            let proxy = proxy.clone();
 | 
			
		||||
            thread::spawn(|| process_gamepad_input(mappings, proxy));
 | 
			
		||||
        }
 | 
			
		||||
        Self {
 | 
			
		||||
            client,
 | 
			
		||||
            proxy,
 | 
			
		||||
            mappings,
 | 
			
		||||
            controllers,
 | 
			
		||||
            viewports: HashMap::new(),
 | 
			
		||||
            focused: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn open(&mut self, event_loop: &ActiveEventLoop, window: Box<dyn AppWindow>) {
 | 
			
		||||
        let viewport_id = window.viewport_id();
 | 
			
		||||
        if self.viewports.contains_key(&viewport_id) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        self.viewports
 | 
			
		||||
            .insert(viewport_id, Viewport::new(event_loop, window));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ApplicationHandler<UserEvent> for App {
 | 
			
		||||
impl ApplicationHandler<UserEvent> for Application {
 | 
			
		||||
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
 | 
			
		||||
        let mut window = GameWindow::new(
 | 
			
		||||
            event_loop,
 | 
			
		||||
            self.client.clone(),
 | 
			
		||||
            self.input_mapper.clone(),
 | 
			
		||||
            self.proxy.clone(),
 | 
			
		||||
        );
 | 
			
		||||
        window.init();
 | 
			
		||||
        self.windows.insert(window.id(), Box::new(window));
 | 
			
		||||
        let app = GameWindow::new(self.client.clone(), self.proxy.clone(), SimId::Player1);
 | 
			
		||||
        let wrapper = Viewport::new(event_loop, Box::new(app));
 | 
			
		||||
        self.focused = Some(wrapper.id());
 | 
			
		||||
        self.viewports.insert(wrapper.id(), wrapper);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn window_event(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        event_loop: &ActiveEventLoop,
 | 
			
		||||
        window_id: WindowId,
 | 
			
		||||
        window_id: winit::window::WindowId,
 | 
			
		||||
        event: WindowEvent,
 | 
			
		||||
    ) {
 | 
			
		||||
        if let WindowEvent::KeyboardInput { event, .. } = &event {
 | 
			
		||||
            if self.controller.key_event(event) {
 | 
			
		||||
                self.client
 | 
			
		||||
                    .send_command(EmulatorCommand::SetKeys(self.controller.pressed()));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        let Some(window) = self.windows.get_mut(&window_id) else {
 | 
			
		||||
        let Some(viewport) = self
 | 
			
		||||
            .viewports
 | 
			
		||||
            .values_mut()
 | 
			
		||||
            .find(|v| v.window.id() == window_id)
 | 
			
		||||
        else {
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        window.handle_event(event_loop, &Event::WindowEvent { window_id, event });
 | 
			
		||||
        let viewport_id = viewport.id();
 | 
			
		||||
        match &event {
 | 
			
		||||
            WindowEvent::KeyboardInput { event, .. } => {
 | 
			
		||||
                self.controllers.handle_key_event(event);
 | 
			
		||||
                viewport.app.handle_key_event(event);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
 | 
			
		||||
        match event {
 | 
			
		||||
            UserEvent::OpenWindow(mut window) => {
 | 
			
		||||
                window.init();
 | 
			
		||||
                self.windows.insert(window.id(), window);
 | 
			
		||||
            WindowEvent::Focused(new_focused) => {
 | 
			
		||||
                self.focused = new_focused.then_some(viewport_id);
 | 
			
		||||
            }
 | 
			
		||||
            UserEvent::CloseWindow(window_id) => {
 | 
			
		||||
                self.windows.remove(&window_id);
 | 
			
		||||
            _ => {}
 | 
			
		||||
        }
 | 
			
		||||
        let mut queue_redraw = false;
 | 
			
		||||
        let mut inactive_viewports = HashSet::new();
 | 
			
		||||
        match viewport.on_window_event(event) {
 | 
			
		||||
            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(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        event_loop: &ActiveEventLoop,
 | 
			
		||||
        device_id: winit::event::DeviceId,
 | 
			
		||||
        _event_loop: &ActiveEventLoop,
 | 
			
		||||
        _device_id: winit::event::DeviceId,
 | 
			
		||||
        event: winit::event::DeviceEvent,
 | 
			
		||||
    ) {
 | 
			
		||||
        for window in self.windows.values_mut() {
 | 
			
		||||
            window.handle_event(
 | 
			
		||||
                event_loop,
 | 
			
		||||
                &Event::DeviceEvent {
 | 
			
		||||
                    device_id,
 | 
			
		||||
                    event: event.clone(),
 | 
			
		||||
                },
 | 
			
		||||
        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.state.on_mouse_motion(delta);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
 | 
			
		||||
        match event {
 | 
			
		||||
            UserEvent::GamepadEvent(event) => {
 | 
			
		||||
                self.controllers.handle_gamepad_event(&event);
 | 
			
		||||
                let Some(viewport) = self
 | 
			
		||||
                    .focused
 | 
			
		||||
                    .as_ref()
 | 
			
		||||
                    .and_then(|id| self.viewports.get_mut(id))
 | 
			
		||||
                else {
 | 
			
		||||
                    return;
 | 
			
		||||
                };
 | 
			
		||||
                viewport.app.handle_gamepad_event(&event);
 | 
			
		||||
            }
 | 
			
		||||
            UserEvent::OpenInput => {
 | 
			
		||||
                let input = InputWindow::new(self.mappings.clone());
 | 
			
		||||
                self.open(event_loop, Box::new(input));
 | 
			
		||||
            }
 | 
			
		||||
            UserEvent::OpenPlayer2 => {
 | 
			
		||||
                let p2 = GameWindow::new(self.client.clone(), self.proxy.clone(), SimId::Player2);
 | 
			
		||||
                self.open(event_loop, Box::new(p2));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
 | 
			
		||||
        if let Some(viewport) = self.viewports.get(&ViewportId::ROOT) {
 | 
			
		||||
            viewport.window.request_redraw();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct Viewport {
 | 
			
		||||
    painter: egui_wgpu::winit::Painter,
 | 
			
		||||
    ctx: Context,
 | 
			
		||||
    info: ViewportInfo,
 | 
			
		||||
    commands: Vec<ViewportCommand>,
 | 
			
		||||
    builder: ViewportBuilder,
 | 
			
		||||
    window: Arc<Window>,
 | 
			
		||||
    state: egui_winit::State,
 | 
			
		||||
    app: Box<dyn AppWindow>,
 | 
			
		||||
}
 | 
			
		||||
impl Viewport {
 | 
			
		||||
    pub fn new(event_loop: &ActiveEventLoop, mut app: Box<dyn AppWindow>) -> Self {
 | 
			
		||||
        let mut painter = egui_wgpu::winit::Painter::new(
 | 
			
		||||
            egui_wgpu::WgpuConfiguration::default(),
 | 
			
		||||
            1,
 | 
			
		||||
            None,
 | 
			
		||||
            false,
 | 
			
		||||
            true,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        let ctx = Context::default();
 | 
			
		||||
        let mut fonts = FontDefinitions::empty();
 | 
			
		||||
        fonts.font_data.insert(
 | 
			
		||||
            "Selawik".into(),
 | 
			
		||||
            FontData::from_static(include_bytes!("../assets/selawik.ttf")),
 | 
			
		||||
        );
 | 
			
		||||
        fonts
 | 
			
		||||
            .families
 | 
			
		||||
            .get_mut(&FontFamily::Proportional)
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .insert(0, "Selawik".into());
 | 
			
		||||
        ctx.set_fonts(fonts);
 | 
			
		||||
        ctx.style_mut(|s| {
 | 
			
		||||
            s.wrap_mode = Some(TextWrapMode::Extend);
 | 
			
		||||
            s.visuals.menu_rounding = Default::default();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let mut info = ViewportInfo::default();
 | 
			
		||||
        let builder = app.initial_viewport();
 | 
			
		||||
        let (window, state) = create_window_and_state(&ctx, event_loop, &builder, &mut painter);
 | 
			
		||||
        egui_winit::update_viewport_info(&mut info, &ctx, &window, true);
 | 
			
		||||
 | 
			
		||||
        app.on_init(painter.render_state().as_ref().unwrap());
 | 
			
		||||
        Self {
 | 
			
		||||
            painter,
 | 
			
		||||
            ctx,
 | 
			
		||||
            info,
 | 
			
		||||
            commands: vec![],
 | 
			
		||||
            builder,
 | 
			
		||||
            window,
 | 
			
		||||
            state,
 | 
			
		||||
            app,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
 | 
			
		||||
        for window in self.windows.values_mut() {
 | 
			
		||||
            window.handle_event(event_loop, &Event::AboutToWait);
 | 
			
		||||
    pub fn id(&self) -> ViewportId {
 | 
			
		||||
        self.app.viewport_id()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn on_window_event(&mut self, event: WindowEvent) -> Option<Action> {
 | 
			
		||||
        let response = self.state.on_window_event(&self.window, &event);
 | 
			
		||||
        egui_winit::update_viewport_info(
 | 
			
		||||
            &mut self.info,
 | 
			
		||||
            self.state.egui_ctx(),
 | 
			
		||||
            &self.window,
 | 
			
		||||
            false,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        match event {
 | 
			
		||||
            WindowEvent::RedrawRequested => Some(Action::Redraw),
 | 
			
		||||
            WindowEvent::CloseRequested => Some(Action::Close),
 | 
			
		||||
            WindowEvent::Resized(size) => {
 | 
			
		||||
                let (Some(width), Some(height)) =
 | 
			
		||||
                    (NonZero::new(size.width), NonZero::new(size.height))
 | 
			
		||||
                else {
 | 
			
		||||
                    return None;
 | 
			
		||||
                };
 | 
			
		||||
                self.painter
 | 
			
		||||
                    .on_window_resized(ViewportId::ROOT, width, height);
 | 
			
		||||
                None
 | 
			
		||||
            }
 | 
			
		||||
            _ if response.repaint => Some(Action::Redraw),
 | 
			
		||||
            _ => None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn redraw(&mut self, event_loop: &ActiveEventLoop) -> Option<Action> {
 | 
			
		||||
        let mut input = self.state.take_egui_input(&self.window);
 | 
			
		||||
        input.viewports = std::iter::once((ViewportId::ROOT, self.info.clone())).collect();
 | 
			
		||||
        let mut output = self.ctx.run(input, |ctx| {
 | 
			
		||||
            self.app.show(ctx);
 | 
			
		||||
        });
 | 
			
		||||
        let clipped_primitives = self.ctx.tessellate(output.shapes, output.pixels_per_point);
 | 
			
		||||
        self.painter.paint_and_update_textures(
 | 
			
		||||
            ViewportId::ROOT,
 | 
			
		||||
            output.pixels_per_point,
 | 
			
		||||
            [0.0, 0.0, 0.0, 0.0],
 | 
			
		||||
            &clipped_primitives,
 | 
			
		||||
            &output.textures_delta,
 | 
			
		||||
            false,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub trait AppWindow {
 | 
			
		||||
    fn id(&self) -> WindowId;
 | 
			
		||||
    fn init(&mut self);
 | 
			
		||||
    fn handle_event(&mut self, event_loop: &ActiveEventLoop, event: &Event<UserEvent>);
 | 
			
		||||
impl Drop for Viewport {
 | 
			
		||||
    fn drop(&mut self) {
 | 
			
		||||
        self.app.on_destroy();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub enum UserEvent {
 | 
			
		||||
    OpenWindow(Box<dyn AppWindow>),
 | 
			
		||||
    CloseWindow(WindowId),
 | 
			
		||||
    GamepadEvent(gilrs::Event),
 | 
			
		||||
    OpenInput,
 | 
			
		||||
    OpenPlayer2,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Debug for UserEvent {
 | 
			
		||||
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
			
		||||
        match self {
 | 
			
		||||
            Self::OpenWindow(window) => f.debug_tuple("OpenWindow").field(&window.id()).finish(),
 | 
			
		||||
            Self::CloseWindow(window_id) => f.debug_tuple("CloseWindow").field(window_id).finish(),
 | 
			
		||||
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>) {
 | 
			
		||||
    let Ok(mut gilrs) = Gilrs::new() else {
 | 
			
		||||
        eprintln!("could not connect gamepad listener");
 | 
			
		||||
        return;
 | 
			
		||||
    };
 | 
			
		||||
    while let Some(event) = gilrs.next_event_blocking(None) {
 | 
			
		||||
        if event.event == EventType::Connected {
 | 
			
		||||
            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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,264 +0,0 @@
 | 
			
		|||
use std::{
 | 
			
		||||
    ops::{Deref, DerefMut},
 | 
			
		||||
    sync::Arc,
 | 
			
		||||
    time::Instant,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use imgui::{FontSource, MouseCursor, SuspendedContext, WindowToken};
 | 
			
		||||
use imgui_wgpu::{Renderer, RendererConfig};
 | 
			
		||||
use imgui_winit_support::WinitPlatform;
 | 
			
		||||
use pollster::block_on;
 | 
			
		||||
#[cfg(target_os = "windows")]
 | 
			
		||||
use winit::platform::windows::{CornerPreference, WindowAttributesExtWindows as _};
 | 
			
		||||
use winit::{
 | 
			
		||||
    dpi::{LogicalSize, PhysicalSize, Size},
 | 
			
		||||
    event_loop::ActiveEventLoop,
 | 
			
		||||
    window::{Window, WindowAttributes},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub struct WindowStateBuilder<'a> {
 | 
			
		||||
    event_loop: &'a ActiveEventLoop,
 | 
			
		||||
    attributes: WindowAttributes,
 | 
			
		||||
}
 | 
			
		||||
impl<'a> WindowStateBuilder<'a> {
 | 
			
		||||
    pub fn new(event_loop: &'a ActiveEventLoop) -> Self {
 | 
			
		||||
        let attributes = Window::default_attributes();
 | 
			
		||||
        #[cfg(target_os = "windows")]
 | 
			
		||||
        let attributes = attributes.with_corner_preference(CornerPreference::DoNotRound);
 | 
			
		||||
        Self {
 | 
			
		||||
            event_loop,
 | 
			
		||||
            attributes,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn with_title<T: Into<String>>(self, title: T) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            attributes: self.attributes.with_title(title),
 | 
			
		||||
            ..self
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn with_inner_size<S: Into<Size>>(self, size: S) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            attributes: self.attributes.with_inner_size(size),
 | 
			
		||||
            ..self
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn build(self) -> WindowState {
 | 
			
		||||
        WindowState::new(self.event_loop, self.attributes)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct WindowState {
 | 
			
		||||
    pub device: wgpu::Device,
 | 
			
		||||
    pub queue: Arc<wgpu::Queue>,
 | 
			
		||||
    pub window: Arc<Window>,
 | 
			
		||||
    pub surface_desc: wgpu::SurfaceConfiguration,
 | 
			
		||||
    pub surface: wgpu::Surface<'static>,
 | 
			
		||||
    pub hidpi_factor: f64,
 | 
			
		||||
    pub minimized: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl WindowState {
 | 
			
		||||
    fn new(event_loop: &ActiveEventLoop, attributes: WindowAttributes) -> Self {
 | 
			
		||||
        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
 | 
			
		||||
            backends: wgpu::Backends::PRIMARY,
 | 
			
		||||
            ..Default::default()
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let window = Arc::new(event_loop.create_window(attributes).unwrap());
 | 
			
		||||
 | 
			
		||||
        let size = window.inner_size();
 | 
			
		||||
        let hidpi_factor = window.scale_factor();
 | 
			
		||||
        let surface = instance.create_surface(window.clone()).unwrap();
 | 
			
		||||
 | 
			
		||||
        let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
 | 
			
		||||
            power_preference: wgpu::PowerPreference::HighPerformance,
 | 
			
		||||
            compatible_surface: Some(&surface),
 | 
			
		||||
            force_fallback_adapter: false,
 | 
			
		||||
        }))
 | 
			
		||||
        .unwrap();
 | 
			
		||||
 | 
			
		||||
        let (device, queue) =
 | 
			
		||||
            block_on(adapter.request_device(&wgpu::DeviceDescriptor::default(), None)).unwrap();
 | 
			
		||||
        let queue = Arc::new(queue);
 | 
			
		||||
 | 
			
		||||
        // Set up swap chain
 | 
			
		||||
        let surface_desc = wgpu::SurfaceConfiguration {
 | 
			
		||||
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
 | 
			
		||||
            format: wgpu::TextureFormat::Bgra8UnormSrgb,
 | 
			
		||||
            width: size.width,
 | 
			
		||||
            height: size.height,
 | 
			
		||||
            present_mode: wgpu::PresentMode::Fifo,
 | 
			
		||||
            desired_maximum_frame_latency: 2,
 | 
			
		||||
            alpha_mode: wgpu::CompositeAlphaMode::Auto,
 | 
			
		||||
            view_formats: vec![wgpu::TextureFormat::Bgra8Unorm],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        surface.configure(&device, &surface_desc);
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            device,
 | 
			
		||||
            queue,
 | 
			
		||||
            window,
 | 
			
		||||
            surface_desc,
 | 
			
		||||
            surface,
 | 
			
		||||
            hidpi_factor,
 | 
			
		||||
            minimized: false,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn logical_size(&self) -> LogicalSize<u32> {
 | 
			
		||||
        PhysicalSize::new(self.surface_desc.width, self.surface_desc.height)
 | 
			
		||||
            .to_logical(self.hidpi_factor)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn handle_resize(&mut self, size: &PhysicalSize<u32>) {
 | 
			
		||||
        if size.width > 0 && size.height > 0 {
 | 
			
		||||
            self.minimized = false;
 | 
			
		||||
            self.surface_desc.width = size.width;
 | 
			
		||||
            self.surface_desc.height = size.height;
 | 
			
		||||
            self.surface.configure(&self.device, &self.surface_desc);
 | 
			
		||||
        } else {
 | 
			
		||||
            self.minimized = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct ImguiState {
 | 
			
		||||
    pub context: ContextGuard,
 | 
			
		||||
    pub platform: WinitPlatform,
 | 
			
		||||
    pub renderer: Renderer,
 | 
			
		||||
    pub clear_color: wgpu::Color,
 | 
			
		||||
    pub last_frame: Instant,
 | 
			
		||||
    pub last_cursor: Option<MouseCursor>,
 | 
			
		||||
}
 | 
			
		||||
impl ImguiState {
 | 
			
		||||
    pub fn new(window: &WindowState) -> Self {
 | 
			
		||||
        let mut context_guard = ContextGuard::new();
 | 
			
		||||
        let mut context = context_guard.lock().unwrap();
 | 
			
		||||
 | 
			
		||||
        let mut platform = imgui_winit_support::WinitPlatform::new(&mut context);
 | 
			
		||||
        platform.attach_window(
 | 
			
		||||
            context.io_mut(),
 | 
			
		||||
            &window.window,
 | 
			
		||||
            imgui_winit_support::HiDpiMode::Default,
 | 
			
		||||
        );
 | 
			
		||||
        context.set_ini_filename(None);
 | 
			
		||||
 | 
			
		||||
        let font_size = (16.0 * window.hidpi_factor) as f32;
 | 
			
		||||
        context.io_mut().font_global_scale = (1.0 / window.hidpi_factor) as f32;
 | 
			
		||||
 | 
			
		||||
        context.fonts().add_font(&[FontSource::TtfData {
 | 
			
		||||
            data: include_bytes!("../../assets/selawk.ttf"),
 | 
			
		||||
            size_pixels: font_size,
 | 
			
		||||
            config: Some(imgui::FontConfig {
 | 
			
		||||
                oversample_h: 1,
 | 
			
		||||
                pixel_snap_h: true,
 | 
			
		||||
                size_pixels: font_size,
 | 
			
		||||
                ..Default::default()
 | 
			
		||||
            }),
 | 
			
		||||
        }]);
 | 
			
		||||
 | 
			
		||||
        let style = context.style_mut();
 | 
			
		||||
        style.use_light_colors();
 | 
			
		||||
 | 
			
		||||
        //
 | 
			
		||||
        // Set up dear imgui wgpu renderer
 | 
			
		||||
        //
 | 
			
		||||
        let renderer_config = RendererConfig {
 | 
			
		||||
            texture_format: window.surface_desc.format,
 | 
			
		||||
            ..Default::default()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let renderer = Renderer::new(&mut context, &window.device, &window.queue, renderer_config);
 | 
			
		||||
 | 
			
		||||
        let last_frame = Instant::now();
 | 
			
		||||
        let last_cursor = None;
 | 
			
		||||
 | 
			
		||||
        drop(context);
 | 
			
		||||
        Self {
 | 
			
		||||
            context: context_guard,
 | 
			
		||||
            platform,
 | 
			
		||||
            renderer,
 | 
			
		||||
            clear_color: wgpu::Color::BLACK,
 | 
			
		||||
            last_frame,
 | 
			
		||||
            last_cursor,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct ContextGuard {
 | 
			
		||||
    value: Option<SuspendedContext>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ContextGuard {
 | 
			
		||||
    fn new() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            value: Some(SuspendedContext::create()),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn lock(&mut self) -> Option<ContextLock<'_>> {
 | 
			
		||||
        let sus = self.value.take()?;
 | 
			
		||||
        match sus.activate() {
 | 
			
		||||
            Ok(ctx) => Some(ContextLock {
 | 
			
		||||
                ctx: Some(ctx),
 | 
			
		||||
                holder: self,
 | 
			
		||||
            }),
 | 
			
		||||
            Err(sus) => {
 | 
			
		||||
                self.value = Some(sus);
 | 
			
		||||
                None
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct ContextLock<'a> {
 | 
			
		||||
    ctx: Option<imgui::Context>,
 | 
			
		||||
    holder: &'a mut ContextGuard,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> Deref for ContextLock<'a> {
 | 
			
		||||
    type Target = imgui::Context;
 | 
			
		||||
    fn deref(&self) -> &Self::Target {
 | 
			
		||||
        self.ctx.as_ref().unwrap()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> DerefMut for ContextLock<'a> {
 | 
			
		||||
    fn deref_mut(&mut self) -> &mut Self::Target {
 | 
			
		||||
        self.ctx.as_mut().unwrap()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> Drop for ContextLock<'a> {
 | 
			
		||||
    fn drop(&mut self) {
 | 
			
		||||
        self.holder.value = self.ctx.take().map(|c| c.suspend())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub trait UiExt {
 | 
			
		||||
    fn fullscreen_window(&self) -> Option<WindowToken<'_>>;
 | 
			
		||||
    fn right_align_text<T: AsRef<str>>(&self, text: T, space: f32);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl UiExt for imgui::Ui {
 | 
			
		||||
    fn fullscreen_window(&self) -> Option<WindowToken<'_>> {
 | 
			
		||||
        self.window("fullscreen")
 | 
			
		||||
            .position([0.0, 0.0], imgui::Condition::Always)
 | 
			
		||||
            .size(self.io().display_size, imgui::Condition::Always)
 | 
			
		||||
            .flags(imgui::WindowFlags::NO_DECORATION)
 | 
			
		||||
            .begin()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn right_align_text<T: AsRef<str>>(&self, text: T, space: f32) {
 | 
			
		||||
        let width = self.calc_text_size(text.as_ref())[0];
 | 
			
		||||
        let [left, y] = self.cursor_pos();
 | 
			
		||||
        let right = left + space;
 | 
			
		||||
        self.set_cursor_pos([right - width, y]);
 | 
			
		||||
        self.text(text);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										384
									
								
								src/app/game.rs
								
								
								
								
							
							
						
						
									
										384
									
								
								src/app/game.rs
								
								
								
								
							| 
						 | 
				
			
			@ -1,384 +0,0 @@
 | 
			
		|||
use std::{
 | 
			
		||||
    sync::{Arc, RwLock},
 | 
			
		||||
    time::Instant,
 | 
			
		||||
};
 | 
			
		||||
use wgpu::util::DeviceExt as _;
 | 
			
		||||
use winit::{
 | 
			
		||||
    dpi::LogicalSize,
 | 
			
		||||
    event::{Event, WindowEvent},
 | 
			
		||||
    event_loop::{ActiveEventLoop, EventLoopProxy},
 | 
			
		||||
    window::WindowId,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    emulator::{EmulatorClient, EmulatorCommand},
 | 
			
		||||
    input::InputMapper,
 | 
			
		||||
    renderer::GameRenderer,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use super::{
 | 
			
		||||
    common::{ImguiState, WindowState, WindowStateBuilder},
 | 
			
		||||
    input::InputWindow,
 | 
			
		||||
    AppWindow, UserEvent,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub struct GameWindow {
 | 
			
		||||
    window: WindowState,
 | 
			
		||||
    imgui: Option<ImguiState>,
 | 
			
		||||
    pipeline: wgpu::RenderPipeline,
 | 
			
		||||
    bind_group: wgpu::BindGroup,
 | 
			
		||||
    client: EmulatorClient,
 | 
			
		||||
    input_mapper: Arc<RwLock<InputMapper>>,
 | 
			
		||||
    proxy: EventLoopProxy<UserEvent>,
 | 
			
		||||
    paused_due_to_minimize: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl GameWindow {
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        event_loop: &ActiveEventLoop,
 | 
			
		||||
        client: EmulatorClient,
 | 
			
		||||
        input_mapper: Arc<RwLock<InputMapper>>,
 | 
			
		||||
        proxy: EventLoopProxy<UserEvent>,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        let window = WindowStateBuilder::new(event_loop)
 | 
			
		||||
            .with_title("Shrooms VB")
 | 
			
		||||
            .with_inner_size(LogicalSize::new(384, 244))
 | 
			
		||||
            .build();
 | 
			
		||||
        let device = &window.device;
 | 
			
		||||
 | 
			
		||||
        let eyes = Arc::new(GameRenderer::create_texture(device, "eye"));
 | 
			
		||||
        client.send_command(EmulatorCommand::SetRenderer(GameRenderer {
 | 
			
		||||
            queue: window.queue.clone(),
 | 
			
		||||
            eyes: eyes.clone(),
 | 
			
		||||
        }));
 | 
			
		||||
        let eyes = eyes.create_view(&wgpu::TextureViewDescriptor::default());
 | 
			
		||||
        let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default());
 | 
			
		||||
        let colors = Colors {
 | 
			
		||||
            left: [1.0, 0.0, 0.0, 1.0],
 | 
			
		||||
            right: [0.0, 0.7734375, 0.9375, 1.0],
 | 
			
		||||
        };
 | 
			
		||||
        let color_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
 | 
			
		||||
            label: Some("colors"),
 | 
			
		||||
            contents: bytemuck::bytes_of(&colors),
 | 
			
		||||
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
 | 
			
		||||
        });
 | 
			
		||||
        let texture_bind_group_layout =
 | 
			
		||||
            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
 | 
			
		||||
                label: Some("texture bind group layout"),
 | 
			
		||||
                entries: &[
 | 
			
		||||
                    wgpu::BindGroupLayoutEntry {
 | 
			
		||||
                        binding: 0,
 | 
			
		||||
                        visibility: wgpu::ShaderStages::FRAGMENT,
 | 
			
		||||
                        ty: wgpu::BindingType::Texture {
 | 
			
		||||
                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
 | 
			
		||||
                            view_dimension: wgpu::TextureViewDimension::D2,
 | 
			
		||||
                            multisampled: false,
 | 
			
		||||
                        },
 | 
			
		||||
                        count: None,
 | 
			
		||||
                    },
 | 
			
		||||
                    wgpu::BindGroupLayoutEntry {
 | 
			
		||||
                        binding: 1,
 | 
			
		||||
                        visibility: wgpu::ShaderStages::FRAGMENT,
 | 
			
		||||
                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
 | 
			
		||||
                        count: None,
 | 
			
		||||
                    },
 | 
			
		||||
                    wgpu::BindGroupLayoutEntry {
 | 
			
		||||
                        binding: 2,
 | 
			
		||||
                        visibility: wgpu::ShaderStages::FRAGMENT,
 | 
			
		||||
                        ty: wgpu::BindingType::Buffer {
 | 
			
		||||
                            ty: wgpu::BufferBindingType::Uniform,
 | 
			
		||||
                            has_dynamic_offset: false,
 | 
			
		||||
                            min_binding_size: None,
 | 
			
		||||
                        },
 | 
			
		||||
                        count: None,
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
 | 
			
		||||
            label: Some("bind group"),
 | 
			
		||||
            layout: &texture_bind_group_layout,
 | 
			
		||||
            entries: &[
 | 
			
		||||
                wgpu::BindGroupEntry {
 | 
			
		||||
                    binding: 0,
 | 
			
		||||
                    resource: wgpu::BindingResource::TextureView(&eyes),
 | 
			
		||||
                },
 | 
			
		||||
                wgpu::BindGroupEntry {
 | 
			
		||||
                    binding: 1,
 | 
			
		||||
                    resource: wgpu::BindingResource::Sampler(&sampler),
 | 
			
		||||
                },
 | 
			
		||||
                wgpu::BindGroupEntry {
 | 
			
		||||
                    binding: 2,
 | 
			
		||||
                    resource: color_buf.as_entire_binding(),
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let shader = device.create_shader_module(wgpu::include_wgsl!("../anaglyph.wgsl"));
 | 
			
		||||
        let render_pipeline_layout =
 | 
			
		||||
            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
 | 
			
		||||
                label: Some("render pipeline layout"),
 | 
			
		||||
                bind_group_layouts: &[&texture_bind_group_layout],
 | 
			
		||||
                push_constant_ranges: &[],
 | 
			
		||||
            });
 | 
			
		||||
        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
 | 
			
		||||
            label: Some("render pipeline"),
 | 
			
		||||
            layout: Some(&render_pipeline_layout),
 | 
			
		||||
            vertex: wgpu::VertexState {
 | 
			
		||||
                module: &shader,
 | 
			
		||||
                entry_point: "vs_main",
 | 
			
		||||
                buffers: &[],
 | 
			
		||||
                compilation_options: wgpu::PipelineCompilationOptions::default(),
 | 
			
		||||
            },
 | 
			
		||||
            fragment: Some(wgpu::FragmentState {
 | 
			
		||||
                module: &shader,
 | 
			
		||||
                entry_point: "fs_main",
 | 
			
		||||
                targets: &[Some(wgpu::ColorTargetState {
 | 
			
		||||
                    format: wgpu::TextureFormat::Bgra8UnormSrgb,
 | 
			
		||||
                    blend: Some(wgpu::BlendState::REPLACE),
 | 
			
		||||
                    write_mask: wgpu::ColorWrites::ALL,
 | 
			
		||||
                })],
 | 
			
		||||
                compilation_options: wgpu::PipelineCompilationOptions::default(),
 | 
			
		||||
            }),
 | 
			
		||||
            primitive: wgpu::PrimitiveState {
 | 
			
		||||
                topology: wgpu::PrimitiveTopology::TriangleList,
 | 
			
		||||
                strip_index_format: None,
 | 
			
		||||
                front_face: wgpu::FrontFace::Ccw,
 | 
			
		||||
                cull_mode: Some(wgpu::Face::Back),
 | 
			
		||||
                polygon_mode: wgpu::PolygonMode::Fill,
 | 
			
		||||
                unclipped_depth: false,
 | 
			
		||||
                conservative: false,
 | 
			
		||||
            },
 | 
			
		||||
            depth_stencil: None,
 | 
			
		||||
            multisample: wgpu::MultisampleState {
 | 
			
		||||
                count: 1,
 | 
			
		||||
                mask: !0,
 | 
			
		||||
                alpha_to_coverage_enabled: false,
 | 
			
		||||
            },
 | 
			
		||||
            multiview: None,
 | 
			
		||||
            cache: None,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            window,
 | 
			
		||||
            imgui: None,
 | 
			
		||||
            pipeline: render_pipeline,
 | 
			
		||||
            bind_group,
 | 
			
		||||
            client,
 | 
			
		||||
            input_mapper,
 | 
			
		||||
            proxy,
 | 
			
		||||
            paused_due_to_minimize: false,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn draw(&mut self, event_loop: &ActiveEventLoop) {
 | 
			
		||||
        let window = &mut self.window;
 | 
			
		||||
        let imgui = self.imgui.as_mut().unwrap();
 | 
			
		||||
        let mut context = imgui.context.lock().unwrap();
 | 
			
		||||
        let mut new_size = None;
 | 
			
		||||
 | 
			
		||||
        let now = Instant::now();
 | 
			
		||||
        context.io_mut().update_delta_time(now - imgui.last_frame);
 | 
			
		||||
        imgui.last_frame = now;
 | 
			
		||||
 | 
			
		||||
        let frame = match window.surface.get_current_texture() {
 | 
			
		||||
            Ok(frame) => frame,
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                if !self.window.minimized {
 | 
			
		||||
                    eprintln!("dropped frame: {e:?}");
 | 
			
		||||
                }
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        imgui
 | 
			
		||||
            .platform
 | 
			
		||||
            .prepare_frame(context.io_mut(), &window.window)
 | 
			
		||||
            .expect("Failed to prepare frame");
 | 
			
		||||
        let ui = context.new_frame();
 | 
			
		||||
        let mut menu_height = 0.0;
 | 
			
		||||
        ui.main_menu_bar(|| {
 | 
			
		||||
            menu_height = ui.window_size()[1];
 | 
			
		||||
            ui.menu("ROM", || {
 | 
			
		||||
                if ui.menu_item("Open ROM") {
 | 
			
		||||
                    let rom = native_dialog::FileDialog::new()
 | 
			
		||||
                        .add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
 | 
			
		||||
                        .show_open_single_file()
 | 
			
		||||
                        .unwrap();
 | 
			
		||||
                    if let Some(path) = rom {
 | 
			
		||||
                        self.client.send_command(EmulatorCommand::LoadGame(path));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if ui.menu_item("Quit") {
 | 
			
		||||
                    event_loop.exit();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            ui.menu("Emulation", || {
 | 
			
		||||
                let has_game = self.client.has_game();
 | 
			
		||||
                if self.client.is_running() {
 | 
			
		||||
                    if ui.menu_item_config("Pause").enabled(has_game).build() {
 | 
			
		||||
                        self.client.send_command(EmulatorCommand::Pause);
 | 
			
		||||
                    }
 | 
			
		||||
                } else if ui.menu_item_config("Resume").enabled(has_game).build() {
 | 
			
		||||
                    self.client.send_command(EmulatorCommand::Resume);
 | 
			
		||||
                }
 | 
			
		||||
                if ui.menu_item_config("Reset").enabled(has_game).build() {
 | 
			
		||||
                    self.client.send_command(EmulatorCommand::Reset);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            ui.menu("Video", || {
 | 
			
		||||
                let current_dims = window.logical_size();
 | 
			
		||||
                for scale in 1..=4 {
 | 
			
		||||
                    let label = format!("x{scale}");
 | 
			
		||||
                    let dims = LogicalSize::new(384 * scale, 224 * scale + 20);
 | 
			
		||||
                    let selected = dims == current_dims;
 | 
			
		||||
                    if ui.menu_item_config(label).selected(selected).build() {
 | 
			
		||||
                        if let Some(size) = window.window.request_inner_size(dims) {
 | 
			
		||||
                            window.handle_resize(&size);
 | 
			
		||||
                            new_size = Some(size);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            ui.menu("Input", || {
 | 
			
		||||
                if ui.menu_item("Bind Inputs") {
 | 
			
		||||
                    let input_window = Box::new(InputWindow::new(
 | 
			
		||||
                        event_loop,
 | 
			
		||||
                        self.input_mapper.clone(),
 | 
			
		||||
                        self.proxy.clone(),
 | 
			
		||||
                    ));
 | 
			
		||||
                    self.proxy
 | 
			
		||||
                        .send_event(UserEvent::OpenWindow(input_window))
 | 
			
		||||
                        .unwrap();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let mut encoder: wgpu::CommandEncoder = window
 | 
			
		||||
            .device
 | 
			
		||||
            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
 | 
			
		||||
 | 
			
		||||
        if imgui.last_cursor != ui.mouse_cursor() {
 | 
			
		||||
            imgui.last_cursor = ui.mouse_cursor();
 | 
			
		||||
            imgui.platform.prepare_render(ui, &window.window);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let view = frame
 | 
			
		||||
            .texture
 | 
			
		||||
            .create_view(&wgpu::TextureViewDescriptor::default());
 | 
			
		||||
        let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
 | 
			
		||||
            label: None,
 | 
			
		||||
            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
 | 
			
		||||
                view: &view,
 | 
			
		||||
                resolve_target: None,
 | 
			
		||||
                ops: wgpu::Operations {
 | 
			
		||||
                    load: wgpu::LoadOp::Clear(imgui.clear_color),
 | 
			
		||||
                    store: wgpu::StoreOp::Store,
 | 
			
		||||
                },
 | 
			
		||||
            })],
 | 
			
		||||
            depth_stencil_attachment: None,
 | 
			
		||||
            timestamp_writes: None,
 | 
			
		||||
            occlusion_query_set: None,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Draw the game
 | 
			
		||||
        rpass.set_pipeline(&self.pipeline);
 | 
			
		||||
        let window_width = window.surface_desc.width as f32;
 | 
			
		||||
        let window_height = window.surface_desc.height as f32;
 | 
			
		||||
        let menu_height = menu_height * window.hidpi_factor as f32;
 | 
			
		||||
        let ((x, y), (width, height)) =
 | 
			
		||||
            compute_game_bounds(window_width, window_height, menu_height);
 | 
			
		||||
        rpass.set_viewport(x, y, width, height, 0.0, 1.0);
 | 
			
		||||
        rpass.set_bind_group(0, &self.bind_group, &[]);
 | 
			
		||||
        rpass.draw(0..6, 0..1);
 | 
			
		||||
 | 
			
		||||
        // Draw the menu on top of the game
 | 
			
		||||
        rpass.set_viewport(0.0, 0.0, window_width, window_height, 0.0, 1.0);
 | 
			
		||||
        imgui
 | 
			
		||||
            .renderer
 | 
			
		||||
            .render(context.render(), &window.queue, &window.device, &mut rpass)
 | 
			
		||||
            .expect("Rendering failed");
 | 
			
		||||
 | 
			
		||||
        drop(rpass);
 | 
			
		||||
 | 
			
		||||
        if let Some(size) = new_size {
 | 
			
		||||
            imgui.platform.handle_event::<UserEvent>(
 | 
			
		||||
                context.io_mut(),
 | 
			
		||||
                &window.window,
 | 
			
		||||
                &Event::WindowEvent {
 | 
			
		||||
                    window_id: window.window.id(),
 | 
			
		||||
                    event: WindowEvent::Resized(size),
 | 
			
		||||
                },
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        window.queue.submit(Some(encoder.finish()));
 | 
			
		||||
 | 
			
		||||
        frame.present();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AppWindow for GameWindow {
 | 
			
		||||
    fn id(&self) -> WindowId {
 | 
			
		||||
        self.window.window.id()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn init(&mut self) {
 | 
			
		||||
        self.imgui = Some(ImguiState::new(&self.window));
 | 
			
		||||
        self.window.window.request_redraw();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn handle_event(&mut self, event_loop: &ActiveEventLoop, event: &Event<UserEvent>) {
 | 
			
		||||
        match event {
 | 
			
		||||
            Event::WindowEvent { event, .. } => match event {
 | 
			
		||||
                WindowEvent::Resized(size) => {
 | 
			
		||||
                    self.window.handle_resize(size);
 | 
			
		||||
                    if self.window.minimized {
 | 
			
		||||
                        if self.client.is_running() {
 | 
			
		||||
                            self.client.send_command(EmulatorCommand::Pause);
 | 
			
		||||
                            self.paused_due_to_minimize = true;
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if self.paused_due_to_minimize {
 | 
			
		||||
                        self.client.send_command(EmulatorCommand::Resume);
 | 
			
		||||
                        self.paused_due_to_minimize = false;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                WindowEvent::CloseRequested => event_loop.exit(),
 | 
			
		||||
                WindowEvent::RedrawRequested => self.draw(event_loop),
 | 
			
		||||
                _ => (),
 | 
			
		||||
            },
 | 
			
		||||
            Event::AboutToWait => {
 | 
			
		||||
                self.window.window.request_redraw();
 | 
			
		||||
            }
 | 
			
		||||
            _ => (),
 | 
			
		||||
        }
 | 
			
		||||
        let window = &self.window;
 | 
			
		||||
        let Some(imgui) = self.imgui.as_mut() else {
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        let mut context = imgui.context.lock().unwrap();
 | 
			
		||||
        imgui
 | 
			
		||||
            .platform
 | 
			
		||||
            .handle_event(context.io_mut(), &window.window, event);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn compute_game_bounds(
 | 
			
		||||
    window_width: f32,
 | 
			
		||||
    window_height: f32,
 | 
			
		||||
    menu_height: f32,
 | 
			
		||||
) -> ((f32, f32), (f32, f32)) {
 | 
			
		||||
    let available_width = window_width;
 | 
			
		||||
    let available_height = window_height - menu_height;
 | 
			
		||||
 | 
			
		||||
    let width = available_width.min(available_height * 384.0 / 224.0);
 | 
			
		||||
    let height = available_height.min(available_width * 224.0 / 384.0);
 | 
			
		||||
    let x = (available_width - width) / 2.0;
 | 
			
		||||
    let y = menu_height + (available_height - height) / 2.0;
 | 
			
		||||
    ((x, y), (width, height))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
 | 
			
		||||
#[repr(C)]
 | 
			
		||||
struct Colors {
 | 
			
		||||
    left: [f32; 4],
 | 
			
		||||
    right: [f32; 4],
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										223
									
								
								src/app/input.rs
								
								
								
								
							
							
						
						
									
										223
									
								
								src/app/input.rs
								
								
								
								
							| 
						 | 
				
			
			@ -1,223 +0,0 @@
 | 
			
		|||
use std::{
 | 
			
		||||
    sync::{Arc, RwLock},
 | 
			
		||||
    time::Instant,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use winit::{
 | 
			
		||||
    dpi::LogicalSize,
 | 
			
		||||
    event::{Event, KeyEvent, WindowEvent},
 | 
			
		||||
    event_loop::{ActiveEventLoop, EventLoopProxy},
 | 
			
		||||
    platform::modifier_supplement::KeyEventExtModifierSupplement,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use crate::{input::InputMapper, shrooms_vb_core::VBKey};
 | 
			
		||||
 | 
			
		||||
use super::{
 | 
			
		||||
    common::{ImguiState, UiExt, WindowState, WindowStateBuilder},
 | 
			
		||||
    AppWindow, UserEvent,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub struct InputWindow {
 | 
			
		||||
    window: WindowState,
 | 
			
		||||
    imgui: Option<ImguiState>,
 | 
			
		||||
    input_mapper: Arc<RwLock<InputMapper>>,
 | 
			
		||||
    proxy: EventLoopProxy<UserEvent>,
 | 
			
		||||
    now_binding: Option<VBKey>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const KEY_NAMES: [(VBKey, &str); 14] = [
 | 
			
		||||
    (VBKey::LU, "Up"),
 | 
			
		||||
    (VBKey::LD, "Down"),
 | 
			
		||||
    (VBKey::LL, "Left"),
 | 
			
		||||
    (VBKey::LR, "Right"),
 | 
			
		||||
    (VBKey::SEL, "Select"),
 | 
			
		||||
    (VBKey::STA, "Start"),
 | 
			
		||||
    (VBKey::B, "B"),
 | 
			
		||||
    (VBKey::A, "A"),
 | 
			
		||||
    (VBKey::LT, "L-Trigger"),
 | 
			
		||||
    (VBKey::RT, "R-Trigger"),
 | 
			
		||||
    (VBKey::RU, "R-Up"),
 | 
			
		||||
    (VBKey::RD, "R-Down"),
 | 
			
		||||
    (VBKey::RL, "R-Left"),
 | 
			
		||||
    (VBKey::RR, "R-Right"),
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
impl InputWindow {
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        event_loop: &ActiveEventLoop,
 | 
			
		||||
        input_mapper: Arc<RwLock<InputMapper>>,
 | 
			
		||||
        proxy: EventLoopProxy<UserEvent>,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        let window = WindowStateBuilder::new(event_loop)
 | 
			
		||||
            .with_title("Bind Inputs")
 | 
			
		||||
            .with_inner_size(LogicalSize::new(600, 400))
 | 
			
		||||
            .build();
 | 
			
		||||
        Self {
 | 
			
		||||
            window,
 | 
			
		||||
            imgui: None,
 | 
			
		||||
            input_mapper,
 | 
			
		||||
            now_binding: None,
 | 
			
		||||
            proxy,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn draw(&mut self) {
 | 
			
		||||
        let window = &mut self.window;
 | 
			
		||||
        let imgui = self.imgui.as_mut().unwrap();
 | 
			
		||||
        let mut context = imgui.context.lock().unwrap();
 | 
			
		||||
 | 
			
		||||
        let now = Instant::now();
 | 
			
		||||
        context.io_mut().update_delta_time(now - imgui.last_frame);
 | 
			
		||||
        imgui.last_frame = now;
 | 
			
		||||
 | 
			
		||||
        let frame = match window.surface.get_current_texture() {
 | 
			
		||||
            Ok(frame) => frame,
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                if !self.window.minimized {
 | 
			
		||||
                    eprintln!("dropped frame: {e:?}");
 | 
			
		||||
                }
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        imgui
 | 
			
		||||
            .platform
 | 
			
		||||
            .prepare_frame(context.io_mut(), &window.window)
 | 
			
		||||
            .expect("Failed to prepare frame");
 | 
			
		||||
        let ui = context.new_frame();
 | 
			
		||||
 | 
			
		||||
        let mut render_key_bindings = || {
 | 
			
		||||
            if let Some(table) = ui.begin_table("controls", 2) {
 | 
			
		||||
                let binding_names = {
 | 
			
		||||
                    let mapper = self.input_mapper.read().unwrap();
 | 
			
		||||
                    mapper.binding_names()
 | 
			
		||||
                };
 | 
			
		||||
                ui.table_next_row();
 | 
			
		||||
 | 
			
		||||
                for (key, name) in KEY_NAMES {
 | 
			
		||||
                    let binding = binding_names.get(&key).map(|s| s.as_str());
 | 
			
		||||
                    ui.table_next_column();
 | 
			
		||||
                    let [space, _] = ui.content_region_avail();
 | 
			
		||||
                    ui.group(|| {
 | 
			
		||||
                        ui.right_align_text(name, space * 0.20);
 | 
			
		||||
                        ui.same_line();
 | 
			
		||||
                        let label_text = if self.now_binding == Some(key) {
 | 
			
		||||
                            "Press any input"
 | 
			
		||||
                        } else {
 | 
			
		||||
                            binding.unwrap_or("")
 | 
			
		||||
                        };
 | 
			
		||||
                        let label = format!("{}##{}", label_text, name);
 | 
			
		||||
                        if ui.button_with_size(label, [space * 0.60, 0.0]) {
 | 
			
		||||
                            self.now_binding = Some(key);
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                    ui.same_line();
 | 
			
		||||
                    if ui.button(format!("Clear##{name}")) {
 | 
			
		||||
                        let mut mapper = self.input_mapper.write().unwrap();
 | 
			
		||||
                        mapper.clear_binding(key);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                table.end();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if let Some(window) = ui.fullscreen_window() {
 | 
			
		||||
            if let Some(tabs) = ui.tab_bar("tabs") {
 | 
			
		||||
                if let Some(tab) = ui.tab_item("Player 1") {
 | 
			
		||||
                    render_key_bindings();
 | 
			
		||||
                    tab.end();
 | 
			
		||||
                }
 | 
			
		||||
                tabs.end();
 | 
			
		||||
            }
 | 
			
		||||
            window.end();
 | 
			
		||||
        }
 | 
			
		||||
        let mut encoder: wgpu::CommandEncoder = window
 | 
			
		||||
            .device
 | 
			
		||||
            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
 | 
			
		||||
 | 
			
		||||
        if imgui.last_cursor != ui.mouse_cursor() {
 | 
			
		||||
            imgui.last_cursor = ui.mouse_cursor();
 | 
			
		||||
            imgui.platform.prepare_render(ui, &window.window);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let view = frame
 | 
			
		||||
            .texture
 | 
			
		||||
            .create_view(&wgpu::TextureViewDescriptor::default());
 | 
			
		||||
        let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
 | 
			
		||||
            label: None,
 | 
			
		||||
            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
 | 
			
		||||
                view: &view,
 | 
			
		||||
                resolve_target: None,
 | 
			
		||||
                ops: wgpu::Operations {
 | 
			
		||||
                    load: wgpu::LoadOp::Clear(imgui.clear_color),
 | 
			
		||||
                    store: wgpu::StoreOp::Store,
 | 
			
		||||
                },
 | 
			
		||||
            })],
 | 
			
		||||
            depth_stencil_attachment: None,
 | 
			
		||||
            timestamp_writes: None,
 | 
			
		||||
            occlusion_query_set: None,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Draw the game
 | 
			
		||||
        imgui
 | 
			
		||||
            .renderer
 | 
			
		||||
            .render(context.render(), &window.queue, &window.device, &mut rpass)
 | 
			
		||||
            .expect("Rendering failed");
 | 
			
		||||
 | 
			
		||||
        drop(rpass);
 | 
			
		||||
 | 
			
		||||
        window.queue.submit(Some(encoder.finish()));
 | 
			
		||||
 | 
			
		||||
        frame.present();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn try_bind_key(&mut self, event: &KeyEvent) {
 | 
			
		||||
        if !event.state.is_pressed() {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        let Some(vb) = self.now_binding.take() else {
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        let mut mapper = self.input_mapper.write().unwrap();
 | 
			
		||||
        mapper.bind_key(vb, event.key_without_modifiers());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AppWindow for InputWindow {
 | 
			
		||||
    fn id(&self) -> winit::window::WindowId {
 | 
			
		||||
        self.window.window.id()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn init(&mut self) {
 | 
			
		||||
        self.imgui = Some(ImguiState::new(&self.window));
 | 
			
		||||
        self.window.window.request_redraw();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn handle_event(&mut self, _: &ActiveEventLoop, event: &Event<UserEvent>) {
 | 
			
		||||
        match event {
 | 
			
		||||
            Event::WindowEvent { event, .. } => match event {
 | 
			
		||||
                WindowEvent::Resized(size) => self.window.handle_resize(size),
 | 
			
		||||
                WindowEvent::CloseRequested => self
 | 
			
		||||
                    .proxy
 | 
			
		||||
                    .send_event(UserEvent::CloseWindow(self.id()))
 | 
			
		||||
                    .unwrap(),
 | 
			
		||||
                WindowEvent::KeyboardInput { event, .. } => self.try_bind_key(event),
 | 
			
		||||
                WindowEvent::RedrawRequested => self.draw(),
 | 
			
		||||
                _ => (),
 | 
			
		||||
            },
 | 
			
		||||
            Event::AboutToWait => {
 | 
			
		||||
                self.window.window.request_redraw();
 | 
			
		||||
            }
 | 
			
		||||
            _ => (),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let window = &self.window;
 | 
			
		||||
        let Some(imgui) = self.imgui.as_mut() else {
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        let mut context = imgui.context.lock().unwrap();
 | 
			
		||||
        imgui
 | 
			
		||||
            .platform
 | 
			
		||||
            .handle_event(context.io_mut(), &window.window, event);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +1,5 @@
 | 
			
		|||
use std::time::Duration;
 | 
			
		||||
 | 
			
		||||
use anyhow::{bail, Result};
 | 
			
		||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
 | 
			
		||||
use itertools::Itertools;
 | 
			
		||||
| 
						 | 
				
			
			@ -95,7 +97,7 @@ impl Audio {
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        while self.sample_sink.slots() < self.sampler.output_frames_max() * 2 {
 | 
			
		||||
            std::hint::spin_loop();
 | 
			
		||||
            std::thread::sleep(Duration::from_micros(500));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,50 +1,128 @@
 | 
			
		|||
use std::sync::{Arc, RwLock};
 | 
			
		||||
 | 
			
		||||
use winit::event::{ElementState, KeyEvent};
 | 
			
		||||
use gilrs::{ev::Code, Event as GamepadEvent, EventType, GamepadId};
 | 
			
		||||
use winit::{
 | 
			
		||||
    event::{ElementState, KeyEvent},
 | 
			
		||||
    keyboard::PhysicalKey,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use crate::{input::InputMapper, shrooms_vb_core::VBKey};
 | 
			
		||||
use crate::{
 | 
			
		||||
    emulator::{EmulatorClient, EmulatorCommand, SimId, VBKey},
 | 
			
		||||
    input::{InputMapping, MappingProvider},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub struct ControllerState {
 | 
			
		||||
    input_mapper: Arc<RwLock<InputMapper>>,
 | 
			
		||||
    pressed: VBKey,
 | 
			
		||||
pub struct Controller {
 | 
			
		||||
    pub sim_id: SimId,
 | 
			
		||||
    state: VBKey,
 | 
			
		||||
    mapping: Arc<RwLock<InputMapping>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ControllerState {
 | 
			
		||||
    pub fn new(input_mapper: Arc<RwLock<InputMapper>>) -> Self {
 | 
			
		||||
impl Controller {
 | 
			
		||||
    pub fn new(sim_id: SimId, mappings: &MappingProvider) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            input_mapper,
 | 
			
		||||
            pressed: VBKey::SGN,
 | 
			
		||||
            sim_id,
 | 
			
		||||
            state: VBKey::SGN,
 | 
			
		||||
            mapping: mappings.for_sim(sim_id).clone(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn pressed(&self) -> VBKey {
 | 
			
		||||
        self.pressed
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn key_event(&mut self, event: &KeyEvent) -> bool {
 | 
			
		||||
        let Some(input) = self.key_event_to_input(event) else {
 | 
			
		||||
            return false;
 | 
			
		||||
        };
 | 
			
		||||
    pub fn key_event(&mut self, event: &KeyEvent) -> Option<VBKey> {
 | 
			
		||||
        let keys = self.map_keys(&event.physical_key)?;
 | 
			
		||||
        match event.state {
 | 
			
		||||
            ElementState::Pressed => {
 | 
			
		||||
                if self.pressed.contains(input) {
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
                self.pressed.insert(input);
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
            ElementState::Released => {
 | 
			
		||||
                if !self.pressed.contains(input) {
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
                self.pressed.remove(input);
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
            ElementState::Pressed => self.update_state(keys, VBKey::empty()),
 | 
			
		||||
            ElementState::Released => self.update_state(VBKey::empty(), keys),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn key_event_to_input(&self, event: &KeyEvent) -> Option<VBKey> {
 | 
			
		||||
        let mapper = self.input_mapper.read().unwrap();
 | 
			
		||||
        mapper.key_event(event)
 | 
			
		||||
    pub fn gamepad_event(&mut self, event: &GamepadEvent) -> Option<VBKey> {
 | 
			
		||||
        let (pressed, released) = match event.event {
 | 
			
		||||
            EventType::ButtonPressed(_, code) => {
 | 
			
		||||
                let mappings = self.map_button(&event.id, &code)?;
 | 
			
		||||
                (mappings, VBKey::empty())
 | 
			
		||||
            }
 | 
			
		||||
            EventType::ButtonReleased(_, code) => {
 | 
			
		||||
                let mappings = self.map_button(&event.id, &code)?;
 | 
			
		||||
                (VBKey::empty(), mappings)
 | 
			
		||||
            }
 | 
			
		||||
            EventType::AxisChanged(_, value, code) => {
 | 
			
		||||
                let (neg, pos) = self.map_axis(&event.id, &code)?;
 | 
			
		||||
                let mut pressed = VBKey::empty();
 | 
			
		||||
                let mut released = VBKey::empty();
 | 
			
		||||
                if value < -0.75 {
 | 
			
		||||
                    pressed = pressed.union(neg);
 | 
			
		||||
                }
 | 
			
		||||
                if value > 0.75 {
 | 
			
		||||
                    pressed = pressed.union(pos);
 | 
			
		||||
                }
 | 
			
		||||
                if value > -0.65 {
 | 
			
		||||
                    released = released.union(neg);
 | 
			
		||||
                }
 | 
			
		||||
                if value < 0.65 {
 | 
			
		||||
                    released = released.union(pos);
 | 
			
		||||
                }
 | 
			
		||||
                (pressed, released)
 | 
			
		||||
            }
 | 
			
		||||
            _ => {
 | 
			
		||||
                return None;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        self.update_state(pressed, released)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update_state(&mut self, pressed: VBKey, released: VBKey) -> Option<VBKey> {
 | 
			
		||||
        let old_state = self.state;
 | 
			
		||||
        self.state = self.state.union(pressed).difference(released);
 | 
			
		||||
        if self.state != old_state {
 | 
			
		||||
            Some(self.state)
 | 
			
		||||
        } else {
 | 
			
		||||
            None
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn map_keys(&self, key: &PhysicalKey) -> Option<VBKey> {
 | 
			
		||||
        self.mapping.read().unwrap().map_keyboard(key)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn map_button(&self, id: &GamepadId, code: &Code) -> Option<VBKey> {
 | 
			
		||||
        self.mapping.read().unwrap().map_button(id, code)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn map_axis(&self, id: &GamepadId, code: &Code) -> Option<(VBKey, VBKey)> {
 | 
			
		||||
        self.mapping.read().unwrap().map_axis(id, code)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct ControllerManager {
 | 
			
		||||
    client: EmulatorClient,
 | 
			
		||||
    controllers: [Controller; 2],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ControllerManager {
 | 
			
		||||
    pub fn new(client: EmulatorClient, mappings: &MappingProvider) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            client,
 | 
			
		||||
            controllers: [
 | 
			
		||||
                Controller::new(SimId::Player1, mappings),
 | 
			
		||||
                Controller::new(SimId::Player2, mappings),
 | 
			
		||||
            ],
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn handle_key_event(&mut self, event: &KeyEvent) {
 | 
			
		||||
        for controller in &mut self.controllers {
 | 
			
		||||
            if let Some(pressed) = controller.key_event(event) {
 | 
			
		||||
                self.client
 | 
			
		||||
                    .send_command(EmulatorCommand::SetKeys(controller.sim_id, pressed));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn handle_gamepad_event(&mut self, event: &GamepadEvent) {
 | 
			
		||||
        for controller in &mut self.controllers {
 | 
			
		||||
            if let Some(pressed) = controller.gamepad_event(event) {
 | 
			
		||||
                self.client
 | 
			
		||||
                    .send_command(EmulatorCommand::SetKeys(controller.sim_id, pressed));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										285
									
								
								src/emulator.rs
								
								
								
								
							
							
						
						
									
										285
									
								
								src/emulator.rs
								
								
								
								
							| 
						 | 
				
			
			@ -1,8 +1,9 @@
 | 
			
		|||
use std::{
 | 
			
		||||
    collections::HashMap,
 | 
			
		||||
    fs,
 | 
			
		||||
    path::{Path, PathBuf},
 | 
			
		||||
    sync::{
 | 
			
		||||
        atomic::{AtomicBool, Ordering},
 | 
			
		||||
        atomic::{AtomicBool, AtomicUsize, Ordering},
 | 
			
		||||
        mpsc::{self, RecvError, TryRecvError},
 | 
			
		||||
        Arc,
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -10,17 +11,37 @@ use std::{
 | 
			
		|||
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    audio::Audio,
 | 
			
		||||
    renderer::GameRenderer,
 | 
			
		||||
    shrooms_vb_core::{CoreVB, VBKey},
 | 
			
		||||
};
 | 
			
		||||
use crate::{audio::Audio, graphics::TextureSink};
 | 
			
		||||
pub use shrooms_vb_core::VBKey;
 | 
			
		||||
use shrooms_vb_core::{Sim, EXPECTED_FRAME_SIZE};
 | 
			
		||||
 | 
			
		||||
mod shrooms_vb_core;
 | 
			
		||||
 | 
			
		||||
pub struct EmulatorBuilder {
 | 
			
		||||
    rom: Option<PathBuf>,
 | 
			
		||||
    commands: mpsc::Receiver<EmulatorCommand>,
 | 
			
		||||
    running: Arc<AtomicBool>,
 | 
			
		||||
    has_game: Arc<AtomicBool>,
 | 
			
		||||
    sim_count: Arc<AtomicUsize>,
 | 
			
		||||
    running: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    has_game: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    audio_on: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    linked: Arc<AtomicBool>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
 | 
			
		||||
pub enum SimId {
 | 
			
		||||
    Player1,
 | 
			
		||||
    Player2,
 | 
			
		||||
}
 | 
			
		||||
impl SimId {
 | 
			
		||||
    pub const fn values() -> [Self; 2] {
 | 
			
		||||
        [Self::Player1, Self::Player2]
 | 
			
		||||
    }
 | 
			
		||||
    pub const fn to_index(self) -> usize {
 | 
			
		||||
        match self {
 | 
			
		||||
            Self::Player1 => 0,
 | 
			
		||||
            Self::Player2 => 1,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl EmulatorBuilder {
 | 
			
		||||
| 
						 | 
				
			
			@ -29,13 +50,19 @@ impl EmulatorBuilder {
 | 
			
		|||
        let builder = Self {
 | 
			
		||||
            rom: None,
 | 
			
		||||
            commands,
 | 
			
		||||
            running: Arc::new(AtomicBool::new(false)),
 | 
			
		||||
            has_game: Arc::new(AtomicBool::new(false)),
 | 
			
		||||
            sim_count: Arc::new(AtomicUsize::new(0)),
 | 
			
		||||
            running: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]),
 | 
			
		||||
            has_game: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]),
 | 
			
		||||
            audio_on: Arc::new([AtomicBool::new(true), AtomicBool::new(true)]),
 | 
			
		||||
            linked: Arc::new(AtomicBool::new(false)),
 | 
			
		||||
        };
 | 
			
		||||
        let client = EmulatorClient {
 | 
			
		||||
            queue,
 | 
			
		||||
            sim_count: builder.sim_count.clone(),
 | 
			
		||||
            running: builder.running.clone(),
 | 
			
		||||
            has_game: builder.has_game.clone(),
 | 
			
		||||
            audio_on: builder.audio_on.clone(),
 | 
			
		||||
            linked: builder.linked.clone(),
 | 
			
		||||
        };
 | 
			
		||||
        (builder, client)
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -48,69 +75,170 @@ impl EmulatorBuilder {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn build(self) -> Result<Emulator> {
 | 
			
		||||
        let mut emulator = Emulator::new(self.commands, self.running, self.has_game)?;
 | 
			
		||||
        let mut emulator = Emulator::new(
 | 
			
		||||
            self.commands,
 | 
			
		||||
            self.sim_count,
 | 
			
		||||
            self.running,
 | 
			
		||||
            self.has_game,
 | 
			
		||||
            self.audio_on,
 | 
			
		||||
            self.linked,
 | 
			
		||||
        )?;
 | 
			
		||||
        if let Some(path) = self.rom {
 | 
			
		||||
            emulator.load_rom(&path)?;
 | 
			
		||||
            emulator.load_rom(SimId::Player1, &path)?;
 | 
			
		||||
        }
 | 
			
		||||
        Ok(emulator)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct Emulator {
 | 
			
		||||
    sim: CoreVB,
 | 
			
		||||
    sims: Vec<Sim>,
 | 
			
		||||
    audio: Audio,
 | 
			
		||||
    commands: mpsc::Receiver<EmulatorCommand>,
 | 
			
		||||
    renderer: Option<GameRenderer>,
 | 
			
		||||
    running: Arc<AtomicBool>,
 | 
			
		||||
    has_game: Arc<AtomicBool>,
 | 
			
		||||
    sim_count: Arc<AtomicUsize>,
 | 
			
		||||
    running: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    has_game: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    audio_on: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    linked: Arc<AtomicBool>,
 | 
			
		||||
    renderers: HashMap<SimId, TextureSink>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Emulator {
 | 
			
		||||
    fn new(
 | 
			
		||||
        commands: mpsc::Receiver<EmulatorCommand>,
 | 
			
		||||
        running: Arc<AtomicBool>,
 | 
			
		||||
        has_game: Arc<AtomicBool>,
 | 
			
		||||
        sim_count: Arc<AtomicUsize>,
 | 
			
		||||
        running: Arc<[AtomicBool; 2]>,
 | 
			
		||||
        has_game: Arc<[AtomicBool; 2]>,
 | 
			
		||||
        audio_on: Arc<[AtomicBool; 2]>,
 | 
			
		||||
        linked: Arc<AtomicBool>,
 | 
			
		||||
    ) -> Result<Self> {
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            sim: CoreVB::new(),
 | 
			
		||||
            sims: vec![],
 | 
			
		||||
            audio: Audio::init()?,
 | 
			
		||||
            commands,
 | 
			
		||||
            renderer: None,
 | 
			
		||||
            sim_count,
 | 
			
		||||
            running,
 | 
			
		||||
            has_game,
 | 
			
		||||
            audio_on,
 | 
			
		||||
            linked,
 | 
			
		||||
            renderers: HashMap::new(),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn load_rom(&mut self, path: &Path) -> Result<()> {
 | 
			
		||||
    pub fn load_rom(&mut self, sim_id: SimId, path: &Path) -> Result<()> {
 | 
			
		||||
        let bytes = fs::read(path)?;
 | 
			
		||||
        self.sim.reset();
 | 
			
		||||
        self.sim.load_rom(bytes)?;
 | 
			
		||||
        self.has_game.store(true, Ordering::Release);
 | 
			
		||||
        self.running.store(true, Ordering::Release);
 | 
			
		||||
        self.reset_sim(sim_id, Some(bytes))?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn start_second_sim(&mut self, rom: Option<PathBuf>) -> Result<()> {
 | 
			
		||||
        let bytes = if let Some(path) = rom {
 | 
			
		||||
            Some(fs::read(path)?)
 | 
			
		||||
        } else {
 | 
			
		||||
            self.sims.first().and_then(|s| s.clone_rom())
 | 
			
		||||
        };
 | 
			
		||||
        self.reset_sim(SimId::Player2, bytes)?;
 | 
			
		||||
        self.link_sims();
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn reset_sim(&mut self, sim_id: SimId, new_rom: Option<Vec<u8>>) -> Result<()> {
 | 
			
		||||
        let index = sim_id.to_index();
 | 
			
		||||
        while self.sims.len() <= index {
 | 
			
		||||
            self.sims.push(Sim::new());
 | 
			
		||||
        }
 | 
			
		||||
        self.sim_count.store(self.sims.len(), Ordering::Relaxed);
 | 
			
		||||
        let sim = &mut self.sims[index];
 | 
			
		||||
        sim.reset();
 | 
			
		||||
        if let Some(bytes) = new_rom {
 | 
			
		||||
            sim.load_rom(bytes)?;
 | 
			
		||||
            self.has_game[index].store(true, Ordering::Release);
 | 
			
		||||
        }
 | 
			
		||||
        if self.has_game[index].load(Ordering::Acquire) {
 | 
			
		||||
            self.running[index].store(true, Ordering::Release);
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn link_sims(&mut self) {
 | 
			
		||||
        let (first, second) = self.sims.split_at_mut(1);
 | 
			
		||||
        let Some(first) = first.first_mut() else {
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        let Some(second) = second.first_mut() else {
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        first.link(second);
 | 
			
		||||
        self.linked.store(true, Ordering::Release);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn unlink_sims(&mut self) {
 | 
			
		||||
        let Some(first) = self.sims.first_mut() else {
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        first.unlink();
 | 
			
		||||
        self.linked.store(false, Ordering::Release);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn stop_second_sim(&mut self) {
 | 
			
		||||
        self.renderers.remove(&SimId::Player2);
 | 
			
		||||
        self.sims.truncate(1);
 | 
			
		||||
        self.sim_count.store(self.sims.len(), Ordering::Relaxed);
 | 
			
		||||
        self.running[SimId::Player2.to_index()].store(false, Ordering::Release);
 | 
			
		||||
        self.has_game[SimId::Player2.to_index()].store(false, Ordering::Release);
 | 
			
		||||
        self.linked.store(false, Ordering::Release);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn run(&mut self) {
 | 
			
		||||
        let mut eye_contents = vec![0u8; 384 * 224 * 2];
 | 
			
		||||
        let mut audio_samples = vec![];
 | 
			
		||||
        loop {
 | 
			
		||||
            let mut idle = true;
 | 
			
		||||
            if self.running.load(Ordering::Acquire) {
 | 
			
		||||
                idle = false;
 | 
			
		||||
                self.sim.emulate_frame();
 | 
			
		||||
            let p1_running = self.running[SimId::Player1.to_index()].load(Ordering::Acquire);
 | 
			
		||||
            let p2_running = self.running[SimId::Player2.to_index()].load(Ordering::Acquire);
 | 
			
		||||
            let mut idle = p1_running || p2_running;
 | 
			
		||||
            if p1_running && p2_running {
 | 
			
		||||
                Sim::emulate_many(&mut self.sims);
 | 
			
		||||
            } else if p1_running {
 | 
			
		||||
                self.sims[SimId::Player1.to_index()].emulate();
 | 
			
		||||
            } else if p2_running {
 | 
			
		||||
                self.sims[SimId::Player2.to_index()].emulate();
 | 
			
		||||
            }
 | 
			
		||||
            if let Some(renderer) = &mut self.renderer {
 | 
			
		||||
                if self.sim.read_pixels(&mut eye_contents) {
 | 
			
		||||
 | 
			
		||||
            for sim_id in SimId::values() {
 | 
			
		||||
                let Some(renderer) = self.renderers.get_mut(&sim_id) else {
 | 
			
		||||
                    continue;
 | 
			
		||||
                };
 | 
			
		||||
                let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
 | 
			
		||||
                    continue;
 | 
			
		||||
                };
 | 
			
		||||
                if sim.read_pixels(&mut eye_contents) {
 | 
			
		||||
                    idle = false;
 | 
			
		||||
                    renderer.render(&eye_contents);
 | 
			
		||||
                    if renderer.queue_render(&eye_contents).is_err() {
 | 
			
		||||
                        self.renderers.remove(&sim_id);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            self.sim.read_samples(&mut audio_samples);
 | 
			
		||||
            if !audio_samples.is_empty() {
 | 
			
		||||
            }
 | 
			
		||||
            let p1_audio =
 | 
			
		||||
                p1_running && self.audio_on[SimId::Player1.to_index()].load(Ordering::Acquire);
 | 
			
		||||
            let p2_audio =
 | 
			
		||||
                p2_running && self.audio_on[SimId::Player2.to_index()].load(Ordering::Acquire);
 | 
			
		||||
            let weight = if p1_audio && p2_audio { 0.5 } else { 1.0 };
 | 
			
		||||
            if p1_audio {
 | 
			
		||||
                if let Some(sim) = self.sims.get_mut(SimId::Player1.to_index()) {
 | 
			
		||||
                    sim.read_samples(&mut audio_samples, weight);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if p2_audio {
 | 
			
		||||
                if let Some(sim) = self.sims.get_mut(SimId::Player2.to_index()) {
 | 
			
		||||
                    sim.read_samples(&mut audio_samples, weight);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if audio_samples.is_empty() {
 | 
			
		||||
                audio_samples.resize(EXPECTED_FRAME_SIZE, 0.0);
 | 
			
		||||
            } else {
 | 
			
		||||
                idle = false;
 | 
			
		||||
            }
 | 
			
		||||
            self.audio.update(&audio_samples);
 | 
			
		||||
            audio_samples.clear();
 | 
			
		||||
            }
 | 
			
		||||
            if idle {
 | 
			
		||||
                // The game is paused, and we have output all the video/audio we have.
 | 
			
		||||
                // Block the thread until a new command comes in.
 | 
			
		||||
| 
						 | 
				
			
			@ -137,28 +265,54 @@ impl Emulator {
 | 
			
		|||
 | 
			
		||||
    fn handle_command(&mut self, command: EmulatorCommand) {
 | 
			
		||||
        match command {
 | 
			
		||||
            EmulatorCommand::SetRenderer(renderer) => {
 | 
			
		||||
                self.renderer = Some(renderer);
 | 
			
		||||
            EmulatorCommand::SetRenderer(sim_id, renderer) => {
 | 
			
		||||
                self.renderers.insert(sim_id, renderer);
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::LoadGame(path) => {
 | 
			
		||||
                if let Err(error) = self.load_rom(&path) {
 | 
			
		||||
            EmulatorCommand::LoadGame(sim_id, path) => {
 | 
			
		||||
                if let Err(error) = self.load_rom(sim_id, &path) {
 | 
			
		||||
                    eprintln!("error loading rom: {}", error);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::StartSecondSim(path) => {
 | 
			
		||||
                if let Err(error) = self.start_second_sim(path) {
 | 
			
		||||
                    eprintln!("error starting second sim: {}", error);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::StopSecondSim => {
 | 
			
		||||
                self.stop_second_sim();
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::Pause => {
 | 
			
		||||
                self.running.store(false, Ordering::Release);
 | 
			
		||||
                for sim in SimId::values() {
 | 
			
		||||
                    self.running[sim.to_index()].store(false, Ordering::Release);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::Resume => {
 | 
			
		||||
                if self.has_game.load(Ordering::Acquire) {
 | 
			
		||||
                    self.running.store(true, Ordering::Relaxed);
 | 
			
		||||
                for sim_id in SimId::values() {
 | 
			
		||||
                    let index = sim_id.to_index();
 | 
			
		||||
                    if self.has_game[index].load(Ordering::Acquire) {
 | 
			
		||||
                        self.running[index].store(true, Ordering::Relaxed);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            EmulatorCommand::Reset => {
 | 
			
		||||
                self.sim.reset();
 | 
			
		||||
                self.running.store(true, Ordering::Release);
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::SetKeys(keys) => {
 | 
			
		||||
                self.sim.set_keys(keys);
 | 
			
		||||
            EmulatorCommand::SetAudioEnabled(p1, p2) => {
 | 
			
		||||
                self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release);
 | 
			
		||||
                self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release);
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::Link => {
 | 
			
		||||
                self.link_sims();
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::Unlink => {
 | 
			
		||||
                self.unlink_sims();
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::Reset(sim_id) => {
 | 
			
		||||
                if let Err(error) = self.reset_sim(sim_id, None) {
 | 
			
		||||
                    eprintln!("error resetting sim: {}", error);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            EmulatorCommand::SetKeys(sim_id, keys) => {
 | 
			
		||||
                if let Some(sim) = self.sims.get_mut(sim_id.to_index()) {
 | 
			
		||||
                    sim.set_keys(keys);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -166,27 +320,44 @@ impl Emulator {
 | 
			
		|||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub enum EmulatorCommand {
 | 
			
		||||
    SetRenderer(GameRenderer),
 | 
			
		||||
    LoadGame(PathBuf),
 | 
			
		||||
    SetRenderer(SimId, TextureSink),
 | 
			
		||||
    LoadGame(SimId, PathBuf),
 | 
			
		||||
    StartSecondSim(Option<PathBuf>),
 | 
			
		||||
    StopSecondSim,
 | 
			
		||||
    Pause,
 | 
			
		||||
    Resume,
 | 
			
		||||
    Reset,
 | 
			
		||||
    SetKeys(VBKey),
 | 
			
		||||
    SetAudioEnabled(bool, bool),
 | 
			
		||||
    Link,
 | 
			
		||||
    Unlink,
 | 
			
		||||
    Reset(SimId),
 | 
			
		||||
    SetKeys(SimId, VBKey),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct EmulatorClient {
 | 
			
		||||
    queue: mpsc::Sender<EmulatorCommand>,
 | 
			
		||||
    running: Arc<AtomicBool>,
 | 
			
		||||
    has_game: Arc<AtomicBool>,
 | 
			
		||||
    sim_count: Arc<AtomicUsize>,
 | 
			
		||||
    running: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    has_game: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    audio_on: Arc<[AtomicBool; 2]>,
 | 
			
		||||
    linked: Arc<AtomicBool>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl EmulatorClient {
 | 
			
		||||
    pub fn is_running(&self) -> bool {
 | 
			
		||||
        self.running.load(Ordering::Acquire)
 | 
			
		||||
    pub fn has_player_2(&self) -> bool {
 | 
			
		||||
        self.sim_count.load(Ordering::Acquire) == 2
 | 
			
		||||
    }
 | 
			
		||||
    pub fn has_game(&self) -> bool {
 | 
			
		||||
        self.has_game.load(Ordering::Acquire)
 | 
			
		||||
    pub fn is_running(&self, sim_id: SimId) -> bool {
 | 
			
		||||
        self.running[sim_id.to_index()].load(Ordering::Acquire)
 | 
			
		||||
    }
 | 
			
		||||
    pub fn has_game(&self, sim_id: SimId) -> bool {
 | 
			
		||||
        self.has_game[sim_id.to_index()].load(Ordering::Acquire)
 | 
			
		||||
    }
 | 
			
		||||
    pub fn are_sims_linked(&self) -> bool {
 | 
			
		||||
        self.linked.load(Ordering::Acquire)
 | 
			
		||||
    }
 | 
			
		||||
    pub fn is_audio_enabled(&self, sim_id: SimId) -> bool {
 | 
			
		||||
        self.audio_on[sim_id.to_index()].load(Ordering::Acquire)
 | 
			
		||||
    }
 | 
			
		||||
    pub fn send_command(&self, command: EmulatorCommand) {
 | 
			
		||||
        if let Err(err) = self.queue.send(command) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,6 +25,12 @@ enum VBDataType {
 | 
			
		|||
    F32 = 5,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[repr(i32)]
 | 
			
		||||
#[derive(FromPrimitive, ToPrimitive)]
 | 
			
		||||
enum VBOption {
 | 
			
		||||
    PseudoHalt = 0,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bitflags! {
 | 
			
		||||
    #[repr(transparent)]
 | 
			
		||||
    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
 | 
			
		||||
| 
						 | 
				
			
			@ -54,6 +60,8 @@ type OnFrame = extern "C" fn(sim: *mut VB) -> c_int;
 | 
			
		|||
extern "C" {
 | 
			
		||||
    #[link_name = "vbEmulate"]
 | 
			
		||||
    fn vb_emulate(sim: *mut VB, cycles: *mut u32) -> c_int;
 | 
			
		||||
    #[link_name = "vbEmulateEx"]
 | 
			
		||||
    fn vb_emulate_ex(sims: *mut *mut VB, count: c_uint, cycles: *mut u32) -> c_int;
 | 
			
		||||
    #[link_name = "vbGetCartROM"]
 | 
			
		||||
    fn vb_get_cart_rom(sim: *mut VB, size: *mut u32) -> *mut c_void;
 | 
			
		||||
    #[link_name = "vbGetPixels"]
 | 
			
		||||
| 
						 | 
				
			
			@ -81,10 +89,14 @@ extern "C" {
 | 
			
		|||
    fn vb_reset(sim: *mut VB);
 | 
			
		||||
    #[link_name = "vbSetCartROM"]
 | 
			
		||||
    fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int;
 | 
			
		||||
    #[link_name = "vbSetKeys"]
 | 
			
		||||
    fn vb_set_keys(sim: *mut VB, keys: u16) -> u16;
 | 
			
		||||
    #[link_name = "vbSetFrameCallback"]
 | 
			
		||||
    fn vb_set_frame_callback(sim: *mut VB, on_frame: OnFrame);
 | 
			
		||||
    #[link_name = "vbSetKeys"]
 | 
			
		||||
    fn vb_set_keys(sim: *mut VB, keys: u16) -> u16;
 | 
			
		||||
    #[link_name = "vbSetOption"]
 | 
			
		||||
    fn vb_set_option(sim: *mut VB, key: VBOption, value: c_int);
 | 
			
		||||
    #[link_name = "vbSetPeer"]
 | 
			
		||||
    fn vb_set_peer(sim: *mut VB, peer: *mut VB);
 | 
			
		||||
    #[link_name = "vbSetSamples"]
 | 
			
		||||
    fn vb_set_samples(
 | 
			
		||||
        sim: *mut VB,
 | 
			
		||||
| 
						 | 
				
			
			@ -108,19 +120,21 @@ extern "C" fn on_frame(sim: *mut VB) -> i32 {
 | 
			
		|||
 | 
			
		||||
const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4;
 | 
			
		||||
const AUDIO_CAPACITY_FLOATS: usize = AUDIO_CAPACITY_SAMPLES * 2;
 | 
			
		||||
pub const EXPECTED_FRAME_SIZE: usize = 834 * 2;
 | 
			
		||||
 | 
			
		||||
struct VBState {
 | 
			
		||||
    frame_seen: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct CoreVB {
 | 
			
		||||
#[repr(transparent)]
 | 
			
		||||
pub struct Sim {
 | 
			
		||||
    sim: *mut VB,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SAFETY: the memory pointed to by sim is valid
 | 
			
		||||
unsafe impl Send for CoreVB {}
 | 
			
		||||
unsafe impl Send for Sim {}
 | 
			
		||||
 | 
			
		||||
impl CoreVB {
 | 
			
		||||
impl Sim {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        // init the VB instance itself
 | 
			
		||||
        let size = unsafe { vb_size_of() };
 | 
			
		||||
| 
						 | 
				
			
			@ -128,6 +142,7 @@ impl CoreVB {
 | 
			
		|||
        let memory = vec![0u64; size.div_ceil(4)];
 | 
			
		||||
        let sim: *mut VB = Box::into_raw(memory.into_boxed_slice()).cast();
 | 
			
		||||
        unsafe { vb_init(sim) };
 | 
			
		||||
        unsafe { vb_set_option(sim, VBOption::PseudoHalt, 1) };
 | 
			
		||||
        unsafe { vb_reset(sim) };
 | 
			
		||||
 | 
			
		||||
        // set up userdata
 | 
			
		||||
| 
						 | 
				
			
			@ -140,7 +155,7 @@ impl CoreVB {
 | 
			
		|||
        let samples: *mut c_void = Box::into_raw(audio_buffer.into_boxed_slice()).cast();
 | 
			
		||||
        unsafe { vb_set_samples(sim, samples, VBDataType::F32, AUDIO_CAPACITY_SAMPLES as u32) };
 | 
			
		||||
 | 
			
		||||
        CoreVB { sim }
 | 
			
		||||
        Sim { sim }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn reset(&mut self) {
 | 
			
		||||
| 
						 | 
				
			
			@ -162,6 +177,17 @@ impl CoreVB {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn clone_rom(&self) -> Option<Vec<u8>> {
 | 
			
		||||
        let mut size = 0;
 | 
			
		||||
        let rom = unsafe { vb_get_cart_rom(self.sim, &mut size) };
 | 
			
		||||
        if rom.is_null() {
 | 
			
		||||
            return None;
 | 
			
		||||
        }
 | 
			
		||||
        // SAFETY: rom definitely points to a valid array of `size` bytes
 | 
			
		||||
        let slice: &[u8] = unsafe { slice::from_raw_parts(rom.cast(), size as usize) };
 | 
			
		||||
        Some(slice.to_vec())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn unload_rom(&mut self) -> Option<Vec<u8>> {
 | 
			
		||||
        let mut size = 0;
 | 
			
		||||
        let rom = unsafe { vb_get_cart_rom(self.sim, &mut size) };
 | 
			
		||||
| 
						 | 
				
			
			@ -173,11 +199,26 @@ impl CoreVB {
 | 
			
		|||
        Some(vec)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn emulate_frame(&mut self) {
 | 
			
		||||
    pub fn link(&mut self, peer: &mut Sim) {
 | 
			
		||||
        unsafe { vb_set_peer(self.sim, peer.sim) };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn unlink(&mut self) {
 | 
			
		||||
        unsafe { vb_set_peer(self.sim, ptr::null_mut()) };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn emulate(&mut self) {
 | 
			
		||||
        let mut cycles = 20_000_000;
 | 
			
		||||
        unsafe { vb_emulate(self.sim, &mut cycles) };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn emulate_many(sims: &mut [Sim]) {
 | 
			
		||||
        let mut cycles = 20_000_000;
 | 
			
		||||
        let count = sims.len() as c_uint;
 | 
			
		||||
        let sims = sims.as_mut_ptr().cast();
 | 
			
		||||
        unsafe { vb_emulate_ex(sims, count, &mut cycles) };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn read_pixels(&mut self, buffers: &mut [u8]) -> bool {
 | 
			
		||||
        // SAFETY: the *mut VB owns its userdata.
 | 
			
		||||
        // There is no way for the userdata to be null or otherwise invalid.
 | 
			
		||||
| 
						 | 
				
			
			@ -203,14 +244,17 @@ impl CoreVB {
 | 
			
		|||
        true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn read_samples(&mut self, samples: &mut Vec<f32>) {
 | 
			
		||||
    pub fn read_samples(&mut self, samples: &mut Vec<f32>, weight: f32) {
 | 
			
		||||
        let mut position = 0;
 | 
			
		||||
        let ptr =
 | 
			
		||||
            unsafe { vb_get_samples(self.sim, ptr::null_mut(), ptr::null_mut(), &mut position) };
 | 
			
		||||
        // SAFETY: position is an offset in a buffer of (f32, f32). so, position * 2 is an offset in a buffer of f32.
 | 
			
		||||
        let read_samples: &mut [f32] =
 | 
			
		||||
            unsafe { slice::from_raw_parts_mut(ptr.cast(), position as usize * 2) };
 | 
			
		||||
        samples.extend_from_slice(read_samples);
 | 
			
		||||
        let read_samples: &[f32] =
 | 
			
		||||
            unsafe { slice::from_raw_parts(ptr.cast(), position as usize * 2) };
 | 
			
		||||
        samples.resize(read_samples.len(), 0.0);
 | 
			
		||||
        for (index, sample) in read_samples.iter().enumerate() {
 | 
			
		||||
            samples[index] += sample * weight;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        unsafe {
 | 
			
		||||
            vb_set_samples(
 | 
			
		||||
| 
						 | 
				
			
			@ -227,7 +271,7 @@ impl CoreVB {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Drop for CoreVB {
 | 
			
		||||
impl Drop for Sim {
 | 
			
		||||
    fn drop(&mut self) {
 | 
			
		||||
        let ptr =
 | 
			
		||||
            unsafe { vb_get_samples(self.sim, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()) };
 | 
			
		||||
| 
						 | 
				
			
			@ -243,6 +287,9 @@ impl Drop for CoreVB {
 | 
			
		|||
        // SAFETY: we made this pointer ourselves, we can for sure free it
 | 
			
		||||
        unsafe { drop(Box::from_raw(ptr)) };
 | 
			
		||||
 | 
			
		||||
        // If we're linked to another sim, unlink from them.
 | 
			
		||||
        unsafe { vb_set_peer(self.sim, ptr::null_mut()) };
 | 
			
		||||
 | 
			
		||||
        let len = unsafe { vb_size_of() }.div_ceil(4);
 | 
			
		||||
        // SAFETY: the sim's memory originally came from a Vec<u64>
 | 
			
		||||
        let bytes: Vec<u64> = unsafe { Vec::from_raw_parts(self.sim.cast(), len, len) };
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,143 @@
 | 
			
		|||
use std::{
 | 
			
		||||
    sync::{
 | 
			
		||||
        atomic::{AtomicU64, Ordering},
 | 
			
		||||
        mpsc, Arc, Mutex, MutexGuard,
 | 
			
		||||
    },
 | 
			
		||||
    thread,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use anyhow::{bail, Result};
 | 
			
		||||
use itertools::Itertools as _;
 | 
			
		||||
use wgpu::{
 | 
			
		||||
    Device, Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, Queue, Texture,
 | 
			
		||||
    TextureDescriptor, TextureFormat, TextureUsages, TextureView, TextureViewDescriptor,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct TextureSink {
 | 
			
		||||
    buffers: Arc<BufferPool>,
 | 
			
		||||
    sink: mpsc::Sender<u64>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TextureSink {
 | 
			
		||||
    pub fn new(device: &Device, queue: Arc<Queue>) -> (Self, TextureView) {
 | 
			
		||||
        let texture = Self::create_texture(device);
 | 
			
		||||
        let view = texture.create_view(&TextureViewDescriptor::default());
 | 
			
		||||
        let buffers = Arc::new(BufferPool::new());
 | 
			
		||||
        let (sink, source) = mpsc::channel();
 | 
			
		||||
        let bufs = buffers.clone();
 | 
			
		||||
        thread::spawn(move || {
 | 
			
		||||
            let mut local_buf = vec![0; 384 * 224 * 2];
 | 
			
		||||
            while let Ok(id) = source.recv() {
 | 
			
		||||
                {
 | 
			
		||||
                    let Some(bytes) = bufs.read(id) else {
 | 
			
		||||
                        continue;
 | 
			
		||||
                    };
 | 
			
		||||
                    local_buf.copy_from_slice(bytes.as_slice());
 | 
			
		||||
                }
 | 
			
		||||
                Self::write_texture(&queue, &texture, local_buf.as_slice());
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        let sink = Self { buffers, sink };
 | 
			
		||||
        (sink, view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn queue_render(&mut self, bytes: &[u8]) -> Result<()> {
 | 
			
		||||
        let id = {
 | 
			
		||||
            let (mut buf, id) = self.buffers.write()?;
 | 
			
		||||
            buf.copy_from_slice(bytes);
 | 
			
		||||
            id
 | 
			
		||||
        };
 | 
			
		||||
        self.sink.send(id)?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn create_texture(device: &Device) -> Texture {
 | 
			
		||||
        let desc = TextureDescriptor {
 | 
			
		||||
            label: Some("eyes"),
 | 
			
		||||
            size: Extent3d {
 | 
			
		||||
                width: 384,
 | 
			
		||||
                height: 224,
 | 
			
		||||
                depth_or_array_layers: 1,
 | 
			
		||||
            },
 | 
			
		||||
            mip_level_count: 1,
 | 
			
		||||
            sample_count: 1,
 | 
			
		||||
            dimension: wgpu::TextureDimension::D2,
 | 
			
		||||
            format: TextureFormat::Rg8Unorm,
 | 
			
		||||
            usage: TextureUsages::COPY_SRC
 | 
			
		||||
                | TextureUsages::COPY_DST
 | 
			
		||||
                | TextureUsages::TEXTURE_BINDING,
 | 
			
		||||
            view_formats: &[TextureFormat::Rg8Unorm],
 | 
			
		||||
        };
 | 
			
		||||
        device.create_texture(&desc)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn write_texture(queue: &Queue, texture: &Texture, bytes: &[u8]) {
 | 
			
		||||
        let texture = ImageCopyTexture {
 | 
			
		||||
            texture,
 | 
			
		||||
            mip_level: 0,
 | 
			
		||||
            origin: Origin3d::ZERO,
 | 
			
		||||
            aspect: wgpu::TextureAspect::All,
 | 
			
		||||
        };
 | 
			
		||||
        let size = Extent3d {
 | 
			
		||||
            width: 384,
 | 
			
		||||
            height: 224,
 | 
			
		||||
            depth_or_array_layers: 1,
 | 
			
		||||
        };
 | 
			
		||||
        let data_layout = ImageDataLayout {
 | 
			
		||||
            offset: 0,
 | 
			
		||||
            bytes_per_row: Some(384 * 2),
 | 
			
		||||
            rows_per_image: Some(224),
 | 
			
		||||
        };
 | 
			
		||||
        queue.write_texture(texture, bytes, data_layout, size);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
struct BufferPool {
 | 
			
		||||
    buffers: [Buffer; 3],
 | 
			
		||||
}
 | 
			
		||||
impl BufferPool {
 | 
			
		||||
    fn new() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            buffers: std::array::from_fn(|i| Buffer::new(i as u64)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn read(&self, id: u64) -> Option<MutexGuard<'_, Vec<u8>>> {
 | 
			
		||||
        let buf = self
 | 
			
		||||
            .buffers
 | 
			
		||||
            .iter()
 | 
			
		||||
            .find(|buf| buf.id.load(Ordering::Acquire) == id)?;
 | 
			
		||||
        buf.data.lock().ok()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn write(&self) -> Result<(MutexGuard<'_, Vec<u8>>, u64)> {
 | 
			
		||||
        let (min, max) = self
 | 
			
		||||
            .buffers
 | 
			
		||||
            .iter()
 | 
			
		||||
            .minmax_by_key(|buf| buf.id.load(Ordering::Acquire))
 | 
			
		||||
            .into_option()
 | 
			
		||||
            .unwrap();
 | 
			
		||||
        let Ok(lock) = min.data.lock() else {
 | 
			
		||||
            bail!("lock was poisoned")
 | 
			
		||||
        };
 | 
			
		||||
        let id = max.id.load(Ordering::Acquire) + 1;
 | 
			
		||||
        min.id.store(id, Ordering::Release);
 | 
			
		||||
        Ok((lock, id))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
struct Buffer {
 | 
			
		||||
    data: Mutex<Vec<u8>>,
 | 
			
		||||
    id: AtomicU64,
 | 
			
		||||
}
 | 
			
		||||
impl Buffer {
 | 
			
		||||
    fn new(id: u64) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            data: Mutex::new(vec![0; 384 * 224 * 2]),
 | 
			
		||||
            id: AtomicU64::new(id),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										379
									
								
								src/input.rs
								
								
								
								
							
							
						
						
									
										379
									
								
								src/input.rs
								
								
								
								
							| 
						 | 
				
			
			@ -1,71 +1,344 @@
 | 
			
		|||
use std::collections::HashMap;
 | 
			
		||||
 | 
			
		||||
use winit::{
 | 
			
		||||
    event::KeyEvent,
 | 
			
		||||
    keyboard::{Key, NamedKey},
 | 
			
		||||
    platform::modifier_supplement::KeyEventExtModifierSupplement,
 | 
			
		||||
use std::{
 | 
			
		||||
    collections::{hash_map::Entry, HashMap},
 | 
			
		||||
    sync::{Arc, RwLock},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use crate::shrooms_vb_core::VBKey;
 | 
			
		||||
use gilrs::{ev::Code, Axis, Button, Gamepad, GamepadId};
 | 
			
		||||
use winit::keyboard::{KeyCode, PhysicalKey};
 | 
			
		||||
 | 
			
		||||
pub struct InputMapper {
 | 
			
		||||
    vb_bindings: HashMap<VBKey, Key>,
 | 
			
		||||
    key_bindings: HashMap<Key, VBKey>,
 | 
			
		||||
use crate::emulator::{SimId, VBKey};
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
 | 
			
		||||
struct DeviceId(u16, u16);
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct GamepadInfo {
 | 
			
		||||
    pub id: GamepadId,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    device_id: DeviceId,
 | 
			
		||||
    pub bound_to: Option<SimId>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl InputMapper {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        let mut mapper = Self {
 | 
			
		||||
            vb_bindings: HashMap::new(),
 | 
			
		||||
            key_bindings: HashMap::new(),
 | 
			
		||||
pub trait Mappings {
 | 
			
		||||
    fn mapping_names(&self) -> HashMap<VBKey, Vec<String>>;
 | 
			
		||||
    fn clear_mappings(&mut self, key: VBKey);
 | 
			
		||||
    fn clear_all_mappings(&mut self);
 | 
			
		||||
    fn use_default_mappings(&mut self);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct GamepadMapping {
 | 
			
		||||
    buttons: HashMap<Code, VBKey>,
 | 
			
		||||
    axes: HashMap<Code, (VBKey, VBKey)>,
 | 
			
		||||
    default_buttons: HashMap<Code, VBKey>,
 | 
			
		||||
    default_axes: HashMap<Code, (VBKey, VBKey)>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl GamepadMapping {
 | 
			
		||||
    fn for_gamepad(gamepad: &Gamepad) -> Self {
 | 
			
		||||
        let mut default_buttons = HashMap::new();
 | 
			
		||||
        let mut default_button = |btn: Button, key: VBKey| {
 | 
			
		||||
            if let Some(code) = gamepad.button_code(btn) {
 | 
			
		||||
                default_buttons.insert(code, key);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        mapper.bind_key(VBKey::SEL, Key::Character("a".into()));
 | 
			
		||||
        mapper.bind_key(VBKey::STA, Key::Character("s".into()));
 | 
			
		||||
        mapper.bind_key(VBKey::B, Key::Character("d".into()));
 | 
			
		||||
        mapper.bind_key(VBKey::A, Key::Character("f".into()));
 | 
			
		||||
        mapper.bind_key(VBKey::LT, Key::Character("e".into()));
 | 
			
		||||
        mapper.bind_key(VBKey::RT, Key::Character("r".into()));
 | 
			
		||||
        mapper.bind_key(VBKey::RU, Key::Character("i".into()));
 | 
			
		||||
        mapper.bind_key(VBKey::RL, Key::Character("j".into()));
 | 
			
		||||
        mapper.bind_key(VBKey::RD, Key::Character("k".into()));
 | 
			
		||||
        mapper.bind_key(VBKey::RR, Key::Character("l".into()));
 | 
			
		||||
        mapper.bind_key(VBKey::LU, Key::Named(NamedKey::ArrowUp));
 | 
			
		||||
        mapper.bind_key(VBKey::LL, Key::Named(NamedKey::ArrowLeft));
 | 
			
		||||
        mapper.bind_key(VBKey::LD, Key::Named(NamedKey::ArrowDown));
 | 
			
		||||
        mapper.bind_key(VBKey::LR, Key::Named(NamedKey::ArrowRight));
 | 
			
		||||
        mapper
 | 
			
		||||
        default_button(Button::South, VBKey::A);
 | 
			
		||||
        default_button(Button::West, VBKey::B);
 | 
			
		||||
        default_button(Button::RightTrigger, VBKey::RT);
 | 
			
		||||
        default_button(Button::LeftTrigger, VBKey::LT);
 | 
			
		||||
        default_button(Button::Start, VBKey::STA);
 | 
			
		||||
        default_button(Button::Select, VBKey::SEL);
 | 
			
		||||
 | 
			
		||||
        let mut default_axes = HashMap::new();
 | 
			
		||||
        let mut default_axis = |axis: Axis, neg: VBKey, pos: VBKey| {
 | 
			
		||||
            if let Some(code) = gamepad.axis_code(axis) {
 | 
			
		||||
                default_axes.insert(code, (neg, pos));
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        default_axis(Axis::LeftStickX, VBKey::LL, VBKey::LR);
 | 
			
		||||
        default_axis(Axis::LeftStickY, VBKey::LD, VBKey::LU);
 | 
			
		||||
        default_axis(Axis::RightStickX, VBKey::RL, VBKey::RR);
 | 
			
		||||
        default_axis(Axis::RightStickY, VBKey::RD, VBKey::RU);
 | 
			
		||||
        default_axis(Axis::DPadX, VBKey::LL, VBKey::LR);
 | 
			
		||||
        default_axis(Axis::DPadY, VBKey::LD, VBKey::LU);
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            buttons: default_buttons.clone(),
 | 
			
		||||
            axes: default_axes.clone(),
 | 
			
		||||
            default_buttons,
 | 
			
		||||
            default_axes,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn binding_names(&self) -> HashMap<VBKey, String> {
 | 
			
		||||
        self.vb_bindings
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(|(k, v)| {
 | 
			
		||||
                let name = match v {
 | 
			
		||||
                    Key::Character(char) => char.to_string(),
 | 
			
		||||
                    Key::Named(key) => format!("{:?}", key),
 | 
			
		||||
    pub fn add_button_mapping(&mut self, key: VBKey, code: Code) {
 | 
			
		||||
        let entry = self.buttons.entry(code).or_insert(VBKey::empty());
 | 
			
		||||
        *entry = entry.union(key);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add_axis_neg_mapping(&mut self, key: VBKey, code: Code) {
 | 
			
		||||
        let entry = self
 | 
			
		||||
            .axes
 | 
			
		||||
            .entry(code)
 | 
			
		||||
            .or_insert((VBKey::empty(), VBKey::empty()));
 | 
			
		||||
        entry.0 = entry.0.union(key);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add_axis_pos_mapping(&mut self, key: VBKey, code: Code) {
 | 
			
		||||
        let entry = self
 | 
			
		||||
            .axes
 | 
			
		||||
            .entry(code)
 | 
			
		||||
            .or_insert((VBKey::empty(), VBKey::empty()));
 | 
			
		||||
        entry.1 = entry.1.union(key);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Mappings for GamepadMapping {
 | 
			
		||||
    fn mapping_names(&self) -> HashMap<VBKey, Vec<String>> {
 | 
			
		||||
        let mut results: HashMap<VBKey, Vec<String>> = HashMap::new();
 | 
			
		||||
        for (axis, (left_keys, right_keys)) in &self.axes {
 | 
			
		||||
            for key in left_keys.iter() {
 | 
			
		||||
                results.entry(key).or_default().push(format!("-{axis}"));
 | 
			
		||||
            }
 | 
			
		||||
            for key in right_keys.iter() {
 | 
			
		||||
                results.entry(key).or_default().push(format!("+{axis}"));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        for (button, keys) in &self.buttons {
 | 
			
		||||
            for key in keys.iter() {
 | 
			
		||||
                results.entry(key).or_default().push(format!("{button}"));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        results
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn clear_mappings(&mut self, key: VBKey) {
 | 
			
		||||
        self.axes.retain(|_, (left, right)| {
 | 
			
		||||
            *left = left.difference(key);
 | 
			
		||||
            *right = right.difference(key);
 | 
			
		||||
            !(left.is_empty() && right.is_empty())
 | 
			
		||||
        });
 | 
			
		||||
        self.buttons.retain(|_, keys| {
 | 
			
		||||
            *keys = keys.difference(key);
 | 
			
		||||
            !keys.is_empty()
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn clear_all_mappings(&mut self) {
 | 
			
		||||
        self.axes.clear();
 | 
			
		||||
        self.buttons.clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn use_default_mappings(&mut self) {
 | 
			
		||||
        self.axes = self.default_axes.clone();
 | 
			
		||||
        self.buttons = self.default_buttons.clone();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default)]
 | 
			
		||||
pub struct InputMapping {
 | 
			
		||||
    keys: HashMap<PhysicalKey, VBKey>,
 | 
			
		||||
    gamepads: HashMap<GamepadId, Arc<RwLock<GamepadMapping>>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl InputMapping {
 | 
			
		||||
    pub fn map_keyboard(&self, key: &PhysicalKey) -> Option<VBKey> {
 | 
			
		||||
        self.keys.get(key).copied()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn map_button(&self, id: &GamepadId, code: &Code) -> Option<VBKey> {
 | 
			
		||||
        let mappings = self.gamepads.get(id)?.read().unwrap();
 | 
			
		||||
        mappings.buttons.get(code).copied()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn map_axis(&self, id: &GamepadId, code: &Code) -> Option<(VBKey, VBKey)> {
 | 
			
		||||
        let mappings = self.gamepads.get(id)?.read().unwrap();
 | 
			
		||||
        mappings.axes.get(code).copied()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add_keyboard_mapping(&mut self, key: VBKey, keyboard_key: PhysicalKey) {
 | 
			
		||||
        let entry = self.keys.entry(keyboard_key).or_insert(VBKey::empty());
 | 
			
		||||
        *entry = entry.union(key);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Mappings for InputMapping {
 | 
			
		||||
    fn mapping_names(&self) -> HashMap<VBKey, Vec<String>> {
 | 
			
		||||
        let mut results: HashMap<VBKey, Vec<String>> = HashMap::new();
 | 
			
		||||
        for (keyboard_key, keys) in &self.keys {
 | 
			
		||||
            let name = match keyboard_key {
 | 
			
		||||
                PhysicalKey::Code(code) => format!("{code:?}"),
 | 
			
		||||
                k => format!("{:?}", k),
 | 
			
		||||
            };
 | 
			
		||||
                (*k, name)
 | 
			
		||||
            })
 | 
			
		||||
            .collect()
 | 
			
		||||
            for key in keys.iter() {
 | 
			
		||||
                results.entry(key).or_default().push(name.clone());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        results
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn bind_key(&mut self, vb: VBKey, key: Key) {
 | 
			
		||||
        if let Some(old) = self.vb_bindings.insert(vb, key.clone()) {
 | 
			
		||||
            self.key_bindings.remove(&old);
 | 
			
		||||
        }
 | 
			
		||||
        self.key_bindings.insert(key, vb);
 | 
			
		||||
    fn clear_mappings(&mut self, key: VBKey) {
 | 
			
		||||
        self.keys.retain(|_, keys| {
 | 
			
		||||
            *keys = keys.difference(key);
 | 
			
		||||
            !keys.is_empty()
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn clear_binding(&mut self, vb: VBKey) {
 | 
			
		||||
        if let Some(old) = self.vb_bindings.remove(&vb) {
 | 
			
		||||
            self.key_bindings.remove(&old);
 | 
			
		||||
        }
 | 
			
		||||
    fn clear_all_mappings(&mut self) {
 | 
			
		||||
        self.keys.clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn key_event(&self, event: &KeyEvent) -> Option<VBKey> {
 | 
			
		||||
        self.key_bindings
 | 
			
		||||
            .get(&event.key_without_modifiers())
 | 
			
		||||
            .cloned()
 | 
			
		||||
    fn use_default_mappings(&mut self) {
 | 
			
		||||
        self.keys.clear();
 | 
			
		||||
        let mut default_key = |code, key| {
 | 
			
		||||
            self.keys.insert(PhysicalKey::Code(code), key);
 | 
			
		||||
        };
 | 
			
		||||
        default_key(KeyCode::KeyA, VBKey::SEL);
 | 
			
		||||
        default_key(KeyCode::KeyS, VBKey::STA);
 | 
			
		||||
        default_key(KeyCode::KeyD, VBKey::B);
 | 
			
		||||
        default_key(KeyCode::KeyF, VBKey::A);
 | 
			
		||||
        default_key(KeyCode::KeyE, VBKey::LT);
 | 
			
		||||
        default_key(KeyCode::KeyR, VBKey::RT);
 | 
			
		||||
        default_key(KeyCode::KeyI, VBKey::RU);
 | 
			
		||||
        default_key(KeyCode::KeyJ, VBKey::RL);
 | 
			
		||||
        default_key(KeyCode::KeyK, VBKey::RD);
 | 
			
		||||
        default_key(KeyCode::KeyL, VBKey::RR);
 | 
			
		||||
        default_key(KeyCode::ArrowUp, VBKey::LU);
 | 
			
		||||
        default_key(KeyCode::ArrowLeft, VBKey::LL);
 | 
			
		||||
        default_key(KeyCode::ArrowDown, VBKey::LD);
 | 
			
		||||
        default_key(KeyCode::ArrowRight, VBKey::LR);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct MappingProvider {
 | 
			
		||||
    device_mappings: Arc<RwLock<HashMap<DeviceId, Arc<RwLock<GamepadMapping>>>>>,
 | 
			
		||||
    sim_mappings: HashMap<SimId, Arc<RwLock<InputMapping>>>,
 | 
			
		||||
    gamepad_info: Arc<RwLock<HashMap<GamepadId, GamepadInfo>>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl MappingProvider {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        let mut mappings = HashMap::new();
 | 
			
		||||
 | 
			
		||||
        let mut p1_mappings = InputMapping::default();
 | 
			
		||||
        p1_mappings.use_default_mappings();
 | 
			
		||||
        let p2_mappings = InputMapping::default();
 | 
			
		||||
 | 
			
		||||
        mappings.insert(SimId::Player1, Arc::new(RwLock::new(p1_mappings)));
 | 
			
		||||
        mappings.insert(SimId::Player2, Arc::new(RwLock::new(p2_mappings)));
 | 
			
		||||
        Self {
 | 
			
		||||
            device_mappings: Arc::new(RwLock::new(HashMap::new())),
 | 
			
		||||
            gamepad_info: Arc::new(RwLock::new(HashMap::new())),
 | 
			
		||||
            sim_mappings: mappings,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn for_sim(&self, sim_id: SimId) -> &Arc<RwLock<InputMapping>> {
 | 
			
		||||
        self.sim_mappings.get(&sim_id).unwrap()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn for_gamepad(&self, gamepad_id: GamepadId) -> Option<Arc<RwLock<GamepadMapping>>> {
 | 
			
		||||
        let lock = self.gamepad_info.read().unwrap();
 | 
			
		||||
        let device_id = lock.get(&gamepad_id)?.device_id;
 | 
			
		||||
        drop(lock);
 | 
			
		||||
        let lock = self.device_mappings.read().unwrap();
 | 
			
		||||
        lock.get(&device_id).cloned()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn handle_gamepad_connect(&self, gamepad: &Gamepad) {
 | 
			
		||||
        let device_id = DeviceId(
 | 
			
		||||
            gamepad.vendor_id().unwrap_or_default(),
 | 
			
		||||
            gamepad.product_id().unwrap_or_default(),
 | 
			
		||||
        );
 | 
			
		||||
        let mut lock = self.device_mappings.write().unwrap();
 | 
			
		||||
        let mappings = match lock.entry(device_id) {
 | 
			
		||||
            Entry::Vacant(entry) => {
 | 
			
		||||
                let mappings = GamepadMapping::for_gamepad(gamepad);
 | 
			
		||||
                entry.insert(Arc::new(RwLock::new(mappings)))
 | 
			
		||||
            }
 | 
			
		||||
            Entry::Occupied(entry) => entry.into_mut(),
 | 
			
		||||
        }
 | 
			
		||||
        .clone();
 | 
			
		||||
        drop(lock);
 | 
			
		||||
        let mut lock = self.gamepad_info.write().unwrap();
 | 
			
		||||
        let bound_to = SimId::values()
 | 
			
		||||
            .into_iter()
 | 
			
		||||
            .find(|sim_id| lock.values().all(|info| info.bound_to != Some(*sim_id)));
 | 
			
		||||
        if let Entry::Vacant(entry) = lock.entry(gamepad.id()) {
 | 
			
		||||
            let info = GamepadInfo {
 | 
			
		||||
                id: *entry.key(),
 | 
			
		||||
                name: gamepad.name().to_string(),
 | 
			
		||||
                device_id,
 | 
			
		||||
                bound_to,
 | 
			
		||||
            };
 | 
			
		||||
            entry.insert(info);
 | 
			
		||||
        }
 | 
			
		||||
        drop(lock);
 | 
			
		||||
        if let Some(sim_id) = bound_to {
 | 
			
		||||
            self.for_sim(sim_id)
 | 
			
		||||
                .write()
 | 
			
		||||
                .unwrap()
 | 
			
		||||
                .gamepads
 | 
			
		||||
                .insert(gamepad.id(), mappings);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn handle_gamepad_disconnect(&self, gamepad_id: GamepadId) {
 | 
			
		||||
        let mut lock = self.gamepad_info.write().unwrap();
 | 
			
		||||
        let Some(info) = lock.remove(&gamepad_id) else {
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        if let Some(sim_id) = info.bound_to {
 | 
			
		||||
            self.for_sim(sim_id)
 | 
			
		||||
                .write()
 | 
			
		||||
                .unwrap()
 | 
			
		||||
                .gamepads
 | 
			
		||||
                .remove(&gamepad_id);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn assign_gamepad(&self, gamepad_id: GamepadId, sim_id: SimId) {
 | 
			
		||||
        self.unassign_gamepad(gamepad_id);
 | 
			
		||||
        let mut lock = self.gamepad_info.write().unwrap();
 | 
			
		||||
        let Some(info) = lock.get_mut(&gamepad_id) else {
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        info.bound_to = Some(sim_id);
 | 
			
		||||
        let device_id = info.device_id;
 | 
			
		||||
        drop(lock);
 | 
			
		||||
        let Some(device_mappings) = self
 | 
			
		||||
            .device_mappings
 | 
			
		||||
            .write()
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .get(&device_id)
 | 
			
		||||
            .cloned()
 | 
			
		||||
        else {
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        self.for_sim(sim_id)
 | 
			
		||||
            .write()
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .gamepads
 | 
			
		||||
            .insert(gamepad_id, device_mappings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn unassign_gamepad(&self, gamepad_id: GamepadId) {
 | 
			
		||||
        let mut lock = self.gamepad_info.write().unwrap();
 | 
			
		||||
        let Some(info) = lock.get_mut(&gamepad_id) else {
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        if let Some(sim_id) = info.bound_to {
 | 
			
		||||
            let mut sim_mapping = self.for_sim(sim_id).write().unwrap();
 | 
			
		||||
            sim_mapping.gamepads.remove(&gamepad_id);
 | 
			
		||||
        }
 | 
			
		||||
        info.bound_to = None;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn gamepad_info(&self) -> Vec<GamepadInfo> {
 | 
			
		||||
        self.gamepad_info
 | 
			
		||||
            .read()
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .values()
 | 
			
		||||
            .cloned()
 | 
			
		||||
            .collect()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										20
									
								
								src/main.rs
								
								
								
								
							
							
						
						
									
										20
									
								
								src/main.rs
								
								
								
								
							| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
use std::{path::PathBuf, process};
 | 
			
		||||
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use app::App;
 | 
			
		||||
use app::Application;
 | 
			
		||||
use clap::Parser;
 | 
			
		||||
use emulator::EmulatorBuilder;
 | 
			
		||||
use thread_priority::{ThreadBuilder, ThreadPriority};
 | 
			
		||||
| 
						 | 
				
			
			@ -11,16 +11,28 @@ mod app;
 | 
			
		|||
mod audio;
 | 
			
		||||
mod controller;
 | 
			
		||||
mod emulator;
 | 
			
		||||
mod graphics;
 | 
			
		||||
mod input;
 | 
			
		||||
mod renderer;
 | 
			
		||||
mod shrooms_vb_core;
 | 
			
		||||
mod window;
 | 
			
		||||
 | 
			
		||||
#[derive(Parser)]
 | 
			
		||||
struct Args {
 | 
			
		||||
    rom: Option<PathBuf>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(windows)]
 | 
			
		||||
fn set_process_priority_to_high() -> Result<()> {
 | 
			
		||||
    use windows::Win32::{Foundation, System::Threading};
 | 
			
		||||
    let process = unsafe { Threading::GetCurrentProcess() };
 | 
			
		||||
    unsafe { Threading::SetPriorityClass(process, Threading::HIGH_PRIORITY_CLASS)? };
 | 
			
		||||
    unsafe { Foundation::CloseHandle(process)? };
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn main() -> Result<()> {
 | 
			
		||||
    #[cfg(windows)]
 | 
			
		||||
    set_process_priority_to_high()?;
 | 
			
		||||
 | 
			
		||||
    let args = Args::parse();
 | 
			
		||||
 | 
			
		||||
    let (mut builder, client) = EmulatorBuilder::new();
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +57,6 @@ fn main() -> Result<()> {
 | 
			
		|||
    let event_loop = EventLoop::with_user_event().build().unwrap();
 | 
			
		||||
    event_loop.set_control_flow(ControlFlow::Poll);
 | 
			
		||||
    let proxy = event_loop.create_proxy();
 | 
			
		||||
    event_loop.run_app(&mut App::new(client, proxy))?;
 | 
			
		||||
    event_loop.run_app(&mut Application::new(client, proxy))?;
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,53 +0,0 @@
 | 
			
		|||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
use wgpu::{
 | 
			
		||||
    Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, Queue, Texture, TextureDescriptor,
 | 
			
		||||
    TextureFormat, TextureUsages,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct GameRenderer {
 | 
			
		||||
    pub queue: Arc<Queue>,
 | 
			
		||||
    pub eyes: Arc<Texture>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl GameRenderer {
 | 
			
		||||
    pub fn render(&self, buffer: &[u8]) {
 | 
			
		||||
        let texture = ImageCopyTexture {
 | 
			
		||||
            texture: &self.eyes,
 | 
			
		||||
            mip_level: 0,
 | 
			
		||||
            origin: Origin3d::ZERO,
 | 
			
		||||
            aspect: wgpu::TextureAspect::All,
 | 
			
		||||
        };
 | 
			
		||||
        let size = Extent3d {
 | 
			
		||||
            width: 384,
 | 
			
		||||
            height: 224,
 | 
			
		||||
            depth_or_array_layers: 1,
 | 
			
		||||
        };
 | 
			
		||||
        let data_layout = ImageDataLayout {
 | 
			
		||||
            offset: 0,
 | 
			
		||||
            bytes_per_row: Some(384 * 2),
 | 
			
		||||
            rows_per_image: Some(224),
 | 
			
		||||
        };
 | 
			
		||||
        self.queue.write_texture(texture, buffer, data_layout, size);
 | 
			
		||||
    }
 | 
			
		||||
    pub fn create_texture(device: &wgpu::Device, name: &str) -> Texture {
 | 
			
		||||
        let desc = TextureDescriptor {
 | 
			
		||||
            label: Some(name),
 | 
			
		||||
            size: Extent3d {
 | 
			
		||||
                width: 384,
 | 
			
		||||
                height: 224,
 | 
			
		||||
                depth_or_array_layers: 1,
 | 
			
		||||
            },
 | 
			
		||||
            mip_level_count: 1,
 | 
			
		||||
            sample_count: 1,
 | 
			
		||||
            dimension: wgpu::TextureDimension::D2,
 | 
			
		||||
            format: TextureFormat::Rg8Unorm,
 | 
			
		||||
            usage: TextureUsages::COPY_SRC
 | 
			
		||||
                | TextureUsages::COPY_DST
 | 
			
		||||
                | TextureUsages::TEXTURE_BINDING,
 | 
			
		||||
            view_formats: &[TextureFormat::Rg8Unorm],
 | 
			
		||||
        };
 | 
			
		||||
        device.create_texture(&desc)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
use egui::{Context, ViewportBuilder, ViewportId};
 | 
			
		||||
pub use game::GameWindow;
 | 
			
		||||
pub use input::InputWindow;
 | 
			
		||||
use winit::event::KeyEvent;
 | 
			
		||||
 | 
			
		||||
mod game;
 | 
			
		||||
mod game_screen;
 | 
			
		||||
mod input;
 | 
			
		||||
 | 
			
		||||
pub trait AppWindow {
 | 
			
		||||
    fn viewport_id(&self) -> ViewportId;
 | 
			
		||||
    fn initial_viewport(&self) -> ViewportBuilder;
 | 
			
		||||
    fn show(&mut self, ctx: &Context);
 | 
			
		||||
    fn on_init(&mut self, render_state: &egui_wgpu::RenderState) {
 | 
			
		||||
        let _ = render_state;
 | 
			
		||||
    }
 | 
			
		||||
    fn on_destroy(&mut self) {}
 | 
			
		||||
    fn handle_key_event(&mut self, event: &KeyEvent) {
 | 
			
		||||
        let _ = event;
 | 
			
		||||
    }
 | 
			
		||||
    fn handle_gamepad_event(&mut self, event: &gilrs::Event) {
 | 
			
		||||
        let _ = event;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,181 @@
 | 
			
		|||
use crate::{
 | 
			
		||||
    app::UserEvent,
 | 
			
		||||
    emulator::{EmulatorClient, EmulatorCommand, SimId},
 | 
			
		||||
};
 | 
			
		||||
use egui::{
 | 
			
		||||
    menu, Button, CentralPanel, Color32, Context, Frame, Response, TopBottomPanel, Ui,
 | 
			
		||||
    ViewportBuilder, ViewportCommand, ViewportId, WidgetText,
 | 
			
		||||
};
 | 
			
		||||
use winit::event_loop::EventLoopProxy;
 | 
			
		||||
 | 
			
		||||
use super::{game_screen::GameScreen, AppWindow};
 | 
			
		||||
 | 
			
		||||
pub struct GameWindow {
 | 
			
		||||
    client: EmulatorClient,
 | 
			
		||||
    proxy: EventLoopProxy<UserEvent>,
 | 
			
		||||
    sim_id: SimId,
 | 
			
		||||
    screen: Option<GameScreen>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl GameWindow {
 | 
			
		||||
    pub fn new(client: EmulatorClient, proxy: EventLoopProxy<UserEvent>, sim_id: SimId) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            client,
 | 
			
		||||
            proxy,
 | 
			
		||||
            sim_id,
 | 
			
		||||
            screen: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn show_menu(&mut self, ctx: &Context, ui: &mut Ui) {
 | 
			
		||||
        ui.menu_button("ROM", |ui| {
 | 
			
		||||
            if ui.button("Open ROM").clicked() {
 | 
			
		||||
                let rom = rfd::FileDialog::new()
 | 
			
		||||
                    .add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
 | 
			
		||||
                    .pick_file();
 | 
			
		||||
                if let Some(path) = rom {
 | 
			
		||||
                    self.client
 | 
			
		||||
                        .send_command(EmulatorCommand::LoadGame(SimId::Player1, path));
 | 
			
		||||
                }
 | 
			
		||||
                ui.close_menu();
 | 
			
		||||
            }
 | 
			
		||||
            if ui.button("Quit").clicked() {
 | 
			
		||||
                ctx.send_viewport_cmd(ViewportCommand::Close);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        ui.menu_button("Emulation", |ui| {
 | 
			
		||||
            let has_game = self.client.has_game(self.sim_id);
 | 
			
		||||
            if self.client.is_running(self.sim_id) {
 | 
			
		||||
                if ui.add_enabled(has_game, Button::new("Pause")).clicked() {
 | 
			
		||||
                    self.client.send_command(EmulatorCommand::Pause);
 | 
			
		||||
                    ui.close_menu();
 | 
			
		||||
                }
 | 
			
		||||
            } else if ui.add_enabled(has_game, Button::new("Resume")).clicked() {
 | 
			
		||||
                self.client.send_command(EmulatorCommand::Resume);
 | 
			
		||||
                ui.close_menu();
 | 
			
		||||
            }
 | 
			
		||||
            if ui.add_enabled(has_game, Button::new("Reset")).clicked() {
 | 
			
		||||
                self.client
 | 
			
		||||
                    .send_command(EmulatorCommand::Reset(self.sim_id));
 | 
			
		||||
                ui.close_menu();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        ui.menu_button("Video", |ui| {
 | 
			
		||||
            let current_dims = ctx.input(|i| i.viewport().inner_rect.unwrap());
 | 
			
		||||
            let current_dims = current_dims.max - current_dims.min;
 | 
			
		||||
 | 
			
		||||
            for scale in 1..=4 {
 | 
			
		||||
                let label = format!("x{scale}");
 | 
			
		||||
                let scale = scale as f32;
 | 
			
		||||
                let dims = (384.0 * scale, 224.0 * scale + 22.0).into();
 | 
			
		||||
                if ui
 | 
			
		||||
                    .selectable_button((current_dims - dims).length() < 1.0, label)
 | 
			
		||||
                    .clicked()
 | 
			
		||||
                {
 | 
			
		||||
                    ctx.send_viewport_cmd(ViewportCommand::InnerSize(dims));
 | 
			
		||||
                    ui.close_menu();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        ui.menu_button("Audio", |ui| {
 | 
			
		||||
            let p1_enabled = self.client.is_audio_enabled(SimId::Player1);
 | 
			
		||||
            let p2_enabled = self.client.is_audio_enabled(SimId::Player2);
 | 
			
		||||
            if ui.selectable_button(p1_enabled, "Player 1").clicked() {
 | 
			
		||||
                self.client
 | 
			
		||||
                    .send_command(EmulatorCommand::SetAudioEnabled(!p1_enabled, p2_enabled));
 | 
			
		||||
                ui.close_menu();
 | 
			
		||||
            }
 | 
			
		||||
            if ui.selectable_button(p2_enabled, "Player 2").clicked() {
 | 
			
		||||
                self.client
 | 
			
		||||
                    .send_command(EmulatorCommand::SetAudioEnabled(p1_enabled, !p2_enabled));
 | 
			
		||||
                ui.close_menu();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        ui.menu_button("Input", |ui| {
 | 
			
		||||
            if ui.button("Bind Inputs").clicked() {
 | 
			
		||||
                self.proxy.send_event(UserEvent::OpenInput).unwrap();
 | 
			
		||||
                ui.close_menu();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        ui.menu_button("Multiplayer", |ui| {
 | 
			
		||||
            if self.sim_id == SimId::Player1
 | 
			
		||||
                && !self.client.has_player_2()
 | 
			
		||||
                && ui.button("Open Player 2").clicked()
 | 
			
		||||
            {
 | 
			
		||||
                self.client
 | 
			
		||||
                    .send_command(EmulatorCommand::StartSecondSim(None));
 | 
			
		||||
                self.proxy.send_event(UserEvent::OpenPlayer2).unwrap();
 | 
			
		||||
                ui.close_menu();
 | 
			
		||||
            }
 | 
			
		||||
            if self.client.has_player_2() {
 | 
			
		||||
                let linked = self.client.are_sims_linked();
 | 
			
		||||
                if linked && ui.button("Unlink").clicked() {
 | 
			
		||||
                    self.client.send_command(EmulatorCommand::Unlink);
 | 
			
		||||
                    ui.close_menu();
 | 
			
		||||
                }
 | 
			
		||||
                if !linked && ui.button("Link").clicked() {
 | 
			
		||||
                    self.client.send_command(EmulatorCommand::Link);
 | 
			
		||||
                    ui.close_menu();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 initial_viewport(&self) -> ViewportBuilder {
 | 
			
		||||
        ViewportBuilder::default()
 | 
			
		||||
            .with_title("Shrooms VB")
 | 
			
		||||
            .with_inner_size((384.0, 246.0))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn show(&mut self, ctx: &Context) {
 | 
			
		||||
        TopBottomPanel::top("menubar")
 | 
			
		||||
            .exact_height(22.0)
 | 
			
		||||
            .show(ctx, |ui| {
 | 
			
		||||
                menu::bar(ui, |ui| {
 | 
			
		||||
                    self.show_menu(ctx, ui);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        let frame = Frame::central_panel(&ctx.style()).fill(Color32::BLACK);
 | 
			
		||||
        CentralPanel::default().frame(frame).show(ctx, |ui| {
 | 
			
		||||
            if let Some(screen) = self.screen.as_ref() {
 | 
			
		||||
                ui.add(screen);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn on_init(&mut self, render_state: &egui_wgpu::RenderState) {
 | 
			
		||||
        let (screen, sink) = GameScreen::init(render_state);
 | 
			
		||||
        self.client
 | 
			
		||||
            .send_command(EmulatorCommand::SetRenderer(self.sim_id, sink));
 | 
			
		||||
        self.screen = Some(screen)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn on_destroy(&mut self) {
 | 
			
		||||
        if self.sim_id == SimId::Player2 {
 | 
			
		||||
            self.client.send_command(EmulatorCommand::StopSecondSim);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
trait UiExt {
 | 
			
		||||
    fn selectable_button(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl UiExt for Ui {
 | 
			
		||||
    fn selectable_button(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response {
 | 
			
		||||
        self.style_mut().visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
 | 
			
		||||
        self.style_mut().visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT;
 | 
			
		||||
        self.style_mut().visuals.widgets.active.bg_fill = Color32::TRANSPARENT;
 | 
			
		||||
        let mut selected = selected;
 | 
			
		||||
        self.checkbox(&mut selected, text)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,206 @@
 | 
			
		|||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
use egui::Widget;
 | 
			
		||||
use wgpu::{util::DeviceExt as _, BindGroup, BindGroupLayout, RenderPipeline};
 | 
			
		||||
 | 
			
		||||
use crate::graphics::TextureSink;
 | 
			
		||||
 | 
			
		||||
pub struct GameScreen {
 | 
			
		||||
    bind_group: Arc<BindGroup>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl GameScreen {
 | 
			
		||||
    fn init_pipeline(render_state: &egui_wgpu::RenderState) {
 | 
			
		||||
        let device = &render_state.device;
 | 
			
		||||
 | 
			
		||||
        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
 | 
			
		||||
            label: Some("texture bind group layout"),
 | 
			
		||||
            entries: &[
 | 
			
		||||
                wgpu::BindGroupLayoutEntry {
 | 
			
		||||
                    binding: 0,
 | 
			
		||||
                    visibility: wgpu::ShaderStages::FRAGMENT,
 | 
			
		||||
                    ty: wgpu::BindingType::Texture {
 | 
			
		||||
                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
 | 
			
		||||
                        view_dimension: wgpu::TextureViewDimension::D2,
 | 
			
		||||
                        multisampled: false,
 | 
			
		||||
                    },
 | 
			
		||||
                    count: None,
 | 
			
		||||
                },
 | 
			
		||||
                wgpu::BindGroupLayoutEntry {
 | 
			
		||||
                    binding: 1,
 | 
			
		||||
                    visibility: wgpu::ShaderStages::FRAGMENT,
 | 
			
		||||
                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
 | 
			
		||||
                    count: None,
 | 
			
		||||
                },
 | 
			
		||||
                wgpu::BindGroupLayoutEntry {
 | 
			
		||||
                    binding: 2,
 | 
			
		||||
                    visibility: wgpu::ShaderStages::FRAGMENT,
 | 
			
		||||
                    ty: wgpu::BindingType::Buffer {
 | 
			
		||||
                        ty: wgpu::BufferBindingType::Uniform,
 | 
			
		||||
                        has_dynamic_offset: false,
 | 
			
		||||
                        min_binding_size: None,
 | 
			
		||||
                    },
 | 
			
		||||
                    count: None,
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let shader = device.create_shader_module(wgpu::include_wgsl!("../anaglyph.wgsl"));
 | 
			
		||||
        let render_pipeline_layout =
 | 
			
		||||
            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
 | 
			
		||||
                label: Some("render pipeline layout"),
 | 
			
		||||
                bind_group_layouts: &[&bind_group_layout],
 | 
			
		||||
                push_constant_ranges: &[],
 | 
			
		||||
            });
 | 
			
		||||
        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
 | 
			
		||||
            label: Some("render pipeline"),
 | 
			
		||||
            layout: Some(&render_pipeline_layout),
 | 
			
		||||
            vertex: wgpu::VertexState {
 | 
			
		||||
                module: &shader,
 | 
			
		||||
                entry_point: "vs_main",
 | 
			
		||||
                buffers: &[],
 | 
			
		||||
                compilation_options: wgpu::PipelineCompilationOptions::default(),
 | 
			
		||||
            },
 | 
			
		||||
            fragment: Some(wgpu::FragmentState {
 | 
			
		||||
                module: &shader,
 | 
			
		||||
                entry_point: "fs_main",
 | 
			
		||||
                targets: &[Some(wgpu::ColorTargetState {
 | 
			
		||||
                    format: wgpu::TextureFormat::Bgra8Unorm,
 | 
			
		||||
                    blend: Some(wgpu::BlendState::REPLACE),
 | 
			
		||||
                    write_mask: wgpu::ColorWrites::ALL,
 | 
			
		||||
                })],
 | 
			
		||||
                compilation_options: wgpu::PipelineCompilationOptions::default(),
 | 
			
		||||
            }),
 | 
			
		||||
            primitive: wgpu::PrimitiveState {
 | 
			
		||||
                topology: wgpu::PrimitiveTopology::TriangleList,
 | 
			
		||||
                strip_index_format: None,
 | 
			
		||||
                front_face: wgpu::FrontFace::Ccw,
 | 
			
		||||
                cull_mode: Some(wgpu::Face::Back),
 | 
			
		||||
                polygon_mode: wgpu::PolygonMode::Fill,
 | 
			
		||||
                unclipped_depth: false,
 | 
			
		||||
                conservative: false,
 | 
			
		||||
            },
 | 
			
		||||
            depth_stencil: None,
 | 
			
		||||
            multisample: wgpu::MultisampleState {
 | 
			
		||||
                count: 1,
 | 
			
		||||
                mask: !0,
 | 
			
		||||
                alpha_to_coverage_enabled: false,
 | 
			
		||||
            },
 | 
			
		||||
            multiview: None,
 | 
			
		||||
            cache: None,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        render_state
 | 
			
		||||
            .renderer
 | 
			
		||||
            .write()
 | 
			
		||||
            .callback_resources
 | 
			
		||||
            .insert(SharedGameScreenResources {
 | 
			
		||||
                pipeline: render_pipeline,
 | 
			
		||||
                bind_group_layout,
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn init(render_state: &egui_wgpu::RenderState) -> (Self, TextureSink) {
 | 
			
		||||
        Self::init_pipeline(render_state);
 | 
			
		||||
 | 
			
		||||
        let device = &render_state.device;
 | 
			
		||||
        let queue = &render_state.queue;
 | 
			
		||||
 | 
			
		||||
        let (sink, texture_view) = TextureSink::new(device, queue.clone());
 | 
			
		||||
        let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default());
 | 
			
		||||
        let colors = Colors {
 | 
			
		||||
            left: [1.0, 0.0, 0.0, 1.0],
 | 
			
		||||
            right: [0.0, 0.7734375, 0.9375, 1.0],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let color_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
 | 
			
		||||
            label: Some("colors"),
 | 
			
		||||
            contents: bytemuck::bytes_of(&colors),
 | 
			
		||||
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let renderer = render_state.renderer.read();
 | 
			
		||||
        let resources: &SharedGameScreenResources = renderer.callback_resources.get().unwrap();
 | 
			
		||||
 | 
			
		||||
        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
 | 
			
		||||
            label: Some("bind group"),
 | 
			
		||||
            layout: &resources.bind_group_layout,
 | 
			
		||||
            entries: &[
 | 
			
		||||
                wgpu::BindGroupEntry {
 | 
			
		||||
                    binding: 0,
 | 
			
		||||
                    resource: wgpu::BindingResource::TextureView(&texture_view),
 | 
			
		||||
                },
 | 
			
		||||
                wgpu::BindGroupEntry {
 | 
			
		||||
                    binding: 1,
 | 
			
		||||
                    resource: wgpu::BindingResource::Sampler(&sampler),
 | 
			
		||||
                },
 | 
			
		||||
                wgpu::BindGroupEntry {
 | 
			
		||||
                    binding: 2,
 | 
			
		||||
                    resource: color_buf.as_entire_binding(),
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        (
 | 
			
		||||
            Self {
 | 
			
		||||
                bind_group: Arc::new(bind_group),
 | 
			
		||||
            },
 | 
			
		||||
            sink,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Widget for &GameScreen {
 | 
			
		||||
    fn ui(self, ui: &mut egui::Ui) -> egui::Response {
 | 
			
		||||
        let response = ui.allocate_rect(ui.clip_rect(), egui::Sense::hover());
 | 
			
		||||
        let callback = egui_wgpu::Callback::new_paint_callback(
 | 
			
		||||
            response.rect,
 | 
			
		||||
            GameScreenCallback {
 | 
			
		||||
                bind_group: self.bind_group.clone(),
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
        ui.painter().add(callback);
 | 
			
		||||
        response
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct GameScreenCallback {
 | 
			
		||||
    bind_group: Arc<BindGroup>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl egui_wgpu::CallbackTrait for GameScreenCallback {
 | 
			
		||||
    fn paint(
 | 
			
		||||
        &self,
 | 
			
		||||
        info: egui::PaintCallbackInfo,
 | 
			
		||||
        render_pass: &mut wgpu::RenderPass<'static>,
 | 
			
		||||
        callback_resources: &egui_wgpu::CallbackResources,
 | 
			
		||||
    ) {
 | 
			
		||||
        let resources: &SharedGameScreenResources = callback_resources.get().unwrap();
 | 
			
		||||
        let viewport = info.viewport_in_pixels();
 | 
			
		||||
        let left = viewport.left_px as f32;
 | 
			
		||||
        let top = viewport.top_px as f32;
 | 
			
		||||
        let width = viewport.width_px as f32;
 | 
			
		||||
        let height = viewport.height_px as f32;
 | 
			
		||||
        let aspect_ratio = 384.0 / 224.0;
 | 
			
		||||
        let w = width.min(height * aspect_ratio);
 | 
			
		||||
        let h = height.min(width / aspect_ratio);
 | 
			
		||||
        let x = left + (width - w) / 2.0;
 | 
			
		||||
        let y = top + (height - h) / 2.0;
 | 
			
		||||
        render_pass.set_pipeline(&resources.pipeline);
 | 
			
		||||
        render_pass.set_bind_group(0, &self.bind_group, &[]);
 | 
			
		||||
        render_pass.set_viewport(x, y, w, h, 0.0, 1.0);
 | 
			
		||||
        render_pass.draw(0..6, 0..1);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct SharedGameScreenResources {
 | 
			
		||||
    pipeline: RenderPipeline,
 | 
			
		||||
    bind_group_layout: BindGroupLayout,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
 | 
			
		||||
#[repr(C)]
 | 
			
		||||
struct Colors {
 | 
			
		||||
    left: [f32; 4],
 | 
			
		||||
    right: [f32; 4],
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,257 @@
 | 
			
		|||
use egui::{
 | 
			
		||||
    Button, CentralPanel, Context, Label, Layout, TopBottomPanel, Ui, ViewportBuilder, ViewportId,
 | 
			
		||||
};
 | 
			
		||||
use egui_extras::{Column, TableBuilder};
 | 
			
		||||
use gilrs::{EventType, GamepadId};
 | 
			
		||||
use std::sync::RwLock;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    emulator::{SimId, VBKey},
 | 
			
		||||
    input::{MappingProvider, Mappings},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use super::AppWindow;
 | 
			
		||||
 | 
			
		||||
pub struct InputWindow {
 | 
			
		||||
    mappings: MappingProvider,
 | 
			
		||||
    now_binding: Option<VBKey>,
 | 
			
		||||
    active_tab: InputTab,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const KEY_NAMES: [(VBKey, &str); 14] = [
 | 
			
		||||
    (VBKey::LU, "Up"),
 | 
			
		||||
    (VBKey::LD, "Down"),
 | 
			
		||||
    (VBKey::LL, "Left"),
 | 
			
		||||
    (VBKey::LR, "Right"),
 | 
			
		||||
    (VBKey::SEL, "Select"),
 | 
			
		||||
    (VBKey::STA, "Start"),
 | 
			
		||||
    (VBKey::B, "B"),
 | 
			
		||||
    (VBKey::A, "A"),
 | 
			
		||||
    (VBKey::LT, "L-Trigger"),
 | 
			
		||||
    (VBKey::RT, "R-Trigger"),
 | 
			
		||||
    (VBKey::RU, "R-Up"),
 | 
			
		||||
    (VBKey::RD, "R-Down"),
 | 
			
		||||
    (VBKey::RL, "R-Left"),
 | 
			
		||||
    (VBKey::RR, "R-Right"),
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
impl InputWindow {
 | 
			
		||||
    pub fn new(mappings: MappingProvider) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            mappings,
 | 
			
		||||
            now_binding: None,
 | 
			
		||||
            active_tab: InputTab::Player1,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn show_bindings<T: Mappings>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ui: &mut Ui,
 | 
			
		||||
        mappings: &RwLock<T>,
 | 
			
		||||
        bind_message: &str,
 | 
			
		||||
    ) {
 | 
			
		||||
        ui.horizontal(|ui| {
 | 
			
		||||
            if ui.button("Use defaults").clicked() {
 | 
			
		||||
                mappings.write().unwrap().use_default_mappings();
 | 
			
		||||
                self.now_binding = None;
 | 
			
		||||
            }
 | 
			
		||||
            if ui.button("Clear all").clicked() {
 | 
			
		||||
                mappings.write().unwrap().clear_all_mappings();
 | 
			
		||||
                self.now_binding = None;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        ui.separator();
 | 
			
		||||
        let mut names = {
 | 
			
		||||
            let mapping = mappings.read().unwrap();
 | 
			
		||||
            mapping.mapping_names()
 | 
			
		||||
        };
 | 
			
		||||
        TableBuilder::new(ui)
 | 
			
		||||
            .column(Column::remainder())
 | 
			
		||||
            .column(Column::remainder())
 | 
			
		||||
            .cell_layout(Layout::left_to_right(egui::Align::Center))
 | 
			
		||||
            .body(|mut body| {
 | 
			
		||||
                for keys in KEY_NAMES.chunks_exact(2) {
 | 
			
		||||
                    body.row(20.0, |mut row| {
 | 
			
		||||
                        for (key, name) in keys {
 | 
			
		||||
                            let binding = names.remove(key).map(|mut s| {
 | 
			
		||||
                                s.sort();
 | 
			
		||||
                                s.join(", ")
 | 
			
		||||
                            });
 | 
			
		||||
                            row.col(|ui| {
 | 
			
		||||
                                let size = ui.available_size_before_wrap();
 | 
			
		||||
                                let width = size.x;
 | 
			
		||||
                                let height = size.y;
 | 
			
		||||
                                ui.add_sized((width * 0.2, height), Label::new(*name));
 | 
			
		||||
                                let label_text = if self.now_binding == Some(*key) {
 | 
			
		||||
                                    bind_message
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    binding.as_deref().unwrap_or("")
 | 
			
		||||
                                };
 | 
			
		||||
                                if ui
 | 
			
		||||
                                    .add_sized((width * 0.6, height), Button::new(label_text))
 | 
			
		||||
                                    .clicked()
 | 
			
		||||
                                {
 | 
			
		||||
                                    self.now_binding = Some(*key);
 | 
			
		||||
                                }
 | 
			
		||||
                                if ui
 | 
			
		||||
                                    .add_sized(ui.available_size(), Button::new("Clear"))
 | 
			
		||||
                                    .clicked()
 | 
			
		||||
                                {
 | 
			
		||||
                                    let mut mapping = mappings.write().unwrap();
 | 
			
		||||
                                    mapping.clear_mappings(*key);
 | 
			
		||||
                                    self.now_binding = None;
 | 
			
		||||
                                }
 | 
			
		||||
                            });
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn show_key_bindings(&mut self, ui: &mut Ui, sim_id: SimId) {
 | 
			
		||||
        let mappings = self.mappings.for_sim(sim_id).clone();
 | 
			
		||||
        self.show_bindings(ui, &mappings, "Press any key");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn show_gamepads(&mut self, ui: &mut Ui) {
 | 
			
		||||
        let mut gamepads = self.mappings.gamepad_info();
 | 
			
		||||
        gamepads.sort_by_key(|g| usize::from(g.id));
 | 
			
		||||
 | 
			
		||||
        if gamepads.is_empty() {
 | 
			
		||||
            ui.label("No gamepads connected.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (index, gamepad) in gamepads.into_iter().enumerate() {
 | 
			
		||||
            ui.horizontal(|ui| {
 | 
			
		||||
                ui.label(format!("Gamepad {index}: {}", gamepad.name));
 | 
			
		||||
                let mut bound_to = gamepad.bound_to;
 | 
			
		||||
                let mut rebind = false;
 | 
			
		||||
                rebind |= ui
 | 
			
		||||
                    .selectable_value(&mut bound_to, Some(SimId::Player1), "Player 1")
 | 
			
		||||
                    .changed();
 | 
			
		||||
                rebind |= ui
 | 
			
		||||
                    .selectable_value(&mut bound_to, Some(SimId::Player2), "Player 2")
 | 
			
		||||
                    .changed();
 | 
			
		||||
                rebind |= ui.selectable_value(&mut bound_to, None, "Nobody").changed();
 | 
			
		||||
                if rebind {
 | 
			
		||||
                    match bound_to {
 | 
			
		||||
                        Some(sim_id) => self.mappings.assign_gamepad(gamepad.id, sim_id),
 | 
			
		||||
                        None => self.mappings.unassign_gamepad(gamepad.id),
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                ui.separator();
 | 
			
		||||
                if ui.button("Rebind").clicked() {
 | 
			
		||||
                    self.active_tab = InputTab::RebindGamepad(gamepad.id);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn show_gamepad_bindings(&mut self, ui: &mut Ui, gamepad_id: GamepadId) {
 | 
			
		||||
        let Some(mappings) = self.mappings.for_gamepad(gamepad_id) else {
 | 
			
		||||
            self.active_tab = InputTab::Gamepads;
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        self.show_bindings(ui, &mappings, "Press any input");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AppWindow for InputWindow {
 | 
			
		||||
    fn viewport_id(&self) -> ViewportId {
 | 
			
		||||
        ViewportId::from_hash_of("input")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn initial_viewport(&self) -> ViewportBuilder {
 | 
			
		||||
        ViewportBuilder::default()
 | 
			
		||||
            .with_title("Bind Inputs")
 | 
			
		||||
            .with_inner_size((600.0, 400.0))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn show(&mut self, ctx: &Context) {
 | 
			
		||||
        TopBottomPanel::top("options").show(ctx, |ui| {
 | 
			
		||||
            ui.horizontal(|ui| {
 | 
			
		||||
                let old_active_tab = self.active_tab;
 | 
			
		||||
                ui.selectable_value(&mut self.active_tab, InputTab::Player1, "Player 1");
 | 
			
		||||
                ui.selectable_value(&mut self.active_tab, InputTab::Player2, "Player 2");
 | 
			
		||||
                ui.selectable_value(&mut self.active_tab, InputTab::Gamepads, "Gamepads");
 | 
			
		||||
                if matches!(self.active_tab, InputTab::RebindGamepad(_)) {
 | 
			
		||||
                    let tab = self.active_tab;
 | 
			
		||||
                    ui.selectable_value(&mut self.active_tab, tab, "Rebind Gamepad");
 | 
			
		||||
                }
 | 
			
		||||
                if old_active_tab != self.active_tab {
 | 
			
		||||
                    self.now_binding = None;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        CentralPanel::default().show(ctx, |ui| {
 | 
			
		||||
            match self.active_tab {
 | 
			
		||||
                InputTab::Player1 => self.show_key_bindings(ui, SimId::Player1),
 | 
			
		||||
                InputTab::Player2 => self.show_key_bindings(ui, SimId::Player2),
 | 
			
		||||
                InputTab::Gamepads => self.show_gamepads(ui),
 | 
			
		||||
                InputTab::RebindGamepad(id) => self.show_gamepad_bindings(ui, id),
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn handle_key_event(&mut self, event: &winit::event::KeyEvent) {
 | 
			
		||||
        if !event.state.is_pressed() {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        let sim_id = match self.active_tab {
 | 
			
		||||
            InputTab::Player1 => SimId::Player1,
 | 
			
		||||
            InputTab::Player2 => SimId::Player2,
 | 
			
		||||
            _ => {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        let Some(vb) = self.now_binding.take() else {
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        let mut mappings = self.mappings.for_sim(sim_id).write().unwrap();
 | 
			
		||||
        mappings.add_keyboard_mapping(vb, event.physical_key);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn handle_gamepad_event(&mut self, event: &gilrs::Event) {
 | 
			
		||||
        let InputTab::RebindGamepad(gamepad_id) = self.active_tab else {
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        if gamepad_id != event.id {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        let Some(mappings) = self.mappings.for_gamepad(gamepad_id) else {
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        let Some(vb) = self.now_binding else {
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        match event.event {
 | 
			
		||||
            EventType::ButtonPressed(_, code) => {
 | 
			
		||||
                let mut mapping = mappings.write().unwrap();
 | 
			
		||||
                mapping.add_button_mapping(vb, code);
 | 
			
		||||
                self.now_binding.take();
 | 
			
		||||
            }
 | 
			
		||||
            EventType::AxisChanged(_, value, code) => {
 | 
			
		||||
                if value < -0.75 {
 | 
			
		||||
                    let mut mapping = mappings.write().unwrap();
 | 
			
		||||
                    mapping.add_axis_neg_mapping(vb, code);
 | 
			
		||||
                    self.now_binding.take();
 | 
			
		||||
                }
 | 
			
		||||
                if value > 0.75 {
 | 
			
		||||
                    let mut mapping = mappings.write().unwrap();
 | 
			
		||||
                    mapping.add_axis_pos_mapping(vb, code);
 | 
			
		||||
                    self.now_binding.take();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            _ => {}
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Copy, PartialEq, Eq)]
 | 
			
		||||
enum InputTab {
 | 
			
		||||
    Player1,
 | 
			
		||||
    Player2,
 | 
			
		||||
    Gamepads,
 | 
			
		||||
    RebindGamepad(GamepadId),
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue