Merge branch 'fix/vaapi-h264-h265-hardware-proof' into integration/decode-aliasing-program

This commit is contained in:
2026-08-07 23:12:02 +02:00
@@ -2548,4 +2548,472 @@ mod tests {
"the vector displays 250 frames (274 coded, 24 hidden)"
);
}
// ---------------------------------------------------------------------
// H.264 / H.265 — the two legs that had never decoded a frame anywhere
// ---------------------------------------------------------------------
/// The vendored H.264 vector: **250 access units** of 320x240 High 4:2:0 8-bit,
/// TWO slice NALUs per picture (500 slice NALs over 250 AUs, 4 IDRs). The same
/// file, at the same relative path, that `pf-vkdecode`'s Vulkan legs and
/// `video_d3d11_native`'s D3D11VA leg decode — so a count that disagrees with
/// theirs is this rung's problem rather than the vector's.
const H264_25FPS: &[u8] = include_bytes!(
"../../pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264"
);
/// The vendored H.265 twin: 250 access units, 320x240 Main 8-bit 4:2:0, ONE slice
/// per picture, one `IDR_N_LP` then 249 TRAIL pictures.
const H265_25FPS: &[u8] = include_bytes!(
"../../pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265"
);
/// HEVC **Main 10**: 50 access units of 320x240 4:2:0 ten-bit, from libx265.
///
/// Worth a third leg rather than a variation on the second because ten bits is a
/// different VAAPI PROFILE (`VAProfileHEVCMain10`), a different render-target
/// format (`VA_RT_FORMAT_YUV420_10`) and a different surface fourcc (**P010**, not
/// NV12) — three branches of `Session::build` that no 8-bit leg reaches, on the
/// path every HDR session takes. `finish` refuses a surface whose exported fourcc
/// is not the one the pool was created with, so this leg is also the only thing
/// that would catch a driver quietly handing back NV12 for a ten-bit stream.
const MAIN10_H265: &[u8] = include_bytes!("../../pf-vkdecode/tests/data/test-main10.h265");
/// Both 8-bit vectors are 250 access units.
const H26X_AU_COUNT: usize = 250;
/// The Main 10 vector is 50.
const MAIN10_AU_COUNT: usize = 50;
/// How many frames each vector can yield THROUGH THIS RUNG — which is not how many
/// frames it contains, and the gap is a property of the rung worth stating once
/// here rather than three times below.
///
/// [`finish`] shows `outputs.last()` and never more: one frame per access unit, at
/// most. So an access unit whose plan bumps SEVERAL pictures out of the DPB — which
/// is what an IDR with `no_output_of_prior_pics_flag` clear does, and what ordinary
/// B-pyramid reordering does at every other picture — displays the last of them and
/// drops the rest, and an access unit whose plan outputs nothing yet displays
/// nothing. There is no end-of-stream flush either, so whatever is still in the DPB
/// when the vector ends never comes out.
///
/// Measured on `.25` and reproduced exactly by
/// [`the_planner_already_says_how_many_frames_these_legs_can_deliver`], which is
/// what keeps these three numbers explanations rather than recordings:
///
/// | vector | pictures the planner outputs | this rung delivers | dropped |
/// |---|---|---|---|
/// | H.264 | 243 (7 stranded in the DPB) | **225** | 18, at the 3 IDRs that drain the DPB |
/// | H.265 | 249 (1 stranded) | **204** | 45, one on each of the 45 AUs that bump two |
/// | Main 10 | 48 (2 stranded) | **45** | 3, likewise |
///
/// ⚠ This does NOT bite punktfunk's own streams and is not what these legs exist to
/// find: hosts emit zero-reorder low-delay output with no B pictures, so `outputs`
/// never holds more than one picture and the rung is exact. It is the same
/// divergence `video_d3d11_native`'s parity module records for the D3D11VA rung,
/// and it is written down here for the same reason — a conformance vector reorders,
/// punktfunk does not, and a reader comparing 225 against "250 frames" needs to
/// know which of the two they are looking at.
const H264_DELIVERED: usize = 225;
const H265_DELIVERED: usize = 204;
const MAIN10_DELIVERED: usize = 45;
/// Byte offsets of every Annex-B NAL header in `stream`, in order.
///
/// Emulation prevention guarantees `00 00 01` cannot appear inside a NAL payload,
/// so scanning for it finds start codes and nothing else; the header begins on the
/// byte after. Hand-rolled for the same reason [`split_ivf`] is — `pf-client-core`
/// does not depend on the vendored parser crate — and a VERBATIM port of
/// `video_d3d11_native`'s, so the two platform rungs are driven over the same
/// access units rather than over two splitters free to disagree. Kept honest by
/// the AU counts [`the_annex_b_splitters_still_cut_the_vendored_vectors`] asserts
/// on every ordinary Linux test run, which no plausible splitter bug survives.
fn nal_headers(stream: &[u8]) -> Vec<usize> {
let mut out = Vec::new();
let mut i = 0usize;
while i + 3 <= stream.len() {
if stream[i..i + 3] == [0x00, 0x00, 0x01] {
out.push(i + 3);
i += 3;
} else {
i += 1;
}
}
out
}
/// Split `stream` into access units, given a per-NAL `(is_slice, starts_a_picture)`
/// rule. A new AU begins at a non-VCL NALU following slices, or at a slice that
/// declares itself the first of a picture when the current AU already has slices —
/// the same rule pf-bitstream applies, spelled once for both codecs.
fn split_aus(stream: &[u8], classify: impl Fn(&[u8], usize) -> (bool, bool)) -> Vec<&[u8]> {
let mut aus = Vec::new();
let mut au_start = 0usize;
let mut au_has_slice = false;
for header in nal_headers(stream) {
let (is_slice, first_in_picture) = classify(stream, header);
// The start code owning this header: three bytes, plus the optional
// leading zero byte of the four-byte form.
let mut start = header - 3;
if start > 0 && stream[start - 1] == 0x00 {
start -= 1;
}
if au_has_slice && (!is_slice || first_in_picture) {
aus.push(&stream[au_start..start]);
au_start = start;
au_has_slice = false;
}
au_has_slice |= is_slice;
}
aus.push(&stream[au_start..]);
aus
}
/// H.264: one-byte NAL header, `nal_unit_type` in the low 5 bits (1 = non-IDR
/// slice, 5 = IDR slice), and `first_mb_in_slice == 0` is the top bit of the byte
/// after it. Load-bearing rather than decorative on this vector: it codes two
/// slices per picture, so without the flag every picture would be split in two.
fn split_h264_aus(stream: &[u8]) -> Vec<&[u8]> {
split_aus(stream, |s, h| {
let is_slice = matches!(s[h] & 0x1f, 1 | 5);
let first = is_slice && s.get(h + 1).is_some_and(|b| b & 0x80 != 0);
(is_slice, first)
})
}
/// H.265: TWO-byte NAL header, `nal_unit_type` in bits 1..7 of the first byte and
/// "is a slice" the numeric range `< 32`, so `first_slice_segment_in_pic_flag` is
/// the top bit of the byte at `+2` where H.264 reads `+1`. Getting either wrong
/// silently merges or splits AUs, which surfaces as a frame-count mismatch a long
/// way from its cause.
fn split_h265_aus(stream: &[u8]) -> Vec<&[u8]> {
split_aus(stream, |s, h| {
let is_slice = (s[h] >> 1) & 0x3f < 32;
let first = is_slice && s.get(h + 2).is_some_and(|b| b & 0x80 != 0);
(is_slice, first)
})
}
/// The splitters cut both vendored vectors into the access units every other rung's
/// legs count, and the Main 10 vector really is ten-bit.
///
/// NOT `#[ignore]`d, unlike everything below it: the splitters are pure CPU, they
/// are a hand-rolled copy of code that lives in two other crates, and a drift in
/// them would reach the hardware legs as a frame-count mismatch on a box someone
/// had to walk to. Ordinary `cargo test -p pf-client-core --lib` catches it here
/// instead.
///
/// The ten-bit check is the same guard `video_d3d11_native` carries and for the
/// same reason: a regenerated vector that came out 8-bit would turn
/// [`hevc_main10_decodes_the_ten_bit_vector_on_this_machines_vaapi`] into a second
/// run of the 8-bit path wearing a ten-bit name, and it would pass.
#[test]
fn the_annex_b_splitters_still_cut_the_vendored_vectors() {
assert_eq!(
split_h264_aus(H264_25FPS).len(),
H26X_AU_COUNT,
"H.264 vector access units"
);
assert_eq!(
split_h265_aus(H265_25FPS).len(),
H26X_AU_COUNT,
"H.265 vector access units"
);
let main10 = split_h265_aus(MAIN10_H265);
assert_eq!(main10.len(), MAIN10_AU_COUNT, "Main 10 vector access units");
let mut planner = pf_vaadec::H265Planner::new();
let plan = planner
.plan_au(main10[0])
.expect("the Main 10 vector's first access unit must plan");
assert_eq!(
(
plan.picture.chroma_format_idc,
plan.picture.bit_depth_luma_minus8
),
(1, 2),
"the Main 10 vector must be 4:2:0 at ten bits"
);
}
/// Access units whose plan outputs at least one picture — one delivered frame each,
/// and the ONLY thing that separates [`H264_DELIVERED`] and friends from the
/// vectors' frame counts.
///
/// Two small walks rather than one generic one because the two planners share no
/// trait: `AuPlan` and `AuPlanH265` are different types with the same `dpb.outputs`
/// field, which is exactly the shape a macro would obscure for six saved lines.
fn output_bearing_aus_h264(aus: &[&[u8]]) -> usize {
let mut planner = pf_vaadec::H264Planner::new();
aus.iter()
.filter(|au| {
!planner
.plan_au(au)
.expect("the vendored H.264 vector plans")
.dpb
.outputs
.is_empty()
})
.count()
}
/// [`output_bearing_aus_h264`] for HEVC. A skipped RASL picture counts as no
/// output, which is what the rung does with it too ([`NativeVaapiDecoder::decode`]).
fn output_bearing_aus_h265(aus: &[&[u8]]) -> usize {
let mut planner = pf_vaadec::H265Planner::new();
aus.iter()
.filter(|au| match planner.plan_au(au) {
Ok(plan) => !plan.dpb.outputs.is_empty(),
Err(pf_vaadec::PlanErrorH265::RaslSkipped { .. }) => false,
Err(e) => panic!("the vendored HEVC vector must plan: {e:?}"),
})
.count()
}
/// The three delivered-frame counts the hardware legs assert are what the PLANNER
/// implies, not what a hardware run happened to print.
///
/// This is the difference between a number that explains itself and a number
/// somebody wrote down: it runs on any Linux box, with no GPU and no libva, and it
/// fails the moment a vector is regenerated or the planner's bumping changes —
/// which would otherwise show up as three mysterious hardware failures on a machine
/// somebody had to walk to. See [`H264_DELIVERED`] for why the counts are below the
/// vectors' frame counts at all.
#[test]
fn the_planner_already_says_how_many_frames_these_legs_can_deliver() {
assert_eq!(
output_bearing_aus_h264(&split_h264_aus(H264_25FPS)),
H264_DELIVERED,
"H.264: access units whose plan outputs a picture"
);
assert_eq!(
output_bearing_aus_h265(&split_h265_aus(H265_25FPS)),
H265_DELIVERED,
"H.265: access units whose plan outputs a picture"
);
assert_eq!(
output_bearing_aus_h265(&split_h265_aus(MAIN10_H265)),
MAIN10_DELIVERED,
"Main 10: access units whose plan outputs a picture"
);
}
/// The first frame a leg got back — enough to say the rung exported a real surface
/// of the right shape and pixel format, which is the most it can honestly claim.
#[derive(Clone, Copy)]
struct FirstFrame {
width: u32,
height: u32,
fourcc: u32,
modifier: u64,
keyframe: bool,
}
/// Drive one Annex-B vector's access units through a freshly built rung and report
/// how many frames came back and what the first one was.
///
/// Shared by all three H.26x legs so that "the H.265 leg proves the same thing the
/// H.264 leg does" is a fact about one function rather than a claim about three
/// hand-copied ones — the same reasoning `pf-vkdecode`'s `common` module records
/// for binding its three decoders to one driver.
///
/// # What these legs prove, and what they do not
///
/// Deliberately weaker than the Vulkan and D3D11VA H.26x legs, and the difference
/// is worth stating rather than hiding behind a test name: those hash every decoded
/// frame against libavcodec's goldens, because both can read their decoded surface
/// back. This rung hands out a **DRM-PRIME dmabuf** whose memory the driver tiles,
/// so there is no CPU-readable image to hash without adding a
/// `vaDeriveImage`/`vaGetImage` path that production does not use and does not
/// want. So this asserts what CAN be asserted honestly — that every access unit is
/// accepted, that the expected number of frames comes back, and that each one is a
/// real exported surface of the right shape and fourcc — and it is **NOT frame-hash
/// parity**. It is what turns "never decoded a frame anywhere" into a measurement;
/// promoting these legs to `verified` still wants parity, and parity wants a
/// readback path that does not exist yet.
///
/// Fails loudly rather than skipping when the device has no entry point for the
/// profile: these legs are `#[ignore]`d, so they only run when someone deliberately
/// points them at a box that is supposed to have one, and a silent pass there is
/// the invisible-failure mode this whole program exists to end.
fn run_annex_b(
codec: pf_vaadec::Codec,
stream: StreamFormat,
aus: &[&[u8]],
label: &str,
) -> (usize, FirstFrame) {
let mut decoder = NativeVaapiDecoder::new(codec, stream).unwrap_or_else(|e| {
panic!(
"{label}: this box is supposed to have a VAAPI {label} decode entry point: {e:#}"
)
});
eprintln!("VAAPI {label} rung constructed: {}", decoder.name());
let mut delivered = 0usize;
let mut first: Option<FirstFrame> = None;
for (index, au) in aus.iter().enumerate() {
match decoder.decode(au) {
Ok(Some(frame)) => {
assert!(
!frame.planes.is_empty(),
"{label} AU {index}: a delivered frame exported no dmabuf planes"
);
if first.is_none() {
first = Some(FirstFrame {
width: frame.width,
height: frame.height,
fourcc: frame.fourcc,
modifier: frame.modifier,
keyframe: frame.keyframe,
});
}
delivered += 1;
}
Ok(None) => {}
Err(e) => panic!("{label} AU {index}: VAAPI decode failed: {e:#}"),
}
}
let first = first.unwrap_or_else(|| panic!("{label}: not one frame came back"));
let FirstFrame {
width,
height,
fourcc,
modifier,
keyframe,
} = first;
eprintln!(
"VAAPI {label}: {delivered} of {} access units delivered a frame, first \
{width}x{height} fourcc={:?} modifier={modifier:#x} keyframe={keyframe}",
aus.len(),
std::str::from_utf8(&fourcc.to_le_bytes()).unwrap_or("?"),
);
(delivered, first)
}
/// Does this machine's VAAPI actually DECODE H.264 — the question the evidence
/// table has answered "no hardware has ever tried" since M6.
///
/// See [`run_annex_b`] for what this proves and, more importantly, what it does
/// not: it is a decode measurement, not frame-hash parity.
#[test]
#[ignore = "needs a machine with a libva runtime and an H.264 VLD entry point"]
fn h264_decodes_the_vendored_vector_on_this_machines_vaapi() {
let aus = split_h264_aus(H264_25FPS);
assert_eq!(aus.len(), H26X_AU_COUNT, "the H.264 vector is 250 AUs");
let (delivered, first) = run_annex_b(
pf_vaadec::Codec::H264,
StreamFormat::SDR_420_8,
&aus,
"H.264",
);
assert_eq!((first.width, first.height), (320, 240), "320x240");
assert_eq!(
first.fourcc,
pf_vaadec::VA_FOURCC_NV12,
"an 8-bit pool exports NV12"
);
assert_eq!(
delivered,
output_bearing_aus_h264(&aus),
"every access unit whose plan outputs a picture must deliver one"
);
assert_eq!(
delivered, H264_DELIVERED,
"see H264_DELIVERED for why this is 225 and not 250"
);
// ⚠ A DEFECT this leg found, asserted so that fixing it is noticed rather than
// so that it is preserved. `finish` is handed the CURRENT access unit's
// `is_idr`, not the flag of the picture it is about to display — and on a
// reordering stream those are different pictures. The first frame delivered
// here IS the IDR, bumped out several access units after it decoded, and it
// arrives flagged `keyframe: false`; conversely the AU that drains the DPB at a
// later IDR flags whichever OLD picture it displays as a keyframe. The flag is
// `DecodedImage::is_keyframe`, the pump's post-loss re-anchor signal, so a rung
// that mislabels it would keep asking for a keyframe it has already been sent.
// It cannot bite punktfunk today for the same reason the frame count cannot:
// hosts emit zero-reorder output, so the decoded picture and the displayed one
// are always the same picture. Fix it and this line is the one to delete.
assert!(
!first.keyframe,
"the rung labels the ACCESS UNIT, not the picture it delivers — if this \
now passes the label was fixed, which is good; delete this assertion"
);
}
/// The same question for H.265, whose leg has never decoded a frame either.
///
/// Not a redundant copy of the H.264 leg: HEVC reaches an entirely different
/// conversion in `pf-vaadec` (its own picture parameters, its own reference-picture
/// set, its own slice header) and a different arm of [`NativeVaapiDecoder::decode`]
/// — including the `RaslSkipped` Ok-skip no other codec has.
#[test]
#[ignore = "needs a machine with a libva runtime and an HEVC Main VLD entry point"]
fn h265_decodes_the_vendored_vector_on_this_machines_vaapi() {
let aus = split_h265_aus(H265_25FPS);
assert_eq!(aus.len(), H26X_AU_COUNT, "the H.265 vector is 250 AUs");
let (delivered, first) = run_annex_b(
pf_vaadec::Codec::H265,
StreamFormat::SDR_420_8,
&aus,
"H.265",
);
assert_eq!((first.width, first.height), (320, 240), "320x240");
assert_eq!(
first.fourcc,
pf_vaadec::VA_FOURCC_NV12,
"an 8-bit pool exports NV12"
);
assert_eq!(
delivered,
output_bearing_aus_h265(&aus),
"every access unit whose plan outputs a picture must deliver one"
);
assert_eq!(
delivered, H265_DELIVERED,
"see H264_DELIVERED for why this is 204 and not 250"
);
assert!(!first.keyframe, "the same mislabel the H.264 leg documents");
}
/// And the ten-bit leg, which is the one every HDR session lands on.
///
/// The fourcc assertion is the point of running it at all: `Session::build` picks
/// P010 from the SPS's bit depth, and a pool that came out NV12 would be a ten-bit
/// stream decoded into an 8-bit surface.
#[test]
#[ignore = "needs a machine with a libva runtime and an HEVC Main 10 VLD entry point"]
fn hevc_main10_decodes_the_ten_bit_vector_on_this_machines_vaapi() {
let aus = split_h265_aus(MAIN10_H265);
assert_eq!(aus.len(), MAIN10_AU_COUNT, "the Main 10 vector is 50 AUs");
let (delivered, first) = run_annex_b(
pf_vaadec::Codec::H265,
StreamFormat {
bit_depth: 10,
..StreamFormat::SDR_420_8
},
&aus,
"HEVC Main 10",
);
assert_eq!((first.width, first.height), (320, 240), "320x240");
assert_eq!(
first.fourcc,
pf_vaadec::VA_FOURCC_P010,
"a ten-bit stream must build a P010 pool, not an 8-bit one"
);
assert_eq!(
delivered,
output_bearing_aus_h265(&aus),
"every access unit whose plan outputs a picture must deliver one"
);
assert_eq!(
delivered, MAIN10_DELIVERED,
"see H264_DELIVERED for why this is 45 and not 50"
);
assert!(!first.keyframe, "the same mislabel the H.264 leg documents");
}
}