From 7c2ac0e4d8ae5b66e6d588b53ba90cc7359d9125 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 26 Jun 2026 03:35:16 -0400 Subject: [PATCH] hw: propagate stream colour tags to VAAPI frames (fixes washed-out HDR) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom: properly-converted BT.2020 PQ/HLG clips rendered progressively desaturated vs the SDR reference — the signature of the transfer/primaries tags not reaching the NV12→RGB shader, so HDR code values were decoded as plain sRGB/BT.709 (no gamut expansion, wrong EOTF). VAAPI hardware-decoded frames frequently leave color_trc/color_primaries/ colorspace/color_range unspecified — the authoritative values live on the codec context (parsed from the bitstream), which the importer never sees. Before importing, copy any unspecified colour field from the codec context onto the frame, so the importer detects PQ/HLG + BT.2020 correctly. Also add a one-time importer log (LB_VIDEO_DEBUG) of the detected ten_bit/full_range/colorspace/primaries/trc to confirm what's picked up. Co-Authored-By: Claude Opus 4.8 --- .../lightningbeam-core/src/video.rs | 21 +++++++++++++++++++ .../lightningbeam-editor/src/hw_video.rs | 13 ++++++++++++ 2 files changed, 34 insertions(+) diff --git a/lightningbeam-ui/lightningbeam-core/src/video.rs b/lightningbeam-ui/lightningbeam-core/src/video.rs index 31c31ce..8fb8552 100644 --- a/lightningbeam-ui/lightningbeam-core/src/video.rs +++ b/lightningbeam-ui/lightningbeam-core/src/video.rs @@ -460,6 +460,27 @@ impl VideoDecoder { if gpu_out { // Hardware + GPU consumer: import the VAAPI surface as wgpu NV12 textures // (no CPU copy). + // VAAPI hw frames often don't carry the stream's colour tags, so the + // importer (which only sees the frame) would mis-detect transfer/gamut. + // Copy the authoritative values from the codec context (parsed from the + // bitstream) onto the frame when it left them unspecified. + unsafe { + use ffmpeg::ffi::*; + let fp = frame.as_mut_ptr(); + let cp = decoder.as_ptr(); + if (*fp).color_trc == AVColorTransferCharacteristic::AVCOL_TRC_UNSPECIFIED { + (*fp).color_trc = (*cp).color_trc; + } + if (*fp).color_primaries == AVColorPrimaries::AVCOL_PRI_UNSPECIFIED { + (*fp).color_primaries = (*cp).color_primaries; + } + if (*fp).colorspace == AVColorSpace::AVCOL_SPC_UNSPECIFIED { + (*fp).colorspace = (*cp).colorspace; + } + if (*fp).color_range == AVColorRange::AVCOL_RANGE_UNSPECIFIED { + (*fp).color_range = (*cp).color_range; + } + } let importer = self.importer.as_ref().unwrap(); match unsafe { importer.import(frame.as_mut_ptr() as *mut std::ffi::c_void) } { Some(gpu) => { diff --git a/lightningbeam-ui/lightningbeam-editor/src/hw_video.rs b/lightningbeam-ui/lightningbeam-editor/src/hw_video.rs index f3226d6..2f349f9 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/hw_video.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/hw_video.rs @@ -17,6 +17,8 @@ use std::sync::{Arc, Mutex}; struct SharedHwImporter { device: wgpu::Device, adapter: wgpu::Adapter, + /// Log the detected colour info once (under LB_VIDEO_DEBUG) rather than per frame. + logged: std::sync::atomic::AtomicBool, } impl HwVideoImporter for SharedHwImporter { @@ -99,6 +101,16 @@ impl HwVideoImporter for SharedHwImporter { }; let coeffs = ycbcr_coeffs(kr, kb); + if std::env::var("LB_VIDEO_DEBUG").is_ok() + && !self.logged.swap(true, std::sync::atomic::Ordering::Relaxed) + { + eprintln!( + "[hw_video] {}x{} ten_bit={} full_range={} colorspace={:?} primaries={:?} trc={:?}", + width, height, ten_bit, full_range, + (*frame).colorspace, (*frame).color_primaries, (*frame).color_trc, + ); + } + // Transfer characteristic → which EOTF the compositor applies to reach scene-linear. let transfer = match (*frame).color_trc { ff::AVColorTransferCharacteristic::AVCOL_TRC_SMPTE2084 => VideoTransfer::Pq, @@ -143,6 +155,7 @@ pub fn install(vm: &Arc>, device: &wgpu::Device, adapter: &w let importer = Arc::new(SharedHwImporter { device: device.clone(), adapter: adapter.clone(), + logged: std::sync::atomic::AtomicBool::new(false), }); if let Ok(mut vm) = vm.lock() { vm.set_hardware_decode(