diff --git a/Cargo.toml b/Cargo.toml index 855efdb..23abe96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ bitflags = "2" bytemuck = { version = "1", features = ["derive"] } clap = { version = "4", features = ["derive"] } cpal = "0.15" -imgui = "0.12" +imgui = { version = "0.12", features = ["tables-api"] } imgui-wgpu = { git = "https://github.com/Yatekii/imgui-wgpu-rs", rev = "2edd348" } imgui-winit-support = "0.13" itertools = "0.13" diff --git a/src/app.rs b/src/app.rs index 96d0a1d..421fbd8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,518 +1,117 @@ -use imgui::*; -use imgui_wgpu::{Renderer, RendererConfig}; -use imgui_winit_support::WinitPlatform; -use pollster::block_on; -use std::{sync::Arc, time::Instant}; -use wgpu::util::DeviceExt as _; -#[cfg(target_os = "windows")] -use winit::platform::windows::{CornerPreference, WindowAttributesExtWindows as _}; +use std::{collections::HashMap, fmt::Debug}; + +use game::GameWindow; use winit::{ application::ApplicationHandler, - dpi::{LogicalSize, PhysicalSize}, event::{Event, WindowEvent}, - event_loop::ActiveEventLoop, - window::Window, + event_loop::{ActiveEventLoop, EventLoopProxy}, + window::WindowId, }; -use crate::{ - controller::ControllerState, - emulator::{EmulatorClient, EmulatorCommand}, - renderer::GameRenderer, -}; +use crate::emulator::EmulatorClient; -struct ImguiState { - context: imgui::Context, - platform: WinitPlatform, - renderer: Renderer, - clear_color: wgpu::Color, - last_frame: Instant, - last_cursor: Option, -} - -struct AppWindow { - device: wgpu::Device, - queue: Arc, - window: Arc, - surface_desc: wgpu::SurfaceConfiguration, - surface: wgpu::Surface<'static>, - hidpi_factor: f64, - pipeline: wgpu::RenderPipeline, - bind_group: wgpu::BindGroup, - imgui: Option, -} - -impl AppWindow { - fn setup_gpu(event_loop: &ActiveEventLoop, client: &EmulatorClient) -> Self { - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::PRIMARY, - ..Default::default() - }); - - let window = { - let size = LogicalSize::new(384, 244); - - let attributes = Window::default_attributes() - .with_inner_size(size) - .with_title("Shrooms VB"); - #[cfg(target_os = "windows")] - let attributes = attributes.with_corner_preference(CornerPreference::DoNotRound); - 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); - let eyes = Arc::new(GameRenderer::create_texture(&device, "eye")); - client.send_command(EmulatorCommand::SetRenderer(GameRenderer { - queue: 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(), - }, - ], - }); - - // 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); - - 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, - }); - - let imgui = None; - Self { - device, - queue, - window, - surface_desc, - surface, - hidpi_factor, - pipeline: render_pipeline, - bind_group, - imgui, - } - } - - fn setup_imgui(&mut self) { - let mut context = imgui::Context::create(); - let mut platform = imgui_winit_support::WinitPlatform::new(&mut context); - platform.attach_window( - context.io_mut(), - &self.window, - imgui_winit_support::HiDpiMode::Default, - ); - context.set_ini_filename(None); - - let font_size = (16.0 * self.hidpi_factor) as f32; - context.io_mut().font_global_scale = (1.0 / self.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 clear_color = wgpu::Color::BLACK; - - let renderer_config = RendererConfig { - texture_format: self.surface_desc.format, - ..Default::default() - }; - - let renderer = Renderer::new(&mut context, &self.device, &self.queue, renderer_config); - - let last_frame = Instant::now(); - let last_cursor = None; - - self.imgui = Some(ImguiState { - context, - platform, - renderer, - clear_color, - last_frame, - last_cursor, - }) - } - - fn new(event_loop: &ActiveEventLoop, client: &EmulatorClient) -> Self { - let mut window = Self::setup_gpu(event_loop, client); - window.setup_imgui(); - window - } -} +mod common; +mod game; +mod input; pub struct App { - window: Option, + windows: HashMap>, + focused_window: Option, client: EmulatorClient, - controller: ControllerState, + proxy: EventLoopProxy, } impl App { - pub fn new(client: EmulatorClient) -> Self { - let controller = ControllerState::new(); - client.send_command(EmulatorCommand::SetKeys(controller.pressed())); + pub fn new(client: EmulatorClient, proxy: EventLoopProxy) -> Self { Self { - window: None, + windows: HashMap::new(), + focused_window: None, client, - controller, + proxy, } } + + fn active_window(&mut self) -> Option<&mut Box> { + let active_window = self.focused_window?; + self.windows.get_mut(&active_window) + } } -impl ApplicationHandler for App { +impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { - self.window = Some(AppWindow::new(event_loop, &self.client)); + let mut window = GameWindow::new(event_loop, self.client.clone(), self.proxy.clone()); + window.init(); + self.focused_window = Some(window.id()); + self.windows.insert(window.id(), Box::new(window)); } fn window_event( &mut self, event_loop: &ActiveEventLoop, - window_id: winit::window::WindowId, + window_id: WindowId, event: WindowEvent, ) { - let window = self.window.as_mut().unwrap(); - let imgui = window.imgui.as_mut().unwrap(); - - match &event { - WindowEvent::Resized(size) => { - window.surface_desc.width = size.width; - window.surface_desc.height = size.height; - window - .surface - .configure(&window.device, &window.surface_desc); + if let WindowEvent::Focused(focused) = event { + if focused { + self.focused_window = Some(window_id); + } else { + self.focused_window = None; } - WindowEvent::CloseRequested => event_loop.exit(), - WindowEvent::KeyboardInput { event, .. } => { - if self.controller.key_event(event) { - self.client - .send_command(EmulatorCommand::SetKeys(self.controller.pressed())); - } - } - WindowEvent::RedrawRequested => { - let now = Instant::now(); - imgui - .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) => { - eprintln!("dropped frame: {e:?}"); - return; - } - }; - imgui - .platform - .prepare_frame(imgui.context.io_mut(), &window.window) - .expect("Failed to prepare frame"); - let ui = imgui.context.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 = PhysicalSize::new( - window.surface_desc.width, - window.surface_desc.height, - ) - .to_logical(window.hidpi_factor); - 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.surface_desc.width = size.width; - window.surface_desc.height = size.height; - window - .surface - .configure(&window.device, &window.surface_desc); - } - } - } - }); - }); - - 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(&window.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, &window.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( - imgui.context.render(), - &window.queue, - &window.device, - &mut rpass, - ) - .expect("Rendering failed"); - - drop(rpass); - - window.queue.submit(Some(encoder.finish())); - - frame.present(); - } - _ => (), } - - imgui.platform.handle_event::<()>( - imgui.context.io_mut(), - &window.window, - &Event::WindowEvent { window_id, event }, - ); + let Some(window) = self.windows.get_mut(&window_id) else { + return; + }; + window.handle_event(event_loop, &Event::WindowEvent { window_id, event }); } - fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: ()) { - let window = self.window.as_mut().unwrap(); - let imgui = window.imgui.as_mut().unwrap(); - imgui.platform.handle_event::<()>( - imgui.context.io_mut(), - &window.window, - &Event::UserEvent(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); + } + UserEvent::CloseWindow(window_id) => { + self.windows.remove(&window_id); + } + } } fn device_event( &mut self, - _event_loop: &ActiveEventLoop, + event_loop: &ActiveEventLoop, device_id: winit::event::DeviceId, event: winit::event::DeviceEvent, ) { - let window = self.window.as_mut().unwrap(); - let imgui = window.imgui.as_mut().unwrap(); - imgui.platform.handle_event::<()>( - imgui.context.io_mut(), - &window.window, - &Event::DeviceEvent { device_id, event }, - ); + let Some(window) = self.active_window() else { + return; + }; + window.handle_event(event_loop, &Event::DeviceEvent { device_id, event }); } - fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { - let window = self.window.as_mut().unwrap(); - let imgui = window.imgui.as_mut().unwrap(); - window.window.request_redraw(); - imgui.platform.handle_event::<()>( - imgui.context.io_mut(), - &window.window, - &Event::AboutToWait, - ); + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + let Some(window) = self.active_window() else { + return; + }; + window.handle_event(event_loop, &Event::AboutToWait); } } -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)) +pub trait AppWindow { + fn id(&self) -> WindowId; + fn init(&mut self); + fn handle_event(&mut self, event_loop: &ActiveEventLoop, event: &Event); } -#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] -#[repr(C)] -struct Colors { - left: [f32; 4], - right: [f32; 4], +pub enum UserEvent { + OpenWindow(Box), + CloseWindow(WindowId), +} + +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(), + } + } } diff --git a/src/app/common.rs b/src/app/common.rs new file mode 100644 index 0000000..9aaf8b0 --- /dev/null +++ b/src/app/common.rs @@ -0,0 +1,248 @@ +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>(self, title: T) -> Self { + Self { + attributes: self.attributes.with_title(title), + ..self + } + } + + pub fn with_inner_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, + pub window: Arc, + pub surface_desc: wgpu::SurfaceConfiguration, + pub surface: wgpu::Surface<'static>, + pub hidpi_factor: f64, +} + +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, + } + } + + pub fn logical_size(&self) -> LogicalSize { + PhysicalSize::new(self.surface_desc.width, self.surface_desc.height) + .to_logical(self.hidpi_factor) + } + + pub fn handle_resize(&mut self, size: &PhysicalSize) { + self.surface_desc.width = size.width; + self.surface_desc.height = size.height; + self.surface.configure(&self.device, &self.surface_desc); + } +} + +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, +} +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, +} + +impl ContextGuard { + fn new() -> Self { + Self { + value: Some(SuspendedContext::create()), + } + } + + pub fn lock(&mut self) -> Option> { + 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, + 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>; +} + +impl UiExt for imgui::Ui { + fn fullscreen_window(&self) -> Option> { + self.window("fullscreen") + .position([0.0, 0.0], imgui::Condition::Always) + .size(self.window_size(), imgui::Condition::Always) + .flags(imgui::WindowFlags::NO_DECORATION) + .begin() + } +} diff --git a/src/app/game.rs b/src/app/game.rs new file mode 100644 index 0000000..41c50cc --- /dev/null +++ b/src/app/game.rs @@ -0,0 +1,357 @@ +use std::{sync::Arc, time::Instant}; +use wgpu::util::DeviceExt as _; +use winit::{ + dpi::LogicalSize, + event::{Event, KeyEvent, WindowEvent}, + event_loop::{ActiveEventLoop, EventLoopProxy}, + window::WindowId, +}; + +use crate::{ + controller::ControllerState, + emulator::{EmulatorClient, EmulatorCommand}, + renderer::GameRenderer, +}; + +use super::{ + common::{ImguiState, WindowState, WindowStateBuilder}, + input::InputWindow, + AppWindow, UserEvent, +}; + +pub struct GameWindow { + window: WindowState, + imgui: Option, + pipeline: wgpu::RenderPipeline, + bind_group: wgpu::BindGroup, + client: EmulatorClient, + controller: ControllerState, + proxy: EventLoopProxy, +} + +impl GameWindow { + pub fn new( + event_loop: &ActiveEventLoop, + client: EmulatorClient, + proxy: EventLoopProxy, + ) -> 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, + }); + + let controller = ControllerState::new(); + client.send_command(EmulatorCommand::SetKeys(controller.pressed())); + Self { + window, + imgui: None, + pipeline: render_pipeline, + bind_group, + client, + controller, + proxy, + } + } + + 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 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) => { + 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); + } + } + } + }); + ui.menu("Input", || { + if ui.menu_item("Map Input") { + let input_window = Box::new(InputWindow::new(event_loop, 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); + + window.queue.submit(Some(encoder.finish())); + + frame.present(); + } + + fn handle_key_event(&mut self, event: &KeyEvent) { + if self.controller.key_event(event) { + self.client + .send_command(EmulatorCommand::SetKeys(self.controller.pressed())); + } + } +} + +impl AppWindow for GameWindow { + fn id(&self) -> WindowId { + self.window.window.id() + } + + fn init(&mut self) { + self.imgui = Some(ImguiState::new(&self.window)); + } + + fn handle_event(&mut self, event_loop: &ActiveEventLoop, event: &Event) { + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::Resized(size) => self.window.handle_resize(size), + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::KeyboardInput { event, .. } => self.handle_key_event(event), + 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], +} diff --git a/src/app/input.rs b/src/app/input.rs new file mode 100644 index 0000000..276f297 --- /dev/null +++ b/src/app/input.rs @@ -0,0 +1,142 @@ +use std::time::Instant; + +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ActiveEventLoop, EventLoopProxy}, +}; + +use super::{ + common::{ImguiState, UiExt as _, WindowState, WindowStateBuilder}, + AppWindow, UserEvent, +}; + +pub struct InputWindow { + window: WindowState, + imgui: Option, + proxy: EventLoopProxy, +} + +impl InputWindow { + pub fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy) -> Self { + let window = WindowStateBuilder::new(event_loop) + .with_title("Map Inputs") + .build(); + Self { + window, + imgui: 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) => { + eprintln!("dropped frame: {e:?}"); + return; + } + }; + imgui + .platform + .prepare_frame(context.io_mut(), &window.window) + .expect("Failed to prepare frame"); + let ui = context.new_frame(); + + if let Some(window) = ui.fullscreen_window() { + if let Some(table) = ui.begin_table("controls", 2) { + ui.table_next_row(); + + ui.table_next_column(); + ui.text("Key"); + + ui.table_next_column(); + ui.text("Value"); + table.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(); + } +} + +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)); + } + + fn handle_event(&mut self, _: &ActiveEventLoop, event: &Event) { + 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::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); + } +} diff --git a/src/emulator.rs b/src/emulator.rs index 8682209..6e113a8 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -160,6 +160,7 @@ pub enum EmulatorCommand { SetKeys(VBKey), } +#[derive(Clone)] pub struct EmulatorClient { queue: mpsc::Sender, running: Arc, diff --git a/src/main.rs b/src/main.rs index 7a588ae..bca1441 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,8 +36,9 @@ fn main() -> Result<()> { emulator.run(); }); - let event_loop = EventLoop::new().unwrap(); + let event_loop = EventLoop::with_user_event().build().unwrap(); event_loop.set_control_flow(ControlFlow::Poll); - event_loop.run_app(&mut App::new(client))?; + let proxy = event_loop.create_proxy(); + event_loop.run_app(&mut App::new(client, proxy))?; Ok(()) }