use std::{borrow::Cow, num::NonZeroU32}; use pollster::FutureExt; use thiserror::Error; use winit::{ dpi::{LogicalPosition, LogicalSize, PhysicalSize}, error::OsError, event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::{Fullscreen, Window, WindowBuilder}, }; /// 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), } /// Describes how a window may be resized #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct Resizable { /// The minimum width of the window, or None if unconstrained pub min_width: Option, /// The minimum height of the window, or None if unconstrained pub min_height: Option, /// The maximum width of the window, or None if unconstrained pub max_width: Option, /// The maximum height of the window, or None if unconstrained pub max_height: Option, } /// Information about a window, that is not fullscreened #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct WindowInfo { pub default_x: i32, pub default_y: i32, pub resizable: Option, pub default_maximized: bool, } impl Default for WindowInfo { fn default() -> Self { Self { default_x: 100, default_y: 100, resizable: Some(Resizable::default()), default_maximized: false, } } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum WindowMode { Windowed(WindowInfo), // TODO support choosing a monitor BorderlessFullscreen, // TODO exclusive fullscreen } impl Default for WindowMode { fn default() -> Self { Self::Windowed(WindowInfo::default()) } } #[derive(Clone, Debug, PartialEq, Eq)] // TODO window icon pub struct RenderWindowConfig<'a> { /// The width of the window, once initialized pub default_width: NonZeroU32, /// The height of the window, once initialized pub default_height: NonZeroU32, /// The window may be fullscreen pub mode: WindowMode, /// The title for the window pub title: Cow<'a, str>, /// If true, a low-power device will be selected as the GPU, if possible pub low_power: bool, /// If true, Fifo mode is used to present frames. If false, then Mailbox or /// Immediate will be used if available. Otherwise, Fifo will be used. pub vsync: bool, } impl<'a> Default for RenderWindowConfig<'a> { fn default() -> Self { Self { default_width: NonZeroU32::new(640).unwrap(), default_height: NonZeroU32::new(480).unwrap(), mode: WindowMode::default(), title: "Alligator Game".into(), low_power: true, vsync: true, } } } 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 // 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 #[allow(clippy::missing_panics_doc)] pub fn new( config: RenderWindowConfig, event_loop: &EventLoop<()>, ) -> Result { // start building the window let mut builder = WindowBuilder::new() .with_title(config.title) .with_inner_size(LogicalSize::new( config.default_width.get(), config.default_height.get(), )); match config.mode { WindowMode::Windowed(window_info) => { builder = builder.with_maximized(window_info.default_maximized); if let Some(resizing_options) = window_info.resizable { if resizing_options.max_height.is_some() || resizing_options.max_width.is_some() { builder = builder.with_max_inner_size(LogicalSize::new( resizing_options.max_width.unwrap_or(NonZeroU32::MAX).get(), resizing_options.max_height.unwrap_or(NonZeroU32::MAX).get(), )); } if resizing_options.min_height.is_some() || resizing_options.min_width.is_some() { builder = builder.with_min_inner_size(LogicalSize::new( resizing_options.min_width.unwrap_or(NonZeroU32::MAX).get(), resizing_options.min_height.unwrap_or(NonZeroU32::MAX).get(), )); } } else { builder = builder.with_resizable(false); } // TODO clamp the position to the monitor's size builder = builder.with_position(LogicalPosition::new( window_info.default_x, window_info.default_y, )); } WindowMode::BorderlessFullscreen => { builder = builder.with_fullscreen(Some(Fullscreen::Borderless(None))); } } let window = builder.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 = if config.low_power { wgpu::PowerPreference::LowPower } else { wgpu::PowerPreference::HighPerformance }; // 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(); let present_mode = if config.vsync { wgpu::PresentMode::Fifo } else { let modes = surface.get_supported_modes(&adapter); if modes.contains(&wgpu::PresentMode::Mailbox) { wgpu::PresentMode::Mailbox } else if modes.contains(&wgpu::PresentMode::Immediate) { wgpu::PresentMode::Immediate } else { wgpu::PresentMode::Fifo } }; // configuration for the surface let config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: surface.get_supported_formats(&adapter)[0], width: config.default_width.get(), height: config.default_height.get(), present_mode, }; 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), } } _ => {} }) } }