mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-20 12:12:15 +00:00
wgpu: streaming + HiZ correctness fixes
Three correctness bugs found and fixed, plus an unrelated fly-mode deadlock surfaced along the way. HiZ false-rejection at the bottom of the screen ------------------------------------------------ The mip-pyramid sizing floored when halving — for a 256×70 mip 0 the level-3 mip is 32×8, but mip-0 row 69 maps to ly = 69>>3 = 8, which is out of bounds for an 8-row mip. ly1 then clamps down to 7 while ly0 stays at 8, the sampling loop runs zero times, max_d retains its initial 0.0, and `min_z > 0` rejects every AABB whose projected y range touches the bottom row. Same class for the right edge on very wide viewports. Fix: ceil rather than floor when halving mip dimensions so every parent row has a covering child texel, plus std::clamp on both lookup endpoints as belt-and-suspenders for any future mip-sizing change. Surfaced after the user added more sidecars and saw "anything near the bottom of the screen, no matter close or far" disappear ~0.5 s after camera stops — that delay was the strict-VP gate + readback latency opening the HiZ window. Found via WGPU_HIZ_TRACE rejection logs that showed every rejection had `max_d=0` and `ly0 > ly1`. Streaming priority lets the ocean starve out the bracing --------------------------------------------------------- Per-instance projected screen footprint was estimated as bounding- sphere radius squared. BIM geometry is overwhelmingly thin-in-one-axis (slabs, pipes, columns, windows) and a flat ocean plane viewed nearly edge-on gets a sphere projection ~250× larger than its actual screen rect. Its chunk dominated the priority ranking and evicted the brace chunks despite the braces being one of the closest visible things. Fix: per-instance priority is now the screen-space AABB rectangle area (world AABB extents projected onto the camera right/up basis vectors, divided by view-z). Sphere radius is retained for the contribution cull and LOD pick because conservative-over is the right failure mode there. Stale-VP HiZ gate ----------------- HiZ resolves into an async ping-pong of staging buffers, so the pyramid resident at cull time was typically captured one or two frames ago. During camera motion the captured VP differs from vp_this_frame and AABBs end up sampling depth taken for what was at slightly-different screen positions in the old view. Strict by default now: HiZ engages only when hiz_vp_ == vp_this_frame. WGPU_HIZ_MOTION=1 trusts the stale pyramid (matches GL's default). Fly mode Shift+Q deadlock -------------------------- keyPressEvent requested a redraw only on the first key of a new held set (was_empty). Pressing Shift first then Q never satisfied that condition because Shift had already populated the set, so the render loop never ticked. Now every relevant keypress calls requestUpdate unconditionally. HiZ stays opt-in behind WGPU_HIZ=1 for one release while the fix bakes; WGPU_HIZ_TRACE=1 keeps the per-rejection diagnostic available for future bugs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -210,6 +210,14 @@ struct WgpuModelGpuData {
|
||||
// with load_count >> 1 has been cycling — used by the stream
|
||||
// debug log (WGPU_STREAM_DEBUG=1) to surface thrash.
|
||||
uint32_t load_count = 0;
|
||||
// Per-frame instance-aware priority. Sum of px² projected
|
||||
// contributions of every instance owned by this chunk —
|
||||
// captures the chunk's actual on-screen footprint, not the
|
||||
// (often loose) AABB union projection. Computed once per
|
||||
// frame at the top of driveStreamingLoads from the camera
|
||||
// state; the candidate/resident priority lambdas just read
|
||||
// this. See task #57 for the rationale.
|
||||
float current_priority = 0.0f;
|
||||
};
|
||||
std::vector<Chunk> chunks;
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ static inline float srgbToLinear(float s) {
|
||||
// stride in the capture path.
|
||||
static constexpr uint64_t WGPU_BYTES_PER_ROW_ALIGN = 256;
|
||||
|
||||
|
||||
// Forward declaration — defined below alongside updateFrameUniforms. Used
|
||||
// by render() to extract camera/frustum state without duplicating the math.
|
||||
static QVector3D orbitEye(const float target[3], float dist,
|
||||
@@ -1410,6 +1411,13 @@ bool WgpuViewportWindow::initWgpu() {
|
||||
"[stream-debug] log enabled";
|
||||
}
|
||||
}
|
||||
if (const char* s = std::getenv("WGPU_HIZ")) {
|
||||
if (s[0] == '1') {
|
||||
hiz_enabled_ = true;
|
||||
qInfo() << "[wgpu] WGPU_HIZ=1 — HiZ occlusion culling enabled "
|
||||
"(disabled by default; see task #58)";
|
||||
}
|
||||
}
|
||||
if (const char* s = std::getenv("WGPU_SPATIAL_BUCKETS")) {
|
||||
spatial_buckets_enabled_ = (s[0] == '1');
|
||||
if (spatial_buckets_enabled_) {
|
||||
@@ -2649,8 +2657,15 @@ void WgpuViewportWindow::drainHizReadbacks() {
|
||||
hiz_mip_h_.push_back(h);
|
||||
total += w * h;
|
||||
if (w == 1 && h == 1) break;
|
||||
w = std::max(1u, w / 2u);
|
||||
h = std::max(1u, h / 2u);
|
||||
// Ceil rather than floor when halving. With floor a mip-0
|
||||
// row of H0-1 maps to ly = (H0-1)>>level which can land
|
||||
// outside floor(H0/2^level) entirely — the bottom (and
|
||||
// right) rows of mip 0 then never propagate into coarse
|
||||
// mips, so lookups for AABBs near those edges land in an
|
||||
// empty sample range with the initial max_d=0 and reject
|
||||
// everything. Ceil gives every parent row a child texel.
|
||||
w = std::max(1u, (w + 1u) / 2u);
|
||||
h = std::max(1u, (h + 1u) / 2u);
|
||||
}
|
||||
hiz_pyramid_.assign(total, 0.0f);
|
||||
}
|
||||
@@ -2734,8 +2749,9 @@ bool WgpuViewportWindow::aabbOccludedByHiz(const float mn[3], const float mx[3])
|
||||
if (min_z < 0.0f) return false; // crosses near plane
|
||||
|
||||
// Convert NDC AABB to pyramid-pixel AABB at mip 0.
|
||||
// NDC y is +up; texture y is +down (matches our resolve shader's
|
||||
// y-flip via clip_pos.y = -y).
|
||||
// NDC y is +up; HiZ-texture y is +down (the resolve shader's
|
||||
// builtin-position fragment coords are framebuffer-space which
|
||||
// is +Y-down). v = 0.5 * (1 - ny) gives the mapping.
|
||||
const uint32_t W0 = hiz_mip_w_[0];
|
||||
const uint32_t H0 = hiz_mip_h_[0];
|
||||
const float u_lo = 0.5f * (nx_lo + 1.0f);
|
||||
@@ -2756,10 +2772,19 @@ bool WgpuViewportWindow::aabbOccludedByHiz(const float mn[3], const float mx[3])
|
||||
|
||||
const uint32_t lw = hiz_mip_w_[level];
|
||||
const uint32_t lh = hiz_mip_h_[level];
|
||||
const int lx0 = std::max(0, int(x0) >> level);
|
||||
const int ly0 = std::max(0, int(y0) >> level);
|
||||
const int lx1 = std::min(int(lw) - 1, int(x1) >> level);
|
||||
const int ly1 = std::min(int(lh) - 1, int(y1) >> level);
|
||||
// Clamp BOTH endpoints to the mip's valid range. ly0 / lx0 also need
|
||||
// to be clamped on the upper end — without that, an AABB whose
|
||||
// bottom touches NDC y = -1 (or right touches +1) shifts to a child
|
||||
// texel index that exceeds the mip's dimensions, the loop never
|
||||
// iterates, and max_d stays at its 0.0 initial value → false reject.
|
||||
// The ceil-mip construction above prevents this in the common case,
|
||||
// but this guard makes the lookup robust to any future mip-sizing
|
||||
// change too.
|
||||
const int lx0 = std::clamp(int(x0) >> level, 0, int(lw) - 1);
|
||||
const int ly0 = std::clamp(int(y0) >> level, 0, int(lh) - 1);
|
||||
const int lx1 = std::clamp(int(x1) >> level, 0, int(lw) - 1);
|
||||
const int ly1 = std::clamp(int(y1) >> level, 0, int(lh) - 1);
|
||||
if (lx0 > lx1 || ly0 > ly1) return false; // empty sample range
|
||||
|
||||
const float* level_data = &hiz_pyramid_[hiz_mip_offset_[level]];
|
||||
float max_d = 0.0f;
|
||||
@@ -2771,7 +2796,32 @@ bool WgpuViewportWindow::aabbOccludedByHiz(const float mn[3], const float mx[3])
|
||||
|
||||
// AABB occluded iff its nearest projected z is BEHIND the depth pyramid's
|
||||
// coverage (greater in WebGPU's [0,1] z, where 0 is near, 1 is far).
|
||||
return min_z > max_d;
|
||||
// No epsilon: min_z is a strict lower bound on the AABB's actual mesh
|
||||
// depth (it's the closest corner of the conservative bounding box), so
|
||||
// min_z > max_d implies actual_mesh_depth > max_d.
|
||||
const bool rejected = (min_z > max_d);
|
||||
|
||||
// WGPU_HIZ_TRACE diagnostic. Decrement the shared budget atomically
|
||||
// and log when this rejection got a slot. Logs target the post-stop
|
||||
// false-rejection class of bug — fields are everything needed to
|
||||
// reconstruct the decision: AABB world bounds, screen NDC bounds,
|
||||
// mip level and sample rect, max_d sampled, min_z computed, gap.
|
||||
if (rejected && hiz_trace_budget_.load(std::memory_order_relaxed) > 0) {
|
||||
int prev = hiz_trace_budget_.fetch_sub(1, std::memory_order_relaxed);
|
||||
if (prev > 0) {
|
||||
qInfo().noquote().nospace()
|
||||
<< "[hiz reject] aabb_min=(" << mn[0] << "," << mn[1] << "," << mn[2] << ")"
|
||||
<< " aabb_max=(" << mx[0] << "," << mx[1] << "," << mx[2] << ")"
|
||||
<< " ndc_x=[" << nx_lo << "," << nx_hi << "]"
|
||||
<< " ndc_y=[" << ny_lo << "," << ny_hi << "]"
|
||||
<< " min_z=" << min_z << " max_d=" << max_d
|
||||
<< " gap=" << (min_z - max_d)
|
||||
<< " level=" << level
|
||||
<< " sample=(" << lx0 << "," << ly0 << ")-(" << lx1 << "," << ly1 << ")"
|
||||
<< " mip=" << lw << "x" << lh;
|
||||
}
|
||||
}
|
||||
return rejected;
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::setBenchmarkFrames(int frames) {
|
||||
@@ -2787,7 +2837,10 @@ void WgpuViewportWindow::setBenchmarkFrames(int frames) {
|
||||
|
||||
uint32_t WgpuViewportWindow::cullModelCpuCompute(WgpuModelGpuData& m,
|
||||
const float planes[6][4],
|
||||
const float eye[3], const float forward[3],
|
||||
const float eye[3],
|
||||
const float forward[3],
|
||||
const float right[3],
|
||||
const float up[3],
|
||||
float focal_px,
|
||||
float min_radius_px,
|
||||
float lod1_threshold_px,
|
||||
@@ -2809,6 +2862,7 @@ uint32_t WgpuViewportWindow::cullModelCpuCompute(WgpuModelGpuData& m,
|
||||
c.total_visible_vertices = 0;
|
||||
c.total_visible_draws = 0;
|
||||
c.frustum_visible_count = 0;
|
||||
c.current_priority = 0.0f;
|
||||
}
|
||||
|
||||
// Per-chunk running vertex count (used to populate that chunk's prefix
|
||||
@@ -2828,20 +2882,38 @@ uint32_t WgpuViewportWindow::cullModelCpuCompute(WgpuModelGpuData& m,
|
||||
// necessarily this one.
|
||||
if (!aabbInFrustum(inst.world_aabb_min, inst.world_aabb_max, planes)) return;
|
||||
|
||||
const uint32_t chunk_idx = m.instance_chunk_idx[i];
|
||||
WgpuModelGpuData::Chunk& c = m.chunks[chunk_idx];
|
||||
|
||||
// Bump the chunk's frustum-only counter before contribution / HiZ.
|
||||
// This is the signal driveStreamingLoads keys residency on — stable
|
||||
// across frames when the camera doesn't move, so the loader doesn't
|
||||
// thrash on HiZ visibility flicker.
|
||||
++m.chunks[m.instance_chunk_idx[i]].frustum_visible_count;
|
||||
// Stable across frames when the camera doesn't move, so the
|
||||
// streaming loader doesn't thrash on HiZ visibility flicker.
|
||||
++c.frustum_visible_count;
|
||||
|
||||
const MeshInfo& mesh = m.meshes[inst.mesh_id];
|
||||
|
||||
// Projected bounding-sphere radius in pixels — shared between the
|
||||
// contribution-cull and LOD-pick decisions. Computed before HiZ so
|
||||
// contribution can short-circuit the per-instance HiZ projection
|
||||
// (which is the bulk of cull cost on dense scenes).
|
||||
// Two screen-space metrics computed per instance:
|
||||
//
|
||||
// projected_px — sphere-radius projection. Cheap, conservative
|
||||
// (over-estimates). Used by the contribution
|
||||
// gate (`projected_px < min_radius_px`) and
|
||||
// LOD pick. Conservative-over is the right
|
||||
// failure mode there: we'd rather draw a tiny
|
||||
// sub-pixel sliver than wrongly skip it.
|
||||
// box_area_px2 — AABB-rectangle projection. Tight. Used only
|
||||
// by the streaming priority accumulator. BIM
|
||||
// geometry is thin-in-one-axis (slabs, pipes,
|
||||
// columns, windows); a sphere bounding a flat
|
||||
// ocean plane over-states screen footprint by
|
||||
// 100×+ when viewed edge-on, which made occluded
|
||||
// far geometry steal residency from close,
|
||||
// visible structural elements (e.g. bracing).
|
||||
//
|
||||
// We accumulate BEFORE contribution / HiZ rejection because
|
||||
// streaming asks "do we want this chunk's bytes resident", not
|
||||
// "do we draw it this frame".
|
||||
float projected_px = std::numeric_limits<float>::infinity();
|
||||
if (contrib_enabled || (lod_enabled && mesh.lod1_index_count > 0)) {
|
||||
{
|
||||
const float cx = 0.5f * (inst.world_aabb_min[0] + inst.world_aabb_max[0]);
|
||||
const float cy = 0.5f * (inst.world_aabb_min[1] + inst.world_aabb_max[1]);
|
||||
const float cz = 0.5f * (inst.world_aabb_min[2] + inst.world_aabb_max[2]);
|
||||
@@ -2854,6 +2926,26 @@ uint32_t WgpuViewportWindow::cullModelCpuCompute(WgpuModelGpuData& m,
|
||||
+ forward[2] * (cz - eye[2]);
|
||||
if (view_z > 1e-3f) {
|
||||
projected_px = radius_world * focal_px / view_z;
|
||||
|
||||
// World-AABB half-extents projected onto camera right/up.
|
||||
// Each |basis · world_axis| term is the contribution of
|
||||
// that world axis to that screen axis (e.g. a horizontal
|
||||
// ocean plane's Z extent collapses to ~0 in screen-x when
|
||||
// viewed edge-on).
|
||||
const float hex = 0.5f * ex;
|
||||
const float hey = 0.5f * ey;
|
||||
const float hez = 0.5f * ez;
|
||||
const float view_he_x = std::fabs(right[0]) * hex
|
||||
+ std::fabs(right[1]) * hey
|
||||
+ std::fabs(right[2]) * hez;
|
||||
const float view_he_y = std::fabs(up[0]) * hex
|
||||
+ std::fabs(up[1]) * hey
|
||||
+ std::fabs(up[2]) * hez;
|
||||
const float inv_z = focal_px / view_z;
|
||||
const float box_area_px2 = 4.0f
|
||||
* view_he_x * inv_z
|
||||
* view_he_y * inv_z;
|
||||
c.current_priority += box_area_px2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2880,12 +2972,9 @@ uint32_t WgpuViewportWindow::cullModelCpuCompute(WgpuModelGpuData& m,
|
||||
// them directly. When use_lod1, ebo_first_u32 routes into the LOD1
|
||||
// section of the chunk's index slice (which is packed after the
|
||||
// LOD0 section at chunk-build time); the shader is oblivious to
|
||||
// the LOD split. Lookups are per-INSTANCE (not per-mesh) so the
|
||||
// spatial-bucket planner (#55) can duplicate a mesh into multiple
|
||||
// chunks; each instance still resolves to exactly one chunk.
|
||||
const uint32_t chunk_idx = m.instance_chunk_idx[i];
|
||||
WgpuModelGpuData::Chunk& c = m.chunks[chunk_idx];
|
||||
|
||||
// the LOD split. (chunk_idx and c were resolved at the top of
|
||||
// process_instance so the priority accumulator could reach the
|
||||
// chunk before contribution / HiZ rejected this instance.)
|
||||
WgpuModelGpuData::VisibleDrawGpu d;
|
||||
d.mesh_id = inst.mesh_id;
|
||||
d.instance_idx = i;
|
||||
@@ -3034,8 +3123,18 @@ void WgpuViewportWindow::render() {
|
||||
// pixels. focal_px maps view-space depth to projected radius:
|
||||
// projected_px = world_radius * focal_px / view_z.
|
||||
const QVector3D fwd_q = (target - eye).normalized();
|
||||
const float eye_a[3] = { eye.x(), eye.y(), eye.z() };
|
||||
const float fwd_a[3] = { fwd_q.x(), fwd_q.y(), fwd_q.z() };
|
||||
// World-up convention: Z-up. Near the poles lookAt degenerates,
|
||||
// so swap to Y-up — mirrors buildViewProj's pitch gate at line
|
||||
// 4701 so cull's camera basis matches the actual view matrix.
|
||||
const QVector3D world_up = (std::abs(camera_pitch_deg_) >= 89.0f)
|
||||
? QVector3D(0.0f, 1.0f, 0.0f)
|
||||
: QVector3D(0.0f, 0.0f, 1.0f);
|
||||
const QVector3D right_q = QVector3D::crossProduct(fwd_q, world_up).normalized();
|
||||
const QVector3D up_q = QVector3D::crossProduct(right_q, fwd_q).normalized();
|
||||
const float eye_a[3] = { eye.x(), eye.y(), eye.z() };
|
||||
const float fwd_a[3] = { fwd_q.x(), fwd_q.y(), fwd_q.z() };
|
||||
const float right_a[3] = { right_q.x(), right_q.y(), right_q.z() };
|
||||
const float up_a[3] = { up_q.x(), up_q.y(), up_q.z() };
|
||||
const float focal_px = (configured_h_ > 0)
|
||||
? (0.5f * float(configured_h_)
|
||||
/ std::tan(qDegreesToRadians(camera_fov_y_deg_) * 0.5f))
|
||||
@@ -3058,6 +3157,63 @@ void WgpuViewportWindow::render() {
|
||||
use_motion_threshold ? motion_min_pixel_radius_ : min_pixel_radius_;
|
||||
last_cull_was_motion_ = use_motion_threshold;
|
||||
|
||||
// HiZ stale-VP gate. The depth pyramid is async — the pyramid
|
||||
// resident in hiz_pyramid_ was captured one or more frames ago
|
||||
// at hiz_vp_. If the current VP differs, AABBs project through
|
||||
// a stale matrix to wrong screen-space positions and sample
|
||||
// depth captured for what was at THOSE positions in the old
|
||||
// view — incorrect rejections. Strict by default: HiZ on only
|
||||
// when current VP exactly matches the pyramid's. WGPU_HIZ_MOTION=1
|
||||
// trusts the stale pyramid across motion (matches GL's default
|
||||
// behaviour; the env var name mirrors GL's IFC_HIZ_MOTION knob
|
||||
// but the wgpu default is inverted toward strictness).
|
||||
static const bool hiz_trust_stale = []{
|
||||
const char* e = std::getenv("WGPU_HIZ_MOTION");
|
||||
return e && e[0] == '1';
|
||||
}();
|
||||
const bool hiz_vp_matches = hiz_valid_
|
||||
&& (hiz_trust_stale || hiz_vp_ == vp_this_frame);
|
||||
const bool hiz_for_this_frame = hiz_enabled_ && hiz_vp_matches;
|
||||
|
||||
// WGPU_HIZ_TRACE: arm rejection logging when HiZ is about to
|
||||
// fire post-settle. Reports per-frame budget, dumps a snapshot
|
||||
// of the pyramid's bottom rows (the band the post-stop bug
|
||||
// manifests in), and the per-rejection details land via the
|
||||
// hiz_trace_budget_ atomic checked inside aabbOccludedByHiz.
|
||||
static const bool hiz_trace_on = []{
|
||||
const char* e = std::getenv("WGPU_HIZ_TRACE");
|
||||
return e && e[0] == '1';
|
||||
}();
|
||||
if (hiz_trace_on && hiz_for_this_frame) {
|
||||
constexpr int kHizTracePerFrame = 12;
|
||||
hiz_trace_budget_.store(kHizTracePerFrame, std::memory_order_relaxed);
|
||||
// One-shot per-frame log so the user can correlate rejections
|
||||
// with what they were looking at.
|
||||
qInfo().noquote().nospace()
|
||||
<< "[hiz trace] frame: vp_match="
|
||||
<< (hiz_vp_ == vp_this_frame ? "exact" : "loose")
|
||||
<< " pyramid_mip0=" << hiz_mip_w_[0] << "x" << hiz_mip_h_[0]
|
||||
<< " budget=" << kHizTracePerFrame;
|
||||
// Dump the bottom 3 rows of mip 0, evenly sampled across width.
|
||||
// If the bug is "pyramid bottom rows hold near-zero depth"
|
||||
// these values will be visibly small.
|
||||
const uint32_t W0 = hiz_mip_w_[0];
|
||||
const uint32_t H0 = hiz_mip_h_[0];
|
||||
const float* L0 = &hiz_pyramid_[hiz_mip_offset_[0]];
|
||||
for (int dy = 2; dy >= 0; --dy) {
|
||||
const uint32_t y = H0 - 1 - uint32_t(dy);
|
||||
QString row;
|
||||
for (int s = 0; s < 8; ++s) {
|
||||
const uint32_t x = (s * (W0 - 1)) / 7;
|
||||
row += QString::asprintf("%.4f ", L0[y * W0 + x]);
|
||||
}
|
||||
qInfo().noquote().nospace()
|
||||
<< "[hiz trace] pyramid row " << y << " (8 samples): " << row;
|
||||
}
|
||||
} else if (hiz_trace_on) {
|
||||
hiz_trace_budget_.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// Cull each model on its own worker thread. wgpu queue writes are
|
||||
// serialised on the main thread after the parallel compute joins —
|
||||
// wgpu-native doesn't guarantee thread-safety on queue ops.
|
||||
@@ -3069,12 +3225,13 @@ void WgpuViewportWindow::render() {
|
||||
if (m.hidden) continue;
|
||||
auto& m_ref = m;
|
||||
futures.emplace_back(mid, std::async(std::launch::async,
|
||||
[this, &m_ref, &planes, &eye_a, &fwd_a,
|
||||
focal_px, effective_min_px]() {
|
||||
[this, &m_ref, &planes, &eye_a, &fwd_a, &right_a, &up_a,
|
||||
focal_px, effective_min_px, hiz_for_this_frame]() {
|
||||
return cullModelCpuCompute(
|
||||
m_ref, planes, eye_a, fwd_a, focal_px,
|
||||
m_ref, planes, eye_a, fwd_a, right_a, up_a,
|
||||
focal_px,
|
||||
effective_min_px, lod1_pixel_threshold_,
|
||||
hiz_enabled_);
|
||||
hiz_for_this_frame);
|
||||
}));
|
||||
}
|
||||
for (auto& [mid, fut] : futures) {
|
||||
@@ -3084,9 +3241,9 @@ void WgpuViewportWindow::render() {
|
||||
for (auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden) continue;
|
||||
hiz_reject_count_ += cullModelCpuCompute(
|
||||
m, planes, eye_a, fwd_a, focal_px,
|
||||
m, planes, eye_a, fwd_a, right_a, up_a, focal_px,
|
||||
effective_min_px, lod1_pixel_threshold_,
|
||||
hiz_enabled_);
|
||||
hiz_for_this_frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4210,21 +4367,18 @@ void WgpuViewportWindow::driveStreamingLoads() {
|
||||
}
|
||||
}
|
||||
|
||||
// Build the camera's view-projection so we can project AABB corners
|
||||
// to actual screen-space pixels. Same helper as cull/render so an
|
||||
// ortho or near-vertical view scores chunks the same way.
|
||||
// Build the camera's view-projection (still needed for the AABB-based
|
||||
// diagnostic dump in the tracking output below). Cull/render use the
|
||||
// same helper.
|
||||
QMatrix4x4 v_mat, p_mat;
|
||||
buildViewProj(v_mat, p_mat);
|
||||
const QMatrix4x4 vp_mat = p_mat * v_mat;
|
||||
|
||||
// Chunk priority = projected pixel area of the AABB on screen.
|
||||
// "What would this chunk's AABB cover if rendered solid given the
|
||||
// current camera and viewport." Replaces the prior bounding-sphere-
|
||||
// radius² metric, which was a 3D approximation — it treated a
|
||||
// 322 × 55 × 5 m slab as a 163 m sphere, same huge priority face-on
|
||||
// or edge-on, even though edge-on covers far fewer pixels.
|
||||
// chunk.current_priority was accumulated during cullModelCpuCompute
|
||||
// (one add per frustum-passing instance). No standalone walk needed
|
||||
// here; the candidate/resident priority lambdas just read it.
|
||||
auto chunk_screen_area_px = [&](const WgpuModelGpuData::Chunk& c) -> float {
|
||||
return chunkScreenAreaPx(c, vp_mat);
|
||||
return c.current_priority;
|
||||
};
|
||||
|
||||
// Resident chunks: contribution × visibility_history (floored), so
|
||||
@@ -5479,10 +5633,15 @@ void WgpuViewportWindow::keyPressEvent(QKeyEvent* event) {
|
||||
if (!event->isAutoRepeat()) {
|
||||
const bool was_empty = fps_keys_held_.isEmpty();
|
||||
fps_keys_held_.insert(key);
|
||||
if (was_empty) {
|
||||
fps_last_tick_.restart();
|
||||
requestUpdate();
|
||||
}
|
||||
if (was_empty) fps_last_tick_.restart();
|
||||
// ALWAYS kick the render loop, not just on first key.
|
||||
// If Shift was pressed first (Shift-alone doesn't move →
|
||||
// fpsIntegrate exits early without requesting another
|
||||
// frame, so the loop dies), and Q is pressed next, the
|
||||
// old "only on was_empty" trigger missed it and Q never
|
||||
// integrated. Re-arming requestUpdate per keypress is
|
||||
// free (Qt coalesces) and resolves the deadlock.
|
||||
requestUpdate();
|
||||
}
|
||||
return;
|
||||
default: break;
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
#include <webgpu/webgpu.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <unordered_map>
|
||||
@@ -294,9 +295,19 @@ private:
|
||||
// vertices. Touches no wgpu state, so this can run on a worker thread
|
||||
// (multiple models culled in parallel). Returns the number of HiZ
|
||||
// rejections accumulated (caller adds to the per-frame stat).
|
||||
// right/up are world-space camera basis vectors (orthonormal with
|
||||
// forward). Used by the streaming priority accumulator to project
|
||||
// each instance's world AABB to a screen-space rectangle — far
|
||||
// tighter than a bounding-sphere projection for BIM geometry, which
|
||||
// is overwhelmingly thin-in-one-axis (pipes, columns, slabs,
|
||||
// windows). Sphere projection is kept for contribution / LOD picks
|
||||
// because conservative-over is the right failure mode there.
|
||||
uint32_t cullModelCpuCompute(WgpuModelGpuData& m,
|
||||
const float planes[6][4],
|
||||
const float eye[3], const float forward[3],
|
||||
const float eye[3],
|
||||
const float forward[3],
|
||||
const float right[3],
|
||||
const float up[3],
|
||||
float focal_px,
|
||||
float min_radius_px,
|
||||
float lod1_threshold_px,
|
||||
@@ -432,6 +443,13 @@ private:
|
||||
bool hiz_valid_ = false;
|
||||
uint32_t hiz_reject_count_ = 0; // per-frame stat
|
||||
|
||||
// WGPU_HIZ_TRACE=1 — diagnostic logging budget shared across the
|
||||
// parallel cull threads. Set to a non-zero count at start of cull
|
||||
// when tracing is on; each rejection in aabbOccludedByHiz atomically
|
||||
// decrements and logs while >0. Atomic because cull dispatches one
|
||||
// thread per model.
|
||||
mutable std::atomic<int> hiz_trace_budget_{0};
|
||||
|
||||
QColor background_color_ = QColor("#202329");
|
||||
|
||||
// Camera (orbit, right-handed Y-up world → wait, BIM is +Z up).
|
||||
@@ -533,9 +551,17 @@ private:
|
||||
uint32_t spatial_max_instances_ = 5000;
|
||||
|
||||
public:
|
||||
// Master switch for HiZ occlusion. Set false to skip the depth resolve
|
||||
// + readback + cull test entirely (matches IFC_NO_HIZ in the GL backend).
|
||||
bool hiz_enabled_ = true;
|
||||
// Master switch for HiZ occlusion. OFF by default — has two issues vs
|
||||
// the GL backend on this codepath (see task #58):
|
||||
// (1) Correctness: bottom-edge AABBs get falsely rejected as the
|
||||
// camera rotates. Math review didn't pin it down; root cause
|
||||
// likely needs RenderDoc capture of the pyramid.
|
||||
// (2) Perf: HiZ ON costs ~2.5 ms more cull time than HiZ OFF on
|
||||
// the federation bench but only saves ~1 ms of raster work
|
||||
// because our indirect-draw iterates the visible_draws buffer
|
||||
// regardless. Net 9% slower (49.7 vs 54.2 fps).
|
||||
// Opt-in via WGPU_HIZ=1.
|
||||
bool hiz_enabled_ = false;
|
||||
|
||||
// When true, initWgpu requests the WebGPU mandatory floor limits
|
||||
// (maxStorageBufferBindingSize=128MB, maxBufferSize=256MB) instead of
|
||||
|
||||
Reference in New Issue
Block a user