feat(video): HDR10/P010 end to end (phase 6)
The client now advertises VIDEO_CAP_10BIT|HDR and carries the result all the way to glass: - csc_rows is bit-depth exact (10-bit limited code points differ from 8-bit by ~half a code) and folds in the P010/X6 MSB-packing factor; new 10-bit white/black tests. - The CSC shader grows a params block: mode 0 passes the transfer through (SDR as-is, or PQ onto an HDR10 swapchain); mode 1 tonemaps PQ→SDR in-shader (ST.2084 EOTF, 203-nit reference white exposure, BT.2020→709, soft maxRGB rolloff, sRGB encode) for desktops without an HDR surface. PUNKTFUNK_TONEMAP_PEAK tunes the rolloff. - The presenter probes VK_EXT_swapchain_colorspace + an HDR10/ST.2084 10-bit surface format and flips modes in-band with the stream's PQ signaling: fence-quiesce, then CSC pass + video image (10-bit A2B10G10R10 intermediate — PQ in 8 bits bands) + overlay pipe + swapchain rebuild through the deferred-destroy rules. - P010 decodes through all three paths: Vulkan Video (X6 multiplanar pool, R10X6 plane views), VAAPI dmabuf (R16/RG1616 plane imports), software (swscale as before). - session pump advertises the caps; the host still gates Main10 behind its PUNKTFUNK_10BIT policy. Probed on glass hardware: the KDE/NVIDIA surface exposes A2B10G10R10+HDR10_ST2084, so true PQ passthrough is available there. Known v1 gaps: software-decode PQ shows untonemapped (8-bit RGBA carries the transfer baked); the SDR overlay composites unscaled onto an HDR10 surface (dim OSD); no vkSetHdrMetadataEXT yet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,20 @@
|
||||
// NV12 → RGBA with the stream's CICP signaling — the Vulkan port of the GL presenter's
|
||||
// fragment shader (video_gl.rs). The YUV→RGB matrix + range expansion arrive as three
|
||||
// push-constant rows precomputed on the CPU (csc.rs `csc_rows`): rgb[i] = dot(r_i.xyz,
|
||||
// yuv) + r_i.w — one shader for BT.601/709/2020, full/limited. The chroma plane is
|
||||
// half-res; the linear sampler interpolates, same as the GL path.
|
||||
// YCbCr (2-plane 4:2:0) → RGBA with the stream's CICP signaling — the Vulkan port of
|
||||
// the GL presenter's fragment shader, grown depth- and HDR-aware.
|
||||
//
|
||||
// The YUV→RGB matrix + range expansion arrive as three push-constant rows precomputed
|
||||
// on the CPU (csc.rs `csc_rows` — bit-depth exact, including the P010/X6 MSB-packing
|
||||
// factor): rgb[i] = dot(r_i.xyz, yuv) + r_i.w. One shader for BT.601/709/2020,
|
||||
// full/limited, 8- and 10-bit. The chroma plane is half-res; the linear sampler
|
||||
// interpolates, same as the GL path.
|
||||
//
|
||||
// params.x selects the output mode:
|
||||
// 0 — passthrough: the transfer stays baked (SDR BT.709 shown as-is; PQ BT.2020
|
||||
// written to an HDR10 swapchain that expects exactly PQ-encoded values).
|
||||
// 1 — PQ → SDR tonemap (an HDR stream on a desktop without an HDR10 surface):
|
||||
// PQ EOTF → linear light (nits/10000), exposure anchored at the 203-nit HDR
|
||||
// reference white, BT.2020→709 primaries, a soft maxRGB rolloff for highlights
|
||||
// (BT.2390-flavored simplicity, not libplacebo), then sRGB encode.
|
||||
// params.y = tonemap peak (display-relative, ~= peak_nits / 203).
|
||||
//
|
||||
// Regenerate: shaders/build.sh (committed .spv, no build-time toolchain).
|
||||
#version 450
|
||||
@@ -17,20 +29,62 @@ layout(push_constant) uniform Csc {
|
||||
vec4 r0;
|
||||
vec4 r1;
|
||||
vec4 r2;
|
||||
vec4 params; // x: mode, y: tonemap peak, z/w: reserved
|
||||
} pc;
|
||||
|
||||
// SMPTE ST.2084 (PQ) EOTF: code value → display-referred linear, normalized to 1.0 =
|
||||
// 10000 nits.
|
||||
vec3 pq_eotf(vec3 e) {
|
||||
const float m1 = 0.1593017578125; // 2610/16384
|
||||
const float m2 = 78.84375; // 2523/4096 * 128
|
||||
const float c1 = 0.8359375; // 3424/4096
|
||||
const float c2 = 18.8515625; // 2413/4096 * 32
|
||||
const float c3 = 18.6875; // 2392/4096 * 32
|
||||
vec3 p = pow(max(e, vec3(0.0)), vec3(1.0 / m2));
|
||||
return pow(max(p - c1, vec3(0.0)) / (c2 - c3 * p), vec3(1.0 / m1));
|
||||
}
|
||||
|
||||
// BT.2020 → BT.709 primaries (linear light).
|
||||
vec3 bt2020_to_709(vec3 c) {
|
||||
return mat3(
|
||||
1.6605, -0.1246, -0.0182,
|
||||
-0.5876, 1.1329, -0.1006,
|
||||
-0.0728, -0.0083, 1.1187
|
||||
) * c;
|
||||
}
|
||||
|
||||
// Linear → sRGB OETF.
|
||||
vec3 srgb_oetf(vec3 c) {
|
||||
c = clamp(c, 0.0, 1.0);
|
||||
bvec3 lo = lessThanEqual(c, vec3(0.0031308));
|
||||
vec3 hi = 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055;
|
||||
return mix(hi, c * 12.92, vec3(lo));
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 yuv = vec3(texture(u_y, v_uv).r, texture(u_c, v_uv).rg);
|
||||
frag = vec4(
|
||||
clamp(
|
||||
vec3(
|
||||
dot(pc.r0.xyz, yuv) + pc.r0.w,
|
||||
dot(pc.r1.xyz, yuv) + pc.r1.w,
|
||||
dot(pc.r2.xyz, yuv) + pc.r2.w
|
||||
),
|
||||
0.0,
|
||||
1.0
|
||||
),
|
||||
1.0
|
||||
vec3 rgb = vec3(
|
||||
dot(pc.r0.xyz, yuv) + pc.r0.w,
|
||||
dot(pc.r1.xyz, yuv) + pc.r1.w,
|
||||
dot(pc.r2.xyz, yuv) + pc.r2.w
|
||||
);
|
||||
|
||||
if (pc.params.x > 0.5) {
|
||||
// PQ BT.2020 → SDR BT.709: linearize, anchor exposure at the 203-nit HDR
|
||||
// reference white (SDR diffuse white), convert primaries, roll off highlights.
|
||||
vec3 lin = pq_eotf(clamp(rgb, 0.0, 1.0)) * (10000.0 / 203.0);
|
||||
lin = max(bt2020_to_709(lin), vec3(0.0));
|
||||
float peak = max(pc.params.y, 1.0001);
|
||||
float l = max(lin.r, max(lin.g, lin.b));
|
||||
if (l > 1.0) {
|
||||
// Soft maxRGB rolloff: identity below 1.0, asymptotic to `peak` above —
|
||||
// keeps colors from clipping to white the way per-channel clamp would.
|
||||
float mapped = 1.0 + (l - 1.0) / (1.0 + (l - 1.0) / (peak - 1.0));
|
||||
lin *= mapped / l;
|
||||
}
|
||||
rgb = srgb_oetf(lin);
|
||||
} else {
|
||||
rgb = clamp(rgb, 0.0, 1.0);
|
||||
}
|
||||
frag = vec4(rgb, 1.0);
|
||||
}
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user