diff --git a/Cargo.lock b/Cargo.lock index 47793739..7eafd170 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,12 +18,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" -[[package]] -name = "acorn_prng" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4812dd5f835c603721f95a2c057394fc12a9d53206588664e68247e7cb25549b" - [[package]] name = "adler2" version = "2.0.0" @@ -85,6 +79,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -535,6 +535,20 @@ dependencies = [ "serde", ] +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "2.34.0" @@ -701,10 +715,20 @@ dependencies = [ ] [[package]] -name = "craballoc" -version = "0.1.11" +name = "core-text" +version = "20.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fab2b385e6d66aa64ef46e97f8752018c8d3393d1ed58a25fe5ff29931ab129" +checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" +dependencies = [ + "core-foundation", + "core-graphics", + "foreign-types", + "libc", +] + +[[package]] +name = "craballoc" +version = "0.2.0" dependencies = [ "async-channel 1.9.0", "bytemuck", @@ -718,9 +742,7 @@ dependencies = [ [[package]] name = "crabslab" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d3486b5979b1c73ca6ba48f69e2abd7941dd72b0e725519d113aef61ff7a236" +version = "0.6.4" dependencies = [ "crabslab-derive", "futures-lite 1.13.0", @@ -730,9 +752,7 @@ dependencies = [ [[package]] name = "crabslab-derive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef999dd82fff9dc1f2cf371c0f9a6315016b9562a04811fbefae2d80da6a1fad" +version = "0.4.4" dependencies = [ "proc-macro2", "quote", @@ -828,6 +848,27 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -876,6 +917,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" +[[package]] +name = "dwrote" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70182709525a3632b2ba96b6569225467b18ecb4a77f46d255f713a6bebf05fd" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + [[package]] name = "either" version = "1.13.0" @@ -957,7 +1010,7 @@ dependencies = [ "futures-lite 1.13.0", "gltf", "icosahedron", - "image", + "image 0.25.5", "img-diff", "lazy_static", "loading-bytes", @@ -1066,6 +1119,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + [[package]] name = "float_next_after" version = "1.0.0" @@ -1078,6 +1137,31 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +[[package]] +name = "font-kit" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b64b34f4efd515f905952d91bc185039863705592c0c53ae6d979805dd154520" +dependencies = [ + "bitflags 2.8.0", + "byteorder", + "core-foundation", + "core-graphics", + "core-text", + "dirs", + "dwrote", + "float-ord", + "freetype-sys", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1105,6 +1189,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -1169,6 +1264,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gif" version = "0.13.1" @@ -1220,7 +1325,7 @@ dependencies = [ "base64", "byteorder", "gltf-json", - "image", + "image 0.25.5", "lazy_static", "serde_json", "urlencoding", @@ -1406,6 +1511,30 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icosahedron" version = "0.1.1" @@ -1420,6 +1549,20 @@ dependencies = [ "serde_json", ] +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "jpeg-decoder", + "num-traits", + "png", +] + [[package]] name = "image" version = "0.25.5" @@ -1430,7 +1573,7 @@ dependencies = [ "byteorder-lite", "color_quant", "exr", - "gif", + "gif 0.13.1", "image-webp", "num-traits", "png", @@ -1458,7 +1601,7 @@ name = "img-diff" version = "0.1.0" dependencies = [ "glam", - "image", + "image 0.25.5", "snafu 0.7.5", ] @@ -2212,6 +2355,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "orbclient" version = "0.3.48" @@ -2280,6 +2429,25 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf07ef4804cfa9aea3b04a7bbdd5a40031dbb6b4f2cbaf2b011666c80c5b4f2" +dependencies = [ + "rustc_version", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2339,6 +2507,52 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "chrono", + "font-kit", + "image 0.24.9", + "lazy_static", + "num-traits", + "pathfinder_geometry", + "plotters-backend", + "plotters-bitmap", + "plotters-svg", + "ttf-parser 0.20.0", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-bitmap" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ce181e3f6bf82d6c1dc569103ca7b1bd964c60ba03d7e6cdfbb3e3eb7f7405" +dependencies = [ + "gif 0.12.0", + "image 0.24.9", + "plotters-backend", +] + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "png" version = "0.17.16" @@ -2707,6 +2921,17 @@ dependencies = [ "bitflags 2.8.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "regex" version = "1.11.1" @@ -2746,7 +2971,6 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" name = "renderling" version = "0.5.0" dependencies = [ - "acorn_prng", "assert_approx_eq", "async-channel 1.9.0", "bytemuck", @@ -2764,12 +2988,13 @@ dependencies = [ "gltf", "half", "icosahedron", - "image", + "image 0.25.5", "img-diff", "log", "metal", "naga", "pathdiff", + "plotters", "pretty_assertions", "quote", "renderling_build", @@ -2805,7 +3030,7 @@ dependencies = [ "env_logger", "futures-lite 1.13.0", "glyph_brush", - "image", + "image 0.25.5", "img-diff", "loading-bytes", "log", @@ -2835,6 +3060,15 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.43" @@ -2903,6 +3137,12 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + [[package]] name = "send_wrapper" version = "0.6.0" @@ -3084,7 +3324,7 @@ dependencies = [ [[package]] name = "spirv-std" version = "0.9.0" -source = "git+https://github.com/Rust-GPU/rust-gpu?rev=6e2c84d#6e2c84d4fe64e32df4c060c5a7f3e35a32e45421" +source = "git+https://github.com/LegNeato/rust-gpu.git?rev=16b61ce#16b61ce9c871afa7d305d0b35e165585bde77546" dependencies = [ "bitflags 1.3.2", "glam", @@ -3096,7 +3336,7 @@ dependencies = [ [[package]] name = "spirv-std-macros" version = "0.9.0" -source = "git+https://github.com/Rust-GPU/rust-gpu?rev=6e2c84d#6e2c84d4fe64e32df4c060c5a7f3e35a32e45421" +source = "git+https://github.com/LegNeato/rust-gpu.git?rev=16b61ce#16b61ce9c871afa7d305d0b35e165585bde77546" dependencies = [ "proc-macro2", "quote", @@ -3107,7 +3347,7 @@ dependencies = [ [[package]] name = "spirv-std-types" version = "0.9.0" -source = "git+https://github.com/Rust-GPU/rust-gpu?rev=6e2c84d#6e2c84d4fe64e32df4c060c5a7f3e35a32e45421" +source = "git+https://github.com/LegNeato/rust-gpu.git?rev=16b61ce#16b61ce9c871afa7d305d0b35e165585bde77546" [[package]] name = "static_assertions" @@ -3879,6 +4119,12 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + [[package]] name = "windows-result" version = "0.2.0" @@ -3907,6 +4153,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -4164,6 +4419,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + [[package]] name = "x11-dl" version = "2.21.0" @@ -4249,6 +4513,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index a79115a9..66e32ba5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,14 +16,14 @@ exclude = ["./shaders"] resolver = "2" [workspace.dependencies] -acorn_prng = "3.0" assert_approx_eq = "1.1.0" async-channel = "1.8" bytemuck = { version = "1.19.0", features = ["derive"] } cfg_aliases = "0.2" clap = { version = "4.5.23", features = ["derive"] } -craballoc = { version = "0.1.11" } -crabslab = { version = "0.6.3", default-features = false } +craballoc = { version = "0.2.0", path = "../crabslab/crates/craballoc" } +crabslab = { version = "0.6.3", default-features = false, path = "../crabslab/crates/crabslab" } +plotters = "0.3.7" ctor = "0.2.2" dagga = "0.2.1" env_logger = "0.10.0" @@ -41,7 +41,7 @@ serde = {version = "1.0", features = ["derive"]} serde_json = "1.0.117" send_wrapper = "0.6.0" snafu = "0.8" -spirv-std = { git = "https://github.com/Rust-GPU/rust-gpu", rev = "6e2c84d" } +spirv-std = { git = "https://github.com/LegNeato/rust-gpu.git", rev = "16b61ce" } syn = { version = "2.0.49", features = ["full", "extra-traits", "parsing"] } tracing = "0.1.41" wasm-bindgen = "0.2" @@ -62,4 +62,4 @@ opt-level = 3 opt-level = 3 [patch.crates-io] -spirv-std = { git = "https://github.com/Rust-GPU/rust-gpu", rev = "6e2c84d" } +spirv-std = { git = "https://github.com/LegNeato/rust-gpu.git", rev = "16b61ce" } diff --git a/crates/renderling-build/src/lib.rs b/crates/renderling-build/src/lib.rs index a14edf47..ae01fc7c 100644 --- a/crates/renderling-build/src/lib.rs +++ b/crates/renderling-build/src/lib.rs @@ -1,4 +1,8 @@ #![allow(unexpected_cfgs)] +use naga::{ + back::wgsl::WriterFlags, + valid::{ValidationFlags, Validator}, +}; use quote::quote; #[derive(Debug, serde::Deserialize)] @@ -91,19 +95,35 @@ fn wgsl(spv_filepath: impl AsRef, destination: impl AsRef = None; + for (vflags, name) in [ + (ValidationFlags::empty(), "empty"), + (ValidationFlags::all(), "all"), + ] { + let mut validator = Validator::new(vflags, Default::default()); + match validator.validate(&module) { + Err(e) => { + panic_msg = Some(format!( + "Could not validate '{}' with WGSL validation flags {name}: {}", + spv_filepath.as_ref().display(), + e.emit_to_string(&wgsl) + )); + } + Ok(i) => { + wgsl = naga::back::wgsl::write_string(&module, &i, WriterFlags::empty()).unwrap(); + } + }; + } + let destination = destination.as_ref().with_extension("wgsl"); std::fs::write(destination, wgsl).unwrap(); + if let Some(msg) = panic_msg { + panic!( + "{msg}\nWGSL was written to {}", + spv_filepath.as_ref().display() + ); + } } pub struct RenderlingPaths { diff --git a/crates/renderling-ui/src/lib.rs b/crates/renderling-ui/src/lib.rs index 388eb8a5..819edc0b 100644 --- a/crates/renderling-ui/src/lib.rs +++ b/crates/renderling-ui/src/lib.rs @@ -28,7 +28,7 @@ //! Happy hacking! use std::sync::{Arc, RwLock}; -use craballoc::prelude::Hybrid; +use craballoc::prelude::{Hybrid, SourceId}; use crabslab::Id; use glyph_brush::ab_glyph; use renderling::{ @@ -150,7 +150,7 @@ pub struct Ui { // // The `usize` key here is the update source notifier index, which is needed // to re-order after any transform performs an update. - transforms: Arc>>, + transforms: Arc>>, default_stroke_options: Arc>, default_fill_options: Arc>, } diff --git a/crates/renderling/Cargo.toml b/crates/renderling/Cargo.toml index d92bec8f..5d715df0 100644 --- a/crates/renderling/Cargo.toml +++ b/crates/renderling/Cargo.toml @@ -73,15 +73,15 @@ wgpu = { workspace = true, features = ["spirv"] } winit = { workspace = true, optional = true } [dev-dependencies] -acorn_prng.workspace = true -assert_approx_eq = {workspace = true} +assert_approx_eq.workspace = true ctor = "0.2.2" -env_logger = {workspace = true} +env_logger.workspace = true example = { path = "../example" } fastrand = "2.1.1" icosahedron = "0.1" img-diff = { path = "../img-diff" } naga.workspace = true +plotters.workspace = true ttf-parser = "0.20.0" wgpu-core.workspace = true winit.workspace = true diff --git a/crates/renderling/shaders/cull-compute_culling.spv b/crates/renderling/shaders/cull-compute_culling.spv index 677b805d..307fa570 100644 Binary files a/crates/renderling/shaders/cull-compute_culling.spv and b/crates/renderling/shaders/cull-compute_culling.spv differ diff --git a/crates/renderling/shaders/debug-debug_overlay_fragment.spv b/crates/renderling/shaders/debug-debug_overlay_fragment.spv index fd85e0dc..a7d8734b 100644 Binary files a/crates/renderling/shaders/debug-debug_overlay_fragment.spv and b/crates/renderling/shaders/debug-debug_overlay_fragment.spv differ diff --git a/crates/renderling/shaders/light-light_tiling_compute_tiles.spv b/crates/renderling/shaders/light-light_tiling_compute_tiles.spv new file mode 100644 index 00000000..5fc27659 Binary files /dev/null and b/crates/renderling/shaders/light-light_tiling_compute_tiles.spv differ diff --git a/crates/renderling/shaders/light-light_tiling_compute_tiles_multisampled.spv b/crates/renderling/shaders/light-light_tiling_compute_tiles_multisampled.spv new file mode 100644 index 00000000..11ca70bd Binary files /dev/null and b/crates/renderling/shaders/light-light_tiling_compute_tiles_multisampled.spv differ diff --git a/crates/renderling/shaders/light-light_tiling_depth_pre_pass.spv b/crates/renderling/shaders/light-light_tiling_depth_pre_pass.spv new file mode 100644 index 00000000..e6b236e4 Binary files /dev/null and b/crates/renderling/shaders/light-light_tiling_depth_pre_pass.spv differ diff --git a/crates/renderling/shaders/light-shadow_mapping_vertex.spv b/crates/renderling/shaders/light-shadow_mapping_vertex.spv index a2d57ff6..0e9598cf 100644 Binary files a/crates/renderling/shaders/light-shadow_mapping_vertex.spv and b/crates/renderling/shaders/light-shadow_mapping_vertex.spv differ diff --git a/crates/renderling/shaders/manifest.json b/crates/renderling/shaders/manifest.json index 4a7da913..836de729 100644 --- a/crates/renderling/shaders/manifest.json +++ b/crates/renderling/shaders/manifest.json @@ -104,6 +104,21 @@ "entry_point": "ibl::diffuse_irradiance::di_convolution_fragment", "wgsl_entry_point": "ibldiffuse_irradiancedi_convolution_fragment" }, + { + "source_path": "shaders/light-light_tiling_compute_tiles.spv", + "entry_point": "light::light_tiling_compute_tiles", + "wgsl_entry_point": "lightlight_tiling_compute_tiles" + }, + { + "source_path": "shaders/light-light_tiling_compute_tiles_multisampled.spv", + "entry_point": "light::light_tiling_compute_tiles_multisampled", + "wgsl_entry_point": "lightlight_tiling_compute_tiles_multisampled" + }, + { + "source_path": "shaders/light-light_tiling_depth_pre_pass.spv", + "entry_point": "light::light_tiling_depth_pre_pass", + "wgsl_entry_point": "lightlight_tiling_depth_pre_pass" + }, { "source_path": "shaders/light-shadow_mapping_fragment.spv", "entry_point": "light::shadow_mapping_fragment", diff --git a/crates/renderling/shaders/skybox-skybox_cubemap_vertex.spv b/crates/renderling/shaders/skybox-skybox_cubemap_vertex.spv index f4a16170..9b2b8b0c 100644 Binary files a/crates/renderling/shaders/skybox-skybox_cubemap_vertex.spv and b/crates/renderling/shaders/skybox-skybox_cubemap_vertex.spv differ diff --git a/crates/renderling/shaders/stage-go_thing.spv b/crates/renderling/shaders/stage-go_thing.spv new file mode 100644 index 00000000..586fa688 Binary files /dev/null and b/crates/renderling/shaders/stage-go_thing.spv differ diff --git a/crates/renderling/shaders/stage-renderlet_fragment.spv b/crates/renderling/shaders/stage-renderlet_fragment.spv index d4760ae9..08b1392e 100644 Binary files a/crates/renderling/shaders/stage-renderlet_fragment.spv and b/crates/renderling/shaders/stage-renderlet_fragment.spv differ diff --git a/crates/renderling/shaders/stage-renderlet_vertex.spv b/crates/renderling/shaders/stage-renderlet_vertex.spv index 330a7ac7..79c7c3ba 100644 Binary files a/crates/renderling/shaders/stage-renderlet_vertex.spv and b/crates/renderling/shaders/stage-renderlet_vertex.spv differ diff --git a/crates/renderling/src/bloom/cpu.rs b/crates/renderling/src/bloom/cpu.rs index 5d1c38c7..4c28bf0a 100644 --- a/crates/renderling/src/bloom/cpu.rs +++ b/crates/renderling/src/bloom/cpu.rs @@ -408,8 +408,7 @@ impl Bloom { let runtime = runtime.as_ref(); let resolution = UVec2::new(hdr_texture.width(), hdr_texture.height()); - let slab = - SlabAllocator::new_with_label(runtime, wgpu::BufferUsages::empty(), Some("bloom-slab")); + let slab = SlabAllocator::new(runtime, "bloom-slab", wgpu::BufferUsages::empty()); let downsample_pixel_sizes = slab.new_array( config_resolutions(resolution).map(|r| 1.0 / Vec2::new(r.x as f32, r.y as f32)), ); diff --git a/crates/renderling/src/bvol.rs b/crates/renderling/src/bvol.rs index dc59422a..0be3907f 100644 --- a/crates/renderling/src/bvol.rs +++ b/crates/renderling/src/bvol.rs @@ -13,7 +13,7 @@ use crabslab::SlabItem; use glam::{Mat4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; -#[cfg(target_arch = "spirv")] +#[cfg(gpu)] use spirv_std::num_traits::Float; use crate::{camera::Camera, transform::Transform}; @@ -130,6 +130,10 @@ impl Aabb { (self.min + self.max) * 0.5 } + pub fn extents(&self) -> Vec3 { + self.max - self.center() + } + pub fn diagonal_length(&self) -> f32 { self.min.distance(self.max) } @@ -182,6 +186,18 @@ impl Aabb { }) .collect() } + + /// Returns whether this `Aabb` intersects another `Aabb`. + /// + /// Returns `false` if the two are touching, but not overlapping. + pub fn intersects_aabb(&self, other: &Aabb) -> bool { + self.min.x < other.max.x + && self.max.x > other.min.x + && self.min.y < other.max.y + && self.max.y > other.min.y + && self.min.z < other.max.z + && self.max.z > other.min.z + } } /// Six planes of a view frustum. @@ -316,6 +332,94 @@ impl Frustum { } } +/// Bounding box consisting of a center and three half extents. +/// +/// Essentially a point at the center and a vector pointing from +/// the center to the corner. +/// +/// This is _not_ an axis aligned bounding box. +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Clone, Copy, Default, PartialEq, SlabItem)] +pub struct BoundingBox { + pub center: Vec3, + pub half_extent: Vec3, +} + +impl BoundingBox { + pub fn from_min_max(min: Vec3, max: Vec3) -> Self { + let center = (min + max) / 2.0; + let half_extent = max - center; + Self { + center, + half_extent, + } + } + + pub fn distance(&self, point: Vec3) -> f32 { + let p = point - self.center; + let component_edge_distance = p.abs() - self.half_extent; + let outside = component_edge_distance.max(Vec3::ZERO).length(); + let inside = component_edge_distance + .x + .max(component_edge_distance.y) + .min(0.0); + inside + outside + } + + #[cfg(cpu)] + /// Return a triangle mesh connecting this `Aabb`'s corners. + /// + /// ```ignore + /// y 1_____2 _____ + /// | / /| /| | (same box, left and front sides removed) + /// |___x 0/___3/ | /7|____|6 + /// / | | / | / / + /// z/ |____|/ 4|/____/5 + /// + /// 7 is min + /// 3 is max + /// ``` + pub fn get_mesh(&self) -> [(Vec3, Vec3); 36] { + // Deriving the corner positions from centre and half-extent, + + let p0 = Vec3::new(-self.half_extent.x, self.half_extent.y, self.half_extent.z); + let p1 = Vec3::new(-self.half_extent.x, self.half_extent.y, -self.half_extent.z); + let p2 = Vec3::new(self.half_extent.x, self.half_extent.y, -self.half_extent.z); + let p3 = self.half_extent; + let p4 = Vec3::new(-self.half_extent.x, -self.half_extent.y, self.half_extent.z); + let p5 = Vec3::new(self.half_extent.x, -self.half_extent.y, self.half_extent.z); + let p6 = Vec3::new(self.half_extent.x, -self.half_extent.y, -self.half_extent.z); + // min + let p7 = -self.half_extent; + + let positions = + crate::math::convex_mesh([p0, p1, p2, p3, p4, p5, p6, p7].map(|p| p + self.center)); + + // Attach per-triangle face normals. + let vertices: Vec<(Vec3, Vec3)> = positions + .chunks_exact(3) + .flat_map(|chunk| match chunk { + [a, b, c] => { + let n = crate::math::triangle_face_normal(*a, *b, *c); + [(*a, n), (*b, n), (*c, n)] + } + _ => unreachable!(), + }) + .collect(); + + // Convert into fixed-size array (12 triangles × 3 vertices). + vertices + .try_into() + .unwrap_or_else(|v: Vec<(Vec3, Vec3)>| panic!("expected 36 vertices, got {}", v.len())) + } + + pub fn contains_point(&self, point: Vec3) -> bool { + let delta = (point - self.center).abs(); + let extent = self.half_extent.abs(); + delta.x <= extent.x && delta.y <= extent.y && delta.z <= extent.z + } +} + /// Bounding sphere consisting of a center and radius. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[derive(Clone, Copy, Default, PartialEq, SlabItem)] @@ -470,7 +574,7 @@ impl BVol for Aabb { mod test { use glam::{Mat4, Quat}; - use crate::{stage::Vertex, Context}; + use crate::{pbr::Material, stage::Vertex, Context}; use super::*; @@ -523,41 +627,84 @@ mod test { } #[test] - fn bounding_sphere_from_min_max() { - let ctx = Context::headless(100, 100); + fn bounding_box_from_min_max() { + let ctx = Context::headless(256, 256); let stage = ctx .new_stage() - .with_lighting(false) - .with_background_color(Vec4::ONE) - .with_debug_overlay(true) - .with_frustum_culling(false); - - let projection = Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 20.0); - let view = Mat4::look_at_rh(Vec3::new(0.0, 0.0, 2.0), Vec3::ZERO, Vec3::Y); - let _camera = stage.new_camera(Camera::new(projection, view)); - - let mut min = Vec3::splat(f32::INFINITY); - let mut max = Vec3::splat(f32::NEG_INFINITY); - let _rez = stage + .with_background_color(Vec4::ZERO) + .with_msaa_sample_count(4) + .with_lighting(true); + let _camera = stage.new_camera({ + Camera::new( + // BUG: using orthographic here renderes nothing + // Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, 10.0, -10.0), + crate::camera::perspective(256.0, 256.0), + Mat4::look_at_rh(Vec3::new(-3.0, 3.0, 5.0) * 0.5, Vec3::ZERO, Vec3::Y), + ) + }); + let _lights = crate::test::make_two_directional_light_setup(&stage); + + let white = stage.new_material(Material { + albedo_factor: Vec4::ONE, + ..Default::default() + }); + let red = stage.new_material(Material { + albedo_factor: Vec4::new(1.0, 0.0, 0.0, 1.0), + ..Default::default() + }); + + let _w = stage .builder() - .with_vertices(crate::math::unit_cube().into_iter().map(|(p, n)| { - min = min.min(p); - max = max.max(p); - Vertex::default() - .with_position(p) - .with_normal(n) - .with_color(Vec4::new(1.0, 0.0, 0.0, 1.0)) - })) - .with_bounds({ - log::info!("bounds: {:?}", (min, max)); - (min, max) - }) + .with_material_id(white.id()) + .with_vertices( + crate::math::unit_cube() + .into_iter() + .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)), + ) .build(); + let mut corners = vec![]; + for x in [-1.0, 1.0] { + for y in [-1.0, 1.0] { + for z in [-1.0, 1.0] { + corners.push(Vec3::new(x, y, z)); + } + } + } + let mut rs = vec![]; + for corner in corners.iter() { + let bb = BoundingBox { + center: Vec3::new(0.5, 0.5, 0.5) * corner, + half_extent: Vec3::splat(0.25), + }; + assert!( + bb.contains_point(bb.center), + "BoundingBox {bb:?} does not contain center" + ); + + rs.push( + stage + .builder() + .with_material_id(red.id()) + .with_vertices( + bb.get_mesh() + .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)), + ) + .build(), + ); + } + let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); let img = frame.read_image().unwrap(); - img_diff::save("bvol/bounding_sphere_from_min_max.png", img); - frame.present(); + img_diff::assert_img_eq("bvol/bounding_box/get_mesh.png", img); + } + + #[test] + fn aabb_intersection() { + let a = Aabb::new(Vec3::ZERO, Vec3::ONE); + let b = Aabb::new(Vec3::splat(0.9), Vec3::splat(1.9)); + assert!(a.intersects_aabb(&b)); + assert!(b.intersects_aabb(&a)); } } diff --git a/crates/renderling/src/context.rs b/crates/renderling/src/context.rs index 9b1fa59a..f9507cfb 100644 --- a/crates/renderling/src/context.rs +++ b/crates/renderling/src/context.rs @@ -429,6 +429,14 @@ impl Frame { } } +/// Configurable default values to use when creating new [`Stage`]s. +#[derive(Debug, Clone, Copy)] +pub(crate) struct GlobalStageConfig { + pub(crate) atlas_size: wgpu::Extent3d, + pub(crate) shadow_map_atlas_size: wgpu::Extent3d, + pub(crate) use_compute_culling: bool, +} + /// Contains the adapter, device, queue, [`RenderTarget`] and initial atlas sizing. /// /// A `Context` is created to initialize rendering to a window, canvas or @@ -443,7 +451,7 @@ pub struct Context { runtime: WgpuRuntime, adapter: Arc, render_target: RenderTarget, - pub(crate) atlas_size: Arc>, + pub(crate) stage_config: Arc>, } impl AsRef for Context { @@ -464,13 +472,21 @@ impl Context { let w = limits .max_texture_dimension_2d .min(crate::atlas::ATLAS_SUGGESTED_SIZE); - let atlas_size = Arc::new(RwLock::new(wgpu::Extent3d { - width: w, - height: w, - depth_or_array_layers: adapter - .limits() - .max_texture_array_layers - .min(crate::atlas::ATLAS_SUGGESTED_LAYERS), + let stage_config = Arc::new(RwLock::new(GlobalStageConfig { + atlas_size: wgpu::Extent3d { + width: w, + height: w, + depth_or_array_layers: adapter + .limits() + .max_texture_array_layers + .min(crate::atlas::ATLAS_SUGGESTED_LAYERS), + }, + shadow_map_atlas_size: wgpu::Extent3d { + width: w, + height: w, + depth_or_array_layers: 4, + }, + use_compute_culling: false, })); Self { adapter, @@ -479,7 +495,7 @@ impl Context { queue: queue.into(), }, render_target: target, - atlas_size, + stage_config, } } @@ -651,14 +667,11 @@ impl Context { }) } - /// Set the default texture size for any atlas. + /// Set the default texture size for the material atlas. /// /// * Width is `size.x` and must be a power of two. /// * Height is `size.y`, must match `size.x` and must be a power of two. /// * Layers is `size.z` and must be two or greater. - /// - /// ## Panics - /// Will panic if the above conditions are not met. pub fn set_default_atlas_texture_size(&self, size: impl Into) -> &Self { let size = size.into(); let size = wgpu::Extent3d { @@ -667,11 +680,11 @@ impl Context { depth_or_array_layers: size.z, }; crate::atlas::check_size(size); - *self.atlas_size.write().unwrap() = size; + self.stage_config.write().unwrap().atlas_size = size; self } - /// Set the default texture size for any atlas. + /// Set the default texture size for the material atlas. /// /// * Width is `size.x` and must be a power of two. /// * Height is `size.y`, must match `size.x` and must be a power of two. @@ -684,6 +697,61 @@ impl Context { self } + /// Set the default texture size for the shadow mapping atlas. + /// + /// * Width is `size.x` and must be a power of two. + /// * Height is `size.y`, must match `size.x` and must be a power of two. + /// * Layers is `size.z` and must be two or greater. + pub fn set_shadow_mapping_atlas_texture_size(&self, size: impl Into) -> &Self { + let size = size.into(); + let size = wgpu::Extent3d { + width: size.x, + height: size.y, + depth_or_array_layers: size.z, + }; + crate::atlas::check_size(size); + self.stage_config.write().unwrap().shadow_map_atlas_size = size; + self + } + + /// Set the default texture size for the shadow mapping atlas. + /// + /// * Width is `size.x` and must be a power of two. + /// * Height is `size.y`, must match `size.x` and must be a power of two. + /// * Layers is `size.z` and must be greater than zero. + /// + /// ## Panics + /// Will panic if the above conditions are not met. + pub fn with_shadow_mapping_atlas_texture_size(self, size: impl Into) -> Self { + self.set_shadow_mapping_atlas_texture_size(size); + self + } + + /// Set the use of direct drawing. + /// + /// Default is **false**. + /// + /// If set to **true**, all compute culling, including frustum and occlusion culling, + /// will **not** run. + pub fn set_use_direct_draw(&self, use_direct_drawing: bool) { + self.stage_config.write().unwrap().use_compute_culling = !use_direct_drawing; + } + + /// Set the use of direct drawing. + /// + /// Default is **false**. + /// + /// If set to **true**, all compute culling is turned **off**. + /// This includes frustum and occlusion culling. + pub fn with_use_direct_draw(self, use_direct_drawing: bool) -> Self { + self.set_use_direct_draw(use_direct_drawing); + self + } + + pub fn get_use_direct_draw(&self) -> bool { + !self.stage_config.read().unwrap().use_compute_culling + } + /// Create and return a new [`Stage`] renderer. pub fn new_stage(&self) -> Stage { Stage::new(self) diff --git a/crates/renderling/src/cubemap/cpu.rs b/crates/renderling/src/cubemap/cpu.rs index 62c030a8..c0740e32 100644 --- a/crates/renderling/src/cubemap/cpu.rs +++ b/crates/renderling/src/cubemap/cpu.rs @@ -317,7 +317,7 @@ mod test { ); scene_cubemap.run(&stage); - let slab = SlabAllocator::new(&ctx, wgpu::BufferUsages::empty()); + let slab = SlabAllocator::new(&ctx, "cubemap-sampling-test", wgpu::BufferUsages::empty()); let uv = slab.new_value(Vec3::ZERO); let buffer = slab.commit(); let label = Some("cubemap-sampling-test"); @@ -580,13 +580,8 @@ mod test { // add in some deterministic pseudo-randomn points { - let order = acorn_prng::Order::new(666); - let seed = acorn_prng::Seed::new(1_000_000); - let mut prng = acorn_prng::Acorn::new(order, seed); - let mut rf32 = move || { - let u = prng.generate_u32_between_range(0..=u32::MAX); - f32::from_bits(u) - }; + let mut prng = crate::math::GpuRng::new(666); + let mut rf32 = move || prng.gen_f32(0.0, 1.0); let mut rxvec3 = { || Vec3::new(f32::MAX, rf32(), rf32()).normalize_or(Vec3::X) }; // let mut rvec3 = || Vec3::new(rf32(), rf32(), rf32()); uvs.extend((0..20).map(|_| rxvec3())); diff --git a/crates/renderling/src/cull/cpu.rs b/crates/renderling/src/cull/cpu.rs index ac3ec7ae..781f5168 100644 --- a/crates/renderling/src/cull/cpu.rs +++ b/crates/renderling/src/cull/cpu.rs @@ -223,7 +223,7 @@ pub struct DepthPyramid { } impl DepthPyramid { - const LABEL: Option<&'static str> = Some("depth-pyramid"); + const LABEL: &str = "depth-pyramid"; fn allocate( size: UVec2, @@ -249,7 +249,7 @@ impl DepthPyramid { } pub fn new(runtime: impl AsRef, size: UVec2) -> Self { - let slab = SlabAllocator::new_with_label(runtime, wgpu::BufferUsages::empty(), Self::LABEL); + let slab = SlabAllocator::new(runtime, Self::LABEL, wgpu::BufferUsages::empty()); let desc = slab.new_value(DepthPyramidDescriptor::default()); let (mip_data, mip) = Self::allocate(size, &desc, &slab); diff --git a/crates/renderling/src/draw.rs b/crates/renderling/src/draw.rs index 84a0e740..d2990b7a 100644 --- a/crates/renderling/src/draw.rs +++ b/crates/renderling/src/draw.rs @@ -6,14 +6,14 @@ //! [`Stage::render`](crate::prelude::Stage::render). use crabslab::SlabItem; -#[cfg(not(target_arch = "spirv"))] +#[cfg(cpu)] mod cpu; -#[cfg(not(target_arch = "spirv"))] +#[cfg(cpu)] pub use cpu::*; /// Argument buffer layout for draw_indirect commands. #[repr(C)] -#[cfg_attr(not(target_arch = "spirv"), derive(bytemuck::Pod, bytemuck::Zeroable))] +#[cfg_attr(cpu, derive(Debug, bytemuck::Pod, bytemuck::Zeroable))] #[derive(Clone, Copy, Default, SlabItem)] pub struct DrawIndirectArgs { pub vertex_count: u32, diff --git a/crates/renderling/src/draw/cpu.rs b/crates/renderling/src/draw/cpu.rs index a8ec0653..a92f432a 100644 --- a/crates/renderling/src/draw/cpu.rs +++ b/crates/renderling/src/draw/cpu.rs @@ -32,15 +32,6 @@ impl InternalRenderlet { inner: WeakHybrid::from_hybrid(hr), } } - - fn copy_inner(&self) -> Option { - let hy = self.get_hybrid()?; - Some(hy.get()) - } - - fn get_hybrid(&self) -> Option> { - self.inner.upgrade() - } } /// Issues indirect draw calls. @@ -59,11 +50,8 @@ impl IndirectDraws { depth_texture: &Texture, ) -> Self { let runtime = runtime.as_ref(); - let indirect_slab = SlabAllocator::new_with_label( - runtime, - wgpu::BufferUsages::INDIRECT, - Some("indirect-slab"), - ); + let indirect_slab = + SlabAllocator::new(runtime, "indirect-slab", wgpu::BufferUsages::INDIRECT); Self { compute_culling: ComputeCulling::new( runtime, @@ -78,6 +66,7 @@ impl IndirectDraws { fn invalidate(&mut self) { if !self.draws.is_empty() { + log::trace!("draining indirect draws after invalidation"); let _ = self.draws.drain(..); } } @@ -165,7 +154,6 @@ pub struct DrawCalls { /// Internal representation of all staged renderlets. internal_renderlets: Vec, pub(crate) drawing_strategy: DrawingStrategy, - stage_slab_buffer: SlabBuffer, } impl DrawCalls { @@ -208,7 +196,6 @@ impl DrawCalls { DrawingStrategy::Direct } }, - stage_slab_buffer: stage_slab_buffer.clone(), } } @@ -275,7 +262,7 @@ impl DrawCalls { pub fn renderlets_iter(&self) -> impl Iterator> + '_ { self.internal_renderlets.iter().map(|ir| ir.inner.clone()) } - + /// /// Perform upkeep on queued draw calls and synchronize internal buffers. pub fn upkeep(&mut self) { let mut redraw_args = false; @@ -302,7 +289,7 @@ impl DrawCalls { self.internal_renderlets.len() } - /// Perform pre-draw steps like compute culling, if available. + /// Perform pre-draw steps like frustum and occlusion culling, if available. /// /// This does not do upkeep, please call [`DrawCalls::upkeep`] before /// calling this function. @@ -345,7 +332,24 @@ impl DrawCalls { } } + /// Draw into the given `RenderPass` by directly calling each draw. + pub fn draw_direct(&self, render_pass: &mut wgpu::RenderPass) { + for ir in self.internal_renderlets.iter() { + // UNWRAP: panic on purpose + if let Some(hr) = ir.inner.upgrade() { + let ir = hr.get(); + let vertex_range = 0..ir.get_vertex_count(); + let id = hr.id(); + let instance_range = id.inner()..id.inner() + 1; + render_pass.draw(vertex_range, instance_range); + } + } + } + /// Draw into the given `RenderPass`. + /// + /// This method draws using the indirect draw buffer, if possible, otherwise + /// it falls back to `draw_direct`. pub fn draw(&self, render_pass: &mut wgpu::RenderPass) { let num_draw_calls = self.draw_count(); if num_draw_calls > 0 { @@ -362,16 +366,7 @@ impl DrawCalls { } DrawingStrategy::Direct => { log::trace!("drawing {num_draw_calls} renderlets using direct"); - for ir in self.internal_renderlets.iter() { - // UNWRAP: panic on purpose - if let Some(hr) = ir.inner.upgrade() { - let ir = hr.get(); - let vertex_range = 0..ir.get_vertex_count(); - let id = hr.id(); - let instance_range = id.inner()..id.inner() + 1; - render_pass.draw(vertex_range, instance_range); - } - } + self.draw_direct(render_pass); } } } diff --git a/crates/renderling/src/draw_indirect.rs b/crates/renderling/src/draw_indirect.rs deleted file mode 100644 index cad1e2bc..00000000 --- a/crates/renderling/src/draw_indirect.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crabslab::SlabItem; - -#[derive(Default, Debug, Clone, Copy, PartialEq, SlabItem)] -pub struct DrawIndirect { - pub vertex_count: u32, - pub instance_count: u32, - pub base_vertex: u32, - pub base_instance: u32, -} diff --git a/crates/renderling/src/geometry/cpu.rs b/crates/renderling/src/geometry/cpu.rs index 0c8adc31..0402c4aa 100644 --- a/crates/renderling/src/geometry/cpu.rs +++ b/crates/renderling/src/geometry/cpu.rs @@ -1,6 +1,8 @@ //! CPU side of the [super::geometry](geometry) module. //! +use std::sync::{Arc, Mutex}; + use craballoc::{ runtime::WgpuRuntime, slab::{SlabAllocator, SlabBuffer}, @@ -23,6 +25,11 @@ use crate::{ pub struct Geometry { slab: SlabAllocator, descriptor: Hybrid, + /// Holds the current camera just in case the user drops it, + /// this way we never lose a camera that is in use. Dropping + /// the camera would cause a blank screen, which is very confusing + /// =( + _camera: Arc>>>, } impl AsRef for Geometry { @@ -41,14 +48,17 @@ impl Geometry { // TODO: move atlas size into materials. pub fn new(runtime: impl AsRef, resolution: UVec2, atlas_size: UVec2) -> Self { let runtime = runtime.as_ref(); - let slab = - SlabAllocator::new_with_label(runtime, wgpu::BufferUsages::empty(), Some("geometry")); + let slab = SlabAllocator::new(runtime, "geometry", wgpu::BufferUsages::empty()); let descriptor = slab.new_value(GeometryDescriptor { atlas_size, resolution, ..Default::default() }); - Self { slab, descriptor } + Self { + slab, + descriptor, + _camera: Default::default(), + } } pub fn runtime(&self) -> &WgpuRuntime { @@ -74,18 +84,18 @@ impl Geometry { pub fn new_camera(&self, camera: Camera) -> Hybrid { let c = self.slab.new_value(camera); if self.descriptor.get().camera_id.is_none() { - log::info!("automatically using camera: {:?}", c.id()); - self.descriptor.modify(|cfg| { - cfg.camera_id = c.id(); - }); + self.use_camera(&c); } c } /// Set all geometry to use the given camera. pub fn use_camera(&self, camera: impl AsRef>) { - self.descriptor - .modify(|cfg| cfg.camera_id = camera.as_ref().id()); + let c = camera.as_ref(); + log::info!("using camera: {:?}", c.id()); + // Save a clone so we never lose the active camera, even if the user drops it + *self._camera.lock().unwrap() = Some(c.clone()); + self.descriptor.modify(|cfg| cfg.camera_id = c.id()); } pub fn new_transform(&self, transform: Transform) -> Hybrid { diff --git a/crates/renderling/src/lib.rs b/crates/renderling/src/lib.rs index 89ab0b87..f985379d 100644 --- a/crates/renderling/src/lib.rs +++ b/crates/renderling/src/lib.rs @@ -215,17 +215,42 @@ mod test { use glam::{Mat3, Mat4, Quat, UVec2, Vec2, Vec3, Vec4}; use img_diff::DiffCfg; + use light::{AnalyticalLightBundle, DirectionalLightDescriptor}; use pretty_assertions::assert_eq; + use stage::Stage; #[ctor::ctor] fn init_logging() { let _ = env_logger::builder().is_test(true).try_init(); + log::info!("logging is on"); } pub fn workspace_dir() -> std::path::PathBuf { std::path::PathBuf::from(std::env!("CARGO_WORKSPACE_DIR")) } + pub fn make_two_directional_light_setup( + stage: &Stage, + ) -> (AnalyticalLightBundle, AnalyticalLightBundle) { + let sunlight_a = stage.new_analytical_light( + DirectionalLightDescriptor { + direction: Vec3::new(-0.8, -1.0, 0.5).normalize(), + color: Vec4::ONE, + intensity: 100.0, + }, + None, + ); + let sunlight_b = stage.new_analytical_light( + DirectionalLightDescriptor { + direction: Vec3::new(1.0, 1.0, -0.1).normalize(), + color: Vec4::ONE, + intensity: 10.0, + }, + None, + ); + (sunlight_a, sunlight_b) + } + #[allow(unused, reason = "Used in debugging on macos")] pub fn capture_gpu_frame( ctx: &Context, @@ -991,7 +1016,7 @@ mod test { stage.render(&frame.view()); let img = frame.read_image().unwrap(); assert_eq!(size, UVec2::new(img.width(), img.height())); - img_diff::save("stage/resize_100.png", img); + img_diff::assert_img_eq("stage/resize_100.png", img); frame.present(); let new_size = UVec2::new(200, 200); @@ -1002,7 +1027,37 @@ mod test { stage.render(&frame.view()); let img = frame.read_image().unwrap(); assert_eq!(new_size, UVec2::new(img.width(), img.height())); - img_diff::save("stage/resize_200.png", img); + img_diff::assert_img_eq("stage/resize_200.png", img); + frame.present(); + } + + #[test] + fn can_direct_draw_cube() { + let size = UVec2::new(100, 100); + let ctx = Context::headless(size.x, size.y).with_use_direct_draw(true); + let stage = ctx.new_stage(); + + // create the CMY cube + let camera_position = Vec3::new(0.0, 12.0, 20.0); + let _camera = stage.new_camera(Camera::new( + Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 0.1, 100.0), + Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y), + )); + let _rez = stage + .builder() + .with_vertices(gpu_cube_vertices()) + .with_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() + }) + .build(); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img = frame.read_image().unwrap(); + assert_eq!(size, UVec2::new(img.width(), img.height())); + img_diff::assert_img_eq("stage/resize_100.png", img); frame.present(); } } diff --git a/crates/renderling/src/light.rs b/crates/renderling/src/light.rs index fdd9698b..ad855371 100644 --- a/crates/renderling/src/light.rs +++ b/crates/renderling/src/light.rs @@ -3,14 +3,20 @@ //! Directional, point and spot lights. //! //! Shadow mapping. +//! +//! Tiling. use crabslab::{Array, Id, Slab, SlabItem}; -use glam::{Mat4, UVec2, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; -use spirv_std::spirv; +use glam::{Mat4, UVec2, UVec3, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; +#[cfg(gpu)] +use spirv_std::num_traits::Float; +use spirv_std::{spirv, Image}; use crate::{ atlas::{AtlasDescriptor, AtlasTexture}, + bvol::{Aabb, BoundingSphere}, cubemap::{CubemapDescriptor, CubemapFaceDirection}, - math::{IsSampler, IsVector, Sample2dArray}, + geometry::GeometryDescriptor, + math::{Fetch, IsSampler, IsVector, Sample2dArray}, stage::Renderlet, transform::Transform, }; @@ -25,6 +31,11 @@ mod shadow_map; #[cfg(cpu)] pub use shadow_map::*; +#[cfg(cpu)] +mod tiling; +#[cfg(cpu)] +pub use tiling::*; + /// Root descriptor of the lighting system. #[derive(Clone, Copy, Default, SlabItem, core::fmt::Debug)] #[offsets] @@ -207,9 +218,6 @@ impl SpotLightCalculation { node_transform: Mat4, fragment_world_position: Vec3, ) -> Self { - #[cfg(gpu)] - use spirv_std::num_traits::Float; - let light_position = node_transform.transform_point3(spot_light_descriptor.position); let frag_position = fragment_world_position; let frag_to_light = light_position - frag_position; @@ -262,8 +270,8 @@ pub struct SpotLightDescriptor { impl Default for SpotLightDescriptor { fn default() -> Self { let white = Vec4::splat(1.0); - let inner_cutoff = core::f32::consts::PI / 3.0; - let outer_cutoff = core::f32::consts::PI / 2.0; + let inner_cutoff = 0.077143565; + let outer_cutoff = 0.09075713; let direction = Vec3::new(0.0, -1.0, 0.0); let color = white; let intensity = 1.0; @@ -354,6 +362,7 @@ impl DirectionalLightDescriptor { pub struct PointLightDescriptor { pub position: Vec3, pub color: Vec4, + /// Expressed as candelas. pub intensity: f32, } @@ -402,6 +411,17 @@ impl PointLightDescriptor { }), ) } + + /// Returns the radius of illumination in meters. + /// + /// + /// • General indoor lighting: Around 100 to 300 lux. + /// • Office lighting: Typically around 300 to 500 lux. + /// • Reading or task lighting: Around 500 to 750 lux. + /// • Detailed work (e.g., drafting, surgery): 1000 lux or more. + pub fn radius_of_illumination(&self, minimum_illuminance_lux: f32) -> f32 { + (self.intensity / minimum_illuminance_lux).sqrt() + } } #[repr(u32)] @@ -637,7 +657,7 @@ impl ShadowCalculation { crate::println!("closest_depth: {shadow_map_depth}"); let bias = (bias_max * (1.0 - surface_normal.dot(*light_direction))).max(*bias_min); - if (fragment_depth - bias) > shadow_map_depth { + if (fragment_depth - bias) >= shadow_map_depth { shadow += 1.0 } total += 1.0; @@ -727,6 +747,400 @@ impl ShadowCalculation { } } +/// Depth pre-pass for the light tiling feature. +/// +/// This shader writes all staged [`Renderlet`]'s depth into a buffer. +/// +/// This shader is very much like [`shadow_mapping_vertex`], except that +/// shader gets its projection+view matrix from the light stored in a +/// `ShadowMapDescriptor`. +/// +/// Here we want to render as normal forward pass would, with the `Renderlet`'s view +/// and the [`Camera`]'s projection. +/// +/// ## Note +/// This shader will likely be expanded to include parts of occlusion culling and order +/// independent transparency. +#[spirv(vertex)] +pub fn light_tiling_depth_pre_pass( + // Points at a `Renderlet`. + #[spirv(instance_index)] renderlet_id: Id, + // Which vertex within the renderlet are we rendering? + #[spirv(vertex_index)] vertex_index: u32, + // The slab where the renderlet's geometry is staged + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32], + // Output clip coords + #[spirv(position)] out_clip_pos: &mut Vec4, +) { + let renderlet = geometry_slab.read_unchecked(renderlet_id); + if !renderlet.visible { + // put it outside the clipping frustum + *out_clip_pos = Vec3::splat(100.0).extend(1.0); + return; + } + + let camera_id = geometry_slab + .read_unchecked(Id::::new(0) + GeometryDescriptor::OFFSET_OF_CAMERA_ID); + let camera = geometry_slab.read_unchecked(camera_id); + + let (_vertex, _transform, _model_matrix, world_pos) = + renderlet.get_vertex_info(vertex_index, geometry_slab); + + *out_clip_pos = camera.view_projection() * world_pos.extend(1.0); +} + +/// Marker trait for abstracting over depth texture types. +pub trait IsDepth { + type Texture; +} + +pub struct DepthImage2d; +impl IsDepth for DepthImage2d { + type Texture = Image!(2D, type=f32, sampled, depth); +} + +pub struct DepthImage2dMultisampled; +impl IsDepth for DepthImage2dMultisampled { + type Texture = Image!(2D, type=f32, sampled, depth, multisampled=true); +} + +fn screen_space_to_clip_space(resolution: Vec2, screen_point: Vec2) -> Vec2 { + let normalized = screen_point / resolution; + Vec2::new(normalized.x, 1.0 - normalized.y) * 2.0 - 1.0 +} + +/// A tile of screen space used to cull lights. +#[derive(Clone, Copy, Default, SlabItem, core::fmt::Debug)] +#[offsets] +pub struct LightTile { + /// Minimum depth of objects found within the frustum of the tile. + pub depth_min: u32, + /// Maximum depth of objects foudn within the frustum of the tile. + pub depth_max: u32, + /// The count of lights in this tile. + /// + /// Also, the next available light index. + pub next_light_index: u32, + /// List of light ids that intersect this tile's frustum. + pub lights_array: Array>, +} + +/// Descriptor of the light tiling operation, which culls lights by accumulating +/// them into lists that illuminate tiles of the screen. +#[derive(Clone, Copy, Default, SlabItem, core::fmt::Debug)] +pub struct LightTilingDescriptor { + pub depth_texture_size: UVec2, + /// Array pointing to the lighting "tiles". + pub tiles_array: Array, +} + +impl LightTilingDescriptor { + pub const TILE_SIZE: UVec2 = UVec2::new(16, 16); + + pub fn tile_dimensions(&self) -> UVec2 { + let x = (self.depth_texture_size.x as f32 / Self::TILE_SIZE.x as f32).ceil(); + let y = (self.depth_texture_size.y as f32 / Self::TILE_SIZE.y as f32).ceil(); + UVec2::new(x as u32, y as u32) + } +} + +struct LightTilingInvocation { + global_id: UVec3, + descriptor: LightTilingDescriptor, +} + +impl LightTilingInvocation { + fn new(global_id: UVec3, descriptor: LightTilingDescriptor) -> Self { + Self { + global_id, + descriptor, + } + } + + /// The fragment's position. + /// + /// X range is 0 to (width - 1), Y range is 0 to (height - 1). + fn frag_pos(&self) -> UVec2 { + self.global_id.xy() + } + + /// The number of tiles in X and Y within the depth texture. + fn tile_dimensions(&self) -> UVec2 { + self.descriptor.tile_dimensions() + } + + /// The tile's position among all tiles. + fn tile_pos(&self) -> UVec2 { + self.global_id.xy() / LightTilingDescriptor::TILE_SIZE + } + + /// The tile's index in all the [`LightTilingDescriptor`]'s `tile_array`. + fn tile_index(&self) -> usize { + let tile_pos = self.tile_pos(); + let tile_dimensions = self.tile_dimensions(); + (tile_pos.y * tile_dimensions.x + tile_pos.x) as usize + } + + /// The index of the fragment within its tile. + // + // TODO: verify this is correct + fn frag_index(&self) -> usize { + // The fragment's xy position within its tile + let frag_tile = self.frag_pos() % LightTilingDescriptor::TILE_SIZE; + // The fragment's index in the tile, which will be 0..256 if TILE_SIZE is 16x16 + (frag_tile.y * LightTilingDescriptor::TILE_SIZE.x + frag_tile.x) as usize + } + + /// Compute the min and max depth of one fragment/invocation for light tiling. + /// + /// Returns the **indices** of the min and max depths of the tile. + fn compute_min_and_max_depth( + &self, + depth_texture: &impl Fetch, + _lighting_slab: &[u32], + tiling_slab: &mut [u32], + ) -> (usize, usize) { + let frag_pos = self.frag_pos(); + // Depth frag value at the fragment position + let frag_depth: f32 = depth_texture.fetch(frag_pos).x; + // Fragment depth scaled to min/max of u32 values + // + // This is so we can compare with normal atomic ops instead of using the float extension + let frag_depth_u32: u32 = (u32::MAX as f32 * frag_depth).round() as u32; + + // The tile's index in all the tiles + let tile_index = self.tile_index(); + let tiling_desc = tiling_slab.read_unchecked(Id::::new(0)); + // index of the tile's min depth atomic value in the tiling slab + let tile_id = tiling_desc.tiles_array.at(tile_index); + let min_depth_index = (tile_id + LightTile::OFFSET_OF_DEPTH_MIN).index(); + // index of the tile's max depth atomic value in the tiling slab + let max_depth_index = (tile_id + LightTile::OFFSET_OF_DEPTH_MAX).index(); + + let _prev_min_depth = unsafe { + spirv_std::arch::atomic_u_min::< + u32, + { spirv_std::memory::Scope::Workgroup as u32 }, + { spirv_std::memory::Semantics::WORKGROUP_MEMORY.bits() }, + >(&mut tiling_slab[min_depth_index], frag_depth_u32) + }; + let _prev_max_depth = unsafe { + spirv_std::arch::atomic_u_max::< + u32, + { spirv_std::memory::Scope::Workgroup as u32 }, + { spirv_std::memory::Semantics::WORKGROUP_MEMORY.bits() }, + >(&mut tiling_slab[max_depth_index], frag_depth_u32) + }; + + (min_depth_index, max_depth_index) + } + + /// Determine whether this invocation should run. + fn should_invoke(&self) -> bool { + self.global_id.x < self.descriptor.depth_texture_size.x + && self.global_id.y < self.descriptor.depth_texture_size.y + } + + /// Returns whether this invocation's X and Y coords are divided + /// evenly by the tile size. + fn frag_pos_is_tile_corner(&self) -> bool { + let frag_pos = self.frag_pos(); + frag_pos.x % LightTilingDescriptor::TILE_SIZE.x == 0 + && frag_pos.y % LightTilingDescriptor::TILE_SIZE.y == 0 + } + + fn clear_tiles(&self, tiling_slab: &mut [u32]) { + if self.frag_pos_is_tile_corner() { + // only continue if this is the invocation in the top-left of the tile, as + // we only need one invocation per tile. + let tile_index = self.tile_index(); + let tile_id = self.descriptor.tiles_array.at(tile_index); + + { + let mut tile = tiling_slab.read(tile_id); + tile.depth_min = u32::MAX; + tile.depth_max = 0; + tile.next_light_index = 0; + tiling_slab.write(tile_id, &tile); + } + + // index of the tile's min depth atomic value in the tiling slab + let min_depth_index = (tile_id + LightTile::OFFSET_OF_DEPTH_MIN).index(); + // index of the tile's max depth atomic value in the tiling slab + let max_depth_index = (tile_id + LightTile::OFFSET_OF_DEPTH_MAX).index(); + + tiling_slab[min_depth_index] = u32::MAX; + tiling_slab[max_depth_index] = 0; + } + } + + fn compute_light_lists( + &self, + geometry_slab: &[u32], + lighting_slab: &[u32], + tiling_slab: &mut [u32], + min_depth_index: usize, + max_depth_index: usize, + ) { + // At this point we know the depth has been computed, so now we can construct the tile's frustum + // in clip space. + let depth_min_u32 = tiling_slab[min_depth_index]; + let depth_max_u32 = tiling_slab[max_depth_index]; + let depth_min = depth_min_u32 as f32 / u32::MAX as f32; + let depth_max = depth_max_u32 as f32 / u32::MAX as f32; + + let resolution = self.descriptor.depth_texture_size.as_vec2(); + let tile_tl_screen_space = self.tile_pos().as_vec2() * resolution; + // The tile's aabb in screen space / viewport space. + // + // This is roughly the frustum. + let tile_aabb_ss = { + let min = tile_tl_screen_space.extend(depth_min); + let max = (tile_tl_screen_space + LightTilingDescriptor::TILE_SIZE.as_vec2()) + .extend(depth_max); + Aabb { min, max } + }; + + let tile_index = self.tile_index(); + let tile_id = self.descriptor.tiles_array.at(tile_index); + let tile_lights_array = tiling_slab.read(tile_id + LightTile::OFFSET_OF_LIGHTS_ARRAY); + let next_light_id = tile_id + LightTile::OFFSET_OF_NEXT_LIGHT_INDEX; + + let camera_id = geometry_slab.read_unchecked( + Id::::new(0) + GeometryDescriptor::OFFSET_OF_CAMERA_ID, + ); + let camera = geometry_slab.read_unchecked(camera_id); + + let frag_light_index = self.frag_index(); + // List of all analytical lights in the scene + let analytical_lights_array = lighting_slab.read_unchecked( + Id::::new(0) + + LightingDescriptor::OFFSET_OF_ANALYTICAL_LIGHTS_ARRAY, + ); + // The number of fragments in one tile + let count_of_fragments_in_one_tile = + (LightTilingDescriptor::TILE_SIZE.x * LightTilingDescriptor::TILE_SIZE.y) as usize; + // Each invocation will calculate a few lights' contribution to the tile, until all lights + // have been visited + for step in 0..(analytical_lights_array.len() / count_of_fragments_in_one_tile) + 1 { + let light_index = step * count_of_fragments_in_one_tile + frag_light_index; + if light_index >= analytical_lights_array.len() { + break; + } + let light_id = lighting_slab.read_unchecked(analytical_lights_array.at(light_index)); + let light = lighting_slab.read_unchecked(light_id); + let transform = geometry_slab.read(light.transform_id); + // let should_add = match light.light_type { + // LightStyle::Directional => true, + // LightStyle::Point => { + // let point_light = lighting_slab.read(light.into_point_id()); + // let center = Mat4::from(transform).transform_point3(point_light.position); + // let radius = point_light.radius_of_illumination(1.0); + // let sphere = BoundingSphere::new(center, radius); + // let aabb_ss = sphere.project_onto_viewport(&camera, resolution); + // aabb_ss.intersects_aabb(&tile_aabb_ss) + // } + // LightStyle::Spot => false, + // }; + + // if should_add { + let next_index = unsafe { + spirv_std::arch::atomic_i_increment::< + u32, + { spirv_std::memory::Scope::Workgroup as u32 }, + { spirv_std::memory::Semantics::WORKGROUP_MEMORY.bits() }, + >(&mut tiling_slab[(next_light_id).index()]) + }; + if next_index as usize >= tile_lights_array.len() { + break; + } + // tiling_slab[next_index as usize] = light_id.inner(); + // } + } + } + + // TODO: think about breaking the light tiling "compute tiles" shader up into sub-shaders. + // It would also be possible to join or parallelize some of this work with frustum culling + // and occlusion culling. + fn compute_tiles( + &self, + depth_texture: &impl Fetch, + geometry_slab: &[u32], + lighting_slab: &[u32], + tiling_slab: &mut [u32], + ) { + self.clear_tiles(tiling_slab); + unsafe { + spirv_std::arch::workgroup_memory_barrier_with_group_sync(); + } + let (min_index, max_index) = + self.compute_min_and_max_depth(depth_texture, lighting_slab, tiling_slab); + unsafe { + spirv_std::arch::workgroup_memory_barrier_with_group_sync(); + } + self.compute_light_lists( + geometry_slab, + lighting_slab, + tiling_slab, + min_index, + max_index, + ); + } +} + +pub fn light_tiling_compute_tiles_impl( + geometry_slab: &[u32], + lighting_slab: &[u32], + tiling_slab: &mut [u32], + depth_texture: &impl Fetch, + global_id: UVec3, +) { + let descriptor = tiling_slab.read(Id::::new(0)); + let invocation = LightTilingInvocation::new(global_id, descriptor); + if invocation.should_invoke() { + invocation.compute_tiles(depth_texture, geometry_slab, lighting_slab, tiling_slab); + } +} + +/// Light culling compute shader, **without** a multisampled depth texture. +// TODO: this shader does not need the geometry slab, as the size is held in the +// tiling slab. +#[spirv(compute(threads(16, 16, 1)))] +pub fn light_tiling_compute_tiles( + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32], + #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] lighting_slab: &[u32], + #[spirv(storage_buffer, descriptor_set = 0, binding = 2)] tiling_slab: &mut [u32], + #[spirv(descriptor_set = 0, binding = 3)] depth_texture: &::Texture, + #[spirv(global_invocation_id)] global_id: UVec3, +) { + light_tiling_compute_tiles_impl( + geometry_slab, + lighting_slab, + tiling_slab, + depth_texture, + global_id, + ) +} + +/// Light culling compute shader, with a multisampled depth texture. +#[spirv(compute(threads(16, 16, 1)))] +pub fn light_tiling_compute_tiles_multisampled( + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32], + #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] lighting_slab: &[u32], + #[spirv(storage_buffer, descriptor_set = 0, binding = 2)] tiling_slab: &mut [u32], + #[spirv(descriptor_set = 0, binding = 3)] + depth_texture: &::Texture, + #[spirv(global_invocation_id)] global_id: UVec3, +) { + light_tiling_compute_tiles_impl( + geometry_slab, + lighting_slab, + tiling_slab, + depth_texture, + global_id, + ) +} + #[cfg(test)] mod test { use super::*; @@ -790,4 +1204,65 @@ mod test { "should be outside" ); } + + #[test] + fn screen_space_to_clip_space_sanity() { + let resolution = Vec2::new(800.0, 600.0); + let tl = Vec2::new(0.0, 0.0); + let tr = Vec2::new(resolution.x, 0.0); + let bl = Vec2::new(0.0, resolution.y); + let br = resolution; + assert_eq!( + Vec2::new(-1.0, 1.0), + screen_space_to_clip_space(resolution, tl) + ); + assert_eq!( + Vec2::new(1.0, 1.0), + screen_space_to_clip_space(resolution, tr) + ); + assert_eq!( + Vec2::new(-1.0, -1.0), + screen_space_to_clip_space(resolution, bl) + ); + assert_eq!( + Vec2::new(1.0, -1.0), + screen_space_to_clip_space(resolution, br) + ); + } + + #[test] + fn light_tile_fragment_indices() { + let descriptor = LightTilingDescriptor { + depth_texture_size: UVec2::splat(200), + tiles_array: Default::default(), + }; + assert_eq!( + 0, + LightTilingInvocation::new(UVec3::ZERO, descriptor).frag_index() + ); + assert_eq!( + 1, + LightTilingInvocation::new(UVec3::new(1, 0, 0), descriptor).frag_index() + ); + assert_eq!( + 2, + LightTilingInvocation::new(UVec3::new(2, 0, 0), descriptor).frag_index() + ); + assert_eq!( + 3, + LightTilingInvocation::new(UVec3::new(3, 0, 0), descriptor).frag_index() + ); + assert_eq!( + 16, + LightTilingInvocation::new(UVec3::new(0, 1, 0), descriptor).frag_index() + ); + assert_eq!( + 17, + LightTilingInvocation::new(UVec3::new(1, 1, 0), descriptor).frag_index() + ); + assert_eq!( + 18, + LightTilingInvocation::new(UVec3::new(2, 1, 0), descriptor).frag_index() + ); + } } diff --git a/crates/renderling/src/light/cpu.rs b/crates/renderling/src/light/cpu.rs index b8d5991e..929f0cf5 100644 --- a/crates/renderling/src/light/cpu.rs +++ b/crates/renderling/src/light/cpu.rs @@ -248,8 +248,6 @@ impl LightingBindGroupLayoutEntries { } impl Lighting { - const LABEL: Option<&str> = Some("lighting"); - /// Create the atlas used to store all shadow maps. fn create_shadow_map_atlas( light_slab: &SlabAllocator, @@ -267,23 +265,10 @@ impl Lighting { ) } - fn create_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { - let LightingBindGroupLayoutEntries { - light_slab, - shadow_map_image, - shadow_map_sampler, - } = LightingBindGroupLayoutEntries::new(0); - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Self::LABEL, - entries: &[light_slab, shadow_map_image, shadow_map_sampler], - }) - } - /// Create a new [`Lighting`] manager. - pub fn new(geometry: &Geometry) -> Self { + pub fn new(atlas_size: wgpu::Extent3d, geometry: &Geometry) -> Self { let runtime = geometry.runtime(); - let light_slab = - SlabAllocator::new_with_label(runtime, wgpu::BufferUsages::empty(), Some("light-slab")); + let light_slab = SlabAllocator::new(runtime, "light-slab", wgpu::BufferUsages::empty()); let lighting_descriptor = light_slab.new_value(LightingDescriptor::default()); let light_slab_buffer = light_slab.commit(); let shadow_map_update_bindgroup_layout: Arc<_> = @@ -292,15 +277,7 @@ impl Lighting { ShadowMap::create_update_pipeline(&runtime.device, &shadow_map_update_bindgroup_layout) .into(); Self { - shadow_map_atlas: Self::create_shadow_map_atlas( - &light_slab, - // TODO: make the shadow map atlas size configurable - wgpu::Extent3d { - width: 1024, - height: 1024, - depth_or_array_layers: 4, - }, - ), + shadow_map_atlas: Self::create_shadow_map_atlas(&light_slab, atlas_size), analytical_lights: Default::default(), analytical_lights_array: Arc::new(Mutex::new(light_slab.new_array([]))), geometry_slab: geometry.slab_allocator().clone(), @@ -435,12 +412,19 @@ impl Lighting { .new_array(guard.iter().map(|bundle| bundle.light.id())); } } - self.lighting_descriptor.set(LightingDescriptor { - analytical_lights_array: self.analytical_lights_array.lock().unwrap().array(), - shadow_map_atlas_descriptor_id: self.shadow_map_atlas.descriptor_id(), - update_shadow_map_id: Id::NONE, - update_shadow_map_texture_index: 0, - }); + self.lighting_descriptor.modify( + |LightingDescriptor { + analytical_lights_array, + shadow_map_atlas_descriptor_id, + update_shadow_map_id, + update_shadow_map_texture_index, + }| { + *analytical_lights_array = self.analytical_lights_array.lock().unwrap().array(); + *shadow_map_atlas_descriptor_id = self.shadow_map_atlas.descriptor_id(); + *update_shadow_map_id = Id::NONE; + *update_shadow_map_texture_index = 0; + }, + ); self.light_slab.commit() } } @@ -448,9 +432,37 @@ impl Lighting { #[cfg(test)] mod test { - use glam::Vec3; + use core::time::Duration; + use std::time::Instant; + + use craballoc::runtime::CpuRuntime; + use crabslab::{Array, CpuSlab, Slab}; + use glam::{UVec3, Vec3, Vec4, Vec4Swizzles}; + use plotters::{ + chart::{ChartBuilder, SeriesLabelPosition}, + prelude::{ + BitMapBackend, Circle, EmptyElement, IntoDrawingArea, IntoSegmentedCoord, PathElement, + Text, + }, + series::{Histogram, LineSeries, PointSeries}, + style::{Color, IntoFont, ShapeStyle}, + }; + use spirv_std::num_traits::Zero; - use crate::{light::SpotLightCalculation, prelude::Transform}; + use crate::{ + bvol::BoundingBox, + camera::Camera, + draw::DrawIndirectArgs, + geometry::GeometryDescriptor, + light::{ + LightTile, LightTiling, LightTilingDescriptor, LightTilingInvocation, + SpotLightCalculation, + }, + math::GpuRng, + pbr::Material, + prelude::Transform, + stage::{Renderlet, Stage, Vertex}, + }; use super::*; @@ -522,7 +534,7 @@ mod test { let m = 32.0; let (w, h) = (16.0f32 * m, 9.0 * m); let ctx = crate::Context::headless(w as u32, h as u32); - let mut stage = ctx.new_stage().with_msaa_sample_count(4); + let stage = ctx.new_stage().with_msaa_sample_count(4); let doc = stage .load_gltf_document_from_path( crate::test::workspace_dir() @@ -539,7 +551,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); let img = frame.read_image().unwrap(); - img_diff::assert_img_eq("lights/spot_lights/one.png", img); + img_diff::assert_img_eq("light/spot_lights/one.png", img); frame.present(); } @@ -552,7 +564,7 @@ mod test { let w = 800.0; let h = 800.0; let ctx = crate::Context::headless(w as u32, h as u32); - let mut stage = ctx + let stage = ctx .new_stage() .with_lighting(true) .with_msaa_sample_count(4); @@ -565,11 +577,6 @@ mod test { ) .unwrap(); let camera = doc.cameras.first().unwrap(); - // TODO: investigate using the camera's aspect for any frame size. - // A `TextureView` of the frame could be created that renders to the frame - // within the camera's expected aspect ratio. - // - // We'd probably need to constrain rendering to one camera, though. camera .as_ref() .modify(|cam| cam.set_projection(crate::camera::perspective(w, h))); @@ -584,7 +591,534 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); let img = frame.read_image().unwrap(); - img_diff::assert_img_eq("lights/spot_lights/frame.png", img); + img_diff::assert_img_eq("light/spot_lights/frame.png", img); + frame.present(); + } + + #[test] + fn light_tiling_light_bounds() { + let magnification = 8; + let w = 16.0 * 2.0f32.powi(magnification); + let h = 9.0 * 2.0f32.powi(magnification); + let ctx = crate::Context::headless(w as u32, h as u32); + let stage = ctx.new_stage().with_msaa_sample_count(4); + let doc = stage + .load_gltf_document_from_path( + crate::test::workspace_dir() + .join("gltf") + .join("light_tiling_test.glb"), + ) + .unwrap(); + let camera = doc.cameras.first().unwrap(); + + stage.use_camera(camera); + + let _lights = crate::test::make_two_directional_light_setup(&stage); + + // Here we only want to render the bounding boxes of the renderlets, + // so mark the renderlets themeselves invisible + doc.renderlets_iter().for_each(|hy_rend| { + hy_rend.modify(|r| { + r.visible = false; + }); + }); + + let colors = [0x6DE1D2FF, 0xFFD63AFF, 0x6DE1D2FF, 0xF75A5AFF].map(|albedo_factor| { + stage.new_material(Material { + albedo_factor: { + let mut color = crate::math::hex_to_vec4(albedo_factor); + crate::color::linear_xfer_vec4(&mut color); + color + }, + ..Default::default() + }) + }); + let mut resources = vec![]; + for (i, node) in doc.nodes.iter().enumerate() { + if node.mesh.is_none() { + continue; + } + let transform = Mat4::from(node.transform.get_global_transform()); + if let Some(mesh_index) = node.mesh { + log::info!("mesh: {}", node.name.as_deref().unwrap_or("unknown")); + let mesh = &doc.meshes[mesh_index]; + for prim in mesh.primitives.iter() { + let (min, max) = prim.bounding_box; + let min = transform.transform_point3(min); + let max = transform.transform_point3(max); + let bb = BoundingBox::from_min_max(min, max); + if bb.half_extent.min_element().is_zero() { + log::warn!("bounding box is not a volume, skipping"); + continue; + } + log::info!("min: {min}, max: {max}"); + resources.push( + stage + .builder() + .with_vertices({ + bb.get_mesh() + .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)) + }) + .with_material_id(colors[i % colors.len()].id()) + .build(), + ); + } + } + } + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img = frame.read_image().unwrap(); + img_diff::save("light/tiling/bounds.png", img); + frame.present(); + } + + fn gen_vec3(prng: &mut GpuRng) -> Vec3 { + let x = prng.gen_f32(-120.0, 120.0); + let y = prng.gen_f32(0.0, 80.0); + let z = prng.gen_f32(-120.0, 120.0); + Vec3::new(x, y, z) + } + + fn gen_light( + stage: &Stage, + prng: &mut GpuRng, + bounding_boxes: &[BoundingBox], + ) -> ( + Hybrid, + HybridArray, + Hybrid, + AnalyticalLightBundle, + Hybrid, + ) { + let mut position = gen_vec3(prng); + while bounding_boxes.iter().any(|bb| bb.contains_point(position)) { + position = gen_vec3(prng); + } + + let color = Vec4::new( + prng.gen_f32(0.0, 1.0), + prng.gen_f32(0.0, 1.0), + prng.gen_f32(0.0, 1.0), + 1.0, + ); + + let scale = prng.gen_f32(0.1, 1.0); + + let light_bb = BoundingBox { + center: Vec3::ZERO, + half_extent: Vec3::new(scale, scale, scale) * 0.5, + }; + + // let inner_cutoff = prng.gen_f32(0.04, 0.09); + // let outer_cutoff = prng.gen_f32(inner_cutoff, 0.16); + + // Also make a renderlet for the light, so we can see where it is. + let rez = stage + .builder() + .with_transform(Transform { + translation: position, + ..Default::default() + }) + .with_vertices( + light_bb + .get_mesh() + .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)), + ) + .with_material(Material { + albedo_factor: color, + has_lighting: false, + emissive_factor: color.xyz(), + emissive_strength_multiplier: 100.0, + ..Default::default() + }) + .suffix({ + let intensity = scale * 100.0; + // let light_descriptor = SpotLightDescriptor { + // position, + // color, + // intensity, + // direction: Vec3::NEG_Y, + // inner_cutoff, + // outer_cutoff, + // }; + let light_descriptor = PointLightDescriptor { + position, + color, + intensity, + }; + let nested_transform = stage.new_nested_transform(); + nested_transform.modify(|t| { + t.translation = position; + }); + stage.new_analytical_light(light_descriptor, None) + }) + .build(); + rez + } + + fn size() -> UVec2 { + UVec2::new( + (10.0 * 2.0f32.powi(8)) as u32, + (9.0 * 2.0f32.powi(8)) as u32, + ) + } + + fn make_camera() -> Camera { + let size = size(); + Camera::new( + Mat4::perspective_rh( + std::f32::consts::FRAC_PI_4, + size.x as f32 / size.y as f32, + 50.0, + 600.0, + ), + Mat4::look_at_rh(Vec3::new(250.0, 200.0, 250.0), Vec3::ZERO, Vec3::Y), + ) + } + + #[test] + fn light_tiling_positions() { + let w = 32; + let h = 32; + let slab = SlabAllocator::new(CpuRuntime, "test", ()); + let descriptor = slab.new_value(LightTilingDescriptor { + depth_texture_size: UVec2::new(w, h), + ..Default::default() + }); + let tiled_size = descriptor.get().tile_dimensions(); + println!("tiled_size: {tiled_size}"); + let tiles = slab.new_array(vec![ + LightTile::default(); + (tiled_size.x * tiled_size.y) as usize + ]); + descriptor.modify(|d| { + d.tiles_array = tiles.array(); + }); + let desc = descriptor.get(); + let mut tiling_slab = slab.commit().as_vec().clone(); + + let mut img = image::RgbImage::new(w, h); + let mut light_img = image::RgbImage::new(w, h); + for x in 0..w { + for y in 0..h { + let global_id = UVec3::new(x, y, 0); + let invocation = LightTilingInvocation::new(global_id, descriptor.get()); + if invocation.should_invoke() { + let pixel = img.get_pixel_mut(x, y); + let r = (x as f32 / w as f32 * 255.0) as u8; + let g = (y as f32 / h as f32 * 255.0) as u8; + pixel.0 = [r, g, 0x00]; + + if invocation.frag_pos_is_tile_corner() { + pixel.0[0] = 0xFF - pixel.0[0]; + pixel.0[1] = 0xFF - pixel.0[1]; + pixel.0[2] = 0xFF - pixel.0[2]; + + let tile_dimensions = tiled_size; + let tile_index = invocation.tile_index(); + println!("frag_pos: {}", invocation.frag_pos()); + println!("tile_pos: {}", invocation.tile_pos()); + println!("tile_index: {tile_index}"); + let num_tiles = tile_dimensions.x * tile_dimensions.y; + + // index of the tile's min depth atomic value in the tiling slab + let min_depth_index = (invocation.descriptor.tiles_array.at(tile_index) + + LightTile::OFFSET_OF_DEPTH_MIN) + .index(); + // index of the tile's max depth atomic value in the tiling slab + let max_depth_index = (invocation.descriptor.tiles_array.at(tile_index) + + LightTile::OFFSET_OF_DEPTH_MAX) + .index(); + + let percent = tile_index as f32 / num_tiles as f32; //frag_pos.x as f32 / self.descriptor.depth_texture_size.x as f32; + tiling_slab[min_depth_index] = (percent * u32::MAX as f32) as u32; + tiling_slab[max_depth_index] = u32::MAX; //(percent * u32::MAX as f32) as u32; + } + + let pixel = light_img.get_pixel_mut(x, y); + let index = invocation.frag_index(); + println!("index: {index}"); + let value = crate::math::scaled_f32_to_u8(index as f32 / (16.0 * 16.0)); + pixel.0[0] = value; + pixel.0[1] = value; + pixel.0[2] = value; + } + } + } + img_diff::save("light/tiling/positions.png", img); + img_diff::save("light/tiling/frag_pos.png", light_img); + + let (mins, maxs) = tiling_slab + .read_vec(desc.tiles_array) + .into_iter() + .map(|tile| { + ( + crate::math::scaled_u32_to_u8(tile.depth_min), + crate::math::scaled_u32_to_u8(tile.depth_max), + ) + }) + .unzip(); + let mins_img = image::GrayImage::from_vec(tiled_size.x, tiled_size.y, mins).unwrap(); + img_diff::save("light/tiling/positions-mins.png", mins_img); + let maxs_img = image::GrayImage::from_vec(tiled_size.x, tiled_size.y, maxs).unwrap(); + img_diff::save("light/tiling/positions-maxs.png", maxs_img); + } + + #[test] + /// Test the light tiling feature. + fn light_tiling_sanity() { + let _ = env_logger::builder().is_test(true).try_init(); + let size = size(); + let ctx = crate::Context::headless(size.x, size.y); + let stage = ctx + .new_stage() + .with_lighting(false) + .with_bloom(true) + .with_bloom_mix_strength(0.5); + + let doc = stage + .load_gltf_document_from_path( + crate::test::workspace_dir() + .join("gltf") + .join("light_tiling_test.glb"), + ) + .unwrap(); + + let camera = stage.new_camera(make_camera()); + stage.use_camera(camera); + snapshot(&ctx, &stage, "light/tiling/1-no-lighting.png"); + + stage.set_has_lighting(true); + + let moonlight = doc.lights.first().unwrap(); + let _shadow = { + let sm = stage + .new_shadow_map(moonlight, UVec2::splat(1024), 0.1, 256.0) + .unwrap(); + sm.shadowmap_descriptor.modify(|d| { + d.bias_min = 0.0; + d.bias_max = 0.0; + d.pcf_samples = 2; + }); + sm.update(&stage, doc.renderlets_iter()).unwrap(); + sm + }; + snapshot(&ctx, &stage, "light/tiling/2-before-lights.png"); + + crate::test::capture_gpu_frame(&ctx, "light/tiling/2.gputrace", || { + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + frame.present(); + }); + + let mut bounding_boxes = vec![]; + for node in doc.nodes.iter() { + if node.mesh.is_none() { + continue; + } + let transform = Mat4::from(node.transform.get_global_transform()); + if let Some(mesh_index) = node.mesh { + let mesh = &doc.meshes[mesh_index]; + for prim in mesh.primitives.iter() { + let (min, max) = prim.bounding_box; + let min = transform.transform_point3(min); + let max = transform.transform_point3(max); + let bb = BoundingBox::from_min_max(min, max); + if bb.half_extent.min_element().is_zero() { + continue; + } + bounding_boxes.push(bb); + } + } + } + log::info!("have {} bounding boxes", bounding_boxes.len()); + + let mut prng = crate::math::GpuRng::new(666); + let mut lights = vec![]; + + for _ in 0..MAX_LIGHTS { + lights.push(gen_light(&stage, &mut prng, &bounding_boxes)); + } + snapshot(&ctx, &stage, "light/tiling/3-after-lights.png"); + + // Remove the light meshes + for (_, _, _, _, renderlet) in lights.iter() { + stage.remove_renderlet(renderlet); + } + snapshot(&ctx, &stage, "light/tiling/4-after-lights-no-meshes.png"); + + let tiling = LightTiling::new(ctx.runtime(), false, size, 32); + let desc = tiling.descriptor().get(); + let depth = stage.depth_texture.read().unwrap(); + let mut depth_img = crate::texture::read_depth_texture_f32( + ctx.runtime(), + size.x as usize, + size.y as usize, + depth.texture.as_ref(), + ) + .unwrap(); + // let mut depth_img = crate::texture::read_depth_texture_to_image( + // ctx.runtime(), + // size.x as usize, + // size.y as usize, + // &depth.texture, + // ) + // .unwrap(); + // img_diff::normalize_gray_img(&mut depth_img); + img_diff::save("light/tiling/5-depth.png", depth_img); + tiling.run(&stage.geometry.commit(), &stage.lighting.commit(), &depth); + let (mut mins_img, mut maxs_img, mut lights_img) = + futures_lite::future::block_on(tiling.read_images()); + img_diff::normalize_gray_img(&mut mins_img); + img_diff::normalize_gray_img(&mut maxs_img); + img_diff::normalize_gray_img(&mut lights_img); + img_diff::save("light/tiling/5-mins.png", mins_img); + img_diff::save("light/tiling/5-maxs.png", maxs_img); + img_diff::save("light/tiling/5-lights.png", lights_img); + + return; + log::info!("running stats"); + + // Stats + let mut stats = LightTilingStats::default(); + for number_of_lights in [ + 1, + MAX_LIGHTS / 8, + MAX_LIGHTS / 4, + MAX_LIGHTS / 2, + ((MAX_LIGHTS / 2) + MAX_LIGHTS) / 2, + MAX_LIGHTS, + ] { + let mut run = LightTilingStatsRun { + number_of_lights, + iterations: vec![], + }; + + for (i, (_, _, _, light, _)) in lights.iter().enumerate() { + stage.remove_light(light); + if i < number_of_lights { + stage.add_light(light); + } + } + + const NUM_RUNS: usize = 2; + for i in 0..NUM_RUNS { + log::info!("{number_of_lights} {i} running"); + let start = Instant::now(); + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + frame.present(); + ctx.get_device().poll(wgpu::Maintain::wait()); + let duration = start.elapsed(); + run.iterations.push(duration); + } + stats.runs.push(run); + } + plot(stats); + } + + fn snapshot(ctx: &crate::Context, stage: &Stage, path: &str) { + let frame = ctx.get_next_frame().unwrap(); + let start = std::time::Instant::now(); + stage.render(&frame.view()); + let elapsed = start.elapsed(); + log::info!("shapshot: {}s '{path}'", elapsed.as_secs_f32()); + let img = frame.read_image().unwrap(); + img_diff::save(path, img); frame.present(); } + + const MAX_LIGHTS: usize = 1024; + + struct LightTilingStatsRun { + number_of_lights: usize, + iterations: Vec, + } + + impl LightTilingStatsRun { + fn avg_frame_time(&self) -> f32 { + let total: Duration = self.iterations.iter().sum(); + total.as_secs_f32() / self.iterations.len() as f32 + } + } + + #[derive(Default)] + struct LightTilingStats { + runs: Vec, + } + + fn plot(stats: LightTilingStats) { + let path = crate::test::workspace_dir().join("test_output/lights/tiling/frame-time.png"); + let root_drawing_area = BitMapBackend::new(&path, (800, 600)).into_drawing_area(); + root_drawing_area.fill(&plotters::style::WHITE).unwrap(); + + let mut chart = ChartBuilder::on(&root_drawing_area) + .caption( + "Renderling lighting frame time", + ("sans-serif", 50).into_font(), + ) + .margin(30) + .margin_right(100) + .x_label_area_size(30) + .y_label_area_size(30) + .build_cartesian_2d( + 0..MAX_LIGHTS + 1, + 0.0..stats + .runs + .iter() + .map(|r| r.avg_frame_time()) + .max_by(|a, b| a.total_cmp(b)) + .unwrap_or_default(), + ) + .unwrap(); + chart + .configure_mesh() + .x_desc("number of lights") + .y_desc("avg fps") + .draw() + .unwrap(); + + chart + .draw_series(LineSeries::new( + stats + .runs + .iter() + .map(|r| (r.number_of_lights, r.avg_frame_time())), + plotters::style::RED, + )) + .unwrap() + .label("without-tiling") + .legend(|(x, y)| { + PathElement::new(vec![(x, y), (x + 20, y)], plotters::style::RED.filled()) + }); + chart + .draw_series(PointSeries::of_element( + stats + .runs + .iter() + .map(|r| (r.number_of_lights, r.avg_frame_time())), + 5, + ShapeStyle::from(&plotters::style::RED).filled(), + &|(num_lights, seconds_per_frame), size, style| { + EmptyElement::at((num_lights, seconds_per_frame)) + + Circle::new((0, 0), size, style) + + Text::new( + format!("({num_lights}, {:.2} fps)", 1.0 / seconds_per_frame), + (0, 15), + ("sans-serif", 15), + ) + }, + )) + .unwrap(); + + chart + .configure_series_labels() + .position(SeriesLabelPosition::LowerRight) + .margin(20) + .label_font(("sans-serif", 20)) + .draw() + .unwrap(); + root_drawing_area.present().unwrap(); + } } diff --git a/crates/renderling/src/light/shadow_map.rs b/crates/renderling/src/light/shadow_map.rs index 910edbbc..c6dfe5ee 100644 --- a/crates/renderling/src/light/shadow_map.rs +++ b/crates/renderling/src/light/shadow_map.rs @@ -620,7 +620,7 @@ mod test { let w = 800.0; let h = 800.0; let ctx = crate::Context::headless(w as u32, h as u32); - let mut stage = ctx + let stage = ctx .new_stage() .with_lighting(true) .with_background_color(Vec3::splat(0.05087).extend(1.0)) diff --git a/crates/renderling/src/light/tiling.rs b/crates/renderling/src/light/tiling.rs new file mode 100644 index 00000000..68becbb5 --- /dev/null +++ b/crates/renderling/src/light/tiling.rs @@ -0,0 +1,273 @@ +//! Implementation of light tiling. +//! +//! For more info on what light tiling _is_, see +//! [this blog post](https://renderling.xyz/articles/live/light_tiling.html). +// TODO: Auto-generate more pipeline linkage like layout, bindgroups and pipeline itself. + +use core::sync::atomic::AtomicUsize; +use std::sync::Arc; + +use craballoc::{ + runtime::WgpuRuntime, + slab::{SlabAllocator, SlabAllocatorError, SlabBuffer}, + value::{GpuArray, Hybrid}, +}; +use crabslab::{Id, Slab}; +use glam::UVec2; +use snafu::OptionExt; + +use crate::bindgroup::ManagedBindGroup; + +use super::{LightTile, LightTilingDescriptor}; + +pub struct LightTiling { + // depth_pre_pass_pipeline: Arc, + tiling_slab: SlabAllocator, + tiling_descriptor: Hybrid, + tiles: GpuArray, + bind_group_creation_time: Arc, + depth_texture_id: Arc, + compute_tiles_bind_group_layout: Arc, + compute_tiles_bind_group: ManagedBindGroup, + compute_tiles_pipeline: Arc, +} + +impl LightTiling { + fn create_compute_tiles_pipeline( + device: &wgpu::Device, + multisampled: bool, + ) -> ( + wgpu::ComputePipeline, + wgpu::PipelineLayout, + wgpu::BindGroupLayout, + ) { + let label = Some("light-tiling-compute-tiles"); + let module = if multisampled { + crate::linkage::light_tiling_compute_tiles_multisampled::linkage(device) + } else { + crate::linkage::light_tiling_compute_tiles::linkage(device) + }; + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label, + entries: &[ + // Geometry slab + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + // Lighting slab + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + // Tiling slab + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: false }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + // Depth texture + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Depth, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled, + }, + count: None, + }, + ], + }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label, + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + let compute_tiles_pipeline = + device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { + label, + layout: Some(&pipeline_layout), + module: &module.module, + entry_point: Some(module.entry_point), + compilation_options: wgpu::PipelineCompilationOptions::default(), + cache: None, + }); + (compute_tiles_pipeline, pipeline_layout, bind_group_layout) + } + + pub(crate) fn descriptor(&self) -> &Hybrid { + &self.tiling_descriptor + } + + pub fn new( + runtime: impl AsRef, + multisampled: bool, + depth_texture_size: UVec2, + max_lights_per_tile: usize, + ) -> Self { + let runtime = runtime.as_ref(); + let (compute_tiles_pipeline, _, compute_tiles_bind_group_layout) = + Self::create_compute_tiles_pipeline(&runtime.device, multisampled); + let tiling_slab = SlabAllocator::new(runtime, "tiling", wgpu::BufferUsages::empty()); + let desc = LightTilingDescriptor { + depth_texture_size, + ..Default::default() + }; + let tiling_descriptor = tiling_slab.new_value(desc); + let tiled_size = desc.tile_dimensions(); + let mut tiles = Vec::new(); + for _ in 0..tiled_size.x * tiled_size.y { + let lights = tiling_slab.new_array(vec![Id::NONE; max_lights_per_tile]); + tiles.push(LightTile { + lights_array: lights.array(), + ..Default::default() + }); + } + let tiles = tiling_slab.new_array(tiles).into_gpu_only(); + tiling_descriptor.modify(|d| { + d.tiles_array = tiles.array(); + }); + Self { + tiling_slab, + tiling_descriptor, + tiles, + bind_group_creation_time: Default::default(), + depth_texture_id: Default::default(), + compute_tiles_bind_group_layout: compute_tiles_bind_group_layout.into(), + compute_tiles_bind_group: Default::default(), + compute_tiles_pipeline: compute_tiles_pipeline.into(), + } + } + + pub fn run( + &self, + geometry_slab: &SlabBuffer, + lighting_slab: &SlabBuffer, + depth_texture: &crate::texture::Texture, + ) { + let runtime = self.tiling_slab.runtime(); + let depth_texture_size = self.tiling_descriptor.modify(|d| { + d.depth_texture_size = depth_texture.size(); + d.depth_texture_size + }); + let tiling_slab_buffer = self.tiling_slab.commit(); + let label = Some("light-tiling-compute-tiles"); + let mut encoder = runtime + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); + { + let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { + label, + timestamp_writes: None, + }); + compute_pass.set_pipeline(&self.compute_tiles_pipeline); + + // UNWRAP: safe because we know there are elements + let latest_buffer_creation = [ + tiling_slab_buffer.creation_time(), + geometry_slab.creation_time(), + lighting_slab.creation_time(), + ] + .into_iter() + .max() + .unwrap(); + let prev_buffer_creation = self + .bind_group_creation_time + .swap(latest_buffer_creation, std::sync::atomic::Ordering::Relaxed); + let prev_depth_texture_id = self + .depth_texture_id + .swap(depth_texture.id(), std::sync::atomic::Ordering::Relaxed); + let should_invalidate = tiling_slab_buffer.is_new_this_commit() + || prev_buffer_creation < latest_buffer_creation + || prev_depth_texture_id < depth_texture.id(); + let bind_group = self.compute_tiles_bind_group.get(should_invalidate, || { + runtime + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label, + layout: &self.compute_tiles_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: geometry_slab.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: lighting_slab.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: tiling_slab_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::TextureView(&depth_texture.view), + }, + ], + }) + }); + compute_pass.set_bind_group(0, bind_group.as_ref(), &[]); + + let x = depth_texture_size.x / 16 + 1; + let y = depth_texture_size.y / 16 + 1; + let z = 1; + compute_pass.dispatch_workgroups(x, y, z); + } + runtime.queue.submit(Some(encoder.finish())); + } + + #[cfg(test)] + pub(crate) async fn read_images( + &self, + ) -> (image::GrayImage, image::GrayImage, image::GrayImage) { + let size = self.tiling_descriptor.get().depth_texture_size / 16; + let slab = self.tiling_slab.read(..).await.unwrap(); + log::info!("tiling slab size: {}", slab.len()); + let desc = slab.read(Id::::new(0)); + log::info!("desc: {desc:#?}"); + assert_eq!(size.x * size.y, desc.tiles_array.len() as u32); + let (mins, maxs, lights) = slab + .read_vec(desc.tiles_array) + .into_iter() + .map(|tile| { + ( + crate::math::scaled_u32_to_u8(tile.depth_min), + crate::math::scaled_u32_to_u8(tile.depth_max), + crate::math::scaled_f32_to_u8( + tile.next_light_index as f32 / tile.lights_array.len() as f32, + ), + ) + }) + .fold( + (vec![], vec![], vec![]), + |(mut ays, mut bees, mut cees), (a, b, c)| { + ays.push(a); + bees.push(b); + cees.push(c); + (ays, bees, cees) + }, + ); + let mins_img = image::GrayImage::from_vec(size.x, size.y, mins).unwrap(); + let maxs_img = image::GrayImage::from_vec(size.x, size.y, maxs).unwrap(); + let lights_img = image::GrayImage::from_vec(size.x, size.y, lights).unwrap(); + (mins_img, maxs_img, lights_img) + } +} diff --git a/crates/renderling/src/linkage.rs b/crates/renderling/src/linkage.rs index 4230a81b..261d0996 100644 --- a/crates/renderling/src/linkage.rs +++ b/crates/renderling/src/linkage.rs @@ -25,6 +25,9 @@ pub mod debug_overlay_vertex; pub mod di_convolution_fragment; pub mod generate_mipmap_fragment; pub mod generate_mipmap_vertex; +pub mod light_tiling_compute_tiles; +pub mod light_tiling_compute_tiles_multisampled; +pub mod light_tiling_depth_pre_pass; pub mod prefilter_environment_cubemap_fragment; pub mod prefilter_environment_cubemap_vertex; pub mod renderlet_fragment; diff --git a/crates/renderling/src/linkage/light_tiling_compute_tiles.rs b/crates/renderling/src/linkage/light_tiling_compute_tiles.rs new file mode 100644 index 00000000..5f6e36fa --- /dev/null +++ b/crates/renderling/src/linkage/light_tiling_compute_tiles.rs @@ -0,0 +1,37 @@ +#![allow(dead_code)] +//! Automatically generated by Renderling's `build.rs`. +use crate::linkage::ShaderLinkage; +#[cfg(not(target_arch = "wasm32"))] +mod target { + pub const ENTRY_POINT: &str = "light::light_tiling_compute_tiles"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_spirv!("../../shaders/light-light_tiling_compute_tiles.spv") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!( + "creating native linkage for {}", + "light_tiling_compute_tiles" + ); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +#[cfg(target_arch = "wasm32")] +mod target { + pub const ENTRY_POINT: &str = "lightlight_tiling_compute_tiles"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_wgsl!("../../shaders/light-light_tiling_compute_tiles.wgsl") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!("creating web linkage for {}", "light_tiling_compute_tiles"); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +pub fn linkage(device: &wgpu::Device) -> ShaderLinkage { + target::linkage(device) +} diff --git a/crates/renderling/src/linkage/light_tiling_compute_tiles_multisampled.rs b/crates/renderling/src/linkage/light_tiling_compute_tiles_multisampled.rs new file mode 100644 index 00000000..24ea36f3 --- /dev/null +++ b/crates/renderling/src/linkage/light_tiling_compute_tiles_multisampled.rs @@ -0,0 +1,40 @@ +#![allow(dead_code)] +//! Automatically generated by Renderling's `build.rs`. +use crate::linkage::ShaderLinkage; +#[cfg(not(target_arch = "wasm32"))] +mod target { + pub const ENTRY_POINT: &str = "light::light_tiling_compute_tiles_multisampled"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_spirv!("../../shaders/light-light_tiling_compute_tiles_multisampled.spv") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!( + "creating native linkage for {}", + "light_tiling_compute_tiles_multisampled" + ); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +#[cfg(target_arch = "wasm32")] +mod target { + pub const ENTRY_POINT: &str = "lightlight_tiling_compute_tiles_multisampled"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_wgsl!("../../shaders/light-light_tiling_compute_tiles_multisampled.wgsl") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!( + "creating web linkage for {}", + "light_tiling_compute_tiles_multisampled" + ); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +pub fn linkage(device: &wgpu::Device) -> ShaderLinkage { + target::linkage(device) +} diff --git a/crates/renderling/src/linkage/light_tiling_depth_pre_pass.rs b/crates/renderling/src/linkage/light_tiling_depth_pre_pass.rs new file mode 100644 index 00000000..0423c693 --- /dev/null +++ b/crates/renderling/src/linkage/light_tiling_depth_pre_pass.rs @@ -0,0 +1,37 @@ +#![allow(dead_code)] +//! Automatically generated by Renderling's `build.rs`. +use crate::linkage::ShaderLinkage; +#[cfg(not(target_arch = "wasm32"))] +mod target { + pub const ENTRY_POINT: &str = "light::light_tiling_depth_pre_pass"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_spirv!("../../shaders/light-light_tiling_depth_pre_pass.spv") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!( + "creating native linkage for {}", + "light_tiling_depth_pre_pass" + ); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +#[cfg(target_arch = "wasm32")] +mod target { + pub const ENTRY_POINT: &str = "lightlight_tiling_depth_pre_pass"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_wgsl!("../../shaders/light-light_tiling_depth_pre_pass.wgsl") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!("creating web linkage for {}", "light_tiling_depth_pre_pass"); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +pub fn linkage(device: &wgpu::Device) -> ShaderLinkage { + target::linkage(device) +} diff --git a/crates/renderling/src/material/cpu.rs b/crates/renderling/src/material/cpu.rs index 4a7e97f1..a8ef1a17 100644 --- a/crates/renderling/src/material/cpu.rs +++ b/crates/renderling/src/material/cpu.rs @@ -23,8 +23,7 @@ impl AsRef for Materials { impl Materials { pub fn new(runtime: impl AsRef, atlas_size: wgpu::Extent3d) -> Self { - let slab = - SlabAllocator::new_with_label(runtime, wgpu::BufferUsages::empty(), Some("materials")); + let slab = SlabAllocator::new(runtime, "materials", wgpu::BufferUsages::empty()); let atlas = Atlas::new(&slab, atlas_size, None, Some("materials-atlas"), None); Self { slab, atlas } } diff --git a/crates/renderling/src/math.rs b/crates/renderling/src/math.rs index 530d13ae..cd677c6d 100644 --- a/crates/renderling/src/math.rs +++ b/crates/renderling/src/math.rs @@ -8,13 +8,37 @@ //! Lastly, it provides some constant geometry used in many shaders. use core::ops::Mul; use spirv_std::{ - image::{Cubemap, Image2d, Image2dArray}, + image::{sample_with, Cubemap, Image2d, Image2dArray, ImageWithMethods}, Image, Sampler, }; pub use glam::*; pub use spirv_std::num_traits::{clamp, Float, Zero}; +pub trait Fetch { + type Output; + + fn fetch(&self, coords: Coords) -> Self::Output; +} + +impl Fetch for Image!(2D, type=f32, sampled, depth) { + type Output = Vec4; + + fn fetch(&self, coords: UVec2) -> Self::Output { + self.fetch_with(coords, sample_with::lod(0)) + } +} + +impl Fetch for Image!(2D, type=f32, sampled, depth, multisampled=true) { + type Output = Vec4; + + fn fetch(&self, coords: UVec2) -> Self::Output { + // TODO: check whether this is doing what we think it's doing. + // (We think its doing roughly the same thing as the non-multisampled version above) + self.fetch_with(coords, sample_with::sample_index(0)) + } +} + pub trait IsSampler: Copy + Clone {} impl IsSampler for () {} @@ -88,6 +112,14 @@ mod cpu { /// value when sampled. pub struct ConstTexture(Vec4); + impl Fetch for ConstTexture { + type Output = Vec4; + + fn fetch(&self, _coords: UVec2) -> Self::Output { + self.0 + } + } + impl Sample2d for ConstTexture { type Sampler = (); @@ -133,6 +165,21 @@ mod cpu { } } + impl Fetch for CpuTexture2d + where + P: image::Pixel, + Container: std::ops::Deref, + { + type Output = Vec4; + + fn fetch(&self, coords: UVec2) -> Self::Output { + let x = coords.x.clamp(0, self.image.width() - 1); + let y = coords.y.clamp(0, self.image.height() - 1); + let p = self.image.get_pixel(x, y); + (self.convert_fn)(p) + } + } + impl Sample2d for CpuTexture2d where P: image::Pixel, @@ -143,14 +190,9 @@ mod cpu { fn sample_by_lod(&self, _sampler: Self::Sampler, uv: glam::Vec2, _lod: f32) -> Vec4 { // TODO: lerp the CPU texture sampling // TODO: use configurable wrap mode on CPU sampling - let px = uv.x.clamp(0.0, 1.0) * self.image.width() as f32; - let py = uv.y.clamp(0.0, 1.0) * self.image.height() as f32; - println!("sampling: ({px}, {py})"); - let p = self.image.get_pixel( - px.round().min(self.image.width() as f32) as u32, - py.round().min(self.image.height() as f32) as u32, - ); - (self.convert_fn)(p) + let px = uv.x.clamp(0.0, 1.0) * (self.image.width() as f32 - 1.0); + let py = uv.y.clamp(0.0, 1.0) * (self.image.height() as f32 - 1.0); + self.fetch(UVec2::new(px.round() as u32, py.round() as u32)) } } @@ -219,6 +261,11 @@ mod cpu { pub fn scaled_f32_to_u8(f: f32) -> u8 { (f * 255.0) as u8 } + + /// Convert a u32 in rang 0-u32::MAX to a u8 in rang 0-255. + pub fn scaled_u32_to_u8(u: u32) -> u8 { + ((u as f32 / u32::MAX as f32) * 255.0) as u8 + } } #[cfg(not(target_arch = "spirv"))] pub use cpu::*; @@ -442,7 +489,13 @@ pub fn smoothstep(edge_in: f32, edge_out: f32, mut x: f32) -> f32 { pub fn triangle_face_normal(p1: Vec3, p2: Vec3, p3: Vec3) -> Vec3 { let a = p1 - p2; let b = p1 - p3; - let n: Vec3 = a.cross(b).normalize(); + let n: Vec3 = a.cross(b).alt_norm_or_zero(); + #[cfg(cpu)] + debug_assert_ne!( + Vec3::ZERO, + n, + "normal is zero - p1: {p1}, p2: {p2}, p3: {p3}" + ); n } @@ -590,6 +643,45 @@ pub const fn convex_mesh([p0, p1, p2, p3, p4, p5, p6, p7]: [Vec3; 8]) -> [Vec3; ] } +/// An PCG PRNG that is optimized for GPUs, in that it is fast to evaluate and accepts +/// sequential ids as it's initial state without sacrificing on RNG quality. +/// +/// https://www.reedbeta.com/blog/hash-functions-for-gpu-rendering/ +/// https://jcgt.org/published/0009/03/02/ +/// +/// Thanks to Firestar99 at +/// +pub struct GpuRng(pub u32); + +impl GpuRng { + pub fn new(state: u32) -> GpuRng { + Self(state) + } + + pub fn gen(&mut self) -> u32 { + let state = self.0; + self.0 = if cfg!(gpu) { + self.0 * 747796405 + 2891336453 + } else { + self.0.wrapping_sub(747796405).wrapping_add(2891336453) + }; + let word = (state >> ((state >> 28) + 4)) ^ state; + let word = if cfg!(gpu) { + word * 277803737 + } else { + word.wrapping_mul(277803737) + }; + (word >> 22) ^ word + } + + pub fn gen_f32(&mut self, min: f32, max: f32) -> f32 { + let range = max - min; + let numerator = self.gen(); + let percentage = numerator as f32 / u32::MAX as f32; + min + range * percentage + } +} + #[cfg(test)] mod test { use super::*; diff --git a/crates/renderling/src/skybox/cpu.rs b/crates/renderling/src/skybox/cpu.rs index 878a58a7..c0774873 100644 --- a/crates/renderling/src/skybox/cpu.rs +++ b/crates/renderling/src/skybox/cpu.rs @@ -187,8 +187,7 @@ impl Skybox { let runtime = runtime.as_ref(); log::trace!("creating skybox"); - let slab = - SlabAllocator::new_with_label(runtime, wgpu::BufferUsages::VERTEX, Some("skybox-slab")); + let slab = SlabAllocator::new(runtime, "skybox-slab", wgpu::BufferUsages::VERTEX); let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 10.0); let camera = slab.new_value(Camera::default().with_projection(proj)); diff --git a/crates/renderling/src/stage.rs b/crates/renderling/src/stage.rs index 2df876d6..d077b6e6 100644 --- a/crates/renderling/src/stage.rs +++ b/crates/renderling/src/stage.rs @@ -81,7 +81,7 @@ impl Skin { /// For more info on morph targets, see /// #[derive(Clone, Copy, Default, PartialEq, SlabItem)] -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[cfg_attr(cpu, derive(Debug))] pub struct MorphTarget { pub position: Vec3, pub normal: Vec3, @@ -183,6 +183,19 @@ impl Vertex { .alt_norm_or_zero() .extend(n_cross_t_dot_s_sign) } + + #[cfg(cpu)] + /// A triangle list mesh of points. + pub fn cube_mesh() -> [Vertex; 36] { + let mut mesh = [Vertex::default(); 36]; + let unit_cube = crate::math::unit_cube(); + debug_assert_eq!(36, unit_cube.len()); + for (i, (position, normal)) in unit_cube.into_iter().enumerate() { + mesh[i].position = position; + mesh[i].normal = normal; + } + mesh + } } /// A draw call used to render some geometry. @@ -460,132 +473,6 @@ pub fn test_i8_i16_extraction( } } -#[cfg(feature = "test_spirv_atomics")] -#[spirv(compute(threads(32)))] -pub fn test_atomic_i_increment( - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] global_index: &mut u32, - #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] times: &u32, -) { - let mut i = 0u32; - loop { - if i >= *times { - break; - } - let _ = unsafe { - spirv_std::arch::atomic_i_increment::< - u32, - { spirv_std::memory::Scope::Workgroup as u32 }, - { spirv_std::memory::Semantics::NONE.bits() }, - >(global_index) - }; - i += 1; - } -} - -#[cfg(feature = "test_spirv_atomics")] -#[spirv(compute(threads(32)))] -pub fn test_atomic_load_and_store( - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] global_index: &mut u32, - #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] times: &u32, -) { - for _ in 0..*times { - let loaded = unsafe { - spirv_std::arch::atomic_load::< - u32, - { spirv_std::memory::Scope::Workgroup as u32 }, - { spirv_std::memory::Semantics::NONE.bits() }, - >(global_index) - }; - unsafe { - spirv_std::arch::atomic_store::< - u32, - { spirv_std::memory::Scope::Workgroup as u32 }, - { spirv_std::memory::Semantics::NONE.bits() }, - >(global_index, loaded + 2) - }; - } -} - -#[cfg(feature = "test_spirv_atomics")] -#[spirv(compute(threads(32)))] -pub fn test_atomic_exchange( - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] global_index: &mut u32, - #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] times: &u32, -) { - let mut n = 0u32; - for _ in 0..*times { - n += unsafe { - spirv_std::arch::atomic_exchange::< - u32, - { spirv_std::memory::Scope::Workgroup as u32 }, - { spirv_std::memory::Semantics::NONE.bits() }, - >(global_index, n) - }; - } -} - -#[cfg(feature = "test_spirv_atomics")] -#[spirv(compute(threads(32)))] -pub fn test_atomic_compare_exchange( - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] global_index: &mut u32, - #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] times: &u32, -) { - for n in 0..*times { - unsafe { - spirv_std::arch::atomic_compare_exchange::< - u32, - { spirv_std::memory::Scope::Workgroup as u32 }, - { spirv_std::memory::Semantics::WORKGROUP_MEMORY.bits() }, - { spirv_std::memory::Semantics::WORKGROUP_MEMORY.bits() }, - >(global_index, n, 3) - }; - } -} - -#[cfg(feature = "test_spirv_atomics")] -#[spirv(compute(threads(32)))] -pub fn test_atomic_i_decrement( - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] global_index: &mut u32, - #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] output: &mut [u32], -) { - loop { - let i = unsafe { - spirv_std::arch::atomic_i_decrement::< - u32, - { spirv_std::memory::Scope::Workgroup as u32 }, - { spirv_std::memory::Semantics::NONE.bits() }, - >(global_index) - }; - output[i as usize] = i; - if i == 0 { - break; - } - } -} - -#[cfg(feature = "test_spirv_atomics")] -#[spirv(compute(threads(32)))] -pub fn test_atomic_i_add_sub( - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] global_index: &mut u32, - #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] output: &mut [u32], -) { - let i = unsafe { - spirv_std::arch::atomic_i_add::< - u32, - { spirv_std::memory::Scope::Workgroup as u32 }, - { spirv_std::memory::Semantics::NONE.bits() }, - >(global_index, 2) - }; - - output[i as usize] = unsafe { - spirv_std::arch::atomic_i_sub::< - u32, - { spirv_std::memory::Scope::Workgroup as u32 }, - { spirv_std::memory::Semantics::NONE.bits() }, - >(global_index, i) - }; -} - #[cfg(test)] mod test { use craballoc::{prelude::SlabAllocator, runtime::CpuRuntime}; @@ -629,7 +516,7 @@ mod test { } #[expect(clippy::needless_borrows_for_generic_args, reason = "riffraff")] - let slab = SlabAllocator::::new(&CpuRuntime, ()); + let slab = SlabAllocator::::new(&CpuRuntime, "transform", ()); let a = NestedTransform::new(&slab); a.set(Transform { translation: Vec3::splat(100.0), diff --git a/crates/renderling/src/stage/cpu.rs b/crates/renderling/src/stage/cpu.rs index 93bed9ce..541f216e 100644 --- a/crates/renderling/src/stage/cpu.rs +++ b/crates/renderling/src/stage/cpu.rs @@ -291,7 +291,7 @@ impl<'a> RenderletBuilder<'a, ()> { } impl<'a, T: Bundle> RenderletBuilder<'a, T> { - fn suffix(self, element: S) -> RenderletBuilder<'a, T::Suffixed> { + pub fn suffix(self, element: S) -> RenderletBuilder<'a, T::Suffixed> { RenderletBuilder { data: self.data, resources: self.resources.suffix(element), @@ -482,7 +482,7 @@ impl Stage { self.geometry.new_camera(camera) } - /// Set the given camera as the default one to use when rendering. + /// Use the given camera when rendering. pub fn use_camera(&self, camera: impl AsRef>) { self.geometry.use_camera(camera); } @@ -689,6 +689,18 @@ impl Stage { /// Enable shadow mapping for the given [`AnalyticalLightBundle`], creating /// a new [`ShadowMap`]. + /// + /// ## Tips for making a good shadow map + /// + /// 1. Make sure the map is big enough. + /// Using a big map can fix some peter panning issues, even before + /// messing with bias in the [`ShadowMapDescriptor`]. + /// The bigger the map, the cleaner the shadows will be. This can + /// also solve PCF problems. + /// 2. Don't set PCF samples too high in the [`ShadowMapDescriptor`], as + /// this can _cause_ peter panning. + /// 3. Ensure the **znear** and **zfar** parameters make sense, as the + /// shadow map uses these to determine how much of the scene to cover. pub fn new_shadow_map( &self, analytical_light_bundle: &AnalyticalLightBundle, @@ -923,13 +935,16 @@ impl Stage { let runtime = ctx.runtime(); let device = &runtime.device; let resolution @ UVec2 { x: w, y: h } = ctx.get_size(); - let atlas_size = *ctx.atlas_size.read().unwrap(); + let stage_config = *ctx.stage_config.read().unwrap(); let geometry = Geometry::new( ctx, resolution, - UVec2::new(atlas_size.width, atlas_size.height), + UVec2::new( + stage_config.atlas_size.width, + stage_config.atlas_size.height, + ), ); - let materials = Materials::new(runtime, atlas_size); + let materials = Materials::new(runtime, stage_config.atlas_size); let multisample_count = 1; let hdr_texture = Arc::new(RwLock::new(Texture::create_hdr_texture( device, @@ -953,13 +968,13 @@ impl Stage { multisample_count, ); let geometry_buffer = geometry.slab_allocator().commit(); - let lighting = Lighting::new(&geometry); + let lighting = Lighting::new(stage_config.shadow_map_atlas_size, &geometry); Self { materials, draw_calls: Arc::new(RwLock::new(DrawCalls::new( ctx, - true, + ctx.get_use_direct_draw(), &geometry_buffer, &depth_texture, ))), @@ -1565,7 +1580,7 @@ impl NestedTransform { self.mark_dirty(); } - pub fn get_notifier_index(&self) -> usize { + pub fn get_notifier_index(&self) -> SourceId { self.global_transform.notifier_index() } @@ -1687,7 +1702,7 @@ mod test { clippy::needless_borrows_for_generic_args, reason = "This is just riff-raff, as it doesn't compile without the borrow." )] - let slab = SlabAllocator::::new(&CpuRuntime, ()); + let slab = SlabAllocator::::new(&CpuRuntime, "transform", ()); // Setup a hierarchy of transforms log::info!("new"); let root = NestedTransform::new(&slab); diff --git a/crates/renderling/src/stage/gltf_support.rs b/crates/renderling/src/stage/gltf_support.rs index 9bab4c25..7ed60905 100644 --- a/crates/renderling/src/stage/gltf_support.rs +++ b/crates/renderling/src/stage/gltf_support.rs @@ -315,7 +315,7 @@ pub struct GltfPrimitive { impl GltfPrimitive { pub fn from_gltf( - stage: &mut Stage, + stage: &Stage, primitive: gltf::Primitive, buffer_data: &[gltf::buffer::Data], materials: &HybridArray, @@ -581,7 +581,7 @@ pub struct GltfMesh { impl GltfMesh { fn from_gltf( - stage: &mut Stage, + stage: &Stage, buffer_data: &[gltf::buffer::Data], materials: &HybridArray, mesh: gltf::Mesh, @@ -616,7 +616,7 @@ impl AsRef> for GltfCamera { } impl GltfCamera { - fn new(stage: &mut Stage, gltf_camera: gltf::Camera<'_>, transform: &NestedTransform) -> Self { + fn new(stage: &Stage, gltf_camera: gltf::Camera<'_>, transform: &NestedTransform) -> Self { log::debug!("camera: {}", gltf_camera.name().unwrap_or("unknown")); log::debug!(" transform: {:#?}", transform.get_global_transform()); let projection = match gltf_camera.projection() { @@ -711,7 +711,7 @@ pub struct GltfSkin { impl GltfSkin { pub fn from_gltf( - stage: &mut Stage, + stage: &Stage, buffer_data: &[gltf::buffer::Data], nodes: &[GltfNode], skin: gltf::Skin, @@ -787,7 +787,7 @@ pub struct GltfDocument { impl GltfDocument { pub fn from_gltf( - stage: &mut Stage, + stage: &Stage, document: &gltf::Document, buffer_data: Vec, images: Vec, @@ -901,7 +901,7 @@ impl GltfDocument { fn transform_for_node( nesting_level: usize, - stage: &mut Stage, + stage: &Stage, cache: &mut HashMap, node: &gltf::Node, ) -> NestedTransform { @@ -1176,7 +1176,7 @@ impl GltfDocument { impl Stage { pub fn load_gltf_document_from_path( - &mut self, + &self, path: impl AsRef, ) -> Result { let (document, buffers, images) = gltf::import(path)?; @@ -1184,7 +1184,7 @@ impl Stage { } pub fn load_gltf_document_from_bytes( - &mut self, + &self, bytes: impl AsRef<[u8]>, ) -> Result { let (document, buffers, images) = gltf::import_slice(bytes)?; @@ -1223,7 +1223,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 mut stage = ctx + let stage = ctx .new_stage() .with_lighting(false) .with_bloom(false) @@ -1243,7 +1243,7 @@ mod test { // Ensures we can read a minimal gltf file with a simple triangle mesh. fn minimal_mesh() { let ctx = Context::headless(20, 20); - let mut stage = ctx + let stage = ctx .new_stage() .with_lighting(false) .with_bloom(false) @@ -1270,7 +1270,7 @@ mod test { // This ensures we are decoding images correctly. fn gltf_images() { let ctx = Context::headless(100, 100); - let mut stage = ctx + let stage = ctx .new_stage() .with_lighting(false) .with_background_color(Vec4::splat(1.0)); @@ -1319,7 +1319,7 @@ mod test { fn simple_texture() { let size = 100; let ctx = Context::headless(size, size); - let mut stage = ctx + let stage = ctx .new_stage() .with_background_color(Vec3::splat(0.0).extend(1.0)) // There are no lights in the scene and the material isn't marked as "unlit", so @@ -1347,7 +1347,7 @@ mod test { fn normal_mapping_brick_sphere() { let size = 600; let ctx = Context::headless(size, size); - let mut stage = ctx + let stage = ctx .new_stage() .with_lighting(true) .with_background_color(Vec4::ONE); @@ -1379,7 +1379,7 @@ mod test { #[test] fn rigged_fox() { let ctx = Context::headless(256, 256); - let mut stage = ctx + let stage = ctx .new_stage() .with_lighting(false) .with_vertex_skinning(false) @@ -1463,7 +1463,7 @@ mod test { // taking into account that the gltf files may have been // saved with Y up, or with Z up let ctx = Context::headless(100, 100); - let mut stage = ctx.new_stage(); + let stage = ctx.new_stage(); let doc = stage .load_gltf_document_from_path( crate::test::workspace_dir() diff --git a/crates/renderling/src/stage/gltf_support/anime.rs b/crates/renderling/src/stage/gltf_support/anime.rs index 4617a948..79bdb001 100644 --- a/crates/renderling/src/stage/gltf_support/anime.rs +++ b/crates/renderling/src/stage/gltf_support/anime.rs @@ -776,7 +776,7 @@ mod test { #[test] fn gltf_simple_animation() { let ctx = Context::headless(16, 16); - let mut stage = ctx + let stage = ctx .new_stage() .with_bloom(false) .with_background_color(Vec3::ZERO.extend(1.0)); @@ -807,7 +807,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); let img = frame.read_image().unwrap(); - img_diff::save(&format!("animation/triangle{i}.png"), img); + img_diff::save(format!("animation/triangle{i}.png"), img); frame.present(); } } diff --git a/crates/renderling/src/texture.rs b/crates/renderling/src/texture.rs index c844abbe..8eb488fb 100644 --- a/crates/renderling/src/texture.rs +++ b/crates/renderling/src/texture.rs @@ -8,7 +8,7 @@ use std::{ use craballoc::runtime::WgpuRuntime; use glam::UVec2; use image::{ - load_from_memory, DynamicImage, GenericImage, GenericImageView, ImageBuffer, ImageError, + load_from_memory, DynamicImage, GenericImage, GenericImageView, ImageBuffer, ImageError, Luma, PixelWithColorType, Rgba32FImage, }; use mips::MipMapGenerator; @@ -779,6 +779,19 @@ pub fn read_depth_texture_to_image( Some(img_buffer) } +pub fn read_depth_texture_f32( + runtime: impl AsRef, + width: usize, + height: usize, + texture: &wgpu::Texture, +) -> Option, Vec>> { + let depth_copied_buffer = Texture::read(runtime.as_ref(), texture, width, height, 1, 4); + let pixels = depth_copied_buffer.pixels(&runtime.as_ref().device); + let pixels = bytemuck::cast_slice::(&pixels).to_vec(); + let img_buffer = image::ImageBuffer::from_raw(width as u32, height as u32, pixels)?; + Some(img_buffer) +} + /// A depth texture. pub struct DepthTexture { pub(crate) runtime: WgpuRuntime, diff --git a/crates/renderling/src/tonemapping/cpu.rs b/crates/renderling/src/tonemapping/cpu.rs index 18e0f7fd..e1df286d 100644 --- a/crates/renderling/src/tonemapping/cpu.rs +++ b/crates/renderling/src/tonemapping/cpu.rs @@ -100,11 +100,7 @@ impl Tonemapping { frame_texture_format: wgpu::TextureFormat, hdr_texture: &Texture, ) -> Self { - let slab = SlabAllocator::new_with_label( - runtime, - wgpu::BufferUsages::empty(), - Some("tonemapping-slab"), - ); + let slab = SlabAllocator::new(runtime, "tonemapping-slab", wgpu::BufferUsages::empty()); let config = slab.new_value(TonemapConstants::default()); let label = Some("tonemapping"); diff --git a/gltf/light_tiling_test.glb b/gltf/light_tiling_test.glb new file mode 100644 index 00000000..742296f8 Binary files /dev/null and b/gltf/light_tiling_test.glb differ diff --git a/test_img/bvol/bounding_box/get_mesh.png b/test_img/bvol/bounding_box/get_mesh.png new file mode 100644 index 00000000..b437f0c6 Binary files /dev/null and b/test_img/bvol/bounding_box/get_mesh.png differ diff --git a/test_img/lights/spot_lights/frame.png b/test_img/light/spot_lights/frame.png similarity index 100% rename from test_img/lights/spot_lights/frame.png rename to test_img/light/spot_lights/frame.png diff --git a/test_img/lights/spot_lights/one.png b/test_img/light/spot_lights/one.png similarity index 100% rename from test_img/lights/spot_lights/one.png rename to test_img/light/spot_lights/one.png diff --git a/test_img/stage/resize_100.png b/test_img/stage/resize_100.png new file mode 100644 index 00000000..33651b99 Binary files /dev/null and b/test_img/stage/resize_100.png differ diff --git a/test_img/stage/resize_200.png b/test_img/stage/resize_200.png new file mode 100644 index 00000000..b131904e Binary files /dev/null and b/test_img/stage/resize_200.png differ