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",