undocumented_unsafe_blocks joins unsafe_op_in_unsafe_fn in
[workspace.lints], and the ~100 scattered per-file #![deny(...)] attributes
(85 files) are deleted — a new crate, or a new module in an old one, is now
covered on creation rather than on remembering. The per-file form is how
pf-vkhdr-layer, wdk-probe and half of pf-clipboard stayed uncovered.
There are THREE workspaces, so the claim is made three times: the main
Cargo.toml, packaging/windows/drivers (workspace table + [lints]
workspace = true in all seven members), and packaging/windows/pf-vkhdr-layer
(its [lints] table, previous commit). pf-update now opts into workspace
lints; the two vendored member snapshots (cros-codecs, usbip-sim) stay out
deliberately and now both say so.
Newly-covered fallout was two link-sanity tests (pyrowave-sys, libvpl-sys)
— proofs written. Stale prose that claimed the workspace held
unsafe_op_in_unsafe_fn at "warn" (it has been deny) or pointed at the
deleted attributes is corrected.
nvenc_core.rs is carved OUT of the unsafe_op_in_unsafe_fn fence: its
exemption rationale ("raw entry-table calls almost line for line") was
false — the file makes zero FFI calls. Its unsafe surface is C-union writes
whose soundness hangs on which codec arm is active, and its own 4:4:4 note
records the shipped bug (hevcConfig bytes stamped onto an AV1 config) that
per-operation blocks make visible. It now runs the strictest discipline in
the crate: clippy::multiple_unsafe_ops_per_block at deny, one union access
per block, each naming its codec guard.
Verified here: cargo fmt clean in all three workspaces; native clippy
-D warnings clean for everything that compiles on macOS (the three
pre-existing mac-native failures — pf-client-core wol.rs, pf-encode
dead-code/closure-call, probe mic_burst — reproduce on the clean tree).
Linux/Windows legs ride the .25/.133 gate.
425 lines
19 KiB
Rust
425 lines
19 KiB
Rust
//! GPU smoke tests — `#[ignore]`d because they need real Vulkan Video hardware.
|
|
//!
|
|
//! Run on a Vulkan-Video box with:
|
|
//!
|
|
//! ```text
|
|
//! cargo test -p pf-vkdecode -- --ignored
|
|
//! ```
|
|
//!
|
|
//! Environment expectations (the fleet's RADV boxes .21/.25, the NVIDIA .173, or
|
|
//! any machine like them):
|
|
//! - a Vulkan 1.3 loader on the library path (`libvulkan.so.1` / `vulkan-1.dll`);
|
|
//! - a physical device advertising `VK_KHR_video_queue`,
|
|
//! `VK_KHR_video_decode_queue` and the leg's codec extension
|
|
//! (`VK_KHR_video_decode_h264` / `VK_KHR_video_decode_h265` /
|
|
//! `VK_KHR_video_decode_av1`), with a queue
|
|
//! family carrying `VIDEO_DECODE_KHR` ops for that codec;
|
|
//! - `timelineSemaphore` + `synchronization2` feature support (Vulkan 1.3 core);
|
|
//! - on RADV, `RADV_PERFTEST=video_decode` in the environment — for AV1 exactly as
|
|
//! for the other two, and without it the AV1 leg reports missing silicon it has.
|
|
//!
|
|
//! One leg per codec, running the SAME body ([`smoke`]) over the vendored 25fps
|
|
//! vector of that codec — a box that decodes only some of the three runs those legs
|
|
//! and reports the rest as "no physical device with VK_KHR_video_decode_…", which is
|
|
//! a fact about the box rather than a failure (AV1 is the one most likely to say so
|
|
//! on today's fleet). Device bring-up lives in `tests/common/mod.rs`.
|
|
//!
|
|
//! What they prove: device wrap → caps query/derivation on REAL caps → session +
|
|
//! parameters creation → the decoupled picture pool → 48 AUs of the vendored
|
|
//! 25fps vector decoded through `vkCmdDecodeVideoKHR` — well past DPB-full, so
|
|
//! slot re-activation binds fresh pool images repeatedly — while the consumer
|
|
//! HOLDS FOUR delivered frames unreleased at steady state (the real client's
|
|
//! pipeline shape: bounded channels, FrameStore preroll, in-flight present).
|
|
//! Every frame's RESULT_STATUS_ONLY query must read COMPLETE before its
|
|
//! release. This is the regression test for the .25 field failure class: any
|
|
//! pool sizing that ignores the stream's DPB depth or the client's hold depth
|
|
//! starves exactly here. What they deliberately do NOT prove (that is
|
|
//! `gpu_parity`'s and WP-D on-glass's ground): pixel correctness vs the ffmpeg
|
|
//! rung, presenter interop (the `value + 1` signal-back — no presenter runs here,
|
|
//! so releases pass `false`), soak, and both vendors' DPB arrangements at once
|
|
//! (each box exercises only its own).
|
|
|
|
mod common;
|
|
|
|
use ash::vk;
|
|
use common::TestDecoder;
|
|
use pf_vkdecode::DecodeStatus;
|
|
use pf_vkdecode::DecodedVkFrame;
|
|
use pf_vkdecode::NoopQueueLock;
|
|
use pf_vkdecode::VkAv1Decoder;
|
|
use pf_vkdecode::VkH264Decoder;
|
|
use pf_vkdecode::VkH265Decoder;
|
|
|
|
/// AUs fed: far past every vector's DPB depth (`max_dpb_frames = 7` for the
|
|
/// H.264 clip, eight reference slots for AV1), so DPB slots re-activate onto fresh
|
|
/// pool images repeatedly.
|
|
const AUS: usize = 48;
|
|
/// The REAL client's consumption shape: the consumer holds four delivered frames
|
|
/// and releases only the oldest beyond that (its channels + preroll + in-flight
|
|
/// present hold ~4-7).
|
|
const CLIENT_HOLD: usize = 4;
|
|
/// 48 AUs may legitimately leave a few pictures buffered for reorder; anything
|
|
/// below this is a delivery failure, not reordering.
|
|
const MIN_DELIVERED: usize = 40;
|
|
|
|
/// The geometry one leg's vector must deliver.
|
|
struct Geometry {
|
|
/// The vector's display (conformance-window) region.
|
|
display: (u32, u32),
|
|
/// The ALLOCATED extent, when the leg knows it for a fact.
|
|
///
|
|
/// `pictureAccessGranularity` rounds the coded size up, so this is a
|
|
/// per-vector AND per-driver fact, not a property of the bitstream. The H.264
|
|
/// leg has asserted `(320, 240)` on the fleet since WP-B and keeps asserting
|
|
/// it; the H.265 leg has NO hardware evidence yet, so it asserts only the
|
|
/// invariant that always holds (allocated >= display) and PRINTS what it got
|
|
/// — which is exactly what a first fleet run needs in order to pin it later.
|
|
exact_coded: Option<(u32, u32)>,
|
|
}
|
|
|
|
/// Decode [`AUS`] access units while holding [`CLIENT_HOLD`] frames, asserting the
|
|
/// decode verdict of every frame before its release.
|
|
///
|
|
/// One body for all three codecs (over `common::TestDecoder`) so "the AV1 leg proves
|
|
/// what the H.264 leg proves" is structural rather than a claim about three copies.
|
|
fn smoke(decoder: &mut impl TestDecoder, aus: &[&[u8]], geometry: &Geometry) {
|
|
// The smoke legs exist to prove the PRODUCTION pool arrangement survives 48
|
|
// AUs at the client's hold depth. `PF_VKD_TEST_READBACK` adds TRANSFER_SRC to
|
|
// the picture pool for whoever sets it, so a shell that exported it while
|
|
// iterating on the parity legs would quietly test a pool production never
|
|
// builds — and the leg would still pass. Refuse rather than mislead.
|
|
assert!(
|
|
std::env::var_os("PF_VKD_TEST_READBACK").is_none(),
|
|
"PF_VKD_TEST_READBACK is set in the environment: it grows the picture pool \
|
|
a usage flag production never carries, so this leg would no longer be \
|
|
testing the production pool arrangement. Unset it for the smoke legs \
|
|
(the parity legs set it themselves, under the same GPU lock)."
|
|
);
|
|
// Status is read (COMPLETE required, the program's whole point) as each frame
|
|
// retires; `take_ready` is drained every AU so nothing is stranded. No
|
|
// presenter runs here, so releases report `presenter_signaled = false` (no
|
|
// `value + 1` write-back).
|
|
let mut held: std::collections::VecDeque<DecodedVkFrame> = std::collections::VecDeque::new();
|
|
let mut delivered = 0usize;
|
|
let mut geometry_checked = false;
|
|
for (index, au) in aus.iter().enumerate().take(AUS) {
|
|
let mut next = decoder.decode(au).unwrap_or_else(|e| {
|
|
panic!(
|
|
"AU {index}: decode failed: {e}\n state: {}",
|
|
decoder.debug_snapshot()
|
|
)
|
|
});
|
|
while let Some(frame) = next {
|
|
if !geometry_checked {
|
|
assert_eq!(
|
|
(frame.crop.width, frame.crop.height),
|
|
geometry.display,
|
|
"the vector's display region"
|
|
);
|
|
assert!(
|
|
frame.coded_width >= frame.crop.width
|
|
&& frame.coded_height >= frame.crop.height,
|
|
"the ALLOCATED extent ({}x{}) must cover the display region ({}x{})",
|
|
frame.coded_width,
|
|
frame.coded_height,
|
|
frame.crop.width,
|
|
frame.crop.height,
|
|
);
|
|
if let Some(exact) = geometry.exact_coded {
|
|
assert_eq!(
|
|
(frame.coded_width, frame.coded_height),
|
|
exact,
|
|
"ALLOCATED extent (this vector needs no granularity padding here)"
|
|
);
|
|
}
|
|
// A pool built for the wrong picture format decodes and then
|
|
// renders with the wrong maths (`DecodedVkFrame::format` docs);
|
|
// all three vectors are 8-bit 4:2:0, so all three must land on NV12.
|
|
assert_eq!(
|
|
frame.format,
|
|
pf_vkdecode::NV12,
|
|
"8-bit 4:2:0 vector must decode into an NV12 pool"
|
|
);
|
|
assert_ne!(frame.image, vk::Image::null());
|
|
assert_ne!(frame.semaphore, vk::Semaphore::null());
|
|
assert!(frame.value > 0);
|
|
eprintln!(
|
|
"geometry: allocated {}x{} display {}x{} format {:?} layout {:?}",
|
|
frame.coded_width,
|
|
frame.coded_height,
|
|
frame.crop.width,
|
|
frame.crop.height,
|
|
frame.format,
|
|
frame.layout,
|
|
);
|
|
geometry_checked = true;
|
|
}
|
|
held.push_back(frame);
|
|
delivered += 1;
|
|
// Steady state: keep CLIENT_HOLD frames in hand, retire beyond.
|
|
while held.len() > CLIENT_HOLD {
|
|
let oldest = held.pop_front().expect("nonempty");
|
|
assert_eq!(
|
|
decoder.wait_status(&oldest),
|
|
DecodeStatus::Ok,
|
|
"AU {index}: decode op not COMPLETE\n state: {}",
|
|
decoder.debug_snapshot()
|
|
);
|
|
decoder
|
|
.release_frame(&oldest, false)
|
|
.unwrap_or_else(|e| panic!("AU {index}: release failed: {e}"));
|
|
}
|
|
next = decoder.take_ready();
|
|
}
|
|
}
|
|
// Retire the tail the consumer still holds.
|
|
for frame in held.drain(..) {
|
|
assert_eq!(decoder.wait_status(&frame), DecodeStatus::Ok);
|
|
decoder
|
|
.release_frame(&frame, false)
|
|
.expect("tail frames release");
|
|
}
|
|
assert!(
|
|
delivered >= MIN_DELIVERED,
|
|
"expected at least {MIN_DELIVERED} delivered frames from {AUS} AUs, got {delivered}"
|
|
);
|
|
// The DPB mode the caps derivation chose, and whether this box answers per-op
|
|
// status at all — a passing run should say so too (failure paths already carry
|
|
// the snapshot).
|
|
eprintln!(
|
|
"final state: {} status_queries={}",
|
|
decoder.debug_snapshot(),
|
|
decoder.status_queries()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "needs a Vulkan Video H.264 decode device (fleet boxes; see module docs)"]
|
|
fn h264_decodes_48_aus_holding_four_frames_like_the_real_client() {
|
|
// One codec at a time on the device (see `common::gpu_lock`).
|
|
let _gpu = common::gpu_lock();
|
|
|
|
let setup = common::bring_up(&common::Request {
|
|
codec: common::H264,
|
|
// The smoke legs submit nothing outside the decoder, so a decode-only
|
|
// device is usable (and its EXCLUSIVE pool sharing is worth exercising).
|
|
graphics: common::Graphics::DecodeFamilyIsFine,
|
|
report_families: true,
|
|
});
|
|
let handles = setup.handles();
|
|
{
|
|
// SAFETY: `setup` outlives this block (destroyed below, after the decoder
|
|
// drops at the block's end), it was created with the H.264 decode
|
|
// extensions + timeline/sync2 features, and its queue fields name the
|
|
// families/queues it created.
|
|
let mut decoder = unsafe { VkH264Decoder::new(&handles, Box::new(NoopQueueLock)) }
|
|
.expect("wrap the device");
|
|
smoke(
|
|
&mut decoder,
|
|
&common::split_h264_aus(common::TEST_25FPS_H264),
|
|
&Geometry {
|
|
display: (320, 240),
|
|
exact_coded: Some((320, 240)),
|
|
},
|
|
);
|
|
}
|
|
// SAFETY: the decoder is gone (its Drop drained the queue and destroyed its
|
|
// session/pools), and nothing else references the setup's handles.
|
|
unsafe { setup.destroy() };
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "needs a Vulkan Video H.265 decode device (fleet boxes; see module docs)"]
|
|
fn h265_decodes_48_aus_holding_four_frames_like_the_real_client() {
|
|
// One codec at a time on the device (see `common::gpu_lock`).
|
|
let _gpu = common::gpu_lock();
|
|
|
|
let setup = common::bring_up(&common::Request {
|
|
codec: common::H265,
|
|
graphics: common::Graphics::DecodeFamilyIsFine,
|
|
report_families: true,
|
|
});
|
|
let handles = setup.handles();
|
|
{
|
|
// SAFETY: as the H.264 leg — `setup` outlives this block and was created
|
|
// with the H.265 decode extensions + timeline/sync2 features.
|
|
let mut decoder = unsafe { VkH265Decoder::new(&handles, Box::new(NoopQueueLock)) }
|
|
.expect("wrap the device");
|
|
// The construction-time shape gate the client's ladder relies on, on the
|
|
// vector's own facts (Main, 4:2:0, 8-bit → NV12). Called here rather than
|
|
// left to the first AU so a device that cannot host the combination says
|
|
// so as a refusal with a caps reason, not as a mid-stream decode failure —
|
|
// and so this path has hardware evidence at all.
|
|
decoder
|
|
.probe_stream_support(1, 0)
|
|
.expect("the box must host H.265 Main 8-bit 4:2:0 (the vector's shape)");
|
|
smoke(
|
|
&mut decoder,
|
|
&common::split_h265_aus(common::TEST_25FPS_H265),
|
|
&Geometry {
|
|
display: (320, 240),
|
|
// No hardware evidence for HEVC's `pictureAccessGranularity` on
|
|
// any fleet box yet; the leg prints what it allocates instead of
|
|
// asserting a number nobody has observed.
|
|
exact_coded: None,
|
|
},
|
|
);
|
|
}
|
|
// SAFETY: as the H.264 leg — the decoder is gone and nothing else references
|
|
// the setup's handles.
|
|
unsafe { setup.destroy() };
|
|
}
|
|
|
|
/// The AV1 leg — the rung's first hardware evidence of ANY kind.
|
|
///
|
|
/// The same 48 access units at the same client hold depth, but AV1 loads the pool
|
|
/// harder than either H.26x leg does and that is the point of running it: the first
|
|
/// 48 temporal units carry 53 coded frames to show 48 (the number
|
|
/// [`the_delivery_floor_is_under_what_the_planners_emit_from_the_first_48_aus`]
|
|
/// prints), each hidden frame keeps a pool image resident as a reference while
|
|
/// nothing displays it, and eight reference slots re-activate against that. A pool
|
|
/// sized as if one access unit meant one picture starves exactly here — which is this
|
|
/// leg's whole job, since `gpu_parity`'s AV1 leg would report the same starvation as a
|
|
/// decode failure with a less obvious cause.
|
|
#[test]
|
|
#[ignore = "needs a Vulkan Video AV1 decode device (fleet boxes; see module docs)"]
|
|
fn av1_decodes_48_aus_holding_four_frames_like_the_real_client() {
|
|
// One codec at a time on the device (see `common::gpu_lock`).
|
|
let _gpu = common::gpu_lock();
|
|
|
|
let setup = common::bring_up(&common::Request {
|
|
codec: common::AV1,
|
|
graphics: common::Graphics::DecodeFamilyIsFine,
|
|
report_families: true,
|
|
});
|
|
let handles = setup.handles();
|
|
{
|
|
// SAFETY: as the H.264 leg — `setup` outlives this block and was created
|
|
// with the AV1 decode extension + timeline/sync2 features.
|
|
let mut decoder = unsafe { VkAv1Decoder::new(&handles, Box::new(NoopQueueLock)) }
|
|
.expect("wrap the device");
|
|
// The construction-time shape gate on the vector's own facts (Main, 4:2:0,
|
|
// 8-bit, NO film grain → NV12). The film-grain argument is the one that has
|
|
// no H.26x counterpart: grain synthesis is part of the Vulkan decode PROFILE,
|
|
// so a box offering only the grain-enabled profile refuses HERE with a caps
|
|
// reason rather than at the first temporal unit.
|
|
decoder.probe_stream_support(1, 8, false).expect(
|
|
"the box must host AV1 Main 4:2:0 8-bit without film grain (the vector's shape)",
|
|
);
|
|
smoke(
|
|
&mut decoder,
|
|
&common::split_av1_aus(common::TEST_25FPS_AV1),
|
|
&Geometry {
|
|
display: (320, 240),
|
|
// As HEVC: no hardware evidence for AV1's `pictureAccessGranularity`
|
|
// on any fleet box yet, so the leg prints what it allocates rather
|
|
// than asserting a number nobody has observed. AV1's decode extent is
|
|
// the POST-superres width, which is another reason not to guess.
|
|
exact_coded: None,
|
|
},
|
|
);
|
|
}
|
|
// SAFETY: as the H.264 leg — the decoder is gone and nothing else references
|
|
// the setup's handles.
|
|
unsafe { setup.destroy() };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CPU coherence guards — NOT `#[ignore]`d.
|
|
//
|
|
// The legs above only run on the fleet, so [`MIN_DELIVERED`] would otherwise be a
|
|
// number copied from the H.264 leg and never checked against the H.265 or AV1
|
|
// vector's own reorder depth. It is the CPU planner that decides how many of the
|
|
// first [`AUS`] pictures can possibly be delivered — the decoder builds exactly one
|
|
// frame per `dpb.outputs` id — so the floor is checkable here, without a GPU, and
|
|
// a re-synced vector that reorders more deeply fails HERE instead of looking like
|
|
// a pool-starvation bug on hardware.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn the_delivery_floor_is_under_what_the_planners_emit_from_the_first_48_aus() {
|
|
let h264 = {
|
|
let mut planner = pf_bitstream::h264::H264Planner::new();
|
|
common::split_h264_aus(common::TEST_25FPS_H264)
|
|
.iter()
|
|
.take(AUS)
|
|
.enumerate()
|
|
.map(|(index, au)| {
|
|
planner
|
|
.plan_au(au)
|
|
.unwrap_or_else(|e| panic!("H.264 AU {index} must plan, got {e:?}"))
|
|
.dpb
|
|
.outputs
|
|
.len()
|
|
})
|
|
.sum::<usize>()
|
|
};
|
|
let h265 = {
|
|
let mut planner = pf_bitstream::h265::H265Planner::new();
|
|
common::split_h265_aus(common::TEST_25FPS_H265)
|
|
.iter()
|
|
.take(AUS)
|
|
.enumerate()
|
|
.map(|(index, au)| {
|
|
planner
|
|
.plan_au(au)
|
|
.unwrap_or_else(|e| panic!("H.265 AU {index} must plan, got {e:?}"))
|
|
.dpb
|
|
.outputs
|
|
.len()
|
|
})
|
|
.sum::<usize>()
|
|
};
|
|
// AV1 needs the extra fold: one temporal unit can plan SEVERAL frames, so the
|
|
// outputs of a unit are the outputs of all of its plans — and counting one plan
|
|
// per unit is exactly how a reader would under-count here.
|
|
let (av1, av1_frames) = {
|
|
let mut planner = pf_bitstream::av1::Av1Planner::new();
|
|
let mut outputs = 0usize;
|
|
let mut frames = 0usize;
|
|
for (index, au) in common::split_av1_aus(common::TEST_25FPS_AV1)
|
|
.iter()
|
|
.take(AUS)
|
|
.enumerate()
|
|
{
|
|
let plans = planner
|
|
.plan_au(au)
|
|
.unwrap_or_else(|e| panic!("AV1 temporal unit {index} must plan, got {e:?}"));
|
|
frames += plans.len();
|
|
outputs += plans.iter().map(|p| p.dpb.outputs.len()).sum::<usize>();
|
|
}
|
|
(outputs, frames)
|
|
};
|
|
eprintln!(
|
|
"outputs from the first {AUS} AUs: h264={h264} h265={h265} av1={av1} \
|
|
(av1 decoded {av1_frames} frames to show {av1} — the hidden ones)"
|
|
);
|
|
// No `flush` here on purpose: the smoke legs do not flush either, so the
|
|
// planner's un-flushed output count is exactly the frame budget they have.
|
|
assert!(
|
|
h264 >= MIN_DELIVERED,
|
|
"the H.264 leg asserts >= {MIN_DELIVERED} delivered but the planner only \
|
|
outputs {h264} pictures from the first {AUS} AUs"
|
|
);
|
|
assert!(
|
|
h265 >= MIN_DELIVERED,
|
|
"the H.265 leg asserts >= {MIN_DELIVERED} delivered but the planner only \
|
|
outputs {h265} pictures from the first {AUS} AUs"
|
|
);
|
|
assert!(
|
|
av1 >= MIN_DELIVERED,
|
|
"the AV1 leg asserts >= {MIN_DELIVERED} delivered but the planner only \
|
|
outputs {av1} pictures from the first {AUS} temporal units"
|
|
);
|
|
// The AV1 leg's load is not the same as the other two's, and the assertion above
|
|
// cannot see the difference: the pool must hold the hidden frames as well as the
|
|
// shown ones. If these ever became equal, the leg would have stopped exercising
|
|
// multi-frame temporal units — the one pool pressure AV1 has that H.26x has not —
|
|
// while still passing everything above.
|
|
assert!(
|
|
av1_frames > av1,
|
|
"the first {AUS} AV1 temporal units must decode MORE frames ({av1_frames}) \
|
|
than they show ({av1}); equal counts mean the hidden-frame coverage is gone"
|
|
);
|
|
}
|