From dd5644d26faf777bef161dc61515fe15e56555b5 Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Wed, 15 Jul 2026 14:14:54 +0200 Subject: [PATCH] fix: PT-9b - honest variance for unseedable disocclusions; 7x7 borrow The PT-9 seeding handled ROTATION, where a few pixels per frame stream in at the screen edge next to converged history. Under TRANSLATION - the actual walking-indoors case - a whole COLUMN of texels streams in per frame, the 5x5 borrow window finds nothing but fellow newborns, and the unseeded pixels fell back to the old degenerate state: a raw 1-spp sample with variance EXACTLY zero (m2 - m1 squared of a single value). Zero variance tells the wavelet filter the pixel is CONVERGED, so the raw outliers survived all four a-trous iterations as a visible noise band along the leading screen edge. Two changes, both in the temporal kernel, realtime mode only: - the borrow window grows to 7x7 (newborn pixels only, so the cost is motion-proportional and small); - a newborn with NOTHING to borrow now writes a frank variance (max(l squared, 0.25)) instead of the degenerate zero - a 1-sample estimate has unknown variance, not none - so the a-trous blurs those pixels hard until real statistics exist one frame later. Verified: both golden PT tests pass; the translating-dolly capture (sideways translation, worst-case parallax) shows the noise band gone at unchanged fps; progressive mode untouched. Claude-Session: https://claude.ai/code/session_018574BdCfSjdpNK3WLgx1hP --- native/shared/src/renderer/shaders/pt.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/native/shared/src/renderer/shaders/pt.rs b/native/shared/src/renderer/shaders/pt.rs index c3290a3..36035e6 100644 --- a/native/shared/src/renderer/shaders/pt.rs +++ b/native/shared/src/renderer/shaders/pt.rs @@ -1401,6 +1401,7 @@ fn cs_main(@builtin(global_invocation_id) gid: vec3) { } var n_hist = 0.0; + var seeded = false; if (wsum > 1e-3) { hist_rgb /= wsum; hist_m1 /= wsum; @@ -1430,8 +1431,11 @@ fn cs_main(@builtin(global_invocation_id) gid: vec3) { var seed_m2 = 0.0; var seed_n = 0.0; var seed_w = 0.0; - for (var by = -2; by <= 2; by = by + 1) { - for (var bx = -2; bx <= 2; bx = bx + 1) { + // 7x7: under TRANSLATION a whole COLUMN of texels streams in per + // frame (rotation only trickles a few px), so a 5x5 often found + // nothing but fellow newborns and the band stayed raw. + for (var by = -3; by <= 3; by = by + 1) { + for (var bx = -3; bx <= 3; bx = bx + 1) { if (bx == 0 && by == 0) { continue; } let q = vec2(i32(gid.x) + bx, i32(gid.y) + by); if (q.x < 0 || q.y < 0 || q.x >= i32(u.size.x) || q.y >= i32(u.size.y)) { @@ -1454,6 +1458,7 @@ fn cs_main(@builtin(global_invocation_id) gid: vec3) { hist_m1 = seed_m1 / seed_w; hist_m2 = seed_m2 / seed_w; n_hist = min((seed_n / seed_w) * 0.5, 8.0); + seeded = true; } } // Canonical blend: cumulative average while the history is @@ -1472,7 +1477,18 @@ fn cs_main(@builtin(global_invocation_id) gid: vec3) { // wavelet filter's luminance sigma. Young history makes this // unreliable; the first à-trous iteration substitutes a // spatial estimate when n < 4 (accum.w carries n via moments). - let variance = max(m2 - m1 * m1, 0.0); + var variance = max(m2 - m1 * m1, 0.0); + // A newborn with NOTHING to borrow is a 1-sample estimate whose true + // variance is unknown — not zero, which is what m2 - m1² of a single + // value degenerates to. Zero variance tells the wavelet the pixel is + // CONVERGED, so the raw outlier survived every iteration: that was + // the residual noise band on wide stream-ins under camera + // translation. Write a frank variance instead so the à-trous blurs + // these pixels hard; one converged frame later the real statistics + // take over. + if (n_new <= 1.5 && !seeded) { + variance = max(l_new * l_new, 0.25); + } accum_out[idx] = vec4(out_irr, variance); moments_out[idx] = vec4(m1, m2, n_new, depth); if (debug == 22.0) {