use crate::config::RenderWindowConfig; use pollster::FutureExt; use thiserror::Error; use winit::{ dpi::PhysicalSize, error::OsError, event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::Window, }; /// No device could be found which supports the given surface #[derive(Clone, Copy, Debug, PartialEq, Eq, Error)] #[error("No GPU could be found on this machine")] pub struct NoGpuError { /// Prevents this type from being constructed _priv: (), } impl NoGpuError { /// Create a new error const fn new() -> Self { Self { _priv: () } } } #[derive(Debug, Error)] pub enum NewRendererError { #[error(transparent)] NoGpu(#[from] NoGpuError), #[error(transparent)] WindowInitError(#[from] OsError), } pub struct Renderer { surface: wgpu::Surface, device: wgpu::Device, queue: wgpu::Queue, config: wgpu::SurfaceConfiguration, window: Window, } // TODO make this more complete impl Renderer { /// Initializes the renderer /// /// # Errors /// /// Returns a [`NoGpu`] error if no device could be detected that can /// display to the window /// /// # Panics /// /// This function **must** be called on the main thread, or else it may /// panic on some platforms. // TODO make it possible to use without a window (ie, use a bitmap in memory as a surface) // TODO this function needs to be smaller pub fn new( config: &RenderWindowConfig, event_loop: &EventLoop<()>, ) -> Result { // build the window let window = config.to_window().build(event_loop)?; // the instance's main purpose is to create an adapter and a surface let instance = wgpu::Instance::new(wgpu::Backends::all()); // the surface is the part of the screen we'll draw to let surface = unsafe { instance.create_surface(&window) }; let power_preference = config.power_preference(); // the adapter is the handle to the GPU let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference, compatible_surface: Some(&surface), force_fallback_adapter: false, }) .block_on(); let adapter = adapter.or_else(|| { instance .enumerate_adapters(wgpu::Backends::all()) .find(|adapter| !surface.get_supported_formats(adapter).is_empty()) }); let Some(adapter) = adapter else { return Err(NoGpuError::new().into()) }; // gets a connection to the device, as well as a handle to its command queue // the options chosen here ensure that this is guaranteed to not panic let (device, queue) = adapter .request_device( &wgpu::DeviceDescriptor { label: None, features: wgpu::Features::empty(), limits: wgpu::Limits::default(), }, None, ) .block_on() .unwrap(); // configuration for the surface let config = config.to_surface_configuration( &surface.get_supported_modes(&adapter), surface.get_supported_formats(&adapter)[0], ); surface.configure(&device, &config); Ok(Self { surface, device, queue, config, window, }) } /// Resize just the renderer. The window will remain unchanged fn resize_renderer(&mut self, size: PhysicalSize) { if size.width == 0 || size.height == 0 { log::error!("The window was somehow set to a size of zero"); return; } self.config.height = size.height; self.config.width = size.width; self.surface.configure(&self.device, &self.config); } /// Renders a new frame to the window /// /// # Errors /// /// A number of problems could occur here. A timeout could occur while /// trying to acquire the next frame. There may also be no more memory left /// that can be used for the new frame. fn render(&mut self) -> Result<(), wgpu::SurfaceError> { // the new texture we can render to let output = self.surface.get_current_texture()?; let view = output .texture .create_view(&wgpu::TextureViewDescriptor::default()); // this will allow us to send commands to the gpu let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Render Encoder"), }); { let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Render Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: true, }, })], depth_stencil_attachment: None, }); } // the encoder can't finish building the command buffer until the // render pass is dropped // submit the command buffer to the GPU self.queue.submit(std::iter::once(encoder.finish())); output.present(); Ok(()) } /// Run the renderer indefinitely pub fn run(mut self, event_loop: EventLoop<()>) -> ! { event_loop.run(move |event, _, control_flow| match event { Event::WindowEvent { window_id, event } => { if window_id == self.window.id() { match event { WindowEvent::Resized(size) => self.resize_renderer(size), WindowEvent::CloseRequested => *control_flow = ControlFlow::ExitWithCode(0), _ => (), } } } Event::MainEventsCleared => { match self.render() { Ok(_) => {} // reconfigure the surface if it's been lost Err(wgpu::SurfaceError::Lost) => self.resize_renderer(self.window.inner_size()), // if we ran out of memory, then we'll die Err(wgpu::SurfaceError::OutOfMemory) => { *control_flow = ControlFlow::ExitWithCode(1); } // otherwise, we'll just log the error Err(e) => log::error!("{}", e), } } _ => {} }) } }