GPU scattering-LUT precompute pass — design
The chosen design for FLIGHT-SIM #7 (roadmap §2-GPU): move the atmospheric transmittance LUT generation off the CPU and onto the GPU as a compute pass, keep the CPU model (atmos.rs) as the reference oracle + fallback, and produce a sampled GPU texture as the seam a future sky.wgsl reads. Behind the wgpu feature; the landed 21-pass facett-map3d pipeline is untouched (L2 additive, no regression).
The pieces (src/scatter_gpu.rs + src/scatter_lut.wgsl)
scatter_lut.wgsl·cs_transmittance— a@workgroup_size(64)compute shader, one invocation per(altitude, μ)texel. It ray-marches the curved planet's Rayleigh + Mie + ozone extinction to the top of the atmosphere and storesexp(-optical_depth)as RGB — a line-for-line twin ofatmos::optical_depth_to_top(same km constants, same march order, same ground/top ray-shell roots).GpuScatterLut::new(device)— builds the compute pipeline (uniformLutParamsbinding 0 + a read-write storage buffer binding 1); validates the WGSL on build.GpuScatterLut::precompute(device, queue, h, mu, steps)— dispatchesceil(h·mu / 64)workgroups, reads the storage buffer back (headless blockingdevice.poll), and uploads the result into a sampledRgba32Floattexture (muwide ×htall). Returns [GpuLut]{ texels, texture, view }.GpuLut::parity_vs(cpu)— the texel-for-texel comparison against the CPUTransmittanceLut, split above/below the local horizon (see the knife-edge note).
Why the CPU model stays (L2 / L5a)
The compute shader is a twin, not a replacement. atmos::TransmittanceLut remains (a) the fallback for hosts with no adapter (wasm / a GPU-less laptop / a deck pane) and (b) the oracle the GPU output is validated against. The LUT type + the SunSky seam are reused from the existing modules, never forked — this pass only adds a GPU producer behind them.
The horizon-tangent knife-edge (the one real subtlety)
The near ray/ground intersection t = -rμ − √(r²(μ²−1)+R_g²) suffers catastrophic cancellation at the horizon tangent (where the two terms are nearly equal and the root collapses toward 0). There, CPU-f32 and GPU-f32 round the sqrt to opposite sides of the ground-hit branch, so a single texel per altitude row flips between "hits ground (short path, T≈1)" and "misses (long grazing path, T≈0)". This is a property of the model at the tangent, and it lands entirely in the μ < 0 (below-local-horizon) band — the occluded region a sun-transmittance lookup never samples (a sun below the horizon is blocked). So parity is judged on the sampled region: max_err_upper over μ ≥ 0 must be < 5e-4 (observed ~2e-5), and the > 1e-2 outliers must be ≤ h+2 and all below the horizon — proving the divergence is the degenerate tangent line, not a shader bug.
Alternatives rejected
- A storage texture the shader writes directly —
rgba32floatstorage-write needs adapter-specific format features; a storage buffer + awrite_textureupload is portable to software adapters (the laptop tester), so that is chosen. - Wiring the LUT texture into
sky.wgslnow — that touches the landed 21-pass pipeline (regression surface). Deferred: this pass produces the texture + proves it; the sky-pass sampling swap is the next, separately-tested step. - A tolerance that ignores the knife-edge globally — too weak (a broad shader bug could hide). The above/below-horizon split keeps the check RED-when-broken.