Skip to content

direct: export multi-plane YUV as one dma-buf object per plane - #446

Merged
elFarto merged 2 commits into
elFarto:masterfrom
AoTofu:pr/chromium-direct-backend
Jul 9, 2026
Merged

elFarto merged 2 commits into
elFarto:masterfrom
AoTofu:pr/chromium-direct-backend

Conversation

@AoTofu

@AoTofu AoTofu commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

The single-buffer export packs every plane into one dma-buf with the chroma plane at a non-zero byte offset inside a shared block-linear buffer. Chromium's multi-plane import handles that, but per-plane importers (mpv/GStreamer/ffmpeg, which import each layer as a standalone dma-buf) can't detile a tiled plane that starts at a byte offset, so NV12 chroma came out scrambled/stretched under mpv --hwdec=vaapi. Since the direct backend is now the default, this broke hardware decode for every non-Chromium VA-API client.

Allocate each plane in its own dma-buf object at offset 0 instead, while still forcing a single shared block height across planes (via the existing unified layout) so all objects report one DRM modifier -- the invariant Chromium's one-modifier-per-buffer rule needs. Per-plane importers now detile the chroma plane correctly, and Chromium keeps working (num_objects>1 with a common modifier is accepted). The plane's CUDA array is created at the block-aligned height so CUDA tiles it with the advertised block height (same guard as the 144p green-chroma fix). The single-buffer layout stays available behind NVD_SINGLE_BUFFER for comparison.

Verified on RTX 5080: mpv --hwdec=vaapi now renders NV12 correctly at 144p/720p/ 1080p (was corrupt), Chrome still HW-decodes 720p with no GPU-process crash, and H264/VP9/AV1/HEVC10 decode is unchanged.

The single-buffer export packs every plane into one dma-buf with the chroma
plane at a non-zero byte offset inside a shared block-linear buffer. Chromium's
multi-plane import handles that, but per-plane importers (mpv/GStreamer/ffmpeg,
which import each layer as a standalone dma-buf) can't detile a tiled plane that
starts at a byte offset, so NV12 chroma came out scrambled/stretched under
mpv --hwdec=vaapi. Since the direct backend is now the default, this broke
hardware decode for every non-Chromium VA-API client.

Allocate each plane in its own dma-buf object at offset 0 instead, while still
forcing a single shared block height across planes (via the existing unified
layout) so all objects report one DRM modifier -- the invariant Chromium's
one-modifier-per-buffer rule needs. Per-plane importers now detile the chroma
plane correctly, and Chromium keeps working (num_objects>1 with a common
modifier is accepted). The plane's CUDA array is created at the block-aligned
height so CUDA tiles it with the advertised block height (same guard as the
144p green-chroma fix). The single-buffer layout stays available behind
NVD_SINGLE_BUFFER for comparison.

Verified on RTX 5080: mpv --hwdec=vaapi now renders NV12 correctly at 144p/720p/
1080p (was corrupt), Chrome still HW-decodes 720p with no GPU-process crash, and
H264/VP9/AV1/HEVC10 decode is unchanged.
@AoTofu

AoTofu commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Quick follow-up on the 10-bit (P010) note I made earlier β€” I dug into it and it turns out not to be a driver bug, so I wanted to correct the record.

10-bit actually plays fine on the direct backend with mpv's OpenGL interop:

mpv --hwdec=vaapi --vo=gpu --gpu-api=opengl <10bit file>
#VO: [gpu] 1280x720 vaapi[p010] -> displayed directly, no conversion

The hang only happens with mpv's Vulkan/libplacebo interop. Looking at the exported descriptors (both planes share the same block-linear modifier), the difference is just the chroma layer's format: NV12 chroma is RG88 and imports fine, but P010 chroma is RG1616, and NVIDIA's Vulkan can't import an R16G16 dma-buf with the block-linear modifier (plain R16, as in the luma plane, is fine). When the Vulkan import fails, mpv falls back to inserting scale_vaapi (p010 -> nv12), and that VPP pass fails too because this driver's VideoProc only outputs ARGB, not NV12 β€” which is what surfaces as "error on filtering" and the stuck first frame.

So it's a Vulkan-interop limitation rather than something wrong in the driver: decode and export are fine (OpenGL interop and plain ffmpeg decode both work). Workaround for anyone hitting it is --gpu-api=opengl.

I also noticed the driver advertises VAEntrypointVideoProc but never fills in vtable_vpp, so vaQueryVideoProcPipelineCaps returns UNIMPLEMENTED. Wiring that up is harmless and helps clients that query VPP caps (e.g. ffmpeg's scale_vaapi), but it doesn't change the mpv Vulkan case (that dies later at the actual VPP render), so I've left it out of this PR β€” happy to send it separately if you think it's worth having.

@j4reporting

Copy link
Copy Markdown

works great on my tu106.

@AoTofu

AoTofu commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Great to hear, thanks for testing! Good to know it works on TU106 as well.

The direct backend called getenv("NVD_SINGLE_BUFFER") on every surface
allocation. Read it once in the constructor like every other NVD_* env
and expose it via nvdSingleBufferForced().
@elFarto

elFarto commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Thanks for this, it fixes it for me aswell.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants