fix(encode): Vulkan-HEVC full-RPS reference retention + AV1 feature gate (RFI review)
ci / web (push) Successful in 56s
ci / docs-site (push) Successful in 1m0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 14s
decky / build-publish (push) Successful in 30s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 52s
apple / swift (push) Successful in 4m47s
ci / bench (push) Successful in 6m2s
docker / deploy-docs (push) Successful in 24s
windows-host / package (push) Successful in 8m6s
arch / build-publish (push) Successful in 11m4s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m19s
android / android (push) Successful in 16m58s
deb / build-publish (push) Successful in 17m45s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m38s
ci / rust (push) Successful in 24m8s
apple / screenshots (push) Successful in 20m38s
ci / web (push) Successful in 56s
ci / docs-site (push) Successful in 1m0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 14s
decky / build-publish (push) Successful in 30s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 52s
apple / swift (push) Successful in 4m47s
ci / bench (push) Successful in 6m2s
docker / deploy-docs (push) Successful in 24s
windows-host / package (push) Successful in 8m6s
arch / build-publish (push) Successful in 11m4s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m19s
android / android (push) Successful in 16m58s
deb / build-publish (push) Successful in 17m45s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m38s
ci / rust (push) Successful in 24m8s
apple / screenshots (push) Successful in 20m38s
2026-07-12 review of the host encoders / client decoders / RFI plane. NVENC (both), AMF-LTR, the session glue, and the client RfiTracker came out clean; every fix lands in the Vulkan Video backend + dispatch: 1. HEVC: author each P-frame's short-term RPS to retain ALL resident DPB pictures (minus the setup slot), not just its one reference. HEVC 8.3.2 evicts unlisted pictures, and clients keep FEEDING the decoder while frozen — so with the old single-pic RPS, a conforming parser (FFmpeg = the Linux VAAPI/Vulkan and Windows D3D11VA clients) had already discarded the picture an RFI recovery anchor references whenever a fed post-loss frame preceded it: generate_missing_ref, and the "clean" anchor plus everything chained after it decodes as garbage. Pure builder (`build_h265_rps_s0`) + unit tests; AV1 needs nothing (slot-based retention). The smoke test now encodes a fed post-loss frame between loss@4 and anchor@6 so an ffmpeg decode of the dropped dump exercises exactly this (expect ONE POC-4 complaint, never POC 3) — revalidate on the AMD box; this NVIDIA dev box fails the backend earlier at HEVC header retrieval (pre-existing). 2. AV1: chain PhysicalDeviceVideoEncodeAV1FeaturesKHR (videoEncodeAV1 = TRUE, stype 1000513004) into device creation — spec-required for the ENCODE_AV1 codec op; RADV tolerated the omission, validation layers and stricter drivers do not. 3. RFI decline no longer self-arms force_kf — that bypassed the session glue's 750 ms IDR cooldown, turning a storm of hopeless RFI requests into one full IDR each. Decline like NVENC/AMF and let the caller's coalesced keyframe path own the fallback; add the missing first>last guard for parity. 4. open_video_backend now returns the label of the branch that ACTUALLY opened, so the mgmt API / web console reports "vulkan" instead of "vaapi" for the default-on Vulkan sessions (the old dispatch-mirror resolved_backend_label went stale when the backend gained its VAAPI fallback; deleted). Structure: the ~230-line inline HEVC coding block moves to record_coding_h265 (symmetric with record_coding_av1) and the duplicated pre-encode barriers dedupe into begin_encode_cmd. Follow-up plan (separate, punktfunk-planning): bring the post-loss freeze + RECOVERY_ANCHOR/POINT lift to the Android/Windows/Apple clients via a shared ReanchorGate (design/client-reanchor-freeze-parity.md). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -373,7 +373,7 @@ pub fn open_video(
|
|||||||
bit_depth: u8,
|
bit_depth: u8,
|
||||||
chroma: ChromaFormat,
|
chroma: ChromaFormat,
|
||||||
) -> Result<Box<dyn Encoder>> {
|
) -> Result<Box<dyn Encoder>> {
|
||||||
let inner = open_video_backend(
|
let (inner, backend) = open_video_backend(
|
||||||
codec,
|
codec,
|
||||||
format,
|
format,
|
||||||
width,
|
width,
|
||||||
@@ -385,10 +385,12 @@ pub fn open_video(
|
|||||||
chroma,
|
chroma,
|
||||||
)?;
|
)?;
|
||||||
// Record what this session encodes on (the mgmt API's "currently used GPU"): the backend label
|
// Record what this session encodes on (the mgmt API's "currently used GPU"): the backend label
|
||||||
// mirrors the dispatch `open_video_backend` just took, the GPU identity is the same selection
|
// is reported by `open_video_backend` from the branch that ACTUALLY opened — not re-derived by
|
||||||
// the capturer was created on ([`crate::gpu::selected_gpu`]). Dropping the returned encoder
|
// mirroring its dispatch, which went stale the moment a backend gained an internal fallback
|
||||||
// ends the record, so the live count is correct by construction.
|
// (the default-on Vulkan Video path falls back to VAAPI on a failed open, and a dispatch
|
||||||
let backend = resolved_backend_label(cuda);
|
// mirror would report "vaapi" for every Vulkan session or vice versa). The GPU identity is the
|
||||||
|
// same selection the capturer was created on ([`crate::gpu::selected_gpu`]). Dropping the
|
||||||
|
// returned encoder ends the record, so the live count is correct by construction.
|
||||||
let gpu = if backend == "software" {
|
let gpu = if backend == "software" {
|
||||||
crate::gpu::ActiveGpu {
|
crate::gpu::ActiveGpu {
|
||||||
id: String::new(),
|
id: String::new(),
|
||||||
@@ -418,40 +420,6 @@ pub fn open_video(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The display label of the backend [`open_video_backend`] resolves — kept in lockstep with its
|
|
||||||
/// dispatch (`windows_resolved_backend` on Windows; the `PUNKTFUNK_ENCODER`/auto match on Linux).
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn resolved_backend_label(_cuda: bool) -> &'static str {
|
|
||||||
match windows_resolved_backend() {
|
|
||||||
WindowsBackend::Nvenc => "nvenc",
|
|
||||||
WindowsBackend::Amf => "amf",
|
|
||||||
WindowsBackend::Qsv => "qsv",
|
|
||||||
WindowsBackend::Software => "software",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn resolved_backend_label(cuda: bool) -> &'static str {
|
|
||||||
match crate::config::config().encoder_pref.as_str() {
|
|
||||||
"nvenc" | "nvidia" | "cuda" => "nvenc",
|
|
||||||
"vaapi" | "amd" | "intel" => "vaapi",
|
|
||||||
"vulkan" | "vulkan-video" => "vulkan",
|
|
||||||
"software" | "sw" | "openh264" => "software",
|
|
||||||
_ => {
|
|
||||||
if cuda || !linux_auto_is_vaapi() {
|
|
||||||
"nvenc"
|
|
||||||
} else {
|
|
||||||
"vaapi"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
|
||||||
fn resolved_backend_label(_cuda: bool) -> &'static str {
|
|
||||||
"none"
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ties the [`crate::gpu`] live-session record to the encoder's lifetime; pure delegation
|
/// Ties the [`crate::gpu`] live-session record to the encoder's lifetime; pure delegation
|
||||||
/// otherwise.
|
/// otherwise.
|
||||||
struct TrackedEncoder {
|
struct TrackedEncoder {
|
||||||
@@ -489,6 +457,10 @@ impl Encoder for TrackedEncoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open the platform encoder backend. Returns the encoder together with the display label of the
|
||||||
|
/// branch that ACTUALLY opened (`nvenc`/`vaapi`/`vulkan`/`amf`/`qsv`/`software`) — the label feeds
|
||||||
|
/// the mgmt API's live-session record, and only the open site knows which internal fallback won
|
||||||
|
/// (e.g. Vulkan Video falling back to VAAPI).
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn open_video_backend(
|
fn open_video_backend(
|
||||||
codec: Codec,
|
codec: Codec,
|
||||||
@@ -500,7 +472,7 @@ fn open_video_backend(
|
|||||||
cuda: bool,
|
cuda: bool,
|
||||||
bit_depth: u8,
|
bit_depth: u8,
|
||||||
chroma: ChromaFormat,
|
chroma: ChromaFormat,
|
||||||
) -> Result<Box<dyn Encoder>> {
|
) -> Result<(Box<dyn Encoder>, &'static str)> {
|
||||||
validate_dimensions(codec, width, height)?;
|
validate_dimensions(codec, width, height)?;
|
||||||
// Refresh/fps must be positive and sane: fps feeds the encoder time_base (`Rational(1, fps)`)
|
// Refresh/fps must be positive and sane: fps feeds the encoder time_base (`Rational(1, fps)`)
|
||||||
// and the pts→ns conversion (`pts * 1e9 / fps`), so 0 builds a 1/0 rational / divides by zero.
|
// and the pts→ns conversion (`pts * 1e9 / fps`), so 0 builds a 1/0 rational / divides by zero.
|
||||||
@@ -534,7 +506,7 @@ fn open_video_backend(
|
|||||||
// RFI loss recovery the VAAPI path can't express); a failed open falls back to VAAPI so the
|
// RFI loss recovery the VAAPI path can't express); a failed open falls back to VAAPI so the
|
||||||
// stream never dies over the new path. `format`/`bit_depth`/`chroma` only matter to VAAPI —
|
// stream never dies over the new path. `format`/`bit_depth`/`chroma` only matter to VAAPI —
|
||||||
// the Vulkan backend imports the dmabuf and does its own 8-bit 4:2:0 CSC.
|
// the Vulkan backend imports the dmabuf and does its own 8-bit 4:2:0 CSC.
|
||||||
let open_amd_intel = || -> Result<Box<dyn Encoder>> {
|
let open_amd_intel = || -> Result<(Box<dyn Encoder>, &'static str)> {
|
||||||
#[cfg(feature = "vulkan-encode")]
|
#[cfg(feature = "vulkan-encode")]
|
||||||
if matches!(codec, Codec::H265 | Codec::Av1) && vulkan_encode_enabled() {
|
if matches!(codec, Codec::H265 | Codec::Av1) && vulkan_encode_enabled() {
|
||||||
match vulkan_video::VulkanVideoEncoder::open(codec, width, height, fps, bitrate_bps)
|
match vulkan_video::VulkanVideoEncoder::open(codec, width, height, fps, bitrate_bps)
|
||||||
@@ -545,7 +517,7 @@ fn open_video_backend(
|
|||||||
"Linux Vulkan Video encode (real RFI via DPB reference slots) — \
|
"Linux Vulkan Video encode (real RFI via DPB reference slots) — \
|
||||||
set PUNKTFUNK_VULKAN_ENCODE=0 for libav VAAPI"
|
set PUNKTFUNK_VULKAN_ENCODE=0 for libav VAAPI"
|
||||||
);
|
);
|
||||||
return Ok(Box::new(e) as Box<dyn Encoder>);
|
return Ok((Box::new(e) as Box<dyn Encoder>, "vulkan"));
|
||||||
}
|
}
|
||||||
Err(e) => tracing::warn!(
|
Err(e) => tracing::warn!(
|
||||||
error = %format!("{e:#}"),
|
error = %format!("{e:#}"),
|
||||||
@@ -563,10 +535,10 @@ fn open_video_backend(
|
|||||||
bit_depth,
|
bit_depth,
|
||||||
chroma,
|
chroma,
|
||||||
)
|
)
|
||||||
.map(|e| Box::new(e) as Box<dyn Encoder>)
|
.map(|e| (Box::new(e) as Box<dyn Encoder>, "vaapi"))
|
||||||
};
|
};
|
||||||
match pref {
|
let open_nvidia = || -> Result<(Box<dyn Encoder>, &'static str)> {
|
||||||
"nvenc" | "nvidia" | "cuda" => open_nvenc_probed(
|
open_nvenc_probed(
|
||||||
codec,
|
codec,
|
||||||
format,
|
format,
|
||||||
width,
|
width,
|
||||||
@@ -576,7 +548,11 @@ fn open_video_backend(
|
|||||||
cuda,
|
cuda,
|
||||||
bit_depth,
|
bit_depth,
|
||||||
chroma,
|
chroma,
|
||||||
),
|
)
|
||||||
|
.map(|e| (e, "nvenc"))
|
||||||
|
};
|
||||||
|
match pref {
|
||||||
|
"nvenc" | "nvidia" | "cuda" => open_nvidia(),
|
||||||
"vaapi" | "amd" | "intel" => open_amd_intel(),
|
"vaapi" | "amd" | "intel" => open_amd_intel(),
|
||||||
// Force the raw Vulkan Video HEVC backend (real RFI). Needs `--features vulkan-encode`.
|
// Force the raw Vulkan Video HEVC backend (real RFI). Needs `--features vulkan-encode`.
|
||||||
"vulkan" | "vulkan-video" => {
|
"vulkan" | "vulkan-video" => {
|
||||||
@@ -588,7 +564,7 @@ fn open_video_backend(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
vulkan_video::VulkanVideoEncoder::open(codec, width, height, fps, bitrate_bps)
|
vulkan_video::VulkanVideoEncoder::open(codec, width, height, fps, bitrate_bps)
|
||||||
.map(|e| Box::new(e) as Box<dyn Encoder>)
|
.map(|e| (Box::new(e) as Box<dyn Encoder>, "vulkan"))
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "vulkan-encode"))]
|
#[cfg(not(feature = "vulkan-encode"))]
|
||||||
{
|
{
|
||||||
@@ -611,24 +587,14 @@ fn open_video_backend(
|
|||||||
}
|
}
|
||||||
let _ = (cuda, bit_depth); // software path is CPU + 8-bit only
|
let _ = (cuda, bit_depth); // software path is CPU + 8-bit only
|
||||||
sw::OpenH264Encoder::open(format, width, height, fps, bitrate_bps)
|
sw::OpenH264Encoder::open(format, width, height, fps, bitrate_bps)
|
||||||
.map(|e| Box::new(e) as Box<dyn Encoder>)
|
.map(|e| (Box::new(e) as Box<dyn Encoder>, "software"))
|
||||||
}
|
}
|
||||||
"auto" | "" => {
|
"auto" | "" => {
|
||||||
// A CUDA frame can ONLY be consumed by NVENC. Otherwise the shared auto decision
|
// A CUDA frame can ONLY be consumed by NVENC. Otherwise the shared auto decision
|
||||||
// (manual web-console GPU preference, else the NVIDIA-presence probe) picks the
|
// (manual web-console GPU preference, else the NVIDIA-presence probe) picks the
|
||||||
// backend — see `linux_auto_is_vaapi`.
|
// backend — see `linux_auto_is_vaapi`.
|
||||||
if cuda || !linux_auto_is_vaapi() {
|
if cuda || !linux_auto_is_vaapi() {
|
||||||
open_nvenc_probed(
|
open_nvidia()
|
||||||
codec,
|
|
||||||
format,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
fps,
|
|
||||||
bitrate_bps,
|
|
||||||
cuda,
|
|
||||||
bit_depth,
|
|
||||||
chroma,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
open_amd_intel()
|
open_amd_intel()
|
||||||
}
|
}
|
||||||
@@ -681,7 +647,7 @@ fn open_video_backend(
|
|||||||
bit_depth,
|
bit_depth,
|
||||||
chroma,
|
chroma,
|
||||||
)
|
)
|
||||||
.map(|e| Box::new(e) as Box<dyn Encoder>)
|
.map(|e| (Box::new(e) as Box<dyn Encoder>, "nvenc"))
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "nvenc"))]
|
#[cfg(not(feature = "nvenc"))]
|
||||||
{
|
{
|
||||||
@@ -710,7 +676,7 @@ fn open_video_backend(
|
|||||||
bit_depth,
|
bit_depth,
|
||||||
chroma,
|
chroma,
|
||||||
)
|
)
|
||||||
.map(|e| Box::new(e) as Box<dyn Encoder>)
|
.map(|e| (Box::new(e) as Box<dyn Encoder>, "amf"))
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
e.context(
|
e.context(
|
||||||
"native AMF encode failed to open (update the AMD driver / amfrt64.dll \
|
"native AMF encode failed to open (update the AMD driver / amfrt64.dll \
|
||||||
@@ -734,7 +700,7 @@ fn open_video_backend(
|
|||||||
bit_depth,
|
bit_depth,
|
||||||
chroma,
|
chroma,
|
||||||
)
|
)
|
||||||
.map(|e| Box::new(e) as Box<dyn Encoder>)
|
.map(|e| (Box::new(e) as Box<dyn Encoder>, "qsv"))
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "amf-qsv"))]
|
#[cfg(not(feature = "amf-qsv"))]
|
||||||
{
|
{
|
||||||
@@ -761,7 +727,7 @@ fn open_video_backend(
|
|||||||
fps,
|
fps,
|
||||||
bitrate_bps.min(SW_BITRATE_CEIL),
|
bitrate_bps.min(SW_BITRATE_CEIL),
|
||||||
)
|
)
|
||||||
.map(|e| Box::new(e) as Box<dyn Encoder>)
|
.map(|e| (Box::new(e) as Box<dyn Encoder>, "software"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ pub const ST_CAPABILITIES: i32 = 1_000_513_000;
|
|||||||
pub const ST_SESSION_PARAMETERS_CREATE_INFO: i32 = 1_000_513_001;
|
pub const ST_SESSION_PARAMETERS_CREATE_INFO: i32 = 1_000_513_001;
|
||||||
pub const ST_PICTURE_INFO: i32 = 1_000_513_002;
|
pub const ST_PICTURE_INFO: i32 = 1_000_513_002;
|
||||||
pub const ST_DPB_SLOT_INFO: i32 = 1_000_513_003;
|
pub const ST_DPB_SLOT_INFO: i32 = 1_000_513_003;
|
||||||
|
pub const ST_PHYSICAL_DEVICE_FEATURES: i32 = 1_000_513_004;
|
||||||
pub const ST_PROFILE_INFO: i32 = 1_000_513_005;
|
pub const ST_PROFILE_INFO: i32 = 1_000_513_005;
|
||||||
pub const ST_RATE_CONTROL_INFO: i32 = 1_000_513_006;
|
pub const ST_RATE_CONTROL_INFO: i32 = 1_000_513_006;
|
||||||
pub const ST_RATE_CONTROL_LAYER_INFO: i32 = 1_000_513_007;
|
pub const ST_RATE_CONTROL_LAYER_INFO: i32 = 1_000_513_007;
|
||||||
@@ -337,6 +338,17 @@ pub struct VideoEncodeAV1ProfileInfoKHR {
|
|||||||
pub std_profile: StdVideoAV1Profile,
|
pub std_profile: StdVideoAV1Profile,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `VkPhysicalDeviceVideoEncodeAV1FeaturesKHR` — the `videoEncodeAV1` feature MUST be enabled at
|
||||||
|
/// device creation for any `VK_VIDEO_CODEC_OPERATION_ENCODE_AV1` use (a spec requirement RADV may
|
||||||
|
/// tolerate omitting but validation layers and stricter drivers do not).
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct PhysicalDeviceVideoEncodeAV1FeaturesKHR {
|
||||||
|
pub s_type: vk::StructureType,
|
||||||
|
pub p_next: *mut c_void,
|
||||||
|
pub video_encode_av1: vk::Bool32,
|
||||||
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub struct VideoEncodeAV1CapabilitiesKHR {
|
pub struct VideoEncodeAV1CapabilitiesKHR {
|
||||||
|
|||||||
@@ -58,6 +58,52 @@ fn pick_recovery_slot(slot_wire: &[i64], loss_first: i64) -> Option<usize> {
|
|||||||
best
|
best
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The S0 (past-reference) half of an HEVC short-term RPS that **retains every resident DPB
|
||||||
|
/// picture**, not just the one this frame predicts from. The RPS is the decoder's only retention
|
||||||
|
/// signal (HEVC 8.3.2: any DPB picture absent from the current RPS is marked "unused for
|
||||||
|
/// reference" and reclaimed) — an RPS naming only the active reference lets a conforming decoder
|
||||||
|
/// evict the rest, and the RFI recovery anchor then references a picture the client already
|
||||||
|
/// discarded: FFmpeg's HEVC parser (the Linux VAAPI/Vulkan and Windows D3D11VA clients) conceals
|
||||||
|
/// with a generated gray reference and every following frame chains off the corruption — exactly
|
||||||
|
/// at the moment the anchor claims the picture is clean. Listing all residents (with
|
||||||
|
/// `used_by_curr_pic` set only for the real reference) keeps the host and client DPBs in lockstep,
|
||||||
|
/// so any slot [`pick_recovery_slot`] can pick is decodable by construction.
|
||||||
|
///
|
||||||
|
/// `setup_idx` — the slot this frame reconstructs into — is excluded: its old occupant dies with
|
||||||
|
/// this frame on the host, so the decoder must drop it too (also keeping the retained count at
|
||||||
|
/// `DPB_SLOTS - 1` + the current picture = the SPS `max_dec_pic_buffering` budget).
|
||||||
|
///
|
||||||
|
/// Returns `(num_negative_pics, delta_poc_s0_minus1, used_by_curr_pic_s0_flag)`.
|
||||||
|
fn build_h265_rps_s0(
|
||||||
|
slot_poc: &[i32],
|
||||||
|
setup_idx: usize,
|
||||||
|
ref_poc: i32,
|
||||||
|
cur_poc: i32,
|
||||||
|
) -> (u8, [u16; 16], u16) {
|
||||||
|
// Residents, newest first — S0 is ordered by descending POC (ascending delta from `cur_poc`).
|
||||||
|
let mut pocs: Vec<i32> = slot_poc
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|&(s, &p)| s != setup_idx && p >= 0 && p < cur_poc)
|
||||||
|
.map(|(_, &p)| p)
|
||||||
|
.collect();
|
||||||
|
pocs.sort_unstable_by(|a, b| b.cmp(a));
|
||||||
|
pocs.truncate(16); // delta_poc_s0_minus1 capacity (STD_VIDEO_H265_MAX_DPB_SIZE)
|
||||||
|
let mut deltas = [0u16; 16];
|
||||||
|
let mut used = 0u16;
|
||||||
|
let mut prev = cur_poc;
|
||||||
|
for (i, &p) in pocs.iter().enumerate() {
|
||||||
|
// delta_poc_s0_minus1[i] codes the gap to the PREVIOUS S0 entry (the spec's cumulative
|
||||||
|
// DeltaPocS0 chain), not to the current picture.
|
||||||
|
deltas[i] = (prev - p - 1) as u16;
|
||||||
|
if p == ref_poc {
|
||||||
|
used |= 1 << i;
|
||||||
|
}
|
||||||
|
prev = p;
|
||||||
|
}
|
||||||
|
(pocs.len() as u8, deltas, used)
|
||||||
|
}
|
||||||
|
|
||||||
/// One in-flight frame's private GPU resources. The encoder keeps a small ring of these so a
|
/// One in-flight frame's private GPU resources. The encoder keeps a small ring of these so a
|
||||||
/// frame's GPU work (CSC + encode) overlaps the CPU capturing and submitting the next one:
|
/// frame's GPU work (CSC + encode) overlaps the CPU capturing and submitting the next one:
|
||||||
/// `submit()` records into a free slot and returns without blocking; `poll()` reads back the
|
/// `submit()` records into a free slot and returns without blocking; `poll()` reads back the
|
||||||
@@ -327,15 +373,24 @@ impl VulkanVideoEncoder {
|
|||||||
}
|
}
|
||||||
let mut sync2 =
|
let mut sync2 =
|
||||||
vk::PhysicalDeviceSynchronization2Features::default().synchronization2(true);
|
vk::PhysicalDeviceSynchronization2Features::default().synchronization2(true);
|
||||||
let device = instance
|
let mut device_ci = vk::DeviceCreateInfo::default()
|
||||||
.create_device(
|
|
||||||
pd,
|
|
||||||
&vk::DeviceCreateInfo::default()
|
|
||||||
.queue_create_infos(&qcis)
|
.queue_create_infos(&qcis)
|
||||||
.enabled_extension_names(&dev_exts)
|
.enabled_extension_names(&dev_exts)
|
||||||
.push_next(&mut sync2),
|
.push_next(&mut sync2);
|
||||||
None,
|
// The AV1-encode feature gate: `videoEncodeAV1` must be enabled for any ENCODE_AV1 use
|
||||||
)
|
// (spec requirement; vendored struct since ash 0.38 predates it — chained raw like the
|
||||||
|
// profile above).
|
||||||
|
let mut av1_features = av1b::PhysicalDeviceVideoEncodeAV1FeaturesKHR {
|
||||||
|
s_type: av1b::stype(av1b::ST_PHYSICAL_DEVICE_FEATURES),
|
||||||
|
p_next: std::ptr::null_mut(),
|
||||||
|
video_encode_av1: vk::TRUE,
|
||||||
|
};
|
||||||
|
if av1 {
|
||||||
|
av1_features.p_next = device_ci.p_next as *mut c_void;
|
||||||
|
device_ci.p_next = &av1_features as *const _ as *const c_void;
|
||||||
|
}
|
||||||
|
let device = instance
|
||||||
|
.create_device(pd, &device_ci, None)
|
||||||
.context("create device")?;
|
.context("create device")?;
|
||||||
let encode_queue = device.get_device_queue(encode_family, 0);
|
let encode_queue = device.get_device_queue(encode_family, 0);
|
||||||
let compute_queue = device.get_device_queue(compute_family, 0);
|
let compute_queue = device.get_device_queue(compute_family, 0);
|
||||||
@@ -829,7 +884,6 @@ impl VulkanVideoEncoder {
|
|||||||
frame: &CapturedFrame,
|
frame: &CapturedFrame,
|
||||||
wire: i64,
|
wire: i64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
use ash::vk::native as h;
|
|
||||||
let (w, h_px) = (self.width, self.height);
|
let (w, h_px) = (self.width, self.height);
|
||||||
// Copy this slot's Vulkan handles into locals (all `vk::*` handles are Copy) so the rest of
|
// Copy this slot's Vulkan handles into locals (all `vk::*` handles are Copy) so the rest of
|
||||||
// the function can still borrow `&mut self` for the import/CSC helpers without aliasing
|
// the function can still borrow `&mut self` for the import/CSC helpers without aliasing
|
||||||
@@ -875,12 +929,8 @@ impl VulkanVideoEncoder {
|
|||||||
if !is_idr && setup_idx == ref_slot {
|
if !is_idr && setup_idx == ref_slot {
|
||||||
setup_idx = (setup_idx + 1) % DPB_SLOTS as usize;
|
setup_idx = (setup_idx + 1) % DPB_SLOTS as usize;
|
||||||
}
|
}
|
||||||
let ref_poc = if is_idr { 0 } else { self.slot_poc[ref_slot] };
|
|
||||||
let ref_delta = (poc - ref_poc).max(1);
|
|
||||||
|
|
||||||
// ---- 2. RGB source -> compute_cmd: prep barriers + CSC + copy into nv12_src ----
|
// ---- 2. RGB source -> compute_cmd: prep barriers + CSC + copy into nv12_src ----
|
||||||
let cw = frame.width.min(w);
|
|
||||||
let ch = frame.height.min(h_px);
|
|
||||||
let dev = self.device.clone(); // cheap handle clone -> lets us also call &mut self helpers
|
let dev = self.device.clone(); // cheap handle clone -> lets us also call &mut self helpers
|
||||||
dev.begin_command_buffer(
|
dev.begin_command_buffer(
|
||||||
compute_cmd,
|
compute_cmd,
|
||||||
@@ -985,7 +1035,6 @@ impl VulkanVideoEncoder {
|
|||||||
}
|
}
|
||||||
_ => bail!("vulkan-encode: unsupported FramePayload (need Dmabuf or Cpu RGB)"),
|
_ => bail!("vulkan-encode: unsupported FramePayload (need Dmabuf or Cpu RGB)"),
|
||||||
};
|
};
|
||||||
let _ = (cw, ch);
|
|
||||||
self.bind_rgb(csc_set, rgb_view);
|
self.bind_rgb(csc_set, rgb_view);
|
||||||
|
|
||||||
// y/uv -> GENERAL (shader write); nv12_src -> GENERAL (transfer dst, discard prior)
|
// y/uv -> GENERAL (shader write); nv12_src -> GENERAL (transfer dst, discard prior)
|
||||||
@@ -1099,14 +1148,160 @@ impl VulkanVideoEncoder {
|
|||||||
);
|
);
|
||||||
dev.end_command_buffer(compute_cmd)?;
|
dev.end_command_buffer(compute_cmd)?;
|
||||||
|
|
||||||
// ---- 3. record encode into `cmd`: codec-specific Std authoring + begin/encode/end.
|
// ---- 3. record encode into `cmd`: codec-specific Std authoring + begin/encode/end ----
|
||||||
// AV1 is self-contained in its own method; HEVC stays inline in the `else` below. ----
|
|
||||||
if self.codec == Codec::Av1 {
|
if self.codec == Codec::Av1 {
|
||||||
self.record_coding_av1(
|
self.record_coding_av1(
|
||||||
&dev, cmd, query_pool, bs_buf, nv12_src, nv12_view, is_idr, recovery, ref_slot,
|
&dev, cmd, query_pool, bs_buf, nv12_src, nv12_view, is_idr, recovery, ref_slot,
|
||||||
setup_idx, poc,
|
setup_idx, poc,
|
||||||
)?;
|
)?;
|
||||||
} else {
|
} else {
|
||||||
|
self.record_coding_h265(
|
||||||
|
&dev, cmd, query_pool, bs_buf, nv12_src, nv12_view, is_idr, ref_slot, setup_idx,
|
||||||
|
poc,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 4. submit compute (signal csc_sem) then encode (wait csc_sem, signal fence).
|
||||||
|
// Non-blocking: `fence` is polled later so this frame's CSC+encode overlaps the next
|
||||||
|
// capture/submit. Per-slot cmd/sem/fence make ring frames independent; the DPB
|
||||||
|
// barrier above orders slot N's reconstruct-write before N+1's reference-read. ----
|
||||||
|
dev.reset_fences(&[fence])?;
|
||||||
|
let ccmds = [compute_cmd];
|
||||||
|
let sems = [csc_sem];
|
||||||
|
dev.queue_submit(
|
||||||
|
self.compute_queue,
|
||||||
|
&[vk::SubmitInfo::default()
|
||||||
|
.command_buffers(&ccmds)
|
||||||
|
.signal_semaphores(&sems)],
|
||||||
|
vk::Fence::null(),
|
||||||
|
)?;
|
||||||
|
let ecmds = [cmd];
|
||||||
|
let wait_stages = [vk::PipelineStageFlags::ALL_COMMANDS];
|
||||||
|
dev.queue_submit(
|
||||||
|
self.encode_queue,
|
||||||
|
&[vk::SubmitInfo::default()
|
||||||
|
.command_buffers(&ecmds)
|
||||||
|
.wait_semaphores(&sems)
|
||||||
|
.wait_dst_stage_mask(&wait_stages)],
|
||||||
|
fence,
|
||||||
|
)?;
|
||||||
|
// Stash the metadata `read_slot` needs once `fence` signals.
|
||||||
|
self.frames[slot].pts_ns = frame.pts_ns;
|
||||||
|
self.frames[slot].keyframe = is_idr;
|
||||||
|
self.frames[slot].recovery_anchor = recovery;
|
||||||
|
|
||||||
|
// ---- 5. advance DPB bookkeeping (in submission order, before returning) ----
|
||||||
|
if is_idr {
|
||||||
|
self.slot_wire.iter_mut().for_each(|s| *s = -1);
|
||||||
|
self.slot_poc.iter_mut().for_each(|s| *s = -1);
|
||||||
|
}
|
||||||
|
self.slot_wire[setup_idx] = wire;
|
||||||
|
self.slot_poc[setup_idx] = poc;
|
||||||
|
self.prev_slot = setup_idx;
|
||||||
|
self.poc = poc + 1;
|
||||||
|
self.enc_count += 1;
|
||||||
|
self.first_frame = false;
|
||||||
|
self.force_kf = false;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Begin `cmd` and record the pre-encode setup shared by both codecs: the query-pool reset,
|
||||||
|
/// nv12_src GENERAL → VIDEO_ENCODE_SRC (the CSC semaphore already ordered the copy before
|
||||||
|
/// this), and the DPB transition — on the first frame a whole-image UNDEFINED → DPB init;
|
||||||
|
/// afterwards the cross-command-buffer pipelining barrier that orders the previous frame's
|
||||||
|
/// reconstruct-write before this frame's reference read/write (the in-flight ring records
|
||||||
|
/// frame N+1 while N still encodes; the barrier's first scope covers all prior-submitted
|
||||||
|
/// encode work on this queue, spanning the separate command buffers).
|
||||||
|
unsafe fn begin_encode_cmd(
|
||||||
|
&self,
|
||||||
|
dev: &ash::Device,
|
||||||
|
cmd: vk::CommandBuffer,
|
||||||
|
query_pool: vk::QueryPool,
|
||||||
|
nv12_src: vk::Image,
|
||||||
|
) -> Result<()> {
|
||||||
|
dev.begin_command_buffer(
|
||||||
|
cmd,
|
||||||
|
&vk::CommandBufferBeginInfo::default()
|
||||||
|
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
|
||||||
|
)?;
|
||||||
|
dev.cmd_reset_query_pool(cmd, query_pool, 0, 1);
|
||||||
|
let dpb_barrier = if self.first_frame {
|
||||||
|
vk::ImageMemoryBarrier2::default()
|
||||||
|
.src_stage_mask(vk::PipelineStageFlags2::NONE)
|
||||||
|
.src_access_mask(vk::AccessFlags2::NONE)
|
||||||
|
.dst_stage_mask(vk::PipelineStageFlags2::VIDEO_ENCODE_KHR)
|
||||||
|
.dst_access_mask(vk::AccessFlags2::VIDEO_ENCODE_WRITE_KHR)
|
||||||
|
.old_layout(vk::ImageLayout::UNDEFINED)
|
||||||
|
.new_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR)
|
||||||
|
} else {
|
||||||
|
vk::ImageMemoryBarrier2::default()
|
||||||
|
.src_stage_mask(vk::PipelineStageFlags2::VIDEO_ENCODE_KHR)
|
||||||
|
.src_access_mask(vk::AccessFlags2::VIDEO_ENCODE_WRITE_KHR)
|
||||||
|
.dst_stage_mask(vk::PipelineStageFlags2::VIDEO_ENCODE_KHR)
|
||||||
|
.dst_access_mask(
|
||||||
|
vk::AccessFlags2::VIDEO_ENCODE_READ_KHR
|
||||||
|
| vk::AccessFlags2::VIDEO_ENCODE_WRITE_KHR,
|
||||||
|
)
|
||||||
|
.old_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR)
|
||||||
|
.new_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR)
|
||||||
|
}
|
||||||
|
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
||||||
|
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
||||||
|
.image(self.dpb_image)
|
||||||
|
.subresource_range(vk::ImageSubresourceRange {
|
||||||
|
aspect_mask: vk::ImageAspectFlags::COLOR,
|
||||||
|
base_mip_level: 0,
|
||||||
|
level_count: 1,
|
||||||
|
base_array_layer: 0,
|
||||||
|
layer_count: DPB_SLOTS,
|
||||||
|
});
|
||||||
|
let pre_enc = [
|
||||||
|
vk::ImageMemoryBarrier2::default()
|
||||||
|
.src_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS)
|
||||||
|
.src_access_mask(vk::AccessFlags2::NONE)
|
||||||
|
.dst_stage_mask(vk::PipelineStageFlags2::VIDEO_ENCODE_KHR)
|
||||||
|
.dst_access_mask(vk::AccessFlags2::VIDEO_ENCODE_READ_KHR)
|
||||||
|
.old_layout(vk::ImageLayout::GENERAL)
|
||||||
|
.new_layout(vk::ImageLayout::VIDEO_ENCODE_SRC_KHR)
|
||||||
|
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
||||||
|
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
||||||
|
.image(nv12_src)
|
||||||
|
.subresource_range(color_range(0)),
|
||||||
|
dpb_barrier,
|
||||||
|
];
|
||||||
|
dev.cmd_pipeline_barrier2(
|
||||||
|
cmd,
|
||||||
|
&vk::DependencyInfo::default().image_memory_barriers(&pre_enc),
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Author the HEVC Std structs + record begin/encode/end for one frame into `cmd` — the HEVC
|
||||||
|
/// twin of [`record_coding_av1`]. RFI lever: a recovery anchor is an ordinary P whose
|
||||||
|
/// `RefPicList0` names the known-good slot; what makes it decodable is the FULL short-term RPS
|
||||||
|
/// ([`build_h265_rps_s0`]) every P-frame carries, which keeps all resident DPB pictures alive
|
||||||
|
/// at the decoder so any slot the anchor references is still there.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
unsafe fn record_coding_h265(
|
||||||
|
&self,
|
||||||
|
dev: &ash::Device,
|
||||||
|
cmd: vk::CommandBuffer,
|
||||||
|
query_pool: vk::QueryPool,
|
||||||
|
bs_buf: vk::Buffer,
|
||||||
|
nv12_src: vk::Image,
|
||||||
|
nv12_view: vk::ImageView,
|
||||||
|
is_idr: bool,
|
||||||
|
ref_slot: usize,
|
||||||
|
setup_idx: usize,
|
||||||
|
poc: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
use ash::vk::native as h;
|
||||||
|
let ext2d = vk::Extent2D {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
};
|
||||||
|
let ref_poc = if is_idr { 0 } else { self.slot_poc[ref_slot] };
|
||||||
|
|
||||||
let mut pic_flags: h::StdVideoEncodeH265PictureInfoFlags = std::mem::zeroed();
|
let mut pic_flags: h::StdVideoEncodeH265PictureInfoFlags = std::mem::zeroed();
|
||||||
pic_flags.set_is_reference(1);
|
pic_flags.set_is_reference(1);
|
||||||
if is_idr {
|
if is_idr {
|
||||||
@@ -1121,10 +1316,15 @@ impl VulkanVideoEncoder {
|
|||||||
h::StdVideoH265PictureType_STD_VIDEO_H265_PICTURE_TYPE_P
|
h::StdVideoH265PictureType_STD_VIDEO_H265_PICTURE_TYPE_P
|
||||||
};
|
};
|
||||||
std_pic.PicOrderCntVal = poc;
|
std_pic.PicOrderCntVal = poc;
|
||||||
|
let (num_neg, deltas, used) = build_h265_rps_s0(&self.slot_poc, setup_idx, ref_poc, poc);
|
||||||
|
// A P-frame's active reference must be one of the retained pictures — `ref_slot` is always
|
||||||
|
// resident and never the setup slot (record_submit bumps a collision), so a miss here means
|
||||||
|
// the DPB bookkeeping desynced.
|
||||||
|
debug_assert!(is_idr || used != 0, "reference POC missing from the RPS");
|
||||||
let mut rps: h::StdVideoH265ShortTermRefPicSet = std::mem::zeroed();
|
let mut rps: h::StdVideoH265ShortTermRefPicSet = std::mem::zeroed();
|
||||||
rps.num_negative_pics = 1;
|
rps.num_negative_pics = num_neg;
|
||||||
rps.delta_poc_s0_minus1[0] = (ref_delta - 1) as u16;
|
rps.delta_poc_s0_minus1 = deltas;
|
||||||
rps.used_by_curr_pic_s0_flag = 1;
|
rps.used_by_curr_pic_s0_flag = used;
|
||||||
let mut ref_lists: h::StdVideoEncodeH265ReferenceListsInfo = std::mem::zeroed();
|
let mut ref_lists: h::StdVideoEncodeH265ReferenceListsInfo = std::mem::zeroed();
|
||||||
ref_lists.RefPicList0 = [0xff; 15];
|
ref_lists.RefPicList0 = [0xff; 15];
|
||||||
ref_lists.RefPicList1 = [0xff; 15];
|
ref_lists.RefPicList1 = [0xff; 15];
|
||||||
@@ -1153,10 +1353,6 @@ impl VulkanVideoEncoder {
|
|||||||
.std_picture_info(&std_pic);
|
.std_picture_info(&std_pic);
|
||||||
|
|
||||||
// setup slot (reconstruct into) + reference slot (read from)
|
// setup slot (reconstruct into) + reference slot (read from)
|
||||||
let ext2d = vk::Extent2D {
|
|
||||||
width: w,
|
|
||||||
height: h_px,
|
|
||||||
};
|
|
||||||
let setup_res = vk::VideoPictureResourceInfoKHR::default()
|
let setup_res = vk::VideoPictureResourceInfoKHR::default()
|
||||||
.coded_extent(ext2d)
|
.coded_extent(ext2d)
|
||||||
.image_view_binding(self.dpb_views[setup_idx]);
|
.image_view_binding(self.dpb_views[setup_idx]);
|
||||||
@@ -1222,77 +1418,7 @@ impl VulkanVideoEncoder {
|
|||||||
rc.p_next = &h265_rc as *const _ as *const c_void;
|
rc.p_next = &h265_rc as *const _ as *const c_void;
|
||||||
let rc_ptr = &rc as *const _ as *const c_void;
|
let rc_ptr = &rc as *const _ as *const c_void;
|
||||||
|
|
||||||
dev.begin_command_buffer(
|
self.begin_encode_cmd(dev, cmd, query_pool, nv12_src)?;
|
||||||
cmd,
|
|
||||||
&vk::CommandBufferBeginInfo::default()
|
|
||||||
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
|
|
||||||
)?;
|
|
||||||
dev.cmd_reset_query_pool(cmd, query_pool, 0, 1);
|
|
||||||
// nv12_src GENERAL -> VIDEO_ENCODE_SRC (semaphore already ordered the CSC copy before this)
|
|
||||||
let mut pre_enc = vec![vk::ImageMemoryBarrier2::default()
|
|
||||||
.src_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS)
|
|
||||||
.src_access_mask(vk::AccessFlags2::NONE)
|
|
||||||
.dst_stage_mask(vk::PipelineStageFlags2::VIDEO_ENCODE_KHR)
|
|
||||||
.dst_access_mask(vk::AccessFlags2::VIDEO_ENCODE_READ_KHR)
|
|
||||||
.old_layout(vk::ImageLayout::GENERAL)
|
|
||||||
.new_layout(vk::ImageLayout::VIDEO_ENCODE_SRC_KHR)
|
|
||||||
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
|
||||||
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
|
||||||
.image(nv12_src)
|
|
||||||
.subresource_range(color_range(0))];
|
|
||||||
if self.first_frame {
|
|
||||||
pre_enc.push(
|
|
||||||
vk::ImageMemoryBarrier2::default()
|
|
||||||
.src_stage_mask(vk::PipelineStageFlags2::NONE)
|
|
||||||
.src_access_mask(vk::AccessFlags2::NONE)
|
|
||||||
.dst_stage_mask(vk::PipelineStageFlags2::VIDEO_ENCODE_KHR)
|
|
||||||
.dst_access_mask(vk::AccessFlags2::VIDEO_ENCODE_WRITE_KHR)
|
|
||||||
.old_layout(vk::ImageLayout::UNDEFINED)
|
|
||||||
.new_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR)
|
|
||||||
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
|
||||||
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
|
||||||
.image(self.dpb_image)
|
|
||||||
.subresource_range(vk::ImageSubresourceRange {
|
|
||||||
aspect_mask: vk::ImageAspectFlags::COLOR,
|
|
||||||
base_mip_level: 0,
|
|
||||||
level_count: 1,
|
|
||||||
base_array_layer: 0,
|
|
||||||
layer_count: DPB_SLOTS,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Pipelining hazard: the previous frame's encode reconstruct-writes its DPB setup slot
|
|
||||||
// while this one may already be recording. Order that write before this frame's
|
|
||||||
// reference-read/write of the DPB. Barrier first scope covers all prior-submitted encode
|
|
||||||
// work on this queue (submission order), so it spans the two separate command buffers.
|
|
||||||
pre_enc.push(
|
|
||||||
vk::ImageMemoryBarrier2::default()
|
|
||||||
.src_stage_mask(vk::PipelineStageFlags2::VIDEO_ENCODE_KHR)
|
|
||||||
.src_access_mask(vk::AccessFlags2::VIDEO_ENCODE_WRITE_KHR)
|
|
||||||
.dst_stage_mask(vk::PipelineStageFlags2::VIDEO_ENCODE_KHR)
|
|
||||||
.dst_access_mask(
|
|
||||||
vk::AccessFlags2::VIDEO_ENCODE_READ_KHR
|
|
||||||
| vk::AccessFlags2::VIDEO_ENCODE_WRITE_KHR,
|
|
||||||
)
|
|
||||||
.old_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR)
|
|
||||||
.new_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR)
|
|
||||||
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
|
||||||
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
|
||||||
.image(self.dpb_image)
|
|
||||||
.subresource_range(vk::ImageSubresourceRange {
|
|
||||||
aspect_mask: vk::ImageAspectFlags::COLOR,
|
|
||||||
base_mip_level: 0,
|
|
||||||
level_count: 1,
|
|
||||||
base_array_layer: 0,
|
|
||||||
layer_count: DPB_SLOTS,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
dev.cmd_pipeline_barrier2(
|
|
||||||
cmd,
|
|
||||||
&vk::DependencyInfo::default().image_memory_barriers(&pre_enc),
|
|
||||||
);
|
|
||||||
|
|
||||||
let begin_slots: &[vk::VideoReferenceSlotInfoKHR] =
|
let begin_slots: &[vk::VideoReferenceSlotInfoKHR] =
|
||||||
if is_idr { &begin_i } else { &begin_p };
|
if is_idr { &begin_i } else { &begin_p };
|
||||||
let mut begin = vk::VideoBeginCodingInfoKHR::default()
|
let mut begin = vk::VideoBeginCodingInfoKHR::default()
|
||||||
@@ -1329,57 +1455,16 @@ impl VulkanVideoEncoder {
|
|||||||
dev.cmd_end_query(cmd, query_pool, 0);
|
dev.cmd_end_query(cmd, query_pool, 0);
|
||||||
(self.vq_dev.fp().cmd_end_video_coding_khr)(cmd, &vk::VideoEndCodingInfoKHR::default());
|
(self.vq_dev.fp().cmd_end_video_coding_khr)(cmd, &vk::VideoEndCodingInfoKHR::default());
|
||||||
dev.end_command_buffer(cmd)?;
|
dev.end_command_buffer(cmd)?;
|
||||||
} // end HEVC branch
|
|
||||||
|
|
||||||
// ---- 4. submit compute (signal csc_sem) then encode (wait csc_sem, signal fence).
|
|
||||||
// Non-blocking: `fence` is polled later so this frame's CSC+encode overlaps the next
|
|
||||||
// capture/submit. Per-slot cmd/sem/fence make ring frames independent; the DPB
|
|
||||||
// barrier above orders slot N's reconstruct-write before N+1's reference-read. ----
|
|
||||||
dev.reset_fences(&[fence])?;
|
|
||||||
let ccmds = [compute_cmd];
|
|
||||||
let sems = [csc_sem];
|
|
||||||
dev.queue_submit(
|
|
||||||
self.compute_queue,
|
|
||||||
&[vk::SubmitInfo::default()
|
|
||||||
.command_buffers(&ccmds)
|
|
||||||
.signal_semaphores(&sems)],
|
|
||||||
vk::Fence::null(),
|
|
||||||
)?;
|
|
||||||
let ecmds = [cmd];
|
|
||||||
let wait_stages = [vk::PipelineStageFlags::ALL_COMMANDS];
|
|
||||||
dev.queue_submit(
|
|
||||||
self.encode_queue,
|
|
||||||
&[vk::SubmitInfo::default()
|
|
||||||
.command_buffers(&ecmds)
|
|
||||||
.wait_semaphores(&sems)
|
|
||||||
.wait_dst_stage_mask(&wait_stages)],
|
|
||||||
fence,
|
|
||||||
)?;
|
|
||||||
// Stash the metadata `read_slot` needs once `fence` signals.
|
|
||||||
self.frames[slot].pts_ns = frame.pts_ns;
|
|
||||||
self.frames[slot].keyframe = is_idr;
|
|
||||||
self.frames[slot].recovery_anchor = recovery;
|
|
||||||
|
|
||||||
// ---- 5. advance DPB bookkeeping (in submission order, before returning) ----
|
|
||||||
if is_idr {
|
|
||||||
self.slot_wire.iter_mut().for_each(|s| *s = -1);
|
|
||||||
self.slot_poc.iter_mut().for_each(|s| *s = -1);
|
|
||||||
}
|
|
||||||
self.slot_wire[setup_idx] = wire;
|
|
||||||
self.slot_poc[setup_idx] = poc;
|
|
||||||
self.prev_slot = setup_idx;
|
|
||||||
self.poc = poc + 1;
|
|
||||||
self.enc_count += 1;
|
|
||||||
self.first_frame = false;
|
|
||||||
self.force_kf = false;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Author the AV1 Std structs + record begin/encode/end for one frame into `cmd`. Self-contained
|
/// Author the AV1 Std structs + record begin/encode/end for one frame into `cmd` — the AV1
|
||||||
/// (own begin/end command-buffer + pre-encode barriers) — the AV1 twin of the inline HEVC path in
|
/// twin of [`record_coding_h265`]. RFI lever: an IDR **or** a recovery frame breaks the CDF
|
||||||
/// `record_submit`. RFI lever: an IDR **or** a recovery frame breaks the CDF chain
|
/// chain (`primary_ref_frame = PRIMARY_REF_NONE` + `error_resilient_mode`) so it decodes
|
||||||
/// (`primary_ref_frame = PRIMARY_REF_NONE` + `error_resilient_mode`) so it decodes independent of
|
/// independent of the lost frames' probability context, while a normal P inherits context
|
||||||
/// the lost frames' probability context, while a normal P inherits context (name 0 → `ref_slot`).
|
/// (name 0 → `ref_slot`). Unlike HEVC, reference retention needs no per-frame syntax: AV1's 8
|
||||||
|
/// virtual reference slots persist until `refresh_frame_flags` overwrites them, mirroring the
|
||||||
|
/// host's DPB ring by construction.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
unsafe fn record_coding_av1(
|
unsafe fn record_coding_av1(
|
||||||
&self,
|
&self,
|
||||||
@@ -1570,70 +1655,8 @@ impl VulkanVideoEncoder {
|
|||||||
rc.p_next = &av1_rc as *const _ as *const c_void;
|
rc.p_next = &av1_rc as *const _ as *const c_void;
|
||||||
let rc_ptr = &rc as *const _ as *const c_void;
|
let rc_ptr = &rc as *const _ as *const c_void;
|
||||||
|
|
||||||
// ---- record cmd: begin, pre-encode barriers + query reset, begin/encode/end coding ----
|
// ---- record cmd: begin + shared pre-encode barriers, then begin/encode/end coding ----
|
||||||
dev.begin_command_buffer(
|
self.begin_encode_cmd(dev, cmd, query_pool, nv12_src)?;
|
||||||
cmd,
|
|
||||||
&vk::CommandBufferBeginInfo::default()
|
|
||||||
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
|
|
||||||
)?;
|
|
||||||
dev.cmd_reset_query_pool(cmd, query_pool, 0, 1);
|
|
||||||
let mut pre_enc = vec![vk::ImageMemoryBarrier2::default()
|
|
||||||
.src_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS)
|
|
||||||
.src_access_mask(vk::AccessFlags2::NONE)
|
|
||||||
.dst_stage_mask(vk::PipelineStageFlags2::VIDEO_ENCODE_KHR)
|
|
||||||
.dst_access_mask(vk::AccessFlags2::VIDEO_ENCODE_READ_KHR)
|
|
||||||
.old_layout(vk::ImageLayout::GENERAL)
|
|
||||||
.new_layout(vk::ImageLayout::VIDEO_ENCODE_SRC_KHR)
|
|
||||||
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
|
||||||
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
|
||||||
.image(nv12_src)
|
|
||||||
.subresource_range(color_range(0))];
|
|
||||||
let dpb_range = vk::ImageSubresourceRange {
|
|
||||||
aspect_mask: vk::ImageAspectFlags::COLOR,
|
|
||||||
base_mip_level: 0,
|
|
||||||
level_count: 1,
|
|
||||||
base_array_layer: 0,
|
|
||||||
layer_count: DPB_SLOTS,
|
|
||||||
};
|
|
||||||
if self.first_frame {
|
|
||||||
pre_enc.push(
|
|
||||||
vk::ImageMemoryBarrier2::default()
|
|
||||||
.src_stage_mask(vk::PipelineStageFlags2::NONE)
|
|
||||||
.src_access_mask(vk::AccessFlags2::NONE)
|
|
||||||
.dst_stage_mask(vk::PipelineStageFlags2::VIDEO_ENCODE_KHR)
|
|
||||||
.dst_access_mask(vk::AccessFlags2::VIDEO_ENCODE_WRITE_KHR)
|
|
||||||
.old_layout(vk::ImageLayout::UNDEFINED)
|
|
||||||
.new_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR)
|
|
||||||
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
|
||||||
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
|
||||||
.image(self.dpb_image)
|
|
||||||
.subresource_range(dpb_range),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Same pipelining DPB self-barrier as the HEVC path: order the previous frame's
|
|
||||||
// reconstruct-write before this frame's reference read/write across the two command buffers.
|
|
||||||
pre_enc.push(
|
|
||||||
vk::ImageMemoryBarrier2::default()
|
|
||||||
.src_stage_mask(vk::PipelineStageFlags2::VIDEO_ENCODE_KHR)
|
|
||||||
.src_access_mask(vk::AccessFlags2::VIDEO_ENCODE_WRITE_KHR)
|
|
||||||
.dst_stage_mask(vk::PipelineStageFlags2::VIDEO_ENCODE_KHR)
|
|
||||||
.dst_access_mask(
|
|
||||||
vk::AccessFlags2::VIDEO_ENCODE_READ_KHR
|
|
||||||
| vk::AccessFlags2::VIDEO_ENCODE_WRITE_KHR,
|
|
||||||
)
|
|
||||||
.old_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR)
|
|
||||||
.new_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR)
|
|
||||||
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
|
||||||
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
|
|
||||||
.image(self.dpb_image)
|
|
||||||
.subresource_range(dpb_range),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
dev.cmd_pipeline_barrier2(
|
|
||||||
cmd,
|
|
||||||
&vk::DependencyInfo::default().image_memory_barriers(&pre_enc),
|
|
||||||
);
|
|
||||||
|
|
||||||
let begin_slots: &[vk::VideoReferenceSlotInfoKHR] =
|
let begin_slots: &[vk::VideoReferenceSlotInfoKHR] =
|
||||||
if is_idr { &begin_i } else { &begin_p };
|
if is_idr { &begin_i } else { &begin_p };
|
||||||
let mut begin = vk::VideoBeginCodingInfoKHR::default()
|
let mut begin = vk::VideoBeginCodingInfoKHR::default()
|
||||||
@@ -1748,6 +1771,10 @@ impl Encoder for VulkanVideoEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn invalidate_ref_frames(&mut self, first_frame: i64, last_frame: i64) -> bool {
|
fn invalidate_ref_frames(&mut self, first_frame: i64, last_frame: i64) -> bool {
|
||||||
|
// Nonsense range → decline (same contract as the NVENC/AMF backends).
|
||||||
|
if first_frame < 0 || first_frame > last_frame {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Can we anchor a clean P-frame to a resident slot strictly older than the loss?
|
// Can we anchor a clean P-frame to a resident slot strictly older than the loss?
|
||||||
match pick_recovery_slot(&self.slot_wire, first_frame) {
|
match pick_recovery_slot(&self.slot_wire, first_frame) {
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
@@ -1755,8 +1782,15 @@ impl Encoder for VulkanVideoEncoder {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.force_kf = true;
|
// Decline WITHOUT self-arming an IDR: the caller owns the fallback, and its
|
||||||
tracing::debug!(first_frame, last_frame, "vulkan-encode RFI declined: no resident reference older than the loss — caller will keyframe");
|
// keyframe path is cooldown-coalesced — arming `force_kf` here would bypass that
|
||||||
|
// and turn a storm of hopeless RFI requests into one full IDR per request.
|
||||||
|
tracing::debug!(
|
||||||
|
first_frame,
|
||||||
|
last_frame,
|
||||||
|
"vulkan-encode RFI declined: no resident reference older than the loss — \
|
||||||
|
caller falls back to its (coalesced) keyframe path"
|
||||||
|
);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2537,10 +2571,58 @@ unsafe fn build_parameters_av1(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::VulkanVideoEncoder;
|
use super::{build_h265_rps_s0, pick_recovery_slot, VulkanVideoEncoder};
|
||||||
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
||||||
use crate::encode::{Codec, Encoder};
|
use crate::encode::{Codec, Encoder};
|
||||||
|
|
||||||
|
/// The RFI anchor picker: newest resident wire strictly older than the loss; empty/newer
|
||||||
|
/// slots never qualify.
|
||||||
|
#[test]
|
||||||
|
fn recovery_slot_picks_newest_pre_loss() {
|
||||||
|
// slots hold wires 5..12 (ring position arbitrary); loss starts at 9 → anchor = wire 8.
|
||||||
|
let wires = [8i64, 9, 10, 11, 12, 5, 6, 7];
|
||||||
|
assert_eq!(pick_recovery_slot(&wires, 9), Some(0));
|
||||||
|
// loss older than everything resident → no anchor (caller keyframes).
|
||||||
|
assert_eq!(pick_recovery_slot(&wires, 5), None);
|
||||||
|
// empty slots (-1) are skipped.
|
||||||
|
assert_eq!(pick_recovery_slot(&[-1, 3, -1, 4], 5), Some(3));
|
||||||
|
assert_eq!(pick_recovery_slot(&[-1; 8], 5), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The full-retention RPS: every resident picture is listed (so the decoder keeps it), the
|
||||||
|
/// setup slot's dying occupant is not, and `used_by_curr_pic` marks exactly the real reference.
|
||||||
|
#[test]
|
||||||
|
fn h265_rps_retains_all_residents() {
|
||||||
|
// Steady state: slots hold POCs 8..15, current POC 16, reconstructing over the slot that
|
||||||
|
// holds POC 8 (the oldest), referencing POC 15 (the newest).
|
||||||
|
let slot_poc = [8i32, 9, 10, 11, 12, 13, 14, 15];
|
||||||
|
let (n, deltas, used) = build_h265_rps_s0(&slot_poc, 0, 15, 16);
|
||||||
|
assert_eq!(n, 7, "all residents except the dying setup occupant");
|
||||||
|
// S0 is newest-first with cumulative deltas: POCs 15,14,...,9 → every step is 1.
|
||||||
|
assert_eq!(&deltas[..7], &[0u16; 7], "delta_minus1 chain of 1-steps");
|
||||||
|
assert_eq!(used, 1 << 0, "only the newest (POC 15) is actively used");
|
||||||
|
|
||||||
|
// Recovery shape: reference an OLDER picture (POC 12) while newer residents stay listed.
|
||||||
|
let (n, deltas, used) = build_h265_rps_s0(&slot_poc, 0, 12, 16);
|
||||||
|
assert_eq!(n, 7);
|
||||||
|
assert_eq!(used, 1 << 3, "POC 12 is 4th-newest → S0 index 3");
|
||||||
|
assert_eq!(&deltas[..7], &[0u16; 7]);
|
||||||
|
|
||||||
|
// Sparse DPB right after an IDR: only POCs 0..2 resident, gaps encoded in the deltas.
|
||||||
|
let slot_poc = [0i32, 1, 2, -1, -1, -1, -1, -1];
|
||||||
|
let (n, deltas, used) = build_h265_rps_s0(&slot_poc, 3, 2, 3);
|
||||||
|
assert_eq!(n, 3);
|
||||||
|
assert_eq!(&deltas[..3], &[0, 0, 0]);
|
||||||
|
assert_eq!(used, 1 << 0);
|
||||||
|
|
||||||
|
// Non-adjacent POCs: current 10, residents {9, 6, 2} → deltas-minus1 {0, 2, 3}.
|
||||||
|
let slot_poc = [2i32, -1, 6, -1, 9, -1, -1, -1];
|
||||||
|
let (n, deltas, used) = build_h265_rps_s0(&slot_poc, 7, 6, 10);
|
||||||
|
assert_eq!(n, 3);
|
||||||
|
assert_eq!(&deltas[..3], &[0, 2, 3]);
|
||||||
|
assert_eq!(used, 1 << 1, "POC 6 is the 2nd-newest → S0 index 1");
|
||||||
|
}
|
||||||
|
|
||||||
fn cpu_frame(w: u32, h: u32, pts_ns: u64, fill: [u8; 4]) -> CapturedFrame {
|
fn cpu_frame(w: u32, h: u32, pts_ns: u64, fill: [u8; 4]) -> CapturedFrame {
|
||||||
let mut buf = vec![0u8; (w * h * 4) as usize];
|
let mut buf = vec![0u8; (w * h * 4) as usize];
|
||||||
for px in buf.chunks_exact_mut(4) {
|
for px in buf.chunks_exact_mut(4) {
|
||||||
@@ -2555,10 +2637,21 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Index of the wire frame the smoke run "loses" and drops from the client-view dump.
|
||||||
|
const SMOKE_LOST: usize = 4;
|
||||||
|
/// Index of the recovery-anchor frame — the RFI fires just before this submission, and one
|
||||||
|
/// normal P (frame 5, referencing the lost frame 4) is encoded IN BETWEEN, mirroring a real
|
||||||
|
/// session where the loss report round-trips while the encoder keeps producing. That fed
|
||||||
|
/// post-loss frame is what makes the dump exercise reference RETENTION: a conforming decoder
|
||||||
|
/// processes its RPS before the anchor arrives, so the anchor's reference (frame 3) survives
|
||||||
|
/// only because every P-frame's RPS lists all resident DPB pictures ([`build_h265_rps_s0`]).
|
||||||
|
const SMOKE_ANCHOR: usize = 6;
|
||||||
|
|
||||||
/// Full `open` → IDR → P-frames → RFI-recovery path through the real [`VulkanVideoEncoder`],
|
/// Full `open` → IDR → P-frames → RFI-recovery path through the real [`VulkanVideoEncoder`],
|
||||||
/// codec-parameterized. Exercises the CPU→NV12 compute CSC, the NV12 plane copy, the DPB ring and
|
/// codec-parameterized. Exercises the CPU→NV12 compute CSC, the NV12 plane copy, the DPB ring and
|
||||||
/// the reference-slot RFI end-to-end; returns the AUs. Loss of wire frame 3 is simulated so frame
|
/// the reference-slot RFI end-to-end; returns the AUs. Wire frame [`SMOKE_LOST`] is "lost", one
|
||||||
/// 4 becomes a clean recovery anchor referencing frame 2 (no IDR).
|
/// normal P referencing it is still encoded (the in-flight window), then frame [`SMOKE_ANCHOR`]
|
||||||
|
/// is the clean recovery anchor referencing pre-loss frame 3 (no IDR).
|
||||||
fn run_smoke(codec: Codec) -> Vec<crate::encode::EncodedFrame> {
|
fn run_smoke(codec: Codec) -> Vec<crate::encode::EncodedFrame> {
|
||||||
let env_dim = |k: &str, d: u32| {
|
let env_dim = |k: &str, d: u32| {
|
||||||
std::env::var(k)
|
std::env::var(k)
|
||||||
@@ -2577,13 +2670,16 @@ mod tests {
|
|||||||
[200, 200, 40, 255],
|
[200, 200, 40, 255],
|
||||||
[40, 200, 200, 255],
|
[40, 200, 200, 255],
|
||||||
[200, 40, 200, 255],
|
[200, 40, 200, 255],
|
||||||
|
[120, 200, 80, 255],
|
||||||
|
[80, 120, 200, 255],
|
||||||
];
|
];
|
||||||
let mut aus: Vec<crate::encode::EncodedFrame> = Vec::new();
|
let mut aus: Vec<crate::encode::EncodedFrame> = Vec::new();
|
||||||
for (i, c) in colors.iter().enumerate() {
|
for (i, c) in colors.iter().enumerate() {
|
||||||
if i == 4 {
|
if i == SMOKE_ANCHOR {
|
||||||
// simulate loss of wire frame 3 → expect a clean recovery anchor referencing frame 2
|
// The client reports wire frame SMOKE_LOST lost → the next frame must re-anchor
|
||||||
|
// on a resident pre-loss reference (newest older than the loss = frame 3).
|
||||||
assert!(
|
assert!(
|
||||||
enc.invalidate_ref_frames(3, 3),
|
enc.invalidate_ref_frames(SMOKE_LOST as i64, SMOKE_LOST as i64),
|
||||||
"RFI should find an older-than-loss slot"
|
"RFI should find an older-than-loss slot"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2609,21 +2705,28 @@ mod tests {
|
|||||||
if i == 0 {
|
if i == 0 {
|
||||||
assert!(au.keyframe, "frame 0 must be IDR");
|
assert!(au.keyframe, "frame 0 must be IDR");
|
||||||
}
|
}
|
||||||
if i == 4 {
|
if i == SMOKE_ANCHOR {
|
||||||
assert!(
|
assert!(
|
||||||
au.recovery_anchor && !au.keyframe,
|
au.recovery_anchor && !au.keyframe,
|
||||||
"frame 4 must be a clean recovery P-frame, not IDR"
|
"frame {SMOKE_ANCHOR} must be a clean recovery P-frame, not IDR"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert_eq!(keyframes, 1, "exactly one IDR (frame 0)");
|
assert_eq!(keyframes, 1, "exactly one IDR (frame 0)");
|
||||||
assert_eq!(anchors, 1, "exactly one recovery anchor (frame 4)");
|
assert_eq!(
|
||||||
|
anchors, 1,
|
||||||
|
"exactly one recovery anchor (frame {SMOKE_ANCHOR})"
|
||||||
|
);
|
||||||
aus
|
aus
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dump the full stream + a "frame-3-lost" stream to `$HOME/vkenc-host-smoke*.{ext}` for an
|
/// Dump the full stream + a client-view stream with AU [`SMOKE_LOST`] removed to
|
||||||
/// out-of-band `ffmpeg` decode check (both must decode 0-error; the dropped one proves the
|
/// `$HOME/vkenc-host-smoke*.{ext}` for an out-of-band `ffmpeg` decode check. The full stream
|
||||||
/// recovery anchor healed real loss without an IDR).
|
/// must decode 0-error. The dropped one mirrors what a real client feeds its decoder: expect
|
||||||
|
/// exactly ONE missing-reference complaint (frame 5 referencing the lost frame 4 — the
|
||||||
|
/// concealment the client's freeze hides) and NONE at the anchor — a complaint about the
|
||||||
|
/// anchor's reference (frame 3 / POC 3) means reference retention regressed and the "clean"
|
||||||
|
/// re-anchor ships corruption.
|
||||||
fn dump_smoke(aus: &[crate::encode::EncodedFrame], ext: &str) {
|
fn dump_smoke(aus: &[crate::encode::EncodedFrame], ext: &str) {
|
||||||
let Ok(home) = std::env::var("HOME") else {
|
let Ok(home) = std::env::var("HOME") else {
|
||||||
return;
|
return;
|
||||||
@@ -2639,12 +2742,15 @@ mod tests {
|
|||||||
let dropped: Vec<u8> = aus
|
let dropped: Vec<u8> = aus
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(i, _)| *i != 3)
|
.filter(|(i, _)| *i != SMOKE_LOST)
|
||||||
.flat_map(|(_, a)| a.data.iter().copied())
|
.flat_map(|(_, a)| a.data.iter().copied())
|
||||||
.collect();
|
.collect();
|
||||||
let p2 = format!("{home}/vkenc-host-smoke-dropped.{ext}");
|
let p2 = format!("{home}/vkenc-host-smoke-dropped.{ext}");
|
||||||
let _ = std::fs::write(&p2, &dropped);
|
let _ = std::fs::write(&p2, &dropped);
|
||||||
eprintln!("run_smoke: wrote {p2} (frame 3 dropped; recovery@4 anchors to frame 2)");
|
eprintln!(
|
||||||
|
"run_smoke: wrote {p2} (frame {SMOKE_LOST} dropped; frame 5 conceals, \
|
||||||
|
recovery@{SMOKE_ANCHOR} anchors to frame 3 and must decode clean)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// HEVC smoke. `#[ignore]`d so it only runs where a real `VK_KHR_video_encode_h265` driver exists
|
/// HEVC smoke. `#[ignore]`d so it only runs where a real `VK_KHR_video_encode_h265` driver exists
|
||||||
|
|||||||
Reference in New Issue
Block a user