summaryrefslogtreecommitdiff
path: root/render
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
parentc31d51487082c6cf243ecd10da71a15a78d41add (diff)
Rename the subdirectories
Diffstat (limited to 'render')
-rw-r--r--render/Cargo.toml41
-rw-r--r--render/examples/black.rs31
-rw-r--r--render/examples/bmp.rs94
-rw-r--r--render/examples/bunnymark.rs158
-rw-r--r--render/examples/res/bunny.ffbin0 -> 8208 bytes
-rw-r--r--render/examples/res/gator.bmpbin0 -> 750054 bytes
-rw-r--r--render/examples/res/gator.ffbin0 -> 2000016 bytes
-rw-r--r--render/examples/res/ghost.icobin0 -> 67646 bytes
-rw-r--r--render/shaders/sprite.wgsl62
-rw-r--r--render/src/camera.rs187
-rw-r--r--render/src/config.rs198
-rw-r--r--render/src/instance.rs167
-rw-r--r--render/src/lib.rs21
-rw-r--r--render/src/renderer.rs444
-rw-r--r--render/src/texture.rs250
-rw-r--r--render/src/vertex.rs39
16 files changed, 1692 insertions, 0 deletions
diff --git a/render/Cargo.toml b/render/Cargo.toml
new file mode 100644
index 0000000..87536c1
--- /dev/null
+++ b/render/Cargo.toml
@@ -0,0 +1,41 @@
+[package]
+name = "alligator_render"
+version = "0.1.0"
+edition = "2021"
+rust-version = "1.65"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+alligator_resources = { path = "../resources" }
+
+bytemuck = { version = "1", features = ["derive"] }
+thiserror = "1"
+profiling = "1"
+wgpu = "0.15"
+winit = "0.28"
+image = "0.24"
+cgmath = "0.18"
+pollster = "0.2"
+log = "0.4"
+parking_lot = "0.12"
+texture_packer = { git="https://github.com/botahamec/piston_texture_packer", branch="u16" }
+
+tracy-client = { version = "0.15", optional = true }
+dhat = { version = "0.3", optional = true }
+
+[lib]
+crate-type = ["cdylib", "lib"]
+
+[features]
+dhat = ["dep:dhat"]
+profile-with-tracy = ["tracy-client", "profiling/profile-with-tracy"]
+
+[[example]]
+name = "black"
+
+[[example]]
+name = "bmp"
+
+[[example]]
+name = "bunnymark"
diff --git a/render/examples/black.rs b/render/examples/black.rs
new file mode 100644
index 0000000..655cbde
--- /dev/null
+++ b/render/examples/black.rs
@@ -0,0 +1,31 @@
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+use std::sync::Arc;
+
+use alligator_render::{RenderWindowConfig, Renderer};
+use alligator_resources::texture::{TextureManager, TextureManagerConfig};
+
+fn update(_renderer: &mut Renderer) {}
+
+fn main() {
+ let start = std::time::Instant::now();
+
+ // configure the render window
+ let render_config = RenderWindowConfig {
+ //vsync: false,
+ //mode: alligator_render::config::WindowMode::BorderlessFullscreen,
+ title: "Black Screen.exe",
+ ..Default::default()
+ };
+
+ let texture_config = TextureManagerConfig {
+ initial_capacity: 0,
+ max_size: 0,
+ };
+
+ let texture_manager = Arc::new(TextureManager::new(&texture_config));
+ let renderer = Renderer::new(&render_config, texture_manager).unwrap();
+ println!("Startup time: {:?}", start.elapsed());
+
+ renderer.run(update);
+}
diff --git a/render/examples/bmp.rs b/render/examples/bmp.rs
new file mode 100644
index 0000000..9d864d0
--- /dev/null
+++ b/render/examples/bmp.rs
@@ -0,0 +1,94 @@
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+use std::{num::NonZeroU32, sync::Arc};
+
+use alligator_render::{Instance, RenderWindowConfig, Renderer};
+use alligator_resources::texture::{ImageFormat, TextureManager, TextureManagerConfig};
+
+#[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 render_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 texture_config = TextureManagerConfig {
+ initial_capacity: 3,
+ max_size: 3_000_000,
+ };
+
+ let texture_manager = Arc::new(TextureManager::new(&texture_config));
+ let mut renderer = Renderer::new(&render_config, texture_manager.clone()).unwrap();
+
+ // render the alligator
+ let gator = include_bytes!("res/gator.ff");
+ let gator_id = texture_manager
+ .load_from_memory(gator, ImageFormat::Farbfeld)
+ .unwrap();
+ renderer.textures_mut().load_texture(gator_id).unwrap();
+ let gator_width = renderer.textures_mut().texture_width(gator_id).unwrap();
+ let gator_height = renderer.textures_mut().texture_height(gator_id).unwrap();
+ let gator_x = renderer.textures_mut().texture_x(gator_id).unwrap();
+ let gator_y = renderer.textures_mut().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 = texture_manager
+ .load_from_memory(icon, ImageFormat::Ico)
+ .unwrap();
+ renderer.textures_mut().load_texture(icon_id).unwrap();
+ let icon_width = renderer.textures_mut().texture_width(icon_id).unwrap();
+ let icon_height = renderer.textures_mut().texture_height(icon_id).unwrap();
+ let icon_x = renderer.textures_mut().texture_x(icon_id).unwrap();
+ let icon_y = renderer.textures_mut().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 = texture_manager
+ .load_from_memory(gator, ImageFormat::Bmp)
+ .unwrap();
+ let gator_width = renderer.textures_mut().texture_width(gator_id).unwrap();
+ let gator_height = renderer.textures_mut().texture_height(gator_id).unwrap();
+ let gator_x = renderer.textures_mut().texture_x(gator_id).unwrap();
+ let gator_y = renderer.textures_mut().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/render/examples/bunnymark.rs b/render/examples/bunnymark.rs
new file mode 100644
index 0000000..1579cf0
--- /dev/null
+++ b/render/examples/bunnymark.rs
@@ -0,0 +1,158 @@
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+use std::{num::NonZeroU32, sync::Arc, time::Instant};
+
+use alligator_render::{Instance, InstanceId, RenderWindowConfig, Renderer};
+use alligator_resources::texture::{ImageFormat, TextureId, TextureManager, TextureManagerConfig};
+
+fn xorshift_plus(seed: &mut [u64; 2]) -> u64 {
+ let mut t = seed[0];
+ let s = seed[1];
+
+ t ^= t << 23;
+ t ^= t >> 18;
+ t ^= s ^ (s >> 5);
+
+ seed[0] = s;
+ seed[1] = t;
+
+ t.wrapping_add(s)
+}
+
+#[derive(Debug)]
+struct State {
+ texture_id: TextureId,
+ bunnies: Vec<Bunny>,
+ previous_timestamp: Option<Instant>,
+ seed: [u64; 2],
+ stopped: bool,
+}
+
+impl State {
+ fn new(texture_id: TextureId) -> Self {
+ Self {
+ texture_id,
+ bunnies: Vec::with_capacity(10_000_000),
+ previous_timestamp: None,
+ seed: [0x0D15EA5E8BADF00D, 0xDECAFBADDEADBEAF],
+ stopped: false,
+ }
+ }
+
+ #[profiling::function]
+ fn update(&mut self, renderer: &mut Renderer) {
+ let Some(instant) = self.previous_timestamp else {
+ self.previous_timestamp = Some(Instant::now());
+ return;
+ };
+
+ let frame_time = instant.elapsed();
+ let fps = 1.0 / frame_time.as_secs_f32();
+
+ renderer.set_title(&format!(
+ "BunnyMark - {} bunnies - {} FPS",
+ self.bunnies.len(),
+ fps.round()
+ ));
+
+ if fps < 15.0 {
+ self.stopped = true;
+ }
+
+ self.previous_timestamp = Some(Instant::now());
+
+ if self.stopped {
+ return;
+ }
+
+ for bunny in self.bunnies.iter_mut() {
+ let instance = renderer
+ .instances_mut()
+ .get_instance_mut(bunny.instance_id)
+ .unwrap();
+
+ instance.position[0] += bunny.velocity_x;
+ instance.position[1] += bunny.velocity_y;
+
+ if !(-1.5..1.5).contains(&instance.position[0]) {
+ instance.position[0] = instance.position[0].clamp(-1.0, 1.0);
+ bunny.velocity_x = -bunny.velocity_x;
+ }
+
+ if !(-0.75..0.75).contains(&instance.position[1]) {
+ instance.position[1] = instance.position[1].clamp(-0.5, 0.5);
+ bunny.velocity_y *= -0.90;
+ }
+
+ bunny.velocity_y -= 0.005;
+ }
+
+ for _ in 0..=(fps as u64 * 50) {
+ let texture_x = renderer.textures_mut().texture_x(self.texture_id).unwrap();
+ let texture_y = renderer.textures_mut().texture_y(self.texture_id).unwrap();
+ let texture_height = renderer
+ .textures_mut()
+ .texture_height(self.texture_id)
+ .unwrap();
+ let texture_width = renderer
+ .textures_mut()
+ .texture_width(self.texture_id)
+ .unwrap();
+ let instance_id = renderer.instances_mut().push_instance(Instance {
+ texture_coordinates: [texture_x, texture_y],
+ texture_size: [texture_width, texture_height],
+ size: [0.08, 0.08],
+ position: [-1.5, 0.70],
+ ..Default::default()
+ });
+
+ let velocity_x = (xorshift_plus(&mut self.seed) % 1_000_000) as f32 / 25_000_000.0;
+ let velocity_y = (xorshift_plus(&mut self.seed) % 1_000_000) as f32 / 25_000_000.0;
+ self.bunnies.push(Bunny {
+ instance_id,
+ velocity_x,
+ velocity_y,
+ });
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+struct Bunny {
+ instance_id: InstanceId,
+ velocity_x: f32,
+ velocity_y: f32,
+}
+
+fn main() {
+ #[cfg(feature = "profile-with-tracy")]
+ profiling::tracy_client::Client::start();
+ profiling::register_thread!("main");
+
+ // configure the render window
+ let render_config = RenderWindowConfig {
+ title: "BunnyMark",
+ instance_capacity: 150_000,
+ default_width: NonZeroU32::new(1280).unwrap(),
+ default_height: NonZeroU32::new(720).unwrap(),
+ vsync: false,
+ low_power: false,
+ ..Default::default()
+ };
+
+ let texture_config = TextureManagerConfig {
+ initial_capacity: 1,
+ max_size: 10_000,
+ };
+
+ let bunny = include_bytes!("res/bunny.ff");
+ let texture_manager = Arc::new(TextureManager::new(&texture_config));
+ let mut renderer = Renderer::new(&render_config, texture_manager.clone()).unwrap();
+ let texture_id = texture_manager
+ .load_from_memory(bunny, ImageFormat::Farbfeld)
+ .unwrap();
+ renderer.textures_mut().load_texture(texture_id).unwrap();
+
+ let state = Box::leak(Box::new(State::new(texture_id)));
+ renderer.run(|r| state.update(r));
+}
diff --git a/render/examples/res/bunny.ff b/render/examples/res/bunny.ff
new file mode 100644
index 0000000..64c5a69
--- /dev/null
+++ b/render/examples/res/bunny.ff
Binary files differ
diff --git a/render/examples/res/gator.bmp b/render/examples/res/gator.bmp
new file mode 100644
index 0000000..e752b56
--- /dev/null
+++ b/render/examples/res/gator.bmp
Binary files differ
diff --git a/render/examples/res/gator.ff b/render/examples/res/gator.ff
new file mode 100644
index 0000000..aac1bcb
--- /dev/null
+++ b/render/examples/res/gator.ff
Binary files differ
diff --git a/render/examples/res/ghost.ico b/render/examples/res/ghost.ico
new file mode 100644
index 0000000..102de00
--- /dev/null
+++ b/render/examples/res/ghost.ico
Binary files differ
diff --git a/render/shaders/sprite.wgsl b/render/shaders/sprite.wgsl
new file mode 100644
index 0000000..276a8ef
--- /dev/null
+++ b/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_mat = mat2x2<f32>(a, b);
+ let rotated = rotation_mat * 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/render/src/camera.rs b/render/src/camera.rs
new file mode 100644
index 0000000..ecece90
--- /dev/null
+++ b/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/render/src/config.rs b/render/src/config.rs
new file mode 100644
index 0000000..c3cc6b6
--- /dev/null
+++ b/render/src/config.rs
@@ -0,0 +1,198 @@
+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
new file mode 100644
index 0000000..e346cae
--- /dev/null
+++ b/render/src/instance.rs
@@ -0,0 +1,167 @@
+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 sorted = {
+ profiling::scope!("depth sorting");
+ let mut sorted = self.instances.clone();
+ sorted.sort_by(|a, b| a.z_index.total_cmp(&b.z_index));
+ sorted
+ };
+
+ queue.write_buffer(
+ &self.instance_buffer,
+ 0 as wgpu::BufferAddress,
+ bytemuck::cast_slice(&sorted),
+ );
+ }
+}
diff --git a/render/src/lib.rs b/render/src/lib.rs
new file mode 100644
index 0000000..0d76cc8
--- /dev/null
+++ b/render/src/lib.rs
@@ -0,0 +1,21 @@
+#![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::*;
+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
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!();
+ }
+ _ => {}
+ });
+ }
+}
diff --git a/render/src/texture.rs b/render/src/texture.rs
new file mode 100644
index 0000000..76e77a8
--- /dev/null
+++ b/render/src/texture.rs
@@ -0,0 +1,250 @@
+use std::error::Error;
+use std::num::NonZeroU32;
+use std::sync::Arc;
+
+use alligator_resources::texture::{LoadError, Rgba16Texture, TextureId, TextureManager};
+use image::{EncodableLayout, GenericImage, RgbaImage};
+use texture_packer::TexturePacker;
+use texture_packer::{
+ exporter::{ExportResult, ImageExporter},
+ TexturePackerConfig,
+};
+use thiserror::Error;
+
+/// 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(#[from] PackError),
+ #[error("{}", .0)]
+ BadImage(#[from] LoadError),
+ #[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 this Debug
+// TODO make these resizable
+pub struct TextureAtlas {
+ textures: Arc<TextureManager>,
+ packer: TexturePacker<'static, Rgba16Texture, 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(&mut self, id: TextureId) -> Result<f32, TextureError> {
+ let frame = match self.texture_frame(id) {
+ Some(frame) => frame,
+ None => {
+ self.load_texture(id)?;
+ self.texture_frame(id).unwrap()
+ }
+ };
+ let property = frame.frame.$prop;
+ let value = property as f32 / self.$divisor as f32;
+ Ok(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,
+ textures: Arc<TextureManager>,
+ 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 {
+ textures,
+ 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
+ pub fn load_texture(&mut self, id: TextureId) -> Result<TextureId, TextureError> {
+ self.changed = true;
+ let img = self.textures.load_texture(id)?;
+ 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, and give it a new size
+ pub fn clear(&mut self, width: u32, height: u32) {
+ self.changed = true;
+ self.width = width;
+ self.height = height;
+ self.packer = TexturePacker::new_skyline(TexturePackerConfig {
+ max_width: self.width,
+ max_height: self.height,
+ ..Default::default()
+ });
+ }
+
+ /// Fill the GPU texture atlas
+ #[profiling::function]
+ pub(crate) 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/render/src/vertex.rs b/render/src/vertex.rs
new file mode 100644
index 0000000..570eec4
--- /dev/null
+++ b/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,
+ }
+ }
+}