summaryrefslogtreecommitdiff
path: root/render/src/renderer.rs
diff options
context:
space:
mode:
authorMicha White <botahamec@outlook.com>2023-02-13 00:24:07 -0500
committerMicha White <botahamec@outlook.com>2023-02-13 00:24:07 -0500
commit861b467b95be55db3a42182b77dba944869bf49f (patch)
treef58057d5ab9ad53601332981a31553b0ad05fe1b /render/src/renderer.rs
parentc31d51487082c6cf243ecd10da71a15a78d41add (diff)
Rename the subdirectories
Diffstat (limited to 'render/src/renderer.rs')
-rw-r--r--render/src/renderer.rs444
1 files changed, 444 insertions, 0 deletions
diff --git a/render/src/renderer.rs b/render/src/renderer.rs
new file mode 100644
index 0000000..f5b486d
--- /dev/null
+++ b/render/src/renderer.rs
@@ -0,0 +1,444 @@
+use std::num::NonZeroU32;
+use std::{convert::TryInto, sync::Arc};
+
+use crate::{
+ vertex::SQUARE, Camera, Instance, InstanceBuffer, RenderWindowConfig, TextureAtlas, Vertex,
+};
+use alligator_resources::texture::TextureManager;
+use pollster::FutureExt;
+use thiserror::Error;
+use wgpu::{include_wgsl, util::DeviceExt};
+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: () }
+ }
+}
+
+/// No device could be found which supports the given surface
+#[derive(Clone, Copy, Debug, Error)]
+#[error("A WebGPU or WebGL context could not be obtained")]
+pub struct NoWebContextError {
+ /// Prevents this type from being constructed
+ _priv: (),
+}
+
+impl NoWebContextError {
+ /// Create a new error
+ const fn new() -> Self {
+ Self { _priv: () }
+ }
+}
+
+#[derive(Debug, Error)]
+pub enum NewRendererError {
+ #[error(transparent)]
+ NoGpu(#[from] NoGpuError),
+ #[error(transparent)]
+ NoWebContext(#[from] NoWebContextError),
+ #[error(transparent)]
+ // TODO better error
+ WindowInitError(#[from] OsError),
+}
+
+// TODO make this Debug
+pub struct Renderer {
+ // TODO move some of this data elsewhere
+ surface: wgpu::Surface,
+ surface_config: wgpu::SurfaceConfiguration,
+ supported_present_modes: Box<[wgpu::PresentMode]>,
+ device: wgpu::Device,
+ queue: wgpu::Queue,
+ render_pipeline: wgpu::RenderPipeline,
+ square_vertex_buffer: wgpu::Buffer,
+ square_vertices: u32,
+ instances: InstanceBuffer,
+ camera: Camera,
+ textures: TextureAtlas,
+ event_loop: Option<EventLoop<()>>,
+ window: Window,
+}
+
+fn get_adapter(
+ instance: &wgpu::Instance,
+ surface: &wgpu::Surface,
+ power_preference: wgpu::PowerPreference,
+) -> Result<wgpu::Adapter, NoGpuError> {
+ let adapter = instance
+ .request_adapter(&wgpu::RequestAdapterOptions {
+ power_preference,
+ compatible_surface: Some(surface),
+ force_fallback_adapter: false,
+ })
+ .block_on(); // TODO this takes too long
+
+ let adapter = adapter.or_else(|| {
+ instance
+ .enumerate_adapters(wgpu::Backends::PRIMARY)
+ .find(|adapter| !surface.get_capabilities(adapter).formats.is_empty())
+ });
+
+ adapter.ok_or(NoGpuError::new())
+}
+
+fn sprite_render_pipeline(
+ device: &wgpu::Device,
+ texture_format: wgpu::TextureFormat,
+ render_pipeline_layout: &wgpu::PipelineLayout,
+) -> wgpu::RenderPipeline {
+ let shader = device.create_shader_module(include_wgsl!("../shaders/sprite.wgsl"));
+
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("Sprite Render Pipeline"),
+ layout: Some(render_pipeline_layout),
+ // information about the vertex shader
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: "vs_main",
+ buffers: &[Vertex::desc(), Instance::desc()],
+ },
+ // information about the fragment shader
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: "fs_main",
+ targets: &[Some(wgpu::ColorTargetState {
+ format: texture_format,
+ blend: Some(wgpu::BlendState::ALPHA_BLENDING),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ }),
+ primitive: wgpu::PrimitiveState {
+ // don't render the back of a sprite
+ cull_mode: Some(wgpu::Face::Back),
+ ..Default::default()
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState::default(),
+ multiview: None,
+ })
+}
+
+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,
+ textures: Arc<TextureManager>,
+ ) -> Result<Self, NewRendererError> {
+ // build the window
+ let event_loop = EventLoop::new();
+ let window = config.to_window().build(&event_loop)?;
+ let event_loop = Some(event_loop);
+
+ // the instance's main purpose is to create an adapter and a surface
+ let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
+ backends: wgpu::Backends::VULKAN,
+ dx12_shader_compiler: wgpu::Dx12Compiler::Fxc, // TODO support DXC
+ });
+
+ // the surface is the part of the screen we'll draw to
+ let surface =
+ unsafe { instance.create_surface(&window) }.map_err(|_| NoWebContextError::new())?;
+
+ let power_preference = config.power_preference();
+
+ // the adapter is the handle to the GPU
+ let adapter = get_adapter(&instance, &surface, power_preference)?;
+
+ // 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 {
+ features: wgpu::Features::empty(),
+ limits: wgpu::Limits {
+ max_buffer_size: adapter.limits().max_buffer_size,
+ max_texture_dimension_2d: adapter.limits().max_texture_dimension_2d,
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ None,
+ )
+ .block_on()
+ .expect("there was no device with the selected features");
+
+ // configuration for the surface
+ let capabilities = surface.get_capabilities(&adapter);
+ let supported_present_modes = capabilities.present_modes.into_boxed_slice();
+ let supported_alpha_modes = capabilities.alpha_modes.into_boxed_slice();
+ let surface_config =
+ config.to_surface_configuration(&supported_present_modes, &supported_alpha_modes);
+ surface.configure(&device, &surface_config);
+
+ // create the camera
+ let width = window.inner_size().width;
+ let height = window.inner_size().height;
+ let (camera, camera_bind_group_layout) = Camera::new(&device, width, height);
+
+ // the vertex buffer used for rendering squares
+ let square_vertices = SQUARE
+ .len()
+ .try_into()
+ .expect("expected fewer than 3 billion vertices in a square");
+ let square_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("Square Vertex Buffer"),
+ contents: bytemuck::cast_slice(&SQUARE),
+ usage: wgpu::BufferUsages::VERTEX,
+ });
+
+ // create the instance buffer
+ let instances = InstanceBuffer::new(&device, config.instance_capacity);
+
+ // TODO make this configurable
+ let (textures, texture_layout) = TextureAtlas::new(
+ &device,
+ textures,
+ window.inner_size().width,
+ window.inner_size().height,
+ );
+
+ let render_pipeline_layout =
+ device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("Sprite Render Pipeline Layout"),
+ bind_group_layouts: &[&camera_bind_group_layout, &texture_layout],
+ push_constant_ranges: &[],
+ });
+
+ // set up a pipeline for sprite rendering
+ let render_pipeline =
+ sprite_render_pipeline(&device, surface_config.format, &render_pipeline_layout);
+
+ Ok(Self {
+ surface,
+ surface_config,
+ supported_present_modes,
+ device,
+ queue,
+ render_pipeline,
+ square_vertex_buffer,
+ square_vertices,
+ instances,
+ camera,
+ textures,
+ event_loop,
+ window,
+ })
+ }
+
+ /// Reconfigure the surface
+ fn reconfigure(&mut self) {
+ self.surface.configure(&self.device, &self.surface_config);
+ }
+
+ /// 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.camera.set_size(size.width, size.height);
+ self.reconfigure();
+ }
+
+ /// Set the physical window and renderer size
+ pub fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) {
+ let size = PhysicalSize::new(width.get(), height.get());
+ self.window.set_inner_size(size);
+ self.resize_renderer(size);
+ }
+
+ /// Set vsync on or off. See `[RenderWindowConfig::present_mode]` for more details.
+ pub fn set_vsync(&mut self, vsync: bool) {
+ self.surface_config.present_mode =
+ RenderWindowConfig::present_mode(vsync, &self.supported_present_modes);
+ self.reconfigure();
+ }
+
+ /// Set the window's title
+ pub fn set_title(&mut self, title: &str) {
+ self.window.set_title(title);
+ }
+
+ /// The reference buffer
+ pub const fn instances(&self) -> &InstanceBuffer {
+ &self.instances
+ }
+
+ /// The reference buffer
+ pub fn instances_mut(&mut self) -> &mut InstanceBuffer {
+ &mut self.instances
+ }
+
+ /// Get the camera information
+ pub const fn camera(&self) -> &Camera {
+ &self.camera
+ }
+
+ /// Get a mutable reference to the camera
+ pub fn camera_mut(&mut self) -> &mut Camera {
+ &mut self.camera
+ }
+
+ /// Get a reference to the texture atlas
+ pub const fn textures(&self) -> &TextureAtlas {
+ &self.textures
+ }
+
+ /// Get a mutable reference to the texture atlas
+ pub fn textures_mut(&mut self) -> &mut TextureAtlas {
+ &mut self.textures
+ }
+
+ /// 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.
+ // TODO this needs to be smaller
+ // TODO don't return wgpu errors
+ #[profiling::function]
+ fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
+ // 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 num_instances = self.instances.len();
+ self.instances.fill_buffer(&self.device, &self.queue);
+ self.camera.refresh(&self.queue);
+ self.textures.fill_textures(&self.queue);
+
+ // the new texture we can render to
+ let output = self.surface.get_current_texture()?;
+ let view = output
+ .texture
+ .create_view(&wgpu::TextureViewDescriptor::default());
+
+ {
+ profiling::scope!("encode render pass");
+ 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.set_bind_group(0, self.camera.bind_group(), &[]);
+ render_pass.set_bind_group(1, self.textures.bind_group(), &[]);
+ render_pass.set_vertex_buffer(0, self.square_vertex_buffer.slice(..));
+ render_pass.set_vertex_buffer(1, self.instances.buffer_slice());
+ render_pass.draw(0..self.square_vertices, 0..num_instances);
+ }
+ // the encoder can't finish building the command buffer until the
+ // render pass is dropped
+
+ // submit the command buffer to the GPU
+ profiling::scope!("submit render");
+ self.queue.submit(std::iter::once(encoder.finish()));
+ output.present();
+
+ Ok(())
+ }
+
+ /// Take the event loop out of the Renderer, without moving it
+ ///
+ /// # Panics
+ ///
+ /// This method must only be called once
+ // TODO This is a quick fix to get the event loop inside the renderer.
+ // In the future, we should make a separate struct that contains the
+ // renderer and the event loop, which we move the event loop out of
+ // while still being able to move the renderer.
+ fn event_loop(&mut self) -> EventLoop<()> {
+ self.event_loop.take().unwrap()
+ }
+
+ /// Run the renderer indefinitely
+ // TODO this needs to be smaller
+ pub fn run<F: FnMut(&mut Self) + 'static>(mut self, mut f: F) -> ! {
+ self.window.set_visible(true);
+ let event_loop = self.event_loop();
+ 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 => {
+ f(&mut self);
+
+ // a memory leak occurs if we render a zero-size window,
+ // along with a `SurfaceError::Outdated`. I don't know why that
+ // happens, but let's make wgpu happy.
+ // https://github.com/gfx-rs/wgpu/issues/1783#issuecomment-1328463201
+ if self.window.inner_size().width != 0 && self.window.inner_size().height != 0 {
+ match self.render() {
+ Ok(_) => {}
+ // reconfigure the surface if it's been lost
+ Err(wgpu::SurfaceError::Lost) => {
+ self.reconfigure();
+ }
+ // 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),
+ }
+ } else {
+ *control_flow = ControlFlow::Wait;
+ }
+ profiling::finish_frame!();
+ }
+ _ => {}
+ });
+ }
+}