ifcviewer: same-frame HiZ occlusion cull on GPU (step 3d)

Two-phase compute-cull dispatch when IFC_GPU_CULL=1:

  Phase 1  frustum + contribution + LOD, no HiZ  → survivors
  Depth    render survivors depth-only into half-viewport FBO
  Build    GPU compute max-reduce depth → R32F mip pyramid
  Phase 2  same cull + HiZ test                  → final survivors
  Color    render final survivors

The compact shader's new hizOccluded() projects 8 AABB corners to
screen space, picks the mip level where the covered rect fits in ≤2×2
texels, and rejects when the AABB's near-depth exceeds the pyramid's
max depth.

New GPU resources (per-window):
  hiz_gpu_fbo_ / hiz_gpu_depth_tex_  — depth-only FBO at half viewport
  hiz_gpu_pyramid_tex_                — R32F mipmapped pyramid
  hiz_gpu_copy_prog_                  — compute: depth → pyramid L0
  hiz_gpu_reduce_prog_                — compute: max-reduce L(n-1)→L(n)
  hiz_gpu_depth_prog_                 — vertex + trivial fragment

On a dense 18-model BIM dataset:
  survivors:  140k → 65k  (HiZ rejects ~50%)
  triangles:  22M  → 13M
  gpu_cull:   0.06ms → 22.5ms  (depth pre-pass CP overhead)

The depth pre-pass suffers the same empty-sub-draws CP overhead as the
color pass (690k commands, most with instanceCount=0).  Once MDI
compaction lands, both passes will be fast.  For now, net FPS is flat
(savings on color ≈ cost of depth pre-pass).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-04-17 10:12:56 +10:00
parent e5ed7b53d4
commit a34c36d22e
3 changed files with 304 additions and 45 deletions
+21 -8
View File
@@ -258,6 +258,7 @@ private:
// Called after uploadInstanceAabbs at finalize / applyCachedModel once
// m.meshes[].instance_count has been populated.
void uploadGpuCullStaticBuffers(ModelGpuData& m);
void ensureHizGpuResources(int vp_w, int vp_h);
// Frustum-cull m's instances (BVH if available, else linear scan),
// build the per-mesh DrawElementsIndirectCommand array + flat visible
@@ -297,16 +298,28 @@ private:
GLuint pick_program_ = 0;
GLuint axis_program_ = 0;
// Phase 3E compute cull. When IFC_GPU_CULL=1, render() uses the GPU
// path exclusively: cull_reset_program_ zeros each mesh's instanceCount
// in gpu_indirect_buffer, then cull_compact_program_ runs frustum +
// contribution cull per instance and atomically appends survivors into
// gpu_visible_ssbo at mesh_base[mesh_id] + local_slot. No LOD / HiZ /
// reflection bucketing yet — reflected instances render with wrong
// winding under the gate, which is why this stays gated until the
// fwd/rev split lands (step 3b).
// Phase 3E compute cull. Two-phase dispatch when IFC_GPU_CULL=1:
// Phase 1: frustum + contribution + LOD, no HiZ → depth pre-pass
// Phase 2: same + HiZ test → final survivors for color pass
GLuint cull_reset_program_ = 0;
GLuint cull_compact_program_ = 0;
// Depth-only program for the HiZ depth pre-pass — same vertex shader
// as main_program_, trivial fragment shader.
GLuint hiz_gpu_depth_prog_ = 0;
// GPU HiZ pyramid: depth pre-pass renders into hiz_gpu_fbo_ at
// hiz_gpu_w_ × hiz_gpu_h_; copy+reduce compute shaders build a
// max-reduction mip chain in hiz_gpu_pyramid_tex_ (R32F).
GLuint hiz_gpu_fbo_ = 0;
GLuint hiz_gpu_depth_tex_ = 0;
GLuint hiz_gpu_pyramid_tex_ = 0;
GLuint hiz_gpu_copy_prog_ = 0;
GLuint hiz_gpu_reduce_prog_ = 0;
int hiz_gpu_w_ = 0;
int hiz_gpu_h_ = 0;
int hiz_gpu_levels_ = 0;
uint32_t gpu_cull_last_survivors_ = 0;
uint32_t gpu_cull_last_input_ = 0;
uint64_t gpu_cull_ns_ = 0; // per-window accumulator