summaryrefslogtreecommitdiff
path: root/src/renderer.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/renderer.rs')
-rw-r--r--src/renderer.rs250
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(())
+ }
+}