Merge branch 'fix/hevc-lowdelay-parity-gate' into integration/decode-aliasing-program

This commit is contained in:
2026-08-07 23:12:22 +02:00
9 changed files with 1446 additions and 37 deletions
+191 -26
View File
@@ -33,6 +33,15 @@
//! ([`pf_dxvadec::DecodePlanDxva::release_after_decode`]), and the stream is now
//! vendored so `low_delay_host_h264_every_frame_hashes_bit_identical_to_libavcodec`
//! holds the rung to what it streams rather than only to what it conforms to.
//!
//! HEVC is EXEMPT from that defect, and since 2026-08-07 that is a measurement rather
//! than an argument: `H265Planner` snapshots `dpb_refs` after `decode_rps`, so an
//! RPS-dropped picture never reaches `RefPicList`, and a vendored low-delay HEVC
//! stream from the same host confirms it — 115 of its 120 access units retire a
//! picture, 0 alias, and all 115 WOULD alias if the snapshot moved one call earlier.
//! `low_delay_host_h265_every_frame_hashes_bit_identical_to_libavcodec` is the pixel
//! leg; pf-dxvadec's `pic_h265` tests pin the numbers and drive the counterfactual
//! through the conversion.
//! * **AV1** — wired in M7, and frame-hash parity on the SAME two GPUs since 2026-08-07:
//! 250/250 delivered frames bit-identical to libavcodec on the RTX 3500 Ada and on the
//! Intel Arc. It streams 4K60 on both with a clean 5-minute soak, but that is throughput
@@ -46,8 +55,21 @@
//! [`NativeD3d11Decoder::frame_av1`] now applies the conversion's
//! `release_after_decode` once the decode op is issued.
//!
//! ⚠ Still no SOAK on the goldens, so this leg's evidence is one 250-frame vector on two
//! vendors — narrower than the H.264/H.265 legs above.
//! Since 2026-08-07 a SECOND AV1 stream runs beside the vector: our own host's 4K
//! output, `low_delay_host_av1_every_frame_hashes_bit_identical_to_libavcodec`. Not for
//! the aliasing — the vector covers that better than any host stream could — but
//! because every frame of the vector is `tile_cols = tile_rows = 1`, so every tile
//! array `plan_to_dxva_av1` fills had only ever been written at index 0. Our encoder
//! splits 4K into two tile rows carried in one Tile Group OBU, which is two tile
//! RECORDS from one group; 1440p and below measured single-tile, so 4K is the only
//! shape that has it.
//!
//! ⚠ Still no SOAK on the goldens, so this leg's evidence is two vendored streams on two
//! vendors — narrower than the H.264/H.265 legs above. ⚠⚠ And both are FILES. "250/250
//! delivered frames bit-identical" was true for the entire period the host was shipping
//! only the FIRST TILE of every 4K frame: that verification ran against a vendored file
//! while the truncation lived in packetisation, and this suite stayed green throughout.
//! Nothing here covers fragmentation, reassembly or loss.
//!
//! A refusal or an init failure logs and falls through to the standard ladder, so neither the
//! pin nor the `auto` admission can cost a session its decoder.
@@ -1767,6 +1789,32 @@ mod parity {
const GOLDENS_H265: &str =
include_str!("../../pf-vkdecode/tests/data/test-25fps-h265.nv12.sha256");
/// **Our own host's low-delay HEVC** and its goldens — the H.265 twin of
/// [`LOWDELAY_H264`], vendored for the opposite reason.
///
/// The H.264 stream is here because this rung was WRONG and only that shape could
/// show it. This one is here because HEVC is believed RIGHT — `H265Planner`
/// snapshots `dpb_refs` after `decode_rps`, so an RPS-dropped picture is never in
/// the marked set `RefPicList` is built from, and `plan_to_dxva_h265` is the one
/// conversion of the three that still releases inline. 120 pictures of 640x480
/// IPPP, `sps_max_num_reorder_pics = 0`, a five-picture DPB against four marked
/// references: 115 of the 120 access units retire a picture, `removed ∩ dpb_refs`
/// is 0 of 120, and all 115 would alias under the other snapshot ordering
/// (pf-dxvadec's `pic_h265` tests pin every one of those numbers, and drive the
/// counterfactual through the conversion itself).
///
/// Provenance, the `punktfunk-host spike` command and the ffmpeg cross-check are in
/// the golden file's header.
const LOWDELAY_H265: &[u8] =
include_bytes!("../../pf-vkdecode/tests/data/lowdelay-640x480.h265");
const GOLDENS_LOWDELAY_H265: &str =
include_str!("../../pf-vkdecode/tests/data/lowdelay-640x480-h265.nv12.sha256");
/// The HEVC low-delay stream's frame count. A separate constant from
/// [`LOWDELAY_FRAME_COUNT`] on purpose: two files, two encoder runs, and one
/// regenerated at another length must fail on its own leg.
const LOWDELAY_H265_FRAME_COUNT: usize = 120;
/// Both vendored vectors are 250 display frames.
const FRAME_COUNT: usize = 250;
@@ -1800,6 +1848,42 @@ mod parity {
const AV1_DECODED_COUNT: usize = 274;
const AV1_SHOWN_COUNT: usize = 250;
/// The vendored AV1 vector's render region, and what its goldens hash.
const DISPLAY_AV1: (u32, u32) = (320, 240);
/// **Our own host's AV1**, and the only stream this rung decodes with more than
/// ONE TILE.
///
/// Unlike the H.264 and H.265 low-delay siblings this is not about the
/// release-ordering defect — the vendored vector already aliases on 268 of its 274
/// frames, which is exactly why parity caught that one here. It closes a different
/// gap: no host-generated AV1 stream had pixel coverage anywhere, and our encoder's
/// AV1 is structurally unlike the vector. At 4K the split encode emits
/// `tile_cols = 1, tile_rows = 2` — `height_in_sbs_minus_1 = [16, 16]` — with both
/// tiles in a SINGLE Tile Group OBU. 1440p and below measured single-tile, so 4K is
/// the only shape that has the property; 60 frames rather than 120 pays for it, at
/// 261 KB.
///
/// ⚠ A file fixture is not the wire path, and on AV1 that distinction has already
/// cost a release. "250/250 delivered frames bit-identical" was true for the whole
/// period the host was shipping only the first tile of every 4K frame — the
/// verification ran against a vendored file and the truncation lived in
/// packetisation. This leg gives the multi-tile shape pixel coverage on the DECODE
/// rung and proves nothing about fragmentation, reassembly or loss.
const LOWDELAY_AV1: &[u8] =
include_bytes!("../../pf-vkdecode/tests/data/lowdelay-3840x2160.ivf.av1");
const GOLDENS_LOWDELAY_AV1: &str =
include_str!("../../pf-vkdecode/tests/data/lowdelay-3840x2160-av1.nv12.sha256");
/// 60 units, 60 decoded, 60 shown — three constants, never derived from each
/// other. Our host emits one shown frame per temporal unit with no hidden frames
/// and no `show_existing_frame`, which is the OPPOSITE shape to the vendored
/// vector's 250 / 274 / 250 and the reason the harness takes all three.
const LOWDELAY_AV1_UNIT_COUNT: usize = 60;
const LOWDELAY_AV1_DECODED_COUNT: usize = 60;
const LOWDELAY_AV1_SHOWN_COUNT: usize = 60;
const DISPLAY_LOWDELAY_AV1: (u32, u32) = (3840, 2160);
/// The golden file's hash lines (comments and blanks skipped).
fn golden_hashes(file: &'static str) -> Vec<&'static str> {
file.lines()
@@ -2007,7 +2091,7 @@ mod parity {
/// the whole difference. `display` is still the planner's own output list;
/// AV1 has no bumping process, so a picture is output by the unit that shows
/// it and there is no flush to drain at the end.
fn order_av1(units: &[&[u8]]) -> Order {
fn order_av1(units: &[&[u8]], render: (u32, u32)) -> Order {
let mut planner = pf_dxvadec::Av1Planner::new();
let mut order = Order {
decode: Vec::new(),
@@ -2027,8 +2111,8 @@ mod parity {
);
assert_eq!(
(plan.picture.render_width, plan.picture.render_height),
(320, 240),
"unit {index}: the goldens are the 320x240 render region"
render,
"unit {index}: the goldens are the {render:?} render region"
);
if let Some(id) = plan.dpb.stored {
order.decode.push(id);
@@ -2315,18 +2399,45 @@ mod parity {
/// ⚠ Still unexercised, because the vendored vector has none:
/// `show_existing_frame`.
fn av1_parity_run(units: &[&[u8]], order: &Order, goldens: &[&str]) {
av1_parity_run_against(
units,
order,
goldens,
AV1_UNIT_COUNT,
AV1_DECODED_COUNT,
AV1_SHOWN_COUNT,
"AV1",
);
}
/// [`av1_parity_run`] with its stream's own counts, for the leg that does not
/// decode the vendored vector.
///
/// The three counts are three parameters, never derived from one another: the
/// vendored vector is 250 units / 274 decoded / 250 shown, and our host's stream is
/// 60 / 60 / 60. A harness that computed "hidden = 0" or "decoded = units" from
/// either would silently stop checking the other.
fn av1_parity_run_against(
units: &[&[u8]],
order: &Order,
goldens: &[&str],
unit_count: usize,
decoded_count: usize,
shown_count: usize,
label: &str,
) {
assert_eq!(
units.len(),
AV1_UNIT_COUNT,
"the IVF reader disagrees with the vector's temporal-unit count"
unit_count,
"{label}: the IVF reader disagrees with the stream's temporal-unit count"
);
assert_eq!(order.decode.len(), AV1_DECODED_COUNT);
assert_eq!(order.decode.len(), decoded_count);
assert_eq!(order.per_unit.len(), units.len());
assert_eq!(order.display.len(), goldens.len());
let luid = pinned_adapter();
let mut decoder = NativeD3d11Decoder::new(Codec::Av1, StreamFormat::SDR_420_8, luid, false)
.unwrap_or_else(|e| panic!("AV1: the box must host AV1 Profile 0 — {e:#}"));
.unwrap_or_else(|e| panic!("{label}: the box must host AV1 Profile 0 — {e:#}"));
let mut readback = Readback {
ctx: decoder.context.clone(),
staging: None,
@@ -2374,20 +2485,23 @@ mod parity {
decoded += 1;
}
}
assert_eq!(decoded, AV1_DECODED_COUNT);
assert_eq!(decoded, decoded_count);
assert_eq!(
presented, AV1_SHOWN_COUNT,
"every unit of this vector shows exactly one frame, so the production \
path must have handed back {AV1_SHOWN_COUNT} pictures"
presented, shown_count,
"{label}: every unit of this stream shows exactly one frame, so the \
production path must have handed back {shown_count} pictures"
);
let hidden = AV1_DECODED_COUNT - presented;
let hidden = decoded_count - presented;
assert_eq!(
hidden,
AV1_DECODED_COUNT - AV1_SHOWN_COUNT,
"the rung must have decoded 24 frames it never handed back — this counts \
what `decode_av1` RETURNED against what it decoded, so at zero the \
`!sub.show` suppression is not working (or this vector stopped hiding \
frames, which `the_av1_vector_hides_frames…` would catch first)"
decoded_count - shown_count,
"{label}: the rung must have decoded {} frames it never handed back — this \
counts what `decode_av1` RETURNED against what it decoded, so a mismatch \
on the vendored vector means the `!sub.show` suppression is not working \
(or it stopped hiding frames, which `the_av1_vector_hides_frames…` would \
catch first). On a stream with no hidden frames both sides are zero and \
this is a tautology — deliberately, so one harness serves both shapes",
decoded_count - shown_count
);
let mut mismatches = 0usize;
@@ -2397,7 +2511,7 @@ mod parity {
.unwrap_or_else(|| panic!("display frame {n} names PicId {id}, never decoded"));
if got != golden {
if mismatches < 10 {
eprintln!("AV1: display frame {n} (PicId {id}): {got} != {golden}");
eprintln!("{label}: display frame {n} (PicId {id}): {got} != {golden}");
}
mismatches += 1;
}
@@ -2405,15 +2519,15 @@ mod parity {
assert_eq!(
mismatches,
0,
"AV1: {mismatches}/{} frames diverge from libavcodec (first 10 above; frame \
"{label}: {mismatches}/{} frames diverge from libavcodec (first 10 above; frame \
0 is a key frame — if IT mismatches suspect the readback geometry \
(pitch/crop/plane offset) or the tile records rather than the reference \
handling)",
goldens.len()
);
eprintln!(
"AV1: {} delivered frames bit-identical to libavcodec, {hidden} hidden frames \
decoded and withheld",
"{label}: {} delivered frames bit-identical to libavcodec, {hidden} hidden \
frames decoded and withheld",
goldens.len()
);
}
@@ -2465,7 +2579,7 @@ mod parity {
#[ignore = "diagnostic, needs a Windows D3D11 video device (see module docs)"]
fn av1_divergence_map() {
let units = split_ivf(TEST_25FPS_AV1);
let order = order_av1(&units);
let order = order_av1(&units, DISPLAY_AV1);
let goldens = golden_hashes(GOLDENS_AV1);
// Plan facts per PicId, from a planner run alongside the decoder's own.
@@ -2593,10 +2707,35 @@ mod parity {
#[ignore = "needs a Windows D3D11 video device (see module docs)"]
fn av1_every_delivered_frame_hashes_bit_identical_to_libavcodec() {
let units = split_ivf(TEST_25FPS_AV1);
let order = order_av1(&units);
let order = order_av1(&units, DISPLAY_AV1);
av1_parity_run(&units, &order, &golden_hashes(GOLDENS_AV1));
}
/// **Our own host's AV1, at the only resolution where it emits more than one tile.**
///
/// The leg above runs a vector whose every frame is `tile_cols = tile_rows = 1`, so
/// every tile field `plan_to_dxva_av1` fills is the degenerate case. This stream is
/// `tile_rows = 2` on all 60 frames with both tiles in one Tile Group OBU, which is
/// the 4K split-encode shape the host actually ships — and it is 4K, so the
/// readback moves 12.4 MB per frame rather than 115 KB. See [`LOWDELAY_AV1`] for
/// what it does and does not cover; the short version is that it is a file, and the
/// last AV1 truncation lived somewhere a file cannot reach.
#[test]
#[ignore = "needs a Windows D3D11 video device (see module docs)"]
fn low_delay_host_av1_every_frame_hashes_bit_identical_to_libavcodec() {
let units = split_ivf(LOWDELAY_AV1);
let order = order_av1(&units, DISPLAY_LOWDELAY_AV1);
av1_parity_run_against(
&units,
&order,
&golden_hashes(GOLDENS_LOWDELAY_AV1),
LOWDELAY_AV1_UNIT_COUNT,
LOWDELAY_AV1_DECODED_COUNT,
LOWDELAY_AV1_SHOWN_COUNT,
"AV1 (low-delay host stream, 4K two-tile)",
);
}
#[test]
#[ignore = "needs a Windows D3D11 video device (see module docs)"]
fn h264_every_frame_hashes_bit_identical_to_libavcodec() {
@@ -2655,6 +2794,32 @@ mod parity {
);
}
/// The HEVC twin of the low-delay H.264 leg — and the one that keeps HEVC's
/// exemption from the release-ordering defect a standing hardware fact.
///
/// `h265_every_frame_hashes_bit_identical_to_libavcodec` decodes a vector that
/// REORDERS, so it never puts an RPS drop and the eviction it causes in one access
/// unit and cannot see this class at all. This stream does, on 115 of its 120
/// access units — see [`LOWDELAY_H265`]. If a refactor ever moved `H265Planner`'s
/// snapshot ahead of `decode_rps` (where the other two planners take theirs), this
/// rung would name one surface as both `CurrPic` and a `RefPicList` entry on all
/// 115, and this leg is what would say so in pixels.
#[test]
#[ignore = "needs a Windows D3D11 video device (see module docs)"]
fn low_delay_host_h265_every_frame_hashes_bit_identical_to_libavcodec() {
let aus = split_h265_aus(LOWDELAY_H265);
let order = order_h265(&aus);
parity_run(
Codec::H265,
StreamFormat::SDR_420_8,
&aus,
&order,
&golden_hashes(GOLDENS_LOWDELAY_H265),
LOWDELAY_H265_FRAME_COUNT,
"H.265 (low-delay host stream)",
);
}
/// 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
@@ -2756,7 +2921,7 @@ mod parity {
fn the_ivf_reader_agrees_with_the_planner_and_the_av1_goldens() {
let units = split_ivf(TEST_25FPS_AV1);
assert_eq!(units.len(), AV1_UNIT_COUNT, "AV1 temporal units");
let order = order_av1(&units);
let order = order_av1(&units, DISPLAY_AV1);
assert_eq!(
order.decode.len(),
AV1_DECODED_COUNT,
+130
View File
@@ -857,6 +857,19 @@ mod tests {
"../../pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1"
);
/// **Our own host's AV1 at 4K**, vendored beside the goldens the GPU legs decode it
/// against (`lowdelay-3840x2160-av1.nv12.sha256` carries the `punktfunk-host spike`
/// command and the ffmpeg cross-check).
///
/// It is here for ONE property the vendored vector cannot supply: **two tiles**.
/// Every frame of `test-25fps.ivf.av1` is `tile_cols = tile_rows = 1`, so
/// [`the_tile_sizes_are_superblock_counts_not_the_coded_minus_one`] can only ever
/// read index 0 of the tile arrays and assert the rest are zero. This stream is
/// `tile_cols = 1, tile_rows = 2` on all 60 frames, with both tiles in a single
/// Tile Group OBU — the 4K split-encode shape the host ships.
const LOWDELAY_3840X2160_AV1: &[u8] =
include_bytes!("../../pf-vkdecode/tests/data/lowdelay-3840x2160.ivf.av1");
/// Convert one plan the way the RUNG must: the conversion, then the releases it
/// defers past the decode op ([`DecodePlanDxvaAv1::release_after_decode`]).
///
@@ -1588,6 +1601,123 @@ mod tests {
assert_eq!(frames, 274);
}
/// The same tile arrays with a SECOND tile in them — the case the vendored vector
/// cannot reach and this conversion had therefore never been run against.
///
/// [`the_tile_sizes_are_superblock_counts_not_the_coded_minus_one`] asserts index 0
/// is right and `1..` are zero, which is everything a one-tile vector can say. Both
/// halves of that are shapes a multi-tile bug would satisfy: a conversion that
/// wrote only tile 0 and left the rest zero would pass it on every frame of the
/// vector and hand the driver a frame with half its height missing here.
///
/// So this pins the row arrays with both entries live, that the second entry is the
/// SECOND tile's size rather than a repeat of the first, and that everything past
/// the grid is still zero. `tile_rows = 2` with `height_in_sbs_minus_1 = [16, 16]`
/// against a 2160-line frame is 17 + 17 = 34 superblocks of 64, i.e. 2176 lines —
/// the padded height, which is the arithmetic the one-tile test's `div_ceil` also
/// checks but cannot check twice.
#[test]
fn a_two_tile_frame_fills_both_row_entries_and_leaves_the_rest_zero() {
let mut planner = Av1Planner::new();
let mut slots = SlotMap::new(NUM_REF_SLOTS);
let mut frames = 0u32;
for packet in IvfIterator::new(LOWDELAY_3840X2160_AV1) {
for plan in planner
.plan_au(packet)
.expect("the low-delay 4K stream plans")
{
if plan.dpb.stored.is_none() {
continue;
}
let dx = convert(packet, &plan, &mut slots);
frames += 1;
let t = &plan.header.tile_info;
// `#[repr(packed)]` — copy the block out before reading its arrays.
let tiles = dx.pic_params.tiles;
let (widths, heights) = (tiles.widths, tiles.heights);
assert_eq!(
(tiles.cols, tiles.rows),
(1, 2),
"frame {frames}: this stream is vendored FOR its second tile row. \
One tile here means it was regenerated below 4K (1440p and down \
measured single-tile) and this test has quietly become a duplicate \
of the vendored vector's"
);
let sb = if plan.sequence.use_128x128_superblock {
128
} else {
64
};
assert_eq!(
(widths[0], 0u16),
(plan.header.frame_width.div_ceil(sb) as u16, 0u16),
"frame {frames}: the single tile COLUMN spans the whole width"
);
// Both row entries, each the coded value plus one — and read
// independently, so a conversion that broadcast entry 0 across the
// array would still have to get entry 1's own coded value right.
assert_eq!(
(heights[0], heights[1]),
(
t.height_in_sbs_minus_1[0] as u16 + 1,
t.height_in_sbs_minus_1[1] as u16 + 1
),
"frame {frames}: each tile row's height is its OWN \
`height_in_sbs_minus_1 + 1`"
);
assert_eq!(
u32::from(heights[0]) + u32::from(heights[1]),
plan.header.frame_height.div_ceil(sb),
"frame {frames}: the two tile rows must tile the frame exactly — a \
short second row is a frame with missing lines, which is precisely \
the shape the host once shipped over the wire"
);
// Past the grid the arrays stay zero: a driver reading `rows` entries
// never sees them, and a phantom entry is a tile the frame has not.
assert!(widths[1..].iter().all(|w| *w == 0));
assert!(heights[2..].iter().all(|h| *h == 0));
// TWO tile RECORDS from ONE tile group, with distinct rows and
// non-empty spans. `tile_cols * tile_rows` records is libavcodec's own
// count, and this stream is the only one here where it exceeds the
// number of tile GROUPS — so a conversion that emitted one record per
// group (the coarser shape the module docs warn against) is
// indistinguishable from a correct one on the vendored vector and
// fails here.
assert_eq!(
plan.tiles.len(),
1,
"frame {frames}: both tiles arrive in one Tile Group OBU"
);
assert_eq!(
dx.tiles.len(),
2,
"frame {frames}: one record per TILE, not per tile group"
);
assert_eq!(
(dx.tiles[0].row, dx.tiles[0].column),
(0, 0),
"frame {frames}: tile 0 is row 0"
);
assert_eq!(
(dx.tiles[1].row, dx.tiles[1].column),
(1, 0),
"frame {frames}: tile 1 is the SECOND ROW of a single column — a \
(0, 1) here means rows and columns are transposed, which one \
square tile grid could never show"
);
assert!(
dx.tiles.iter().all(|r| r.data_size > 0),
"frame {frames}: every tile record must span real bytes; a \
zero-length second record is the whole bottom half of the frame \
missing"
);
}
}
assert_eq!(frames, 60, "the low-delay 4K stream is 60 coded frames");
}
/// Three fields whose correct value is a SENTINEL or a constant, on every frame
/// of the vector — none of which any other assertion here would notice.
///
+212 -6
View File
@@ -690,6 +690,18 @@ mod tests {
"../../pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/64x64-I-P-B-P.h265"
);
/// **Our own host's HEVC**, and the only stream in this repository that reaches
/// the DPB pressure HEVC's exemption is claimed against: low-delay IPPP, 120
/// pictures of 640x480, `sps_max_num_reorder_pics = 0`, and a five-picture DPB
/// against the four pictures 8.3.2 keeps marked.
///
/// Vendored beside the goldens the GPU legs decode it against (that file's header
/// carries the `punktfunk-host spike` command and the ffmpeg cross-check), the
/// same way `lowdelay-640x480.h264` is, and read from the same path by all three
/// crates that need it.
const LOWDELAY_640X480_H265: &[u8] =
include_bytes!("../../pf-vkdecode/tests/data/lowdelay-640x480.h265");
/// Test-only AU splitter, mirroring pf-vkdecode's (which mirrors
/// pf-bitstream's `#[cfg(test)]`-private helper).
fn split_into_aus(stream: &[u8]) -> Vec<&[u8]> {
@@ -936,12 +948,12 @@ mod tests {
/// leave the first assertion passing and break the second on the first AU whose
/// RPS drops a picture, which on this vector is most of them.
///
/// ⚠ No low-delay HEVC stream is vendored, so unlike H.264 this is not backed by a
/// hardware leg on our own encoder's output. It was MEASURED once, 2026-08-07, on
/// a 300-picture 1080p low-delay HEVC stream from a punktfunk host: 0 access units
/// with a `removed ∩ dpb_refs` intersection, against 297 of 300 for H.264 from the
/// same host and the same run. Vendoring that stream is the way to make this a
/// standing guarantee rather than a re-derivable argument.
/// This test covers the VENDORED VECTOR only, which reorders and therefore cannot
/// reach the DPB pressure the exemption is really claimed against. The stream that
/// can is [`LOWDELAY_640X480_H265`], and it carries its own pair of tests below —
/// [`the_low_delay_stream_reaches_the_dpb_pressure_and_hevc_still_does_not_alias`]
/// and [`the_low_delay_stream_would_alias_if_the_snapshot_moved_ahead_of_the_rps`],
/// the second of which drives the alias through this very conversion.
#[test]
fn the_current_picture_is_named_by_curr_pic_and_never_aliases_a_reference() {
let mut aus_with_removals = 0usize;
@@ -982,6 +994,200 @@ mod tests {
);
}
/// The marked DPB as an access unit's `decode_rps` FINDS it — the set
/// `dpb_snapshot()` would return from the other side of that call.
///
/// Exact, not approximate. `H265Planner::begin_picture` runs `decode_rps` →
/// `update_dpb_before_decoding` → `dpb_snapshot`, and the only thing between AU
/// N-1's snapshot and AU N's `decode_rps` is `finish_picture(N-1)` storing its
/// picture marked "used for short-term reference". So the pre-RPS marked set is
/// exactly `dpb_refs(N-1) {stored(N-1)}` — no DPB replay needed, and no
/// dependence on the planner internals staying reachable from a test.
///
/// A sub-layer non-reference picture is stored but NOT marked, so it is excluded;
/// the callers assert their streams contain none, which keeps the reconstruction
/// honest rather than merely defensive.
fn pre_rps_marked(prev: Option<&AuPlan>) -> Vec<RefPic> {
let Some(prev) = prev else {
return Vec::new();
};
let mut marked = prev.dpb_refs.clone();
if let Some(id) = prev.dpb.stored {
if prev.picture.is_reference {
marked.push(RefPic {
id,
pic_order_cnt: prev.picture.pic_order_cnt,
is_long_term: false,
});
}
}
marked
}
/// The exemption, measured on the stream that can actually falsify it.
///
/// [`the_current_picture_is_named_by_curr_pic_and_never_aliases_a_reference`] runs
/// over `test-25fps.h265`, which REORDERS — a picture the RPS drops stays alive for
/// output past the access unit that dropped it, so the eviction and the unmarking
/// never land together and the vector cannot reach the precondition however hard it
/// is run. That is the same blindness that let the H.264 defect survive two
/// milestones behind a 250/250 green vector.
///
/// This stream reaches it. Three numbers, and the third is what gives the second
/// its meaning:
///
/// - **115 of 120** access units retire a picture (a five-picture DPB against four
/// marked references and `sps_max_num_reorder_pics = 0`);
/// - **0** of those retirements intersect the access unit's own `dpb_refs` — the
/// exemption, measured on our own encoder's output rather than argued;
/// - **115** of them intersect the PRE-RPS marked set, so a snapshot taken one call
/// earlier would alias on every single one.
///
/// [`the_low_delay_stream_would_alias_if_the_snapshot_moved_ahead_of_the_rps`]
/// then drives that counterfactual through the conversion itself.
#[test]
fn the_low_delay_stream_reaches_the_dpb_pressure_and_hevc_still_does_not_alias() {
let converted = convert_stream(LOWDELAY_640X480_H265);
assert_eq!(converted.len(), 120, "the low-delay stream is 120 pictures");
let mut with_removals = 0usize;
let mut both = 0usize;
let mut would_alias = 0usize;
for (i, (plan, dxva)) in converted.iter().enumerate() {
// The consequence, over every access unit of the stream: the decode target
// is `CurrPic` and appears in no reference entry.
assert_eq!(dxva.pic_params.CurrPic.index(), dxva.setup_slot);
assert!(!dxva.pic_params.CurrPic.associated());
for r in &dxva.refs {
assert_ne!(
r.slot, dxva.setup_slot,
"AU {i}: reference picture {} shares surface {} with the decode \
target HEVC has acquired the H.264/AV1 defect",
r.id, r.slot
);
}
assert!(
plan.picture.is_reference,
"AU {i}: this stream carries no sub-layer non-reference pictures, \
which is what makes `pre_rps_marked` exact"
);
if !plan.dpb.removed.is_empty() {
with_removals += 1;
}
both += plan
.dpb
.removed
.iter()
.filter(|id| plan.dpb_refs.iter().any(|r| r.id == **id))
.count();
let before = pre_rps_marked(i.checked_sub(1).map(|prev| &converted[prev].0));
would_alias += plan
.dpb
.removed
.iter()
.filter(|id| before.iter().any(|r| r.id == **id))
.count();
}
assert_eq!(
with_removals, 115,
"the stream must still retire a picture on nearly every access unit; \
without that both numbers below are trivially zero"
);
assert_eq!(
both, 0,
"{both} picture(s) are in an access unit's own marked DPB AND removed by \
it the H.264/AV1 aliasing precondition, which HEVC is supposed to be \
structurally incapable of. `H265Planner`'s snapshot has moved ahead of \
`decode_rps`. Restore the ordering, or give this conversion the \
`release_after_decode` deferral the other two carry; do NOT relax this"
);
assert_eq!(
would_alias, 115,
"the fixture must stay CAPABLE of exposing the defect it rules out. A \
regenerated stream that reordered, or whose DPB was deeper than its \
reference count, would report 0 here and the zero above would then \
prove exactly as much as `test-25fps.h264`'s zero proved, which was nothing"
);
}
/// The counterfactual driven through the CONVERSION, not just the planner's
/// arithmetic.
///
/// `H265Planner` snapshotting one call earlier is a plausible refactor — it is
/// where `H264Planner` and `Av1Planner` both snapshot, and both needed
/// `release_after_decode` because of it. This test simulates exactly that by
/// handing `plan_to_dxva_h265` the PRE-RPS marked set as `dpb_refs` and nothing
/// else changed, then asserts the alias appears: the dropped picture enters
/// `RefPicList` as a *Foll* entry with its slot resolved BEFORE the removals are
/// released, `SlotMap::assign` hands that freed slot straight to `CurrPic`, and one
/// surface is named as both the decode target and a picture the frame predicts from.
///
/// So the guarantee is not "we looked and it was fine". It is: this stream reaches
/// the shape, this conversion breaks on it under the other snapshot ordering, and
/// the ordering we ship is why it does not.
#[test]
fn the_low_delay_stream_would_alias_if_the_snapshot_moved_ahead_of_the_rps() {
let mut planner = H265Planner::new();
let mut slots: Option<SlotMap> = None;
let mut plans: Vec<AuPlan> = Vec::new();
let mut aliased = 0usize;
let mut converted = 0usize;
for (i, au) in split_into_aus(LOWDELAY_640X480_H265)
.into_iter()
.enumerate()
{
let plan = planner.plan_au(au).expect("the low-delay stream plans");
let map = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames));
// The ONE mutation: the marked DPB as it stood before this AU's RPS ran.
let mut as_if = plan.clone();
as_if.dpb_refs = pre_rps_marked(plans.last());
// The reconstruction validates itself, so a planner change that broke the
// reasoning behind `pre_rps_marked` fails HERE with its reason rather than
// quietly turning the count below into a different measurement. Marking
// only ever grows between two access units' RPS derivations (the previous
// picture is stored marked; C.5.2.2's removal takes only already-unmarked
// pictures), so the pre-RPS set is a strict SUPERSET of the post-RPS one.
for rp in plan.dpb_refs.iter().chain(
as_if
.rps
.st_curr_before
.iter()
.chain(&as_if.rps.st_curr_after)
.chain(&as_if.rps.lt_curr),
) {
assert!(
as_if.dpb_refs.iter().any(|r| r.id == rp.id),
"AU {i}: picture {} is in the post-RPS marked DPB (or a current \
set) but not in the reconstructed pre-RPS one marking is no \
longer monotone across an access unit boundary, and this test is \
measuring a different mutation than the one it documents",
rp.id
);
}
let dxva = plan_to_dxva_h265(&as_if, map, i as u32 + 1).expect("conversion");
converted += 1;
if dxva.refs.iter().any(|r| r.slot == dxva.setup_slot) {
aliased += 1;
}
plans.push(plan);
}
assert_eq!(converted, 120);
assert_eq!(
aliased, 115,
"the pre-RPS snapshot must alias on every access unit that retires a \
picture. {aliased} of 120 did if this is 0, the stream no longer \
reaches the shape and the exemption asserted by the test above is \
unfalsifiable again; regenerate the fixture rather than relaxing this"
);
}
#[test]
fn the_irap_picture_sets_all_three_picture_type_flags_and_the_others_set_none() {
let converted = convert_stream(TEST_25FPS);
+89
View File
@@ -534,6 +534,95 @@ mod tests {
aus
}
/// **Our own host's low-delay HEVC**, vendored beside the goldens the GPU legs
/// decode it against (`tests/data/lowdelay-640x480-h265.nv12.sha256` carries the
/// `punktfunk-host spike` command and the ffmpeg cross-check). 120 pictures of
/// 640x480 IPPP, a five-picture DPB against four marked references and
/// `sps_max_num_reorder_pics = 0`, so 115 of its 120 access units retire a picture.
const LOWDELAY_640X480_H265: &[u8] = include_bytes!("../tests/data/lowdelay-640x480.h265");
/// This rung is immune to the release-ordering defect for a STRONGER reason than
/// the DXVA one, and this pins the difference instead of asserting it in prose.
///
/// Both HEVC conversions release `removed` inline and let [`SlotMap::assign`] hand
/// the freed slot to the decode target. DXVA survives that because `H265Planner`
/// snapshots `dpb_refs` AFTER `decode_rps`, so the retired picture is not in the
/// marked DPB `RefPicList` is built from — a property of the PLANNER, one call
/// away from being untrue, which is why `pf_dxvadec::pic_h265` drives the
/// counterfactual through its conversion.
///
/// This conversion never reads `dpb_refs` at all. `pReferenceSlots` is spec-defined
/// as the slots the decode operation USES, so it binds `plan.rps` — the three
/// current sets, which `decode_rps` derives and which therefore cannot name a
/// picture that same RPS just dropped. The test proves it the only way that means
/// anything: it hands the conversion a `dpb_refs` deliberately widened to the
/// PRE-RPS marked set (the mutation that makes the DXVA rung alias on 115 of these
/// 120 access units) and asserts nothing changes here.
///
/// If this ever fails, someone has made this conversion bind the marked DPB — a
/// legitimate thing to want, since a *Foll* long-term anchor invisible to the
/// hardware is the RFI failure shape — and it now needs the `release_after_decode`
/// deferral the H.264 and AV1 conversions carry.
#[test]
fn a_pre_rps_marked_dpb_changes_nothing_here_because_the_current_sets_are_what_bind() {
let aus = split_into_aus(LOWDELAY_640X480_H265);
let mut planner = H265Planner::new();
let mut slots: Option<SlotMap> = None;
let mut prev: Option<AuPlan> = None;
let mut converted = 0usize;
let mut widened = 0usize;
for au in aus {
let plan = planner.plan_au(au).expect("the low-delay stream plans");
let map = slots.get_or_insert_with(|| SlotMap::new(plan.picture.max_dpb_frames));
// The marked DPB as this AU's `decode_rps` found it: the previous AU's
// snapshot plus the picture it stored. Exactly what `dpb_snapshot()` would
// return from above `decode_rps` instead of below it.
let mut as_if = plan.clone();
as_if.dpb_refs = match &prev {
None => Vec::new(),
Some(prev) => {
let mut marked = prev.dpb_refs.clone();
if let Some(id) = prev.dpb.stored {
assert!(
prev.picture.is_reference,
"this stream carries no sub-layer non-reference pictures"
);
marked.push(RefPic {
id,
pic_order_cnt: prev.picture.pic_order_cnt,
is_long_term: false,
});
}
marked
}
};
if as_if.dpb_refs.len() > plan.dpb_refs.len() {
widened += 1;
}
let vk = plan_to_vk_h265(&as_if, map).expect("conversion");
converted += 1;
for r in &vk.refs {
assert_ne!(
r.slot, vk.setup_slot,
"a reference aliases the setup slot even though this conversion \
binds only the current RPS sets it has started reading \
`dpb_refs`, and now needs a deferred release"
);
}
prev = Some(plan);
}
assert_eq!(converted, 120);
assert_eq!(
widened, 115,
"the mutation must actually widen the marked set on the access units that \
retire a picture, else this test asserts nothing about anything"
);
}
#[test]
fn the_full_25fps_vector_converts_with_stable_slots_and_start_code_offsets() {
let aus = split_into_aus(TEST_25FPS);
@@ -0,0 +1,126 @@
# SHA-256 per DELIVERED frame of lowdelay-3840x2160.ivf.av1, DISPLAY order — 60 frames.
#
# Each frame is the 3840x2160 render region as tightly packed NV12:
# Y plane 3840*2160 bytes, then interleaved UV 3840*1080 bytes = 12441600 bytes/frame.
# (`render_width`/`render_height` equal the frame size, so there is no crop.)
#
# THE STREAM IS OURS, not a conformance vector, and that is the point of it.
# `punktfunk-host spike` on .21 (NVENC, RTX 5070 Ti, driver 610.57.04,
# punktfunk-host 0.25.0-0.00011708), 2026-08-07:
#
# punktfunk-host spike --source synthetic --codec av1 --width 3840 --height 2160 \
# --fps 60 --seconds 1 --bitrate 1 --no-loopback --out lowdelay-3840x2160.av1
# ffmpeg -f obu -i lowdelay-3840x2160.av1 -c copy -f ivf lowdelay-3840x2160.ivf.av1
#
# The spike writes the low-overhead OBU stream; the IVF wrapper is added so this file
# is framed exactly like the vendored vector and `common::split_av1_aus` — the
# vendored parser's own `IvfIterator` — splits it with no second implementation that
# could disagree. `-c copy` re-frames, it does not re-encode.
#
# ⭐ WHY 4K, when every other fixture here is chosen to be small. It is the ONLY
# resolution at which our encoder emits more than one tile. Measured on the same box,
# same command, 2026-08-07: 1280x720, 1920x1080 and 2560x1440 all give
# `tile_cols = tile_rows = 1`; 3840x2160 gives `tile_cols = 1, tile_rows = 2` —
# `width_in_sbs_minus_1 = [59]`, `height_in_sbs_minus_1 = [16, 16]` — and BOTH tiles
# ride in ONE Tile Group OBU (`tg_start = 0, tg_end = 1`). That is the exact shape
# behind the defect where the host shipped only the first tile of every 4K frame, and
# a single-tile fixture cannot express it at all.
#
# 60 frames rather than 120 to pay for it: one second at 60 fps is 261 KB, which is
# SMALLER than the 282 KB H.264 and 270 KB H.265 low-delay fixtures, and still leaves
# 55 of the 60 access units exercising the reference-slot pressure below.
#
# 60 = 60 = 60, and that is itself worth pinning. Unlike the vendored vector (250
# temporal units carrying 274 coded frames, 24 of them hidden), THIS stream is one
# coded frame per temporal unit, all shown: 60 units, 60 coded frames, 60 displayed,
# one KEY frame, zero hidden, zero `show_existing_frame`. The parity legs' frame
# accounting must not silently assume either shape, so the CPU guard asserts all of
# these numbers rather than deriving one from another.
#
# Main 4:2:0 8-bit (`seq_profile = 0`, `high_bitdepth = 0`, `mono_chrome = 0`) and NO
# FILM GRAIN, so the Vulkan decode profile is the grain-DISABLED one, exactly as for
# the vendored vector — see that file's header for why grain is a profile property
# rather than a per-frame toggle.
#
# ⚠ WHAT THIS FIXTURE DOES NOT COVER. It is a FILE, and a file is not the wire path.
# The headline "250/250 delivered frames bit-identical to libavcodec" was true for AV1
# the entire time the host was shipping only the first tile of every 4K frame: that
# verification ran against a vendored file, the packetisation and reassembly it never
# touched were where the frames were being truncated, and the suite stayed green. This
# fixture closes a different gap — it is the first pixel evidence for AV1 from our own
# encoder, in a multi-tile shape — and it closes NOTHING about fragmentation,
# reassembly, loss or the session's AU boundaries. Those need an end-to-end test.
#
# Goldens from libavcodec's SOFTWARE decoder (AV1 decoding is exactly specified, so
# every conformant decoder is bit-identical):
#
# ffmpeg -i lowdelay-3840x2160.ivf.av1 -f rawvideo -pix_fmt nv12 \
# -fps_mode passthrough ref.yuv
# # then split ref.yuv into 12441600-byte frames and sha256 each
#
# CROSS-CHECKED between two independent builds on two architectures whose
# 746,496,000-byte raw outputs are BYTE-IDENTICAL (not merely equal per frame):
# sha256 90c5be20342cba4d80bd0ceb1568cc3e2037f427d34c658895c7742968c93600 from both
# ffmpeg n8.1.2 (Arch/CachyOS, gcc 16, x86_64, libdav1d)
# ffmpeg 8.1.1 (Homebrew, clang, macOS arm64, libdav1d)
# 60 of 60 digests distinct.
5c587f77a16733533c178c115cf4fad5866b13ebc4bf7673a72be062b4aab9d9
b07527d4a8d7fb8b4ce53d97818e86493237e98c492e5718d42ea0974cd00da2
59156f4f4d3ef030fe884e7085a2f1b3887ba00e0c8e58c5e3f425a31f5aade6
693bbc7da0f499d2955e6c13ed6ee3c13603faed938bc357264c534bed9faa0f
14d1722b39ef61ede697a87f5d11eafcf7d9a812fb966fd3142c483036a08b87
94e6cd41d300298677b014650e5f19b420c38e82741452da573d189d947f1a41
f832d8126cdf3195be4a25aa5e80eefdd2c1089dce5bb3d3531f9bdb58e9c3b0
be9248bec91e4bd5ab06e3a32d1ac02b759ae94735ecc88b14b845c5e4f24120
581fdce69ff5188c949612fd00bd801c717ab9fc1f2de635d734879aae0a7bdd
9256582fc2d242e57a2fbd414a481d4cfca494fa56254bdb15004b4d65c866c6
7955081dd59795399a1cb7a3eeade261ba01ef89c8c27f1e133c958be054ddec
5c8e963f60385add18001a3992fe5c5abb697391213f0f992144e5bba8f00745
13062b6a6e2c557f4b8956a2f05a9ec941b3bd15b6ff4c337662b3eeabc75c41
a80151a16345232be1db60fe2c1e7e61df38a1f676868fcc1b367fa42cdfaf9e
3bb8d3934ce8b772d922c3f9004a2ce4db0bb1b9dcc63c5ac2ac90900b5693ae
0e2b7d0148bcb1873256fba13058607b6088dc0478e9ed4773b657cd5d2d736f
a620a352fd7f71675ea408a53c65e193f986c4e03da8008df996858410804323
d89b4fdf771dbae3422dc193b8425af78a8a27520a09f2c8039fc6bb1501fd9a
8bbdfbcb580c3c2694b8666869abd4cc39083d3dcf68d9b9ddfcc0f87ba8579e
f779dcc8cc901b9fd878ab7f7e0b74ffe6123711fca9a9cde5c7f79f8edfc2d7
172994574f60daea3dc17097a9ef001528cdd469388ee1c168021e622fb9616b
4ff8294a6e9866cab539ec30a434484370b74b529d4f1b8a83186b6cf3862abb
4a23e983efa8d890f89f654813032996af9ec74967e0bfe3bde9c1157e7c31af
3f35ff00a47818184adcc25d86546503d62dd15ee1992971fe8c8d378fb1112d
dd39f5cb55e8ec815dc8608f59612823cbfb96bdf4a470c9255ce745fbe43a7d
62208c265976740965ae43a75c63e18eb1f6dce9781c98db105c4b4e6bcaee05
a4227c20e5b717c8d6f3bc02290bf5c6f14ffb5655ee0b5d56ea906de39471a8
99e146b61d3591d32f297b87eb02957bbfe7a323bf6b09da0f25840018ff3168
7abfa53dc8d8902eee943cad6bd3d9893954c04f52ef999f64822ebab935f474
69a8db91bc8767047ee167362d235c494be63957efeef400e12d447c65136438
8786d36d405ab6b2493e98415784e33fe09b49fe9730ad899ec1db418ab71122
fe5bb80bdf4a100305b157fb1f60732bebb9c7b70ab22726004bb4fc9925705f
264595ab85a2f8e30d316e48b02d8863c341d8ddc9d34e42affbad9520958027
cc96871daa10607f13227d6dbabf56f9ffea7eb2acc4d71272cffae286da1970
911bf0ef3c152bf45f527de8b4deb5a60d80f5793bf7488dd57009b1cbdf51be
543851960eb05d8b2a0a3b7ee50ce6fcc975b4ac6404247464e535364e610804
e650439e22ebc94c24479a3d28472afb776389f4eb9aff62c8eaa7b96223e014
3ca9d4ba265c19540c73eb51c9d4003ca3b762cf4d89f6d771d6c9edb07df65f
f106640e15ae6b53056c9fb64e94b2a3ccce80a32f8146a5cb170363c1a5d019
b7004a617d142bb7238063c35e1173888aa028282fdbdad8ffaca3519fb0228d
98f03d7add2fbd046b6805514d0844abbf7c2ee43de2509d3344843ad2d7220d
15c7a6c87d1ded36db5b5aa9e8394b184d3a6c7ccee4e1041b2473db5a1d0d2c
8d98f9655751c2c2afc7d4a87dac58c0d185e7bf2d897f93b0e999cef7897920
91cc279dacbdae06ca5cde05c395e73cb675802e1a4f1ac3ae95a0344e2ef6ec
d042152a54fb3a0c129c2beba6c73609c4e9e4abb77d5c0bd731832bd70c6ab8
5a9a9ccfce845caefcbfca7ac553674f84ff9a7f2846c55541512290a89a9295
6ed1c17c84a6c454600b079ab709782cfd494adc5433613738158c5163e60211
116d24252b578e6e4519611b9542372175b3fbed7ff76276daae6e0c8d2cfa77
edaf4b5827b58b5712266d84914706b76b4aaefffa2e70a703f5261e9c774c00
85b63f4a86b1e5e19372dfa98040382c94114bdff710b301430ec1894c0d06ab
bf19eb95a3fdeed167d2a2a65fbbcbd426125451de649d018f7c1e2bdfeced50
e0dc2d2422909b997cd39d2d66842f2f9da9f9002f6b46b93ed2666a324e04ab
68ae1aad5ed221897100680c932f5c5ce71ba4957cc6b784a9ebc0775342d714
784acdc2a7cbb0cd135c01dac00d4d63385fce60f4d053d30e9b972d97bc6db6
5a08dac838c5f012e79e60cdf17ab25db8b58ad83df9b2d1b94a7c1f40e04943
0c53d382480cc0ef5402dcd71d80c23662a9c3078b3989371eca11a4000cc4dc
a50fafe9ad90c4cc77dc6ada3184e8a7fcd744fd57855ee05d40cc96e6cc10af
eb1b41ec865ab2b6090cba6603c687bd059aac1d183ed76846cad1b9a579c7e0
92d9d93b9cccb596ac7022aaca97e32b4ec7417dc8f0047a9575957464d6b5d5
210d44dfefe088b47caac2861171abdcef07c828ab673fa98a6e64cda8d42107
@@ -0,0 +1,165 @@
# SHA-256 per decoded frame of lowdelay-640x480.h265, DISPLAY order — 120 frames.
# Each frame is the full 640x480 picture as tightly packed NV12:
# Y plane 640*480 bytes, then interleaved UV 640*240 bytes = 460800 bytes/frame.
# (No conformance window: 640 and 480 are both multiples of MinCbSizeY, so the
# coded size IS the display size — the same reason the H.264 sibling picked 640x480.)
#
# THE STREAM IS OURS, not a conformance vector, and that is the point of it.
# `punktfunk-host spike` on .21 (NVENC, RTX 5070 Ti, driver 610.57.04,
# punktfunk-host 0.25.0-0.00011708), 2026-08-07:
#
# punktfunk-host spike --source synthetic --codec h265 --width 640 --height 480 \
# --fps 60 --seconds 2 --bitrate 1 --no-loopback --out lowdelay-640x480.h265
#
# Main profile, 4:2:0, 8-bit, level 3.1. LOW-DELAY IPPP: one IDR then 119 trailing P
# pictures, ONE slice segment each, no B pictures, `sps_max_num_reorder_pics = 0`, so
# a picture is output the moment it decodes. Its SPS says
# `sps_max_dec_pic_buffering_minus1 = 4` — a five-picture DPB — and 8.3.2 leaves FOUR
# pictures marked in steady state (116 of 120 access units), so four references plus
# the current picture fill the DPB exactly. Each P names exactly one of them
# (`numRefL0 = 1`); the other three are 8.3.2 *Foll* pictures the RFI window keeps.
#
# That is the H.264 fixture's shape in HEVC's vocabulary, and it is what puts an RPS
# drop and the eviction it causes in ONE access unit: 115 of these 120 access units
# remove exactly one picture. `test-25fps.h265` cannot reach it — it reorders.
#
# ⭐ HEVC is nonetheless EXEMPT from the aliasing that cost H.264 and AV1 a deferred
# slot release, and this stream exists to keep PROVING that rather than to be the
# stream that breaks it. `H265Planner` snapshots `dpb_refs` AFTER `decode_rps`, so
# the 115 dropped pictures are never in the marked set `RefPicList` is built from:
# measured `removed ∩ dpb_refs` = 0 of 120 access units. Move that snapshot above
# `decode_rps` and the SAME 115 access units alias — the counterfactual is measured,
# not argued (pf-dxvadec's `pic_h265` tests pin both numbers). HEVC's freedom is a
# property of the planner, not of the streams we happened to test.
#
# Goldens from libavcodec's SOFTWARE decoder, the same ground truth every other
# golden set here uses (H.265 decoding is exactly specified, so every conformant
# decoder is bit-identical):
#
# ffmpeg -i lowdelay-640x480.h265 -f rawvideo -pix_fmt nv12 \
# -fps_mode passthrough ref.yuv
# # then split ref.yuv into 460800-byte frames and sha256 each
#
# ffmpeg version n8.1.2 (Arch/CachyOS, gcc 16, x86_64) — cross-checked BIT-IDENTICAL
# against ffmpeg 8.1.1 (Homebrew, clang, macOS arm64), all 120 frames. 120 of 120
# digests distinct.
9f9c0286da4917dd02897f75d61142f0e6e950c2795e835f6afcc849d6778e5b
269e68e0917f62050cc6e134a9355d621dbb6dd1ab162f809b86c44df3d8fef0
3297b14670d010b01f7c5afba4a1309055965d4b1cbdcf644f3ae59540330e79
f6f7b529995b9d658a7a30a2b8df8381a494b18d4a92441c350591e2221656cd
c32d3e28e7322d08a5a4c2044b3d6f6a8a5b093818e378e9f68ecbf787c3e89e
51b534a44bd5292dbfe189108f0cd2ceaeafd35433762113d25ad17aee95ee9f
fe23e229a7154d5ddd965e7131a3db1928fbcc91525b96d06bf354ecb390cd36
a18313577c9f658af750ac5deb71275b7cb31847030064b7aa1eabfa1bca8b6d
bd372841ca00e7ed8cf88bd5b47f1d2aee0a87c9c40fc1d921ce5135c5ff0782
baa5e7a7a2c86ac428dfe3ed3bfdbe399ff8f339186c48c181840a738010e6a7
43e84f559307c12c0a4d8acfd259cc3a73aa6d9e2951dcf05c45e4185becf75b
c85779050cbd80826c554e62d5b9435f6e6cc31c0013d288045260fe576dfe8a
93f5292b0436b37290e644f0e45aa987e39c97666f182e54709eae42512e40b7
163fa586ec9813079ed0427bfa046f846359c540763b4437062c181a219d64b2
2851fa698eb3889235d16a7e9497153b57f6e1ba79133f038586c13623b1893f
3ca46e9b37fe4c0330893f5d91ed9592bfa65725d703be58d00bb57d2aedaa8d
30a70c37b70f224198c09b7606464fa9f2ed578cb33a467832b2864db01bafe6
7cd5448fc7d149f311fabef84893bb91b5127c22fdba429eabee1b10a266eb0b
79bda1e5dd8136eb2dc5cfc607dcb1bdd2a3b58a3c85133f52e54f21bb138e99
9aa3f7dc22d68dbf76cb1a71fd476f5f8b272b7dd6676383945c37611ce26f56
75b1151b63f8e071cadd3656edae21e6755cdef194730e4d9d364df67ff63b56
a6fe1a709aa69329fa96ab90aa4561a57b42cdca1d6dc3f41b00b8466bbd2bfc
a309129ad54517d8839ca4ebf12bdcc4c563049fe991d8a7c0fa57ba8c75c6ee
dff37bb18055c1f4739168c882ec694b027998b960e341d6815a47cd87942a14
ff334206b81c8664cf2a59df91d0cd3fd3bc8f45f6ec918312df75e61d6adf74
916f50363a424907d97dd1914808770ee06716f70c6040a44d0c1c9c66004b3c
a2bdd4550eb5a43742d667a1538102838dd81e7930c76b607df23dee4d0c2c03
e5f70c0a5df394b9dbe49bb6cefb0c0ce799496ee1cb02ba85366415376d9863
f154dac657645f5aff1e0f47aa8ffd66b7205808fdf345c9b153a70770eb0702
5d83483a3135908c233f9c8719115b823ca4c384cc3d48f9d43b2ce174e55c12
60ea060bd5f80d2ed291f7fd87a317adc8a8eab5f2ea0bcda130e06f0bc4b21b
3cbd406035c302f48874c509f0a2f32f2a3ee6072d87a97ad91728abe9778176
43c319ed065cb55d59a6d9fe98a70884563ce11109e54dfd99d5a4ce695ef355
a6416844bc2d08e80415350ea6a77b37e0a8c5ea0a13bb284ac6f4f3a54882a4
b49fc7a97971eef0c7ce3264a5245de9c323fdb8c1f8d5410f5ffda6bca6c187
23754bb46f0938a29cf3b58cb78530701b3ed7e8a2d6fc55a168b2b0e6c9bf19
9bd927da6114b539327c3c9010e01a19bea941fac296acc7cea4ff59ee2e21cb
774b5b0f9872c4d95f071ea16dc848d99c7bbafa22228d6530501d1e9c405e22
ee244d9663d700085033571e63649beb169f3464b5c47b435beb5e56b6513033
c552bc82788cb0b793361a3beed54b0f1ccbb03c1a34d29491a07acea615b980
31a92d0c3a8906c742034f36720481d50d2b7913f2f8225f78ef6ab8fb104a8a
3f0581af032d83dd7cc2cf0afff04d225a48a42dae3e3cfb6cc72b1218b49dc8
6f0bb48a98e7a971238bb4d90432ffc675c55bab2571387e0fdc9f2d8acfdc7e
4f51f9aaa83108587eb3871a22c7ebb34a7f66dbdd81689de03f10f16f8e3207
90a47f56c6aedd15de6dc3798bab53f394c4095c1fea933f2d8af83763d2431f
1e8c8505e4e9f7ada9a60b6d6f66d30961c3f5fe70777a71b4e110079a435164
6909300c3db9cd6db449bd6e3002b94052da02000105f89bf38ad4babab4e339
caf9794215bd2ceebbabcebbd7d3de3d86ac9fdced421ab30ef0cb1eea9f472b
232b867d97898839e331b9a9aa971e5593b4e360833ae1230272d426fd3796a5
3472664dc348f434ffd0b58215cbdd17c9d18fb8112c0b1bd3781fd23da77edc
cdfb83932eecd4c5acaa12bad0d7f04d2a87dcd2812cd0a10ff3ddab3ced0484
4862527f8256fa9c1817c7db32a9237ca9078554de336c7ec231cdf92fd8b481
95ef6abd712678dec27238e9f5d983c632ea606c68a17db8c13db5f984106753
2a37fa16ac36bdba0a934a4fd756bb0dc40685c4db63cb1c919f1737e2bcd497
798d6c594aa1924205e947e8b48a49822dae5310f1938e31d7fba6b538bb0689
f7e8fcbfe4074d0e8cffc88ac5be30cfcaae2bdb4d6f35b32c627ef1eb1e5b44
87eedb3cd8a47c70cf23c7025a53176dac83d9928d66bbc9750169b15df65fee
e3f24c0c33a4442c7f3ccc4fdc4d42116a92c1afa2a108d9867493c25971c6c6
6fba290349aea3ad2f0b189ddf608c07e3c7062e5659464453e9198f80fb1a06
fb1503234b54566ab4d4fb8f155ec779e6e83c14eaa2ebbc51502636b8142362
25a2d7396d612601b65306d166fa9353eaa63a820c88a4a2ba4891ebd6527c73
52e600c978c89b4e0580ea63a4131edd445cb1a83d5978db99e09bf115795c43
c576c1f537bd10065c93ce01bee7821b5d947d418c42f2941a65c106a8866ded
fd925cf61ffdd98fad5388c8bde2f3e4dfaaff0de0badd3e28998dcdaaa22790
ed06b90b70480467e2bff3aa44fe26356065393b09916ca55b705147e79a4532
bc311e2abeeceb28e409a5c117a303100a5b8bd72a9212da8370ed125d23267f
611f87e03ab96ef0e683968611f9ee9b6c0052457a513217f9a20cfadf98c0c8
781aa26b54f6e3a941c973c1685323ebb6a4c8a024d575a219138d2f7500f12c
e1bf924e000066db999ea9df03f444544ef9b5ced6903fd6e11679b1bcde4288
1dbbe9f9039f67d630e0474830a9b4908453dabc705a726990e5da33f8265ef0
6b196b22ee1b5f5a41129fd6ea4c31217010d656d07eba1b519372483e225d5a
f98c6557358d7d69ad084a39780e46c3f6d2c882a95b14e196453cf5683f2e1e
2d1f2d2e730d80b0a0e9186cf706fee66e5919b724cc048fc26905766baeb4fb
fc6e7638091a5a8fea2c2fb93de326466d0b8b2791a137b6c326cf873f780091
12ace0c4843c04cd1ab0b24d11374f92862300dfb04402c53dbb2a3846d3cd1e
ec3b094cf0a85e64794afb1a423fcf6ac2d4ac49bcd0eafaff93c87c5ea5da80
4eabff56ae2893cb8d18b994e8922d98517f4bfc26f03572917ee04e9c1f2c21
c836a55b5690dc31db03d5373b65660a7a56b3dd6ecb1335daecfdd798dbae49
3513c96a153957e34cb5b4568fed9cbc9f008b40c7f7c8888419e280b9552e72
98d993995f82e40736a96d78088f79ef5b1e23a2f2c8612a995581d2e4339c89
a7e18f4986c14870c0f5703287acf0015e84265c59cd81ec4da249a156eaf344
c67d1b2cdfd341d605fb1a0104fbf39dca7a220a20864f6e258c5261b363d2d6
d49987c3cc626dea121b049f26ff92619fabc0c9ac2f4d7222fd5a4f3cd05106
c1af9fdac737422cad1e70fae5660268f51c1fa2ad565617d9aef0c6567bc1f2
988b41b171cbefd8844891dbf1dda9586f4f7d855fdedf9084ce1a35211bf4c7
0c89259a251df6835d43f2d41b3ab680191ff8a2a1b52f38067f5ff6e64c46ef
463acd75505bf1c7f9cdbac61ffc447fe7dff36832dd6df2da8c6fb034a7ea5d
b87b3ae2d51b989010cbc44d020db3be1df563727cb1828ed20be75138b391ca
f224bab42976a46f976e3792ec15c7c143fd40c255577ced88c4dc423f9e521d
83248fdcccac574bf2550594894668e2294b60a51e02511bd7ed1a3d62a0a833
e21a46b8aa370956a53c6059663c95846d6de9c21b911e9b35328642cd2b6a93
1b25f894a5e039940be81195d73bc47b81dbe3de0a02172c47e8f3de53ff453e
b91a7d7f4545adec12ab1f1646a152aa7a411f52fe85ba6d0a05e26233bd9554
76fb5d9740f2f31d27c017d14f2641dd71debd5ce1322f993e4d4525f86913e2
f69e6a8e6d1f9022fa3b4d7e27cae866d827b0d70ed4a1475cec54703e3028be
89e799beb6d7196de0f1aaec171c0aa7c5ab5b5061e7ea6431a4b9ca2f9f305d
a48dd4d45122187814b66952700536336d2d86e3cf4c90499416ee4178c82df6
716723e849b5a3fc13460c45b7b4f236bdb077a40eae3f1bd6f63970eccfd6d1
6d6f03ccd7e223c150bf4dab4667ef4cca83ef24db79dda73c1d3d132cd8abec
f6b9a0795d6202a1f868768fead3a3072f5ac84a31bb741b1c2e6435bddea335
93db9ff6332ea465c026c6d994e214d7fc035dac3b28cc5df5100633c4b3d355
f2f7b1a021abe83b5635da215b72d341c58532fe0aaa6c72570e1ab4d8bd702c
e1cc57cae82018761af96d75485c9564c742b9c180709f41cdac28a08b4734c1
bd8b75c1bddaca942b0645efd1774ca6cdf2b1254f65aa3bb52160a635c5820e
454d9a6027f89a23680378b0ff469b0ddaa277f1daa1fc953ffbf3a86c9b56e2
52dd5395d96d8097515707287fe2f53c140e46addca3d4407b1b9f9685569ce3
df850ef27b8e2db55f1f03e4fe686a22897b5266e37ba6d56cf97e9adc0a6e25
d6ee7735b22fa97e1ee85fcbf7bea1609ed703e98a4076e1474e8d152794ec6a
4bfcb046b8175eed1732bf52d5abd0540d73333e1b3efbf86b8dfb1a76aeca57
632c87450e3b5b763d87890b5ed5ce38775af5359b858f716cfbff651838c01e
2784dd87812c4df4793ff50409626d47bebd4149caa4b5098efb5ec63c59b5a1
480b2f63b02ba2436a3d026701adafc27ed8fbc100ebe232257ec62c54166767
81d1b8196e7bacd89ef90494b48772b1eef52afbee49aaf907eb264eeb11d660
a045bf5565f399c293363f2d08315b60ae6bd6a524ca537668e3afb3b9893d9a
767ffcd6a3b2574f641c1f298c4606cce88b08f76ff5abbdbfd8c8cc8b88cb86
54cb01c9f411e1994b5a2b2cea22032e232a1d76874b5b6a991474058c3b3dfa
feba0cc0e710d509963cf7f7c6bd62e6904d6c5446fb784bd3508520e1a3371a
ae0c6d458623d31eeb59e9170f983dec17ca5885614dbc82e67ae45b9ecb11c9
e6d0f8d4ee60e94199ee39ed6f7b2f6c4d4835993c981086f5b022ba4bd1da6a
d70133bc51be4b64510a0411c07dc2479b04f18b1b561cce21b91f6a3a9508a2
Binary file not shown.
+533 -5
View File
@@ -46,6 +46,28 @@
//! prefix for a driver to mis-skip and no second framing to test (see
//! `common::split_av1_aus`). Its absence is deliberate.
//!
//! # The three legs that decode OUR OWN streams
//!
//! [`LOWDELAY_H264`], [`LOWDELAY_H265`] and [`LOWDELAY_AV1`] are not conformance
//! vectors — they are `punktfunk-host spike` output, vendored because a conformance
//! vector proves conformance to itself and the encoder we ship behind is a different
//! stream. Each is here for its own reason:
//!
//! * **H.264** caught a defect the vector is structurally blind to — 117 of its 120
//! access units named one surface as both the decode target and a reference.
//! * **H.265** is EXEMPT from that defect for a structural reason, and an exemption
//! with no stream behind it is how the H.264 defect survived two milestones.
//! * **AV1** is neither: the vendored AV1 vector already aliases on 268 of its 274
//! frames, so that class was covered. It is here because the vector is ONE TILE on
//! every frame while our encoder splits 4K into two tile rows, so every tile array
//! the conversions fill had only ever been exercised at index 0.
//!
//! All three are backed by a non-ignored CPU guard asserting the stream still has the
//! property it was vendored for. ⚠ And all three are FILES. A file fixture says
//! nothing about packetisation, reassembly or loss — which for AV1 is not a
//! hypothetical caveat but a recorded failure: this suite reported 250/250 throughout
//! the period the host was shipping only the first tile of every 4K frame.
//!
//! # Why the AV1 leg exists at all
//!
//! Because until it did, the AV1 rung had no pixel evidence whatsoever. An
@@ -139,6 +161,85 @@ const GOLDENS_LOWDELAY: &str = include_str!("data/lowdelay-640x480.nv12.sha256")
const LOWDELAY_FRAME_COUNT: usize = 120;
const DISPLAY_LOWDELAY: (u32, u32) = (640, 480);
/// **Our own host's HEVC**, the twin of [`LOWDELAY_H264`] — and the one vendored to
/// keep an exemption honest rather than to catch a defect.
///
/// H.264 and AV1 both had to defer their slot releases past the decode op because
/// their planners snapshot the marked DPB BEFORE the marking that retires a picture.
/// `H265Planner` snapshots AFTER `decode_rps`, so an RPS-dropped picture is never in
/// the set `RefPicList`/`pReferenceSlots` is built from, and the HEVC conversions
/// still release inline. That argument is correct — and it was, until this stream,
/// backed by `test-25fps.h265` (which REORDERS, so it cannot reach the shape at all)
/// plus one throwaway measurement.
///
/// This stream reaches the shape. `sps_max_dec_pic_buffering_minus1 = 4` against four
/// pictures marked in steady state, `sps_max_num_reorder_pics = 0`: 115 of its 120
/// access units retire exactly one picture, and **all 115 of them would alias** if
/// the snapshot moved above `decode_rps`. Measured `removed ∩ dpb_refs` is 0 of 120,
/// so the exemption is a measurement on our own encoder's output rather than a
/// re-derivable argument.
///
/// ⚠ On THIS rung that counterfactual is about the planner, not about
/// [`pf_vkdecode::plan_to_vk_h265`]: Vulkan's `pReferenceSlots` is spec-defined as the
/// slots the decode operation uses, so the conversion binds `plan.rps` — the three
/// current sets, which `decode_rps` itself derives — and never reads `dpb_refs` at
/// all. The DXVA rung is the one that binds the whole marked DPB (`RefPicList` is
/// spec-defined that way, and an RFI long-term anchor must survive in it), so it is
/// the rung a moved snapshot would actually alias on;
/// `pf_dxvadec::pic_h265`'s tests drive that counterfactual through the conversion.
/// What the leg below adds on this rung is the thing no HEVC leg here had: PIXELS
/// from our own encoder, under a DPB that evicts and reuses a slot on 115 of 120
/// access units instead of a vector whose reordering keeps eviction slack.
///
/// Provenance, the `punktfunk-host spike` command and the two-build ffmpeg
/// cross-check are in the golden file's header, as for the H.264 sibling.
const LOWDELAY_H265: &[u8] = include_bytes!("data/lowdelay-640x480.h265");
const GOLDENS_LOWDELAY_H265: &str = include_str!("data/lowdelay-640x480-h265.nv12.sha256");
/// **Our own host's AV1**, and the only stream here with more than ONE TILE.
///
/// The vendored AV1 vector already exercises the reference-slot aliasing shape (268 of
/// its 274 frames), so unlike the H.264 and H.265 siblings this is not vendored to
/// close that. It closes a different gap: no host-generated AV1 stream was tested at
/// pixel level anywhere, and our encoder's AV1 is structurally unlike the vector —
/// `RFI_DPB = 5` references, reference-frame invalidation, and at 4K a split encode
/// that puts **two tile rows in one frame**.
///
/// 4K is not a size choice, it is the only shape that has the property. Measured on
/// .21, same command at four resolutions: 1280x720, 1920x1080 and 2560x1440 all give
/// `tile_cols = tile_rows = 1`; 3840x2160 gives `tile_cols = 1, tile_rows = 2` with
/// both tiles in ONE Tile Group OBU. It is paid for with 60 frames instead of 120,
/// which lands at 261 KB — under both other low-delay fixtures.
///
/// ⚠ It is a FILE, and a file is not the wire path. "250/250 bit-identical to
/// libavcodec" was true for AV1 throughout the period the host was shipping only the
/// first tile of every 4K frame: that number came from a vendored file while the
/// truncation lived in packetisation. This fixture gives the multi-tile shape pixel
/// coverage on the DECODE rungs and says nothing whatever about fragmentation,
/// reassembly or loss. The golden file's header says the same, at length.
const LOWDELAY_AV1: &[u8] = include_bytes!("data/lowdelay-3840x2160.ivf.av1");
const GOLDENS_LOWDELAY_AV1: &str = include_str!("data/lowdelay-3840x2160-av1.nv12.sha256");
/// The low-delay AV1 stream's temporal units, DISPLAYED frames and render region.
///
/// Units and frames are two constants holding 60 rather than one, and that is
/// deliberate: for the vendored vector they are 250 and 250 while the CODED count is
/// 274, and a leg that derived one from the other would be asserting AV1's frame
/// accounting instead of measuring it.
const LOWDELAY_AV1_UNIT_COUNT: usize = 60;
const LOWDELAY_AV1_FRAME_COUNT: usize = 60;
const DISPLAY_LOWDELAY_AV1: (u32, u32) = (3840, 2160);
/// The HEVC low-delay stream's own frame count and display region.
///
/// Deliberately NOT shared with [`LOWDELAY_FRAME_COUNT`]/[`DISPLAY_LOWDELAY`] even
/// though the two fixtures agree today: they are separate files from separate
/// encoder configurations, and one regenerated at another size must fail on its own
/// leg rather than silently redefine the other's geometry. Same reason
/// [`DISPLAY_H264`] and [`DISPLAY_H265`] are two constants holding 320x240.
const LOWDELAY_H265_FRAME_COUNT: usize = 120;
const DISPLAY_LOWDELAY_H265: (u32, u32) = (640, 480);
/// The Main 10 vector is 50 display frames.
const MAIN10_FRAME_COUNT: usize = 50;
@@ -932,6 +1033,39 @@ fn main10_every_frame_hashes_bit_identical_to_libavcodec() {
);
}
/// The HEVC twin of [`low_delay_host_h264_every_frame_hashes_bit_identical_to_libavcodec`]:
/// **our own host's HEVC**, in the shape the vendored vector cannot produce.
///
/// Its job is the opposite of the H.264 leg's. That one exists because the rung was
/// broken and only this stream shape could show it. This one exists because until now
/// no HEVC leg anywhere had decoded a single frame our own encoder produced: both
/// existing legs run vendored vectors, and the H.264 sibling is the standing proof
/// that a vector's silence about a stream shape is not evidence.
///
/// What it exercises that `h265_every_frame_hashes_bit_identical_to_libavcodec` does
/// not: a five-picture DPB with four references marked and no reordering, so the
/// `SlotMap` retires and reissues a slot on 115 of the 120 access units, back to back,
/// with the decode target taking the slot freed in the same access unit. The vector
/// reorders, which keeps that eviction slack and never puts the two together.
///
/// It is NOT the leg that would catch a moved `dpb_snapshot()` — see [`LOWDELAY_H265`]
/// for why that lands on the DXVA rung instead, and
/// [`the_low_delay_h265_stream_agrees_with_its_goldens_and_keeps_the_exemption_falsifiable`]
/// for the guard that keeps the planner property itself pinned, on CPU, in ordinary CI.
#[test]
#[ignore = "needs a Vulkan Video H.265 decode device (fleet boxes; see module docs)"]
fn low_delay_host_h265_every_frame_hashes_bit_identical_to_libavcodec() {
h265_parity_run(
&common::split_h265_aus(LOWDELAY_H265),
GOLDENS_LOWDELAY_H265,
LOWDELAY_H265_FRAME_COUNT,
0,
EXPECTED_FORMAT,
DISPLAY_LOWDELAY_H265,
"H.265 (low-delay host stream)",
);
}
/// The HEVC leg of the production prefix form — the one that would have caught
/// the shipped defect. See [`h264_four_byte_start_codes_decode_bit_identically`].
#[test]
@@ -960,24 +1094,52 @@ fn h265_four_byte_start_codes_decode_bit_identically() {
/// `show_existing_frame`) is the point at which this should grow the same parameters
/// the H.265 body carries — not before.
fn av1_parity_run(aus: &[&[u8]], label: &str) {
av1_parity_run_against(
aus,
label,
GOLDENS_AV1,
"data/test-25fps-av1.nv12.sha256",
FRAME_COUNT,
FRAME_COUNT,
DISPLAY_AV1,
);
}
/// [`av1_parity_run`] with its stream's own goldens and geometry, for the leg that
/// does not decode the vendored vector.
///
/// `units` and `frames` are SEPARATE parameters and must stay so. They are equal for
/// the low-delay host stream (one shown frame per temporal unit) and unequal for the
/// vendored vector only in the sense that its 250 units carry 274 coded frames of
/// which 250 are shown — deriving either from the other is exactly the assumption
/// AV1 punishes.
fn av1_parity_run_against(
aus: &[&[u8]],
label: &str,
goldens_file: &'static str,
goldens_path: &str,
units: usize,
frames: usize,
display: (u32, u32),
) {
// As the other legs: one codec at a time on the device, and the `set_var` below
// happens only under this lock (see `common::gpu_lock`).
let _gpu = common::gpu_lock();
std::env::set_var("PF_VKD_TEST_READBACK", "1");
let goldens = golden_hashes(GOLDENS_AV1);
let goldens = golden_hashes(goldens_file);
// Non-vacuity, before any hardware is touched: the right number of entries, all
// real digests, all distinct (see the helper's docs — a frozen-frame decoder
// must not be able to pass this leg).
assert_goldens_are_a_real_set(&goldens, FRAME_COUNT, "data/test-25fps-av1.nv12.sha256");
assert_goldens_are_a_real_set(&goldens, frames, goldens_path);
// …and the leg must actually be fed something. An IVF whose packets failed to
// parse would hand `collect_hashes` an empty AU list, which delivers no frames
// and would then fail as a frame-count mismatch that reads like a decoder defect.
assert_eq!(
aus.len(),
FRAME_COUNT,
"{label}: the vector must split into {FRAME_COUNT} temporal units"
units,
"{label}: the stream must split into {units} temporal units"
);
let setup = common::bring_up(&common::Request {
@@ -1014,7 +1176,7 @@ fn av1_parity_run(aus: &[&[u8]], label: &str) {
setup.pd,
&setup.device,
setup.graphics_qf,
DISPLAY_AV1,
display,
EXPECTED_FORMAT,
)
};
@@ -1058,6 +1220,33 @@ fn av1_every_frame_hashes_bit_identical_to_libavcodec() {
av1_parity_run(&common::split_av1_aus(common::TEST_25FPS_AV1), "AV1");
}
/// **Our own host's AV1, at the only resolution where it emits more than one tile.**
///
/// The leg above proves the conversion against a vector with `tile_cols = tile_rows
/// = 1` on every one of its 274 frames, so every tile-info field it exercises is the
/// degenerate case: one `width_in_sbs_minus_1`, one `height_in_sbs_minus_1`, one
/// `context_update_tile_id`, `TileCols = TileRows = 1`. This stream carries
/// `tile_rows = 2` with `height_in_sbs_minus_1 = [16, 16]` on all 60 frames, and both
/// tiles arrive in a single Tile Group OBU — so a conversion that got the tile arrays,
/// the per-tile sizing or the tile-group range wrong would decode the vector perfectly
/// and this stream visibly (see [`LOWDELAY_AV1`]).
///
/// It is also 4K, which no other parity leg in this program is: the readback moves
/// 12,441,600 bytes per frame instead of 115,200.
#[test]
#[ignore = "needs a Vulkan Video AV1 decode device (fleet boxes; see module docs)"]
fn low_delay_host_av1_every_frame_hashes_bit_identical_to_libavcodec() {
av1_parity_run_against(
&common::split_av1_aus(LOWDELAY_AV1),
"AV1 (low-delay host stream, 4K two-tile)",
GOLDENS_LOWDELAY_AV1,
"data/lowdelay-3840x2160-av1.nv12.sha256",
LOWDELAY_AV1_UNIT_COUNT,
LOWDELAY_AV1_FRAME_COUNT,
DISPLAY_LOWDELAY_AV1,
);
}
/// Frame 0's pixels against libavcodec's, byte for byte — the diagnostic leg.
///
/// [`av1_every_frame_hashes_bit_identical_to_libavcodec`] is the verdict; this is
@@ -1725,6 +1914,345 @@ fn the_low_delay_stream_agrees_with_its_goldens_and_still_exercises_the_aliasing
);
}
/// The HEVC low-delay stream's CPU guard — the twin of the H.264 one above, with the
/// extra assertion HEVC needs and H.264 does not.
///
/// H.264's guard pins that the stream still ALIASES (117 of 120), because its GPU leg
/// exists to catch a defect. HEVC's leg exists to keep an exemption from rotting, so
/// pinning `both == 0` alone would be exactly the vacuous check `fd6241a2` called out:
/// zero is also what a stream that never removes anything reports, and what a stream
/// that reorders reports. So this pins three numbers instead:
///
/// - **115 access units remove a picture** — the stream reaches the DPB pressure at all;
/// - **0 of them intersect `dpb_refs`** — the exemption, measured;
/// - **115 of them WOULD intersect** a snapshot taken before `decode_rps`.
///
/// The third is what makes the second worth having. `pre_rps_marked(N)` is exact rather
/// than approximate: `begin_picture` runs `decode_rps` → `update_dpb_before_decoding` →
/// `dpb_snapshot`, and the only thing that happens between AU N-1's snapshot and AU N's
/// `decode_rps` is `finish_picture(N-1)` storing its picture marked short-term. So the
/// marked set AU N's RPS sees is exactly `dpb_refs(N-1) {stored(N-1)}`, which is what
/// `dpb_snapshot()` would have returned from the other side of that call.
#[test]
fn the_low_delay_h265_stream_agrees_with_its_goldens_and_keeps_the_exemption_falsifiable() {
use pf_bitstream::h265::H265Planner;
let goldens = golden_hashes(GOLDENS_LOWDELAY_H265);
assert_goldens_are_a_real_set(
&goldens,
LOWDELAY_H265_FRAME_COUNT,
"data/lowdelay-640x480-h265.nv12.sha256",
);
let aus = common::split_h265_aus(LOWDELAY_H265);
assert_eq!(aus.len(), LOWDELAY_H265_FRAME_COUNT);
let mut planner = H265Planner::new();
let mut outputs = 0usize;
let mut iraps = 0usize;
let mut with_removals = 0usize;
let mut both = 0usize;
let mut would_alias = 0usize;
let mut first_sps = None;
// The marked DPB as AU N's `decode_rps` finds it: AU N-1's snapshot plus the
// picture AU N-1 stored. See the doc comment for why this is exact.
let mut pre_rps_marked: Vec<u64> = Vec::new();
for (index, au) in aus.iter().enumerate() {
let plan = planner.plan_au(au).unwrap_or_else(|e| {
panic!("AU {index}: the low-delay HEVC stream must plan, got {e:?}")
});
outputs += plan.dpb.outputs.len();
iraps += usize::from(plan.picture.is_irap);
if !plan.dpb.removed.is_empty() {
with_removals += 1;
}
both += plan
.dpb
.removed
.iter()
.filter(|id| plan.dpb_refs.iter().any(|r| r.id == **id))
.count();
would_alias += plan
.dpb
.removed
.iter()
.filter(|id| pre_rps_marked.contains(id))
.count();
// The picture shape both HEVC legs hard-code: `probe_stream_support(1, 0)`
// and an NV12 pool. Fail here, on CPU, rather than as a confusing
// hardware-only refusal.
assert_eq!(
(
plan.picture.chroma_format_idc,
plan.picture.bit_depth_luma_minus8
),
(1, 0),
"AU {index}: the low-delay HEVC stream must stay Main 4:2:0 8-bit"
);
if index == 0 {
assert!(plan.picture.is_idr, "the stream opens with an IDR");
assert_eq!(
(plan.picture.coded_width, plan.picture.coded_height),
DISPLAY_LOWDELAY_H265,
"the stream is 640x480"
);
assert_eq!(
(
plan.picture.display_crop.x,
plan.picture.display_crop.y,
plan.picture.display_crop.width,
plan.picture.display_crop.height,
),
(0, 0, DISPLAY_LOWDELAY_H265.0, DISPLAY_LOWDELAY_H265.1),
"640 and 480 are both multiples of MinCbSizeY, so there is no \
conformance window and the coded size IS what the goldens hashed"
);
}
first_sps.get_or_insert((
plan.picture.max_dpb_frames,
plan.sps.max_num_reorder_pics[usize::from(plan.sps.max_sub_layers_minus1)],
));
pre_rps_marked = plan.dpb_refs.iter().map(|r| r.id).collect();
if let Some(id) = plan.dpb.stored {
assert!(
plan.picture.is_reference,
"AU {index}: every picture of this stream is a reference — a \
sub-layer non-reference picture would break the pre-RPS \
reconstruction below"
);
pre_rps_marked.push(id);
}
}
outputs += planner.flush().outputs.len();
assert_eq!(
outputs,
goldens.len(),
"the planner outputs {outputs} pictures but the goldens carry {} hashes",
goldens.len()
);
assert_eq!(
iraps, 1,
"the stream holds exactly one IRAP (the opening IDR); a CRA/BLA would make \
RASL skips reachable and the expected frame count needs rederiving"
);
assert_eq!(
first_sps,
Some((5, 0)),
"DPB depth and sps_max_num_reorder_pics — a five-picture DPB against the four \
pictures 8.3.2 keeps marked, with no reordering, is what puts an RPS drop and \
the eviction it causes in one access unit"
);
assert_eq!(
with_removals, 115,
"the stream must still retire a picture on nearly every access unit; without \
that the two numbers below are both trivially zero"
);
assert_eq!(
both, 0,
"{both} picture(s) are in an access unit's own marked DPB AND removed by it. \
That is the H.264/AV1 aliasing precondition, and HEVC is supposed to be \
structurally incapable of it `H265Planner`'s snapshot has moved ahead of \
`decode_rps`. Restore the ordering, or give the HEVC conversions the \
`release_after_decode` deferral the other two carry; do NOT relax this number"
);
assert_eq!(
would_alias, 115,
"the fixture must stay CAPABLE of exposing the defect it is here to rule out. \
A regenerated stream that reordered, or that carried a DPB deeper than its \
reference count, would report 0 here and the zero above would then prove \
nothing at all, exactly as `test-25fps.h264` proved nothing for two milestones"
);
}
/// The AV1 low-delay stream's CPU guard, and the property it was vendored for: **more
/// than one tile**.
///
/// A regenerated fixture could lose that in two silent ways — a re-run at a lower
/// resolution (1440p and below are single-tile on this encoder) or a driver/encoder
/// change that stopped splitting — and in both cases the GPU leg would go on passing
/// 60/60 while duplicating what the vendored vector already covers. So the tile shape
/// is asserted per frame, not sampled.
///
/// It also pins AV1's frame accounting explicitly rather than by derivation. The
/// vendored vector is 250 units / 274 coded / 24 hidden / 250 shown; this stream is
/// 60 / 60 / 0 / 60. Neither is the general case, and a leg that assumed either would
/// break on the other for reasons that look like a decoder defect.
#[test]
fn the_low_delay_av1_stream_agrees_with_its_goldens_and_still_carries_two_tiles() {
use pf_bitstream::av1::Av1Planner;
let goldens = golden_hashes(GOLDENS_LOWDELAY_AV1);
assert_goldens_are_a_real_set(
&goldens,
LOWDELAY_AV1_FRAME_COUNT,
"data/lowdelay-3840x2160-av1.nv12.sha256",
);
let aus = common::split_av1_aus(LOWDELAY_AV1);
assert_eq!(
aus.len(),
LOWDELAY_AV1_UNIT_COUNT,
"the low-delay AV1 stream is {LOWDELAY_AV1_UNIT_COUNT} temporal units"
);
assert!(
aus.iter().all(|au| !au.is_empty()),
"no temporal unit is empty — an IVF reader returning empty packets would make \
the parity leg decode nothing and blame the decoder"
);
let mut planner = Av1Planner::new();
let mut outputs = 0usize;
let mut coded_frames = 0usize;
let mut multi_frame_units = 0usize;
let mut hidden = 0usize;
let mut show_existing = 0usize;
let mut keys = 0usize;
let mut with_removals = 0usize;
let mut aliasing_shape = 0usize;
for (index, au) in aus.iter().enumerate() {
let plans = planner.plan_au(au).unwrap_or_else(|e| {
panic!("temporal unit {index}: the low-delay stream must plan, got {e:?}")
});
if plans.len() > 1 {
multi_frame_units += 1;
}
for plan in &plans {
coded_frames += 1;
outputs += plan.dpb.outputs.len();
keys += usize::from(plan.picture.is_key);
hidden += usize::from(!plan.picture.show_frame);
if plan.dpb.stored.is_none() {
show_existing += 1;
}
assert!(
plan.warnings.is_empty(),
"temporal unit {index}: a clean stream plans without warnings, got {:?}",
plan.warnings
);
// THE PROPERTY. Two tile ROWS, one tile COLUMN, both tiles in a single
// Tile Group OBU — the 4K split-encode shape, on every frame including
// the key frame.
let tile = &plan.header.tile_info;
assert_eq!(
(tile.tile_cols, tile.tile_rows),
(1, 2),
"frame {coded_frames} (unit {index}): this fixture exists because our \
encoder emits TWO TILE ROWS at 4K. A single-tile stream here means it \
was regenerated at a lower resolution (1440p and below measured \
single-tile) or the encoder stopped splitting either way the GPU leg \
below is now a duplicate of the vendored vector's and this fixture's \
260 KB buys nothing. Regenerate at 3840x2160; do NOT relax this"
);
assert_eq!(
(
tile.width_in_sbs_minus_1[0],
tile.height_in_sbs_minus_1[0],
tile.height_in_sbs_minus_1[1],
),
(59, 16, 16),
"frame {coded_frames}: the per-tile superblock sizing the conversions \
copy into their tile arrays"
);
assert_eq!(
plan.tiles.len(),
1,
"frame {coded_frames}: both tiles ride in ONE Tile Group OBU"
);
assert_eq!(
(plan.tiles[0].tg_start, plan.tiles[0].tg_end),
(0, 1),
"frame {coded_frames}: the single tile group covers tiles 0..=1 — a \
range of 0..=0 is the truncation shape the host once shipped"
);
// The picture shape both AV1 legs hard-code (`probe_stream_support(1, 8,
// false)` plus an NV12 pool). Film grain especially: it is part of the
// Vulkan decode PROFILE, so a grain-bearing stream is a different device
// requirement, not merely different pixels.
assert_eq!(
(
plan.picture.chroma_format_idc,
plan.picture.bit_depth,
plan.sequence.film_grain_params_present,
),
(1, 8, false),
"frame {coded_frames}: Main 4:2:0 8-bit, no film grain"
);
if coded_frames == 1 {
assert!(plan.picture.is_key, "the stream opens on a key frame");
assert_eq!(
(plan.picture.render_width, plan.picture.render_height),
DISPLAY_LOWDELAY_AV1,
"the render region the readback crops to and the goldens hash"
);
assert_eq!(
(plan.picture.upscaled_width, plan.picture.frame_height),
DISPLAY_LOWDELAY_AV1,
"no superres and no AV1 conformance-window equivalent — the coded \
picture IS the render region"
);
}
if !plan.dpb.removed.is_empty() {
with_removals += 1;
}
aliasing_shape += plan
.dpb
.removed
.iter()
.filter(|id| plan.dpb_refs.iter().any(|r| r.id == **id))
.count();
}
}
// AV1's frame accounting, pinned rather than derived. This stream is the SIMPLE
// shape — one shown frame per temporal unit — which is exactly why it must be
// stated: the vendored vector is not, and a leg that learned its habits from one
// of them silently mis-counts the other.
assert_eq!(
(
coded_frames,
outputs,
multi_frame_units,
hidden,
show_existing,
keys
),
(
LOWDELAY_AV1_FRAME_COUNT,
LOWDELAY_AV1_FRAME_COUNT,
0,
0,
0,
1
),
"coded / displayed / multi-frame units / hidden / show_existing / key frames — \
our host emits one shown frame per temporal unit and one key frame at the \
head, against the vendored vector's 274 / 250 / 24 / 24 / 0 / 1"
);
assert_eq!(
outputs,
goldens.len(),
"the planner outputs {outputs} pictures but the goldens carry {}",
goldens.len()
);
// Not the reason this fixture exists — the vendored vector already aliases on 268
// of its 274 frames — but recorded so a regeneration cannot quietly drop below the
// vector's coverage while claiming to be the host-shaped stream.
assert_eq!(
(with_removals, aliasing_shape),
(55, 55),
"55 of the 60 frames displace a reference they still name, which is the \
precondition `release_after_decode` exists for"
);
}
#[test]
fn the_main10_vector_is_ten_bit_and_agrees_with_its_goldens() {
use pf_bitstream::h265::H265Planner;