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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user