diff options
Diffstat (limited to 'src/renderer.rs')
| -rw-r--r-- | src/renderer.rs | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..f937183 --- /dev/null +++ b/src/renderer.rs @@ -0,0 +1,250 @@ +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<NonZeroU32>, + pub min_height: Option<NonZeroU32>, + pub max_width: Option<NonZeroU32>, + pub max_height: Option<NonZeroU32>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WindowInfo { + pub default_x: i32, + pub default_y: i32, + pub resizability: Option<Resizable>, + 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<Box<str>>, + 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<Self, NewRendererError> { + 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<u32> { + PhysicalSize::new(self.config.width, self.config.height) + } + + // TODO return error for zero-sized windows + pub fn resize(&mut self, size: PhysicalSize<u32>) { + 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(()) + } +} |
