nv12: convert with the source colorspace matrix (not hardcoded BT.709)
The NV12→RGB pass hardcoded BT.709, so SD (BT.601) clips had slightly wrong hues.
Read each frame's AVColorSpace in the importer and derive the Y'CbCr→R'G'B'
matrix (BT.709/601/240M/2020; Unspecified guessed by height like swscale/players),
carry the four coefficients on GpuVideoFrame, and apply them in the shader.
- core: GpuVideoFrame.coeffs + ycbcr_coeffs(kr, kb) helper.
- hw_video.rs: map AVColorSpace → (kr, kb) → coeffs.
- nv12_blit{.rs,.wgsl}: uniform grows to 80 bytes (adds a coeffs vec4); the matrix
multiply uses params.coeffs instead of literals.
BT.2020's transfer is still approximated as sRGB. The DRM-modifier-without-SAMPLED case stays a graceful software fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1c537d99da
commit
6348e57de0
|
|
@ -715,6 +715,22 @@ pub struct GpuVideoFrame {
|
||||||
/// Source YUV range: true = full/PC (0–255), false = limited/TV (16–235). Drives the NV12→RGB
|
/// Source YUV range: true = full/PC (0–255), false = limited/TV (16–235). Drives the NV12→RGB
|
||||||
/// offset/scale in the compositor.
|
/// offset/scale in the compositor.
|
||||||
pub full_range: bool,
|
pub full_range: bool,
|
||||||
|
/// Y'CbCr→R'G'B' matrix coefficients derived from the frame's colorspace (BT.709/601/2020),
|
||||||
|
/// so SD (BT.601) and HD/UHD clips each convert correctly: `[Cr→R, Cb→G, Cr→G, Cb→B]`.
|
||||||
|
/// R = Y + c[0]·Cr, G = Y + c[1]·Cb + c[2]·Cr, B = Y + c[3]·Cb
|
||||||
|
pub coeffs: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Y'CbCr→R'G'B' matrix coefficients (`[Cr→R, Cb→G, Cr→G, Cb→B]`) from the luma weights `kr`/`kb`
|
||||||
|
/// (`kg = 1−kr−kb`). BT.709 → `[1.5748, −0.1873, −0.4681, 1.8556]`.
|
||||||
|
pub fn ycbcr_coeffs(kr: f32, kb: f32) -> [f32; 4] {
|
||||||
|
let kg = 1.0 - kr - kb;
|
||||||
|
[
|
||||||
|
2.0 * (1.0 - kr),
|
||||||
|
-2.0 * kb * (1.0 - kb) / kg,
|
||||||
|
-2.0 * kr * (1.0 - kr) / kg,
|
||||||
|
2.0 * (1.0 - kb),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Imports a decoded VAAPI surface (a `*mut AVFrame`, passed as an opaque pointer so core needn't
|
/// Imports a decoded VAAPI surface (a `*mut AVFrame`, passed as an opaque pointer so core needn't
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@
|
||||||
|
|
||||||
use ffmpeg_next::ffi as ff;
|
use ffmpeg_next::ffi as ff;
|
||||||
use gpu_video_encoder::dmabuf::{self, Nv12DmaBuf};
|
use gpu_video_encoder::dmabuf::{self, Nv12DmaBuf};
|
||||||
use lightningbeam_core::video::{GpuVideoFrame, HwDeviceHandle, HwVideoImporter, VideoManager};
|
use lightningbeam_core::video::{
|
||||||
|
ycbcr_coeffs, GpuVideoFrame, HwDeviceHandle, HwVideoImporter, VideoManager,
|
||||||
|
};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
/// Imports decoded VAAPI surfaces onto the shared wgpu device. Holds clones of the shared
|
/// Imports decoded VAAPI surfaces onto the shared wgpu device. Holds clones of the shared
|
||||||
|
|
@ -52,6 +54,29 @@ impl HwVideoImporter for SharedHwImporter {
|
||||||
};
|
};
|
||||||
let full_range = (*frame).color_range == ff::AVColorRange::AVCOL_RANGE_JPEG;
|
let full_range = (*frame).color_range == ff::AVColorRange::AVCOL_RANGE_JPEG;
|
||||||
|
|
||||||
|
// Luma weights (kr, kb) from the frame's matrix coefficients, so SD (BT.601) and HD/UHD
|
||||||
|
// (BT.709) clips each convert with the right matrix. Unspecified → guess by height, as
|
||||||
|
// players/swscale do. SMPTE240M and BT.2020 are handled too (the latter's transfer is still
|
||||||
|
// approximated as sRGB — fine for SDR; true HDR is out of scope).
|
||||||
|
let (kr, kb) = match (*frame).colorspace {
|
||||||
|
ff::AVColorSpace::AVCOL_SPC_BT709 => (0.2126, 0.0722),
|
||||||
|
ff::AVColorSpace::AVCOL_SPC_BT470BG | ff::AVColorSpace::AVCOL_SPC_SMPTE170M => {
|
||||||
|
(0.299, 0.114)
|
||||||
|
}
|
||||||
|
ff::AVColorSpace::AVCOL_SPC_SMPTE240M => (0.212, 0.087),
|
||||||
|
ff::AVColorSpace::AVCOL_SPC_BT2020_NCL | ff::AVColorSpace::AVCOL_SPC_BT2020_CL => {
|
||||||
|
(0.2627, 0.0593)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if height <= 576 {
|
||||||
|
(0.299, 0.114) // SD → BT.601
|
||||||
|
} else {
|
||||||
|
(0.2126, 0.0722) // HD/UHD → BT.709
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let coeffs = ycbcr_coeffs(kr, kb);
|
||||||
|
|
||||||
let imported = dmabuf::import_raw(&self.device, &self.adapter, &buf);
|
let imported = dmabuf::import_raw(&self.device, &self.adapter, &buf);
|
||||||
ff::av_frame_free(&mut (drm_f as *mut _)); // the fd was dup'd into Vulkan
|
ff::av_frame_free(&mut (drm_f as *mut _)); // the fd was dup'd into Vulkan
|
||||||
let (y, uv) = imported.ok()?.into_planes();
|
let (y, uv) = imported.ok()?.into_planes();
|
||||||
|
|
@ -61,6 +86,7 @@ impl HwVideoImporter for SharedHwImporter {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
full_range,
|
full_range,
|
||||||
|
coeffs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,13 @@
|
||||||
|
|
||||||
use crate::gpu_brush::BlitTransform;
|
use crate::gpu_brush::BlitTransform;
|
||||||
|
|
||||||
/// Uniform: the `viewport_uv → frame_uv` affine (same packing as [`BlitTransform`]) plus the
|
/// Uniform: the `viewport_uv → frame_uv` affine (same packing as [`BlitTransform`]), the Y'CbCr→RGB
|
||||||
/// full-range flag. 64 bytes (48 for the matrix + a u32 + 12 padding).
|
/// matrix coefficients, and the full-range flag. 80 bytes (48 matrix + 16 coeffs + u32 + 12 pad).
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
struct Nv12Params {
|
struct Nv12Params {
|
||||||
transform: BlitTransform,
|
transform: BlitTransform,
|
||||||
|
coeffs: [f32; 4],
|
||||||
full_range: u32,
|
full_range: u32,
|
||||||
_pad: [u32; 3],
|
_pad: [u32; 3],
|
||||||
}
|
}
|
||||||
|
|
@ -127,9 +128,11 @@ impl Nv12BlitPipeline {
|
||||||
target_view: &wgpu::TextureView,
|
target_view: &wgpu::TextureView,
|
||||||
transform: &BlitTransform,
|
transform: &BlitTransform,
|
||||||
full_range: bool,
|
full_range: bool,
|
||||||
|
coeffs: [f32; 4],
|
||||||
) {
|
) {
|
||||||
let params = Nv12Params {
|
let params = Nv12Params {
|
||||||
transform: *transform,
|
transform: *transform,
|
||||||
|
coeffs,
|
||||||
full_range: full_range as u32,
|
full_range: full_range as u32,
|
||||||
_pad: [0; 3],
|
_pad: [0; 3],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,10 @@ struct Nv12Params {
|
||||||
col0: vec4<f32>,
|
col0: vec4<f32>,
|
||||||
col1: vec4<f32>,
|
col1: vec4<f32>,
|
||||||
col2: vec4<f32>,
|
col2: vec4<f32>,
|
||||||
// .x = full_range flag; .yzw padding. A vec4 keeps the struct 64 bytes (matching the Rust
|
// Y'CbCr→R'G'B' matrix from the source colorspace: [Cr→R, Cb→G, Cr→G, Cb→B].
|
||||||
// `u32 + [u32; 3]`); a bare u32 + vec3 would round up to 80 and mismatch the bound buffer.
|
coeffs: vec4<f32>,
|
||||||
|
// .x = full_range flag; .yzw padding. A vec4 keeps each block 16-aligned and the struct size
|
||||||
|
// matching the Rust `[f32;4] + u32 + [u32;3]` (80 bytes).
|
||||||
flags: vec4<u32>,
|
flags: vec4<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,10 +75,10 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
Cr = (cbcr.g * 255.0 - 128.0) / 224.0;
|
Cr = (cbcr.g * 255.0 - 128.0) / 224.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// BT.709 Y'CbCr → gamma-encoded R'G'B'.
|
// Y'CbCr → gamma-encoded R'G'B' using the source colorspace's matrix.
|
||||||
let r = Y + 1.5748 * Cr;
|
let r = Y + params.coeffs.x * Cr;
|
||||||
let g = Y - 0.1873 * Cb - 0.4681 * Cr;
|
let g = Y + params.coeffs.y * Cb + params.coeffs.z * Cr;
|
||||||
let b = Y + 1.8556 * Cb;
|
let b = Y + params.coeffs.w * Cb;
|
||||||
let rgb_gamma = clamp(vec3<f32>(r, g, b), vec3<f32>(0.0), vec3<f32>(1.0));
|
let rgb_gamma = clamp(vec3<f32>(r, g, b), vec3<f32>(0.0), vec3<f32>(1.0));
|
||||||
|
|
||||||
// R'G'B' is gamma-encoded; the HDR target is linear → undo the transfer.
|
// R'G'B' is gamma-encoded; the HDR target is linear → undo the transfer.
|
||||||
|
|
|
||||||
|
|
@ -1703,7 +1703,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
let y_view = gpu.y.create_view(&Default::default());
|
let y_view = gpu.y.create_view(&Default::default());
|
||||||
let uv_view = gpu.uv.create_view(&Default::default());
|
let uv_view = gpu.uv.create_view(&Default::default());
|
||||||
shared.nv12_blit.blit(
|
shared.nv12_blit.blit(
|
||||||
device, queue, &y_view, &uv_view, hdr_layer_view, &bt, gpu.full_range,
|
device, queue, &y_view, &uv_view, hdr_layer_view, &bt, gpu.full_range, gpu.coeffs,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Reuse the GPU texture for this frame if it's unchanged (a
|
// Reuse the GPU texture for this frame if it's unchanged (a
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue