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:
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
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user