diff --git a/Cargo.toml b/Cargo.toml index 89a2c405..f368fc8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,8 @@ members = [ "crates/renderling", "crates/renderling-shader", "crates/renderling-gpui", - "crates/renderling-derive", + "crates/crabslab", + "crates/crabslab-derive", ] exclude = ["./shaders"] @@ -14,11 +15,13 @@ exclude = ["./shaders"] resolver = "2" [workspace.dependencies] +async-channel = "1.8" 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.2" +glam = { version = "0.24.2", default-features = false } +snafu = "0.7" winit = { version = "0.27" } wgpu = { version = "0.17" } diff --git a/DEVLOG.md b/DEVLOG.md index 6e355b06..942d6786 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -27,7 +27,7 @@ Debugging rabbit hole: * Maybe print out the brightness texture and make sure it's populated? * Losing steam here, especially since bloom needs to be re-done as "physically based". -TODO: Part out the +### Physically Based Bloom ## Thu Dec 21, 2023 diff --git a/crates/renderling-derive/Cargo.toml b/crates/crabslab-derive/Cargo.toml similarity index 91% rename from crates/renderling-derive/Cargo.toml rename to crates/crabslab-derive/Cargo.toml index 5ed681c9..21dab827 100644 --- a/crates/renderling-derive/Cargo.toml +++ b/crates/crabslab-derive/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "renderling-derive" +name = "crabslab-derive" version = "0.1.0" edition = "2021" diff --git a/crates/renderling-derive/src/lib.rs b/crates/crabslab-derive/src/lib.rs similarity index 88% rename from crates/renderling-derive/src/lib.rs rename to crates/crabslab-derive/src/lib.rs index a7b41a6a..76464ffe 100644 --- a/crates/renderling-derive/src/lib.rs +++ b/crates/crabslab-derive/src/lib.rs @@ -31,7 +31,7 @@ fn get_params(input: &DeriveInput) -> syn::Result { _ => { return Err(syn::Error::new( name.span(), - "deriving Slabbed only supports structs".to_string(), + "deriving SlabItem only supports structs".to_string(), )) } }; @@ -41,7 +41,7 @@ fn get_params(input: &DeriveInput) -> syn::Result { .map(|field| { let ty = &field.ty; quote! { - <#ty as renderling_shader::slab::Slabbed>::slab_size() + <#ty as crabslab::SlabItem>::slab_size() } }) .collect(); @@ -69,7 +69,7 @@ fn get_params(input: &DeriveInput) -> syn::Result { }) } -#[proc_macro_derive(Slabbed)] +#[proc_macro_derive(SlabItem)] pub fn derive_from_slab(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input: DeriveInput = syn::parse_macro_input!(input); let name = &input.ident; @@ -88,8 +88,7 @@ pub fn derive_from_slab(input: proc_macro::TokenStream) -> proc_macro::TokenStre /// Adds a `CanFetch<'lt>` bound on each of the system data types. fn constrain_system_data_types(clause: &mut WhereClause, tys: &[Type]) { for ty in tys.iter() { - let where_predicate: WherePredicate = - syn::parse_quote!(#ty : renderling_shader::slab::Slabbed); + let where_predicate: WherePredicate = syn::parse_quote!(#ty : crabslab::SlabItem); clause.predicates.push(where_predicate); } } @@ -129,9 +128,9 @@ pub fn derive_from_slab(input: proc_macro::TokenStream) -> proc_macro::TokenStre FieldName::Ident(field) => Ident::new(&format!("offset_of_{}", field), field.span()), }; offsets.push(quote! { - pub fn #ident() -> renderling_shader::id::Offset<#ty> { - renderling_shader::id::Offset::new( - #(<#offset_tys as renderling_shader::slab::Slabbed>::slab_size()+)* + pub fn #ident() -> crabslab::Offset<#ty> { + crabslab::Offset::new( + #(<#offset_tys as crabslab::SlabItem>::slab_size()+)* 0 ) } @@ -147,7 +146,7 @@ pub fn derive_from_slab(input: proc_macro::TokenStream) -> proc_macro::TokenStre } #[automatically_derived] - impl #impl_generics renderling_shader::slab::Slabbed for #name #ty_generics #where_clause + impl #impl_generics crabslab::SlabItem for #name #ty_generics #where_clause { fn slab_size() -> usize { #(#sizes)+* diff --git a/crates/crabslab/Cargo.toml b/crates/crabslab/Cargo.toml new file mode 100644 index 00000000..2e618e4d --- /dev/null +++ b/crates/crabslab/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "crabslab" +version = "0.1.0" +edition = "2021" +description = "Slab allocator focused on GPU compute (rust-gpu)" +repository = "https://github.com/schell/renderling" +license = "MIT OR Apache-2.0" +keywords = ["game", "graphics", "shader", "rendering"] +categories = ["rendering", "game-development", "graphics"] +readme = "README.md" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["wgpu", "glam", "futures-lite"] +futures-lite = ["dep:futures-lite"] +glam = ["dep:glam"] +wgpu = ["dep:wgpu", "dep:bytemuck", "dep:snafu", "dep:async-channel", "dep:log"] + +[dependencies] +async-channel = {workspace=true, optional=true} +bytemuck = {workspace=true, optional=true} +futures-lite = {workspace=true, optional=true} +log = {workspace=true, optional=true} +crabslab-derive = { version = "0.1.0", path = "../crabslab-derive" } +snafu = {workspace=true, optional=true} +wgpu = {workspace=true, optional=true} + +[target.'cfg(not(target_arch = "spirv"))'.dependencies] +glam = { workspace = true, features = ["std"], optional = true } + +[target.'cfg(target_arch = "spirv")'.dependencies] +glam = { version = "0.24.2", default-features = false, features = ["libm"], optional = true } diff --git a/crates/crabslab/README.md b/crates/crabslab/README.md new file mode 100644 index 00000000..5756011b --- /dev/null +++ b/crates/crabslab/README.md @@ -0,0 +1,34 @@ +
+ slabcraft for crabs +
+ +## what +`crabslab` is a slab implementation focused on marshalling data from CPUs to GPUs. + +## why +### Opinion +Working with shaders is much easier using a slab. + +### rust-gpu +This crate was made to work with [`rust-gpu`](https://github.com/EmbarkStudios/rust-gpu/). +Specifically, using this crate it is possible to pack your types into a buffer on the CPU +and then read your types from the slab on the GPU (in Rust). + +### Other no-std platforms +Even though this crate was written with `rust-gpu` in mind, it should work in other `no-std` +contexts. + +## how +`crabslab` includes: +* a few traits: + - `Slab` + - `GrowableSlab` + - `SlabItem` +* a derive macro for `SlabItem` +* a few structs for working with various slabs + - `Id` + - `Array` + - `Offset` +* a helper struct `CpuSlab` +* a feature-gated helper for using slabs with `wgpu` - `WgpuBuffer` + - [example](src/wgpu_slab.rs#L344) diff --git a/crates/crabslab/crabslab.png b/crates/crabslab/crabslab.png new file mode 100644 index 00000000..454cce23 Binary files /dev/null and b/crates/crabslab/crabslab.png differ diff --git a/crates/renderling-shader/src/array.rs b/crates/crabslab/src/array.rs similarity index 89% rename from crates/renderling-shader/src/array.rs rename to crates/crabslab/src/array.rs index 34088471..9d97af11 100644 --- a/crates/renderling-shader/src/array.rs +++ b/crates/crabslab/src/array.rs @@ -1,8 +1,7 @@ //! A slab-allocated array. use core::marker::PhantomData; -use crate::id::Id; -use crate::slab::Slabbed; +use crate::{id::Id, slab::SlabItem}; #[derive(Clone, Copy)] pub struct ArrayIter { @@ -10,7 +9,7 @@ pub struct ArrayIter { index: usize, } -impl Iterator for ArrayIter { +impl Iterator for ArrayIter { type Item = Id; fn next(&mut self) -> Option { @@ -24,6 +23,7 @@ impl Iterator for ArrayIter { } } +/// A pointer to contiguous `T` elements in a slab. #[repr(C)] pub struct Array { // u32 offset in the slab @@ -59,9 +59,12 @@ impl From> for Array { impl core::fmt::Debug for Array { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { if self.is_null() { - f.write_fmt(format_args!("Array<{}>(null)", core::any::type_name::())) + f.write_fmt(core::format_args!( + "Array<{}>(null)", + core::any::type_name::() + )) } else { - f.write_fmt(format_args!( + f.write_fmt(core::format_args!( "Array<{}>({}, {})", core::any::type_name::(), self.index, @@ -77,7 +80,7 @@ impl PartialEq for Array { } } -impl Slabbed for Array { +impl SlabItem for Array { fn slab_size() -> usize { 2 } @@ -103,7 +106,7 @@ impl Slabbed for Array { } } -impl Default for Array { +impl Default for Array { fn default() -> Self { Self { index: u32::MAX, @@ -140,7 +143,7 @@ impl Array { pub fn at(&self, index: usize) -> Id where - T: Slabbed, + T: SlabItem, { if index >= self.len() { Id::NONE @@ -163,7 +166,7 @@ impl Array { /// Convert this array into a `u32` array. pub fn into_u32_array(self) -> Array where - T: Slabbed, + T: SlabItem, { Array { index: self.index, @@ -176,7 +179,7 @@ impl Array { /// Return the slice of the slab that this array represents. pub fn sub_slab<'a>(&'a self, slab: &'a [u32]) -> &[u32] where - T: Slabbed, + T: SlabItem, { let arr = self.into_u32_array(); &slab[arr.index as usize..(arr.index + arr.len) as usize] diff --git a/crates/renderling-shader/src/id.rs b/crates/crabslab/src/id.rs similarity index 84% rename from crates/renderling-shader/src/id.rs rename to crates/crabslab/src/id.rs index 0bd379aa..7ca1444d 100644 --- a/crates/renderling-shader/src/id.rs +++ b/crates/crabslab/src/id.rs @@ -1,13 +1,13 @@ //! Typed identifiers that can also be used as indices. use core::marker::PhantomData; -use crate::{self as renderling_shader, slab::Slabbed}; +use crate::{self as crabslab, slab::SlabItem}; pub const ID_NONE: u32 = u32::MAX; -/// An identifier. +/// An identifier that can be used to read or write a type from/into the slab. #[repr(transparent)] -#[derive(bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(SlabItem)] pub struct Id(pub(crate) u32, PhantomData); impl PartialOrd for Id { @@ -72,9 +72,12 @@ impl Default for Id { impl core::fmt::Debug for Id { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { if self.is_none() { - f.write_fmt(format_args!("Id<{}>(null)", &core::any::type_name::(),)) + f.write_fmt(core::format_args!( + "Id<{}>(null)", + &core::any::type_name::(), + )) } else { - f.write_fmt(format_args!( + f.write_fmt(core::format_args!( "Id<{}>({})", &core::any::type_name::(), &self.0 @@ -143,12 +146,12 @@ impl Id { /// The offset of a field relative a parent's `Id`. /// -/// Offset functions are automatically derived for `Slabbed` structs. +/// Offset functions are automatically derived for `SlabItem` structs. /// /// ```rust -/// use renderling_shader::{id::{Id, Offset}, slab::{Slab, Slabbed}}; +/// use crabslab::{Id, Offset, Slab, SlabItem}; /// -/// #[derive(Debug, Default, PartialEq, Slabbed)] +/// #[derive(Debug, Default, PartialEq, SlabItem)] /// pub struct Parent { /// pub child_a: u32, /// pub child_b: u32, @@ -157,7 +160,10 @@ impl Id { /// let mut slab = [0u32; 10]; /// /// let parent_id = Id::new(3); -/// let parent = Parent{ child_a: 0, child_b: 1 }; +/// let parent = Parent { +/// child_a: 0, +/// child_b: 1, +/// }; /// slab.write(parent_id, &parent); /// assert_eq!(parent, slab.read(parent_id)); /// @@ -203,15 +209,20 @@ impl Offset { #[cfg(test)] mod test { - use crate::stage::GpuEntity; - use super::*; + #[derive(SlabItem)] + struct MyEntity { + name: u32, + age: f32, + destiny: [u32; 3], + } + #[test] fn id_size() { assert_eq!( std::mem::size_of::(), - std::mem::size_of::>(), + std::mem::size_of::>(), "id is not u32" ); } diff --git a/crates/crabslab/src/lib.rs b/crates/crabslab/src/lib.rs new file mode 100644 index 00000000..22aa6848 --- /dev/null +++ b/crates/crabslab/src/lib.rs @@ -0,0 +1,18 @@ +#![cfg_attr(target_arch = "spirv", no_std)] +//! Creating and crafting a tasty slab of memory. + +mod array; +pub use array::*; + +mod id; +pub use id::*; + +mod slab; +pub use slab::*; + +#[cfg(feature = "wgpu")] +mod wgpu_slab; +#[cfg(feature = "wgpu")] +pub use wgpu_slab::*; + +pub use crabslab_derive::SlabItem; diff --git a/crates/renderling-shader/src/slab.rs b/crates/crabslab/src/slab.rs similarity index 62% rename from crates/renderling-shader/src/slab.rs rename to crates/crabslab/src/slab.rs index c6b0a899..1a90c36d 100644 --- a/crates/renderling-shader/src/slab.rs +++ b/crates/crabslab/src/slab.rs @@ -1,18 +1,17 @@ -//! Slab storage and operations. -use core::marker::PhantomData; - -pub use renderling_derive::Slabbed; +//! Slab traits. +use core::{default::Default, marker::PhantomData}; +pub use crabslab_derive::SlabItem; use crate::{array::Array, id::Id}; /// Determines the "size" of a type when stored in a slab of `&[u32]`, /// and how to read/write it from/to the slab. /// -/// `Slabbed` can be automatically derived for struct and tuple types, +/// `SlabItem` can be automatically derived for struct and tuple types, /// so long as those types are relatively simple. So far, autoderiving /// fields with these types will **not compile** on one or more targets: /// * `PhantomData` - will not compile on `target_arch = "spirv"` -pub trait Slabbed: core::any::Any + Sized { +pub trait SlabItem: core::any::Any + Sized { /// The number of `u32`s this type occupies in a slab of `&[u32]`. fn slab_size() -> usize; @@ -31,7 +30,7 @@ pub trait Slabbed: core::any::Any + Sized { fn write_slab(&self, index: usize, slab: &mut [u32]) -> usize; } -impl Slabbed for bool { +impl SlabItem for bool { fn slab_size() -> usize { 1 } @@ -48,7 +47,7 @@ impl Slabbed for bool { } } -impl Slabbed for u32 { +impl SlabItem for u32 { fn slab_size() -> usize { 1 } @@ -72,7 +71,7 @@ impl Slabbed for u32 { } } -impl Slabbed for f32 { +impl SlabItem for f32 { fn slab_size() -> usize { 1 } @@ -96,7 +95,7 @@ impl Slabbed for f32 { } } -impl Slabbed for Option { +impl SlabItem for Option { fn slab_size() -> usize { 1 + T::slab_size() } @@ -126,9 +125,9 @@ impl Slabbed for Option { } } -impl Slabbed for [T; N] { +impl SlabItem for [T; N] { fn slab_size() -> usize { - ::slab_size() * N + ::slab_size() * N } fn read_slab(&mut self, mut index: usize, slab: &[u32]) -> usize { @@ -146,7 +145,8 @@ impl Slabbed for [T; N] { } } -impl Slabbed for glam::Mat4 { +#[cfg(feature = "glam")] +impl SlabItem for glam::Mat4 { fn read_slab(&mut self, index: usize, slab: &[u32]) -> usize { let Self { x_axis, @@ -178,7 +178,8 @@ impl Slabbed for glam::Mat4 { } } -impl Slabbed for glam::Vec2 { +#[cfg(feature = "glam")] +impl SlabItem for glam::Vec2 { fn slab_size() -> usize { 2 } @@ -199,7 +200,7 @@ impl Slabbed for glam::Vec2 { } } -impl Slabbed for glam::Vec3 { +impl SlabItem for glam::Vec3 { fn slab_size() -> usize { 3 } @@ -221,7 +222,8 @@ impl Slabbed for glam::Vec3 { } } -impl Slabbed for glam::Vec4 { +#[cfg(feature = "glam")] +impl SlabItem for glam::Vec4 { fn slab_size() -> usize { 4 } @@ -242,7 +244,7 @@ impl Slabbed for glam::Vec4 { } } -impl Slabbed for glam::Quat { +impl SlabItem for glam::Quat { fn slab_size() -> usize { 16 } @@ -264,7 +266,8 @@ impl Slabbed for glam::Quat { } } -impl Slabbed for glam::UVec2 { +#[cfg(feature = "glam")] +impl SlabItem for glam::UVec2 { fn slab_size() -> usize { 2 } @@ -282,7 +285,8 @@ impl Slabbed for glam::UVec2 { } } -impl Slabbed for glam::UVec3 { +#[cfg(feature = "glam")] +impl SlabItem for glam::UVec3 { fn slab_size() -> usize { 3 } @@ -302,7 +306,8 @@ impl Slabbed for glam::UVec3 { } } -impl Slabbed for glam::UVec4 { +#[cfg(feature = "glam")] +impl SlabItem for glam::UVec4 { fn slab_size() -> usize { 4 } @@ -327,7 +332,7 @@ impl Slabbed for glam::UVec4 { } } -impl Slabbed for PhantomData { +impl SlabItem for PhantomData { fn slab_size() -> usize { 0 } @@ -347,15 +352,15 @@ pub trait Slab { fn len(&self) -> usize; /// Returns whether the slab may contain the value with the given id. - fn contains(&self, id: Id) -> bool { + 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; + fn read(&self, id: Id) -> T; #[cfg(not(target_arch = "spirv"))] - fn read_vec(&self, array: crate::array::Array) -> Vec { + 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); @@ -366,32 +371,34 @@ pub trait Slab { /// Write the type into the slab at the index. /// - /// Return the next index, or the same index if writing would overlap the slab. - fn write_indexed(&mut self, t: &T, index: usize) -> usize; + /// Return the next index, or the same index if writing would overlap the + /// slab. + fn write_indexed(&mut self, t: &T, index: usize) -> usize; /// Write a slice of the type into the slab at the index. /// - /// Return the next index, or the same index if writing would overlap the slab. - fn write_indexed_slice(&mut self, t: &[T], index: usize) -> usize; + /// Return the next index, or the same index if writing would overlap the + /// slab. + fn write_indexed_slice(&mut self, t: &[T], index: usize) -> usize; /// Write the type into the slab at the position of the given `Id`. /// /// This likely performs a partial write if the given `Id` is out of bounds. - fn write(&mut self, id: Id, t: &T) { + fn write(&mut self, id: Id, t: &T) { let _ = self.write_indexed(t, id.index()); } - /// Write contiguous elements into the slab at the position of the given `Array`. + /// Write contiguous elements into the slab at the position of the given + /// `Array`. /// /// ## NOTE - /// This does nothing if the length of `Array` is greater than the length of `data`. - fn write_array(&mut self, array: Array, data: &[T]) { + /// This does nothing if the length of `Array` is greater than the length of + /// `data`. + fn write_array(&mut self, array: Array, data: &[T]) { if array.len() > data.len() { return; } - for i in 0..array.len() { - let _ = self.write(array.at(i), &data[i]); - } + let _ = self.write_indexed_slice(data, array.starting_index()); } } @@ -400,17 +407,17 @@ impl Slab for [u32] { self.len() } - fn read(&self, id: Id) -> T { + fn read(&self, id: Id) -> T { let mut t = T::default(); let _ = t.read_slab(id.index(), self); t } - fn write_indexed(&mut self, t: &T, index: usize) -> usize { + fn write_indexed(&mut self, t: &T, index: usize) -> usize { t.write_slab(index, self) } - fn write_indexed_slice(&mut self, t: &[T], index: usize) -> usize { + fn write_indexed_slice(&mut self, t: &[T], index: usize) -> usize { let mut index = index; for item in t { index = item.write_slab(index, self); @@ -425,27 +432,196 @@ impl Slab for Vec { self.len() } - fn read(&self, id: Id) -> T { + fn read(&self, id: Id) -> T { self.as_slice().read(id) } - fn write_indexed(&mut self, t: &T, index: usize) -> usize { + fn write_indexed(&mut self, t: &T, index: usize) -> usize { self.as_mut_slice().write_indexed(t, index) } - fn write_indexed_slice(&mut self, t: &[T], index: usize) -> usize { + fn write_indexed_slice(&mut self, t: &[T], index: usize) -> usize { self.as_mut_slice().write_indexed_slice(t, index) } } +/// Trait for slabs of `u32`s that can store many types, and can grow to fit. +pub trait GrowableSlab: Slab { + /// Return the current capacity of the slab. + fn capacity(&self) -> usize; + + /// Reserve enough space on the slab to fit the given capacity. + fn reserve_capacity(&mut self, capacity: usize); + + /// Increment the length of the slab by `n` u32s. + /// + /// Returns the previous length. + fn increment_len(&mut self, n: usize) -> usize; + + + /// Expands the slab to fit the given number of `T`s, if necessary. + fn maybe_expand_to_fit(&mut self, len: usize) { + let size = T::slab_size(); + let capacity = self.capacity(); + //log::trace!( + // "append_slice: {size} * {ts_len} + {len} ({}) >= {capacity}", + // size * ts_len + len + //); + let capacity_needed = self.len() + size * len; + if capacity_needed > capacity { + let mut new_capacity = capacity * 2; + while new_capacity < capacity_needed { + new_capacity = (new_capacity * 2).max(2); + } + self.reserve_capacity(new_capacity); + } + } + + /// Preallocate space for one `T` element, but don't write anything to the + /// buffer. + /// + /// The returned `Id` can be used to write later with [`Self::write`]. + /// + /// NOTE: This changes the next available buffer index and may change the + /// buffer capacity. + fn allocate(&mut self) -> Id { + self.maybe_expand_to_fit::(1); + let index = self.increment_len(T::slab_size()); + Id::from(index) + } + + /// Preallocate space for `len` `T` elements, but don't write to + /// the buffer. + /// + /// This can be used to allocate space for a bunch of elements that get + /// written later with [`Self::write_array`]. + /// + /// NOTE: This changes the length of the buffer and may change the capacity. + fn allocate_array(&mut self, len: usize) -> Array { + if len == 0 { + return Array::default(); + } + self.maybe_expand_to_fit::(len); + let index = self.increment_len(T::slab_size() * len); + Array::new(index as u32, len as u32) + } + + /// Append to the end of the buffer. + /// + /// Returns the `Id` of the written element. + fn append(&mut self, t: &T) -> Id { + let id = self.allocate::(); + // IGNORED: safe because we just allocated the id + let _ = self.write(id, t); + id + } + + /// Append a slice to the end of the buffer, resizing if necessary + /// and returning a slabbed array. + /// + /// Returns the `Array` of the written elements. + fn append_array(&mut self, ts: &[T]) -> Array { + let array = self.allocate_array::(ts.len()); + // IGNORED: safe because we just allocated the array + let _ = self.write_array(array, ts); + array + } +} + +/// A wrapper around a `GrowableSlab` that provides convenience methods for +/// working with CPU-side slabs. +/// +/// Working with slabs on the CPU is much more convenient because the underlying +/// buffer `B` is often a growable type, like `Vec`. This wrapper provides +/// methods for appending to the end of the buffer with automatic resizing and +/// for preallocating space for elements that will be written later. +pub struct CpuSlab { + slab: B, +} + +impl AsRef for CpuSlab { + fn as_ref(&self) -> &B { + &self.slab + } +} + +impl AsMut for CpuSlab { + fn as_mut(&mut self) -> &mut B { + &mut self.slab + } +} + +impl Slab for CpuSlab { + fn len(&self) -> usize { + self.slab.len() + } + + fn read(&self, id: Id) -> T { + self.slab.read(id) + } + + fn write_indexed(&mut self, t: &T, index: usize) -> usize { + self.slab.write_indexed(t, index) + } + + fn write_indexed_slice(&mut self, t: &[T], index: usize) -> usize { + self.slab.write_indexed_slice(t, index) + } +} + +impl GrowableSlab for CpuSlab { + fn capacity(&self) -> usize { + self.slab.capacity() + } + + fn reserve_capacity(&mut self, capacity: usize) { + self.slab.reserve_capacity(capacity); + } + + fn increment_len(&mut self, n: usize) -> usize { + self.slab.increment_len(n) + } +} + +impl CpuSlab { + /// Create a new `SlabBuffer` with the given slab. + pub fn new(slab: B) -> Self { + Self { slab } + } +} + +#[cfg(not(target_arch = "spirv"))] +impl GrowableSlab for Vec { + fn capacity(&self) -> usize { + Vec::capacity(self) + } + + fn reserve_capacity(&mut self, capacity: usize) { + Vec::reserve(self, capacity - self.capacity()); + } + + fn increment_len(&mut self, n: usize) -> usize { + let index = self.len(); + self.extend(core::iter::repeat(0).take(n)); + index + } +} + #[cfg(test)] mod test { use glam::Vec4; - use crate::{array::Array, stage::Vertex}; + use crate::{self as crabslab, Array, CpuSlab, SlabItem}; use super::*; + #[derive(Debug, Default, PartialEq, SlabItem)] + struct Vertex { + position: Vec4, + color: Vec4, + uv: glam::Vec2, + } + #[test] fn slab_array_readwrite() { let mut slab = [0u32; 16]; @@ -513,4 +689,27 @@ mod test { let array = slab.read(vertices_id); assert_eq!(vertices, array); } + + #[test] + fn cpuslab_sanity() { + let mut slab = CpuSlab::new(vec![]); + let v = 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() + }; + let id = slab.append(&v); + assert_eq!(Id::new(0), id); + assert_eq!(v, slab.read(id)); + + let f32s = [1.1, 2.2, 3.3, 4.4f32]; + let array = slab.append_array(&f32s); + assert_eq!(1.1, slab.read(array.at(0))); + assert_eq!(2.2, slab.read(array.at(1))); + assert_eq!(3.3, slab.read(array.at(2))); + assert_eq!(4.4, slab.read(array.at(3))); + + let f32_vec = slab.read_vec(array); + assert_eq!(f32s, f32_vec[..]); + } } diff --git a/crates/crabslab/src/wgpu_slab.rs b/crates/crabslab/src/wgpu_slab.rs new file mode 100644 index 00000000..ec7af5f4 --- /dev/null +++ b/crates/crabslab/src/wgpu_slab.rs @@ -0,0 +1,376 @@ +//! CPU side of slab storage using `wgpu`. +use std::{ + ops::Deref, + sync::{atomic::AtomicUsize, Arc}, +}; + +use crate::{GrowableSlab, Id, Slab, SlabItem}; +use snafu::{IntoError, ResultExt, Snafu}; + +#[derive(Debug, Snafu)] +pub enum SlabError { + #[snafu(display( + "Out of capacity. Tried to write {type_is}(slab size={slab_size}) at {index} but capacity \ + is {capacity}", + ))] + Capacity { + type_is: &'static str, + slab_size: usize, + index: usize, + capacity: usize, + }, + + #[snafu(display( + "Out of capacity. Tried to write an array of {elements} {type_is}(each of slab \ + size={slab_size}) at {index} but capacity is {capacity}", + ))] + ArrayCapacity { + type_is: &'static str, + elements: usize, + slab_size: usize, + index: usize, + capacity: usize, + }, + + #[snafu(display( + "Array({type_is}) length mismatch. Tried to write {data_len} elements into array of \ + length {array_len}", + ))] + ArrayLen { + type_is: &'static str, + array_len: usize, + data_len: usize, + }, + + #[snafu(display("Async recv error: {source}"))] + AsyncRecv { source: async_channel::RecvError }, + + #[snafu(display("Async error: {source}"))] + Async { source: wgpu::BufferAsyncError }, +} + +pub fn print_slab(slab: &[u32], starting_index: usize) { + for (u, i) in slab.iter().zip(starting_index..) { + println!("{i:02}: {u:032b} {u:010} {:?}", f32::from_bits(*u)); + } +} + +/// A slab buffer used by the stage to store heterogeneous objects. +pub struct WgpuBuffer { + pub(crate) buffer: wgpu::Buffer, + device: Arc, + queue: Arc, + // The number of u32 elements currently stored in the buffer. + // + // This is the next index to write into. + len: AtomicUsize, + // The total number of u32 elements that can be stored in the buffer. + capacity: AtomicUsize, +} + +impl Slab for WgpuBuffer { + fn len(&self) -> usize { + self.len.load(std::sync::atomic::Ordering::Relaxed) + } + + fn read(&self, id: Id) -> T { + futures_lite::future::block_on(self.read_async(id)).unwrap() + } + + fn write_indexed(&mut self, t: &T, index: usize) -> usize { + let byte_offset = index * std::mem::size_of::(); + let size = T::slab_size(); + let mut bytes = vec![0u32; size]; + let _ = bytes.write_indexed(t, 0); + let capacity = self.capacity(); + if index + size > capacity { + log::error!( + "could not write to slab: {}", + CapacitySnafu { + type_is: std::any::type_name::(), + slab_size: T::slab_size(), + index, + capacity + } + .into_error(snafu::NoneError) + ); + return index; + } + let encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + self.queue.write_buffer( + &self.buffer, + byte_offset as u64, + bytemuck::cast_slice(bytes.as_slice()), + ); + self.queue.submit(std::iter::once(encoder.finish())); + + index + size + } + + fn write_indexed_slice(&mut self, t: &[T], index: usize) -> usize { + let capacity = self.capacity(); + let size = T::slab_size() * t.len(); + if index + size > capacity { + log::error!( + "could not write array to slab: {}", + ArrayCapacitySnafu { + capacity, + type_is: std::any::type_name::(), + elements: t.len(), + slab_size: T::slab_size(), + index + } + .into_error(snafu::NoneError) + ); + return index; + } + let encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + let mut u32_data = vec![0u32; size]; + let _ = u32_data.write_indexed_slice(t, 0); + let byte_offset = index * std::mem::size_of::(); + self.queue.write_buffer( + &self.buffer, + byte_offset as u64, + bytemuck::cast_slice(&u32_data), + ); + self.queue.submit(std::iter::once(encoder.finish())); + + index + size + } +} + +impl GrowableSlab for WgpuBuffer { + fn capacity(&self) -> usize { + self.capacity.load(std::sync::atomic::Ordering::Relaxed) + } + + /// Resize the slab buffer. + /// + /// This creates a new buffer and writes the data from the old into the new. + fn reserve_capacity(&mut self, 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 new_buffer = Self::new_buffer(&self.device, new_capacity, self.buffer.usage()); + let mut encoder = self + .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, + 0, + &new_buffer, + 0, + (len * std::mem::size_of::()) as u64, + ); + self.queue.submit(std::iter::once(encoder.finish())); + self.buffer = new_buffer; + self.capacity + .store(new_capacity, std::sync::atomic::Ordering::Relaxed); + } + } + + fn increment_len(&mut self, n: usize) -> usize { + self.len.fetch_add(n, std::sync::atomic::Ordering::Relaxed) + } +} + +impl WgpuBuffer { + 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 + | usage, + mapped_at_creation: false, + }) + } + + /// Create a new slab buffer with a capacity of `capacity` u32 elements. + pub fn new( + device: impl Into>, + queue: impl Into>, + capacity: usize, + ) -> Self { + Self::new_usage(device, queue, capacity, wgpu::BufferUsages::empty()) + } + + /// Create a new slab buffer with a capacity of `capacity` u32 elements. + pub fn new_usage( + device: impl Into>, + queue: impl Into>, + capacity: usize, + usage: wgpu::BufferUsages, + ) -> Self { + let device = device.into(); + let queue = queue.into(); + Self { + buffer: Self::new_buffer(&device, capacity, usage), + len: AtomicUsize::new(0).into(), + capacity: AtomicUsize::new(capacity).into(), + device, + queue, + } + } + + #[cfg(feature = "futures-lite")] + /// Read from the slab buffer synchronously. + pub fn block_on_read_raw(&self, start: usize, len: usize) -> Result, SlabError> { + futures_lite::future::block_on(self.read_raw(start, len)) + } + + /// Read from the slab buffer. + pub async fn read_raw(&self, 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 = self.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("SlabBuffer::read_raw"), + size: output_buffer_size, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + log::trace!( + "copy_buffer_to_buffer byte_offset:{byte_offset}, \ + output_buffer_size:{output_buffer_size}", + ); + encoder.copy_buffer_to_buffer( + &self.buffer, + byte_offset as u64, + &output_buffer, + 0, + output_buffer_size, + ); + self.queue.submit(std::iter::once(encoder.finish())); + + let buffer_slice = output_buffer.slice(..); + let (tx, rx) = async_channel::bounded(1); + buffer_slice.map_async(wgpu::MapMode::Read, move |res| tx.try_send(res).unwrap()); + self.device.poll(wgpu::Maintain::Wait); + rx.recv() + .await + .context(AsyncRecvSnafu)? + .context(AsyncSnafu)?; + let bytes = buffer_slice.get_mapped_range(); + Ok(bytemuck::cast_slice(bytes.deref()).to_vec()) + } + + /// Read from the slab buffer. + pub async fn read_async(&self, id: Id) -> Result { + let vec = self.read_raw(id.index(), T::slab_size()).await?; + let t = Slab::read(vec.as_slice(), Id::::new(0)); + Ok(t) + } + + /// Get the underlying buffer. + pub fn get_buffer(&self) -> &wgpu::Buffer { + &self.buffer + } +} + +#[cfg(test)] +mod test { + use crate::CpuSlab; + + use super::*; + + fn get_device_and_queue() -> (wgpu::Device, wgpu::Queue) { + // The instance is a handle to our GPU + // BackendBit::PRIMARY => Vulkan + Metal + DX12 + Browser WebGPU + let backends = wgpu::Backends::all(); + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends, + dx12_shader_compiler: wgpu::Dx12Compiler::default(), + }); + + let limits = wgpu::Limits::default(); + + let adapter = futures_lite::future::block_on(instance.request_adapter( + &wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + compatible_surface: None, + force_fallback_adapter: false, + }, + )) + .unwrap(); + + let info = adapter.get_info(); + log::trace!( + "using adapter: '{}' backend:{:?} driver:'{}'", + info.name, + info.backend, + info.driver + ); + + futures_lite::future::block_on(adapter.request_device( + &wgpu::DeviceDescriptor { + features: + // this one is a funny requirement, it seems it is needed if + // using storage buffers in vertex shaders, even if those + // shaders are read-only + wgpu::Features::VERTEX_WRITABLE_STORAGE, + limits, + label: None, + }, + None, // Trace path + )) + .unwrap() + } + + #[test] + fn slab_buffer_roundtrip() { + let (device, queue) = get_device_and_queue(); + let buffer = WgpuBuffer::new(device, queue, 2); + let mut slab = CpuSlab::new(buffer); + slab.append(&42); + slab.append(&1); + let id = Id::<[u32; 2]>::new(0); + let t = slab.read(id); + assert_eq!([42, 1], t, "read back what we wrote"); + + println!("overflow"); + let id = Id::::new(2); + slab.write(id, &666); + assert_eq!(2, slab.len()); + + println!("append"); + slab.append(&666); + let id = Id::<[u32; 3]>::new(0); + let t = slab.read(id); + 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_array(&points); + let slab_u32 = + futures_lite::future::block_on(slab.as_ref().read_raw(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_array(&points); + let slab_u32 = + futures_lite::future::block_on(slab.as_ref().read_raw(0, slab.len())).unwrap(); + let points_out = slab_u32.read_vec::(array); + assert_eq!(points, points_out); + } +} diff --git a/crates/img-diff/src/lib.rs b/crates/img-diff/src/lib.rs index c6b93f01..98220bf1 100644 --- a/crates/img-diff/src/lib.rs +++ b/crates/img-diff/src/lib.rs @@ -6,6 +6,21 @@ use std::path::Path; const TEST_IMG_DIR: &str = "../../test_img"; const TEST_OUTPUT_DIR: &str = "../../test_output"; +const PIXEL_MAGNITUDE_THRESHOLD: f32 = 0.1; +const IMAGE_DIFF_THRESHOLD: f32 = 0.05; + +fn checkerboard_background_color(x: u32, y: u32) -> Vec4 { + let size = 16; + let x_square_index = x / size; + let x_grey = x_square_index % 2 == 0; + let y_square_index = y / size; + let y_grey = y_square_index % 2 == 0; + if (x_grey && y_grey) || (!x_grey && !y_grey) { + Vec4::from([0.5, 0.5, 0.5, 1.0]) + } else { + Vec4::from([1.0, 1.0, 1.0, 1.0]) + } +} #[derive(Debug, Snafu)] enum ImgDiffError { @@ -13,6 +28,27 @@ enum ImgDiffError { ImageSize, } +pub struct DiffCfg { + /// The threshold for a pixel to be considered different. + /// + /// Difference is measured as the magnitude of vector subtraction + /// between the two pixels. + pub pixel_threshold: f32, + /// The percentage of "different" pixels (as determined using + /// `pixel_threshold`) to "correct" pixels that the image must contain + /// before it is considered an error. + pub image_threshold: f32, +} + +impl Default for DiffCfg { + fn default() -> Self { + Self { + pixel_threshold: PIXEL_MAGNITUDE_THRESHOLD, + image_threshold: IMAGE_DIFF_THRESHOLD, + } + } +} + fn get_results( left_image: &Rgba32FImage, right_image: &Rgba32FImage, @@ -29,11 +65,14 @@ fn get_results( if left_pixel == right_pixel { None } else { + // pre-multiply alpha let left_pixel = Vec4::from(left_pixel.0); + let left_pixel = (left_pixel * left_pixel.w).xyz(); let right_pixel = Vec4::from(right_pixel.0); + let right_pixel = (right_pixel * right_pixel.w).xyz(); let delta = (left_pixel - right_pixel).abs(); if delta.length() > threshold { - Some((x, y, delta)) + Some((x, y, delta.extend(1.0))) } else { None } @@ -47,8 +86,22 @@ fn get_results( } else { let mut output_image = image::ImageBuffer::from_pixel(width, height, Rgba([0.0, 0.0, 0.0, 0.0])); + + for x in 0..width { + for y in 0..height { + output_image.put_pixel(x, y, Rgba(checkerboard_background_color(x, y).into())); + } + } + for (x, y, delta) in results { - let color = delta.xyz().extend(1.0); + let bg = checkerboard_background_color(x, y); + let a = 1.0 - delta.z; + let color = Vec4::new( + bg.x * a + delta.x, + bg.y * a + delta.y, + bg.z * a + delta.z, + 1.0, + ); output_image.put_pixel(x, y, Rgba(color.into())); } Ok(Some((diffs, output_image))) @@ -61,11 +114,28 @@ pub fn save(filename: &str, seen: impl Into) { seen.into().save(path).unwrap(); } -pub fn assert_eq(filename: &str, lhs: impl Into, rhs: impl Into) { +pub fn assert_eq_cfg( + filename: &str, + lhs: impl Into, + rhs: impl Into, + cfg: DiffCfg, +) { let lhs = lhs.into(); let lhs = lhs.into_rgba32f(); let rhs = rhs.into().into_rgba32f(); - if let Some((diffs, diff_image)) = get_results(&lhs, &rhs, 0.5).unwrap() { + let DiffCfg { + pixel_threshold, + image_threshold, + } = cfg; + if let Some((diffs, diff_image)) = get_results(&lhs, &rhs, pixel_threshold).unwrap() { + println!("{filename} has {diffs} pixel differences (threshold={pixel_threshold})"); + let percent_diff = diffs as f32 / (lhs.width() * lhs.height()) as f32; + println!("{filename}'s image is {percent_diff} different (threshold={image_threshold})"); + if percent_diff < image_threshold { + return; + } + + let mut dir = Path::new(TEST_OUTPUT_DIR).join(filename); dir.set_extension(""); std::fs::create_dir_all(&dir).expect("cannot create test output dir"); @@ -93,7 +163,11 @@ pub fn assert_eq(filename: &str, lhs: impl Into, rhs: impl Into) { +pub fn assert_eq(filename: &str, lhs: impl Into, rhs: impl Into) { + assert_eq_cfg(filename, lhs, rhs, DiffCfg::default()) +} + +pub fn assert_img_eq_cfg(filename: &str, seen: impl Into, cfg: DiffCfg) { let cwd = std::env::current_dir().expect("no cwd"); let lhs = image::open(Path::new(TEST_IMG_DIR).join(filename)).unwrap_or_else(|_| { panic!( @@ -101,5 +175,9 @@ pub fn assert_img_eq(filename: &str, seen: impl Into) { cwd.join(filename).display() ) }); - assert_eq(filename, lhs, seen) + assert_eq_cfg(filename, lhs, seen, cfg) +} + +pub fn assert_img_eq(filename: &str, seen: impl Into) { + assert_img_eq_cfg(filename, seen, DiffCfg::default()) } diff --git a/crates/loading-bytes/Cargo.toml b/crates/loading-bytes/Cargo.toml index da4ce86a..07df88fa 100644 --- a/crates/loading-bytes/Cargo.toml +++ b/crates/loading-bytes/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] async-fs = "^1.6" js-sys = "^0.3" -snafu = "^0.7" +snafu = {workspace = true} wasm-bindgen = "^0.2" wasm-bindgen-futures = "^0.4" web-sys = { version = "^0.3", features = ["Request", "RequestInit", "Response"] } \ No newline at end of file diff --git a/crates/renderling-shader/Cargo.toml b/crates/renderling-shader/Cargo.toml index 777e9ac0..27529f16 100644 --- a/crates/renderling-shader/Cargo.toml +++ b/crates/renderling-shader/Cargo.toml @@ -6,22 +6,25 @@ description = "Shared types and functions for renderling shaders compiled w/ rus # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["lib", "dylib"] + [features] default = [] [dependencies] -bytemuck = { workspace = true } -renderling-derive = { version = "0.1.0", path = "../renderling-derive" } spirv-std = "0.9" [target.'cfg(not(target_arch = "spirv"))'.dependencies] -glam = { workspace = true, features = ["bytemuck"] } +glam = { workspace = true, features = ["std"] } +crabslab = { version = "0.1.0", path = "../crabslab" } [target.'cfg(not(target_arch = "spirv"))'.dev-dependencies] -glam = { workspace = true, features = ["debug-glam-assert", "bytemuck"] } +glam = { workspace = true, features = ["std", "debug-glam-assert"] } [target.'cfg(target_arch = "spirv")'.dependencies] -glam = { version = "0.24.2", default-features = false, features = ["libm", "bytemuck"] } +glam = { version = "0.24.2", default-features = false, features = ["libm"] } +crabslab = { version = "0.1.0", path = "../crabslab", default-features = false, features = ["glam"] } [dev-dependencies] image = { workspace = true } diff --git a/crates/renderling-shader/src/bits.rs b/crates/renderling-shader/src/bits.rs index 680915db..193a48fd 100644 --- a/crates/renderling-shader/src/bits.rs +++ b/crates/renderling-shader/src/bits.rs @@ -2,7 +2,7 @@ use core::ops::RangeInclusive; -use crate::{id::Id, slab::Slab}; +use crabslab::{Id, Slab}; /// Statically define a shift/mask range as a literal range of bits. pub const fn bits(range: RangeInclusive) -> (u32, u32) { @@ -102,9 +102,9 @@ pub fn extract_u16( // slab of u32s slab: &[u32], ) -> (u32, usize, usize) { - // NOTE: This should only have two entries, but we'll still handle the case where - // the extraction is not aligned to a u32 boundary by reading as if it were, and then - // re-aligning. + // NOTE: This should only have two entries, but we'll still handle the case + // where the extraction is not aligned to a u32 boundary by reading as if it + // were, and then re-aligning. const SHIFT_MASKS: [((u32, u32), usize, usize); 4] = [ (U16_0_BITS, 2, 0), (U16_0_BITS, 2, 0), diff --git a/crates/renderling-shader/src/convolution.rs b/crates/renderling-shader/src/convolution.rs index d84321f2..9f89cd93 100644 --- a/crates/renderling-shader/src/convolution.rs +++ b/crates/renderling-shader/src/convolution.rs @@ -1,6 +1,7 @@ //! Convolution shaders. //! //! These shaders convolve various functions to produce cached maps. +use crabslab::{Id, Slab, SlabItem}; use glam::{UVec2, Vec2, Vec3, Vec4, Vec4Swizzles}; use spirv_std::{ image::{Cubemap, Image2d}, @@ -11,7 +12,7 @@ use spirv_std::{ #[cfg(target_arch = "spirv")] use spirv_std::num_traits::Float; -use crate::{pbr, stage::GpuConstants, IsVector}; +use crate::{pbr, stage::Camera, IsVector}; fn radical_inverse_vdc(mut bits: u32) -> f32 { bits = (bits << 16u32) | (bits >> 16u32); @@ -148,31 +149,46 @@ pub fn integrate_brdf_doesnt_work(mut n_dot_v: f32, roughness: f32) -> Vec2 { Vec2::new(a, b) } +/// Used by [`vertex_prefilter_environment_cubemap`] to read the camera and +/// roughness values from the slab. +#[derive(Default, SlabItem)] +pub struct VertexPrefilterEnvironmentCubemapIds { + pub camera: Id, + pub roughness: Id, +} + +/// Uses the `instance_index` as the [`Id`] of a [`PrefilterEnvironmentIds`]. +/// roughness value. #[spirv(vertex)] pub fn vertex_prefilter_environment_cubemap( - #[spirv(uniform, descriptor_set = 0, binding = 0)] constants: &GpuConstants, - in_pos: Vec3, + #[spirv(instance_index)] instance_index: u32, + #[spirv(vertex_index)] vertex_id: u32, + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], out_pos: &mut Vec3, + out_roughness: &mut f32, #[spirv(position)] gl_pos: &mut Vec4, ) { + let in_pos = crate::math::CUBE[vertex_id as usize]; + let VertexPrefilterEnvironmentCubemapIds { camera, roughness } = + slab.read(Id::new(instance_index)); + let camera = slab.read(camera); + *out_roughness = slab.read(roughness); *out_pos = in_pos; - *gl_pos = constants.camera_projection * constants.camera_view * in_pos.extend(1.0); + *gl_pos = camera.projection * camera.view * in_pos.extend(1.0); } /// Lambertian prefilter. #[spirv(fragment)] pub fn fragment_prefilter_environment_cubemap( - #[spirv(uniform, descriptor_set = 0, binding = 1)] roughness: &f32, - #[spirv(descriptor_set = 0, binding = 2)] environment_cubemap: &Cubemap, - #[spirv(descriptor_set = 0, binding = 3)] sampler: &Sampler, + #[spirv(descriptor_set = 0, binding = 1)] environment_cubemap: &Cubemap, + #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler, in_pos: Vec3, + in_roughness: f32, frag_color: &mut Vec4, ) { let mut n = in_pos.alt_norm_or_zero(); // `wgpu` and vulkan's y coords are flipped from opengl n.y *= -1.0; - // These moves are redundant but the names have connections to the PBR - // equations. let r = n; let v = r; @@ -181,12 +197,12 @@ pub fn fragment_prefilter_environment_cubemap( for i in 0..SAMPLE_COUNT { let xi = hammersley(i, SAMPLE_COUNT); - let h = importance_sample_ggx(xi, n, *roughness); + let h = importance_sample_ggx(xi, n, in_roughness); let l = (2.0 * v.dot(h) * h - v).alt_norm_or_zero(); let n_dot_l = n.dot(l).max(0.0); if n_dot_l > 0.0 { - let mip_level = if *roughness == 0.0 { + let mip_level = if in_roughness == 0.0 { 0.0 } else { calc_lod(n_dot_l) @@ -273,15 +289,44 @@ pub fn fragment_bloom( *frag_color = result.extend(1.0); } +#[repr(C)] +#[derive(Clone, Copy)] +struct Vert { + pos: [f32; 3], + uv: [f32; 2], +} + +/// A screen-space quad. +const BRDF_VERTS: [Vert; 6] = { + let bl = Vert { + pos: [-1.0, -1.0, 0.0], + uv: [0.0, 1.0], + }; + let br = Vert { + pos: [1.0, -1.0, 0.0], + uv: [1.0, 1.0], + }; + let tl = Vert { + pos: [-1.0, 1.0, 0.0], + uv: [0.0, 0.0], + }; + let tr = Vert { + pos: [1.0, 1.0, 0.0], + uv: [1.0, 0.0], + }; + + [bl, br, tr, bl, tr, tl] +}; + #[spirv(vertex)] pub fn vertex_brdf_lut_convolution( - in_pos: glam::Vec3, - in_uv: glam::Vec2, + #[spirv(vertex_index)] vertex_id: u32, out_uv: &mut glam::Vec2, #[spirv(position)] gl_pos: &mut glam::Vec4, ) { - *out_uv = in_uv; - *gl_pos = in_pos.extend(1.0); + let Vert { pos, uv } = BRDF_VERTS[vertex_id as usize]; + *out_uv = Vec2::from(uv); + *gl_pos = Vec3::from(pos).extend(1.0); } #[spirv(fragment)] diff --git a/crates/renderling-shader/src/debug.rs b/crates/renderling-shader/src/debug.rs index 3cd747ef..6868dcd5 100644 --- a/crates/renderling-shader/src/debug.rs +++ b/crates/renderling-shader/src/debug.rs @@ -1,6 +1,5 @@ //! Debugging helpers. -use crate as renderling_shader; -use crate::slab::Slabbed; +use crabslab::SlabItem; /// Used to debug shaders by early exiting the shader and attempting to display /// the value as shaded colors. @@ -58,16 +57,19 @@ pub enum DebugChannel { /// Displays only the occlusion color for the fragment. Occlusion, - /// Displays only the calculated emissive effect (emissive_tex_color * emissive_factor * emissive_strength) of the fragment. + /// Displays only the calculated emissive effect (emissive_tex_color * + /// emissive_factor * emissive_strength) of the fragment. Emissive, - /// Displays only the emissive color (from the emissive map texture) of the fragment. + /// Displays only the emissive color (from the emissive map texture) of the + /// fragment. UvEmissive, /// Displays only teh emissive factor of the fragment. EmissiveFactor, - /// Displays only the emissive strength of the fragment (KHR_materials_emissive_strength). + /// Displays only the emissive strength of the fragment + /// (KHR_materials_emissive_strength). EmissiveStrength, } @@ -102,7 +104,7 @@ impl DebugChannel { /// /// Create one using `DebugChannel::into`. #[repr(transparent)] -#[derive(Default, Clone, Copy, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Default, Clone, Copy, PartialEq, Eq, SlabItem)] pub struct DebugMode(u32); impl core::fmt::Debug for DebugMode { diff --git a/crates/renderling-shader/src/gltf.rs b/crates/renderling-shader/src/gltf.rs index b41e11fe..c7d042ea 100644 --- a/crates/renderling-shader/src/gltf.rs +++ b/crates/renderling-shader/src/gltf.rs @@ -1,21 +1,15 @@ //! Gltf types that are used in shaders. +use crabslab::{Array, Id, Slab, SlabItem}; use glam::{Vec2, Vec3, Vec4}; -use crate::{ - self as renderling_shader, - array::Array, - id::Id, - pbr::PbrMaterial, - slab::{Slab, Slabbed}, - texture::GpuTexture, -}; +use crate::{pbr::PbrMaterial, texture::GpuTexture}; #[repr(transparent)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfBuffer(pub Array); #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfBufferView { // Pointer to the parent buffer. pub buffer: Id, @@ -42,7 +36,7 @@ pub enum DataType { F32, } -impl Slabbed for DataType { +impl SlabItem for DataType { fn slab_size() -> usize { // 1 u32::slab_size() @@ -84,7 +78,7 @@ pub enum Dimensions { Mat4, } -impl Slabbed for Dimensions { +impl SlabItem for Dimensions { fn slab_size() -> usize { 1 } @@ -121,15 +115,13 @@ impl Slabbed for Dimensions { } #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfAccessor { // The byte size of each element that this accessor describes. - // /// For example, if the accessor describes a `Vec3` of F32s, then // the size is 3 * 4 = 12. pub size: u32, // A point to the parent view this accessor reads from. - // /// This may be Id::NONE if the corresponding accessor is sparse. pub view: Id, // The offset relative to the start of the parent **buffer view** in bytes. @@ -607,7 +599,7 @@ impl GltfAccessor { } #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfPrimitive { pub vertex_count: u32, pub material: Id, @@ -719,7 +711,7 @@ impl GltfPrimitive { } #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfMesh { pub primitives: Array, pub weights: Array, @@ -758,7 +750,7 @@ impl Default for GltfCamera { } } -impl Slabbed for GltfCamera { +impl SlabItem for GltfCamera { fn slab_size() -> usize { 1 + 4 } @@ -847,7 +839,7 @@ pub enum GltfLightKind { }, } -impl Slabbed for GltfLightKind { +impl SlabItem for GltfLightKind { fn slab_size() -> usize { 1 // hash + 2 // inner_cone_angle, outer_cone_angle @@ -896,7 +888,7 @@ impl Slabbed for GltfLightKind { } } -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfLight { pub color: glam::Vec3, pub intensity: f32, @@ -905,14 +897,14 @@ pub struct GltfLight { pub kind: GltfLightKind, } -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfSkin { pub joints: Array>, pub inverse_bind_matrices: Id, pub skeleton: Id, } -#[derive(Clone, Copy, Slabbed)] +#[derive(Clone, Copy, SlabItem)] pub struct GltfNode { pub camera: Id, pub children: Array>, @@ -950,7 +942,7 @@ pub enum GltfInterpolation { CubicSpline, } -impl Slabbed for GltfInterpolation { +impl SlabItem for GltfInterpolation { fn slab_size() -> usize { 1 } @@ -978,7 +970,7 @@ impl Slabbed for GltfInterpolation { } } -#[derive(Default, Clone, Copy, PartialEq, Slabbed)] +#[derive(Default, Clone, Copy, PartialEq, SlabItem)] pub struct GltfAnimationSampler { pub input: Id, pub output: Id, @@ -995,7 +987,7 @@ pub enum GltfTargetProperty { MorphTargetWeights, } -impl Slabbed for GltfTargetProperty { +impl SlabItem for GltfTargetProperty { fn slab_size() -> usize { 1 } @@ -1025,33 +1017,34 @@ impl Slabbed for GltfTargetProperty { } } -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfTarget { pub node: Id, pub property: GltfTargetProperty, } -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfChannel { pub sampler: Id, pub target: GltfTarget, } -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfAnimation { pub channels: Array, pub samplers: Array, } -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfScene { pub nodes: Array>, } /// A document of Gltf data. /// -/// This tells where certain parts of the Gltf document are stored in the [`Stage`]'s slab. -#[derive(Default, Clone, Copy, Slabbed)] +/// This tells where certain parts of the Gltf document are stored in the +/// [`Stage`]'s slab. +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfDocument { pub accessors: Array, pub animations: Array, diff --git a/crates/renderling-shader/src/lib.rs b/crates/renderling-shader/src/lib.rs index 75f555f2..77677fb3 100644 --- a/crates/renderling-shader/src/lib.rs +++ b/crates/renderling-shader/src/lib.rs @@ -13,16 +13,13 @@ use spirv_std::{ Sampler, }; -pub mod array; pub mod bits; pub mod convolution; pub mod debug; pub mod gltf; -pub mod id; pub mod math; pub mod pbr; pub mod skybox; -pub mod slab; pub mod stage; pub mod texture; pub mod tonemapping; @@ -69,9 +66,9 @@ pub trait IsMatrix { /// matrix is expected to be a 3D affine transformation matrix otherwise /// the output will be invalid. /// - /// Will return `(Vec3::ONE, Quat::IDENTITY, Vec3::ZERO)` if the determinant of - /// `self` is zero or if the resulting scale vector contains any zero elements - /// when `glam_assert` is enabled. + /// Will return `(Vec3::ONE, Quat::IDENTITY, Vec3::ZERO)` if the determinant + /// of `self` is zero or if the resulting scale vector contains any zero + /// elements when `glam_assert` is enabled. /// /// This is required instead of using /// [`glam::Mat4::to_scale_rotation_translation`], because that uses diff --git a/crates/renderling-shader/src/pbr.rs b/crates/renderling-shader/src/pbr.rs index a13e3a13..cc7b4b91 100644 --- a/crates/renderling-shader/src/pbr.rs +++ b/crates/renderling-shader/src/pbr.rs @@ -5,17 +5,13 @@ //! * https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/5b1b7f48a8cb2b7aaef00d08fdba18ccc8dd331b/source/Renderer/shaders/pbr.frag //! * https://github.khronos.org/glTF-Sample-Viewer-Release/ +use crabslab::{Array, Id, Slab, SlabItem}; use glam::{Vec2, Vec3, Vec4, Vec4Swizzles}; -use renderling_derive::Slabbed; #[cfg(target_arch = "spirv")] use spirv_std::num_traits::Float; use crate::{ - self as renderling_shader, - array::Array, - id::Id, math, - slab::Slab, stage::{light::LightStyle, GpuLight, LightType, LightingModel}, texture::GpuTexture, IsSampler, IsVector, Sample2d, SampleCube, @@ -28,7 +24,7 @@ use crate::{ /// [`SceneBuilder`](crate::SceneBuilder). #[repr(C)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Clone, Copy, PartialEq, SlabItem)] pub struct PbrMaterial { // x, y, z is emissive factor, default [0.0, 0.0, 0.0] // w is emissive strength multiplier (gltf's KHR_materials_emissive_strength extension), diff --git a/crates/renderling-shader/src/skybox.rs b/crates/renderling-shader/src/skybox.rs index 36a69e0e..6adeda62 100644 --- a/crates/renderling-shader/src/skybox.rs +++ b/crates/renderling-shader/src/skybox.rs @@ -1,5 +1,6 @@ //! Skybox shader. +use crabslab::{Id, Slab}; use glam::{Mat3, Mat4, Vec2, Vec3, Vec4, Vec4Swizzles}; use spirv_std::{ image::{Cubemap, Image2d}, @@ -9,13 +10,7 @@ use spirv_std::{ #[cfg(target_arch = "spirv")] use spirv_std::num_traits::Float; -use crate::{ - id::Id, - math, - slab::Slab, - stage::{Camera, GpuConstants}, - IsVector, -}; +use crate::{math, stage::Camera, IsVector}; const INV_ATAN: Vec2 = Vec2::new(0.1591, core::f32::consts::FRAC_1_PI); @@ -28,23 +23,9 @@ pub fn direction_to_equirectangular_uv(dir: Vec3) -> Vec2 { uv } +/// Vertex shader for a skybox. #[spirv(vertex)] pub fn vertex( - #[spirv(vertex_index)] vertex_id: u32, - #[spirv(uniform, descriptor_set = 0, binding = 0)] constants: &GpuConstants, - local_pos: &mut Vec3, - #[spirv(position)] gl_pos: &mut Vec4, -) { - let point = math::CUBE[vertex_id as usize]; - *local_pos = point; - let camera_view_without_translation = Mat3::from_mat4(constants.camera_view); - let rot_view = Mat4::from_mat3(camera_view_without_translation); - let clip_pos = constants.camera_projection * rot_view * point.extend(1.0); - *gl_pos = clip_pos.xyww(); -} - -#[spirv(vertex)] -pub fn slabbed_vertex( #[spirv(instance_index)] camera_index: u32, #[spirv(vertex_index)] vertex_index: u32, #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], @@ -61,18 +42,6 @@ pub fn slabbed_vertex( *clip_pos = position.xyww(); } -/// Colors a skybox using a cubemap texture. -#[spirv(fragment)] -pub fn stage_skybox_cubemap( - #[spirv(descriptor_set = 1, binding = 8)] texture: &Cubemap, - #[spirv(descriptor_set = 1, binding = 9)] sampler: &Sampler, - local_pos: Vec3, - out_color: &mut Vec4, -) { - let env_color: Vec3 = texture.sample(*sampler, local_pos.alt_norm_or_zero()).xyz(); - *out_color = env_color.extend(1.0); -} - /// Colors a skybox using a cubemap texture. #[spirv(fragment)] pub fn fragment_cubemap( @@ -85,19 +54,24 @@ pub fn fragment_cubemap( *out_color = env_color.extend(1.0); } -/// Passes the singular `Vec3` position attribute to the fragment shader unchanged, -/// while transforming `gl_pos` by the camera projection*view; +/// Draws a cubemap. +/// +/// Uses the `instance_index` as the [`Id`] for a [`Camera`]. /// -/// Used to create a cubemap from an equirectangular image as well as cubemap convolutions. +/// Used to create a cubemap from an equirectangular image as well as cubemap +/// convolutions. #[spirv(vertex)] -pub fn vertex_position_passthru( - #[spirv(uniform, descriptor_set = 0, binding = 0)] constants: &GpuConstants, - in_pos: Vec3, +pub fn vertex_cubemap( + #[spirv(instance_index)] camera_index: u32, + #[spirv(vertex_index)] vertex_index: u32, + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], local_pos: &mut Vec3, #[spirv(position)] gl_pos: &mut Vec4, ) { - *local_pos = in_pos; - *gl_pos = constants.camera_projection * constants.camera_view * in_pos.extend(1.0); + let camera = slab.read(Id::::new(camera_index)); + let pos = crate::math::CUBE[vertex_index as usize]; + *local_pos = pos; + *gl_pos = camera.projection * camera.view * pos.extend(1.0); } /// Colors a skybox using an equirectangular texture. diff --git a/crates/renderling-shader/src/stage.rs b/crates/renderling-shader/src/stage.rs index a507f349..105afdaf 100644 --- a/crates/renderling-shader/src/stage.rs +++ b/crates/renderling-shader/src/stage.rs @@ -5,6 +5,7 @@ //! //! To read more about the technique, check out these resources: //! * https://stackoverflow.com/questions/59686151/what-is-gpu-driven-rendering +use crabslab::{Array, Id, Slab, SlabItem, ID_NONE}; use glam::{mat3, Mat4, Quat, UVec2, UVec3, Vec2, Vec3, Vec4, Vec4Swizzles}; use spirv_std::{ image::{Cubemap, Image2d}, @@ -15,14 +16,10 @@ use spirv_std::{ use spirv_std::num_traits::*; use crate::{ - self as renderling_shader, - array::Array, bits::{bits, extract, insert}, debug::*, gltf::{GltfMesh, GltfNode}, - id::{Id, ID_NONE}, pbr::{self, PbrMaterial}, - slab::{Slab, Slabbed}, texture::GpuTexture, IsMatrix, IsSampler, IsVector, Sample2d, SampleCube, }; @@ -32,7 +29,7 @@ pub mod light; /// A vertex in a mesh. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Clone, Copy, PartialEq, SlabItem)] pub struct Vertex { pub position: Vec4, pub color: Vec4, @@ -174,7 +171,7 @@ impl Vertex { #[repr(transparent)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Copy, Clone, Default, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Copy, Clone, Default, PartialEq, Eq, SlabItem)] pub struct LightType(u32); #[cfg(not(target_arch = "spirv"))] @@ -201,7 +198,7 @@ impl LightType { /// A light capable of representing a directional, point or spotlight. #[repr(C)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Copy, Clone, Default, SlabItem)] pub struct GpuLight { pub position: Vec4, pub direction: Vec4, @@ -215,19 +212,7 @@ pub struct GpuLight { /// Determines the lighting to use in an ubershader. #[repr(transparent)] -#[derive( - Clone, - Copy, - Default, - PartialEq, - Eq, - PartialOrd, - Ord, - Debug, - bytemuck::Pod, - bytemuck::Zeroable, - Slabbed, -)] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug, SlabItem)] pub struct LightingModel(u32); impl LightingModel { @@ -248,7 +233,7 @@ impl LightingModel { /// ### Provides info about if the entity is a skin. #[repr(transparent)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Clone, Copy, Default, PartialEq, SlabItem)] pub struct GpuEntityInfo(pub u32); impl GpuEntityInfo { @@ -311,7 +296,7 @@ impl GpuEntityInfo { /// A bundle of GPU components. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Clone, Copy, PartialEq, SlabItem)] pub struct GpuEntity { // The id of this entity. `Id::NONE` means this entity is not in use. pub id: Id, @@ -454,7 +439,7 @@ impl GpuEntity { /// Boolean toggles that cause the renderer to turn on/off certain features. #[repr(transparent)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Default, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Default, Clone, Copy, PartialEq, SlabItem)] pub struct GpuToggles(pub u32); impl GpuToggles { @@ -487,7 +472,7 @@ impl GpuToggles { /// Unforms/constants for a scene's worth of rendering. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Default, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Default, Clone, Copy, PartialEq, SlabItem)] pub struct GpuConstants { pub camera_projection: Mat4, pub camera_view: Mat4, @@ -498,7 +483,7 @@ pub struct GpuConstants { } #[repr(C)] -#[derive(Default, Debug, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Default, Debug, Clone, Copy, PartialEq, SlabItem)] pub struct DrawIndirect { pub vertex_count: u32, pub instance_count: u32, @@ -942,7 +927,7 @@ pub fn main_fragment_impl( /// also update the camera's position. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Default, Clone, Copy, PartialEq, Slabbed)] +#[derive(Default, Clone, Copy, PartialEq, SlabItem)] pub struct Camera { pub projection: Mat4, pub view: Mat4, @@ -989,7 +974,7 @@ impl Camera { /// This should be the first struct in the stage's slab. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Clone, Copy, PartialEq, Slabbed)] +#[derive(Clone, Copy, PartialEq, SlabItem)] pub struct StageLegend { pub atlas_size: UVec2, pub debug_mode: DebugMode, @@ -1012,7 +997,7 @@ impl Default for StageLegend { #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Default, Clone, Copy, PartialEq, Slabbed)] +#[derive(Default, Clone, Copy, PartialEq, SlabItem)] pub struct GltfVertexData { // A path of node ids that leads to the node that contains the mesh. pub parent_node_path: Array>, @@ -1024,7 +1009,7 @@ pub struct GltfVertexData { #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Clone, Copy, PartialEq, Slabbed)] +#[derive(Clone, Copy, PartialEq, SlabItem)] pub struct Transform { pub translation: Vec3, pub rotation: Quat, @@ -1044,7 +1029,7 @@ impl Default for Transform { /// A rendering "command" that draws a single mesh from a top-level node. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Default, Clone, Copy, PartialEq, Slabbed)] +#[derive(Default, Clone, Copy, PartialEq, SlabItem)] pub struct RenderUnit { // Which node are we rendering, and what is the path through its // ancestors to get to it. @@ -1543,7 +1528,8 @@ pub fn compute_cull_entities( } #[spirv(compute(threads(32)))] -/// A shader to ensure that we can extract i8 and i16 values from a storage buffer. +/// A shader to ensure that we can extract i8 and i16 values from a storage +/// buffer. pub fn test_i8_i16_extraction( #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &mut [u32], #[spirv(global_invocation_id)] global_id: UVec3, @@ -1558,30 +1544,3 @@ pub fn test_i8_i16_extraction( slab[index] = value as u32; } } - -#[cfg(test)] -mod test { - use crate::{self as renderling_shader, id::Id, slab::Slab}; - use renderling_shader::slab::Slabbed; - - #[derive(Default, Debug, PartialEq, Slabbed)] - struct TheType { - a: glam::Vec3, - b: glam::Vec2, - c: glam::Vec4, - } - - #[test] - fn slabbed_writeread() { - let mut slab = [0u32; 100]; - let the = TheType { - a: glam::Vec3::new(0.0, 1.0, 2.0), - b: glam::Vec2::new(3.0, 4.0), - c: glam::Vec4::new(5.0, 6.0, 7.0, 8.0), - }; - let index = slab.write_indexed(&the, 0); - assert_eq!(9, index); - let the2 = slab.read(Id::::new(0)); - assert_eq!(the, the2); - } -} diff --git a/crates/renderling-shader/src/stage/light.rs b/crates/renderling-shader/src/stage/light.rs index e4961040..7ac06ba6 100644 --- a/crates/renderling-shader/src/stage/light.rs +++ b/crates/renderling-shader/src/stage/light.rs @@ -1,10 +1,10 @@ //! Stage lighting. -use crate::{self as renderling_shader, id::Id, slab::Slabbed}; +use crabslab::{Id, SlabItem}; use glam::{Vec3, Vec4}; #[repr(C)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Copy, Clone, Slabbed)] +#[derive(Copy, Clone, SlabItem)] pub struct SpotLight { pub position: Vec3, pub direction: Vec3, @@ -39,7 +39,7 @@ impl Default for SpotLight { #[repr(C)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Copy, Clone, Slabbed)] +#[derive(Copy, Clone, SlabItem)] pub struct DirectionalLight { pub direction: Vec3, pub color: Vec4, @@ -62,7 +62,7 @@ impl Default for DirectionalLight { #[repr(C)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Copy, Clone, Slabbed)] +#[derive(Copy, Clone, SlabItem)] pub struct PointLight { pub position: Vec3, pub attenuation: Vec3, @@ -112,7 +112,7 @@ pub enum LightStyle { Spot = 2, } -impl Slabbed for LightStyle { +impl SlabItem for LightStyle { fn slab_size() -> usize { 1 } @@ -135,10 +135,11 @@ impl Slabbed for LightStyle { } } -/// A type-erased linked-list-of-lights that is used as a slab pointer to any light type. +/// A type-erased linked-list-of-lights that is used as a slab pointer to any +/// light type. #[repr(C)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Copy, Clone, PartialEq, Slabbed)] +#[derive(Copy, Clone, PartialEq, SlabItem)] pub struct Light { // The type of the light pub light_type: LightStyle, diff --git a/crates/renderling-shader/src/texture.rs b/crates/renderling-shader/src/texture.rs index 84646fa9..e3a11be4 100644 --- a/crates/renderling-shader/src/texture.rs +++ b/crates/renderling-shader/src/texture.rs @@ -1,11 +1,8 @@ //! GPU textures. +use crabslab::SlabItem; use glam::{UVec2, Vec2}; -use renderling_derive::Slabbed; -use crate::{ - self as renderling_shader, - bits::{bits, extract, insert}, -}; +use crate::bits::{bits, extract, insert}; #[cfg(target_arch = "spirv")] use spirv_std::num_traits::*; @@ -13,7 +10,7 @@ use spirv_std::num_traits::*; // TODO: Completely rework the way we represent texture modes. #[repr(transparent)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Clone, Copy, Default, PartialEq, SlabItem)] pub struct TextureModes(u32); impl TextureModes { @@ -50,7 +47,7 @@ impl TextureModes { /// A GPU texture. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Clone, Copy, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Clone, Copy, Default, PartialEq, SlabItem)] pub struct GpuTexture { // The top left offset of texture in the atlas. pub offset_px: UVec2, @@ -114,7 +111,7 @@ pub fn clamp(input: f32) -> f32 { /// How edges should be handled in texture addressing/wrapping. #[repr(transparent)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, PartialEq, Eq, Default, bytemuck::Pod, bytemuck::Zeroable)] +#[derive(Clone, Copy, PartialEq, Eq, Default)] pub struct TextureAddressMode(u32); impl core::fmt::Display for TextureAddressMode { diff --git a/crates/renderling-shader/src/tonemapping.rs b/crates/renderling-shader/src/tonemapping.rs index 7c5bff14..6226dcbc 100644 --- a/crates/renderling-shader/src/tonemapping.rs +++ b/crates/renderling-shader/src/tonemapping.rs @@ -4,6 +4,7 @@ //! * https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/5b1b7f48a8cb2b7aaef00d08fdba18ccc8dd331b/source/Renderer/shaders/tonemapping.glsl //! * https://64.github.io/tonemapping +use crabslab::{Slab, SlabItem}; use glam::{mat3, Mat3, Vec2, Vec3, Vec4, Vec4Swizzles}; use spirv_std::{image::Image2d, spirv, Sampler}; @@ -75,7 +76,7 @@ fn tone_map_aces_hill(mut color: Vec3) -> Vec3 { #[repr(transparent)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, Default, PartialEq, Eq, bytemuck::Zeroable, bytemuck::Pod)] +#[derive(Clone, Copy, Default, PartialEq, Eq, SlabItem)] pub struct Tonemap(u32); impl Tonemap { @@ -87,7 +88,7 @@ impl Tonemap { } #[repr(C)] -#[derive(Clone, Copy, PartialEq, bytemuck::Zeroable, bytemuck::Pod)] +#[derive(Clone, Copy, PartialEq, SlabItem)] pub struct TonemapConstants { pub tonemap: Tonemap, pub exposure: f32, @@ -102,7 +103,8 @@ impl Default for TonemapConstants { } } -pub fn tonemap(mut color: Vec4, constants: &TonemapConstants) -> Vec4 { +pub fn tonemap(mut color: Vec4, slab: &[u32]) -> Vec4 { + let constants = slab.read::(0u32.into()); color *= constants.exposure; match constants.tonemap { @@ -143,16 +145,13 @@ pub fn vertex( #[spirv(fragment)] pub fn fragment( - #[spirv(descriptor_set = 0, binding = 0)] texture: &Image2d, - #[spirv(descriptor_set = 0, binding = 1)] sampler: &Sampler, - #[spirv(uniform, descriptor_set = 1, binding = 0)] constants: &TonemapConstants, - #[spirv(descriptor_set = 2, binding = 0)] bloom_texture: &Image2d, - #[spirv(descriptor_set = 2, binding = 1)] bloom_sampler: &Sampler, + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], + #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d, + #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler, in_uv: glam::Vec2, output: &mut glam::Vec4, ) { let color: Vec4 = texture.sample(*sampler, in_uv); - let bloom: Vec4 = bloom_texture.sample(*bloom_sampler, in_uv); - let color = tonemap(color + bloom, constants); + let color = tonemap(color, slab); *output = color; } diff --git a/crates/renderling-shader/src/tutorial.rs b/crates/renderling-shader/src/tutorial.rs index e3ddff8d..f13fcf4e 100644 --- a/crates/renderling-shader/src/tutorial.rs +++ b/crates/renderling-shader/src/tutorial.rs @@ -1,13 +1,9 @@ //! Shaders used in the intro tutorial. +use crabslab::{Array, Id, Slab, SlabItem}; use glam::{Mat4, Vec4, Vec4Swizzles}; use spirv_std::spirv; -use crate::{ - array::Array, - id::Id, - slab::{Slab, Slabbed}, - stage::{RenderUnit, Vertex}, -}; +use crate::stage::{RenderUnit, Vertex}; /// Simple fragment shader that writes the input color to the output color. #[spirv(fragment)] diff --git a/crates/renderling-shader/src/ui.rs b/crates/renderling-shader/src/ui.rs index ccd57968..b52d5bb0 100644 --- a/crates/renderling-shader/src/ui.rs +++ b/crates/renderling-shader/src/ui.rs @@ -2,13 +2,14 @@ //! //! This is mostly for rendering text. +use crabslab::SlabItem; use glam::{Mat4, UVec2, Vec2, Vec4}; use spirv_std::{image::Image2d, spirv, Sampler}; /// A vertex in a mesh. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive(Clone, Copy, PartialEq)] pub struct UiVertex { pub position: Vec2, pub uv: Vec2, @@ -43,14 +44,14 @@ impl UiVertex { } #[repr(C)] -#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +#[derive(Clone, Copy, SlabItem)] pub struct UiConstants { pub canvas_size: UVec2, pub camera_translation: Vec2, } #[repr(transparent)] -#[derive(Clone, Copy, Default, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive(Clone, Copy, Default, PartialEq, Eq)] pub struct UiMode(pub u32); impl UiMode { @@ -59,7 +60,7 @@ impl UiMode { } #[repr(C)] -#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +#[derive(Clone, Copy)] pub struct UiDrawParams { pub translation: Vec2, pub scale: Vec2, diff --git a/crates/renderling/Cargo.toml b/crates/renderling/Cargo.toml index e9bbe260..ac41fe54 100644 --- a/crates/renderling/Cargo.toml +++ b/crates/renderling/Cargo.toml @@ -19,25 +19,25 @@ wasm = ["wgpu/fragile-send-sync-non-atomic-wasm"] [dependencies] ab_glyph = { version = "0.2", optional = true } any_vec = "0.13" -async-channel = "1.8" +async-channel = {workspace=true} +bytemuck = { workspace = true } +crabslab = { version = "0.1.0", path = "../crabslab" } crunch = "0.5" futures-lite = {workspace=true} +glam = { workspace = true, default-features = false, features = ["bytemuck", "libm"] } +gltf = { workspace = true, optional = true } glyph_brush = { version = "0.7", optional = true } half = "2.3" +image = { workspace = true, features = ["hdr"] } +log = { workspace = true } moongraph = { version = "0.3.5", features = ["dot"] } raw-window-handle = { version = "0.5", optional = true } renderling-shader = { path = "../renderling-shader" } rustc-hash = "1.1" send_wrapper = "0.6" -snafu = "0.7" - -image = { workspace = true, features = ["hdr"] } -gltf = { workspace = true, optional = true } -bytemuck = { workspace = true } -log = { workspace = true } -glam = { workspace = true, default-features = false, features = ["bytemuck", "libm"] } -winit = { workspace = true, optional = true } +snafu = {workspace=true} wgpu = { workspace = true, features = ["spirv"] } +winit = { workspace = true, optional = true } [package.metadata.docs.rs] features = ["gltf", "text", "raw-window-handle", "winit"] diff --git a/crates/renderling/src/atlas.rs b/crates/renderling/src/atlas.rs index 4977ab5a..fc3fb2f1 100644 --- a/crates/renderling/src/atlas.rs +++ b/crates/renderling/src/atlas.rs @@ -1,17 +1,19 @@ //! Texture atlas. //! -//! All images are packed into an atlas at scene build time. +//! All images are packed into an atlas at staging time. //! Texture descriptors describing where in the atlas an image is, //! and how callsites should sample pixels is packed into a buffer //! on the GPU. This makes the number of texture binds _very_ low. //! -//! An atlas should be temporary until we can use bindless techniques +//! ## NOTE: +//! `Atlas` is a temporary work around until we can use bindless techniques //! on web. use glam::UVec2; use image::{EncodableLayout, RgbaImage}; use snafu::prelude::*; -use crate::SceneImage; +mod atlas_image; +pub use atlas_image::*; fn gpu_frame_from_rect(r: crunch::Rect) -> (UVec2, UVec2) { ( @@ -31,9 +33,9 @@ pub enum AtlasError { pub enum Packing { Img { index: usize, - image: SceneImage, + image: AtlasImage, }, - AtlasImg { + GpuImg { index: usize, offset_px: UVec2, size_px: UVec2, @@ -44,42 +46,42 @@ impl Packing { pub fn width(&self) -> u32 { match self { Packing::Img { image, .. } => image.width, - Packing::AtlasImg { size_px, .. } => size_px.x, + Packing::GpuImg { size_px, .. } => size_px.x, } } pub fn height(&self) -> u32 { match self { Packing::Img { image, .. } => image.height, - Packing::AtlasImg { size_px, .. } => size_px.y, + Packing::GpuImg { size_px, .. } => size_px.y, } } pub fn index(&self) -> usize { match self { Packing::Img { index, .. } => *index, - Packing::AtlasImg { index, .. } => *index, + Packing::GpuImg { index, .. } => *index, } } pub fn set_index(&mut self, index: usize) { match self { Packing::Img { index: i, .. } => *i = index, - Packing::AtlasImg { index: i, .. } => *i = index, + Packing::GpuImg { index: i, .. } => *i = index, } } - pub fn as_scene_img_mut(&mut self) -> Option<&mut SceneImage> { + pub fn as_scene_img_mut(&mut self) -> Option<&mut AtlasImage> { match self { Packing::Img { image, .. } => Some(image), - Packing::AtlasImg { .. } => None, + Packing::GpuImg { .. } => None, } } - pub fn as_scene_img(&self) -> Option<&SceneImage> { + pub fn as_scene_img(&self) -> Option<&AtlasImage> { match self { Packing::Img { image, .. } => Some(image), - Packing::AtlasImg { .. } => None, + Packing::GpuImg { .. } => None, } } } @@ -199,8 +201,8 @@ impl Atlas { /// but doesn't send any data to the GPU. pub fn pack_preview<'a>( device: &wgpu::Device, - images: impl IntoIterator, - ) -> Result, AtlasError> { + images: impl IntoIterator, + ) -> Result, AtlasError> { let images = images.into_iter().collect::>(); let len = images.len(); let limit = device.limits().max_texture_dimension_1d; @@ -220,7 +222,7 @@ impl Atlas { pub fn commit_preview( device: &wgpu::Device, queue: &wgpu::Queue, - crunch::PackedItems { w, h, items }: crunch::PackedItems, + crunch::PackedItems { w, h, items }: crunch::PackedItems, ) -> Result { let mut atlas = Atlas::new(device, queue, UVec2::new(w as u32, h as u32)); atlas.rects = items @@ -269,7 +271,7 @@ impl Atlas { pub fn repack_preview( &self, device: &wgpu::Device, - images: impl IntoIterator, + images: impl IntoIterator, ) -> Result { let mut images = images.into_iter().collect::>(); let len = images.len() + self.rects.len(); @@ -277,7 +279,7 @@ impl Atlas { device.limits().max_texture_dimension_1d as usize, self.rects .iter() - .map(|r| Packing::AtlasImg { + .map(|r| Packing::GpuImg { index: 0, offset_px: UVec2::new(r.x as u32, r.y as u32), size_px: UVec2::new(r.w as u32, r.y as u32), @@ -352,7 +354,7 @@ impl Atlas { ); atlas.rects[index] = rect; } - Packing::AtlasImg { + Packing::GpuImg { index, offset_px, size_px, @@ -396,11 +398,10 @@ impl Atlas { /// /// Returns a vector of ids that determine the locations of the given images /// within the atlas. - /// pub fn pack( device: &wgpu::Device, queue: &wgpu::Queue, - images: impl IntoIterator, + images: impl IntoIterator, ) -> Result { let images = images.into_iter().collect::>(); let items = Self::pack_preview(device, images)?; @@ -411,7 +412,7 @@ impl Atlas { &self, device: &wgpu::Device, queue: &wgpu::Queue, - images: impl IntoIterator, + images: impl IntoIterator, ) -> Result { let images = images.into_iter().collect::>(); let items = self.repack_preview(device, images)?; @@ -498,8 +499,11 @@ impl Atlas { /// /// This is primarily for testing. /// + /// The resulting image will be in a **linear** color space. + /// /// ## Panics - /// Panics if the pixels read from the GPU cannot be converted into an `RgbaImage`. + /// Panics if the pixels read from the GPU cannot be converted into an + /// `RgbaImage`. pub fn atlas_img(&self, device: &wgpu::Device, queue: &wgpu::Queue) -> RgbaImage { let buffer = crate::Texture::read( &self.texture.texture, @@ -510,7 +514,7 @@ impl Atlas { 4, 1, ); - buffer.into_rgba(device).unwrap() + buffer.into_linear_rgba(device).unwrap() } } @@ -525,6 +529,7 @@ mod test { }, Renderling, }; + use crabslab::GrowableSlab; use glam::{Vec2, Vec3, Vec4}; use super::*; @@ -534,10 +539,10 @@ mod test { let r = Renderling::headless(100, 100); let (device, queue) = r.get_device_and_queue_owned(); println!("{}", std::env::current_dir().unwrap().display()); - let cheetah = SceneImage::from_path("../../img/cheetah.jpg").unwrap(); - let dirt = SceneImage::from_path("../../img/dirt.jpg").unwrap(); - let happy_mac = SceneImage::from_path("../../img/happy_mac.png").unwrap(); - let sandstone = SceneImage::from_path("../../img/sandstone.png").unwrap(); + let cheetah = AtlasImage::from_path("../../img/cheetah.jpg").unwrap(); + let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap(); + let happy_mac = AtlasImage::from_path("../../img/happy_mac.png").unwrap(); + let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap(); let atlas1 = Atlas::pack(&device, &queue, vec![cheetah, dirt]).unwrap(); let atlas2 = Atlas::pack(&device, &queue, vec![happy_mac, sandstone]).unwrap(); let atlas3 = atlas1.merge(&device, &queue, &atlas2).unwrap(); @@ -549,7 +554,7 @@ mod test { fn atlas_uv_mapping() { let mut r = Renderling::headless(32, 32).with_background_color(Vec3::splat(0.0).extend(1.0)); - let stage = r.new_stage(); + let mut stage = r.new_stage(); stage.configure_graph(&mut r, true); let (projection, view) = crate::camera::default_ortho2d(32.0, 32.0); let camera = stage.append(&Camera { @@ -557,9 +562,9 @@ mod test { view, ..Default::default() }); - let dirt = SceneImage::from_path("../../img/dirt.jpg").unwrap(); - let sandstone = SceneImage::from_path("../../img/sandstone.png").unwrap(); - let texels = SceneImage::from_path("../../test_img/atlas/uv_mapping.png").unwrap(); + let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap(); + let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap(); + let texels = AtlasImage::from_path("../../test_img/atlas/uv_mapping.png").unwrap(); let textures = stage.set_images([dirt, sandstone, texels]).unwrap(); let mut texels_tex = textures[2]; texels_tex @@ -596,18 +601,20 @@ mod test { material_id, ) .build(); + let mesh = stage.append(&mesh); let node = stage.append(&GltfNode { - mesh: stage.append(&mesh), + mesh, ..Default::default() }); let transform = stage.append(&Transform { scale: Vec3::new(32.0, 32.0, 1.0), ..Default::default() }); + let node_path = stage.append_array(&[node]); let _unit = stage.draw_unit(&RenderUnit { camera, transform, - node_path: stage.append_array(&[node]), + node_path, vertex_count: 6, ..Default::default() }); @@ -626,7 +633,7 @@ mod test { let w = sheet_w * 3 + 2; let h = sheet_h; let mut r = Renderling::headless(w, h).with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0)); - let stage = r.new_stage(); + let mut stage = r.new_stage(); stage.configure_graph(&mut r, true); let (projection, view) = crate::camera::default_ortho2d(w as f32, h as f32); @@ -636,9 +643,9 @@ mod test { ..Default::default() }); - let dirt = SceneImage::from_path("../../img/dirt.jpg").unwrap(); - let sandstone = SceneImage::from_path("../../img/sandstone.png").unwrap(); - let texels = SceneImage::from_path("../../img/happy_mac.png").unwrap(); + let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap(); + let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap(); + let texels = AtlasImage::from_path("../../img/happy_mac.png").unwrap(); let textures = stage.set_images([dirt, sandstone, texels]).unwrap(); let texel_tex = textures[2]; let mut clamp_tex = texel_tex; @@ -659,18 +666,21 @@ mod test { .modes .set_wrap_t(TextureAddressMode::MIRRORED_REPEAT); + let albedo_texture = stage.append(&clamp_tex); let clamp_material_id = stage.append(&PbrMaterial { - albedo_texture: stage.append(&clamp_tex), + albedo_texture, lighting_model: LightingModel::NO_LIGHTING, ..Default::default() }); + let albedo_texture = stage.append(&repeat_tex); let repeat_material_id = stage.append(&PbrMaterial { - albedo_texture: stage.append(&repeat_tex), + albedo_texture, lighting_model: LightingModel::NO_LIGHTING, ..Default::default() }); + let albedo_texture = stage.append(&mirror_tex); let mirror_material_id = stage.append(&PbrMaterial { - albedo_texture: stage.append(&mirror_tex), + albedo_texture, lighting_model: LightingModel::NO_LIGHTING, ..Default::default() }); @@ -707,50 +717,70 @@ mod test { p }; - let _clamp = stage.draw_unit(&RenderUnit { - camera, - node_path: stage.append_array(&[stage.append(&GltfNode { - mesh: stage.append(&GltfMesh { - primitives: stage.append_array(&[clamp_prim]), - ..Default::default() - }), + let _clamp = { + let primitives = stage.append_array(&[clamp_prim]); + let mesh = stage.append(&GltfMesh { + primitives, ..Default::default() - })]), - vertex_count: 6, - ..Default::default() - }); - let _repeat = stage.draw_unit(&RenderUnit { - camera, - node_path: stage.append_array(&[stage.append(&GltfNode { - mesh: stage.append(&GltfMesh { - primitives: stage.append_array(&[repeat_prim]), - ..Default::default() - }), + }); + let node = stage.append(&GltfNode { + mesh, ..Default::default() - })]), - vertex_count: 6, - transform: stage.append(&Transform { + }); + let node_path = stage.append_array(&[node]); + stage.draw_unit(&RenderUnit { + camera, + node_path, + vertex_count: 6, + ..Default::default() + }) + }; + let _repeat = { + let primitives = stage.append_array(&[repeat_prim]); + let mesh = stage.append(&GltfMesh { + primitives, + ..Default::default() + }); + let node = stage.append(&GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let transform = stage.append(&Transform { translation: Vec3::new(sheet_w + 1.0, 0.0, 0.0), ..Default::default() - }), - ..Default::default() - }); - let _mirror = stage.draw_unit(&RenderUnit { - camera, - node_path: stage.append_array(&[stage.append(&GltfNode { - mesh: stage.append(&GltfMesh { - primitives: stage.append_array(&[mirror_prim]), - ..Default::default() - }), + }); + stage.draw_unit(&RenderUnit { + camera, + node_path, + vertex_count: 6, + transform, ..Default::default() - })]), - vertex_count: 6, - transform: stage.append(&Transform { + }) + }; + let _mirror = { + let primitives = stage.append_array(&[mirror_prim]); + let mesh = stage.append(&GltfMesh { + primitives, + ..Default::default() + }); + let node = stage.append(&GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let transform = stage.append(&Transform { translation: Vec3::new(sheet_w as f32 * 2.0 + 2.0, 0.0, 0.0), ..Default::default() - }), - ..Default::default() - }); + }); + stage.draw_unit(&RenderUnit { + camera, + node_path, + vertex_count: 6, + transform, + ..Default::default() + }) + }; let img = r.render_image().unwrap(); img_diff::assert_img_eq("atlas/uv_wrapping.png", img); @@ -766,7 +796,7 @@ mod test { let w = sheet_w * 3 + 2; let h = sheet_h; let mut r = Renderling::headless(w, h).with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0)); - let stage = r.new_stage(); + let mut stage = r.new_stage(); stage.configure_graph(&mut r, true); let (projection, view) = crate::camera::default_ortho2d(w as f32, h as f32); @@ -776,9 +806,9 @@ mod test { ..Default::default() }); - let dirt = SceneImage::from_path("../../img/dirt.jpg").unwrap(); - let sandstone = SceneImage::from_path("../../img/sandstone.png").unwrap(); - let texels = SceneImage::from_path("../../img/happy_mac.png").unwrap(); + let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap(); + let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap(); + let texels = AtlasImage::from_path("../../img/happy_mac.png").unwrap(); let textures = stage.set_images([dirt, sandstone, texels]).unwrap(); let texel_tex = textures[2]; @@ -800,18 +830,23 @@ mod test { .modes .set_wrap_t(TextureAddressMode::MIRRORED_REPEAT); + let albedo_texture = stage.append(&clamp_tex); let clamp_material_id = stage.append(&PbrMaterial { - albedo_texture: stage.append(&clamp_tex), + albedo_texture, lighting_model: LightingModel::NO_LIGHTING, ..Default::default() }); + + let albedo_texture = stage.append(&repeat_tex); let repeat_material_id = stage.append(&PbrMaterial { - albedo_texture: stage.append(&repeat_tex), + albedo_texture, lighting_model: LightingModel::NO_LIGHTING, ..Default::default() }); + + let albedo_texture = stage.append(&mirror_tex); let mirror_material_id = stage.append(&PbrMaterial { - albedo_texture: stage.append(&mirror_tex), + albedo_texture, lighting_model: LightingModel::NO_LIGHTING, ..Default::default() }); @@ -849,50 +884,70 @@ mod test { p }; - let _clamp = stage.draw_unit(&RenderUnit { - camera, - node_path: stage.append_array(&[stage.append(&GltfNode { - mesh: stage.append(&GltfMesh { - primitives: stage.append_array(&[clamp_prim]), - ..Default::default() - }), + let _clamp = { + let primitives = stage.append_array(&[clamp_prim]); + let mesh = stage.append(&GltfMesh { + primitives, ..Default::default() - })]), - vertex_count: 6, - ..Default::default() - }); - let _repeat = stage.draw_unit(&RenderUnit { - camera, - node_path: stage.append_array(&[stage.append(&GltfNode { - mesh: stage.append(&GltfMesh { - primitives: stage.append_array(&[repeat_prim]), - ..Default::default() - }), + }); + let node = stage.append(&GltfNode { + mesh, ..Default::default() - })]), - vertex_count: 6, - transform: stage.append(&Transform { + }); + let node_path = stage.append_array(&[node]); + stage.draw_unit(&RenderUnit { + camera, + node_path, + vertex_count: 6, + ..Default::default() + }) + }; + let _repeat = { + let primitives = stage.append_array(&[repeat_prim]); + let mesh = stage.append(&GltfMesh { + primitives, + ..Default::default() + }); + let node = stage.append(&GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let transform = stage.append(&Transform { translation: Vec3::new(sheet_w + 1.0, 0.0, 0.0), ..Default::default() - }), - ..Default::default() - }); - let _mirror = stage.draw_unit(&RenderUnit { - camera, - node_path: stage.append_array(&[stage.append(&GltfNode { - mesh: stage.append(&GltfMesh { - primitives: stage.append_array(&[mirror_prim]), - ..Default::default() - }), + }); + stage.draw_unit(&RenderUnit { + camera, + node_path, + transform, + vertex_count: 6, ..Default::default() - })]), - vertex_count: 6, - transform: stage.append(&Transform { + }) + }; + let _mirror = { + let primitives = stage.append_array(&[mirror_prim]); + let mesh = stage.append(&GltfMesh { + primitives, + ..Default::default() + }); + let node = stage.append(&GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let transform = stage.append(&Transform { translation: Vec3::new(sheet_w as f32 * 2.0 + 2.0, 0.0, 0.0), ..Default::default() - }), - ..Default::default() - }); + }); + stage.draw_unit(&RenderUnit { + camera, + node_path, + vertex_count: 6, + transform, + ..Default::default() + }) + }; let img = r.render_image().unwrap(); img_diff::assert_img_eq("atlas/negative_uv_wrapping.png", img); diff --git a/crates/renderling/src/atlas/atlas_image.rs b/crates/renderling/src/atlas/atlas_image.rs new file mode 100644 index 00000000..ff18bd50 --- /dev/null +++ b/crates/renderling/src/atlas/atlas_image.rs @@ -0,0 +1,307 @@ +//! Images and texture formats. +//! +//! Used to represent textures before they are sent to the GPU, in the +//! [`AtlasBuilder`]. +use image::EncodableLayout; +use snafu::prelude::*; + +#[derive(Debug, Snafu)] +pub enum AtlasImageError { + #[snafu(display("Cannot load image '{}' from cwd '{:?}': {source}", path.display(), std::env::current_dir()))] + CannotLoad { + source: std::io::Error, + path: std::path::PathBuf, + }, + + #[snafu(display("Image error: {source}"))] + Image { source: image::error::ImageError }, +} + +#[derive(Clone, Copy, Debug)] +pub enum AtlasImageFormat { + R8, + R8G8, + R8G8B8, + R8G8B8A8, + R16, + R16G16, + R16G16B16, + R16G16B16A16, + R16G16B16A16FLOAT, + R32G32B32FLOAT, + R32G32B32A32FLOAT, +} + +impl AtlasImageFormat { + pub fn from_wgpu_texture_format(value: wgpu::TextureFormat) -> Option { + // TODO: implement more AtlasImageFormat conversions from wgpu::TetxureFormat + match value { + wgpu::TextureFormat::R8Uint => Some(AtlasImageFormat::R8), + wgpu::TextureFormat::R16Uint => Some(AtlasImageFormat::R16), + wgpu::TextureFormat::Rg8Uint => Some(AtlasImageFormat::R8G8), + wgpu::TextureFormat::Rg16Uint => Some(AtlasImageFormat::R16G16), + wgpu::TextureFormat::Rgba16Float => Some(AtlasImageFormat::R16G16B16A16FLOAT), + _ => None, + } + } +} + +/// Image data in transit from CPU to GPU. +#[derive(Clone, Debug)] +pub struct AtlasImage { + pub pixels: Vec, + pub width: u32, + pub height: u32, + pub format: AtlasImageFormat, + // Whether or not to convert from sRGB color space into linear color space. + pub apply_linear_transfer: bool, +} + +#[cfg(feature = "gltf")] +impl From for AtlasImage { + fn from(value: gltf::image::Data) -> Self { + let pixels = value.pixels; + let width = value.width; + let height = value.height; + let format = match value.format { + gltf::image::Format::R8 => AtlasImageFormat::R8, + gltf::image::Format::R8G8 => AtlasImageFormat::R8G8, + gltf::image::Format::R8G8B8 => AtlasImageFormat::R8G8B8, + gltf::image::Format::R8G8B8A8 => AtlasImageFormat::R8G8B8A8, + gltf::image::Format::R16 => AtlasImageFormat::R16, + gltf::image::Format::R16G16 => AtlasImageFormat::R16G16, + gltf::image::Format::R16G16B16 => AtlasImageFormat::R16G16B16, + gltf::image::Format::R16G16B16A16 => AtlasImageFormat::R16G16B16A16, + gltf::image::Format::R32G32B32FLOAT => AtlasImageFormat::R32G32B32FLOAT, + gltf::image::Format::R32G32B32A32FLOAT => AtlasImageFormat::R32G32B32A32FLOAT, + }; + + AtlasImage { + pixels, + format, + // Determining this gets deferred until material construction + apply_linear_transfer: false, + width, + height, + } + } +} + +impl From for AtlasImage { + fn from(value: image::DynamicImage) -> Self { + let width = value.width(); + let height = value.height(); + + use AtlasImageFormat::*; + let (pixels, format) = match value { + image::DynamicImage::ImageLuma8(img) => (img.into_vec(), R8), + i @ image::DynamicImage::ImageLumaA8(_) => (i.into_rgba8().into_vec(), R8G8B8A8), + image::DynamicImage::ImageRgb8(img) => (img.into_vec(), R8G8B8), + image::DynamicImage::ImageRgba8(img) => (img.into_vec(), R8G8B8A8), + image::DynamicImage::ImageLuma16(img) => (img.as_bytes().to_vec(), R16), + i @ image::DynamicImage::ImageLumaA16(_) => { + (i.into_rgba16().as_bytes().to_vec(), R16G16B16A16) + } + i @ image::DynamicImage::ImageRgb16(_) => (i.as_bytes().to_vec(), R16G16B16), + i @ image::DynamicImage::ImageRgba16(_) => (i.as_bytes().to_vec(), R16G16B16A16), + i @ image::DynamicImage::ImageRgb32F(_) => (i.as_bytes().to_vec(), R32G32B32FLOAT), + i @ image::DynamicImage::ImageRgba32F(_) => (i.as_bytes().to_vec(), R32G32B32A32FLOAT), + _ => todo!(), + }; + AtlasImage { + pixels, + format, + // Most of the time when people are using `image` to load images, those images + // have color data that was authored in sRGB space. + apply_linear_transfer: true, + width, + height, + } + } +} + +impl TryFrom for AtlasImage { + type Error = AtlasImageError; + + fn try_from(value: std::path::PathBuf) -> Result { + let img = image::open(value).context(ImageSnafu)?; + Ok(img.into()) + } +} + +impl AtlasImage { + pub fn from_hdr_path(p: impl AsRef) -> Result { + let bytes = std::fs::read(p.as_ref()).with_context(|_| CannotLoadSnafu { + path: std::path::PathBuf::from(p.as_ref()), + })?; + Self::from_hdr_bytes(&bytes) + } + + pub fn from_hdr_bytes(bytes: &[u8]) -> Result { + // Decode HDR data. + let decoder = image::codecs::hdr::HdrDecoder::new(bytes).context(ImageSnafu)?; + let width = decoder.metadata().width; + let height = decoder.metadata().height; + let pixels = decoder.read_image_hdr().unwrap(); + + // Add alpha data. + let mut pixel_data: Vec = Vec::new(); + for pixel in pixels { + pixel_data.push(pixel[0]); + pixel_data.push(pixel[1]); + pixel_data.push(pixel[2]); + pixel_data.push(1.0); + } + let mut pixels = vec![]; + pixels.extend_from_slice(bytemuck::cast_slice(pixel_data.as_slice())); + + Ok(Self { + pixels, + width, + height, + format: AtlasImageFormat::R32G32B32A32FLOAT, + apply_linear_transfer: false, + }) + } + + pub fn from_path(p: impl AsRef) -> Result { + Self::try_from(p.as_ref().to_path_buf()) + } + + pub fn into_rgba8(self) -> Option { + let pixels = convert_to_rgba8_bytes(self.pixels, self.format, self.apply_linear_transfer); + image::RgbaImage::from_vec(self.width, self.height, pixels) + } +} + +fn u16_to_u8(c: u16) -> u8 { + ((c as f32 / 65535.0) * 255.0) as u8 +} + +fn f32_to_u8(c: f32) -> u8 { + (c / 255.0) as u8 +} + +/// Interpret/convert the pixel data into rgba8 pixels. +/// +/// This applies the linear transfer function if `apply_linear_transfer` is +/// `true`. +pub fn convert_to_rgba8_bytes( + mut bytes: Vec, + format: AtlasImageFormat, + apply_linear_transfer: bool, +) -> Vec { + use crate::color::*; + log::trace!("converting image of format {format:?}"); + // Convert using linear transfer, if needed + if apply_linear_transfer { + log::trace!(" converting to linear color space (from sRGB)"); + match format { + AtlasImageFormat::R8 + | AtlasImageFormat::R8G8 + | AtlasImageFormat::R8G8B8 + | AtlasImageFormat::R8G8B8A8 => { + bytes.iter_mut().for_each(linear_xfer_u8); + } + AtlasImageFormat::R16 + | AtlasImageFormat::R16G16 + | AtlasImageFormat::R16G16B16 + | AtlasImageFormat::R16G16B16A16 => { + let bytes: &mut [u16] = bytemuck::cast_slice_mut(&mut bytes); + bytes.into_iter().for_each(linear_xfer_u16); + } + AtlasImageFormat::R16G16B16A16FLOAT => { + let bytes: &mut [u16] = bytemuck::cast_slice_mut(&mut bytes); + bytes.into_iter().for_each(linear_xfer_f16); + } + AtlasImageFormat::R32G32B32FLOAT | AtlasImageFormat::R32G32B32A32FLOAT => { + let bytes: &mut [f32] = bytemuck::cast_slice_mut(&mut bytes); + bytes.into_iter().for_each(linear_xfer_f32); + } + } + } + + // Convert to rgba8 + match format { + AtlasImageFormat::R8 => bytes.into_iter().flat_map(|r| [r, 0, 0, 255]).collect(), + AtlasImageFormat::R8G8 => bytes + .chunks_exact(2) + .flat_map(|p| { + if let [r, g] = p { + [*r, *g, 0, 255] + } else { + unreachable!() + } + }) + .collect(), + AtlasImageFormat::R8G8B8 => bytes + .chunks_exact(3) + .flat_map(|p| { + if let [r, g, b] = p { + [*r, *g, *b, 255] + } else { + unreachable!() + } + }) + .collect(), + AtlasImageFormat::R8G8B8A8 => bytes, + AtlasImageFormat::R16 => bytemuck::cast_slice::(&bytes) + .into_iter() + .flat_map(|r| [u16_to_u8(*r), 0, 0, 255]) + .collect(), + AtlasImageFormat::R16G16 => bytemuck::cast_slice::(&bytes) + .chunks_exact(2) + .flat_map(|p| { + if let [r, g] = p { + [u16_to_u8(*r), u16_to_u8(*g), 0, 255] + } else { + unreachable!() + } + }) + .collect(), + AtlasImageFormat::R16G16B16 => bytemuck::cast_slice::(&bytes) + .chunks_exact(3) + .flat_map(|p| { + if let [r, g, b] = p { + [u16_to_u8(*r), u16_to_u8(*g), u16_to_u8(*b), 255] + } else { + unreachable!() + } + }) + .collect(), + + AtlasImageFormat::R16G16B16A16 => bytemuck::cast_slice::(&bytes) + .into_iter() + .copied() + .map(u16_to_u8) + .collect(), + AtlasImageFormat::R16G16B16A16FLOAT => bytemuck::cast_slice::(&bytes) + .into_iter() + .map(|bits| half::f16::from_bits(*bits).to_f32()) + .collect::>() + .chunks_exact(4) + .flat_map(|p| { + if let [r, g, b, a] = p { + [f32_to_u8(*r), f32_to_u8(*g), f32_to_u8(*b), f32_to_u8(*a)] + } else { + unreachable!() + } + }) + .collect(), + AtlasImageFormat::R32G32B32FLOAT => bytemuck::cast_slice::(&bytes) + .chunks_exact(3) + .flat_map(|p| { + if let [r, g, b] = p { + [f32_to_u8(*r), f32_to_u8(*g), f32_to_u8(*b), 255] + } else { + unreachable!() + } + }) + .collect(), + AtlasImageFormat::R32G32B32A32FLOAT => bytemuck::cast_slice::(&bytes) + .into_iter() + .copied() + .map(f32_to_u8) + .collect(), + } +} diff --git a/crates/renderling/src/bloom.rs b/crates/renderling/src/bloom.rs deleted file mode 100644 index 73d2cd11..00000000 --- a/crates/renderling/src/bloom.rs +++ /dev/null @@ -1,414 +0,0 @@ -//! Resources for the bloom filter pass. - -use std::sync::Arc; - -use glam::UVec2; -use moongraph::{View, ViewMut}; - -use crate::{HdrSurface, Uniform}; - -fn create_bloom_texture( - device: &wgpu::Device, - queue: &wgpu::Queue, - width: u32, - height: u32, -) -> crate::Texture { - crate::Texture::new_with( - device, - queue, - Some("bloom pingpong tex"), - Some(wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING), - Some(device.create_sampler(&wgpu::SamplerDescriptor { - mag_filter: wgpu::FilterMode::Nearest, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - })), - wgpu::TextureFormat::Rgba16Float, - 4, - 1, - width, - height, - 1, - &[], - ) -} - -fn create_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("bloom"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: false }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 3, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), - count: None, - }, - ], - }) -} - -fn create_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline { - let bg_layout = create_bindgroup_layout(device); - let pp_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("bloom"), - bind_group_layouts: &[&bg_layout], - push_constant_ranges: &[], - }); - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("bloom filter"), - layout: Some(&pp_layout), - vertex: wgpu::VertexState { - module: &device.create_shader_module(wgpu::include_spirv!( - "linkage/convolution-vertex_generate_mipmap.spv" - )), - entry_point: "convolution::vertex_generate_mipmap", - 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: None, - fragment: Some(wgpu::FragmentState { - module: &device.create_shader_module(wgpu::include_spirv!( - "linkage/convolution-fragment_bloom.spv" - )), - entry_point: "convolution::fragment_bloom", - targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba16Float, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - multisample: wgpu::MultisampleState { - mask: !0, - alpha_to_coverage_enabled: false, - count: 1, - }, - multiview: None, - }) -} - -fn create_bindgroup( - device: &wgpu::Device, - horizontal_uniform: &Uniform, - size_uniform: &Uniform, - texture: &crate::Texture, -) -> wgpu::BindGroup { - device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("bloom filter"), - layout: &create_bindgroup_layout(device), - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer( - horizontal_uniform.buffer().as_entire_buffer_binding(), - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Buffer( - size_uniform.buffer().as_entire_buffer_binding(), - ), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::TextureView(&texture.view), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: wgpu::BindingResource::Sampler(&texture.sampler), - }, - ], - }) -} - -pub struct BloomFilter { - pub on: bool, - textures: [crate::Texture; 2], - tonemap_bindgroup: Arc, - pipeline: wgpu::RenderPipeline, - horizontal_uniform: Uniform, - size_uniform: Uniform, - initial_bindgroup: Option, - bindgroups: [wgpu::BindGroup; 2], -} - -impl BloomFilter { - pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, width: u32, height: u32) -> Self { - let tonemap_bg_layout = crate::hdr::texture_and_sampler_layout(device, Some("bloom")); - let textures = [ - create_bloom_texture(device, queue, width, height), - create_bloom_texture(device, queue, width, height), - ]; - let tonemap_bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("tonemap-bloom"), - layout: &tonemap_bg_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&textures[1].view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&textures[1].sampler), - }, - ], - }); - let size_uniform = Uniform::new( - device, - UVec2::new(width, height), - wgpu::BufferUsages::empty(), - wgpu::ShaderStages::FRAGMENT, - ); - let horizontal_uniform = Uniform::new( - device, - 1, - wgpu::BufferUsages::empty(), - wgpu::ShaderStages::FRAGMENT, - ); - let bindgroups = [ - // bindgroup 'A' reads from pingpong 1 and writes to pingpong 0 (see `run`) - create_bindgroup(device, &horizontal_uniform, &size_uniform, &textures[1]), - // bindgroup 'B' reads from pingpong 0 and writes to pingpong 1 (see `run`) - create_bindgroup(device, &horizontal_uniform, &size_uniform, &textures[0]), - ]; - BloomFilter { - on: true, - pipeline: create_pipeline(device), - size_uniform, - horizontal_uniform, - textures, - initial_bindgroup: None, - bindgroups, - tonemap_bindgroup: tonemap_bindgroup.into(), - } - } - - pub fn run( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - hdr_surface: &crate::HdrSurface, - ) -> Arc { - log::trace!("bloom running"); - let brightness_texture = &hdr_surface.brightness_texture; - // update the size if the size has changed - let size = brightness_texture.texture.size(); - let size = UVec2::new(size.width, size.height); - let prev_size = *self.size_uniform; - if size != prev_size { - log::trace!(" bloom size changed from: {size:?}"); - *self.size_uniform = size; - self.size_uniform.update(queue); - } - - if brightness_texture.texture.size() != self.textures[0].texture.size() { - log::trace!(" brightness size changed"); - let width = size.x; - let height = size.y; - self.textures = [ - create_bloom_texture(device, queue, width, height), - create_bloom_texture(device, queue, width, height), - ]; - self.bindgroups = [ - create_bindgroup( - device, - &self.horizontal_uniform, - &self.size_uniform, - &self.textures[1], - ), - create_bindgroup( - device, - &self.horizontal_uniform, - &self.size_uniform, - &self.textures[0], - ), - ]; - } - - // if the brightness texture is not - if self.initial_bindgroup.is_none() { - log::trace!(" creating initial bindgroup"); - self.initial_bindgroup = Some( - // initial bindgroup reads from brightness texture - create_bindgroup( - device, - &self.horizontal_uniform, - &self.size_uniform, - brightness_texture, - ), - ); - }; - // UNWRAP: safe because we just set it above - let initial_bindgroup = self.initial_bindgroup.as_ref().unwrap(); - - // first do a clear pass on the pingpong textures - crate::frame::conduct_clear_pass( - device, - queue, - Some("bloom filter clear"), - vec![&self.textures[0].view, &self.textures[1].view], - None, - wgpu::Color::TRANSPARENT, - ); - - for i in 0..10 { - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some(&format!("bloom-filter{i}")), - }); - - // index == 0 is group 'A', 1 is group 'B' - let index = i % 2; - - *self.horizontal_uniform = index as u32; - self.horizontal_uniform.update(queue); - - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some(&format!("bloomfilter{i}_index{index}")), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &self.textures[index].view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, - })], - depth_stencil_attachment: None, - }); - render_pass.set_pipeline(&self.pipeline); - - // * if i == 0 we read from brightness_texture and write to textures[0] - // * if index == 1 we read from textures[0] and write to textures[1] - // * if index == 0 we read from textures[1] and write to textures[0] - let bindgroup = if i == 0 { - initial_bindgroup - } else { - &self.bindgroups[index] - }; - render_pass.set_bind_group(0, bindgroup, &[]); - render_pass.draw(0..6, 0..1); - } - - queue.submit([encoder.finish()]); - } - - self.tonemap_bindgroup.clone() - } -} - -pub struct BloomResult(pub Option>); - -pub fn bloom_filter( - (device, queue, mut bloom, hdr): ( - View, - View, - ViewMut, - View, - ), -) -> Result<(BloomResult,), crate::WgpuStateError> { - let may_bg = if bloom.on { - let bg = bloom.run(&device, &queue, &hdr); - Some(bg) - } else { - None - }; - Ok((BloomResult(may_bg),)) -} - -#[cfg(test)] -mod test { - use glam::{Mat4, Vec3}; - - use crate::Renderling; - - use super::BloomFilter; - - #[test] - fn bloom_on_off() { - let mut renderling = - Renderling::headless(100, 100).with_background_color(glam::Vec4::splat(1.0)); - let mut builder = renderling.new_scene(); - let loader = builder - .gltf_load("../../gltf/EmissiveStrengthTest.glb") - .unwrap(); - // find the bounding box of the model so we can display it correctly - let mut min = Vec3::splat(f32::INFINITY); - let mut max = Vec3::splat(f32::NEG_INFINITY); - for node in loader.nodes.iter() { - let entity = builder.entities.get(node.entity_id.index()).unwrap(); - let (translation, rotation, scale) = entity.get_world_transform(&builder.entities); - let tfrm = Mat4::from_scale_rotation_translation(scale, rotation, translation); - if let Some(mesh_index) = node.gltf_mesh_index { - for primitive in loader.meshes.get(mesh_index).unwrap().iter() { - let bbmin = tfrm.transform_point3(primitive.bounding_box.min); - let bbmax = tfrm.transform_point3(primitive.bounding_box.max); - min = min.min(bbmin); - max = max.max(bbmax); - } - } - } - - let length = min.distance(max); - let (projection, _) = crate::camera::default_perspective(100.0, 100.0); - let view = crate::camera::look_at(Vec3::new(0.0, 0.0, length), Vec3::ZERO, Vec3::Y); - builder.set_camera(projection, view); - let scene = builder.build().unwrap(); - - renderling.setup_render_graph(crate::RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() - }); - let img = renderling.render_image().unwrap(); - img_diff::assert_img_eq("bloom/on.png", img); - - { - let bloom = renderling - .graph - .get_resource_mut::() - .unwrap() - .unwrap(); - bloom.on = false; - } - let img = renderling.render_image().unwrap(); - img_diff::assert_img_eq("bloom/off.png", img); - } -} diff --git a/crates/renderling/src/color.rs b/crates/renderling/src/color.rs index 40ca48cf..eeffa559 100644 --- a/crates/renderling/src/color.rs +++ b/crates/renderling/src/color.rs @@ -4,6 +4,10 @@ pub fn linear_xfer_u8(c: &mut u8) { *c = ((*c as f32 / 255.0).powf(2.2) * 255.0) as u8; } +pub fn opto_xfer_u8(c: &mut u8) { + *c = ((*c as f32 / 255.0).powf(1.0 / 2.2) * 255.0) as u8; +} + pub fn linear_xfer_u16(c: &mut u16) { *c = ((*c as f32 / 65535.0).powf(2.2) * 65535.0) as u16; } diff --git a/crates/renderling/src/cubemap.rs b/crates/renderling/src/cubemap.rs index 8eed46d7..f785778a 100644 --- a/crates/renderling/src/cubemap.rs +++ b/crates/renderling/src/cubemap.rs @@ -1,9 +1,5 @@ //! Render pipelines and layouts for creating cubemaps. -use renderling_shader::stage::GpuConstants; - -use crate::Uniform; - pub fn cubemap_making_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("cubemap-making bindgroup"), @@ -12,7 +8,7 @@ pub fn cubemap_making_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroup binding: 0, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, + ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None, }, @@ -41,7 +37,7 @@ pub fn cubemap_making_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroup pub fn cubemap_making_bindgroup( device: &wgpu::Device, label: Option<&str>, - constants: &Uniform, + buffer: &wgpu::Buffer, // The texture to sample the environment from texture: &crate::Texture, ) -> wgpu::BindGroup { @@ -51,9 +47,7 @@ pub fn cubemap_making_bindgroup( entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::Buffer( - constants.buffer().as_entire_buffer_binding(), - ), + resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()), }, wgpu::BindGroupEntry { binding: 1, @@ -74,9 +68,8 @@ impl CubemapMakingRenderPipeline { /// images. pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { log::trace!("creating cubemap-making render pipeline with format '{format:?}'"); - let vertex_shader = device.create_shader_module(wgpu::include_spirv!( - "linkage/skybox-vertex_position_passthru.spv" - )); + let vertex_shader = + device.create_shader_module(wgpu::include_spirv!("linkage/skybox-vertex_cubemap.spv")); let fragment_shader = device.create_shader_module(wgpu::include_spirv!( "linkage/skybox-fragment_equirectangular.spv" )); @@ -92,17 +85,8 @@ impl CubemapMakingRenderPipeline { layout: Some(&pp_layout), vertex: wgpu::VertexState { module: &vertex_shader, - entry_point: "skybox::vertex_position_passthru", - buffers: &[wgpu::VertexBufferLayout { - array_stride: { - let position_size = std::mem::size_of::(); - position_size as wgpu::BufferAddress - }, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &wgpu::vertex_attr_array![ - 0 => Float32x3 - ], - }], + entry_point: "skybox::vertex_cubemap", + buffers: &[], }, primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, diff --git a/crates/renderling/src/diffuse_irradiance.rs b/crates/renderling/src/diffuse_irradiance.rs index 9c83bed6..5e53a2fc 100644 --- a/crates/renderling/src/diffuse_irradiance.rs +++ b/crates/renderling/src/diffuse_irradiance.rs @@ -74,8 +74,8 @@ impl DiffuseIrradianceConvolutionRenderPipeline { /// Create the rendering pipeline that performs a convolution. pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { log::trace!("creating convolution render pipeline with format '{format:?}'"); - let vertex_shader = device - .create_shader_module(wgpu::include_spirv!("linkage/vertex_position_passthru.spv")); + let vertex_shader = + device.create_shader_module(wgpu::include_spirv!("linkage/vertex_cubemap.spv")); log::trace!("creating fragment shader"); let fragment_shader = device.create_shader_module(wgpu::include_wgsl!( "wgsl/diffuse_irradiance_convolution.wgsl" @@ -88,23 +88,15 @@ impl DiffuseIrradianceConvolutionRenderPipeline { bind_group_layouts: &[&bg_layout], push_constant_ranges: &[], }); + // TODO: merge irradiance pipeline with the pipeline in cubemap.rs let pipeline = DiffuseIrradianceConvolutionRenderPipeline(device.create_render_pipeline( &wgpu::RenderPipelineDescriptor { label: Some("convolution pipeline"), layout: Some(&pp_layout), vertex: wgpu::VertexState { module: &vertex_shader, - entry_point: "skybox::vertex_position_passthru", - buffers: &[wgpu::VertexBufferLayout { - array_stride: { - let position_size = std::mem::size_of::(); - position_size as wgpu::BufferAddress - }, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &wgpu::vertex_attr_array![ - 0 => Float32x3 - ], - }], + entry_point: "skybox::vertex_cubemap", + buffers: &[], }, primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, diff --git a/crates/renderling/src/hdr.rs b/crates/renderling/src/hdr.rs index f8d5fda5..0a09668a 100644 --- a/crates/renderling/src/hdr.rs +++ b/crates/renderling/src/hdr.rs @@ -1,12 +1,13 @@ //! High definition rendering types and techniques. //! //! Also includes bloom effect. +use crabslab::{CpuSlab, Slab, SlabItem, WgpuBuffer}; use moongraph::*; use renderling_shader::tonemapping::TonemapConstants; use crate::{ frame::FrameTextureView, math::Vec4, BackgroundColor, DepthTexture, Device, Queue, - RenderTarget, ScreenSize, Uniform, WgpuStateError, + RenderTarget, ScreenSize, WgpuStateError, }; /// A texture, tonemapping pipeline and uniform used for high dynamic range @@ -15,12 +16,9 @@ use crate::{ /// See https://learnopengl.com/Advanced-Lighting/HDR. pub struct HdrSurface { pub hdr_texture: crate::Texture, - pub brightness_texture: crate::Texture, - pub texture_bindgroup: wgpu::BindGroup, - pub no_bloom_texture: crate::Texture, - pub no_bloom_bindgroup: wgpu::BindGroup, + pub bindgroup: wgpu::BindGroup, pub tonemapping_pipeline: wgpu::RenderPipeline, - pub constants: Uniform, + pub slab: CpuSlab, } impl HdrSurface { @@ -59,59 +57,66 @@ impl HdrSurface { ) } - pub fn create_texture_bindgroup( + pub fn create_bindgroup( device: &wgpu::Device, - texture: &crate::Texture, + hdr_texture: &crate::Texture, + slab_buffer: &wgpu::Buffer, ) -> wgpu::BindGroup { - let hdr_texture_layout = scene_hdr_surface_bindgroup_layout(&device); device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("HdrSurface texture bindgroup"), - layout: &hdr_texture_layout, + label: Some("HdrSurface bindgroup"), + layout: &bindgroup_layout(&device, Some("HdrSurface bindgroup")), entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::TextureView(&texture.view), + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: slab_buffer, + offset: 0, + size: None, + }), }, wgpu::BindGroupEntry { binding: 1, - resource: wgpu::BindingResource::Sampler(&texture.sampler), + resource: wgpu::BindingResource::TextureView(&hdr_texture.view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&hdr_texture.sampler), }, ], }) } - pub fn color_attachments(&self) -> [Option; 2] { - [ - Some(wgpu::RenderPassColorAttachment { - view: &self.hdr_texture.view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, - }), - Some(wgpu::RenderPassColorAttachment { - view: &self.brightness_texture.view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, - }), - ] + pub fn color_attachments(&self) -> [Option; 1] { + [Some(wgpu::RenderPassColorAttachment { + view: &self.hdr_texture.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, + })] } } -pub fn texture_and_sampler_layout( - device: &wgpu::Device, - label: Option<&str>, -) -> wgpu::BindGroupLayout { +pub fn bindgroup_layout(device: &wgpu::Device, label: Option<&str>) -> wgpu::BindGroupLayout { device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label, entries: &[ + // slab wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + // hdr texture + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: false }, view_dimension: wgpu::TextureViewDimension::D2, @@ -119,8 +124,9 @@ pub fn texture_and_sampler_layout( }, count: None, }, + // hdr sampler wgpu::BindGroupLayoutEntry { - binding: 1, + binding: 2, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), count: None, @@ -129,16 +135,6 @@ pub fn texture_and_sampler_layout( }) } -fn scene_hdr_surface_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { - texture_and_sampler_layout(device, Some("hdr buffer bindgroup")) -} - -/// Layout for the bloom texture+sampler that get added to the color before -/// tonemapping. -fn blend_bloom_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { - texture_and_sampler_layout(device, Some("blend bloom")) -} - pub fn create_hdr_render_surface( (device, queue, size, target): ( View, @@ -147,13 +143,10 @@ pub fn create_hdr_render_surface( View, ), ) -> Result<(HdrSurface,), WgpuStateError> { - let (constants, constants_layout) = Uniform::new_and_layout( - &device, - TonemapConstants::default(), - wgpu::BufferUsages::UNIFORM, - wgpu::ShaderStages::FRAGMENT, - ); - let bloom_layout = blend_bloom_layout(&device); + let buffer = WgpuBuffer::new(&*device, &*queue, TonemapConstants::slab_size()); + let mut slab = CpuSlab::new(buffer); + // TODO: make the tonemapping configurable + slab.write(0u32.into(), &TonemapConstants::default()); let size = wgpu::Extent3d { width: size.width, height: size.height, @@ -165,10 +158,10 @@ pub fn create_hdr_render_surface( device.create_shader_module(wgpu::include_spirv!("linkage/tonemapping-vertex.spv")); let fragment_shader = device.create_shader_module(wgpu::include_spirv!("linkage/tonemapping-fragment.spv")); - let hdr_texture_layout = scene_hdr_surface_bindgroup_layout(&device); + let hdr_layout = bindgroup_layout(&device, Some("hdr tonemapping")); let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label, - bind_group_layouts: &[&hdr_texture_layout, &constants_layout, &bloom_layout], + bind_group_layouts: &[&hdr_layout], push_constant_ranges: &[], }); let tonemapping_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { @@ -202,28 +195,14 @@ pub fn create_hdr_render_surface( multiview: None, }); - let no_bloom_texture = HdrSurface::create_texture(&device, &queue, 1, 1); - let no_bloom_bindgroup = HdrSurface::create_texture_bindgroup(&device, &no_bloom_texture); - Ok((HdrSurface { - texture_bindgroup: HdrSurface::create_texture_bindgroup(&device, &hdr_texture), - brightness_texture: HdrSurface::create_texture(&device, &queue, size.width, size.height), - no_bloom_texture, - no_bloom_bindgroup, + bindgroup: HdrSurface::create_bindgroup(&device, &hdr_texture, slab.as_ref().get_buffer()), hdr_texture, tonemapping_pipeline, - constants, + slab, },)) } -/// Update the `HdrSurface` uniforms. -pub fn hdr_surface_update( - (queue, mut hdr_surface): (View, ViewMut), -) -> Result<(), WgpuStateError> { - hdr_surface.constants.update(&queue); - Ok(()) -} - /// Conduct a clear pass on the window surface, the hdr surface and the depth /// texture. pub fn clear_surface_hdr_and_depth( @@ -252,13 +231,25 @@ pub fn clear_surface_hdr_and_depth( &device, &queue, Some("clear_frame_and_depth"), - vec![ - &frame.view, - &hdr.hdr_texture.view, - &hdr.brightness_texture.view, - ], + vec![&frame.view, &hdr.hdr_texture.view], Some(&depth_view), color, ); Ok(()) } + +/// Resize the HDR surface to match [`ScreenSize`]. +pub fn resize_hdr_surface( + (device, queue, size, mut hdr): ( + View, + View, + View, + ViewMut, + ), +) -> Result<(), WgpuStateError> { + let ScreenSize { width, height } = *size; + hdr.hdr_texture = HdrSurface::create_texture(&device, &queue, width, height); + hdr.bindgroup = + HdrSurface::create_bindgroup(&device, &hdr.hdr_texture, hdr.slab.as_ref().get_buffer()); + Ok(()) +} diff --git a/crates/renderling/src/ibl/diffuse_irradiance.rs b/crates/renderling/src/ibl/diffuse_irradiance.rs index 010395bf..403c23a0 100644 --- a/crates/renderling/src/ibl/diffuse_irradiance.rs +++ b/crates/renderling/src/ibl/diffuse_irradiance.rs @@ -1,7 +1,4 @@ //! Pipeline and bindings for for diffuse irradiance convolution shaders. -use renderling_shader::stage::GpuConstants; - -use crate::Uniform; pub fn diffuse_irradiance_convolution_bindgroup_layout( device: &wgpu::Device, @@ -13,7 +10,7 @@ pub fn diffuse_irradiance_convolution_bindgroup_layout( binding: 0, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, + ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None, }, @@ -42,7 +39,7 @@ pub fn diffuse_irradiance_convolution_bindgroup_layout( pub fn diffuse_irradiance_convolution_bindgroup( device: &wgpu::Device, label: Option<&str>, - constants: &Uniform, + buffer: &wgpu::Buffer, // The texture to sample the environment from texture: &crate::Texture, ) -> wgpu::BindGroup { @@ -52,9 +49,7 @@ pub fn diffuse_irradiance_convolution_bindgroup( entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::Buffer( - constants.buffer().as_entire_buffer_binding(), - ), + resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()), }, wgpu::BindGroupEntry { binding: 1, @@ -74,9 +69,8 @@ impl DiffuseIrradianceConvolutionRenderPipeline { /// Create the rendering pipeline that performs a convolution. pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { log::trace!("creating convolution render pipeline with format '{format:?}'"); - let vertex_shader = device.create_shader_module(wgpu::include_spirv!( - "../linkage/skybox-vertex_position_passthru.spv" - )); + let vertex_shader = device + .create_shader_module(wgpu::include_spirv!("../linkage/skybox-vertex_cubemap.spv")); log::trace!("creating fragment shader"); let fragment_shader = device.create_shader_module(wgpu::include_wgsl!( // TODO: rewrite this shader in Rust after atomics are added to naga spv @@ -90,23 +84,15 @@ impl DiffuseIrradianceConvolutionRenderPipeline { bind_group_layouts: &[&bg_layout], push_constant_ranges: &[], }); + // TODO: merge irradiance pipeline with cubemap let pipeline = DiffuseIrradianceConvolutionRenderPipeline(device.create_render_pipeline( &wgpu::RenderPipelineDescriptor { label: Some("convolution pipeline"), layout: Some(&pp_layout), vertex: wgpu::VertexState { module: &vertex_shader, - entry_point: "skybox::vertex_position_passthru", - buffers: &[wgpu::VertexBufferLayout { - array_stride: { - let position_size = std::mem::size_of::(); - position_size as wgpu::BufferAddress - }, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &wgpu::vertex_attr_array![ - 0 => Float32x3 - ], - }], + entry_point: "skybox::vertex_cubemap", + buffers: &[], }, primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, diff --git a/crates/renderling/src/ibl/prefiltered_environment.rs b/crates/renderling/src/ibl/prefiltered_environment.rs index 117c8651..b662c672 100644 --- a/crates/renderling/src/ibl/prefiltered_environment.rs +++ b/crates/renderling/src/ibl/prefiltered_environment.rs @@ -1,13 +1,9 @@ //! Pipeline for creating a prefiltered environment map from an existing //! environment cubemap. -use crate::Uniform; -use renderling_shader::stage::GpuConstants; - pub fn create_pipeline_and_bindgroup( device: &wgpu::Device, - constants: &Uniform, - roughness: &Uniform, + buffer: &wgpu::Buffer, environment_texture: &crate::Texture, ) -> (wgpu::RenderPipeline, wgpu::BindGroup) { let label = Some("prefiltered environment"); @@ -16,9 +12,9 @@ pub fn create_pipeline_and_bindgroup( entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, - visibility: wgpu::ShaderStages::VERTEX, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, + ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None, }, @@ -27,16 +23,6 @@ pub fn create_pipeline_and_bindgroup( wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::Cube, @@ -45,7 +31,7 @@ pub fn create_pipeline_and_bindgroup( count: None, }, wgpu::BindGroupLayoutEntry { - binding: 3, + binding: 2, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, @@ -59,22 +45,14 @@ pub fn create_pipeline_and_bindgroup( entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::Buffer( - constants.buffer().as_entire_buffer_binding(), - ), + resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()), }, wgpu::BindGroupEntry { binding: 1, - resource: wgpu::BindingResource::Buffer( - roughness.buffer().as_entire_buffer_binding(), - ), - }, - wgpu::BindGroupEntry { - binding: 2, resource: wgpu::BindingResource::TextureView(&environment_texture.view), }, wgpu::BindGroupEntry { - binding: 3, + binding: 2, resource: wgpu::BindingResource::Sampler(&environment_texture.sampler), }, ], @@ -96,13 +74,7 @@ pub fn create_pipeline_and_bindgroup( vertex: wgpu::VertexState { module: &vertex_shader, entry_point: "convolution::vertex_prefilter_environment_cubemap", - buffers: &[wgpu::VertexBufferLayout { - array_stride: 3 * std::mem::size_of::() as u64, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &wgpu::vertex_attr_array![ - 0 => Float32x3 - ], - }], + buffers: &[], }, primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, diff --git a/crates/renderling/src/lib.rs b/crates/renderling/src/lib.rs index b0959ff8..4282cabd 100644 --- a/crates/renderling/src/lib.rs +++ b/crates/renderling/src/lib.rs @@ -18,8 +18,7 @@ //! - forward+ style pipeline, configurable lighting model per material //! - [ ] light tiling //! - [ ] occlusion culling -//! - [x] physically based shading -//! atlas) +//! - [x] physically based shading atlas) //! - [x] gltf support //! - [x] scenes, nodes //! - [x] cameras @@ -30,7 +29,7 @@ //! - [x] animations //! - [x] high definition rendering //! - [x] image based lighting -//! - [x] bloom +//! - [ ] bloom //! - [ ] ssao //! - [ ] depth of field //! @@ -41,7 +40,6 @@ // TODO: Audit the API and make it more ergonomic/predictable. mod atlas; -pub mod bloom; mod buffer_array; mod camera; pub mod cubemap; @@ -52,16 +50,15 @@ mod linkage; pub mod math; pub mod mesh; mod renderer; -mod scene; mod skybox; -mod slab; mod stage; mod state; -#[cfg(feature = "text")] -mod text; +//#[cfg(feature = "text")] +//mod text; mod texture; +mod tonemapping; //mod tutorial; -mod ui; +//mod ui; mod uniform; pub use atlas::*; @@ -70,15 +67,14 @@ pub use camera::*; pub use hdr::*; use image::GenericImageView; pub use renderer::*; -pub use scene::*; pub use skybox::*; -pub use slab::*; pub use stage::*; pub use state::*; -#[cfg(feature = "text")] -pub use text::*; +//#[cfg(feature = "text")] +//pub use text::*; pub use texture::*; -pub use ui::*; +pub use tonemapping::*; +//pub use ui::*; pub use uniform::*; pub mod color; @@ -96,8 +92,8 @@ pub mod graph { pub type RenderNode = Node; } +pub use crabslab::*; 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::*; @@ -147,97 +143,6 @@ impl crate::shader::SampleCube for CpuCubemap { } } -/// Set up the render graph, including: -/// * 3d scene objects -/// * skybox -/// * bloom filter -/// * hdr tonemapping -/// * UI -/// -/// This is mostly for internal use. See [`Renderling::setup_render_graph`]. -pub fn setup_render_graph( - r: &mut Renderling, - scene: Scene, - ui_scene: UiScene, - ui_objects: impl IntoIterator, - with_screen_capture: bool, - with_bloom: bool, -) { - // add resources - let ui_objects = UiDrawObjects(ui_objects.into_iter().collect::>()); - r.graph.add_resource(ui_scene); - r.graph.add_resource(ui_objects); - r.graph.add_resource(scene); - let ui_pipeline = UiRenderPipeline( - r.graph - .visit(|(device, target): (View, View)| { - create_ui_pipeline(&device, target.format()) - }) - .unwrap(), - ); - r.graph.add_resource(ui_pipeline); - - let (hdr_surface,) = r - .graph - .visit(hdr::create_hdr_render_surface) - .unwrap() - .unwrap(); - let device = r.get_device(); - let queue = r.get_queue(); - let hdr_texture_format = hdr_surface.hdr_texture.texture.format(); - let size = hdr_surface.hdr_texture.texture.size(); - let scene_render_pipeline = - SceneRenderPipeline(create_scene_render_pipeline(device, hdr_texture_format)); - let compute_cull_pipeline = - SceneComputeCullPipeline(create_scene_compute_cull_pipeline(device)); - let skybox_pipeline = crate::skybox::create_skybox_render_pipeline(device, hdr_texture_format); - let mut bloom = crate::bloom::BloomFilter::new(device, queue, size.width, size.height); - bloom.on = with_bloom; - r.graph.add_resource(scene_render_pipeline); - r.graph.add_resource(hdr_surface); - r.graph.add_resource(compute_cull_pipeline); - r.graph.add_resource(skybox_pipeline); - r.graph.add_resource(bloom); - - // pre-render subgraph - use frame::{clear_depth, create_frame, present}; - - #[cfg(not(target_arch = "wasm32"))] - let scene_cull = scene_cull_gpu; - #[cfg(target_arch = "wasm32")] - let scene_cull = scene_cull_cpu; - r.graph - .add_subgraph(graph!( - create_frame, - clear_surface_hdr_and_depth, - hdr_surface_update, - scene_update < scene_cull, - ui_scene_update - )) - .add_barrier(); - - // render subgraph - use crate::bloom::bloom_filter; - r.graph - .add_subgraph(graph!( - scene_render - < skybox_render - < bloom_filter - < tonemapping - < clear_depth - < ui_scene_render - )) - .add_barrier(); - - // post-render subgraph - r.graph.add_subgraph(if with_screen_capture { - use crate::frame::copy_frame_to_post; - graph!(copy_frame_to_post < present) - } else { - graph!(present) - }); -} - #[cfg(test)] #[ctor::ctor] fn init_logging() { @@ -255,10 +160,11 @@ fn init_logging() { #[cfg(test)] mod test { use super::*; - use glam::{Mat3, Mat4, Quat, UVec2, Vec2, Vec3, Vec4, Vec4Swizzles}; + use glam::{Mat3, Mat4, Quat, Vec2, Vec3, Vec4}; use pretty_assertions::assert_eq; use renderling_shader::{ gltf as gl, + pbr::PbrMaterial, stage::{light::*, Camera, RenderUnit, Transform, Vertex}, }; @@ -298,7 +204,7 @@ mod test { // This tests our ability to draw a CMYK triangle in the top left corner. fn cmy_triangle_sanity() { let mut r = Renderling::headless(100, 100).with_background_color(Vec4::splat(1.0)); - let stage = r.new_stage(); + let mut stage = r.new_stage(); stage.configure_graph(&mut r, true); let (projection, view) = default_ortho2d(100.0, 100.0); let camera = stage.append(&Camera { @@ -310,8 +216,9 @@ mod test { .new_mesh() .with_primitive(right_tri_vertices(), [], Id::NONE) .build(); + let mesh = stage.append(&mesh); let node = stage.append(&gl::GltfNode { - mesh: stage.append(&mesh), + mesh, ..Default::default() }); let node_path = stage.append_array(&[node]); @@ -332,7 +239,7 @@ mod test { // We do this by writing over the previous transform in the stage. fn cmy_triangle_update_transform() { let mut r = Renderling::headless(100, 100).with_background_color(Vec4::splat(1.0)); - let stage = r.new_stage(); + let mut stage = r.new_stage(); stage.configure_graph(&mut r, true); let (projection, view) = default_ortho2d(100.0, 100.0); let camera = stage.append(&Camera::new(projection, view)); @@ -340,14 +247,16 @@ mod test { .new_mesh() .with_primitive(right_tri_vertices(), [], Id::NONE) .build(); + let mesh = stage.append(&mesh); let node = stage.append(&gl::GltfNode { - mesh: stage.append(&mesh), + mesh, ..Default::default() }); let transform = stage.append(&Transform::default()); + let node_path = stage.append_array(&[node]); let _tri = stage.draw_unit(&RenderUnit { camera, - node_path: stage.append_array(&[node]), + node_path, vertex_count: 3, transform, ..Default::default() @@ -355,16 +264,14 @@ mod test { let _ = r.render_image().unwrap(); - stage - .write( - transform, - &Transform { - translation: Vec3::new(100.0, 0.0, 0.0), - rotation: Quat::from_axis_angle(Vec3::Z, std::f32::consts::FRAC_PI_2), - scale: Vec3::new(0.5, 0.5, 1.0), - }, - ) - .unwrap(); + stage.write( + transform, + &Transform { + translation: Vec3::new(100.0, 0.0, 0.0), + rotation: Quat::from_axis_angle(Vec3::Z, std::f32::consts::FRAC_PI_2), + scale: Vec3::new(0.5, 0.5, 1.0), + }, + ); let img = r.render_image().unwrap(); img_diff::assert_img_eq("cmy_triangle_update_transform.png", img); @@ -413,7 +320,7 @@ mod test { // Tests our ability to draw a CMYK cube. fn cmy_cube_sanity() { let mut r = Renderling::headless(100, 100).with_background_color(Vec4::splat(1.0)); - let stage = r.new_stage(); + let mut stage = r.new_stage(); stage.configure_graph(&mut r, true); let camera_position = Vec3::new(0.0, 12.0, 20.0); let camera = stage.append(&Camera { @@ -427,24 +334,27 @@ mod test { .new_mesh() .with_primitive(vertices, [], Id::NONE) .build(); + let mesh = stage.append(&mesh); + let node = stage.append(&gl::GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); let transform = Transform { scale: Vec3::new(6.0, 6.0, 6.0), rotation: Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4), ..Default::default() }; - let node = stage.append(&gl::GltfNode { - mesh: stage.append(&mesh), - ..Default::default() - }); + let transform = stage.append(&transform); let _cube = stage.draw_unit(&RenderUnit { camera, vertex_count, - node_path: stage.append_array(&[node]), - transform: stage.append(&transform), + node_path, + transform, ..Default::default() }); let img = r.render_image().unwrap(); - img_diff::assert_img_eq("cmy_cube.png", img); + img_diff::assert_img_eq("cmy_cube/sanity.png", img); } #[test] @@ -452,7 +362,7 @@ mod test { // them. fn cmy_cube_visible() { let mut r = Renderling::headless(100, 100).with_background_color(Vec4::splat(1.0)); - let stage = r.new_stage(); + let mut stage = r.new_stage(); stage.configure_graph(&mut r, true); let (projection, view) = camera::default_perspective(100.0, 100.0); let camera = stage.append(&Camera { @@ -466,8 +376,9 @@ mod test { .new_mesh() .with_primitive(vertices, [], Id::NONE) .build(); + let mesh = stage.append(&mesh); let node = stage.append(&gl::GltfNode { - mesh: stage.append(&mesh), + mesh, ..Default::default() }); let mut render_unit = RenderUnit { @@ -491,7 +402,7 @@ mod test { // we should see two colored cubes let img = r.render_image().unwrap(); - img_diff::assert_img_eq("cmy_cube_visible_before.png", img.clone()); + img_diff::assert_img_eq("cmy_cube/visible_before.png", img.clone()); let img_before = img; // update cube two making it invisible @@ -499,22 +410,22 @@ mod test { // we should see only one colored cube let img = r.render_image().unwrap(); - img_diff::assert_img_eq("cmy_cube_visible_after.png", img); + img_diff::assert_img_eq("cmy_cube/visible_after.png", img); // update cube two making in visible again stage.show_unit(cube_two); // we should see two colored cubes again let img = r.render_image().unwrap(); - img_diff::assert_eq("cmy_cube_visible_before_again.png", img_before, img); + img_diff::assert_eq("cmy_cube/visible_before_again.png", img_before, img); } #[test] - // Tests the ability to specify indexed vertices, as well as the ability to update - // a field within a struct stored on the slab by offset. + // Tests the ability to specify indexed vertices, as well as the ability to + // update a field within a struct stored on the slab by offset. fn cmy_cube_remesh() { let mut r = Renderling::headless(100, 100).with_background_color(Vec4::splat(1.0)); - let stage = r.new_stage().with_lighting(false); + let mut stage = r.new_stage().with_lighting(false); stage.configure_graph(&mut r, true); let (projection, view) = camera::default_perspective(100.0, 100.0); let camera = stage.append(&Camera { @@ -529,8 +440,9 @@ mod test { .new_mesh() .with_primitive(pyramid_vertices, pyramid_indices, Id::NONE) .build(); + let mesh = stage.append(&pyramid_mesh); let pyramid_node = stage.append(&gl::GltfNode { - mesh: stage.append(&pyramid_mesh), + mesh, ..Default::default() }); let pyramid_node_path = stage.append_array(&[pyramid_node]); @@ -542,34 +454,36 @@ mod test { .new_mesh() .with_primitive(cube_vertices, cube_indices, Id::NONE) .build(); + let mesh = stage.append(&cube_mesh); let cube_node = stage.append(&gl::GltfNode { - mesh: stage.append(&cube_mesh), + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[cube_node]); + let transform = stage.append(&Transform { + scale: Vec3::new(10.0, 10.0, 10.0), ..Default::default() }); + let cube = stage.draw_unit(&RenderUnit { camera, vertex_count: cube_vertex_count as u32, - node_path: stage.append_array(&[cube_node]), - transform: stage.append(&Transform { - scale: Vec3::new(10.0, 10.0, 10.0), - ..Default::default() - }), + node_path, + transform, ..Default::default() }); - // we should see a cube + // we should see a cube (in sRGB color space) let img = r.render_image().unwrap(); - img_diff::assert_img_eq("cmy_cube_remesh_before.png", img); + img_diff::assert_img_eq("cmy_cube/remesh_before.png", img); // Update the cube mesh to a pyramid by overwriting the `.node_path` field // of `RenderUnit` - stage - .write(cube + RenderUnit::offset_of_node_path(), &pyramid_node_path) - .unwrap(); + stage.write(cube + RenderUnit::offset_of_node_path(), &pyramid_node_path); - // we should see a pyramid + // we should see a pyramid (in sRGB color space) let img = r.render_image().unwrap(); - img_diff::assert_img_eq("cmy_cube_remesh_after.png", img); + img_diff::assert_img_eq("cmy_cube/remesh_after.png", img); } fn gpu_uv_unit_cube() -> Vec { @@ -619,10 +533,11 @@ mod test { } #[test] - // Tests that updating the material actually updates the rendering of an unlit mesh + // Tests that updating the material actually updates the rendering of an unlit + // mesh fn unlit_textured_cube_material() { let mut r = Renderling::headless(100, 100).with_background_color(Vec4::splat(0.0)); - let stage = r.new_stage(); + let mut stage = r.new_stage(); stage.configure_graph(&mut r, true); let (projection, view) = camera::default_perspective(100.0, 100.0); let camera = stage.append(&Camera { @@ -630,8 +545,8 @@ mod test { view, ..Default::default() }); - let sandstone = SceneImage::from(image::open("../../img/sandstone.png").unwrap()); - let dirt = SceneImage::from(image::open("../../img/dirt.jpg").unwrap()); + let sandstone = AtlasImage::from(image::open("../../img/sandstone.png").unwrap()); + let dirt = AtlasImage::from(image::open("../../img/dirt.jpg").unwrap()); let textures = stage.set_images([sandstone, dirt]).unwrap(); let sandstone_tex = textures[0]; let dirt_tex = textures[1]; @@ -647,17 +562,20 @@ mod test { .new_mesh() .with_primitive(vertices, [], material_id) .build(); + let mesh = stage.append(&mesh); let node = stage.append(&gl::GltfNode { - mesh: stage.append(&mesh), + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let transform = stage.append(&Transform { + scale: Vec3::new(10.0, 10.0, 10.0), ..Default::default() }); let cube = stage.draw_unit(&RenderUnit { camera, - node_path: stage.append_array(&[node]), - transform: stage.append(&Transform { - scale: Vec3::new(10.0, 10.0, 10.0), - ..Default::default() - }), + node_path, + transform, vertex_count, ..Default::default() }); @@ -668,7 +586,7 @@ mod test { img_diff::assert_img_eq("unlit_textured_cube_material_before.png", img); // update the material's texture on the GPU - stage.write(sandstone_tex_id, &dirt_tex).unwrap(); + stage.write(sandstone_tex_id, &dirt_tex); // we should see a cube with a dirty texture let img = r.render_image().unwrap(); @@ -681,7 +599,7 @@ mod test { fn multi_node_scene() { let mut r = Renderling::headless(100, 100).with_background_color(Vec3::splat(0.0).extend(1.0)); - let stage = r.new_stage(); + let mut stage = r.new_stage(); stage.configure_graph(&mut r, true); let (projection, view) = camera::default_ortho2d(100.0, 100.0); @@ -692,8 +610,9 @@ mod test { }); // now test the textures functionality - let img = SceneImage::from_path("../../img/cheetah.jpg").unwrap(); - let textures = stage.append_array(&stage.set_images([img]).unwrap()); + let img = AtlasImage::from_path("../../img/cheetah.jpg").unwrap(); + let textures = stage.set_images([img]).unwrap(); + let textures = stage.append_array(&textures); let cheetah_material = stage.append(&PbrMaterial { albedo_texture: textures.at(0), lighting_model: LightingModel::NO_LIGHTING, @@ -729,35 +648,49 @@ mod test { prim.material = Id::NONE; prim }; - let _unit = stage.draw_unit(&RenderUnit { - camera, - vertex_count: cheetah_primitive.vertex_count, - node_path: stage.append_array(&[stage.append(&gl::GltfNode { - mesh: stage.append(&gl::GltfMesh { - primitives: stage.append_array(&[color_primitive]), - ..Default::default() - }), + let _unit = { + let primitives = stage.append_array(&[color_primitive]); + let mesh = stage.append(&gl::GltfMesh { + primitives, ..Default::default() - })]), - ..Default::default() - }); - let _cheetah_unit = stage.draw_unit(&RenderUnit { - camera, - transform: stage.append(&Transform { + }); + let node = stage.append(&gl::GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + + stage.draw_unit(&RenderUnit { + camera, + vertex_count: cheetah_primitive.vertex_count, + node_path, + ..Default::default() + }) + }; + let _cheetah_unit = { + let primitives = stage.append_array(&[cheetah_primitive]); + let mesh = stage.append(&gl::GltfMesh { + primitives, + ..Default::default() + }); + let node = stage.append(&gl::GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let transform = stage.append(&Transform { translation: Vec3::new(15.0, 35.0, 0.5), scale: Vec3::new(0.5, 0.5, 1.0), ..Default::default() - }), - vertex_count: cheetah_primitive.vertex_count, - node_path: stage.append_array(&[stage.append(&gl::GltfNode { - mesh: stage.append(&gl::GltfMesh { - primitives: stage.append_array(&[cheetah_primitive]), - ..Default::default() - }), + }); + stage.draw_unit(&RenderUnit { + camera, + transform, + vertex_count: cheetah_primitive.vertex_count, + node_path, ..Default::default() - })]), - ..Default::default() - }); + }) + }; let img = r.render_image().unwrap(); @@ -769,7 +702,7 @@ mod test { fn scene_cube_directional() { let mut r = Renderling::headless(100, 100).with_background_color(Vec3::splat(0.0).extend(1.0)); - let stage = r.new_stage(); + let mut stage = r.new_stage(); stage.configure_graph(&mut r, true); let (projection, _) = camera::default_perspective(100.0, 100.0); @@ -819,13 +752,16 @@ mod test { .collect::>(); let vertex_count = verts.len() as u32; let mesh = stage.new_mesh().with_primitive(verts, [], material).build(); + let mesh = stage.append(&mesh); + let node = stage.append(&gl::GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); let _cube = stage.draw_unit(&RenderUnit { camera, vertex_count, - node_path: stage.append_array(&[stage.append(&gl::GltfNode { - mesh: stage.append(&mesh), - ..Default::default() - })]), + node_path, ..Default::default() }); let img = r.render_image().unwrap(); @@ -874,7 +810,7 @@ mod test { fn scene_parent_sanity() { let mut r = Renderling::headless(100, 100); r.set_background_color(Vec4::splat(0.0)); - let stage = r.new_stage(); + let mut stage = r.new_stage(); stage.configure_graph(&mut r, true); let (projection, view) = camera::default_ortho2d(100.0, 100.0); let camera = stage.append(&Camera::new(projection, view)); @@ -921,74 +857,83 @@ mod test { let red_node = stage.allocate::(); // Write the nodes now that we have references to them all - stage - .write( - root_node, - &gl::GltfNode { - children: stage.append_array(&[cyan_node]), - scale: Vec3::new(25.0, 25.0, 1.0), - ..Default::default() - }, - ) - .unwrap(); - stage - .write( - cyan_node, - &gl::GltfNode { - mesh: stage.append(&gl::GltfMesh { - primitives: stage.append_array(&[cyan_primitive]), - ..Default::default() - }), - children: stage.append_array(&[yellow_node]), - translation: Vec3::new(1.0, 1.0, 0.0), - ..Default::default() - }, - ) - .unwrap(); - stage - .write( - yellow_node, - &gl::GltfNode { - mesh: stage.append(&gl::GltfMesh { - primitives: stage.append_array(&[yellow_primitive]), - ..Default::default() - }), - children: stage.append_array(&[red_node]), - translation: Vec3::new(1.0, 1.0, 0.0), - ..Default::default() - }, - ) - .unwrap(); - stage - .write( - red_node, - &gl::GltfNode { - mesh: stage.append(&gl::GltfMesh { - primitives: stage.append_array(&[red_primitive]), - ..Default::default() - }), - translation: Vec3::new(1.0, 1.0, 0.0), - ..Default::default() - }, - ) - .unwrap(); + let children = stage.append_array(&[cyan_node]); + stage.write( + root_node, + &gl::GltfNode { + children, + scale: Vec3::new(25.0, 25.0, 1.0), + ..Default::default() + }, + ); + + let primitives = stage.append_array(&[cyan_primitive]); + let children = stage.append_array(&[yellow_node]); + let mesh = stage.append(&gl::GltfMesh { + primitives, + ..Default::default() + }); + stage.write( + cyan_node, + &gl::GltfNode { + mesh, + children, + translation: Vec3::new(1.0, 1.0, 0.0), + ..Default::default() + }, + ); + + let primitives = stage.append_array(&[yellow_primitive]); + let children = stage.append_array(&[red_node]); + let mesh = stage.append(&gl::GltfMesh { + primitives, + ..Default::default() + }); + stage.write( + yellow_node, + &gl::GltfNode { + mesh, + children, + translation: Vec3::new(1.0, 1.0, 0.0), + ..Default::default() + }, + ); + + let primitives = stage.append_array(&[red_primitive]); + let mesh = stage.append(&gl::GltfMesh { + primitives, + ..Default::default() + }); + stage.write( + red_node, + &gl::GltfNode { + mesh, + translation: Vec3::new(1.0, 1.0, 0.0), + ..Default::default() + }, + ); + let node_path = stage.append_array(&[root_node, cyan_node]); let _cyan_unit = stage.draw_unit(&RenderUnit { camera, vertex_count: 3, - node_path: stage.append_array(&[root_node, cyan_node]), + node_path, ..Default::default() }); + + let node_path = stage.append_array(&[root_node, cyan_node, yellow_node]); let _yellow_unit = stage.draw_unit(&RenderUnit { camera, vertex_count: 3, - node_path: stage.append_array(&[root_node, cyan_node, yellow_node]), + node_path, ..Default::default() }); + + let node_path = stage.append_array(&[root_node, cyan_node, yellow_node, red_node]); let _red_unit = stage.draw_unit(&RenderUnit { camera, vertex_count: 3, - node_path: stage.append_array(&[root_node, cyan_node, yellow_node, red_node]), + node_path, ..Default::default() }); @@ -1015,7 +960,7 @@ mod test { let ss = 600; let mut r = Renderling::headless(ss, ss).with_background_color(Vec3::splat(0.0).extend(1.0)); - let stage = r.new_stage(); + let mut stage = r.new_stage(); stage.configure_graph(&mut r, true); let radius = 0.5; @@ -1068,28 +1013,32 @@ mod test { roughness_factor: roughness, ..Default::default() }); + let primitives = stage.append_array(&[prim]); + let mesh = stage.append(&gl::GltfMesh { + primitives, + ..Default::default() + }); + let node = stage.append(&gl::GltfNode { + mesh, + translation: Vec3::new(x, y, 0.0), + ..Default::default() + }); + let node_path = stage.append_array(&[node]); let _entity = stage.draw_unit(&RenderUnit { camera, vertex_count: prim.vertex_count, - node_path: stage.append_array(&[stage.append(&gl::GltfNode { - mesh: stage.append(&gl::GltfMesh { - primitives: stage.append_array(&[prim]), - ..Default::default() - }), - translation: Vec3::new(x, y, 0.0), - ..Default::default() - })]), + node_path, ..Default::default() }); } } let (device, queue) = r.get_device_and_queue_owned(); - let hdr_image = SceneImage::from_hdr_path("../../img/hdr/resting_place.hdr").unwrap(); - let skybox = crate::skybox::Skybox::new(&device, &queue, hdr_image, camera); + let hdr_image = AtlasImage::from_hdr_path("../../img/hdr/resting_place.hdr").unwrap(); + let skybox = crate::skybox::Skybox::new(device, queue, hdr_image, camera); stage.set_skybox(skybox); - let img = r.render_image().unwrap(); + let img = r.render_linear_image().unwrap(); img_diff::assert_img_eq("pbr/metallic_roughness_spheres.png", img); } diff --git a/crates/renderling/src/linkage/convolution-fragment_bloom.spv b/crates/renderling/src/linkage/convolution-fragment_bloom.spv index 2efb0137..a9709123 100644 Binary files a/crates/renderling/src/linkage/convolution-fragment_bloom.spv and b/crates/renderling/src/linkage/convolution-fragment_bloom.spv differ diff --git a/crates/renderling/src/linkage/convolution-fragment_brdf_lut_convolution.spv b/crates/renderling/src/linkage/convolution-fragment_brdf_lut_convolution.spv index c085089e..a9c0d3ff 100644 Binary files a/crates/renderling/src/linkage/convolution-fragment_brdf_lut_convolution.spv and b/crates/renderling/src/linkage/convolution-fragment_brdf_lut_convolution.spv differ diff --git a/crates/renderling/src/linkage/convolution-fragment_prefilter_environment_cubemap.spv b/crates/renderling/src/linkage/convolution-fragment_prefilter_environment_cubemap.spv index 37fc0096..16dadac5 100644 Binary files a/crates/renderling/src/linkage/convolution-fragment_prefilter_environment_cubemap.spv and b/crates/renderling/src/linkage/convolution-fragment_prefilter_environment_cubemap.spv differ diff --git a/crates/renderling/src/linkage/convolution-vertex_brdf_lut_convolution.spv b/crates/renderling/src/linkage/convolution-vertex_brdf_lut_convolution.spv index 6b37843c..062f70b4 100644 Binary files a/crates/renderling/src/linkage/convolution-vertex_brdf_lut_convolution.spv and b/crates/renderling/src/linkage/convolution-vertex_brdf_lut_convolution.spv differ diff --git a/crates/renderling/src/linkage/convolution-vertex_generate_mipmap.spv b/crates/renderling/src/linkage/convolution-vertex_generate_mipmap.spv index 70f72ada..0f6af9d7 100644 Binary files a/crates/renderling/src/linkage/convolution-vertex_generate_mipmap.spv and b/crates/renderling/src/linkage/convolution-vertex_generate_mipmap.spv differ diff --git a/crates/renderling/src/linkage/convolution-vertex_prefilter_environment_cubemap.spv b/crates/renderling/src/linkage/convolution-vertex_prefilter_environment_cubemap.spv index eb663fbf..2c24629b 100644 Binary files a/crates/renderling/src/linkage/convolution-vertex_prefilter_environment_cubemap.spv and b/crates/renderling/src/linkage/convolution-vertex_prefilter_environment_cubemap.spv differ diff --git a/crates/renderling/src/linkage/skybox-fragment_cubemap.spv b/crates/renderling/src/linkage/skybox-fragment_cubemap.spv index c6945d99..b985be73 100644 Binary files a/crates/renderling/src/linkage/skybox-fragment_cubemap.spv and b/crates/renderling/src/linkage/skybox-fragment_cubemap.spv differ diff --git a/crates/renderling/src/linkage/skybox-fragment_equirectangular.spv b/crates/renderling/src/linkage/skybox-fragment_equirectangular.spv index 3f80ad1e..19ffa5e7 100644 Binary files a/crates/renderling/src/linkage/skybox-fragment_equirectangular.spv and b/crates/renderling/src/linkage/skybox-fragment_equirectangular.spv differ diff --git a/crates/renderling/src/linkage/skybox-slabbed_vertex.spv b/crates/renderling/src/linkage/skybox-slabbed_vertex.spv deleted file mode 100644 index 47ea414c..00000000 Binary files a/crates/renderling/src/linkage/skybox-slabbed_vertex.spv and /dev/null differ diff --git a/crates/renderling/src/linkage/skybox-stage_skybox_cubemap.spv b/crates/renderling/src/linkage/skybox-stage_skybox_cubemap.spv deleted file mode 100644 index c025bede..00000000 Binary files a/crates/renderling/src/linkage/skybox-stage_skybox_cubemap.spv and /dev/null differ diff --git a/crates/renderling/src/linkage/skybox-vertex.spv b/crates/renderling/src/linkage/skybox-vertex.spv index 779c8041..1283523f 100644 Binary files a/crates/renderling/src/linkage/skybox-vertex.spv and b/crates/renderling/src/linkage/skybox-vertex.spv differ diff --git a/crates/renderling/src/linkage/skybox-vertex_cubemap.spv b/crates/renderling/src/linkage/skybox-vertex_cubemap.spv new file mode 100644 index 00000000..f7054025 Binary files /dev/null and b/crates/renderling/src/linkage/skybox-vertex_cubemap.spv differ diff --git a/crates/renderling/src/linkage/skybox-vertex_position_passthru.spv b/crates/renderling/src/linkage/skybox-vertex_position_passthru.spv deleted file mode 100644 index 94e7cdd3..00000000 Binary files a/crates/renderling/src/linkage/skybox-vertex_position_passthru.spv and /dev/null differ diff --git a/crates/renderling/src/linkage/stage-compute_cull_entities.spv b/crates/renderling/src/linkage/stage-compute_cull_entities.spv index 70a1934e..902bca54 100644 Binary files a/crates/renderling/src/linkage/stage-compute_cull_entities.spv and b/crates/renderling/src/linkage/stage-compute_cull_entities.spv differ diff --git a/crates/renderling/src/linkage/stage-gltf_fragment.spv b/crates/renderling/src/linkage/stage-gltf_fragment.spv index c8200e30..84ab130f 100644 Binary files a/crates/renderling/src/linkage/stage-gltf_fragment.spv and b/crates/renderling/src/linkage/stage-gltf_fragment.spv differ diff --git a/crates/renderling/src/linkage/stage-gltf_vertex.spv b/crates/renderling/src/linkage/stage-gltf_vertex.spv index 3a58d9d4..3ddefd49 100644 Binary files a/crates/renderling/src/linkage/stage-gltf_vertex.spv and b/crates/renderling/src/linkage/stage-gltf_vertex.spv differ diff --git a/crates/renderling/src/linkage/stage-main_fragment_scene.spv b/crates/renderling/src/linkage/stage-main_fragment_scene.spv index cbde1de8..0a63c106 100644 Binary files a/crates/renderling/src/linkage/stage-main_fragment_scene.spv and b/crates/renderling/src/linkage/stage-main_fragment_scene.spv differ diff --git a/crates/renderling/src/linkage/stage-main_vertex_scene.spv b/crates/renderling/src/linkage/stage-main_vertex_scene.spv index b65d5bdf..df2b7b0b 100644 Binary files a/crates/renderling/src/linkage/stage-main_vertex_scene.spv and b/crates/renderling/src/linkage/stage-main_vertex_scene.spv differ diff --git a/crates/renderling/src/linkage/stage-test_i8_i16_extraction.spv b/crates/renderling/src/linkage/stage-test_i8_i16_extraction.spv index d685467f..1fad7572 100644 Binary files a/crates/renderling/src/linkage/stage-test_i8_i16_extraction.spv and b/crates/renderling/src/linkage/stage-test_i8_i16_extraction.spv differ diff --git a/crates/renderling/src/linkage/tonemapping-fragment.spv b/crates/renderling/src/linkage/tonemapping-fragment.spv index f7b72dec..4a8c61aa 100644 Binary files a/crates/renderling/src/linkage/tonemapping-fragment.spv and b/crates/renderling/src/linkage/tonemapping-fragment.spv differ diff --git a/crates/renderling/src/linkage/tonemapping-vertex.spv b/crates/renderling/src/linkage/tonemapping-vertex.spv index 9b866b93..35d63094 100644 Binary files a/crates/renderling/src/linkage/tonemapping-vertex.spv and b/crates/renderling/src/linkage/tonemapping-vertex.spv differ diff --git a/crates/renderling/src/linkage/tutorial-implicit_isosceles_vertex.spv b/crates/renderling/src/linkage/tutorial-implicit_isosceles_vertex.spv index d9155e82..1b706e6c 100644 Binary files a/crates/renderling/src/linkage/tutorial-implicit_isosceles_vertex.spv and b/crates/renderling/src/linkage/tutorial-implicit_isosceles_vertex.spv differ diff --git a/crates/renderling/src/linkage/tutorial-slabbed_render_unit.spv b/crates/renderling/src/linkage/tutorial-slabbed_render_unit.spv index f1518e41..8ce4c700 100644 Binary files a/crates/renderling/src/linkage/tutorial-slabbed_render_unit.spv and b/crates/renderling/src/linkage/tutorial-slabbed_render_unit.spv differ diff --git a/crates/renderling/src/linkage/tutorial-slabbed_vertices.spv b/crates/renderling/src/linkage/tutorial-slabbed_vertices.spv index 0cf72c79..9001018d 100644 Binary files a/crates/renderling/src/linkage/tutorial-slabbed_vertices.spv and b/crates/renderling/src/linkage/tutorial-slabbed_vertices.spv differ diff --git a/crates/renderling/src/linkage/tutorial-slabbed_vertices_no_instance.spv b/crates/renderling/src/linkage/tutorial-slabbed_vertices_no_instance.spv index b1991b0f..f6ab41b9 100644 Binary files a/crates/renderling/src/linkage/tutorial-slabbed_vertices_no_instance.spv and b/crates/renderling/src/linkage/tutorial-slabbed_vertices_no_instance.spv differ diff --git a/crates/renderling/src/linkage/ui-fragment.spv b/crates/renderling/src/linkage/ui-fragment.spv index 8a70212e..1d1880bd 100644 Binary files a/crates/renderling/src/linkage/ui-fragment.spv and b/crates/renderling/src/linkage/ui-fragment.spv differ diff --git a/crates/renderling/src/linkage/ui-vertex.spv b/crates/renderling/src/linkage/ui-vertex.spv index c191daa7..e6374f92 100644 Binary files a/crates/renderling/src/linkage/ui-vertex.spv and b/crates/renderling/src/linkage/ui-vertex.spv differ diff --git a/crates/renderling/src/renderer.rs b/crates/renderling/src/renderer.rs index 7ba5b142..07ef6887 100644 --- a/crates/renderling/src/renderer.rs +++ b/crates/renderling/src/renderer.rs @@ -5,8 +5,8 @@ use snafu::prelude::*; use std::{ops::Deref, sync::Arc}; use crate::{ - hdr::HdrSurface, CreateSurfaceFn, Graph, RenderTarget, Scene, SceneBuilder, Stage, - TextureError, UiDrawObject, UiScene, UiSceneBuilder, View, ViewMut, WgpuStateError, + CreateSurfaceFn, Graph, RenderTarget, Stage, TextureError, + /* UiScene, UiSceneBuilder, */ View, ViewMut, WgpuStateError, }; #[derive(Debug, Snafu)] @@ -38,19 +38,12 @@ pub enum RenderlingError { #[snafu(display("gltf import failed: {}", source))] GltfImport { source: gltf::Error }, - //#[snafu(display("could not create scene: {}", source))] - // Scene { source: crate::GltfError }, #[snafu(display("missing resource"))] Resource { key: TypeKey }, #[snafu(display("{source}"))] Graph { source: moongraph::GraphError }, - //#[snafu(display("{source}"))] - // Lights { source: crate::light::LightsError }, - - //#[snafu(display("{source}"))] - // Object { source: crate::object::ObjectError }, #[snafu(display( "Missing PostRenderBuffer resource. Ensure a node that creates PostRenderBuffer (like \ PostRenderbufferCreate) is present in the graph: {source}" @@ -82,6 +75,12 @@ impl Deref for Device { } } +impl Into> for &Device { + fn into(self) -> Arc { + self.0.clone() + } +} + /// A thread-safe, clonable wrapper around `wgpu::Queue`. #[derive(Clone)] pub struct Queue(pub Arc); @@ -94,6 +93,12 @@ impl Deref for Queue { } } +impl Into> for &Queue { + fn into(self) -> Arc { + self.0.clone() + } +} + /// A thread-safe, clonable wrapper around `wgpu::Adapter`. #[derive(Clone)] pub struct Adapter(pub Arc); @@ -127,30 +132,6 @@ impl Deref for DepthTexture { /// The global background color. pub struct BackgroundColor(pub Vec4); -/// A helper struct for configuring calls to `Renderling::setup_render_graph`. -pub struct RenderGraphConfig { - pub scene: Option, - pub ui: Option, - pub objs: Vec, - // Whether or not to use the screen capture node. - // You probably want this to be `true` if you are rendering headless. - pub with_screen_capture: bool, - // Whether or not to use bloom filter in post-processing. - pub with_bloom: bool, -} - -impl Default for RenderGraphConfig { - fn default() -> Self { - Self { - scene: Default::default(), - ui: Default::default(), - objs: Default::default(), - with_screen_capture: false, - with_bloom: true, - } - } -} - /// A graph-based renderer that manages GPU resources for cameras, materials and /// meshes. pub struct Renderling { @@ -260,8 +241,8 @@ impl Renderling { /// Create a new headless renderer. /// /// ## Panics - /// This function will panic if an adapter cannot be found. For example this would - /// happen on machines without a GPU. + /// This function will panic if an adapter cannot be found. For example this + /// would happen on machines without a GPU. pub fn headless(width: u32, height: u32) -> Self { futures_lite::future::block_on(Self::try_new_headless(width, height)).unwrap() } @@ -300,13 +281,7 @@ impl Renderling { .unwrap(); // The renderer doesn't _always_ have an HrdSurface, so we don't unwrap this // one. - let _ = self.graph.visit( - |(device, queue, mut hdr): (View, View, ViewMut)| { - hdr.hdr_texture = HdrSurface::create_texture(&device, &queue, width, height); - hdr.texture_bindgroup = - HdrSurface::create_texture_bindgroup(&device, &hdr.hdr_texture); - }, - ); + let _ = self.graph.visit(crate::hdr::resize_hdr_surface); } pub fn create_texture

( @@ -405,45 +380,54 @@ impl Renderling { Stage::new(device, queue) } - pub fn new_scene(&self) -> SceneBuilder { - let (device, queue) = self.get_device_and_queue_owned(); - SceneBuilder::new(device.0, queue.0) - } - - pub fn empty_scene(&self) -> Scene { - self.new_scene().build().unwrap() - } - - pub fn new_ui_scene(&self) -> UiSceneBuilder<'_> { - let (device, _) = self.get_device_and_queue_owned(); - let queue = self.get_queue(); - UiSceneBuilder::new(device.0.clone(), queue) - } + //pub fn new_ui_scene(&self) -> UiSceneBuilder<'_> { + // let (device, _) = self.get_device_and_queue_owned(); + // let queue = self.get_queue(); + // UiSceneBuilder::new(device.0.clone(), queue) + //} - pub fn empty_ui_scene(&self) -> UiScene { - self.new_ui_scene().build() - } + //pub fn empty_ui_scene(&self) -> UiScene { + // self.new_ui_scene().build() + //} - #[cfg(feature = "text")] - /// Create a new `GlyphCache` used to cache text rendering info. - pub fn new_glyph_cache(&self, fonts: Vec) -> crate::GlyphCache { - crate::GlyphCache::new(fonts) - } + //#[cfg(feature = "text")] + ///// Create a new `GlyphCache` used to cache text rendering info. + //pub fn new_glyph_cache(&self, fonts: Vec) -> + // crate::GlyphCache { crate::GlyphCache::new(fonts) + //} - /// Sets up the render graph with the given scenes and objects. + /// Render into an image. /// - /// The scenes and objects may be "visited" later, or even retrieved. - pub fn setup_render_graph(&mut self, config: RenderGraphConfig) { - let RenderGraphConfig { - scene, - ui, - objs, - with_screen_capture, - with_bloom, - } = config; - let scene = scene.unwrap_or_else(|| self.empty_scene()); - let ui = ui.unwrap_or_else(|| self.empty_ui_scene()); - crate::setup_render_graph(self, scene, ui, objs, with_screen_capture, with_bloom) + /// This should be called after rendering, before presentation. + /// Good for getting headless screen grabs. + /// + /// For this call to succeed, the `PostRenderBufferCreate::create` node must + /// be present in the graph. + /// + /// The resulting image will be in the color space of the internal + /// [`RenderTarget`]. + /// + /// ## Note + /// This operation can take a long time, depending on how big the screen is. + pub fn render_image(&mut self) -> Result { + use crate::frame::PostRenderBuffer; + + self.render()?; + let buffer = self + .graph + .remove_resource::() + .context(MissingPostRenderBufferSnafu)? + .context(ResourceSnafu { + key: TypeKey::new::(), + })?; + let device = self.get_device(); + let is_srgb = self.get_render_target().format().is_srgb(); + let img = if is_srgb { + buffer.0.into_srgba(device).context(TextureSnafu)? + } else { + buffer.0.into_linear_rgba(device).context(TextureSnafu)? + }; + Ok(img) } /// Render into an image. @@ -454,9 +438,11 @@ impl Renderling { /// For this call to succeed, the `PostRenderBufferCreate::create` node must /// be present in the graph. /// + /// The resulting image will be in a linear color space. + /// /// ## Note /// This operation can take a long time, depending on how big the screen is. - pub fn render_image(&mut self) -> Result { + pub fn render_linear_image(&mut self) -> Result { use crate::frame::PostRenderBuffer; self.render()?; @@ -468,7 +454,7 @@ impl Renderling { key: TypeKey::new::(), })?; let device = self.get_device(); - let img = buffer.0.into_rgba(device).context(TextureSnafu)?; + let img = buffer.0.into_linear_rgba(device).context(TextureSnafu)?; Ok(img) } diff --git a/crates/renderling/src/scene.rs b/crates/renderling/src/scene.rs index 5313ed7f..7ad43448 100644 --- a/crates/renderling/src/scene.rs +++ b/crates/renderling/src/scene.rs @@ -6,10 +6,11 @@ use moongraph::{Move, View, ViewMut}; use renderling_shader::debug::DebugChannel; use snafu::prelude::*; -pub use renderling_shader::stage::{ - DrawIndirect, GpuConstants, GpuEntity, GpuLight, GpuToggles, Vertex, +pub use renderling_shader::{ + pbr::PbrMaterial, + stage::{DrawIndirect, GpuConstants, GpuEntity, GpuLight, GpuToggles, Vertex}, + texture::*, }; -pub use renderling_shader::{pbr::PbrMaterial, texture::*}; use crate::{ bloom::BloomResult, frame::FrameTextureView, hdr::HdrSurface, Atlas, BufferArray, DepthTexture, @@ -1115,44 +1116,3 @@ pub fn scene_render( queue.submit(std::iter::once(encoder.finish())); Ok(()) } - -/// Conducts the HDR tone mapping, writing the HDR surface texture to the (most -/// likely) sRGB window surface. -pub fn tonemapping( - (device, queue, frame, hdr_frame, bloom_result): ( - View, - View, - View, - View, - Move, - ), -) -> Result<(), SceneError> { - log::trace!("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, - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &frame, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, - })], - depth_stencil_attachment: None, - }); - render_pass.set_pipeline(&hdr_frame.tonemapping_pipeline); - render_pass.set_bind_group(0, &hdr_frame.texture_bindgroup, &[]); - render_pass.set_bind_group(1, hdr_frame.constants.bindgroup(), &[]); - let bloom_bg = bloom_result.0.as_deref().unwrap_or_else(|| { - log::trace!(" no bloom bindgroup"); - &hdr_frame.no_bloom_bindgroup - }); - render_pass.set_bind_group(2, bloom_bg, &[]); - render_pass.draw(0..6, 0..1); - drop(render_pass); - - queue.submit(std::iter::once(encoder.finish())); - Ok(()) -} diff --git a/crates/renderling/src/scene/gltf_support.rs b/crates/renderling/src/scene/gltf_support.rs deleted file mode 100644 index 32239742..00000000 --- a/crates/renderling/src/scene/gltf_support.rs +++ /dev/null @@ -1,1416 +0,0 @@ -//! GLTF support for renderling GPU scenes. -use glam::{Mat4, Quat, Vec2, Vec3, Vec4, Vec4Swizzles}; -use gltf::khr_lights_punctual::Kind; -use rustc_hash::FxHashMap; -use snafu::prelude::*; - -use super::*; -use crate::{ - shader::texture::{GpuTexture, TextureAddressMode}, - Id, -}; - -mod anime; -pub use anime::*; - -#[derive(Debug, Snafu)] -pub enum GltfLoaderError { - #[snafu(display("{source}"))] - Gltf { source: gltf::Error, cwd: String }, - - #[snafu(display("Unsupported gltf image format: {:?}", format))] - UnsupportedImageFormat { format: gltf::image::Format }, - - #[snafu(display("Missing image {}", index))] - MissingImage { index: usize }, - - #[snafu(display("Missing image for texture {tex_id:?}"))] - MissingTextureImage { tex_id: Id }, - - #[snafu(display("Missing node {}", index))] - MissingNode { index: usize }, - - #[snafu(display("Invalid image"))] - InvalidImage, - - #[snafu(display("Error during scene building phase: {source}"))] - Scene { source: crate::SceneError }, - - #[snafu(display("{what} is missing texture={index}"))] - MissingTexture { what: &'static str, index: usize }, - - #[snafu(display("Unsupported primitive mode: {:?}", mode))] - PrimitiveMode { mode: gltf::mesh::Mode }, - - #[snafu(display("No {} attribute for mesh", attribute.to_string()))] - MissingAttribute { attribute: gltf::Semantic }, - - #[snafu(display("{what} is missing material {:?} {:?}", index, name))] - MissingMaterial { - what: &'static str, - index: Option, - name: Option, - }, - - #[snafu(display("Missing mesh {:?} {:?}", index, name))] - MissingMesh { - index: Option, - name: Option, - }, - - #[snafu(display("Missing entity {id:?}"))] - MissingEntity { id: Id }, - - #[snafu(display("Missing animation channel inputs"))] - MissingInputs, - - #[snafu(display("Missing animation channel outputs"))] - MissingOutputs, -} - -pub struct GltfStore { - dense: Vec>, - names: FxHashMap>, -} - -impl Default for GltfStore { - fn default() -> Self { - Self { - dense: Default::default(), - names: Default::default(), - } - } -} - -impl GltfStore { - pub fn iter(&self) -> impl Iterator { - self.dense.iter().flatten() - } - - pub fn iter_mut(&mut self) -> impl Iterator { - self.dense.iter_mut().flatten() - } - - pub fn remove(&mut self, index: usize, name: Option) -> Option { - if let Some(name) = name { - if let Some(indices) = self.names.get_mut(&name) { - indices.retain(|i| *i != index); - } - } - self.dense.get_mut(index)?.take() - } - - pub fn insert(&mut self, index: usize, name: Option, item: T) -> Option { - if self.dense.len() <= index { - self.dense.resize_with(index + 1, || None); - } - while self.dense.len() <= index { - self.dense.push(None); - } - let existing = self.remove(index, name.clone()); - self.dense[index] = Some(item); - if let Some(name) = name { - let indices = self.names.entry(name).or_default(); - indices.push(index); - } - existing - } - - pub fn get_name(&self, index: usize) -> Option<&String> { - self.names.iter().find_map(|(name, indices)| { - if indices.contains(&index) { - Some(name) - } else { - None - } - }) - } - - pub fn get(&self, index: usize) -> Option<&T> { - self.dense.get(index)?.as_ref() - } - - pub fn get_mut(&mut self, index: usize) -> Option<&mut T> { - self.dense.get_mut(index)?.as_mut() - } - - pub fn get_by_name(&self, name: &str) -> impl Iterator + '_ { - if let Some(indices) = self.names.get(name) { - Box::new(indices.iter().flat_map(|index| self.get(*index))) - as Box> - } else { - Box::new(std::iter::empty()) as Box> - } - } - - pub fn len(&self) -> usize { - self.dense.len() - } -} - -#[derive(Clone, Copy)] -pub struct GltfBoundingBox { - pub min: Vec3, - pub max: Vec3, -} - -impl From> for GltfBoundingBox { - fn from(gltf::mesh::Bounds { min, max }: gltf::mesh::Bounds<[f32; 3]>) -> Self { - GltfBoundingBox { - min: min.into(), - max: max.into(), - } - } -} - -#[derive(Clone, Default)] -pub struct GltfNode { - pub entity_id: Id, - // Contains an index into the GltfLoader.cameras, lights or meshes fields. - pub gltf_camera_index: Option, - pub gltf_light_index: Option, - pub gltf_mesh_index: Option, - pub gltf_skin_index: Option, - pub child_ids: Vec>, -} - -#[derive(Clone)] -pub struct GltfMeshPrim { - pub vertex_start: u32, - pub vertex_count: u32, - pub material_id: Id, - pub bounding_box: GltfBoundingBox, - pub num_morph_targets: u32, - pub morph_targets_have_positions: bool, - pub morph_targets_have_normals: bool, - pub morph_targets_have_tangents: bool, - pub weights: Vec, -} - -/// The result of loading a gltf file into a [`SceneBuilder`]. -/// -/// Contains indexed and named lookups for resources contained within the loaded -/// gltf file. -/// -/// To load a gltf file into a scene thereby creating a `GltfLoader` you can use -/// [`SceneBuilder::gltf_load`]. -#[derive(Default)] -pub struct GltfLoader { - // Contains the indices of SceneBuilder images loaded - pub images: Vec, - pub cameras: GltfStore<(Mat4, Mat4)>, - pub lights: GltfStore>, - pub textures: GltfStore>, - pub default_material: Id, - pub materials: GltfStore>, - pub meshes: GltfStore>, - pub nodes: GltfStore, - pub animations: GltfStore, -} - -impl GltfLoader { - /// Load everything into a scene builder and return the loader. - pub fn load( - builder: &mut SceneBuilder, - document: gltf::Document, - buffers: Vec, - images: Vec, - ) -> Result { - let mut loader = GltfLoader::default(); - - log::trace!("node hierarchy:"); - for node in document.nodes() { - // This associates the node with a GpuEntity and transform, which - // we need in order to load meshes, because mesh vertices may reference - // GpuEntity ids in the 'joints' field. - let _ = loader.load_shallow_node(node, builder)?; - } - for node in document.nodes() { - let index = node.index(); - let name = node.name().map(String::from); - // UNWRAP: safe because we already created and stored all the nodes - let parent_id = loader.nodes.get(index).unwrap().entity_id; - log::trace!("node {index} {name:?}"); - let mut printed = false; - for child in node.children() { - let child_index = child.index(); - let child_name = child.name().map(String::from); - if index == child_index { - continue; - } - if !printed { - printed = true; - log::trace!("contains children:"); - } - log::trace!(" node {child_index} {child_name:?}"); - // UNWRAP: safe because we already created and stored all the nodes - let child_id = loader.nodes.get(child_index).unwrap().entity_id; - let child_entity = builder.entities.get_mut(child_id.index()).unwrap(); - child_entity.parent = parent_id; - } - } - - if !builder.materials.is_empty() { - loader.default_material = Id::new(0); - } - - for (i, image) in images.into_iter().enumerate() { - // let format = image_data_format_to_wgpu(image.format)?; - // let num_channels = image_data_format_num_channels(image.format); - log::trace!("adding image {} with format {:?}", i, image.format); - let scene_img = SceneImage::from(image); - let image_index = builder.add_image(scene_img); - loader.images.push(image_index); - log::trace!(" with index={image_index} in the scene builder"); - } - - loader.load_textures(builder, &document)?; - loader.load_materials(builder, &document)?; - - log::debug!("adding meshlets"); - for mesh in document.meshes() { - loader.load_mesh(mesh, builder, &document, &buffers)?; - } - - for node in document.nodes() { - // We don't call GltfLoader::load_node here because that function will - // also load any children of this node, which will lead to doubles when - // we encounter those children in this loop. - let _ = loader.load_node(node, builder, &buffers)?; - } - - log::debug!("adding animations"); - loader.load_animations(&document, &buffers)?; - - Ok(loader) - } - - fn load_textures( - &mut self, - builder: &mut SceneBuilder, - document: &gltf::Document, - ) -> Result<(), GltfLoaderError> { - log::debug!("loading textures"); - for texture in document.textures() { - self.load_texture(texture, builder)?; - } - Ok(()) - } - - fn load_texture( - &mut self, - texture: gltf::Texture<'_>, - builder: &mut SceneBuilder, - ) -> Result, GltfLoaderError> { - let index = texture.index(); - let name = texture.name().map(String::from); - let image_loader_index = texture.source().index(); - let image_index = - self.images - .get(image_loader_index) - .copied() - .context(MissingImageSnafu { - index: image_loader_index, - })?; - fn mode(mode: gltf::texture::WrappingMode) -> TextureAddressMode { - match mode { - gltf::texture::WrappingMode::ClampToEdge => TextureAddressMode::CLAMP_TO_EDGE, - gltf::texture::WrappingMode::MirroredRepeat => TextureAddressMode::MIRRORED_REPEAT, - gltf::texture::WrappingMode::Repeat => TextureAddressMode::REPEAT, - } - } - let mode_s = mode(texture.sampler().wrap_s()); - let mode_t = mode(texture.sampler().wrap_t()); - let params = TextureParams { - image_index, - mode_s, - mode_t, - }; - - let texture_id = builder.add_texture(params); - log::trace!( - "adding texture index:{index} name:{name:?} id:{texture_id:?} with wrapping \ - s:{mode_s} t:{mode_t}" - ); - let _ = self.textures.insert(index, name, texture_id); - Ok(texture_id) - } - - /// Return the scene `Id` for the gltf texture at the given - /// index, if possible. - /// - /// If the texture at the given index has not been loaded into the - /// [`SceneBuilder`], it will be. - pub fn texture_at( - &mut self, - index: usize, - builder: &mut SceneBuilder, - document: &gltf::Document, - ) -> Result, GltfLoaderError> { - if let Some(id) = self.textures.get(index) { - Ok(*id) - } else { - let texture = - document - .textures() - .find(|t| t.index() == index) - .context(MissingTextureSnafu { - what: "document", - index, - })?; - self.load_texture(texture, builder) - } - } - - fn load_materials( - &mut self, - builder: &mut SceneBuilder, - document: &gltf::Document, - ) -> Result<(), GltfLoaderError> { - log::debug!("loading materials"); - let mut total = 0; - for material in document.materials() { - self.load_material(material, builder, document)?; - total += 1; - } - log::debug!(" {total} materials"); - Ok(()) - } - - fn load_material( - &mut self, - material: gltf::Material<'_>, - builder: &mut SceneBuilder, - document: &gltf::Document, - ) -> Result, GltfLoaderError> { - let index = material.index(); - let name = material.name().map(String::from); - log::trace!("loading material {index:?} {name:?}"); - let pbr = material.pbr_metallic_roughness(); - let material = if material.unlit() { - log::trace!(" is unlit"); - let (albedo_texture, albedo_tex_coord) = if let Some(info) = pbr.base_color_texture() { - let index = info.texture().index(); - let tex_id = self.texture_at(index, builder, document)?; - (tex_id, info.tex_coord()) - } else { - (Id::NONE, 0) - }; - builder - .get_image_for_texture_id_mut(&albedo_texture) - .context(MissingTextureImageSnafu { - tex_id: albedo_texture, - })? - .apply_linear_transfer = true; - PbrMaterial { - albedo_texture, - albedo_tex_coord, - albedo_factor: pbr.base_color_factor().into(), - ..Default::default() - } - } else { - log::trace!(" is pbr"); - let albedo_factor: Vec4 = pbr.base_color_factor().into(); - let (albedo_texture, albedo_tex_coord) = if let Some(info) = pbr.base_color_texture() { - let index = info.texture().index(); - let tex_id = self.texture_at(index, builder, document)?; - builder - .get_image_for_texture_id_mut(&tex_id) - .context(MissingTextureImageSnafu { tex_id })? - .apply_linear_transfer = true; - (tex_id, info.tex_coord()) - } else { - (Id::NONE, 0) - }; - - let ( - metallic_factor, - roughness_factor, - metallic_roughness_texture, - metallic_roughness_tex_coord, - ) = if let Some(info) = pbr.metallic_roughness_texture() { - let index = info.texture().index(); - let tex_id = self.texture_at(index, builder, document)?; - (1.0, 1.0, tex_id, info.tex_coord()) - } else { - (pbr.metallic_factor(), pbr.roughness_factor(), Id::NONE, 0) - }; - - let (normal_texture, normal_tex_coord) = - if let Some(norm_tex) = material.normal_texture() { - let tex_id = self.texture_at(norm_tex.texture().index(), builder, document)?; - (tex_id, norm_tex.tex_coord()) - } else { - (Id::NONE, 0) - }; - - let (ao_strength, ao_texture, ao_tex_coord) = if let Some(occlusion_tex) = - material.occlusion_texture() - { - let tex_id = self.texture_at(occlusion_tex.texture().index(), builder, document)?; - (occlusion_tex.strength(), tex_id, occlusion_tex.tex_coord()) - } else { - (0.0, Id::NONE, 0) - }; - - let (emissive_texture, emissive_tex_coord) = if let Some(emissive_tex) = - material.emissive_texture() - { - let tex_id = self.texture_at(emissive_tex.texture().index(), builder, document)?; - builder - .get_image_for_texture_id_mut(&tex_id) - .context(MissingTextureImageSnafu { tex_id })? - .apply_linear_transfer = true; - (tex_id, emissive_tex.tex_coord()) - } else { - (Id::NONE, 0) - }; - let emissive_factor = Vec3::from(material.emissive_factor()) - .extend(material.emissive_strength().unwrap_or(1.0)); - - PbrMaterial { - albedo_factor, - metallic_factor, - roughness_factor, - albedo_texture, - metallic_roughness_texture, - normal_texture, - ao_texture, - albedo_tex_coord, - metallic_roughness_tex_coord, - normal_tex_coord, - ao_tex_coord, - ao_strength, - emissive_factor, - emissive_texture, - emissive_tex_coord, - lighting_model: crate::shader::stage::LightingModel::PBR_LIGHTING, - ..Default::default() - } - }; - - let material_id = builder.add_material(material); - - // If this material doesn't have an index it's because it's the default material - // for this gltf file. - if let Some(index) = index { - let _ = self.materials.insert(index, name, material_id); - } else { - self.default_material = material_id; - } - Ok(material_id) - } - - /// Return the scene `Id` for the gltf material at the given - /// index, if possible. - /// - /// If the material at the given index has not been loaded into the - /// [`SceneBuilder`], it will be. - /// - /// Providing `None` returns the id of the default material, or `Id::NONE` - /// if there is none. - pub fn material_at( - &mut self, - may_index: Option, - builder: &mut SceneBuilder, - document: &gltf::Document, - ) -> Result, GltfLoaderError> { - if let Some(index) = may_index { - if let Some(material_id) = self.materials.get(index) { - Ok(*material_id) - } else { - let material = document - .materials() - .find(|material| material.index() == Some(index)) - .context(MissingMaterialSnafu { - what: "document", - name: None, - index: Some(index), - })?; - self.load_material(material, builder, document) - } - } else { - Ok(self.default_material) - } - } - - fn load_mesh( - &mut self, - mesh: gltf::Mesh<'_>, - builder: &mut SceneBuilder, - document: &gltf::Document, - buffers: &[gltf::buffer::Data], - ) -> Result<(), GltfLoaderError> { - let mesh_index = mesh.index(); - let mesh_name = mesh.name().map(String::from); - log::trace!("loading mesh {mesh_index} {mesh_name:?}"); - - let mut mesh_primitives = vec![]; - for primitive in mesh.primitives() { - log::trace!(" reading primitive {}", primitive.index()); - log::trace!(" bounds: {:?}", primitive.bounding_box()); - snafu::ensure!( - primitive.mode() == gltf::mesh::Mode::Triangles, - PrimitiveModeSnafu { - mode: primitive.mode() - } - ); - let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()])); - let positions = reader - .read_positions() - .context(MissingAttributeSnafu { - attribute: gltf::Semantic::Positions, - })? - .map(Vec3::from) - .collect::>(); - log::trace!(" {} vertices", positions.len()); - if positions.len() <= 10 { - log::trace!(" positions:"); - for (i, p) in positions.iter().enumerate() { - log::trace!(" {i}: {p:?}"); - } - } - let mut gen_normals = false; - let normals: Box> = - if let Some(normals) = reader.read_normals() { - log::trace!(" with normals"); - Box::new(normals.map(Vec3::from)) - } else { - log::trace!(" no normals (will generate)"); - gen_normals = true; - Box::new(std::iter::repeat(Vec3::ZERO)) - }; - let mut gen_tangents = false; - let tangents: Box> = - if let Some(tangents) = reader.read_tangents() { - log::trace!(" with tangents"); - Box::new(tangents.map(Vec4::from)) - } else { - log::trace!(" no tangents (will generate)"); - gen_tangents = true; - Box::new(std::iter::repeat(Vec4::ZERO)) - }; - let colors: Box> = if let Some(colors) = reader.read_colors(0) - { - log::trace!(" colored"); - let colors = colors.into_rgba_f32(); - Box::new(colors.map(Vec4::from)) - } else { - log::trace!(" not colored"); - Box::new(std::iter::repeat(Vec4::splat(1.0))) - }; - let uv0: Box> = if let Some(uvs) = reader.read_tex_coords(0) { - let uvs = uvs.into_f32().map(Vec2::from); - log::trace!(" uv0: {} vertices", uvs.len()); - Box::new(uvs) - } else { - log::trace!(" uv0: none"); - Box::new(std::iter::repeat(Vec2::ZERO)) - }; - let uv1: Box> = if let Some(uvs) = reader.read_tex_coords(1) { - let uvs = uvs.into_f32().map(Vec2::from); - log::trace!(" uv1: {} vertices", uvs.len()); - Box::new(uvs) - } else { - log::trace!(" uv1: none"); - Box::new(std::iter::repeat(Vec2::ZERO)) - }; - let uvs = uv0 - .zip(uv1) - .map(|(uv0, uv1)| Vec4::new(uv0.x, uv0.y, uv1.x, uv1.y)); - - // See the GLTF spec on morph targets - // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#morph-targets - let mut num_morph_targets_positions = 0; - let mut num_morph_targets_normals = 0; - let mut num_morph_targets_tangents = 0; - let morph_targets = reader - .read_morph_targets() - .map(|(may_ps, may_ns, may_ts)| { - let may_ps = may_ps.map(|ps| ps.collect::>()); - let may_ns = may_ns.map(|ns| ns.collect::>()); - let may_ts = may_ts.map(|ts| ts.collect::>()); - num_morph_targets_positions = num_morph_targets_positions - .max(may_ps.as_ref().map(Vec::len).unwrap_or_default()); - num_morph_targets_normals = num_morph_targets_normals - .max(may_ps.as_ref().map(Vec::len).unwrap_or_default()); - num_morph_targets_tangents = num_morph_targets_tangents - .max(may_ps.as_ref().map(Vec::len).unwrap_or_default()); - (may_ps, may_ns, may_ts) - }) - .collect::>(); - let num_morph_targets = morph_targets.len(); - log::trace!(" {num_morph_targets} morph targets"); - let has_morph_targets = num_morph_targets_positions > 0 - || num_morph_targets_normals > 0 - || num_morph_targets_tangents > 0; - - let joint_weights_normalized = primitive - .attributes() - .find_map(|att| match att { - (gltf::Semantic::Weights(_), acc) => { - let n = acc.normalized(); - let ns = if n { "normalized" } else { "unnormalized" }; - let dt = acc.data_type(); - log::trace!(" joint weights {ns} {dt:?}"); - Some(n) - } - _ => None, - }) - .unwrap_or_default(); - - let joints = reader - .read_joints(0) - .map(|joints| { - let joints: Box> = Box::new( - joints - .into_u16() - .map(|[a, b, c, d]| [a as u32, b as u32, c as u32, d as u32]), - ); - joints - }) - .unwrap_or_else(|| Box::new(std::iter::repeat([0; 4]))); - - fn normalize_weights([a, b, c, d]: [f32; 4]) -> [f32; 4] { - let v = Vec4::new(a, b, c, d); - let manhatten = v.x.abs() + v.y.abs() + v.z.abs() + v.w.abs(); - let v = if manhatten <= f32::EPSILON { - Vec4::X - } else { - v / manhatten - }; - v.to_array() - } - - let joint_weights = reader - .read_weights(0) - .map(|weights| { - let weights: Box> = - Box::new(weights.into_f32().map(|w| { - if joint_weights_normalized { - w - } else { - normalize_weights(w) - } - })); - weights - }) - .unwrap_or_else(|| Box::new(std::iter::repeat([0.0, 0.0, 0.0, 0.0]))); - - let vertices = positions - .iter() - .zip(colors.zip(uvs.zip(normals.zip(tangents.zip(joints.zip(joint_weights)))))) - .enumerate() - .map( - |(i, (position, (color, (uv, (normal, (tangent, (joints, weights)))))))| { - ( - Vertex { - position: position.extend(0.0), - color, - uv, - normal: normal.extend(0.0), - tangent, - joints, - weights, - }, - morph_targets - .iter() - .map(|(mps, mns, mts)| { - ( - mps.as_ref().map(|ps| ps[i]), - mns.as_ref().map(|ns| ns[i]), - mts.as_ref().map(|ts| ts[i]), - ) - }) - .collect::>(), - ) - }, - ) - .collect::>(); - drop(morph_targets); - - // We don't yet support indices, so we'll just repeat vertices - let mut vertices = if let Some(indices) = reader.read_indices() { - let indices = indices.into_u32(); - indices - .map(|i| vertices[i as usize].clone()) - .collect::>() - } else { - vertices - }; - if gen_normals || gen_tangents { - vertices.chunks_mut(3).for_each(|t| match t { - [(a, _), (b, _), (c, _)] => { - if gen_normals { - let n = Vertex::generate_normal( - a.position.xyz(), - b.position.xyz(), - c.position.xyz(), - ); - a.normal = n.extend(a.normal.w); - b.normal = n.extend(b.normal.w); - c.normal = n.extend(c.normal.w); - } - if gen_tangents { - let tangent = Vertex::generate_tangent( - a.position.xyz(), - a.uv.xy(), - b.position.xyz(), - b.uv.xy(), - c.position.xyz(), - c.uv.xy(), - ); - debug_assert!(!tangent.w.is_nan(), "tangent is NaN"); - a.tangent = tangent; - b.tangent = tangent; - c.tangent = tangent; - } - } - _ => unreachable!("safe because we know these are triangles"), - }); - } - - // If we have morph targets we'll represent them by creating separate meshlets - // after the first "original" meshlet. That way we can index into them by using - // the vertex count, which MUST be the same according to the spec. - let (original_meshlet, morph_target_meshlets) = if has_morph_targets { - if num_morph_targets_positions > 0 { - log::trace!(" {num_morph_targets_positions} positions"); - } - if num_morph_targets_normals > 0 { - log::trace!(" {num_morph_targets_normals} normals"); - } - if num_morph_targets_tangents > 0 { - log::trace!(" {num_morph_targets_tangents} tangents"); - } - - // TODO: Optimization - preset capacity on these arrays. - let mut morph_target_meshlets = vec![vec![]; num_morph_targets]; - let mut original_meshlet = vec![]; - for (vert, targets) in vertices.into_iter() { - original_meshlet.push(vert); - for (i, (may_ps, may_ns, may_ts)) in targets.into_iter().enumerate() { - let p = may_ps.map(Vec3::from).unwrap_or_default().extend(0.0); - let n = may_ns.map(Vec3::from).unwrap_or_default().extend(0.0); - let t = may_ts.map(Vec3::from).unwrap_or_default().extend(0.0); - let mut v = Vertex::default(); - v.position = p; - v.normal = n; - v.tangent = t; - morph_target_meshlets[i].push(v); - } - } - (original_meshlet, morph_target_meshlets) - } else { - (vertices.into_iter().map(|(v, _)| v).collect(), vec![]) - }; - - // Here we add the morph targets as contiguous meshlets occurring directly after - // the original. This is because the GpuEntity has an array of 8 possible morph - // target weights, where each weight's index in the array maps to - // this contiguous meshlet. See GpuEntity::get_vertex - // for that indexing operation. - let (vertex_start, vertex_count) = builder.add_meshlet(original_meshlet); - for morph_target_meshlet in morph_target_meshlets.into_iter() { - let _ = builder.add_meshlet(morph_target_meshlet); - } - let weights = mesh.weights().map(|ws| ws.to_vec()).unwrap_or_default(); - if !weights.is_empty() { - log::trace!(" weights:"); - } - for (i, w) in weights.iter().enumerate() { - log::trace!(" {i}: {w}"); - } - let material_index = primitive.material().index(); - let material_id = self.material_at(material_index, builder, document)?; - let bounding_box = primitive.bounding_box().into(); - mesh_primitives.push(GltfMeshPrim { - weights, - vertex_start, - vertex_count, - material_id, - bounding_box, - num_morph_targets: num_morph_targets as u32, - morph_targets_have_positions: num_morph_targets_positions > 0, - morph_targets_have_normals: num_morph_targets_normals > 0, - morph_targets_have_tangents: num_morph_targets_tangents > 0, - }); - } - let _ = self - .meshes - .insert(mesh.index(), mesh.name().map(String::from), mesh_primitives); - Ok(()) - } - - /// Load a node, setting its transform and type. - fn load_shallow_node( - &mut self, - node: gltf::Node<'_>, - builder: &mut SceneBuilder, - ) -> Result, GltfLoaderError> { - let index = node.index(); - let name = node.name().map(String::from); - log::trace!("loading node {index} {name:?}",); - let (position, rotation, scale) = node.transform().decomposed(); - let position = Vec3::from(position); - let rotation = Quat::from_array(rotation); - let scale = Vec3::from(scale); - log::trace!(" position: {position:?}"); - log::trace!(" rotation: {rotation:?}"); - log::trace!(" scale: {scale:?}"); - - let mut gltf_node = GltfNode::default(); - - if let Some(camera) = node.camera() { - log::trace!(" is camera"); - gltf_node.gltf_camera_index = Some(camera.index()); - } - if let Some(mesh) = node.mesh() { - log::trace!(" is mesh"); - gltf_node.gltf_mesh_index = Some(mesh.index()); - } - if let Some(light) = node.light() { - log::trace!(" is light"); - gltf_node.gltf_light_index = Some(light.index()); - } - if let Some(skin) = node.skin() { - log::trace!(" is skin"); - gltf_node.gltf_skin_index = Some(skin.index()); - } - let mut printed = false; - for child in node.children() { - if !printed { - log::trace!(" is parent"); - printed = true; - } - log::trace!( - " child {} {:?}", - child.index(), - child.name().map(String::from) - ); - } - - let entity = builder - .new_entity() - .with_position(position) - .with_rotation(rotation) - .with_scale(scale) - .build(); - gltf_node.entity_id = entity.id; - let _ = self.nodes.insert(index, name, gltf_node); - - Ok(entity.id) - } - - fn load_node( - &mut self, - node: gltf::Node<'_>, - builder: &mut SceneBuilder, - buffers: &[gltf::buffer::Data], - ) -> Result<(), GltfLoaderError> { - let index = node.index(); - let name = node.name().map(String::from); - let entity_id = self.nodes.get(node.index()).unwrap().entity_id; - log::trace!("fleshing out node {index} {name:?} {entity_id:?}"); - let (position, rotation, _scale) = { - let entity = builder.entities.get_mut(entity_id.index()).unwrap(); - (entity.position, entity.rotation, entity.scale) - }; - - if let Some(camera) = node.camera() { - let projection = match camera.projection() { - gltf::camera::Projection::Orthographic(o) => Mat4::orthographic_rh( - -o.xmag(), - o.xmag(), - -o.ymag(), - o.ymag(), - o.znear(), - o.zfar(), - ), - gltf::camera::Projection::Perspective(p) => { - let fovy = p.yfov(); - let aspect = p.aspect_ratio().unwrap_or(1.0); - if let Some(zfar) = p.zfar() { - Mat4::perspective_rh(fovy, aspect, p.znear(), zfar) - } else { - Mat4::perspective_infinite_rh( - p.yfov(), - p.aspect_ratio().unwrap_or(1.0), - p.znear(), - ) - } - } - }; - let view: Mat4 = Mat4::from_cols_array_2d(&node.transform().matrix()).inverse(); - - let _ = self.cameras.insert( - camera.index(), - camera.name().map(String::from), - (projection, view), - ); - } - - if let Some(mesh) = node.mesh() { - let index = mesh.index(); - log::trace!(" node is mesh {index}"); - let prims = self.meshes.get(index).context(MissingMeshSnafu { - index, - name: mesh - .name() - .map(String::from) - .unwrap_or("unknown".to_string()), - })?; - - let node_weights = node.weights().map(|ws| ws.to_vec()); - if let Some(ws) = node_weights.as_ref() { - log::trace!(" node weights:"); - for (i, w) in ws.iter().enumerate() { - log::trace!(" {i}: {w}"); - } - } - let children = if prims.len() == 1 { - log::trace!(" with only 1 primitive, so no children needed"); - let GltfMeshPrim { - vertex_start, - vertex_count, - material_id, - bounding_box: _, - weights, - num_morph_targets, - morph_targets_have_positions, - morph_targets_have_normals, - morph_targets_have_tangents, - } = &prims[0]; - - let entity = builder.entities.get_mut(entity_id.index()).unwrap(); - entity.mesh_first_vertex = *vertex_start; - entity.mesh_vertex_count = *vertex_count; - entity.material = *material_id; - entity.info.set_num_morph_targets(*num_morph_targets as u8); - entity - .info - .set_morph_targets_have_positions(*morph_targets_have_positions); - entity - .info - .set_morph_targets_have_normals(*morph_targets_have_normals); - entity - .info - .set_morph_targets_have_tangents(*morph_targets_have_tangents); - entity.set_morph_target_weights(weights.iter().copied()); - vec![] - } else { - log::trace!(" with {} child primitives:", prims.len()); - prims - .iter() - .map( - |GltfMeshPrim { - vertex_start, - vertex_count, - material_id, - bounding_box: _, - weights, - num_morph_targets, - morph_targets_have_positions, - morph_targets_have_normals, - morph_targets_have_tangents, - }| { - let child = builder - .new_entity() - .with_starting_vertex_and_count(*vertex_start, *vertex_count) - .with_material(*material_id) - .with_num_morph_targets(*num_morph_targets as u8) - .with_morph_targets_have_positions(*morph_targets_have_positions) - .with_morph_targets_have_normals(*morph_targets_have_normals) - .with_morph_targets_have_tangents(*morph_targets_have_tangents) - .with_morph_target_weights(weights.clone()) - .with_parent(entity_id) - .build() - .id; - log::trace!(" child {child:?}"); - log::trace!(" weights {weights:?}"); - log::trace!(" num_morph_targets {}", num_morph_targets); - child - }, - ) - .collect::>() - }; - self.nodes.get_mut(index).unwrap().child_ids = children; - } - - if let Some(light) = node.light() { - let color = Vec3::from(light.color()).extend(1.0); - let direction = Mat4::from_quat(rotation).transform_vector3(Vec3::NEG_Z); - let intensity = light.intensity(); - let gpu_light = match light.kind() { - Kind::Directional => builder - .new_directional_light() - .with_direction(direction) - .with_color(color) - .with_intensity(intensity) - .build(), - Kind::Point => builder - .new_point_light() - .with_position(position.xyz()) - .with_color(color) - .with_intensity(intensity) - .build(), - Kind::Spot { - inner_cone_angle, - outer_cone_angle, - } => builder - .new_spot_light() - .with_position(position.xyz()) - .with_direction(direction) - .with_color(color) - .with_intensity(intensity) - .with_cutoff(inner_cone_angle, outer_cone_angle) - .build(), - }; - log::trace!(" node is {}", from_gltf_light_kind(light.kind())); - log::trace!(" with color : {color:?}"); - log::trace!(" with direction: {direction:?}"); - log::trace!( - " with intensity: {intensity:?} {}", - gltf_light_intensity_units(light.kind()) - ); - let _ = self.lights.insert(light.index(), None, gpu_light); - } - - if let Some(skin) = node.skin() { - log::trace!(" node is a skin"); - if let Some(matrices) = skin - .reader(|buffer| Some(&buffers[buffer.index()])) - .read_inverse_bind_matrices() - { - let mut joint_ids = vec![]; - for (matrix, joint_node) in matrices.zip(skin.joints()) { - let index = joint_node.index(); - let name = joint_node.name().map(String::from); - let gltf_node = self.nodes.get(index).unwrap(); - let id = gltf_node.entity_id; - joint_ids.push(id); - log::trace!(" with joint {index} {name:?} {id:?}"); - let joint_entity = builder.entities.get_mut(id.index()).unwrap(); - joint_entity.inverse_bind_matrix = Mat4::from_cols_array_2d(&matrix); - } - let skin_entity = builder.entities.get_mut(entity_id.index()).unwrap(); - skin_entity.info.set_is_skin(true); - assert!( - joint_ids.len() <= skin_entity.skin_joint_ids.len(), - "renderling only supports {} joints for skinning, which is less than the \ - required {} for this model", - skin_entity.skin_joint_ids.len(), - joint_ids.len() - ); - for (i, id) in joint_ids - .into_iter() - .take(skin_entity.skin_joint_ids.len()) - .enumerate() - { - skin_entity.skin_joint_ids[i] = id; - } - } - } - - Ok(()) - } - - pub fn load_animation( - &mut self, - animation: gltf::Animation, - buffers: &[gltf::buffer::Data], - ) -> Result<(), GltfLoaderError> { - let index = animation.index(); - let name = animation.name().map(String::from); - log::trace!("loading animation {index} {name:?}"); - let mut r_animation = GltfAnimation::default(); - r_animation.name = name; - for (i, channel) in animation.channels().enumerate() { - log::trace!(" channel {i}"); - let reader = channel.reader(|buffer| Some(&buffers[buffer.index()])); - let inputs = reader.read_inputs().context(MissingInputsSnafu)?; - let outputs = reader.read_outputs().context(MissingOutputsSnafu)?; - let keyframes = inputs.map(|t| Keyframe(t)).collect::>(); - log::trace!(" with {} keyframes", keyframes.len()); - let interpolation = channel.sampler().interpolation().into(); - log::trace!(" using {interpolation} interpolation"); - let index = channel.target().node().index(); - let name = channel.target().node().name(); - log::trace!(" of node {index} {name:?}"); - let tween = Tween { - properties: match outputs { - gltf::animation::util::ReadOutputs::Translations(ts) => { - log::trace!(" tweens translations"); - TweenProperties::Translations(ts.map(Vec3::from).collect()) - } - gltf::animation::util::ReadOutputs::Rotations(rs) => { - log::trace!(" tweens rotations"); - TweenProperties::Rotations(rs.into_f32().map(Quat::from_array).collect()) - } - gltf::animation::util::ReadOutputs::Scales(ss) => { - log::trace!(" tweens scales"); - TweenProperties::Scales(ss.map(Vec3::from).collect()) - } - gltf::animation::util::ReadOutputs::MorphTargetWeights(ws) => { - log::trace!(" tweens morph target weights"); - let ws = ws.into_f32().collect::>(); - let num_morph_targets = ws.len() / keyframes.len(); - log::trace!(" weights length : {}", ws.len()); - log::trace!(" keyframes length: {}", keyframes.len()); - log::trace!(" morph targets : {}", num_morph_targets); - TweenProperties::MorphTargetWeights( - ws.chunks_exact(num_morph_targets) - .map(|chunk| chunk.iter().copied().collect::>()) - .collect(), - ) - } - }, - keyframes, - interpolation, - target_node_index: index, - target_entity_id: { - let node = self.nodes.get(index).context(MissingNodeSnafu { index })?; - node.entity_id - }, - }; - r_animation.tweens.push(tween); - } - - let total_time = r_animation.length_in_seconds(); - log::trace!(" taking {total_time} seconds in total"); - - self.animations.insert( - animation.index(), - animation.name().map(String::from), - r_animation, - ); - Ok(()) - } - - pub fn load_animations( - &mut self, - document: &gltf::Document, - buffers: &[gltf::buffer::Data], - ) -> Result<(), GltfLoaderError> { - for animation in document.animations() { - self.load_animation(animation, buffers)?; - } - - Ok(()) - } -} - -#[cfg(all(test, feature = "gltf"))] -mod test { - use glam::{Vec3, Vec4}; - - use crate::{camera, shader::pbr::PbrMaterial, LightingModel, RenderGraphConfig, Renderling}; - - //#[test] - //fn normal_mapping_brick_sphere() { - // let size = 600; - // let mut r = - // Renderling::headless(size, size).with_background_color(Vec3::splat(1.0).extend(1.0)); - // let mut builder = r.new_scene(); - // let loader = builder.gltf_load("../../gltf/red_brick_03_1k.glb").unwrap(); - // let (projection, view) = loader.cameras.get(0).copied().unwrap(); - // builder.set_camera(projection, view); - - // let scene = builder.build().unwrap(); - // r.setup_render_graph(RenderGraphConfig { - // scene: Some(scene), - // with_screen_capture: true, - // ..Default::default() - // }); - - // let img = r.render_image().unwrap(); - // println!("saving frame"); - // img_diff::assert_img_eq("gltf_normal_mapping_brick_sphere.png", img); - //} - - #[test] - // Tests that we can reuse the same builder for multiple loaders, building - // up a scene of multiple gltf documents. - fn can_load_multiple_gltfs_into_one_builder() { - let size = 600; - let mut r = - Renderling::headless(size, size).with_background_color(Vec3::splat(1.0).extend(1.0)); - let mut builder = r.new_scene(); - let brick_loader = builder.gltf_load("../../gltf/red_brick_03_1k.glb").unwrap(); - let (projection, view) = brick_loader.cameras.get(0).copied().unwrap(); - builder.set_camera(projection, view); - - let _another_sun = builder - .new_directional_light() - .with_color(Vec4::ONE) - .with_direction(Vec3::NEG_Z) - .build(); - - let brick_sphere_id = brick_loader - .nodes - .get_by_name("Sphere") - .next() - .unwrap() - .entity_id; - { - // move the sphere over so we can see both models - let brick_sphere = builder.entities.get_mut(brick_sphere_id.index()).unwrap(); - brick_sphere.position = Vec4::new(-0.2, 0.0, 0.0, 0.0); - } - - let bust_loader = builder.gltf_load("../../gltf/marble_bust_1k.glb").unwrap(); - let bust_id = bust_loader - .nodes - .get_by_name("marble_bust_01") - .next() - .unwrap() - .entity_id; - { - // move the bust over too - let bust = builder.entities.get_mut(bust_id.index()).unwrap(); - bust.position = Vec4::new(0.2, -0.1, 0.2, 0.0); - } - - let scene = builder.build().unwrap(); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - with_bloom: false, - ..Default::default() - }); - - let img = r.render_image().unwrap(); - println!("saving frame"); - img_diff::assert_img_eq("gltf_can_load_multiple.png", img.clone()); - } - - #[cfg(feature = "gltf")] - #[test] - fn simple_animation() { - let mut r = Renderling::headless(50, 50).with_background_color(Vec4::ONE); - - let projection = camera::perspective(50.0, 50.0); - let view = camera::look_at(Vec3::Z * 3.0, Vec3::ZERO, Vec3::Y); - let mut builder = r.new_scene().with_camera(projection, view); - let default_material = builder.add_material(PbrMaterial { - albedo_factor: Vec4::new(0.0, 0.0, 0.0, 0.5), - lighting_model: LightingModel::NO_LIGHTING, - ..Default::default() - }); - - let loader = builder - .gltf_load("../../gltf/animated_triangle.gltf") - .unwrap(); - let tri_id = loader.nodes.get(0).unwrap().entity_id; - { - let entity = builder.entities.get_mut(tri_id.index()).unwrap(); - entity.material = default_material; - } - let mut entities = builder.entities.clone(); - let scene = builder.build().unwrap(); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() - }); - let img = r.render_image().unwrap(); - img_diff::assert_img_eq("gltf_simple_animation.png", img); - - assert_eq!(1, loader.animations.len()); - - let anime = loader.animations.get(0).unwrap(); - println!("anime: {:?}", anime); - assert_eq!(1.0, anime.tweens[0].length_in_seconds()); - - let num = 8; - for i in 0..8 { - let t = i as f32 / num as f32; - let transforms = anime.get_properties_at_time(t).unwrap(); - let scene = r.graph.get_resource_mut::().unwrap().unwrap(); - for (id, tween_prop) in transforms.into_iter() { - let entity = entities.get_mut(id.index()).unwrap(); - match tween_prop { - crate::TweenProperty::Translation(t) => { - entity.position = t.extend(0.0); - } - crate::TweenProperty::Rotation(r) => { - entity.rotation = r; - } - crate::TweenProperty::Scale(s) => { - entity.scale = s.extend(0.0); - } - crate::TweenProperty::MorphTargetWeights(ws) => { - entity.set_morph_target_weights(ws); - } - } - scene.update_entity(*entity).unwrap(); - } - let img = r.render_image().unwrap(); - img_diff::assert_img_eq(&format!("gltf_simple_animation_after/{i}.png"), img); - } - } - - #[cfg(feature = "gltf")] - #[test] - fn simple_skin() { - use crate::{Scene, TweenProperty, ViewMut}; - - let size = 100; - let mut r = - Renderling::headless(size, size).with_background_color(Vec3::splat(0.0).extend(1.0)); - let projection = camera::perspective(50.0, 50.0); - let view = camera::look_at(Vec3::Z * 4.0, Vec3::ZERO, Vec3::Y); - let mut builder = r.new_scene().with_camera(projection, view); - let loader = builder - .gltf_load("../../gltf/gltfTutorial_019_SimpleSkin.gltf") - .unwrap(); - let skin_animation = loader.animations.get(0).unwrap(); - let skin_animation_duration = skin_animation.length_in_seconds(); - let mut entities = builder.entities.clone(); - assert!(entities[0].info.is_skin()); - let scene = builder.build().unwrap(); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() - }); - - let frames = 4; - for i in 0..=frames { - let time = if i == 0 { - 0.0 - } else { - i as f32 / frames as f32 * skin_animation_duration - }; - r.graph - .visit(|mut scene: ViewMut| { - for (id, tween_prop) in skin_animation.get_properties_at_time(time).unwrap() { - let ent = entities.get_mut(id.index()).unwrap(); - match tween_prop { - TweenProperty::Translation(t) => { - ent.position = t.extend(ent.position.w); - } - TweenProperty::Rotation(r) => { - ent.rotation = r; - } - TweenProperty::Scale(s) => { - if s == Vec3::ZERO { - log::trace!("scale is zero at time: {time:?}"); - panic!("animation: {skin_animation:#?}"); - } - ent.scale = s.extend(ent.scale.w); - } - TweenProperty::MorphTargetWeights(ws) => { - ent.set_morph_target_weights(ws); - } - } - scene.update_entity(*ent).unwrap(); - } - }) - .unwrap(); - - let img = r.render_image().unwrap(); - img_diff::assert_img_eq(&format!("gltf_simple_skin/{i}.png"), img); - } - } -} diff --git a/crates/renderling/src/skybox.rs b/crates/renderling/src/skybox.rs index b3baa83a..89f119ad 100644 --- a/crates/renderling/src/skybox.rs +++ b/crates/renderling/src/skybox.rs @@ -1,8 +1,13 @@ //! An HDR skybox. +use std::sync::Arc; + +use crabslab::{CpuSlab, GrowableSlab, Slab, SlabItem, WgpuBuffer}; use glam::{Mat4, Vec3}; -use renderling_shader::stage::GpuConstants; -use crate::{SceneImage, Uniform}; +use crate::{ + atlas::AtlasImage, + shader::{convolution::VertexPrefilterEnvironmentCubemapIds, stage::Camera}, +}; /// Render pipeline used to draw a skybox. pub struct SkyboxRenderPipeline(pub wgpu::RenderPipeline); @@ -15,7 +20,7 @@ pub fn skybox_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { binding: 0, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, + ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None, }, @@ -43,7 +48,7 @@ pub fn skybox_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { pub fn create_skybox_bindgroup( device: &wgpu::Device, - constants: &Uniform, + slab_buffer: &wgpu::Buffer, texture: &crate::Texture, ) -> wgpu::BindGroup { device.create_bind_group(&wgpu::BindGroupDescriptor { @@ -52,7 +57,7 @@ pub fn create_skybox_bindgroup( entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: constants.buffer().as_entire_binding(), + resource: slab_buffer.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 1, @@ -84,7 +89,7 @@ pub fn create_skybox_render_pipeline( }); SkyboxRenderPipeline( device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("skybox pipeline"), + label: Some("skybox render pipeline"), layout: Some(&pp_layout), vertex: wgpu::VertexState { module: &vertex_shader, @@ -143,32 +148,33 @@ pub struct Skybox { // `Id` of the camera to use for rendering the skybox. // // The camera is used to determine the orientation of the skybox. - pub camera: crate::shader::id::Id, + pub camera: crabslab::Id, } impl Skybox { /// Create an empty, transparent skybox. - pub fn empty(device: &wgpu::Device, queue: &wgpu::Queue) -> Self { + pub fn empty(device: crate::Device, queue: crate::Queue) -> Self { log::trace!("creating empty skybox"); - let hdr_img = SceneImage { + let hdr_img = AtlasImage { pixels: vec![0u8; 4 * 4], width: 1, height: 1, - format: crate::SceneImageFormat::R32G32B32A32FLOAT, + format: crate::AtlasImageFormat::R32G32B32A32FLOAT, apply_linear_transfer: false, }; - Self::new(device, queue, hdr_img, crate::shader::id::Id::NONE) + Self::new(device, queue, hdr_img, crabslab::Id::::NONE) } /// Create a new `Skybox`. pub fn new( - device: &wgpu::Device, - queue: &wgpu::Queue, - hdr_img: SceneImage, - camera: crate::shader::id::Id, + device: crate::Device, + queue: crate::Queue, + hdr_img: AtlasImage, + camera: crabslab::Id, ) -> Self { log::trace!("creating skybox"); - let equirectangular_texture = Skybox::hdr_texture_from_scene_image(device, queue, hdr_img); + let equirectangular_texture = + Skybox::hdr_texture_from_atlas_image(&device, &queue, hdr_img); let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 10.0); let views = [ Mat4::look_at_rh( @@ -202,67 +208,30 @@ impl Skybox { Vec3::new(0.0, -1.0, 0.0), ), ]; - // Create unit cube for projections. - let cube_vertices: [[f32; 3]; 8] = [ - // front - [-1.0, -1.0, 1.0], - [1.0, -1.0, 1.0], - [1.0, 1.0, 1.0], - [-1.0, 1.0, 1.0], - // back - [-1.0, -1.0, -1.0], - [1.0, -1.0, -1.0], - [1.0, 1.0, -1.0], - [-1.0, 1.0, -1.0], - ]; - let cube_elements: [u16; 36] = [ - // front - 0, 1, 2, 2, 3, 0, // right - 1, 5, 6, 6, 2, 1, // back - 7, 6, 5, 5, 4, 7, // left - 4, 0, 3, 3, 7, 4, // bottom - 4, 5, 1, 1, 0, 4, // top - 3, 2, 6, 6, 7, 3, - ]; - - let unit_cube_mesh = crate::mesh::Mesh::new( - device, - Some("unit cube"), - cube_vertices, - Some(cube_elements), - ); // Create environment map. let environment_cubemap = Skybox::create_environment_map_from_hdr( - device, - queue, + device.clone(), + queue.clone(), &equirectangular_texture, - &unit_cube_mesh, proj, views, ); // Convolve the environment map. - let irradiance_cubemap = Skybox::create_irradiance_map( - device, - queue, - &environment_cubemap, - &unit_cube_mesh, - proj, - views, - ); + let irradiance_cubemap = + Skybox::create_irradiance_map(&device, &queue, &environment_cubemap, proj, views); // Generate specular IBL pre-filtered environment map. let prefiltered_environment_cubemap = Skybox::create_prefiltered_environment_map( - device, - queue, + &device, + &queue, &environment_cubemap, - &unit_cube_mesh, proj, views, ); - let brdf_lut = Skybox::create_precomputed_brdf_texture(device, queue); + let brdf_lut = Skybox::create_precomputed_brdf_texture(&device, &queue); Skybox { environment_cubemap, @@ -273,11 +242,11 @@ impl Skybox { } } - /// Convert an HDR [`SceneImage`] into a texture. - pub fn hdr_texture_from_scene_image( + /// Convert an HDR [`AtlasImage`] into a texture. + pub fn hdr_texture_from_atlas_image( device: &wgpu::Device, queue: &wgpu::Queue, - img: SceneImage, + img: AtlasImage, ) -> crate::Texture { crate::Texture::new_with( device, @@ -306,48 +275,50 @@ impl Skybox { queue: &wgpu::Queue, hdr_data: &[u8], ) -> crate::Texture { - let img = SceneImage::from_hdr_bytes(hdr_data).unwrap(); - Self::hdr_texture_from_scene_image(device, queue, img) + let img = AtlasImage::from_hdr_bytes(hdr_data).unwrap(); + Self::hdr_texture_from_atlas_image(device, queue, img) } fn create_environment_map_from_hdr( - device: &wgpu::Device, - queue: &wgpu::Queue, + device: crate::Device, + queue: crate::Queue, hdr_texture: &crate::Texture, - unit_cube_mesh: &crate::mesh::Mesh, proj: Mat4, views: [Mat4; 6], ) -> crate::Texture { // Create the cubemap-making pipeline. let pipeline = crate::cubemap::CubemapMakingRenderPipeline::new( - device, + &device, wgpu::TextureFormat::Rgba16Float, ); - let mut constants = crate::uniform::Uniform::new( - device, - GpuConstants { - camera_projection: proj, - ..Default::default() - }, + + let buffer = WgpuBuffer::new_usage( + device.0.clone(), + queue.0.clone(), + Camera::slab_size(), wgpu::BufferUsages::VERTEX, - wgpu::ShaderStages::VERTEX, + ); + let mut slab = CpuSlab::new(buffer); + slab.write( + crabslab::Id::new(0), + &Camera::default().with_projection(proj), ); let bindgroup = crate::cubemap::cubemap_making_bindgroup( - device, + &device, Some("environment cubemap"), - &constants, + &slab.as_ref().get_buffer(), hdr_texture, ); Self::render_cubemap( - device, - queue, + &device, + &queue, "environment", &pipeline.0, - &mut constants, + &mut slab, + proj, &bindgroup, - unit_cube_mesh, views, 512, Some(9), @@ -359,9 +330,9 @@ impl Skybox { queue: &wgpu::Queue, label_prefix: &str, pipeline: &wgpu::RenderPipeline, - constants: &mut Uniform, + slab: &mut CpuSlab, + projection: Mat4, bindgroup: &wgpu::BindGroup, - unit_cube_mesh: &crate::mesh::Mesh, views: [Mat4; 6], texture_size: u32, mip_levels: Option, @@ -396,8 +367,7 @@ impl Skybox { ); // update the view to point at one of the cube faces - constants.camera_view = views[i]; - constants.update(queue); + slab.write(0u32.into(), &Camera::new(projection, views[i])); { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -415,7 +385,7 @@ impl Skybox { render_pass.set_pipeline(pipeline); render_pass.set_bind_group(0, &bindgroup, &[]); - unit_cube_mesh.draw(&mut render_pass); + render_pass.draw(0..36, 0..1); } queue.submit([encoder.finish()]); @@ -441,42 +411,43 @@ impl Skybox { } fn create_irradiance_map( - device: &wgpu::Device, - queue: &wgpu::Queue, + device: impl Into>, + queue: impl Into>, environment_texture: &crate::Texture, - unit_cube_mesh: &crate::mesh::Mesh, proj: Mat4, views: [Mat4; 6], ) -> crate::Texture { + let device = device.into(); + let queue = queue.into(); let pipeline = crate::ibl::diffuse_irradiance::DiffuseIrradianceConvolutionRenderPipeline::new( - device, + &device, wgpu::TextureFormat::Rgba16Float, ); - let mut constants = crate::uniform::Uniform::new( - device, - GpuConstants { - camera_projection: proj, - ..Default::default() - }, + let buffer = WgpuBuffer::new_usage( + device.clone(), + queue.clone(), + Camera::slab_size(), wgpu::BufferUsages::VERTEX, - wgpu::ShaderStages::VERTEX, ); + let mut slab = CpuSlab::new(buffer); + slab.write(0u32.into(), &Camera::default().with_projection(proj)); + let bindgroup = crate::ibl::diffuse_irradiance::diffuse_irradiance_convolution_bindgroup( - device, + &device, Some("irradiance"), - &constants, + &slab.as_ref().get_buffer(), environment_texture, ); Self::render_cubemap( - device, - queue, + &device, + &queue, "irradiance", &pipeline.0, - &mut constants, + &mut slab, + proj, &bindgroup, - unit_cube_mesh, views, 32, None, @@ -484,33 +455,29 @@ impl Skybox { } fn create_prefiltered_environment_map( - device: &wgpu::Device, - queue: &wgpu::Queue, + device: impl Into>, + queue: impl Into>, environment_texture: &crate::Texture, - unit_cube_mesh: &crate::mesh::Mesh, proj: Mat4, views: [Mat4; 6], ) -> crate::Texture { - let mut constants = crate::uniform::Uniform::new( - device, - GpuConstants { - camera_projection: proj, - ..Default::default() - }, + let device = device.into(); + let queue = queue.into(); + let buffer = WgpuBuffer::new_usage( + device.clone(), + queue.clone(), + Camera::slab_size(), wgpu::BufferUsages::VERTEX, - wgpu::ShaderStages::VERTEX, - ); - let mut roughness = Uniform::::new( - device, - 0.0, - wgpu::BufferUsages::empty(), - wgpu::ShaderStages::VERTEX_FRAGMENT, ); + let mut slab = CpuSlab::new(buffer); + // Write the camera at 0 and then the roughness + let camera = slab.append(&Camera::default().with_projection(proj)); + let roughness = slab.append(&0.0f32); + let id = slab.append(&VertexPrefilterEnvironmentCubemapIds { camera, roughness }); let (pipeline, bindgroup) = crate::ibl::prefiltered_environment::create_pipeline_and_bindgroup( - device, - &constants, - &roughness, + &device, + &slab.as_ref().get_buffer(), environment_texture, ); @@ -522,16 +489,15 @@ impl Skybox { let mip_height: u32 = 128 >> mip_level; // update the roughness for these mips - *roughness = mip_level as f32 / 4.0; - roughness.update(queue); + slab.write(roughness, &(mip_level as f32 / 4.0)); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("specular convolution"), }); let cubemap_face = crate::Texture::new_with( - device, - queue, + &device, + &queue, Some(&format!("cubemap{i}{mip_level}prefiltered_environment")), Some(wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC), None, @@ -545,8 +511,7 @@ impl Skybox { ); // update the view to point at one of the cube faces - constants.camera_view = views[i]; - constants.update(queue); + slab.write(camera, &Camera::new(proj, views[i])); { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -564,7 +529,7 @@ impl Skybox { render_pass.set_pipeline(&pipeline); render_pass.set_bind_group(0, &bindgroup, &[]); - unit_cube_mesh.draw(&mut render_pass); + render_pass.draw(0..36, id.inner()..id.inner() + 1); } queue.submit([encoder.finish()]); @@ -573,8 +538,8 @@ impl Skybox { } crate::Texture::new_cubemap_texture( - device, - queue, + &device, + &queue, Some(&format!("prefiltered environment cubemap")), 128, cubemap_faces.as_slice(), @@ -587,35 +552,6 @@ impl Skybox { device: &wgpu::Device, queue: &wgpu::Queue, ) -> crate::Texture { - #[repr(C)] - #[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)] - struct Vert { - pos: [f32; 3], - uv: [f32; 2], - } - - let bl = Vert { - pos: [-1.0, -1.0, 0.0], - uv: [0.0, 1.0], - }; - let br = Vert { - pos: [1.0, -1.0, 0.0], - uv: [1.0, 1.0], - }; - let tl = Vert { - pos: [-1.0, 1.0, 0.0], - uv: [0.0, 0.0], - }; - let tr = Vert { - pos: [1.0, 1.0, 0.0], - uv: [1.0, 0.0], - }; - - let vertices = [bl, br, tr, bl, tr, tl]; - - let screen_space_quad_mesh = - crate::mesh::Mesh::from_vertices(device, Some("brdf_lut"), vertices); - let vertex_module = device.create_shader_module(wgpu::include_spirv!( "linkage/convolution-vertex_brdf_lut_convolution.spv" )); @@ -628,14 +564,7 @@ impl Skybox { vertex: wgpu::VertexState { module: &vertex_module, entry_point: "convolution::vertex_brdf_lut_convolution", - buffers: &[wgpu::VertexBufferLayout { - array_stride: (3 + 2) * std::mem::size_of::() as u64, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &wgpu::vertex_attr_array![ - 0 => Float32x3, - 1 => Float32x2 - ], - }], + buffers: &[], }, primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, @@ -702,7 +631,7 @@ impl Skybox { }); render_pass.set_pipeline(&pipeline); - screen_space_quad_mesh.draw(&mut render_pass); + render_pass.draw(0..6, 0..1); } queue.submit([encoder.finish()]); framebuffer @@ -711,37 +640,39 @@ impl Skybox { #[cfg(test)] mod test { + use crabslab::GrowableSlab; use glam::Vec3; use super::*; - use crate::{RenderGraphConfig, Renderling}; + use crate::Renderling; #[test] fn hdr_skybox_scene() { let mut r = Renderling::headless(600, 400); let proj = crate::camera::perspective(600.0, 400.0); let view = crate::camera::look_at(Vec3::new(0.0, 0.0, 2.0), Vec3::ZERO, Vec3::Y); - let mut builder = r.new_scene().with_camera(proj, view); - builder.add_skybox_image_from_path("../../img/hdr/resting_place.hdr"); - let scene = builder.build().unwrap(); + + let mut stage = r.new_stage(); + stage.configure_graph(&mut r, true); + + let camera = stage.append(&Camera::new(proj, view)); + let skybox = stage + .new_skybox_from_path("../../img/hdr/resting_place.hdr", camera) + .unwrap(); assert_eq!( wgpu::TextureFormat::Rgba16Float, - scene.skybox.irradiance_cubemap.texture.format() + skybox.irradiance_cubemap.texture.format() ); assert_eq!( wgpu::TextureFormat::Rgba16Float, - scene - .skybox - .prefiltered_environment_cubemap - .texture - .format() + skybox.prefiltered_environment_cubemap.texture.format() ); for i in 0..6 { // save out the irradiance face let copied_buffer = crate::Texture::read_from( - &scene.skybox.irradiance_cubemap.texture, + &skybox.irradiance_cubemap.texture, r.get_device(), r.get_queue(), 32, @@ -765,7 +696,7 @@ mod test { let mip_size = 128u32 >> mip_level; // save out the prefiltered environment faces' mips let copied_buffer = crate::Texture::read_from( - &scene.skybox.prefiltered_environment_cubemap.texture, + &skybox.prefiltered_environment_cubemap.texture, r.get_device(), r.get_queue(), mip_size as usize, @@ -792,12 +723,9 @@ mod test { } } - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() - }); - let img = r.render_image().unwrap(); + stage.set_skybox(skybox); + + let img = r.render_linear_image().unwrap(); img_diff::assert_img_eq("skybox/hdr.png", img); } diff --git a/crates/renderling/src/slab.rs b/crates/renderling/src/slab.rs deleted file mode 100644 index 62e6a090..00000000 --- a/crates/renderling/src/slab.rs +++ /dev/null @@ -1,446 +0,0 @@ -//! CPU side of slab storage. -// TODO: part out Id, Slab, etc into new "slab" libs. -// https://discord.com/channels/750717012564770887/750717499737243679/1187537792910229544 -use std::{ - ops::Deref, - sync::{atomic::AtomicUsize, Arc, RwLock}, -}; - -use renderling_shader::{array::Array, id::Id}; -use snafu::{ResultExt, Snafu}; - -pub use renderling_shader::slab::{Slab, Slabbed}; - -#[derive(Debug, Snafu)] -pub enum SlabError { - #[snafu(display( - "Out of capacity. Tried to write {type_is}(slab size={slab_size}) \ - at {index} but capacity is {capacity}", - ))] - Capacity { - type_is: &'static str, - slab_size: usize, - index: usize, - capacity: usize, - }, - - #[snafu(display( - "Out of capacity. Tried to write an array of {elements} {type_is}\ - (each of slab size={slab_size}) \ - at {index} but capacity is {capacity}", - ))] - ArrayCapacity { - type_is: &'static str, - elements: usize, - slab_size: usize, - index: usize, - capacity: usize, - }, - - #[snafu(display( - "Array({type_is}) length mismatch. Tried to write {data_len} elements \ - into array of length {array_len}", - ))] - ArrayLen { - type_is: &'static str, - array_len: usize, - data_len: usize, - }, - - #[snafu(display("Async recv error: {source}"))] - AsyncRecv { source: async_channel::RecvError }, - - #[snafu(display("Async error: {source}"))] - Async { source: wgpu::BufferAsyncError }, -} - -pub fn print_slab(slab: &[u32], starting_index: usize) { - for (u, i) in slab.iter().zip(starting_index..) { - println!("{i:02}: {u:032b} {u:010} {:?}", f32::from_bits(*u)); - } -} - -/// 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 { - pub(crate) buffer: Arc>, - // The number of u32 elements currently stored in the buffer. - // - // This is the next index to write into. - len: Arc, - // The total number of u32 elements that can be stored in the buffer. - capacity: Arc, -} - -impl SlabBuffer { - 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 - | 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: 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.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.load(std::sync::atomic::Ordering::Relaxed) - } - - fn maybe_expand_to_fit( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - len: usize, - ) { - let size = T::slab_size(); - let capacity = self.capacity(); - //log::trace!( - // "append_slice: {size} * {ts_len} + {len} ({}) >= {capacity}", - // size * ts_len + len - //); - let capacity_needed = self.len() + size * 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); - } - } - - /// Preallocate space for one `T` element, but don't write anything to the buffer. - /// - /// This can be used to write later with [`Self::write`]. - /// - /// NOTE: This changes the next available buffer index and may change the buffer capacity. - pub fn allocate(&self, device: &wgpu::Device, queue: &wgpu::Queue) -> Id { - self.maybe_expand_to_fit::(device, queue, 1); - let index = self - .len - .fetch_add(T::slab_size(), std::sync::atomic::Ordering::Relaxed); - Id::from(index) - } - - /// Preallocate space for `len` `T` elements, but don't write to - /// the buffer. - /// - /// This can be used to allocate space for a bunch of elements that get written - /// later with [`Self::write_array`]. - /// - /// NOTE: This changes the length of the buffer and may change the capacity. - pub fn allocate_array( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - len: usize, - ) -> Array { - if len == 0 { - return Array::default(); - } - self.maybe_expand_to_fit::(device, queue, len); - let index = self - .len - .fetch_add(T::slab_size() * len, std::sync::atomic::Ordering::Relaxed); - Array::new(index as u32, len as u32) - } - - /// Write into the slab buffer, modifying in place. - /// - /// NOTE: This has no effect on the length of the buffer. - pub fn write( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - id: Id, - data: &T, - ) -> Result<(), SlabError> { - let byte_offset = id.index() * std::mem::size_of::(); - let size = T::slab_size(); - let mut bytes = vec![0u32; size]; - let _ = bytes.write_indexed(data, 0); - let capacity = self.capacity(); - snafu::ensure!( - id.index() + size <= capacity, - CapacitySnafu { - type_is: std::any::type_name::(), - slab_size: T::slab_size(), - index: id.index(), - capacity - } - ); - let encoder = - device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - queue.write_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()), - ); - queue.submit(std::iter::once(encoder.finish())); - Ok(()) - } - - /// Write elements into the slab buffer, modifying in place. - /// - /// NOTE: This has no effect on the length of the buffer. - /// - /// ## Errors - /// Errors if the capacity is exceeded. - pub fn write_array( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - array: Array, - data: &[T], - ) -> Result<(), SlabError> { - snafu::ensure!( - array.len() == data.len(), - ArrayLenSnafu { - type_is: std::any::type_name::(), - array_len: array.len(), - data_len: data.len() - } - ); - let capacity = self.capacity(); - let size = T::slab_size() * array.len(); - snafu::ensure!( - array.starting_index() + size <= capacity, - ArrayCapacitySnafu { - capacity, - type_is: std::any::type_name::(), - elements: array.len(), - slab_size: T::slab_size(), - index: array.at(0).index() - } - ); - let encoder = - device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - let mut u32_data = vec![0u32; size]; - let _ = u32_data.write_indexed_slice(data, 0); - let byte_offset = array.starting_index() * std::mem::size_of::(); - queue.write_buffer( - // UNWRAP: if we can't lock we want to panic - &self.buffer.read().unwrap(), - byte_offset as u64, - bytemuck::cast_slice(&u32_data), - ); - queue.submit(std::iter::once(encoder.finish())); - Ok(()) - } - - /// Read from the slab buffer synchronously. - pub fn block_on_read_raw( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - start: usize, - len: usize, - ) -> Result, SlabError> { - futures_lite::future::block_on(self.read_raw(device, queue, start, len)) - } - - /// Read from the slab buffer. - pub async fn read_raw( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - 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("SlabBuffer::read_raw"), - size: output_buffer_size, - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, - mapped_at_creation: false, - }); - - let mut encoder = - device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - log::trace!( - "copy_buffer_to_buffer byte_offset:{byte_offset}, \ - output_buffer_size:{output_buffer_size}", - ); - encoder.copy_buffer_to_buffer( - // UNWRAP: if we can't lock we want to panic - &self.buffer.read().unwrap(), - byte_offset as u64, - &output_buffer, - 0, - output_buffer_size, - ); - queue.submit(std::iter::once(encoder.finish())); - - let buffer_slice = output_buffer.slice(..); - let (tx, rx) = async_channel::bounded(1); - buffer_slice.map_async(wgpu::MapMode::Read, move |res| tx.try_send(res).unwrap()); - device.poll(wgpu::Maintain::Wait); - rx.recv() - .await - .context(AsyncRecvSnafu)? - .context(AsyncSnafu)?; - let bytes = buffer_slice.get_mapped_range(); - 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( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - t: &T, - ) -> Id { - let id = self.allocate::(device, queue); - // IGNORED: safe because we just allocated the id - let _ = self.write(device, queue, id, t); - id - } - - /// Append a slice to the end of the buffer, resizing if necessary - /// and returning a slabbed array. - pub fn append_array( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - ts: &[T], - ) -> Array { - let array = self.allocate_array::(device, queue, ts.len()); - // IGNORED: safe because we just allocated the array - let _ = self.write_array(device, queue, array, ts); - array - } - - /// Resize the slab buffer. - /// - /// This creates a new buffer and writes the data from the old into the new. - 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( - &buffer, - 0, - &new_buffer, - 0, - (len * std::mem::size_of::()) as u64, - ); - queue.submit(std::iter::once(encoder.finish())); - *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)] -mod test { - use crate::Renderling; - - use super::*; - - #[test] - fn slab_buffer_roundtrip() { - println!("write"); - let _ = env_logger::builder().is_test(true).try_init(); - let r = Renderling::headless(10, 10); - let device = r.get_device(); - let queue = r.get_queue(); - 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()); - - 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_array(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_array(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 4429321d..faea6e4b 100644 --- a/crates/renderling/src/stage.rs +++ b/crates/renderling/src/stage.rs @@ -7,21 +7,18 @@ use std::{ sync::{atomic::AtomicBool, Arc, Mutex, RwLock}, }; +use crabslab::{Array, CpuSlab, GrowableSlab, Id, Slab, SlabItem, WgpuBuffer}; use moongraph::{View, ViewMut}; use renderling_shader::{ - array::Array, debug::DebugMode, - id::Id, - slab::Slabbed, - stage::{light::Light, RenderUnit, StageLegend}, + stage::{light::Light, Camera, RenderUnit, StageLegend}, texture::GpuTexture, }; use snafu::Snafu; use crate::{ - bloom::{BloomFilter, BloomResult}, - Atlas, AtlasError, DepthTexture, Device, HdrSurface, Queue, SceneImage, Skybox, SlabBuffer, - SlabError, + Atlas, AtlasError, AtlasImage, AtlasImageError, DepthTexture, Device, HdrSurface, Queue, + Skybox, SlabError, }; #[cfg(feature = "gltf")] @@ -56,13 +53,13 @@ impl From for StageError { /// A clone of a stage is a reference to the same stage. #[derive(Clone)] pub struct Stage { - pub(crate) slab: SlabBuffer, + pub(crate) slab: Arc>>, pub(crate) atlas: Arc>, pub(crate) skybox: Arc>, + pub(crate) skybox_bindgroup: Arc>>>, pub(crate) pipeline: Arc>>>, pub(crate) skybox_pipeline: Arc>>>, pub(crate) has_skybox: Arc, - pub(crate) bloom: Arc>, pub(crate) has_bloom: Arc, pub(crate) buffers_bindgroup: Arc>>>, pub(crate) textures_bindgroup: Arc>>>, @@ -71,6 +68,45 @@ pub struct Stage { pub(crate) queue: Queue, } +impl Slab for Stage { + fn len(&self) -> usize { + // UNWRAP: if we can't acquire the lock we want to panic. + self.slab.read().unwrap().len() + } + + fn read(&self, id: Id) -> T { + // UNWRAP: if we can't acquire the lock we want to panic. + self.slab.read().unwrap().read(id) + } + + fn write_indexed(&mut self, t: &T, index: usize) -> usize { + // UNWRAP: if we can't acquire the lock we want to panic. + self.slab.write().unwrap().write_indexed(t, index) + } + + fn write_indexed_slice(&mut self, t: &[T], index: usize) -> usize { + // UNWRAP: if we can't acquire the lock we want to panic. + self.slab.write().unwrap().write_indexed_slice(t, index) + } +} + +impl GrowableSlab for Stage { + fn capacity(&self) -> usize { + // UNWRAP: if we can't acquire the lock we want to panic. + self.slab.write().unwrap().capacity() + } + + fn reserve_capacity(&mut self, capacity: usize) { + // UNWRAP: if we can't acquire the lock we want to panic. + self.slab.write().unwrap().reserve_capacity(capacity) + } + + fn increment_len(&mut self, n: usize) -> usize { + // UNWRAP: if we can't acquire the lock we want to panic. + self.slab.write().unwrap().increment_len(n) + } +} + impl Stage { /// Create a new stage. pub fn new(device: Device, queue: Queue) -> Self { @@ -79,14 +115,18 @@ impl Stage { atlas_size: atlas.size, ..Default::default() }; - let s = Self { - slab: SlabBuffer::new(&device, 256), + let mut s = Self { + slab: Arc::new(RwLock::new(CpuSlab::new(WgpuBuffer::new( + device.0.clone(), + queue.0.clone(), + 256, + )))), pipeline: Default::default(), atlas: Arc::new(RwLock::new(atlas)), - skybox: Arc::new(RwLock::new(Skybox::empty(&device, &queue))), + skybox: Arc::new(RwLock::new(Skybox::empty(device.clone(), queue.clone()))), + skybox_bindgroup: Default::default(), skybox_pipeline: Default::default(), has_skybox: Arc::new(AtomicBool::new(false)), - bloom: Arc::new(RwLock::new(BloomFilter::new(&device, &queue, 1, 1))), has_bloom: Arc::new(AtomicBool::new(false)), buffers_bindgroup: Default::default(), textures_bindgroup: Default::default(), @@ -94,97 +134,50 @@ impl Stage { device, queue, }; - let _ = s.append(&legend); + s.append(&legend); s } - /// Allocate some storage for a type on the slab, but don't write it. - pub fn allocate(&self) -> Id { - self.slab.allocate(&self.device, &self.queue) - } - - /// Allocate contiguous storage for `len` elements of a type on the slab, but don't write them. - pub fn allocate_array(&self, len: usize) -> Array { - self.slab.allocate_array(&self.device, &self.queue, len) - } - - /// Write an object to the slab. - pub fn write(&self, id: Id, object: &T) -> Result<(), SlabError> { - self.slab.write(&self.device, &self.queue, id, object)?; - Ok(()) - } - - /// Write many objects to the slab. - pub fn write_array( - &self, - array: Array, - objects: &[T], - ) -> Result<(), SlabError> { - let () = self - .slab - .write_array(&self.device, &self.queue, array, objects)?; - Ok(()) - } - - /// Add an object to the slab and return its ID. - pub fn append(&self, object: &T) -> Id { - self.slab.append(&self.device, &self.queue, object) - } - - /// Add a slice of objects to the slab and return an [`Array`]. - pub fn append_array(&self, objects: &[T]) -> Array { - self.slab.append_array(&self.device, &self.queue, objects) - } - /// Set the debug mode. - pub fn set_debug_mode(&self, debug_mode: DebugMode) { + pub fn set_debug_mode(&mut self, debug_mode: DebugMode) { let id = Id::::from(StageLegend::offset_of_debug_mode()); - // UNWRAP: safe because the debug mode offset is guaranteed to be valid. - self.slab - .write(&self.device, &self.queue, id, &debug_mode) - .unwrap(); + self.write(id, &debug_mode); } /// Set the debug mode. - pub fn with_debug_mode(self, debug_mode: DebugMode) -> Self { + pub fn with_debug_mode(mut self, debug_mode: DebugMode) -> Self { self.set_debug_mode(debug_mode); self } /// Set whether the stage uses lighting. - pub fn set_has_lighting(&self, use_lighting: bool) { + pub fn set_has_lighting(&mut self, use_lighting: bool) { let id = Id::::from(StageLegend::offset_of_has_lighting()); - // UNWRAP: safe because the has lighting offset is guaranteed to be valid. - self.slab - .write(&self.device, &self.queue, id, &use_lighting) - .unwrap(); + self.write(id, &use_lighting); } /// Set whether the stage uses lighting. - pub fn with_lighting(self, use_lighting: bool) -> Self { + pub fn with_lighting(mut self, use_lighting: bool) -> Self { self.set_has_lighting(use_lighting); self } /// Set the lights to use for shading. - pub fn set_lights(&self, lights: Array) { + pub fn set_lights(&mut self, lights: Array) { let id = Id::>::from(StageLegend::offset_of_light_array()); - // UNWRAP: safe because light array offset is guaranteed to be valid. - self.slab - .write(&self.device, &self.queue, id, &lights) - .unwrap(); + self.write(id, &lights); } /// Set the images to use for the atlas. /// - /// Resets the atlas, packing it with the given images and returning a vector of the textures - /// ready to be staged. + /// Resets the atlas, packing it with the given images and returning a + /// vector of the textures ready to be staged. /// /// ## WARNING /// This invalidates any currently staged `GpuTextures`. pub fn set_images( - &self, - images: impl IntoIterator, + &mut self, + images: impl IntoIterator, ) -> Result, StageError> { // UNWRAP: if we can't write the atlas we want to panic let mut atlas = self.atlas.write().unwrap(); @@ -194,7 +187,8 @@ impl Stage { let _ = self.textures_bindgroup.lock().unwrap().take(); // The atlas size must be reset let size_id = Id::::from(StageLegend::offset_of_atlas_size()); - self.write(size_id, &atlas.size)?; + // UNWRAP: if we can't write to the stage legend we want to panic + self.slab.write().unwrap().write(size_id, &atlas.size); let textures = atlas .frames() @@ -313,83 +307,39 @@ impl Stage { } /// Return the skybox render pipeline, creating it if necessary. - pub fn get_skybox_pipeline(&self) -> Arc { - fn create_skybox_render_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline { - log::trace!("creating stage's skybox render pipeline"); - let vertex_shader = device - .create_shader_module(wgpu::include_spirv!("linkage/skybox-slabbed_vertex.spv")); - let fragment_shader = device.create_shader_module(wgpu::include_spirv!( - "linkage/skybox-stage_skybox_cubemap.spv" - )); - let stage_slab_buffers_layout = Stage::buffers_bindgroup_layout(&device); - let textures_layout = Stage::textures_bindgroup_layout(&device); - let label = Some("stage skybox"); - let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label, - bind_group_layouts: &[&stage_slab_buffers_layout, &textures_layout], - push_constant_ranges: &[], - }); - - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("skybox pipeline"), - layout: Some(&layout), - vertex: wgpu::VertexState { - module: &vertex_shader, - entry_point: "skybox::slabbed_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::LessEqual, - 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, - // TODO: remove renderling_shader::skybox::fragment_cubemap after porting - // to GLTF - entry_point: "skybox::stage_skybox_cubemap", - targets: &[ - Some(wgpu::ColorTargetState { - format: crate::hdr::HdrSurface::TEXTURE_FORMAT, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - }), - Some(wgpu::ColorTargetState { - format: crate::hdr::HdrSurface::TEXTURE_FORMAT, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - }), - ], - }), - multiview: None, - }) - } - + pub fn get_skybox_pipeline_and_bindgroup( + &self, + ) -> (Arc, Arc) { // UNWRAP: safe because we're only ever called from the render thread. let mut pipeline = self.skybox_pipeline.write().unwrap(); - if let Some(pipeline) = pipeline.as_ref() { + let pipeline = if let Some(pipeline) = pipeline.as_ref() { pipeline.clone() } else { - let p = Arc::new(create_skybox_render_pipeline(&self.device)); + let p = Arc::new( + crate::skybox::create_skybox_render_pipeline( + &self.device, + crate::hdr::HdrSurface::TEXTURE_FORMAT, + ) + .0, + ); *pipeline = Some(p.clone()); p - } + }; + // UNWRAP: safe because we're only ever called from the render thread. + let mut bindgroup = self.skybox_bindgroup.lock().unwrap(); + let bindgroup = if let Some(bindgroup) = bindgroup.as_ref() { + bindgroup.clone() + } else { + let slab = self.slab.read().unwrap(); + let bg = Arc::new(crate::skybox::create_skybox_bindgroup( + &self.device, + slab.as_ref().get_buffer(), + &self.skybox.read().unwrap().environment_cubemap, + )); + *bindgroup = Some(bg.clone()); + bg + }; + (pipeline, bindgroup) } /// Return the main render pipeline, creating it if necessary. @@ -440,18 +390,11 @@ impl Stage { fragment: Some(wgpu::FragmentState { module: &fragment_shader, entry_point: "stage::gltf_fragment", - targets: &[ - Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba16Float, - 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, - }), - ], + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba16Float, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], }), multiview: None, }); @@ -470,34 +413,32 @@ impl Stage { } pub fn get_slab_buffers_bindgroup(&self) -> Arc { - fn create_slab_buffers_bindgroup( - device: &wgpu::Device, - pipeline: &wgpu::RenderPipeline, - stage_slab: &SlabBuffer, - ) -> wgpu::BindGroup { - let label = Some("stage slab buffer"); - 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(), - }], - }); - stage_slab_buffers_bindgroup - } - // UNWRAP: safe because we're only ever called from the render thread. let mut bindgroup = self.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.slab, - )); + let b = Arc::new({ + let device: &wgpu::Device = &self.device; + let pipeline: &wgpu::RenderPipeline = &self.get_pipeline(); + let label = Some("stage slab buffer"); + 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: self + .slab + .read() + .unwrap() + .as_ref() + .get_buffer() + .as_entire_binding(), + }], + }); + stage_slab_buffers_bindgroup + }); *bindgroup = Some(b.clone()); b } @@ -590,8 +531,8 @@ impl Stage { } /// Draw the [`RenderUnit`] each frame, and immediately return its `Id`. - pub fn draw_unit(&self, unit: &RenderUnit) -> Id { - let id = self.slab.append(&self.device, &self.queue, unit); + pub fn draw_unit(&mut self, unit: &RenderUnit) -> Id { + let id = self.append(unit); let draw = DrawUnit { id, vertex_count: unit.vertex_count, @@ -660,8 +601,8 @@ impl Stage { use crate::{ frame::{copy_frame_to_post, create_frame, present}, graph::{graph, Graph}, - hdr::{clear_surface_hdr_and_depth, create_hdr_render_surface, hdr_surface_update}, - scene::tonemapping, + hdr::{clear_surface_hdr_and_depth, create_hdr_render_surface}, + tonemapping, }; let (hdr_surface,) = r.graph.visit(create_hdr_render_surface).unwrap().unwrap(); @@ -670,11 +611,7 @@ impl Stage { // pre-render r.graph - .add_subgraph(graph!( - create_frame, - clear_surface_hdr_and_depth, - hdr_surface_update - )) + .add_subgraph(graph!(create_frame, clear_surface_hdr_and_depth)) .add_barrier(); // render @@ -699,7 +636,8 @@ impl Stage { /// This is primarily used for debugging. /// /// ## Panics - /// Panics if the pixels read from the GPU cannot be converted into an `RgbaImage`. + /// Panics if the pixels read from the GPU cannot be converted into an + /// `RgbaImage`. pub fn read_atlas_image(&self) -> image::RgbaImage { // UNWRAP: if we can't acquire the lock we want to panic. self.atlas @@ -710,12 +648,32 @@ impl Stage { /// Read all the data from the stage. /// - /// This blocks until the GPU buffer is mappable, and then copies the data into a vector. + /// This blocks until the GPU buffer is mappable, and then copies the data + /// into a vector. /// /// This is primarily used for debugging. pub fn read_slab(&self) -> Result, SlabError> { + // UNWRAP: if we can't acquire the lock we want to panic. self.slab - .block_on_read_raw(&self.device, &self.queue, 0, self.slab.len()) + .read() + .unwrap() + .as_ref() + .block_on_read_raw(0, self.len()) + } + + + pub fn new_skybox_from_path( + &self, + path: impl AsRef, + camera: Id, + ) -> Result { + let hdr = AtlasImage::from_hdr_path(path)?; + Ok(Skybox::new( + self.device.clone(), + self.queue.clone(), + hdr, + camera, + )) } } @@ -727,7 +685,8 @@ pub struct DrawUnit { pub visible: bool, } -/// Provides a way to communicate with the stage about how you'd like your objects drawn. +/// Provides a way to communicate with the stage about how you'd like your +/// objects drawn. pub(crate) enum StageDrawStrategy { Direct(Vec), } @@ -735,22 +694,24 @@ pub(crate) enum StageDrawStrategy { /// Render the stage. pub fn stage_render( (stage, hdr_frame, depth): (ViewMut, View, View), -) -> Result<(BloomResult,), SlabError> { +) -> Result<(), SlabError> { 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 may_skybox_pipeline = if stage.has_skybox.load(std::sync::atomic::Ordering::Relaxed) { - Some(stage.get_skybox_pipeline()) - } else { - None - }; - let mut may_bloom_filter = if stage.has_bloom.load(std::sync::atomic::Ordering::Relaxed) { - // UNWRAP: if we can't acquire the lock we want to panic. - Some(stage.bloom.write().unwrap()) + let has_skybox = stage.has_skybox.load(std::sync::atomic::Ordering::Relaxed); + let may_skybox_pipeline_and_bindgroup = if has_skybox { + Some(stage.get_skybox_pipeline_and_bindgroup()) } else { None }; + //let mut may_bloom_filter = if + // stage.has_bloom.load(std::sync::atomic::Ordering::Relaxed) { // UNWRAP: + // if we can't acquire the lock we want to panic. Some(stage.bloom. + // write().unwrap()) + //} else { + // None + //}; // UNWRAP: if we can't read we want to panic. let draws = stage.draws.read().unwrap(); @@ -781,23 +742,20 @@ pub fn stage_render( .draw(0..unit.vertex_count, unit.id.inner()..unit.id.inner() + 1); } } - } //render_pass.multi_draw_indirect(&indirect_buffer, 0, stage.number_of_indirect_draws()); + } /* render_pass.multi_draw_indirect(&indirect_buffer, 0, + * stage.number_of_indirect_draws()); */ } - if let Some(pipeline) = may_skybox_pipeline.as_ref() { + if let Some((pipeline, bindgroup)) = may_skybox_pipeline_and_bindgroup.as_ref() { log::trace!("rendering skybox"); // UNWRAP: if we can't acquire the lock we want to panic. let skybox = stage.skybox.read().unwrap(); render_pass.set_pipeline(pipeline); + render_pass.set_bind_group(0, bindgroup, &[]); render_pass.draw(0..36, skybox.camera.inner()..skybox.camera.inner() + 1); } } stage.queue.submit(std::iter::once(encoder.finish())); - let bloom_result = BloomResult( - may_bloom_filter - .as_mut() - .map(|bloom| bloom.run(&stage.device, &stage.queue, &hdr_frame)), - ); - Ok((bloom_result,)) + Ok(()) } diff --git a/crates/renderling/src/stage/gltf_support.rs b/crates/renderling/src/stage/gltf_support.rs index f298fe82..740a0f7a 100644 --- a/crates/renderling/src/stage/gltf_support.rs +++ b/crates/renderling/src/stage/gltf_support.rs @@ -7,7 +7,7 @@ use crate::{ stage::{Camera, LightingModel}, texture::{GpuTexture, TextureAddressMode, TextureModes}, }, - SceneImage, + AtlasImage, }; use glam::{Quat, Vec2, Vec3, Vec4}; use renderling_shader::stage::Vertex; @@ -49,11 +49,11 @@ pub enum StageGltfError { MissingCamera { index: usize }, #[snafu(display("{source}"))] - Slab { source: crate::slab::SlabError }, + Slab { source: crabslab::SlabError }, } -impl From for StageGltfError { - fn from(source: crate::slab::SlabError) -> Self { +impl From for StageGltfError { + fn from(source: crabslab::SlabError) -> Self { Self::Slab { source } } } @@ -118,7 +118,7 @@ pub fn make_accessor(accessor: gltf::Accessor<'_>, views: &Array impl Stage { pub fn load_gltf_document_from_path( - &self, + &mut self, path: impl AsRef, ) -> Result<(gltf::Document, GltfDocument), StageGltfError> { let (document, buffers, images) = gltf::import(path)?; @@ -127,7 +127,7 @@ impl Stage { } pub fn load_gltf_document( - &self, + &mut self, document: &gltf::Document, buffer_data: Vec, images: Vec, @@ -137,7 +137,7 @@ impl Stage { for (i, buffer) in buffer_data.iter().enumerate() { let slice: &[u32] = bytemuck::cast_slice(&buffer); let buffer = self.append_array(slice); - self.write(buffers.at(i), &GltfBuffer(buffer))?; + self.write(buffers.at(i), &GltfBuffer(buffer)); } log::trace!("Loading views into the GPU"); @@ -154,7 +154,7 @@ impl Stage { length, stride, }; - self.write(id, &gltf_view)?; + self.write(id, &gltf_view); } log::trace!("Loading accessors into the GPU"); @@ -203,17 +203,18 @@ impl Stage { .collect::>(); let cameras = self.append_array(&cameras); - // We need the (re)packing of the atlas before we marshal the images into the GPU - // because we need their frames for textures and materials, but we need to know - // if the materials require us to apply a linear transfer. So we'll get the preview - // repacking first, then update the frames in the textures. + // We need the (re)packing of the atlas before we marshal the images into the + // GPU because we need their frames for textures and materials, but we + // need to know if the materials require us to apply a linear transfer. + // So we'll get the preview repacking first, then update the frames in + // the textures. let (mut repacking, atlas_offset) = { // UNWRAP: if we can't lock the atlas, we want to panic. let atlas = self.atlas.read().unwrap(); let atlas_offset = atlas.rects.len(); ( atlas - .repack_preview(&self.device, images.into_iter().map(SceneImage::from)) + .repack_preview(&self.device, images.into_iter().map(AtlasImage::from)) .context(AtlasSnafu)?, atlas_offset, ) @@ -253,7 +254,7 @@ impl Stage { }; let texture_id = textures.at(i); log::trace!(" texture {i} {texture_id:?}: {texture:#?}"); - self.write(texture_id, &texture)?; + self.write(texture_id, &texture); } log::trace!("Creating materials"); @@ -406,7 +407,7 @@ impl Stage { } }; log::trace!(" material {material_id:?}: {material:#?}",); - self.write(material_id, &material)?; + self.write(material_id, &material); } let number_of_new_images = repacking.new_images_len(); @@ -424,7 +425,7 @@ impl Stage { let _ = self.textures_bindgroup.lock().unwrap().take(); // The atlas size must be reset let size_id = StageLegend::offset_of_atlas_size().into(); - self.write(size_id, &size)?; + self.slab.write().unwrap().write(size_id, &size); } fn log_accessor(gltf_accessor: gltf::Accessor<'_>) { @@ -733,7 +734,7 @@ impl Stage { weights, }; log::trace!(" writing primitive {id:?}:\n{prim:#?}"); - self.write(id, &prim)?; + self.write(id, &prim); } let weights = mesh.weights().unwrap_or(&[]); let weights = self.append_array(weights); @@ -743,7 +744,7 @@ impl Stage { primitives, weights, }, - )?; + ); } log::trace!("Loading lights"); let lights_array = self.allocate_array::( @@ -777,7 +778,7 @@ impl Stage { intensity, kind, }, - )?; + ); } } let lights = lights_array; @@ -834,7 +835,7 @@ impl Stage { light, skin, }, - )?; + ); } log::trace!("Loading skins"); @@ -888,7 +889,7 @@ impl Stage { let mut stored_samplers = vec![]; for (i, sampler) in animation.samplers().enumerate() { let sampler = create_sampler(accessors, sampler); - self.write(samplers.at(i), &sampler)?; + self.write(samplers.at(i), &sampler); // Store it later so we can figure out the index of the sampler // used by the channel. // @@ -915,12 +916,12 @@ impl Stage { .position(|s| s == &sampler) .context(MissingSamplerSnafu)?; let sampler = samplers.at(index); - self.write(channels.at(i), &GltfChannel { target, sampler })?; + self.write(channels.at(i), &GltfChannel { target, sampler }); } self.write( animations.at(animation.index()), &GltfAnimation { channels, samplers }, - )?; + ); } log::trace!("Loading scenes"); @@ -931,7 +932,7 @@ impl Stage { .map(|node| nodes.at(node.index())) .collect::>(); let nodes = self.append_array(&nodes); - self.write(scenes.at(scene.index()), &GltfScene { nodes })?; + self.write(scenes.at(scene.index()), &GltfScene { nodes }); } log::trace!("Done loading gltf"); @@ -1005,7 +1006,7 @@ impl Stage { /// Draw the given `gltf::Node` with the given `Camera`. /// `parents` is a list of the parent nodes of the given node. fn draw_gltf_node_with<'a>( - &self, + &mut self, gpu_doc: &GltfDocument, camera_id: Id, node: gltf::Node<'a>, @@ -1022,8 +1023,10 @@ impl Stage { //let range = light.range().unwrap_or(f32::MAX); //let intensity = light.intensity(); //match light.kind() { - // gltf::khr_lights_punctual::Kind::Directional => GltfLightKind::Directional, - // gltf::khr_lights_punctual::Kind::Point => GltfLightKind::Point, + // gltf::khr_lights_punctual::Kind::Directional => + // GltfLightKind::Directional, + // gltf::khr_lights_punctual::Kind::Point => + // GltfLightKind::Point, // gltf::khr_lights_punctual::Kind::Spot { // inner_cone_angle, // outer_cone_angle, @@ -1072,10 +1075,10 @@ impl Stage { units } - /// Draw the given [`gltf::Node`] using the given [`Camera`] and return the ids of the - /// render units that were created. + /// Draw the given [`gltf::Node`] using the given [`Camera`] and return the + /// ids of the render units that were created. pub fn draw_gltf_node( - &self, + &mut self, gpu_doc: &GltfDocument, camera_id: Id, node: gltf::Node<'_>, @@ -1083,10 +1086,10 @@ impl Stage { self.draw_gltf_node_with(gpu_doc, camera_id, node, vec![]) } - /// Draw the given [`gltf::Scene`] using the given [`Camera`] and return the ids of the - /// render units that were created. + /// Draw the given [`gltf::Scene`] using the given [`Camera`] and return the + /// ids of the render units that were created. pub fn draw_gltf_scene( - &self, + &mut self, gpu_doc: &GltfDocument, camera_id: Id, scene: gltf::Scene<'_>, @@ -1103,7 +1106,7 @@ impl Stage { /// ## Note /// This does **not** generate tangents or normals. pub fn new_primitive( - &self, + &mut self, vertices: impl IntoIterator, indices: impl IntoIterator, material: Id, @@ -1205,7 +1208,8 @@ impl Stage { let tangents = self.append(&GltfAccessor { size: 4 * 4, // 4 tangent components * 4 bytes each view, - offset: 16 * 4, // (3 + 1) position + 4 color + 4 uv + (3 + 1) normal components * 4 bytes each + offset: 16 * 4, /* (3 + 1) position + 4 color + 4 uv + (3 + 1) normal components * 4 + * bytes each */ count: vertex_count as u32, data_type: DataType::F32, dimensions: Dimensions::Vec4, @@ -1253,7 +1257,7 @@ impl Stage { /// [`gltf::Document`]. /// /// This is useful if you have non-GLTF assets that you want to render. - pub fn new_mesh(&self) -> GltfMeshBuilder { + pub fn new_mesh(&mut self) -> GltfMeshBuilder { GltfMeshBuilder::new(self) } } @@ -1263,12 +1267,12 @@ impl Stage { /// /// This is useful if you have non-GLTF assets that you want to render. pub struct GltfMeshBuilder<'a> { - stage: &'a Stage, + stage: &'a mut Stage, primitives: Vec, } impl<'a> GltfMeshBuilder<'a> { - pub fn new(stage: &'a Stage) -> Self { + pub fn new(stage: &'a mut Stage) -> Self { Self { stage, primitives: vec![], @@ -1310,18 +1314,14 @@ impl<'a> GltfMeshBuilder<'a> { /// /// This is useful if you have non-GLTF assets that you want to render. pub struct GltfDocumentBuilder<'a> { - stage: &'a Stage, + stage: &'a mut Stage, } impl<'a> GltfDocumentBuilder<'a> { - pub fn new(stage: &'a Stage) -> Self { + pub fn new(stage: &'a mut Stage) -> Self { Self { stage } } - pub fn new_mesh(&self) -> GltfMeshBuilder<'a> { - GltfMeshBuilder::new(self.stage) - } - pub fn build(self) -> GltfDocument { let accessors = Array::default(); let animations = Array::default(); @@ -1354,19 +1354,17 @@ impl<'a> GltfDocumentBuilder<'a> { #[cfg(test)] mod test { - use glam::{Vec2, Vec3, Vec4, Vec4Swizzles}; - use renderling_shader::stage::{GpuConstants, GpuEntity}; - use crate::{ shader::{ - array::Array, gltf::*, pbr::PbrMaterial, - slab::Slab, stage::{Camera, LightingModel, RenderUnit, Transform, Vertex}, }, - Id, Renderling, Stage, + Renderling, Stage, }; + use crabslab::{Array, GrowableSlab, Id, Slab}; + use glam::{Vec2, Vec3, Vec4, Vec4Swizzles}; + use renderling_shader::stage::{GpuConstants, GpuEntity}; #[test] fn get_vertex_count_primitive_sanity() { @@ -1450,7 +1448,7 @@ mod test { let projection = crate::camera::perspective(100.0, 50.0); let position = Vec3::new(1.0, 0.5, 1.5); let view = crate::camera::look_at(position, Vec3::new(1.0, 0.5, 0.0), Vec3::Y); - let stage = Stage::new(device.clone(), queue.clone()).with_lighting(false); + let mut stage = Stage::new(device.clone(), queue.clone()).with_lighting(false); stage.configure_graph(&mut r, true); let gpu_doc = stage .load_gltf_document(&document, buffers.clone(), images) @@ -1476,7 +1474,7 @@ mod test { let mut r = Renderling::headless(20, 20).with_background_color(Vec3::splat(0.0).extend(1.0)); let (device, queue) = r.get_device_and_queue_owned(); - let stage = Stage::new(device, queue).with_lighting(false); + let mut stage = Stage::new(device, queue).with_lighting(false); stage.configure_graph(&mut r, true); let (document, buffers, images) = gltf::import("../../gltf/gltfTutorial_003_MinimalGltfFile.gltf").unwrap(); @@ -1504,7 +1502,7 @@ mod test { // child primitive's geometry correctly. fn render_unit_transforms_primitive_geometry() { let mut r = Renderling::headless(50, 50).with_background_color(Vec4::splat(1.0)); - let stage = r.new_stage().with_lighting(false); + let mut stage = r.new_stage().with_lighting(false); stage.configure_graph(&mut r, true); let (projection, view) = crate::camera::default_ortho2d(50.0, 50.0); let camera = stage.append(&Camera::new(projection, view)); @@ -1527,8 +1525,9 @@ mod test { .with_color(white), ]; let primitive = stage.new_primitive(vertices, [0, 3, 2, 0, 2, 1], Id::NONE); + let primitives = stage.append_array(&[primitive]); let mesh = stage.append(&GltfMesh { - primitives: stage.append_array(&[primitive]), + primitives, ..Default::default() }); let node = stage.append(&GltfNode { @@ -1547,7 +1546,7 @@ mod test { node_path, ..Default::default() }); - let img = r.render_image().unwrap(); + let img = r.render_linear_image().unwrap(); img_diff::assert_img_eq("gltf/render_unit_transforms_primitive_geometry.png", img); } @@ -1558,7 +1557,7 @@ mod test { fn gltf_images() { let mut r = Renderling::headless(100, 100).with_background_color(Vec4::splat(1.0)); let (device, queue) = r.get_device_and_queue_owned(); - let stage = Stage::new(device.clone(), queue.clone()).with_lighting(false); + let mut stage = Stage::new(device.clone(), queue.clone()).with_lighting(false); stage.configure_graph(&mut r, true); let (document, buffers, images) = gltf::import("../../gltf/cheetah_cone.glb").unwrap(); let gpu_doc = stage @@ -1575,29 +1574,28 @@ mod test { ..Default::default() }); println!("material_id: {:#?}", material_id); - let mesh = stage.append( - &stage - .new_mesh() - .with_primitive( - [ - Vertex::default() - .with_position([0.0, 0.0, 0.0]) - .with_uv0([0.0, 0.0]), - Vertex::default() - .with_position([1.0, 0.0, 0.0]) - .with_uv0([1.0, 0.0]), - Vertex::default() - .with_position([1.0, 1.0, 0.0]) - .with_uv0([1.0, 1.0]), - Vertex::default() - .with_position([0.0, 1.0, 0.0]) - .with_uv0([0.0, 1.0]), - ], - [0, 3, 2, 0, 2, 1], - material_id, - ) - .build(), - ); + let mesh = stage + .new_mesh() + .with_primitive( + [ + Vertex::default() + .with_position([0.0, 0.0, 0.0]) + .with_uv0([0.0, 0.0]), + Vertex::default() + .with_position([1.0, 0.0, 0.0]) + .with_uv0([1.0, 0.0]), + Vertex::default() + .with_position([1.0, 1.0, 0.0]) + .with_uv0([1.0, 1.0]), + Vertex::default() + .with_position([0.0, 1.0, 0.0]) + .with_uv0([0.0, 1.0]), + ], + [0, 3, 2, 0, 2, 1], + material_id, + ) + .build(); + let mesh = stage.append(&mesh); let node = stage.append(&GltfNode { mesh, ..Default::default() @@ -1618,7 +1616,7 @@ mod test { primitive_index: 0, }); - let img = r.render_image().unwrap(); + let img = r.render_linear_image().unwrap(); img_diff::assert_img_eq("gltf_images.png", img); } @@ -1628,7 +1626,7 @@ mod test { let mut r = Renderling::headless(size, size).with_background_color(Vec3::splat(0.0).extend(1.0)); let (device, queue) = r.get_device_and_queue_owned(); - let stage = Stage::new(device.clone(), queue.clone()) + let mut stage = Stage::new(device.clone(), queue.clone()) // There are no lights in the scene and the material isn't marked as "unlit", so // let's force it to be unlit. .with_lighting(false); @@ -1647,24 +1645,29 @@ mod test { img_diff::assert_img_eq("gltf_simple_texture.png", img); } - #[test] - fn normal_mapping_brick_sphere() { - let size = 600; - let mut r = - Renderling::headless(size, size).with_background_color(Vec3::splat(1.0).extend(1.0)); - let stage = r.new_stage().with_lighting(true).with_bloom(true); - stage.configure_graph(&mut r, true); - let (cpu_doc, gpu_doc) = stage - .load_gltf_document_from_path("../../gltf/red_brick_03_1k.glb") - .unwrap(); - let camera = stage.create_camera_from_gltf(&cpu_doc, 0).unwrap(); - let camera_id = stage.append(&camera); - let _unit_ids = - stage.draw_gltf_scene(&gpu_doc, camera_id, cpu_doc.default_scene().unwrap()); - - let img = r.render_image().unwrap(); - img_diff::assert_img_eq("gltf_normal_mapping_brick_sphere.png", img); - } + // This can be uncommented when we support lighting from GLTF files + //#[test] + //// Demonstrates how to load and render a gltf file containing lighting and a + //// normal map. + //fn normal_mapping_brick_sphere() { + // let size = 600; + // let mut r = + // Renderling::headless(size, + // size).with_background_color(Vec3::splat(1.0).extend(1.0)); let mut + // stage = r.new_stage().with_lighting(true).with_bloom(true); + // stage.configure_graph(&mut r, true); + // let (cpu_doc, gpu_doc) = stage + // .load_gltf_document_from_path("../../gltf/red_brick_03_1k.glb") + // .unwrap(); + // let camera = stage.create_camera_from_gltf(&cpu_doc, 0).unwrap(); + // let camera_id = stage.append(&camera); + // let _unit_ids = + // stage.draw_gltf_scene(&gpu_doc, camera_id, + // cpu_doc.default_scene().unwrap()); + + // let img = r.render_image().unwrap(); + // img_diff::assert_img_eq("gltf_normal_mapping_brick_sphere.png", img); + //} #[test] // Demonstrates how to generate a mesh primitive on the CPU, long hand. @@ -1672,8 +1675,8 @@ mod test { let size = 100; let mut r = Renderling::headless(size, size).with_background_color(Vec3::splat(0.0).extend(1.0)); - let (device, queue) = r.get_device_and_queue_owned(); - let stage = Stage::new(device.clone(), queue.clone()) + let mut stage = r + .new_stage() // There are no lights in the scene and the material isn't marked as "unlit", so // let's force it to be unlit. .with_lighting(false); @@ -1773,7 +1776,7 @@ mod test { let invocation = VertexInvocation::invoke(unit_id.inner(), 0, &data); println!("invoctaion: {invocation:#?}"); - let img = r.render_image().unwrap(); + let img = r.render_linear_image().unwrap(); img_diff::assert_img_eq("gltf/cmy_tri.png", img); } diff --git a/crates/renderling/src/texture.rs b/crates/renderling/src/texture.rs index 4be840df..785fe8a0 100644 --- a/crates/renderling/src/texture.rs +++ b/crates/renderling/src/texture.rs @@ -552,8 +552,8 @@ impl Texture { /// Generate `mipmap_levels - 1` mipmaps for the given texture. /// /// ## Note - /// Ensure that `self` only has one mip level. If not it will try to sample from - /// an empty mip. + /// Ensure that `self` only has one mip level. If not it will try to sample + /// from an empty mip. pub fn generate_mips( &mut self, device: &wgpu::Device, @@ -805,17 +805,17 @@ impl CopiedTextureBuffer { Ok(image::DynamicImage::from(img_buffer)) } - /// Convert the post render buffer into an internal-format [`SceneImage`]. + /// Convert the post render buffer into an internal-format [`AtlasImage`]. pub fn into_scene_image( self, device: &wgpu::Device, - ) -> Result { + ) -> Result { let pixels = self.pixels(device); - let img = crate::SceneImage { + let img = crate::AtlasImage { pixels, width: self.dimensions.width as u32, height: self.dimensions.height as u32, - format: crate::SceneImageFormat::from_wgpu_texture_format(self.format) + format: crate::AtlasImageFormat::from_wgpu_texture_format(self.format) .context(UnsupportedFormatSnafu)?, apply_linear_transfer: false, }; @@ -826,11 +826,13 @@ impl CopiedTextureBuffer { /// /// Ensures that the pixels are in a linear color space by applying the /// linear transfer if the texture this buffer was copied from was sRGB. - pub fn into_rgba(self, device: &wgpu::Device) -> Result { + pub fn into_linear_rgba(self, device: &wgpu::Device) -> Result { let format = self.format; let mut img_buffer = self.into_image::>(device)?.into_rgba8(); if format.is_srgb() { - log::trace!("converting applying linear transfer to srgb pixels"); + log::trace!( + "converting by applying linear transfer fn to srgb pixels (sRGB -> linear)" + ); // Convert back to linear img_buffer.pixels_mut().for_each(|p| { crate::color::linear_xfer_u8(&mut p.0[0]); @@ -842,6 +844,29 @@ impl CopiedTextureBuffer { Ok(img_buffer) } + + /// Convert the post render buffer into an RgbaImage. + /// + /// Ensures that the pixels are in a linear color space by applying the + /// linear transfer if the texture this buffer was copied from was sRGB. + pub fn into_srgba(self, device: &wgpu::Device) -> Result { + let format = self.format; + let mut img_buffer = self.into_image::>(device)?.into_rgba8(); + if !format.is_srgb() { + log::trace!( + "converting by applying opto transfer fn to linear pixels (linear -> sRGB)" + ); + // Convert back to linear + img_buffer.pixels_mut().for_each(|p| { + crate::color::opto_xfer_u8(&mut p.0[0]); + crate::color::opto_xfer_u8(&mut p.0[1]); + crate::color::opto_xfer_u8(&mut p.0[2]); + crate::color::opto_xfer_u8(&mut p.0[3]); + }); + } + + Ok(img_buffer) + } } #[cfg(test)] diff --git a/crates/renderling/src/tonemapping.rs b/crates/renderling/src/tonemapping.rs new file mode 100644 index 00000000..f246a16c --- /dev/null +++ b/crates/renderling/src/tonemapping.rs @@ -0,0 +1,37 @@ +use moongraph::{GraphError, Move, View}; + +use crate::{frame::FrameTextureView, Device, HdrSurface, Queue}; + +/// Conducts the HDR tone mapping, writing the HDR surface texture to the (most +/// likely) sRGB window surface. +pub fn tonemapping( + (device, queue, frame, hdr_frame): ( + View, + View, + View, + View, + ), +) -> Result<(), GraphError> { + log::trace!("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, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, + })], + depth_stencil_attachment: None, + }); + render_pass.set_pipeline(&hdr_frame.tonemapping_pipeline); + render_pass.set_bind_group(0, &hdr_frame.bindgroup, &[]); + render_pass.draw(0..6, 0..1); + drop(render_pass); + + queue.submit(std::iter::once(encoder.finish())); + Ok(()) +} diff --git a/crates/renderling/src/ui.rs b/crates/renderling/src/ui.rs index f1d94df8..3039179f 100644 --- a/crates/renderling/src/ui.rs +++ b/crates/renderling/src/ui.rs @@ -7,6 +7,7 @@ use std::{ sync::Arc, }; +use crabslab::{CpuSlab, Slab, SlabItem, WgpuBuffer}; use glam::{UVec2, Vec2, Vec4}; use snafu::prelude::*; @@ -345,7 +346,8 @@ impl<'a> UiDrawObjectBuilder<'a> { pub struct UiScene { device: Arc, - constants: Uniform, + /// Slab containing `UiConstants`. + slab: CpuSlab, _default_texture: Texture, default_texture_bindgroup: wgpu::BindGroup, } @@ -353,22 +355,26 @@ pub struct UiScene { impl UiScene { pub fn new( device: Arc, - queue: &wgpu::Queue, + queue: Arc, canvas_size: UVec2, camera_translation: Vec2, ) -> Self { - let constants = Uniform::new( - &device, - UiConstants { + let buffer = WgpuBuffer::new( + device.clone(), + UiConstants::slab_size(), + wgpu::BufferUsages::empty(), + ); + let mut slab = CpuSlab::new(buffer); + slab.write( + 0u32.into(), + &UiConstants { canvas_size, camera_translation, }, - wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - wgpu::ShaderStages::VERTEX, ); let texture = Texture::new( &device, - queue, + &queue, Some("UiScene.default_texture"), None, 4, diff --git a/shaders/shader-crate/Cargo.toml b/shaders/shader-crate/Cargo.toml deleted file mode 100644 index 9b375b65..00000000 --- a/shaders/shader-crate/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "shader-crate" -version = "0.1.0" -authors = ["Schell Carl Scivally "] -edition = "2021" -license = "MIT OR Apache-2.0" -keywords = ["game", "graphics", "shader", "rendering"] -categories = ["rendering", "game-development", "graphics"] -description = "Shaders for the renderling project" - -[lib] -crate-type = ["dylib"] - -[dependencies] -renderling-shader = { path = "../../crates/renderling-shader" } -spirv-std = "0.9" -glam = { version = "0.24.2", default-features = false, features = ["libm"]} - -# Enable incremental by default in release mode. -[profile.release] -incremental = true -# HACK(eddyb) this is the default but without explicitly specifying it, Cargo -# will treat the identical settings in `[profile.release.build-override]` below -# as different sets of `rustc` flags and will not reuse artifacts between them. -codegen-units = 256 - -# Compile build-dependencies in release mode with the same settings -# as regular dependencies (including the incremental enabled above). -[profile.release.build-override] -opt-level = 3 -incremental = true -codegen-units = 256 \ No newline at end of file diff --git a/shaders/shader-crate/src/lib.rs b/shaders/shader-crate/src/lib.rs deleted file mode 100644 index 0fb9a7a2..00000000 --- a/shaders/shader-crate/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Shader entry points. -#![no_std] -#![feature(lang_items)] -pub use renderling_shader::*; diff --git a/shaders/src/main.rs b/shaders/src/main.rs index e30416ee..aaea227d 100644 --- a/shaders/src/main.rs +++ b/shaders/src/main.rs @@ -10,6 +10,10 @@ struct Cli { #[clap(short, action = clap::ArgAction::Count)] verbosity: u8, + /// Directory containing the shader crate to compile. + #[clap(long, short, default_value = "renderling-shader")] + shader_crate: std::path::PathBuf, + /// Path to the output directory for the compiled shaders. #[clap(long, short, default_value = "../crates/renderling/src/linkage")] output_dir: std::path::PathBuf, @@ -18,6 +22,7 @@ struct Cli { fn main() -> Result<(), Box> { let Cli { verbosity, + shader_crate, output_dir, } = Cli::parse(); let level = match verbosity { @@ -33,10 +38,18 @@ fn main() -> Result<(), Box> { std::fs::create_dir_all(&output_dir).unwrap(); + let shader_crate = std::path::Path::new("../crates/").join(shader_crate); + assert!( + shader_crate.exists(), + "shader crate '{}' does not exist. Current dir is {}", + shader_crate.display(), + std::env::current_dir().unwrap().display() + ); + let CompileResult { entry_points, module, - } = SpirvBuilder::new("shader-crate", "spirv-unknown-vulkan1.2") + } = SpirvBuilder::new(shader_crate, "spirv-unknown-vulkan1.2") .print_metadata(MetadataPrintout::None) .multimodule(true) .build()?; diff --git a/test_img/cmy_cube/remesh_after.png b/test_img/cmy_cube/remesh_after.png new file mode 100644 index 00000000..a79ecaae Binary files /dev/null and b/test_img/cmy_cube/remesh_after.png differ diff --git a/test_img/cmy_cube/remesh_before.png b/test_img/cmy_cube/remesh_before.png new file mode 100644 index 00000000..3e43585c Binary files /dev/null and b/test_img/cmy_cube/remesh_before.png differ diff --git a/test_img/cmy_cube.png b/test_img/cmy_cube/sanity.png similarity index 100% rename from test_img/cmy_cube.png rename to test_img/cmy_cube/sanity.png diff --git a/test_img/cmy_cube_visible_after.png b/test_img/cmy_cube/visible_after.png similarity index 100% rename from test_img/cmy_cube_visible_after.png rename to test_img/cmy_cube/visible_after.png diff --git a/test_img/cmy_cube_visible_before.png b/test_img/cmy_cube/visible_before.png similarity index 100% rename from test_img/cmy_cube_visible_before.png rename to test_img/cmy_cube/visible_before.png diff --git a/test_img/cmy_cube_remesh_after.png b/test_img/cmy_cube_remesh_after.png deleted file mode 100644 index 7ee6d1cc..00000000 Binary files a/test_img/cmy_cube_remesh_after.png and /dev/null differ diff --git a/test_img/cmy_cube_remesh_before.png b/test_img/cmy_cube_remesh_before.png deleted file mode 100644 index c4c23d03..00000000 Binary files a/test_img/cmy_cube_remesh_before.png and /dev/null differ