test(qsv): converter→ring-profile-P010→encoder live e2e (the RTV-written seam)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Generated
+1
@@ -2881,6 +2881,7 @@ dependencies = [
|
|||||||
"libvpl-sys",
|
"libvpl-sys",
|
||||||
"nvidia-video-codec-sdk",
|
"nvidia-video-codec-sdk",
|
||||||
"openh264",
|
"openh264",
|
||||||
|
"pf-capture",
|
||||||
"pf-frame",
|
"pf-frame",
|
||||||
"pf-gpu",
|
"pf-gpu",
|
||||||
"pf-host-config",
|
"pf-host-config",
|
||||||
|
|||||||
@@ -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
|
/// 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
|
/// 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.
|
/// 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<ID3D11Device> = None;
|
||||||
|
let mut context: Option<ID3D11DeviceContext> = 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<ID3D11Texture2D> = 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<ID3D11ShaderResourceView> = 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<ID3D11Texture2D> = 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")]
|
#[cfg(target_os = "windows")]
|
||||||
pub fn hdr_p010_selftest_at(w: u32, h: u32, vendor: Option<u32>) -> Result<()> {
|
pub fn hdr_p010_selftest_at(w: u32, h: u32, vendor: Option<u32>) -> Result<()> {
|
||||||
use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_UNKNOWN};
|
use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_UNKNOWN};
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ tracing = "0.1"
|
|||||||
# A test writer for the NVENC backend's unit tests (`with_test_writer().try_init()`).
|
# A test writer for the NVENC backend's unit tests (`with_test_writer().try_init()`).
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
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]
|
[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies]
|
||||||
# Software H.264 (openh264, BSD-2) — the GPU-less encode path on both platforms.
|
# Software H.264 (openh264, BSD-2) — the GPU-less encode path on both platforms.
|
||||||
openh264 = "0.9"
|
openh264 = "0.9"
|
||||||
|
|||||||
@@ -2171,4 +2171,79 @@ mod tests {
|
|||||||
path.display()
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user