use crate::config::RenderWindowConfig;
use pollster::FutureExt;
use thiserror::Error;
use wgpu::include_wgsl;
use winit::{
dpi::PhysicalSize,
error::OsError,
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::Window,
};
/// No device could be found which supports the given surface
#[derive(Clone, Copy, Debug, 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(Debug)]
pub struct Renderer {
surface: wgpu::Surface,
device: wgpu::Device,
queue: wgpu::Queue,
surface_config: wgpu::SurfaceConfiguration,
render_pipeline: wgpu::RenderPipeline,
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
///
/// # Panics
///
/// This function **must** be called on the main thread, or else it may
/// panic on some platforms.
// 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
pub fn new(
config: &RenderWindowConfig,
event_loop: &EventLoop<()>,
) -> Result<Self, NewRendererError> {
// build the window
let window = config.to_window().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 = config.power_preference();
// 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();
// configuration for the surface
let surface_config = config.to_surface_configuration(
&surface.get_supported_modes(&adapter),
surface.get_supported_formats(&adapter)[0],
);
surface.configure(&device, &surface_config);
// set up a pipeline for sprite rendering
let shader = device.create_shader_module(include_wgsl!("../shaders/sprite.wgsl"));
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render Pipeline Layout"),
..Default::default()
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Render Pipeline"),
layout: Some(&render_pipeline_layout),
// information about the vertex shader
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[],
},
// information about the fragment shader
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: surface_config.format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
// save some memory
topology: wgpu::PrimitiveTopology::TriangleStrip,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
// don't render the back of a sprite
cull_mode: Some(wgpu::Face::Back),
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
});
Ok(Self {
surface,
device,
queue,
surface_config,
render_pipeline,
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.surface_config.height = size.height;
self.surface_config.width = size.width;
self.surface.configure(&self.device, &self.surface_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 mut 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,
});
render_pass.set_pipeline(&self.render_pipeline);
render_pass.draw(0..4, 0..1);
}
// 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::RedrawRequested(window_id) => {
if window_id == self.window.id() {
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),
}
}
}
Event::MainEventsCleared => {
self.window.request_redraw();
}
_ => {}
})
}
}
|