use std::num::NonZeroU32; use thiserror::Error; use winit::{ dpi::{LogicalSize, PhysicalSize}, error::OsError, event_loop::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), } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Resizable { pub min_width: Option, pub min_height: Option, pub max_width: Option, pub max_height: Option, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct WindowInfo { pub default_x: i32, pub default_y: i32, pub resizability: Option, pub default_maximized: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum WindowMode { Windowed(WindowInfo), // TODO support choosing a monitor BorderlessFullscreen, // TODO exclusive fullscreen } #[derive(Clone, Debug, PartialEq, Eq)] // TODO to consider: don't allow the x and y to be too big // TODO window icon pub struct RendererConfig { pub default_width: NonZeroU32, pub default_height: NonZeroU32, pub size: WindowMode, pub title: Option>, pub low_power: bool, pub vsync: bool, } pub struct Renderer { surface: wgpu::Surface, device: wgpu::Device, queue: wgpu::Queue, config: wgpu::SurfaceConfiguration, window: Window, event_loop: EventLoop<()>, } // 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 winit (ie, use a bitmap in memory as a surface) // TODO make PowerPreference a configuration option // TODO check for zero size windows #[allow(clippy::missing_panics_doc)] pub async fn new(config: RendererConfig) -> Result { let mut builder = WindowBuilder::new() .with_title(config.title.unwrap_or_else(|| "Alligator Game".into())) .with_inner_size(LogicalSize::new( config.default_width.get(), config.default_height.get(), )); match config.size { WindowMode::Windowed(size) => { builder = builder.with_maximized(size.default_maximized); if let Some(resizing_options) = size.resizability { 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); } } WindowMode::BorderlessFullscreen => { builder = builder.with_fullscreen(Some(Fullscreen::Borderless(None))); } } let event_loop = EventLoop::new(); 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 // TODO guarantee the window to stay open longer than the surface 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, }) .await; 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, ) .await .unwrap(); // configuration for the surface let config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: surface.get_supported_formats(&adapter)[0], // TODO make this configurable width: config.default_width.get(), height: config.default_height.get(), present_mode: wgpu::PresentMode::Mailbox, // TODO make this configurable }; surface.configure(&device, &config); Ok(Self { surface, device, queue, config, window, event_loop, }) } pub const fn size(&self) -> PhysicalSize { PhysicalSize::new(self.config.width, self.config.height) } // TODO return error for zero-sized windows pub fn resize(&mut self, size: PhysicalSize) { if size.width > 0 && size.height > 0 { 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. pub 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(()) } }