diff options
| author | Micha White <botahamec@outlook.com> | 2022-10-20 20:39:44 -0400 |
|---|---|---|
| committer | Micha White <botahamec@outlook.com> | 2022-10-20 20:39:44 -0400 |
| commit | 93347346e8bd8f7412ae03a0858dd307a1df2e0d (patch) | |
| tree | 17805956857c76b5fed3f47a821fcdf6141cf7a6 /alligator_render | |
| parent | e337741969160603f06a7f2b30cda375eeef99fb (diff) | |
Moved files into workspace
Diffstat (limited to 'alligator_render')
| -rw-r--r-- | alligator_render/Cargo.toml | 33 | ||||
| -rw-r--r-- | alligator_render/examples/black.rs | 21 | ||||
| -rw-r--r-- | alligator_render/examples/bmp.rs | 88 | ||||
| -rw-r--r-- | alligator_render/examples/res/gator.bmp | bin | 0 -> 750054 bytes | |||
| -rw-r--r-- | alligator_render/examples/res/gator.ff | bin | 0 -> 2000016 bytes | |||
| -rw-r--r-- | alligator_render/examples/res/ghost.ico | bin | 0 -> 67646 bytes | |||
| -rw-r--r-- | alligator_render/shaders/sprite.wgsl | 62 | ||||
| -rw-r--r-- | alligator_render/src/camera.rs | 187 | ||||
| -rw-r--r-- | alligator_render/src/config.rs | 193 | ||||
| -rw-r--r-- | alligator_render/src/instance.rs | 163 | ||||
| -rw-r--r-- | alligator_render/src/lib.rs | 22 | ||||
| -rw-r--r-- | alligator_render/src/renderer.rs | 410 | ||||
| -rw-r--r-- | alligator_render/src/texture.rs | 287 | ||||
| -rw-r--r-- | alligator_render/src/vertex.rs | 39 |
14 files changed, 1505 insertions, 0 deletions
diff --git a/alligator_render/Cargo.toml b/alligator_render/Cargo.toml new file mode 100644 index 0000000..01c5738 --- /dev/null +++ b/alligator_render/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "alligator_render" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +winit = "0.27" +log = "0.4" +wgpu = "0.14" +thiserror = "1" +pollster = "0.2" +bytemuck = { version = "1.4", features = ["derive"] } +cgmath = "0.18" +image = "0.24" +texture_packer = "0.24" +profiling = "1" +tracy-client = { version = "0.14", optional = true } +dhat = "0.3" + +[lib] +crate-type = ["cdylib", "lib"] + +[features] +dhat = [] +profile-with-tracy = ["tracy-client", "profiling/profile-with-tracy"] + +[[example]] +name = "black" + +[[example]] +name = "bmp" diff --git a/alligator_render/examples/black.rs b/alligator_render/examples/black.rs new file mode 100644 index 0000000..c66b080 --- /dev/null +++ b/alligator_render/examples/black.rs @@ -0,0 +1,21 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use alligator_render::{RenderWindowConfig, Renderer}; + +fn update(_renderer: &mut Renderer) {} + +fn main() { + let start = std::time::Instant::now(); + // configure the render window + let config = RenderWindowConfig { + //vsync: false, + //mode: alligator_render::config::WindowMode::BorderlessFullscreen, + title: "Black Screen.exe", + ..Default::default() + }; + + let renderer = Renderer::new(&config).unwrap(); + println!("Startup time: {:?}", start.elapsed()); + + renderer.run(&update); +} diff --git a/alligator_render/examples/bmp.rs b/alligator_render/examples/bmp.rs new file mode 100644 index 0000000..af71863 --- /dev/null +++ b/alligator_render/examples/bmp.rs @@ -0,0 +1,88 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use std::num::NonZeroU32; + +use alligator_render::{ImageFormat, Instance, RenderWindowConfig, Renderer}; + +#[profiling::function] +fn update(renderer: &mut Renderer) { + let camera = renderer.camera_mut(); + camera.set_rotation(camera.rotation() + 0.01); +} + +fn main() { + // configure the render window + let config = RenderWindowConfig { + title: "Bumper Stickers", + instance_capacity: 2, + default_width: NonZeroU32::new(1280).unwrap(), + default_height: NonZeroU32::new(720).unwrap(), + //mode: alligator_render::config::WindowMode::BorderlessFullscreen, + //vsync: false, + ..Default::default() + }; + + let mut renderer = Renderer::new(&config).unwrap(); + + // render the alligator + let gator = include_bytes!("res/gator.ff"); + let gator_id = renderer + .textures_mut() + .load_from_memory(gator, ImageFormat::Farbfeld) + .unwrap(); + let gator_width = renderer.textures().texture_width(gator_id).unwrap(); + let gator_height = renderer.textures().texture_height(gator_id).unwrap(); + let gator_x = renderer.textures().texture_x(gator_id).unwrap(); + let gator_y = renderer.textures().texture_y(gator_id).unwrap(); + + renderer.instances_mut().push_instance(Instance { + position: [-0.5, 0.5], + size: [1.5; 2], + z_index: 1.0, + texture_size: [gator_width, gator_height], + texture_coordinates: [gator_x, gator_y], + ..Default::default() + }); + + // render the ghost + let icon = include_bytes!("res/ghost.ico"); + let icon_id = renderer + .textures_mut() + .load_from_memory(icon, ImageFormat::Ico) + .unwrap(); + let icon_width = renderer.textures().texture_width(icon_id).unwrap(); + let icon_height = renderer.textures().texture_height(icon_id).unwrap(); + let icon_x = renderer.textures().texture_x(icon_id).unwrap(); + let icon_y = renderer.textures().texture_y(icon_id).unwrap(); + + renderer.instances_mut().push_instance(Instance { + position: [0.5, 0.5], + size: [0.75; 2], + rotation: 0.5, + z_index: 1.0, + texture_size: [icon_width, icon_height], + texture_coordinates: [icon_x, icon_y], + ..Default::default() + }); + + // render the bitmap alligator + let gator = include_bytes!("res/gator.bmp"); + let gator_id = renderer + .textures_mut() + .load_from_memory(gator, ImageFormat::Bmp) + .unwrap(); + let gator_width = renderer.textures().texture_width(gator_id).unwrap(); + let gator_height = renderer.textures().texture_height(gator_id).unwrap(); + let gator_x = renderer.textures().texture_x(gator_id).unwrap(); + let gator_y = renderer.textures().texture_y(gator_id).unwrap(); + + renderer.instances_mut().push_instance(Instance { + position: [0.0, -0.5], + size: [1.5; 2], + texture_size: [gator_width, gator_height], + texture_coordinates: [gator_x, gator_y], + ..Default::default() + }); + + renderer.run(&update); +} diff --git a/alligator_render/examples/res/gator.bmp b/alligator_render/examples/res/gator.bmp Binary files differnew file mode 100644 index 0000000..e752b56 --- /dev/null +++ b/alligator_render/examples/res/gator.bmp diff --git a/alligator_render/examples/res/gator.ff b/alligator_render/examples/res/gator.ff Binary files differnew file mode 100644 index 0000000..aac1bcb --- /dev/null +++ b/alligator_render/examples/res/gator.ff diff --git a/alligator_render/examples/res/ghost.ico b/alligator_render/examples/res/ghost.ico Binary files differnew file mode 100644 index 0000000..102de00 --- /dev/null +++ b/alligator_render/examples/res/ghost.ico diff --git a/alligator_render/shaders/sprite.wgsl b/alligator_render/shaders/sprite.wgsl new file mode 100644 index 0000000..60b5773 --- /dev/null +++ b/alligator_render/shaders/sprite.wgsl @@ -0,0 +1,62 @@ + +@group(0) @binding(0) +var<uniform> camera: mat4x4<f32>; + +struct VertexInput { + @location(0) position: vec2<f32> +} + +struct InstanceInput { + @location(1) position: vec2<f32>, + @location(2) size: vec2<f32>, + @location(3) texture_coordinates: vec2<f32>, + @location(4) texture_size: vec2<f32>, + @location(5) texture_atlas_index: u32, + @location(6) rotation: f32, + @location(7) z_index: f32, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4<f32>, + @location(0) texture_coordinates: vec2<f32>, + @location(1) texture_atlas_index: u32 +} + +@vertex +fn vs_main(model: VertexInput, instance: InstanceInput) -> VertexOutput { + var out: VertexOutput; + + // rotate the sprite + let rotation = -instance.rotation; + let a = vec2<f32>(cos(rotation), sin(rotation)); + let b = vec2<f32>(-a[1], a[0]); + let rotation = mat2x2<f32>(a, b); + let rotated = rotation * model.position; + + // scale the sprite + let scaled = rotated * instance.size; + + // move the sprite + let position2d = scaled + instance.position; + + // camera stuff + let position4d = vec4<f32>(position2d, instance.z_index, 1.0); + let position = camera * position4d; + + let tex_coords = vec2<f32>(model.position[0] + 0.5, 1.0 - (model.position[1] + 0.5)); + + out.clip_position = position; + out.texture_atlas_index = instance.texture_atlas_index; + out.texture_coordinates = tex_coords * instance.texture_size + instance.texture_coordinates; + return out; +} + +@group(1) @binding(0) +var t_diffuse: texture_2d<f32>; +@group(1) @binding(1) +var s_diffuse: sampler; + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { + return textureSample(t_diffuse, s_diffuse, in.texture_coordinates); +}
\ No newline at end of file diff --git a/alligator_render/src/camera.rs b/alligator_render/src/camera.rs new file mode 100644 index 0000000..ecece90 --- /dev/null +++ b/alligator_render/src/camera.rs @@ -0,0 +1,187 @@ +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/alligator_render/src/config.rs b/alligator_render/src/config.rs new file mode 100644 index 0000000..c73c357 --- /dev/null +++ b/alligator_render/src/config.rs @@ -0,0 +1,193 @@ +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. + 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: 0, // TODO this should probably be bigger + } + } +} + +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], + texture_format: wgpu::TextureFormat, + ) -> 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: texture_format, + width: self.default_width.get(), + height: self.default_height.get(), + alpha_mode, + present_mode, + } + } + + /// 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/alligator_render/src/instance.rs b/alligator_render/src/instance.rs new file mode 100644 index 0000000..2d1808f --- /dev/null +++ b/alligator_render/src/instance.rs @@ -0,0 +1,163 @@ +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], + /// The index of the texture atlas to use + pub texture_atlas_index: u32, + /// Rotation, in radians + pub rotation: f32, + /// z-index + pub z_index: f32, +} + +impl Default for Instance { + fn default() -> Self { + Self { + position: [0.0; 2], + size: [1.0; 2], + rotation: 0.0, + z_index: 0.0, + texture_coordinates: [0.0; 2], + texture_size: [1.0; 2], + texture_atlas_index: 0, + } + } +} + +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); + } + + // the instances must be sorted by z-index before being handed to the GPU + let mut sorted = self.instances.clone(); + sorted.sort_by(|a, b| a.z_index.total_cmp(&b.z_index)); + + queue.write_buffer( + &self.instance_buffer, + 0 as wgpu::BufferAddress, + bytemuck::cast_slice(&sorted), + ); + } +} diff --git a/alligator_render/src/lib.rs b/alligator_render/src/lib.rs new file mode 100644 index 0000000..f5403f2 --- /dev/null +++ b/alligator_render/src/lib.rs @@ -0,0 +1,22 @@ +#![feature(let_else)] +#![feature(nonzero_min_max)] +#![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::RenderWindowConfig; +pub use instance::Instance; +pub(crate) use instance::InstanceBuffer; +pub use renderer::Renderer; +pub use texture::ImageFormat; +pub(crate) use texture::TextureAtlas; +pub(crate) use vertex::Vertex; diff --git a/alligator_render/src/renderer.rs b/alligator_render/src/renderer.rs new file mode 100644 index 0000000..afcb92b --- /dev/null +++ b/alligator_render/src/renderer.rs @@ -0,0 +1,410 @@ +use std::{convert::TryInto, num::NonZeroU32}; + +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: () } + } +} + +#[derive(Debug, Error)] +pub enum NewRendererError { + #[error(transparent)] + NoGpu(#[from] NoGpuError), + #[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_supported_formats(adapter).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> { + #[cfg(feature = "profile-with-tracy")] + profiling::tracy_client::Client::start(); + profiling::register_thread!("main"); + + // 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::Backends::VULKAN); + + // 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 = 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(), + ..Default::default() + }, + None, + ) + .block_on() + .expect("there was no device with the selected features"); + + // configuration for the surface + let supported_present_modes = surface + .get_supported_present_modes(&adapter) + .into_boxed_slice(); + let supported_alpha_modes = surface + .get_supported_alpha_modes(&adapter) + .into_boxed_slice(); + let surface_config = config.to_surface_configuration( + &supported_present_modes, + &supported_alpha_modes, + surface.get_supported_formats(&adapter)[0], + ); + 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 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. + #[profiling::function] + 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 num_instances = self.instances.len(); + self.instances.fill_buffer(&self.device, &self.queue); + self.camera.refresh(&self.queue); + self.textures.fill_textures(&self.queue); + + { + 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 + pub fn run(mut self, f: &'static dyn Fn(&mut Self)) -> ! { + 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); + 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), + }; + profiling::finish_frame!(); + } + _ => {} + }); + } +} diff --git a/alligator_render/src/texture.rs b/alligator_render/src/texture.rs new file mode 100644 index 0000000..e343508 --- /dev/null +++ b/alligator_render/src/texture.rs @@ -0,0 +1,287 @@ +use std::error::Error; +use std::num::NonZeroU32; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use image::error::DecodingError; +use image::{EncodableLayout, GenericImage, ImageError, RgbaImage}; +use texture_packer::TexturePacker; +use texture_packer::{ + exporter::{ExportResult, ImageExporter}, + TexturePackerConfig, +}; +use thiserror::Error; + +static NEXT_TEXTURE_ID: AtomicUsize = AtomicUsize::new(0); + +/// The unique ID for a subtexture +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct TextureId(usize); + +impl TextureId { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self(NEXT_TEXTURE_ID.fetch_add(1, Ordering::Relaxed)) + } +} + +/// These are the formats supported by the renderer. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ImageFormat { + Bmp, + Ico, + Farbfeld, +} + +impl ImageFormat { + const fn format(self) -> image::ImageFormat { + match self { + Self::Bmp => image::ImageFormat::Bmp, + Self::Ico => image::ImageFormat::Ico, + Self::Farbfeld => image::ImageFormat::Farbfeld, + } + } +} + +/// The texture did not fit in the texture atlas +#[derive(Debug, Error)] +#[error("{:?}", .0)] +pub struct PackError(PackErrorInternal); + +// TODO this can be removed when a new texture packer is made +type PackErrorInternal = impl std::fmt::Debug; + +#[derive(Error, Debug)] +pub enum TextureError { + #[error("{:?}", .0)] + TextureTooLarge( + #[source] + #[from] + PackError, + ), + #[error("{}", .0)] + BadImage( + #[source] + #[from] + DecodingError, + ), // TODO don't export this + #[error("Unexpected Error (this is a bug in alligator_render): {}", .0)] + Unexpected(#[source] Box<dyn Error>), +} + +impl From<ImageError> for TextureError { + fn from(ie: ImageError) -> Self { + match ie { + ImageError::Decoding(de) => de.into(), + _ => Self::Unexpected(Box::new(ie)), + } + } +} + +/// 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 this Debug +// TODO make these resizable +pub struct TextureAtlas { + packer: TexturePacker<'static, image::RgbaImage, TextureId>, + diffuse_texture: wgpu::Texture, + diffuse_bind_group: wgpu::BindGroup, + image: RgbaImage, + width: u32, + height: u32, + changed: bool, +} + +macro_rules! texture_info { + ($name: ident, $prop: ident, $divisor: ident) => { + pub fn $name(&self, id: TextureId) -> Option<f32> { + let frame = self.texture_frame(id)?; + let property = frame.frame.$prop; + let value = property as f32 / self.$divisor as f32; + Some(value) + } + }; +} + +impl TextureAtlas { + /// Creates a new texture atlas, with the given size + // TODO why is this u32? + // 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::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + }); + + // 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 { + packer: TexturePacker::new_skyline(TexturePackerConfig { + max_width: width, + max_height: height, + allow_rotation: false, + trim: false, + texture_padding: 0, + ..Default::default() + }), + diffuse_texture, + diffuse_bind_group, + width, + height, + image: RgbaImage::from_raw( + width, + height, + vec![0; 4 * width as usize * height as usize], + ) + .unwrap(), + changed: true, + }, + texture_bind_group_layout, + ) + } + + /// get the bind group for the texture + pub(crate) const fn bind_group(&self) -> &wgpu::BindGroup { + &self.diffuse_bind_group + } + + /// Load a new subtexture from memory + // TODO support RGBA16 + pub fn load_from_memory( + &mut self, + buf: &[u8], + format: ImageFormat, + ) -> Result<TextureId, TextureError> { + let img = image::load_from_memory_with_format(buf, format.format())?.into_rgba8(); + let id = TextureId::new(); + self.packer.pack_own(id, img).map_err(PackError)?; + Ok(id) + } + + /// Get the frame for s particular subtexture + fn texture_frame(&self, id: TextureId) -> Option<&texture_packer::Frame<TextureId>> { + self.packer.get_frame(&id) + } + + texture_info!(texture_width, w, width); + texture_info!(texture_height, h, height); + texture_info!(texture_x, x, width); + texture_info!(texture_y, y, height); + + /// Fill the cached image + fn fill_image(&mut self) -> ExportResult<()> { + let atlas = { + profiling::scope!("export atlas"); + ImageExporter::export(&self.packer)? + }; + profiling::scope!("copy image"); + self.image + .copy_from(&atlas, 0, 0) + .expect("image cache is too small"); + Ok(()) + } + + /// Clear the texture atlas + pub fn clear(&mut self) { + self.packer = TexturePacker::new_skyline(TexturePackerConfig { + max_width: self.width, + max_height: self.height, + ..Default::default() + }); + } + + /// Fill the GPU texture atlas + #[profiling::function] + pub fn fill_textures(&mut self, queue: &wgpu::Queue) { + // saves time if nothing changed since the last time we did this + // FIXME This doesn't do much good once we get procedurally generated animation + // We'll have to create our own texture packer, with mutable subtextures, + // and more efficient algorithms. This'll also make frame times more consistent + if !self.changed { + return; + } + + let atlas_size = extent_3d(self.width, self.height); + + // put the packed texture into the base image + if let Err(e) = self.fill_image() { + log::error!("{}", e); + } + + // copy that to the gpu + 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: NonZeroU32::new(atlas_size.width * 4), + rows_per_image: NonZeroU32::new(atlas_size.height), + }, + atlas_size, + ); + + self.changed = false; + } +} diff --git a/alligator_render/src/vertex.rs b/alligator_render/src/vertex.rs new file mode 100644 index 0000000..570eec4 --- /dev/null +++ b/alligator_render/src/vertex.rs @@ -0,0 +1,39 @@ +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, + } + } +} |
