From 6c8cb5b9e08ee29935cb8eede2c5080e1e81bb2a Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 26 Jun 2026 03:16:28 -0400 Subject: [PATCH] hw: P010 (10-bit) DMA-BUF import for real HDR video MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 10/12-bit HDR decodes to P010-style VAAPI surfaces (16-bit planes), which the 8-bit NV12 import couldn't handle — real HDR clips fell back to software (losing the HDR). Import them as R16/Rg16 plane textures: - dmabuf::Nv12DmaBuf gains `ten_bit`; import_raw picks R16_UNORM/R16G16_UNORM (vk) + R16Unorm/Rg16Unorm (wgpu) when set, else the existing R8/Rg8. - hw_video.rs detects 10-bit from the hw frames context sw_format (P010/P012/P016) and sets it. The NV12→RGB shader is unchanged: it samples normalized floats, and the limited/full de-quant lands at the same values regardless of bit depth. - vk_device requests TEXTURE_FORMAT_16BIT_NORM when the adapter supports it (R16/ Rg16 need it); absent → 10-bit falls back to software, 8-bit unaffected. Pairs with the PQ/HLG/BT.2020 colour math so HDR10/HLG clips now reach the GPU path end-to-end. NV12 callers (encoder, decode primitive) pass ten_bit=false. Co-Authored-By: Claude Opus 4.8 --- .../gpu-video-encoder/src/decoder.rs | 1 + .../gpu-video-encoder/src/dmabuf.rs | 27 +++++++++++++++---- .../gpu-video-encoder/src/encoder.rs | 1 + .../gpu-video-encoder/src/vk_device.rs | 5 +++- .../lightningbeam-editor/src/hw_video.rs | 21 +++++++++++++++ 5 files changed, 49 insertions(+), 6 deletions(-) diff --git a/lightningbeam-ui/gpu-video-encoder/src/decoder.rs b/lightningbeam-ui/gpu-video-encoder/src/decoder.rs index 9f64856..884c66f 100644 --- a/lightningbeam-ui/gpu-video-encoder/src/decoder.rs +++ b/lightningbeam-ui/gpu-video-encoder/src/decoder.rs @@ -194,6 +194,7 @@ impl VaapiDecoder { y_pitch: y_pl.pitch as u64, uv_offset: uv_pl.offset as u64, uv_pitch: uv_pl.pitch as u64, + ten_bit: false, }; let imported = dmabuf::import_raw(&self.drm.device, &self.drm.adapter, &buf); ff::av_frame_free(&mut (drm_f as *mut _)); // the fd was dup'd into Vulkan diff --git a/lightningbeam-ui/gpu-video-encoder/src/dmabuf.rs b/lightningbeam-ui/gpu-video-encoder/src/dmabuf.rs index 492e0b4..7cc72b4 100644 --- a/lightningbeam-ui/gpu-video-encoder/src/dmabuf.rs +++ b/lightningbeam-ui/gpu-video-encoder/src/dmabuf.rs @@ -6,7 +6,7 @@ use crate::vaapi::MappedSurface; use crate::vk_device::DrmDevice; use ash::vk; -/// Plane layout for a single-object NV12 DMA-BUF (the common VAAPI case). +/// Plane layout for a single-object NV12/P010 DMA-BUF (the common VAAPI case). #[derive(Clone, Copy)] pub struct Nv12DmaBuf { pub fd: i32, @@ -18,6 +18,9 @@ pub struct Nv12DmaBuf { pub y_pitch: u64, pub uv_offset: u64, pub uv_pitch: u64, + /// True for 10/12/16-bit content (P010 etc.): planes are 16-bit (R16/Rg16) rather than 8-bit + /// (R8/Rg8). The sampled float is normalized either way, so the consumer needs no change. + pub ten_bit: bool, } /// Frees the shared imported `VkDeviceMemory` once both plane images are gone. Held by @@ -68,6 +71,7 @@ pub fn import(drm: &DrmDevice, surf: &MappedSurface) -> Result i, diff --git a/lightningbeam-ui/gpu-video-encoder/src/vk_device.rs b/lightningbeam-ui/gpu-video-encoder/src/vk_device.rs index 93a67aa..fcff428 100644 --- a/lightningbeam-ui/gpu-video-encoder/src/vk_device.rs +++ b/lightningbeam-ui/gpu-video-encoder/src/vk_device.rs @@ -143,7 +143,10 @@ unsafe fn create_inner(windowed: bool) -> Result { open_device, &wgpu::DeviceDescriptor { label: Some("drm-import-device"), - required_features: wgpu::Features::empty(), + // R16/Rg16 plane textures for P010 (10-bit HDR) import need this; request it only + // when the adapter supports it (else 10-bit falls back to software decode). + required_features: wgpu_adapter.features() + & wgpu::Features::TEXTURE_FORMAT_16BIT_NORM, // Vello's compute pipelines need more than downlevel limits (e.g. // max_storage_buffers_per_shader_stage >= 5). This device only ever runs on a // real VAAPI-capable GPU, so request the adapter's full limits. diff --git a/lightningbeam-ui/lightningbeam-editor/src/hw_video.rs b/lightningbeam-ui/lightningbeam-editor/src/hw_video.rs index d3f915d..f3226d6 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/hw_video.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/hw_video.rs @@ -36,6 +36,26 @@ impl HwVideoImporter for SharedHwImporter { let obj = &(*desc).objects[0]; let width = (*frame).width as u32; let height = (*frame).height as u32; + + // 10/12/16-bit content decodes to P010-style surfaces (16-bit planes). Detect via the hw + // frames context's software format so the import builds R16/Rg16 textures. + let ten_bit = { + let hwfc = (*frame).hw_frames_ctx; + if hwfc.is_null() { + false + } else { + let ctx = (*hwfc).data as *const ff::AVHWFramesContext; + matches!( + (*ctx).sw_format, + ff::AVPixelFormat::AV_PIX_FMT_P010LE + | ff::AVPixelFormat::AV_PIX_FMT_P010BE + | ff::AVPixelFormat::AV_PIX_FMT_P012LE + | ff::AVPixelFormat::AV_PIX_FMT_P012BE + | ff::AVPixelFormat::AV_PIX_FMT_P016LE + | ff::AVPixelFormat::AV_PIX_FMT_P016BE + ) + } + }; // NV12: Y then UV — two layers (one plane each) or one layer with two planes. let (y_pl, uv_pl) = if (*desc).nb_layers >= 2 { (&(*desc).layers[0].planes[0], &(*desc).layers[1].planes[0]) @@ -52,6 +72,7 @@ impl HwVideoImporter for SharedHwImporter { y_pitch: y_pl.pitch as u64, uv_offset: uv_pl.offset as u64, uv_pitch: uv_pl.pitch as u64, + ten_bit, }; let full_range = (*frame).color_range == ff::AVColorRange::AVCOL_RANGE_JPEG;