diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e2f0ca6..fba56cc 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -2311,6 +2311,13 @@ changed in each (171-175 byte-identical, the rest within their run-to-run spread pass. The district's tree builds in 333 ms against main's 321 (one load each; an earlier load of the same build 338). No shader changed. +**A scene's features as `FEAT` bits (2026-11-14, after it).** Vertex colours, density grids, instance colours, unlit +materials and the background were `@if` flags: each combination another kernel source. They are now bits of `FEAT` +(the pipeline-overridable constant the other features use), their bindings and fields always declared, and +`OBJECT_STRIDE` an override from F_TINT. Only units still change the source. render-identity against main (native, +merged, uniform tables, guarded and not): 0 of 190 changed; web-hello passes; Bistro's trace the same natively and +merged. + ## 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 b817b5c..3e68924 100644 --- a/docs/RUNTIME.md +++ b/docs/RUNTIME.md @@ -1237,11 +1237,10 @@ The path-tracing kernel is one WESL module: `pathtrace.wgsl`, then `trace_units. translation (`@if(flag)` on a declaration or a statement, `@if(flag) { .. } @else { .. }` on a block of declarations). wesl-rs (WESL's compiler in Rust, natively and in a browser alike) makes a kernel's WGSL from a variant (gpu.rs `KernelVariant`, `kernel_wgsl`): the traversal (`hwrt`), a device's switches (`strict`, -`cutout_lite`, `cutout_lite2`, `last_direct`, `light_pick`), a scene's features that change declarations (`vcolor`, -`grid`, `tint`, `unlit`, `units`, `background`; the other features switch code under the pipeline-overridable constant -`FEAT`), the binding layout (`merged`, the browser's; `uniform_tables`, Android's) and the loop guard (`guard`). -pathtrace.wgsl's head names them; each is said where it changes something, and a flag the source names that a -kernel is not given is an error. +`cutout_lite`, `cutout_lite2`, `last_direct`, `light_pick`), software traversal's units (`units`), the binding layout (`merged`, the browser's; `uniform_tables`, Android's) and the loop guard (`guard`). +pathtrace.wgsl's head names them; a flag the source names that a kernel is not given is an error. A scene's other +features are bits of `FEAT`, a pipeline-overridable constant: one kernel source for all of them (`OBJECT_STRIDE` +too is an override, from F_TINT). - The numbers a variant is compiled for are WESL constants, which the source imports (`import constants::..`; gpu.rs `KernelVariant::constants`): the merged layout's array lengths, the uniform tables', the loop guard's limit. One the source imports and is not given is an error. diff --git a/renderer/shaders/pathtrace.wgsl b/renderer/shaders/pathtrace.wgsl index 481b225..f87dc92 100644 --- a/renderer/shaders/pathtrace.wgsl +++ b/renderer/shaders/pathtrace.wgsl @@ -12,9 +12,8 @@ // strict, cutout_lite, cutout_lite2, last_direct, light_pick // a device's switches (gpu.rs `KernelSwitches`: what a GPU the kernel as written is wrong or // too slow on needs) -// vcolor, grid, tint, unlit, units, background -// a scene's features that change the kernel's declarations rather than switch code in it -// (gpu.rs `F_SOURCE`; the others are `FEAT`, a pipeline-overridable constant) +// units software traversal's second level (gpu.rs `F_UNITS`); a scene's other features are bits +// of `FEAT`, a pipeline-overridable constant // merged the merged binding layout (merged.rs: at most eight storage buffers a stage, a browser's) // uniform_tables four tables in uniform buffers (merged.rs `UNIFORM_TABLES`: Android's drivers) // guard the loop guard (gpu.rs `loop_guard_of`, `guard_loops`) @@ -55,7 +54,7 @@ const EMIT_RIS_M: u32 = 6u; const VOL_RATE_MASK: u32 = 1u; const MAT_DOUBLE_SIDED: i32 = 1; const MAT_EMITTER_CARD: i32 = 2; -@if(unlit) const MAT_UNLIT: i32 = 4; +const MAT_UNLIT: i32 = 4; // Live (real-time) mode: write this frame's 1-spp sample + first-hit distance instead of accumulating. override LIVE: bool = false; // Features the scene uses (gpu::F_*): the rest is compiled out, so plain scenes pay nothing for them. @@ -74,6 +73,11 @@ const F_PEMIT: u32 = 65536u; // opaque particles: their emitter handling (nomina const F_HMEDIA: u32 = 131072u; // homogeneous media (water): live attenuation of paths and shadow rays (media_homogeneous) const F_AERIAL: u32 = 16777216u; // the sky's aerial perspective (aerial_tr) on segments ending on a surface const F_PROC: u32 = 262144u; // procedural geometry (ProceduralMesh): AABB candidates in every query, `surface_proc` +const F_TINT: u32 = 524288u; // instance colours: OBJECT_STRIDE 4, `obj_tint` +const F_UNLIT: u32 = 1048576u; // MeshBasicMaterial (MAT_UNLIT) +const F_BACKGROUND: u32 = 2097152u; // scene.background (`sky_mis`) +const F_VCOLOR: u32 = 4194304u; // vertex colours (`vcolor`) +const F_GRID: u32 = 33554432u; // density grids (`vgrid`) fn has(f: u32) -> bool { return (FEAT & f) != 0u; } struct Params { @@ -102,7 +106,7 @@ struct Params { ltree: vec4, // light tree (ltree.rs): first node in `emitters` (bits), its entry count (bits: the moving emitters' first), P(pick a moving one), candidates at camera hits (0 = off) procs: vec4, // procedural geometry (F_PROC): its data's first word in env_cdf, object count, first record in `instances`, - aerial: vec4, // the sky's aerial perspective (F_AERIAL): density multiple, Rayleigh scale, Mie scale, - - @if(background) background: vec4, // three.js scene.background (radiance), 1 + background: vec4, // three.js scene.background (radiance), 1 (F_BACKGROUND) }; struct TriAttr { n0: vec4, n1: vec4, n2: vec4, uv: vec4, t: vec4, x: vec4 }; @@ -199,12 +203,12 @@ struct Medium { w0: vec4, w1: vec4, w2: vec4, a: vec4, b: ve fn medium_at(i: u32) -> Medium { return media[i]; } } } -// Density grids (gpu.rs F_GRID, `GridsGpu`): field A of every grid, stacked along z. -@if(grid) @group(0) @binding(19) var grid_tex: texture_3d; +// Density grids (gpu.rs F_GRID, `GridsGpu`): field A of every grid, stacked along z; one cell in scenes without. +@group(0) @binding(19) var grid_tex: texture_3d; // Vertex colours (three.js `vertexColors`; gpu.rs F_VCOLOR): three sRGB 8-bit colours per triangle, after the previous // vertices in `prev_verts` from vec4 P.procs.w. -@if(vcolor) fn vcolor_word(k: u32) -> u32 { return bitcast(prev_verts[P.procs.w + k / 4u][k % 4u]); } -@if(vcolor) fn vcolor(t: u32, u: f32, v: f32) -> vec3 { +fn vcolor_word(k: u32) -> u32 { return bitcast(prev_verts[P.procs.w + k / 4u][k % 4u]); } +fn vcolor(t: u32, u: f32, v: f32) -> vec3 { let c0 = srgb_to_linear(unpack4x8unorm(vcolor_word(t * 3u)).rgb); let c1 = srgb_to_linear(unpack4x8unorm(vcolor_word(t * 3u + 1u)).rgb); let c2 = srgb_to_linear(unpack4x8unorm(vcolor_word(t * 3u + 2u)).rgb); @@ -802,8 +806,8 @@ fn glass_at(h: Hit, d: vec3, t: f32) -> GlassAt { var tr = clamp(m.layer.x, 0.0, 1.0) * (1.0 - clamp(m.params.x, 0.0, 1.0)); if (tr <= 0.0) { return g; } g.tint = m.base_color.rgb; - @if(tint) g.tint *= obj_tint(object_of(a)); - @if(vcolor) g.tint *= vcolor(h.tri, h.u, h.v); + if (has(F_TINT)) { g.tint *= obj_tint(object_of(a)); } + if (has(F_VCOLOR)) { g.tint *= vcolor(h.tri, h.u, h.v); } if (has(F_TEX) && (m.tex0.x >= 0 || m.uv.z > 0.5)) { let uv = mesh_uv(a, m, h.u, h.v); var lod = mesh_lod(a, m) + log2(max(t * 2.0 * P.cam_pos.w / lod_height(), 1e-7)) - LOD_BIAS; @@ -1054,10 +1058,9 @@ fn emissive_material_function(f: i32, i: MatIn, m: Material, base: MatOut) -> Ma fn object_of(a: TriAttr) -> u32 { return bitcast(a.x.y); } // vec4s per object (scene.rs OBJECT_STRIDE): (pivot, power), (bounds size, time of the last power edit), per-instance // data (`MatIn.custom`). -@if(!tint) const OBJECT_STRIDE: u32 = 3u; -@if(tint) const OBJECT_STRIDE: u32 = 4u; +override OBJECT_STRIDE: u32 = select(3u, 4u, (FEAT & F_TINT) != 0u); // An instanced object's instance colour (three.js `instanceColor`), white for every other object. -@if(tint) fn obj_tint(oi: u32) -> vec3 { return object_at(oi * OBJECT_STRIDE + 3u).rgb; } +fn obj_tint(oi: u32) -> vec3 { return object_at(oi * OBJECT_STRIDE + 3u).rgb; } // Instance power (UE material "Power", scripts animate it): scales the emission of everything the node draws. fn obj_power(a: TriAttr) -> f32 { return object_at(object_of(a) * OBJECT_STRIDE).w; } @@ -1149,8 +1152,8 @@ fn surface(o: vec3, d: vec3, h: Hit, width: f32) -> Surface { s.front = front; s.card = m.tex1.z == MAT_EMITTER_CARD; var base = m.base_color.rgb; - @if(tint) base *= obj_tint(object_of(a)); - @if(vcolor) base *= vcolor(i, h.u, h.v); + if (has(F_TINT)) { base *= obj_tint(object_of(a)); } + if (has(F_VCOLOR)) { base *= vcolor(i, h.u, h.v); } var rough = m.params.y; var metal = m.params.x; var spec = m.params.z; @@ -1161,7 +1164,7 @@ fn surface(o: vec3, d: vec3, h: Hit, width: f32) -> Surface { } s.emissive = emission(m, uv, lod) * obj_power(a); // three.js MeshBasicMaterial: its colour to camera rays and mirror-like paths, lit by nothing, lighting nothing. - @if(unlit) if (m.tex1.z == MAT_UNLIT) { + if (has(F_UNLIT) && m.tex1.z == MAT_UNLIT) { s.emissive = select(vec3(0.0), base, cur_depth == 0u || mirror_path); base = vec3(0.0); spec = 0.0; @@ -1238,7 +1241,7 @@ fn surface_proc(o: vec3, d: vec3, h: Hit, width: f32) -> Surface { s.front = front; s.card = false; var base = m.base_color.rgb; - @if(tint) base *= obj_tint(oi); + if (has(F_TINT)) { base *= obj_tint(oi); } var rough = m.params.y; var metal = m.params.x; var spec = m.params.z; @@ -1540,7 +1543,7 @@ fn offset_origin(p: vec3, n: vec3) -> vec3 { // environment and the directional lights' disks, MIS-weighted against their sampling in `direct_light`. fn sky_mis(d: vec3, pdf: f32) -> vec3 { // three.js scene.background: what camera rays (and paths bent straight through glass) see where nothing is. - @if(background) if (pdf <= 0.0) { return P.background.rgb; } + if (has(F_BACKGROUND) && pdf <= 0.0) { return P.background.rgb; } var wgt = 1.0; if (pdf > 0.0 && P.counts2.y != 0u) { wgt = power_heuristic(pdf, env_pdf(d)); @@ -2121,7 +2124,7 @@ struct VIn { lp: vec3, // position in the volume's own space: the box [-0.5, 0.5]^3 / the sphere of radius 0.5 time: f32, // scene time, s custom: vec4, // the node's customData - @if(grid) grid: vec2, // the node's density grid: its first layer in the atlas, its cells (10 bits an axis; 0: none) + grid: vec2, // the node's density grid: its first layer in the atlas, its cells (10 bits an axis; 0: none) }; struct VOut { @@ -2132,7 +2135,7 @@ struct VOut { // The node's density grid at `lp` (the volume's own space, the box [-0.5, 0.5]^3): field A's four numbers, trilinear // between cell centres, the border cells' beyond them; zeros for a node without a grid. -@if(grid) fn vgrid(i: VIn, lp: vec3) -> vec4 { +fn vgrid(i: VIn, lp: vec3) -> vec4 { if (i.grid.y == 0u) { return vec4(0.0); } let n = vec3(vec3(i.grid.y & 1023u, (i.grid.y >> 10u) & 1023u, (i.grid.y >> 20u) & 1023u)); let c = clamp((lp + 0.5) * n, vec3(0.5), n - 0.5) + vec3(0.0, 0.0, f32(i.grid.x)); @@ -2176,7 +2179,7 @@ fn medium_eval(k: u32, x: vec3) -> VOut { i.lp = medium_local(m, x); i.time = P.time.x; i.custom = m.c; - @if(grid) i.grid = vec2(bitcast(m.b.z), bitcast(m.b.w)); + i.grid = vec2(bitcast(m.b.z), bitcast(m.b.w)); var r = volume_function(m.a.x, i, m.p); r.extinction = clamp(r.extinction, 0.0, m.b.x); return r; diff --git a/renderer/src/gpu.rs b/renderer/src/gpu.rs index 3f1531f..8230466 100644 --- a/renderer/src/gpu.rs +++ b/renderer/src/gpu.rs @@ -113,8 +113,7 @@ pub struct Params { pub procs: [u32; 4], /// The sky's aerial perspective (F_AERIAL; `scene::aerial_params`): density multiple, Rayleigh scale, Mie scale, -. pub aerial: [f32; 4], - /// three.js `scene.background` (F_BACKGROUND): its radiance, 1. Last, and read only by the kernel patched for it - /// (`KernelVariant::flags`): every other kernel's `Params` ends before it. + /// three.js `scene.background` (F_BACKGROUND): its radiance, 1. pub background: [f32; 4], } @@ -734,23 +733,8 @@ impl KernelVariant { KernelVariant { hwrt, switches, features, ..Default::default() } } - /// The flags of the kernel's conditional translation. A scene's: - /// - vcolor (F_VCOLOR), three.js `vertexColors` (`scene::tri_colors`): three sRGB 8-bit colours per triangle after - /// the previous vertices in `prev_verts` (from vec4 `Params::procs.w`; the kernel has no binding to spare), - /// interpolated in linear at the hit, multiply the base colour of surfaces and glass (white for triangles - /// without them). - /// - grid (F_GRID), density grids (`scene::GridBuild`): the grid atlas at binding 19 (a texture: the buffers are - /// at Metal's limit), `VIn::grid` and `vgrid` for volume functions. - /// - tint (F_TINT), instance colours (`scene::object_stride`): object entries of 4 vec4s, the 4th the instance - /// colour, which multiplies the base colour of surfaces, procedural surfaces and glass (three.js `instanceColor`). - /// - unlit (F_UNLIT), three.js MeshBasicMaterial (`scene::MAT_UNLIT`): its colour (x map, x tint) is emission seen - /// by camera rays and mirror-like paths only, its albedo 0: lit by nothing, lighting nothing (in no emitter list). - /// - units (F_UNITS), software traversal's second level (trace_units.wgsl): the still scene's walk hands its hit - /// to the units' and has no stack of its own (the module's, set to zero once an invocation and not once a ray). - /// - background (F_BACKGROUND), three.js `scene.background`: camera rays that miss, and paths bent straight on - /// through glass (the rays `sky_mis` gets with pdf 0), see `Params::background`, which `Params` ends with in - /// this kernel only. - pub fn flags(&self) -> [(&'static str, bool); 15] { + /// The flags of the kernel's conditional translation (pathtrace.wgsl's head). + pub fn flags(&self) -> [(&'static str, bool); 10] { let (k, f) = (&self.switches, |bit: u32| self.features & bit != 0); [ ("hwrt", self.hwrt), @@ -759,12 +743,7 @@ impl KernelVariant { ("cutout_lite2", k.cutout_lite >= 2), ("last_direct", k.last_direct), ("light_pick", k.light_pick), - ("vcolor", f(F_VCOLOR)), - ("grid", f(F_GRID)), - ("tint", f(F_TINT)), - ("unlit", f(F_UNLIT)), ("units", f(F_UNITS)), - ("background", f(F_BACKGROUND)), ("merged", self.merged.is_some()), ("uniform_tables", self.uniform_tables), ("guard", self.guard.is_some()), @@ -1290,20 +1269,20 @@ pub const F_HMEDIA: u32 = 131072; /// Procedural geometry (`ProceduralMesh`; pathtrace.wgsl `proc_*`): AABB candidates in every ray query, its hits' /// own surface path. Compiled out where no scene has any (`THREERT_FEATURES_ON` forces it in, to measure that cost). pub const F_PROC: u32 = 262144; -/// Instance colours (`InstancedMesh.setColorAt`): object entries of 4 vec4s, the base colour times the 4th (`KernelVariant::flags`; -/// `scene::object_stride`). +/// Instance colours (`InstancedMesh.setColorAt`): object entries of 4 vec4s, the base colour times the 4th +/// (`scene::object_stride`). pub const F_TINT: u32 = 524288; -/// three.js MeshBasicMaterial (`scene::MAT_UNLIT`): seen by camera rays, lit by nothing, lighting nothing (`KernelVariant::flags`). +/// three.js MeshBasicMaterial (`scene::MAT_UNLIT`): seen by camera rays, lit by nothing, lighting nothing. pub const F_UNLIT: u32 = 1 << 20; -/// three.js `scene.background`: camera rays that miss see `Params::background` (`KernelVariant::flags`). +/// three.js `scene.background`: camera rays that miss see `Params::background`. pub const F_BACKGROUND: u32 = 1 << 21; -/// The features that change the kernel's declarations (`KernelVariant::flags`, `@if` in its source) rather than switch code in it. -/// three.js `vertexColors` (`scene::tri_colors`): a colour per triangle vertex multiplies the base colour (F_SOURCE). +/// three.js `vertexColors` (`scene::tri_colors`): a colour per triangle vertex multiplies the base colour. pub const F_VCOLOR: u32 = 1 << 22; -/// Density grids (`scene::GridBuild`): the grid atlas (binding 19) and `vgrid` for volume functions (F_SOURCE). (Bit -/// 24 is F_AERIAL's.) +/// Density grids (`scene::GridBuild`): the grid atlas (binding 19) and `vgrid` for volume functions. (Bit 24 is +/// F_AERIAL's.) pub const F_GRID: u32 = 1 << 25; -pub const F_SOURCE: u32 = F_TINT | F_UNLIT | F_BACKGROUND | F_VCOLOR | F_GRID | F_UNITS; +/// The features that change the kernel's source (an `@if` flag) rather than switch code in it (`FEAT`). +pub const F_SOURCE: u32 = F_UNITS; /// Software traversal's units (units.rs; shaders/trace_units.wgsl): the second level's walk, spliced into the /// software traversal after the still scene's (F_SOURCE). Never with hardware traversal. pub const F_UNITS: u32 = 1 << 26; @@ -3326,7 +3305,7 @@ mod tests { for hwrt in [false, true] { println!("source kernel hwrt={hwrt} {}", sha(&kernel_wgsl(&template, &KernelVariant::new(hwrt, KernelSwitches::default(), 0)))); for &k in &switches { - for f in [0, F_VCOLOR, F_GRID, F_TINT, F_UNLIT, F_UNITS, F_BACKGROUND, F_SOURCE] { + for f in [0, F_UNITS] { naga_valid(&kernel_wgsl(&template, &KernelVariant::new(hwrt, k, if hwrt { f & !F_UNITS } else { f }))); } let all_features = KernelVariant::new(hwrt, k, if hwrt { F_SOURCE & !F_UNITS } else { F_SOURCE }); @@ -3517,23 +3496,25 @@ mod tests { assert_eq!(words[..2], [11, 22]); } - /// Each feature that changes the kernel's declarations (`KernelVariant::flags`) changes what it says, and nothing of it - /// is in a kernel without it. + /// A scene's features other than units are `FEAT` bits, not source: the kernel's text is the same for each, and + /// what they switch is fixed when a pipeline sets `FEAT` (naga's override processing; `OBJECT_STRIDE` from F_TINT). #[test] - fn the_source_features_change_what_they_say() { + fn scene_features_are_feat_bits() { let t = kernel_template(); let v = |f: u32| kernel_wgsl(&t, &KernelVariant::new(false, KernelSwitches::default(), f)); let base = v(0); - let tint = v(F_TINT); - assert!(tint.contains("const OBJECT_STRIDE: u32 = 4u;") && !tint.contains("const OBJECT_STRIDE: u32 = 3u;")); - assert_eq!(tint.matches("*= obj_tint(").count(), 3); - assert!(base.contains("const OBJECT_STRIDE: u32 = 3u;") && !base.contains("obj_tint")); - assert!(v(F_UNLIT).contains("MAT_UNLIT") && !base.contains("MAT_UNLIT")); - assert!(v(F_BACKGROUND).contains("P.background.rgb") && !base.contains("background")); - let vc = v(F_VCOLOR); - assert!(vc.contains("fn vcolor(") && vc.matches("*= vcolor(").count() == 2 && !base.contains("vcolor")); - let gr = v(F_GRID); - assert!(gr.contains("var grid_tex: texture_3d") && gr.contains("fn vgrid(") && gr.contains("i.grid = ") && !base.contains("grid_tex") && !base.contains("vgrid")); + for f in [F_TINT, F_UNLIT, F_BACKGROUND, F_VCOLOR, F_GRID] { + assert_eq!(v(f), base, "{f:#x}"); + } + assert_ne!(v(F_UNITS), base); + let m = naga::front::wgsl::parse_str(&base).unwrap(); + let info = naga::valid::Validator::new(naga::valid::ValidationFlags::all(), naga::valid::Capabilities::all()).validate(&m).unwrap(); + for (feat, stride) in [(0, 3.0), (F_TINT, 4.0)] { + let c = naga::back::PipelineConstants::from([("LIVE".to_string(), 0.0), ("FEAT".to_string(), feat as f64)]); + let (m, _) = naga::back::pipeline_constants::process_overrides(&m, &info, None, &c).unwrap(); + let (_, k) = m.constants.iter().find(|(_, k)| k.name.as_deref() == Some("OBJECT_STRIDE")).expect("OBJECT_STRIDE"); + assert_eq!(m.global_expressions[k.init], naga::Expression::Literal(naga::Literal::U32(stride as u32))); + } } /// Structural features other than F_INST (opaque particles, media) do not make a scene count as instanced: a group diff --git a/renderer/src/scene.rs b/renderer/src/scene.rs index 892b645..11b0862 100644 --- a/renderer/src/scene.rs +++ b/renderer/src/scene.rs @@ -13,7 +13,7 @@ use std::sync::Arc; pub const MAT_DOUBLE_SIDED: i32 = 1; pub const MAT_EMITTER_CARD: i32 = 2; -/// three.js MeshBasicMaterial (`desc::Material::unlit`; the kernel's F_UNLIT, `@if(unlit)`: gpu.rs `KernelVariant::flags`). +/// three.js MeshBasicMaterial (`desc::Material::unlit`; the kernel's F_UNLIT). pub const MAT_UNLIT: i32 = 4; pub const TRI_NO_SHADOW: u32 = 1; /// Alpha-tested triangle (cutout material): traversal runs an any-hit test.