test(vkdecode): the ten-bit path finally has pixels

Every golden set in this program was eight-bit. So the strongest thing anyone
could say about ten-bit decode was that a Main10 session BUILDS and streams
clean — which is not the same claim, and is exactly the shape of claim this
program has been burned by. A Main10 stream decoding to garbage logs just as
cleanly: HEVC Main10 on D3D11VA has no per-picture status query at all, and on
the Vulkan side the devices that matter report queryResultStatusSupport=false.
The HDR legs were measuring that the pipe ran, not that the pixels were right.

So: a Main10 vector and its goldens, and a ten-bit leg that runs them.

The vector is 50 frames of 320x240 HEVC Main 10 4:2:0 from libx265 — 48 KB,
generated by a command recorded in the golden file's header along with
everything else needed to regenerate it. The goldens come from libavcodec's
software decoder and were cross-checked between two independent builds on two
architectures (ffmpeg 8.1.1 Homebrew/macOS-arm64 and 8.0.1 Ubuntu/x86_64),
which agreed on all 50.

The goldens are P010, NOT yuv420p10le, and that distinction is the whole
reason this could have quietly gone wrong: P010 puts the ten bits in the HIGH
bits of each little-endian 16-bit word with the low six zeroed, which is what
a D3D11 P010 surface and Vulkan's G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16
both contain. Hashing LSB-aligned samples against MSB-aligned ones would fail
on every frame on every driver, for a reason that has nothing to do with
decoding. One golden file therefore serves both native rungs.

The readback is now depth-aware. Its only eight-bit assumption was the second
region's buffer_offset, which is a BYTE offset where the extents around it are
TEXELS — that plus the buffer size are the whole change, because
bufferRowLength = 0 already packs rows at the copy extent. The expected pool
format moved onto the readback so the sizing and the per-frame assertion come
from one source; a readback sized for eight bits that then accepted a ten-bit
frame would hash half a picture and blame the decoder.

A CPU guard asserts the vector really is ten-bit — 4:2:0, both depths
minus8 == 2, 320x240, 50 access units, 50 planned outputs. Without it a
regenerated eight-bit vector would turn the ten-bit leg into a second run of
the eight-bit path wearing a ten-bit name, and it would PASS, because its
goldens would have been regenerated alongside it. That guard is not ignored,
so it runs on macOS and in the container rather than only on the fleet.

Hardware: HEVC Main 10 50/50 bit-identical on NVIDIA 610.43.03 (Linux) and on
the Steam Deck's RADV/VanGogh — first run on both, which also confirms the
P010/3PACK16 layout match rather than assuming it. The four eight-bit legs are
unchanged and still green on both boxes.
This commit is contained in:
2026-08-06 13:32:31 +02:00
parent d3e000768d
commit c91a482b4e
3 changed files with 236 additions and 13 deletions
Binary file not shown.
@@ -0,0 +1,83 @@
# SHA-256 per decoded frame of test-main10.h265, DISPLAY order - 50 frames.
#
# Each frame is the 320x240 picture as tightly packed P010:
# Y plane 320*240 16-bit words = 153600 bytes
# UV plane 160*120 interleaved (U,V) 16-bit pairs = 76800 bytes
# total 230400 bytes/frame
#
# P010, NOT yuv420p10le: the ten bits sit in the HIGH bits of each little-endian
# 16-bit word (15..6) with the low six zeroed, which is what a D3D11 P010 surface
# and a Vulkan G10X6_B10X6R10X6 image both contain. Hashing yuv420p10le instead
# would compare LSB-aligned samples against MSB-aligned ones and fail everywhere
# for a reason that has nothing to do with the decode.
#
# This vector carries no conformance window - coded size IS display size.
#
# Why it exists: every other golden set in this program is 8-bit, so no rung had
# pixel evidence for its ten-bit path. The HDR legs proved a Main10 session BUILDS
# and runs clean, which is not the same claim - D3D11VA has no per-picture status
# query, so a Main10 stream decoding to garbage would log exactly as cleanly.
#
# Generated 2026-08-06 from libavcodec's SOFTWARE decoder (HEVC decoding is exactly
# specified - every conformant decoder is bit-identical), and CROSS-CHECKED between
# two independent builds on two architectures that agreed on all 50 frames:
# ffmpeg 8.1.1 (Homebrew, macOS arm64)
# ffmpeg 8.0.1-3ubuntu2 (Ubuntu, x86_64)
#
# Vector generation (libx265, Main 10, 4:2:0, 2 s at 25 fps):
# ffmpeg -f lavfi -i testsrc2=size=320x240:rate=25:duration=2 \
# -c:v libx265 -pix_fmt yuv420p10le -x265-params "log-level=none:profile=main10" \
# -f hevc test-main10.h265
#
# Goldens:
# ffmpeg -i test-main10.h265 -f rawvideo -pix_fmt p010le - | <sha256 per 230400 bytes>
fe40d5f2aac672155dd200cc64ae3c6c47dc90e6dfcc59f634db0603ea553068
ba62cb199082881c76eca0bccdd41201fd9c1142e97ce89747ef64a836c31f4f
2494e3695cb7be56aabedbfae3dddd10ba0c2e0ad08b51bbe03d7e2d26baccb8
8195990eeb8475d01abd2f0c27cbf20a3abe8d2fc273996aa66528277070aec1
ff45876b191c83688a37898e0fa9757b03215f2d2cf1a1a50f9899c837873a04
a684b50bf67c59052c138cda318c83f85d55132c8f9e8e28a430ea6731fa59c5
65e4dea6aaab1e73a942b3c3a9334cd2a215c36709a4338f3dfc71bf8dca5819
9d8deda227c79a931be366370093c42c5e8ce417044069c883d144197eb089be
48dbfe1702045aa13e3d6f7fa3f95aae2e863c0b792f77a444504c373bc28313
f700d0bb43c2df17e49b036125f313ae41f31ee75f1d6ad895e36e7145a402ca
ec351982a1e14ef5edaba99d578e9d77fa3644ffdd7101d5d3d95b484313b9f7
0526ca0453f5eb05b9cfb694d5a0d30cdd3fdd9e7914b4a527b33fd0c2ad3cef
07fdeeaeb8de894030c60a4fcfccf116488e9f9dbf19de59963f279c9b4bf3de
ef15d30d0bbd9f2bb454a10d30451151ae4300b0b5844f8dda2c06f4feb41d40
f8e7bf24d3d8c3938c4a3b0bf7f2d81af457ecedee22cbf0cfeed8e4e67baa95
fb48d15f9462cf359acab5ae48c91176b7b2931a57755815d4539bc4f3e8ad4b
a8bec00e24b39b55fcca51fde360fce130400202c1ac9370657c60578536acc4
f3fe62e6ed17d4e59dc16b7c8627d494047163e14e5a0521792d3d9680a06fe1
b811ebb99d9339e3fce4a2db070a315385ea7570f01a3d18275bc5a97a359850
b38cd7adda00d5f8e33b7cb8e0d18fdc94686ab16e72bbc67ecb6521d19ab412
cf7946c7be96da66e256e2235ee0f7b2a35e1e344fea2214e2045e8bd5b548c7
635916c2a10c1086efccb71c505a71f4a77e2007f5df165f0be15ea1359eec5b
e70cc81406480acfd686091d023ddb0b5315230fa2449a5d73ff403ea0f6d532
825301a5c79ed4b28f503af92ae569fdb66c53de60c661b93d96467cc2ed6b8d
9739d731f87ad1dd21d26331bb4f2d28857815ed4c2b1d45dde2744da1efa60a
313d001c0c2203eef18a2386c73e63fdf9ac0417dba4339fce95b11c73b68b84
74597aa7d898c4ba79f88fff2c37cfb54dda2f9e9834cbc92b1b090ae730efa3
ef22cd7812608173266162af239e845c66ef71d025ce17a4f815af33a8617760
c6f5f5ce376f4b5dd82f9d01cc16081a352736051eb00b352cf3f4d15ca5de17
a854b63bd8d288734548ddc792c3d67c5e3ec744264038b5f0a319038b129112
f3a14d11678dbd8e4dcfbc8ea0b1cd3503cb2e695858f03cac1e9613744308b2
8d6abe1632b2586ba830a69ea509c72645c1e7cb939adffba05ec846716d2ba9
f809f1e5cff7402bbc281ef4651760858dc5e8feed6e815bdbe780972f03fe0d
43c64be95cfd7512a96f8d82e48198766f7eab292023c2b84780075cae4d0544
4e9e8f2336bafb17267c2deb18b9fd4fc561b2df3cc0ab63a6654d8946a0362a
1003635166c1d34360bca984e31a06dea1583859d0cb515eac6c65f13629ffd2
31ce8f6f38026317ed31bafc11c8ecdc5fa6c14cf72835126a6def695c4934d2
eaeb25f2e4f6a881fc3d707a3076eab45c5605880c953f552591d199ba48ab0b
ec6aa73e67516f2e6053cfb3ab6df304ee4093190ef6b52442b2ad2cf598f287
f07f7d2d5e5d48d37804e33234a0fa029cc7abf60ca19d21663ef8435d9c3ad8
a0f5d8dab08fe6764eb46c46eb4320a105329b805ab6dd0f4ce0758b4fc10409
3f98a8dc1bd2c7eca8fc39945195b16de868db1ff0445d9ac2aa74dbcc370b8a
0ca2d9f7251aa020c14a6548922e55cbf010d14c2c3a2a27db3eb8a9a4c185b8
79b6e0898dfdddca8a878ace9742aa3607b3a968e56b95d3aef03dea3b11cf71
59487538cdcd63a48a28e2682fd56cc6dbe67ca00c696ddf06b21f605dac91dd
dc98f561f0ecc58824123fd8cfa5f0c2ce96bf4fafa56ca1d062b00df12f2d7f
394804f6a890eeecf6ffbd7a54015cc248ced7e0a951e921a329d615522d3102
7f8d56e2e57099e1cd456c9cb67748de521a398180b86f52e06bb6ef6ca8d816
a860ede88b1f66e2d08a1755f6d49e70b553270c61e709ca0b1ca5adac3e1594
c6b7df04a6292863041fe141f6a4cb4eaf0d27f267e733e1ba75a6fe3f8f05cc
+153 -13
View File
@@ -67,6 +67,16 @@ const GOLDENS_H264: &str = include_str!("data/test-25fps.nv12.sha256");
/// The H.265 twin, cross-checked between two independent FFmpeg builds (header).
const GOLDENS_H265: &str = include_str!("data/test-25fps-h265.nv12.sha256");
/// The ten-bit vector and its goldens. No hardware leg in this file consumes them
/// yet — the D3D11VA rung is where the ten-bit parity leg currently runs — but the
/// files live here, beside the other goldens, so the guard that keeps them honest
/// belongs here too and runs on every platform rather than only on Windows.
const TEST_MAIN10_H265: &[u8] = include_bytes!("data/test-main10.h265");
const GOLDENS_MAIN10: &str = include_str!("data/test-main10.p010.sha256");
/// The Main 10 vector is 50 display frames.
const MAIN10_FRAME_COUNT: usize = 50;
/// The H.264 vector's display (conformance-window) region; the goldens hash
/// exactly this as tightly packed NV12.
const DISPLAY_H264: (u32, u32) = (320, 240);
@@ -126,7 +136,17 @@ struct Readback {
mapped: *const u8,
/// The display region every read copies, and the crop it requires.
display: (u32, u32),
/// `w * h * 3 / 2` — the tightly packed NV12 frame this buffer holds.
/// The picture format the pool must carry. Held here rather than read from a
/// module constant so the sizing below and the per-frame assertion come from
/// ONE source — a readback sized for eight bits that then accepted a ten-bit
/// frame would hash half a picture and blame the decoder.
format: vk::Format,
/// 1 for NV12, 2 for the `3PACK16` ten-bit family (its samples are 16-bit
/// words with the ten bits in the high end — the same layout P010 has, which
/// is why one golden file serves both this rung and the D3D11VA one).
bytes_per_sample: u32,
/// `w * h * 3 / 2 * bytes_per_sample` — the tightly packed frame this buffer
/// holds.
frame_bytes: usize,
}
@@ -142,16 +162,22 @@ impl Readback {
device: &ash::Device,
graphics_qf: u32,
display: (u32, u32),
format: vk::Format,
) -> Self {
let (width, height) = display;
// The two-plane copy below halves both dimensions for the R8G8 plane, so
// The two-plane copy below halves both dimensions for the chroma plane, so
// an odd display region would silently drop a chroma row/column.
assert_eq!(
(width % 2, height % 2),
(0, 0),
"the display region must be chroma-aligned"
);
let frame_bytes = (width * height * 3 / 2) as usize;
let bytes_per_sample = match format {
f if f == pf_vkdecode::NV12 => 1,
f if f == pf_vkdecode::P010 => 2,
other => panic!("readback has no sample size for {other:?}"),
};
let frame_bytes = (width * height * 3 / 2 * bytes_per_sample) as usize;
// SAFETY: fn contract — live device, queue 0 of this family exists.
let queue = unsafe { device.get_device_queue(graphics_qf, 0) };
@@ -219,6 +245,8 @@ impl Readback {
memory,
mapped,
display,
format,
bytes_per_sample,
frame_bytes,
}
}
@@ -309,7 +337,9 @@ impl Readback {
},
},
vk::BufferImageCopy {
buffer_offset: u64::from(width * height),
// A BYTE offset, unlike the extents above, which are texels: the
// luma plane occupies `w * h * bytes_per_sample` bytes.
buffer_offset: u64::from(width * height * self.bytes_per_sample),
buffer_row_length: 0,
buffer_image_height: 0,
image_subresource: layers(vk::ImageAspectFlags::PLANE_1),
@@ -440,8 +470,9 @@ fn consume_frame(
// hash differently for a reason no mismatch report could explain
// (`DecodedVkFrame::format` docs) — refuse it here instead.
assert_eq!(
frame.format, EXPECTED_FORMAT,
"frame {index}: 8-bit 4:2:0 vector must decode into an NV12 pool"
frame.format, readback.format,
"frame {index}: the vector must decode into the pool format the readback \
was built for"
);
// SAFETY: the frame is delivered and unreleased on the readback's device;
// the pool carries TRANSFER_SRC (PF_VKD_TEST_READBACK was set before the
@@ -573,6 +604,7 @@ fn h264_parity_run(aus: &[&[u8]], label: &str) {
&setup.device,
setup.graphics_qf,
DISPLAY_H264,
EXPECTED_FORMAT,
)
};
let hashes = collect_hashes(&mut decoder, &readback, aus);
@@ -614,16 +646,24 @@ fn h264_four_byte_start_codes_decode_bit_identically() {
/// The H.265 twin of [`h264_parity_run`]; see its docs for why the AUs are a
/// parameter.
fn h265_parity_run(aus: &[&[u8]], label: &str) {
fn h265_parity_run(
aus: &[&[u8]],
goldens_file: &'static str,
expected_frames: usize,
bit_depth_luma_minus8: u8,
format: vk::Format,
display: (u32, u32),
label: &str,
) {
// As the H.264 leg: one codec at a time, `set_var` under the lock.
let _gpu = common::gpu_lock();
std::env::set_var("PF_VKD_TEST_READBACK", "1");
let goldens = golden_hashes(GOLDENS_H265);
let goldens = golden_hashes(goldens_file);
assert_eq!(
goldens.len(),
FRAME_COUNT,
expected_frames,
"the golden file carries one hash per libavcodec frame"
);
@@ -644,8 +684,10 @@ fn h265_parity_run(aus: &[&[u8]], label: &str) {
// host the combination refuses here with a caps reason instead of failing
// mid-stream.
decoder
.probe_stream_support(1, 0)
.expect("the box must host H.265 Main 8-bit 4:2:0 (the vector's shape)");
.probe_stream_support(1, bit_depth_luma_minus8)
.unwrap_or_else(|e| {
panic!("{label}: the box must host this H.265 shape — {e:?}");
});
// SAFETY: as the H.264 leg — live instance/device, queue 0 of
// `graphics_qf` exists; destroyed at the end of this block.
let readback = unsafe {
@@ -654,7 +696,8 @@ fn h265_parity_run(aus: &[&[u8]], label: &str) {
setup.pd,
&setup.device,
setup.graphics_qf,
DISPLAY_H265,
display,
format,
)
};
let hashes = collect_hashes(&mut decoder, &readback, aus);
@@ -673,7 +716,37 @@ fn h265_parity_run(aus: &[&[u8]], label: &str) {
#[test]
#[ignore = "needs a Vulkan Video H.265 decode device (fleet boxes; see module docs)"]
fn h265_every_frame_hashes_bit_identical_to_libavcodec() {
h265_parity_run(&common::split_h265_aus(common::TEST_25FPS_H265), "H.265");
h265_parity_run(
&common::split_h265_aus(common::TEST_25FPS_H265),
GOLDENS_H265,
FRAME_COUNT,
0,
EXPECTED_FORMAT,
DISPLAY_H265,
"H.265",
);
}
/// The ten-bit path — the only leg in this file that is not eight-bit.
///
/// Every other golden set in this program is NV12, so no rung had pixel evidence
/// for its ten-bit path: the HDR legs proved a Main10 session BUILDS and streams
/// clean, which a stream decoding to garbage would also do. The goldens are P010
/// and the Vulkan pool is `G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16`, whose
/// samples are 16-bit words with the ten bits in the high end — the same layout,
/// which is why one golden file serves this rung and the D3D11VA one.
#[test]
#[ignore = "needs a Vulkan Video H.265 Main 10 decode device (fleet boxes; see module docs)"]
fn main10_every_frame_hashes_bit_identical_to_libavcodec() {
h265_parity_run(
&common::split_h265_aus(TEST_MAIN10_H265),
GOLDENS_MAIN10,
MAIN10_FRAME_COUNT,
2,
pf_vkdecode::P010,
(320, 240),
"HEVC Main 10",
);
}
/// The HEVC leg of the production prefix form — the one that would have caught
@@ -684,6 +757,11 @@ fn h265_four_byte_start_codes_decode_bit_identically() {
let stream = common::h265_four_byte_start_codes(common::TEST_25FPS_H265);
h265_parity_run(
&common::split_h265_aus(&stream),
GOLDENS_H265,
FRAME_COUNT,
0,
EXPECTED_FORMAT,
DISPLAY_H265,
"H.265 (4-byte start codes)",
);
}
@@ -844,6 +922,68 @@ fn h264_goldens_and_au_split_agree_with_the_planner() {
);
}
#[test]
fn the_main10_vector_is_ten_bit_and_agrees_with_its_goldens() {
use pf_bitstream::h265::H265Planner;
let goldens = golden_hashes(GOLDENS_MAIN10);
assert_eq!(
goldens.len(),
MAIN10_FRAME_COUNT,
"data/test-main10.p010.sha256 must carry one hash per display frame"
);
assert!(
goldens
.iter()
.all(|line| line.len() == 64 && line.bytes().all(|b| b.is_ascii_hexdigit())),
"every golden line is a bare lowercase SHA-256 hex digest"
);
let aus = common::split_h265_aus(TEST_MAIN10_H265);
assert_eq!(
aus.len(),
MAIN10_FRAME_COUNT,
"the Main 10 vector is {MAIN10_FRAME_COUNT} access units"
);
let mut planner = H265Planner::new();
let mut outputs = 0usize;
for (index, au) in aus.iter().enumerate() {
let plan = planner.plan_au(au).unwrap_or_else(|e| {
panic!("AU {index}: the Main 10 vector must plan without errors, got {e:?}")
});
// The whole reason this vector exists. Every other golden set in this
// program is eight-bit; a regenerated vector that came out eight-bit would
// turn the ten-bit parity leg into a second run of the eight-bit path, and
// it would PASS, because its goldens would have been regenerated with it.
assert_eq!(
(
plan.picture.chroma_format_idc,
plan.picture.bit_depth_luma_minus8,
plan.picture.bit_depth_chroma_minus8,
),
(1, 2, 2),
"AU {index}: the Main 10 vector must stay 4:2:0 at ten bits"
);
if index == 0 {
assert!(plan.picture.is_idr, "the vector opens with an IDR");
assert_eq!(
(plan.picture.coded_width, plan.picture.coded_height),
(320, 240),
"the goldens hash a 320x240 picture"
);
}
outputs += plan.dpb.outputs.len();
}
outputs += planner.flush().outputs.len();
assert_eq!(
outputs,
goldens.len(),
"the planner outputs {outputs} pictures but the goldens carry {}",
goldens.len()
);
}
/// Count Annex-B start codes in `stream` as `(total, three_byte)`.
///
/// Emulation prevention guarantees `00 00 01` cannot occur inside a NAL payload,