Skip to content

Commit cdc50c6

Browse files
committed
test(render): prove visibility geometry reconstruction
1 parent 2dffe29 commit cdc50c6

3 files changed

Lines changed: 452 additions & 61 deletions

File tree

docs/perf/008-visibility-buffer.md

Lines changed: 40 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ The implementation contract is now:
1717
- reconstruct perspective-correct barycentrics from the referenced
1818
triangle's clip positions rather than storing them or expanding shared
1919
vertices;
20+
- require the optional WebGPU `primitive-index` capability and preserve the
21+
existing 96-byte `Vertex3D` storage layout as six packed `vec4<u32>` lanes;
2022
- admit only static opaque/masked shared-arena geometry with Tier-A global
2123
materials; blend, transmission, layered/custom, skinned, deforming, and
2224
unsupported content stays on the forward compatibility path;
@@ -28,12 +30,15 @@ The implementation contract is now:
2830
reconstructed output, and transient allocation.
2931

3032
The shared CPU/WGSL ABI and perspective reconstruction live in
31-
`renderer/visibility_buffer.rs` and
32-
`shaders/visibility_buffer/reconstruct.wgsl`. The first runtime milestone is
33-
a diagnostic raster/readback oracle; production composition follows only
34-
after that oracle catches ID, winding, clipping, and interpolation faults.
33+
`renderer/visibility_buffer.rs`,
34+
`shaders/visibility_buffer/reconstruct.wgsl`, and
35+
`shaders/visibility_buffer/geometry.wgsl`. Hardware readback oracles now prove
36+
ID/winding rasterization, perspective reconstruction, non-zero first-index and
37+
base-vertex addressing, the exact packed `Vertex3D` byte layout, and all 24
38+
reconstructed vertex lanes. The next milestone is an opt-in runtime A/B pass;
39+
production composition follows only after it passes the no-regression gate.
3540

36-
## Problem
41+
## Original problem statement (historical)
3742

3843
The `main_hdr_pass` writes four MRTs at the full physical resolution:
3944

@@ -44,9 +49,10 @@ The `main_hdr_pass` writes four MRTs at the full physical resolution:
4449
| `velocity_rt` | Rg16Float | 4 |
4550
| `albedo_rt` | Rgba8Unorm | 4 |
4651

47-
Total: **18 bytes per pixel written** by every fragment. At 1600×900 that's
48-
26 MB per pass per frame, with overdraw multiplying the real write count.
49-
Bandwidth-bound on integrated GPUs.
52+
Total: **18 bytes per winning pixel written** by the current main pass. At
53+
1600×900 that is 26 MB per full-surface pass before attachment compression or
54+
backend effects. The old claim that overdraw multiplies all four writes is no
55+
longer valid because the alpha-aware depth prepass rejects hidden fragments.
5056

5157
UE5's Nanite uses a **visibility buffer** instead: store only `(triangle_id,
5258
barycentrics)` (~8 bytes) in the G-buffer, defer material evaluation to the
@@ -66,20 +72,22 @@ This is a significant refactor — do ticket 005 (depth prepass) first. Then:
6672
3. **MRTs that post-FX consumes** (normal, albedo, material, velocity) can
6773
either be rebuilt per-pixel in the shading pass OR kept as separate passes.
6874
Simplest path: the shading pass writes them alongside the final HDR
69-
colour — still one write per pixel, vs 4 writes per overdrawn pixel today.
75+
colour. That preserves the winning-pixel attachment footprint, so any gain
76+
must come from the cheaper visibility raster or improved scheduling and
77+
must be demonstrated by the total-pass A/B.
7078

71-
## Simpler intermediate step
79+
## Simpler intermediate step (landed)
7280

73-
If full visibility buffer is too much, consider **dropping unused MRTs when
74-
features are off**:
81+
Bloom already **drops unused MRTs when features are off** on the constrained
82+
`lean_mrt` route:
7583

7684
- `velocity_rt` is only needed when TAA or motion-blur is on.
7785
- `albedo_rt` is only needed when SSGI or SSR is on.
7886
- `material_rt` is needed for SSR and the shadow map sampler stuff.
7987

80-
Rebuild the `scene_pipeline` with 2 or 3 MRT targets when the user has
81-
disabled the dependent post-FX. Cut 30-50% of the MRT bandwidth in low-quality
82-
modes. This is a ~2-day win instead of 2 weeks.
88+
The constrained scene pipeline uses fewer MRT targets when dependent post-FX
89+
is disabled, reducing its attachment bandwidth without changing the full
90+
quality path.
8391

8492
## References
8593

@@ -102,9 +110,11 @@ modes. This is a ~2-day win instead of 2 weeks.
102110

103111
## Notes for the implementer
104112

105-
- wgpu needs a storage buffer of per-mesh vertex data (triangle index buffer
106-
+ vertex attribute buffer, GPU-indexed by mesh_id). That aligns with
107-
ticket 009 (GPU-driven rendering).
113+
- Reuse #28's STORAGE-capable shared vertex/index arenas and `GpuDrawRecord`.
114+
`draw.y` is the first index, `bitcast<i32>(draw.z)` is the base vertex, and
115+
`draw.w` is the material ID. Do not declare `Vertex3D` with native WGSL
116+
`vec3` fields: storage alignment would not match Rust's tightly packed
117+
offsets. Use the checked six-`vec4<u32>` decoder.
108118
- Animated meshes (skinned) need special handling — triangle positions change
109119
per frame. Either compute-skin to a fixed buffer first, or keep animated
110120
meshes on the traditional path and use visibility buffer for static only.
@@ -116,40 +126,15 @@ modes. This is a ~2-day win instead of 2 weeks.
116126
pass, SSR/SSGI/SSAO inputs.
117127
- `native/shared/src/scene.rs` — mesh_id assignment, vertex buffer layout.
118128

119-
## Deferred — reopen criteria
120-
121-
Real GPU bandwidth win (~14 MB/frame at 1600×900 × overdraw factor, on
122-
a benchmark that currently writes 26 MB/pass) but **invisible behind
123-
the vsync cap on Sponza**. The main perf target landed at 60 fps vsync
124-
on full quality, so any further pass-cost reduction just gives headroom
125-
we can't measure here.
126-
127-
Reopen when one of these triggers:
128-
129-
- **A target scene pushes past the 16.7 ms vsync ceiling on the
130-
benchmark machine.** The 50%+ fragment-bandwidth reduction from a
131-
visibility buffer is the remaining GPU-side lever for Sponza-class
132-
bandwidth-bound scenes.
133-
- **Integrated / mobile GPUs become a priority.** Bandwidth matters
134-
disproportionately more on tile-based and integrated hardware; this
135-
ticket is the single biggest available reduction.
136-
- **Overdraw-heavy scenes** (foliage, hair, transparent-dense
137-
particles) become the target. The "every visible pixel shades
138-
exactly once" property of a visibility buffer + depth-prepass combo
139-
is essentially the only way to keep overdraw from eating bandwidth.
140-
141-
Effort when reopening is a 2+ week redesign: main_hdr_pass output
142-
becomes `Rgba32Uint (tri_id, u, v, mesh_id)` only, a new shading pass
143-
fetches vertex data from per-mesh storage buffers and evaluates PBR,
144-
downstream MRT consumers (SSR / SSGI / SSAO / post-FX) need to read
145-
from the rebuilt material channels rather than the current 4-MRT
146-
layout. Ticket 005's depth-prepass is a natural prerequisite (it was
147-
deprioritized but would become useful again here). Ticket 009's
148-
unified vertex/index buffers are a hard prerequisite (the shading
149-
pass needs a single bindless-style fetch across all meshes).
150-
151-
The "simpler intermediate step" in the approach section above — drop
152-
unused MRTs when features are off — is a legitimate ~2-day quick win
153-
for low-quality modes (`--quality 1` / `--quality 0` users on
154-
integrated hardware). That's the most-likely first concrete follow-up
155-
when this ticket reopens.
129+
## Activation state
130+
131+
The ticket is under active qualification, but the shipping path remains off.
132+
The old percentage and MB/frame estimates are not activation evidence because
133+
they predate the alpha-aware depth prepass and omit the visibility shading
134+
pass. Enablement requires uncapped captures on at least the representative
135+
discrete and integrated/mobile tiers, with per-pass GPU timestamps, total
136+
transient bytes, full compatibility composition, and governed image diffs.
137+
138+
The low-quality `lean_mrt` intermediate already drops unused material/albedo
139+
attachments on constrained profiles. That remains the safe bandwidth path
140+
for adapters which lack `primitive-index` or do not win the runtime A/B.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Bloom shared-geometry storage ABI for visibility shading — version 1.
2+
//
3+
// Vertex3D is a tightly packed 96-byte Rust/vertex-buffer record. WGSL
4+
// storage structs give vec3 values 16-byte alignment, so spelling the record
5+
// with native vec3 fields would silently read the wrong offsets. Six vec4<u32>
6+
// lanes preserve the exact 24-word byte layout and are decoded explicitly.
7+
8+
const BLOOM_VERTEX3D_WORDS: u32 = 24u;
9+
10+
struct BloomPackedVertex3D {
11+
words_0: vec4<u32>,
12+
words_1: vec4<u32>,
13+
words_2: vec4<u32>,
14+
words_3: vec4<u32>,
15+
words_4: vec4<u32>,
16+
words_5: vec4<u32>,
17+
};
18+
19+
struct BloomVertex3D {
20+
position: vec3<f32>,
21+
normal: vec3<f32>,
22+
color: vec4<f32>,
23+
uv: vec2<f32>,
24+
joints: vec4<f32>,
25+
weights: vec4<f32>,
26+
tangent: vec4<f32>,
27+
};
28+
29+
fn bloom_decode_vertex3d(packed: BloomPackedVertex3D) -> BloomVertex3D {
30+
return BloomVertex3D(
31+
bitcast<vec3<f32>>(packed.words_0.xyz),
32+
bitcast<vec3<f32>>(vec3<u32>(
33+
packed.words_0.w,
34+
packed.words_1.x,
35+
packed.words_1.y,
36+
)),
37+
bitcast<vec4<f32>>(vec4<u32>(packed.words_1.zw, packed.words_2.xy)),
38+
bitcast<vec2<f32>>(packed.words_2.zw),
39+
bitcast<vec4<f32>>(packed.words_3),
40+
bitcast<vec4<f32>>(packed.words_4),
41+
bitcast<vec4<f32>>(packed.words_5),
42+
);
43+
}
44+
45+
fn bloom_interpolate2(a: vec2<f32>, b: vec2<f32>, c: vec2<f32>, bary: vec3<f32>) -> vec2<f32> {
46+
return a * bary.x + b * bary.y + c * bary.z;
47+
}
48+
49+
fn bloom_interpolate3(a: vec3<f32>, b: vec3<f32>, c: vec3<f32>, bary: vec3<f32>) -> vec3<f32> {
50+
return a * bary.x + b * bary.y + c * bary.z;
51+
}
52+
53+
fn bloom_interpolate4(a: vec4<f32>, b: vec4<f32>, c: vec4<f32>, bary: vec3<f32>) -> vec4<f32> {
54+
return a * bary.x + b * bary.y + c * bary.z;
55+
}

0 commit comments

Comments
 (0)