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(())
}
}
|