feat(encode): AV1 on the Linux Vulkan Video encoder (real RFI)
ci / web (push) Successful in 1m12s
ci / docs-site (push) Successful in 1m22s
decky / build-publish (push) Successful in 19s
ci / bench (push) Successful in 5m36s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
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 8s
arch / build-publish (push) Successful in 12m32s
docker / deploy-docs (push) Successful in 21s
android / android (push) Successful in 13m27s
ci / rust (push) Successful in 17m43s
deb / build-publish (push) Successful in 13m40s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m19s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m36s
windows-host / package (push) Successful in 14m43s
apple / swift (push) Successful in 4m42s
apple / screenshots (push) Successful in 19m20s
ci / web (push) Successful in 1m12s
ci / docs-site (push) Successful in 1m22s
decky / build-publish (push) Successful in 19s
ci / bench (push) Successful in 5m36s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
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 8s
arch / build-publish (push) Successful in 12m32s
docker / deploy-docs (push) Successful in 21s
android / android (push) Successful in 13m27s
ci / rust (push) Successful in 17m43s
deb / build-publish (push) Successful in 13m40s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m19s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m36s
windows-host / package (push) Successful in 14m43s
apple / swift (push) Successful in 4m42s
apple / screenshots (push) Successful in 19m20s
Extend the raw Vulkan Video backend to AV1 (`VK_KHR_video_encode_av1`) alongside HEVC, so AMD/Intel Linux hosts get the same clean-P-frame loss recovery for AV1 that HEVC already has — no full IDR on packet loss. ash 0.38.0+1.3.281 predates the AV1-encode extension (finalized in Vulkan 1.3.290) and bumping ash breaks the SDL/Vulkan client (it drops the lifetime on AllocationCallbacks, which sdl3-sys still generates). So the AV1-encode structs/flags/enums are vendored host-only in `encode/linux/vk_av1_encode.rs`, copied verbatim from ash-master's generated code and chained into ash's generic video-encode calls via raw p_next — the common StdVideoAV1* types (from AV1 decode) are reused from ash 1.3.281. `vulkan_video.rs` gains a parallel AV1 path: AV1 Main profile/caps/session (+ max-level session-create), a bit-packed sequence-header OBU + per-frame temporal-delimiter framing (Vulkan AV1 encode, unlike H26x, emits only the frame OBU), and per-frame StdVideoEncodeAV1PictureInfo with the RFI reference model — a normal P inherits CDF context from its reference for compression, while an IDR or recovery anchor sets primary_ref_frame=NONE + error_resilient_mode so it decodes independent of the (possibly lost) frames since its reference. HEVC recording is unchanged; the shared CSC / ring / DPB-barrier pipeline is reused as-is. Codec routing in `open_video_backend` extends the HEVC arm to HEVC|AV1. The seq header enables only order-hint (+128px superblocks per caps), matching FFmpeg's proven Vulkan AV1 config — enabling CDEF/restoration made VCN emit frame-header sections our seq header didn't match, desyncing every inter frame. Headless-validated on real RADV (780M): open + 6-frame encode (I P P P P P) decodes 0-error on both dav1d and ffmpeg/cbs; the RFI recovery anchor at frame 4 is a clean P (not IDR), and dropping the "lost" frame 3 still decodes clean (re-anchored to frame 2). HEVC smoke unchanged (no regression). `cargo clippy --features vulkan-encode -- -D warnings` and the no-feature build both green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -536,12 +536,13 @@ fn open_video_backend(
|
||||
// the Vulkan backend imports the dmabuf and does its own 8-bit 4:2:0 CSC.
|
||||
let open_amd_intel = || -> Result<Box<dyn Encoder>> {
|
||||
#[cfg(feature = "vulkan-encode")]
|
||||
if codec == Codec::H265 && vulkan_encode_enabled() {
|
||||
if matches!(codec, Codec::H265 | Codec::Av1) && vulkan_encode_enabled() {
|
||||
match vulkan_video::VulkanVideoEncoder::open(codec, width, height, fps, bitrate_bps)
|
||||
{
|
||||
Ok(e) => {
|
||||
tracing::info!(
|
||||
"Linux Vulkan Video HEVC encode (real RFI via DPB reference slots) — \
|
||||
codec = ?codec,
|
||||
"Linux Vulkan Video encode (real RFI via DPB reference slots) — \
|
||||
set PUNKTFUNK_VULKAN_ENCODE=0 for libav VAAPI"
|
||||
);
|
||||
return Ok(Box::new(e) as Box<dyn Encoder>);
|
||||
@@ -581,9 +582,9 @@ fn open_video_backend(
|
||||
"vulkan" | "vulkan-video" => {
|
||||
#[cfg(feature = "vulkan-encode")]
|
||||
{
|
||||
if codec != Codec::H265 {
|
||||
if !matches!(codec, Codec::H265 | Codec::Av1) {
|
||||
anyhow::bail!(
|
||||
"the Vulkan Video encoder is HEVC-only; the session negotiated {codec:?}"
|
||||
"the Vulkan Video encoder supports HEVC + AV1; the session negotiated {codec:?}"
|
||||
);
|
||||
}
|
||||
vulkan_video::VulkanVideoEncoder::open(codec, width, height, fps, bitrate_bps)
|
||||
@@ -1251,6 +1252,13 @@ mod vaapi;
|
||||
#[cfg(all(target_os = "linux", feature = "vulkan-encode"))]
|
||||
#[path = "encode/linux/vulkan_video.rs"]
|
||||
mod vulkan_video;
|
||||
// Vendored `VK_KHR_video_encode_av1` bindings (host-only) — the AV1 encode structs our pinned
|
||||
// `ash 0.38.0+1.3.281` predates (finalized Vulkan 1.3.290). Copied verbatim from ash-master's
|
||||
// generated code rather than bumping `ash` (which breaks the SDL/Vulkan client). Consumed by
|
||||
// `vulkan_video.rs` via `super::vk_av1_encode`.
|
||||
#[cfg(all(target_os = "linux", feature = "vulkan-encode"))]
|
||||
#[path = "encode/linux/vk_av1_encode.rs"]
|
||||
mod vk_av1_encode;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -0,0 +1,488 @@
|
||||
//! Vendored `VK_KHR_video_encode_av1` bindings — the AV1-encode structs, `StdVideoEncodeAV1*`
|
||||
//! types and struct-type constants that our pinned `ash 0.38.0+1.3.281` does not ship (the
|
||||
//! extension was finalized in Vulkan 1.3.290). Bumping `ash` to git-master (`+1.4.352`, which has
|
||||
//! them) breaks the *client*: it drops the lifetime on `vk::AllocationCallbacks`, and `sdl3-sys`'s
|
||||
//! `ash` feature still generates `AllocationCallbacks<'static>`, so the presenter's SDL/Vulkan
|
||||
//! surface path won't compile. Rather than churn the client for a host-only need, we vendor just
|
||||
//! the encode-side definitions here, **copied verbatim from ash-master's generated code** (so the
|
||||
//! layouts are correct-by-construction) and chain them into ash's generic video-encode-queue calls
|
||||
//! via raw `p_next`, exactly as the HEVC path already chains its rate-control struct.
|
||||
//!
|
||||
//! Everything *common* to AV1 (sequence header, tile/quant/loop-filter/CDEF/… sub-structs, the
|
||||
//! `StdVideoAV1*` enums) is already present in 1.3.281's `ash::vk::native` — AV1 **decode** brought
|
||||
//! it in — so we reuse those and vendor only the encode-specific pieces. Delete this module and
|
||||
//! switch to `ash::vk::*` once `ash` publishes a 1.4.x release and `sdl3-sys` regenerates.
|
||||
#![allow(non_snake_case, non_camel_case_types, dead_code)]
|
||||
|
||||
use ash::vk;
|
||||
use ash::vk::native::{
|
||||
StdVideoAV1FrameType, StdVideoAV1InterpolationFilter, StdVideoAV1Level, StdVideoAV1Profile,
|
||||
StdVideoAV1SequenceHeader, StdVideoAV1TxMode,
|
||||
};
|
||||
use std::ffi::{c_void, CStr};
|
||||
|
||||
/// `VK_KHR_video_encode_av1` extension name — ash 0.38's `ash::khr::video_encode_av1` doesn't exist,
|
||||
/// so we pass this raw to `enabled_extension_names`.
|
||||
pub const EXTENSION_NAME: &CStr = c"VK_KHR_video_encode_av1";
|
||||
|
||||
// ---------- struct-type (VkStructureType) values — construct via `vk::StructureType::from_raw` ----------
|
||||
pub const ST_CAPABILITIES: i32 = 1_000_513_000;
|
||||
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_DPB_SLOT_INFO: i32 = 1_000_513_003;
|
||||
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_LAYER_INFO: i32 = 1_000_513_007;
|
||||
pub const ST_SESSION_CREATE_INFO: i32 = 1_000_513_009;
|
||||
pub const ST_GOP_REMAINING_FRAME_INFO: i32 = 1_000_513_010;
|
||||
|
||||
/// `VK_VIDEO_CODEC_OPERATION_ENCODE_AV1_BIT_KHR` (bit 18).
|
||||
pub const VIDEO_CODEC_OPERATION_ENCODE_AV1: u32 = 0x0004_0000;
|
||||
/// `VK_MAX_VIDEO_AV1_REFERENCES_PER_FRAME_KHR` — LAST..ALTREF (the 7 inter reference names).
|
||||
pub const MAX_VIDEO_AV1_REFERENCES_PER_FRAME: usize = 7;
|
||||
/// `STD_VIDEO_AV1_PRIMARY_REF_NONE` — a frame that inherits no CDF/context from any reference
|
||||
/// (the recovery-anchor lever: a clean P-frame independent of prior probability state).
|
||||
pub const PRIMARY_REF_NONE: u8 = 7;
|
||||
/// `VK_VIDEO_ENCODE_AV1_SUPERBLOCK_SIZE_128_BIT_KHR` (bit 1 of the superblock-size flags).
|
||||
pub const SUPERBLOCK_SIZE_128: u32 = 0x2;
|
||||
|
||||
// `VkVideoEncodeAV1PredictionModeKHR`
|
||||
pub const PREDICTION_MODE_INTRA_ONLY: i32 = 0;
|
||||
pub const PREDICTION_MODE_SINGLE_REFERENCE: i32 = 1;
|
||||
// `VkVideoEncodeAV1RateControlGroupKHR`
|
||||
pub const RC_GROUP_INTRA: i32 = 0;
|
||||
pub const RC_GROUP_PREDICTIVE: i32 = 1;
|
||||
pub const RC_GROUP_BIPREDICTIVE: i32 = 2;
|
||||
|
||||
// AV1 reference names (index into `reference_name_slot_indices`, which is 0-based over LAST..ALTREF).
|
||||
pub const REFERENCE_NAME_LAST_FRAME_IDX: usize = 0; // STD_VIDEO_AV1_REFERENCE_NAME_LAST_FRAME - 1
|
||||
|
||||
// ---------- bindgen bitfield helper (copied verbatim from ash-master native.rs) ----------
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Default, Copy, Clone)]
|
||||
pub struct __BindgenBitfieldUnit<Storage> {
|
||||
storage: Storage,
|
||||
}
|
||||
impl<Storage> __BindgenBitfieldUnit<Storage> {
|
||||
#[inline]
|
||||
pub const fn new(storage: Storage) -> Self {
|
||||
Self { storage }
|
||||
}
|
||||
}
|
||||
impl<Storage> __BindgenBitfieldUnit<Storage>
|
||||
where
|
||||
Storage: AsRef<[u8]> + AsMut<[u8]>,
|
||||
{
|
||||
#[inline]
|
||||
pub fn get_bit(&self, index: usize) -> bool {
|
||||
let byte_index = index / 8;
|
||||
let byte = self.storage.as_ref()[byte_index];
|
||||
let bit_index = if cfg!(target_endian = "big") {
|
||||
7 - (index % 8)
|
||||
} else {
|
||||
index % 8
|
||||
};
|
||||
byte & (1 << bit_index) == (1 << bit_index)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_bit(&mut self, index: usize, val: bool) {
|
||||
let byte_index = index / 8;
|
||||
let byte = &mut self.storage.as_mut()[byte_index];
|
||||
let bit_index = if cfg!(target_endian = "big") {
|
||||
7 - (index % 8)
|
||||
} else {
|
||||
index % 8
|
||||
};
|
||||
let mask = 1 << bit_index;
|
||||
if val {
|
||||
*byte |= mask;
|
||||
} else {
|
||||
*byte &= !mask;
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn get(&self, bit_offset: usize, bit_width: u8) -> u64 {
|
||||
let mut val = 0;
|
||||
for i in 0..(bit_width as usize) {
|
||||
if self.get_bit(i + bit_offset) {
|
||||
let index = if cfg!(target_endian = "big") {
|
||||
bit_width as usize - 1 - i
|
||||
} else {
|
||||
i
|
||||
};
|
||||
val |= 1 << index;
|
||||
}
|
||||
}
|
||||
val
|
||||
}
|
||||
#[inline]
|
||||
pub fn set(&mut self, bit_offset: usize, bit_width: u8, val: u64) {
|
||||
for i in 0..(bit_width as usize) {
|
||||
let mask = 1 << i;
|
||||
let val_bit_is_set = val & mask == mask;
|
||||
let index = if cfg!(target_endian = "big") {
|
||||
bit_width as usize - 1 - i
|
||||
} else {
|
||||
i
|
||||
};
|
||||
self.set_bit(index + bit_offset, val_bit_is_set);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Std encode structs (copied from ash-master native.rs; common Std types reused from ash) ----------
|
||||
#[repr(C, align(4))]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct StdVideoEncodeAV1PictureInfoFlags {
|
||||
pub _bitfield_align_1: [u8; 0],
|
||||
pub _bitfield_1: __BindgenBitfieldUnit<[u8; 4usize]>,
|
||||
}
|
||||
impl StdVideoEncodeAV1PictureInfoFlags {
|
||||
#[inline]
|
||||
pub fn set_error_resilient_mode(&mut self, val: u32) {
|
||||
self._bitfield_1.set(0, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_disable_cdf_update(&mut self, val: u32) {
|
||||
self._bitfield_1.set(1, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_use_superres(&mut self, val: u32) {
|
||||
self._bitfield_1.set(2, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_render_and_frame_size_different(&mut self, val: u32) {
|
||||
self._bitfield_1.set(3, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_allow_screen_content_tools(&mut self, val: u32) {
|
||||
self._bitfield_1.set(4, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_is_filter_switchable(&mut self, val: u32) {
|
||||
self._bitfield_1.set(5, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_force_integer_mv(&mut self, val: u32) {
|
||||
self._bitfield_1.set(6, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_frame_size_override_flag(&mut self, val: u32) {
|
||||
self._bitfield_1.set(7, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_buffer_removal_time_present_flag(&mut self, val: u32) {
|
||||
self._bitfield_1.set(8, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_allow_intrabc(&mut self, val: u32) {
|
||||
self._bitfield_1.set(9, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_frame_refs_short_signaling(&mut self, val: u32) {
|
||||
self._bitfield_1.set(10, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_allow_high_precision_mv(&mut self, val: u32) {
|
||||
self._bitfield_1.set(11, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_is_motion_mode_switchable(&mut self, val: u32) {
|
||||
self._bitfield_1.set(12, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_use_ref_frame_mvs(&mut self, val: u32) {
|
||||
self._bitfield_1.set(13, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_disable_frame_end_update_cdf(&mut self, val: u32) {
|
||||
self._bitfield_1.set(14, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_allow_warped_motion(&mut self, val: u32) {
|
||||
self._bitfield_1.set(15, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_reduced_tx_set(&mut self, val: u32) {
|
||||
self._bitfield_1.set(16, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_skip_mode_present(&mut self, val: u32) {
|
||||
self._bitfield_1.set(17, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_delta_q_present(&mut self, val: u32) {
|
||||
self._bitfield_1.set(18, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_delta_lf_present(&mut self, val: u32) {
|
||||
self._bitfield_1.set(19, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_delta_lf_multi(&mut self, val: u32) {
|
||||
self._bitfield_1.set(20, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_segmentation_enabled(&mut self, val: u32) {
|
||||
self._bitfield_1.set(21, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_segmentation_update_map(&mut self, val: u32) {
|
||||
self._bitfield_1.set(22, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_segmentation_temporal_update(&mut self, val: u32) {
|
||||
self._bitfield_1.set(23, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_segmentation_update_data(&mut self, val: u32) {
|
||||
self._bitfield_1.set(24, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_UsesLr(&mut self, val: u32) {
|
||||
self._bitfield_1.set(25, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_usesChromaLr(&mut self, val: u32) {
|
||||
self._bitfield_1.set(26, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_show_frame(&mut self, val: u32) {
|
||||
self._bitfield_1.set(27, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_showable_frame(&mut self, val: u32) {
|
||||
self._bitfield_1.set(28, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_reserved(&mut self, val: u32) {
|
||||
self._bitfield_1.set(29, 3, val as u64)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct StdVideoEncodeAV1PictureInfo {
|
||||
pub flags: StdVideoEncodeAV1PictureInfoFlags,
|
||||
pub frame_type: StdVideoAV1FrameType,
|
||||
pub frame_presentation_time: u32,
|
||||
pub current_frame_id: u32,
|
||||
pub order_hint: u8,
|
||||
pub primary_ref_frame: u8,
|
||||
pub refresh_frame_flags: u8,
|
||||
pub coded_denom: u8,
|
||||
pub render_width_minus_1: u16,
|
||||
pub render_height_minus_1: u16,
|
||||
pub interpolation_filter: StdVideoAV1InterpolationFilter,
|
||||
pub TxMode: StdVideoAV1TxMode,
|
||||
pub delta_q_res: u8,
|
||||
pub delta_lf_res: u8,
|
||||
pub ref_order_hint: [u8; 8usize],
|
||||
pub ref_frame_idx: [i8; 7usize],
|
||||
pub reserved1: [u8; 3usize],
|
||||
pub delta_frame_id_minus_1: [u32; 7usize],
|
||||
pub pTileInfo: *const ash::vk::native::StdVideoAV1TileInfo,
|
||||
pub pQuantization: *const ash::vk::native::StdVideoAV1Quantization,
|
||||
pub pSegmentation: *const ash::vk::native::StdVideoAV1Segmentation,
|
||||
pub pLoopFilter: *const ash::vk::native::StdVideoAV1LoopFilter,
|
||||
pub pCDEF: *const ash::vk::native::StdVideoAV1CDEF,
|
||||
pub pLoopRestoration: *const ash::vk::native::StdVideoAV1LoopRestoration,
|
||||
pub pGlobalMotion: *const ash::vk::native::StdVideoAV1GlobalMotion,
|
||||
pub pExtensionHeader: *const StdVideoEncodeAV1ExtensionHeader,
|
||||
pub pBufferRemovalTimes: *const u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct StdVideoEncodeAV1ReferenceInfoFlags {
|
||||
pub _bitfield_align_1: [u32; 0],
|
||||
pub _bitfield_1: __BindgenBitfieldUnit<[u8; 4usize]>,
|
||||
}
|
||||
impl StdVideoEncodeAV1ReferenceInfoFlags {
|
||||
#[inline]
|
||||
pub fn set_disable_frame_end_update_cdf(&mut self, val: u32) {
|
||||
self._bitfield_1.set(0, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_segmentation_enabled(&mut self, val: u32) {
|
||||
self._bitfield_1.set(1, 1, val as u64)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct StdVideoEncodeAV1ReferenceInfo {
|
||||
pub flags: StdVideoEncodeAV1ReferenceInfoFlags,
|
||||
pub RefFrameId: u32,
|
||||
pub frame_type: StdVideoAV1FrameType,
|
||||
pub OrderHint: u8,
|
||||
pub reserved1: [u8; 3usize],
|
||||
pub pExtensionHeader: *const StdVideoEncodeAV1ExtensionHeader,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct StdVideoEncodeAV1ExtensionHeader {
|
||||
pub temporal_id: u8,
|
||||
pub spatial_id: u8,
|
||||
}
|
||||
|
||||
// ---------- KHR extension structs (repr(C); lifetimes/PhantomData dropped — layout-identical,
|
||||
// chained by raw p_next). Flag/enum newtypes flattened to their u32/i32 repr. ----------
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1ProfileInfoKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *const c_void,
|
||||
pub std_profile: StdVideoAV1Profile,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1CapabilitiesKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *mut c_void,
|
||||
pub flags: u32,
|
||||
pub max_level: StdVideoAV1Level,
|
||||
pub coded_picture_alignment: vk::Extent2D,
|
||||
pub max_tiles: vk::Extent2D,
|
||||
pub min_tile_size: vk::Extent2D,
|
||||
pub max_tile_size: vk::Extent2D,
|
||||
pub superblock_sizes: u32,
|
||||
pub max_single_reference_count: u32,
|
||||
pub single_reference_name_mask: u32,
|
||||
pub max_unidirectional_compound_reference_count: u32,
|
||||
pub max_unidirectional_compound_group1_reference_count: u32,
|
||||
pub unidirectional_compound_reference_name_mask: u32,
|
||||
pub max_bidirectional_compound_reference_count: u32,
|
||||
pub max_bidirectional_compound_group1_reference_count: u32,
|
||||
pub max_bidirectional_compound_group2_reference_count: u32,
|
||||
pub bidirectional_compound_reference_name_mask: u32,
|
||||
pub max_temporal_layer_count: u32,
|
||||
pub max_spatial_layer_count: u32,
|
||||
pub max_operating_points: u32,
|
||||
pub min_q_index: u32,
|
||||
pub max_q_index: u32,
|
||||
pub prefers_gop_remaining_frames: vk::Bool32,
|
||||
pub requires_gop_remaining_frames: vk::Bool32,
|
||||
pub std_syntax_flags: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1SessionParametersCreateInfoKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *const c_void,
|
||||
pub p_std_sequence_header: *const StdVideoAV1SequenceHeader,
|
||||
pub p_std_decoder_model_info: *const c_void,
|
||||
pub std_operating_point_count: u32,
|
||||
pub p_std_operating_points: *const c_void,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1PictureInfoKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *const c_void,
|
||||
pub prediction_mode: i32,
|
||||
pub rate_control_group: i32,
|
||||
pub constant_q_index: u32,
|
||||
pub p_std_picture_info: *const StdVideoEncodeAV1PictureInfo,
|
||||
pub reference_name_slot_indices: [i32; MAX_VIDEO_AV1_REFERENCES_PER_FRAME],
|
||||
pub primary_reference_cdf_only: vk::Bool32,
|
||||
pub generate_obu_extension_header: vk::Bool32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1DpbSlotInfoKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *const c_void,
|
||||
pub p_std_reference_info: *const StdVideoEncodeAV1ReferenceInfo,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1RateControlInfoKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *const c_void,
|
||||
pub flags: u32,
|
||||
pub gop_frame_count: u32,
|
||||
pub key_frame_period: u32,
|
||||
pub consecutive_bipredictive_frame_count: u32,
|
||||
pub temporal_layer_count: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1QIndexKHR {
|
||||
pub intra_q_index: u32,
|
||||
pub predictive_q_index: u32,
|
||||
pub bipredictive_q_index: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1FrameSizeKHR {
|
||||
pub intra_frame_size: u32,
|
||||
pub predictive_frame_size: u32,
|
||||
pub bipredictive_frame_size: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1RateControlLayerInfoKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *const c_void,
|
||||
pub use_min_q_index: vk::Bool32,
|
||||
pub min_q_index: VideoEncodeAV1QIndexKHR,
|
||||
pub use_max_q_index: vk::Bool32,
|
||||
pub max_q_index: VideoEncodeAV1QIndexKHR,
|
||||
pub use_max_frame_size: vk::Bool32,
|
||||
pub max_frame_size: VideoEncodeAV1FrameSizeKHR,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1GopRemainingFrameInfoKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *const c_void,
|
||||
pub use_gop_remaining_frames: vk::Bool32,
|
||||
pub gop_remaining_intra: u32,
|
||||
pub gop_remaining_predictive: u32,
|
||||
pub gop_remaining_bipredictive: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1SessionCreateInfoKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *const c_void,
|
||||
pub use_max_level: vk::Bool32,
|
||||
pub max_level: StdVideoAV1Level,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct StdVideoEncodeAV1OperatingPointInfoFlags {
|
||||
pub _bitfield_align_1: [u32; 0],
|
||||
pub _bitfield_1: __BindgenBitfieldUnit<[u8; 4usize]>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct StdVideoEncodeAV1OperatingPointInfo {
|
||||
pub flags: StdVideoEncodeAV1OperatingPointInfoFlags,
|
||||
pub operating_point_idc: u16,
|
||||
pub seq_level_idx: u8,
|
||||
pub seq_tier: u8,
|
||||
pub decoder_buffer_delay: u32,
|
||||
pub encoder_buffer_delay: u32,
|
||||
pub initial_display_delay_minus_1: u8,
|
||||
}
|
||||
|
||||
/// `vk::StructureType` for a raw `ST_*` constant above.
|
||||
#[inline]
|
||||
pub fn stype(raw: i32) -> vk::StructureType {
|
||||
vk::StructureType::from_raw(raw)
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
//! Raw **Vulkan Video** HEVC encoder (`VK_KHR_video_encode_h265`) with true reference-frame
|
||||
//! invalidation — the open-stack AMD/Intel-Linux twin of the direct-NVENC RFI path. The app owns
|
||||
//! the DPB, so loss recovery is a clean P-frame that re-references a known-good older slot (no IDR).
|
||||
//! Raw **Vulkan Video** HEVC + AV1 encoder (`VK_KHR_video_encode_h265` / `_av1`) with true
|
||||
//! reference-frame invalidation — the open-stack AMD/Intel-Linux twin of the direct-NVENC RFI path.
|
||||
//! The app owns the DPB, so loss recovery is a clean P-frame that re-references a known-good older
|
||||
//! slot (no IDR): HEVC via an explicit short-term RPS, AV1 via `ref_frame_idx` + a
|
||||
//! `primary_ref_frame = NONE` recovery anchor that also breaks the CDF chain.
|
||||
//!
|
||||
//! Capture delivers packed RGB (dmabuf/CPU); this backend imports it, runs an on-GPU RGB→NV12
|
||||
//! BT.709 compute CSC, then encodes. Proven end-to-end in `punktfunk-planning/design/vkenc-probe-harness`.
|
||||
//! Opt-in via `PUNKTFUNK_VULKAN_ENCODE`; gated to HEVC + a device that advertises h265 encode.
|
||||
//! Opt-in via `PUNKTFUNK_VULKAN_ENCODE`; gated to HEVC/AV1 + a device that advertises the encode op.
|
||||
//! The AV1 encode structs our pinned `ash 0.38` predates are vendored in `vk_av1_encode.rs`.
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
||||
@@ -29,6 +32,9 @@ const DPB_SLOTS: u32 = 8;
|
||||
/// latency — on-glass validated as rock-solid at 1080p@240, so it is the real-time default;
|
||||
/// backpressure kicks in at the 2nd unread frame. Distinct from `DPB_SLOTS` (reference pool).
|
||||
const RING_DEFAULT: usize = 2;
|
||||
/// AV1 base quantizer index (0..=255) seeded into every frame. CBR rate control overrides it per
|
||||
/// frame; it only matters as the starting point and for the (rate-control-ignored) constant-Q path.
|
||||
const AV1_BASE_Q_IDX: u8 = 128;
|
||||
|
||||
/// Resolve the in-flight ring depth: `PUNKTFUNK_VULKAN_INFLIGHT` (clamped 2..=6), else `RING_DEFAULT`.
|
||||
fn ring_depth() -> usize {
|
||||
@@ -97,11 +103,18 @@ pub struct VulkanVideoEncoder {
|
||||
compute_family: u32,
|
||||
mem_props: vk::PhysicalDeviceMemoryProperties,
|
||||
|
||||
// --- codec ---
|
||||
codec: Codec, // H265 or Av1 — selects the Std-struct authoring + header framing
|
||||
|
||||
// --- video session ---
|
||||
session: vk::VideoSessionKHR,
|
||||
session_mem: Vec<vk::DeviceMemory>,
|
||||
params: vk::VideoSessionParametersKHR,
|
||||
header: Vec<u8>, // VPS/SPS/PPS bytes, prepended to each keyframe
|
||||
// Keyframe prefix: HEVC = VPS/SPS/PPS; AV1 = temporal-delimiter OBU + sequence-header OBU.
|
||||
header: Vec<u8>,
|
||||
// Per-(non-key)-frame prefix: empty for HEVC (headers ride keyframes only); AV1 = a
|
||||
// temporal-delimiter OBU that opens every temporal unit (Vulkan emits only the frame OBU).
|
||||
frame_prefix: Vec<u8>,
|
||||
|
||||
// --- DPB ---
|
||||
dpb_image: vk::Image,
|
||||
@@ -137,7 +150,9 @@ pub struct VulkanVideoEncoder {
|
||||
// --- state ---
|
||||
width: u32,
|
||||
height: u32,
|
||||
poc: i32, // monotonic HEVC picture-order-count
|
||||
render_w: u32, // real (pre-alignment) dimensions — AV1 render_size / HEVC conformance window
|
||||
render_h: u32,
|
||||
poc: i32, // monotonic HEVC picture-order-count (reused as AV1 order_hint counter)
|
||||
enc_count: u64, // total frames encoded — drives the DPB ring cursor
|
||||
auto_wire: i64, // fallback wire index when submit() (not submit_indexed) is used
|
||||
first_frame: bool, // needs RESET + DPB layout transition + CBR install + IDR
|
||||
@@ -153,20 +168,45 @@ unsafe impl Send for VulkanVideoEncoder {}
|
||||
impl VulkanVideoEncoder {
|
||||
/// Signature mirrors the other Linux backends' `open` (see `nvenc_cuda::NvencCudaEncoder::open`).
|
||||
pub fn open(codec: Codec, width: u32, height: u32, fps: u32, bitrate_bps: u64) -> Result<Self> {
|
||||
if codec != Codec::H265 {
|
||||
bail!("vulkan-encode backend is HEVC-only (got {codec:?})");
|
||||
if !matches!(codec, Codec::H265 | Codec::Av1) {
|
||||
bail!("vulkan-encode backend supports HEVC + AV1 only (got {codec:?})");
|
||||
}
|
||||
// align coded extent to the encode granularity (64x16 on RADV); a conformance window
|
||||
// crops the aligned padding back to (width,height) on the decoder.
|
||||
// align coded extent to the encode granularity (64x16 on RADV). HEVC crops the padding back
|
||||
// to (width,height) via a conformance window; AV1 signals it via render_size (see build).
|
||||
let w = (width + 63) & !63;
|
||||
let h = (height + 15) & !15;
|
||||
// SAFETY: `open_inner` only issues Vulkan calls whose preconditions it establishes itself
|
||||
// (valid instance/device, correctly-chained create-infos); all handles are freshly created
|
||||
// here and owned by the returned `Self`. No aliasing or outside invariants are involved.
|
||||
unsafe { Self::open_inner(w, h, width, height, fps.max(1), bitrate_bps.max(1_000_000)) }
|
||||
unsafe {
|
||||
Self::open_inner(
|
||||
codec,
|
||||
w,
|
||||
h,
|
||||
width,
|
||||
height,
|
||||
fps.max(1),
|
||||
bitrate_bps.max(1_000_000),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn open_inner(w: u32, h: u32, rw: u32, rh: u32, fps: u32, bitrate: u64) -> Result<Self> {
|
||||
unsafe fn open_inner(
|
||||
codec: Codec,
|
||||
w: u32,
|
||||
h: u32,
|
||||
rw: u32,
|
||||
rh: u32,
|
||||
fps: u32,
|
||||
bitrate: u64,
|
||||
) -> Result<Self> {
|
||||
use super::vk_av1_encode as av1b;
|
||||
let av1 = codec == Codec::Av1;
|
||||
let codec_op = if av1 {
|
||||
vk::VideoCodecOperationFlagsKHR::from_raw(av1b::VIDEO_CODEC_OPERATION_ENCODE_AV1)
|
||||
} else {
|
||||
vk::VideoCodecOperationFlagsKHR::ENCODE_H265
|
||||
};
|
||||
let entry = ash::Entry::load().context("load vulkan loader")?;
|
||||
let app = vk::ApplicationInfo::default().api_version(vk::API_VERSION_1_3);
|
||||
let instance = entry
|
||||
@@ -194,9 +234,7 @@ impl VulkanVideoEncoder {
|
||||
.queue_family_properties
|
||||
.queue_flags
|
||||
.contains(vk::QueueFlags::VIDEO_ENCODE_KHR)
|
||||
&& video[i]
|
||||
.video_codec_operations
|
||||
.contains(vk::VideoCodecOperationFlagsKHR::ENCODE_H265)
|
||||
&& video[i].video_codec_operations.contains(codec_op)
|
||||
{
|
||||
found = Some((pd, i as u32));
|
||||
break;
|
||||
@@ -206,7 +244,7 @@ impl VulkanVideoEncoder {
|
||||
break;
|
||||
}
|
||||
}
|
||||
found.context("no VK_KHR_video_encode_h265 queue on any device")?
|
||||
found.context("no VK_KHR_video_encode queue for the requested codec on any device")?
|
||||
};
|
||||
let mem_props = instance.get_physical_device_memory_properties(pd);
|
||||
|
||||
@@ -218,38 +256,60 @@ impl VulkanVideoEncoder {
|
||||
.context("no compute queue")? as u32
|
||||
};
|
||||
|
||||
// the H265 Main encode profile
|
||||
// the encode profile — H265 Main, or AV1 Main (AV1 profile chained raw since ash 0.38 lacks it)
|
||||
let mut h265_profile = vk::VideoEncodeH265ProfileInfoKHR::default()
|
||||
.std_profile_idc(vk::native::StdVideoH265ProfileIdc_STD_VIDEO_H265_PROFILE_IDC_MAIN);
|
||||
let mut av1_profile = av1b::VideoEncodeAV1ProfileInfoKHR {
|
||||
s_type: av1b::stype(av1b::ST_PROFILE_INFO),
|
||||
p_next: std::ptr::null(),
|
||||
std_profile: vk::native::StdVideoAV1Profile_STD_VIDEO_AV1_PROFILE_MAIN,
|
||||
};
|
||||
let mut usage = vk::VideoEncodeUsageInfoKHR::default()
|
||||
.video_usage_hints(vk::VideoEncodeUsageFlagsKHR::STREAMING)
|
||||
.video_content_hints(vk::VideoEncodeContentFlagsKHR::RENDERED)
|
||||
.tuning_mode(vk::VideoEncodeTuningModeKHR::ULTRA_LOW_LATENCY);
|
||||
let profile = vk::VideoProfileInfoKHR::default()
|
||||
.video_codec_operation(vk::VideoCodecOperationFlagsKHR::ENCODE_H265)
|
||||
let mut profile = vk::VideoProfileInfoKHR::default()
|
||||
.video_codec_operation(codec_op)
|
||||
.chroma_subsampling(vk::VideoChromaSubsamplingFlagsKHR::TYPE_420)
|
||||
.luma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::TYPE_8)
|
||||
.chroma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::TYPE_8)
|
||||
.push_next(&mut h265_profile)
|
||||
.push_next(&mut usage);
|
||||
if av1 {
|
||||
// prepend the AV1 profile into the p_next chain (it can't `push_next` — vendored struct)
|
||||
av1_profile.p_next = profile.p_next;
|
||||
profile.p_next = &av1_profile as *const _ as *const c_void;
|
||||
} else {
|
||||
profile = profile.push_next(&mut h265_profile);
|
||||
}
|
||||
|
||||
// capabilities (chain required for encode) -> std header version
|
||||
// capabilities (codec chain required for encode) -> std header version, coded alignment, RC modes
|
||||
let mut h265_caps = vk::VideoEncodeH265CapabilitiesKHR::default();
|
||||
let mut av1_caps: av1b::VideoEncodeAV1CapabilitiesKHR = std::mem::zeroed();
|
||||
av1_caps.s_type = av1b::stype(av1b::ST_CAPABILITIES);
|
||||
let mut enc_caps = vk::VideoEncodeCapabilitiesKHR::default();
|
||||
let mut caps = vk::VideoCapabilitiesKHR::default()
|
||||
.push_next(&mut enc_caps)
|
||||
.push_next(&mut h265_caps);
|
||||
let mut caps = vk::VideoCapabilitiesKHR::default().push_next(&mut enc_caps);
|
||||
if av1 {
|
||||
av1_caps.p_next = caps.p_next;
|
||||
caps.p_next = &mut av1_caps as *mut _ as *mut c_void;
|
||||
} else {
|
||||
caps = caps.push_next(&mut h265_caps);
|
||||
}
|
||||
let r = (vq_inst.fp().get_physical_device_video_capabilities_khr)(pd, &profile, &mut caps);
|
||||
if r != vk::Result::SUCCESS {
|
||||
bail!("get_physical_device_video_capabilities: {r:?}");
|
||||
}
|
||||
let std_hdr = caps.std_header_version;
|
||||
let av1_superblock128 = av1 && (av1_caps.superblock_sizes & av1b::SUPERBLOCK_SIZE_128 != 0);
|
||||
|
||||
// logical device: encode + compute queues + video extensions
|
||||
// logical device: encode + compute queues + video extensions (AV1 ext name is raw — ash lacks it)
|
||||
let dev_exts = [
|
||||
ash::khr::video_queue::NAME.as_ptr(),
|
||||
ash::khr::video_encode_queue::NAME.as_ptr(),
|
||||
ash::khr::video_encode_h265::NAME.as_ptr(),
|
||||
if av1 {
|
||||
av1b::EXTENSION_NAME.as_ptr()
|
||||
} else {
|
||||
ash::khr::video_encode_h265::NAME.as_ptr()
|
||||
},
|
||||
ash::khr::external_memory_fd::NAME.as_ptr(),
|
||||
ash::ext::external_memory_dma_buf::NAME.as_ptr(),
|
||||
ash::ext::image_drm_format_modifier::NAME.as_ptr(),
|
||||
@@ -283,8 +343,14 @@ impl VulkanVideoEncoder {
|
||||
let vq_dev = ash::khr::video_queue::Device::new(&instance, &device);
|
||||
let venc_dev = ash::khr::video_encode_queue::Device::new(&instance, &device);
|
||||
|
||||
// ---- video session ----
|
||||
let session_ci = vk::VideoSessionCreateInfoKHR::default()
|
||||
// ---- video session ---- (AV1 pins the max level from caps via a chained create-info)
|
||||
let av1_sci = av1b::VideoEncodeAV1SessionCreateInfoKHR {
|
||||
s_type: av1b::stype(av1b::ST_SESSION_CREATE_INFO),
|
||||
p_next: std::ptr::null(),
|
||||
use_max_level: vk::TRUE,
|
||||
max_level: av1_caps.max_level,
|
||||
};
|
||||
let mut session_ci = vk::VideoSessionCreateInfoKHR::default()
|
||||
.queue_family_index(encode_family)
|
||||
.video_profile(&profile)
|
||||
.picture_format(NV12)
|
||||
@@ -296,6 +362,9 @@ impl VulkanVideoEncoder {
|
||||
.max_dpb_slots(DPB_SLOTS + 1)
|
||||
.max_active_reference_pictures(1)
|
||||
.std_header_version(&std_hdr);
|
||||
if av1 {
|
||||
session_ci.p_next = &av1_sci as *const _ as *const c_void;
|
||||
}
|
||||
let mut session = vk::VideoSessionKHR::null();
|
||||
let r = (vq_dev.fp().create_video_session_khr)(
|
||||
device.handle(),
|
||||
@@ -346,9 +415,25 @@ impl VulkanVideoEncoder {
|
||||
bail!("bind_video_session_memory: {r:?}");
|
||||
}
|
||||
|
||||
// ---- session parameters (VPS/SPS/PPS) + retrieve header ----
|
||||
let (params, header) =
|
||||
build_parameters(&device, &vq_dev, &venc_dev, session, w, h, rw, rh)?;
|
||||
// ---- session parameters + header framing (HEVC: VPS/SPS/PPS on keyframes; AV1: a
|
||||
// temporal-delimiter OBU per frame + a sequence-header OBU on keyframes) ----
|
||||
let (params, header, frame_prefix) = if av1 {
|
||||
build_parameters_av1(
|
||||
&device,
|
||||
&vq_dev,
|
||||
session,
|
||||
w,
|
||||
h,
|
||||
rw,
|
||||
rh,
|
||||
av1_caps.max_level,
|
||||
av1_superblock128,
|
||||
)?
|
||||
} else {
|
||||
let (p, hdr) =
|
||||
build_parameters_h265(&device, &vq_dev, &venc_dev, session, w, h, rw, rh)?;
|
||||
(p, hdr, Vec::new())
|
||||
};
|
||||
|
||||
// ---- DPB image (NV12 OPTIMAL, ring of slots) — encode queue only ----
|
||||
let mut profile_list =
|
||||
@@ -488,10 +573,12 @@ impl VulkanVideoEncoder {
|
||||
compute_queue,
|
||||
compute_family,
|
||||
mem_props,
|
||||
codec,
|
||||
session,
|
||||
session_mem,
|
||||
params,
|
||||
header,
|
||||
frame_prefix,
|
||||
dpb_image,
|
||||
dpb_mem,
|
||||
dpb_views,
|
||||
@@ -514,6 +601,8 @@ impl VulkanVideoEncoder {
|
||||
fps,
|
||||
width: w,
|
||||
height: h,
|
||||
render_w: rw,
|
||||
render_h: rh,
|
||||
poc: 0,
|
||||
enc_count: 0,
|
||||
auto_wire: 0,
|
||||
@@ -1010,7 +1099,14 @@ impl VulkanVideoEncoder {
|
||||
);
|
||||
dev.end_command_buffer(compute_cmd)?;
|
||||
|
||||
// ---- 3. author HEVC Std structs + record encode into `cmd` ----
|
||||
// ---- 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 {
|
||||
self.record_coding_av1(
|
||||
&dev, cmd, query_pool, bs_buf, nv12_src, nv12_view, is_idr, recovery, ref_slot,
|
||||
setup_idx, poc,
|
||||
)?;
|
||||
} else {
|
||||
let mut pic_flags: h::StdVideoEncodeH265PictureInfoFlags = std::mem::zeroed();
|
||||
pic_flags.set_is_reference(1);
|
||||
if is_idr {
|
||||
@@ -1233,6 +1329,7 @@ impl VulkanVideoEncoder {
|
||||
dev.cmd_end_query(cmd, query_pool, 0);
|
||||
(self.vq_dev.fp().cmd_end_video_coding_khr)(cmd, &vk::VideoEndCodingInfoKHR::default());
|
||||
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
|
||||
@@ -1278,8 +1375,307 @@ impl VulkanVideoEncoder {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read one completed slot's bitstream into an `EncodedFrame` (VPS/SPS/PPS prepended on
|
||||
/// keyframes). Caller must have confirmed the slot's fence is signaled (blocking wait or probe).
|
||||
/// Author the AV1 Std structs + record begin/encode/end for one frame into `cmd`. Self-contained
|
||||
/// (own begin/end command-buffer + pre-encode barriers) — the AV1 twin of the inline HEVC path in
|
||||
/// `record_submit`. RFI lever: an IDR **or** a recovery frame breaks the CDF chain
|
||||
/// (`primary_ref_frame = PRIMARY_REF_NONE` + `error_resilient_mode`) so it decodes independent of
|
||||
/// the lost frames' probability context, while a normal P inherits context (name 0 → `ref_slot`).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
unsafe fn record_coding_av1(
|
||||
&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,
|
||||
recovery: bool,
|
||||
ref_slot: usize,
|
||||
setup_idx: usize,
|
||||
order: i32,
|
||||
) -> Result<()> {
|
||||
use super::vk_av1_encode as av1;
|
||||
use ash::vk::native as h;
|
||||
let ext2d = vk::Extent2D {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
};
|
||||
|
||||
// ---- required AV1 frame sub-structs (single tile; no CDEF/LR/segmentation/global-motion) ----
|
||||
let mut tile_flags: h::StdVideoAV1TileInfoFlags = std::mem::zeroed();
|
||||
tile_flags.set_uniform_tile_spacing_flag(1);
|
||||
let mut tile_info: h::StdVideoAV1TileInfo = std::mem::zeroed();
|
||||
tile_info.flags = tile_flags;
|
||||
tile_info.TileCols = 1;
|
||||
tile_info.TileRows = 1;
|
||||
|
||||
let mut quant: h::StdVideoAV1Quantization = std::mem::zeroed();
|
||||
quant.base_q_idx = AV1_BASE_Q_IDX;
|
||||
|
||||
let mut loop_filter: h::StdVideoAV1LoopFilter = std::mem::zeroed();
|
||||
// AV1 default_loop_filter_ref_deltas (spec 7.14.1): intra +1, golden/bwd/altref2/altref -1.
|
||||
loop_filter.loop_filter_ref_deltas = [1, 0, 0, 0, -1, 0, -1, -1];
|
||||
|
||||
let cdef: h::StdVideoAV1CDEF = std::mem::zeroed();
|
||||
|
||||
let mut lr: h::StdVideoAV1LoopRestoration = std::mem::zeroed();
|
||||
lr.FrameRestorationType =
|
||||
[h::StdVideoAV1FrameRestorationType_STD_VIDEO_AV1_FRAME_RESTORATION_TYPE_NONE; 3];
|
||||
|
||||
let seg: h::StdVideoAV1Segmentation = std::mem::zeroed();
|
||||
let gm: h::StdVideoAV1GlobalMotion = std::mem::zeroed();
|
||||
|
||||
// Order hints of the 8 physical reference buffers (DPB slots), 0 where empty.
|
||||
let mut ref_order_hint = [0u8; 8];
|
||||
for (i, &poc) in self.slot_poc.iter().enumerate().take(8) {
|
||||
ref_order_hint[i] = poc.max(0) as u8;
|
||||
}
|
||||
|
||||
// ---- Std picture info ----
|
||||
// A recovery anchor (or IDR) is error-resilient + inherits no CDF context, so it decodes
|
||||
// independent of the (possibly lost) frames since its reference — the AV1 RFI lever. Normal
|
||||
// P-frames inherit context from their reference (primary_ref = name 0 → `ref_slot`) for
|
||||
// compression, exactly like the HEVC path's reference chain.
|
||||
let independent = is_idr || recovery;
|
||||
let mut pic_flags: av1::StdVideoEncodeAV1PictureInfoFlags = std::mem::zeroed();
|
||||
pic_flags.set_show_frame(1);
|
||||
if independent {
|
||||
pic_flags.set_error_resilient_mode(1);
|
||||
}
|
||||
let mut std_pic: av1::StdVideoEncodeAV1PictureInfo = std::mem::zeroed();
|
||||
std_pic.flags = pic_flags;
|
||||
std_pic.frame_type = if is_idr {
|
||||
h::StdVideoAV1FrameType_STD_VIDEO_AV1_FRAME_TYPE_KEY
|
||||
} else {
|
||||
h::StdVideoAV1FrameType_STD_VIDEO_AV1_FRAME_TYPE_INTER
|
||||
};
|
||||
std_pic.order_hint = order as u8;
|
||||
std_pic.primary_ref_frame = if independent {
|
||||
av1::PRIMARY_REF_NONE
|
||||
} else {
|
||||
0
|
||||
};
|
||||
std_pic.refresh_frame_flags = if is_idr { 0xff } else { 1u8 << setup_idx };
|
||||
std_pic.render_width_minus_1 = (self.render_w - 1) as u16;
|
||||
std_pic.render_height_minus_1 = (self.render_h - 1) as u16;
|
||||
std_pic.interpolation_filter = 0; // EIGHTTAP
|
||||
std_pic.TxMode = h::StdVideoAV1TxMode_STD_VIDEO_AV1_TX_MODE_SELECT;
|
||||
std_pic.ref_order_hint = ref_order_hint;
|
||||
if !is_idr {
|
||||
// single-reference P: every reference name maps to the (recovery or previous) DPB slot.
|
||||
std_pic.ref_frame_idx = [ref_slot as i8; 7];
|
||||
}
|
||||
std_pic.pTileInfo = &tile_info;
|
||||
std_pic.pQuantization = &quant;
|
||||
std_pic.pLoopFilter = &loop_filter;
|
||||
std_pic.pCDEF = &cdef;
|
||||
std_pic.pLoopRestoration = &lr;
|
||||
std_pic.pSegmentation = &seg;
|
||||
std_pic.pGlobalMotion = &gm;
|
||||
|
||||
// ---- KHR picture info ----
|
||||
let av1_pic = av1::VideoEncodeAV1PictureInfoKHR {
|
||||
s_type: av1::stype(av1::ST_PICTURE_INFO),
|
||||
p_next: std::ptr::null(),
|
||||
prediction_mode: if is_idr {
|
||||
av1::PREDICTION_MODE_INTRA_ONLY
|
||||
} else {
|
||||
av1::PREDICTION_MODE_SINGLE_REFERENCE
|
||||
},
|
||||
rate_control_group: if is_idr {
|
||||
av1::RC_GROUP_INTRA
|
||||
} else {
|
||||
av1::RC_GROUP_PREDICTIVE
|
||||
},
|
||||
constant_q_index: quant.base_q_idx as u32,
|
||||
p_std_picture_info: &std_pic,
|
||||
reference_name_slot_indices: if is_idr {
|
||||
[-1; av1::MAX_VIDEO_AV1_REFERENCES_PER_FRAME]
|
||||
} else {
|
||||
[ref_slot as i32; av1::MAX_VIDEO_AV1_REFERENCES_PER_FRAME]
|
||||
},
|
||||
primary_reference_cdf_only: 0,
|
||||
generate_obu_extension_header: 0,
|
||||
};
|
||||
|
||||
// ---- setup (reconstruct into) + reference (read from) DPB slots ----
|
||||
let setup_res = vk::VideoPictureResourceInfoKHR::default()
|
||||
.coded_extent(ext2d)
|
||||
.image_view_binding(self.dpb_views[setup_idx]);
|
||||
let mut setup_ref_std: av1::StdVideoEncodeAV1ReferenceInfo = std::mem::zeroed();
|
||||
setup_ref_std.frame_type = std_pic.frame_type;
|
||||
setup_ref_std.OrderHint = order as u8;
|
||||
let setup_dpb = av1::VideoEncodeAV1DpbSlotInfoKHR {
|
||||
s_type: av1::stype(av1::ST_DPB_SLOT_INFO),
|
||||
p_next: std::ptr::null(),
|
||||
p_std_reference_info: &setup_ref_std,
|
||||
};
|
||||
let mut setup_slot = vk::VideoReferenceSlotInfoKHR::default()
|
||||
.slot_index(setup_idx as i32)
|
||||
.picture_resource(&setup_res);
|
||||
setup_slot.p_next = &setup_dpb as *const _ as *const c_void;
|
||||
let mut begin_setup = vk::VideoReferenceSlotInfoKHR::default()
|
||||
.slot_index(-1)
|
||||
.picture_resource(&setup_res);
|
||||
begin_setup.p_next = &setup_dpb as *const _ as *const c_void;
|
||||
|
||||
let ref_res = vk::VideoPictureResourceInfoKHR::default()
|
||||
.coded_extent(ext2d)
|
||||
.image_view_binding(self.dpb_views[ref_slot]);
|
||||
let mut ref_ref_std: av1::StdVideoEncodeAV1ReferenceInfo = std::mem::zeroed();
|
||||
ref_ref_std.frame_type = if self.slot_poc[ref_slot] == 0 {
|
||||
h::StdVideoAV1FrameType_STD_VIDEO_AV1_FRAME_TYPE_KEY
|
||||
} else {
|
||||
h::StdVideoAV1FrameType_STD_VIDEO_AV1_FRAME_TYPE_INTER
|
||||
};
|
||||
ref_ref_std.OrderHint = self.slot_poc[ref_slot].max(0) as u8;
|
||||
let ref_dpb = av1::VideoEncodeAV1DpbSlotInfoKHR {
|
||||
s_type: av1::stype(av1::ST_DPB_SLOT_INFO),
|
||||
p_next: std::ptr::null(),
|
||||
p_std_reference_info: &ref_ref_std,
|
||||
};
|
||||
let mut ref_begin = vk::VideoReferenceSlotInfoKHR::default()
|
||||
.slot_index(ref_slot as i32)
|
||||
.picture_resource(&ref_res);
|
||||
ref_begin.p_next = &ref_dpb as *const _ as *const c_void;
|
||||
let mut ref_enc = vk::VideoReferenceSlotInfoKHR::default()
|
||||
.slot_index(ref_slot as i32)
|
||||
.picture_resource(&ref_res);
|
||||
ref_enc.p_next = &ref_dpb as *const _ as *const c_void;
|
||||
let begin_p = [ref_begin, begin_setup];
|
||||
let begin_i = [begin_setup];
|
||||
let enc_refs = [ref_enc];
|
||||
|
||||
// ---- CBR rate control (generic layer + AV1 codec info chained manually) ----
|
||||
let rc_layer = [vk::VideoEncodeRateControlLayerInfoKHR::default()
|
||||
.average_bitrate(self.bitrate)
|
||||
.max_bitrate(self.bitrate)
|
||||
.frame_rate_numerator(self.fps)
|
||||
.frame_rate_denominator(1)];
|
||||
let av1_rc = av1::VideoEncodeAV1RateControlInfoKHR {
|
||||
s_type: av1::stype(av1::ST_RATE_CONTROL_INFO),
|
||||
p_next: std::ptr::null(),
|
||||
flags: 0,
|
||||
gop_frame_count: 0,
|
||||
key_frame_period: 0,
|
||||
consecutive_bipredictive_frame_count: 0,
|
||||
temporal_layer_count: 1,
|
||||
};
|
||||
let mut rc = vk::VideoEncodeRateControlInfoKHR::default()
|
||||
.rate_control_mode(vk::VideoEncodeRateControlModeFlagsKHR::CBR)
|
||||
.layers(&rc_layer)
|
||||
.virtual_buffer_size_in_ms(1000)
|
||||
.initial_virtual_buffer_size_in_ms(500);
|
||||
rc.p_next = &av1_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 ----
|
||||
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 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] =
|
||||
if is_idr { &begin_i } else { &begin_p };
|
||||
let mut begin = vk::VideoBeginCodingInfoKHR::default()
|
||||
.video_session(self.session)
|
||||
.video_session_parameters(self.params)
|
||||
.reference_slots(begin_slots);
|
||||
if !self.first_frame {
|
||||
begin.p_next = rc_ptr;
|
||||
}
|
||||
(self.vq_dev.fp().cmd_begin_video_coding_khr)(cmd, &begin);
|
||||
if self.first_frame {
|
||||
let mut ctrl = vk::VideoCodingControlInfoKHR::default().flags(
|
||||
vk::VideoCodingControlFlagsKHR::RESET
|
||||
| vk::VideoCodingControlFlagsKHR::ENCODE_RATE_CONTROL,
|
||||
);
|
||||
ctrl.p_next = rc_ptr;
|
||||
(self.vq_dev.fp().cmd_control_video_coding_khr)(cmd, &ctrl);
|
||||
}
|
||||
dev.cmd_begin_query(cmd, query_pool, 0, vk::QueryControlFlags::empty());
|
||||
let src_res = vk::VideoPictureResourceInfoKHR::default()
|
||||
.coded_extent(ext2d)
|
||||
.image_view_binding(nv12_view);
|
||||
let mut enc = vk::VideoEncodeInfoKHR::default()
|
||||
.dst_buffer(bs_buf)
|
||||
.dst_buffer_offset(0)
|
||||
.dst_buffer_range(self.bs_size)
|
||||
.src_picture_resource(src_res)
|
||||
.setup_reference_slot(&setup_slot);
|
||||
if !is_idr {
|
||||
enc = enc.reference_slots(&enc_refs);
|
||||
}
|
||||
enc.p_next = &av1_pic as *const _ as *const c_void;
|
||||
(self.venc_dev.fp().cmd_encode_video_khr)(cmd, &enc);
|
||||
dev.cmd_end_query(cmd, query_pool, 0);
|
||||
(self.vq_dev.fp().cmd_end_video_coding_khr)(cmd, &vk::VideoEndCodingInfoKHR::default());
|
||||
dev.end_command_buffer(cmd)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read one completed slot's bitstream into an `EncodedFrame`, prepending the header framing:
|
||||
/// HEVC keyframes carry VPS/SPS/PPS; AV1 opens every temporal unit with a TD OBU and prepends the
|
||||
/// sequence-header OBU on keyframes. Caller must have confirmed the slot's fence is signaled.
|
||||
unsafe fn read_slot(&mut self, slot: usize) -> Result<EncodedFrame> {
|
||||
let dev = self.device.clone();
|
||||
let f = &self.frames[slot];
|
||||
@@ -1288,10 +1684,13 @@ impl VulkanVideoEncoder {
|
||||
let (off, len) = (fb[0][0] as usize, fb[0][1] as usize);
|
||||
let p =
|
||||
dev.map_memory(f.bs_mem, 0, vk::WHOLE_SIZE, vk::MemoryMapFlags::empty())? as *const u8;
|
||||
let mut data = Vec::with_capacity(self.header.len() + len);
|
||||
if f.keyframe {
|
||||
data.extend_from_slice(&self.header);
|
||||
} // keyframes carry VPS/SPS/PPS
|
||||
let prefix: &[u8] = if f.keyframe {
|
||||
&self.header
|
||||
} else {
|
||||
&self.frame_prefix
|
||||
};
|
||||
let mut data = Vec::with_capacity(prefix.len() + len);
|
||||
data.extend_from_slice(prefix);
|
||||
data.extend_from_slice(std::slice::from_raw_parts(p.add(off), len));
|
||||
dev.unmap_memory(f.bs_mem);
|
||||
Ok(EncodedFrame {
|
||||
@@ -1791,7 +2190,7 @@ unsafe fn make_frame(
|
||||
|
||||
/// Author VPS/SPS/PPS (Main, level 4.0, low-latency, conformance-window crop) and return the
|
||||
/// session-parameters object + the encoded header bytes (VPS+SPS+PPS NALs) for keyframes.
|
||||
unsafe fn build_parameters(
|
||||
unsafe fn build_parameters_h265(
|
||||
device: &ash::Device,
|
||||
vq_dev: &ash::khr::video_queue::Device,
|
||||
venc_dev: &ash::khr::video_encode_queue::Device,
|
||||
@@ -1906,6 +2305,236 @@ unsafe fn build_parameters(
|
||||
Ok((params, buf))
|
||||
}
|
||||
|
||||
/// AV1 low-overhead OBU bit-writer (MSB-first), used to hand-pack the sequence-header OBU that
|
||||
/// Vulkan AV1 encode (unlike H26x) never emits itself.
|
||||
struct Av1BitWriter {
|
||||
buf: Vec<u8>,
|
||||
cur: u8,
|
||||
fill: u8,
|
||||
}
|
||||
impl Av1BitWriter {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
buf: Vec::new(),
|
||||
cur: 0,
|
||||
fill: 0,
|
||||
}
|
||||
}
|
||||
fn bit(&mut self, b: u32) {
|
||||
self.cur = (self.cur << 1) | (b as u8 & 1);
|
||||
self.fill += 1;
|
||||
if self.fill == 8 {
|
||||
self.buf.push(self.cur);
|
||||
self.cur = 0;
|
||||
self.fill = 0;
|
||||
}
|
||||
}
|
||||
fn put(&mut self, val: u32, bits: u32) {
|
||||
for i in (0..bits).rev() {
|
||||
self.bit((val >> i) & 1);
|
||||
}
|
||||
}
|
||||
/// Flush, zero-padding the final partial byte (OBU size field delimits the payload).
|
||||
fn finish(mut self) -> Vec<u8> {
|
||||
if self.fill > 0 {
|
||||
self.cur <<= 8 - self.fill;
|
||||
self.buf.push(self.cur);
|
||||
}
|
||||
self.buf
|
||||
}
|
||||
}
|
||||
|
||||
/// AV1 leb128 (little-endian base-128) encoding of an OBU size.
|
||||
fn leb128(mut v: u64) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
loop {
|
||||
let mut byte = (v & 0x7f) as u8;
|
||||
v >>= 7;
|
||||
if v != 0 {
|
||||
byte |= 0x80;
|
||||
}
|
||||
out.push(byte);
|
||||
if v == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Bit-pack a `sequence_header_obu` (AV1 spec §5.5) into a size-delimited OBU. The field values here
|
||||
/// MUST mirror the `StdVideoAV1SequenceHeader` handed to the driver in `build_parameters_av1` so the
|
||||
/// driver-emitted frame OBUs parse against this header. Single operating point, 8-bit 4:2:0,
|
||||
/// order-hint on, CDEF+restoration+filter-intra allowed, everything exotic (compound/warp/superres)
|
||||
/// disabled — the profile our single-reference P-frame encoder actually uses.
|
||||
fn av1_sequence_header_obu(
|
||||
sb128: bool,
|
||||
fwb: u32,
|
||||
fhb: u32,
|
||||
max_w_m1: u32,
|
||||
max_h_m1: u32,
|
||||
order_hint_bits_minus_1: u32,
|
||||
seq_level_idx: u32,
|
||||
) -> Vec<u8> {
|
||||
let mut w = Av1BitWriter::new();
|
||||
w.put(0, 3); // seq_profile = MAIN
|
||||
w.bit(0); // still_picture
|
||||
w.bit(0); // reduced_still_picture_header
|
||||
w.bit(0); // timing_info_present_flag
|
||||
w.bit(0); // initial_display_delay_present_flag
|
||||
w.put(0, 5); // operating_points_cnt_minus_1 = 0
|
||||
w.put(0, 12); // operating_point_idc[0]
|
||||
w.put(seq_level_idx, 5); // seq_level_idx[0]
|
||||
if seq_level_idx > 7 {
|
||||
w.bit(0); // seq_tier[0] = 0
|
||||
}
|
||||
w.put(fwb, 4); // frame_width_bits_minus_1
|
||||
w.put(fhb, 4); // frame_height_bits_minus_1
|
||||
w.put(max_w_m1, fwb + 1); // max_frame_width_minus_1
|
||||
w.put(max_h_m1, fhb + 1); // max_frame_height_minus_1
|
||||
w.bit(0); // frame_id_numbers_present_flag
|
||||
w.bit(sb128 as u32); // use_128x128_superblock
|
||||
w.bit(0); // enable_filter_intra
|
||||
w.bit(0); // enable_intra_edge_filter
|
||||
w.bit(0); // enable_interintra_compound
|
||||
w.bit(0); // enable_masked_compound
|
||||
w.bit(0); // enable_warped_motion
|
||||
w.bit(0); // enable_dual_filter
|
||||
w.bit(1); // enable_order_hint
|
||||
w.bit(0); // enable_jnt_comp
|
||||
w.bit(0); // enable_ref_frame_mvs
|
||||
w.bit(1); // seq_choose_screen_content_tools -> seq_force_screen_content_tools = SELECT
|
||||
w.bit(1); // seq_choose_integer_mv -> seq_force_integer_mv = SELECT
|
||||
w.put(order_hint_bits_minus_1, 3); // order_hint_bits_minus_1
|
||||
w.bit(0); // enable_superres
|
||||
w.bit(0); // enable_cdef
|
||||
w.bit(0); // enable_restoration
|
||||
// color_config(): 8-bit 4:2:0, unspecified primaries/transfer/matrix, limited range
|
||||
w.bit(0); // high_bitdepth
|
||||
w.bit(0); // mono_chrome
|
||||
w.bit(0); // color_description_present_flag
|
||||
w.bit(0); // color_range (studio/limited)
|
||||
w.put(0, 2); // chroma_sample_position = CSP_UNKNOWN (subsampling_x==subsampling_y==1 for profile 0)
|
||||
w.bit(0); // separate_uv_delta_q
|
||||
w.bit(0); // film_grain_params_present
|
||||
|
||||
// trailing_bits(): a stop `1` bit then zero-pad to a byte (the size field delimits the OBU, but
|
||||
// the parser still requires the trailing_one_bit — dav1d/cbs reject a plain zero pad).
|
||||
w.bit(1);
|
||||
let payload = w.finish();
|
||||
let mut obu = vec![0x0au8]; // obu_header: type=OBU_SEQUENCE_HEADER(1), has_size_field=1
|
||||
obu.extend_from_slice(&leb128(payload.len() as u64));
|
||||
obu.extend_from_slice(&payload);
|
||||
obu
|
||||
}
|
||||
|
||||
/// AV1 session parameters + header framing. Vulkan AV1 encode emits only the per-frame OBU, so we
|
||||
/// return the app-owned prefixes: a temporal-delimiter OBU that opens every temporal unit
|
||||
/// (`frame_prefix`), and TD + the bit-packed sequence-header OBU for keyframes (`header`).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
unsafe fn build_parameters_av1(
|
||||
device: &ash::Device,
|
||||
vq_dev: &ash::khr::video_queue::Device,
|
||||
session: vk::VideoSessionKHR,
|
||||
w: u32,
|
||||
h: u32,
|
||||
_rw: u32,
|
||||
_rh: u32,
|
||||
max_level: ash::vk::native::StdVideoAV1Level,
|
||||
sb128: bool,
|
||||
) -> Result<(vk::VideoSessionParametersKHR, Vec<u8>, Vec<u8>)> {
|
||||
use super::vk_av1_encode as av1;
|
||||
use ash::vk::native as hh;
|
||||
|
||||
let fwb = 31 - w.leading_zeros(); // av_log2(w): enough bits for max_frame_width_minus_1 = w-1
|
||||
let fhb = 31 - h.leading_zeros();
|
||||
let order_hint_bits_minus_1: u32 = 7; // OrderHintBits = 8
|
||||
let seq_level_idx = max_level; // StdVideoAV1Level's numeric value IS the AV1 seq_level_idx
|
||||
|
||||
// ---- Std sequence header (must match the OBU packed below) ----
|
||||
let mut cc_flags: hh::StdVideoAV1ColorConfigFlags = std::mem::zeroed();
|
||||
let _ = &mut cc_flags; // all zero: mono_chrome/color_range/description/separate_uv_delta_q = 0
|
||||
let mut cc: hh::StdVideoAV1ColorConfig = std::mem::zeroed();
|
||||
cc.flags = cc_flags;
|
||||
cc.BitDepth = 8;
|
||||
cc.subsampling_x = 1;
|
||||
cc.subsampling_y = 1;
|
||||
cc.color_primaries = hh::StdVideoAV1ColorPrimaries_STD_VIDEO_AV1_COLOR_PRIMARIES_BT_UNSPECIFIED;
|
||||
cc.transfer_characteristics =
|
||||
hh::StdVideoAV1TransferCharacteristics_STD_VIDEO_AV1_TRANSFER_CHARACTERISTICS_UNSPECIFIED;
|
||||
cc.matrix_coefficients =
|
||||
hh::StdVideoAV1MatrixCoefficients_STD_VIDEO_AV1_MATRIX_COEFFICIENTS_UNSPECIFIED;
|
||||
cc.chroma_sample_position =
|
||||
hh::StdVideoAV1ChromaSamplePosition_STD_VIDEO_AV1_CHROMA_SAMPLE_POSITION_UNKNOWN;
|
||||
|
||||
// Match FFmpeg's Vulkan AV1 encoder (proven on this RADV/VCN path): the ONLY coding tools
|
||||
// enabled are order-hint and (per caps) 128x128 superblocks. CDEF, loop restoration, filter-
|
||||
// intra, warped/compound motion, superres all OFF — enabling them made VCN emit frame-header
|
||||
// sections whose bit layout our sequence header didn't match, desyncing every inter frame.
|
||||
let mut sh_flags: hh::StdVideoAV1SequenceHeaderFlags = std::mem::zeroed();
|
||||
if sb128 {
|
||||
sh_flags.set_use_128x128_superblock(1);
|
||||
}
|
||||
sh_flags.set_enable_order_hint(1);
|
||||
let mut sh: hh::StdVideoAV1SequenceHeader = std::mem::zeroed();
|
||||
sh.flags = sh_flags;
|
||||
sh.seq_profile = hh::StdVideoAV1Profile_STD_VIDEO_AV1_PROFILE_MAIN;
|
||||
sh.frame_width_bits_minus_1 = fwb as u8;
|
||||
sh.frame_height_bits_minus_1 = fhb as u8;
|
||||
sh.max_frame_width_minus_1 = (w - 1) as u16;
|
||||
sh.max_frame_height_minus_1 = (h - 1) as u16;
|
||||
sh.order_hint_bits_minus_1 = order_hint_bits_minus_1 as u8;
|
||||
sh.seq_force_integer_mv = 2; // SELECT
|
||||
sh.seq_force_screen_content_tools = 2; // SELECT
|
||||
sh.pColorConfig = &cc;
|
||||
|
||||
// ---- single operating point conveying the level/tier the driver targets ----
|
||||
let op = av1::StdVideoEncodeAV1OperatingPointInfo {
|
||||
flags: std::mem::zeroed(),
|
||||
operating_point_idc: 0,
|
||||
seq_level_idx: seq_level_idx as u8,
|
||||
seq_tier: 0,
|
||||
decoder_buffer_delay: 0,
|
||||
encoder_buffer_delay: 0,
|
||||
initial_display_delay_minus_1: 0,
|
||||
};
|
||||
let ops = [op];
|
||||
let av1_spci = av1::VideoEncodeAV1SessionParametersCreateInfoKHR {
|
||||
s_type: av1::stype(av1::ST_SESSION_PARAMETERS_CREATE_INFO),
|
||||
p_next: std::ptr::null(),
|
||||
p_std_sequence_header: &sh,
|
||||
p_std_decoder_model_info: std::ptr::null(),
|
||||
std_operating_point_count: 1,
|
||||
p_std_operating_points: ops.as_ptr() as *const c_void,
|
||||
};
|
||||
let mut ci = vk::VideoSessionParametersCreateInfoKHR::default().video_session(session);
|
||||
ci.p_next = &av1_spci as *const _ as *const c_void;
|
||||
let mut params = vk::VideoSessionParametersKHR::null();
|
||||
let r = (vq_dev.fp().create_video_session_parameters_khr)(
|
||||
device.handle(),
|
||||
&ci,
|
||||
std::ptr::null(),
|
||||
&mut params,
|
||||
);
|
||||
if r != vk::Result::SUCCESS {
|
||||
bail!("create_video_session_parameters (av1): {r:?}");
|
||||
}
|
||||
|
||||
// ---- header framing: TD every temporal unit; TD + seq-header OBU on keyframes ----
|
||||
let td = vec![0x12u8, 0x00]; // temporal_delimiter OBU (type=2, size=0)
|
||||
let seq_obu = av1_sequence_header_obu(
|
||||
sb128,
|
||||
fwb,
|
||||
fhb,
|
||||
w - 1,
|
||||
h - 1,
|
||||
order_hint_bits_minus_1,
|
||||
seq_level_idx,
|
||||
);
|
||||
let mut keyframe_prefix = td.clone();
|
||||
keyframe_prefix.extend_from_slice(&seq_obu);
|
||||
Ok((params, keyframe_prefix, td))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::VulkanVideoEncoder;
|
||||
@@ -1926,17 +2555,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Full `open` → IDR → P-frames → RFI-recovery path through the real [`VulkanVideoEncoder`] on
|
||||
/// whatever Vulkan device is present (RADV on the test bed). Exercises the CPU→NV12 compute CSC,
|
||||
/// the NV12 plane copy, the DPB ring and the reference-slot RFI end-to-end, and dumps the
|
||||
/// elementary stream to `$HOME/vkenc-host-smoke.h265` for an out-of-band `ffmpeg` decode check.
|
||||
/// `#[ignore]`d so it only runs where a real `VK_KHR_video_encode_h265` driver exists — build in
|
||||
/// the distrobox, run on the host:
|
||||
/// cargo test -p punktfunk-host --features vulkan-encode --no-run
|
||||
/// <host> target/debug/deps/punktfunk_host-<hash> --ignored --nocapture vulkan_smoke
|
||||
#[test]
|
||||
#[ignore = "needs a real VK_KHR_video_encode_h265 device (run on the RADV host, not the build box)"]
|
||||
fn vulkan_smoke() {
|
||||
/// 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
|
||||
/// the reference-slot RFI end-to-end; returns the AUs. Loss of wire frame 3 is simulated so frame
|
||||
/// 4 becomes a clean recovery anchor referencing frame 2 (no IDR).
|
||||
fn run_smoke(codec: Codec) -> Vec<crate::encode::EncodedFrame> {
|
||||
let env_dim = |k: &str, d: u32| {
|
||||
std::env::var(k)
|
||||
.ok()
|
||||
@@ -1944,7 +2567,7 @@ mod tests {
|
||||
.unwrap_or(d)
|
||||
};
|
||||
let (w, h) = (env_dim("PF_SMOKE_W", 256), env_dim("PF_SMOKE_H", 256));
|
||||
let mut enc = VulkanVideoEncoder::open(Codec::H265, w, h, 60, 10_000_000).expect("open");
|
||||
let mut enc = VulkanVideoEncoder::open(codec, w, h, 60, 10_000_000).expect("open");
|
||||
assert!(enc.caps().supports_rfi, "must advertise RFI");
|
||||
|
||||
let colors = [
|
||||
@@ -1995,28 +2618,50 @@ mod tests {
|
||||
}
|
||||
assert_eq!(keyframes, 1, "exactly one IDR (frame 0)");
|
||||
assert_eq!(anchors, 1, "exactly one recovery anchor (frame 4)");
|
||||
aus
|
||||
}
|
||||
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
/// Dump the full stream + a "frame-3-lost" stream to `$HOME/vkenc-host-smoke*.{ext}` for an
|
||||
/// out-of-band `ffmpeg` decode check (both must decode 0-error; the dropped one proves the
|
||||
/// recovery anchor healed real loss without an IDR).
|
||||
fn dump_smoke(aus: &[crate::encode::EncodedFrame], ext: &str) {
|
||||
let Ok(home) = std::env::var("HOME") else {
|
||||
return;
|
||||
};
|
||||
let full: Vec<u8> = aus.iter().flat_map(|a| a.data.iter().copied()).collect();
|
||||
let p1 = format!("{home}/vkenc-host-smoke.h265");
|
||||
let p1 = format!("{home}/vkenc-host-smoke.{ext}");
|
||||
let _ = std::fs::write(&p1, &full);
|
||||
eprintln!(
|
||||
"vulkan_smoke: wrote {p1} ({} bytes, {} AUs)",
|
||||
"run_smoke: wrote {p1} ({} bytes, {} AUs)",
|
||||
full.len(),
|
||||
aus.len()
|
||||
);
|
||||
// Drop the "lost" AU (wire frame 3). Because the recovery frame re-anchored to frame 2,
|
||||
// this must still decode cleanly — the on-host proof that the ported RFI heals real loss
|
||||
// without an IDR (a non-RFI encoder's frame 4 would reference frame 3 → decoder corrupts).
|
||||
let dropped: Vec<u8> = aus
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| *i != 3)
|
||||
.flat_map(|(_, a)| a.data.iter().copied())
|
||||
.collect();
|
||||
let p2 = format!("{home}/vkenc-host-smoke-dropped.h265");
|
||||
let p2 = format!("{home}/vkenc-host-smoke-dropped.{ext}");
|
||||
let _ = std::fs::write(&p2, &dropped);
|
||||
eprintln!("vulkan_smoke: wrote {p2} (frame 3 dropped; recovery@4 anchors to frame 2)");
|
||||
}
|
||||
eprintln!("run_smoke: wrote {p2} (frame 3 dropped; recovery@4 anchors to frame 2)");
|
||||
}
|
||||
|
||||
/// HEVC smoke. `#[ignore]`d so it only runs where a real `VK_KHR_video_encode_h265` driver exists
|
||||
/// — build in the distrobox, run on the host:
|
||||
/// cargo test -p punktfunk-host --features vulkan-encode --no-run
|
||||
/// <host> target/debug/deps/punktfunk_host-<hash> --ignored --nocapture vulkan_smoke
|
||||
#[test]
|
||||
#[ignore = "needs a real VK_KHR_video_encode_h265 device (run on the RADV host, not the build box)"]
|
||||
fn vulkan_smoke() {
|
||||
dump_smoke(&run_smoke(Codec::H265), "h265");
|
||||
}
|
||||
|
||||
/// AV1 smoke — same path over `VK_KHR_video_encode_av1`. Dumps `.obu` (low-overhead OBU stream:
|
||||
/// our TD + seq-header prefixes ahead of each Vulkan-emitted frame OBU) for `ffmpeg` to decode.
|
||||
#[test]
|
||||
#[ignore = "needs a real VK_KHR_video_encode_av1 device (run on the RADV host, not the build box)"]
|
||||
fn vulkan_smoke_av1() {
|
||||
dump_smoke(&run_smoke(Codec::Av1), "obu");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user