refactor(pf-encode): split vulkan_video's construction tail into vk_build.rs (WP7.5)
ci / web (push) Successful in 56s
ci / rust-arm64 (push) Failing after 2m18s
ci / docs-site (push) Successful in 1m2s
android / android (push) Failing after 4m7s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
apple / swift (push) Successful in 5m21s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 6m10s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 5m40s
deb / build-publish-client-arm64 (push) Successful in 7m57s
docker / build-push-arm64cross (push) Successful in 10s
deb / build-publish (push) Successful in 10m6s
docker / deploy-docs (push) Successful in 24s
deb / build-publish-host (push) Successful in 10m30s
arch / build-publish (push) Successful in 13m57s
windows-host / package (push) Successful in 14m6s
ci / rust (push) Successful in 22m9s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m30s
apple / screenshots (push) Successful in 24m30s
ci / web (push) Successful in 56s
ci / rust-arm64 (push) Failing after 2m18s
ci / docs-site (push) Successful in 1m2s
android / android (push) Failing after 4m7s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
apple / swift (push) Successful in 5m21s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 6m10s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 5m40s
deb / build-publish-client-arm64 (push) Successful in 7m57s
docker / build-push-arm64cross (push) Successful in 10s
deb / build-publish (push) Successful in 10m6s
docker / deploy-docs (push) Successful in 24s
deb / build-publish-host (push) Successful in 10m30s
arch / build-publish (push) Successful in 13m57s
windows-host / package (push) Successful in 14m6s
ci / rust (push) Successful in 22m9s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m30s
apple / screenshots (push) Successful in 24m30s
The ~820-line tail of free builders — make_frame/make_frame_csc/ make_frame_common, make_video_image, probe_rgb_direct, the H.265/AV1 parameter-set writers and the AV1 bit-writer — moves to a #[path] child module, the amf_sys.rs shape: the child sees the parent's private items (Frame and friends), so the split costs zero visibility churn. Six parent-called items went pub(super); five stay child-private (dead_code is per-item and each is used within the child). vulkan_video.rs drops 5,292 → 4,489 lines and the construction unsafe gets its own review surface; steady-state encode logic stays in the parent. ⚠ Trap recorded for future child-module splits: inline `use super::X` statements INSIDE moved fn bodies silently change meaning (super shifts one level) — vk_av1_encode/vk_valve_rgb imports needed crate:: paths. Proven on-glass, not just compiled: all 8 vulkan GPU smokes green under the validation layers on the 780M post-split (H.265 + AV1, RGB-direct, CSC, CPU paths — every moved constructor exercised). nvenc_cuda.rs and qsv.rs are DECLINED the same treatment, with evidence in the handoff doc: no equivalent self-contained seam — their candidate regions are ~150-line loader/accessor clusters interleaved with the encoders' own state types, and a thin-forwarder impl split is exactly the churn a no-defect phase penalizes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<u32> {
|
||||||
|
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<vk::Format>,
|
||||||
|
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<u8>)> {
|
||||||
|
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<u8>,
|
||||||
|
cur: u8,
|
||||||
|
fill: u8,
|
||||||
|
}
|
||||||
|
impl Av1BitWriter {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
buf: Vec::new(),
|
||||||
|
cur: 0,
|
||||||
|
fill: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn bit(&mut self, b: u32) {
|
||||||
|
self.cur = (self.cur << 1) | (b as u8 & 1);
|
||||||
|
self.fill += 1;
|
||||||
|
if self.fill == 8 {
|
||||||
|
self.buf.push(self.cur);
|
||||||
|
self.cur = 0;
|
||||||
|
self.fill = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn put(&mut self, val: u32, bits: u32) {
|
||||||
|
for i in (0..bits).rev() {
|
||||||
|
self.bit((val >> i) & 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Flush, zero-padding the final partial byte (OBU size field delimits the payload).
|
||||||
|
fn finish(mut self) -> Vec<u8> {
|
||||||
|
if self.fill > 0 {
|
||||||
|
self.cur <<= 8 - self.fill;
|
||||||
|
self.buf.push(self.cur);
|
||||||
|
}
|
||||||
|
self.buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AV1 leb128 (little-endian base-128) encoding of an OBU size.
|
||||||
|
fn leb128(mut v: u64) -> Vec<u8> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
loop {
|
||||||
|
let mut byte = (v & 0x7f) as u8;
|
||||||
|
v >>= 7;
|
||||||
|
if v != 0 {
|
||||||
|
byte |= 0x80;
|
||||||
|
}
|
||||||
|
out.push(byte);
|
||||||
|
if v == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bit-pack a `sequence_header_obu` (AV1 spec §5.5) into a size-delimited OBU. The field values here
|
||||||
|
/// MUST mirror the `StdVideoAV1SequenceHeader` handed to the driver in `build_parameters_av1` so the
|
||||||
|
/// driver-emitted frame OBUs parse against this header. Single operating point, 8-bit 4:2:0,
|
||||||
|
/// order-hint on, CDEF+restoration+filter-intra allowed, everything exotic (compound/warp/superres)
|
||||||
|
/// disabled — the profile our single-reference P-frame encoder actually uses.
|
||||||
|
fn av1_sequence_header_obu(
|
||||||
|
sb128: bool,
|
||||||
|
fwb: u32,
|
||||||
|
fhb: u32,
|
||||||
|
max_w_m1: u32,
|
||||||
|
max_h_m1: u32,
|
||||||
|
order_hint_bits_minus_1: u32,
|
||||||
|
seq_level_idx: u32,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut w = Av1BitWriter::new();
|
||||||
|
w.put(0, 3); // seq_profile = MAIN
|
||||||
|
w.bit(0); // still_picture
|
||||||
|
w.bit(0); // reduced_still_picture_header
|
||||||
|
w.bit(0); // timing_info_present_flag
|
||||||
|
w.bit(0); // initial_display_delay_present_flag
|
||||||
|
w.put(0, 5); // operating_points_cnt_minus_1 = 0
|
||||||
|
w.put(0, 12); // operating_point_idc[0]
|
||||||
|
w.put(seq_level_idx, 5); // seq_level_idx[0]
|
||||||
|
if seq_level_idx > 7 {
|
||||||
|
w.bit(0); // seq_tier[0] = 0
|
||||||
|
}
|
||||||
|
w.put(fwb, 4); // frame_width_bits_minus_1
|
||||||
|
w.put(fhb, 4); // frame_height_bits_minus_1
|
||||||
|
w.put(max_w_m1, fwb + 1); // max_frame_width_minus_1
|
||||||
|
w.put(max_h_m1, fhb + 1); // max_frame_height_minus_1
|
||||||
|
w.bit(0); // frame_id_numbers_present_flag
|
||||||
|
w.bit(sb128 as u32); // use_128x128_superblock
|
||||||
|
w.bit(0); // enable_filter_intra
|
||||||
|
w.bit(0); // enable_intra_edge_filter
|
||||||
|
w.bit(0); // enable_interintra_compound
|
||||||
|
w.bit(0); // enable_masked_compound
|
||||||
|
w.bit(0); // enable_warped_motion
|
||||||
|
w.bit(0); // enable_dual_filter
|
||||||
|
w.bit(1); // enable_order_hint
|
||||||
|
w.bit(0); // enable_jnt_comp
|
||||||
|
w.bit(0); // enable_ref_frame_mvs
|
||||||
|
w.bit(1); // seq_choose_screen_content_tools -> seq_force_screen_content_tools = SELECT
|
||||||
|
w.bit(1); // seq_choose_integer_mv -> seq_force_integer_mv = SELECT
|
||||||
|
w.put(order_hint_bits_minus_1, 3); // order_hint_bits_minus_1
|
||||||
|
w.bit(0); // enable_superres
|
||||||
|
w.bit(0); // enable_cdef
|
||||||
|
w.bit(0); // enable_restoration
|
||||||
|
// color_config(): 8-bit 4:2:0, unspecified primaries/transfer/matrix, limited range
|
||||||
|
w.bit(0); // high_bitdepth
|
||||||
|
w.bit(0); // mono_chrome
|
||||||
|
w.bit(0); // color_description_present_flag
|
||||||
|
w.bit(0); // color_range (studio/limited)
|
||||||
|
w.put(0, 2); // chroma_sample_position = CSP_UNKNOWN (subsampling_x==subsampling_y==1 for profile 0)
|
||||||
|
w.bit(0); // separate_uv_delta_q
|
||||||
|
w.bit(0); // film_grain_params_present
|
||||||
|
|
||||||
|
// trailing_bits(): a stop `1` bit then zero-pad to a byte (the size field delimits the OBU, but
|
||||||
|
// the parser still requires the trailing_one_bit — dav1d/cbs reject a plain zero pad).
|
||||||
|
w.bit(1);
|
||||||
|
let payload = w.finish();
|
||||||
|
let mut obu = vec![0x0au8]; // obu_header: type=OBU_SEQUENCE_HEADER(1), has_size_field=1
|
||||||
|
obu.extend_from_slice(&leb128(payload.len() as u64));
|
||||||
|
obu.extend_from_slice(&payload);
|
||||||
|
obu
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AV1 session parameters + header framing. Vulkan AV1 encode emits only the per-frame OBU, so we
|
||||||
|
/// return the app-owned prefixes: a temporal-delimiter OBU that opens every temporal unit
|
||||||
|
/// (`frame_prefix`), and TD + the bit-packed sequence-header OBU for keyframes (`header`).
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
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<u8>, Vec<u8>)> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
@@ -4039,827 +4039,15 @@ impl Drop for VulkanVideoEncoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- free helpers ----------
|
// 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
|
||||||
fn align_up(v: u64, a: u64) -> u64 {
|
// friends), so the split costs no visibility churn.
|
||||||
v.div_ceil(a) * a
|
#[path = "vk_build.rs"]
|
||||||
}
|
mod build;
|
||||||
|
use self::build::{
|
||||||
/// Probe for the RGB-direct encode source (design/vulkan-rgb-direct-encode.md): can this device
|
align_up, build_parameters_av1, build_parameters_h265, make_frame, make_video_image,
|
||||||
/// take the captured RGB dmabuf directly, with the VCN EFC front-end doing the 709-narrow CSC,
|
probe_rgb_direct,
|
||||||
/// 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<u32> {
|
|
||||||
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<vk::Format>,
|
|
||||||
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<u8>)> {
|
|
||||||
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<u8>,
|
|
||||||
cur: u8,
|
|
||||||
fill: u8,
|
|
||||||
}
|
|
||||||
impl Av1BitWriter {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
buf: Vec::new(),
|
|
||||||
cur: 0,
|
|
||||||
fill: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn bit(&mut self, b: u32) {
|
|
||||||
self.cur = (self.cur << 1) | (b as u8 & 1);
|
|
||||||
self.fill += 1;
|
|
||||||
if self.fill == 8 {
|
|
||||||
self.buf.push(self.cur);
|
|
||||||
self.cur = 0;
|
|
||||||
self.fill = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn put(&mut self, val: u32, bits: u32) {
|
|
||||||
for i in (0..bits).rev() {
|
|
||||||
self.bit((val >> i) & 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Flush, zero-padding the final partial byte (OBU size field delimits the payload).
|
|
||||||
fn finish(mut self) -> Vec<u8> {
|
|
||||||
if self.fill > 0 {
|
|
||||||
self.cur <<= 8 - self.fill;
|
|
||||||
self.buf.push(self.cur);
|
|
||||||
}
|
|
||||||
self.buf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// AV1 leb128 (little-endian base-128) encoding of an OBU size.
|
|
||||||
fn leb128(mut v: u64) -> Vec<u8> {
|
|
||||||
let mut out = Vec::new();
|
|
||||||
loop {
|
|
||||||
let mut byte = (v & 0x7f) as u8;
|
|
||||||
v >>= 7;
|
|
||||||
if v != 0 {
|
|
||||||
byte |= 0x80;
|
|
||||||
}
|
|
||||||
out.push(byte);
|
|
||||||
if v == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bit-pack a `sequence_header_obu` (AV1 spec §5.5) into a size-delimited OBU. The field values here
|
|
||||||
/// MUST mirror the `StdVideoAV1SequenceHeader` handed to the driver in `build_parameters_av1` so the
|
|
||||||
/// driver-emitted frame OBUs parse against this header. Single operating point, 8-bit 4:2:0,
|
|
||||||
/// order-hint on, CDEF+restoration+filter-intra allowed, everything exotic (compound/warp/superres)
|
|
||||||
/// disabled — the profile our single-reference P-frame encoder actually uses.
|
|
||||||
fn av1_sequence_header_obu(
|
|
||||||
sb128: bool,
|
|
||||||
fwb: u32,
|
|
||||||
fhb: u32,
|
|
||||||
max_w_m1: u32,
|
|
||||||
max_h_m1: u32,
|
|
||||||
order_hint_bits_minus_1: u32,
|
|
||||||
seq_level_idx: u32,
|
|
||||||
) -> Vec<u8> {
|
|
||||||
let mut w = Av1BitWriter::new();
|
|
||||||
w.put(0, 3); // seq_profile = MAIN
|
|
||||||
w.bit(0); // still_picture
|
|
||||||
w.bit(0); // reduced_still_picture_header
|
|
||||||
w.bit(0); // timing_info_present_flag
|
|
||||||
w.bit(0); // initial_display_delay_present_flag
|
|
||||||
w.put(0, 5); // operating_points_cnt_minus_1 = 0
|
|
||||||
w.put(0, 12); // operating_point_idc[0]
|
|
||||||
w.put(seq_level_idx, 5); // seq_level_idx[0]
|
|
||||||
if seq_level_idx > 7 {
|
|
||||||
w.bit(0); // seq_tier[0] = 0
|
|
||||||
}
|
|
||||||
w.put(fwb, 4); // frame_width_bits_minus_1
|
|
||||||
w.put(fhb, 4); // frame_height_bits_minus_1
|
|
||||||
w.put(max_w_m1, fwb + 1); // max_frame_width_minus_1
|
|
||||||
w.put(max_h_m1, fhb + 1); // max_frame_height_minus_1
|
|
||||||
w.bit(0); // frame_id_numbers_present_flag
|
|
||||||
w.bit(sb128 as u32); // use_128x128_superblock
|
|
||||||
w.bit(0); // enable_filter_intra
|
|
||||||
w.bit(0); // enable_intra_edge_filter
|
|
||||||
w.bit(0); // enable_interintra_compound
|
|
||||||
w.bit(0); // enable_masked_compound
|
|
||||||
w.bit(0); // enable_warped_motion
|
|
||||||
w.bit(0); // enable_dual_filter
|
|
||||||
w.bit(1); // enable_order_hint
|
|
||||||
w.bit(0); // enable_jnt_comp
|
|
||||||
w.bit(0); // enable_ref_frame_mvs
|
|
||||||
w.bit(1); // seq_choose_screen_content_tools -> seq_force_screen_content_tools = SELECT
|
|
||||||
w.bit(1); // seq_choose_integer_mv -> seq_force_integer_mv = SELECT
|
|
||||||
w.put(order_hint_bits_minus_1, 3); // order_hint_bits_minus_1
|
|
||||||
w.bit(0); // enable_superres
|
|
||||||
w.bit(0); // enable_cdef
|
|
||||||
w.bit(0); // enable_restoration
|
|
||||||
// color_config(): 8-bit 4:2:0, unspecified primaries/transfer/matrix, limited range
|
|
||||||
w.bit(0); // high_bitdepth
|
|
||||||
w.bit(0); // mono_chrome
|
|
||||||
w.bit(0); // color_description_present_flag
|
|
||||||
w.bit(0); // color_range (studio/limited)
|
|
||||||
w.put(0, 2); // chroma_sample_position = CSP_UNKNOWN (subsampling_x==subsampling_y==1 for profile 0)
|
|
||||||
w.bit(0); // separate_uv_delta_q
|
|
||||||
w.bit(0); // film_grain_params_present
|
|
||||||
|
|
||||||
// trailing_bits(): a stop `1` bit then zero-pad to a byte (the size field delimits the OBU, but
|
|
||||||
// the parser still requires the trailing_one_bit — dav1d/cbs reject a plain zero pad).
|
|
||||||
w.bit(1);
|
|
||||||
let payload = w.finish();
|
|
||||||
let mut obu = vec![0x0au8]; // obu_header: type=OBU_SEQUENCE_HEADER(1), has_size_field=1
|
|
||||||
obu.extend_from_slice(&leb128(payload.len() as u64));
|
|
||||||
obu.extend_from_slice(&payload);
|
|
||||||
obu
|
|
||||||
}
|
|
||||||
|
|
||||||
/// AV1 session parameters + header framing. Vulkan AV1 encode emits only the per-frame OBU, so we
|
|
||||||
/// return the app-owned prefixes: a temporal-delimiter OBU that opens every temporal unit
|
|
||||||
/// (`frame_prefix`), and TD + the bit-packed sequence-header OBU for keyframes (`header`).
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
unsafe fn build_parameters_av1(
|
|
||||||
device: &ash::Device,
|
|
||||||
vq_dev: &ash::khr::video_queue::Device,
|
|
||||||
session: vk::VideoSessionKHR,
|
|
||||||
w: u32,
|
|
||||||
h: u32,
|
|
||||||
_rw: u32,
|
|
||||||
_rh: u32,
|
|
||||||
max_level: ash::vk::native::StdVideoAV1Level,
|
|
||||||
sb128: bool,
|
|
||||||
quality_level: u32,
|
|
||||||
) -> Result<(vk::VideoSessionParametersKHR, Vec<u8>, Vec<u8>)> {
|
|
||||||
use super::vk_av1_encode as av1;
|
|
||||||
use ash::vk::native as hh;
|
|
||||||
|
|
||||||
let fwb = 31 - w.leading_zeros(); // av_log2(w): enough bits for max_frame_width_minus_1 = w-1
|
|
||||||
let fhb = 31 - h.leading_zeros();
|
|
||||||
let order_hint_bits_minus_1: u32 = 7; // OrderHintBits = 8
|
|
||||||
let seq_level_idx = max_level; // StdVideoAV1Level's numeric value IS the AV1 seq_level_idx
|
|
||||||
|
|
||||||
// ---- Std sequence header (must match the OBU packed below) ----
|
|
||||||
let mut cc_flags: hh::StdVideoAV1ColorConfigFlags = std::mem::zeroed();
|
|
||||||
let _ = &mut cc_flags; // all zero: mono_chrome/color_range/description/separate_uv_delta_q = 0
|
|
||||||
let mut cc: hh::StdVideoAV1ColorConfig = std::mem::zeroed();
|
|
||||||
cc.flags = cc_flags;
|
|
||||||
cc.BitDepth = 8;
|
|
||||||
cc.subsampling_x = 1;
|
|
||||||
cc.subsampling_y = 1;
|
|
||||||
cc.color_primaries = hh::StdVideoAV1ColorPrimaries_STD_VIDEO_AV1_COLOR_PRIMARIES_BT_UNSPECIFIED;
|
|
||||||
cc.transfer_characteristics =
|
|
||||||
hh::StdVideoAV1TransferCharacteristics_STD_VIDEO_AV1_TRANSFER_CHARACTERISTICS_UNSPECIFIED;
|
|
||||||
cc.matrix_coefficients =
|
|
||||||
hh::StdVideoAV1MatrixCoefficients_STD_VIDEO_AV1_MATRIX_COEFFICIENTS_UNSPECIFIED;
|
|
||||||
cc.chroma_sample_position =
|
|
||||||
hh::StdVideoAV1ChromaSamplePosition_STD_VIDEO_AV1_CHROMA_SAMPLE_POSITION_UNKNOWN;
|
|
||||||
|
|
||||||
// Match FFmpeg's Vulkan AV1 encoder (proven on this RADV/VCN path): the ONLY coding tools
|
|
||||||
// enabled are order-hint and (per caps) 128x128 superblocks. CDEF, loop restoration, filter-
|
|
||||||
// intra, warped/compound motion, superres all OFF — enabling them made VCN emit frame-header
|
|
||||||
// sections whose bit layout our sequence header didn't match, desyncing every inter frame.
|
|
||||||
let mut sh_flags: hh::StdVideoAV1SequenceHeaderFlags = std::mem::zeroed();
|
|
||||||
if sb128 {
|
|
||||||
sh_flags.set_use_128x128_superblock(1);
|
|
||||||
}
|
|
||||||
sh_flags.set_enable_order_hint(1);
|
|
||||||
let mut sh: hh::StdVideoAV1SequenceHeader = std::mem::zeroed();
|
|
||||||
sh.flags = sh_flags;
|
|
||||||
sh.seq_profile = hh::StdVideoAV1Profile_STD_VIDEO_AV1_PROFILE_MAIN;
|
|
||||||
sh.frame_width_bits_minus_1 = fwb as u8;
|
|
||||||
sh.frame_height_bits_minus_1 = fhb as u8;
|
|
||||||
sh.max_frame_width_minus_1 = (w - 1) as u16;
|
|
||||||
sh.max_frame_height_minus_1 = (h - 1) as u16;
|
|
||||||
sh.order_hint_bits_minus_1 = order_hint_bits_minus_1 as u8;
|
|
||||||
sh.seq_force_integer_mv = 2; // SELECT
|
|
||||||
sh.seq_force_screen_content_tools = 2; // SELECT
|
|
||||||
sh.pColorConfig = &cc;
|
|
||||||
|
|
||||||
// ---- single operating point conveying the level/tier the driver targets ----
|
|
||||||
let op = av1::StdVideoEncodeAV1OperatingPointInfo {
|
|
||||||
flags: std::mem::zeroed(),
|
|
||||||
operating_point_idc: 0,
|
|
||||||
seq_level_idx: seq_level_idx as u8,
|
|
||||||
seq_tier: 0,
|
|
||||||
decoder_buffer_delay: 0,
|
|
||||||
encoder_buffer_delay: 0,
|
|
||||||
initial_display_delay_minus_1: 0,
|
|
||||||
};
|
|
||||||
let ops = [op];
|
|
||||||
let av1_spci = av1::VideoEncodeAV1SessionParametersCreateInfoKHR {
|
|
||||||
s_type: av1::stype(av1::ST_SESSION_PARAMETERS_CREATE_INFO),
|
|
||||||
p_next: std::ptr::null(),
|
|
||||||
p_std_sequence_header: &sh,
|
|
||||||
p_std_decoder_model_info: std::ptr::null(),
|
|
||||||
std_operating_point_count: 1,
|
|
||||||
p_std_operating_points: ops.as_ptr() as *const c_void,
|
|
||||||
};
|
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
Reference in New Issue
Block a user