feat(vkdecode): HEVC's exemption stops being an argument and becomes a vendored stream
`fd6241a2` made HEVC's freedom from the release-ordering defect falsifiable on CPU and
recorded what was still missing: no low-delay HEVC stream was vendored, so the exemption
rested on a structural argument plus one throwaway measurement. This vendors the stream,
and the exemption HELD.
**The fixture.** `lowdelay-640x480.h265`, 270 KB, 120 pictures — `punktfunk-host spike
--source synthetic --codec h265 --width 640 --height 480 --fps 60 --seconds 2 --bitrate 1`
on .21 (NVENC, RTX 5070 Ti, driver 610.57.04). Deliberately the H.264 sibling's resolution
and frame count: the two are then directly comparable, 640 and 480 are both multiples of
MinCbSizeY so there is no conformance window and a hash mismatch can only be decode rather
than readback geometry, and 270 KB sits alongside the 282 KB already accepted for H.264.
Goldens are libavcodec's software decode, cross-checked BIT-IDENTICAL across ffmpeg n8.1.2
(Arch, x86_64) and 8.1.1 (Homebrew, macOS arm64), 120 of 120 digests distinct.
**The exemption held, measured rather than argued.** `sps_max_dec_pic_buffering_minus1 = 4`
against the four pictures 8.3.2 keeps marked in steady state, `sps_max_num_reorder_pics = 0`,
`numRefL0 = 1` — a five-picture DPB filled exactly by four references plus the current
picture. 115 of the 120 access units retire a picture, and `removed ∩ dpb_refs` is **0 of
120**. A 300-picture 1080p stream from the same host reports the same shape: 295
retirements, 0 intersections. It is the encoder and not the resolution, exactly as for
H.264.
**A zero proves nothing on its own, so the fixture is pinned by its counterfactual.**
`test-25fps.h264` reported zero for two milestones while every stream we ship aliased on
99% of its frames. So the guarantee here is not "we looked and it was fine": hand
`plan_to_dxva_h265` the marked DPB as it stood BEFORE `decode_rps` — the mutation a
snapshot move would cause, reconstructed exactly as `dpb_refs(N-1) ∪ {stored(N-1)}` — and
the alias appears on **115 of 120** access units, driven through the real conversion rather
than through planner arithmetic. If a regeneration ever produced a stream that reordered,
or a DPB deeper than its reference count, that 115 collapses to 0 and the tests say so
instead of continuing to pass.
**The two rungs are exempt for different reasons, and the asymmetry is now a gate.** DXVA
binds the whole marked DPB — `RefPicList` is spec-defined that way, and an RFI long-term
anchor has to survive in it — so its exemption really is `H265Planner`'s snapshot ordering,
one call away from being untrue. `plan_to_vk_h265` never reads `dpb_refs` at all:
`pReferenceSlots` is the slots the operation uses, so it binds the current RPS sets, which
`decode_rps` itself derives and which therefore cannot name a picture that same RPS just
dropped. A new test feeds that conversion the identical widened snapshot and asserts
nothing changes, so a future change making the Vulkan rung bind the marked DPB — a
legitimate thing to want, since a *Foll* anchor invisible to the hardware is the RFI
failure shape — fails loudly instead of silently acquiring the defect.
What the Vulkan pixel leg adds is therefore NOT aliasing coverage, and its docs say so:
it is the first HEVC frame either rung has decoded from our own encoder, under a DPB that
retires and reissues a slot on 115 of 120 access units back to back, where the vendored
vector's reordering keeps that eviction slack.
Legs: `low_delay_host_h265_every_frame_hashes_bit_identical_to_libavcodec` on the Vulkan
rung (10 ignored legs now, up from 9) and on the D3D11VA rung, plus three non-ignored CPU
guards that run in ordinary CI.
Verified: 10/10 Vulkan parity legs on .21 (RTX 5070 Ti, 610.57.04), the new one 120/120
bit-identical; workspace clippy `-D warnings` and `cargo fmt --all --check` clean on .21.
This commit is contained in:
@@ -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
|
||||
@@ -1767,6 +1776,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;
|
||||
|
||||
@@ -2655,6 +2690,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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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.
@@ -46,6 +46,18 @@
|
||||
//! prefix for a driver to mis-skip and no second framing to test (see
|
||||
//! `common::split_av1_aus`). Its absence is deliberate.
|
||||
//!
|
||||
//! # The two legs that decode OUR OWN streams
|
||||
//!
|
||||
//! [`LOWDELAY_H264`] and [`LOWDELAY_H265`] 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. The
|
||||
//! H.264 one is here because it 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); the H.265 one is here because HEVC 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. Both are backed by a non-ignored CPU guard that
|
||||
//! asserts the stream still has the property it was vendored for.
|
||||
//!
|
||||
//! # Why the AV1 leg exists at all
|
||||
//!
|
||||
//! Because until it did, the AV1 rung had no pixel evidence whatsoever. An
|
||||
@@ -139,6 +151,51 @@ 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");
|
||||
|
||||
/// 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 +989,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]
|
||||
@@ -1725,6 +1815,160 @@ 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"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_main10_vector_is_ten_bit_and_agrees_with_its_goldens() {
|
||||
use pf_bitstream::h265::H265Planner;
|
||||
|
||||
Reference in New Issue
Block a user