feat(clients/windows): D3D11VA hardware decode in the session client — Vulkan chain becomes vulkan → d3d11va → software

The vendor-agnostic DXVA path for GPUs without Vulkan Video (Intel's Windows driver foremost,
which previously landed on CPU decode). Ported from the retired WinUI presenter's decoder with
its Intel-safe discipline intact (decode pool stays libavcodec-derived — a hand-built pool
broke Intel at the first SubmitDecoderBuffers), on a decode device LUID-matched to the
presenter's adapter.

Hand-off is a ring of shareable BGRA8 textures (SHARED_NTHANDLE | SHARED_KEYEDMUTEX) filled by
the fixed-function ID3D11VideoProcessor (NV12/P010 → BGRA8, colour spaces from the per-frame
CICP; PQ is tone-mapped to SDR by the processor — HDR-first boxes take Vulkan Video). BGRA is
deliberate: importing a multiplanar NV12 D3D11 texture device-losts on NVIDIA however it is
consumed (plane-view sampling and DMA copy both validation-clean, both TDR — bisected), while
single-plane RGBA D3D11↔Vulkan interop is the path Chromium/ANGLE exercise on every driver.
The presenter imports a slot's NT handle per frame (VK_KHR_external_memory_win32, gated on the
spec-required external-format probe) and blits it into the video image — no CSC pass; the DXGI
keyed mutex (key 0 both sides, drop-tolerant) is the cross-API lock and visibility barrier.

Verified live vs a real host at 5120x1440@240 HEVC on an RTX 4090: 240 fps, e2e 2.7/3.0 ms
p50/p95 under the Khronos validation layer — parity with Vulkan Video (2.6 ms); auto still
resolves vulkan on NVIDIA. PUNKTFUNK_DECODER=d3d11va forces it; import/present failures demote
to software on the existing streak contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 10:57:10 +02:00
parent d0fa8bd3ee
commit a69a83b545
10 changed files with 1231 additions and 16 deletions
Generated
+1
View File
@@ -2784,6 +2784,7 @@ dependencies = [
"tracing", "tracing",
"ureq", "ureq",
"wasapi", "wasapi",
"windows 0.62.2 (git+https://github.com/microsoft/windows-rs?rev=a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f)",
] ]
[[package]] [[package]]
+13
View File
@@ -44,3 +44,16 @@ sdl3 = { version = "0.18", features = ["hidapi"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
wasapi = "0.23" wasapi = "0.23"
sdl3 = { version = "0.18", features = ["hidapi", "build-from-source"] } sdl3 = { version = "0.18", features = ["hidapi", "build-from-source"] }
# D3D11VA decode (video_d3d11.rs): device/adapter selection, DXVA probes, and the shared
# NT-handle hand-off ring. Same pinned rev as clients/windows so the workspace builds ONE
# windows-rs.
windows = { git = "https://github.com/microsoft/windows-rs", rev = "a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f", features = [
"Win32_Foundation",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
# IDXGIResource1::CreateSharedHandle takes an optional SECURITY_ATTRIBUTES — the
# method itself is feature-gated behind this.
"Win32_Security",
] }
+2
View File
@@ -33,5 +33,7 @@ pub mod session;
pub mod trust; pub mod trust;
#[cfg(any(target_os = "linux", windows))] #[cfg(any(target_os = "linux", windows))]
pub mod video; pub mod video;
#[cfg(windows)]
pub mod video_d3d11;
pub mod wol; pub mod wol;
+4
View File
@@ -333,6 +333,8 @@ fn pump(
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
DecodedImage::Dmabuf(_) => "vaapi", DecodedImage::Dmabuf(_) => "vaapi",
DecodedImage::VkFrame(_) => "vulkan", DecodedImage::VkFrame(_) => "vulkan",
#[cfg(windows)]
DecodedImage::D3d11(_) => "d3d11va",
}; };
if total_frames == 1 { if total_frames == 1 {
let (w, h, path) = match &image { let (w, h, path) = match &image {
@@ -340,6 +342,8 @@ fn pump(
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
DecodedImage::Dmabuf(d) => (d.width, d.height, "vaapi-dmabuf"), DecodedImage::Dmabuf(d) => (d.width, d.height, "vaapi-dmabuf"),
DecodedImage::VkFrame(v) => (v.width, v.height, "vulkan-video"), DecodedImage::VkFrame(v) => (v.width, v.height, "vulkan-video"),
#[cfg(windows)]
DecodedImage::D3d11(d) => (d.width, d.height, "d3d11va"),
}; };
tracing::info!(width = w, height = h, path, "first frame decoded"); tracing::info!(width = w, height = h, path, "first frame decoded");
} }
+68 -7
View File
@@ -19,9 +19,10 @@
//! Both run `AV_CODEC_FLAG_LOW_DELAY`; the host encodes zero-reorder streams (no //! Both run `AV_CODEC_FLAG_LOW_DELAY`; the host encodes zero-reorder streams (no
//! B-frames, in-band parameter sets on every IDR), so decode is strictly one-in/one-out. //! B-frames, in-band parameter sets on every IDR), so decode is strictly one-in/one-out.
//! //!
//! On Windows the VAAPI/dmabuf backend does not exist (DRM-PRIME is a Linux concept), so //! On Windows the VAAPI/dmabuf backend does not exist (DRM-PRIME is a Linux concept); the
//! the chain is Vulkan → software — Intel (no Vulkan Video in its Windows driver) lands //! chain there is Vulkan → **D3D11VA** (`crate::video_d3d11` — the vendor-agnostic DXVA
//! on software. Everything dmabuf-shaped is `cfg(target_os = "linux")`-gated inline. //! path, which is how Intel's Windows driver gets hardware decode without Vulkan Video)
//! → software. Everything dmabuf-shaped is `cfg(target_os = "linux")`-gated inline.
// bindgen's C-enum repr is target-dependent (u32 on Linux/clang, i32 on MSVC), so the // bindgen's C-enum repr is target-dependent (u32 on Linux/clang, i32 on MSVC), so the
// pf-ffvk Vulkan flag/enum casts below are required on one platform and no-ops on the // pf-ffvk Vulkan flag/enum casts below are required on one platform and no-ops on the
@@ -50,12 +51,21 @@ pub struct DecodedFrame {
pub image: DecodedImage, pub image: DecodedImage,
} }
/// Re-exported so consumers (the presenter) name every frame type through `video::`.
#[cfg(windows)]
pub use crate::video_d3d11::D3d11Frame;
pub enum DecodedImage { pub enum DecodedImage {
Cpu(CpuFrame), Cpu(CpuFrame),
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
Dmabuf(DmabufFrame), Dmabuf(DmabufFrame),
/// FFmpeg Vulkan Video output: a VkImage already on the PRESENTER's device. /// FFmpeg Vulkan Video output: a VkImage already on the PRESENTER's device.
VkFrame(VkVideoFrame), VkFrame(VkVideoFrame),
/// D3D11VA output copied into a shareable NT-handle texture the presenter imports
/// (`VK_KHR_external_memory_win32`) — the DXVA path for GPUs without Vulkan Video
/// (Intel's Windows driver foremost). See `crate::video_d3d11`.
#[cfg(windows)]
D3d11(crate::video_d3d11::D3d11Frame),
} }
/// One Vulkan-decoded frame. The image lives on the presenter's own VkDevice (the /// One Vulkan-decoded frame. The image lives on the presenter's own VkDevice (the
@@ -113,7 +123,7 @@ impl ColorDesc {
/// ///
/// # Safety /// # Safety
/// `frame` must point to a valid `AVFrame` (alive for the duration of the call). /// `frame` must point to a valid `AVFrame` (alive for the duration of the call).
unsafe fn from_raw(frame: *const ffmpeg::ffi::AVFrame) -> ColorDesc { pub(crate) unsafe fn from_raw(frame: *const ffmpeg::ffi::AVFrame) -> ColorDesc {
// SAFETY: caller guarantees a live AVFrame; these are plain enum field reads. // SAFETY: caller guarantees a live AVFrame; these are plain enum field reads.
unsafe { unsafe {
ColorDesc { ColorDesc {
@@ -185,6 +195,8 @@ enum Backend {
Vulkan(VulkanDecoder), Vulkan(VulkanDecoder),
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
Vaapi(VaapiDecoder), Vaapi(VaapiDecoder),
#[cfg(windows)]
D3d11va(crate::video_d3d11::D3d11vaDecoder),
Software(SoftwareDecoder), Software(SoftwareDecoder),
} }
@@ -254,8 +266,8 @@ fn quiet_ffmpeg_log() {
impl Decoder { impl Decoder {
/// `codec_id` is the codec the host resolved in the Welcome (never assume HEVC). /// `codec_id` is the codec the host resolved in the Welcome (never assume HEVC).
/// `pref` is the Settings "Video decoder" value (`auto`/`vulkan`/`vaapi`/`software`; /// `pref` is the Settings "Video decoder" value (`auto`/`vulkan`/`vaapi`/`d3d11va`/
/// `hardware` — the WinUI shell's stored value from its D3D11VA era — reads as auto). /// `software`; `hardware` — the WinUI shell's stored value — reads as auto).
/// `vk` is the presenter's shared Vulkan device when its stack can run FFmpeg's /// `vk` is the presenter's shared Vulkan device when its stack can run FFmpeg's
/// Vulkan Video decoder — decode lands as VkImages the presenter samples directly. /// Vulkan Video decoder — decode lands as VkImages the presenter samples directly.
/// Precedence: the `PUNKTFUNK_DECODER` env override wins (support/debug escape /// Precedence: the `PUNKTFUNK_DECODER` env override wins (support/debug escape
@@ -281,7 +293,10 @@ impl Decoder {
}) })
}; };
if matches!(choice.as_str(), "auto" | "" | "vulkan" | "hardware") { if matches!(choice.as_str(), "auto" | "" | "vulkan" | "hardware") {
match vk { // `video_decode` gates the Vulkan Video attempt: the presenter now exports its
// handle bundle even when the device has no decode queue (Windows D3D11 interop
// rides the same struct), so presence alone no longer implies a usable decoder.
match vk.filter(|v| v.video_decode) {
Some(vk) => match VulkanDecoder::new(codec_id, vk) { Some(vk) => match VulkanDecoder::new(codec_id, vk) {
Ok(v) => { Ok(v) => {
tracing::info!( tracing::info!(
@@ -326,6 +341,37 @@ impl Decoder {
} }
} }
} }
// Windows: D3D11VA is the vendor-agnostic DXVA fallback when Vulkan Video isn't
// available (Intel's Windows driver foremost) — gated on the presenter having the
// win32 external-memory import path, else its frames could never reach the screen.
#[cfg(windows)]
if choice != "software" && choice != "vulkan" {
match vk.filter(|v| v.d3d11_import) {
Some(v) => {
match crate::video_d3d11::D3d11vaDecoder::new(codec_id, v.adapter_luid) {
Ok(d) => {
tracing::info!(
?codec_id,
"D3D11VA hardware decode active (shared-texture hand-off)"
);
return done(Backend::D3d11va(d));
}
Err(e) => {
if choice == "d3d11va" {
return Err(e.context("PUNKTFUNK_DECODER=d3d11va but it failed"));
}
tracing::info!(reason = %format!("{e:#}"),
"D3D11VA unavailable — software decode");
}
}
}
None if choice == "d3d11va" => bail!(
"PUNKTFUNK_DECODER=d3d11va but the presenter's device lacks the win32 \
external-memory import extensions — see the presenter log"
),
None => {}
}
}
if choice == "software" { if choice == "software" {
// Say WHY hardware wasn't even attempted — a stored "software" preference // Say WHY hardware wasn't even attempted — a stored "software" preference
// (or the env override) silently skipping vulkan/vaapi has burned real // (or the env override) silently skipping vulkan/vaapi has burned real
@@ -380,6 +426,8 @@ impl Decoder {
Backend::Vulkan(v) => v.decode(au).map(|f| f.map(DecodedImage::VkFrame)), Backend::Vulkan(v) => v.decode(au).map(|f| f.map(DecodedImage::VkFrame)),
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
Backend::Vaapi(v) => v.decode(au).map(|f| f.map(DecodedImage::Dmabuf)), Backend::Vaapi(v) => v.decode(au).map(|f| f.map(DecodedImage::Dmabuf)),
#[cfg(windows)]
Backend::D3d11va(d) => d.decode(au).map(|f| f.map(DecodedImage::D3d11)),
Backend::Software(s) => return Ok(s.decode(au)?.map(DecodedImage::Cpu)), Backend::Software(s) => return Ok(s.decode(au)?.map(DecodedImage::Cpu)),
}; };
match result { match result {
@@ -390,6 +438,8 @@ impl Decoder {
Err(e) => { Err(e) => {
let which = match self.backend { let which = match self.backend {
Backend::Vulkan(_) => "Vulkan Video", Backend::Vulkan(_) => "Vulkan Video",
#[cfg(windows)]
Backend::D3d11va(_) => "D3D11VA",
_ => "VAAPI", _ => "VAAPI",
}; };
self.vaapi_fails += 1; self.vaapi_fails += 1;
@@ -778,6 +828,17 @@ pub struct VulkanDecodeDevice {
pub f_sampler_ycbcr: bool, pub f_sampler_ycbcr: bool,
pub f_timeline_semaphore: bool, pub f_timeline_semaphore: bool,
pub f_synchronization2: bool, pub f_synchronization2: bool,
/// Vulkan Video decode is actually usable on this device (decode queue + extensions +
/// features). The bundle now exists even without it — Windows D3D11 interop rides the
/// same struct — so consumers gate the FFmpeg-Vulkan decoder on THIS, not on `Some`.
pub video_decode: bool,
/// The presenter enabled `VK_KHR_external_memory_win32` + `VK_KHR_win32_keyed_mutex`:
/// D3D11 shared-texture frames can reach the screen. Always `false` off Windows.
pub d3d11_import: bool,
/// `VkPhysicalDeviceIDProperties::deviceLUID` when the driver reports one — the D3D11VA
/// backend creates its decode device on the SAME adapter so shared textures never cross
/// GPUs. `None` when not reported (or off Windows, where it's unused).
pub adapter_luid: Option<[u8; 8]>,
} }
/// `fourcc(a,b,c,d)` — the DRM FourCC packing (little-endian, `a | b<<8 | c<<16 | d<<24`). /// `fourcc(a,b,c,d)` — the DRM FourCC packing (little-endian, `a | b<<8 | c<<16 | d<<24`).
+698
View File
@@ -0,0 +1,698 @@
//! D3D11VA hardware decode (Windows) for the Vulkan presenter — the vendor-agnostic DXVA
//! path that covers what Vulkan Video can't (Intel's Windows driver foremost, which has no
//! video-decode queue and previously landed on CPU decode).
//!
//! Ported from the retired in-process WinUI presenter's decoder (`clients/windows/src/video.rs`)
//! with one structural change: that presenter sampled D3D11 textures directly, while ours draws
//! with Vulkan. Bridging rules, all learned the hard way there:
//!
//! * The **decode pool stays libavcodec-derived** (`get_format` sets no frames context): a
//! hand-built pool validated on NVIDIA was rejected by Intel at the first
//! `SubmitDecoderBuffers` — and Intel is the GPU this backend exists for. That also means the
//! decode surfaces carry no share flags, so they can't be imported into Vulkan directly.
//! * Each decoded slice goes through the fixed-function **`ID3D11VideoProcessor`**
//! (`VideoProcessorBlt`, NV12/P010 → BGRA8 — the conversion every Windows video player
//! exercises on every vendor) into a small ring of **shareable RGBA textures** created with
//! `SHARED_NTHANDLE | SHARED_KEYEDMUTEX`. Single-plane RGBA is deliberate: the presenter's
//! Vulkan import of a *multiplanar* NV12 D3D11 texture device-losts on NVIDIA no matter how
//! it's consumed (plane-view sampling, DMA copy — all validation-clean, all TDR; bisected
//! 2026-07-09), while RGBA D3D11↔Vulkan interop is the path Chromium/ANGLE ship everywhere.
//! The presenter imports a ring slot's NT handle per frame (`pf-presenter/src/d3d11.rs`,
//! `VK_KHR_external_memory_win32`) and blits it straight into its video image — the frames
//! arrive as ready sRGB, no CSC pass.
//! * Cross-API exclusion + write→read visibility ride the slot's keyed mutex
//! (`VK_KHR_win32_keyed_mutex`); both sides take and release it with **key 0**: a frame the
//! presenter drops (arrival-paced, newest wins) is simply never acquired, which a
//! key-ping-pong protocol would deadlock on.
//! * An HDR (PQ/BT.2020) stream is tone-mapped to SDR by the video processor (input colour
//! space `G2084_P2020`, output sRGB): correct picture, no HDR presentation on this backend —
//! its targets (Intel iGPU laptops) are SDR panels; HDR-first boxes take Vulkan Video.
//!
//! The decode device is created on the **presenter's adapter** (matched by the Vulkan device's
//! LUID) so the shared textures never cross GPUs on a multi-adapter box.
use crate::video::ColorDesc;
use anyhow::{anyhow, bail, Context as _, Result};
use ffmpeg_next as ffmpeg;
use std::ffi::c_void;
use std::ptr;
use windows::core::{Interface, GUID};
use windows::Win32::Foundation::HANDLE;
use windows::Win32::Graphics::Direct3D::{D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_11_1};
use windows::Win32::Graphics::Direct3D11::{
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Multithread, ID3D11Texture2D,
ID3D11VideoContext1, ID3D11VideoDevice, ID3D11VideoProcessor,
ID3D11VideoProcessorEnumerator, ID3D11VideoProcessorOutputView, D3D11_BIND_RENDER_TARGET,
D3D11_BIND_SHADER_RESOURCE, D3D11_CREATE_DEVICE_BGRA_SUPPORT,
D3D11_CREATE_DEVICE_VIDEO_SUPPORT, D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX,
D3D11_RESOURCE_MISC_SHARED_NTHANDLE, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC,
D3D11_USAGE_DEFAULT, D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE,
D3D11_VIDEO_PROCESSOR_CONTENT_DESC, D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC,
D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC, D3D11_VIDEO_PROCESSOR_STREAM,
D3D11_VIDEO_USAGE_PLAYBACK_NORMAL, D3D11_VPIV_DIMENSION_TEXTURE2D,
D3D11_VPOV_DIMENSION_TEXTURE2D,
};
use windows::Win32::Graphics::Dxgi::Common::{
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709, DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P709,
DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_LEFT_P2020, DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P2020,
DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709, DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM,
DXGI_FORMAT_NV12, DXGI_FORMAT_P010, DXGI_RATIONAL, DXGI_SAMPLE_DESC,
};
use windows::Win32::Graphics::Dxgi::{
CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1, IDXGIKeyedMutex, IDXGIResource1,
DXGI_ADAPTER_FLAG_SOFTWARE, DXGI_SHARED_RESOURCE_READ, DXGI_SHARED_RESOURCE_WRITE,
};
/// Ring of shareable hand-off textures. Bounds how many decoded-but-unpresented frames can
/// exist without a slot being rewritten under an in-flight older frame: the pump's decoded
/// channel holds 2 and the presenter drains to newest with one frame in flight, so 3 are ever
/// outstanding — 6 leaves margin without meaningful VRAM cost.
const RING_SLOTS: usize = 6;
/// Keyed-mutex acquire budget (ms) on the DECODE side. The presenter holds a slot only for one
/// submit's GPU lifetime; multiple seconds means the render thread died — surface an error
/// (which demotes to software) instead of wedging the decode loop.
const ACQUIRE_TIMEOUT_MS: u32 = 2000;
/// Probe pool size — mirrors what libavcodec sizes for a worst-case DPB (legacy value).
const DECODE_POOL_SIZE: i32 = 12;
/// `D3D11_BIND_DECODER` — the decode pool's ONLY bind flag (see `get_format_d3d11`).
const BIND_DECODER: u32 = 0x200;
// DXVA decode-profile GUIDs (`dxva.h`), defined locally so no extra windows-rs feature or
// metadata surface is pulled in for four constants.
const PROFILE_H264_VLD_NOFGT: GUID = GUID::from_u128(0x1b81be68_a0c7_11d3_b984_00c04f2e73c5);
const PROFILE_HEVC_VLD_MAIN: GUID = GUID::from_u128(0x5b11d51b_2f4c_4452_bcc3_09f2a1160cc0);
const PROFILE_HEVC_VLD_MAIN10: GUID = GUID::from_u128(0x107af0e0_ef1a_4d19_aba8_67a163073d13);
const PROFILE_AV1_VLD_PROFILE0: GUID = GUID::from_u128(0xb8be4ccb_cf53_46ba_8d59_d6b8a6da5d2a);
/// One decoded frame, parked in a ring slot the presenter imports by NT handle. Plain POD —
/// the ring (and its handles) belong to the decoder and outlive every in-flight frame; the
/// presenter must NOT close the handle. Cross-API exclusion + visibility ride the slot's
/// keyed mutex (key 0 on both sides), not this struct.
pub struct D3d11Frame {
pub width: u32,
pub height: u32,
/// What the ring slot actually CONTAINS after the video processor's conversion: sRGB
/// BT.709 full-range RGB — regardless of the stream's own CICP (a PQ stream was
/// tone-mapped). The presenter keys SDR/HDR handling off this, so it always reads SDR.
pub color: ColorDesc,
/// The ring slot's NT shared handle (`IDXGIResource1::CreateSharedHandle`), stable for the
/// ring's lifetime. Raw `isize` so the frame crosses the pump→presenter channel.
pub handle: isize,
/// Ring generation — bumped when the ring is rebuilt (stream size change), so a
/// presenter-side import cache could never alias a stale handle. Informational today
/// (the presenter imports per frame).
pub generation: u32,
}
// --- FFmpeg hwcontext_d3d11va ABI (repr(C) mirrors, same as the legacy decoder) --------------
/// `hwcontext_d3d11va.h` — `AVHWDeviceContext::hwctx` for D3D11VA. FFmpeg installs the
/// `ID3D11Multithread` default lock + multithread protection during init, which is what lets
/// the presenter-side device share textures with the decode thread safely.
#[repr(C)]
struct AVD3D11VADeviceContext {
device: *mut c_void, // ID3D11Device*
device_context: *mut c_void, // ID3D11DeviceContext*
video_device: *mut c_void, // ID3D11VideoDevice*
video_context: *mut c_void, // ID3D11VideoContext*
lock: *mut c_void, // void (*)(void*)
unlock: *mut c_void, // void (*)(void*)
lock_ctx: *mut c_void,
}
/// `hwcontext_d3d11va.h` — `AVHWFramesContext::hwctx`. A user-built frames context gets NO
/// default bind flags (BindFlags 0 → `CreateTexture2D` E_INVALIDARG); only the probe below
/// builds one, and it sets `BIND_DECODER` exactly like libavcodec's own path.
#[repr(C)]
struct AVD3D11VAFramesContext {
texture: *mut c_void, // ID3D11Texture2D* (null → FFmpeg allocates the pool)
bind_flags: u32, // UINT BindFlags
misc_flags: u32, // UINT MiscFlags
texture_infos: *mut c_void, // AVD3D11FrameDescriptor* (FFmpeg-managed)
}
fn averr(what: &str, code: i32) -> anyhow::Error {
anyhow!("{what}: {}", ffmpeg::Error::from(code))
}
/// libavcodec's `get_format` callback: pick the D3D11 hw surface format and nothing else.
/// Deliberately does NOT build a frames context — with `hw_device_ctx` set and `hw_frames_ctx`
/// left null, libavcodec derives the decode pool itself (`ff_decode_get_hw_frames_ctx`),
/// applying every vendor quirk: DXVA surface alignment (128 for HEVC/AV1), DPB-based pool
/// sizing, and the decoder-only `D3D11_BIND_DECODER` flags. A hand-built context validated on
/// NVIDIA was rejected by Intel at the first `SubmitDecoderBuffers` (E_INVALIDARG) — the
/// vendor-proof path is the one the ffmpeg CLI/mpv ship.
unsafe extern "C" fn get_format_d3d11(
avctx: *mut ffmpeg::ffi::AVCodecContext,
mut list: *const ffmpeg::ffi::AVPixelFormat,
) -> ffmpeg::ffi::AVPixelFormat {
use ffmpeg::ffi::*;
unsafe {
if (*avctx).hw_device_ctx.is_null() {
return AVPixelFormat::AV_PIX_FMT_NONE;
}
while *list != AVPixelFormat::AV_PIX_FMT_NONE {
if *list == AVPixelFormat::AV_PIX_FMT_D3D11 {
return AVPixelFormat::AV_PIX_FMT_D3D11;
}
list = list.add(1);
}
AVPixelFormat::AV_PIX_FMT_NONE
}
}
/// Does the adapter expose a DXVA decode profile for `codec_id`? Checked before building the
/// FFmpeg hwdevice because hwaccel selection (`get_format`) only runs on the FIRST access
/// unit — an unsupported profile would otherwise burn the opening IDR and recover through the
/// mid-stream demotion path instead of committing to software up front.
fn decode_profile_supported(device: &ID3D11Device, codec_id: ffmpeg::codec::Id) -> Result<()> {
let video: ID3D11VideoDevice = device
.cast()
.context("device lacks ID3D11VideoDevice (created without VIDEO_SUPPORT)")?;
let profiles: Vec<GUID> = unsafe {
let n = video.GetVideoDecoderProfileCount();
(0..n)
.filter_map(|i| video.GetVideoDecoderProfile(i).ok())
.collect()
};
let (wanted, format, name): (GUID, DXGI_FORMAT, &str) = match codec_id {
ffmpeg::codec::Id::H264 => (PROFILE_H264_VLD_NOFGT, DXGI_FORMAT_NV12, "H.264 VLD NoFGT"),
ffmpeg::codec::Id::HEVC => (PROFILE_HEVC_VLD_MAIN, DXGI_FORMAT_NV12, "HEVC Main"),
ffmpeg::codec::Id::AV1 => (PROFILE_AV1_VLD_PROFILE0, DXGI_FORMAT_NV12, "AV1 Profile 0"),
other => bail!("no DXVA profile known for {other:?}"),
};
let ok = profiles.contains(&wanted)
&& unsafe { video.CheckVideoDecoderFormat(&wanted, format) }
.map(|b| b.as_bool())
.unwrap_or(false);
if !ok {
bail!("adapter exposes no {name} decode profile");
}
// 10-bit (a mid-session HDR upgrade needs Main10): informational — if it's missing, the
// decode error → software demotion + keyframe re-request path covers the switch.
if codec_id == ffmpeg::codec::Id::HEVC {
let main10 = profiles.contains(&PROFILE_HEVC_VLD_MAIN10)
&& unsafe { video.CheckVideoDecoderFormat(&PROFILE_HEVC_VLD_MAIN10, DXGI_FORMAT_P010) }
.map(|b| b.as_bool())
.unwrap_or(false);
tracing::info!(main10, "HEVC Main10 (10-bit/HDR) decode profile");
}
Ok(())
}
/// Predict whether D3D11VA decode will work by doing EXACTLY what the decoder's `get_format`
/// leads to — allocate an `AVHWFramesContext` (decoder-only pool) and initialize it, which
/// creates the real NV12 decode surface array. On a GPU/driver that can't create the pool this
/// fails here, up front, so the session commits to software from the first frame (a clean,
/// gap-free stream) instead of dying mid-stream on the opening IDR.
unsafe fn d3d11va_decode_supported(hw_device: *mut ffmpeg::ffi::AVBufferRef) -> bool {
use ffmpeg::ffi::*;
unsafe {
let frames_ref = av_hwframe_ctx_alloc(hw_device);
if frames_ref.is_null() {
return false;
}
let frames = (*frames_ref).data as *mut AVHWFramesContext;
(*frames).format = AVPixelFormat::AV_PIX_FMT_D3D11;
(*frames).sw_format = AVPixelFormat::AV_PIX_FMT_NV12;
(*frames).width = 1920;
(*frames).height = 1152; // 128-aligned 1080p surface (the HEVC DXVA alignment)
(*frames).initial_pool_size = DECODE_POOL_SIZE;
let fhw = (*frames).hwctx as *mut AVD3D11VAFramesContext;
(*fhw).bind_flags = BIND_DECODER;
let r = av_hwframe_ctx_init(frames_ref);
let mut fr = frames_ref;
av_buffer_unref(&mut fr);
r >= 0
}
}
/// Create the decode device on the presenter's adapter. `luid` is the Vulkan device's
/// `VkPhysicalDeviceIDProperties::deviceLUID` (little-endian LowPart‖HighPart) — matching it
/// keeps the shared textures on one GPU. `None`/no match falls back to the first hardware
/// adapter (single-GPU boxes; a WARP-only box fails out to software decode).
fn create_device(luid: Option<[u8; 8]>) -> Result<(ID3D11Device, ID3D11DeviceContext)> {
let factory: IDXGIFactory1 = unsafe { CreateDXGIFactory1() }.context("CreateDXGIFactory1")?;
let mut chosen: Option<IDXGIAdapter1> = None;
let mut fallback: Option<IDXGIAdapter1> = None;
for i in 0.. {
let Ok(adapter) = (unsafe { factory.EnumAdapters1(i) }) else {
break;
};
let Ok(desc) = (unsafe { adapter.GetDesc1() }) else {
continue;
};
if desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE.0 as u32 != 0 {
continue; // WARP can't hardware-decode; software decode covers that box anyway
}
if fallback.is_none() {
fallback = Some(adapter.clone());
}
if let Some(want) = luid {
let mut have = [0u8; 8];
have[..4].copy_from_slice(&desc.AdapterLuid.LowPart.to_le_bytes());
have[4..].copy_from_slice(&desc.AdapterLuid.HighPart.to_le_bytes());
if have == want {
chosen = Some(adapter);
break;
}
}
}
if chosen.is_none() && luid.is_some() && fallback.is_some() {
tracing::warn!("no DXGI adapter matches the Vulkan device LUID — using the first hardware adapter");
}
let adapter = chosen
.or(fallback)
.ok_or_else(|| anyhow!("no hardware DXGI adapter"))?;
let mut device = None;
let mut context = None;
unsafe {
D3D11CreateDevice(
&adapter,
windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_UNKNOWN,
None,
D3D11_CREATE_DEVICE_VIDEO_SUPPORT | D3D11_CREATE_DEVICE_BGRA_SUPPORT,
Some(&[D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0]),
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut context),
)
}
.context("D3D11CreateDevice")?;
let device = device.ok_or_else(|| anyhow!("D3D11CreateDevice returned no device"))?;
let context = context.ok_or_else(|| anyhow!("D3D11CreateDevice returned no context"))?;
// The decode (FFmpeg video context) and our copy (immediate context) run on the decode
// thread, but FFmpeg's own workers touch the device too — same protection the legacy
// shared device enabled (FFmpeg would install it during hwdevice init anyway; explicit
// keeps the invariant obvious).
if let Ok(mt) = device.cast::<ID3D11Multithread>() {
// Returns the PREVIOUS protection state — nothing to act on.
let _ = unsafe { mt.SetMultithreadProtected(true) };
}
Ok((device, context))
}
/// One shareable ring slot: the NV12/P010 texture, its keyed mutex, and the NT handle the
/// presenter imports. Handle closed on drop (the presenter never owns it).
struct Slot {
/// The shared texture itself — everything below views into it; kept for its lifetime.
_tex: ID3D11Texture2D,
mutex: IDXGIKeyedMutex,
handle: HANDLE,
/// The video processor's render target over the texture — `VideoProcessorBlt`'s target.
out_view: ID3D11VideoProcessorOutputView,
}
impl Drop for Slot {
fn drop(&mut self) {
unsafe {
let _ = windows::Win32::Foundation::CloseHandle(self.handle);
}
}
}
/// The hand-off ring + the video processor that fills it (both sized to the stream, so a
/// mid-stream `Reconfigure` rebuilds the whole bundle). See the module docs.
struct SharedRing {
slots: Vec<Slot>,
vp: ID3D11VideoProcessor,
enumerator: ID3D11VideoProcessorEnumerator,
width: u32,
height: u32,
next: usize,
generation: u32,
}
impl SharedRing {
fn build(
device: &ID3D11Device,
video_device: &ID3D11VideoDevice,
width: u32,
height: u32,
generation: u32,
) -> Result<SharedRing> {
// The video processor: NV12/P010 in, BGRA8 out, 1:1 (no scaling — the Vulkan side
// scales at composite time like every other path). Frame rates are advisory.
let content = D3D11_VIDEO_PROCESSOR_CONTENT_DESC {
InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE,
InputFrameRate: DXGI_RATIONAL {
Numerator: 60,
Denominator: 1,
},
InputWidth: width,
InputHeight: height,
OutputFrameRate: DXGI_RATIONAL {
Numerator: 60,
Denominator: 1,
},
OutputWidth: width,
OutputHeight: height,
Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL,
};
let enumerator = unsafe { video_device.CreateVideoProcessorEnumerator(&content) }
.context("CreateVideoProcessorEnumerator")?;
let vp = unsafe { video_device.CreateVideoProcessor(&enumerator, 0) }
.context("CreateVideoProcessor")?;
let desc = D3D11_TEXTURE2D_DESC {
Width: width,
Height: height,
MipLevels: 1,
ArraySize: 1,
// Single-plane BGRA8: the ONLY hand-off format whose Vulkan import is a
// universally exercised driver path (see the module docs — NV12 import TDRs
// on NVIDIA despite being advertised).
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
// RENDER_TARGET: the video processor's output view renders into it.
BindFlags: (D3D11_BIND_SHADER_RESOURCE.0 | D3D11_BIND_RENDER_TARGET.0) as u32,
CPUAccessFlags: 0,
MiscFlags: (D3D11_RESOURCE_MISC_SHARED_NTHANDLE.0
| D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX.0) as u32,
};
let mut slots = Vec::with_capacity(RING_SLOTS);
for _ in 0..RING_SLOTS {
let mut tex = None;
unsafe { device.CreateTexture2D(&desc, None, Some(&mut tex)) }
.context("create shared hand-off texture")?;
let tex: ID3D11Texture2D = tex.expect("CreateTexture2D succeeded");
let mutex: IDXGIKeyedMutex = tex.cast().context("shared texture lacks IDXGIKeyedMutex")?;
let resource: IDXGIResource1 = tex.cast().context("shared texture lacks IDXGIResource1")?;
let handle = unsafe {
resource.CreateSharedHandle(
None,
(DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE).0,
None,
)
}
.context("CreateSharedHandle")?;
let mut ov_desc = D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC::default();
ov_desc.ViewDimension = D3D11_VPOV_DIMENSION_TEXTURE2D;
ov_desc.Anonymous.Texture2D.MipSlice = 0;
let mut out_view = None;
unsafe {
video_device.CreateVideoProcessorOutputView(
&tex,
&enumerator,
&ov_desc,
Some(&mut out_view),
)
}
.context("CreateVideoProcessorOutputView")?;
let out_view = out_view.expect("output view created");
slots.push(Slot {
_tex: tex,
mutex,
handle,
out_view,
});
}
tracing::info!(width, height, slots = RING_SLOTS, generation,
"D3D11 shared hand-off ring built (VideoProcessor → BGRA8)");
Ok(SharedRing {
slots,
vp,
enumerator,
width,
height,
next: 0,
generation,
})
}
}
pub(crate) struct D3d11vaDecoder {
ctx: *mut ffmpeg::ffi::AVCodecContext,
hw_device: *mut ffmpeg::ffi::AVBufferRef,
packet: *mut ffmpeg::ffi::AVPacket,
frame: *mut ffmpeg::ffi::AVFrame,
device: ID3D11Device,
context: ID3D11DeviceContext,
/// Creates the per-ring video processor + views.
video_device: ID3D11VideoDevice,
/// Runs the per-frame `VideoProcessorBlt`; the `1` interface for the DXGI colour-space
/// setters (Win10 1703+, universally present — init fails to software without it).
video_context1: ID3D11VideoContext1,
ring: Option<SharedRing>,
}
// Single-owner pointers + COM interfaces, only touched from the session pump thread (the
// decode loop); the presenter reaches the shared textures exclusively via their NT handles.
unsafe impl Send for D3d11vaDecoder {}
impl D3d11vaDecoder {
pub(crate) fn new(codec_id: ffmpeg::codec::Id, luid: Option<[u8; 8]>) -> Result<D3d11vaDecoder> {
use ffmpeg::ffi;
let (device, context) = create_device(luid)?;
// The adapter must expose the codec's DXVA profile — checked here, not at the first AU.
decode_profile_supported(&device, codec_id)?;
// The hand-off converter's interfaces, up front (their absence must route to software
// decode NOW, not burn the opening IDR).
let video_device: ID3D11VideoDevice = device
.cast()
.context("device lacks ID3D11VideoDevice (created without VIDEO_SUPPORT)")?;
let video_context1: ID3D11VideoContext1 = context
.cast()
.context("context lacks ID3D11VideoContext1 (pre-1703 Windows?)")?;
unsafe {
let hw_device =
ffi::av_hwdevice_ctx_alloc(ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_D3D11VA);
if hw_device.is_null() {
bail!("av_hwdevice_ctx_alloc(D3D11VA) failed");
}
let devctx = (*hw_device).data as *mut ffi::AVHWDeviceContext;
let d3dctx = (*devctx).hwctx as *mut AVD3D11VADeviceContext;
// Hand FFmpeg an owned ref to the device + immediate context (it Releases them when
// the hwdevice ctx is freed). `into_raw()` transfers a +1 ref without releasing.
(*d3dctx).device = device.clone().into_raw();
(*d3dctx).device_context = context.clone().into_raw();
// lock left null → FFmpeg installs the ID3D11Multithread default lock in init.
let r = ffi::av_hwdevice_ctx_init(hw_device);
if r < 0 {
let mut hw = hw_device;
ffi::av_buffer_unref(&mut hw);
bail!("av_hwdevice_ctx_init: {}", ffmpeg::Error::from(r));
}
// Up-front viability probe (see `d3d11va_decode_supported`).
if !d3d11va_decode_supported(hw_device) {
let mut hw = hw_device;
ffi::av_buffer_unref(&mut hw);
bail!("GPU can't create the D3D11VA decode surface pool");
}
let codec = ffi::avcodec_find_decoder(codec_id.into());
if codec.is_null() {
let mut hw = hw_device;
ffi::av_buffer_unref(&mut hw);
bail!("no {codec_id:?} decoder");
}
let ctx = ffi::avcodec_alloc_context3(codec);
(*ctx).hw_device_ctx = ffi::av_buffer_ref(hw_device);
(*ctx).get_format = Some(get_format_d3d11);
(*ctx).flags |= ffi::AV_CODEC_FLAG_LOW_DELAY as i32;
(*ctx).thread_count = 1; // hwaccel: threads only add latency
// On top of the DPB-based pool libavcodec sizes: margin for the frames briefly held
// between decode and the ring copy (the copy runs immediately, so this is small).
(*ctx).extra_hw_frames = 4;
let r = ffi::avcodec_open2(ctx, codec, ptr::null_mut());
if r < 0 {
let mut ctx = ctx;
ffi::avcodec_free_context(&mut ctx);
let mut hw = hw_device;
ffi::av_buffer_unref(&mut hw);
bail!("avcodec_open2 (D3D11VA): {}", ffmpeg::Error::from(r));
}
Ok(D3d11vaDecoder {
ctx,
hw_device,
packet: ffi::av_packet_alloc(),
frame: ffi::av_frame_alloc(),
device,
context,
video_device,
video_context1,
ring: None,
})
}
}
pub(crate) fn decode(&mut self, au: &[u8]) -> Result<Option<D3d11Frame>> {
use ffmpeg::ffi;
unsafe {
let r = ffi::av_new_packet(self.packet, au.len() as i32);
if r < 0 {
return Err(averr("av_new_packet", r));
}
ptr::copy_nonoverlapping(au.as_ptr(), (*self.packet).data, au.len());
let r = ffi::avcodec_send_packet(self.ctx, self.packet);
ffi::av_packet_unref(self.packet);
if r < 0 {
return Err(averr("send_packet", r));
}
let mut out = None;
loop {
let r = ffi::avcodec_receive_frame(self.ctx, self.frame);
if r == ffmpeg::ffi::AVERROR(ffmpeg::ffi::EAGAIN) {
break;
}
if r < 0 {
return Err(averr("receive_frame", r));
}
let lifted = self.lift();
// The decode surface goes back to the pool NOW — the ring copy (queued ahead
// of any later decoder write on the same immediate context) already owns the
// pixels. No cross-thread AVFrame guard exists in this backend at all.
ffi::av_frame_unref(self.frame);
out = Some(lifted?); // newest wins (one-in/one-out streams make this moot)
}
Ok(out)
}
}
/// Convert the decoded slice into the next ring slot (`VideoProcessorBlt`, NV12/P010 →
/// BGRA8) under its keyed mutex and describe the hand-off. The mutex acquire also
/// back-pressures against the presenter still reading this slot (only possible if the
/// stream runs `RING_SLOTS` ahead of present).
unsafe fn lift(&mut self) -> Result<D3d11Frame> {
use ffmpeg::ffi;
unsafe {
if (*self.frame).format != ffi::AVPixelFormat::AV_PIX_FMT_D3D11 as i32 {
bail!("decoder returned a software frame (no D3D11 surface)");
}
let width = (*self.frame).width as u32;
let height = (*self.frame).height as u32;
let color = ColorDesc::from_raw(self.frame);
// AddRef'd locals so the mutable `ring` borrow below doesn't lock all of `self`.
let video_device = self.video_device.clone();
let video_context1 = self.video_context1.clone();
let context = self.context.clone();
// (Re)build the ring + video processor on first use or a stream size change (the
// hand-off is BGRA8 regardless of the stream's bit depth, so depth never rebuilds).
let rebuild = self
.ring
.as_ref()
.is_none_or(|r| r.width != width || r.height != height);
if rebuild {
let generation = self.ring.as_ref().map_or(0, |r| r.generation + 1);
self.ring = Some(SharedRing::build(
&self.device,
&video_device,
width,
height,
generation,
)?);
}
let ring = self.ring.as_mut().expect("ring built above");
let slot_idx = ring.next;
ring.next = (ring.next + 1) % ring.slots.len();
let slot = &ring.slots[slot_idx];
let raw = (*self.frame).data[0] as *mut c_void;
let src: ID3D11Texture2D = ID3D11Texture2D::from_raw_borrowed(&raw)
.ok_or_else(|| anyhow!("null D3D11 texture on decoded frame"))?
.clone();
let index = (*self.frame).data[1] as usize as u32;
// Input view over THIS slice of the decode array (cheap per-frame object).
let mut iv_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC::default();
iv_desc.FourCC = 0; // surface format speaks for itself
iv_desc.ViewDimension = D3D11_VPIV_DIMENSION_TEXTURE2D;
iv_desc.Anonymous.Texture2D.MipSlice = 0;
iv_desc.Anonymous.Texture2D.ArraySlice = index;
let mut in_view = None;
video_device
.CreateVideoProcessorInputView(&src, &ring.enumerator, &iv_desc, Some(&mut in_view))
.context("CreateVideoProcessorInputView")?;
let in_view = in_view.expect("input view created");
// Colour spaces per frame (the host flips PQ in-band): YCbCr in, sRGB out — a PQ
// stream is tone-mapped to SDR by the processor (module docs). CICP → DXGI enums.
let in_cs = match (color.transfer, color.matrix, color.full_range) {
(16, _, _) => DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_LEFT_P2020,
(_, 9, _) => DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P2020,
(_, _, true) => DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P709,
_ => DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709,
};
video_context1.VideoProcessorSetStreamColorSpace1(&ring.vp, 0, in_cs);
video_context1
.VideoProcessorSetOutputColorSpace1(&ring.vp, DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709);
let stream = D3D11_VIDEO_PROCESSOR_STREAM {
Enable: true.into(),
OutputIndex: 0,
InputFrameOrField: 0,
PastFrames: 0,
FutureFrames: 0,
ppPastSurfaces: ptr::null_mut(),
pInputSurface: std::mem::ManuallyDrop::new(Some(in_view)),
ppFutureSurfaces: ptr::null_mut(),
ppPastSurfacesRight: ptr::null_mut(),
pInputSurfaceRight: std::mem::ManuallyDrop::new(None),
ppFutureSurfacesRight: ptr::null_mut(),
};
let handle = slot.handle.0 as isize;
let generation = ring.generation;
let mut streams = [stream];
slot.mutex
.AcquireSync(0, ACQUIRE_TIMEOUT_MS)
.ok()
.context("keyed-mutex acquire (decode side) timed out")?;
let blt = video_context1.VideoProcessorBlt(&ring.vp, &slot.out_view, 0, &streams);
// Balance the ManuallyDrop refs the stream struct carried BEFORE error-checking.
std::mem::ManuallyDrop::drop(&mut streams[0].pInputSurface);
std::mem::ManuallyDrop::drop(&mut streams[0].pInputSurfaceRight);
let release = slot.mutex.ReleaseSync(0);
blt.ok().context("VideoProcessorBlt")?;
release.ok().context("keyed-mutex release")?;
// Get the conversion moving now — the presenter's GPU-side acquire waits on its
// completion, and an unflushed deferred batch would add a driver-decided delay.
context.Flush();
log_layout_once(width, height, index, color.is_pq());
Ok(D3d11Frame {
width,
height,
// What the slot now CONTAINS: sRGB BT.709 full-range RGB (PQ was tone-mapped).
color: ColorDesc {
primaries: 1,
transfer: 13, // sRGB (H.273)
matrix: 0, // identity — RGB
full_range: true,
},
handle,
generation,
})
}
}
}
impl Drop for D3d11vaDecoder {
fn drop(&mut self) {
use ffmpeg::ffi;
unsafe {
ffi::av_packet_free(&mut self.packet);
ffi::av_frame_free(&mut self.frame);
ffi::avcodec_free_context(&mut self.ctx);
ffi::av_buffer_unref(&mut self.hw_device);
}
// `ring` drops after the codec: no decode can be in flight past avcodec_free_context,
// and the slots' CloseHandle only closes OUR handle — a presenter-side import that is
// still parked keeps its own reference to the payload.
}
}
/// One-time dump of the first decoded surface's layout — the forensics for a new GPU/driver.
fn log_layout_once(width: u32, height: u32, index: u32, pq: bool) {
use std::sync::atomic::{AtomicBool, Ordering};
static ONCE: AtomicBool = AtomicBool::new(true);
if ONCE.swap(false, Ordering::Relaxed) {
tracing::info!(width, height, slice = index, pq, "D3D11VA first frame");
}
}
+182
View File
@@ -0,0 +1,182 @@
//! D3D11 shared-texture → Vulkan import (Windows): the presenter half of the D3D11VA
//! decode path (`pf_client_core::video_d3d11`). Each decoded frame arrives as the NT
//! handle of a shareable **BGRA8** texture (the decoder's VideoProcessor already did
//! YUV→RGB); we import it as a single-plane VkImage (`VK_KHR_external_memory_win32`,
//! dedicated allocation) and the presenter blits it straight into its video image — no
//! CSC pass. Single-plane RGBA is deliberate: importing the earlier multiplanar NV12
//! hand-off device-lost on NVIDIA however it was consumed (sampling or DMA copy, both
//! validation-clean — bisected 2026-07-09), while RGBA D3D11↔Vulkan interop is the path
//! Chromium/ANGLE exercise on every Windows driver.
//!
//! Synchronization is the texture's DXGI **keyed mutex** (`VK_KHR_win32_keyed_mutex`),
//! key 0 on both sides: the submit chains an acquire(0)/release(0) pair, so the GPU
//! waits for the decoder's conversion to complete before reading and the decoder's next
//! `AcquireSync(0)` on that ring slot blocks until our reads are done. Import is
//! per-frame (same discipline as the dmabuf path — parked in `Retired` until the fence
//! proves the GPU past it); NT-handle ownership stays with the decoder ring, the import
//! only references the payload.
use anyhow::{bail, Context as _, Result};
use ash::vk;
use pf_client_core::video::{ColorDesc, D3d11Frame};
/// The two device extensions this path needs; queried at device creation. Broadly present
/// on every Windows driver (NVIDIA/AMD/Intel) — a device without them just reports
/// `supports_d3d11() == false` and the decoder chain skips D3D11VA.
pub const DEVICE_EXTENSIONS: [&std::ffi::CStr; 2] = [
ash::khr::external_memory_win32::NAME,
ash::khr::win32_keyed_mutex::NAME,
];
/// Can this device import a D3D11 BGRA8 texture as a blit source? The spec-required
/// capability probe for the exact image the import path creates — creating an external
/// image the driver doesn't support is undefined behavior (observed as
/// `VK_ERROR_DEVICE_LOST` at the first submits with the old NV12 hand-off).
pub fn import_supported(instance: &ash::Instance, pdev: vk::PhysicalDevice) -> bool {
let mut ext_info = vk::PhysicalDeviceExternalImageFormatInfo::default()
.handle_type(vk::ExternalMemoryHandleTypeFlags::D3D11_TEXTURE);
let fmt_info = vk::PhysicalDeviceImageFormatInfo2::default()
.format(vk::Format::B8G8R8A8_UNORM)
.ty(vk::ImageType::TYPE_2D)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(vk::ImageUsageFlags::TRANSFER_SRC)
.push_next(&mut ext_info);
let mut ext_props = vk::ExternalImageFormatProperties::default();
let mut props = vk::ImageFormatProperties2::default().push_next(&mut ext_props);
let ok = unsafe {
instance.get_physical_device_image_format_properties2(pdev, &fmt_info, &mut props)
}
.is_ok()
&& ext_props
.external_memory_properties
.external_memory_features
.contains(vk::ExternalMemoryFeatureFlags::IMPORTABLE);
tracing::info!(bgra8 = ok, "D3D11 texture → Vulkan import support");
ok
}
/// One imported frame: the BGRA8 image over the shared texture and its imported
/// (dedicated) memory — a blit source, nothing more. Parked until the in-flight fence
/// proves the GPU past the blit, then [`HwFrame::destroy`]ed — the memory is what the
/// keyed-mutex info on the submit references.
pub struct HwFrame {
pub color: ColorDesc,
pub width: u32,
pub height: u32,
image: vk::Image,
memory: vk::DeviceMemory,
}
impl HwFrame {
/// The imported image — the acquire barrier + copy source.
pub fn image(&self) -> vk::Image {
self.image
}
/// The imported memory object — the submit's keyed-mutex acquire/release info needs it.
pub fn memory(&self) -> vk::DeviceMemory {
self.memory
}
pub fn destroy(self, device: &ash::Device) {
unsafe {
device.destroy_image(self.image, None);
device.free_memory(self.memory, None);
}
}
}
/// Import one hand-off frame. Fails cleanly (the caller demotes to software after a
/// streak) on anything the driver rejects: unsupported multiplanar external format,
/// import refusal, no matching memory type.
pub fn import(
device: &ash::Device,
ext_mem_win32: &ash::khr::external_memory_win32::Device,
frame: &D3d11Frame,
) -> Result<HwFrame> {
// The demotion test hook — same contract as the dmabuf path's.
if std::env::var_os("PUNKTFUNK_HW_FAULT").is_some_and(|v| v == "import") {
bail!("injected import failure (PUNKTFUNK_HW_FAULT=import)");
}
let mp_format = vk::Format::B8G8R8A8_UNORM;
let handle_type = vk::ExternalMemoryHandleTypeFlags::D3D11_TEXTURE;
// One single-plane image over the whole texture, transfer-source only — the blit is
// the whole job. Kept maximally "identical" to the D3D11 resource (no view-format
// aliasing, no extra usages).
let mut external_info = vk::ExternalMemoryImageCreateInfo::default().handle_types(handle_type);
let image = unsafe {
device.create_image(
&vk::ImageCreateInfo::default()
.push_next(&mut external_info)
.image_type(vk::ImageType::TYPE_2D)
.format(mp_format)
.extent(vk::Extent3D {
width: frame.width,
height: frame.height,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(vk::ImageUsageFlags::TRANSFER_SRC)
.initial_layout(vk::ImageLayout::UNDEFINED),
None,
)
}
.with_context(|| format!("create {}x{} {mp_format:?} external image", frame.width, frame.height))?;
let result = (|| {
// The handle's importable memory types, intersected with the image's requirement.
let handle = frame.handle as vk::HANDLE;
let mut handle_props = vk::MemoryWin32HandlePropertiesKHR::default();
unsafe {
ext_mem_win32.get_memory_win32_handle_properties(handle_type, handle, &mut handle_props)
}
.context("vkGetMemoryWin32HandlePropertiesKHR")?;
let reqs = unsafe { device.get_image_memory_requirements(image) };
let bits = reqs.memory_type_bits & handle_props.memory_type_bits;
let type_index = (0..32u32)
.find(|i| bits & (1 << i) != 0)
.context("no importable memory type for the D3D11 texture")?;
// Import does NOT take handle ownership (NT handle rule): the decoder ring keeps
// closing its own handle; this allocation references the payload independently.
let mut import_info = vk::ImportMemoryWin32HandleInfoKHR::default()
.handle_type(handle_type)
.handle(handle);
let mut dedicated = vk::MemoryDedicatedAllocateInfo::default().image(image);
let memory = unsafe {
device.allocate_memory(
&vk::MemoryAllocateInfo::default()
.push_next(&mut import_info)
.push_next(&mut dedicated)
.allocation_size(reqs.size)
.memory_type_index(type_index),
None,
)
}
.context("import D3D11 texture memory")?;
if let Err(e) = unsafe { device.bind_image_memory(image, memory, 0) } {
unsafe { device.free_memory(memory, None) };
return Err(e).context("bind imported memory");
}
Ok(memory)
})();
let memory = match result {
Ok(m) => m,
Err(e) => {
unsafe { device.destroy_image(image, None) };
return Err(e);
}
};
Ok(HwFrame {
color: frame.color,
width: frame.width,
height: frame.height,
image,
memory,
})
}
+5 -2
View File
@@ -10,11 +10,14 @@
//! import/present failure streak, demote the decoder to software via the session pump's //! import/present failure streak, demote the decoder to software via the session pump's
//! `force_software` contract, same as the GTK presenter. //! `force_software` contract, same as the GTK presenter.
//! //!
//! Builds on Linux AND Windows; `dmabuf` is the one Linux-only module (DRM-PRIME does //! Builds on Linux AND Windows; `dmabuf` is Linux-only (DRM-PRIME does not exist on
//! not exist on Windows — the decode chain there is Vulkan → software). //! Windows) and `d3d11` is its Windows counterpart (D3D11VA shared-texture import) —
//! the decode chain there is Vulkan → D3D11VA → software.
#[cfg(any(target_os = "linux", windows))] #[cfg(any(target_os = "linux", windows))]
pub mod csc; pub mod csc;
#[cfg(windows)]
pub mod d3d11;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub mod dmabuf; pub mod dmabuf;
#[cfg(any(target_os = "linux", windows))] #[cfg(any(target_os = "linux", windows))]
+41
View File
@@ -817,6 +817,47 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
} }
false false
} }
// D3D11VA: shared-texture import, same gate + failure-streak
// demotion contract as the dmabuf path.
#[cfg(windows)]
DecodedImage::D3d11(d) if presenter.supports_d3d11() && !st.dmabuf_demoted => {
st.hdr = d.color.is_pq();
match presenter.present(
&window,
FrameInput::D3d11(d),
overlay_frame.as_ref(),
) {
Ok(p) => {
st.hw_fails = 0;
p
}
Err(e) => {
st.hw_fails += 1;
tracing::warn!(error = %format!("{e:#}"), fails = st.hw_fails,
"hardware present failed");
if st.hw_fails >= 3 && !st.dmabuf_demoted {
st.dmabuf_demoted = true;
tracing::warn!("demoting the decoder to software");
st.force_software.store(true, Ordering::Relaxed);
}
false
}
}
}
#[cfg(windows)]
DecodedImage::D3d11(_) => {
// No import extensions on this device (or already demoted) — the
// pump rebuilds the decoder as software; frames flow again soon.
if !st.dmabuf_demoted {
st.dmabuf_demoted = true;
tracing::warn!(
"no win32 external-memory import on this device — demoting \
the decoder to software"
);
st.force_software.store(true, Ordering::Relaxed);
}
false
}
// Vulkan-Video: decoded on the presenter's own device — present is // Vulkan-Video: decoded on the presenter's own device — present is
// views + CSC, no import step to gate on. Same failure-streak // views + CSC, no import step to gate on. Same failure-streak
// demotion contract as the dmabuf path. // demotion contract as the dmabuf path.
+217 -7
View File
@@ -37,6 +37,9 @@ pub enum FrameInput<'a> {
Dmabuf(DmabufFrame), Dmabuf(DmabufFrame),
/// FFmpeg Vulkan Video output — a VkImage already on THIS device (zero copy). /// FFmpeg Vulkan Video output — a VkImage already on THIS device (zero copy).
VkFrame(VkVideoFrame), VkFrame(VkVideoFrame),
/// D3D11VA hand-off — a shareable NT-handle texture to import (`d3d11.rs`).
#[cfg(windows)]
D3d11(pf_client_core::video::D3d11Frame),
} }
/// The dmabuf/CSC machinery, present only when the device carries the import extensions. /// The dmabuf/CSC machinery, present only when the device carries the import extensions.
@@ -45,12 +48,22 @@ struct HwCtx {
ext_mem_fd: ash::khr::external_memory_fd::Device, ext_mem_fd: ash::khr::external_memory_fd::Device,
} }
/// The D3D11 shared-texture import machinery, present only when the device carries
/// `VK_KHR_external_memory_win32` + `VK_KHR_win32_keyed_mutex`.
#[cfg(windows)]
struct HwCtxWin {
ext_mem_win32: ash::khr::external_memory_win32::Device,
}
/// A submitted hardware frame parked until the in-flight fence proves the GPU reads /// A submitted hardware frame parked until the in-flight fence proves the GPU reads
/// done: imported dmabuf planes, or a Vulkan-Video frame (FFmpeg's image — we own only /// done: imported dmabuf planes, or a Vulkan-Video frame (FFmpeg's image — we own only
/// the plane views; dropping the frame's guard releases the AVFrame back to the pool). /// the plane views; dropping the frame's guard releases the AVFrame back to the pool).
enum Retired { enum Retired {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
Dmabuf(HwFrame), Dmabuf(HwFrame),
#[cfg(windows)]
D3d11(crate::d3d11::HwFrame),
Vk { Vk {
frame: VkVideoFrame, frame: VkVideoFrame,
views: [vk::ImageView; 2], views: [vk::ImageView; 2],
@@ -62,6 +75,8 @@ impl Retired {
match self { match self {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
Retired::Dmabuf(f) => f.destroy(device), Retired::Dmabuf(f) => f.destroy(device),
#[cfg(windows)]
Retired::D3d11(f) => f.destroy(device),
Retired::Vk { frame, views } => { Retired::Vk { frame, views } => {
unsafe { unsafe {
for v in views { for v in views {
@@ -333,6 +348,10 @@ pub struct Presenter {
/// pass itself is unconditional: Vulkan-Video frames need it everywhere). /// pass itself is unconditional: Vulkan-Video frames need it everywhere).
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
hw: Option<HwCtx>, hw: Option<HwCtx>,
/// D3D11 shared-texture import — `None` when the device lacks the win32 external
/// memory / keyed-mutex extensions.
#[cfg(windows)]
hw_win: Option<HwCtxWin>,
csc: CscPass, csc: CscPass,
/// FFmpeg Vulkan Video decode handles — `None` when the stack can't do it. /// FFmpeg Vulkan Video decode handles — `None` when the stack can't do it.
video_export: Option<pf_client_core::video::VulkanDecodeDevice>, video_export: Option<pf_client_core::video::VulkanDecodeDevice>,
@@ -451,6 +470,31 @@ impl Presenter {
unavailable" unavailable"
); );
} }
// D3D11 shared-texture import (the D3D11VA decode hand-off) — optional exactly
// like the dmabuf set; a device without it keeps Vulkan-Video/software decode.
// Extensions alone aren't the whole gate: the driver must also report the
// multiplanar NV12 image as IMPORTABLE from a D3D11 texture handle
// (vkGetPhysicalDeviceImageFormatProperties2 — creating an unsupported external
// image is UB, observed as VK_ERROR_DEVICE_LOST at the first submits on NVIDIA).
#[cfg(windows)]
let win_capable = crate::d3d11::DEVICE_EXTENSIONS.iter().all(|n| has(n))
&& crate::d3d11::import_supported(&instance, pdev);
#[cfg(windows)]
if win_capable {
dev_exts.extend(crate::d3d11::DEVICE_EXTENSIONS.iter().map(|n| n.as_ptr()));
} else {
tracing::info!(
"device lacks the win32 external-memory/keyed-mutex extensions — D3D11VA \
hardware frames unavailable"
);
}
// The adapter LUID (for the D3D11VA backend to create its decode device on the
// SAME adapter). Core 1.1 query; valid on effectively every Windows driver.
let mut id_props = vk::PhysicalDeviceIDProperties::default();
let mut props2 = vk::PhysicalDeviceProperties2::default().push_next(&mut id_props);
unsafe { instance.get_physical_device_properties2(pdev, &mut props2) };
let adapter_luid: Option<[u8; 8]> =
(id_props.device_luid_valid == vk::TRUE).then_some(id_props.device_luid);
// Static HDR metadata (ST.2086 mastering + CLL) to the presentation engine. // Static HDR metadata (ST.2086 mastering + CLL) to the presentation engine.
// Compositors key their "this app is HDR" signaling on the client pushing // Compositors key their "this app is HDR" signaling on the client pushing
// metadata via vkSetHdrMetadataEXT in addition to picking the HDR10 colorspace // metadata via vkSetHdrMetadataEXT in addition to picking the HDR10 colorspace
@@ -596,12 +640,22 @@ impl Presenter {
} else { } else {
None None
}; };
#[cfg(windows)]
let hw_win = win_capable.then(|| HwCtxWin {
ext_mem_win32: ash::khr::external_memory_win32::Device::new(&instance, &device),
});
let csc = CscPass::new(&device, vk::Format::R8G8B8A8_UNORM)?; let csc = CscPass::new(&device, vk::Format::R8G8B8A8_UNORM)?;
// The exported handle bundle for FFmpeg's Vulkan Video decoder (None = the // The exported handle bundle: FFmpeg Vulkan Video handles when the device can
// decoder chain skips straight to VAAPI/software). Extension lists must mirror // decode, AND (Windows) the D3D11-interop facts — so it's built whenever EITHER
// creation exactly — FFmpeg keys its code paths off the strings. // consumer needs it; `video_decode`/`d3d11_import` tell the decoder chain which
let video_export = if video_ok { // paths are real. Extension lists must mirror creation exactly — FFmpeg keys its
// code paths off the strings.
#[cfg(windows)]
let export_worthy = video_ok || win_capable;
#[cfg(not(windows))]
let export_worthy = video_ok;
let video_export = if export_worthy {
let qf_props = unsafe { instance.get_physical_device_queue_family_properties(pdev) }; let qf_props = unsafe { instance.get_physical_device_queue_family_properties(pdev) };
let mut device_extensions: Vec<CString> = let mut device_extensions: Vec<CString> =
vec![CString::from(ash::khr::swapchain::NAME)]; vec![CString::from(ash::khr::swapchain::NAME)];
@@ -610,6 +664,11 @@ impl Presenter {
device_extensions device_extensions
.extend(dmabuf::DEVICE_EXTENSIONS.iter().map(|n| CString::from(*n))); .extend(dmabuf::DEVICE_EXTENSIONS.iter().map(|n| CString::from(*n)));
} }
#[cfg(windows)]
if win_capable {
device_extensions
.extend(crate::d3d11::DEVICE_EXTENSIONS.iter().map(|n| CString::from(*n)));
}
if has_hdr_metadata { if has_hdr_metadata {
device_extensions.push(CString::from(ash::ext::hdr_metadata::NAME)); device_extensions.push(CString::from(ash::ext::hdr_metadata::NAME));
} }
@@ -628,9 +687,15 @@ impl Presenter {
.map(|e| CString::new(e.as_str()).unwrap()) .map(|e| CString::new(e.as_str()).unwrap())
.collect(), .collect(),
device_extensions, device_extensions,
f_sampler_ycbcr: true, f_sampler_ycbcr: have_f11.sampler_ycbcr_conversion == vk::TRUE,
f_timeline_semaphore: true, f_timeline_semaphore: have_f12.timeline_semaphore == vk::TRUE,
f_synchronization2: true, f_synchronization2: have_f13.synchronization2 == vk::TRUE,
video_decode: video_ok,
#[cfg(windows)]
d3d11_import: win_capable,
#[cfg(not(windows))]
d3d11_import: false,
adapter_luid,
}) })
} else { } else {
None None
@@ -685,6 +750,8 @@ impl Presenter {
qfi, qfi,
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
hw, hw,
#[cfg(windows)]
hw_win,
csc, csc,
video_export, video_export,
overlay_pipe, overlay_pipe,
@@ -881,6 +948,13 @@ impl Presenter {
self.hw.is_some() self.hw.is_some()
} }
/// Whether the D3D11 shared-texture path exists on this device — callers keep the
/// decoder on software when it doesn't.
#[cfg(windows)]
pub fn supports_d3d11(&self) -> bool {
self.hw_win.is_some()
}
/// The FFmpeg Vulkan Video decode handle bundle — `None` when this stack can't /// The FFmpeg Vulkan Video decode handle bundle — `None` when this stack can't
/// (device < 1.3, missing video extensions/queue/features). The decoder chain /// (device < 1.3, missing video extensions/queue/features). The decoder chain
/// falls back to VAAPI/software then. /// falls back to VAAPI/software then.
@@ -980,6 +1054,8 @@ impl Presenter {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
FrameInput::Dmabuf(d) => Some(d.color.is_pq()), FrameInput::Dmabuf(d) => Some(d.color.is_pq()),
FrameInput::VkFrame(v) => Some(v.color.is_pq()), FrameInput::VkFrame(v) => Some(v.color.is_pq()),
#[cfg(windows)]
FrameInput::D3d11(d) => Some(d.color.is_pq()),
}; };
if let Some(pq) = frame_pq { if let Some(pq) = frame_pq {
let want = pq && self.hdr10_format.is_some(); let want = pq && self.hdr10_format.is_some();
@@ -992,6 +1068,8 @@ impl Presenter {
// semaphore. // semaphore.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let mut hw_frame: Option<HwFrame> = None; let mut hw_frame: Option<HwFrame> = None;
#[cfg(windows)]
let mut win_frame: Option<crate::d3d11::HwFrame> = None;
let mut vk_frame: Option<(VkVideoFrame, [vk::ImageView; 2])> = None; let mut vk_frame: Option<(VkVideoFrame, [vk::ImageView; 2])> = None;
let cpu_frame = match input { let cpu_frame = match input {
FrameInput::Redraw => None, FrameInput::Redraw => None,
@@ -1005,6 +1083,15 @@ impl Presenter {
hw_frame = Some(dmabuf::import(&self.device, &hw.ext_mem_fd, d)?); hw_frame = Some(dmabuf::import(&self.device, &hw.ext_mem_fd, d)?);
None None
} }
#[cfg(windows)]
FrameInput::D3d11(d) => {
let hw = self
.hw_win
.as_ref()
.context("D3D11 frame without win32 import support")?;
win_frame = Some(crate::d3d11::import(&self.device, &hw.ext_mem_win32, &d)?);
None
}
FrameInput::VkFrame(v) => { FrameInput::VkFrame(v) => {
let views = self.vkframe_plane_views(&v)?; let views = self.vkframe_plane_views(&v)?;
vk_frame = Some((v, views)); vk_frame = Some((v, views));
@@ -1047,6 +1134,17 @@ impl Presenter {
self.csc self.csc
.bind_planes(&self.device, f.luma_view, f.chroma_view); .bind_planes(&self.device, f.luma_view, f.chroma_view);
} }
#[cfg(windows)]
if let Some(f) = &win_frame {
if self
.video
.as_ref()
.is_none_or(|v| v.width != f.width || v.height != f.height)
{
self.rebuild_video_image(f.width, f.height)?;
tracing::info!(width = f.width, height = f.height, "video image (re)built");
}
}
if let Some((f, views)) = &vk_frame { if let Some((f, views)) = &vk_frame {
if self if self
.video .video
@@ -1086,6 +1184,10 @@ impl Presenter {
if let Some(f) = hw_frame { if let Some(f) = hw_frame {
f.destroy(&self.device); f.destroy(&self.device);
} }
#[cfg(windows)]
if let Some(f) = win_frame {
f.destroy(&self.device);
}
self.recreate_swapchain(window)?; self.recreate_swapchain(window)?;
return Ok(false); return Ok(false);
} }
@@ -1122,6 +1224,49 @@ impl Presenter {
); );
} }
// D3D11 frame: acquire the imported BGRA texture from the external "queue
// family" (the keyed mutex on the submit is the actual cross-API sync) and
// blit it into the video image — the frame arrives as ready sRGB from the
// decoder's VideoProcessor, so there is no CSC pass; the blit converts the
// BGRA→RGBA component order. Same layout dance as the CPU staging path.
#[cfg(windows)]
if let (Some(f), Some(v)) = (&win_frame, &self.video) {
external_acquire_barrier(&self.device, self.cmd_buf, f.image(), self.qfi);
barrier(
&self.device,
self.cmd_buf,
v.image,
vk::ImageLayout::UNDEFINED,
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
);
let extent = vk::Offset3D {
x: v.width as i32,
y: v.height as i32,
z: 1,
};
let blit = vk::ImageBlit::default()
.src_subresource(subresource_layers())
.src_offsets([vk::Offset3D::default(), extent])
.dst_subresource(subresource_layers())
.dst_offsets([vk::Offset3D::default(), extent]);
self.device.cmd_blit_image(
self.cmd_buf,
f.image(),
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
v.image,
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
&[blit],
vk::Filter::NEAREST, // 1:1 — the composite blit below does the scaling
);
barrier(
&self.device,
self.cmd_buf,
v.image,
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
);
}
// Vulkan-Video frame: the decoded image is already on THIS device. Read the // Vulkan-Video frame: the decoded image is already on THIS device. Read the
// live sync state under the frames lock (held through submission — the // live sync state under the frames lock (held through submission — the
// AVVulkanFramesContext contract), acquire from the decode queue family, // AVVulkanFramesContext contract), acquire from the decode queue family,
@@ -1331,6 +1476,33 @@ impl Presenter {
if vk_sync.is_some() { if vk_sync.is_some() {
submit = submit.push_next(&mut timeline); submit = submit.push_next(&mut timeline);
} }
// D3D11 frame: bracket the submit in the shared texture's keyed mutex, key 0
// both ways (the decode side copies under acquire(0)/release(0) too) — the
// GPU-side acquire is what orders our sampling after the decoder's copy, and
// our completion release is what unblocks the ring slot's reuse.
#[cfg(windows)]
let keyed_mem;
#[cfg(windows)]
let keyed_keys = [0u64];
#[cfg(windows)]
let keyed_timeouts = [2000u32];
#[cfg(windows)]
let mut keyed_info;
#[cfg(windows)]
if let Some(f) = &win_frame {
// Bisect knob: PUNKTFUNK_D3D11_NO_MUTEX=1 skips the acquire/release pair
// (torn frames possible — debugging only).
if std::env::var_os("PUNKTFUNK_D3D11_NO_MUTEX").is_none() {
keyed_mem = [f.memory()];
keyed_info = vk::Win32KeyedMutexAcquireReleaseInfoKHR::default()
.acquire_syncs(&keyed_mem)
.acquire_keys(&keyed_keys)
.acquire_timeouts(&keyed_timeouts)
.release_syncs(&keyed_mem)
.release_keys(&keyed_keys);
submit = submit.push_next(&mut keyed_info);
}
}
let submitted = self.device.queue_submit(self.queue, &[submit], self.fence); let submitted = self.device.queue_submit(self.queue, &[submit], self.fence);
// Write the new sync state back and release the frames lock REGARDLESS of // Write the new sync state back and release the frames lock REGARDLESS of
// the submit outcome (an abandoned lock would wedge the decoder). // the submit outcome (an abandoned lock would wedge the decoder).
@@ -1358,6 +1530,10 @@ impl Presenter {
if let Some(f) = hw_frame.take() { if let Some(f) = hw_frame.take() {
self.retired_hw = Some(Retired::Dmabuf(f)); self.retired_hw = Some(Retired::Dmabuf(f));
} }
#[cfg(windows)]
if let Some(f) = win_frame.take() {
self.retired_hw = Some(Retired::D3d11(f));
}
let swapchains = [self.swapchain]; let swapchains = [self.swapchain];
let indices = [index]; let indices = [index];
@@ -2002,6 +2178,40 @@ fn vkframe_acquire_barrier(
} }
} }
/// Acquire an imported D3D11 texture from the EXTERNAL queue family as a copy source.
/// The keyed mutex on the submit is the actual cross-API ordering; per the
/// external-memory rules an UNDEFINED-old-layout transition on externally-bound memory
/// preserves the contents (unlike ordinary images), so this is purely the
/// layout/ownership hop.
#[cfg(windows)]
fn external_acquire_barrier(
device: &ash::Device,
cmd: vk::CommandBuffer,
image: vk::Image,
qfi: u32,
) {
let b = vk::ImageMemoryBarrier::default()
.src_access_mask(vk::AccessFlags::empty())
.dst_access_mask(vk::AccessFlags::TRANSFER_READ)
.old_layout(vk::ImageLayout::UNDEFINED)
.new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
.src_queue_family_index(vk::QUEUE_FAMILY_EXTERNAL)
.dst_queue_family_index(qfi)
.image(image)
.subresource_range(subresource_range());
unsafe {
device.cmd_pipeline_barrier(
cmd,
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::PipelineStageFlags::TRANSFER,
vk::DependencyFlags::empty(),
&[],
&[],
&[b],
);
}
}
/// Acquire a dmabuf plane image from its foreign owner (the VAAPI decoder): queue-family /// Acquire a dmabuf plane image from its foreign owner (the VAAPI decoder): queue-family
/// transfer FOREIGN → ours, UNDEFINED → SHADER_READ_ONLY (content is preserved across /// transfer FOREIGN → ours, UNDEFINED → SHADER_READ_ONLY (content is preserved across
/// the transfer regardless of the UNDEFINED old-layout, per the external-memory rules). /// the transfer regardless of the UNDEFINED old-layout, per the external-memory rules).