summaryrefslogtreecommitdiff
path: root/render/src
diff options
context:
space:
mode:
Diffstat (limited to 'render/src')
-rw-r--r--render/src/camera.rs187
-rw-r--r--render/src/config.rs198
-rw-r--r--render/src/instance.rs150
-rw-r--r--render/src/lib.rs20
-rw-r--r--render/src/renderer.rs438
-rw-r--r--render/src/texture.rs127
-rw-r--r--render/src/vertex.rs39
7 files changed, 0 insertions, 1159 deletions
diff --git a/render/src/camera.rs b/render/src/camera.rs
deleted file mode 100644
index ecece90..0000000
--- a/render/src/camera.rs
+++ /dev/null
@@ -1,187 +0,0 @@
-use std::mem::size_of;
-
-use cgmath::{Matrix4, Vector2};
-
-#[derive(Debug)]
-pub struct Camera {
- position: (f32, f32),
- zoom: f32,
- rotation: f32,
- inverse_aspect_ratio: f32,
- buffer: wgpu::Buffer,
- bind_group: wgpu::BindGroup,
-}
-
-type CameraUniform = [[f32; 4]; 4];
-
-#[allow(clippy::cast_precision_loss)]
-fn inverse_aspect_ratio(width: u32, height: u32) -> f32 {
- (height as f32) / (width as f32)
-}
-
-impl Camera {
- /// Create a new camera, with a position of (0, 0), and a zoom of 1.0
- pub(crate) fn new(
- device: &wgpu::Device,
- width: u32,
- height: u32,
- ) -> (Self, wgpu::BindGroupLayout) {
- let buffer = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Camera Uniform"),
- size: size_of::<CameraUniform>() as wgpu::BufferAddress,
- usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
- let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- label: Some("Camera Bind Group Layout"),
- entries: &[wgpu::BindGroupLayoutEntry {
- binding: 0,
- visibility: wgpu::ShaderStages::VERTEX,
- ty: wgpu::BindingType::Buffer {
- ty: wgpu::BufferBindingType::Uniform,
- has_dynamic_offset: false,
- min_binding_size: None,
- },
- count: None,
- }],
- });
-
- let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("Camera Bind Group"),
- layout: &bind_group_layout,
- entries: &[wgpu::BindGroupEntry {
- binding: 0,
- resource: buffer.as_entire_binding(),
- }],
- });
-
- (
- Self {
- position: (0.0, 0.0),
- zoom: 1.0,
- rotation: 0.0,
- inverse_aspect_ratio: inverse_aspect_ratio(width, height),
- buffer,
- bind_group,
- },
- bind_group_layout,
- )
- }
-
- /// Get the camera's current x position
- #[must_use]
- pub const fn x(&self) -> f32 {
- self.position.0
- }
-
- /// Get the camera's current y position
- #[must_use]
- pub const fn y(&self) -> f32 {
- self.position.1
- }
-
- /// Get the camera's current zoom
- #[must_use]
- pub const fn zoom(&self) -> f32 {
- self.zoom
- }
-
- /// Get the camera's current rotation, in radians
- #[must_use]
- pub const fn rotation(&self) -> f32 {
- self.rotation
- }
-
- /// Set the position of the camera
- pub fn set_position(&mut self, x: f32, y: f32) {
- #[cfg(debug_assertions)]
- if !(-1000.0..1000.0).contains(&x) || !(-1000.0..1000.0).contains(&y) {
- log::warn!(
- "The position of the camera is (x: {}, y: {}). \
- Please keep both the x and y positions above -1000 and below 1000 units. \
- Otherwise, everything will look crazy. \
- For an explanation, see https://www.youtube.com/watch?v=Q2OGwnRik24",
- x,
- y
- );
- }
-
- self.position = (x, y);
- }
-
- /// Set the zoom of the camera
- pub fn set_zoom(&mut self, zoom: f32) {
- #[cfg(debug_assertions)]
- if !(-1000.0..1000.0).contains(&zoom) {
- log::warn!(
- "The zoom of the camera is {}. \
- Please keep above -1000, and below 1000, or else smooth zoom may be difficult. \
- For an explanation, see https://www.youtube.com/watch?v=Q2OGwnRik24",
- zoom
- );
- }
-
- self.zoom = zoom;
- }
-
- /// Set the camera rotation, in radians
- pub fn set_rotation(&mut self, rotation: f32) {
- self.rotation = rotation % std::f32::consts::TAU;
- }
-
- /// Set the aspect ratio of the camera
- pub(crate) fn set_size(&mut self, width: u32, height: u32) {
- self.inverse_aspect_ratio = inverse_aspect_ratio(width, height);
- }
-
- /// Create a matrix that can be multiplied by any vector to transform it
- /// according to the current camera
- #[allow(clippy::wrong_self_convention)]
- fn to_matrix(&self) -> CameraUniform {
- let cos = self.rotation.cos();
- let sin = self.rotation.sin();
-
- let x_axis = Vector2::new(cos, -sin);
- let y_axis = Vector2::new(sin, cos);
-
- let eye = Vector2::new(self.position.0, self.position.1);
- let x_dot = -cgmath::dot(x_axis, eye);
- let y_dot = -cgmath::dot(y_axis, eye);
-
- #[rustfmt::skip]
- let view_matrix = Matrix4::new(
- x_axis.x, y_axis.x, 0.0, 0.0,
- x_axis.y, y_axis.y, 0.0, 0.0,
- 0.0, 0.0, 1.0, 0.0,
- x_dot, y_dot, 0.0, 1.0
- );
-
- #[rustfmt::skip]
- // TODO implement more scaling coordinate systems
- let projection_matrix = Matrix4::new(
- self.inverse_aspect_ratio * self.zoom, 0.0, 0.0, 0.0,
- 0.0, self.zoom, 0.0, 0.0,
- 0.0, 0.0, 1.0 / 256.0, 0.0,
- 0.0, 0.0, 0.0, 1.0
- );
-
- let transform = projection_matrix * view_matrix;
- transform.into()
- }
-
- /// Get the bind group for the camera
- pub(crate) const fn bind_group(&self) -> &wgpu::BindGroup {
- &self.bind_group
- }
-
- /// Refresh the camera buffer for the next frame
- #[profiling::function]
- pub(crate) fn refresh(&self, queue: &wgpu::Queue) {
- queue.write_buffer(
- &self.buffer,
- 0 as wgpu::BufferAddress,
- bytemuck::cast_slice(&self.to_matrix()),
- );
- }
-}
diff --git a/render/src/config.rs b/render/src/config.rs
deleted file mode 100644
index c3cc6b6..0000000
--- a/render/src/config.rs
+++ /dev/null
@@ -1,198 +0,0 @@
-use std::num::NonZeroU32;
-
-use winit::dpi::{LogicalPosition, LogicalSize};
-use winit::window::{Fullscreen, WindowBuilder};
-
-/// Describes how a window may be resized
-#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
-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, Hash)]
-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, Hash)]
-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, Hash)]
-// 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: &'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,
- /// The initial capacity of the instance buffer. The size will increase if
- /// it's not large enough. Increasing this value may improve performance
- /// towards the beginning, if a lot of instances are being created. For
- /// compatibility with older devices, it's recommended to keep this number
- /// below 150 thousand.
- pub instance_capacity: usize,
-}
-
-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",
- low_power: true,
- vsync: true,
- instance_capacity: 500,
- }
- }
-}
-
-impl<'a> RenderWindowConfig<'a> {
- /// Based on the vsync settings, choose a presentation mode
- pub(crate) fn present_mode(
- vsync: bool,
- supported_modes: &[wgpu::PresentMode],
- ) -> wgpu::PresentMode {
- if vsync {
- wgpu::PresentMode::Fifo
- } else if supported_modes.contains(&wgpu::PresentMode::Mailbox) {
- wgpu::PresentMode::Mailbox
- } else if supported_modes.contains(&wgpu::PresentMode::Immediate) {
- wgpu::PresentMode::Immediate
- } else {
- wgpu::PresentMode::Fifo
- }
- }
-
- /// Pick an alpha mode
- fn alpha_mode(supported_modes: &[wgpu::CompositeAlphaMode]) -> wgpu::CompositeAlphaMode {
- if supported_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
- wgpu::CompositeAlphaMode::PostMultiplied
- } else {
- wgpu::CompositeAlphaMode::Auto
- }
- }
-
- /// Create a `WindowBuilder` from the configuration given. This window is
- /// initially invisible and must later be made visible.
- pub(crate) fn to_window(&self) -> WindowBuilder {
- // start building the window
- let mut builder = WindowBuilder::new()
- .with_title(self.title)
- .with_visible(false)
- .with_inner_size(LogicalSize::new(
- self.default_width.get(),
- self.default_height.get(),
- ));
-
- match self.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)));
- }
- }
-
- builder
- }
-
- /// Gets a surface configuration out of the config.
- pub(crate) fn to_surface_configuration(
- &self,
- supported_present_modes: &[wgpu::PresentMode],
- supported_alpha_modes: &[wgpu::CompositeAlphaMode],
- ) -> wgpu::SurfaceConfiguration {
- let present_mode = Self::present_mode(self.vsync, supported_present_modes);
- let alpha_mode = Self::alpha_mode(supported_alpha_modes);
-
- // configuration for the surface
- wgpu::SurfaceConfiguration {
- usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
- format: wgpu::TextureFormat::Bgra8Unorm,
- width: self.default_width.get(),
- height: self.default_height.get(),
- alpha_mode,
- present_mode,
- view_formats: vec![
- wgpu::TextureFormat::Bgra8Unorm,
- wgpu::TextureFormat::Bgra8UnormSrgb,
- ],
- }
- }
-
- /// Get the power preference
- pub(crate) const fn power_preference(&self) -> wgpu::PowerPreference {
- if self.low_power {
- wgpu::PowerPreference::LowPower
- } else {
- wgpu::PowerPreference::HighPerformance
- }
- }
-}
diff --git a/render/src/instance.rs b/render/src/instance.rs
deleted file mode 100644
index 15317c7..0000000
--- a/render/src/instance.rs
+++ /dev/null
@@ -1,150 +0,0 @@
-use std::mem::size_of;
-
-use bytemuck::{Pod, Zeroable};
-
-/// The ID for an instance
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
-pub struct InstanceId(usize);
-
-/// A sprite, that can be used by the alligator shader
-#[repr(C)]
-#[derive(Copy, Clone, Debug, PartialEq, Pod, Zeroable)]
-pub struct Instance {
- /// Position on the screen
- pub position: [f32; 2],
- /// Relative size
- pub size: [f32; 2],
- /// The location of the texture in the texture atlas
- pub texture_coordinates: [f32; 2],
- /// The size of the sprite's texture
- pub texture_size: [f32; 2],
-}
-
-impl Default for Instance {
- fn default() -> Self {
- Self {
- position: [0.0; 2],
- size: [1.0; 2],
- texture_coordinates: [0.0; 2],
- texture_size: [1.0; 2],
- }
- }
-}
-
-impl Instance {
- // whenever this is updated, please also update `sprite.wgsl`
- const ATTRIBUTES: [wgpu::VertexAttribute; 7] = wgpu::vertex_attr_array![
- 1 => Float32x2, 2 => Float32x2, 3 => Float32x2, 4 => Float32x2,
- 5 => Uint32, 6 => Float32, 7 => Float32
- ];
-
- pub(crate) fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
- // make sure these two don't conflict
- debug_assert_eq!(
- Self::ATTRIBUTES[0].shader_location as usize,
- crate::Vertex::ATTRIBUTES.len()
- );
- wgpu::VertexBufferLayout {
- array_stride: size_of::<Self>() as wgpu::BufferAddress,
- step_mode: wgpu::VertexStepMode::Instance,
- attributes: &Self::ATTRIBUTES,
- }
- }
-}
-
-/// A buffer of sprites, for both CPU and GPU memory
-pub struct InstanceBuffer {
- instances: Vec<Instance>,
- instance_buffer: wgpu::Buffer,
- instance_buffer_size: usize,
-}
-
-fn create_buffer(device: &wgpu::Device, instances: &Vec<Instance>) -> wgpu::Buffer {
- device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Sprite Instance Buffer"),
- size: (instances.capacity() * size_of::<Instance>()) as wgpu::BufferAddress,
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- })
-}
-
-impl InstanceBuffer {
- /// Create a new buffer with the given capacity
- pub(crate) fn new(device: &wgpu::Device, capacity: usize) -> Self {
- let instances = Vec::with_capacity(capacity);
- let instance_buffer_size = instances.capacity();
- let instance_buffer = create_buffer(device, &instances);
-
- Self {
- instances,
- instance_buffer,
- instance_buffer_size,
- }
- }
-
- /// The number of sprites
- pub fn len(&self) -> u32 {
- self.instances
- .len()
- .try_into()
- .expect("expected less than 3 billion instances")
- }
-
- /// Returns `true` if there are no sprites
- pub fn is_empty(&self) -> bool {
- self.instances.is_empty()
- }
-
- /// The capacity of the buffer
- pub const fn buffer_size(&self) -> usize {
- self.instance_buffer_size
- }
-
- /// Get a slice containing the entire buffer
- pub(crate) fn buffer_slice(&self) -> wgpu::BufferSlice {
- self.instance_buffer.slice(..)
- }
-
- /// Add a new sprite. The new sprite's `InstanceId` is returned. This ID
- /// becomes invalid if the buffer is cleared.
- pub fn push_instance(&mut self, instance: Instance) -> InstanceId {
- let index = self.instances.len();
- self.instances.push(instance);
- InstanceId(index)
- }
-
- /// Get a specific instance
- pub fn get_instance(&self, id: InstanceId) -> Option<&Instance> {
- self.instances.get(id.0)
- }
-
- /// Get a mutable reference to a specific sprite
- pub fn get_instance_mut(&mut self, id: InstanceId) -> Option<&mut Instance> {
- self.instances.get_mut(id.0)
- }
-
- /// Clear the instance buffer. This invalidates all `InstanceId`'s
- pub fn clear(&mut self) {
- self.instances.clear();
- }
-
- /// Increase the capacity of the buffer
- fn expand_buffer(&mut self, device: &wgpu::Device) {
- self.instance_buffer_size = self.instances.capacity();
- self.instance_buffer = create_buffer(device, &self.instances);
- }
-
- /// Fill the GPU buffer with the sprites in the CPU buffer.
- #[profiling::function]
- pub(crate) fn fill_buffer(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
- if self.instances.len() > self.instance_buffer_size {
- self.expand_buffer(device);
- }
-
- queue.write_buffer(
- &self.instance_buffer,
- 0 as wgpu::BufferAddress,
- bytemuck::cast_slice(&self.instances),
- );
- }
-}
diff --git a/render/src/lib.rs b/render/src/lib.rs
deleted file mode 100644
index 6f96fc2..0000000
--- a/render/src/lib.rs
+++ /dev/null
@@ -1,20 +0,0 @@
-#![feature(type_alias_impl_trait)]
-#![warn(clippy::pedantic)]
-#![warn(clippy::nursery)]
-#![allow(clippy::module_name_repetitions)]
-
-mod camera;
-pub mod config;
-pub mod instance;
-pub mod renderer;
-mod texture;
-mod vertex;
-
-pub(crate) use camera::Camera;
-pub use config::*;
-pub use instance::Instance;
-pub(crate) use instance::InstanceBuffer;
-pub use instance::InstanceId;
-pub use renderer::Renderer;
-pub(crate) use texture::TextureAtlas;
-pub(crate) use vertex::Vertex;
diff --git a/render/src/renderer.rs b/render/src/renderer.rs
deleted file mode 100644
index 4b4f60d..0000000
--- a/render/src/renderer.rs
+++ /dev/null
@@ -1,438 +0,0 @@
-use std::num::NonZeroU32;
-use std::{convert::TryInto, sync::Arc};
-
-use crate::{
- vertex::SQUARE, Camera, Instance, InstanceBuffer, RenderWindowConfig, TextureAtlas, Vertex,
-};
-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::VULKAN)
- .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) -> 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::default());
-
- // 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,
- 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 texture_atlas(&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: wgpu::StoreOp::Discard,
- },
- })],
- depth_stencil_attachment: None,
- timestamp_writes: None,
- occlusion_query_set: 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!();
- }
- _ => {}
- });
- }
-}
diff --git a/render/src/texture.rs b/render/src/texture.rs
deleted file mode 100644
index 90d23d1..0000000
--- a/render/src/texture.rs
+++ /dev/null
@@ -1,127 +0,0 @@
-use std::error::Error;
-
-use image::{EncodableLayout, RgbaImage};
-use thiserror::Error;
-
-#[derive(Error, Debug)]
-pub enum TextureError {
- #[error("Unexpected Error (this is a bug in alligator_render): {}", .0)]
- Unexpected(#[source] Box<dyn Error>),
-}
-
-/// Simpler constructor for a wgpu extent3d
-const fn extent_3d(width: u32, height: u32) -> wgpu::Extent3d {
- wgpu::Extent3d {
- width,
- height,
- depth_or_array_layers: 1,
- }
-}
-
-/// A texture atlas, usable by the renderer
-// TODO make these resizable
-#[derive(Debug)]
-pub struct TextureAtlas {
- diffuse_texture: wgpu::Texture,
- diffuse_bind_group: wgpu::BindGroup,
- image: RgbaImage,
-}
-
-impl TextureAtlas {
- /// Creates a new texture atlas, with the given size
- // TODO this is still too large
- pub fn new(device: &wgpu::Device, width: u32, height: u32) -> (Self, wgpu::BindGroupLayout) {
- let atlas_size = extent_3d(width, height);
- let diffuse_texture = device.create_texture(&wgpu::TextureDescriptor {
- label: Some("Diffuse Texture"),
- size: atlas_size,
- mip_level_count: 1,
- sample_count: 1,
- dimension: wgpu::TextureDimension::D2,
- format: wgpu::TextureFormat::Rgba8Unorm,
- usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
- view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
- });
-
- // TODO I don't think this refreshes anything
- let diffuse_texture_view =
- diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default());
-
- let diffuse_sampler = device.create_sampler(&wgpu::SamplerDescriptor::default());
-
- let texture_bind_group_layout =
- device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- label: Some("Texture Bind Group Layout"),
- entries: &[
- wgpu::BindGroupLayoutEntry {
- binding: 0,
- visibility: wgpu::ShaderStages::FRAGMENT,
- ty: wgpu::BindingType::Texture {
- sample_type: wgpu::TextureSampleType::Float { filterable: true },
- view_dimension: wgpu::TextureViewDimension::D2,
- multisampled: false,
- },
- count: None,
- },
- wgpu::BindGroupLayoutEntry {
- binding: 1,
- visibility: wgpu::ShaderStages::FRAGMENT,
- ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
- count: None,
- },
- ],
- });
-
- let diffuse_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("Diffuse Bind Group"),
- layout: &texture_bind_group_layout,
- entries: &[
- wgpu::BindGroupEntry {
- binding: 0,
- resource: wgpu::BindingResource::TextureView(&diffuse_texture_view),
- },
- wgpu::BindGroupEntry {
- binding: 1,
- resource: wgpu::BindingResource::Sampler(&diffuse_sampler),
- },
- ],
- });
-
- (
- Self {
- diffuse_texture,
- diffuse_bind_group,
- image: RgbaImage::from_raw(
- width,
- height,
- vec![0; 4 * width as usize * height as usize],
- )
- .unwrap(),
- },
- texture_bind_group_layout,
- )
- }
-
- /// get the bind group for the texture
- pub(crate) const fn bind_group(&self) -> &wgpu::BindGroup {
- &self.diffuse_bind_group
- }
-
- pub(crate) fn fill_textures(&self, queue: &wgpu::Queue) {
- queue.write_texture(
- wgpu::ImageCopyTexture {
- texture: &self.diffuse_texture,
- mip_level: 0,
- origin: wgpu::Origin3d::ZERO,
- aspect: wgpu::TextureAspect::All,
- },
- self.image.as_bytes(),
- wgpu::ImageDataLayout {
- offset: 0,
- bytes_per_row: Some(self.image.width() * 4),
- rows_per_image: Some(self.image.height()),
- },
- extent_3d(self.image.width(), self.image.height()),
- );
- }
-}
diff --git a/render/src/vertex.rs b/render/src/vertex.rs
deleted file mode 100644
index 570eec4..0000000
--- a/render/src/vertex.rs
+++ /dev/null
@@ -1,39 +0,0 @@
-use std::mem::size_of;
-
-use bytemuck::{Pod, Zeroable};
-
-/// The vertices needed to form a square
-pub const SQUARE: [Vertex; 6] = [
- Vertex::new(-0.5, -0.5),
- Vertex::new(0.5, -0.5),
- Vertex::new(-0.5, 0.5),
- Vertex::new(0.5, 0.5),
- Vertex::new(-0.5, 0.5),
- Vertex::new(0.5, -0.5),
-];
-
-/// A vertex that is usable by the alligator shader
-#[repr(C)]
-#[derive(Copy, Clone, Debug, PartialEq, Pod, Zeroable)]
-pub struct Vertex {
- position: [f32; 2],
-}
-
-impl Vertex {
- // whenever this is updated, please also update `sprite.wgsl`
- pub(crate) const ATTRIBUTES: [wgpu::VertexAttribute; 1] =
- wgpu::vertex_attr_array![0 => Float32x2];
-
- /// Create a new vertex
- const fn new(x: f32, y: f32) -> Self {
- Self { position: [x, y] }
- }
-
- pub(crate) const fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
- wgpu::VertexBufferLayout {
- array_stride: size_of::<Self>() as wgpu::BufferAddress,
- step_mode: wgpu::VertexStepMode::Vertex,
- attributes: &Self::ATTRIBUTES,
- }
- }
-}