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 winit::{ application::ApplicationHandler, dpi::{LogicalSize, PhysicalSize}, event::{Event, WindowEvent}, event_loop::ActiveEventLoop, window::Window, }; use crate::{ controller::ControllerState, emulator::{EmulatorClient, EmulatorCommand}, renderer::GameRenderer, }; 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 } } pub struct App { window: Option, client: EmulatorClient, controller: ControllerState, } impl App { pub fn new(client: EmulatorClient) -> Self { let controller = ControllerState::new(); client.send_command(EmulatorCommand::SetKeys(controller.pressed())); Self { window: None, client, controller, } } } impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { self.window = Some(AppWindow::new(event_loop, &self.client)); } fn window_event( &mut self, event_loop: &ActiveEventLoop, window_id: winit::window::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); } 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", || { if self.client.is_running() { if ui.menu_item("Pause") { self.client.send_command(EmulatorCommand::Pause); } } else if ui.menu_item("Resume") { self.client.send_command(EmulatorCommand::Resume); } if ui.menu_item("Reset") { 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 }, ); } 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 device_event( &mut self, _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 }, ); } 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 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], }