Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-map3d/.nornir/wasm-webgpu-srgb-parity.md

WASM/WebGPU vs native parity — the brown cast + the clipping report

Rickard's bug: the OSM-3D GPU render (Map3D with with_gpu(true), the facett-map3d HDR pipeline in src/gpu.rs) looks perfect on desktop native (wgpu / nvidia) but on the browser WASM/WebGPU build it is (1) a brownish / muddy colour cast and (2) shows clipping / z-fighting on the ground terrain and on buildings. This note records the root-cause work, the landed fix, and what still needs a live-browser eyeball.

(1) BROWN — ROOT-CAUSED and FIXED

Root cause: a missing final sRGB encode on the non-sRGB WebGPU surface.

The HDR post stack is map3d.wgsl/water.wgsl/sky.wgsl (write linear radiance to the Rgba16Float offscreen) → bloom.wgsl composite (exposure + ACES tonemap + a pow(1/1.15) lift → an LDR target) → fxaa.wgsl present (the final blit into egui's pass). The composite deliberately writes display-referred colour and relies on the surface carrying the sRGB gamma encode (see the bloom.wgsl composite comment "on top of the LDR target's own sRGB encode").

Both the composite→LDR write and the FXAA present→egui write use render_state.target_format (gpu.rs::PostStack::new, and Map3dRenderer::new(..., render_state.target_format) at the bottom of gpu.rs):

The other BROWN hypothesis — a sky-LUT / ground-albedo texture failing to load on wasm and falling back to a brown default — was ruled out: the sky transmittance LUT is CPU-computed and uploaded (SkyTransmittanceLut::from_rgb), the ground/building albedo is procedural in map3d.wgsl, and there are no runtime image/texture fetches in the GPU path (std::fs in terrain.rs is native-only DEM height loading with a synthetic fallback on wasm — not colour).

The fix (landed, green)

fxaa.wgsl — the final present pass — now sRGB-encodes its output when the target is a non-sRGB surface, gated on a uniform flag (U.texel.z):

Native is kept byte-for-byte identical. The value is set to the format predicate only on wasm (cfg!(target_arch = "wasm32")); on native it is forced 0.0. Rationale: the real native window is always *-srgb (already correct), and the ONLY native non-sRGB consumer is the offscreen egui_kittest test harness (Rgba8Unorm), whose luma goldens are calibrated to the raw un-encoded path. Un-gating the encode on native flips those goldens (verified: e.g. flightsim_shadows's ground-pixel classifier drops from >5000 to ~920 px because every colour shifts). Scoping the workaround to wasm gives the browser parity while the native app AND every headless proof stay stable and green.

Note / future cleanup: because the harness renders to a non-sRGB offscreen, it currently does NOT exercise the encode. If the harness is ever switched to an *-srgb offscreen (a more faithful proxy of the real desktop surface), un-gate the encode (drop the cfg!(wasm), use present_encode_srgb(target_format) directly) and recalibrate the luma-threshold goldens to the encoded output.

(2) CLIPPING / z-fighting — investigated, NOT a depth-format divergence

Checked every suspect the report named; the depth pipeline is portable and identical native↔WebGPU, so it is not the root cause:

Most likely explanation (needs a browser eyeball to confirm): the perceived "clipping / z-fighting" is largely a secondary effect of the (now-fixed) sRGB bug. The missing encode crushed the tonal range and fed FXAA the wrong (un-encoded) luma, so coplanar seams (building base ↔ ground, terrain LOD joins) and aliased silhouettes flickered far more visibly than on native, reading as "clipping". The sRGB fix restores FXAA's luma and the tonal range and should markedly reduce it. Any residual true z-fighting is a depth-precision tuning question (large far/near ratio on Depth32Float) that is identical on native and web and would be tuned globally, not a browser-only fix.

Build / test status

Needs a live-browser eyeball (follow-up)