diff --git a/crates/pf-client-core/src/video_d3d11_native.rs b/crates/pf-client-core/src/video_d3d11_native.rs index 7e04c9de..bdbb15d0 100644 --- a/crates/pf-client-core/src/video_d3d11_native.rs +++ b/crates/pf-client-core/src/video_d3d11_native.rs @@ -1039,6 +1039,15 @@ mod parity { /// Both vendored vectors are 250 display frames. const FRAME_COUNT: usize = 250; + /// The Main 10 vector: 50 frames of 320x240 HEVC Main 10 4:2:0, generated by + /// libx265 and hashed from libavcodec's software decode as tightly packed P010. + /// Its provenance, the generation commands and the reason P010 rather than + /// `yuv420p10le` is the golden layout are all in the golden file's header. + const TEST_MAIN10_H265: &[u8] = include_bytes!("../../pf-vkdecode/tests/data/test-main10.h265"); + const GOLDENS_MAIN10: &str = + include_str!("../../pf-vkdecode/tests/data/test-main10.p010.sha256"); + const MAIN10_FRAME_COUNT: usize = 50; + /// The golden file's hash lines (comments and blanks skipped). fn golden_hashes(file: &'static str) -> Vec<&'static str> { file.lines() @@ -1344,11 +1353,19 @@ mod parity { /// Decode `aus` through a real `NativeD3d11Decoder`, hash every picture, and /// compare the planner's display order against libavcodec's goldens. - fn parity_run(codec: Codec, aus: &[&[u8]], order: &Order, goldens: &[&str], label: &str) { + fn parity_run( + codec: Codec, + stream: StreamFormat, + aus: &[&[u8]], + order: &Order, + goldens: &[&str], + expected_aus: usize, + label: &str, + ) { assert_eq!( aus.len(), - FRAME_COUNT, - "{label}: the vector must split into {FRAME_COUNT} access units — a \ + expected_aus, + "{label}: the vector must split into {expected_aus} access units — a \ different count means this file's splitter disagrees with pf-bitstream's, \ and nothing below it is meaningful" ); @@ -1361,7 +1378,7 @@ mod parity { ); let luid = pinned_adapter(); - let mut decoder = NativeD3d11Decoder::new(codec, StreamFormat::SDR_420_8, luid, false) + let mut decoder = NativeD3d11Decoder::new(codec, stream, luid, false) .unwrap_or_else(|e| panic!("{label}: the box must host this profile — {e:#}")); let mut readback = Readback { ctx: decoder.context.clone(), @@ -1422,9 +1439,11 @@ mod parity { let order = order_h264(&aus); parity_run( Codec::H264, + StreamFormat::SDR_420_8, &aus, &order, &golden_hashes(GOLDENS_H264), + FRAME_COUNT, "H.264", ); } @@ -1436,13 +1455,45 @@ mod parity { let order = order_h265(&aus); parity_run( Codec::H265, + StreamFormat::SDR_420_8, &aus, &order, &golden_hashes(GOLDENS_H265), + FRAME_COUNT, "H.265", ); } + /// The ten-bit path, which no golden set in this program covered until now. + /// + /// The HDR legs proved a Main10 session BUILDS and streams clean, which is a + /// weaker claim than it looks: D3D11VA exposes no per-picture status query, so a + /// Main10 stream decoding to garbage logs exactly as cleanly as one decoding + /// correctly. This is the leg that can tell them apart. + /// + /// It also exercises geometry the 8-bit legs cannot: P010 samples are two bytes, + /// so a row is `width * 2`, and HEVC's 128-line granule pads a 240-line picture + /// to a 256-line surface — the chroma plane therefore starts a long way from + /// where the display height would put it. + #[test] + #[ignore = "needs a Windows D3D11 video device (see module docs)"] + fn main10_every_frame_hashes_bit_identical_to_libavcodec() { + let aus = split_h265_aus(TEST_MAIN10_H265); + let order = order_h265(&aus); + parity_run( + Codec::H265, + StreamFormat { + chroma_format_idc: 1, + bit_depth: 10, + }, + &aus, + &order, + &golden_hashes(GOLDENS_MAIN10), + MAIN10_FRAME_COUNT, + "HEVC Main 10", + ); + } + // --------------------------------------------------------------------- // CPU guards — NOT `#[ignore]`d, so ordinary CI notices when this file's // splitter or the goldens drift away from pf-bitstream. @@ -1471,6 +1522,45 @@ mod parity { ); } + #[test] + fn the_main10_vector_really_is_ten_bit() { + let aus = split_h265_aus(TEST_MAIN10_H265); + assert_eq!( + aus.len(), + MAIN10_FRAME_COUNT, + "the Main 10 vector is {MAIN10_FRAME_COUNT} access units" + ); + let order = order_h265(&aus); + assert_eq!( + order.display.len(), + golden_hashes(GOLDENS_MAIN10).len(), + "the planner's output count must match the Main 10 golden count" + ); + + // The point of the leg. A regenerated vector that came out 8-bit would make + // `main10_every_frame_hashes_bit_identical_to_libavcodec` a second run of the + // 8-bit path wearing a ten-bit name — and it would pass, because the goldens + // would have been regenerated alongside it. + let mut planner = H265Planner::new(); + let plan = planner + .plan_au(aus[0]) + .expect("the Main 10 vector's first access unit must plan"); + assert_eq!( + ( + plan.picture.chroma_format_idc, + plan.picture.bit_depth_luma_minus8, + plan.picture.bit_depth_chroma_minus8 + ), + (1, 2, 2), + "the Main 10 vector must be 4:2:0 at ten bits" + ); + assert_eq!( + (plan.picture.coded_width, plan.picture.coded_height), + (320, 240), + "the golden frame size is 320x240" + ); + } + #[test] fn both_vendored_vectors_really_do_reorder() { // The module docs claim the harness must reorder because these vectors do. If