use std::{sync::Arc, 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, SimId}, renderer::GameRenderer, }; use super::{ common::{ImguiState, WindowState, WindowStateBuilder}, AppWindow, UserEvent, }; pub struct GameWindow { window: WindowState, imgui: ImguiState, pipeline: wgpu::RenderPipeline, bind_group: wgpu::BindGroup, sim_id: SimId, client: EmulatorClient, proxy: EventLoopProxy, paused_due_to_minimize: bool, } impl GameWindow { pub fn new( event_loop: &ActiveEventLoop, sim_id: SimId, client: EmulatorClient, proxy: EventLoopProxy, ) -> Self { let title = if sim_id == SimId::Player2 { "Shrooms VB (Player 2)" } else { "Shrooms VB" }; let window = WindowStateBuilder::new(event_loop) .with_title(title) .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( sim_id, 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 imgui = ImguiState::new(&window); Self { window, imgui, pipeline: render_pipeline, bind_group, sim_id, client, proxy, paused_due_to_minimize: false, } } fn draw(&mut self, event_loop: &ActiveEventLoop) { let window = &mut self.window; let imgui = &mut self.imgui; 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(self.sim_id, 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") { self.proxy.send_event(UserEvent::OpenInputWindow).unwrap(); } }); ui.menu("Multiplayer", || { if self.sim_id == SimId::Player1 && ui.menu_item("Open Player 2") { self.client .send_command(EmulatorCommand::StartSecondSim(None)); self.proxy.send_event(UserEvent::OpenPlayer2Window).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::( 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 handle_event(&mut self, event_loop: &ActiveEventLoop, event: &Event) { 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 => { if self.sim_id == SimId::Player2 { self.client.send_command(EmulatorCommand::StopSecondSim); self.proxy.send_event(UserEvent::Close(self.id())).unwrap(); } else { event_loop.exit(); } } WindowEvent::RedrawRequested => self.draw(event_loop), _ => (), }, Event::AboutToWait => { self.window.window.request_redraw(); } _ => (), } let window = &self.window; let imgui = &mut self.imgui; 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], }