137 lines
5.1 KiB
WebGPU Shading Language
137 lines
5.1 KiB
WebGPU Shading Language
// NV12 → linear-RGB blit shader.
|
|
//
|
|
// Samples a hardware-decoded video frame stored as two planes — Y (R8Unorm) and
|
|
// interleaved CbCr (Rg8Unorm, half-res) — converts BT.709 Y'CbCr → gamma-encoded
|
|
// R'G'B', then sRGB→linear, and writes straight-alpha linear into the Rgba16Float
|
|
// HDR layer. This mirrors the software path (swscale → sRGB RGBA8 → sampled as
|
|
// Rgba8UnormSrgb → linear) so hardware- and software-decoded video match.
|
|
//
|
|
// The affine transform (viewport UV → frame UV) is the same packing as
|
|
// canvas_blit.wgsl's BlitTransform; `full_range` selects full vs. studio-swing
|
|
// de-quantization.
|
|
|
|
struct Nv12Params {
|
|
col0: vec4<f32>,
|
|
col1: vec4<f32>,
|
|
col2: vec4<f32>,
|
|
// Y'CbCr→R'G'B' matrix from the source colorspace: [Cr→R, Cb→G, Cr→G, Cb→B].
|
|
coeffs: vec4<f32>,
|
|
// .x = full_range; .y = transfer (0 gamma, 1 PQ, 2 HLG); .z = primaries (0 BT.709, 1 BT.2020).
|
|
// A vec4 keeps each block 16-aligned and the struct 80 bytes (Rust `[f32;4] + u32 + [u32;3]`).
|
|
flags: vec4<u32>,
|
|
}
|
|
|
|
@group(0) @binding(0) var y_tex: texture_2d<f32>;
|
|
@group(0) @binding(1) var samp: sampler;
|
|
@group(0) @binding(2) var<uniform> params: Nv12Params;
|
|
@group(0) @binding(3) var uv_tex: texture_2d<f32>;
|
|
|
|
struct VertexOutput {
|
|
@builtin(position) position: vec4<f32>,
|
|
@location(0) uv: vec2<f32>,
|
|
}
|
|
|
|
@vertex
|
|
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
|
var out: VertexOutput;
|
|
let x = f32((vertex_index & 1u) << 1u);
|
|
let y = f32(vertex_index & 2u);
|
|
out.position = vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
|
|
out.uv = vec2<f32>(x, y);
|
|
return out;
|
|
}
|
|
|
|
fn srgb_to_linear(c: vec3<f32>) -> vec3<f32> {
|
|
let lo = c / 12.92;
|
|
let hi = pow((c + vec3<f32>(0.055)) / 1.055, vec3<f32>(2.4));
|
|
return select(lo, hi, c > vec3<f32>(0.04045));
|
|
}
|
|
|
|
// SMPTE ST 2084 (PQ) EOTF: encoded [0,1] → absolute luminance, then normalize so the 203-nit
|
|
// graphics white = 1.0 (HDR highlights exceed 1.0). Per-channel.
|
|
fn pq_to_linear(c: vec3<f32>) -> vec3<f32> {
|
|
let m1 = 0.1593017578125;
|
|
let m2 = 78.84375;
|
|
let c1 = 0.8359375;
|
|
let c2 = 18.8515625;
|
|
let c3 = 18.6875;
|
|
let e = pow(max(c, vec3<f32>(0.0)), vec3<f32>(1.0 / m2));
|
|
let num = max(e - vec3<f32>(c1), vec3<f32>(0.0));
|
|
let den = vec3<f32>(c2) - c3 * e;
|
|
let nits = pow(num / den, vec3<f32>(1.0 / m1)) * 10000.0; // 0..10000 cd/m²
|
|
return nits / 203.0;
|
|
}
|
|
|
|
// ARIB STD-B67 (HLG) inverse-OETF → scene light, normalized so reference white (signal 0.75) = 1.0.
|
|
// The display OOTF is omitted (scene-referred compositing); approximate but reasonable for SDR-out.
|
|
fn hlg_to_linear(c: vec3<f32>) -> vec3<f32> {
|
|
let a = 0.17883277;
|
|
let b = 0.28466892;
|
|
let cc = 0.55991073;
|
|
let lo = (c * c) / 3.0;
|
|
let hi = (exp((c - vec3<f32>(cc)) / a) + vec3<f32>(b)) / 12.0;
|
|
let scene = select(lo, hi, c > vec3<f32>(0.5));
|
|
return scene / 0.26496256; // hlg_inv_oetf(0.75): put reference white at 1.0
|
|
}
|
|
|
|
// BT.2020 → BT.709 primaries, linear light (ITU-R BT.2087). Out-of-709 colours go negative → clamp.
|
|
fn bt2020_to_bt709(c: vec3<f32>) -> vec3<f32> {
|
|
let r = 1.660491 * c.r - 0.587641 * c.g - 0.072850 * c.b;
|
|
let g = -0.124550 * c.r + 1.132900 * c.g - 0.008349 * c.b;
|
|
let b = -0.018151 * c.r - 0.100579 * c.g + 1.118730 * c.b;
|
|
return max(vec3<f32>(r, g, b), vec3<f32>(0.0));
|
|
}
|
|
|
|
@fragment
|
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
|
let m = mat3x3<f32>(params.col0.xyz, params.col1.xyz, params.col2.xyz);
|
|
let frame_uv = (m * vec3<f32>(in.uv.x, in.uv.y, 1.0)).xy;
|
|
|
|
if frame_uv.x < 0.0 || frame_uv.x > 1.0
|
|
|| frame_uv.y < 0.0 || frame_uv.y > 1.0 {
|
|
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
|
}
|
|
|
|
let yv = textureSample(y_tex, samp, frame_uv).r;
|
|
let cbcr = textureSample(uv_tex, samp, frame_uv).rg;
|
|
|
|
var Y: f32;
|
|
var Cb: f32;
|
|
var Cr: f32;
|
|
if params.flags.x != 0u {
|
|
// Full ("JPEG") range: [0,255] luma, chroma centered at 128.
|
|
Y = yv;
|
|
Cb = cbcr.r - 0.5;
|
|
Cr = cbcr.g - 0.5;
|
|
} else {
|
|
// Studio swing: Y'∈[16,235], Cb/Cr∈[16,240].
|
|
Y = (yv * 255.0 - 16.0) / 219.0;
|
|
Cb = (cbcr.r * 255.0 - 128.0) / 224.0;
|
|
Cr = (cbcr.g * 255.0 - 128.0) / 224.0;
|
|
}
|
|
|
|
// Y'CbCr → gamma-encoded R'G'B' using the source colorspace's matrix.
|
|
let r = Y + params.coeffs.x * Cr;
|
|
let g = Y + params.coeffs.y * Cb + params.coeffs.z * Cr;
|
|
let b = Y + params.coeffs.w * Cb;
|
|
// Valid encoded signal is [0,1]; clamp before the EOTF (HDR comes from the EOTF, not overshoot).
|
|
let rgb_enc = clamp(vec3<f32>(r, g, b), vec3<f32>(0.0), vec3<f32>(1.0));
|
|
|
|
// Encoded R'G'B' → scene-linear (graphics white = 1.0; HDR may exceed 1.0).
|
|
var rgb_lin: vec3<f32>;
|
|
if params.flags.y == 1u {
|
|
rgb_lin = pq_to_linear(rgb_enc);
|
|
} else if params.flags.y == 2u {
|
|
rgb_lin = hlg_to_linear(rgb_enc);
|
|
} else {
|
|
rgb_lin = srgb_to_linear(rgb_enc);
|
|
}
|
|
|
|
// Wide-gamut → BT.709 in linear light to match the compositor's primaries.
|
|
if params.flags.z == 1u {
|
|
rgb_lin = bt2020_to_bt709(rgb_lin);
|
|
}
|
|
|
|
return vec4<f32>(rgb_lin, 1.0);
|
|
}
|