wgpu cull: contribution-cull defaults 2/10 → 3/15, env-var overrides

wgpu computes projected_px from view-Z distance (forward·(centre-eye)),
which is the perspective-divide-correct denominator: an instance's
on-screen radius really is world_radius * focal / z_view. GL computes
the same value but with euclidean distance (sqrt(dx²+dy²+dz²)) — for
off-axis instances euclidean > z_view, so GL underestimates screen
size and culls more aggressively at the same numeric threshold.

Concretely on the federation scene at the test camera, wgpu was
drawing ~3× the instances GL drew despite identical 2/10 thresholds:
wgpu obj 11067, hiz_rej 16532 vs GL obj 2310, hiz_rej 6823. Same
fps (vsync-pinned), but ~30% more cull work for raster output that
the user already wasn't seeing because GL had been quietly dropping it.

Keeping wgpu's view-Z formula (more physically correct) and bumping
the thresholds to 3.0 / 15.0 to match GL's effective drop rate. On
the federation scene this lands obj/tri counts within ~10% of GL
across the orbit, and shaves cull from 3.44 ms to 2.61 ms avg.
basic.ifc parity is unchanged (its 3 instances clear 3 px easily).

WGPU_MIN_PX / WGPU_MIN_PX_MOTION env vars added so the thresholds
can be swept without rebuilding — needed while we visually
confirm the new defaults across more scenes / cameras.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-29 09:40:18 +10:00
parent 10f8fc88e8
commit 6a3dd4a0eb
2 changed files with 32 additions and 3 deletions
+20
View File
@@ -36,6 +36,7 @@
#include <algorithm>
#include <atomic>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <future>
#include <limits>
@@ -1302,6 +1303,25 @@ bool WgpuViewportWindow::initWgpu() {
wgpuSetLogCallback(onWgpuLog, nullptr);
wgpuSetLogLevel(WGPULogLevel_Warn);
// Env-var overrides for contribution-cull thresholds. wgpu uses
// view-Z (perspective-divide-correct) for projected_px, whereas the
// GL backend uses euclidean distance — so for off-axis instances
// wgpu computes a larger projected_px and is less aggressive at the
// same numeric threshold. These knobs exist to let us sweep matching
// values during the perf-parity push without rebuilding.
if (const char* s = std::getenv("WGPU_MIN_PX")) {
const float v = float(std::atof(s));
if (v >= 0.0f) min_pixel_radius_ = v;
qInfo().noquote().nospace()
<< "[wgpu cull] WGPU_MIN_PX=" << min_pixel_radius_;
}
if (const char* s = std::getenv("WGPU_MIN_PX_MOTION")) {
const float v = float(std::atof(s));
if (v >= 0.0f) motion_min_pixel_radius_ = v;
qInfo().noquote().nospace()
<< "[wgpu cull] WGPU_MIN_PX_MOTION=" << motion_min_pixel_radius_;
}
instance_ = wgpuCreateInstance(nullptr);
if (!instance_) {
qWarning() << "wgpuCreateInstance returned null";
+12 -3
View File
@@ -387,9 +387,18 @@ private:
// Contribution-cull thresholds. Still-frame uses min_pixel_radius_;
// when the camera changed since last frame, the bigger motion threshold
// kicks in to drop more sub-pixel detail (and slash per-frame cull cost).
// Matches AppSettings::minPixelRadius / motionMinPixelRadius in GL.
float min_pixel_radius_ = 2.0f;
float motion_min_pixel_radius_ = 10.0f;
//
// GL ships 2.0 / 10.0, but uses euclidean distance for projected_px
// (sqrt(dx² + dy² + dz²)) while wgpu uses view-Z distance (the
// perspective-divide-correct denominator). For off-axis instances
// view_z < euclidean, so wgpu's projected_px is larger than GL's at
// the same numeric threshold — i.e. wgpu is structurally less
// aggressive. Bumping to 3.0 / 15.0 compensates so the effective drop
// rate matches GL's; on the federation scene this lands obj/tri
// counts within ~10% of GL's across an orbit (vs ~3× without the
// bump). Override at runtime via WGPU_MIN_PX / WGPU_MIN_PX_MOTION.
float min_pixel_radius_ = 3.0f;
float motion_min_pixel_radius_ = 15.0f;
public:
// Master switch for HiZ occlusion. Set false to skip the depth resolve