facett-map3d/src/present.wgsl
// ── THE PRESENT BLIT — a plain resample of whatever the AA lane resolved ─────
//
// This file is what `fxaa.wgsl` used to be, and the difference IS the ruling: **there
// is exactly one framebuffer AA scheme in facett and it is TAA** (plus the analytic
// feather in `facett-map/src/gpu/line.wgsl` for 2D line coverage, which is a different
// job and stays). FXAA lived here because the offscreen is `sample_count = 1` and
// something had to take the jaggies off the tonemapped image. TAA now does that
// properly — jittered sample positions accumulated over frames, reprojected by real
// motion vectors — and running FXAA after it would blur the very edges TAA is trying
// to converge, which the history then accumulates. So the present pass does the ONE
// thing a present pass should: sample, encode, blit.
//
// It keeps the sRGB gate verbatim from the pass it replaces, because that is a
// property of the SURFACE, not of the antialiasing: the browser's WebGPU canvas is
// `Bgra8Unorm` (nothing encodes on write) while a native window is `Bgra8UnormSrgb`
// (the hardware encodes). `gpu.rs::present_encode_srgb` decides, `U.texel.z` carries
// it, and dropping it here would bring the dark/muddy-brown browser render back.
//
// Binding layout is the post stack's shared `{uniform, tex, sampler}` — unchanged, so
// the same `sample_bgl` and the same `u_composite` feed it.
struct PresentUniforms {
params: vec4<f32>, // unused here (shared post layout)
texel: vec4<f32>, // texel.xy = 1/width, 1/height; texel.z = sRGB-encode flag
};
@group(0) @binding(0) var<uniform> U: PresentUniforms;
@group(0) @binding(1) var src_tex: texture_2d<f32>;
@group(0) @binding(2) var src_smp: sampler;
struct VsOut {
@builtin(position) clip: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn post_vs(@builtin(vertex_index) vi: u32) -> VsOut {
var p = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0),
);
var out: VsOut;
let xy = p[vi];
out.clip = vec4<f32>(xy, 0.0, 1.0);
out.uv = vec2<f32>(xy.x * 0.5 + 0.5, 1.0 - (xy.y * 0.5 + 0.5));
return out;
}
// Linear → sRGB (IEC 61966-2-1) encode, per channel.
fn linear_to_srgb(c: vec3<f32>) -> vec3<f32> {
let lo = c * 12.92;
let hi = 1.055 * pow(max(c, vec3<f32>(0.0)), vec3<f32>(1.0 / 2.4)) - 0.055;
let cutoff = step(vec3<f32>(0.0031308), c);
return clamp(mix(lo, hi, cutoff), vec3<f32>(0.0), vec3<f32>(1.0));
}
// Encode for the present target: sRGB-encode iff `U.texel.z > 0.5` (a non-sRGB —
// WebGPU browser — surface), else pass through and let the hardware encode.
fn present_encode(rgb: vec3<f32>) -> vec3<f32> {
if (U.texel.z > 0.5) {
return linear_to_srgb(rgb);
}
return rgb;
}
@fragment
fn present_fs(in: VsOut) -> @location(0) vec4<f32> {
// Point-for-point: the source is the same size as the target, so a linear sampler
// at the pixel centre reads exactly one texel. Nothing is filtered, blurred or
// edge-detected here — the antialiasing already happened, upstream and in time.
let c = textureSample(src_tex, src_smp, in.uv);
return vec4<f32>(present_encode(c.rgb), c.a);
}