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.
|
// 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>> {
|
||||||
#[cfg(feature = "vulkan-encode")]
|
#[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)
|
match vulkan_video::VulkanVideoEncoder::open(codec, width, height, fps, bitrate_bps)
|
||||||
{
|
{
|
||||||
Ok(e) => {
|
Ok(e) => {
|
||||||
tracing::info!(
|
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"
|
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>);
|
||||||
@@ -581,9 +582,9 @@ fn open_video_backend(
|
|||||||
"vulkan" | "vulkan-video" => {
|
"vulkan" | "vulkan-video" => {
|
||||||
#[cfg(feature = "vulkan-encode")]
|
#[cfg(feature = "vulkan-encode")]
|
||||||
{
|
{
|
||||||
if codec != Codec::H265 {
|
if !matches!(codec, Codec::H265 | Codec::Av1) {
|
||||||
anyhow::bail!(
|
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)
|
vulkan_video::VulkanVideoEncoder::open(codec, width, height, fps, bitrate_bps)
|
||||||
@@ -1251,6 +1252,13 @@ mod vaapi;
|
|||||||
#[cfg(all(target_os = "linux", feature = "vulkan-encode"))]
|
#[cfg(all(target_os = "linux", feature = "vulkan-encode"))]
|
||||||
#[path = "encode/linux/vulkan_video.rs"]
|
#[path = "encode/linux/vulkan_video.rs"]
|
||||||
mod vulkan_video;
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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
|
//! Raw **Vulkan Video** HEVC + AV1 encoder (`VK_KHR_video_encode_h265` / `_av1`) with true
|
||||||
//! invalidation — the open-stack AMD/Intel-Linux twin of the direct-NVENC RFI path. The app owns
|
//! reference-frame invalidation — the open-stack AMD/Intel-Linux twin of the direct-NVENC RFI path.
|
||||||
//! the DPB, so loss recovery is a clean P-frame that re-references a known-good older slot (no IDR).
|
//! 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
|
//! 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`.
|
//! 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)]
|
#![allow(clippy::too_many_arguments)]
|
||||||
|
|
||||||
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
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;
|
/// 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).
|
/// backpressure kicks in at the 2nd unread frame. Distinct from `DPB_SLOTS` (reference pool).
|
||||||
const RING_DEFAULT: usize = 2;
|
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`.
|
/// Resolve the in-flight ring depth: `PUNKTFUNK_VULKAN_INFLIGHT` (clamped 2..=6), else `RING_DEFAULT`.
|
||||||
fn ring_depth() -> usize {
|
fn ring_depth() -> usize {
|
||||||
@@ -97,11 +103,18 @@ pub struct VulkanVideoEncoder {
|
|||||||
compute_family: u32,
|
compute_family: u32,
|
||||||
mem_props: vk::PhysicalDeviceMemoryProperties,
|
mem_props: vk::PhysicalDeviceMemoryProperties,
|
||||||
|
|
||||||
|
// --- codec ---
|
||||||
|
codec: Codec, // H265 or Av1 — selects the Std-struct authoring + header framing
|
||||||
|
|
||||||
// --- video session ---
|
// --- video session ---
|
||||||
session: vk::VideoSessionKHR,
|
session: vk::VideoSessionKHR,
|
||||||
session_mem: Vec<vk::DeviceMemory>,
|
session_mem: Vec<vk::DeviceMemory>,
|
||||||
params: vk::VideoSessionParametersKHR,
|
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 ---
|
||||||
dpb_image: vk::Image,
|
dpb_image: vk::Image,
|
||||||
@@ -137,7 +150,9 @@ pub struct VulkanVideoEncoder {
|
|||||||
// --- state ---
|
// --- state ---
|
||||||
width: u32,
|
width: u32,
|
||||||
height: 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
|
enc_count: u64, // total frames encoded — drives the DPB ring cursor
|
||||||
auto_wire: i64, // fallback wire index when submit() (not submit_indexed) is used
|
auto_wire: i64, // fallback wire index when submit() (not submit_indexed) is used
|
||||||
first_frame: bool, // needs RESET + DPB layout transition + CBR install + IDR
|
first_frame: bool, // needs RESET + DPB layout transition + CBR install + IDR
|
||||||
@@ -153,20 +168,45 @@ unsafe impl Send for VulkanVideoEncoder {}
|
|||||||
impl VulkanVideoEncoder {
|
impl VulkanVideoEncoder {
|
||||||
/// Signature mirrors the other Linux backends' `open` (see `nvenc_cuda::NvencCudaEncoder::open`).
|
/// 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> {
|
pub fn open(codec: Codec, width: u32, height: u32, fps: u32, bitrate_bps: u64) -> Result<Self> {
|
||||||
if codec != Codec::H265 {
|
if !matches!(codec, Codec::H265 | Codec::Av1) {
|
||||||
bail!("vulkan-encode backend is HEVC-only (got {codec:?})");
|
bail!("vulkan-encode backend supports HEVC + AV1 only (got {codec:?})");
|
||||||
}
|
}
|
||||||
// align coded extent to the encode granularity (64x16 on RADV); a conformance window
|
// align coded extent to the encode granularity (64x16 on RADV). HEVC crops the padding back
|
||||||
// crops the aligned padding back to (width,height) on the decoder.
|
// to (width,height) via a conformance window; AV1 signals it via render_size (see build).
|
||||||
let w = (width + 63) & !63;
|
let w = (width + 63) & !63;
|
||||||
let h = (height + 15) & !15;
|
let h = (height + 15) & !15;
|
||||||
// SAFETY: `open_inner` only issues Vulkan calls whose preconditions it establishes itself
|
// SAFETY: `open_inner` only issues Vulkan calls whose preconditions it establishes itself
|
||||||
// (valid instance/device, correctly-chained create-infos); all handles are freshly created
|
// (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.
|
// 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 entry = ash::Entry::load().context("load vulkan loader")?;
|
||||||
let app = vk::ApplicationInfo::default().api_version(vk::API_VERSION_1_3);
|
let app = vk::ApplicationInfo::default().api_version(vk::API_VERSION_1_3);
|
||||||
let instance = entry
|
let instance = entry
|
||||||
@@ -194,9 +234,7 @@ impl VulkanVideoEncoder {
|
|||||||
.queue_family_properties
|
.queue_family_properties
|
||||||
.queue_flags
|
.queue_flags
|
||||||
.contains(vk::QueueFlags::VIDEO_ENCODE_KHR)
|
.contains(vk::QueueFlags::VIDEO_ENCODE_KHR)
|
||||||
&& video[i]
|
&& video[i].video_codec_operations.contains(codec_op)
|
||||||
.video_codec_operations
|
|
||||||
.contains(vk::VideoCodecOperationFlagsKHR::ENCODE_H265)
|
|
||||||
{
|
{
|
||||||
found = Some((pd, i as u32));
|
found = Some((pd, i as u32));
|
||||||
break;
|
break;
|
||||||
@@ -206,7 +244,7 @@ impl VulkanVideoEncoder {
|
|||||||
break;
|
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);
|
let mem_props = instance.get_physical_device_memory_properties(pd);
|
||||||
|
|
||||||
@@ -218,38 +256,60 @@ impl VulkanVideoEncoder {
|
|||||||
.context("no compute queue")? as u32
|
.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()
|
let mut h265_profile = vk::VideoEncodeH265ProfileInfoKHR::default()
|
||||||
.std_profile_idc(vk::native::StdVideoH265ProfileIdc_STD_VIDEO_H265_PROFILE_IDC_MAIN);
|
.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()
|
let mut usage = vk::VideoEncodeUsageInfoKHR::default()
|
||||||
.video_usage_hints(vk::VideoEncodeUsageFlagsKHR::STREAMING)
|
.video_usage_hints(vk::VideoEncodeUsageFlagsKHR::STREAMING)
|
||||||
.video_content_hints(vk::VideoEncodeContentFlagsKHR::RENDERED)
|
.video_content_hints(vk::VideoEncodeContentFlagsKHR::RENDERED)
|
||||||
.tuning_mode(vk::VideoEncodeTuningModeKHR::ULTRA_LOW_LATENCY);
|
.tuning_mode(vk::VideoEncodeTuningModeKHR::ULTRA_LOW_LATENCY);
|
||||||
let profile = vk::VideoProfileInfoKHR::default()
|
let mut profile = vk::VideoProfileInfoKHR::default()
|
||||||
.video_codec_operation(vk::VideoCodecOperationFlagsKHR::ENCODE_H265)
|
.video_codec_operation(codec_op)
|
||||||
.chroma_subsampling(vk::VideoChromaSubsamplingFlagsKHR::TYPE_420)
|
.chroma_subsampling(vk::VideoChromaSubsamplingFlagsKHR::TYPE_420)
|
||||||
.luma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::TYPE_8)
|
.luma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::TYPE_8)
|
||||||
.chroma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::TYPE_8)
|
.chroma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::TYPE_8)
|
||||||
.push_next(&mut h265_profile)
|
|
||||||
.push_next(&mut usage);
|
.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 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 enc_caps = vk::VideoEncodeCapabilitiesKHR::default();
|
||||||
let mut caps = vk::VideoCapabilitiesKHR::default()
|
let mut caps = vk::VideoCapabilitiesKHR::default().push_next(&mut enc_caps);
|
||||||
.push_next(&mut enc_caps)
|
if av1 {
|
||||||
.push_next(&mut h265_caps);
|
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);
|
let r = (vq_inst.fp().get_physical_device_video_capabilities_khr)(pd, &profile, &mut caps);
|
||||||
if r != vk::Result::SUCCESS {
|
if r != vk::Result::SUCCESS {
|
||||||
bail!("get_physical_device_video_capabilities: {r:?}");
|
bail!("get_physical_device_video_capabilities: {r:?}");
|
||||||
}
|
}
|
||||||
let std_hdr = caps.std_header_version;
|
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 = [
|
let dev_exts = [
|
||||||
ash::khr::video_queue::NAME.as_ptr(),
|
ash::khr::video_queue::NAME.as_ptr(),
|
||||||
ash::khr::video_encode_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::khr::external_memory_fd::NAME.as_ptr(),
|
||||||
ash::ext::external_memory_dma_buf::NAME.as_ptr(),
|
ash::ext::external_memory_dma_buf::NAME.as_ptr(),
|
||||||
ash::ext::image_drm_format_modifier::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 vq_dev = ash::khr::video_queue::Device::new(&instance, &device);
|
||||||
let venc_dev = ash::khr::video_encode_queue::Device::new(&instance, &device);
|
let venc_dev = ash::khr::video_encode_queue::Device::new(&instance, &device);
|
||||||
|
|
||||||
// ---- video session ----
|
// ---- video session ---- (AV1 pins the max level from caps via a chained create-info)
|
||||||
let session_ci = vk::VideoSessionCreateInfoKHR::default()
|
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)
|
.queue_family_index(encode_family)
|
||||||
.video_profile(&profile)
|
.video_profile(&profile)
|
||||||
.picture_format(NV12)
|
.picture_format(NV12)
|
||||||
@@ -296,6 +362,9 @@ impl VulkanVideoEncoder {
|
|||||||
.max_dpb_slots(DPB_SLOTS + 1)
|
.max_dpb_slots(DPB_SLOTS + 1)
|
||||||
.max_active_reference_pictures(1)
|
.max_active_reference_pictures(1)
|
||||||
.std_header_version(&std_hdr);
|
.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 mut session = vk::VideoSessionKHR::null();
|
||||||
let r = (vq_dev.fp().create_video_session_khr)(
|
let r = (vq_dev.fp().create_video_session_khr)(
|
||||||
device.handle(),
|
device.handle(),
|
||||||
@@ -346,9 +415,25 @@ impl VulkanVideoEncoder {
|
|||||||
bail!("bind_video_session_memory: {r:?}");
|
bail!("bind_video_session_memory: {r:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- session parameters (VPS/SPS/PPS) + retrieve header ----
|
// ---- session parameters + header framing (HEVC: VPS/SPS/PPS on keyframes; AV1: a
|
||||||
let (params, header) =
|
// temporal-delimiter OBU per frame + a sequence-header OBU on keyframes) ----
|
||||||
build_parameters(&device, &vq_dev, &venc_dev, session, w, h, rw, rh)?;
|
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 ----
|
// ---- DPB image (NV12 OPTIMAL, ring of slots) — encode queue only ----
|
||||||
let mut profile_list =
|
let mut profile_list =
|
||||||
@@ -488,10 +573,12 @@ impl VulkanVideoEncoder {
|
|||||||
compute_queue,
|
compute_queue,
|
||||||
compute_family,
|
compute_family,
|
||||||
mem_props,
|
mem_props,
|
||||||
|
codec,
|
||||||
session,
|
session,
|
||||||
session_mem,
|
session_mem,
|
||||||
params,
|
params,
|
||||||
header,
|
header,
|
||||||
|
frame_prefix,
|
||||||
dpb_image,
|
dpb_image,
|
||||||
dpb_mem,
|
dpb_mem,
|
||||||
dpb_views,
|
dpb_views,
|
||||||
@@ -514,6 +601,8 @@ impl VulkanVideoEncoder {
|
|||||||
fps,
|
fps,
|
||||||
width: w,
|
width: w,
|
||||||
height: h,
|
height: h,
|
||||||
|
render_w: rw,
|
||||||
|
render_h: rh,
|
||||||
poc: 0,
|
poc: 0,
|
||||||
enc_count: 0,
|
enc_count: 0,
|
||||||
auto_wire: 0,
|
auto_wire: 0,
|
||||||
@@ -1010,7 +1099,14 @@ impl VulkanVideoEncoder {
|
|||||||
);
|
);
|
||||||
dev.end_command_buffer(compute_cmd)?;
|
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();
|
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 {
|
||||||
@@ -1233,6 +1329,7 @@ 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).
|
// ---- 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
|
// Non-blocking: `fence` is polled later so this frame's CSC+encode overlaps the next
|
||||||
@@ -1278,8 +1375,307 @@ impl VulkanVideoEncoder {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read one completed slot's bitstream into an `EncodedFrame` (VPS/SPS/PPS prepended on
|
/// Author the AV1 Std structs + record begin/encode/end for one frame into `cmd`. Self-contained
|
||||||
/// keyframes). Caller must have confirmed the slot's fence is signaled (blocking wait or probe).
|
/// (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> {
|
unsafe fn read_slot(&mut self, slot: usize) -> Result<EncodedFrame> {
|
||||||
let dev = self.device.clone();
|
let dev = self.device.clone();
|
||||||
let f = &self.frames[slot];
|
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 (off, len) = (fb[0][0] as usize, fb[0][1] as usize);
|
||||||
let p =
|
let p =
|
||||||
dev.map_memory(f.bs_mem, 0, vk::WHOLE_SIZE, vk::MemoryMapFlags::empty())? as *const u8;
|
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);
|
let prefix: &[u8] = if f.keyframe {
|
||||||
if f.keyframe {
|
&self.header
|
||||||
data.extend_from_slice(&self.header);
|
} else {
|
||||||
} // keyframes carry VPS/SPS/PPS
|
&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));
|
data.extend_from_slice(std::slice::from_raw_parts(p.add(off), len));
|
||||||
dev.unmap_memory(f.bs_mem);
|
dev.unmap_memory(f.bs_mem);
|
||||||
Ok(EncodedFrame {
|
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
|
/// 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.
|
/// 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,
|
device: &ash::Device,
|
||||||
vq_dev: &ash::khr::video_queue::Device,
|
vq_dev: &ash::khr::video_queue::Device,
|
||||||
venc_dev: &ash::khr::video_encode_queue::Device,
|
venc_dev: &ash::khr::video_encode_queue::Device,
|
||||||
@@ -1906,6 +2305,236 @@ unsafe fn build_parameters(
|
|||||||
Ok((params, buf))
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::VulkanVideoEncoder;
|
use super::VulkanVideoEncoder;
|
||||||
@@ -1926,17 +2555,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Full `open` → IDR → P-frames → RFI-recovery path through the real [`VulkanVideoEncoder`] on
|
/// Full `open` → IDR → P-frames → RFI-recovery path through the real [`VulkanVideoEncoder`],
|
||||||
/// whatever Vulkan device is present (RADV on the test bed). Exercises the CPU→NV12 compute CSC,
|
/// codec-parameterized. Exercises the CPU→NV12 compute CSC, the NV12 plane copy, the DPB ring and
|
||||||
/// the NV12 plane copy, the DPB ring and the reference-slot RFI end-to-end, and dumps the
|
/// the reference-slot RFI end-to-end; returns the AUs. Loss of wire frame 3 is simulated so frame
|
||||||
/// elementary stream to `$HOME/vkenc-host-smoke.h265` for an out-of-band `ffmpeg` decode check.
|
/// 4 becomes a clean recovery anchor referencing frame 2 (no IDR).
|
||||||
/// `#[ignore]`d so it only runs where a real `VK_KHR_video_encode_h265` driver exists — build in
|
fn run_smoke(codec: Codec) -> Vec<crate::encode::EncodedFrame> {
|
||||||
/// 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() {
|
|
||||||
let env_dim = |k: &str, d: u32| {
|
let env_dim = |k: &str, d: u32| {
|
||||||
std::env::var(k)
|
std::env::var(k)
|
||||||
.ok()
|
.ok()
|
||||||
@@ -1944,7 +2567,7 @@ mod tests {
|
|||||||
.unwrap_or(d)
|
.unwrap_or(d)
|
||||||
};
|
};
|
||||||
let (w, h) = (env_dim("PF_SMOKE_W", 256), env_dim("PF_SMOKE_H", 256));
|
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");
|
assert!(enc.caps().supports_rfi, "must advertise RFI");
|
||||||
|
|
||||||
let colors = [
|
let colors = [
|
||||||
@@ -1995,28 +2618,50 @@ mod tests {
|
|||||||
}
|
}
|
||||||
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 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 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);
|
let _ = std::fs::write(&p1, &full);
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"vulkan_smoke: wrote {p1} ({} bytes, {} AUs)",
|
"run_smoke: wrote {p1} ({} bytes, {} AUs)",
|
||||||
full.len(),
|
full.len(),
|
||||||
aus.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
|
let dropped: Vec<u8> = aus
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(i, _)| *i != 3)
|
.filter(|(i, _)| *i != 3)
|
||||||
.flat_map(|(_, a)| a.data.iter().copied())
|
.flat_map(|(_, a)| a.data.iter().copied())
|
||||||
.collect();
|
.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);
|
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