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:
Generated
+1
@@ -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]]
|
||||||
|
|||||||
@@ -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",
|
||||||
|
] }
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`).
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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))]
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
Reference in New Issue
Block a user