From 428bd2519cab8c3cb5b0121afed579c998b6c04b Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Tue, 21 Jul 2026 10:26:19 +0200 Subject: [PATCH] =?UTF-8?q?test(qsv):=20converter=E2=86=92ring-profile-P01?= =?UTF-8?q?0=E2=86=92encoder=20live=20e2e=20(the=20RTV-written=20seam)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- Cargo.lock | 1 + crates/pf-capture/src/windows/dxgi.rs | 129 ++++++++++++++++++++++++ crates/pf-encode/Cargo.toml | 5 + crates/pf-encode/src/enc/windows/qsv.rs | 75 ++++++++++++++ 4 files changed, 210 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index a2b2414f..b8dde365 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2881,6 +2881,7 @@ dependencies = [ "libvpl-sys", "nvidia-video-codec-sdk", "openh264", + "pf-capture", "pf-frame", "pf-gpu", "pf-host-config", diff --git a/crates/pf-capture/src/windows/dxgi.rs b/crates/pf-capture/src/windows/dxgi.rs index 4586a28a..c0e56f7c 100644 --- a/crates/pf-capture/src/windows/dxgi.rs +++ b/crates/pf-capture/src/windows/dxgi.rs @@ -747,6 +747,135 @@ pub fn hdr_p010_selftest() -> Result<()> { /// is NOT 16-aligned (1080 → the encoder's align16 pool seam) and a driver may treat the planar /// RTVs differently at real sizes; the vendor pin matters on dual-GPU boxes where the default /// adapter is not the one the session encodes on. +/// Test support (used by pf-encode's live e2e): the 8 sRGB colour bars (white/yellow/cyan/green/ +/// magenta/red/blue/black, sRGB 1.0 = scRGB 1.0 = 80 nits) as a w×h FP16 scRGB texture on the +/// adapter with `luid`, converted through the REAL [`HdrP010Converter`] into a P010 texture with +/// **`BIND_RENDER_TARGET` only, `MiscFlags` 0 — the exact bind profile of the IDD out-ring** (the +/// CPU-upload encoder tests can't use that profile, so only this path exercises "RTV-written P010 +/// → encoder ingest copy"). Returns `(device, p010)`; expected decoded codes per bar are the +/// bars_pq2020 fixture's: (490,512,512) (478,423,518) (464,525,473) (450,432,476) (350,584,585) +/// (325,448,598) (226,650,535) (64,512,512). +#[cfg(target_os = "windows")] +#[doc(hidden)] +pub fn hdr_p010_convert_bars_on_luid( + luid: [u8; 8], + w: u32, + h: u32, +) -> Result<(ID3D11Device, ID3D11Texture2D)> { + use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_UNKNOWN; + use windows::Win32::Graphics::Dxgi::{CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory4}; + + if w == 0 || h == 0 || w % 2 != 0 || h % 2 != 0 { + bail!("bars pattern needs even non-zero dimensions, got {w}x{h}"); + } + // sRGB primaries at full/zero channels: sRGB EOTF(1.0)=1.0, (0)=0 → the scRGB pattern is + // pure 0/1 floats and the PQ/BT.2020 reference codes above are exact. + const BARS: [(f32, f32, f32); 8] = [ + (1.0, 1.0, 1.0), + (1.0, 1.0, 0.0), + (0.0, 1.0, 1.0), + (0.0, 1.0, 0.0), + (1.0, 0.0, 1.0), + (1.0, 0.0, 0.0), + (0.0, 0.0, 1.0), + (0.0, 0.0, 0.0), + ]; + let bar_w = (w / 8).max(1) as usize; + let mut fp16 = vec![0u16; (w * h * 4) as usize]; + for y in 0..h as usize { + for x in 0..w as usize { + let (r, g, b) = BARS[(x / bar_w).min(7)]; + let i = (y * w as usize + x) * 4; + fp16[i] = f32_to_f16(r); + fp16[i + 1] = f32_to_f16(g); + fp16[i + 2] = f32_to_f16(b); + fp16[i + 3] = f32_to_f16(1.0); + } + } + // SAFETY: same single-device/single-thread contract as `hdr_p010_selftest_at`; the FP16 + // initial-data Vec outlives the synchronous CreateTexture2D; the returned COM handles own + // their references. + unsafe { + let luid = windows::Win32::Foundation::LUID { + LowPart: u32::from_le_bytes(luid[..4].try_into().unwrap()), + HighPart: i32::from_le_bytes(luid[4..].try_into().unwrap()), + }; + let factory: IDXGIFactory4 = CreateDXGIFactory1().context("dxgi factory")?; + let adapter: IDXGIAdapter1 = factory.EnumAdapterByLuid(luid).context("adapter by luid")?; + let mut device: Option = None; + let mut context: Option = None; + D3D11CreateDevice( + &adapter, + D3D_DRIVER_TYPE_UNKNOWN, + HMODULE::default(), + D3D11_CREATE_DEVICE_BGRA_SUPPORT, + Some(&[D3D_FEATURE_LEVEL_11_0]), + D3D11_SDK_VERSION, + Some(&mut device), + None, + Some(&mut context), + ) + .context("D3D11CreateDevice(luid) for bars convert")?; + let device = device.context("null device")?; + let context = context.context("null context")?; + + let src_desc = D3D11_TEXTURE2D_DESC { + Width: w, + Height: h, + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_R16G16B16A16_FLOAT, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32, + ..Default::default() + }; + let init = D3D11_SUBRESOURCE_DATA { + pSysMem: fp16.as_ptr() as *const c_void, + SysMemPitch: w * 8, + SysMemSlicePitch: 0, + }; + let mut src_tex: Option = None; + device + .CreateTexture2D(&src_desc, Some(&init), Some(&mut src_tex)) + .context("CreateTexture2D(fp16 bars)")?; + let src_tex = src_tex.context("null src tex")?; + let mut src_srv: Option = None; + device + .CreateShaderResourceView(&src_tex, None, Some(&mut src_srv)) + .context("CreateShaderResourceView(fp16 bars)")?; + let src_srv = src_srv.context("null src srv")?; + + // The IDD out-ring's exact profile: P010, RENDER_TARGET only, MiscFlags 0. + let p010_desc = D3D11_TEXTURE2D_DESC { + Width: w, + Height: h, + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_P010, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: D3D11_BIND_RENDER_TARGET.0 as u32, + ..Default::default() + }; + let mut p010: Option = None; + device + .CreateTexture2D(&p010_desc, None, Some(&mut p010)) + .context("CreateTexture2D(P010 bars dst)")?; + let p010 = p010.context("null p010 tex")?; + + let conv = HdrP010Converter::new(&device)?; + conv.convert(&device, &context, &src_srv, &p010, w, h)?; + Ok((device, p010)) + } +} + #[cfg(target_os = "windows")] pub fn hdr_p010_selftest_at(w: u32, h: u32, vendor: Option) -> Result<()> { use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_UNKNOWN}; diff --git a/crates/pf-encode/Cargo.toml b/crates/pf-encode/Cargo.toml index 48be23a2..736c5d35 100644 --- a/crates/pf-encode/Cargo.toml +++ b/crates/pf-encode/Cargo.toml @@ -25,6 +25,11 @@ tracing = "0.1" # A test writer for the NVENC backend's unit tests (`with_test_writer().try_init()`). tracing-subscriber = { version = "0.3", features = ["env-filter"] } +[target.'cfg(target_os = "windows")'.dev-dependencies] +# The QSV live e2e drives the REAL HdrP010Converter output (an RTV-written, ring-profile P010 +# texture) into the encoder — the one seam the CPU-upload tests can't reach. +pf-capture = { path = "../pf-capture" } + [target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies] # Software H.264 (openh264, BSD-2) — the GPU-less encode path on both platforms. openh264 = "0.9" diff --git a/crates/pf-encode/src/enc/windows/qsv.rs b/crates/pf-encode/src/enc/windows/qsv.rs index 121b73d6..c90dadf6 100644 --- a/crates/pf-encode/src/enc/windows/qsv.rs +++ b/crates/pf-encode/src/enc/windows/qsv.rs @@ -2171,4 +2171,79 @@ mod tests { path.display() ); } + + /// The PRODUCTION host chain minus the IDD ring: the REAL `HdrP010Converter` renders the 8 + /// sRGB bars into a ring-profile P010 texture (`BIND_RENDER_TARGET` only — RTV-written, not + /// CPU-uploaded) on the VPL implementation's own adapter, and THAT texture goes through the + /// unaligned-height ingest copy into a Main10 encode. Dumped to + /// `%TEMP%\pf_qsv_conv_1080_bars.h265`; expected decode codes = the bars_pq2020 fixture set + /// (see `hdr_p010_convert_bars_on_luid`). + #[test] + fn qsv_live_hdr_converter_e2e_1080_dump() { + const W: u32 = 1920; + const H: u32 = 1080; + + init_tracing(); + let Ok((_l, impls)) = intel_loader() else { + eprintln!("skipping: no VPL loader"); + return; + }; + let Some(imp) = impls.iter().find(|i| i.luid_valid) else { + eprintln!("skipping: no Intel VPL implementation on this box"); + return; + }; + if !probe_can_encode_10bit(Codec::H265) { + eprintln!("skipping: this GPU declines 10-bit HEVC"); + return; + } + let (device, tex) = pf_capture::dxgi::hdr_p010_convert_bars_on_luid(imp.luid, W, H) + .expect("converter bars"); + + let mut enc = QsvEncoder::open( + Codec::H265, + PixelFormat::P010, + W, + H, + 30, + 10_000_000, + 10, + ChromaFormat::Yuv420, + ) + .expect("open"); + enc.set_hdr_meta(Some(test_hdr_meta())); + let mut stream = Vec::new(); + let mut aus = 0usize; + for i in 0..12u32 { + let frame = CapturedFrame { + width: W, + height: H, + pts_ns: i as u64 * 33_333_333, + format: PixelFormat::P010, + payload: FramePayload::D3d11(pf_frame::dxgi::D3d11Frame { + texture: tex.clone(), + device: device.clone(), + pyro: None, + }), + cursor: None, + }; + enc.submit_indexed(&frame, i).expect("submit"); + if let Some(au) = enc.poll().expect("poll") { + aus += 1; + stream.extend_from_slice(&au.data); + } + } + enc.flush().expect("flush"); + while let Some(au) = enc.poll().expect("drain") { + aus += 1; + stream.extend_from_slice(&au.data); + } + assert!(aus >= 10, "expected ≥10 AUs, got {aus}"); + let path = std::env::temp_dir().join("pf_qsv_conv_1080_bars.h265"); + std::fs::write(&path, &stream).expect("write dump"); + println!( + "wrote {aus} AUs ({} bytes) to {}", + stream.len(), + path.display() + ); + } }