summaryrefslogtreecommitdiff
path: root/src/renderer.rs
blob: c9c51bc89a8b1e1c26423b135b7a54cff3c00425 (plain)
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<NonZeroU32>,
	/// The minimum height of the window, or None if unconstrained
	pub min_height: Option<NonZeroU32>,
	/// The maximum width of the window, or None if unconstrained
	pub max_width: Option<NonZeroU32>,
	/// The maximum height of the window, or None if unconstrained
	pub max_height: Option<NonZeroU32>,
}

/// 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<Resizable>,
	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<Self, NewRendererError> {
		// 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<u32>) {
		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),
				}
			}
			_ => {}
		})
	}
}