fix: guard shader pow() bases so a NaN pixel can no longer black out the frame
This commit is contained in:
@@ -128,3 +128,10 @@ Run: `npm run build:preview` then serve `dist` (dev server shows a blank page).
|
||||
- Tests: 970 (82 files): in-frame overtakes for four pool/seed configurations, near-spawn fade-in, the forced swerve against instant 120 u/s, the detector against the sweep and two flashes, and the click predicate.
|
||||
- Budgets: scene chunk 228.15 KB gz9 (+6.06 vs 222.09, cap +8). Perf unchanged: rAF p50 16.7 / p95 16.7-16.8 ms on the six runs, 0 frames over 34 ms, no console errors.
|
||||
- Still weak: there is no visual hint that a click flashes; on lite the overtake comes every ~50 s; the flash is still clearly bright through bloom on vehicles within 100 m (within the 3 Hz, 600 ms interval rule); `traffic-overtake-indicator.webp` is a 120 s capture crop at Infrastructure (centre-lane car at about 200 m with the amber on), `traffic-flash-to-pass.webp` is a before / +1 / +2 frame strip of one flash.
|
||||
|
||||
## Fix: rare one-frame black flash (owner report)
|
||||
- **Root cause:** GLSL `pow(x, y)` is undefined for x < 0 and is NaN on real GPUs. The traffic trail (`pow(vUv.y, 1.6)`) and the shared rear/front pool shader (`pow(vUv.y, 2.2)`) took the interpolated uv straight in. When a quad is seen edge-on or clipped by the near plane (lower-right lanes beside the camera, e.g. a car braking and drifting back past the hero), a fragment can land a hair below 0. That one NaN pixel in the HDR target is spread by the UnrealBloom mips and blurs and by the lens streak (sampled across ±24% of the width), and ACES of NaN gives a black frame.
|
||||
- **Evidence (hardware GL, r6 flags, 96x60 readback each rAF):** unguarded builds gave 4 black frames in about 14.8k (full range) and 4 in 7.4k (confined to y 8800-10600, idle). With a shader-source hook that zeroes NaN/Inf/>60000 fragments there were 0 in about 45k frames. Colouring the offending fragments green placed them on the near lower-right road. Bisecting by material family put them only in the trail (1 event alone) and pool (1+1 alone) shaders. Lamps, bodies and the PBR car gave none. The link to deceleration is plausible (braking cars drift back toward the camera), but it is not proven.
|
||||
- **Fix:** every `pow()` base in the scene shaders is now non-negative on its face. Traffic uses `clamp(vUv.y, 0, 1)`. The 31 other sites in 15 world files get `abs()` for even exponents (where results are identical), and `max(…, 0)` or `clamp(…, 0, 1)` elsewhere (identical wherever the old result was defined). Examples are speed-wake `1 - vUv.y`, expressway `facing`, billboard/dawn/districts fresnel `1 - |dot|`, road film and cones, skyline, yard and terrain. No CPU-side change and no allocations.
|
||||
- **Test:** tests/unit/shader-pow-safety.test.ts scans src for `pow(` (comments stripped). It fails unless the base is a single abs/max/clamp/smoothstep call, optionally scaled by a positive literal. It includes the traffic regression.
|
||||
- **Checks:** 973 tests (83 files), vue-tsc clean, build:preview violations []. After the fix the unguarded probe gave 0 black frames in 21.6k frames (seeds 11 and 19 confined, seed 7 full range). Probe rAF p50 16.7 / p95 33.3 ms, the same as the pre-fix probe runs (the readback inflates p95).
|
||||
|
||||
Reference in New Issue
Block a user