hw: P010 (10-bit) DMA-BUF import for real HDR video

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 <noreply@anthropic.com>
This commit is contained in:
Skyler Lehmkuhl 2026-06-26 03:16:28 -04:00
parent b76eefb404
commit 6c8cb5b9e0
5 changed files with 49 additions and 6 deletions

View File

@ -194,6 +194,7 @@ impl VaapiDecoder {
y_pitch: y_pl.pitch as u64, y_pitch: y_pl.pitch as u64,
uv_offset: uv_pl.offset as u64, uv_offset: uv_pl.offset as u64,
uv_pitch: uv_pl.pitch as u64, uv_pitch: uv_pl.pitch as u64,
ten_bit: false,
}; };
let imported = dmabuf::import_raw(&self.drm.device, &self.drm.adapter, &buf); 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 ff::av_frame_free(&mut (drm_f as *mut _)); // the fd was dup'd into Vulkan

View File

@ -6,7 +6,7 @@ use crate::vaapi::MappedSurface;
use crate::vk_device::DrmDevice; use crate::vk_device::DrmDevice;
use ash::vk; 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)] #[derive(Clone, Copy)]
pub struct Nv12DmaBuf { pub struct Nv12DmaBuf {
pub fd: i32, pub fd: i32,
@ -18,6 +18,9 @@ pub struct Nv12DmaBuf {
pub y_pitch: u64, pub y_pitch: u64,
pub uv_offset: u64, pub uv_offset: u64,
pub uv_pitch: 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 /// 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<ImportedNv12, Str
y_pitch: surf.y_pitch, y_pitch: surf.y_pitch,
uv_offset: surf.uv_offset, uv_offset: surf.uv_offset,
uv_pitch: surf.uv_pitch, uv_pitch: surf.uv_pitch,
ten_bit: false,
}, },
) )
} }
@ -130,8 +134,21 @@ pub fn import_raw(
.map_err(|e| format!("vkCreateImage(modifier) failed: {e:?}")) .map_err(|e| format!("vkCreateImage(modifier) failed: {e:?}"))
}; };
let img_y = make_image(vk::Format::R8_UNORM, buf.width, buf.height, buf.y_pitch)?; // 8-bit NV12 → R8/Rg8 planes; 10/12/16-bit P010-style → R16/Rg16 (sampled value is
let img_uv = make_image(vk::Format::R8G8_UNORM, buf.width / 2, buf.height / 2, buf.uv_pitch)?; // normalized either way, so the NV12→RGB consumer is unchanged).
let (vk_y, vk_uv) = if buf.ten_bit {
(vk::Format::R16_UNORM, vk::Format::R16G16_UNORM)
} else {
(vk::Format::R8_UNORM, vk::Format::R8G8_UNORM)
};
let (wgpu_y, wgpu_uv) = if buf.ten_bit {
(wgpu::TextureFormat::R16Unorm, wgpu::TextureFormat::Rg16Unorm)
} else {
(wgpu::TextureFormat::R8Unorm, wgpu::TextureFormat::Rg8Unorm)
};
let img_y = make_image(vk_y, buf.width, buf.height, buf.y_pitch)?;
let img_uv = make_image(vk_uv, buf.width / 2, buf.height / 2, buf.uv_pitch)?;
let fd_dev = ash::khr::external_memory_fd::Device::new(instance, &raw_device); let fd_dev = ash::khr::external_memory_fd::Device::new(instance, &raw_device);
let mut fd_props = vk::MemoryFdPropertiesKHR::default(); let mut fd_props = vk::MemoryFdPropertiesKHR::default();
@ -206,8 +223,8 @@ pub fn import_raw(
}, },
) )
}; };
let y = wrap(img_y, wgpu::TextureFormat::R8Unorm, buf.width, buf.height); let y = wrap(img_y, wgpu_y, buf.width, buf.height);
let uv = wrap(img_uv, wgpu::TextureFormat::Rg8Unorm, buf.width / 2, buf.height / 2); let uv = wrap(img_uv, wgpu_uv, buf.width / 2, buf.height / 2);
drop(hal_device); drop(hal_device);
Ok(ImportedNv12 { y, uv }) Ok(ImportedNv12 { y, uv })

View File

@ -214,6 +214,7 @@ impl ZeroCopyEncoder {
y_pitch: y.pitch as u64, y_pitch: y.pitch as u64,
uv_offset: uv.offset as u64, uv_offset: uv.offset as u64,
uv_pitch: uv.pitch as u64, uv_pitch: uv.pitch as u64,
ten_bit: false,
}; };
let imported = match dmabuf::import_raw(&self.drm.device, &self.drm.adapter, &buf) { let imported = match dmabuf::import_raw(&self.drm.device, &self.drm.adapter, &buf) {
Ok(i) => i, Ok(i) => i,

View File

@ -143,7 +143,10 @@ unsafe fn create_inner(windowed: bool) -> Result<DrmDevice, String> {
open_device, open_device,
&wgpu::DeviceDescriptor { &wgpu::DeviceDescriptor {
label: Some("drm-import-device"), 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. // 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 // 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. // real VAAPI-capable GPU, so request the adapter's full limits.

View File

@ -36,6 +36,26 @@ impl HwVideoImporter for SharedHwImporter {
let obj = &(*desc).objects[0]; let obj = &(*desc).objects[0];
let width = (*frame).width as u32; let width = (*frame).width as u32;
let height = (*frame).height 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. // 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 { let (y_pl, uv_pl) = if (*desc).nb_layers >= 2 {
(&(*desc).layers[0].planes[0], &(*desc).layers[1].planes[0]) (&(*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, y_pitch: y_pl.pitch as u64,
uv_offset: uv_pl.offset as u64, uv_offset: uv_pl.offset as u64,
uv_pitch: uv_pl.pitch as u64, uv_pitch: uv_pl.pitch as u64,
ten_bit,
}; };
let full_range = (*frame).color_range == ff::AVColorRange::AVCOL_RANGE_JPEG; let full_range = (*frame).color_range == ff::AVColorRange::AVCOL_RANGE_JPEG;