//! 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/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}; use crate::encode::{Codec, EncodedFrame, Encoder, EncoderCaps}; use anyhow::{bail, Context, Result}; use ash::vk; use std::collections::VecDeque; use std::ffi::c_void; use std::os::fd::{AsRawFd, IntoRawFd}; const NV12: vk::Format = vk::Format::G8_B8R8_2PLANE_420_UNORM; /// Max resident dmabuf imports (comfortably above any PipeWire pool depth; imports alias existing /// buffers so this holds handles, not new allocations). const IMPORT_CACHE_CAP: usize = 16; // Prebuilt SPIR-V for the RGB→NV12 BT.709 compute CSC. Source is `rgb2yuv.comp` beside this file; // regenerate with `glslangValidator -V rgb2yuv.comp -o rgb2yuv.spv` after editing the shader. const CSC_SPV: &[u8] = include_bytes!("rgb2yuv.spv"); /// DPB ring depth (well under the RADV `maxDpbSlots=17`); also the RFI recovery window. const DPB_SLOTS: u32 = 8; /// In-flight frame ring: how many captures may have GPU work outstanding at once. 2 overlaps a /// frame's CSC+encode with the next capture (the throughput win) at the lowest possible added /// 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 { std::env::var("PUNKTFUNK_VULKAN_INFLIGHT") .ok() .and_then(|v| v.trim().parse::().ok()) .map(|n| n.clamp(2, 6)) .unwrap_or(RING_DEFAULT) } /// Newest resident DPB slot whose wire index is strictly older than the loss — the clean anchor. fn pick_recovery_slot(slot_wire: &[i64], loss_first: i64) -> Option { let mut best: Option = None; let mut best_wire = -1i64; for (i, &w) in slot_wire.iter().enumerate() { if w >= 0 && w < loss_first && w > best_wire { best = Some(i); best_wire = w; } } best } /// The S0 (past-reference) half of an HEVC short-term RPS that **retains every resident DPB /// picture**, not just the one this frame predicts from. The RPS is the decoder's only retention /// signal (HEVC 8.3.2: any DPB picture absent from the current RPS is marked "unused for /// reference" and reclaimed) — an RPS naming only the active reference lets a conforming decoder /// evict the rest, and the RFI recovery anchor then references a picture the client already /// discarded: FFmpeg's HEVC parser (the Linux VAAPI/Vulkan and Windows D3D11VA clients) conceals /// with a generated gray reference and every following frame chains off the corruption — exactly /// at the moment the anchor claims the picture is clean. Listing all residents (with /// `used_by_curr_pic` set only for the real reference) keeps the host and client DPBs in lockstep, /// so any slot [`pick_recovery_slot`] can pick is decodable by construction. /// /// `setup_idx` — the slot this frame reconstructs into — is excluded: its old occupant dies with /// this frame on the host, so the decoder must drop it too (also keeping the retained count at /// `DPB_SLOTS - 1` + the current picture = the SPS `max_dec_pic_buffering` budget). /// /// Returns `(num_negative_pics, delta_poc_s0_minus1, used_by_curr_pic_s0_flag)`. fn build_h265_rps_s0( slot_poc: &[i32], setup_idx: usize, ref_poc: i32, cur_poc: i32, ) -> (u8, [u16; 16], u16) { // Residents, newest first — S0 is ordered by descending POC (ascending delta from `cur_poc`). let mut pocs: Vec = slot_poc .iter() .enumerate() .filter(|&(s, &p)| s != setup_idx && p >= 0 && p < cur_poc) .map(|(_, &p)| p) .collect(); pocs.sort_unstable_by(|a, b| b.cmp(a)); pocs.truncate(16); // delta_poc_s0_minus1 capacity (STD_VIDEO_H265_MAX_DPB_SIZE) let mut deltas = [0u16; 16]; let mut used = 0u16; let mut prev = cur_poc; for (i, &p) in pocs.iter().enumerate() { // delta_poc_s0_minus1[i] codes the gap to the PREVIOUS S0 entry (the spec's cumulative // DeltaPocS0 chain), not to the current picture. deltas[i] = (prev - p - 1) as u16; if p == ref_poc { used |= 1 << i; } prev = p; } (pocs.len() as u8, deltas, used) } /// One in-flight frame's private GPU resources. The encoder keeps a small ring of these so a /// frame's GPU work (CSC + encode) overlaps the CPU capturing and submitting the next one: /// `submit()` records into a free slot and returns without blocking; `poll()` reads back the /// oldest slot once its `fence` signals. Everything here is written by one frame and read by the /// next-but-K, so it cannot be shared while a submission is outstanding. struct Frame { compute_cmd: vk::CommandBuffer, // CSC (compute+transfer) cmd: vk::CommandBuffer, // encode queue csc_sem: vk::Semaphore, // compute -> encode ordering (this frame only) fence: vk::Fence, // signaled when this frame's encode completes query_pool: vk::QueryPool, // bitstream offset/bytes feedback bs_buf: vk::Buffer, bs_mem: vk::DeviceMemory, csc_set: vk::DescriptorSet, // Y/UV bindings fixed; binding 0 (RGB) rewritten each use y_img: vk::Image, y_mem: vk::DeviceMemory, y_view: vk::ImageView, uv_img: vk::Image, uv_mem: vk::DeviceMemory, uv_view: vk::ImageView, nv12_src: vk::Image, nv12_mem: vk::DeviceMemory, nv12_view: vk::ImageView, // CPU-input staging (lazily sized; only the software-capture / smoke-test path uses it). cpu_img: Option<(vk::Image, vk::DeviceMemory, vk::ImageView, vk::Format)>, cpu_stage: Option<(vk::Buffer, vk::DeviceMemory, u64)>, // Frame metadata, set at submit and read back at poll (valid only while this slot is in flight). pts_ns: u64, keyframe: bool, recovery_anchor: bool, } pub struct VulkanVideoEncoder { // --- vulkan core (owned) --- _entry: ash::Entry, instance: ash::Instance, device: ash::Device, ext_fd: ash::khr::external_memory_fd::Device, vq_dev: ash::khr::video_queue::Device, venc_dev: ash::khr::video_encode_queue::Device, encode_queue: vk::Queue, compute_queue: vk::Queue, 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, params: vk::VideoSessionParametersKHR, // Keyframe prefix: HEVC = VPS/SPS/PPS; AV1 = temporal-delimiter OBU + sequence-header OBU. header: Vec, // 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, // --- DPB --- dpb_image: vk::Image, dpb_mem: vk::DeviceMemory, dpb_views: Vec, slot_wire: Vec, // wire index held per slot (-1 = empty) — RFI/loss domain slot_poc: Vec, // HEVC POC held per slot — reference-delta domain prev_slot: usize, // --- CSC (RGB -> NV12), shared across the frame ring --- csc_pipe: vk::Pipeline, csc_layout: vk::PipelineLayout, csc_dsl: vk::DescriptorSetLayout, csc_pool: vk::DescriptorPool, sampler: vk::Sampler, // Per-buffer dmabuf-import cache, keyed by (st_dev, st_ino) — PipeWire cycles a small fixed pool, // so each underlying buffer is imported ONCE and reused (no per-frame VkImage create/import/destroy). // Imports are read-only per frame, so the ring shares them (concurrent frames read distinct buffers). import_cache: Vec<(u64, u64, vk::Image, vk::DeviceMemory, vk::ImageView)>, // --- in-flight frame ring (pipelining) --- frames: Vec, // per-slot private resources ring: usize, // next slot to record into (round-robin over `frames`) in_flight: VecDeque, // slots submitted but not yet read back, oldest first bs_size: u64, cmd_pool: vk::CommandPool, compute_pool: vk::CommandPool, // --- rate control (CBR), rebuilt-safe --- bitrate: u64, fps: u32, /// A [`reconfigure_bitrate`](Encoder::reconfigure_bitrate) rate not yet installed in the video /// session. The next `record_submit` emits an `ENCODE_RATE_CONTROL` control command carrying it /// (mid-stream) or folds it into the first frame's RESET+RC install, then promotes it into /// `bitrate` — which must keep naming the session's CURRENT state, because every begin-coding /// declares it (the spec requires the declared state to match). pending_bitrate: Option, // --- state --- width: u32, height: u32, 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 force_kf: bool, // request_keyframe / non-recoverable loss -> next frame is IDR pending_loss: Option, // invalidate_ref_frames(first) -> recover on next frame pending: VecDeque, } // SAFETY: the encoder is used only from the single encode thread; all Vulkan handles are owned and // never shared. Matches `NvencCudaEncoder`'s `unsafe impl Send`. 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 { 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). 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( codec, w, h, width, height, fps.max(1), bitrate_bps.max(1_000_000), ) } } unsafe fn open_inner( codec: Codec, w: u32, h: u32, rw: u32, rh: u32, fps: u32, bitrate: u64, ) -> Result { 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 .create_instance( &vk::InstanceCreateInfo::default().application_info(&app), None, ) .context("create instance")?; let vq_inst = ash::khr::video_queue::Instance::new(&entry, &instance); // pick the physical device + encode queue family (skip llvmpipe) let (pd, encode_family) = { let mut found = None; for pd in instance.enumerate_physical_devices()? { let qf_len = instance.get_physical_device_queue_family_properties2_len(pd); let mut video = vec![vk::QueueFamilyVideoPropertiesKHR::default(); qf_len]; let mut qf = vec![vk::QueueFamilyProperties2::default(); qf_len]; for i in 0..qf_len { qf[i].p_next = &mut video[i] as *mut _ as *mut c_void; } instance.get_physical_device_queue_family_properties2(pd, &mut qf); for i in 0..qf_len { if qf[i] .queue_family_properties .queue_flags .contains(vk::QueueFlags::VIDEO_ENCODE_KHR) && video[i].video_codec_operations.contains(codec_op) { found = Some((pd, i as u32)); break; } } if found.is_some() { break; } } 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); // a compute queue family for the CSC (usually family 0) let compute_family = { let qf = instance.get_physical_device_queue_family_properties(pd); qf.iter() .position(|q| q.queue_flags.contains(vk::QueueFlags::COMPUTE)) .context("no compute queue")? as u32 }; // 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 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 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 (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); 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 (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(), 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(), ]; let prio = [1.0f32]; let mut qcis = vec![vk::DeviceQueueCreateInfo::default() .queue_family_index(encode_family) .queue_priorities(&prio)]; if compute_family != encode_family { qcis.push( vk::DeviceQueueCreateInfo::default() .queue_family_index(compute_family) .queue_priorities(&prio), ); } let mut sync2 = vk::PhysicalDeviceSynchronization2Features::default().synchronization2(true); let mut device_ci = vk::DeviceCreateInfo::default() .queue_create_infos(&qcis) .enabled_extension_names(&dev_exts) .push_next(&mut sync2); // The AV1-encode feature gate: `videoEncodeAV1` must be enabled for any ENCODE_AV1 use // (spec requirement; vendored struct since ash 0.38 predates it — chained raw like the // profile above). let mut av1_features = av1b::PhysicalDeviceVideoEncodeAV1FeaturesKHR { s_type: av1b::stype(av1b::ST_PHYSICAL_DEVICE_FEATURES), p_next: std::ptr::null_mut(), video_encode_av1: vk::TRUE, }; if av1 { av1_features.p_next = device_ci.p_next as *mut c_void; device_ci.p_next = &av1_features as *const _ as *const c_void; } let device = instance .create_device(pd, &device_ci, None) .context("create device")?; let encode_queue = device.get_device_queue(encode_family, 0); let compute_queue = device.get_device_queue(compute_family, 0); let ext_fd = ash::khr::external_memory_fd::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); // ---- 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) .max_coded_extent(vk::Extent2D { width: w, height: h, }) .reference_picture_format(NV12) .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(), &session_ci, std::ptr::null(), &mut session, ); if r != vk::Result::SUCCESS { bail!("create_video_session: {r:?}"); } // bind session memory let get_mem = vq_dev.fp().get_video_session_memory_requirements_khr; let mut n = 0u32; let _ = get_mem(device.handle(), session, &mut n, std::ptr::null_mut()); let mut reqs = vec![vk::VideoSessionMemoryRequirementsKHR::default(); n as usize]; let _ = get_mem(device.handle(), session, &mut n, reqs.as_mut_ptr()); let mut session_mem = Vec::new(); let mut binds = Vec::new(); for rq in &reqs { let mr = rq.memory_requirements; let ti = find_mem( &mem_props, mr.memory_type_bits, vk::MemoryPropertyFlags::DEVICE_LOCAL, ); let m = device.allocate_memory( &vk::MemoryAllocateInfo::default() .allocation_size(mr.size) .memory_type_index(ti), None, )?; session_mem.push(m); binds.push( vk::BindVideoSessionMemoryInfoKHR::default() .memory_bind_index(rq.memory_bind_index) .memory(m) .memory_offset(0) .memory_size(mr.size), ); } let r = (vq_dev.fp().bind_video_session_memory_khr)( device.handle(), session, binds.len() as u32, binds.as_ptr(), ); if r != vk::Result::SUCCESS { bail!("bind_video_session_memory: {r:?}"); } // ---- 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 = vk::VideoProfileListInfoKHR::default().profiles(std::slice::from_ref(&profile)); let (dpb_image, dpb_mem) = make_video_image( &device, &mem_props, NV12, w, h, DPB_SLOTS, vk::ImageUsageFlags::VIDEO_ENCODE_DPB_KHR, &mut profile_list, &[], )?; let dpb_views: Vec = (0..DPB_SLOTS) .map(|slot| make_view(&device, dpb_image, NV12, slot)) .collect::>()?; // NV12 encode-src, CSC scratch (Y/UV), bitstream, query and command buffers are all per // in-flight frame (built in `make_frame` below); only the queue-family list is shared here. let fams = if compute_family == encode_family { vec![] } else { vec![compute_family, encode_family] }; // ---- CSC compute pipeline (shared across the frame ring) ---- let sampler = device.create_sampler( &vk::SamplerCreateInfo::default() .mag_filter(vk::Filter::NEAREST) .min_filter(vk::Filter::NEAREST) .address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE) .address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE), None, )?; let spv = ash::util::read_spv(&mut std::io::Cursor::new(CSC_SPV))?; let shader = device.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&spv), None)?; let sb = |b: u32, t: vk::DescriptorType| { vk::DescriptorSetLayoutBinding::default() .binding(b) .descriptor_type(t) .descriptor_count(1) .stage_flags(vk::ShaderStageFlags::COMPUTE) }; let bindings = [ sb(0, vk::DescriptorType::COMBINED_IMAGE_SAMPLER), sb(1, vk::DescriptorType::STORAGE_IMAGE), sb(2, vk::DescriptorType::STORAGE_IMAGE), ]; let csc_dsl = device.create_descriptor_set_layout( &vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings), None, )?; let dsls = [csc_dsl]; let csc_layout = device.create_pipeline_layout( &vk::PipelineLayoutCreateInfo::default().set_layouts(&dsls), None, )?; let stage = vk::PipelineShaderStageCreateInfo::default() .stage(vk::ShaderStageFlags::COMPUTE) .module(shader) .name(c"main"); let csc_pipe = device .create_compute_pipelines( vk::PipelineCache::null(), &[vk::ComputePipelineCreateInfo::default() .layout(csc_layout) .stage(stage)], None, ) .map_err(|(_, e)| e)?[0]; device.destroy_shader_module(shader, None); // One CSC descriptor set + its own Y/UV/NV12/bitstream per in-flight frame. let nframes = ring_depth(); let pool_sizes = [ vk::DescriptorPoolSize::default() .ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) .descriptor_count(nframes as u32), vk::DescriptorPoolSize::default() .ty(vk::DescriptorType::STORAGE_IMAGE) .descriptor_count(2 * nframes as u32), ]; let csc_pool = device.create_descriptor_pool( &vk::DescriptorPoolCreateInfo::default() .max_sets(nframes as u32) .pool_sizes(&pool_sizes), None, )?; // ---- bitstream size (shared) + shared command pools ---- let bs_size = align_up( 3 * w as u64 * h as u64 + (1 << 16), caps.min_bitstream_buffer_size_alignment.max(1), ); let cmd_pool = device.create_command_pool( &vk::CommandPoolCreateInfo::default() .queue_family_index(encode_family) .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER), None, )?; let compute_pool = device.create_command_pool( &vk::CommandPoolCreateInfo::default() .queue_family_index(compute_family) .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER), None, )?; // ---- build the in-flight frame ring ---- let mut frames = Vec::with_capacity(nframes); for _ in 0..nframes { frames.push(make_frame( &device, &mem_props, w, h, &fams, &profile, &mut profile_list, csc_dsl, csc_pool, cmd_pool, compute_pool, bs_size, )?); } Ok(Self { _entry: entry, instance, device, ext_fd, vq_dev, venc_dev, encode_queue, compute_queue, compute_family, mem_props, codec, session, session_mem, params, header, frame_prefix, dpb_image, dpb_mem, dpb_views, slot_wire: vec![-1; DPB_SLOTS as usize], slot_poc: vec![-1; DPB_SLOTS as usize], prev_slot: 0, csc_pipe, csc_layout, csc_dsl, csc_pool, sampler, import_cache: Vec::new(), frames, ring: 0, in_flight: VecDeque::new(), bs_size, cmd_pool, compute_pool, bitrate, fps, pending_bitrate: None, width: w, height: h, render_w: rw, render_h: rh, poc: 0, enc_count: 0, auto_wire: 0, first_frame: true, force_kf: false, pending_loss: None, pending: VecDeque::new(), }) } } impl VulkanVideoEncoder { /// Point a slot's CSC descriptor binding 0 at the current frame's RGB image view. unsafe fn bind_rgb(&self, csc_set: vk::DescriptorSet, rgb_view: vk::ImageView) { let ii0 = [vk::DescriptorImageInfo::default() .sampler(self.sampler) .image_view(rgb_view) .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)]; self.device.update_descriptor_sets( &[vk::WriteDescriptorSet::default() .dst_set(csc_set) .dst_binding(0) .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) .image_info(&ii0)], &[], ); } /// Import a packed-RGB dmabuf as a SAMPLED VkImage (explicit DRM modifier). Caller destroys. unsafe fn import_dmabuf( &self, d: &crate::capture::DmabufFrame, cw: u32, ch: u32, ) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> { let fmt = fourcc_to_vk(d.fourcc) .with_context(|| format!("unsupported dmabuf fourcc {:#x}", d.fourcc))?; let plane = [vk::SubresourceLayout::default() .offset(d.offset as u64) .row_pitch(d.stride as u64)]; let mut drm = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default() .drm_format_modifier(d.modifier) .plane_layouts(&plane); let mut ext = vk::ExternalMemoryImageCreateInfo::default() .handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT); let img = self.device.create_image( &vk::ImageCreateInfo::default() .image_type(vk::ImageType::TYPE_2D) .format(fmt) .extent(vk::Extent3D { width: cw, height: ch, depth: 1, }) .mip_levels(1) .array_layers(1) .samples(vk::SampleCountFlags::TYPE_1) .tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT) .usage(vk::ImageUsageFlags::SAMPLED) .sharing_mode(vk::SharingMode::EXCLUSIVE) .initial_layout(vk::ImageLayout::UNDEFINED) .push_next(&mut ext) .push_next(&mut drm), None, )?; // dup the fd; Vulkan takes ownership of the dup on a successful import. let dup = d.fd.try_clone().context("dup dmabuf fd")?.into_raw_fd(); let fd_props = { let mut p = vk::MemoryFdPropertiesKHR::default(); let _ = (self.ext_fd.fp().get_memory_fd_properties_khr)( self.device.handle(), vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT, dup, &mut p, ); p.memory_type_bits }; let req = self.device.get_image_memory_requirements(img); let bits = req.memory_type_bits & fd_props; let ti = find_mem( &self.mem_props, if bits != 0 { bits } else { req.memory_type_bits }, vk::MemoryPropertyFlags::empty(), ); let mut ded = vk::MemoryDedicatedAllocateInfo::default().image(img); let mut import = vk::ImportMemoryFdInfoKHR::default() .handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT) .fd(dup); let mem = self.device.allocate_memory( &vk::MemoryAllocateInfo::default() .allocation_size(req.size) .memory_type_index(ti) .push_next(&mut ded) .push_next(&mut import), None, )?; self.device.bind_image_memory(img, mem, 0)?; let view = self.device.create_image_view( &vk::ImageViewCreateInfo::default() .image(img) .view_type(vk::ImageViewType::TYPE_2D) .format(fmt) .subresource_range(color_range(0)), None, )?; Ok((img, mem, view)) } /// Import a dmabuf, reusing a cached per-buffer import when the same underlying buffer recurs /// (PipeWire cycles a small fixed pool). Keyed by `(st_dev, st_ino)` because each `DmabufFrame` /// owns a fresh *dup* — a new fd number, same inode. Returns `(image, view, fresh)`; `fresh` is /// true only on a first import (caller uses UNDEFINED old-layout to preserve modifier-tiled data). unsafe fn import_cached( &mut self, d: &crate::capture::DmabufFrame, cw: u32, ch: u32, ) -> Result<(vk::Image, vk::ImageView, bool)> { let mut st: libc::stat = std::mem::zeroed(); let key = if libc::fstat(d.fd.as_raw_fd(), &mut st) == 0 { (st.st_dev as u64, st.st_ino as u64) } else { // fstat failed → uncacheable; a per-frame-unique sentinel key never matches, so this // frame imports fresh (as before) but is still owned by the cache and freed on evict/Drop. (u64::MAX, self.enc_count) }; if let Some(&(_, _, img, _, view)) = self.import_cache.iter().find(|e| (e.0, e.1) == key) { return Ok((img, view, false)); } let (img, mem, view) = self.import_dmabuf(d, cw, ch)?; // Bound the cache; evict oldest (FIFO). A stable PipeWire pool never trips this in steady state // (all imports resident); it only cycles across a pool change (which also rebuilds the session). while self.import_cache.len() >= IMPORT_CACHE_CAP { let (_, _, oi, om, ov) = self.import_cache.remove(0); self.device.destroy_image_view(ov, None); self.device.destroy_image(oi, None); self.device.free_memory(om, None); } self.import_cache.push((key.0, key.1, img, mem, view)); // Fires once per distinct pool buffer then goes quiet in steady state — the signal the cache // is hitting (a per-frame log here would mean inode keying failed and we're re-importing). tracing::debug!( resident = self.import_cache.len(), "vulkan-encode: imported a new dmabuf buffer" ); Ok((img, view, true)) } /// Reusable RGB image + staging buffer for software (CPU) capture, private to one frame slot; /// (re)created on format change. Only the software-capture / smoke-test path exercises this. unsafe fn ensure_cpu_rgb( &mut self, slot: usize, fmt: vk::Format, bytes: &[u8], ) -> Result { let dev = self.device.clone(); let (w, h) = (self.width, self.height); let need = (w * h * 4) as u64; if self.frames[slot].cpu_img.map(|(_, _, _, f)| f) != Some(fmt) { if let Some((i, m, v, _)) = self.frames[slot].cpu_img.take() { dev.destroy_image_view(v, None); dev.destroy_image(i, None); dev.free_memory(m, None); } let (i, m, v) = make_plain_image( &dev, &self.mem_props, fmt, w, h, vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST, )?; self.frames[slot].cpu_img = Some((i, m, v, fmt)); } if self.frames[slot] .cpu_stage .map(|(_, _, s)| s < need) .unwrap_or(true) { if let Some((b, m, _)) = self.frames[slot].cpu_stage.take() { dev.destroy_buffer(b, None); dev.free_memory(m, None); } let buf = dev.create_buffer( &vk::BufferCreateInfo::default() .size(need) .usage(vk::BufferUsageFlags::TRANSFER_SRC), None, )?; let req = dev.get_buffer_memory_requirements(buf); let mem = dev.allocate_memory( &vk::MemoryAllocateInfo::default() .allocation_size(req.size) .memory_type_index(find_mem( &self.mem_props, req.memory_type_bits, vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, )), None, )?; dev.bind_buffer_memory(buf, mem, 0)?; self.frames[slot].cpu_stage = Some((buf, mem, need)); } let (_, m, _) = self.frames[slot].cpu_stage.unwrap(); let p = dev.map_memory(m, 0, vk::WHOLE_SIZE, vk::MemoryMapFlags::empty())? as *mut u8; let n = bytes.len().min(need as usize); std::ptr::copy_nonoverlapping(bytes.as_ptr(), p, n); dev.unmap_memory(m); Ok(self.frames[slot].cpu_img.unwrap().2) } /// Record one frame's CSC + HEVC encode (+ RFI) into ring `slot` and submit it WITHOUT waiting. /// The slot's fence is polled later (`read_slot`) so this frame's GPU work overlaps the next /// capture. `slot` must be free (its prior submission already read back). unsafe fn record_submit( &mut self, slot: usize, frame: &CapturedFrame, wire: i64, ) -> Result<()> { let (w, h_px) = (self.width, self.height); // Copy this slot's Vulkan handles into locals (all `vk::*` handles are Copy) so the rest of // the function can still borrow `&mut self` for the import/CSC helpers without aliasing // `self.frames`. Shared objects (csc_pipe/layout, dpb, session) stay on `self`. let compute_cmd = self.frames[slot].compute_cmd; let cmd = self.frames[slot].cmd; let csc_sem = self.frames[slot].csc_sem; let fence = self.frames[slot].fence; let query_pool = self.frames[slot].query_pool; let bs_buf = self.frames[slot].bs_buf; let csc_set = self.frames[slot].csc_set; let y_img = self.frames[slot].y_img; let uv_img = self.frames[slot].uv_img; let nv12_src = self.frames[slot].nv12_src; let nv12_view = self.frames[slot].nv12_view; // ---- 1. decide frame type + reference (RFI) ---- // Mid-stream rate retarget (`reconfigure_bitrate`): a first frame installs its RC state // fresh (RESET + ENCODE_RATE_CONTROL in the record fns), so a pending rate folds straight // into it; mid-stream it stays pending — the record fns emit an ENCODE_RATE_CONTROL // control command against the declared current state, and step 5 promotes it. if self.first_frame { if let Some(nb) = self.pending_bitrate.take() { self.bitrate = nb; } } let mut is_idr = self.first_frame || self.force_kf; let mut ref_slot = self.prev_slot; let mut recovery = false; if let Some(lf) = self.pending_loss.take() { if !is_idr { match pick_recovery_slot(&self.slot_wire, lf) { Some(s) => { ref_slot = s; recovery = true; tracing::debug!( loss_first = lf, anchor_slot = s, anchor_wire = self.slot_wire[s], "vulkan-encode: emitting clean recovery-anchor P-frame (references a known-good frame older than the loss, no IDR)" ); } None => { is_idr = true; tracing::debug!(loss_first = lf, "vulkan-encode: no resident reference older than the loss — forcing IDR"); } } } } let poc: i32 = if is_idr { 0 } else { self.poc }; let mut setup_idx = (self.enc_count % DPB_SLOTS as u64) as usize; if !is_idr && setup_idx == ref_slot { setup_idx = (setup_idx + 1) % DPB_SLOTS as usize; } // ---- 2. RGB source -> compute_cmd: prep barriers + CSC + copy into nv12_src ---- let dev = self.device.clone(); // cheap handle clone -> lets us also call &mut self helpers dev.begin_command_buffer( compute_cmd, &vk::CommandBufferBeginInfo::default() .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), )?; let rgb_view = match &frame.payload { FramePayload::Dmabuf(d) => { // Reuse the per-buffer import (PipeWire cycles a small pool) — no per-frame VkImage // create/import/destroy. The producer wrote new content out-of-band, so still acquire // from FOREIGN each frame; a fresh import starts UNDEFINED (preserves modifier-tiled // data), a cached one is already SHADER_READ_ONLY_OPTIMAL. let (img, view, fresh) = self.import_cached(d, frame.width, frame.height)?; // First import: acquire from the foreign producer (UNDEFINED preserves the modifier-tiled // bytes). Cached re-read: we still own it, so no queue-family transfer — just a visibility // barrier so the shader read sees the content the producer wrote out-of-band this frame // (single-GPU coherent; the capture layer guarantees the buffer is ready at hand-off). let (old, src_qf, dst_qf) = if fresh { ( vk::ImageLayout::UNDEFINED, vk::QUEUE_FAMILY_FOREIGN_EXT, self.compute_family, ) } else { ( vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, vk::QUEUE_FAMILY_IGNORED, vk::QUEUE_FAMILY_IGNORED, ) }; let acq = vk::ImageMemoryBarrier2::default() .src_stage_mask(vk::PipelineStageFlags2::NONE) .src_access_mask(vk::AccessFlags2::NONE) .dst_stage_mask(vk::PipelineStageFlags2::COMPUTE_SHADER) .dst_access_mask(vk::AccessFlags2::SHADER_READ) .old_layout(old) .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) .src_queue_family_index(src_qf) .dst_queue_family_index(dst_qf) .image(img) .subresource_range(color_range(0)); dev.cmd_pipeline_barrier2( compute_cmd, &vk::DependencyInfo::default().image_memory_barriers(&[acq]), ); view } FramePayload::Cpu(bytes) => { let fmt = pixel_to_vk(frame.format).context("unsupported CPU pixel format")?; let view = self.ensure_cpu_rgb(slot, fmt, bytes)?; let (img, ..) = self.frames[slot].cpu_img.unwrap(); let (stage, ..) = self.frames[slot].cpu_stage.unwrap(); let to_dst = vk::ImageMemoryBarrier2::default() .src_stage_mask(vk::PipelineStageFlags2::NONE) .src_access_mask(vk::AccessFlags2::NONE) .dst_stage_mask(vk::PipelineStageFlags2::ALL_TRANSFER) .dst_access_mask(vk::AccessFlags2::TRANSFER_WRITE) .old_layout(vk::ImageLayout::UNDEFINED) .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) .image(img) .subresource_range(color_range(0)); dev.cmd_pipeline_barrier2( compute_cmd, &vk::DependencyInfo::default().image_memory_barriers(&[to_dst]), ); dev.cmd_copy_buffer_to_image( compute_cmd, stage, img, vk::ImageLayout::TRANSFER_DST_OPTIMAL, &[vk::BufferImageCopy::default() .image_subresource( vk::ImageSubresourceLayers::default() .aspect_mask(vk::ImageAspectFlags::COLOR) .layer_count(1), ) .image_extent(vk::Extent3D { width: self.width, height: self.height, depth: 1, })], ); let to_read = vk::ImageMemoryBarrier2::default() .src_stage_mask(vk::PipelineStageFlags2::ALL_TRANSFER) .src_access_mask(vk::AccessFlags2::TRANSFER_WRITE) .dst_stage_mask(vk::PipelineStageFlags2::COMPUTE_SHADER) .dst_access_mask(vk::AccessFlags2::SHADER_READ) .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) .image(img) .subresource_range(color_range(0)); dev.cmd_pipeline_barrier2( compute_cmd, &vk::DependencyInfo::default().image_memory_barriers(&[to_read]), ); view } _ => bail!("vulkan-encode: unsupported FramePayload (need Dmabuf or Cpu RGB)"), }; self.bind_rgb(csc_set, rgb_view); // y/uv -> GENERAL (shader write); nv12_src -> GENERAL (transfer dst, discard prior) let to_general = |img, dst_stage, dst_access| { vk::ImageMemoryBarrier2::default() .src_stage_mask(vk::PipelineStageFlags2::NONE) .src_access_mask(vk::AccessFlags2::NONE) .dst_stage_mask(dst_stage) .dst_access_mask(dst_access) .old_layout(vk::ImageLayout::UNDEFINED) .new_layout(vk::ImageLayout::GENERAL) .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) .image(img) .subresource_range(color_range(0)) }; let pre = [ to_general( y_img, vk::PipelineStageFlags2::COMPUTE_SHADER, vk::AccessFlags2::SHADER_WRITE, ), to_general( uv_img, vk::PipelineStageFlags2::COMPUTE_SHADER, vk::AccessFlags2::SHADER_WRITE, ), to_general( nv12_src, vk::PipelineStageFlags2::ALL_TRANSFER, vk::AccessFlags2::TRANSFER_WRITE, ), ]; dev.cmd_pipeline_barrier2( compute_cmd, &vk::DependencyInfo::default().image_memory_barriers(&pre), ); dev.cmd_bind_pipeline(compute_cmd, vk::PipelineBindPoint::COMPUTE, self.csc_pipe); dev.cmd_bind_descriptor_sets( compute_cmd, vk::PipelineBindPoint::COMPUTE, self.csc_layout, 0, &[csc_set], &[], ); dev.cmd_dispatch(compute_cmd, (w / 2).div_ceil(8), (h_px / 2).div_ceil(8), 1); // y/uv shader-write -> transfer-read (stay GENERAL); then copy into nv12 planes let yuv_rd = |img| { vk::ImageMemoryBarrier2::default() .src_stage_mask(vk::PipelineStageFlags2::COMPUTE_SHADER) .src_access_mask(vk::AccessFlags2::SHADER_WRITE) .dst_stage_mask(vk::PipelineStageFlags2::ALL_TRANSFER) .dst_access_mask(vk::AccessFlags2::TRANSFER_READ) .old_layout(vk::ImageLayout::GENERAL) .new_layout(vk::ImageLayout::GENERAL) .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) .image(img) .subresource_range(color_range(0)) }; dev.cmd_pipeline_barrier2( compute_cmd, &vk::DependencyInfo::default().image_memory_barriers(&[yuv_rd(y_img), yuv_rd(uv_img)]), ); let plane_copy = |src_aspect, dst_aspect, ew, eh| { vk::ImageCopy::default() .src_subresource( vk::ImageSubresourceLayers::default() .aspect_mask(src_aspect) .layer_count(1), ) .dst_subresource( vk::ImageSubresourceLayers::default() .aspect_mask(dst_aspect) .layer_count(1), ) .extent(vk::Extent3D { width: ew, height: eh, depth: 1, }) }; dev.cmd_copy_image( compute_cmd, y_img, vk::ImageLayout::GENERAL, nv12_src, vk::ImageLayout::GENERAL, &[plane_copy( vk::ImageAspectFlags::COLOR, vk::ImageAspectFlags::PLANE_0, w, h_px, )], ); dev.cmd_copy_image( compute_cmd, uv_img, vk::ImageLayout::GENERAL, nv12_src, vk::ImageLayout::GENERAL, &[plane_copy( vk::ImageAspectFlags::COLOR, vk::ImageAspectFlags::PLANE_1, w / 2, h_px / 2, )], ); dev.end_command_buffer(compute_cmd)?; // ---- 3. record encode into `cmd`: codec-specific Std authoring + begin/encode/end ---- 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 { self.record_coding_h265( &dev, cmd, query_pool, bs_buf, nv12_src, nv12_view, is_idr, ref_slot, setup_idx, poc, )?; } // ---- 4. submit compute (signal csc_sem) then encode (wait csc_sem, signal fence). // Non-blocking: `fence` is polled later so this frame's CSC+encode overlaps the next // capture/submit. Per-slot cmd/sem/fence make ring frames independent; the DPB // barrier above orders slot N's reconstruct-write before N+1's reference-read. ---- dev.reset_fences(&[fence])?; let ccmds = [compute_cmd]; let sems = [csc_sem]; dev.queue_submit( self.compute_queue, &[vk::SubmitInfo::default() .command_buffers(&ccmds) .signal_semaphores(&sems)], vk::Fence::null(), )?; let ecmds = [cmd]; let wait_stages = [vk::PipelineStageFlags::ALL_COMMANDS]; dev.queue_submit( self.encode_queue, &[vk::SubmitInfo::default() .command_buffers(&ecmds) .wait_semaphores(&sems) .wait_dst_stage_mask(&wait_stages)], fence, )?; // Stash the metadata `read_slot` needs once `fence` signals. self.frames[slot].pts_ns = frame.pts_ns; self.frames[slot].keyframe = is_idr; self.frames[slot].recovery_anchor = recovery; // ---- 5. advance DPB bookkeeping (in submission order, before returning) ---- if is_idr { self.slot_wire.iter_mut().for_each(|s| *s = -1); self.slot_poc.iter_mut().for_each(|s| *s = -1); } self.slot_wire[setup_idx] = wire; self.slot_poc[setup_idx] = poc; self.prev_slot = setup_idx; self.poc = poc + 1; self.enc_count += 1; self.first_frame = false; self.force_kf = false; if let Some(nb) = self.pending_bitrate.take() { // The retarget control command is recorded (execution follows submission order): the // session's RC state IS the new rate from this frame on — later begins declare it. self.bitrate = nb; tracing::info!( mbps = nb / 1_000_000, "vulkan-encode: rate control retargeted in place (no IDR)" ); } Ok(()) } /// Begin `cmd` and record the pre-encode setup shared by both codecs: the query-pool reset, /// nv12_src GENERAL → VIDEO_ENCODE_SRC (the CSC semaphore already ordered the copy before /// this), and the DPB transition — on the first frame a whole-image UNDEFINED → DPB init; /// afterwards the cross-command-buffer pipelining barrier that orders the previous frame's /// reconstruct-write before this frame's reference read/write (the in-flight ring records /// frame N+1 while N still encodes; the barrier's first scope covers all prior-submitted /// encode work on this queue, spanning the separate command buffers). unsafe fn begin_encode_cmd( &self, dev: &ash::Device, cmd: vk::CommandBuffer, query_pool: vk::QueryPool, nv12_src: vk::Image, ) -> Result<()> { dev.begin_command_buffer( cmd, &vk::CommandBufferBeginInfo::default() .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), )?; dev.cmd_reset_query_pool(cmd, query_pool, 0, 1); let dpb_barrier = if self.first_frame { vk::ImageMemoryBarrier2::default() .src_stage_mask(vk::PipelineStageFlags2::NONE) .src_access_mask(vk::AccessFlags2::NONE) .dst_stage_mask(vk::PipelineStageFlags2::VIDEO_ENCODE_KHR) .dst_access_mask(vk::AccessFlags2::VIDEO_ENCODE_WRITE_KHR) .old_layout(vk::ImageLayout::UNDEFINED) .new_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR) } else { vk::ImageMemoryBarrier2::default() .src_stage_mask(vk::PipelineStageFlags2::VIDEO_ENCODE_KHR) .src_access_mask(vk::AccessFlags2::VIDEO_ENCODE_WRITE_KHR) .dst_stage_mask(vk::PipelineStageFlags2::VIDEO_ENCODE_KHR) .dst_access_mask( vk::AccessFlags2::VIDEO_ENCODE_READ_KHR | vk::AccessFlags2::VIDEO_ENCODE_WRITE_KHR, ) .old_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR) .new_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR) } .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) .image(self.dpb_image) .subresource_range(vk::ImageSubresourceRange { aspect_mask: vk::ImageAspectFlags::COLOR, base_mip_level: 0, level_count: 1, base_array_layer: 0, layer_count: DPB_SLOTS, }); let pre_enc = [ vk::ImageMemoryBarrier2::default() .src_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS) .src_access_mask(vk::AccessFlags2::NONE) .dst_stage_mask(vk::PipelineStageFlags2::VIDEO_ENCODE_KHR) .dst_access_mask(vk::AccessFlags2::VIDEO_ENCODE_READ_KHR) .old_layout(vk::ImageLayout::GENERAL) .new_layout(vk::ImageLayout::VIDEO_ENCODE_SRC_KHR) .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) .image(nv12_src) .subresource_range(color_range(0)), dpb_barrier, ]; dev.cmd_pipeline_barrier2( cmd, &vk::DependencyInfo::default().image_memory_barriers(&pre_enc), ); Ok(()) } /// Author the HEVC Std structs + record begin/encode/end for one frame into `cmd` — the HEVC /// twin of [`record_coding_av1`]. RFI lever: a recovery anchor is an ordinary P whose /// `RefPicList0` names the known-good slot; what makes it decodable is the FULL short-term RPS /// ([`build_h265_rps_s0`]) every P-frame carries, which keeps all resident DPB pictures alive /// at the decoder so any slot the anchor references is still there. #[allow(clippy::too_many_arguments)] unsafe fn record_coding_h265( &self, dev: &ash::Device, cmd: vk::CommandBuffer, query_pool: vk::QueryPool, bs_buf: vk::Buffer, nv12_src: vk::Image, nv12_view: vk::ImageView, is_idr: bool, ref_slot: usize, setup_idx: usize, poc: i32, ) -> Result<()> { use ash::vk::native as h; let ext2d = vk::Extent2D { width: self.width, height: self.height, }; let ref_poc = if is_idr { 0 } else { self.slot_poc[ref_slot] }; let mut pic_flags: h::StdVideoEncodeH265PictureInfoFlags = std::mem::zeroed(); pic_flags.set_is_reference(1); if is_idr { pic_flags.set_IrapPicFlag(1); } pic_flags.set_pic_output_flag(1); let mut std_pic: h::StdVideoEncodeH265PictureInfo = std::mem::zeroed(); std_pic.flags = pic_flags; std_pic.pic_type = if is_idr { h::StdVideoH265PictureType_STD_VIDEO_H265_PICTURE_TYPE_IDR } else { h::StdVideoH265PictureType_STD_VIDEO_H265_PICTURE_TYPE_P }; std_pic.PicOrderCntVal = poc; let (num_neg, deltas, used) = build_h265_rps_s0(&self.slot_poc, setup_idx, ref_poc, poc); // A P-frame's active reference must be one of the retained pictures — `ref_slot` is always // resident and never the setup slot (record_submit bumps a collision), so a miss here means // the DPB bookkeeping desynced. debug_assert!(is_idr || used != 0, "reference POC missing from the RPS"); let mut rps: h::StdVideoH265ShortTermRefPicSet = std::mem::zeroed(); rps.num_negative_pics = num_neg; rps.delta_poc_s0_minus1 = deltas; rps.used_by_curr_pic_s0_flag = used; let mut ref_lists: h::StdVideoEncodeH265ReferenceListsInfo = std::mem::zeroed(); ref_lists.RefPicList0 = [0xff; 15]; ref_lists.RefPicList1 = [0xff; 15]; ref_lists.RefPicList0[0] = ref_slot as u8; if !is_idr { std_pic.pShortTermRefPicSet = &rps; std_pic.pRefLists = &ref_lists; } let mut sh_flags: h::StdVideoEncodeH265SliceSegmentHeaderFlags = std::mem::zeroed(); sh_flags.set_first_slice_segment_in_pic_flag(1); sh_flags.set_slice_loop_filter_across_slices_enabled_flag(1); let mut std_sh: h::StdVideoEncodeH265SliceSegmentHeader = std::mem::zeroed(); std_sh.flags = sh_flags; std_sh.slice_type = if is_idr { h::StdVideoH265SliceType_STD_VIDEO_H265_SLICE_TYPE_I } else { h::StdVideoH265SliceType_STD_VIDEO_H265_SLICE_TYPE_P }; std_sh.MaxNumMergeCand = 5; let slice = vk::VideoEncodeH265NaluSliceSegmentInfoKHR::default() .constant_qp(0) .std_slice_segment_header(&std_sh); let slices = [slice]; let mut h265_pic = vk::VideoEncodeH265PictureInfoKHR::default() .nalu_slice_segment_entries(&slices) .std_picture_info(&std_pic); // setup slot (reconstruct into) + reference slot (read from) let setup_res = vk::VideoPictureResourceInfoKHR::default() .coded_extent(ext2d) .image_view_binding(self.dpb_views[setup_idx]); let mut setup_std: h::StdVideoEncodeH265ReferenceInfo = std::mem::zeroed(); setup_std.pic_type = std_pic.pic_type; setup_std.PicOrderCntVal = poc; let mut setup_dpb_a = vk::VideoEncodeH265DpbSlotInfoKHR::default().std_reference_info(&setup_std); let mut setup_dpb_b = vk::VideoEncodeH265DpbSlotInfoKHR::default().std_reference_info(&setup_std); let setup_slot = vk::VideoReferenceSlotInfoKHR::default() .slot_index(setup_idx as i32) .picture_resource(&setup_res) .push_next(&mut setup_dpb_a); let begin_setup = vk::VideoReferenceSlotInfoKHR::default() .slot_index(-1) .picture_resource(&setup_res) .push_next(&mut setup_dpb_b); let ref_res = vk::VideoPictureResourceInfoKHR::default() .coded_extent(ext2d) .image_view_binding(self.dpb_views[ref_slot]); let mut ref_std: h::StdVideoEncodeH265ReferenceInfo = std::mem::zeroed(); ref_std.pic_type = if ref_poc == 0 { h::StdVideoH265PictureType_STD_VIDEO_H265_PICTURE_TYPE_IDR } else { h::StdVideoH265PictureType_STD_VIDEO_H265_PICTURE_TYPE_P }; ref_std.PicOrderCntVal = ref_poc; let mut ref_dpb_a = vk::VideoEncodeH265DpbSlotInfoKHR::default().std_reference_info(&ref_std); let mut ref_dpb_b = vk::VideoEncodeH265DpbSlotInfoKHR::default().std_reference_info(&ref_std); let ref_begin = vk::VideoReferenceSlotInfoKHR::default() .slot_index(ref_slot as i32) .picture_resource(&ref_res) .push_next(&mut ref_dpb_a); let ref_enc = vk::VideoReferenceSlotInfoKHR::default() .slot_index(ref_slot as i32) .picture_resource(&ref_res) .push_next(&mut ref_dpb_b); let begin_p = [ref_begin, begin_setup]; let begin_i = [begin_setup]; let enc_refs = [ref_enc]; // CBR rate control (chained manually; push_next would clobber rc.p_next) let rc_layer = [vk::VideoEncodeRateControlLayerInfoKHR::default() .average_bitrate(self.bitrate) .max_bitrate(self.bitrate) .frame_rate_numerator(self.fps) .frame_rate_denominator(1)]; let h265_rc = vk::VideoEncodeH265RateControlInfoKHR::default() .flags(vk::VideoEncodeH265RateControlFlagsKHR::REGULAR_GOP) .gop_frame_count(u32::MAX) .idr_period(u32::MAX) .consecutive_b_frame_count(0) .sub_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 = &h265_rc as *const _ as *const c_void; let rc_ptr = &rc as *const _ as *const c_void; self.begin_encode_cmd(dev, cmd, query_pool, nv12_src)?; 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; } // CBR is current state after frame 0's control (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); } else if let Some(nb) = self.pending_bitrate { // Mid-stream retarget (`reconfigure_bitrate`): `begin` above declared the session's // CURRENT rate-control state (the spec requires the match); this control command // installs the NEW rate — the same CBR shape with only the bitrate moved. No RESET, // no IDR: the DPB and reference chain carry straight on. `record_submit` promotes // `nb` into `self.bitrate` after recording, so later begins declare the new state. let rc_layer2 = [vk::VideoEncodeRateControlLayerInfoKHR::default() .average_bitrate(nb) .max_bitrate(nb) .frame_rate_numerator(self.fps) .frame_rate_denominator(1)]; let mut rc2 = vk::VideoEncodeRateControlInfoKHR::default() .rate_control_mode(vk::VideoEncodeRateControlModeFlagsKHR::CBR) .layers(&rc_layer2) .virtual_buffer_size_in_ms(1000) .initial_virtual_buffer_size_in_ms(500); rc2.p_next = &h265_rc as *const _ as *const c_void; let mut ctrl = vk::VideoCodingControlInfoKHR::default() .flags(vk::VideoCodingControlFlagsKHR::ENCODE_RATE_CONTROL); ctrl.p_next = &rc2 as *const _ as *const c_void; (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) .push_next(&mut h265_pic); if !is_idr { enc = enc.reference_slots(&enc_refs); } (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(()) } /// Author the AV1 Std structs + record begin/encode/end for one frame into `cmd` — the AV1 /// twin of [`record_coding_h265`]. 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`). Unlike HEVC, reference retention needs no per-frame syntax: AV1's 8 /// virtual reference slots persist until `refresh_frame_flags` overwrites them, mirroring the /// host's DPB ring by construction. #[allow(clippy::too_many_arguments)] 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 + shared pre-encode barriers, then begin/encode/end coding ---- self.begin_encode_cmd(dev, cmd, query_pool, nv12_src)?; 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); } else if let Some(nb) = self.pending_bitrate { // Mid-stream retarget (`reconfigure_bitrate`) — see the HEVC twin for the state // discipline (begin declares CURRENT, this control installs NEW, `record_submit` // promotes after recording). No RESET, no IDR. let rc_layer2 = [vk::VideoEncodeRateControlLayerInfoKHR::default() .average_bitrate(nb) .max_bitrate(nb) .frame_rate_numerator(self.fps) .frame_rate_denominator(1)]; let mut rc2 = vk::VideoEncodeRateControlInfoKHR::default() .rate_control_mode(vk::VideoEncodeRateControlModeFlagsKHR::CBR) .layers(&rc_layer2) .virtual_buffer_size_in_ms(1000) .initial_virtual_buffer_size_in_ms(500); rc2.p_next = &av1_rc as *const _ as *const c_void; let mut ctrl = vk::VideoCodingControlInfoKHR::default() .flags(vk::VideoCodingControlFlagsKHR::ENCODE_RATE_CONTROL); ctrl.p_next = &rc2 as *const _ as *const c_void; (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 { let dev = self.device.clone(); let f = &self.frames[slot]; let mut fb = [[0u32; 2]; 1]; dev.get_query_pool_results(f.query_pool, 0, &mut fb, vk::QueryResultFlags::WAIT)?; 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 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 { data, pts_ns: f.pts_ns, keyframe: f.keyframe, recovery_anchor: f.recovery_anchor, }) } /// Acquire a free ring slot (blocking-draining the oldest if the ring is full), record+submit /// this frame into it without waiting, and track it as in-flight (FIFO). unsafe fn enqueue(&mut self, frame: &CapturedFrame, wire: i64) -> Result<()> { // Backpressure: if every slot is outstanding, block on the oldest, read it into `pending`, // and free it — that oldest slot is exactly the round-robin `ring` cursor we reuse next. while self.in_flight.len() >= self.frames.len() { let slot = self.in_flight.pop_front().unwrap(); self.device .wait_for_fences(&[self.frames[slot].fence], true, u64::MAX)?; let done = self.read_slot(slot)?; self.pending.push_back(done); } let slot = self.ring; self.ring = (self.ring + 1) % self.frames.len(); self.record_submit(slot, frame, wire)?; self.in_flight.push_back(slot); Ok(()) } } impl Encoder for VulkanVideoEncoder { fn submit(&mut self, frame: &CapturedFrame) -> Result<()> { let wire = self.auto_wire; self.auto_wire += 1; // SAFETY: `enqueue` records/submits into a free ring slot owned by this encoder without // blocking on GPU completion (poll() does); `&mut self` guarantees exclusive access. unsafe { self.enqueue(frame, wire) } } fn submit_indexed(&mut self, frame: &CapturedFrame, wire_index: u32) -> Result<()> { self.auto_wire = wire_index as i64 + 1; // SAFETY: see `submit` — exclusive `&mut self`, all Vulkan work confined to owned objects. unsafe { self.enqueue(frame, wire_index as i64) } } fn caps(&self) -> EncoderCaps { EncoderCaps { supports_rfi: true, ..Default::default() } } fn request_keyframe(&mut self) { self.force_kf = true; } fn invalidate_ref_frames(&mut self, first_frame: i64, last_frame: i64) -> bool { // Nonsense range → decline (same contract as the NVENC/AMF backends). if first_frame < 0 || first_frame > last_frame { return false; } // Can we anchor a clean P-frame to a resident slot strictly older than the loss? match pick_recovery_slot(&self.slot_wire, first_frame) { Some(_) => { self.pending_loss = Some(first_frame); true } None => { // Decline WITHOUT self-arming an IDR: the caller owns the fallback, and its // keyframe path is cooldown-coalesced — arming `force_kf` here would bypass that // and turn a storm of hopeless RFI requests into one full IDR per request. tracing::debug!( first_frame, last_frame, "vulkan-encode RFI declined: no resident reference older than the loss — \ caller falls back to its (coalesced) keyframe path" ); false } } } fn poll(&mut self) -> Result> { // Backpressure-drained frames (already read, oldest) come out first, then the oldest slot // still in flight once its fence signals — both in submission order. Non-blocking: an // unfinished frame returns None so the caller keeps the pipeline moving. if let Some(f) = self.pending.pop_front() { return Ok(Some(f)); } let Some(&slot) = self.in_flight.front() else { return Ok(None); }; // SAFETY: probing a fence + reading back this slot's own owned objects under `&mut self`. let ready = unsafe { self.device.get_fence_status(self.frames[slot].fence)? }; if !ready { return Ok(None); } self.in_flight.pop_front(); // SAFETY: fence signaled ⟹ this slot's CSC+encode is complete; read its bitstream. Ok(Some(unsafe { self.read_slot(slot)? })) } fn reset(&mut self) -> bool { // Abandon everything in flight: wait the GPU idle, discard unread slots + queued output, and // restart GOP/DPB state so the next frame is a fresh IDR. // SAFETY: `device_wait_idle` guarantees no slot's fence is still pending before we drop them. unsafe { let _ = self.device.device_wait_idle(); } self.in_flight.clear(); self.pending.clear(); self.ring = 0; self.first_frame = true; self.force_kf = false; self.pending_loss = None; self.poc = 0; self.slot_wire.iter_mut().for_each(|s| *s = -1); self.slot_poc.iter_mut().for_each(|s| *s = -1); // A pending `reconfigure_bitrate` rate deliberately survives: the restart's first frame // folds it into the fresh RESET + rate-control install. true } fn reconfigure_bitrate(&mut self, bps: u64) -> bool { // The RC block is re-declared on every recorded frame, so the retarget is just a staged // rate: the next `record_submit` emits an ENCODE_RATE_CONTROL control command carrying it // — no session churn, no IDR. Same floor as `open` (a 0-rate CBR layer is rejected). self.pending_bitrate = Some(bps.max(1_000_000)); true } fn flush(&mut self) -> Result<()> { // Drain every outstanding slot in order into `pending` so a following poll-loop returns them. while let Some(slot) = self.in_flight.pop_front() { // SAFETY: wait this slot's fence, then read back its own owned bitstream objects. unsafe { self.device .wait_for_fences(&[self.frames[slot].fence], true, u64::MAX)?; let done = self.read_slot(slot)?; self.pending.push_back(done); } } Ok(()) } } impl Drop for VulkanVideoEncoder { fn drop(&mut self) { // SAFETY: `device_wait_idle` first guarantees no GPU work still references any object, so // every handle destroyed below is idle and owned solely by `self`; each is freed exactly once // (the drains prevent a double free) and in dependency order (views before images before // memory, per-frame objects before their shared pools, session params before session). unsafe { let _ = self.device.device_wait_idle(); for (_, _, img, mem, view) in std::mem::take(&mut self.import_cache) { self.device.destroy_image_view(view, None); self.device.destroy_image(img, None); self.device.free_memory(mem, None); } // Per-frame ring resources (command buffers, descriptor sets freed with their pools). for f in std::mem::take(&mut self.frames) { self.device.destroy_semaphore(f.csc_sem, None); self.device.destroy_fence(f.fence, None); self.device.destroy_query_pool(f.query_pool, None); self.device.destroy_buffer(f.bs_buf, None); self.device.free_memory(f.bs_mem, None); for (img, mem, view) in [ (f.y_img, f.y_mem, f.y_view), (f.uv_img, f.uv_mem, f.uv_view), (f.nv12_src, f.nv12_mem, f.nv12_view), ] { self.device.destroy_image_view(view, None); self.device.destroy_image(img, None); self.device.free_memory(mem, None); } if let Some((i, m, v, _)) = f.cpu_img { self.device.destroy_image_view(v, None); self.device.destroy_image(i, None); self.device.free_memory(m, None); } if let Some((b, m, _)) = f.cpu_stage { self.device.destroy_buffer(b, None); self.device.free_memory(m, None); } } self.device.destroy_command_pool(self.compute_pool, None); self.device.destroy_command_pool(self.cmd_pool, None); self.device.destroy_pipeline(self.csc_pipe, None); self.device.destroy_pipeline_layout(self.csc_layout, None); self.device.destroy_descriptor_pool(self.csc_pool, None); self.device .destroy_descriptor_set_layout(self.csc_dsl, None); self.device.destroy_sampler(self.sampler, None); for &v in &self.dpb_views { self.device.destroy_image_view(v, None); } self.device.destroy_image(self.dpb_image, None); self.device.free_memory(self.dpb_mem, None); (self.vq_dev.fp().destroy_video_session_parameters_khr)( self.device.handle(), self.params, std::ptr::null(), ); (self.vq_dev.fp().destroy_video_session_khr)( self.device.handle(), self.session, std::ptr::null(), ); for &m in &self.session_mem { self.device.free_memory(m, None); } self.device.destroy_device(None); self.instance.destroy_instance(None); } } } // ---------- free helpers ---------- fn color_range(layer: u32) -> vk::ImageSubresourceRange { vk::ImageSubresourceRange { aspect_mask: vk::ImageAspectFlags::COLOR, base_mip_level: 0, level_count: 1, base_array_layer: layer, layer_count: 1, } } fn align_up(v: u64, a: u64) -> u64 { v.div_ceil(a) * a } unsafe fn find_mem( mp: &vk::PhysicalDeviceMemoryProperties, bits: u32, want: vk::MemoryPropertyFlags, ) -> u32 { for i in 0..mp.memory_type_count { if (bits & (1 << i)) != 0 && mp.memory_types[i as usize].property_flags.contains(want) { return i; } } 0 } /// DRM fourcc -> the VkFormat whose *color* components match (Vulkan handles the byte swizzle). fn fourcc_to_vk(fourcc: u32) -> Option { // fourcc_code(a,b,c,d) = a | b<<8 | c<<16 | d<<24 const XR24: u32 = 0x3432_5258; // XRGB8888 const AR24: u32 = 0x3432_5241; // ARGB8888 const XB24: u32 = 0x3432_4258; // XBGR8888 const AB24: u32 = 0x3432_4241; // ABGR8888 match fourcc { XR24 | AR24 => Some(vk::Format::B8G8R8A8_UNORM), XB24 | AB24 => Some(vk::Format::R8G8B8A8_UNORM), _ => None, } } fn pixel_to_vk(fmt: PixelFormat) -> Option { match fmt { PixelFormat::Bgrx | PixelFormat::Bgra => Some(vk::Format::B8G8R8A8_UNORM), PixelFormat::Rgbx | PixelFormat::Rgba => Some(vk::Format::R8G8B8A8_UNORM), _ => None, } } unsafe fn make_view( device: &ash::Device, image: vk::Image, fmt: vk::Format, layer: u32, ) -> Result { Ok(device.create_image_view( &vk::ImageViewCreateInfo::default() .image(image) .view_type(vk::ImageViewType::TYPE_2D) .format(fmt) .subresource_range(color_range(layer)), None, )?) } unsafe fn make_plain_image( device: &ash::Device, mp: &vk::PhysicalDeviceMemoryProperties, fmt: vk::Format, w: u32, h: u32, usage: vk::ImageUsageFlags, ) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> { let img = device.create_image( &vk::ImageCreateInfo::default() .image_type(vk::ImageType::TYPE_2D) .format(fmt) .extent(vk::Extent3D { width: w, height: h, depth: 1, }) .mip_levels(1) .array_layers(1) .samples(vk::SampleCountFlags::TYPE_1) .tiling(vk::ImageTiling::OPTIMAL) .usage(usage) .initial_layout(vk::ImageLayout::UNDEFINED), None, )?; let req = device.get_image_memory_requirements(img); let mem = device.allocate_memory( &vk::MemoryAllocateInfo::default() .allocation_size(req.size) .memory_type_index(find_mem( mp, req.memory_type_bits, vk::MemoryPropertyFlags::DEVICE_LOCAL, )), None, )?; device.bind_image_memory(img, mem, 0)?; let view = make_view(device, img, fmt, 0)?; Ok((img, mem, view)) } unsafe fn make_video_image( device: &ash::Device, mp: &vk::PhysicalDeviceMemoryProperties, fmt: vk::Format, w: u32, h: u32, layers: u32, usage: vk::ImageUsageFlags, profile_list: &mut vk::VideoProfileListInfoKHR, concurrent: &[u32], ) -> Result<(vk::Image, vk::DeviceMemory)> { let mut ci = vk::ImageCreateInfo::default() .image_type(vk::ImageType::TYPE_2D) .format(fmt) .extent(vk::Extent3D { width: w, height: h, depth: 1, }) .mip_levels(1) .array_layers(layers) .samples(vk::SampleCountFlags::TYPE_1) .tiling(vk::ImageTiling::OPTIMAL) .usage(usage) .initial_layout(vk::ImageLayout::UNDEFINED) .push_next(profile_list); if concurrent.len() >= 2 { ci = ci .sharing_mode(vk::SharingMode::CONCURRENT) .queue_family_indices(concurrent); } else { ci = ci.sharing_mode(vk::SharingMode::EXCLUSIVE); } let img = device.create_image(&ci, None)?; let req = device.get_image_memory_requirements(img); let mem = device.allocate_memory( &vk::MemoryAllocateInfo::default() .allocation_size(req.size) .memory_type_index(find_mem( mp, req.memory_type_bits, vk::MemoryPropertyFlags::DEVICE_LOCAL, )), None, )?; device.bind_image_memory(img, mem, 0)?; Ok((img, mem)) } /// Build one in-flight frame's private resources: NV12 encode-src, Y/UV CSC scratch, its CSC /// descriptor set (Y/UV bound now, RGB per use), the bitstream buffer + feedback query, and the /// per-frame command buffers + sync. `profile_list`/`profile` are borrowed only during creation. unsafe fn make_frame( device: &ash::Device, mem_props: &vk::PhysicalDeviceMemoryProperties, w: u32, h: u32, fams: &[u32], profile: &vk::VideoProfileInfoKHR, profile_list: &mut vk::VideoProfileListInfoKHR, csc_dsl: vk::DescriptorSetLayout, csc_pool: vk::DescriptorPool, cmd_pool: vk::CommandPool, compute_pool: vk::CommandPool, bs_size: u64, ) -> Result { // NV12 encode-src (filled by the CSC copy) — concurrent compute+encode. let (nv12_src, nv12_mem) = make_video_image( device, mem_props, NV12, w, h, 1, vk::ImageUsageFlags::VIDEO_ENCODE_SRC_KHR | vk::ImageUsageFlags::TRANSFER_DST, profile_list, fams, )?; let nv12_view = make_view(device, nv12_src, NV12, 0)?; // CSC scratch (Y R8 full-res, UV RG8 half-res). let (y_img, y_mem, y_view) = make_plain_image( device, mem_props, vk::Format::R8_UNORM, w, h, vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::TRANSFER_SRC, )?; let (uv_img, uv_mem, uv_view) = make_plain_image( device, mem_props, vk::Format::R8G8_UNORM, w / 2, h / 2, vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::TRANSFER_SRC, )?; // Descriptor set — Y/UV storage bindings fixed; binding 0 (RGB) rewritten per use. let dsls = [csc_dsl]; let csc_set = device.allocate_descriptor_sets( &vk::DescriptorSetAllocateInfo::default() .descriptor_pool(csc_pool) .set_layouts(&dsls), )?[0]; let y_info = [vk::DescriptorImageInfo::default() .image_view(y_view) .image_layout(vk::ImageLayout::GENERAL)]; let uv_info = [vk::DescriptorImageInfo::default() .image_view(uv_view) .image_layout(vk::ImageLayout::GENERAL)]; device.update_descriptor_sets( &[ vk::WriteDescriptorSet::default() .dst_set(csc_set) .dst_binding(1) .descriptor_type(vk::DescriptorType::STORAGE_IMAGE) .image_info(&y_info), vk::WriteDescriptorSet::default() .dst_set(csc_set) .dst_binding(2) .descriptor_type(vk::DescriptorType::STORAGE_IMAGE) .image_info(&uv_info), ], &[], ); // Bitstream buffer + feedback query. let bs_buf = device.create_buffer( &vk::BufferCreateInfo::default() .size(bs_size) .usage(vk::BufferUsageFlags::VIDEO_ENCODE_DST_KHR) .push_next(profile_list), None, )?; let bs_req = device.get_buffer_memory_requirements(bs_buf); let bs_mem = device.allocate_memory( &vk::MemoryAllocateInfo::default() .allocation_size(bs_req.size) .memory_type_index(find_mem( mem_props, bs_req.memory_type_bits, vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, )), None, )?; device.bind_buffer_memory(bs_buf, bs_mem, 0)?; let mut fb_ci = vk::QueryPoolVideoEncodeFeedbackCreateInfoKHR::default().encode_feedback_flags( vk::VideoEncodeFeedbackFlagsKHR::BITSTREAM_BUFFER_OFFSET | vk::VideoEncodeFeedbackFlagsKHR::BITSTREAM_BYTES_WRITTEN, ); fb_ci.p_next = profile as *const _ as *const c_void; let mut query_ci = vk::QueryPoolCreateInfo::default() .query_type(vk::QueryType::VIDEO_ENCODE_FEEDBACK_KHR) .query_count(1); query_ci.p_next = &fb_ci as *const _ as *const c_void; let query_pool = device.create_query_pool(&query_ci, None)?; // Command buffers + per-frame sync. let cmd = device.allocate_command_buffers( &vk::CommandBufferAllocateInfo::default() .command_pool(cmd_pool) .command_buffer_count(1), )?[0]; let compute_cmd = device.allocate_command_buffers( &vk::CommandBufferAllocateInfo::default() .command_pool(compute_pool) .command_buffer_count(1), )?[0]; let csc_sem = device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None)?; let fence = device.create_fence(&vk::FenceCreateInfo::default(), None)?; Ok(Frame { compute_cmd, cmd, csc_sem, fence, query_pool, bs_buf, bs_mem, csc_set, y_img, y_mem, y_view, uv_img, uv_mem, uv_view, nv12_src, nv12_mem, nv12_view, cpu_img: None, cpu_stage: None, pts_ns: 0, keyframe: false, recovery_anchor: false, }) } /// 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_h265( device: &ash::Device, vq_dev: &ash::khr::video_queue::Device, venc_dev: &ash::khr::video_encode_queue::Device, session: vk::VideoSessionKHR, w: u32, h: u32, rw: u32, rh: u32, ) -> Result<(vk::VideoSessionParametersKHR, Vec)> { use ash::vk::native as hh; let mut ptl: hh::StdVideoH265ProfileTierLevel = std::mem::zeroed(); ptl.flags.set_general_progressive_source_flag(1); ptl.flags.set_general_frame_only_constraint_flag(1); ptl.general_profile_idc = hh::StdVideoH265ProfileIdc_STD_VIDEO_H265_PROFILE_IDC_MAIN; ptl.general_level_idc = hh::StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_6_0; let mut dpbm: hh::StdVideoH265DecPicBufMgr = std::mem::zeroed(); dpbm.max_dec_pic_buffering_minus1[0] = (DPB_SLOTS - 1) as u8; dpbm.max_num_reorder_pics[0] = 0; dpbm.max_latency_increase_plus1[0] = 0; let mut vps: hh::StdVideoH265VideoParameterSet = std::mem::zeroed(); vps.flags.set_vps_temporal_id_nesting_flag(1); vps.flags.set_vps_sub_layer_ordering_info_present_flag(1); vps.pDecPicBufMgr = &dpbm; vps.pProfileTierLevel = &ptl; let mut sps: hh::StdVideoH265SequenceParameterSet = std::mem::zeroed(); sps.flags.set_sps_temporal_id_nesting_flag(1); sps.flags.set_sps_sub_layer_ordering_info_present_flag(1); sps.chroma_format_idc = hh::StdVideoH265ChromaFormatIdc_STD_VIDEO_H265_CHROMA_FORMAT_IDC_420; sps.pic_width_in_luma_samples = w; sps.pic_height_in_luma_samples = h; sps.log2_max_pic_order_cnt_lsb_minus4 = 4; sps.log2_diff_max_min_luma_coding_block_size = 3; sps.log2_diff_max_min_luma_transform_block_size = 3; sps.max_transform_hierarchy_depth_inter = 4; sps.max_transform_hierarchy_depth_intra = 4; sps.pProfileTierLevel = &ptl; sps.pDecPicBufMgr = &dpbm; if w != rw || h != rh { sps.flags.set_conformance_window_flag(1); sps.conf_win_right_offset = (w - rw) / 2; // 4:2:0 SubWidthC = 2 sps.conf_win_bottom_offset = (h - rh) / 2; // 4:2:0 SubHeightC = 2 } let mut pps: hh::StdVideoH265PictureParameterSet = std::mem::zeroed(); pps.flags.set_cu_qp_delta_enabled_flag(1); pps.flags.set_pps_loop_filter_across_slices_enabled_flag(1); let vps_arr = [vps]; let sps_arr = [sps]; let pps_arr = [pps]; let add = vk::VideoEncodeH265SessionParametersAddInfoKHR::default() .std_vp_ss(&vps_arr) .std_sp_ss(&sps_arr) .std_pp_ss(&pps_arr); let mut h265_ci = vk::VideoEncodeH265SessionParametersCreateInfoKHR::default() .max_std_vps_count(1) .max_std_sps_count(1) .max_std_pps_count(1) .parameters_add_info(&add); let ci = vk::VideoSessionParametersCreateInfoKHR::default() .video_session(session) .push_next(&mut h265_ci); 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: {r:?}"); } let mut get_h265 = vk::VideoEncodeH265SessionParametersGetInfoKHR::default() .write_std_vps(true) .write_std_sps(true) .write_std_pps(true) .std_vps_id(0) .std_sps_id(0) .std_pps_id(0); let get = vk::VideoEncodeSessionParametersGetInfoKHR::default() .video_session_parameters(params) .push_next(&mut get_h265); let get_fn = venc_dev.fp().get_encoded_video_session_parameters_khr; let mut fb = vk::VideoEncodeSessionParametersFeedbackInfoKHR::default(); let mut size: usize = 0; let r = get_fn( device.handle(), &get, &mut fb, &mut size, std::ptr::null_mut(), ); if r != vk::Result::SUCCESS { bail!("get header size: {r:?}"); } let mut buf = vec![0u8; size]; let r = get_fn( device.handle(), &get, &mut fb, &mut size, buf.as_mut_ptr() as *mut c_void, ); if r != vk::Result::SUCCESS { bail!("get header bytes: {r:?}"); } buf.truncate(size); 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, 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 { 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 { 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 { 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, Vec)> { 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::{build_h265_rps_s0, pick_recovery_slot, VulkanVideoEncoder}; use crate::capture::{CapturedFrame, FramePayload, PixelFormat}; use crate::encode::{Codec, Encoder}; /// The RFI anchor picker: newest resident wire strictly older than the loss; empty/newer /// slots never qualify. #[test] fn recovery_slot_picks_newest_pre_loss() { // slots hold wires 5..12 (ring position arbitrary); loss starts at 9 → anchor = wire 8. let wires = [8i64, 9, 10, 11, 12, 5, 6, 7]; assert_eq!(pick_recovery_slot(&wires, 9), Some(0)); // loss older than everything resident → no anchor (caller keyframes). assert_eq!(pick_recovery_slot(&wires, 5), None); // empty slots (-1) are skipped. assert_eq!(pick_recovery_slot(&[-1, 3, -1, 4], 5), Some(3)); assert_eq!(pick_recovery_slot(&[-1; 8], 5), None); } /// The full-retention RPS: every resident picture is listed (so the decoder keeps it), the /// setup slot's dying occupant is not, and `used_by_curr_pic` marks exactly the real reference. #[test] fn h265_rps_retains_all_residents() { // Steady state: slots hold POCs 8..15, current POC 16, reconstructing over the slot that // holds POC 8 (the oldest), referencing POC 15 (the newest). let slot_poc = [8i32, 9, 10, 11, 12, 13, 14, 15]; let (n, deltas, used) = build_h265_rps_s0(&slot_poc, 0, 15, 16); assert_eq!(n, 7, "all residents except the dying setup occupant"); // S0 is newest-first with cumulative deltas: POCs 15,14,...,9 → every step is 1. assert_eq!(&deltas[..7], &[0u16; 7], "delta_minus1 chain of 1-steps"); assert_eq!(used, 1 << 0, "only the newest (POC 15) is actively used"); // Recovery shape: reference an OLDER picture (POC 12) while newer residents stay listed. let (n, deltas, used) = build_h265_rps_s0(&slot_poc, 0, 12, 16); assert_eq!(n, 7); assert_eq!(used, 1 << 3, "POC 12 is 4th-newest → S0 index 3"); assert_eq!(&deltas[..7], &[0u16; 7]); // Sparse DPB right after an IDR: only POCs 0..2 resident, gaps encoded in the deltas. let slot_poc = [0i32, 1, 2, -1, -1, -1, -1, -1]; let (n, deltas, used) = build_h265_rps_s0(&slot_poc, 3, 2, 3); assert_eq!(n, 3); assert_eq!(&deltas[..3], &[0, 0, 0]); assert_eq!(used, 1 << 0); // Non-adjacent POCs: current 10, residents {9, 6, 2} → deltas-minus1 {0, 2, 3}. let slot_poc = [2i32, -1, 6, -1, 9, -1, -1, -1]; let (n, deltas, used) = build_h265_rps_s0(&slot_poc, 7, 6, 10); assert_eq!(n, 3); assert_eq!(&deltas[..3], &[0, 2, 3]); assert_eq!(used, 1 << 1, "POC 6 is the 2nd-newest → S0 index 1"); } fn cpu_frame(w: u32, h: u32, pts_ns: u64, fill: [u8; 4]) -> CapturedFrame { let mut buf = vec![0u8; (w * h * 4) as usize]; for px in buf.chunks_exact_mut(4) { px.copy_from_slice(&fill); } CapturedFrame { width: w, height: h, pts_ns, format: PixelFormat::Bgrx, payload: FramePayload::Cpu(buf), } } /// Index of the wire frame the smoke run "loses" and drops from the client-view dump. const SMOKE_LOST: usize = 4; /// Index of the recovery-anchor frame — the RFI fires just before this submission, and one /// normal P (frame 5, referencing the lost frame 4) is encoded IN BETWEEN, mirroring a real /// session where the loss report round-trips while the encoder keeps producing. That fed /// post-loss frame is what makes the dump exercise reference RETENTION: a conforming decoder /// processes its RPS before the anchor arrives, so the anchor's reference (frame 3) survives /// only because every P-frame's RPS lists all resident DPB pictures ([`build_h265_rps_s0`]). const SMOKE_ANCHOR: usize = 6; /// Full `open` → IDR → P-frames → RFI-recovery path through the real [`VulkanVideoEncoder`], /// 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. Wire frame [`SMOKE_LOST`] is "lost", one /// normal P referencing it is still encoded (the in-flight window), then frame [`SMOKE_ANCHOR`] /// is the clean recovery anchor referencing pre-loss frame 3 (no IDR). fn run_smoke(codec: Codec) -> Vec { let env_dim = |k: &str, d: u32| { std::env::var(k) .ok() .and_then(|v| v.parse().ok()) .unwrap_or(d) }; let (w, h) = (env_dim("PF_SMOKE_W", 256), env_dim("PF_SMOKE_H", 256)); let mut enc = VulkanVideoEncoder::open(codec, w, h, 60, 10_000_000).expect("open"); assert!(enc.caps().supports_rfi, "must advertise RFI"); let colors = [ [40u8, 40, 200, 255], [40, 200, 40, 255], [200, 40, 40, 255], [200, 200, 40, 255], [40, 200, 200, 255], [200, 40, 200, 255], [120, 200, 80, 255], [80, 120, 200, 255], ]; let mut aus: Vec = Vec::new(); for (i, c) in colors.iter().enumerate() { if i == SMOKE_ANCHOR { // The client reports wire frame SMOKE_LOST lost → the next frame must re-anchor // on a resident pre-loss reference (newest older than the loss = frame 3). assert!( enc.invalidate_ref_frames(SMOKE_LOST as i64, SMOKE_LOST as i64), "RFI should find an older-than-loss slot" ); } enc.submit_indexed(&cpu_frame(w, h, i as u64 * 16_666_667, *c), i as u32) .expect("submit"); // The encoder is pipelined now: submit() no longer blocks, so drain whatever completed // (FIFO = submission order) and finish the tail via flush below. while let Some(au) = enc.poll().expect("poll") { aus.push(au); } } enc.flush().expect("flush"); while let Some(au) = enc.poll().expect("poll") { aus.push(au); } assert_eq!(aus.len(), colors.len(), "one AU per submitted frame"); let (mut keyframes, mut anchors) = (0usize, 0usize); for (i, au) in aus.iter().enumerate() { assert!(!au.data.is_empty(), "AU {i} empty"); keyframes += au.keyframe as usize; anchors += au.recovery_anchor as usize; if i == 0 { assert!(au.keyframe, "frame 0 must be IDR"); } if i == SMOKE_ANCHOR { assert!( au.recovery_anchor && !au.keyframe, "frame {SMOKE_ANCHOR} must be a clean recovery P-frame, not IDR" ); } } assert_eq!(keyframes, 1, "exactly one IDR (frame 0)"); assert_eq!( anchors, 1, "exactly one recovery anchor (frame {SMOKE_ANCHOR})" ); aus } /// Dump the full stream + a client-view stream with AU [`SMOKE_LOST`] removed to /// `$HOME/vkenc-host-smoke*.{ext}` for an out-of-band `ffmpeg` decode check. The full stream /// must decode 0-error. The dropped one mirrors what a real client feeds its decoder: expect /// exactly ONE missing-reference complaint (frame 5 referencing the lost frame 4 — the /// concealment the client's freeze hides) and NONE at the anchor — a complaint about the /// anchor's reference (frame 3 / POC 3) means reference retention regressed and the "clean" /// re-anchor ships corruption. fn dump_smoke(aus: &[crate::encode::EncodedFrame], ext: &str) { let Ok(home) = std::env::var("HOME") else { return; }; let full: Vec = aus.iter().flat_map(|a| a.data.iter().copied()).collect(); let p1 = format!("{home}/vkenc-host-smoke.{ext}"); let _ = std::fs::write(&p1, &full); eprintln!( "run_smoke: wrote {p1} ({} bytes, {} AUs)", full.len(), aus.len() ); let dropped: Vec = aus .iter() .enumerate() .filter(|(i, _)| *i != SMOKE_LOST) .flat_map(|(_, a)| a.data.iter().copied()) .collect(); let p2 = format!("{home}/vkenc-host-smoke-dropped.{ext}"); let _ = std::fs::write(&p2, &dropped); eprintln!( "run_smoke: wrote {p2} (frame {SMOKE_LOST} dropped; frame 5 conceals, \ recovery@{SMOKE_ANCHOR} anchors to frame 3 and must decode clean)" ); } /// HEVC smoke. `#[ignore]`d so it only runs where a real `VK_KHR_video_encode_h265` driver exists /// — build in the distrobox, run on the host: /// cargo test -p punktfunk-host --features vulkan-encode --no-run /// target/debug/deps/punktfunk_host- --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"); } }