diff --git a/crates/pf-encode/src/enc/linux/vk_build.rs b/crates/pf-encode/src/enc/linux/vk_build.rs new file mode 100644 index 00000000..1868167c --- /dev/null +++ b/crates/pf-encode/src/enc/linux/vk_build.rs @@ -0,0 +1,836 @@ +//! Session/frame **construction** for the Vulkan Video encoder — the unsafe builders +//! (`make_frame*`, `make_video_image`, `probe_rgb_direct`) and the parameter-set bitstream +//! writers (`build_parameters_h265`/`_av1`, the AV1 sequence-header OBU). Split from +//! `vulkan_video.rs` (WP7.5) the way `amf_sys.rs` was split from `amf.rs`: a `#[path]` child +//! module, so this file sees the parent's private items (`Frame` and friends) with zero +//! visibility churn, and ~800 lines of construction `unsafe` get their own review surface. +//! Steady-state encode logic stays in the parent. + +// The parent's whole item namespace (Frame, the consts, sibling helpers) — the point of the +// child-module shape. External imports are this file's own; `vk_util` is a crate-root sibling, +// so the path is `crate::`, not the parent-relative `super::` the parent uses. +use super::*; +use crate::vk_util::{find_mem, make_plain_image, make_view}; +use anyhow::{bail, Result}; +use ash::vk; +use std::ffi::c_void; + +pub(super) fn align_up(v: u64, a: u64) -> u64 { + v.div_ceil(a) * a +} + +/// Probe for the RGB-direct encode source (design/vulkan-rgb-direct-encode.md): can this device +/// take the captured RGB dmabuf directly, with the VCN EFC front-end doing the 709-narrow CSC, +/// via `VK_VALVE_video_encode_rgb_conversion` (RADV since Mesa 26.0, gated on EFC hardware)? +/// `Ok((x_offset, y_offset))` carries the chroma-siting bits a session must be created with +/// (the preferred available bit per axis); `Err` is the first missing requirement, logged as +/// the open-time verdict. +pub(super) unsafe fn probe_rgb_direct( + instance: &ash::Instance, + vq_inst: &ash::khr::video_queue::Instance, + pd: vk::PhysicalDevice, + codec_op: vk::VideoCodecOperationFlagsKHR, + av1: bool, +) -> Result<(u32, u32), &'static str> { + use crate::vk_av1_encode as av1b; + use crate::vk_valve_rgb as vrgb; + // 1. The device extension must exist (Mesa >= 26.0 AND the VCN has an EFC block). + let Ok(exts) = instance.enumerate_device_extension_properties(pd) else { + return Err("probe-failed(ext-enum)"); + }; + if !exts + .iter() + .any(|e| std::ffi::CStr::from_ptr(e.extension_name.as_ptr()) == vrgb::EXTENSION_NAME) + { + return Err("no-ext(mesa<26.0-or-no-efc)"); + } + // 2. Feature bit. + let mut feat = vrgb::PhysicalDeviceVideoEncodeRgbConversionFeaturesVALVE { + s_type: vrgb::stype(vrgb::ST_PHYSICAL_DEVICE_FEATURES), + p_next: std::ptr::null_mut(), + video_encode_rgb_conversion: vk::FALSE, + }; + let mut f2 = vk::PhysicalDeviceFeatures2 { + p_next: &mut feat as *mut _ as *mut c_void, + ..Default::default() + }; + instance.get_physical_device_features2(pd, &mut f2); + if feat.video_encode_rgb_conversion == vk::FALSE { + return Err("no-feature"); + } + // 3. Capabilities under the rgb-chained profile — the conversion must cover the compute + // CSC's colour math (rgb2yuv.comp: BT.709, narrow range; chroma siting is looser, see + // below). The profile chain is the same one every rgb-direct consumer presents. + let mut ps = RgbProfileStack::new(codec_op); + let profile = *ps.wire(av1); + let mut rgb_caps = vrgb::VideoEncodeRgbConversionCapabilitiesVALVE { + s_type: vrgb::stype(vrgb::ST_CAPABILITIES), + p_next: std::ptr::null_mut(), + rgb_models: 0, + rgb_ranges: 0, + x_chroma_offsets: 0, + y_chroma_offsets: 0, + }; + 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(); + if av1 { + av1_caps.p_next = &mut rgb_caps as *mut _ as *mut c_void; + enc_caps.p_next = &mut av1_caps as *mut _ as *mut c_void; + } else { + h265_caps.p_next = &mut rgb_caps as *mut _ as *mut c_void; + enc_caps.p_next = &mut h265_caps as *mut _ as *mut c_void; + } + caps.p_next = &mut enc_caps as *mut _ as *mut c_void; + let r = (vq_inst.fp().get_physical_device_video_capabilities_khr)(pd, &profile, &mut caps); + if r != vk::Result::SUCCESS { + return Err("no-rgb-profile(caps)"); + } + // Colour model + range must match the shader exactly (709 narrow). Chroma siting is looser + // BY ON-GLASS FINDING (RADV 26.0.4 / 780M): the VCN EFC advertises x=COSITED_EVEN only — + // the canonical H.26x left-cosited siting — while our 2x2-average shader is midpoint. The + // difference is a half-pel chroma-x phase, imperceptible (and EFC's is arguably the more + // correct one since nothing in our bitstream signals siting). Accept either bit per axis + // and choose the closest to the shader's math: midpoint if offered, else cosited-even. + let pick = |offered: u32| -> Option { + if offered & vrgb::CHROMA_OFFSET_MIDPOINT != 0 { + Some(vrgb::CHROMA_OFFSET_MIDPOINT) + } else if offered & vrgb::CHROMA_OFFSET_COSITED_EVEN != 0 { + Some(vrgb::CHROMA_OFFSET_COSITED_EVEN) + } else { + None + } + }; + if rgb_caps.rgb_models & vrgb::MODEL_YCBCR_709 == 0 + || rgb_caps.rgb_ranges & vrgb::RANGE_NARROW == 0 + { + return Err("no-709-narrow"); + } + let (Some(x_offset), Some(y_offset)) = ( + pick(rgb_caps.x_chroma_offsets), + pick(rgb_caps.y_chroma_offsets), + ) else { + return Err("no-chroma-siting"); + }; + // 4. The encode-src format set under this profile must offer BGRA with DRM-modifier tiling — + // the capture hands LINEAR BGRx dmabufs (fourcc XR24), which import as B8G8R8A8_UNORM. + let profile_arr = [profile]; + let plist = vk::VideoProfileListInfoKHR::default().profiles(&profile_arr); + let mut fmt_info = vk::PhysicalDeviceVideoFormatInfoKHR::default() + .image_usage(vk::ImageUsageFlags::VIDEO_ENCODE_SRC_KHR); + fmt_info.p_next = &plist as *const _ as *const c_void; + let get_fmt = vq_inst.fp().get_physical_device_video_format_properties_khr; + let mut count = 0u32; + let r = get_fmt(pd, &fmt_info, &mut count, std::ptr::null_mut()); + if r != vk::Result::SUCCESS || count == 0 { + return Err("no-rgb-format"); + } + let mut props = vec![vk::VideoFormatPropertiesKHR::default(); count as usize]; + let r = get_fmt(pd, &fmt_info, &mut count, props.as_mut_ptr()); + if r != vk::Result::SUCCESS && r != vk::Result::INCOMPLETE { + return Err("no-rgb-format"); + } + if !props[..count as usize].iter().any(|p| { + p.format == vk::Format::B8G8R8A8_UNORM + && p.image_tiling == vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT + }) { + return Err("no-bgra-modifier-tiling"); + } + Ok((x_offset, y_offset)) +} + +pub(super) 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); + // Unwind on failure: callers (the open path) only ever see the completed pair. + let mem = match 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, + ) { + Ok(m) => m, + Err(e) => { + device.destroy_image(img, None); + return Err(e.into()); + } + }; + if let Err(e) = device.bind_image_memory(img, mem, 0) { + device.destroy_image(img, None); + device.free_memory(mem, None); + return Err(e.into()); + } + 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. +/// +/// Builds in place into `f` — a [`Frame::default`] the caller has already parked in its +/// [`VkTeardown`] guard — so every handle is owned by the unwind the moment it exists and a +/// mid-build failure leaks nothing. +pub(super) 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, + sampler: vk::Sampler, + with_ts: bool, + csc: bool, + pad_fmt: Option, + f: &mut Frame, +) -> Result<()> { + // "no cursor uploaded yet" sentinel — a real serial may be 0 (see `prep_cursor`). + f.cursor_serial = u64::MAX; + // Padded-copy staging (unaligned-mode RGB-direct or native NV12): an aligned encode-src in + // the session's picture format, filled by a transfer blit each frame — concurrent compute + // (copy) + encode (source read). TRANSFER_SRC because the width-padding pass self-copies the + // staging image's own last visible column (see `record_pad_blit`). + if let Some(fmt) = pad_fmt { + (f.pad_img, f.pad_mem) = make_video_image( + device, + mem_props, + fmt, + w, + h, + 1, + vk::ImageUsageFlags::VIDEO_ENCODE_SRC_KHR + | vk::ImageUsageFlags::TRANSFER_DST + | vk::ImageUsageFlags::TRANSFER_SRC, + profile_list, + fams, + )?; + f.pad_view = make_view(device, f.pad_img, fmt, 0)?; + } + // RGB-direct sessions never touch the CSC pipeline: no NV12 encode-src, no Y/UV scratch, no + // cursor overlay, no descriptor set — the encode source is the imported RGB itself (or the + // CPU staging image, built lazily). Their Frame keeps the null handles (teardown-safe). + if csc { + make_frame_csc( + device, + mem_props, + w, + h, + fams, + profile_list, + csc_dsl, + csc_pool, + sampler, + f, + )?; + } + make_frame_common( + device, + mem_props, + profile, + profile_list, + cmd_pool, + compute_pool, + bs_size, + with_ts, + f, + ) +} + +/// The CSC-only half of [`make_frame`]: NV12 encode-src + Y/UV scratch + cursor + descriptors. +#[allow(clippy::too_many_arguments)] +unsafe fn make_frame_csc( + device: &ash::Device, + mem_props: &vk::PhysicalDeviceMemoryProperties, + w: u32, + h: u32, + fams: &[u32], + profile_list: &mut vk::VideoProfileListInfoKHR, + csc_dsl: vk::DescriptorSetLayout, + csc_pool: vk::DescriptorPool, + sampler: vk::Sampler, + f: &mut Frame, +) -> Result<()> { + // NV12 encode-src (filled by the CSC copy) — concurrent compute+encode. + (f.nv12_src, f.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, + )?; + f.nv12_view = make_view(device, f.nv12_src, NV12, 0)?; + // CSC scratch (Y R8 full-res, UV RG8 half-res). + (f.y_img, f.y_mem, f.y_view) = make_plain_image( + device, + mem_props, + vk::Format::R8_UNORM, + w, + h, + vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::TRANSFER_SRC, + )?; + (f.uv_img, f.uv_mem, f.uv_view) = make_plain_image( + device, + mem_props, + vk::Format::R8G8_UNORM, + w / 2, + h / 2, + vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::TRANSFER_SRC, + )?; + // Cursor overlay: fixed CURSOR_MAX² RGBA8 sampled image + host staging (cursor-as-metadata). The + // view/descriptor is static (bound at binding 3 below); only the image *content* changes, and + // only when the pointer bitmap does — see `prep_cursor`. + (f.cursor_img, f.cursor_mem, f.cursor_view) = make_plain_image( + device, + mem_props, + vk::Format::R8G8B8A8_UNORM, + CURSOR_MAX, + CURSOR_MAX, + vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST, + )?; + f.cursor_stage = device.create_buffer( + &vk::BufferCreateInfo::default() + .size((CURSOR_MAX * CURSOR_MAX * 4) as u64) + .usage(vk::BufferUsageFlags::TRANSFER_SRC), + None, + )?; + let cs_req = device.get_buffer_memory_requirements(f.cursor_stage); + f.cursor_stage_mem = device.allocate_memory( + &vk::MemoryAllocateInfo::default() + .allocation_size(cs_req.size) + .memory_type_index(find_mem( + mem_props, + cs_req.memory_type_bits, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, + )), + None, + )?; + device.bind_buffer_memory(f.cursor_stage, f.cursor_stage_mem, 0)?; + // Descriptor set — Y/UV storage bindings fixed; binding 0 (RGB) rewritten per use; binding 3 + // (cursor) points at the static cursor image (its layout is SHADER_READ_ONLY once prepped). + let dsls = [csc_dsl]; + f.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(f.y_view) + .image_layout(vk::ImageLayout::GENERAL)]; + let uv_info = [vk::DescriptorImageInfo::default() + .image_view(f.uv_view) + .image_layout(vk::ImageLayout::GENERAL)]; + let cur_info = [vk::DescriptorImageInfo::default() + .sampler(sampler) + .image_view(f.cursor_view) + .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)]; + device.update_descriptor_sets( + &[ + vk::WriteDescriptorSet::default() + .dst_set(f.csc_set) + .dst_binding(1) + .descriptor_type(vk::DescriptorType::STORAGE_IMAGE) + .image_info(&y_info), + vk::WriteDescriptorSet::default() + .dst_set(f.csc_set) + .dst_binding(2) + .descriptor_type(vk::DescriptorType::STORAGE_IMAGE) + .image_info(&uv_info), + vk::WriteDescriptorSet::default() + .dst_set(f.csc_set) + .dst_binding(3) + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .image_info(&cur_info), + ], + &[], + ); + Ok(()) +} + +/// The mode-independent half of [`make_frame`]: bitstream buffer (+ persistent map), feedback +/// query, optional timestamp pool, command buffers and sync objects. +#[allow(clippy::too_many_arguments)] +unsafe fn make_frame_common( + device: &ash::Device, + mem_props: &vk::PhysicalDeviceMemoryProperties, + profile: &vk::VideoProfileInfoKHR, + profile_list: &mut vk::VideoProfileListInfoKHR, + cmd_pool: vk::CommandPool, + compute_pool: vk::CommandPool, + bs_size: u64, + with_ts: bool, + f: &mut Frame, +) -> Result<()> { + // Bitstream buffer + feedback query. + f.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(f.bs_buf); + f.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(f.bs_buf, f.bs_mem, 0)?; + // Map once for the slot's lifetime — read_slot copies AUs straight out of this (coherent + // memory, no per-frame map/unmap); vkFreeMemory implicitly unmaps at teardown. + f.bs_ptr = BsPtr( + device.map_memory(f.bs_mem, 0, vk::WHOLE_SIZE, vk::MemoryMapFlags::empty())? as *const u8, + ); + // PUNKTFUNK_PERF: a 2-slot timestamp pool bracketing this slot's compute batch (CSC split). + if with_ts { + f.ts_pool = device.create_query_pool( + &vk::QueryPoolCreateInfo::default() + .query_type(vk::QueryType::TIMESTAMP) + .query_count(2), + None, + )?; + } + 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; + f.query_pool = device.create_query_pool(&query_ci, None)?; + // Command buffers + per-frame sync. + f.cmd = device.allocate_command_buffers( + &vk::CommandBufferAllocateInfo::default() + .command_pool(cmd_pool) + .command_buffer_count(1), + )?[0]; + f.compute_cmd = device.allocate_command_buffers( + &vk::CommandBufferAllocateInfo::default() + .command_pool(compute_pool) + .command_buffer_count(1), + )?[0]; + f.csc_sem = device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None)?; + f.fence = device.create_fence(&vk::FenceCreateInfo::default(), None)?; + Ok(()) +} + +/// 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. +pub(super) 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, + quality_level: 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); + // Bake the session's quality level into the parameters object — the spec requires it to match + // the level the first frame's ENCODE_QUALITY_LEVEL control installs. + let mut q_info = vk::VideoEncodeQualityLevelInfoKHR::default().quality_level(quality_level); + let ci = vk::VideoSessionParametersCreateInfoKHR::default() + .video_session(session) + .push_next(&mut h265_ci) + .push_next(&mut q_info); + 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 { + // `params` is live but not yet the caller's guard's to unwind — destroy before bailing. + (vq_dev.fp().destroy_video_session_parameters_khr)( + device.handle(), + params, + std::ptr::null(), + ); + 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 { + (vq_dev.fp().destroy_video_session_parameters_khr)( + device.handle(), + params, + std::ptr::null(), + ); + 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)] +pub(super) 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, + quality_level: u32, +) -> Result<(vk::VideoSessionParametersKHR, Vec, Vec)> { + use crate::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, + }; + // Bake the session's quality level into the parameters object (must match the level the first + // frame's ENCODE_QUALITY_LEVEL control installs); chained raw ahead of the vendored AV1 struct. + let mut q_info = vk::VideoEncodeQualityLevelInfoKHR::default().quality_level(quality_level); + q_info.p_next = &av1_spci as *const _ as *const c_void; + let mut ci = vk::VideoSessionParametersCreateInfoKHR::default().video_session(session); + ci.p_next = &q_info 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)) +} diff --git a/crates/pf-encode/src/enc/linux/vulkan_video.rs b/crates/pf-encode/src/enc/linux/vulkan_video.rs index 676460fa..4a0de4d1 100644 --- a/crates/pf-encode/src/enc/linux/vulkan_video.rs +++ b/crates/pf-encode/src/enc/linux/vulkan_video.rs @@ -4039,827 +4039,15 @@ impl Drop for VulkanVideoEncoder { } } -// ---------- free helpers ---------- - -fn align_up(v: u64, a: u64) -> u64 { - v.div_ceil(a) * a -} - -/// Probe for the RGB-direct encode source (design/vulkan-rgb-direct-encode.md): can this device -/// take the captured RGB dmabuf directly, with the VCN EFC front-end doing the 709-narrow CSC, -/// via `VK_VALVE_video_encode_rgb_conversion` (RADV since Mesa 26.0, gated on EFC hardware)? -/// `Ok((x_offset, y_offset))` carries the chroma-siting bits a session must be created with -/// (the preferred available bit per axis); `Err` is the first missing requirement, logged as -/// the open-time verdict. -unsafe fn probe_rgb_direct( - instance: &ash::Instance, - vq_inst: &ash::khr::video_queue::Instance, - pd: vk::PhysicalDevice, - codec_op: vk::VideoCodecOperationFlagsKHR, - av1: bool, -) -> Result<(u32, u32), &'static str> { - use super::vk_av1_encode as av1b; - use super::vk_valve_rgb as vrgb; - // 1. The device extension must exist (Mesa >= 26.0 AND the VCN has an EFC block). - let Ok(exts) = instance.enumerate_device_extension_properties(pd) else { - return Err("probe-failed(ext-enum)"); - }; - if !exts - .iter() - .any(|e| std::ffi::CStr::from_ptr(e.extension_name.as_ptr()) == vrgb::EXTENSION_NAME) - { - return Err("no-ext(mesa<26.0-or-no-efc)"); - } - // 2. Feature bit. - let mut feat = vrgb::PhysicalDeviceVideoEncodeRgbConversionFeaturesVALVE { - s_type: vrgb::stype(vrgb::ST_PHYSICAL_DEVICE_FEATURES), - p_next: std::ptr::null_mut(), - video_encode_rgb_conversion: vk::FALSE, - }; - let mut f2 = vk::PhysicalDeviceFeatures2 { - p_next: &mut feat as *mut _ as *mut c_void, - ..Default::default() - }; - instance.get_physical_device_features2(pd, &mut f2); - if feat.video_encode_rgb_conversion == vk::FALSE { - return Err("no-feature"); - } - // 3. Capabilities under the rgb-chained profile — the conversion must cover the compute - // CSC's colour math (rgb2yuv.comp: BT.709, narrow range; chroma siting is looser, see - // below). The profile chain is the same one every rgb-direct consumer presents. - let mut ps = RgbProfileStack::new(codec_op); - let profile = *ps.wire(av1); - let mut rgb_caps = vrgb::VideoEncodeRgbConversionCapabilitiesVALVE { - s_type: vrgb::stype(vrgb::ST_CAPABILITIES), - p_next: std::ptr::null_mut(), - rgb_models: 0, - rgb_ranges: 0, - x_chroma_offsets: 0, - y_chroma_offsets: 0, - }; - 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(); - if av1 { - av1_caps.p_next = &mut rgb_caps as *mut _ as *mut c_void; - enc_caps.p_next = &mut av1_caps as *mut _ as *mut c_void; - } else { - h265_caps.p_next = &mut rgb_caps as *mut _ as *mut c_void; - enc_caps.p_next = &mut h265_caps as *mut _ as *mut c_void; - } - caps.p_next = &mut enc_caps as *mut _ as *mut c_void; - let r = (vq_inst.fp().get_physical_device_video_capabilities_khr)(pd, &profile, &mut caps); - if r != vk::Result::SUCCESS { - return Err("no-rgb-profile(caps)"); - } - // Colour model + range must match the shader exactly (709 narrow). Chroma siting is looser - // BY ON-GLASS FINDING (RADV 26.0.4 / 780M): the VCN EFC advertises x=COSITED_EVEN only — - // the canonical H.26x left-cosited siting — while our 2x2-average shader is midpoint. The - // difference is a half-pel chroma-x phase, imperceptible (and EFC's is arguably the more - // correct one since nothing in our bitstream signals siting). Accept either bit per axis - // and choose the closest to the shader's math: midpoint if offered, else cosited-even. - let pick = |offered: u32| -> Option { - if offered & vrgb::CHROMA_OFFSET_MIDPOINT != 0 { - Some(vrgb::CHROMA_OFFSET_MIDPOINT) - } else if offered & vrgb::CHROMA_OFFSET_COSITED_EVEN != 0 { - Some(vrgb::CHROMA_OFFSET_COSITED_EVEN) - } else { - None - } - }; - if rgb_caps.rgb_models & vrgb::MODEL_YCBCR_709 == 0 - || rgb_caps.rgb_ranges & vrgb::RANGE_NARROW == 0 - { - return Err("no-709-narrow"); - } - let (Some(x_offset), Some(y_offset)) = ( - pick(rgb_caps.x_chroma_offsets), - pick(rgb_caps.y_chroma_offsets), - ) else { - return Err("no-chroma-siting"); - }; - // 4. The encode-src format set under this profile must offer BGRA with DRM-modifier tiling — - // the capture hands LINEAR BGRx dmabufs (fourcc XR24), which import as B8G8R8A8_UNORM. - let profile_arr = [profile]; - let plist = vk::VideoProfileListInfoKHR::default().profiles(&profile_arr); - let mut fmt_info = vk::PhysicalDeviceVideoFormatInfoKHR::default() - .image_usage(vk::ImageUsageFlags::VIDEO_ENCODE_SRC_KHR); - fmt_info.p_next = &plist as *const _ as *const c_void; - let get_fmt = vq_inst.fp().get_physical_device_video_format_properties_khr; - let mut count = 0u32; - let r = get_fmt(pd, &fmt_info, &mut count, std::ptr::null_mut()); - if r != vk::Result::SUCCESS || count == 0 { - return Err("no-rgb-format"); - } - let mut props = vec![vk::VideoFormatPropertiesKHR::default(); count as usize]; - let r = get_fmt(pd, &fmt_info, &mut count, props.as_mut_ptr()); - if r != vk::Result::SUCCESS && r != vk::Result::INCOMPLETE { - return Err("no-rgb-format"); - } - if !props[..count as usize].iter().any(|p| { - p.format == vk::Format::B8G8R8A8_UNORM - && p.image_tiling == vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT - }) { - return Err("no-bgra-modifier-tiling"); - } - Ok((x_offset, y_offset)) -} - -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); - // Unwind on failure: callers (the open path) only ever see the completed pair. - let mem = match 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, - ) { - Ok(m) => m, - Err(e) => { - device.destroy_image(img, None); - return Err(e.into()); - } - }; - if let Err(e) = device.bind_image_memory(img, mem, 0) { - device.destroy_image(img, None); - device.free_memory(mem, None); - return Err(e.into()); - } - 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. -/// -/// Builds in place into `f` — a [`Frame::default`] the caller has already parked in its -/// [`VkTeardown`] guard — so every handle is owned by the unwind the moment it exists and a -/// mid-build failure leaks nothing. -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, - sampler: vk::Sampler, - with_ts: bool, - csc: bool, - pad_fmt: Option, - f: &mut Frame, -) -> Result<()> { - // "no cursor uploaded yet" sentinel — a real serial may be 0 (see `prep_cursor`). - f.cursor_serial = u64::MAX; - // Padded-copy staging (unaligned-mode RGB-direct or native NV12): an aligned encode-src in - // the session's picture format, filled by a transfer blit each frame — concurrent compute - // (copy) + encode (source read). TRANSFER_SRC because the width-padding pass self-copies the - // staging image's own last visible column (see `record_pad_blit`). - if let Some(fmt) = pad_fmt { - (f.pad_img, f.pad_mem) = make_video_image( - device, - mem_props, - fmt, - w, - h, - 1, - vk::ImageUsageFlags::VIDEO_ENCODE_SRC_KHR - | vk::ImageUsageFlags::TRANSFER_DST - | vk::ImageUsageFlags::TRANSFER_SRC, - profile_list, - fams, - )?; - f.pad_view = make_view(device, f.pad_img, fmt, 0)?; - } - // RGB-direct sessions never touch the CSC pipeline: no NV12 encode-src, no Y/UV scratch, no - // cursor overlay, no descriptor set — the encode source is the imported RGB itself (or the - // CPU staging image, built lazily). Their Frame keeps the null handles (teardown-safe). - if csc { - make_frame_csc( - device, - mem_props, - w, - h, - fams, - profile_list, - csc_dsl, - csc_pool, - sampler, - f, - )?; - } - make_frame_common( - device, - mem_props, - profile, - profile_list, - cmd_pool, - compute_pool, - bs_size, - with_ts, - f, - ) -} - -/// The CSC-only half of [`make_frame`]: NV12 encode-src + Y/UV scratch + cursor + descriptors. -#[allow(clippy::too_many_arguments)] -unsafe fn make_frame_csc( - device: &ash::Device, - mem_props: &vk::PhysicalDeviceMemoryProperties, - w: u32, - h: u32, - fams: &[u32], - profile_list: &mut vk::VideoProfileListInfoKHR, - csc_dsl: vk::DescriptorSetLayout, - csc_pool: vk::DescriptorPool, - sampler: vk::Sampler, - f: &mut Frame, -) -> Result<()> { - // NV12 encode-src (filled by the CSC copy) — concurrent compute+encode. - (f.nv12_src, f.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, - )?; - f.nv12_view = make_view(device, f.nv12_src, NV12, 0)?; - // CSC scratch (Y R8 full-res, UV RG8 half-res). - (f.y_img, f.y_mem, f.y_view) = make_plain_image( - device, - mem_props, - vk::Format::R8_UNORM, - w, - h, - vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::TRANSFER_SRC, - )?; - (f.uv_img, f.uv_mem, f.uv_view) = make_plain_image( - device, - mem_props, - vk::Format::R8G8_UNORM, - w / 2, - h / 2, - vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::TRANSFER_SRC, - )?; - // Cursor overlay: fixed CURSOR_MAX² RGBA8 sampled image + host staging (cursor-as-metadata). The - // view/descriptor is static (bound at binding 3 below); only the image *content* changes, and - // only when the pointer bitmap does — see `prep_cursor`. - (f.cursor_img, f.cursor_mem, f.cursor_view) = make_plain_image( - device, - mem_props, - vk::Format::R8G8B8A8_UNORM, - CURSOR_MAX, - CURSOR_MAX, - vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST, - )?; - f.cursor_stage = device.create_buffer( - &vk::BufferCreateInfo::default() - .size((CURSOR_MAX * CURSOR_MAX * 4) as u64) - .usage(vk::BufferUsageFlags::TRANSFER_SRC), - None, - )?; - let cs_req = device.get_buffer_memory_requirements(f.cursor_stage); - f.cursor_stage_mem = device.allocate_memory( - &vk::MemoryAllocateInfo::default() - .allocation_size(cs_req.size) - .memory_type_index(find_mem( - mem_props, - cs_req.memory_type_bits, - vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, - )), - None, - )?; - device.bind_buffer_memory(f.cursor_stage, f.cursor_stage_mem, 0)?; - // Descriptor set — Y/UV storage bindings fixed; binding 0 (RGB) rewritten per use; binding 3 - // (cursor) points at the static cursor image (its layout is SHADER_READ_ONLY once prepped). - let dsls = [csc_dsl]; - f.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(f.y_view) - .image_layout(vk::ImageLayout::GENERAL)]; - let uv_info = [vk::DescriptorImageInfo::default() - .image_view(f.uv_view) - .image_layout(vk::ImageLayout::GENERAL)]; - let cur_info = [vk::DescriptorImageInfo::default() - .sampler(sampler) - .image_view(f.cursor_view) - .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)]; - device.update_descriptor_sets( - &[ - vk::WriteDescriptorSet::default() - .dst_set(f.csc_set) - .dst_binding(1) - .descriptor_type(vk::DescriptorType::STORAGE_IMAGE) - .image_info(&y_info), - vk::WriteDescriptorSet::default() - .dst_set(f.csc_set) - .dst_binding(2) - .descriptor_type(vk::DescriptorType::STORAGE_IMAGE) - .image_info(&uv_info), - vk::WriteDescriptorSet::default() - .dst_set(f.csc_set) - .dst_binding(3) - .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) - .image_info(&cur_info), - ], - &[], - ); - Ok(()) -} - -/// The mode-independent half of [`make_frame`]: bitstream buffer (+ persistent map), feedback -/// query, optional timestamp pool, command buffers and sync objects. -#[allow(clippy::too_many_arguments)] -unsafe fn make_frame_common( - device: &ash::Device, - mem_props: &vk::PhysicalDeviceMemoryProperties, - profile: &vk::VideoProfileInfoKHR, - profile_list: &mut vk::VideoProfileListInfoKHR, - cmd_pool: vk::CommandPool, - compute_pool: vk::CommandPool, - bs_size: u64, - with_ts: bool, - f: &mut Frame, -) -> Result<()> { - // Bitstream buffer + feedback query. - f.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(f.bs_buf); - f.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(f.bs_buf, f.bs_mem, 0)?; - // Map once for the slot's lifetime — read_slot copies AUs straight out of this (coherent - // memory, no per-frame map/unmap); vkFreeMemory implicitly unmaps at teardown. - f.bs_ptr = BsPtr( - device.map_memory(f.bs_mem, 0, vk::WHOLE_SIZE, vk::MemoryMapFlags::empty())? as *const u8, - ); - // PUNKTFUNK_PERF: a 2-slot timestamp pool bracketing this slot's compute batch (CSC split). - if with_ts { - f.ts_pool = device.create_query_pool( - &vk::QueryPoolCreateInfo::default() - .query_type(vk::QueryType::TIMESTAMP) - .query_count(2), - None, - )?; - } - 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; - f.query_pool = device.create_query_pool(&query_ci, None)?; - // Command buffers + per-frame sync. - f.cmd = device.allocate_command_buffers( - &vk::CommandBufferAllocateInfo::default() - .command_pool(cmd_pool) - .command_buffer_count(1), - )?[0]; - f.compute_cmd = device.allocate_command_buffers( - &vk::CommandBufferAllocateInfo::default() - .command_pool(compute_pool) - .command_buffer_count(1), - )?[0]; - f.csc_sem = device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None)?; - f.fence = device.create_fence(&vk::FenceCreateInfo::default(), None)?; - Ok(()) -} - -/// 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, - quality_level: 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); - // Bake the session's quality level into the parameters object — the spec requires it to match - // the level the first frame's ENCODE_QUALITY_LEVEL control installs. - let mut q_info = vk::VideoEncodeQualityLevelInfoKHR::default().quality_level(quality_level); - let ci = vk::VideoSessionParametersCreateInfoKHR::default() - .video_session(session) - .push_next(&mut h265_ci) - .push_next(&mut q_info); - 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 { - // `params` is live but not yet the caller's guard's to unwind — destroy before bailing. - (vq_dev.fp().destroy_video_session_parameters_khr)( - device.handle(), - params, - std::ptr::null(), - ); - 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 { - (vq_dev.fp().destroy_video_session_parameters_khr)( - device.handle(), - params, - std::ptr::null(), - ); - 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, - quality_level: u32, -) -> 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, - }; - // Bake the session's quality level into the parameters object (must match the level the first - // frame's ENCODE_QUALITY_LEVEL control installs); chained raw ahead of the vendored AV1 struct. - let mut q_info = vk::VideoEncodeQualityLevelInfoKHR::default().quality_level(quality_level); - q_info.p_next = &av1_spci as *const _ as *const c_void; - let mut ci = vk::VideoSessionParametersCreateInfoKHR::default().video_session(session); - ci.p_next = &q_info 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)) -} +// Session/frame construction + parameter-set builders live in `vk_build.rs` (WP7.5) — the +// amf_sys.rs shape: a #[path] child module sees this file's private items (Frame and +// friends), so the split costs no visibility churn. +#[path = "vk_build.rs"] +mod build; +use self::build::{ + align_up, build_parameters_av1, build_parameters_h265, make_frame, make_video_image, + probe_rgb_direct, +}; #[cfg(test)] mod tests {