Skip to content

Commit 232dece

Browse files
committed
feat(render): add opt-in visibility runtime diagnostic
1 parent cdc50c6 commit 232dece

8 files changed

Lines changed: 1259 additions & 8 deletions

File tree

docs/perf/008-visibility-buffer.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,24 @@ The shared CPU/WGSL ABI and perspective reconstruction live in
3535
`shaders/visibility_buffer/geometry.wgsl`. Hardware readback oracles now prove
3636
ID/winding rasterization, perspective reconstruction, non-zero first-index and
3737
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.
38+
reconstructed vertex lanes. The opt-in runtime diagnostic now executes the
39+
depth-equal ID raster and full-screen attribute reconstruction against the
40+
real GPU-driven draw/index/vertex buffers. Production composition follows
41+
only after the completed PBR A/B passes the no-regression gate.
42+
43+
Runtime qualification is explicitly requested before engine attachment:
44+
45+
- `BLOOM_VISIBILITY_BUFFER=validate` runs the private visibility and
46+
reconstructed-normal passes while the forward image remains unchanged;
47+
- `BLOOM_VISIBILITY_BUFFER=debug` additionally overlays reconstructed normals
48+
on admitted pixels, leaving compatibility-rendered content intact so holes
49+
and routing mistakes are visible;
50+
- unset/off requests no `primitive-index` device feature, creates no pipeline,
51+
texture, or bind group, and records no visibility work.
52+
53+
Both modes expose `visibility_buffer_runtime` in the public renderer capability
54+
report, including admitted/compatibility draw counts, current extent, exact
55+
owned bytes, activation reason, and whether the current frame recorded work.
4056

4157
## Original problem statement (historical)
4258

native/shared/src/renderer/frame_resource_stats.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ pub(super) enum BindGroupCreationSite {
2020
AutoExposure,
2121
FinalComposite,
2222
CustomPostPass,
23+
VisibilityBuffer,
2324
}
2425

2526
impl BindGroupCreationSite {
26-
const COUNT: usize = 12;
27+
const COUNT: usize = 13;
2728
#[cfg(not(target_arch = "wasm32"))]
2829
const ALL: [Self; Self::COUNT] = [
2930
Self::SceneCompose,
@@ -38,6 +39,7 @@ impl BindGroupCreationSite {
3839
Self::AutoExposure,
3940
Self::FinalComposite,
4041
Self::CustomPostPass,
42+
Self::VisibilityBuffer,
4143
];
4244

4345
#[cfg(not(target_arch = "wasm32"))]
@@ -55,6 +57,7 @@ impl BindGroupCreationSite {
5557
Self::AutoExposure => "auto_exposure",
5658
Self::FinalComposite => "final_composite",
5759
Self::CustomPostPass => "custom_post_pass",
60+
Self::VisibilityBuffer => "visibility_buffer",
5861
}
5962
}
6063
}

native/shared/src/renderer/gpu_driven.rs

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ use super::{
1414
use std::sync::OnceLock;
1515

1616
pub const GPU_DRIVEN_FEATURES: wgpu::Features = wgpu::Features::INDIRECT_FIRST_INSTANCE;
17+
pub(crate) const DRAW_FLAG_DOUBLE_SIDED: u32 = 1 << 0;
18+
pub(crate) const DRAW_FLAG_VISIBILITY_ELIGIBLE: u32 = 1 << 1;
19+
20+
pub(crate) const fn draw_flags(double_sided: bool, visibility_eligible: bool) -> u32 {
21+
(if double_sided {
22+
DRAW_FLAG_DOUBLE_SIDED
23+
} else {
24+
0
25+
}) | if visibility_eligible {
26+
DRAW_FLAG_VISIBILITY_ELIGIBLE
27+
} else {
28+
0
29+
}
30+
}
1731
/// Below this count, compute/indirect setup costs more than the CPU loop on
1832
/// current Metal hardware. Keep small scenes on the lower-overhead oracle.
1933
pub const GPU_DRIVEN_MIN_DRAWS: usize = 32;
@@ -29,6 +43,7 @@ pub fn request_features_if_supported(supported: wgpu::Features, required: &mut w
2943
if supported.contains(wgpu::Features::MULTI_DRAW_INDIRECT_COUNT) {
3044
*required |= wgpu::Features::MULTI_DRAW_INDIRECT_COUNT;
3145
}
46+
super::visibility_buffer::request_feature_if_supported(supported, required);
3247
}
3348

3449
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
@@ -71,6 +86,7 @@ struct GeometryArena {
7186
free_indices: Vec<FreeRange>,
7287
retired: Vec<RetiredGeometry>,
7388
completion: GpuCompletionTracker,
89+
generation: u64,
7490
}
7591

7692
impl GeometryArena {
@@ -88,6 +104,7 @@ impl GeometryArena {
88104
free_indices: Vec::new(),
89105
retired: Vec::new(),
90106
completion: GpuCompletionTracker::default(),
107+
generation: 0,
91108
}
92109
}
93110

@@ -212,6 +229,7 @@ impl GeometryArena {
212229
self.index = next;
213230
self.index_capacity = new_index_capacity;
214231
}
232+
self.generation = self.generation.wrapping_add(1);
215233
queue.submit(std::iter::once(encoder.finish()));
216234
}
217235
}
@@ -347,6 +365,7 @@ pub struct GpuDrivenRenderer {
347365
depth_pipeline: Option<wgpu::RenderPipeline>,
348366
main_pipeline: Option<wgpu::RenderPipeline>,
349367
main_prepassed_pipeline: Option<wgpu::RenderPipeline>,
368+
visibility: super::visibility_buffer::VisibilityBufferRuntime,
350369
pub(super) draw_scratch: Vec<GpuDrawRecord>,
351370
pub stats: SubmissionStats,
352371
}
@@ -464,6 +483,18 @@ impl GpuDrivenRenderer {
464483
(None, None, None, None)
465484
};
466485

486+
let visibility_source = (super::visibility_buffer::requested_mode().requested() && enabled)
487+
.then(|| make_gpu_scene_shader(scene_source));
488+
let visibility = super::visibility_buffer::VisibilityBufferRuntime::new(
489+
device,
490+
enabled,
491+
&draw_layout,
492+
lighting_layout,
493+
global_material_layout,
494+
joint_layout,
495+
visibility_source.as_deref(),
496+
);
497+
467498
let renderer = Self {
468499
arena: GeometryArena::new(device),
469500
enabled,
@@ -481,6 +512,7 @@ impl GpuDrivenRenderer {
481512
depth_pipeline,
482513
main_pipeline,
483514
main_prepassed_pipeline,
515+
visibility,
484516
draw_scratch: Vec::with_capacity(draw_capacity),
485517
stats: SubmissionStats::default(),
486518
};
@@ -592,6 +624,7 @@ impl GpuDrivenRenderer {
592624
frustum_visible_oracle: u32,
593625
frustum_culled_oracle: u32,
594626
) {
627+
self.visibility.begin_frame();
595628
self.stats = SubmissionStats {
596629
submitted: self.draw_scratch.len() as u32,
597630
compatibility: compatibility_count,
@@ -647,7 +680,8 @@ impl GpuDrivenRenderer {
647680
"\"frustum_visible_oracle\":{},\"frustum_culled_oracle\":{},",
648681
"\"frustum_culled_ratio\":{:.6},",
649682
"\"classification_source\":\"retained-scene conservative CPU oracle\",",
650-
"\"visibility_buffer_contract\":{}}}"
683+
"\"visibility_buffer_contract\":{},",
684+
"\"visibility_buffer_runtime\":{}}}"
651685
),
652686
self.enabled,
653687
self.count_supported,
@@ -658,6 +692,7 @@ impl GpuDrivenRenderer {
658692
self.stats.frustum_culled_oracle,
659693
culled_ratio,
660694
super::visibility_buffer::contract_json(),
695+
self.visibility.report_json(),
661696
)
662697
}
663698

@@ -701,6 +736,100 @@ impl GpuDrivenRenderer {
701736
self.bind_and_draw(pass, lighting, global_materials, joints);
702737
}
703738

739+
pub(crate) fn visibility_diagnostic_enabled(&self) -> bool {
740+
self.visibility.enabled()
741+
}
742+
743+
#[allow(clippy::too_many_arguments)]
744+
pub(crate) fn record_visibility_diagnostic(
745+
&mut self,
746+
device: &wgpu::Device,
747+
encoder: &mut wgpu::CommandEncoder,
748+
profiler: &mut crate::profiler::Profiler,
749+
depth_view: &wgpu::TextureView,
750+
lighting: &wgpu::BindGroup,
751+
global_materials: &wgpu::BindGroup,
752+
joints: &wgpu::BindGroup,
753+
extent: (u32, u32),
754+
) -> super::visibility_buffer::ResourceCreations {
755+
if !self.visibility.enabled() {
756+
return super::visibility_buffer::ResourceCreations::default();
757+
}
758+
if self.draw_scratch.is_empty() {
759+
self.visibility.set_draw_counts(0, self.stats.compatibility);
760+
return super::visibility_buffer::ResourceCreations::default();
761+
}
762+
let eligible = self
763+
.draw_scratch
764+
.iter()
765+
.filter(|draw| {
766+
bitcast_draw_flags(draw.bounds_min[3]) & DRAW_FLAG_VISIBILITY_ELIGIBLE != 0
767+
})
768+
.count() as u32;
769+
let compatibility = self
770+
.stats
771+
.compatibility
772+
.saturating_add(self.draw_scratch.len() as u32 - eligible);
773+
self.visibility.set_draw_counts(eligible, compatibility);
774+
if eligible == 0 {
775+
return super::visibility_buffer::ResourceCreations::default();
776+
}
777+
let creations = self.visibility.ensure_resources(
778+
device,
779+
extent,
780+
&self.arena.vertex,
781+
&self.arena.index,
782+
&self.draw_buffer,
783+
self.draw_capacity,
784+
self.arena.generation,
785+
);
786+
profiler.begin("visibility_raster_pass");
787+
{
788+
let timestamps = profiler.pass_timestamp_writes("visibility_raster_pass");
789+
self.visibility.record_raster(
790+
encoder,
791+
depth_view,
792+
&self.draw_bind_group,
793+
lighting,
794+
global_materials,
795+
joints,
796+
&self.arena.vertex,
797+
&self.arena.index,
798+
&self.indirect_buffer,
799+
&self.counter_buffer,
800+
self.draw_scratch.len() as u32,
801+
self.count_supported,
802+
timestamps,
803+
);
804+
}
805+
profiler.end("visibility_raster_pass");
806+
profiler.begin("visibility_reconstruct_pass");
807+
{
808+
let timestamps = profiler.compute_pass_timestamp_writes("visibility_reconstruct_pass");
809+
self.visibility.record_reconstruct(encoder, timestamps);
810+
}
811+
profiler.end("visibility_reconstruct_pass");
812+
creations
813+
}
814+
815+
pub(crate) fn record_visibility_debug_overlay(
816+
&self,
817+
encoder: &mut wgpu::CommandEncoder,
818+
profiler: &mut crate::profiler::Profiler,
819+
hdr_view: &wgpu::TextureView,
820+
) {
821+
if !self.visibility.debug_overlay_enabled() {
822+
return;
823+
}
824+
profiler.begin("visibility_debug_overlay");
825+
{
826+
let timestamps = profiler.pass_timestamp_writes("visibility_debug_overlay");
827+
self.visibility
828+
.record_debug_overlay(encoder, hdr_view, timestamps);
829+
}
830+
profiler.end("visibility_debug_overlay");
831+
}
832+
704833
fn bind_and_draw<'a>(
705834
&'a self,
706835
pass: &mut wgpu::RenderPass<'a>,
@@ -751,6 +880,10 @@ impl GpuDrivenRenderer {
751880
}
752881
}
753882

883+
fn bitcast_draw_flags(value: f32) -> u32 {
884+
value.to_bits()
885+
}
886+
754887
impl Renderer {
755888
/// Prepare retained scene resources through both the compatibility and
756889
/// GPU-driven paths. Keeping this borrow split inside Renderer lets the
@@ -1102,7 +1235,7 @@ fn create_main_pipeline(
11021235
})
11031236
}
11041237

1105-
fn make_gpu_scene_shader(source: &str) -> String {
1238+
pub(super) fn make_gpu_scene_shader(source: &str) -> String {
11061239
const LEGACY_MATERIALS: &str = r#"@group(2) @binding(0) var base_color_tex: texture_2d<f32>;
11071240
@group(2) @binding(1) var base_color_samp: sampler;
11081241
@group(2) @binding(2) var normal_tex: texture_2d<f32>;
@@ -1332,6 +1465,12 @@ mod tests {
13321465
std::mem::size_of::<wgpu::util::DrawIndexedIndirectArgs>(),
13331466
20
13341467
);
1468+
assert_eq!(draw_flags(false, false), 0);
1469+
assert_eq!(draw_flags(true, false), DRAW_FLAG_DOUBLE_SIDED);
1470+
assert_eq!(
1471+
draw_flags(true, true),
1472+
DRAW_FLAG_DOUBLE_SIDED | DRAW_FLAG_VISIBILITY_ELIGIBLE
1473+
);
13351474
}
13361475

13371476
#[test]

native/shared/src/renderer/scene_pass.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,37 @@ impl Renderer {
149149
}
150150
profiler.end("depth_prepass");
151151

152+
// #27 qualification path. This is inert unless explicitly requested
153+
// with BLOOM_VISIBILITY_BUFFER and the negotiated device retained the
154+
// primitive-index feature. It reads the prepass depth but never writes
155+
// it, and forward remains the authoritative scene renderer.
156+
if self.gpu_driven.visibility_diagnostic_enabled()
157+
&& !self.dbg_skip("prepass")
158+
&& !self.dbg_skip("prepass_draws")
159+
{
160+
if let Some(global_materials) =
161+
self.material_system.indirection.global_bind_group.as_ref()
162+
{
163+
let creations = self.gpu_driven.record_visibility_diagnostic(
164+
&self.device,
165+
encoder,
166+
profiler,
167+
&self.depth_view,
168+
&self.lighting_bind_group,
169+
global_materials,
170+
&self.joint_bind_group,
171+
self.render_extent(),
172+
);
173+
self.frame_resource_stats
174+
.created_physical_textures(creations.textures);
175+
for _ in 0..creations.bind_groups {
176+
self.frame_resource_stats.created_bind_group(
177+
frame_resource_stats::BindGroupCreationSite::VisibilityBuffer,
178+
);
179+
}
180+
}
181+
}
182+
152183
profiler.begin("main_hdr_pass");
153184
// SH-055 diag — "hdr_pass" skips the whole main HDR pass (prepass untouched).
154185
if !self.dbg_skip("hdr_pass") {
@@ -532,6 +563,8 @@ impl Renderer {
532563
// the later SSR pass; base-only frames return before allocating or
533564
// recording anything.
534565
self.record_layered_iridescence_ssr_metadata(encoder, profiler, scene);
566+
self.gpu_driven
567+
.record_visibility_debug_overlay(encoder, profiler, &self.hdr_rt_view);
535568
}
536569
}
537570

@@ -735,14 +768,18 @@ impl Renderer {
735768
};
736769
let uniforms = bytemuck::pod_read_unaligned::<Uniforms3D>(bytes);
737770
let (wmin, wmax) = transform_aabb(&cmd.model, mesh.local_min, mesh.local_max);
771+
let mut draw_flags = gpu_driven::DRAW_FLAG_DOUBLE_SIDED;
772+
if uniforms.misc[2].abs() <= f32::EPSILON {
773+
draw_flags |= gpu_driven::DRAW_FLAG_VISIBILITY_ELIGIBLE;
774+
}
738775
self.gpu_driven
739776
.draw_scratch
740777
.push(gpu_driven::GpuDrawRecord {
741778
uniforms,
742779
// Cached-model prepass semantics are two-sided (foliage
743780
// and cutout cards rely on it). Bit 0 rides in the unused
744781
// bounds lane and is consumed only by the depth shader.
745-
bounds_min: [wmin[0], wmin[1], wmin[2], f32::from_bits(1)],
782+
bounds_min: [wmin[0], wmin[1], wmin[2], f32::from_bits(draw_flags)],
746783
bounds_max: [wmax[0], wmax[1], wmax[2], 0.0],
747784
draw: [
748785
mesh.index_count,

0 commit comments

Comments
 (0)