use imgui::*; use imgui_wgpu::{Renderer, RendererConfig}; use imgui_winit_support::WinitPlatform; use pollster::block_on; use std::{num::NonZero, sync::Arc, time::Instant}; #[cfg(target_os = "windows")] use winit::platform::windows::{CornerPreference, WindowAttributesExtWindows as _}; use winit::{ application::ApplicationHandler, dpi::LogicalSize, event::{ElementState, Event, WindowEvent}, event_loop::ActiveEventLoop, keyboard::Key, window::Window, }; use crate::{ 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 { required_features: wgpu::Features::TEXTURE_BINDING_ARRAY, ..wgpu::DeviceDescriptor::default() }, None, )) .unwrap(); let queue = Arc::new(queue); let eyes = [ Arc::new(GameRenderer::create_texture(&device, "left eye")), Arc::new(GameRenderer::create_texture(&device, "right eye")), ]; client.send_command(EmulatorCommand::SetRenderer(GameRenderer { queue: queue.clone(), eyes: eyes.clone(), })); let eyes = [ eyes[0].create_view(&wgpu::TextureViewDescriptor::default()), eyes[1].create_view(&wgpu::TextureViewDescriptor::default()), ]; let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); 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: NonZero::new(2), }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), 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::TextureViewArray(&[&eyes[0], &eyes[1]]), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, ], }); // 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 = (13.0 * self.hidpi_factor) as f32; context.io_mut().font_global_scale = (1.0 / self.hidpi_factor) as f32; context.fonts().add_font(&[FontSource::DefaultFontData { config: Some(imgui::FontConfig { oversample_h: 1, pixel_snap_h: true, size_pixels: font_size, ..Default::default() }), }]); // // 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, } impl App { pub fn new(client: EmulatorClient) -> Self { Self { window: None, client, } } } 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 = 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], }; window .surface .configure(&window.device, &window.surface_desc); } WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::KeyboardInput { event, .. } => { if let Key::Character("s") = event.logical_key.as_ref() { match event.state { ElementState::Pressed => { self.client.send_command(EmulatorCommand::PressStart) } ElementState::Released => { self.client.send_command(EmulatorCommand::ReleaseStart) } } } } 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 height = 0.0; ui.main_menu_bar(|| { height = ui.window_size()[1]; ui.menu("ROM", || { if ui.menu_item("Open ROM") { println!("clicked"); } if ui.menu_item("Quit") { event_loop.exit(); } }); ui.menu("Emulation", || { if ui.menu_item("Pause") { println!("clicked"); } if ui.menu_item("Reset") { println!("clicked"); } }); }); 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, }); rpass.set_pipeline(&window.pipeline); let hidpi = window.hidpi_factor as f32; rpass.set_viewport(0.0, height * hidpi, 384.0 * hidpi, 224.0 * hidpi, 0.0, 1.0); rpass.set_bind_group(0, &window.bind_group, &[]); rpass.draw(0..6, 0..1); rpass.set_viewport(0.0, 0.0, 384.0 * hidpi, 224.0 * hidpi, 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, ); } }