#![allow(unsafe_code)]
use thiserror::Error;
use vulkanalia::vk;
use vulkanalia::vk::{
DeviceV1_0, HasBuilder, InstanceV1_0, KhrVideoQueueExtensionDeviceCommands,
KhrVideoQueueExtensionInstanceCommands,
};
#[derive(Debug, Clone)]
pub struct EncodedFrame {
pub bitstream: Vec<u8>,
pub coded_width: u32,
pub coded_height: u32,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum VulkanEncodeSessionError {
#[error("failed to load the Vulkan loader: {0}")]
Loader(Box<dyn vulkanalia::loader::LoaderError>),
#[error("vkCreateInstance failed: {0:?}")]
CreateInstance(vk::ErrorCode),
#[error("vkEnumeratePhysicalDevices failed: {0:?}")]
EnumeratePhysicalDevices(vk::ErrorCode),
#[error(
"no physical device advertises a VK_VIDEO_CODEC_OPERATION_ENCODE_H264_BIT_KHR queue family"
)]
NoEncodeCapableDevice,
#[error("{call} failed: {result:?}")]
VkCall {
call: &'static str,
result: vk::ErrorCode,
},
#[error("driver reported no VK_KHR_video_encode_queue image format for usage {usage:?}")]
NoVideoFormat {
usage: vk::ImageUsageFlags,
},
#[error("driver's reported min coded extent {width}x{height} is not macroblock-aligned")]
DegenerateCodedExtent {
width: u32,
height: u32,
},
#[error(
"requested {width}x{height} outside driver-reported coded-extent bounds \
{min_width}x{min_height}..={max_width}x{max_height} (granularity \
{granularity_width}x{granularity_height})"
)]
UnsupportedResolution {
width: u32,
height: u32,
min_width: u32,
min_height: u32,
max_width: u32,
max_height: u32,
granularity_width: u32,
granularity_height: u32,
},
#[error(
"no memory type matches requirements (type_bits={type_bits:#x}, required={required:?})"
)]
NoMemoryType {
type_bits: u32,
required: vk::MemoryPropertyFlags,
},
}
pub(crate) struct InstanceGuard {
pub(crate) instance: vulkanalia::Instance,
}
impl Drop for InstanceGuard {
fn drop(&mut self) {
unsafe { self.instance.destroy_instance(None) };
}
}
pub(crate) struct DeviceGuard {
pub(crate) device: vulkanalia::Device,
}
impl Drop for DeviceGuard {
fn drop(&mut self) {
unsafe { self.device.destroy_device(None) };
}
}
#[derive(Default)]
pub(crate) struct SessionResources {
pub(crate) command_pool: vk::CommandPool,
pub(crate) session: vk::VideoSessionKHR,
pub(crate) session_memories: Vec<vk::DeviceMemory>,
pub(crate) session_parameters: vk::VideoSessionParametersKHR,
pub(crate) input_image_view: vk::ImageView,
pub(crate) input_image: vk::Image,
pub(crate) input_image_memory: vk::DeviceMemory,
pub(crate) dpb_image_view: vk::ImageView,
pub(crate) dpb_image: vk::Image,
pub(crate) dpb_image_memory: vk::DeviceMemory,
pub(crate) staging_buffer: vk::Buffer,
pub(crate) staging_memory: vk::DeviceMemory,
pub(crate) dst_buffer: vk::Buffer,
pub(crate) dst_memory: vk::DeviceMemory,
pub(crate) fence: vk::Fence,
pub(crate) encode_feedback_query_pool: vk::QueryPool,
}
impl SessionResources {
pub(crate) fn destroy(&self, device: &vulkanalia::Device) {
unsafe {
device.destroy_query_pool(self.encode_feedback_query_pool, None);
device.destroy_fence(self.fence, None);
device.destroy_buffer(self.dst_buffer, None);
device.free_memory(self.dst_memory, None);
device.destroy_buffer(self.staging_buffer, None);
device.free_memory(self.staging_memory, None);
device.destroy_image_view(self.dpb_image_view, None);
device.destroy_image(self.dpb_image, None);
device.free_memory(self.dpb_image_memory, None);
device.destroy_image_view(self.input_image_view, None);
device.destroy_image(self.input_image, None);
device.free_memory(self.input_image_memory, None);
device.destroy_video_session_parameters_khr(self.session_parameters, None);
for &memory in &self.session_memories {
device.free_memory(memory, None);
}
device.destroy_video_session_khr(self.session, None);
device.destroy_command_pool(self.command_pool, None);
}
}
}
pub(crate) fn create_instance()
-> Result<(vulkanalia::Entry, InstanceGuard), VulkanEncodeSessionError> {
let loader = unsafe { vulkanalia::loader::LibloadingLoader::new(vulkanalia::loader::LIBRARY) }
.map_err(|error| VulkanEncodeSessionError::Loader(error.into()))?;
let entry =
unsafe { vulkanalia::Entry::new(loader) }.map_err(VulkanEncodeSessionError::Loader)?;
let app_info = vk::ApplicationInfo::builder()
.application_name(b"mediaway-encoder-vulkan-session\0")
.api_version(vk::make_version(1, 3, 0));
let create_info = vk::InstanceCreateInfo::builder().application_info(&app_info);
let instance = unsafe { entry.create_instance(&create_info, None) }
.map_err(VulkanEncodeSessionError::CreateInstance)?;
Ok((entry, InstanceGuard { instance }))
}
fn find_encode_device(
instance: &vulkanalia::Instance,
op: vk::VideoCodecOperationFlagsKHR,
) -> Result<(vk::PhysicalDevice, u32), VulkanEncodeSessionError> {
let physical_devices = unsafe { instance.enumerate_physical_devices() }
.map_err(VulkanEncodeSessionError::EnumeratePhysicalDevices)?;
for physical_device in physical_devices {
let family_count =
unsafe { instance.get_physical_device_queue_family_properties(physical_device) }.len();
let mut video_props: Vec<vk::QueueFamilyVideoPropertiesKHR> = (0..family_count)
.map(|_| vk::QueueFamilyVideoPropertiesKHR::default())
.collect();
let mut families2: Vec<vk::QueueFamilyProperties2> = video_props
.iter_mut()
.map(|entry| {
vk::QueueFamilyProperties2::builder()
.push_next(entry)
.build()
})
.collect();
unsafe {
let mut written = u32::try_from(family_count).unwrap_or(u32::MAX);
(instance
.commands()
.get_physical_device_queue_family_properties2)(
physical_device,
&raw mut written,
families2.as_mut_ptr(),
);
}
let found = video_props
.iter()
.position(|p| p.video_codec_operations.contains(op));
if let Some(index) = found {
let queue_family_index = u32::try_from(index).unwrap_or(u32::MAX);
return Ok((physical_device, queue_family_index));
}
}
Err(VulkanEncodeSessionError::NoEncodeCapableDevice)
}
pub(crate) fn find_h264_encode_device(
instance: &vulkanalia::Instance,
) -> Result<(vk::PhysicalDevice, u32), VulkanEncodeSessionError> {
find_encode_device(instance, vk::VideoCodecOperationFlagsKHR::ENCODE_H264)
}
pub(crate) fn find_hevc_encode_device(
instance: &vulkanalia::Instance,
) -> Result<(vk::PhysicalDevice, u32), VulkanEncodeSessionError> {
find_encode_device(instance, vk::VideoCodecOperationFlagsKHR::ENCODE_H265)
}
pub(crate) fn find_av1_encode_device(
instance: &vulkanalia::Instance,
) -> Result<(vk::PhysicalDevice, u32), VulkanEncodeSessionError> {
find_encode_device(instance, vk::VideoCodecOperationFlagsKHR::ENCODE_AV1)
}
pub(crate) enum EncodeProfile {
H264(vk::VideoEncodeH264ProfileInfoKHR),
Hevc(vk::VideoEncodeH265ProfileInfoKHR),
Av1(vk::VideoEncodeAV1ProfileInfoKHR),
}
impl EncodeProfile {
pub(crate) fn new_h264() -> Self {
Self::H264(
vk::VideoEncodeH264ProfileInfoKHR::builder()
.std_profile_idc(h264_params_profile_idc())
.build(),
)
}
pub(crate) fn new_hevc() -> Self {
Self::Hevc(
vk::VideoEncodeH265ProfileInfoKHR::builder()
.std_profile_idc(hevc_params_profile_idc())
.build(),
)
}
pub(crate) fn new_av1() -> Self {
Self::Av1(
vk::VideoEncodeAV1ProfileInfoKHR::builder()
.std_profile(vulkanalia::vk::video::STD_VIDEO_AV1_PROFILE_MAIN)
.build(),
)
}
pub(crate) fn info(&mut self) -> vk::VideoProfileInfoKHR {
let base = vk::VideoProfileInfoKHR::builder()
.chroma_subsampling(vk::VideoChromaSubsamplingFlagsKHR::_420)
.luma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::_8)
.chroma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::_8);
match self {
Self::H264(h264) => base
.video_codec_operation(vk::VideoCodecOperationFlagsKHR::ENCODE_H264)
.push_next(h264)
.build(),
Self::Hevc(hevc) => base
.video_codec_operation(vk::VideoCodecOperationFlagsKHR::ENCODE_H265)
.push_next(hevc)
.build(),
Self::Av1(av1) => base
.video_codec_operation(vk::VideoCodecOperationFlagsKHR::ENCODE_AV1)
.push_next(av1)
.build(),
}
}
}
const fn h264_params_profile_idc() -> vulkanalia::vk::video::StdVideoH264ProfileIdc {
vulkanalia::vk::video::STD_VIDEO_H264_PROFILE_IDC_BASELINE
}
const fn hevc_params_profile_idc() -> vulkanalia::vk::video::StdVideoH265ProfileIdc {
vulkanalia::vk::video::STD_VIDEO_H265_PROFILE_IDC_MAIN
}
pub(crate) struct Capabilities {
pub(crate) min_coded_extent: vk::Extent2D,
pub(crate) max_coded_extent: vk::Extent2D,
pub(crate) picture_access_granularity: vk::Extent2D,
pub(crate) min_bitstream_buffer_size_alignment: vk::DeviceSize,
pub(crate) std_header_version: vk::ExtensionProperties,
}
impl Capabilities {
pub(crate) const fn validate_requested_extent(
&self,
width: u32,
height: u32,
) -> Result<(), VulkanEncodeSessionError> {
let in_range = width >= self.min_coded_extent.width
&& width <= self.max_coded_extent.width
&& height >= self.min_coded_extent.height
&& height <= self.max_coded_extent.height;
let aligned = self.picture_access_granularity.width != 0
&& self.picture_access_granularity.height != 0
&& width % self.picture_access_granularity.width == 0
&& height % self.picture_access_granularity.height == 0;
if in_range && aligned {
return Ok(());
}
Err(VulkanEncodeSessionError::UnsupportedResolution {
width,
height,
min_width: self.min_coded_extent.width,
min_height: self.min_coded_extent.height,
max_width: self.max_coded_extent.width,
max_height: self.max_coded_extent.height,
granularity_width: self.picture_access_granularity.width,
granularity_height: self.picture_access_granularity.height,
})
}
}
pub(crate) fn query_capabilities(
instance: &vulkanalia::Instance,
physical_device: vk::PhysicalDevice,
profile: &mut EncodeProfile,
) -> Result<Capabilities, VulkanEncodeSessionError> {
let is_hevc = matches!(profile, EncodeProfile::Hevc(_));
let is_av1 = matches!(profile, EncodeProfile::Av1(_));
let profile_info = profile.info();
let mut h264_caps = vk::VideoEncodeH264CapabilitiesKHR::default();
let mut hevc_caps = vk::VideoEncodeH265CapabilitiesKHR::default();
let mut av1_caps = vk::VideoEncodeAV1CapabilitiesKHR::default();
let mut encode_caps = vk::VideoEncodeCapabilitiesKHR::default();
let mut caps_builder = vk::VideoCapabilitiesKHR::builder().push_next(&mut encode_caps);
caps_builder = if is_av1 {
caps_builder.push_next(&mut av1_caps)
} else if is_hevc {
caps_builder.push_next(&mut hevc_caps)
} else {
caps_builder.push_next(&mut h264_caps)
};
let mut caps = caps_builder.build();
let result = unsafe {
instance.get_physical_device_video_capabilities_khr(
physical_device,
&profile_info,
&mut caps,
)
};
result.map_err(|result| VulkanEncodeSessionError::VkCall {
call: "vkGetPhysicalDeviceVideoCapabilitiesKHR",
result,
})?;
if caps.min_coded_extent.width % 16 != 0 || caps.min_coded_extent.height % 16 != 0 {
return Err(VulkanEncodeSessionError::DegenerateCodedExtent {
width: caps.min_coded_extent.width,
height: caps.min_coded_extent.height,
});
}
Ok(Capabilities {
min_coded_extent: caps.min_coded_extent,
max_coded_extent: caps.max_coded_extent,
picture_access_granularity: caps.picture_access_granularity,
min_bitstream_buffer_size_alignment: caps.min_bitstream_buffer_size_alignment,
std_header_version: caps.std_header_version,
})
}
pub(crate) fn query_video_format(
instance: &vulkanalia::Instance,
physical_device: vk::PhysicalDevice,
profile: &mut EncodeProfile,
usage: vk::ImageUsageFlags,
) -> Result<vk::Format, VulkanEncodeSessionError> {
let profile_info = profile.info();
let mut profile_list = vk::VideoProfileListInfoKHR::builder()
.profiles(std::slice::from_ref(&profile_info))
.build();
let format_info = vk::PhysicalDeviceVideoFormatInfoKHR::builder()
.image_usage(usage)
.push_next(&mut profile_list)
.build();
let formats = unsafe {
instance.get_physical_device_video_format_properties_khr(physical_device, &format_info)
}
.map_err(|result| VulkanEncodeSessionError::VkCall {
call: "vkGetPhysicalDeviceVideoFormatPropertiesKHR",
result,
})?;
formats
.first()
.map(|f| f.format)
.ok_or(VulkanEncodeSessionError::NoVideoFormat { usage })
}
pub(crate) fn create_logical_device(
instance: &vulkanalia::Instance,
physical_device: vk::PhysicalDevice,
queue_family_index: u32,
) -> Result<DeviceGuard, VulkanEncodeSessionError> {
let queue_priorities = [1.0f32];
let queue_create_infos = [vk::DeviceQueueCreateInfo::builder()
.queue_family_index(queue_family_index)
.queue_priorities(&queue_priorities)
.build()];
let extension_names: [*const std::ffi::c_char; 5] = [
vk::KHR_VIDEO_QUEUE_EXTENSION.name.as_ptr(),
vk::KHR_VIDEO_ENCODE_QUEUE_EXTENSION.name.as_ptr(),
vk::KHR_VIDEO_ENCODE_H264_EXTENSION.name.as_ptr(),
vk::KHR_VIDEO_ENCODE_H265_EXTENSION.name.as_ptr(),
vk::KHR_VIDEO_ENCODE_AV1_EXTENSION.name.as_ptr(),
];
let create_info = vk::DeviceCreateInfo::builder()
.queue_create_infos(&queue_create_infos)
.enabled_extension_names(&extension_names);
let device = unsafe { instance.create_device(physical_device, &create_info, None) }.map_err(
|result| VulkanEncodeSessionError::VkCall {
call: "vkCreateDevice",
result,
},
)?;
Ok(DeviceGuard { device })
}
pub(crate) fn find_memory_type(
memory_properties: &vk::PhysicalDeviceMemoryProperties,
type_bits: u32,
required: vk::MemoryPropertyFlags,
) -> Result<u32, VulkanEncodeSessionError> {
for i in 0..memory_properties.memory_type_count {
let bit_set = (type_bits >> i) & 1 == 1;
let props_match = memory_properties.memory_types[i as usize]
.property_flags
.contains(required);
if bit_set && props_match {
return Ok(i);
}
}
Err(VulkanEncodeSessionError::NoMemoryType {
type_bits,
required,
})
}
pub(crate) struct EncodeDevice<'a> {
pub(crate) device: &'a vulkanalia::Device,
pub(crate) queue: vk::Queue,
pub(crate) queue_family_index: u32,
}