From fb63d72018f51aa2899c2f88087597b44490463b Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Fri, 1 Dec 2023 18:49:08 +1300 Subject: [PATCH] feat: slab tutorial examples (#60) * feat: slab tutorial examples * remove example-debugging from workspace * github workflow ensure shaders are checked in * skip testing for now --- .github/workflows/push.yaml | 8 +- Cargo.toml | 11 +- NOTES.md | 2 + crates/example/Cargo.toml | 2 +- crates/renderling-shader/Cargo.toml | 4 +- crates/renderling-shader/src/array.rs | 19 +- crates/renderling-shader/src/id.rs | 15 +- crates/renderling-shader/src/lib.rs | 13 +- crates/renderling-shader/src/slab.rs | 116 ++- crates/renderling-shader/src/stage.rs | 51 +- crates/renderling-shader/src/tutorial.rs | 108 +++ crates/renderling/Cargo.toml | 26 +- crates/renderling/src/frame.rs | 33 +- crates/renderling/src/lib.rs | 13 +- crates/renderling/src/linkage/mod.rs | 2 +- .../renderling/src/linkage/skybox-vertex.spv | Bin 3324 -> 3324 bytes .../src/linkage/stage-main_vertex_scene.spv | Bin 43900 -> 43900 bytes .../src/linkage/stage-new_stage_vertex.spv | Bin 60988 -> 60124 bytes .../src/linkage/stage-stage_fragment.spv | Bin 0 -> 131656 bytes .../tutorial-implicit_isosceles_vertex.spv | Bin 0 -> 744 bytes .../linkage/tutorial-passthru_fragment.spv | Bin 0 -> 316 bytes .../linkage/tutorial-slabbed_render_unit.spv | Bin 0 -> 56132 bytes .../src/linkage/tutorial-slabbed_vertices.spv | Bin 0 -> 16148 bytes .../tutorial-slabbed_vertices_no_instance.spv | Bin 0 -> 14632 bytes crates/renderling/src/linkage/ui-vertex.spv | Bin 2368 -> 2368 bytes crates/renderling/src/scene.rs | 32 +- crates/renderling/src/skybox.rs | 35 +- crates/renderling/src/slab.rs | 206 ++-- crates/renderling/src/stage.rs | 652 ++++++++++++- crates/renderling/src/texture.rs | 14 +- crates/renderling/src/tutorial.rs | 882 ++++++++++++++++++ crates/renderling/src/ui.rs | 3 +- crates/sandbox/src/main.rs | 199 ++-- shaders/shader-crate/Cargo.toml | 2 +- shaders/src/main.rs | 2 +- .../tutorial/implicit_isosceles_triangle.png | Bin 0 -> 779 bytes .../tutorial/slabbed_isosceles_triangle.png | Bin 0 -> 3812 bytes ...slabbed_isosceles_triangle_no_instance.png | Bin 0 -> 2763 bytes test_img/tutorial/slabbed_render_unit.png | Bin 0 -> 3812 bytes .../tutorial/slabbed_render_unit_camera.png | Bin 0 -> 3630 bytes 40 files changed, 2191 insertions(+), 259 deletions(-) create mode 100644 crates/renderling-shader/src/tutorial.rs create mode 100644 crates/renderling/src/linkage/stage-stage_fragment.spv create mode 100644 crates/renderling/src/linkage/tutorial-implicit_isosceles_vertex.spv create mode 100644 crates/renderling/src/linkage/tutorial-passthru_fragment.spv create mode 100644 crates/renderling/src/linkage/tutorial-slabbed_render_unit.spv create mode 100644 crates/renderling/src/linkage/tutorial-slabbed_vertices.spv create mode 100644 crates/renderling/src/linkage/tutorial-slabbed_vertices_no_instance.spv create mode 100644 crates/renderling/src/tutorial.rs create mode 100644 test_img/tutorial/implicit_isosceles_triangle.png create mode 100644 test_img/tutorial/slabbed_isosceles_triangle.png create mode 100644 test_img/tutorial/slabbed_isosceles_triangle_no_instance.png create mode 100644 test_img/tutorial/slabbed_render_unit.png create mode 100644 test_img/tutorial/slabbed_render_unit_camera.png diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 41f75f4c..0d2a8db3 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -3,14 +3,18 @@ on: [push] name: push jobs: - renderling-build: + renderling-build-shaders: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: stable - - run: cd shaders && cargo run --release && cd .. && cargo build + - run: | + # ensure the shader binaries were properly checked in + rm -rf crates/renderling/src/linkage/*.spv + cd shaders && cargo run --release && cd .. + git diff --exit-code --no-ext-diff crates/renderling/src/linkage #renderling-test: # runs-on: [ubuntu-latest, gpu] diff --git a/Cargo.toml b/Cargo.toml index f613c64f..f124a478 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,10 @@ resolver = "2" [workspace.dependencies] bytemuck = { version = "1.13.0", features = ["derive"] } +futures-lite = "1.13" gltf = { git = 'https://github.com/gltf-rs/gltf.git', features = ["KHR_lights_punctual", "KHR_materials_unlit", "KHR_materials_emissive_strength", "extras"] } -image = "^0.24" -log = "^0.4" -glam = "^0.24" -winit = { version = "^0.27" } -wgpu = { version = "^0.17" } +image = "0.24" +log = "0.4" +glam = "0.24.2" +winit = { version = "0.27" } +wgpu = { version = "0.17" } diff --git a/NOTES.md b/NOTES.md index 419d1e22..828e181c 100644 --- a/NOTES.md +++ b/NOTES.md @@ -16,6 +16,8 @@ Just pro-cons on tech choices and little things I don't want to forget whil impl * it's Rust - using cargo and Rust module system - expressions! + - type checking! + - editor tooling! ## cons / limititions diff --git a/crates/example/Cargo.toml b/crates/example/Cargo.toml index 0d1daf86..e471f4ff 100644 --- a/crates/example/Cargo.toml +++ b/crates/example/Cargo.toml @@ -12,7 +12,7 @@ renderling-gpui = { path = "../renderling-gpui" } anyhow = "^1.0" clap = { version = "^4.3", features = ["derive"] } env_logger = "0.10.0" -futures-lite = "^1.13" +futures-lite = {workspace=true} icosahedron = "^0.1" instant = "^0.1" loading-bytes = { path = "../loading-bytes" } diff --git a/crates/renderling-shader/Cargo.toml b/crates/renderling-shader/Cargo.toml index b5e4e941..777e9ac0 100644 --- a/crates/renderling-shader/Cargo.toml +++ b/crates/renderling-shader/Cargo.toml @@ -12,7 +12,7 @@ default = [] [dependencies] bytemuck = { workspace = true } renderling-derive = { version = "0.1.0", path = "../renderling-derive" } -spirv-std = "^0.9" +spirv-std = "0.9" [target.'cfg(not(target_arch = "spirv"))'.dependencies] glam = { workspace = true, features = ["bytemuck"] } @@ -21,7 +21,7 @@ glam = { workspace = true, features = ["bytemuck"] } glam = { workspace = true, features = ["debug-glam-assert", "bytemuck"] } [target.'cfg(target_arch = "spirv")'.dependencies] -glam = { version = "^0.24", default-features = false, features = ["libm", "bytemuck"] } +glam = { version = "0.24.2", default-features = false, features = ["libm", "bytemuck"] } [dev-dependencies] image = { workspace = true } diff --git a/crates/renderling-shader/src/array.rs b/crates/renderling-shader/src/array.rs index 17911185..73c57dac 100644 --- a/crates/renderling-shader/src/array.rs +++ b/crates/renderling-shader/src/array.rs @@ -34,7 +34,7 @@ impl Slabbed for Array { } fn read_slab(&mut self, index: usize, slab: &[u32]) -> usize { - if index + Self::slab_size() >= slab.len() { + if index + Self::slab_size() > slab.len() { index } else { let index = self.index.read_slab(index, slab); @@ -44,7 +44,7 @@ impl Slabbed for Array { } fn write_slab(&self, index: usize, slab: &mut [u32]) -> usize { - if index + Self::slab_size() >= slab.len() { + if index + Self::slab_size() > slab.len() { index } else { let index = self.index.write_slab(index, slab); @@ -72,6 +72,7 @@ impl Array { _phantom: PhantomData, } } + pub fn len(&self) -> usize { self.len as usize } @@ -88,19 +89,7 @@ impl Array { if index >= self.len() { Id::NONE } else { - Id::new(self.index + index as u32) + Id::new(self.index + (T::slab_size() * index) as u32) } } } - -impl Array { - fn slab_size() -> usize { - 2 - } - - pub fn read(&self, item: &mut T, item_index: usize, slab: &[u32]) { - let size = T::slab_size(); - let start = self.index as usize + size * item_index; - let _ = item.read_slab(start, slab); - } -} diff --git a/crates/renderling-shader/src/id.rs b/crates/renderling-shader/src/id.rs index c36dff76..f7a2d5f4 100644 --- a/crates/renderling-shader/src/id.rs +++ b/crates/renderling-shader/src/id.rs @@ -69,11 +69,11 @@ impl Default for Id { } } -#[cfg(not(target_arch = "spirv"))] -impl std::fmt::Debug for Id { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple(&format!("Id<{}>", std::any::type_name::())) - .field(&self.0) +impl core::fmt::Debug for Id { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Id") + .field("type", &core::any::type_name::()) + .field("index", &self.0) .finish() } } @@ -122,6 +122,11 @@ impl Id { self.0 as usize } + /// The raw u32 value of this id. + pub fn inner(&self) -> u32 { + self.0 + } + pub fn is_none(&self) -> bool { *self == Id::NONE } diff --git a/crates/renderling-shader/src/lib.rs b/crates/renderling-shader/src/lib.rs index 9285abf1..b567b566 100644 --- a/crates/renderling-shader/src/lib.rs +++ b/crates/renderling-shader/src/lib.rs @@ -4,10 +4,10 @@ use core::ops::Mul; -use glam::{Vec4Swizzles, Vec3, Quat}; -use spirv_std::num_traits::Zero; +use glam::{Quat, Vec3, Vec4Swizzles}; #[cfg(target_arch = "spirv")] use spirv_std::num_traits::Float; +use spirv_std::num_traits::Zero; pub mod array; pub mod bits; @@ -16,10 +16,11 @@ pub mod debug; pub mod id; pub mod math; pub mod pbr; -pub mod stage; pub mod skybox; pub mod slab; +pub mod stage; pub mod tonemapping; +pub mod tutorial; pub mod ui; /// Additional methods for vector types. @@ -138,11 +139,7 @@ impl IsMatrix for glam::Mat4 { return srt_id(); } - let det_sign = if det >= 0.0 { - 1.0 - } else { - -1.0 - }; + let det_sign = if det >= 0.0 { 1.0 } else { -1.0 }; let scale = glam::Vec3::new( self.x_axis.length() * det_sign, diff --git a/crates/renderling-shader/src/slab.rs b/crates/renderling-shader/src/slab.rs index bf9a918a..599094c2 100644 --- a/crates/renderling-shader/src/slab.rs +++ b/crates/renderling-shader/src/slab.rs @@ -121,9 +121,6 @@ impl Slabbed for [T; N] { impl Slabbed for glam::Mat4 { fn read_slab(&mut self, index: usize, slab: &[u32]) -> usize { - if slab.len() < index + 16 { - return index; - } let Self { x_axis, y_axis, @@ -218,11 +215,10 @@ impl Slabbed for glam::Vec4 { if slab.len() < index + 4 { return index; } - let Self { x, y, z, w } = self; - let index = x.read_slab(index, slab); - let index = y.read_slab(index, slab); - let index = z.read_slab(index, slab); - w.read_slab(index, slab) + let index = self.x.read_slab(index, slab); + let index = self.y.read_slab(index, slab); + let index = self.z.read_slab(index, slab); + self.w.read_slab(index, slab) } fn write_slab(&self, index: usize, slab: &mut [u32]) -> usize { @@ -357,9 +353,27 @@ impl Slabbed for PhantomData { } pub trait Slab { + /// Return the number of u32 elements in the slab. + fn len(&self) -> usize; + + /// Returns whether the slab may contain the value with the given id. + fn contains(&self, id: Id) -> bool { + id.index() + T::slab_size() <= self.len() + } + /// Read the type from the slab using the Id as the index. fn read(&self, id: Id) -> T; + #[cfg(not(target_arch = "spirv"))] + fn read_vec(&self, array: crate::array::Array) -> Vec { + let mut vec = Vec::with_capacity(array.len()); + for i in 0..array.len() { + let id = array.at(i); + vec.push(self.read(id)); + } + vec + } + /// Write the type into the slab at the index. /// /// Return the next index, or the same index if writing would overlap the slab. @@ -372,6 +386,10 @@ pub trait Slab { } impl Slab for [u32] { + fn len(&self) -> usize { + self.len() + } + fn read(&self, id: Id) -> T { let mut t = T::default(); let _ = t.read_slab(id.index(), self); @@ -391,8 +409,31 @@ impl Slab for [u32] { } } +#[cfg(not(target_arch = "spirv"))] +impl Slab for Vec { + fn len(&self) -> usize { + self.len() + } + + fn read(&self, id: Id) -> T { + self.as_slice().read(id) + } + + fn write(&mut self, t: &T, index: usize) -> usize { + self.as_mut_slice().write(t, index) + } + + fn write_slice(&mut self, t: &[T], index: usize) -> usize { + self.as_mut_slice().write_slice(t, index) + } +} + #[cfg(test)] mod test { + use glam::Vec4; + + use crate::{array::Array, stage::Vertex}; + use super::*; #[test] @@ -402,5 +443,64 @@ mod test { slab.write(&666, 1); let t = slab.read(Id::<[u32; 2]>::new(0)); assert_eq!([42, 666], t); + let t: Vec = slab.read_vec(Array::new(0, 2)); + assert_eq!([42, 666], t[..]); + slab.write_slice(&[1, 2, 3, 4], 2); + let t: Vec = slab.read_vec(Array::new(2, 4)); + assert_eq!([1, 2, 3, 4], t[..]); + slab.write_slice(&[[1.0, 2.0, 3.0, 4.0], [5.5, 6.5, 7.5, 8.5]], 0); + + let arr = Array::<[f32; 4]>::new(0, 2); + assert_eq!(Id::new(0), arr.at(0)); + assert_eq!(Id::new(4), arr.at(1)); + assert_eq!([1.0, 2.0, 3.0, 4.0], slab.read(arr.at(0))); + assert_eq!([5.5, 6.5, 7.5, 8.5], slab.read(arr.at(1))); + + let geometry = vec![ + Vertex { + position: Vec4::new(0.5, -0.5, 0.0, 1.0), + color: Vec4::new(1.0, 0.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(0.0, 0.5, 0.0, 1.0), + color: Vec4::new(0.0, 1.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(-0.5, -0.5, 0.0, 1.0), + color: Vec4::new(0.0, 0.0, 1.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(-1.0, 1.0, 0.0, 1.0), + color: Vec4::new(1.0, 0.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(-1.0, 0.0, 0.0, 1.0), + color: Vec4::new(0.0, 1.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(0.0, 1.0, 0.0, 1.0), + color: Vec4::new(0.0, 0.0, 1.0, 1.0), + ..Default::default() + }, + ]; + let geometry_slab_size = Vertex::slab_size() * geometry.len(); + let mut slab = vec![0u32; geometry_slab_size + Array::::slab_size()]; + let index = 0usize; + let vertices = Array::::new(index as u32, geometry.len() as u32); + let index = slab.write_slice(&geometry, index); + assert_eq!(geometry_slab_size, index); + let vertices_id = Id::>::from(index); + let index = slab.write(&vertices, index); + assert_eq!(geometry_slab_size + Array::::slab_size(), index); + assert_eq!(Vertex::slab_size() * 6, vertices_id.index()); + assert!(slab.contains(vertices_id),); + + let array = slab.read(vertices_id); + assert_eq!(vertices, array); } } diff --git a/crates/renderling-shader/src/stage.rs b/crates/renderling-shader/src/stage.rs index 8f884387..c7c02815 100644 --- a/crates/renderling-shader/src/stage.rs +++ b/crates/renderling-shader/src/stage.rs @@ -1,4 +1,4 @@ -//! Types used to store and update an entire 3d scene on the GPU. +//! Types used to store and update an entire scene on the GPU. //! //! This is roughly what the [vulkan guide](https://vkguide.dev/docs/gpudriven) //! calls "gpu driven rendering". @@ -897,10 +897,8 @@ pub struct StageLegend { /// transformations. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Default, Clone, Copy, PartialEq, Slabbed)] +#[derive(Clone, Copy, PartialEq, Slabbed)] pub struct RenderUnit { - // Points to an index in the `RenderUnit` slab. - pub id: Id, // Points to an array of `Vertex` in the stage's slab. pub vertices: Array, // Points to a `PbrMaterial` in the stage's slab. @@ -913,14 +911,26 @@ pub struct RenderUnit { pub scale: Vec3, } +impl Default for RenderUnit { + fn default() -> Self { + Self { + vertices: Default::default(), + material: Default::default(), + camera: Default::default(), + position: Vec3::ZERO, + rotation: Quat::IDENTITY, + scale: Vec3::ONE, + } + } +} + #[spirv(vertex)] pub fn new_stage_vertex( // Which render unit are we rendering #[spirv(instance_index)] instance_index: u32, // Which vertex within the render unit are we rendering #[spirv(vertex_index)] vertex_index: u32, - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] unit_slab: &[u32], - #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] stage_slab: &[u32], + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], #[spirv(flat)] out_camera: &mut u32, #[spirv(flat)] out_material: &mut u32, out_color: &mut Vec4, @@ -934,8 +944,8 @@ pub fn new_stage_vertex( #[spirv(position)] gl_pos: &mut Vec4, ) { let unit_id: Id = Id::from(instance_index); - let unit = unit_slab.read(unit_id); - let vertex = stage_slab.read(unit.vertices.at(vertex_index as usize)); + let unit = slab.read(unit_id); + let vertex = slab.read(unit.vertices.at(vertex_index as usize)); let model_matrix = Mat4::from_scale_rotation_translation(unit.scale, unit.rotation, unit.position); *out_material = unit.material.into(); @@ -957,30 +967,31 @@ pub fn new_stage_vertex( *out_norm = normal_w; let view_pos = model_matrix * vertex.position.xyz().extend(1.0); *out_pos = view_pos.xyz(); - let camera = stage_slab.read(unit.camera); + let camera = slab.read(unit.camera); *out_camera = unit.camera.into(); *gl_pos = camera.projection * camera.view * view_pos; } #[allow(clippy::too_many_arguments)] +#[spirv(fragment)] /// Scene fragment shader. pub fn stage_fragment( - atlas: &Image2d, - atlas_sampler: &Sampler, + #[spirv(descriptor_set = 1, binding = 0)] atlas: &Image2d, + #[spirv(descriptor_set = 1, binding = 1)] atlas_sampler: &Sampler, - irradiance: &Cubemap, - irradiance_sampler: &Sampler, + #[spirv(descriptor_set = 1, binding = 2)] irradiance: &Cubemap, + #[spirv(descriptor_set = 1, binding = 3)] irradiance_sampler: &Sampler, - prefiltered: &Cubemap, - prefiltered_sampler: &Sampler, + #[spirv(descriptor_set = 1, binding = 4)] prefiltered: &Cubemap, + #[spirv(descriptor_set = 1, binding = 5)] prefiltered_sampler: &Sampler, - brdf: &Image2d, - brdf_sampler: &Sampler, + #[spirv(descriptor_set = 1, binding = 6)] brdf: &Image2d, + #[spirv(descriptor_set = 1, binding = 7)] brdf_sampler: &Sampler, - slab: &[u32], + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], - in_camera: u32, - in_material: u32, + #[spirv(flat)] in_camera: u32, + #[spirv(flat)] in_material: u32, in_color: Vec4, in_uv0: Vec2, in_uv1: Vec2, diff --git a/crates/renderling-shader/src/tutorial.rs b/crates/renderling-shader/src/tutorial.rs new file mode 100644 index 00000000..5bbe64cd --- /dev/null +++ b/crates/renderling-shader/src/tutorial.rs @@ -0,0 +1,108 @@ +//! Shaders used in the intro tutorial. +use glam::{Mat4, Vec4, Vec4Swizzles}; +use spirv_std::spirv; + +use crate::{ + array::Array, + id::Id, + slab::{Slab, Slabbed}, + stage::{RenderUnit, Vertex}, +}; + +/// Simple fragment shader that writes the input color to the output color. +#[spirv(fragment)] +pub fn passthru_fragment(in_color: Vec4, output: &mut Vec4) { + *output = in_color; +} + +fn implicit_isosceles_triangle(vertex_index: u32) -> Vec4 { + let x = (1 - vertex_index as i32) as f32 * 0.5; + let y = ((vertex_index & 1) as f32 * 2.0 - 1.0) * 0.5; + Vec4::new(x, y, 0.0, 1.0) +} + +/// Simple vertex shader with an implicit isosceles triangle. +#[spirv(vertex)] +pub fn implicit_isosceles_vertex( + // Which vertex within the render unit are we rendering + #[spirv(vertex_index)] vertex_index: u32, + + //#[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], + out_color: &mut Vec4, + #[spirv(position)] clip_pos: &mut Vec4, +) { + let pos = implicit_isosceles_triangle(vertex_index); + *out_color = Vec4::new(1.0, 0.0, 0.0, 1.0); + *clip_pos = pos; +} + +/// This shader uses the vertex index as a slab [`Id`]. The [`Id`] is used to +/// read the vertex from the slab. The vertex's position and color are written +/// to the output. +#[spirv(vertex)] +pub fn slabbed_vertices_no_instance( + // Which vertex within the render unit are we rendering + #[spirv(vertex_index)] vertex_index: u32, + + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], + + out_color: &mut Vec4, + #[spirv(position)] clip_pos: &mut Vec4, +) { + let vertex_id = Id::::from(vertex_index as usize * Vertex::slab_size()); + let vertex = slab.read(vertex_id); + *clip_pos = vertex.position; + *out_color = vertex.color; +} + +/// This shader uses the `instance_index` as a slab [`Id`]. +/// The `instance_index` is the [`Id`] of an [`Array`] of [`Vertex`]s. The +/// `vertex_index` is the index of a [`Vertex`] within the [`Array`]. +#[spirv(vertex)] +pub fn slabbed_vertices( + // Id of the array of vertices we are rendering + #[spirv(instance_index)] instance_index: u32, + // Which vertex within the render unit are we rendering + #[spirv(vertex_index)] vertex_index: u32, + + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], + + out_color: &mut Vec4, + #[spirv(position)] clip_pos: &mut Vec4, +) { + let array_id = Id::>::from(instance_index); + let array = slab.read(array_id); + let vertex_id = array.at(vertex_index as usize); + let vertex = slab.read(vertex_id); + *clip_pos = vertex.position; + *out_color = vertex.color; +} + +/// This shader uses the `instance_index` as a slab [`Id`]. +/// The `instance_index` is the [`Id`] of a [`RenderUnit`]. +/// The [`RenderUnit`] contains an [`Array`] of [`Vertex`]s +/// as its mesh, the [`Id`]s of a [`Material`] and [`Camera`], +/// and TRS transforms. +/// The `vertex_index` is the index of a [`Vertex`] within the +/// [`RenderUnit`]'s `vertices` [`Array`]. +#[spirv(vertex)] +pub fn slabbed_render_unit( + // Id of the array of vertices we are rendering + #[spirv(instance_index)] instance_index: u32, + // Which vertex within the render unit are we rendering + #[spirv(vertex_index)] vertex_index: u32, + + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], + + out_color: &mut Vec4, + #[spirv(position)] clip_pos: &mut Vec4, +) { + let unit_id = Id::::from(instance_index); + let unit = slab.read(unit_id); + let vertex_id = unit.vertices.at(vertex_index as usize); + let vertex = slab.read(vertex_id); + let camera = slab.read(unit.camera); + let model = Mat4::from_scale_rotation_translation(unit.scale, unit.rotation, unit.position); + *clip_pos = camera.projection * camera.view * model * vertex.position.xyz().extend(1.0); + *out_color = vertex.color; +} diff --git a/crates/renderling/Cargo.toml b/crates/renderling/Cargo.toml index dd362edf..949a06e6 100644 --- a/crates/renderling/Cargo.toml +++ b/crates/renderling/Cargo.toml @@ -17,19 +17,19 @@ text = ["ab_glyph", "glyph_brush"] wasm = ["wgpu/fragile-send-sync-non-atomic-wasm"] [dependencies] -ab_glyph = { version = "^0.2", optional = true } -any_vec = "^0.13" -async-channel = "^1.8" -crunch = "^0.5" -futures-lite = "^1.12" -glyph_brush = { version = "^0.7", optional = true } -half = "^2.3" -moongraph = { version = "^0.3.4", features = ["dot"] } -raw-window-handle = { version = "^0.5", optional = true } +ab_glyph = { version = "0.2", optional = true } +any_vec = "0.13" +async-channel = "1.8" +crunch = "0.5" +futures-lite = {workspace=true} +glyph_brush = { version = "0.7", optional = true } +half = "2.3" +moongraph = { version = "0.3.5", features = ["dot"] } +raw-window-handle = { version = "0.5", optional = true } renderling-shader = { path = "../renderling-shader" } -rustc-hash = "^1.1" +rustc-hash = "1.1" send_wrapper = "0.6" -snafu = "^0.7" +snafu = "0.7" image = { workspace = true, features = ["hdr"] } gltf = { workspace = true, optional = true } @@ -45,7 +45,7 @@ features = ["gltf", "text", "raw-window-handle", "winit"] [dev-dependencies] ctor = "0.2.2" env_logger = "0.10.0" -icosahedron = "^0.1" +icosahedron = "0.1" img-diff = { path = "../img-diff" } -naga = { version = "^0.13", features = ["spv-in", "wgsl-out", "wgsl-in", "msl-out"] } +naga = { version = "0.13", features = ["spv-in", "wgsl-out", "wgsl-in", "msl-out"] } pretty_assertions = "1.4.0" diff --git a/crates/renderling/src/frame.rs b/crates/renderling/src/frame.rs index f95ed089..d48c7d5f 100644 --- a/crates/renderling/src/frame.rs +++ b/crates/renderling/src/frame.rs @@ -1,4 +1,7 @@ //! Frame creation and clearing. +//! +//! Contains graph nodes for creating and clearing frames, as well as a +//! `PostRenderBuffer` resource that holds a copy of the last frame's buffer. use std::{ops::Deref, sync::Arc}; use moongraph::*; @@ -8,7 +11,7 @@ use crate::{ RenderTarget, ScreenSize, WgpuStateError, }; -fn default_frame_texture_view( +pub fn default_frame_texture_view( frame_texture: &wgpu::Texture, ) -> (wgpu::TextureView, wgpu::TextureFormat) { let format = frame_texture.format().add_srgb_suffix(); @@ -95,7 +98,8 @@ pub fn conduct_clear_pass( queue.submit(std::iter::once(encoder.finish())); } -/// Conduct a clear pass on the global frame and depth textures. +/// Render graph node to conduct a clear pass on the global frame and depth +/// textures. pub fn clear_frame_and_depth( (device, queue, frame_view, depth, color): ( View, @@ -160,18 +164,19 @@ pub struct PostRenderBufferCreate { frame: View, } -impl PostRenderBufferCreate { - /// Copies the current frame into a `PostRenderBuffer` resource. - /// - /// If rendering to a window surface, this should be called after rendering, - /// before presentation. - pub fn create(self) -> Result<(PostRenderBuffer,), WgpuStateError> { - let ScreenSize { width, height } = *self.size; - let copied_texture_buffer = - self.frame - .copy_to_buffer(&self.device, &self.queue, width, height); - Ok((PostRenderBuffer(copied_texture_buffer),)) - } +/// Copies the current frame into a `PostRenderBuffer` resource. +/// +/// If rendering to a window surface, this should be called after rendering, +/// before presentation. +pub fn copy_frame_to_post( + create: PostRenderBufferCreate, +) -> Result<(PostRenderBuffer,), WgpuStateError> { + let ScreenSize { width, height } = *create.size; + let copied_texture_buffer = + create + .frame + .copy_to_buffer(&create.device, &create.queue, width, height); + Ok((PostRenderBuffer(copied_texture_buffer),)) } /// Consume and present the screen frame to the screen. diff --git a/crates/renderling/src/lib.rs b/crates/renderling/src/lib.rs index d5b6abe3..6bffba7c 100644 --- a/crates/renderling/src/lib.rs +++ b/crates/renderling/src/lib.rs @@ -53,13 +53,14 @@ pub mod math; pub mod mesh; mod renderer; mod scene; -mod slab; mod skybox; +mod slab; mod stage; mod state; #[cfg(feature = "text")] mod text; mod texture; +mod tutorial; mod ui; mod uniform; @@ -69,8 +70,8 @@ pub use camera::*; pub use hdr::*; pub use renderer::*; pub use scene::*; -pub use slab::*; pub use skybox::*; +pub use slab::*; pub use stage::*; pub use state::*; #[cfg(feature = "text")] @@ -96,6 +97,10 @@ pub mod graph { pub use graph::{graph, Graph, GraphError, Move, View, ViewMut}; pub use renderling_shader::id::{Id, ID_NONE}; +pub mod shader { + //! Re-exports of [`renderling_shader`]. + pub use renderling_shader::*; +} /// Set up the render graph, including: /// * 3d scene objects @@ -173,7 +178,7 @@ pub fn setup_render_graph( scene_render < skybox_render < bloom_filter - < scene_tonemapping + < tonemapping < clear_depth < ui_scene_render )) @@ -181,7 +186,7 @@ pub fn setup_render_graph( // post-render subgraph r.graph.add_subgraph(if with_screen_capture { - let copy_frame_to_post = crate::frame::PostRenderBufferCreate::create; + use crate::frame::copy_frame_to_post; graph!(copy_frame_to_post < present) } else { graph!(present) diff --git a/crates/renderling/src/linkage/mod.rs b/crates/renderling/src/linkage/mod.rs index bc5c4bd9..d5bbe760 100644 --- a/crates/renderling/src/linkage/mod.rs +++ b/crates/renderling/src/linkage/mod.rs @@ -27,7 +27,7 @@ mod test { #[test] // Ensure that the shaders can be converted to WGSL. // This is necessary for WASM using WebGPU, because WebGPU only accepts - // WGSL as a shading language.:w + // WGSL as a shading language. fn validate_shaders() { fn validate_src(path: &std::path::PathBuf) -> Result<(), SrcError> { let bytes = std::fs::read(path).context(ReadSnafu)?; diff --git a/crates/renderling/src/linkage/skybox-vertex.spv b/crates/renderling/src/linkage/skybox-vertex.spv index 5a049b9f44d1404cbc87b534fadaf1511406f4d3..779c80410ce0632ff4ced9fc792b264cc888cb3c 100644 GIT binary patch delta 420 zcmYk1K@NgY3`9!=*-J38Ga=@Kpn@CYAzYbw9S`vjB8dku+>3>MgeoMH&dl$$WyprC zYZig??IL+P5!p&8h1OWHQdFuFt))W0Pc2K?DA_CUb`#96W*+b6*UGJb?jQdB>v5O` zj#HrYy`IJzi(5xCMe*k&TTA|YH~z|5HD_}o=fGKyhO-e(Jxv^~dmdvGf(I5efJFqU pglq`!^x^S_W(k}e9Iy3#cn=DnMtRC{okWsry(%sW_r681_zN%X9C82v delta 416 zcmYjMOAdli3@ia;FF|EzLX7-Wz=h!uZuCGL;vGa14`4i!dlt^Tyeco5bUN*{eN#H6 zW8-_dY5jP}MPw_16o?V91S;e)VhP~AYFWqzqynhz98|Ao8tbYbz_ov_AOHO;J4_8H zXDH3bUk_^4YU^y&=i@qc{q(*4#u+(J=6O7YoU56Wa~pHU#&PD{gPxl$vOs|YxQIqZ qnsf;3?4!n-8(=woTu7}m@8IqgCJD2I$Bhz7hS&oaAb(!NNAv|U^c=hZ diff --git a/crates/renderling/src/linkage/stage-main_vertex_scene.spv b/crates/renderling/src/linkage/stage-main_vertex_scene.spv index b18725f9e0a65be76be887b7395add3b8fbf1fdd..7152a7239f59e051c84f52db070933f18a649d5f 100644 GIT binary patch delta 810 zcmYjP-%C?*6#t$zoA+*mZ27P@N%KL-AGZYtA_cNSB5FzcBsMKKHkFN-C2flB-egXQ zs6$BgP!bXD$+-03!{{&Q!52e%$%p7k@SzccL7!u@_VD?f&-tG7JwLwp?r%EwHyvxc zF0DhkR9GpM;1%7+yRK16@<6?>aRO(}Ku|f=Mc7Ng1V5-xhbEysKp)VI=oIV`Kr+Nn z!MbYvH2XE)c}Rd-eW>8jre+m5(YpW(+^>xC>7p`%1{HPKmU zca1*}E!7V-ClxYlyQWZSZS?K%FChv6Q$h4E4uL&hI!=MT7G5W!IquncS z{}O4RVCU_1;|EcJS3)v-8C>KoaDpqL0%h#Eb8W$cCSA_|P3}RPynbDpbVvFzl0A3x zj!u@nJ>ny>xN_?i_-V8qT#g#xo#--nVe|`lWGsW!jj>(_9Pjvh@W=7V23h4iDV4`; z*@HDgv0+V~SWYc|9=nQdwqv~*w<#{;_QV5C@|1qWZ{str)%~4O{?*g)6MQ!=OYFtl zf@E@wzsR&#A7Nb6E-x%QDP<2Wg#$Egzj)W`q%4=aSFDxEuQ63)wnKG zPXjVJfG(LLT@uTwbju%U#hF&@gFR8Pz!h~cwulU2qg1kO)f{|BTf7WkrVnFJ2zCXy z`yZrs5xG@B>VLt0?OdOUjBJvQJa96p$O%nXOUTH)+dQx7{9fzkS6cT#Gt=soa~nVr zknVM$%%8OgCE4TnN@YHBglU$8&Zm^-J?8{v*ykFe6pyH@lhN{5oNwp}4R<@j6A z8(Ou>XCh~NMVVn!lm_%)g1-RYfJ Mf1N~G{#9N14TP_^p8x;= diff --git a/crates/renderling/src/linkage/stage-new_stage_vertex.spv b/crates/renderling/src/linkage/stage-new_stage_vertex.spv index 8614ec389e38233f3bbad1969231b918f689bed1..d4e22e32c54995118f0346a46c29948b286ebd53 100644 GIT binary patch literal 60124 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 M&#mD7>Y`ogA5%>#NB{r; literal 0 HcmV?d00001 diff --git a/crates/renderling/src/linkage/tutorial-passthru_fragment.spv b/crates/renderling/src/linkage/tutorial-passthru_fragment.spv new file mode 100644 index 0000000000000000000000000000000000000000..1169e6301eb5a72c82f34cbd982f666746417b87 GIT binary patch literal 316 zcmYj~NeaSH5JcO=7>8&SMRyYM4k8h=^%#b#M8#sqv#%m(Jyb9!u%HHGY5(O S}#1=3p2^BC< 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 B>%jm3 literal 0 HcmV?d00001 diff --git a/crates/renderling/src/linkage/tutorial-slabbed_vertices.spv b/crates/renderling/src/linkage/tutorial-slabbed_vertices.spv new file mode 100644 index 0000000000000000000000000000000000000000..69157296a2ef4c6c106fef6dbcc6480f62a2b178 GIT binary patch literal 16148 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 literal 0 HcmV?d00001 diff --git a/crates/renderling/src/linkage/tutorial-slabbed_vertices_no_instance.spv b/crates/renderling/src/linkage/tutorial-slabbed_vertices_no_instance.spv new file mode 100644 index 0000000000000000000000000000000000000000..72db0623987fd6affc06eefb84315334af310a98 GIT binary patch literal 14632 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 literal 0 HcmV?d00001 diff --git a/crates/renderling/src/linkage/ui-vertex.spv b/crates/renderling/src/linkage/ui-vertex.spv index 2eb44998c0b6660a1c063ffaf982fea2be9368a7..304e102a30274a31118727c07838652dd4b5f2e2 100644 GIT binary patch delta 60 zcmX>gbUgbU|g8B>$4D3M62*lP5EDTzk-!ccY F0sx>(48{Ne diff --git a/crates/renderling/src/scene.rs b/crates/renderling/src/scene.rs index c63ee4cb..0ddcb7b5 100644 --- a/crates/renderling/src/scene.rs +++ b/crates/renderling/src/scene.rs @@ -373,6 +373,7 @@ pub struct Scene { pub indirect_draws: MutableBufferArray, pub constants: Uniform, pub skybox: Skybox, + pub environment_bindgroup: wgpu::BindGroup, pub atlas: Atlas, skybox_update: Option>, cull_bindgroup: wgpu::BindGroup, @@ -469,21 +470,21 @@ impl Scene { | wgpu::BufferUsages::COPY_SRC, wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ); - let skybox = if let Some(mut skybox) = skybox { + let skybox = if let Some(skybox) = skybox { log::trace!("scene has skybox"); - skybox.environment_bindgroup = crate::skybox::create_skybox_bindgroup( - &device, - &constants, - &skybox.environment_cubemap, - ); skybox } else if let Some(skybox_img) = skybox_image { log::trace!("scene will build a skybox"); - Skybox::new(&device, &queue, &constants, skybox_img) + Skybox::new(&device, &queue, skybox_img) } else { log::trace!("skybox is empty"); - Skybox::empty(&device, &queue, &constants) + Skybox::empty(&device, &queue) }; + let environment_bindgroup = crate::skybox::create_skybox_bindgroup( + &device, + &constants, + &skybox.environment_cubemap, + ); let cull_bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("Scene::new cull_bindgroup"), @@ -538,6 +539,7 @@ impl Scene { lights, skybox, skybox_update: None, + environment_bindgroup, }; scene.update(&device, &queue); Ok(scene) @@ -556,6 +558,7 @@ impl Scene { atlas: _, skybox, skybox_update, + environment_bindgroup, vertices, entities, materials, @@ -580,7 +583,12 @@ impl Scene { // skybox should change image log::trace!("skybox changed"); constants.toggles.set_has_skybox(true); - *skybox = Skybox::new(device, queue, constants, img); + *skybox = Skybox::new(device, queue, img); + *environment_bindgroup = crate::skybox::create_skybox_bindgroup( + device, + constants, + &skybox.environment_cubemap, + ); // we also have to create a new render buffers bindgroup because irradiance is // part of that *render_buffers_bindgroup = create_scene_buffers_bindgroup( @@ -1041,7 +1049,7 @@ pub fn skybox_render( }), }); render_pass.set_pipeline(&pipeline.0); - render_pass.set_bind_group(0, &scene.skybox.environment_bindgroup, &[]); + render_pass.set_bind_group(0, &scene.environment_bindgroup, &[]); render_pass.draw(0..36, 0..1); drop(render_pass); @@ -1108,7 +1116,7 @@ pub fn scene_render( /// Conducts the HDR tone mapping, writing the HDR surface texture to the (most /// likely) sRGB window surface. -pub fn scene_tonemapping( +pub fn tonemapping( (device, queue, frame, hdr_frame, bloom_result): ( View, View, @@ -1117,7 +1125,7 @@ pub fn scene_tonemapping( Move, ), ) -> Result<(), SceneError> { - let label = Some("scene tonemapping"); + let label = Some("tonemapping"); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label, diff --git a/crates/renderling/src/skybox.rs b/crates/renderling/src/skybox.rs index fd0ac397..6c752cb3 100644 --- a/crates/renderling/src/skybox.rs +++ b/crates/renderling/src/skybox.rs @@ -127,12 +127,12 @@ pub fn create_skybox_render_pipeline( } /// An HDR skybox that also provides IBL cubemaps and lookups. -#[derive(Debug)] +/// +/// A clone of a skybox is a reference to the same skybox. +#[derive(Debug, Clone)] pub struct Skybox { // Cubemap texture of the environment cubemap pub environment_cubemap: crate::Texture, - // Bindgroup to use with the default skybox shader - pub environment_bindgroup: wgpu::BindGroup, // Cubemap texture of the pre-computed irradiance cubemap pub irradiance_cubemap: crate::Texture, // Cubemap texture and mip maps of the specular highlights, @@ -144,11 +144,7 @@ pub struct Skybox { impl Skybox { /// Create an empty, transparent skybox. - pub fn empty( - device: &wgpu::Device, - queue: &wgpu::Queue, - constants: &Uniform, - ) -> Self { + pub fn empty(device: &wgpu::Device, queue: &wgpu::Queue) -> Self { log::trace!("creating empty skybox"); let hdr_img = SceneImage { pixels: vec![0u8; 4 * 4], @@ -157,16 +153,11 @@ impl Skybox { format: crate::SceneImageFormat::R32G32B32A32FLOAT, apply_linear_transfer: false, }; - Self::new(device, queue, constants, hdr_img) + Self::new(device, queue, hdr_img) } /// Create a new `Skybox`. - pub fn new( - device: &wgpu::Device, - queue: &wgpu::Queue, - constants: &Uniform, - hdr_img: SceneImage, - ) -> Self { + pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, hdr_img: SceneImage) -> Self { log::trace!("creating skybox"); let equirectangular_texture = Skybox::hdr_texture_from_scene_image(device, queue, hdr_img); let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 10.0); @@ -265,11 +256,6 @@ impl Skybox { let brdf_lut = Skybox::create_precomputed_brdf_texture(device, queue); Skybox { - environment_bindgroup: crate::skybox::create_skybox_bindgroup( - &device, - &constants, - &environment_cubemap, - ), environment_cubemap, irradiance_cubemap, prefiltered_environment_cubemap, @@ -744,7 +730,8 @@ mod test { for i in 0..6 { // save out the irradiance face - let copied_buffer = scene.skybox.irradiance_cubemap.read_from( + let copied_buffer = crate::Texture::read_from( + &scene.skybox.irradiance_cubemap.texture, r.get_device(), r.get_queue(), 32, @@ -767,7 +754,8 @@ mod test { for mip_level in 0..5 { let mip_size = 128u32 >> mip_level; // save out the prefiltered environment faces' mips - let copied_buffer = scene.skybox.prefiltered_environment_cubemap.read_from( + let copied_buffer = crate::Texture::read_from( + &scene.skybox.prefiltered_environment_cubemap.texture, r.get_device(), r.get_queue(), mip_size as usize, @@ -810,7 +798,8 @@ mod test { let (device, queue) = r.get_device_and_queue_owned(); let brdf_lut = Skybox::create_precomputed_brdf_texture(&device, &queue); assert_eq!(wgpu::TextureFormat::Rg16Float, brdf_lut.texture.format()); - let copied_buffer = brdf_lut.read(&device, &queue, 512, 512, 2, 2); + let copied_buffer = + crate::Texture::read(&brdf_lut.texture, &device, &queue, 512, 512, 2, 2); let pixels = copied_buffer.pixels(&device); let pixels: Vec = bytemuck::cast_slice::(pixels.as_slice()) .iter() diff --git a/crates/renderling/src/slab.rs b/crates/renderling/src/slab.rs index 698514ae..f12b9bee 100644 --- a/crates/renderling/src/slab.rs +++ b/crates/renderling/src/slab.rs @@ -1,5 +1,8 @@ -//! CPU side of the slab storage. -use std::ops::Deref; +//! CPU side of slab storage. +use std::{ + ops::Deref, + sync::{atomic::AtomicUsize, Arc, RwLock}, +}; use renderling_shader::{ array::Array, @@ -24,48 +27,66 @@ pub enum SlabError { Async { source: wgpu::BufferAsyncError }, } -/// A slab buffer used by the stage. +/// A slab buffer used by the stage to store heterogeneous objects. +/// +/// A clone of a buffer is a reference to the same buffer. +#[derive(Clone)] pub struct SlabBuffer { - buffer: wgpu::Buffer, + pub(crate) buffer: Arc>, // The number of u32 elements currently stored in the buffer. // // This is the next index to write into. - len: usize, + len: Arc, // The total number of u32 elements that can be stored in the buffer. - capacity: usize, + capacity: Arc, } impl SlabBuffer { - pub fn new(device: &wgpu::Device, capacity: usize) -> Self { - let buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("stage buffer"), + fn new_buffer( + device: &wgpu::Device, + capacity: usize, + usage: wgpu::BufferUsages, + ) -> wgpu::Buffer { + device.create_buffer(&wgpu::BufferDescriptor { + label: Some("SlabBuffer"), size: (capacity * std::mem::size_of::()) as u64, usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST - | wgpu::BufferUsages::COPY_SRC, + | wgpu::BufferUsages::COPY_SRC + | usage, mapped_at_creation: false, - }); + }) + } + /// Create a new slab buffer with a capacity of `capacity` u32 elements. + pub fn new(device: &wgpu::Device, capacity: usize) -> Self { + Self::new_usage(device, capacity, wgpu::BufferUsages::empty()) + } + + /// Create a new slab buffer with a capacity of `capacity` u32 elements. + pub fn new_usage(device: &wgpu::Device, capacity: usize, usage: wgpu::BufferUsages) -> Self { Self { - buffer, - len: 0, - capacity, + buffer: RwLock::new(Self::new_buffer(device, capacity, usage)).into(), + len: AtomicUsize::new(0).into(), + capacity: AtomicUsize::new(capacity).into(), } } + /// The number of u32 elements currently stored in the buffer. pub fn len(&self) -> usize { - self.len + self.len.load(std::sync::atomic::Ordering::Relaxed) } + /// The total number of u32 elements that can be stored in the buffer. pub fn capacity(&self) -> usize { - self.capacity + self.capacity.load(std::sync::atomic::Ordering::Relaxed) } /// Write into the slab buffer, modifying in place. /// /// NOTE: This has no effect on the length of the buffer. pub fn write( - &mut self, + &self, device: &wgpu::Device, queue: &wgpu::Queue, id: Id, @@ -75,17 +96,16 @@ impl SlabBuffer { let size = T::slab_size(); let mut bytes = vec![0u32; size]; let _ = bytes.write(data, 0); + let capacity = self.capacity(); snafu::ensure!( - id.index() + size <= self.capacity, - CapacitySnafu { - id, - capacity: self.capacity - } + id.index() + size <= capacity, + CapacitySnafu { id, capacity } ); let encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); queue.write_buffer( - &self.buffer, + // UNWRAP: if we can't lock we want to panic + &self.buffer.read().unwrap(), byte_offset as u64, bytemuck::cast_slice(bytes.as_slice()), ); @@ -93,14 +113,18 @@ impl SlabBuffer { Ok(()) } - pub async fn read( + /// Read from the slab buffer. + /// + /// `T` is only for the error message. + pub async fn read_raw( &self, device: &wgpu::Device, queue: &wgpu::Queue, - id: Id, - ) -> Result> { - let byte_offset = id.index() * std::mem::size_of::(); - let length = T::slab_size() * std::mem::size_of::(); + start: usize, + len: usize, + ) -> Result, SlabError> { + let byte_offset = start * std::mem::size_of::(); + let length = len * std::mem::size_of::(); let output_buffer_size = length as u64; let output_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some(&format!("SlabBuffer::read<{}>", std::any::type_name::())), @@ -116,7 +140,8 @@ impl SlabBuffer { output_buffer_size:{output_buffer_size}", ); encoder.copy_buffer_to_buffer( - &self.buffer, + // UNWRAP: if we can't lock we want to panic + &self.buffer.read().unwrap(), byte_offset as u64, &output_buffer, 0, @@ -133,70 +158,115 @@ impl SlabBuffer { .context(AsyncRecvSnafu)? .context(AsyncSnafu)?; let bytes = buffer_slice.get_mapped_range(); - let t = Slab::read(bytemuck::cast_slice(bytes.deref()), Id::::new(0)); + Ok(bytemuck::cast_slice(bytes.deref()).to_vec()) + } + + /// Read from the slab buffer. + pub async fn read( + &self, + device: &wgpu::Device, + queue: &wgpu::Queue, + id: Id, + ) -> Result> { + let vec = self + .read_raw(device, queue, id.index(), T::slab_size()) + .await?; + let t = Slab::read(vec.as_slice(), Id::::new(0)); Ok(t) } /// Append to the end of the buffer. pub fn append( - &mut self, + &self, device: &wgpu::Device, queue: &wgpu::Queue, t: &T, ) -> Id { - if T::slab_size() + self.len > self.capacity { - self.resize(device, queue, self.capacity * 2); + let len = self.len(); + let capacity = self.capacity(); + if T::slab_size() + len > capacity { + self.resize(device, queue, capacity * 2); } - let id = Id::::from(self.len); + let id = Id::::from(len); // UNWRAP: We just checked that there is enough capacity, and added some if not. self.write(device, queue, id, t).unwrap(); - self.len += T::slab_size(); + self.len + .store(len + T::slab_size(), std::sync::atomic::Ordering::Relaxed); id } /// Append a slice to the end of the buffer, returning a slab array. pub fn append_slice( - &mut self, + &self, device: &wgpu::Device, queue: &wgpu::Queue, ts: &[T], ) -> Array { - let len = ts.len(); + let ts_len = ts.len(); let size = T::slab_size(); - if size * len + self.len > self.capacity { - self.resize(device, queue, self.capacity * 2); + let capacity = self.capacity(); + let len = self.len(); + //log::trace!( + // "append_slice: {size} * {ts_len} + {len} ({}) >= {capacity}", + // size * ts_len + len + //); + let capacity_needed = size * ts_len + len; + if capacity_needed >= capacity { + let mut new_capacity = capacity * 2; + while new_capacity < capacity_needed { + new_capacity *= 2; + } + self.resize(device, queue, new_capacity); } - let index = self.len as u32; - for t in ts.iter() { + let starting_index = len as u32; + for (i, t) in ts.iter().enumerate() { // UNWRAP: Safe because we just checked that there is enough capacity, // and added some if not. - self.write(device, queue, Id::::from(self.len), t) - .unwrap(); + self.write( + device, + queue, + Id::::from(starting_index + (size * i) as u32), + t, + ) + .unwrap(); } - self.len += size * len; - Array::new(index, len as u32) + self.len + .store(len + size * ts_len, std::sync::atomic::Ordering::Relaxed); + Array::new(starting_index, ts_len as u32) } /// Resize the slab buffer. /// /// This creates a new buffer and writes the data from the old into the new. - pub fn resize(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, capacity: usize) { - if capacity > self.capacity { - let new_buffer = Self::new(device, capacity); + pub fn resize(&self, device: &wgpu::Device, queue: &wgpu::Queue, new_capacity: usize) { + let capacity = self.capacity(); + if new_capacity > capacity { + log::trace!("resizing buffer from {capacity} to {new_capacity}"); + let len = self.len(); + let mut buffer = self.buffer.write().unwrap(); + let new_buffer = Self::new_buffer(device, new_capacity, buffer.usage()); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + // UNWRAP: if we can't lock we want to panic encoder.copy_buffer_to_buffer( - &self.buffer, + &buffer, 0, - &new_buffer.buffer, + &new_buffer, 0, - (self.len * std::mem::size_of::()) as u64, + (len * std::mem::size_of::()) as u64, ); queue.submit(std::iter::once(encoder.finish())); - self.buffer = new_buffer.buffer; - self.capacity = capacity; + *buffer = new_buffer; + self.capacity + .store(new_capacity, std::sync::atomic::Ordering::Relaxed); } } + + /// Get the underlying buffer. + pub fn get_buffer(&self) -> impl Deref + '_ { + // UNWRAP: if we can't lock we want to panic + self.buffer.read().unwrap() + } } #[cfg(test)] @@ -207,26 +277,52 @@ mod test { #[test] fn slab_buffer_roundtrip() { + println!("write"); let _ = env_logger::builder().is_test(true).try_init(); let r = Renderling::headless(10, 10).unwrap(); let device = r.get_device(); let queue = r.get_queue(); - let mut slab = SlabBuffer::new(device, 2); + let slab = SlabBuffer::new(device, 2); slab.append(device, queue, &42); slab.append(device, queue, &1); let id = Id::<[u32; 2]>::new(0); let t = futures_lite::future::block_on(slab.read(device, queue, id)).unwrap(); assert_eq!([42, 1], t, "read back what we wrote"); + + println!("overflow"); let id = Id::::new(2); let err = slab.write(device, queue, id, &666).unwrap_err(); assert_eq!( "Out of capacity. Tried to write u32(slab size=1) at 2 but capacity is 2", err.to_string() ); - assert_eq!(2, slab.len); + assert_eq!(2, slab.len()); + + println!("append"); slab.append(device, queue, &666); let id = Id::<[u32; 3]>::new(0); let t = futures_lite::future::block_on(slab.read(device, queue, id)).unwrap(); assert_eq!([42, 1, 666], t); + + println!("append slice"); + let a = glam::Vec3::new(0.0, 0.0, 0.0); + let b = glam::Vec3::new(1.0, 1.0, 1.0); + let c = glam::Vec3::new(2.0, 2.0, 2.0); + let points = vec![a, b, c]; + let array = slab.append_slice(device, queue, &points); + let slab_u32 = + futures_lite::future::block_on(slab.read_raw::(device, queue, 0, slab.len())) + .unwrap(); + let points_out = slab_u32.read_vec::(array); + assert_eq!(points, points_out); + + println!("append slice 2"); + let points = vec![a, a, a, a, b, b, b, c, c]; + let array = slab.append_slice(device, queue, &points); + let slab_u32 = + futures_lite::future::block_on(slab.read_raw::(device, queue, 0, slab.len())) + .unwrap(); + let points_out = slab_u32.read_vec::(array); + assert_eq!(points, points_out); } } diff --git a/crates/renderling/src/stage.rs b/crates/renderling/src/stage.rs index 2bf080aa..fce17751 100644 --- a/crates/renderling/src/stage.rs +++ b/crates/renderling/src/stage.rs @@ -1,16 +1,18 @@ //! Rendering objects in the scene graph. //! //! Provides a `Stage` object that can be used to render a scene graph. +use std::sync::{Arc, Mutex}; + use renderling_shader::{ array::Array, debug::DebugMode, id::Id, slab::Slabbed, - stage::{GpuLight, StageLegend}, + stage::{DrawIndirect, GpuLight, RenderUnit, StageLegend}, }; use snafu::Snafu; -use crate::{Atlas, Device, Queue, SlabBuffer}; +use crate::{Atlas, Device, Queue, Skybox, SlabBuffer}; pub mod light; @@ -27,21 +29,34 @@ pub enum StageError { } /// Represents an entire scene worth of rendering data. +/// +/// A clone of a stage is a reference to the same stage. +#[derive(Clone)] pub struct Stage { pub(crate) stage_slab: SlabBuffer, pub(crate) render_unit_slab: SlabBuffer, - //pub(crate) atlas: Atlas, + pub(crate) indirect_draws: SlabBuffer, + pub(crate) atlas: Arc>, + pub(crate) skybox: Arc>, + pub(crate) pipeline: Arc>>>, + pub(crate) slab_buffers_bindgroup: Arc>>>, + pub(crate) textures_bindgroup: Arc>>>, pub(crate) device: Device, pub(crate) queue: Queue, } impl Stage { - /// Create a new stage slab with `capacity`, which is + /// Create a new stage. pub fn new(device: Device, queue: Queue, legend: StageLegend) -> Self { let mut s = Self { stage_slab: SlabBuffer::new(&device, 256), render_unit_slab: SlabBuffer::new(&device, 256), - //atlas: Atlas::new(&device, &queue, 0, 0), + indirect_draws: SlabBuffer::new_usage(&device, 256, wgpu::BufferUsages::INDIRECT), + pipeline: Default::default(), + atlas: Arc::new(Mutex::new(Atlas::new(&device, &queue, 1, 1))), + skybox: Arc::new(Mutex::new(Skybox::empty(&device, &queue))), + slab_buffers_bindgroup: Default::default(), + textures_bindgroup: Default::default(), device, queue, }; @@ -63,6 +78,26 @@ impl Stage { .append_slice(&self.device, &self.queue, objects) } + ///// Add a render unit to the stage. + ///// + ///// The render unit will be added to the stage and its ID will be returned. + ///// The ID of the input render unit will be overwritten. + //pub fn add_render_unit(&mut self, mut unit: RenderUnit) -> Id { + // unit.id = Id::from(self.render_unit_slab.len()); + // self.indirect_draws.append( + // &self.device, + // &self.queue, + // &DrawIndirect { + // vertex_count: unit.vertices.len() as u32, + // instance_count: 1, + // base_vertex: 0, + // base_instance: unit.id.into(), + // }, + // ); + // self.render_unit_slab + // .append(&self.device, &self.queue, &unit) + //} + /// Set the debug mode. pub fn set_debug_mode(&mut self, debug_mode: DebugMode) { let id = Id::::from(StageLegend::offset_of_debug_mode()); @@ -133,18 +168,619 @@ impl Stage { self.set_light_array(lights); self } + + /// Return the render pipeline, creating it if necessary. + pub fn get_pipeline(&self) -> Arc { + fn buffers_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { + let visibility = wgpu::ShaderStages::VERTEX + | wgpu::ShaderStages::FRAGMENT + | wgpu::ShaderStages::COMPUTE; + let stage_slab = wgpu::BindGroupLayoutEntry { + binding: 0, + visibility, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }; + let unit_slab = wgpu::BindGroupLayoutEntry { + binding: 1, + visibility, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }; + let indirect_draw_slab = wgpu::BindGroupLayoutEntry { + binding: 2, + visibility, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }; + let entries = vec![stage_slab, unit_slab, indirect_draw_slab]; + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("stage slab buffers"), + entries: &entries, + }) + } + + fn image2d_entry(binding: u32) -> (wgpu::BindGroupLayoutEntry, wgpu::BindGroupLayoutEntry) { + let img = wgpu::BindGroupLayoutEntry { + binding, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }; + let sampler = wgpu::BindGroupLayoutEntry { + binding: binding + 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }; + (img, sampler) + } + + fn cubemap_entry(binding: u32) -> (wgpu::BindGroupLayoutEntry, wgpu::BindGroupLayoutEntry) { + let img = wgpu::BindGroupLayoutEntry { + binding, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::Cube, + multisampled: false, + }, + count: None, + }; + let sampler = wgpu::BindGroupLayoutEntry { + binding: binding + 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }; + (img, sampler) + } + + fn textures_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { + let (atlas, atlas_sampler) = image2d_entry(0); + let (irradiance, irradiance_sampler) = cubemap_entry(2); + let (prefilter, prefilter_sampler) = cubemap_entry(4); + let (brdf, brdf_sampler) = image2d_entry(6); + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("stage textures"), + entries: &[ + atlas, + atlas_sampler, + irradiance, + irradiance_sampler, + prefilter, + prefilter_sampler, + brdf, + brdf_sampler, + ], + }) + } + + fn create_stage_render_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline { + log::trace!("creating stage render pipeline"); + let label = Some("stage render pipeline"); + let vertex_shader = device + .create_shader_module(wgpu::include_spirv!("linkage/stage-new_stage_vertex.spv")); + let fragment_shader = device + .create_shader_module(wgpu::include_spirv!("linkage/stage-stage_fragment.spv")); + let stage_slab_buffers_layout = buffers_bindgroup_layout(device); + let textures_layout = textures_bindgroup_layout(device); + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label, + bind_group_layouts: &[&stage_slab_buffers_layout, &textures_layout], + push_constant_ranges: &[], + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label, + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &vertex_shader, + entry_point: "stage::new_stage_vertex", + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + module: &fragment_shader, + entry_point: "stage::stage_fragment", + targets: &[ + Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8UnormSrgb, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + }), + //Some(wgpu::ColorTargetState { + // format: wgpu::TextureFormat::Rgba16Float, + // blend: Some(wgpu::BlendState::ALPHA_BLENDING), + // write_mask: wgpu::ColorWrites::ALL, + //}), + ], + }), + multiview: None, + }); + pipeline + } + + // UNWRAP: safe because we're only ever called from the render thread. + let mut pipeline = self.pipeline.lock().unwrap(); + if let Some(pipeline) = pipeline.as_ref() { + pipeline.clone() + } else { + let p = Arc::new(create_stage_render_pipeline(&self.device)); + *pipeline = Some(p.clone()); + p + } + } + + pub fn get_slab_buffers_bindgroup(&self) -> Arc { + fn create_slab_buffers_bindgroup( + device: &wgpu::Device, + pipeline: &wgpu::RenderPipeline, + stage_slab: &SlabBuffer, + render_unit_slab: &SlabBuffer, + indirect_draws: &SlabBuffer, + ) -> wgpu::BindGroup { + let label = Some("stage slab buffers"); + let stage_slab_buffers_bindgroup = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label, + layout: &pipeline.get_bind_group_layout(0), + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: stage_slab.get_buffer().as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: render_unit_slab.get_buffer().as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: indirect_draws.get_buffer().as_entire_binding(), + }, + ], + }); + stage_slab_buffers_bindgroup + } + + // UNWRAP: safe because we're only ever called from the render thread. + let mut bindgroup = self.slab_buffers_bindgroup.lock().unwrap(); + if let Some(bindgroup) = bindgroup.as_ref() { + bindgroup.clone() + } else { + let b = Arc::new(create_slab_buffers_bindgroup( + &self.device, + &self.get_pipeline(), + &self.stage_slab, + &self.render_unit_slab, + &self.indirect_draws, + )); + *bindgroup = Some(b.clone()); + b + } + } + + pub fn get_textures_bindgroup(&self) -> Arc { + fn create_textures_bindgroup( + device: &wgpu::Device, + pipeline: &wgpu::RenderPipeline, + atlas: &Atlas, + skybox: &Skybox, + ) -> wgpu::BindGroup { + let label = Some("stage textures"); + let textures_bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { + label, + layout: &pipeline.get_bind_group_layout(1), + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&atlas.texture.view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&atlas.texture.sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::TextureView( + &skybox.irradiance_cubemap.view, + ), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::Sampler( + &skybox.irradiance_cubemap.sampler, + ), + }, + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::TextureView( + &skybox.prefiltered_environment_cubemap.view, + ), + }, + wgpu::BindGroupEntry { + binding: 5, + resource: wgpu::BindingResource::Sampler( + &skybox.prefiltered_environment_cubemap.sampler, + ), + }, + wgpu::BindGroupEntry { + binding: 6, + resource: wgpu::BindingResource::TextureView(&skybox.brdf_lut.view), + }, + wgpu::BindGroupEntry { + binding: 7, + resource: wgpu::BindingResource::Sampler(&skybox.brdf_lut.sampler), + }, + ], + }); + textures_bindgroup + } + + // UNWRAP: safe because we're only ever called from the render thread. + let mut bindgroup = self.textures_bindgroup.lock().unwrap(); + if let Some(bindgroup) = bindgroup.as_ref() { + bindgroup.clone() + } else { + let b = Arc::new(create_textures_bindgroup( + &self.device, + &self.get_pipeline(), + // UNWRAP: we can't acquire locks we want to panic + &self.atlas.lock().unwrap(), + &self.skybox.lock().unwrap(), + )); + *bindgroup = Some(b.clone()); + b + } + } + + pub fn number_of_indirect_draws(&self) -> u32 { + (self.indirect_draws.len() / DrawIndirect::slab_size()) as u32 + } + + pub fn number_of_render_units(&self) -> u32 { + (self.render_unit_slab.len() / RenderUnit::slab_size()) as u32 + } } #[cfg(test)] mod test { - use crate::Renderling; + use glam::Vec3; + use moongraph::{graph, View, ViewMut}; + use renderling_shader::{ + slab::Slab, + stage::{Camera, RenderUnit, Vertex}, + }; + use wgpu::util::DeviceExt; + + use crate::{default_ortho2d, frame::FrameTextureView, DepthTexture, HdrSurface, Renderling}; use super::*; + fn right_tri_vertices() -> Vec { + vec![ + Vertex::default() + .with_position([0.0, 0.0, 0.5]) + .with_color([0.0, 1.0, 1.0, 1.0]), + Vertex::default() + .with_position([0.0, 100.0, 0.5]) + .with_color([1.0, 1.0, 0.0, 1.0]), + Vertex::default() + .with_position([100.0, 0.0, 0.5]) + .with_color([1.0, 0.0, 1.0, 1.0]), + ] + } + + #[cfg(feature = "nene")] + #[test] + fn slab_shader_sanity() { + let r = Renderling::headless(100, 100).unwrap(); + let (device, queue) = r.get_device_and_queue_owned(); + let slab = SlabBuffer::new(&device, 256); + let vertices = slab.append_slice(&device, &queue, &right_tri_vertices()); + let (projection, view) = default_ortho2d(100.0, 100.0); + let camera = slab.append( + &device, + &queue, + &Camera { + projection, + view, + ..Default::default() + }, + ); + let unit = slab.append( + &device, + &queue, + &RenderUnit { + camera, + vertices, + ..Default::default() + }, + ); + + //// Create a bindgroup for the slab so our shader can read out the types. + //let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + // label: Some("slab shader sanity"), + // entries: &[wgpu::BindGroupLayoutEntry { + // binding: 0, + // visibility: wgpu::ShaderStages::VERTEX, + // ty: wgpu::BindingType::Buffer { + // ty: wgpu::BufferBindingType::Storage { read_only: true }, + // has_dynamic_offset: false, + // min_binding_size: None, + // }, + // count: None, + // }], + //}); + //let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + // label: Some("slab shader sanity"), + // bind_group_layouts: &[&bindgroup_layout], + // push_constant_ranges: &[], + //}); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("slab shader sanity"), + layout: None, //Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &device + .create_shader_module(wgpu::include_spirv!("linkage/stage-simple_vertex.spv")), + entry_point: "stage::simple_vertex", + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + module: &device.create_shader_module(wgpu::include_spirv!( + "linkage/stage-simple_fragment.spv" + )), + entry_point: "stage::simple_fragment", + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8UnormSrgb, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + }); + + //let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { + // label: Some("slab shader sanity"), + // layout: &bindgroup_layout, + // entries: &[wgpu::BindGroupEntry { + // binding: 0, + // resource: slab.get_buffer().as_entire_binding(), + // }], + //}); + let depth = crate::texture::Texture::create_depth_texture(&device, 100, 100); + let frame = device.create_texture(&wgpu::TextureDescriptor { + label: Some("slab shader sanity"), + size: wgpu::Extent3d { + width: 100, + height: 100, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + let (frame_view, _) = crate::frame::default_frame_texture_view(&frame); + + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("slab shader sanity"), + }); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("slab shader sanity"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::GREEN), + store: true, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &depth.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }), + stencil_ops: None, + }), + }); + render_pass.set_pipeline(&pipeline); + //render_pass.set_bind_group(0, &bindgroup, &[]); + render_pass.draw(0..3, 0..1); + } + queue.submit(std::iter::once(encoder.finish())); + + let buffer = crate::Texture::read(&frame, &device, &queue, 100, 100, 4, 1); + let img = buffer.into_rgba(&device).unwrap(); + img_diff::save("stage/slab_shader_sanity.png", img); + } + + fn stage_render( + (stage, frame_view, depth): (ViewMut, View, View), + ) -> Result<(), StageError> { + let label = Some("stage render"); + let pipeline = stage.get_pipeline(); + let slab_buffers_bindgroup = stage.get_slab_buffers_bindgroup(); + let textures_bindgroup = stage.get_textures_bindgroup(); + let _indirect_buffer = stage.indirect_draws.get_buffer(); + let mut encoder = stage + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame_view.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &depth.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }), + stencil_ops: None, + }), + }); + render_pass.set_pipeline(&pipeline); + render_pass.set_bind_group(0, &slab_buffers_bindgroup, &[]); + render_pass.set_bind_group(1, &textures_bindgroup, &[]); + //render_pass.multi_draw_indirect(&indirect_buffer, 0, stage.number_of_indirect_draws()); + render_pass.draw(0..3, 0..1); + } + stage.queue.submit(std::iter::once(encoder.finish())); + Ok(()) + } + + #[cfg(feature = "none")] #[test] fn stage_new() { - let r = Renderling::headless(10, 10).unwrap(); + let mut r = Renderling::headless(100, 100) + .unwrap() + .with_background_color(glam::Vec4::splat(1.0)); let (device, queue) = r.get_device_and_queue_owned(); - let _stage = Stage::new(device, queue, StageLegend::default()); + let mut stage = Stage::new(device, queue, StageLegend::default()).with_lighting(true); + let (projection, view) = default_ortho2d(100.0, 100.0); + let camera = Camera { + projection, + view, + position: Vec3::ZERO, + }; + let camera_id = stage.append(&camera); + let vertices = stage.append_slice(&right_tri_vertices()); + println!("vertices: {vertices:?}"); + let mut unit = RenderUnit { + camera: camera_id, + vertices, + ..Default::default() + }; + let unit_id = stage.add_render_unit(unit); + unit.id = unit_id; + assert_eq!(Id::new(0), unit_id); + assert_eq!(1, stage.number_of_render_units()); + assert_eq!(1, stage.number_of_indirect_draws()); + + let stage_slab = futures_lite::future::block_on(stage.stage_slab.read_raw::( + &stage.device, + &stage.queue, + 0, + stage.stage_slab.len(), + )) + .unwrap(); + assert_eq!(camera, stage_slab.read(camera_id)); + assert_eq!(right_tri_vertices(), stage_slab.read_vec(vertices)); + let render_unit_slab = + futures_lite::future::block_on(stage.render_unit_slab.read_raw::( + &stage.device, + &stage.queue, + 0, + stage.render_unit_slab.len(), + )) + .unwrap(); + assert_eq!(unit, render_unit_slab.read(unit_id)); + let indirect_slab = futures_lite::future::block_on(stage.indirect_draws.read_raw::( + &stage.device, + &stage.queue, + 0, + stage.indirect_draws.len(), + )) + .unwrap(); + assert_eq!( + DrawIndirect { + vertex_count: 3, + instance_count: 1, + base_vertex: 0, + base_instance: 0, + }, + indirect_slab.read(Id::new(0)) + ); + + { + // set up the render graph + use crate::{ + frame::{clear_frame_and_depth, create_frame, present}, + graph::{graph, Graph}, + }; + r.graph.add_resource(stage.clone()); + + let (device, queue) = r.get_device_and_queue_owned(); + + // pre-render passes + r.graph + .add_subgraph(graph!(create_frame < clear_frame_and_depth)) + .add_barrier(); + // render passes + r.graph.add_subgraph(graph!(stage_render)).add_barrier(); + // post-render passes + let copy_frame_to_post = crate::frame::PostRenderBufferCreate::create; + r.graph.add_subgraph(graph!(copy_frame_to_post < present)); + } + + let img = r.render_image().unwrap(); + img_diff::save("stage/stage_new.png", img); } } diff --git a/crates/renderling/src/texture.rs b/crates/renderling/src/texture.rs index 9b0a1427..db018d1c 100644 --- a/crates/renderling/src/texture.rs +++ b/crates/renderling/src/texture.rs @@ -470,7 +470,7 @@ impl Texture { /// color/alpha channels and the number of bytes in the underlying /// subpixel type (usually u8=1, u16=2 or f32=4). pub fn read( - &self, + texture: &wgpu::Texture, device: &wgpu::Device, queue: &wgpu::Queue, width: usize, @@ -478,7 +478,8 @@ impl Texture { channels: usize, subpixel_bytes: usize, ) -> CopiedTextureBuffer { - self.read_from( + Self::read_from( + texture, device, queue, width, @@ -496,7 +497,7 @@ impl Texture { /// color/alpha channels and the number of bytes in the underlying /// subpixel type (usually u8=1, u16=2 or f32=4). pub fn read_from( - &self, + texture: &wgpu::Texture, device: &wgpu::Device, queue: &wgpu::Queue, width: usize, @@ -517,7 +518,7 @@ impl Texture { let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("post render screen capture encoder"), }); - let mut source = self.texture.as_image_copy(); + let mut source = texture.as_image_copy(); source.mip_level = mip_level; if let Some(origin) = origin { source.origin = origin; @@ -544,7 +545,7 @@ impl Texture { CopiedTextureBuffer { dimensions, buffer, - format: self.texture.format(), + format: texture.format(), } } @@ -876,7 +877,8 @@ mod test { let mip_width = width >> mip_level; let mip_height = height >> mip_level; // save out the mips - let copied_buffer = mip.read_from( + let copied_buffer = crate::Texture::read_from( + &mip.texture, r.get_device(), r.get_queue(), mip_width as usize, diff --git a/crates/renderling/src/tutorial.rs b/crates/renderling/src/tutorial.rs new file mode 100644 index 00000000..e230ee5c --- /dev/null +++ b/crates/renderling/src/tutorial.rs @@ -0,0 +1,882 @@ +//! A tutorial module for the renderling crate. + +#[cfg(test)] +mod test { + use glam::{Vec3, Vec4}; + + use crate::{ + frame::FrameTextureView, + graph::{graph, Graph, GraphError, View}, + shader::{ + self, + array::Array, + id::Id, + slab::{Slab, Slabbed}, + stage::{Camera, RenderUnit, Vertex}, + }, + DepthTexture, Device, Queue, Renderling, + }; + + #[test] + fn implicit_isosceles_triangle() { + let mut r = Renderling::headless(100, 100).unwrap(); + let (device, _queue) = r.get_device_and_queue_owned(); + let label = Some("implicit isosceles triangle"); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label, + layout: None, + vertex: wgpu::VertexState { + module: &device.create_shader_module(wgpu::include_spirv!( + "linkage/tutorial-implicit_isosceles_vertex.spv" + )), + entry_point: "tutorial::implicit_isosceles_vertex", + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + module: &device.create_shader_module(wgpu::include_spirv!( + "linkage/tutorial-passthru_fragment.spv" + )), + entry_point: "tutorial::passthru_fragment", + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8UnormSrgb, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + }); + + fn render( + (device, queue, pipeline, frame, depth): ( + View, + View, + View, + View, + View, + ), + ) -> Result<(), GraphError> { + let label = Some("implicit isosceles triangle"); + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::GREEN), + store: true, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &depth.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }), + stencil_ops: None, + }), + }); + render_pass.set_pipeline(&pipeline); + render_pass.draw(0..3, 0..1); + } + queue.submit(std::iter::once(encoder.finish())); + Ok(()) + } + + use crate::frame::{clear_frame_and_depth, copy_frame_to_post, create_frame, present}; + r.graph.add_resource(pipeline); + r.graph.add_subgraph(graph!( + create_frame + < clear_frame_and_depth + < render + < copy_frame_to_post + < present + )); + + let img = r.render_image().unwrap(); + img_diff::assert_img_eq("tutorial/implicit_isosceles_triangle.png", img); + } + + #[test] + fn slabbed_isosceles_triangle_no_instance() { + let mut r = Renderling::headless(100, 100).unwrap(); + let (device, queue) = r.get_device_and_queue_owned(); + + // Create our geometry on the slab. + // Don't worry too much about capacity, it can grow. + let slab = crate::slab::SlabBuffer::new(&device, 16); + let vertices = slab.append_slice( + &device, + &queue, + &[ + Vertex { + position: Vec4::new(0.5, -0.5, 0.0, 1.0), + color: Vec4::new(1.0, 0.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(0.0, 0.5, 0.0, 1.0), + color: Vec4::new(0.0, 1.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(-0.5, -0.5, 0.0, 1.0), + color: Vec4::new(0.0, 0.0, 1.0, 1.0), + ..Default::default() + }, + ], + ); + assert_eq!(3, vertices.len()); + + // Create a bindgroup for the slab so our shader can read out the types. + let label = Some("slabbed isosceles triangle"); + let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label, + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label, + bind_group_layouts: &[&bindgroup_layout], + push_constant_ranges: &[], + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &device.create_shader_module(wgpu::include_spirv!( + "linkage/tutorial-slabbed_vertices_no_instance.spv" + )), + entry_point: "tutorial::slabbed_vertices_no_instance", + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + module: &device.create_shader_module(wgpu::include_spirv!( + "linkage/tutorial-passthru_fragment.spv" + )), + entry_point: "tutorial::passthru_fragment", + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8UnormSrgb, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + }); + + let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { + label, + layout: &bindgroup_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: slab.get_buffer().as_entire_binding(), + }], + }); + + struct App { + pipeline: wgpu::RenderPipeline, + bindgroup: wgpu::BindGroup, + vertices: Array, + } + + let app = App { + pipeline, + bindgroup, + vertices, + }; + r.graph.add_resource(app); + + fn render( + (device, queue, app, frame, depth): ( + View, + View, + View, + View, + View, + ), + ) -> Result<(), GraphError> { + let label = Some("slabbed isosceles triangle"); + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), + store: true, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &depth.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }), + stencil_ops: None, + }), + }); + render_pass.set_pipeline(&app.pipeline); + render_pass.set_bind_group(0, &app.bindgroup, &[]); + render_pass.draw(0..app.vertices.len() as u32, 0..1); + } + queue.submit(std::iter::once(encoder.finish())); + Ok(()) + } + + use crate::frame::{clear_frame_and_depth, copy_frame_to_post, create_frame, present}; + r.graph.add_subgraph(graph!( + create_frame + < clear_frame_and_depth + < render + < copy_frame_to_post + < present + )); + + let img = r.render_image().unwrap(); + img_diff::assert_img_eq("tutorial/slabbed_isosceles_triangle_no_instance.png", img); + } + + #[test] + fn slabbed_isosceles_triangle() { + let mut r = Renderling::headless(100, 100).unwrap(); + let (device, queue) = r.get_device_and_queue_owned(); + + // Create our geometry on the slab. + // Don't worry too much about capacity, it can grow. + let slab = crate::slab::SlabBuffer::new(&device, 16); + let geometry = vec![ + Vertex { + position: Vec4::new(0.5, -0.5, 0.0, 1.0), + color: Vec4::new(1.0, 0.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(0.0, 0.5, 0.0, 1.0), + color: Vec4::new(0.0, 1.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(-0.5, -0.5, 0.0, 1.0), + color: Vec4::new(0.0, 0.0, 1.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(-1.0, 1.0, 0.0, 1.0), + color: Vec4::new(1.0, 0.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(-1.0, 0.0, 0.0, 1.0), + color: Vec4::new(0.0, 1.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(0.0, 1.0, 0.0, 1.0), + color: Vec4::new(0.0, 0.0, 1.0, 1.0), + ..Default::default() + }, + ]; + let vertices = slab.append_slice(&device, &queue, &geometry); + let vertices_id = slab.append(&device, &queue, &vertices); + + // Create a bindgroup for the slab so our shader can read out the types. + let label = Some("slabbed isosceles triangle"); + let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label, + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label, + bind_group_layouts: &[&bindgroup_layout], + push_constant_ranges: &[], + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &device.create_shader_module(wgpu::include_spirv!( + "linkage/tutorial-slabbed_vertices.spv" + )), + entry_point: "tutorial::slabbed_vertices", + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + module: &device.create_shader_module(wgpu::include_spirv!( + "linkage/tutorial-passthru_fragment.spv" + )), + entry_point: "tutorial::passthru_fragment", + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8UnormSrgb, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + }); + + let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { + label, + layout: &bindgroup_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: slab.get_buffer().as_entire_binding(), + }], + }); + + struct App { + pipeline: wgpu::RenderPipeline, + bindgroup: wgpu::BindGroup, + vertices_id: Id>, + vertices: Array, + } + + let app = App { + pipeline, + bindgroup, + vertices_id, + vertices, + }; + r.graph.add_resource(app); + + fn render( + (device, queue, app, frame, depth): ( + View, + View, + View, + View, + View, + ), + ) -> Result<(), GraphError> { + let label = Some("slabbed isosceles triangle"); + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), + store: true, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &depth.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }), + stencil_ops: None, + }), + }); + render_pass.set_pipeline(&app.pipeline); + render_pass.set_bind_group(0, &app.bindgroup, &[]); + render_pass.draw( + 0..app.vertices.len() as u32, + app.vertices_id.inner()..app.vertices_id.inner() + 1, + ); + } + queue.submit(std::iter::once(encoder.finish())); + Ok(()) + } + + use crate::frame::{clear_frame_and_depth, copy_frame_to_post, create_frame, present}; + r.graph.add_subgraph(graph!( + create_frame + < clear_frame_and_depth + < render + < copy_frame_to_post + < present + )); + + let img = r.render_image().unwrap(); + img_diff::assert_img_eq("tutorial/slabbed_isosceles_triangle.png", img); + } + + #[test] + fn slabbed_render_unit() { + let mut r = Renderling::headless(100, 100).unwrap(); + let (device, queue) = r.get_device_and_queue_owned(); + + // Create our geometry on the slab. + // Don't worry too much about capacity, it can grow. + let slab = crate::slab::SlabBuffer::new(&device, 16); + let geometry = vec![ + Vertex { + position: Vec4::new(0.5, -0.5, 0.0, 1.0), + color: Vec4::new(1.0, 0.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(0.0, 0.5, 0.0, 1.0), + color: Vec4::new(0.0, 1.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(-0.5, -0.5, 0.0, 1.0), + color: Vec4::new(0.0, 0.0, 1.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(-1.0, 1.0, 0.0, 1.0), + color: Vec4::new(1.0, 0.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(-1.0, 0.0, 0.0, 1.0), + color: Vec4::new(0.0, 1.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(0.0, 1.0, 0.0, 1.0), + color: Vec4::new(0.0, 0.0, 1.0, 1.0), + ..Default::default() + }, + ]; + let vertices = slab.append_slice(&device, &queue, &geometry); + let unit = RenderUnit { + vertices, + ..Default::default() + }; + let unit_id = slab.append(&device, &queue, &unit); + + // Create a bindgroup for the slab so our shader can read out the types. + let label = Some("slabbed isosceles triangle"); + let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label, + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label, + bind_group_layouts: &[&bindgroup_layout], + push_constant_ranges: &[], + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &device.create_shader_module(wgpu::include_spirv!( + "linkage/tutorial-slabbed_render_unit.spv" + )), + entry_point: "tutorial::slabbed_render_unit", + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + module: &device.create_shader_module(wgpu::include_spirv!( + "linkage/tutorial-passthru_fragment.spv" + )), + entry_point: "tutorial::passthru_fragment", + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8UnormSrgb, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + }); + + let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { + label, + layout: &bindgroup_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: slab.get_buffer().as_entire_binding(), + }], + }); + + struct App { + pipeline: wgpu::RenderPipeline, + bindgroup: wgpu::BindGroup, + unit_id: Id, + unit: RenderUnit, + } + + let app = App { + pipeline, + bindgroup, + unit_id, + unit, + }; + r.graph.add_resource(app); + + fn render( + (device, queue, app, frame, depth): ( + View, + View, + View, + View, + View, + ), + ) -> Result<(), GraphError> { + let label = Some("slabbed isosceles triangle"); + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), + store: true, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &depth.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }), + stencil_ops: None, + }), + }); + render_pass.set_pipeline(&app.pipeline); + render_pass.set_bind_group(0, &app.bindgroup, &[]); + render_pass.draw( + 0..app.unit.vertices.len() as u32, + app.unit_id.inner()..app.unit_id.inner() + 1, + ); + } + queue.submit(std::iter::once(encoder.finish())); + Ok(()) + } + + use crate::frame::{clear_frame_and_depth, copy_frame_to_post, create_frame, present}; + r.graph.add_subgraph(graph!( + create_frame + < clear_frame_and_depth + < render + < copy_frame_to_post + < present + )); + + let img = r.render_image().unwrap(); + img_diff::assert_img_eq("tutorial/slabbed_render_unit.png", img); + } + + #[test] + fn slabbed_render_unit_camera() { + let mut r = Renderling::headless(100, 100).unwrap(); + let (device, queue) = r.get_device_and_queue_owned(); + + // Create our geometry on the slab. + // Don't worry too much about capacity, it can grow. + let slab = crate::slab::SlabBuffer::new(&device, 16); + let geometry = vec![ + Vertex { + position: Vec4::new(0.5, -0.5, 0.0, 1.0), + color: Vec4::new(1.0, 0.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(0.0, 0.5, 0.0, 1.0), + color: Vec4::new(0.0, 1.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(-0.5, -0.5, 0.0, 1.0), + color: Vec4::new(0.0, 0.0, 1.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(-1.0, 1.0, 0.0, 1.0), + color: Vec4::new(1.0, 0.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(-1.0, 0.0, 0.0, 1.0), + color: Vec4::new(0.0, 1.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(0.0, 1.0, 0.0, 1.0), + color: Vec4::new(0.0, 0.0, 1.0, 1.0), + ..Default::default() + }, + ]; + let vertices = slab.append_slice(&device, &queue, &geometry); + let (projection, view) = crate::default_ortho2d(100.0, 100.0); + let camera_id = slab.append( + &device, + &queue, + &Camera { + projection, + view, + ..Default::default() + }, + ); + let unit = RenderUnit { + vertices, + camera: camera_id, + position: Vec3::new(50.0, 50.0, 0.0), + scale: Vec3::new(50.0, 50.0, 1.0), + ..Default::default() + }; + let unit_id = slab.append(&device, &queue, &unit); + + // Create a bindgroup for the slab so our shader can read out the types. + let label = Some("slabbed isosceles triangle"); + let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label, + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label, + bind_group_layouts: &[&bindgroup_layout], + push_constant_ranges: &[], + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &device.create_shader_module(wgpu::include_spirv!( + "linkage/tutorial-slabbed_render_unit.spv" + )), + entry_point: "tutorial::slabbed_render_unit", + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + module: &device.create_shader_module(wgpu::include_spirv!( + "linkage/tutorial-passthru_fragment.spv" + )), + entry_point: "tutorial::passthru_fragment", + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8UnormSrgb, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + }); + + let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { + label, + layout: &bindgroup_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: slab.get_buffer().as_entire_binding(), + }], + }); + + struct App { + pipeline: wgpu::RenderPipeline, + bindgroup: wgpu::BindGroup, + unit_id: Id, + unit: RenderUnit, + } + + let app = App { + pipeline, + bindgroup, + unit_id, + unit, + }; + r.graph.add_resource(app); + + fn render( + (device, queue, app, frame, depth): ( + View, + View, + View, + View, + View, + ), + ) -> Result<(), GraphError> { + let label = Some("slabbed isosceles triangle"); + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), + store: true, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &depth.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }), + stencil_ops: None, + }), + }); + render_pass.set_pipeline(&app.pipeline); + render_pass.set_bind_group(0, &app.bindgroup, &[]); + render_pass.draw( + 0..app.unit.vertices.len() as u32, + app.unit_id.inner()..app.unit_id.inner() + 1, + ); + } + queue.submit(std::iter::once(encoder.finish())); + Ok(()) + } + + use crate::frame::{clear_frame_and_depth, copy_frame_to_post, create_frame, present}; + r.graph.add_subgraph(graph!( + create_frame + < clear_frame_and_depth + < render + < copy_frame_to_post + < present + )); + + let img = r.render_image().unwrap(); + img_diff::assert_img_eq("tutorial/slabbed_render_unit_camera.png", img); + } +} diff --git a/crates/renderling/src/ui.rs b/crates/renderling/src/ui.rs index 485bc0f2..507d0f51 100644 --- a/crates/renderling/src/ui.rs +++ b/crates/renderling/src/ui.rs @@ -568,14 +568,13 @@ pub fn setup_ui_render_graph( r.graph.add_resource(pipeline); use crate::{ - frame::{clear_frame_and_depth, create_frame, present}, + frame::{clear_frame_and_depth, copy_frame_to_post, create_frame, present}, graph, Graph, }; let pre_render = crate::graph!(create_frame, clear_frame_and_depth, ui_scene_update).with_barrier(); let render = crate::graph!(ui_scene_render).with_barrier(); let post_render = if with_screen_capture { - let copy_frame_to_post = crate::frame::PostRenderBufferCreate::create; crate::graph!(copy_frame_to_post < present) } else { crate::graph!(present) diff --git a/crates/sandbox/src/main.rs b/crates/sandbox/src/main.rs index 0e285a3f..d3f60f6e 100644 --- a/crates/sandbox/src/main.rs +++ b/crates/sandbox/src/main.rs @@ -1,6 +1,8 @@ use renderling::{ + frame::{clear_frame_and_depth, create_frame, present, FrameTextureView}, + graph::{graph, Graph}, math::{Vec3, Vec4}, - Vertex, Renderling, RenderGraphConfig, + DepthTexture, Device, GraphError, Queue, Renderling, View, }; fn main() { @@ -17,60 +19,148 @@ fn main() { let mut r = Renderling::try_from_window(&window) .unwrap() .with_background_color(Vec3::splat(0.0).extend(1.0)); - let (projection, view) = renderling::default_ortho2d(100.0, 100.0); - let mut builder = r.new_scene().with_camera(projection, view); - let size = 1.0; - let cyan_tri = builder - .new_entity() - .with_meshlet(vec![ - Vertex { - position: Vec4::new(0.0, 0.0, 0.0, 0.0), - color: Vec4::new(0.0, 1.0, 1.0, 1.0), - ..Default::default() - }, - Vertex { - position: Vec4::new(size, 0.0, 0.0, 0.0), - color: Vec4::new(0.0, 1.0, 1.0, 1.0), - ..Default::default() - }, - Vertex { - position: Vec4::new(size, size, 0.0, 0.0), - color: Vec4::new(0.0, 1.0, 1.0, 1.0), - ..Default::default() - }, - ]) - .with_position(Vec3::new(25.0, 25.0, 0.0)) - .with_scale(Vec3::new(25.0, 25.0, 1.0)) - .build(); - let _yellow_tri = builder - .new_entity() - .with_meshlet(vec![ - Vertex { - position: Vec4::new(0.0, 0.0, 0.0, 0.0), - color: Vec4::new(1.0, 1.0, 0.0, 1.0), - ..Default::default() - }, - Vertex { - position: Vec4::new(size, 0.0, 0.0, 0.0), - color: Vec4::new(1.0, 1.0, 0.0, 1.0), - ..Default::default() - }, - Vertex { - position: Vec4::new(size, size, 0.0, 0.0), - color: Vec4::new(1.0, 1.0, 0.0, 1.0), - ..Default::default() - }, - ]) - .with_position(Vec3::new(25.0, 25.0, 0.1)) - .with_parent(&cyan_tri) - .build(); - let scene = builder.build().unwrap(); - //r.setup_render_graph(Some(scene), None, [], false); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - ..Default::default() + //let (projection, view) = renderling::default_ortho2d(100.0, 100.0); + + let (device, _queue) = r.get_device_and_queue_owned(); + //let slab = SlabBuffer::new(&device, 256); + //let vertices = slab.append_slice(&device, &queue, &right_tri_vertices()); + //let (projection, view) = default_ortho2d(100.0, 100.0); + //let camera = slab.append( + // &device, + // &queue, + // &Camera { + // projection, + // view, + // ..Default::default() + // }, + //); + //let unit = slab.append( + // &device, + // &queue, + // &RenderUnit { + // camera, + // vertices, + // ..Default::default() + // }, + //); + + //// Create a bindgroup for the slab so our shader can read out the types. + //let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + // label: Some("slab shader sanity"), + // entries: &[wgpu::BindGroupLayoutEntry { + // binding: 0, + // visibility: wgpu::ShaderStages::VERTEX, + // ty: wgpu::BindingType::Buffer { + // ty: wgpu::BufferBindingType::Storage { read_only: true }, + // has_dynamic_offset: false, + // min_binding_size: None, + // }, + // count: None, + // }], + //}); + //let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + // label: Some("slab shader sanity"), + // bind_group_layouts: &[&bindgroup_layout], + // push_constant_ranges: &[], + //}); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("slab shader sanity"), + layout: None, //Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &device.create_shader_module(wgpu::include_spirv!( + "../../renderling/src/linkage/stage-simple_vertex.spv" + )), + entry_point: "stage::simple_vertex", + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + module: &device.create_shader_module(wgpu::include_spirv!( + "../../renderling/src/linkage/stage-simple_fragment.spv" + )), + entry_point: "stage::simple_fragment", + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Bgra8UnormSrgb, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, }); + //let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { + // label: Some("slab shader sanity"), + // layout: &bindgroup_layout, + // entries: &[wgpu::BindGroupEntry { + // binding: 0, + // resource: slab.get_buffer().as_entire_binding(), + // }], + //}); + + fn render( + (device, queue, pipeline, frame, depth): ( + View, + View, + View, + View, + View, + ), + ) -> Result<(), GraphError> { + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("slab shader sanity"), + }); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("slab shader sanity"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::GREEN), + store: true, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &depth.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }), + stencil_ops: None, + }), + }); + render_pass.set_pipeline(&pipeline); + //render_pass.set_bind_group(0, &bindgroup, &[]); + render_pass.draw(0..3, 0..1); + } + queue.submit(std::iter::once(encoder.finish())); + Ok(()) + } + + r.graph.add_resource(pipeline); + r.graph + .add_subgraph(graph!(create_frame < clear_frame_and_depth < render < present)); + event_loop.run(move |event, _target, control_flow| { *control_flow = winit::event_loop::ControlFlow::Poll; @@ -85,6 +175,9 @@ fn main() { }, .. } => *control_flow = winit::event_loop::ControlFlow::Exit, + winit::event::WindowEvent::Resized(size) => { + r.resize(size.width, size.height); + } _ => {} }, winit::event::Event::MainEventsCleared => { diff --git a/shaders/shader-crate/Cargo.toml b/shaders/shader-crate/Cargo.toml index f3bddc2d..84c83e9e 100644 --- a/shaders/shader-crate/Cargo.toml +++ b/shaders/shader-crate/Cargo.toml @@ -14,4 +14,4 @@ crate-type = ["dylib"] [dependencies] renderling-shader = { path = "../../crates/renderling-shader" } spirv-std = "^0.9" -glam = { version = "0.24.1", default-features = false, features = ["libm"]} +glam = { version = "0.24.2", default-features = false, features = ["libm"]} diff --git a/shaders/src/main.rs b/shaders/src/main.rs index f39af2e9..e30416ee 100644 --- a/shaders/src/main.rs +++ b/shaders/src/main.rs @@ -11,7 +11,7 @@ struct Cli { verbosity: u8, /// Path to the output directory for the compiled shaders. - #[clap(long, short, default_value = "shaders")] + #[clap(long, short, default_value = "../crates/renderling/src/linkage")] output_dir: std::path::PathBuf, } diff --git a/test_img/tutorial/implicit_isosceles_triangle.png b/test_img/tutorial/implicit_isosceles_triangle.png new file mode 100644 index 0000000000000000000000000000000000000000..60ce5ed83902c69a379e0867c8b50110a3ef67c4 GIT binary patch literal 779 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 HHlhCq`R|lp literal 0 HcmV?d00001 diff --git a/test_img/tutorial/slabbed_isosceles_triangle_no_instance.png b/test_img/tutorial/slabbed_isosceles_triangle_no_instance.png new file mode 100644 index 0000000000000000000000000000000000000000..eef29c0b4e9e09bd0158ba9ffc3b6dcf0f53d949 GIT binary patch literal 2763 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 HHlhCq`R|lp literal 0 HcmV?d00001 diff --git a/test_img/tutorial/slabbed_render_unit_camera.png b/test_img/tutorial/slabbed_render_unit_camera.png new file mode 100644 index 0000000000000000000000000000000000000000..4ec80811687dcc8c35a9d95ae0297ec418dfdb76 GIT binary patch literal 3630 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