test(qsv): live on-glass matrix — HEVC, Main10+HDR, AV1 10-bit, LTR-RFI, no-IDR retarget
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1594,32 +1594,74 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Live encode smoke on real Intel hardware: skipped (cleanly) when no Intel VPL
|
fn init_tracing() {
|
||||||
/// implementation exists. Drives NV12 frames from a synthetic D3D11 texture through
|
// Mirror the NVENC tests: visible encoder logs under --nocapture (the LTR accept/
|
||||||
/// submit/poll and checks Annex-B-looking output. Run on the Intel box with
|
// reject and array-texture warnings are the on-glass diagnostics).
|
||||||
/// `cargo test -p pf-encode --features qsv qsv_encode_live -- --nocapture`.
|
let _ = tracing_subscriber::fmt()
|
||||||
#[test]
|
.with_env_filter("pf_encode=debug")
|
||||||
fn qsv_encode_live_smoke() {
|
.with_test_writer()
|
||||||
|
.try_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-AU facts the live matrix asserts on.
|
||||||
|
struct AuMeta {
|
||||||
|
keyframe: bool,
|
||||||
|
recovery_anchor: bool,
|
||||||
|
annexb_start: bool,
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_hdr_meta() -> punktfunk_core::quic::HdrMeta {
|
||||||
|
punktfunk_core::quic::HdrMeta {
|
||||||
|
display_primaries: [[13250, 34500], [7500, 3000], [34000, 16000]], // G, B, R
|
||||||
|
white_point: [15635, 16450],
|
||||||
|
max_display_mastering_luminance: 10_000_000, // 1000 nits in 0.0001 cd/m²
|
||||||
|
min_display_mastering_luminance: 500, // 0.05 nits
|
||||||
|
max_cll: 1000,
|
||||||
|
max_fall: 400,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drive a live encode on the box's Intel implementation. `None` = no usable hardware for
|
||||||
|
/// this (codec, bit depth) — the caller's test skips cleanly. `on_frame` runs before each
|
||||||
|
/// submit (the seam for RFI/retarget mid-stream actions).
|
||||||
|
fn drive_live(
|
||||||
|
codec: Codec,
|
||||||
|
ten_bit: bool,
|
||||||
|
frames: u32,
|
||||||
|
mut on_frame: impl FnMut(&mut QsvEncoder, u32),
|
||||||
|
) -> Option<Vec<AuMeta>> {
|
||||||
use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_UNKNOWN;
|
use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_UNKNOWN;
|
||||||
use windows::Win32::Graphics::Direct3D11::{
|
use windows::Win32::Graphics::Direct3D11::{
|
||||||
D3D11CreateDevice, D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE,
|
D3D11CreateDevice, D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE,
|
||||||
D3D11_SDK_VERSION, D3D11_USAGE_DEFAULT,
|
D3D11_SDK_VERSION, D3D11_USAGE_DEFAULT,
|
||||||
};
|
};
|
||||||
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_NV12, DXGI_SAMPLE_DESC};
|
use windows::Win32::Graphics::Dxgi::Common::{
|
||||||
|
DXGI_FORMAT_NV12, DXGI_FORMAT_P010, DXGI_SAMPLE_DESC,
|
||||||
|
};
|
||||||
use windows::Win32::Graphics::Dxgi::{CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory4};
|
use windows::Win32::Graphics::Dxgi::{CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory4};
|
||||||
|
|
||||||
|
init_tracing();
|
||||||
let Ok((_l, impls)) = intel_loader() else {
|
let Ok((_l, impls)) = intel_loader() else {
|
||||||
eprintln!("skipping: no VPL loader");
|
eprintln!("skipping: no VPL loader");
|
||||||
return;
|
return None;
|
||||||
};
|
};
|
||||||
let Some(imp) = impls.iter().find(|i| i.luid_valid) else {
|
let Some(imp) = impls.iter().find(|i| i.luid_valid) else {
|
||||||
eprintln!("skipping: no Intel VPL implementation on this box");
|
eprintln!("skipping: no Intel VPL implementation on this box");
|
||||||
return;
|
return None;
|
||||||
};
|
};
|
||||||
|
if !probe_can_encode(codec) {
|
||||||
|
eprintln!("skipping: this GPU declines {codec:?} encode");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if ten_bit && !probe_can_encode_10bit(codec) {
|
||||||
|
eprintln!("skipping: this GPU declines 10-bit {codec:?}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
// A device on the Intel adapter the implementation reported.
|
// A device on the Intel adapter the implementation reported.
|
||||||
// SAFETY: self-contained probe owning every COM handle it creates; `EnumAdapterByLuid`
|
// SAFETY: self-contained harness owning every COM handle it creates; `EnumAdapterByLuid`
|
||||||
// gets the LUID the runtime itself reported; `D3D11CreateDevice` fills `device` only
|
// gets the LUID the runtime itself reported; `D3D11CreateDevice` fills `device` only
|
||||||
// on success; the NV12 texture is created and used on that one device/thread.
|
// on success; the NV12/P010 texture is created and used on that one device/thread.
|
||||||
let (device, tex) = unsafe {
|
let (device, tex) = unsafe {
|
||||||
let luid = windows::Win32::Foundation::LUID {
|
let luid = windows::Win32::Foundation::LUID {
|
||||||
LowPart: u32::from_le_bytes(imp.luid[..4].try_into().unwrap()),
|
LowPart: u32::from_le_bytes(imp.luid[..4].try_into().unwrap()),
|
||||||
@@ -1646,7 +1688,11 @@ mod tests {
|
|||||||
Height: 480,
|
Height: 480,
|
||||||
MipLevels: 1,
|
MipLevels: 1,
|
||||||
ArraySize: 1,
|
ArraySize: 1,
|
||||||
Format: DXGI_FORMAT_NV12,
|
Format: if ten_bit {
|
||||||
|
DXGI_FORMAT_P010
|
||||||
|
} else {
|
||||||
|
DXGI_FORMAT_NV12
|
||||||
|
},
|
||||||
SampleDesc: DXGI_SAMPLE_DESC {
|
SampleDesc: DXGI_SAMPLE_DESC {
|
||||||
Count: 1,
|
Count: 1,
|
||||||
Quality: 0,
|
Quality: 0,
|
||||||
@@ -1659,27 +1705,44 @@ mod tests {
|
|||||||
let mut t: Option<ID3D11Texture2D> = None;
|
let mut t: Option<ID3D11Texture2D> = None;
|
||||||
device
|
device
|
||||||
.CreateTexture2D(&desc, None, Some(&mut t))
|
.CreateTexture2D(&desc, None, Some(&mut t))
|
||||||
.expect("nv12 texture");
|
.expect("input texture");
|
||||||
(device.clone(), t.expect("texture"))
|
(device.clone(), t.expect("texture"))
|
||||||
};
|
};
|
||||||
|
let format = if ten_bit {
|
||||||
|
PixelFormat::P010
|
||||||
|
} else {
|
||||||
|
PixelFormat::Nv12
|
||||||
|
};
|
||||||
let mut enc = QsvEncoder::open(
|
let mut enc = QsvEncoder::open(
|
||||||
Codec::H264,
|
codec,
|
||||||
PixelFormat::Nv12,
|
format,
|
||||||
640,
|
640,
|
||||||
480,
|
480,
|
||||||
30,
|
30,
|
||||||
2_000_000,
|
2_000_000,
|
||||||
8,
|
if ten_bit { 10 } else { 8 },
|
||||||
ChromaFormat::Yuv420,
|
ChromaFormat::Yuv420,
|
||||||
)
|
)
|
||||||
.expect("open");
|
.expect("open");
|
||||||
let mut aus = 0usize;
|
if ten_bit {
|
||||||
for i in 0..30u32 {
|
enc.set_hdr_meta(Some(test_hdr_meta()));
|
||||||
|
}
|
||||||
|
let mut aus = Vec::new();
|
||||||
|
let mut push = |au: EncodedFrame| {
|
||||||
|
aus.push(AuMeta {
|
||||||
|
keyframe: au.keyframe,
|
||||||
|
recovery_anchor: au.recovery_anchor,
|
||||||
|
annexb_start: au.data.starts_with(&[0, 0, 0, 1]) || au.data.starts_with(&[0, 0, 1]),
|
||||||
|
len: au.data.len(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
for i in 0..frames {
|
||||||
|
on_frame(&mut enc, i);
|
||||||
let frame = CapturedFrame {
|
let frame = CapturedFrame {
|
||||||
width: 640,
|
width: 640,
|
||||||
height: 480,
|
height: 480,
|
||||||
pts_ns: i as u64 * 33_333_333,
|
pts_ns: i as u64 * 33_333_333,
|
||||||
format: PixelFormat::Nv12,
|
format,
|
||||||
payload: FramePayload::D3d11(pf_frame::dxgi::D3d11Frame {
|
payload: FramePayload::D3d11(pf_frame::dxgi::D3d11Frame {
|
||||||
texture: tex.clone(),
|
texture: tex.clone(),
|
||||||
device: device.clone(),
|
device: device.clone(),
|
||||||
@@ -1688,20 +1751,116 @@ mod tests {
|
|||||||
};
|
};
|
||||||
enc.submit_indexed(&frame, i).expect("submit");
|
enc.submit_indexed(&frame, i).expect("submit");
|
||||||
if let Some(au) = enc.poll().expect("poll") {
|
if let Some(au) = enc.poll().expect("poll") {
|
||||||
if aus == 0 {
|
push(au);
|
||||||
assert!(au.keyframe, "first AU must be an IDR");
|
|
||||||
assert!(
|
|
||||||
au.data.starts_with(&[0, 0, 0, 1]) || au.data.starts_with(&[0, 0, 1]),
|
|
||||||
"AU is not Annex-B"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
aus += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
enc.flush().expect("flush");
|
enc.flush().expect("flush");
|
||||||
while let Some(_au) = enc.poll().expect("drain") {
|
while let Some(au) = enc.poll().expect("drain") {
|
||||||
aus += 1;
|
push(au);
|
||||||
}
|
}
|
||||||
assert!(aus >= 25, "expected ~30 AUs, got {aus}");
|
Some(aus)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_stream_shape(aus: &[AuMeta], frames: u32, annexb: bool) {
|
||||||
|
assert!(
|
||||||
|
aus.len() >= frames as usize - 5,
|
||||||
|
"expected ~{frames} AUs, got {}",
|
||||||
|
aus.len()
|
||||||
|
);
|
||||||
|
assert!(aus[0].keyframe, "first AU must be a keyframe");
|
||||||
|
assert!(aus[0].len > 0);
|
||||||
|
if annexb {
|
||||||
|
assert!(aus[0].annexb_start, "first AU is not Annex-B");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// H.264 8-bit — the Phase-0 spike (design §6).
|
||||||
|
#[test]
|
||||||
|
fn qsv_encode_live_smoke() {
|
||||||
|
let Some(aus) = drive_live(Codec::H264, false, 30, |_, _| {}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
assert_stream_shape(&aus, 30, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HEVC 8-bit.
|
||||||
|
#[test]
|
||||||
|
fn qsv_live_hevc() {
|
||||||
|
let Some(aus) = drive_live(Codec::H265, false, 30, |_, _| {}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
assert_stream_shape(&aus, 30, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HEVC Main10 (P010) with the HDR mastering/CLL grade attached — Phase 2 on-glass.
|
||||||
|
#[test]
|
||||||
|
fn qsv_live_hevc10_hdr() {
|
||||||
|
let Some(aus) = drive_live(Codec::H265, true, 30, |_, _| {}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
assert_stream_shape(&aus, 30, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AV1 10-bit (DG2/Arc + MTL+ only; skips elsewhere) — Phase 2 on-glass. AV1 output is an
|
||||||
|
/// OBU stream, not Annex-B.
|
||||||
|
#[test]
|
||||||
|
fn qsv_live_av1_10bit() {
|
||||||
|
let Some(aus) = drive_live(Codec::Av1, true, 30, |_, _| {}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
assert_stream_shape(&aus, 30, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// LTR-RFI end-to-end — Phase 3 on-glass: invalidate mid-stream and expect the clean
|
||||||
|
/// re-anchor P-frame (recovery_anchor, NOT a keyframe) instead of an IDR.
|
||||||
|
#[test]
|
||||||
|
fn qsv_live_ltr_rfi() {
|
||||||
|
let mut rfi_answered = false;
|
||||||
|
let Some(aus) = drive_live(Codec::H264, false, 60, |enc, i| {
|
||||||
|
if i == 30 && enc.caps().supports_rfi {
|
||||||
|
rfi_answered = enc.invalidate_ref_frames(28, 29);
|
||||||
|
}
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
assert_stream_shape(&aus, 60, true);
|
||||||
|
if !rfi_answered {
|
||||||
|
eprintln!("note: driver declined LTR (supports_rfi=false or no usable slot) — IDR fallback path");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let anchor = aus.iter().position(|a| a.recovery_anchor);
|
||||||
|
assert!(
|
||||||
|
anchor.is_some(),
|
||||||
|
"RFI was answered but no recovery_anchor AU was emitted"
|
||||||
|
);
|
||||||
|
let a = &aus[anchor.unwrap()];
|
||||||
|
assert!(
|
||||||
|
!a.keyframe,
|
||||||
|
"the recovery anchor must be a clean P-frame, not an IDR"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!aus[1..].iter().any(|x| x.keyframe),
|
||||||
|
"an IDR appeared despite successful LTR-RFI recovery"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No-IDR bitrate retarget — Phase 3 on-glass: `reconfigure_bitrate` mid-stream must be
|
||||||
|
/// accepted (HRD off + StartNewSequence=OFF) and must not emit a keyframe.
|
||||||
|
#[test]
|
||||||
|
fn qsv_live_bitrate_retarget() {
|
||||||
|
let mut accepted = false;
|
||||||
|
let Some(aus) = drive_live(Codec::H264, false, 60, |enc, i| {
|
||||||
|
if i == 30 {
|
||||||
|
accepted = enc.reconfigure_bitrate(6_000_000);
|
||||||
|
}
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
assert_stream_shape(&aus, 60, true);
|
||||||
|
assert!(accepted, "the no-IDR bitrate retarget was declined");
|
||||||
|
assert!(
|
||||||
|
!aus[1..].iter().any(|x| x.keyframe),
|
||||||
|
"the bitrate retarget emitted a keyframe (StartNewSequence leak)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user