From 74c6daaee042b287377e88622567f193010af548 Mon Sep 17 00:00:00 2001 From: yujiaxu-ops Date: Sat, 14 Nov 2026 01:20:00 +0800 Subject: [PATCH] The live passes from one place: live.wgsl's declarations read at build time, a table for the rest, a layout a pass A buffer and a pass more took seven edits that had to agree (the declaration, the layout's entries, the pass list, the buffers' sizes and order, the bind group's entries, the dispatch grid in two copies, the merged layout's table). Now: - build.rs reads live.wgsl with naga (a build dependency at wgpu's version, 30: the browser's build carries none of it) into each binding's name, number, kind and element stride, and the bindings each entry point uses. - live/layout.rs holds what the shader cannot say: what each binding is bound to and how many elements a buffer of the passes' own holds (RESOURCES), the passes' order and grids (PASSES); layout::check holds it to the shader (a unit test, and every view as it starts). - Natively each pass has a bind group layout and a bind group of its own, of the bindings its entry point uses. The browser's layout is still the merged one for all passes: post binds 13 storage buffers, compose 11, dtemporal 10 (the other 36 at most eight). - Targets allocates from the table (`buf(name)`); `pass_grid` is `layout::Grid::size`. Checks: cargo test 125 passed; render-identity against the build before, native (171 identical, 19 within spread) and THREERT_MERGED=1 (172, 18): nothing changed; the passes after the trace 6.03-6.09 ms on both builds (Bistro, 1440p at 0.375, alternated). The wasm32 library and the web crate build. Co-Authored-By: Claude Opus 5.5 --- docs/ROADMAP.md | 15 ++ docs/RUNTIME.md | 4 +- renderer/Cargo.toml | 4 + renderer/build.rs | 59 ++++++ renderer/shaders/live.wgsl | 6 +- renderer/src/live/frame.rs | 17 +- renderer/src/live/layout.rs | 226 ++++++++++++++++++++++ renderer/src/live/load.rs | 7 +- renderer/src/live/mod.rs | 28 +-- renderer/src/live/targets.rs | 352 +++++++++++++++-------------------- renderer/web/Cargo.lock | 1 + 11 files changed, 485 insertions(+), 234 deletions(-) create mode 100644 renderer/build.rs create mode 100644 renderer/src/live/layout.rs diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 89f6add..a714dd8 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -2198,6 +2198,21 @@ its two levels: the unchanged trace read the same when alternated). passed by its trail (the light the lamps left on the sky made up the mean); with the trail gone it reads 0.066 for 0.1. What it was for is open (Known issues: lamps in motion keep half their light). +**The live passes from one place (2026-11-14, after it).** A buffer and a pass more (the locks' carry, above) took +seven edits that had to agree: the declaration in live.wgsl, the bind group layout's entries, the pass list, the +buffers' sizes and order, the bind group's entries, the dispatch grid (in two copies), and the merged layout's table. +Now live.wgsl's declarations are the layout: build.rs reads them with naga (the compiler wgpu uses; a build +dependency, so the browser's build carries none of it) into each binding's name, number, kind and element stride and +the bindings each entry point uses, and `live/layout.rs` holds only what the shader cannot say: what each binding is +bound to and how many elements a buffer of the passes' own holds (`RESOURCES`), and the passes' order and grids +(`PASSES`). `layout::check` holds the table to the shader (a unit test, and every view as it starts). Natively each +pass has a bind group layout and a bind group of its own, of the bindings its entry point uses (one shared group of +every binding was the passes' layout before). Measured on 39 passes: 36 bind at most the browser's eight storage +buffers; `post` binds 13 (most of them its debug view modes'), `compose` 11, `dtemporal` 10, so the browser's layout +is still the merged one, for all passes, until those three are rearranged. Check: render-identity against the +build before, native and merged, nothing changed; the passes after the trace 6.03-6.09 ms on both (Bistro, 1440p at +0.375, alternated). + ## 2. Temporal fixes Targets (2026-11-08): bench-temporal, bench-ghosting and their UE MRQ references move to Bistro night (§ 0 step 7); the diff --git a/docs/RUNTIME.md b/docs/RUNTIME.md index 5bf2835..32f8ea0 100644 --- a/docs/RUNTIME.md +++ b/docs/RUNTIME.md @@ -688,7 +688,9 @@ tree (ROADMAP § 6). the node host as `GameSession` does, and sends the canvas' clicks down the same path. `node tools/build-web.ts [game.ts] --serve` builds and serves it (needs the wasm-bindgen CLI of the crate's version). - **The merged binding layout** (`renderer/src/merged.rs`). WebGPU gives a stage 8 storage buffers by default; the - kernel binds 19 and the live passes 25. There is one source: the browser's shaders are written from + kernel binds 19 and the live passes 25. (Natively each live pass has a layout of its own, of the bindings its + entry point uses: `renderer/src/live/layout.rs`, from live.wgsl as build.rs reads it; three passes use more than + eight, so the browser's layout is still one for all, merged.) There is one source: the browser's shaders are written from `pathtrace.wgsl` and `live.wgsl` as text when a module is compiled, and the native ones are untouched. - The kernel's seven tables become the arrays of one struct in one buffer; their lengths are in the source, each a power of two above a floor, so a kernel is compiled again only when a table doubles. The renderer keeps writing diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index ad2830e..150b2bd 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -52,6 +52,10 @@ rustls = { version = "0.23", optional = true, default-features = false, features [target.'cfg(target_os = "macos")'.dependencies] libc = "0.2" +# build.rs reads shaders/live.wgsl's bindings with the compiler wgpu uses (its version: 30), when the crate is built. +[build-dependencies] +naga = { version = "30", features = ["wgsl-in"] } + # src/merged.rs's tests read the generated shaders with the compiler wgpu uses: they are valid, and no stage binds more # storage buffers than a browser allows. [dev-dependencies] diff --git a/renderer/build.rs b/renderer/build.rs new file mode 100644 index 0000000..952bca8 --- /dev/null +++ b/renderer/build.rs @@ -0,0 +1,59 @@ +//! The live passes' bindings as `shaders/live.wgsl` declares them, read with naga (the compiler wgpu uses) when the +//! crate is built, into `$OUT_DIR/live_layout.rs` (`src/live/layout.rs` includes it): each binding's name, number, +//! kind and element stride, and the bindings each entry point uses. The shader is the one place they are written; +//! what it cannot say (a buffer's size, a pass's grid) is `src/live/layout.rs`'s table. At build time, not at run +//! time: a browser's build has no naga (wgpu hands it the WGSL). + +use naga::{AddressSpace, ImageClass, ImageDimension, ScalarKind, StorageAccess, TypeInner}; +use std::fmt::Write; + +fn main() { + let path = "shaders/live.wgsl"; + println!("cargo:rerun-if-changed={path}"); + println!("cargo:rerun-if-changed=build.rs"); + let src = std::fs::read_to_string(path).expect("shaders/live.wgsl"); + let module = naga::front::wgsl::parse_str(&src).unwrap_or_else(|e| panic!("{}", e.emit_to_string(&src))); + let info = naga::valid::Validator::new(naga::valid::ValidationFlags::all(), naga::valid::Capabilities::all()) + .validate(&module) + .unwrap_or_else(|e| panic!("{}", e.emit_to_string(&src))); + let mut out = String::from("// Generated by build.rs from shaders/live.wgsl: do not edit.\n\n"); + // Every bound global, in binding order. + let mut bound: Vec<_> = module.global_variables.iter().filter_map(|(h, v)| v.binding.as_ref().map(|b| (b.binding, h, v))).collect(); + bound.sort_by_key(|b| b.0); + out += "/// Each binding live.wgsl declares (group 0), in binding order.\npub const BINDINGS: &[Binding] = &[\n"; + for (binding, _, v) in &bound { + let name = v.name.as_deref().unwrap_or_else(|| panic!("binding {binding} has no name")); + assert_eq!(v.binding.as_ref().unwrap().group, 0, "live.wgsl binds group 0 only ({name})"); + let ty = &module.types[v.ty].inner; + let kind = match (v.space, ty) { + (AddressSpace::Uniform, _) => "Kind::Uniform".to_string(), + (AddressSpace::Storage { access }, TypeInner::Array { size, stride, .. }) => { + let len = match size { + naga::ArraySize::Constant(n) => n.get(), + _ => 0, + }; + format!("Kind::Storage {{ read_only: {}, stride: {stride}, len: {len} }}", !access.contains(StorageAccess::STORE)) + } + (AddressSpace::Handle, TypeInner::Image { dim, arrayed: false, class: ImageClass::Sampled { kind: ScalarKind::Float, multi: false } }) => { + let dim = match dim { + ImageDimension::D2 => "D2", + ImageDimension::D3 => "D3", + d => panic!("{name}: a {d:?} texture"), + }; + format!("Kind::Texture(wgpu::TextureViewDimension::{dim})") + } + (AddressSpace::Handle, TypeInner::Sampler { comparison: false }) => "Kind::Sampler".to_string(), + (space, _) => panic!("{name}: binding {binding} is a {space:?} this build does not read"), + }; + writeln!(out, " Binding {{ name: \"{name}\", binding: {binding}, kind: {kind} }},").unwrap(); + } + out += "];\n\n/// Each entry point and the bindings it uses.\npub const ENTRY_POINTS: &[(&str, &[u32])] = &[\n"; + for (k, e) in module.entry_points.iter().enumerate() { + let uses = info.get_entry_point(k); + let used: Vec = bound.iter().filter(|(_, h, _)| !uses[*h].is_empty()).map(|(b, _, _)| b.to_string()).collect(); + writeln!(out, " (\"{}\", &[{}]),", e.name, used.join(", ")).unwrap(); + } + out += "];\n"; + let dest = std::path::Path::new(&std::env::var("OUT_DIR").unwrap()).join("live_layout.rs"); + std::fs::write(dest, out).unwrap(); +} diff --git a/renderer/shaders/live.wgsl b/renderer/shaders/live.wgsl index a13e604..c097f0c 100644 --- a/renderer/shaders/live.wgsl +++ b/renderer/shaders/live.wgsl @@ -42,6 +42,10 @@ struct Pixel { color: vec4, albedo: vec4, normal: vec4, emit: vec // ---- The live passes' buffers, by what they hold and how long. // +// The declarations below are the passes' layout: build.rs reads them (with naga) when the renderer is built, and each +// pass binds what its entry point uses; live/layout.rs says what each binding is bound to, how large a buffer of the +// passes' own is, and the passes' order and grids. A buffer more: its declaration here, a line there. +// // What lasts from one frame to the next has a buffer of its own, in two halves: this frame's, which the passes write // (`this_half`), and the previous frame's, which they read (`last_half`); live/frame.rs alternates them (`L.state.x`). A half // is L.size.x * L.size.y elements, indexed by traced sample (by output pixel: `color_hist`). @@ -54,7 +58,7 @@ struct Pixel { color: vec4, albedo: vec4, normal: vec4, emit: vec // color_hist the output grid's colour history: rgb, samples (`upscale`) // up_state the upscaler's state per traced sample (`compose`): x, y `Flicker`; z, w `Lock` (its life and the // view depth of what it is for; where in the traced pixel that is, f16 each) -// What lives within a frame shares buffers whose uses do not overlap (the passes run in live/targets.rs `Passes` order): +// What lives within a frame shares buffers whose uses do not overlap (the passes run in live/layout.rs `PASSES` order): // sample_vel per traced sample: screen velocity (output pixels this frame), view depth, animated shading // (`compose`; `judge`, `upscale`, depth of field and motion blur read it) // judged `judge`'s findings per traced sample (`Judged`), in up_state's previous half: `compose` has read it diff --git a/renderer/src/live/frame.rs b/renderer/src/live/frame.rs index 4cb6965..299c62a 100644 --- a/renderer/src/live/frame.rs +++ b/renderer/src/live/frame.rs @@ -505,14 +505,13 @@ impl LiveCore { state: [1 - self.frame % 2, 0, 0, 0], }; g.queue.write_buffer(&self.targets.params, 0, bytemuck::bytes_of(&lp)); - let bg = &self.sc.bg; // Translucent particles: drawn over the upscaled image right after `upscale` (in sb when depth of field / motion // blur follow, else fa: they have no motion vectors, so they are no part of the colour history; UE draws its // translucency after the upscaler the same way), then their volume voxels trained. // (Nothing to draw while every translucent particle system is hidden in place.) let tl = self.sc.up.translucent.as_ref().filter(|_| g.translucent_particles_live(&self.sc.up)).map(|t| { let pipes = g.translucent_pipelines(&self.sc.up.kernel, true); - let target = if lp.misc2[2] > 0.0 || lp.dof[0] > 0.0 { &self.targets.sb.buffer } else { &self.targets.fa.buffer }; + let target = if lp.misc2[2] > 0.0 || lp.dof[0] > 0.0 { &self.targets.buf("sb").buffer } else { &self.targets.buf("fa").buffer }; // Below full resolution the particles are drawn per traced pixel, then laid over the output (`translucent_up`). let low = (iw, ih) != (w, h); let tp = gpu::TranslucentParams { size: [w, h, t.first, 1], view: [iw, ih, gpu::VOLUME_TRAIN_CAP, low as u32], misc: [VOLUME_TRAIN_SHARE, translucent_hidden(self.frame) as u32 as f32, 0.0, 0.0] }; @@ -554,12 +553,12 @@ impl LiveCore { } if let Some(p) = &self.profile { // One compute pass per stage, each timed. - for (k, (name, pipe)) in self.passes.pipes.iter().enumerate() { + for (k, (name, grid, pipe)) in self.passes.pipes.iter().enumerate() { let q = (k as u32 + 1) * 2; let mut pass = e.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some(name), timestamp_writes: Some(wgpu::ComputePassTimestampWrites { query_set: &p.queries, beginning_of_pass_write_index: Some(q), end_of_pass_write_index: Some(q + 1) }) }); pass.set_pipeline(pipe); - pass.set_bind_group(0, bg, &[]); - let (gw, gh) = pass_grid(name, (w, h), (iw, ih), self.prev_isz); + pass.set_bind_group(0, &self.sc.bgs[k], &[]); + let (gw, gh) = grid.size((w, h), (iw, ih), self.prev_isz); pass.dispatch_workgroups(gw.div_ceil(8), gh.div_ceil(8), 1); drop(pass); if *name == "upscale" && tl.is_some() { @@ -576,10 +575,10 @@ impl LiveCore { } else { let tw = self.timer.as_ref().zip(timed).map(|(t, slot)| wgpu::ComputePassTimestampWrites { query_set: &t.queries, beginning_of_pass_write_index: Some(slot as u32 * 8 + 2), end_of_pass_write_index: Some(slot as u32 * 8 + 3) }); let mut pass = e.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("live post"), timestamp_writes: tw }); - for (name, pipe) in &self.passes.pipes { + for (k, (name, grid, pipe)) in self.passes.pipes.iter().enumerate() { pass.set_pipeline(pipe); - pass.set_bind_group(0, bg, &[]); - let (gw, gh) = pass_grid(name, (w, h), (iw, ih), self.prev_isz); + pass.set_bind_group(0, &self.sc.bgs[k], &[]); + let (gw, gh) = grid.size((w, h), (iw, ih), self.prev_isz); pass.dispatch_workgroups(gw.div_ceil(8), gh.div_ceil(8), 1); if *name == "upscale" { tl_dispatch(&mut pass, false); @@ -645,7 +644,7 @@ impl LiveCore { } p.frames += 1; if p.frames == 120 { - let names: Vec<&str> = std::iter::once("trace").chain(self.passes.pipes.iter().map(|(n, _)| *n)).chain(["cache", "particles", "blas", "translucent", "vtrain"]).collect(); + let names: Vec<&str> = std::iter::once("trace").chain(self.passes.pipes.iter().map(|(n, ..)| *n)).chain(["cache", "particles", "blas", "translucent", "vtrain"]).collect(); let mut rows: Vec = names.iter().zip(&p.sums).map(|(n, s)| format!("{n} {:.2}", s / p.frames as f64)).collect(); rows.push(format!("gridsim {:.2}", p.grid_sum / p.frames as f64)); rows.push(format!("media {:.2}", p.media_sum / p.frames as f64)); diff --git a/renderer/src/live/layout.rs b/renderer/src/live/layout.rs new file mode 100644 index 0000000..d0979fa --- /dev/null +++ b/renderer/src/live/layout.rs @@ -0,0 +1,226 @@ +//! The live passes' layout. What live.wgsl declares is read from it when the crate is built (build.rs, with naga: +//! `BINDINGS`, each binding's name, number, kind and element stride; `ENTRY_POINTS`, the bindings each entry point +//! uses); here is only what the shader cannot say: what each binding is bound to and how many elements a buffer of +//! the passes' own holds (`RESOURCES`), and the passes' order and the grid each runs over (`PASSES`). `check` holds +//! the two to each other (a unit test, and every view as it starts). A buffer or a pass more: its declaration or +//! entry point in live.wgsl, a line here. + +/// A binding live.wgsl declares. +pub(super) struct Binding { + pub(super) name: &'static str, + pub(super) binding: u32, + pub(super) kind: Kind, +} + +pub(super) enum Kind { + Uniform, + /// An array: its element's stride, and its length if the shader fixes it (else 0). + Storage { read_only: bool, stride: u64, len: u64 }, + Texture(wgpu::TextureViewDimension), + Sampler, +} + +include!(concat!(env!("OUT_DIR"), "/live_layout.rs")); + +/// What a binding is bound to. +#[derive(Clone, Copy, PartialEq, Debug)] +pub(super) enum Res { + /// A buffer of the passes' own, of this many elements. + Own(Count), + /// The frame's parameters (`LiveParams`). + Params, + /// The trace's samples (the scene's accumulation buffer). + Samples, + /// What the kernel leaves for the passes (`gpu::KernelBufs`): the specular-lobe rays, the motion, the direct light. + KernelRefl, + KernelMotion, + KernelDirect, + /// The tonemap LUT and its sampler. + Lut, + LutSampler, +} + +/// How many elements a buffer holds, for an output of w x h pixels (a traced grid is at most that). +#[derive(Clone, Copy, PartialEq, Debug)] +pub(super) enum Count { + /// That many per output pixel (2: a lasting buffer's halves, or two slots). + Pixels(u64), + /// One per texel of the quarter-resolution grid. + Quarter, + /// One per texel of the bloom pyramid's levels and stages. + Bloom, + /// The length the shader gives the array. + Declared, +} + +/// Each binding's resource, by the name live.wgsl gives it. +pub(super) const RESOURCES: &[(&str, Res)] = &[ + ("L", Res::Params), + ("sample_px", Res::Samples), + ("diffuse_hist", Res::Own(Count::Pixels(2))), + ("color_hist", Res::Own(Count::Pixels(2))), + ("gbuffer", Res::Own(Count::Pixels(2))), + ("spec_hist", Res::Own(Count::Pixels(2))), + ("diffuse_mom", Res::Own(Count::Pixels(2))), + ("diffuse_fast", Res::Own(Count::Pixels(2))), + ("up_state", Res::Own(Count::Pixels(2))), + ("sample_vel", Res::Own(Count::Pixels(1))), + ("out_px", Res::Own(Count::Pixels(1))), + ("lut", Res::Lut), + ("lut_samp", Res::LutSampler), + ("ae_bins", Res::Own(Count::Declared)), + ("ae_state", Res::Own(Count::Declared)), + ("lumq", Res::Own(Count::Quarter)), + ("lumb", Res::Own(Count::Quarter)), + ("fa", Res::Own(Count::Pixels(1))), + ("fb", Res::Own(Count::Pixels(1))), + ("bloom_a", Res::Own(Count::Bloom)), + ("bloom_b", Res::Own(Count::Quarter)), + ("spec_pp", Res::Own(Count::Pixels(2))), + ("sb", Res::Own(Count::Pixels(1))), + ("refl_in", Res::KernelRefl), + ("refl_res", Res::Own(Count::Pixels(1))), + ("motion_in", Res::KernelMotion), + ("direct_in", Res::KernelDirect), + ("lock_fwd", Res::Own(Count::Pixels(1))), +]; + +/// The grid a pass runs over, in invocations (workgroups of 8 x 8). +#[derive(Clone, Copy, PartialEq, Debug)] +pub(super) enum Grid { + /// The traced grid. + Traced, + /// The previous frame's traced grid. + PrevTraced, + /// The output grid. + Output, + /// A quarter of it each way. + Quarter, + /// Its 16 x 16 tiles. + Tiles16, + /// One workgroup. + One, +} + +/// The passes, in the order a frame runs them (live.wgsl's buffer table says which share a buffer when). +pub(super) const PASSES: &[(&str, Grid)] = &[ + ("refl", Grid::Traced), + ("dtemporal", Grid::Traced), + ("atrous0", Grid::Traced), + ("atrous1", Grid::Traced), + ("atrous2", Grid::Traced), + ("atrous3", Grid::Traced), + ("atrous4", Grid::Traced), + ("stemporal", Grid::Traced), + ("sfilt0", Grid::Traced), + ("sfilt1", Grid::Traced), + ("sfilt2", Grid::Traced), + ("lock_carry", Grid::PrevTraced), + ("compose", Grid::Traced), + ("judge", Grid::Traced), + ("reject", Grid::Traced), + ("upscale", Grid::Output), + ("dof_tiles", Grid::Tiles16), + ("dof", Grid::Output), + ("mblur", Grid::Output), + ("lum_down", Grid::Quarter), + ("exposure", Grid::One), + ("lum_blur", Grid::Quarter), + ("bloom_down", Grid::Quarter), + ("bloom_dn1", Grid::Quarter), + ("bloom_dn2", Grid::Quarter), + ("bloom_dn3", Grid::Quarter), + ("bloom_dn4", Grid::Quarter), + ("bloom_h5", Grid::Quarter), + ("bloom_v5", Grid::Quarter), + ("bloom_h4", Grid::Quarter), + ("bloom_v4", Grid::Quarter), + ("bloom_h3", Grid::Quarter), + ("bloom_v3", Grid::Quarter), + ("bloom_h2", Grid::Quarter), + ("bloom_v2", Grid::Quarter), + ("bloom_h1", Grid::Quarter), + ("bloom_v1", Grid::Quarter), + ("bloom_h0", Grid::Quarter), + ("bloom_v0", Grid::Quarter), + ("post", Grid::Output), +]; + +impl Grid { + /// Invocations, for an output of `out`, a traced grid of `traced` and the previous frame's `prev`. + pub(super) fn size(self, out: (u32, u32), traced: (u32, u32), prev: (u32, u32)) -> (u32, u32) { + match self { + Grid::Traced => traced, + Grid::PrevTraced => (prev.0.max(1), prev.1.max(1)), + Grid::Output => out, + Grid::Quarter => (out.0.div_ceil(4), out.1.div_ceil(4)), + Grid::Tiles16 => (out.0.div_ceil(16), out.1.div_ceil(16)), + Grid::One => (8, 8), + } + } +} + +/// The binding live.wgsl gives `name`. +pub(super) fn binding(name: &str) -> &'static Binding { + BINDINGS.iter().find(|b| b.name == name).unwrap_or_else(|| panic!("live.wgsl binds no `{name}`")) +} + +/// The resource of a binding. +pub(super) fn resource(b: &Binding) -> Res { + RESOURCES.iter().find(|r| r.0 == b.name).unwrap_or_else(|| panic!("live.wgsl binds `{}` (binding {}): it has no line in RESOURCES", b.name, b.binding)).1 +} + +/// The bindings the entry point `name` uses. +pub(super) fn uses(name: &str) -> &'static [u32] { + ENTRY_POINTS.iter().find(|e| e.0 == name).unwrap_or_else(|| panic!("live.wgsl has no entry point `{name}`")).1 +} + +/// The table and the shader agree: every binding has a resource that fits its kind and every resource a binding, +/// every entry point is a pass and every pass an entry point, each once. +pub(super) fn check() { + let mut problems = vec![]; + for b in BINDINGS { + match RESOURCES.iter().filter(|r| r.0 == b.name).count() { + 1 => {} + 0 => problems.push(format!("`{}` (binding {}) has no line in RESOURCES", b.name, b.binding)), + _ => problems.push(format!("`{}` has several lines in RESOURCES", b.name)), + } + } + for (name, res) in RESOURCES { + let Some(b) = BINDINGS.iter().find(|b| b.name == *name) else { + problems.push(format!("RESOURCES names `{name}`, which live.wgsl does not bind")); + continue; + }; + let fits = match (res, &b.kind) { + (Res::Params, Kind::Uniform) | (Res::Lut, Kind::Texture(_)) | (Res::LutSampler, Kind::Sampler) => true, + (Res::Samples | Res::KernelRefl | Res::KernelMotion | Res::KernelDirect, Kind::Storage { read_only, .. }) => *read_only, + (Res::Own(Count::Declared), Kind::Storage { len, .. }) => *len > 0, + (Res::Own(_), Kind::Storage { len, .. }) => *len == 0, + _ => false, + }; + if !fits { + problems.push(format!("`{name}`: {res:?} for a binding that is not one")); + } + } + for (name, _) in ENTRY_POINTS { + match PASSES.iter().filter(|p| p.0 == *name).count() { + 1 => {} + 0 => problems.push(format!("live.wgsl's entry point `{name}` is not in PASSES")), + _ => problems.push(format!("`{name}` is in PASSES more than once")), + } + } + for (name, _) in PASSES { + if !ENTRY_POINTS.iter().any(|e| e.0 == *name) { + problems.push(format!("PASSES names `{name}`, which live.wgsl has no entry point for")); + } + } + assert!(problems.is_empty(), "the live passes' layout (live/layout.rs) and live.wgsl disagree:\n {}", problems.join("\n ")); +} + +#[cfg(test)] +mod tests { + #[test] + fn the_table_and_the_shader_agree() { + super::check(); + } +} diff --git a/renderer/src/live/load.rs b/renderer/src/live/load.rs index 7f7d562..c6c520b 100644 --- a/renderer/src/live/load.rs +++ b/renderer/src/live/load.rs @@ -31,7 +31,8 @@ pub(super) struct LoadedScene { pub(super) triangles: usize, pub(super) up: gpu::Uploaded, pub(super) base: gpu::Params, - pub(super) bg: wgpu::BindGroup, + /// The live passes' bind groups, a pass each (`Targets::bind_groups`). + pub(super) bgs: Vec, pub(super) exposure: f32, pub(super) exposure_settings: ExposureSettings, pub(super) tan_half_fov: f32, @@ -241,7 +242,7 @@ pub(super) fn load_scene(g: &gpu::Gpu, world: &World, assets: &Path, presets: &P base.ltree[3] = crate::ltree::live_candidates() as f32; } base.cam_fwd[3] = w as f32 / h as f32; - let bg = targets.bind_group(&g.device, &passes.layout, &up.accum); + let bgs = targets.bind_groups(&g.device, passes, &up.accum); let exposure_settings = ExposureSettings::new(&sc.camera.exposure, &sc.preset.post); // Manual: 2^-EV100. Auto: a first guess for the firefly clamp (the GPU histogram takes over from frame 1). let exposure = match &sc.camera.exposure { @@ -300,5 +301,5 @@ pub(super) fn load_scene(g: &gpu::Gpu, world: &World, assets: &Path, presets: &P inst_at_load: instances.len(), added: Default::default(), }; - Ok((LoadedScene { timings, triangles, tris, tri_nodes, tri_instances, pool_tris, meshes, instances, gpu_instances, procs, proc_of, proc_first, instanced, instance_features, name: sc.name.clone(), up, base, bg, exposure, exposure_settings, tan_half_fov: c.tan_half_fov, scene_camera, dof: c.dof.clone(), tables, fixed_features, dynamic, edit, sky: None, emit, sky_inputs, mat_runs, tri_order }, sc.preset.post)) + Ok((LoadedScene { timings, triangles, tris, tri_nodes, tri_instances, pool_tris, meshes, instances, gpu_instances, procs, proc_of, proc_first, instanced, instance_features, name: sc.name.clone(), up, base, bgs, exposure, exposure_settings, tan_half_fov: c.tan_half_fov, scene_camera, dof: c.dof.clone(), tables, fixed_features, dynamic, edit, sky: None, emit, sky_inputs, mat_runs, tri_order }, sc.preset.post)) } diff --git a/renderer/src/live/mod.rs b/renderer/src/live/mod.rs index 8a7dede..2eeda4f 100644 --- a/renderer/src/live/mod.rs +++ b/renderer/src/live/mod.rs @@ -47,6 +47,7 @@ use web_time::Instant; mod edits; mod frame; +mod layout; mod load; #[cfg(not(target_arch = "wasm32"))] mod session; @@ -428,21 +429,6 @@ fn dof_params(dof: Option<&crate::desc::Dof>, tan_half_fov: f32, w: u32, h: u32) [diameter * 0.5 / sensor_h / aspect * w as f32, d.focus_distance, 0.025 * w as f32, 0.0] } -/// The grid a live pass runs over (invocations; workgroups of 8 x 8, but `exposure`'s one): the traced grid, the -/// previous frame's (`lock_carry`), the output grid, a quarter of it (bloom, the luminance), or its 16 x 16 tiles. -fn pass_grid(name: &str, out: (u32, u32), traced: (u32, u32), prev_traced: (u32, u32)) -> (u32, u32) { - match name { - "exposure" => (8, 8), - "dof_tiles" => (out.0.div_ceil(16), out.1.div_ceil(16)), - "lock_carry" => (prev_traced.0.max(1), prev_traced.1.max(1)), - "refl" | "dtemporal" | "stemporal" | "compose" | "judge" | "reject" => traced, - _ if name.starts_with("atrous") || name.starts_with("sfilt") => traced, - _ if name.starts_with("bloom") || name.starts_with("lum_") => (out.0.div_ceil(4), out.1.div_ceil(4)), - _ => out, - } -} - - /// Light LOD (`emitter_lod`): emitters within this distance of the camera keep their sampling weight, farther ones /// lose it as (d0 / d)^2. const LIGHT_LOD_D0: f32 = 10.0; @@ -782,7 +768,8 @@ impl LiveCore { let size = self.w as u64 * self.h as u64 * 16; let read = d.create_buffer(&wgpu::BufferDescriptor { label: Some("hdr read"), size, usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false }); let mut e = d.create_command_encoder(&Default::default()); - e.copy_buffer_to_buffer(&self.targets.fa.buffer, self.targets.fa.offset, &read, 0, size); + let fa = self.targets.buf("fa"); + e.copy_buffer_to_buffer(&fa.buffer, fa.offset, &read, 0, size); self.g.queue.submit([e.finish()]); read.slice(..).map_async(wgpu::MapMode::Read, |_| {}); d.poll(wgpu::PollType::wait_indefinitely())?; @@ -813,8 +800,10 @@ impl LiveCore { Ok(v) }; let half = |b: u32, hi: bool| half::f16::from_bits(if hi { (b >> 16) as u16 } else { b as u16 }).to_f32(); - let composed = read(&t.fb.buffer, t.fb.offset, tn * 16)?; - let spec = read(&t.spec_pp.buffer, t.spec_pp.offset + n * 8, tn * 8)?; // spec_pp slot 1: (rgb, history) as two pairs of f16 + let fb = t.buf("fb"); + let composed = read(&fb.buffer, fb.offset, tn * 16)?; + let spec_pp = t.buf("spec_pp"); + let spec = read(&spec_pp.buffer, spec_pp.offset + n * 8, tn * 8)?; // spec_pp slot 1: (rgb, history) as two pairs of f16 let samples = read(&self.sc.up.accum, 0, tn * 80)?; // the kernel's samples (live.wgsl `Pixel`): emit at 48 let (composed, spec, samples): (&[[f32; 4]], &[[u32; 2]], &[[f32; 20]]) = (bytemuck::cast_slice(&composed), bytemuck::cast_slice(&spec), bytemuck::cast_slice(&samples)); let (mut dif, mut spe, mut emi) = (Vec::with_capacity(w * h * 3), Vec::with_capacity(w * h * 3), Vec::with_capacity(w * h * 3)); @@ -846,7 +835,8 @@ impl LiveCore { let d = &self.g.device; let read = d.create_buffer(&wgpu::BufferDescriptor { label: Some("ae read"), size: 16, usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false }); let mut e = d.create_command_encoder(&Default::default()); - e.copy_buffer_to_buffer(&self.targets.ae_state.buffer, self.targets.ae_state.offset, &read, 0, 16); + let ae = self.targets.buf("ae_state"); + e.copy_buffer_to_buffer(&ae.buffer, ae.offset, &read, 0, 16); self.g.queue.submit([e.finish()]); read.slice(..).map_async(wgpu::MapMode::Read, |_| {}); d.poll(wgpu::PollType::wait_indefinitely())?; diff --git a/renderer/src/live/targets.rs b/renderer/src/live/targets.rs index 30ed659..16e660a 100644 --- a/renderer/src/live/targets.rs +++ b/renderer/src/live/targets.rs @@ -1,5 +1,6 @@ //! The live passes' GPU side: the shader module and its pipelines (`Passes`), the screen-size buffers and the bind -//! group (`Targets`), and the tonemap LUT and bloom pyramid they are sized for. +//! groups (`Targets`), from the layout live.wgsl declares and `layout`'s table; and the tonemap LUT and bloom pyramid +//! they are sized for. use super::*; @@ -57,99 +58,78 @@ pub(crate) fn live_source(merged: bool) -> std::borrow::Cow<'static, str> { } pub(super) struct Passes { - pub(super) layout: wgpu::BindGroupLayout, - pub(super) pipes: Vec<(&'static str, wgpu::ComputePipeline)>, + /// Each pass's bind group layout: the bindings it uses. In the merged layout (merged.rs) one for all. + pub(super) layouts: Vec, + /// The passes in `layout::PASSES`' order, with the grid each runs over. + pub(super) pipes: Vec<(&'static str, layout::Grid, wgpu::ComputePipeline)>, +} + +/// A binding's layout entry (live.wgsl's kind of it). +fn layout_entry(b: &layout::Binding) -> wgpu::BindGroupLayoutEntry { + let ty = match b.kind { + layout::Kind::Uniform => wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None }, + layout::Kind::Storage { read_only, .. } => wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only }, has_dynamic_offset: false, min_binding_size: None }, + layout::Kind::Texture(view_dimension) => wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension, multisampled: false }, + layout::Kind::Sampler => wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + }; + wgpu::BindGroupLayoutEntry { binding: b.binding, visibility: wgpu::ShaderStages::COMPUTE, ty, count: None } } impl Passes { pub(super) fn new(g: &gpu::Gpu) -> Passes { + layout::check(); let d = &g.device; let module = d.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("live"), source: wgpu::ShaderSource::Wgsl(live_source(g.merged).into()) }); - let buf = |binding: u32, read_only: bool| wgpu::BindGroupLayoutEntry { - binding, - visibility: wgpu::ShaderStages::COMPUTE, - ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only }, has_dynamic_offset: false, min_binding_size: None }, - count: None, + let pipe = |name: &str, layout: &wgpu::BindGroupLayout| { + let pl = d.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: None, bind_group_layouts: &[Some(layout)], immediate_size: 0 }); + d.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { label: Some(name), layout: Some(&pl), module: &module, entry_point: Some(name), compilation_options: Default::default(), cache: g.cache() }) }; - let mut entries = vec![wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::COMPUTE, - ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None }, - count: None, - }]; - entries.extend([buf(1, true), buf(2, false), buf(3, false), buf(4, false), buf(5, false), buf(6, false), buf(7, false), buf(8, false), buf(9, false), buf(10, false)]); - entries.push(wgpu::BindGroupLayoutEntry { - binding: 11, - visibility: wgpu::ShaderStages::COMPUTE, - ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D3, multisampled: false }, - count: None, - }); - entries.push(wgpu::BindGroupLayoutEntry { binding: 12, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None }); - entries.extend([buf(13, false), buf(14, false), buf(15, false), buf(16, false), buf(17, false), buf(18, false)]); - entries.extend([buf(19, false), buf(20, false), buf(21, false), buf(22, false), buf(23, true), buf(24, false), buf(25, true), buf(26, true), buf(27, false)]); - // The merged layout: the buffers that stay, the slabs, and the offsets' uniform (merged.rs). - if g.merged { + let (layouts, pipes) = if g.merged { + // The merged layout: one for all passes, the buffers that stay, the slabs, and the offsets' uniform. use crate::merged::{LIVE_KEPT, OFFSETS_BINDING, SLABS}; - entries.retain(|e| !matches!(e.ty, wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { .. }, .. }) || LIVE_KEPT.iter().any(|k| k.0 == e.binding)); - entries.extend(SLABS.iter().map(|s| buf(s.binding, s.read_only))); - entries.push(wgpu::BindGroupLayoutEntry { binding: OFFSETS_BINDING, ..entries[0] }); - } - let layout = d.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("live"), entries: &entries }); - let pl = d.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: None, bind_group_layouts: &[Some(&layout)], immediate_size: 0 }); - let pipes = ["refl", "dtemporal", "atrous0", "atrous1", "atrous2", "atrous3", "atrous4", "stemporal", "sfilt0", "sfilt1", "sfilt2", "lock_carry", "compose", "judge", "reject", "upscale", "dof_tiles", "dof", "mblur", "lum_down", "exposure", "lum_blur", "bloom_down", "bloom_dn1", "bloom_dn2", "bloom_dn3", "bloom_dn4", - "bloom_h5", "bloom_v5", "bloom_h4", "bloom_v4", "bloom_h3", "bloom_v3", "bloom_h2", "bloom_v2", "bloom_h1", "bloom_v1", "bloom_h0", "bloom_v0", "post"] - .into_iter() - .map(|name| { - (name, d.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { label: Some(name), layout: Some(&pl), module: &module, entry_point: Some(name), compilation_options: Default::default(), cache: g.cache() })) - }) - .collect(); + let mut entries: Vec = layout::BINDINGS.iter().map(layout_entry).collect(); + let storage = |e: &wgpu::BindGroupLayoutEntry| matches!(e.ty, wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { .. }, .. }); + entries.retain(|e| !storage(e) || LIVE_KEPT.iter().any(|k| k.0 == e.binding)); + entries.extend(SLABS.iter().map(|s| layout_entry(&layout::Binding { name: s.name, binding: s.binding, kind: layout::Kind::Storage { read_only: s.read_only, stride: s.stride, len: 0 } }))); + entries.push(wgpu::BindGroupLayoutEntry { binding: OFFSETS_BINDING, ..layout_entry(layout::binding("L")) }); + let all = d.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("live"), entries: &entries }); + let pipes = layout::PASSES.iter().map(|&(name, grid)| (name, grid, pipe(name, &all))).collect(); + (vec![all], pipes) + } else { + // A layout a pass, of the bindings it uses. + let mut layouts = vec![]; + let mut pipes = vec![]; + for &(name, grid) in layout::PASSES { + let entries: Vec = layout::uses(name).iter().map(|&b| layout_entry(layout::BINDINGS.iter().find(|x| x.binding == b).expect("a binding of live.wgsl"))).collect(); + let l = d.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some(name), entries: &entries }); + pipes.push((name, grid, pipe(name, &l))); + layouts.push(l); + } + (layouts, pipes) + }; g.save_pipeline_cache(); - Passes { layout, pipes } + Passes { layouts, pipes } + } + + /// The layout pass `k` binds. + pub(super) fn layout(&self, k: usize) -> &wgpu::BindGroupLayout { + &self.layouts[if self.layouts.len() == 1 { 0 } else { k }] } } /// Screen-size targets shared across scene reloads. pub struct Targets { pub(super) params: wgpu::Buffer, - /// What lasts from one frame to the next (live.wgsl, "The live passes' buffers"), two halves each: this frame's - /// and the previous frame's (`LiveParams::state`). Per traced sample: the diffuse irradiance's history, its - /// luminance moments and its fast mean in colour, the specular history, the g-buffer, the upscaler's state; per - /// output pixel: the colour history. - pub(super) diffuse_hist: Buf, - pub(super) diffuse_mom: Buf, - pub(super) diffuse_fast: Buf, - pub(super) spec_hist: Buf, - pub(super) gbuffer: Buf, - pub(super) up_state: Buf, - pub(super) color_hist: Buf, - /// What lives within a frame (live.wgsl says who uses each when): per traced sample, screen velocity, view depth - /// and animated shading; the specular filter's two slots; the scratch the diffuse filter and the upscaled image - /// share; the irradiance filter's ping-pong, the composed colour and the image; bloom's pyramid and scratch. - pub(super) sample_vel: Buf, - pub(super) spec_pp: Buf, - pub(super) sb: Buf, - pub(super) fa: Buf, - pub(super) fb: Buf, - pub(super) bloom: [Buf; 2], - /// Reflection resolve: the resolved specular reflection per traced pixel (live.wgsl `refl`), from the trace - /// kernel's specular-lobe rays (2 vec4 per traced pixel, pathtrace.wgsl `refl`: `kernel.refl`). - pub(super) refl_res: Buf, + /// The passes' own buffers (`layout::RESOURCES`' `Own`), by binding: what each holds and when is live.wgsl's + /// table ("The live passes' buffers"). + pub(super) bufs: Vec<(u32, Buf)>, + /// The output (`out_px`: sRGB BGRA8 a pixel), what the hosts draw. pub out: wgpu::Buffer, - /// Auto exposure: 64 histogram buckets (fixed point), state (exposure, target, mean log2 luminance, valid). - pub(super) ae_bins: Buf, - pub(super) ae_state: Buf, - /// Quarter-resolution log2 luminance and its edge-aware blur (local exposure base). - pub(super) lumq: Buf, - pub(super) lumb: Buf, - /// The previous frame's thin-feature locks, carried to where they are now (live.wgsl `lock_carry`), per traced - /// sample: atomic, `compose` clears what it reads. - pub(super) lock_fwd: Buf, /// The kernel's group 1 (`gpu::KernelBufs`): the radiance cache, and what the passes here read of /// a trace: the specular-lobe rays (`refl`), motion vectors per traced pixel (pathtrace.wgsl `motion_out`) and the /// deterministic direct light per traced pixel (`direct_out`). pub kernel: gpu::KernelBufs, - /// The merged layout (merged.rs): the slabs the buffers above are ranges of (`SLABS`' first five; the sixth is - /// the kernel's). + /// The merged layout (merged.rs): the slabs the buffers above are ranges of (`SLABS` but the kernel's). pub(super) slabs: Option<[wgpu::Buffer; 5]>, pub(super) lut_tex: wgpu::Texture, pub(super) lut_view: wgpu::TextureView, @@ -159,11 +139,11 @@ pub struct Targets { impl Targets { /// For a view of `w` x `h` and a radiance cache of `cache` entries. pub(super) fn new(g: &gpu::Gpu, w: u32, h: u32, lut: &[u16], cache: u64) -> Targets { - use crate::merged::{SLABS, SLAB_A, SLAB_F1, SLAB_U2, SLAB_U4, SLAB_V4}; + use crate::merged::{LIVE_BUFS, SLABS, SLAB_K}; let d = &g.device; let n = (w * h) as u64; let sb = |label: &str, bytes: u64| d.create_buffer(&wgpu::BufferDescriptor { label: Some(label), size: bytes, usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC, mapped_at_creation: false }); - let bn = (w.div_ceil(4) * h.div_ceil(4)) as u64 * 16; + let quarter = (w.div_ceil(4) * h.div_ceil(4)) as u64; let texels = |j: u32| bloom_dim(w, h, j).0 as u64 * bloom_dim(w, h, j).1 as u64; let bloom_texels = (0..5).map(texels).sum::() + (0..6).map(|k| texels(bloom_stage_level(k))).sum::(); let lut_tex = d.create_texture(&wgpu::TextureDescriptor { @@ -182,68 +162,47 @@ impl Targets { wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(LUT * 8), rows_per_image: Some(LUT) }, wgpu::Extent3d { width: LUT, height: LUT, depth_or_array_layers: LUT }, ); - // The buffers, in the order of the fields below: a buffer each, or in the merged layout (merged.rs) a range of - // the slab of its element type (each range starts at a multiple of 16 bytes: of every slab's element). - // (The lasting ones are two halves of `n` elements: this frame's and the previous frame's.) - let specs: [(&str, u64, usize); 20] = [ - ("diffuse_hist", 2 * n * 16, SLAB_V4), - ("diffuse_mom", 2 * n * 16, SLAB_V4), - ("diffuse_fast", 2 * n * 8, SLAB_U2), - ("spec_hist", 2 * n * 16, SLAB_V4), - ("gbuffer", 2 * n * 8, SLAB_U2), - ("up_state", 2 * n * 16, SLAB_U4), - ("color_hist", 2 * n * 16, SLAB_V4), - ("sample_vel", n * 16, SLAB_V4), - ("spec_pp", n * 16, SLAB_U2), // two slots of `n` (vec2) - ("sb", n * 16, SLAB_V4), - ("fa", n * 16, SLAB_V4), - ("fb", n * 16, SLAB_V4), - ("bloom_a", bloom_texels * 16, SLAB_V4), - ("bloom_b", bn, SLAB_V4), - ("refl_res", n * 16, SLAB_V4), - ("ae_bins", 64 * 4, SLAB_A), - ("ae_state", 32, SLAB_V4), - ("lumq", bn / 4, SLAB_F1), - ("lumb", bn / 4, SLAB_F1), - ("lock_fwd", n * 4, SLAB_A), - ]; - let slabs: Option<[wgpu::Buffer; 5]> = g.merged.then(|| std::array::from_fn(|k| sb(SLABS[k].name, specs.iter().filter(|s| s.2 == k).map(|s| s.1.next_multiple_of(16)).sum()))); + // The passes' own buffers, in binding order: their bytes, and in the merged layout the slab of their element + // type (merged.rs `LIVE_BUFS`; none: a buffer that stays, `out_px`). Each range in a slab starts at a multiple + // of 16 bytes: of every slab's element. + let own: Vec<(&layout::Binding, u64, Option)> = layout::BINDINGS + .iter() + .filter_map(|b| { + let layout::Res::Own(count) = layout::resource(b) else { return None }; + let layout::Kind::Storage { stride, len, .. } = b.kind else { unreachable!("layout::check") }; + let elements = match count { + layout::Count::Pixels(k) => k * n, + layout::Count::Quarter => quarter, + layout::Count::Bloom => bloom_texels, + layout::Count::Declared => len, + }; + Some((b, elements * stride, LIVE_BUFS.iter().find(|m| m.name == b.name).map(|m| m.slab))) + }) + .collect(); + let slabs: Option<[wgpu::Buffer; 5]> = g.merged.then(|| { + debug_assert_eq!(SLAB_K, 5, "the owned slabs are the first five"); + std::array::from_fn(|k| sb(SLABS[k].name, own.iter().filter(|o| o.2 == Some(k)).map(|o| o.1.next_multiple_of(16)).sum())) + }); let mut at = [0u64; 5]; - let bufs: Vec = specs + let bufs: Vec<(u32, Buf)> = own .iter() - .map(|&(label, bytes, slab)| match &slabs { - None => Buf::whole(sb(label, bytes)), - Some(s) => { - let b = Buf { buffer: s[slab].clone(), offset: at[slab], size: bytes }; - at[slab] += bytes.next_multiple_of(16); - b - } + .map(|&(b, bytes, slab)| { + let buf = match (&slabs, slab) { + (Some(s), Some(k)) => { + let r = Buf { buffer: s[k].clone(), offset: at[k], size: bytes }; + at[k] += bytes.next_multiple_of(16); + r + } + _ => Buf::whole(sb(b.name, bytes)), + }; + (b.binding, buf) }) .collect(); - let mut bufs = bufs.into_iter(); - let mut b = || bufs.next().expect("a buffer of `specs`"); + let out = bufs.iter().find(|b| b.0 == layout::binding("out_px").binding).expect("out_px").1.buffer.clone(); Targets { params: d.create_buffer(&wgpu::BufferDescriptor { label: Some("live params"), size: std::mem::size_of::() as u64, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false }), - diffuse_hist: b(), - diffuse_mom: b(), - diffuse_fast: b(), - spec_hist: b(), - gbuffer: b(), - up_state: b(), - color_hist: b(), - sample_vel: b(), - spec_pp: b(), - sb: b(), - fa: b(), - fb: b(), - bloom: [b(), b()], - refl_res: b(), - out: sb("out", n * 4), - ae_bins: b(), - ae_state: b(), - lumq: b(), - lumb: b(), - lock_fwd: b(), + bufs, + out, kernel: gpu::KernelBufs::new(d, g.merged, g.guard_room(), cache, n * 32, n * 16, n * 16), slabs, lut_view: lut_tex.create_view(&Default::default()), @@ -252,77 +211,68 @@ impl Targets { } } - /// What live.wgsl's buffer bindings hold (besides the uniforms at 0, the trace's samples at 1 and `out` at 10). - pub(super) fn buffers(&self) -> [(u32, &Buf); 23] { - [ - (2, &self.diffuse_hist), - (3, &self.color_hist), - (4, &self.gbuffer), - (5, &self.spec_hist), - (6, &self.diffuse_mom), - (7, &self.diffuse_fast), - (8, &self.up_state), - (9, &self.sample_vel), - (13, &self.ae_bins), - (14, &self.ae_state), - (15, &self.lumq), - (16, &self.lumb), - (17, &self.fa), - (18, &self.fb), - (19, &self.bloom[0]), - (20, &self.bloom[1]), - (21, &self.spec_pp), - (22, &self.sb), - (23, &self.kernel.refl), - (24, &self.refl_res), - (25, &self.kernel.motion), - (26, &self.kernel.direct), - (27, &self.lock_fwd), - ] + /// The buffer live.wgsl binds as `name` (the passes' own, or the kernel's). + pub(super) fn buf(&self, name: &str) -> &Buf { + self.buf_at(layout::binding(name)) } - /// The live passes' bind group. (The lasting buffers' halves alternate by `LiveParams::state`, not by binding.) In - /// the merged layout (merged.rs) it binds the slabs, and the offsets the passes read (`LO`). - pub(super) fn bind_group(&self, d: &wgpu::Device, layout: &wgpu::BindGroupLayout, sample: &wgpu::Buffer) -> wgpu::BindGroup { - let mk = || { - fn e(binding: u32, buf: &wgpu::Buffer) -> wgpu::BindGroupEntry<'_> { - wgpu::BindGroupEntry { binding, resource: buf.as_entire_binding() } - } - let mut entries = vec![ - e(0, &self.params), - e(1, sample), - e(10, &self.out), - wgpu::BindGroupEntry { binding: 11, resource: wgpu::BindingResource::TextureView(&self.lut_view) }, - wgpu::BindGroupEntry { binding: 12, resource: wgpu::BindingResource::Sampler(&self.sampler) }, - ]; - let buffers = self.buffers(); - let offsets; - match (&self.slabs, &self.kernel.merged) { - (Some(slabs), Some((live, _))) => { - use crate::merged::{LIVE_BUFS, OFFSETS_BINDING, SLABS, SLAB_K}; - // Each merged buffer's (first element, length) in its slab, in `MergedOffsets`' order. - let lo: Vec<[u32; 2]> = LIVE_BUFS - .iter() - .map(|m| { - let buf = buffers.iter().find(|b| b.0 == m.binding).expect("a merged buffer's binding").1; - let stride = SLABS[m.slab].stride; - debug_assert!(buf.offset % stride == 0 && buf.size % stride == 0); - [(buf.offset / stride) as u32, (buf.size / stride) as u32] - }) - .collect(); - // (And the struct's unused last pair: `merged::OFFSETS_BYTES`.) - let mut lo = lo; - lo.push([0, 0]); - debug_assert_eq!(lo.len() * 8, crate::merged::OFFSETS_BYTES); - offsets = wgpu::util::DeviceExt::create_buffer_init(d, &wgpu::util::BufferInitDescriptor { label: Some("live offsets"), contents: bytemuck::cast_slice(&lo), usage: wgpu::BufferUsages::UNIFORM }); - entries.extend(slabs.iter().zip(&SLABS).map(|(s, m)| e(m.binding, s))); - entries.push(e(SLABS[SLAB_K].binding, live)); - entries.push(e(OFFSETS_BINDING, &offsets)); - } - _ => entries.extend(buffers.iter().map(|(binding, buf)| wgpu::BindGroupEntry { binding: *binding, resource: buf.binding() })), - } - d.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("live"), layout, entries: &entries }) + fn buf_at(&self, b: &layout::Binding) -> &Buf { + match layout::resource(b) { + layout::Res::KernelRefl => &self.kernel.refl, + layout::Res::KernelMotion => &self.kernel.motion, + layout::Res::KernelDirect => &self.kernel.direct, + layout::Res::Own(_) => &self.bufs.iter().find(|x| x.0 == b.binding).expect("an own buffer").1, + r => panic!("`{}` is bound to {r:?}, not a buffer of the passes'", b.name), + } + } + + /// The bind group of each pass, in `Passes::pipes`' order (`sample`: the trace's samples). In the merged layout + /// (merged.rs) one for all, the slabs and the offsets the passes read (`LO`). + pub(super) fn bind_groups(&self, d: &wgpu::Device, passes: &Passes, sample: &wgpu::Buffer) -> Vec { + let entry = |b: &'_ layout::Binding| -> wgpu::BindGroupEntry<'_> { + let resource = match layout::resource(b) { + layout::Res::Params => self.params.as_entire_binding(), + layout::Res::Samples => sample.as_entire_binding(), + layout::Res::Lut => wgpu::BindingResource::TextureView(&self.lut_view), + layout::Res::LutSampler => wgpu::BindingResource::Sampler(&self.sampler), + _ => self.buf_at(b).binding(), + }; + wgpu::BindGroupEntry { binding: b.binding, resource } }; - mk() + match (&self.slabs, &self.kernel.merged) { + (Some(slabs), Some((live, _))) => { + use crate::merged::{LIVE_BUFS, LIVE_KEPT, OFFSETS_BINDING, SLABS, SLAB_K}; + // Each merged buffer's (first element, length) in its slab, in `MergedOffsets`' order, and the + // struct's unused last pair (`merged::OFFSETS_BYTES`). + let mut lo: Vec<[u32; 2]> = LIVE_BUFS + .iter() + .map(|m| { + let buf = self.buf(m.name); + let stride = SLABS[m.slab].stride; + debug_assert!(buf.offset % stride == 0 && buf.size % stride == 0); + [(buf.offset / stride) as u32, (buf.size / stride) as u32] + }) + .collect(); + lo.push([0, 0]); + debug_assert_eq!(lo.len() * 8, crate::merged::OFFSETS_BYTES); + let offsets = wgpu::util::DeviceExt::create_buffer_init(d, &wgpu::util::BufferInitDescriptor { label: Some("live offsets"), contents: bytemuck::cast_slice(&lo), usage: wgpu::BufferUsages::UNIFORM }); + let storage = |b: &layout::Binding| matches!(b.kind, layout::Kind::Storage { .. }); + let mut entries: Vec = layout::BINDINGS.iter().filter(|b| !storage(b) || LIVE_KEPT.iter().any(|k| k.0 == b.binding)).map(entry).collect(); + entries.extend(slabs.iter().zip(&SLABS).map(|(s, m)| wgpu::BindGroupEntry { binding: m.binding, resource: s.as_entire_binding() })); + entries.push(wgpu::BindGroupEntry { binding: SLABS[SLAB_K].binding, resource: live.as_entire_binding() }); + entries.push(wgpu::BindGroupEntry { binding: OFFSETS_BINDING, resource: offsets.as_entire_binding() }); + let all = d.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("live"), layout: passes.layout(0), entries: &entries }); + vec![all; passes.pipes.len()] + } + _ => passes + .pipes + .iter() + .enumerate() + .map(|(k, (name, ..))| { + let entries: Vec = layout::uses(name).iter().map(|&b| entry(layout::BINDINGS.iter().find(|x| x.binding == b).expect("a binding of live.wgsl"))).collect(); + d.create_bind_group(&wgpu::BindGroupDescriptor { label: Some(name), layout: passes.layout(k), entries: &entries }) + }) + .collect(), + } } } diff --git a/renderer/web/Cargo.lock b/renderer/web/Cargo.lock index 14a631b..814be5f 100644 --- a/renderer/web/Cargo.lock +++ b/renderer/web/Cargo.lock @@ -2402,6 +2402,7 @@ dependencies = [ "half", "image", "libc", + "naga", "physics-core", "pollster", "rayon",