test(encode): D3d11Hw verified on real silicon; the QSV derive is broken upstream of us

Ran the QSV probe on the Intel VM (UHD 750, FFmpeg 8). It fails — and not in the
code it was written to cover:

    [AVHWDeviceContext] Error setting child device handle: -16
    ZeroCopyInner::open(QSV) failed on iteration 0: derive QSV device from D3D11VA

That is `MFX_ERR_UNDEFINED_BEHAVIOR` out of `MFXVideoCORE_SetHandle`, raised inside
`av_hwdevice_ctx_create_derived` — libav's own code, called with the same arguments
as before this branch. Confirmed pre-existing by A/B: the same probe, written against
unmodified `origin/main` with the raw-pointer struct, fails byte-identically on the
same box at the same first iteration. The ownership change is not implicated.

It is also not the hardware. `qsv::tests::qsv_encode_live_smoke` — the native VPL
backend, untouched by this branch — encodes real H.264 on that box via VPL 2.15 with
its own D3D11 zero-copy. So QSV works; libav's QSV-from-D3D11VA derive is what does
not. That matches `zerocopy_active` already defaulting QSV **off** pending Intel
validation, and is now a measured fact rather than a suspicion. The test stays,
`#[ignore]`d, with the finding in its doc comment so the next Intel driver / FFmpeg
bump re-checks it instead of the question being quietly dropped.

What that leaves is real coverage of the half that IS reachable anywhere:
`d3d11hw_alloc_drop_cycles` loops construct/drop on `D3d11Hw`, the hwdevice +
frames-pool pair both Windows zero-copy vendors share, so it covers the AMF path's
ownership without needing AMD hardware. **It passes on the Intel box: 8 cycles, no
abort.** Adapter selection is factored into `test_hw_device`, which prefers a vendor
and skips the Microsoft Basic Render Driver — a punktfunk host enumerates our own
virtual-display adapter too, so `EnumAdapters1(0)` is not a safe assumption.

Verified: QSV VM .47 full `pf-encode --features amf-qsv` suite 39 passed / 0 failed
(EXITCODE=0), with `d3d11hw_alloc_drop_cycles` green on real silicon. Linux .21
workspace check exit 0 / zero errors, pf-encode 33/0, pf-client-core 34/0.
This commit is contained in:
2026-07-28 21:31:41 +02:00
parent 564797a12a
commit 2d78d2a514
+89 -11
View File
@@ -1443,6 +1443,79 @@ impl Encoder for FfmpegWinEncoder {
mod tests {
use super::*;
/// Pick the Intel adapter, or any hardware D3D11 adapter, for the probes below.
///
/// Not `EnumAdapters1(0)`: a punktfunk host box also enumerates our own virtual-display adapter,
/// and "Microsoft Basic Render Driver" (vendor 0x1414) is the software rasterizer, which has no
/// video engine at all.
///
/// # Safety
/// Calls the DXGI enumeration FFI and `make_device`; every result is checked before use and no
/// alias to the adapter outlives the call.
#[cfg(test)]
unsafe fn test_hw_device(prefer_vendor: u32) -> Option<ID3D11Device> {
use windows::Win32::Graphics::Dxgi::{CreateDXGIFactory1, IDXGIFactory1};
let factory: IDXGIFactory1 = CreateDXGIFactory1().ok()?;
let mut preferred = None;
let mut fallback = None;
for i in 0.. {
let Ok(adapter) = factory.EnumAdapters1(i) else {
break; // DXGI_ERROR_NOT_FOUND — end of the list
};
let Ok(desc) = adapter.GetDesc1() else {
continue;
};
let name = String::from_utf16_lossy(&desc.Description)
.trim_end_matches('\0')
.to_string();
eprintln!("adapter {i}: vendor={:#06x} {name}", desc.VendorId);
if desc.VendorId == prefer_vendor && preferred.is_none() {
preferred = Some(adapter);
} else if desc.VendorId != 0x1414 && fallback.is_none() {
fallback = Some(adapter);
}
}
let adapter = preferred.or(fallback)?;
pf_frame::dxgi::make_device(&adapter).ok().map(|(d, _c)| d)
}
/// Construct/drop `D3d11Hw` repeatedly on real silicon — the D3D11VA half of the RAII change,
/// and the half that IS reachable on any GPU.
///
/// `D3d11Hw` owns a hwdevice + frames-pool pair through `AvBuffer`, and it is shared by both
/// Windows zero-copy vendors, so this covers the AMF path's ownership too without needing AMD
/// hardware. Looping is the point, as with `cuda_hw_alloc_drop_cycles`: a double-unref aborts in
/// the CRT, a missed one leaks a device and a pool of eight surfaces per iteration.
///
/// `#[ignore]`d (needs a real D3D11 GPU):
/// `cargo test -p pf-encode --features amf-qsv d3d11hw_alloc_drop_cycles -- --ignored --nocapture`
#[test]
#[ignore = "needs a real D3D11 GPU (run on a GPU host, not the build box)"]
fn d3d11hw_alloc_drop_cycles() {
// SAFETY: see `test_hw_device`; the returned device outlives every `D3d11Hw` built below.
let device = unsafe { test_hw_device(0x8086) }.expect("a hardware D3D11 adapter");
for i in 0..8 {
// SAFETY: `D3d11Hw::new` needs libav initialised (it is, statically, by the ffmpeg-next
// crate's first use here) and a live `ID3D11Device`, which `device` is for the whole
// loop. NV12 at 640x480 with an 8-surface pool are valid pool parameters. The handle
// drops at the end of each iteration — that release is what is under test.
let hw = unsafe {
D3d11Hw::new(
&device,
ffi::AVPixelFormat::AV_PIX_FMT_NV12,
pool_bind_flags(WinVendor::Amf),
640,
480,
8,
)
}
.unwrap_or_else(|e| panic!("D3d11Hw::new failed on iteration {i}: {e:#}"));
assert!(!hw.device_ref.as_ptr().is_null(), "device ref went null");
assert!(!hw.frames_ref.as_ptr().is_null(), "frames ref went null");
}
eprintln!("8 D3d11Hw alloc/drop cycles completed without abort");
}
/// Construct/drop `ZeroCopyInner` on the QSV path, repeatedly, on real Intel silicon.
///
/// This is the one path in the crate where ownership was genuinely ambiguous: `open` builds a
@@ -1458,21 +1531,26 @@ mod tests {
/// whole file unless `PUNKTFUNK_QSV_FFMPEG=1`. Calling `open` directly sidesteps both gates so
/// the ownership itself is what gets exercised.
///
/// ⚠️ **KNOWN TO FAIL on Intel UHD 750 / FFmpeg 8 (2026-07-28), and NOT because of the code it
/// covers.** `av_hwdevice_ctx_create_derived(QSV <- D3D11VA)` returns
/// `MFX_ERR_UNDEFINED_BEHAVIOR (-16)` out of `MFXVideoCORE_SetHandle`, logged by libav as
/// "Error setting child device handle". Confirmed pre-existing by running this same probe
/// against unmodified `origin/main` (raw-pointer struct) on the same box: byte-identical
/// failure, same call, same first iteration. The native VPL backend
/// (`qsv::tests::qsv_encode_live_smoke`) encodes fine on that box, so the silicon and D3D11
/// zero-copy both work — it is libav's QSV-from-D3D11VA derive specifically that does not.
///
/// That matches `zerocopy_active`, which already defaults QSV **off** pending Intel validation.
/// The test is kept (and kept `#[ignore]`d) so the derive gets re-checked on the next Intel
/// driver / FFmpeg bump rather than the question being silently dropped.
///
/// `#[ignore]`d (needs a real Intel QSV device):
/// `cargo test -p pf-encode --features amf-qsv zerocopy_qsv_alloc_drop_cycles -- --ignored --nocapture`
#[test]
#[ignore = "needs a real Intel QSV device (run on an Intel host, not the build box)"]
#[ignore = "needs a real Intel QSV device; currently fails in libav's QSV<-D3D11VA derive (pre-existing, see doc)"]
fn zerocopy_qsv_alloc_drop_cycles() {
use windows::Win32::Graphics::Dxgi::{CreateDXGIFactory1, IDXGIFactory1};
// SAFETY: `CreateDXGIFactory1`/`EnumAdapters1` are the documented enumeration pair and are
// checked here; `make_device` requires a live `IDXGIAdapter1`, which `adapter` is for the
// duration of the call (it outlives it as a local). The returned device is the sole owner of
// its COM object and outlives every `ZeroCopyInner` built from it below.
let (device, _ctx) = unsafe {
let factory: IDXGIFactory1 = CreateDXGIFactory1().expect("CreateDXGIFactory1");
let adapter = factory.EnumAdapters1(0).expect("EnumAdapters1(0)");
pf_frame::dxgi::make_device(&adapter).expect("D3D11 device on adapter 0")
};
// SAFETY: see `test_hw_device`; the device outlives every `ZeroCopyInner` built below.
let device = unsafe { test_hw_device(0x8086) }.expect("an Intel D3D11 adapter");
for i in 0..8 {
let zc = ZeroCopyInner::open(
WinVendor::Qsv,