From dc5ad49bd6d94b9baa672cd62cbbb29759c026d4 Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Mon, 23 Feb 2026 23:25:38 +0100 Subject: [PATCH 1/7] Add support for AMD. --- src/encoder/h264/encode.rs | 95 ++++---- src/encoder/h264/init.rs | 116 ++++++---- src/encoder/h264/mod.rs | 98 ++++---- src/encoder/h264/upload.rs | 39 ---- src/encoder/h265/encode.rs | 79 ++++--- src/encoder/h265/init.rs | 144 ++++++++++-- src/encoder/h265/mod.rs | 113 +++------ src/encoder/h265/upload.rs | 41 ---- src/encoder/resources.rs | 455 +++++++++++++++++++++++++++++++++---- src/image.rs | 2 + src/vulkan.rs | 45 +++- 11 files changed, 809 insertions(+), 418 deletions(-) delete mode 100644 src/encoder/h264/upload.rs delete mode 100644 src/encoder/h265/upload.rs diff --git a/src/encoder/h264/encode.rs b/src/encoder/h264/encode.rs index 472e7b2..dcf57f7 100644 --- a/src/encoder/h264/encode.rs +++ b/src/encoder/h264/encode.rs @@ -1,6 +1,7 @@ -use super::{H264Encoder, MIN_BITSTREAM_BUFFER_SIZE}; +use super::H264Encoder; use crate::encoder::gop::{GopFrameType, GopPosition}; +use crate::encoder::resources::{record_dpb_barriers, MIN_BITSTREAM_BUFFER_SIZE}; use crate::error::{PixelForgeError, Result}; use ash::vk; use tracing::debug; @@ -28,9 +29,9 @@ impl H264Encoder { // Rate control setup. let (rc_mode, average_bitrate, max_bitrate, qp) = match self.config.rate_control_mode { crate::encoder::RateControlMode::Cqp | crate::encoder::RateControlMode::Disabled => ( - vk::VideoEncodeRateControlModeFlagsKHR::VBR, - 100_000_000, // 100 Mbps - 100_000_000, + vk::VideoEncodeRateControlModeFlagsKHR::DISABLED, + 0, + 0, self.config.quality_level as i32, ), crate::encoder::RateControlMode::Cbr => ( @@ -77,32 +78,16 @@ impl H264Encoder { ); } - // Transition DPB image to video encode DPB layout if needed. - let dpb_barrier = vk::ImageMemoryBarrier::default() - .old_layout(vk::ImageLayout::UNDEFINED) - .new_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR) - .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) - .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) - .image(self.dpb_images[self.current_dpb_slot as usize]) - .subresource_range(vk::ImageSubresourceRange { - aspect_mask: vk::ImageAspectFlags::COLOR, - base_mip_level: 0, - level_count: 1, - base_array_layer: 0, - layer_count: 1, - }) - .src_access_mask(vk::AccessFlags::empty()) - .dst_access_mask(vk::AccessFlags::empty()); - + // Transition DPB images for encode. + let ref_dpb_slots: Vec = self.l0_references.iter().map(|r| r.dpb_slot).collect(); unsafe { - self.context.device().cmd_pipeline_barrier( + record_dpb_barriers( + self.context.device(), self.encode_command_buffer, - vk::PipelineStageFlags::TOP_OF_PIPE, - vk::PipelineStageFlags::BOTTOM_OF_PIPE, // Use BOTTOM_OF_PIPE as VIDEO_ENCODE requires sync2 - vk::DependencyFlags::empty(), - &[], - &[], - &[dpb_barrier], + &self.dpb_images, + self.use_layered_dpb, + self.current_dpb_slot, + &ref_dpb_slots, ); } @@ -135,7 +120,7 @@ impl H264Encoder { let slice_qp_delta = match self.config.rate_control_mode { crate::encoder::RateControlMode::Cqp | crate::encoder::RateControlMode::Disabled => { - ((self.config.quality_level as i32) - 26) as i8 + (self.config.quality_level as i32 - 26) as i8 } _ => 0, }; @@ -150,7 +135,7 @@ impl H264Encoder { reserved1: 0, cabac_init_idc: ash::vk::native::StdVideoH264CabacInitIdc_STD_VIDEO_H264_CABAC_INIT_IDC_0, - disable_deblocking_filter_idc: ash::vk::native::StdVideoH264DisableDeblockingFilterIdc_STD_VIDEO_H264_DISABLE_DEBLOCKING_FILTER_IDC_DISABLED, + disable_deblocking_filter_idc: ash::vk::native::StdVideoH264DisableDeblockingFilterIdc_STD_VIDEO_H264_DISABLE_DEBLOCKING_FILTER_IDC_ENABLED, pWeightTable: std::ptr::null(), }; @@ -278,9 +263,9 @@ impl H264Encoder { }; // Create slice NAL unit entry. - // constant_qp should only be set when rate control is DISABLED + // constant_qp should only be set when rate control is DISABLED. let constant_qp = if rc_mode == vk::VideoEncodeRateControlModeFlagsKHR::DISABLED { - self.config.quality_level as i32 + qp } else { 0 }; @@ -297,8 +282,8 @@ impl H264Encoder { let src_picture_resource = vk::VideoPictureResourceInfoKHR::default() .coded_offset(vk::Offset2D { x: 0, y: 0 }) .coded_extent(vk::Extent2D { - width: self.config.dimensions.width, - height: self.config.dimensions.height, + width: self.aligned_width, + height: self.aligned_height, }) .base_array_layer(0) .image_view_binding(self.input_image_view); @@ -307,8 +292,8 @@ impl H264Encoder { let setup_picture_resource = vk::VideoPictureResourceInfoKHR::default() .coded_offset(vk::Offset2D { x: 0, y: 0 }) .coded_extent(vk::Extent2D { - width: self.config.dimensions.width, - height: self.config.dimensions.height, + width: self.aligned_width, + height: self.aligned_height, }) .base_array_layer(0) .image_view_binding(self.dpb_image_views[self.current_dpb_slot as usize]); @@ -333,8 +318,8 @@ impl H264Encoder { vk::VideoPictureResourceInfoKHR::default() .coded_offset(vk::Offset2D { x: 0, y: 0 }) .coded_extent(vk::Extent2D { - width: self.config.dimensions.width, - height: self.config.dimensions.height, + width: self.aligned_width, + height: self.aligned_height, }) .base_array_layer(0) .image_view_binding(self.dpb_image_views[ref_info.dpb_slot as usize]), @@ -381,8 +366,8 @@ impl H264Encoder { let resource = vk::VideoPictureResourceInfoKHR::default() .coded_offset(vk::Offset2D { x: 0, y: 0 }) .coded_extent(vk::Extent2D { - width: self.config.dimensions.width, - height: self.config.dimensions.height, + width: self.aligned_width, + height: self.aligned_height, }) .base_array_layer(0) .image_view_binding(image_view); @@ -580,25 +565,27 @@ impl H264Encoder { ); } - // Reset video coding state for the first frame, then set rate control. + // Reset video coding state for the first frame. + // Combine RESET + RATE_CONTROL + QUALITY_LEVEL into a single control command. + // This matches FFmpeg's approach and is required for AMD RADV. if is_first_frame { - let reset_control_info = vk::VideoCodingControlInfoKHR::default() - .flags(vk::VideoCodingControlFlagsKHR::RESET); - unsafe { - (self.video_queue_fn.fp().cmd_control_video_coding_khr)( - self.encode_command_buffer, - &reset_control_info, - ); - } + let mut quality_level_info = + vk::VideoEncodeQualityLevelInfoKHR::default().quality_level(0); + quality_level_info.p_next = + (&mut rc_info as *mut vk::VideoEncodeRateControlInfoKHR).cast(); + + let mut control_info = vk::VideoCodingControlInfoKHR::default().flags( + vk::VideoCodingControlFlagsKHR::RESET + | vk::VideoCodingControlFlagsKHR::ENCODE_RATE_CONTROL + | vk::VideoCodingControlFlagsKHR::ENCODE_QUALITY_LEVEL, + ); + control_info.p_next = + (&mut quality_level_info as *mut vk::VideoEncodeQualityLevelInfoKHR).cast(); - // After RESET, set the rate control mode. - let mut rate_control = vk::VideoCodingControlInfoKHR::default() - .flags(vk::VideoCodingControlFlagsKHR::ENCODE_RATE_CONTROL); - rate_control.p_next = (&mut rc_info as *mut vk::VideoEncodeRateControlInfoKHR).cast(); unsafe { (self.video_queue_fn.fp().cmd_control_video_coding_khr)( self.encode_command_buffer, - &rate_control, + &control_info, ); } } diff --git a/src/encoder/h264/init.rs b/src/encoder/h264/init.rs index 627f578..a762159 100644 --- a/src/encoder/h264/init.rs +++ b/src/encoder/h264/init.rs @@ -1,11 +1,11 @@ -use super::{H264Encoder, MB_SIZE, MIN_BITSTREAM_BUFFER_SIZE}; +use super::{H264Encoder, MB_SIZE}; use crate::encoder::dpb::{DecodedPictureBuffer, DecodedPictureBufferTrait, DpbConfig}; use crate::encoder::gop::GopStructure; use crate::encoder::resources::{ - allocate_session_memory, create_bitstream_buffer, create_command_resources, create_dpb_images, - create_image, get_video_format, make_codec_name, map_bitstream_buffer, - query_supported_video_formats, + align_up, allocate_session_memory, create_bitstream_buffer, create_command_resources, + create_dpb_images, create_image, get_video_format, lcm, make_codec_name, map_bitstream_buffer, + query_supported_video_formats, MIN_BITSTREAM_BUFFER_SIZE, }; use crate::encoder::PixelFormat; use crate::error::{PixelForgeError, Result}; @@ -103,28 +103,39 @@ impl H264Encoder { ))); } - let gcd = |mut a: u32, mut b: u32| { - while b != 0 { - let tmp = a % b; - a = b; - b = tmp; - } - a - }; - let lcm = |a: u32, b: u32| { - if a == 0 || b == 0 { - 0 - } else { - a / gcd(a, b) * b - } - }; - let align_up = |value: u32, alignment: u32| { - if alignment <= 1 { - value - } else { - value.div_ceil(alignment) * alignment - } - }; + debug!( + "H.264 capabilities: maxLevelIdc={}, maxSliceCount={}, maxPPictureL0ReferenceCount={}, maxBPictureL0ReferenceCount={}, maxL1ReferenceCount={}, maxTemporalLayerCount={}, prefersGopRemainingFrames={}, requiresGopRemainingFrames={}, stdSyntaxFlags={:#010x}", + h264_capabilities.max_level_idc, + h264_capabilities.max_slice_count, + h264_capabilities.max_p_picture_l0_reference_count, + h264_capabilities.max_b_picture_l0_reference_count, + h264_capabilities.max_l1_reference_count, + h264_capabilities.max_temporal_layer_count, + h264_capabilities.prefers_gop_remaining_frames, + h264_capabilities.requires_gop_remaining_frames, + h264_capabilities.std_syntax_flags.as_raw(), + ); + debug!( + "Encode capabilities: encodeInputPictureGranularity={}x{}, supportedEncodeFeedbackFlags={:#010x}, maxQualityLevels={}", + encode_capabilities.encode_input_picture_granularity.width, + encode_capabilities.encode_input_picture_granularity.height, + encode_capabilities.supported_encode_feedback_flags.as_raw(), + encode_capabilities.max_quality_levels, + ); + debug!( + "Video capabilities: flags={:#010x}, minBitstreamBufferOffsetAlignment={}, minBitstreamBufferSizeAlignment={}, minCodedExtent={}x{}, maxCodedExtent={}x{}, maxDpbSlots={}, maxActiveReferencePictures={}, pictureAccessGranularity={}x{}", + capabilities.flags.as_raw(), + capabilities.min_bitstream_buffer_offset_alignment, + capabilities.min_bitstream_buffer_size_alignment, + capabilities.min_coded_extent.width, + capabilities.min_coded_extent.height, + capabilities.max_coded_extent.width, + capabilities.max_coded_extent.height, + capabilities.max_dpb_slots, + capabilities.max_active_reference_pictures, + capabilities.picture_access_granularity.width, + capabilities.picture_access_granularity.height, + ); let gran_w = capabilities.picture_access_granularity.width.max(1); let gran_h = capabilities.picture_access_granularity.height.max(1); @@ -354,7 +365,7 @@ impl H264Encoder { let mut sps_flags: ash::vk::native::StdVideoH264SpsFlags = unsafe { std::mem::zeroed() }; sps_flags.set_constraint_set3_flag(constraint_set3_flag); - sps_flags.set_direct_8x8_inference_flag(0); + sps_flags.set_direct_8x8_inference_flag(1); sps_flags.set_frame_mbs_only_flag(1); if frame_crop_right > 0 || frame_crop_bottom > 0 { sps_flags.set_frame_cropping_flag(1); @@ -396,7 +407,10 @@ impl H264Encoder { vui_flags.set_video_signal_type_present_flag(1); vui_flags.set_video_full_range_flag(1); vui_flags.set_color_description_present_flag(1); - vui_flags.set_nal_hrd_parameters_present_flag(1); + // Do not set HRD parameters when rate control is disabled/CQP. + // HRD with zeroed bitrate values causes device loss on some drivers (AMD). + vui_flags.set_nal_hrd_parameters_present_flag(0); + vui_flags.set_bitstream_restriction_flag(1); let vui = ash::vk::native::StdVideoH264SequenceParameterSetVui { flags: vui_flags, @@ -410,8 +424,8 @@ impl H264Encoder { matrix_coefficients: 1, num_units_in_tick: 0, time_scale: 0, - max_num_reorder_frames: 0, - max_dec_frame_buffering: 0, + max_num_reorder_frames: if config.b_frame_count > 0 { 1 } else { 0 }, + max_dec_frame_buffering: (max_active_reference_pictures + 1) as u8, chroma_sample_loc_type_top_field: 0, chroma_sample_loc_type_bottom_field: 0, reserved1: 0, @@ -427,12 +441,12 @@ impl H264Encoder { bit_depth_luma_minus8, bit_depth_chroma_minus8, log2_max_frame_num_minus4: 4, - pic_order_cnt_type: ash::vk::native::StdVideoH264PocType_STD_VIDEO_H264_POC_TYPE_2, + pic_order_cnt_type: ash::vk::native::StdVideoH264PocType_STD_VIDEO_H264_POC_TYPE_0, offset_for_non_ref_pic: 0, offset_for_top_to_bottom_field: 0, log2_max_pic_order_cnt_lsb_minus4: 4, num_ref_frames_in_pic_order_cnt_cycle: 0, - max_num_ref_frames: 3, + max_num_ref_frames: max_active_reference_pictures as u8, reserved1: 0, pic_width_in_mbs_minus1: pic_width_in_mbs - 1, pic_height_in_map_units_minus1: pic_height_in_map_units - 1, @@ -448,7 +462,7 @@ impl H264Encoder { let mut pps_flags: ash::vk::native::StdVideoH264PpsFlags = unsafe { std::mem::zeroed() }; pps_flags.set_transform_8x8_mode_flag(0); - pps_flags.set_entropy_coding_mode_flag(0); + pps_flags.set_entropy_coding_mode_flag(1); pps_flags.set_deblocking_filter_control_present_flag(1); // vk_video_samples sets chroma QP offsets to 6 for 4:4:4 unless lossless. @@ -465,7 +479,7 @@ impl H264Encoder { num_ref_idx_l0_default_active_minus1: (max_active_reference_pictures as i8 - 1).max(0) as u8, num_ref_idx_l1_default_active_minus1: 0, weighted_bipred_idc: ash::vk::native::StdVideoH264WeightedBipredIdc_STD_VIDEO_H264_WEIGHTED_BIPRED_IDC_DEFAULT, - pic_init_qp_minus26: -8, + pic_init_qp_minus26: 0, pic_init_qs_minus26: 0, chroma_qp_index_offset, second_chroma_qp_index_offset, @@ -485,12 +499,18 @@ impl H264Encoder { .max_std_pps_count(1) .parameters_add_info(&h264_add_info); - let mut params_create_info = - vk::VideoSessionParametersCreateInfoKHR::default().video_session(session); - params_create_info.p_next = (&mut h264_params_create_info + // Chain quality level info into session parameters creation. + // This is required by AMD RADV and matches FFmpeg's approach. + let mut quality_level_info = vk::VideoEncodeQualityLevelInfoKHR::default().quality_level(0); // Use quality level 0 (best quality). + quality_level_info.p_next = (&mut h264_params_create_info as *mut vk::VideoEncodeH264SessionParametersCreateInfoKHR) .cast(); + let mut params_create_info = + vk::VideoSessionParametersCreateInfoKHR::default().video_session(session); + params_create_info.p_next = + (&mut quality_level_info as *mut vk::VideoEncodeQualityLevelInfoKHR).cast(); + let mut session_params = vk::VideoSessionParametersKHR::null(); let result = unsafe { (video_queue_fn.fp().create_video_session_parameters_khr)( @@ -528,6 +548,16 @@ impl H264Encoder { &profile_for_resources, )?; + // Determine DPB mode: use layered DPB when the driver does not advertise + // support for separate reference images (required for AMD RADV). + let supports_separate_dpb = capabilities + .flags + .contains(vk::VideoCapabilityFlagsKHR::SEPARATE_REFERENCE_IMAGES); + let use_layered_dpb = !supports_separate_dpb; + if use_layered_dpb { + info!("Using layered DPB (driver does not support separate reference images)"); + } + // Create DPB images. let (dpb_images, dpb_image_memories, dpb_image_views) = create_dpb_images( &context, @@ -536,6 +566,7 @@ impl H264Encoder { reference_picture_format, dpb_slot_count, &profile_for_resources, + use_layered_dpb, )?; // Create bitstream buffer. @@ -547,8 +578,13 @@ impl H264Encoder { map_bitstream_buffer(&context, bitstream_buffer_memory, MIN_BITSTREAM_BUFFER_SIZE)?; // Create command pool, buffers, and fences. - let cmd_resources = create_command_resources(&context, encode_queue_family)?; + // Use the transfer queue family for upload commands when the encode queue + // doesn't support transfer operations (AMD RADV). + let upload_queue_family = context.transfer_queue_family(); + let cmd_resources = + create_command_resources(&context, encode_queue_family, upload_queue_family)?; let command_pool = cmd_resources.command_pool; + let upload_command_pool = cmd_resources.upload_command_pool; let upload_command_buffer = cmd_resources.upload_command_buffer; let encode_command_buffer = cmd_resources.encode_command_buffer; let upload_fence = cmd_resources.upload_fence; @@ -622,6 +658,8 @@ impl H264Encoder { config: config.clone(), dpb, gop, + aligned_width, + aligned_height, video_queue_fn, video_encode_fn, session, @@ -639,6 +677,7 @@ impl H264Encoder { dpb_image_memories, dpb_image_views, dpb_slot_count, + use_layered_dpb, current_dpb_slot: 0, l0_references: Vec::new(), active_reference_count: max_active_reference_pictures as u32, @@ -646,6 +685,7 @@ impl H264Encoder { bitstream_buffer_memory, bitstream_buffer_ptr, command_pool, + upload_command_pool, upload_command_buffer, upload_fence, encode_command_buffer, diff --git a/src/encoder/h264/mod.rs b/src/encoder/h264/mod.rs index 8e1f95f..9991e1b 100644 --- a/src/encoder/h264/mod.rs +++ b/src/encoder/h264/mod.rs @@ -9,7 +9,9 @@ mod init; use ash::vk; use tracing::debug; -use crate::encoder::resources::{upload_image_to_input, UploadParams}; +use crate::encoder::resources::{ + destroy_encoder_resources, upload_image_to_input, EncoderResources, UploadParams, +}; use crate::error::Result; use crate::encoder::dpb::DecodedPictureBuffer; @@ -17,9 +19,6 @@ use crate::encoder::gop::GopStructure; use crate::encoder::EncodeConfig; use crate::vulkan::VideoContext; -/// Minimum bitstream buffer size. -const MIN_BITSTREAM_BUFFER_SIZE: usize = 2 * 1024 * 1024; - /// H.264 macroblock size in pixels. pub const MB_SIZE: u32 = 16; @@ -37,6 +36,11 @@ pub struct H264Encoder { dpb: DecodedPictureBuffer, gop: GopStructure, + /// Aligned width (macroblock + granularity aligned). + aligned_width: u32, + /// Aligned height (macroblock + granularity aligned). + aligned_height: u32, + // Video session. video_queue_fn: ash::khr::video_queue::Device, video_encode_fn: ash::khr::video_encode_queue::Device, @@ -62,6 +66,8 @@ pub struct H264Encoder { dpb_image_views: Vec, /// Number of DPB slots allocated. dpb_slot_count: usize, + /// Whether the DPB uses a single layered image (true) or separate images (false). + use_layered_dpb: bool, bitstream_buffer: vk::Buffer, bitstream_buffer_memory: vk::DeviceMemory, /// Persistently mapped pointer to the bitstream buffer (avoids per-frame map/unmap). @@ -69,6 +75,7 @@ pub struct H264Encoder { // Command resources. command_pool: vk::CommandPool, + upload_command_pool: vk::CommandPool, upload_command_buffer: vk::CommandBuffer, upload_fence: vk::Fence, encode_command_buffer: vk::CommandBuffer, @@ -117,6 +124,7 @@ impl H264Encoder { height: self.config.dimensions.height, pixel_format: self.config.pixel_format, input_image_layout: self.input_image_layout, + upload_queue: self.context.transfer_queue(), }; upload_image_to_input(&self.context, ¶ms)?; @@ -135,63 +143,37 @@ unsafe impl Send for H264Encoder {} impl Drop for H264Encoder { fn drop(&mut self) { unsafe { - let _ = self.context.device().device_wait_idle(); - self.context - .device() - .destroy_query_pool(self.query_pool, None); - self.context.device().destroy_fence(self.upload_fence, None); - self.context.device().destroy_fence(self.encode_fence, None); - self.context - .device() - .destroy_command_pool(self.command_pool, None); - self.context - .device() - .destroy_buffer(self.bitstream_buffer, None); - // Unmap the persistently mapped bitstream buffer before freeing memory. - self.context - .device() - .unmap_memory(self.bitstream_buffer_memory); - self.context + // Wait on the queues used by the encoder rather than stalling + // the entire device. + let _ = self + .context .device() - .free_memory(self.bitstream_buffer_memory, None); - - for i in 0..self.dpb_slot_count { - self.context - .device() - .destroy_image_view(self.dpb_image_views[i], None); - self.context - .device() - .destroy_image(self.dpb_images[i], None); - self.context - .device() - .free_memory(self.dpb_image_memories[i], None); + .queue_wait_idle(self.context.transfer_queue()); + if let Some(q) = self.context.video_encode_queue() { + let _ = self.context.device().queue_wait_idle(q); } - - self.context - .device() - .destroy_image_view(self.input_image_view, None); - self.context.device().destroy_image(self.input_image, None); - self.context - .device() - .free_memory(self.input_image_memory, None); - - (self - .video_queue_fn - .fp() - .destroy_video_session_parameters_khr)( - self.context.device().handle(), - self.session_params, - std::ptr::null(), + destroy_encoder_resources( + self.context.device(), + &self.video_queue_fn, + &EncoderResources { + query_pool: self.query_pool, + upload_fence: self.upload_fence, + encode_fence: self.encode_fence, + command_pool: self.command_pool, + upload_command_pool: self.upload_command_pool, + bitstream_buffer: self.bitstream_buffer, + bitstream_buffer_memory: self.bitstream_buffer_memory, + input_image: self.input_image, + input_image_memory: self.input_image_memory, + input_image_view: self.input_image_view, + dpb_images: &self.dpb_images, + dpb_image_memories: &self.dpb_image_memories, + dpb_image_views: &self.dpb_image_views, + session: self.session, + session_params: self.session_params, + session_memory: &self.session_memory, + }, ); - (self.video_queue_fn.fp().destroy_video_session_khr)( - self.context.device().handle(), - self.session, - std::ptr::null(), - ); - - for memory in &self.session_memory { - self.context.device().free_memory(*memory, None); - } } } } diff --git a/src/encoder/h264/upload.rs b/src/encoder/h264/upload.rs deleted file mode 100644 index 51ba3cc..0000000 --- a/src/encoder/h264/upload.rs +++ /dev/null @@ -1,39 +0,0 @@ -use super::H264Encoder; - -use crate::encoder::resources::{upload_image_to_input, UploadParams}; -use crate::error::Result; -use ash::vk; -use tracing::debug; - -impl H264Encoder { - /// Upload input frame from a GPU image. - /// - /// This copies from a source NV12 image directly to the encoder's input image, - /// avoiding any CPU-side data copies. The source image must be in NV12 format - /// with the same dimensions as the encoder configuration. The source image - /// should be in GENERAL layout. - pub(super) fn upload_from_image(&mut self, src_image: vk::Image) -> Result<()> { - if src_image == self.input_image { - debug!("Source image is the encoder's input image, skipping upload copy"); - return Ok(()); - } - - let params = UploadParams { - upload_command_buffer: self.upload_command_buffer, - upload_fence: self.upload_fence, - src_image, - dst_image: self.input_image, - width: self.config.dimensions.width, - height: self.config.dimensions.height, - pixel_format: self.config.pixel_format, - input_image_layout: self.input_image_layout, - }; - - upload_image_to_input(&self.context, ¶ms)?; - - // Update tracked layout. - self.input_image_layout = vk::ImageLayout::VIDEO_ENCODE_SRC_KHR; - - Ok(()) - } -} diff --git a/src/encoder/h265/encode.rs b/src/encoder/h265/encode.rs index f1b05f7..907ea27 100644 --- a/src/encoder/h265/encode.rs +++ b/src/encoder/h265/encode.rs @@ -2,9 +2,10 @@ //! //! This module handles the actual frame encoding using Vulkan Video. -use super::{H265Encoder, MIN_BITSTREAM_BUFFER_SIZE}; +use super::H265Encoder; use crate::encoder::gop::{GopFrameType, GopPosition}; +use crate::encoder::resources::{record_dpb_barriers, MIN_BITSTREAM_BUFFER_SIZE}; use crate::error::{PixelForgeError, Result}; use ash::vk; use tracing::debug; @@ -53,33 +54,16 @@ impl H265Encoder { ); } - // Transition DPB image to video encode DPB layout. - // Always transition from UNDEFINED for simplicity and consistency with H264 encoder. - let dpb_barrier = vk::ImageMemoryBarrier::default() - .old_layout(vk::ImageLayout::UNDEFINED) - .new_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR) - .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) - .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) - .image(self.dpb_images[self.current_dpb_slot as usize]) - .subresource_range(vk::ImageSubresourceRange { - aspect_mask: vk::ImageAspectFlags::COLOR, - base_mip_level: 0, - level_count: 1, - base_array_layer: 0, - layer_count: 1, - }) - .src_access_mask(vk::AccessFlags::empty()) - .dst_access_mask(vk::AccessFlags::empty()); - + // Transition DPB images for encode. + let ref_dpb_slots: Vec = self.l0_references.iter().map(|r| r.dpb_slot).collect(); unsafe { - self.context.device().cmd_pipeline_barrier( + record_dpb_barriers( + self.context.device(), self.encode_command_buffer, - vk::PipelineStageFlags::TOP_OF_PIPE, - vk::PipelineStageFlags::BOTTOM_OF_PIPE, - vk::DependencyFlags::empty(), - &[], - &[], - &[dpb_barrier], + &self.dpb_images, + self.use_layered_dpb, + self.current_dpb_slot, + &ref_dpb_slots, ); } @@ -609,24 +593,26 @@ impl H265Encoder { } // Reset video coding state for the first frame. + // Combine RESET + RATE_CONTROL + QUALITY_LEVEL into a single control command. + // This matches FFmpeg's approach and is required for AMD RADV. if is_first_frame { - let reset_control_info = vk::VideoCodingControlInfoKHR::default() - .flags(vk::VideoCodingControlFlagsKHR::RESET); - unsafe { - (self.video_queue_fn.fp().cmd_control_video_coding_khr)( - self.encode_command_buffer, - &reset_control_info, - ); - } + let mut quality_level_info = + vk::VideoEncodeQualityLevelInfoKHR::default().quality_level(0); + quality_level_info.p_next = + (&mut rc_info as *mut vk::VideoEncodeRateControlInfoKHR).cast(); + + let mut control_info = vk::VideoCodingControlInfoKHR::default().flags( + vk::VideoCodingControlFlagsKHR::RESET + | vk::VideoCodingControlFlagsKHR::ENCODE_RATE_CONTROL + | vk::VideoCodingControlFlagsKHR::ENCODE_QUALITY_LEVEL, + ); + control_info.p_next = + (&mut quality_level_info as *mut vk::VideoEncodeQualityLevelInfoKHR).cast(); - // Set rate control after reset. - let mut rate_control = vk::VideoCodingControlInfoKHR::default() - .flags(vk::VideoCodingControlFlagsKHR::ENCODE_RATE_CONTROL); - rate_control.p_next = (&mut rc_info as *mut vk::VideoEncodeRateControlInfoKHR).cast(); unsafe { (self.video_queue_fn.fp().cmd_control_video_coding_khr)( self.encode_command_buffer, - &rate_control, + &control_info, ); } } @@ -663,17 +649,28 @@ impl H265Encoder { // Add DPB synchronization barrier after encoding. { + let post_dpb_image = if self.use_layered_dpb { + self.dpb_images[0] + } else { + self.dpb_images[self.current_dpb_slot as usize] + }; + let post_dpb_layer = if self.use_layered_dpb { + self.current_dpb_slot as u32 + } else { + 0 + }; + let dpb_sync_barrier = vk::ImageMemoryBarrier::default() .old_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR) .new_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR) .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) - .image(self.dpb_images[self.current_dpb_slot as usize]) + .image(post_dpb_image) .subresource_range(vk::ImageSubresourceRange { aspect_mask: vk::ImageAspectFlags::COLOR, base_mip_level: 0, level_count: 1, - base_array_layer: 0, + base_array_layer: post_dpb_layer, layer_count: 1, }) .src_access_mask(vk::AccessFlags::MEMORY_WRITE) diff --git a/src/encoder/h265/init.rs b/src/encoder/h265/init.rs index f61a922..8b1fa2b 100644 --- a/src/encoder/h265/init.rs +++ b/src/encoder/h265/init.rs @@ -1,10 +1,11 @@ -use super::{H265Encoder, CTB_SIZE, MIN_BITSTREAM_BUFFER_SIZE}; +use super::H265Encoder; use crate::encoder::dpb::{DecodedPictureBuffer, DecodedPictureBufferTrait, DpbConfig}; use crate::encoder::gop::GopStructure; use crate::encoder::resources::{ - allocate_session_memory, create_bitstream_buffer, create_command_resources, create_dpb_images, - create_image, get_video_format, make_codec_name, map_bitstream_buffer, + align_up, allocate_session_memory, create_bitstream_buffer, create_command_resources, + create_dpb_images, create_image, get_video_format, lcm, make_codec_name, map_bitstream_buffer, + query_supported_video_formats, MIN_BITSTREAM_BUFFER_SIZE, }; use crate::encoder::{BitDepth, PixelFormat}; use crate::error::{PixelForgeError, Result}; @@ -28,14 +29,9 @@ impl H265Encoder { let width = config.dimensions.width; let height = config.dimensions.height; - // H.265 uses CTB (Coding Tree Block) sizes of 16, 32, or 64 pixels. - // We use 32 as the default CTB size. - let aligned_width = (width + CTB_SIZE - 1) & !(CTB_SIZE - 1); - let aligned_height = (height + CTB_SIZE - 1) & !(CTB_SIZE - 1); - info!( - "Creating H.265 encoder: {}x{} (aligned: {}x{}, CTB size: {}), pixel_format={:?}", - width, height, aligned_width, aligned_height, CTB_SIZE, config.pixel_format + "Creating H.265 encoder: {}x{}, pixel_format={:?}", + width, height, config.pixel_format ); // Load video queue extension functions. @@ -118,6 +114,94 @@ impl H265Encoder { ))); } + // Compute aligned coded extent using capabilities (picture_access_granularity, + // min/max_coded_extent) - required for AMD which reports granularity 64x16 and + // min_coded_extent 130x128. + let ctb_size = super::CTB_SIZE; + let gran_w = capabilities.picture_access_granularity.width.max(1); + let gran_h = capabilities.picture_access_granularity.height.max(1); + let align_w = lcm(ctb_size, gran_w); + let align_h = lcm(ctb_size, gran_h); + + let mut aligned_width = align_up(width, align_w); + let mut aligned_height = align_up(height, align_h); + + aligned_width = aligned_width.max(capabilities.min_coded_extent.width); + aligned_height = aligned_height.max(capabilities.min_coded_extent.height); + + if aligned_width > capabilities.max_coded_extent.width + || aligned_height > capabilities.max_coded_extent.height + { + return Err(PixelForgeError::InvalidInput(format!( + "Requested coded extent {}x{} (aligned to {}x{} with granularity {}x{}) exceeds device max {}x{} for this profile", + width, + height, + aligned_width, + aligned_height, + gran_w, + gran_h, + capabilities.max_coded_extent.width, + capabilities.max_coded_extent.height + ))); + } + + info!( + "Using coded extent {}x{} (granularity {}x{}, min {}x{}, max {}x{})", + aligned_width, + aligned_height, + gran_w, + gran_h, + capabilities.min_coded_extent.width, + capabilities.min_coded_extent.height, + capabilities.max_coded_extent.width, + capabilities.max_coded_extent.height + ); + + // Query supported formats for SRC and DPB usage. + let supported_src_formats = query_supported_video_formats( + &context, + &profile_info, + vk::ImageUsageFlags::VIDEO_ENCODE_SRC_KHR, + )?; + let supported_dpb_formats = query_supported_video_formats( + &context, + &profile_info, + vk::ImageUsageFlags::VIDEO_ENCODE_DPB_KHR, + )?; + + if supported_src_formats.is_empty() { + return Err(PixelForgeError::NoSuitableDevice( + "No supported Vulkan Video SRC formats returned for this H.265 profile".to_string(), + )); + } + info!("Supported SRC formats: {:?}", supported_src_formats); + if supported_dpb_formats.is_empty() { + return Err(PixelForgeError::NoSuitableDevice( + "No supported Vulkan Video DPB formats returned for this H.265 profile".to_string(), + )); + } + info!("Supported DPB formats: {:?}", supported_dpb_formats); + + let picture_format = if supported_src_formats.contains(&video_format) { + video_format + } else { + return Err(PixelForgeError::NoSuitableDevice(format!( + "Preferred input format {:?} is not supported for VIDEO_ENCODE_SRC_KHR. Supported: {:?}", + video_format, supported_src_formats + ))); + }; + + let reference_picture_format = supported_dpb_formats + .iter() + .copied() + .find(|f| *f == picture_format) + .unwrap_or(supported_dpb_formats[0]); + + debug!( + "Selected Vulkan Video formats: picture_format={:?}, reference_picture_format={:?}", + picture_format, reference_picture_format + ); + let max_dpb_slots_supported = capabilities.max_dpb_slots as usize; let max_active_reference_pictures_supported = capabilities.max_active_reference_pictures as usize; @@ -177,12 +261,12 @@ impl H265Encoder { .queue_family_index(encode_queue_family) .flags(vk::VideoSessionCreateFlagsKHR::empty()) .video_profile(&profile_info) - .picture_format(video_format) + .picture_format(picture_format) .max_coded_extent(vk::Extent2D { width: aligned_width, height: aligned_height, }) - .reference_picture_format(video_format) + .reference_picture_format(reference_picture_format) .max_dpb_slots(dpb_slot_count as u32) .max_active_reference_pictures(max_active_reference_pictures as u32) .std_header_version(&std_header_version); @@ -527,12 +611,18 @@ impl H265Encoder { .max_std_pps_count(1) .parameters_add_info(&h265_add_info); - let mut params_create_info = - vk::VideoSessionParametersCreateInfoKHR::default().video_session(session); - params_create_info.p_next = (&mut h265_params_create_info + // Chain quality level info into session parameters creation. + // This is required by AMD RADV and matches FFmpeg's approach. + let mut quality_level_info = vk::VideoEncodeQualityLevelInfoKHR::default().quality_level(0); // Use quality level 0 (best quality). + quality_level_info.p_next = (&mut h265_params_create_info as *mut vk::VideoEncodeH265SessionParametersCreateInfoKHR) .cast(); + let mut params_create_info = + vk::VideoSessionParametersCreateInfoKHR::default().video_session(session); + params_create_info.p_next = + (&mut quality_level_info as *mut vk::VideoEncodeQualityLevelInfoKHR).cast(); + let mut session_params = vk::VideoSessionParametersKHR::null(); let result = unsafe { (video_queue_fn.fp().create_video_session_parameters_khr)( @@ -565,19 +655,30 @@ impl H265Encoder { &context, aligned_width, aligned_height, - video_format, + picture_format, false, &profile_for_resources, )?; + // Determine DPB mode: use layered DPB when the driver does not advertise + // support for separate reference images (required for AMD RADV). + let supports_separate_dpb = capabilities + .flags + .contains(vk::VideoCapabilityFlagsKHR::SEPARATE_REFERENCE_IMAGES); + let use_layered_dpb = !supports_separate_dpb; + if use_layered_dpb { + info!("Using layered DPB (driver does not support separate reference images)"); + } + // Create DPB images. let (dpb_images, dpb_image_memories, dpb_image_views) = create_dpb_images( &context, aligned_width, aligned_height, - video_format, + reference_picture_format, dpb_slot_count, &profile_for_resources, + use_layered_dpb, )?; // Create bitstream buffer. @@ -589,8 +690,13 @@ impl H265Encoder { map_bitstream_buffer(&context, bitstream_buffer_memory, MIN_BITSTREAM_BUFFER_SIZE)?; // Create command pool, buffers, and fences. - let cmd_resources = create_command_resources(&context, encode_queue_family)?; + // Use the transfer queue family for upload commands when the encode queue + // doesn't support transfer operations (AMD RADV). + let upload_queue_family = context.transfer_queue_family(); + let cmd_resources = + create_command_resources(&context, encode_queue_family, upload_queue_family)?; let command_pool = cmd_resources.command_pool; + let upload_command_pool = cmd_resources.upload_command_pool; let upload_command_buffer = cmd_resources.upload_command_buffer; let encode_command_buffer = cmd_resources.encode_command_buffer; let upload_fence = cmd_resources.upload_fence; @@ -682,10 +788,12 @@ impl H265Encoder { dpb_image_memories, dpb_image_views, dpb_slot_count, + use_layered_dpb, bitstream_buffer, bitstream_buffer_memory, bitstream_buffer_ptr, command_pool, + upload_command_pool, upload_command_buffer, upload_fence, encode_command_buffer, diff --git a/src/encoder/h265/mod.rs b/src/encoder/h265/mod.rs index 93c3854..c4bb371 100644 --- a/src/encoder/h265/mod.rs +++ b/src/encoder/h265/mod.rs @@ -11,14 +11,13 @@ use tracing::debug; use crate::encoder::dpb::DecodedPictureBuffer; use crate::encoder::gop::GopStructure; -use crate::encoder::resources::{upload_image_to_input, UploadParams}; +use crate::encoder::resources::{ + destroy_encoder_resources, upload_image_to_input, EncoderResources, UploadParams, +}; use crate::encoder::EncodeConfig; use crate::error::Result; use crate::vulkan::VideoContext; -/// Minimum bitstream buffer size. -const MIN_BITSTREAM_BUFFER_SIZE: usize = 2 * 1024 * 1024; - /// H.265 Coding Tree Block (CTB) size in pixels. pub const CTB_SIZE: u32 = 32; @@ -63,6 +62,8 @@ pub struct H265Encoder { dpb_image_views: Vec, /// Number of DPB slots allocated. dpb_slot_count: usize, + /// Whether the DPB uses a single layered image (true) or separate images (false). + use_layered_dpb: bool, bitstream_buffer: vk::Buffer, bitstream_buffer_memory: vk::DeviceMemory, /// Persistently mapped pointer to the bitstream buffer (avoids per-frame map/unmap). @@ -70,6 +71,7 @@ pub struct H265Encoder { // Command resources. command_pool: vk::CommandPool, + upload_command_pool: vk::CommandPool, upload_command_buffer: vk::CommandBuffer, upload_fence: vk::Fence, encode_command_buffer: vk::CommandBuffer, @@ -120,6 +122,7 @@ impl H265Encoder { height: self.config.dimensions.height, pixel_format: self.config.pixel_format, input_image_layout: self.input_image_layout, + upload_queue: self.context.transfer_queue(), }; upload_image_to_input(&self.context, ¶ms)?; @@ -138,83 +141,37 @@ unsafe impl Send for H265Encoder {} impl Drop for H265Encoder { fn drop(&mut self) { unsafe { - // Wait for device to be idle before destroying resources. - let _ = self.context.device().device_wait_idle(); - - // Destroy query pool. - self.context - .device() - .destroy_query_pool(self.query_pool, None); - - // Destroy fences. - self.context.device().destroy_fence(self.upload_fence, None); - self.context.device().destroy_fence(self.encode_fence, None); - - // Destroy bitstream buffer. - self.context - .device() - .unmap_memory(self.bitstream_buffer_memory); - self.context - .device() - .destroy_buffer(self.bitstream_buffer, None); - self.context - .device() - .free_memory(self.bitstream_buffer_memory, None); - - // Destroy input image. - self.context + // Wait on the queues used by the encoder rather than stalling + // the entire device. + let _ = self + .context .device() - .destroy_image_view(self.input_image_view, None); - self.context.device().destroy_image(self.input_image, None); - self.context - .device() - .free_memory(self.input_image_memory, None); - - // Destroy DPB images. - for i in 0..self.dpb_images.len() { - self.context - .device() - .destroy_image_view(self.dpb_image_views[i], None); - self.context - .device() - .destroy_image(self.dpb_images[i], None); - self.context - .device() - .free_memory(self.dpb_image_memories[i], None); + .queue_wait_idle(self.context.transfer_queue()); + if let Some(q) = self.context.video_encode_queue() { + let _ = self.context.device().queue_wait_idle(q); } - - // Free command buffers. - self.context.device().free_command_buffers( - self.command_pool, - &[self.upload_command_buffer, self.encode_command_buffer], - ); - - // Destroy command pool. - self.context - .device() - .destroy_command_pool(self.command_pool, None); - - // Destroy video session parameters. - (self - .video_queue_fn - .fp() - .destroy_video_session_parameters_khr)( - self.context.device().handle(), - self.session_params, - std::ptr::null(), + destroy_encoder_resources( + self.context.device(), + &self.video_queue_fn, + &EncoderResources { + query_pool: self.query_pool, + upload_fence: self.upload_fence, + encode_fence: self.encode_fence, + command_pool: self.command_pool, + upload_command_pool: self.upload_command_pool, + bitstream_buffer: self.bitstream_buffer, + bitstream_buffer_memory: self.bitstream_buffer_memory, + input_image: self.input_image, + input_image_memory: self.input_image_memory, + input_image_view: self.input_image_view, + dpb_images: &self.dpb_images, + dpb_image_memories: &self.dpb_image_memories, + dpb_image_views: &self.dpb_image_views, + session: self.session, + session_params: self.session_params, + session_memory: &self.session_memory, + }, ); - - // Destroy video session. - (self.video_queue_fn.fp().destroy_video_session_khr)( - self.context.device().handle(), - self.session, - std::ptr::null(), - ); - - // Free session memory. - for mem in &self.session_memory { - self.context.device().free_memory(*mem, None); - } } } } diff --git a/src/encoder/h265/upload.rs b/src/encoder/h265/upload.rs deleted file mode 100644 index e06bbef..0000000 --- a/src/encoder/h265/upload.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! H.265/HEVC encoder frame upload functionality. - -use super::H265Encoder; - -use crate::encoder::resources::{upload_image_to_input, UploadParams}; -use crate::error::Result; -use ash::vk; -use tracing::debug; - -impl H265Encoder { - /// Upload input frame from a GPU image. - /// - /// This copies from a source NV12 image directly to the encoder's input image, - /// avoiding any CPU-side data copies. The source image must be in NV12 format - /// with the same dimensions as the encoder configuration. The source image - /// should be in GENERAL layout. - pub(super) fn upload_from_image(&mut self, src_image: vk::Image) -> Result<()> { - if src_image == self.input_image { - debug!("Source image is the encoder's input image, skipping upload copy"); - return Ok(()); - } - - let params = UploadParams { - upload_command_buffer: self.upload_command_buffer, - upload_fence: self.upload_fence, - src_image, - dst_image: self.input_image, - width: self.config.dimensions.width, - height: self.config.dimensions.height, - pixel_format: self.config.pixel_format, - input_image_layout: self.input_image_layout, - }; - - upload_image_to_input(&self.context, ¶ms)?; - - // Update tracked layout. - self.input_image_layout = vk::ImageLayout::VIDEO_ENCODE_SRC_KHR; - - Ok(()) - } -} diff --git a/src/encoder/resources.rs b/src/encoder/resources.rs index 80d6cc5..8efde31 100644 --- a/src/encoder/resources.rs +++ b/src/encoder/resources.rs @@ -4,7 +4,38 @@ use crate::vulkan::VideoContext; use ash::vk; use std::ptr; -pub fn query_supported_video_formats( +/// Minimum bitstream buffer size. +pub(crate) const MIN_BITSTREAM_BUFFER_SIZE: usize = 2 * 1024 * 1024; + +/// Compute greatest common divisor of two values. +pub(crate) fn gcd(mut a: u32, mut b: u32) -> u32 { + while b != 0 { + let tmp = a % b; + a = b; + b = tmp; + } + a +} + +/// Compute least common multiple of two values. +pub(crate) fn lcm(a: u32, b: u32) -> u32 { + if a == 0 || b == 0 { + 0 + } else { + a / gcd(a, b) * b + } +} + +/// Align a value up to the next multiple of the given alignment. +pub(crate) fn align_up(value: u32, alignment: u32) -> u32 { + if alignment <= 1 { + value + } else { + value.div_ceil(alignment) * alignment + } +} + +pub(crate) fn query_supported_video_formats( context: &VideoContext, profile_info: &vk::VideoProfileInfoKHR, image_usage: vk::ImageUsageFlags, @@ -70,7 +101,7 @@ pub fn query_supported_video_formats( /// Supports YUV420 and YUV444 in 8-bit and 10-bit. /// For YUV444, uses 2-plane (semi-planar) formats from VK_EXT_ycbcr_2plane_444_formats /// which are supported by NVIDIA hardware for video encoding. -pub fn get_video_format(pixel_format: PixelFormat, bit_depth: BitDepth) -> vk::Format { +pub(crate) fn get_video_format(pixel_format: PixelFormat, bit_depth: BitDepth) -> vk::Format { match (pixel_format, bit_depth) { (PixelFormat::Yuv420, BitDepth::Eight) => vk::Format::G8_B8R8_2PLANE_420_UNORM, (PixelFormat::Yuv420, BitDepth::Ten) => { @@ -94,7 +125,7 @@ pub fn get_video_format(pixel_format: PixelFormat, bit_depth: BitDepth) -> vk::F /// /// This creates a null-terminated i8 array of 256 bytes for use with Vulkan /// video extensions. -pub fn make_codec_name(codec_name: &[u8]) -> [i8; 256] { +pub(crate) fn make_codec_name(codec_name: &[u8]) -> [i8; 256] { let mut name = [0i8; 256]; for (i, &byte) in codec_name.iter().enumerate() { if i < 255 { @@ -104,7 +135,7 @@ pub fn make_codec_name(codec_name: &[u8]) -> [i8; 256] { name } -pub fn find_memory_type( +pub(crate) fn find_memory_type( memory_props: &vk::PhysicalDeviceMemoryProperties, type_filter: u32, properties: vk::MemoryPropertyFlags, @@ -117,7 +148,7 @@ pub fn find_memory_type( }) } -pub fn create_bitstream_buffer( +pub(crate) fn create_bitstream_buffer( context: &VideoContext, size: usize, profile_info: &vk::VideoProfileInfoKHR, @@ -170,7 +201,7 @@ pub fn create_bitstream_buffer( /// * `format` - The Vulkan format to use for the image /// * `is_dpb` - If true, create a DPB image; if false, create an input image /// * `profile_info` - Video profile info for the encoder session -pub fn create_image( +pub(crate) fn create_image( context: &VideoContext, width: u32, height: u32, @@ -184,6 +215,34 @@ pub fn create_image( vk::ImageUsageFlags::VIDEO_ENCODE_SRC_KHR | vk::ImageUsageFlags::TRANSFER_DST }; + // For input (non-DPB) images, use CONCURRENT sharing mode when multiple + // queue families need access. The image may be accessed by: + // - The video encode queue (for encoding) + // - The transfer queue (for InputImage upload) + // - The compute queue (for ColorConverter buffer-to-image copy) + let mut queue_families = Vec::new(); + let sharing_mode = if !is_dpb { + if let Some(encode_family) = context.video_encode_queue_family() { + queue_families.push(encode_family); + let transfer_family = context.transfer_queue_family(); + if !queue_families.contains(&transfer_family) { + queue_families.push(transfer_family); + } + let compute_family = context.compute_queue_family(); + if !queue_families.contains(&compute_family) { + queue_families.push(compute_family); + } + } + if queue_families.len() > 1 { + vk::SharingMode::CONCURRENT + } else { + queue_families.clear(); + vk::SharingMode::EXCLUSIVE + } + } else { + vk::SharingMode::EXCLUSIVE + }; + let profiles = [*profile_info]; let mut profile_list = vk::VideoProfileListInfoKHR::default().profiles(&profiles); @@ -200,7 +259,8 @@ pub fn create_image( .samples(vk::SampleCountFlags::TYPE_1) .tiling(vk::ImageTiling::OPTIMAL) .usage(usage) - .sharing_mode(vk::SharingMode::EXCLUSIVE) + .sharing_mode(sharing_mode) + .queue_family_indices(&queue_families) .initial_layout(vk::ImageLayout::UNDEFINED); create_info.p_next = (&mut profile_list as *mut vk::VideoProfileListInfoKHR).cast(); @@ -254,7 +314,7 @@ pub fn create_image( /// Allocate and bind memory for a video session. /// /// Returns the allocated device memory handles. -pub fn allocate_session_memory( +pub(crate) fn allocate_session_memory( context: &VideoContext, session: vk::VideoSessionKHR, video_queue_fn: &ash::khr::video_queue::Device, @@ -351,9 +411,12 @@ pub fn allocate_session_memory( } /// Command resources for encoding operations. -pub struct CommandResources { - /// Command pool. +pub(crate) struct CommandResources { + /// Command pool for encode commands. pub command_pool: vk::CommandPool, + /// Command pool for upload/transfer commands (may differ from command_pool when + /// the encode queue does not support transfer operations). + pub upload_command_pool: vk::CommandPool, /// Command buffer for upload operations. pub upload_command_buffer: vk::CommandBuffer, /// Fence for upload synchronization. @@ -365,13 +428,18 @@ pub struct CommandResources { } /// Create command resources for encoding. -pub fn create_command_resources( +/// +/// `encode_queue_family` is the queue family used for video encode commands. +/// `upload_queue_family` is the queue family used for transfer (upload) commands. +/// They may be the same if the encode queue supports transfer operations. +pub(crate) fn create_command_resources( context: &VideoContext, - queue_family_index: u32, + encode_queue_family: u32, + upload_queue_family: u32, ) -> Result { - // Create command pool. + // Create command pool for encode commands. let pool_create_info = vk::CommandPoolCreateInfo::default() - .queue_family_index(queue_family_index) + .queue_family_index(encode_queue_family) .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER); let command_pool = unsafe { @@ -381,16 +449,44 @@ pub fn create_command_resources( } .map_err(|e| PixelForgeError::CommandBuffer(e.to_string()))?; - // Allocate command buffers. + // Allocate encode command buffer. let alloc_info = vk::CommandBufferAllocateInfo::default() .command_pool(command_pool) .level(vk::CommandBufferLevel::PRIMARY) - .command_buffer_count(2); + .command_buffer_count(1); - let command_buffers = unsafe { context.device().allocate_command_buffers(&alloc_info) } + let encode_command_buffers = unsafe { context.device().allocate_command_buffers(&alloc_info) } .map_err(|e| PixelForgeError::CommandBuffer(e.to_string()))?; - let upload_command_buffer = command_buffers[0]; - let encode_command_buffer = command_buffers[1]; + let encode_command_buffer = encode_command_buffers[0]; + + // Create command pool for upload commands (may be the same family). + let upload_command_pool = if upload_queue_family == encode_queue_family { + command_pool + } else { + let upload_pool_info = vk::CommandPoolCreateInfo::default() + .queue_family_index(upload_queue_family) + .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER); + unsafe { + context + .device() + .create_command_pool(&upload_pool_info, None) + } + .map_err(|e| PixelForgeError::CommandBuffer(e.to_string()))? + }; + + // Allocate upload command buffer from the upload pool. + let upload_alloc_info = vk::CommandBufferAllocateInfo::default() + .command_pool(upload_command_pool) + .level(vk::CommandBufferLevel::PRIMARY) + .command_buffer_count(1); + + let upload_command_buffers = unsafe { + context + .device() + .allocate_command_buffers(&upload_alloc_info) + } + .map_err(|e| PixelForgeError::CommandBuffer(e.to_string()))?; + let upload_command_buffer = upload_command_buffers[0]; // Create fences. let fence_create_info = vk::FenceCreateInfo::default(); @@ -401,6 +497,7 @@ pub fn create_command_resources( Ok(CommandResources { command_pool, + upload_command_pool, upload_command_buffer, upload_fence, encode_command_buffer, @@ -410,32 +507,122 @@ pub fn create_command_resources( /// Create DPB images for video encoding. /// -/// Returns vectors of images, memories, and views. -pub fn create_dpb_images( +/// When `use_layered` is true (required when the driver does not support +/// `VK_VIDEO_CAPABILITY_SEPARATE_REFERENCE_IMAGES_BIT_KHR`), a single +/// `VkImage` with `array_layers = count` is created and one `VkImageView` +/// per layer is returned. The image and memory vectors will have a single +/// entry while the view vector will have `count` entries. +/// +/// When `use_layered` is false the previous behaviour is preserved: one +/// separate image/memory/view per DPB slot. +pub(crate) fn create_dpb_images( context: &VideoContext, width: u32, height: u32, format: vk::Format, count: usize, profile_info: &vk::VideoProfileInfoKHR, + use_layered: bool, ) -> Result<(Vec, Vec, Vec)> { - let mut dpb_images = Vec::with_capacity(count); - let mut dpb_image_memories = Vec::with_capacity(count); - let mut dpb_image_views = Vec::with_capacity(count); - - for _ in 0..count { - let (dpb_image, dpb_image_memory, dpb_image_view) = - create_image(context, width, height, format, true, profile_info)?; - dpb_images.push(dpb_image); - dpb_image_memories.push(dpb_image_memory); - dpb_image_views.push(dpb_image_view); - } + if use_layered { + // Create a single image with `count` array layers. + let profiles = [*profile_info]; + let mut profile_list = vk::VideoProfileListInfoKHR::default().profiles(&profiles); + + let mut create_info = vk::ImageCreateInfo::default() + .image_type(vk::ImageType::TYPE_2D) + .format(format) + .extent(vk::Extent3D { + width, + height, + depth: 1, + }) + .mip_levels(1) + .array_layers(count as u32) + .samples(vk::SampleCountFlags::TYPE_1) + .tiling(vk::ImageTiling::OPTIMAL) + .usage(vk::ImageUsageFlags::VIDEO_ENCODE_DPB_KHR) + .sharing_mode(vk::SharingMode::EXCLUSIVE) + .initial_layout(vk::ImageLayout::UNDEFINED); + create_info.p_next = (&mut profile_list as *mut vk::VideoProfileListInfoKHR).cast(); + + let image = unsafe { context.device().create_image(&create_info, None) } + .map_err(|e| PixelForgeError::ResourceCreation(format!("layered DPB image: {}", e)))?; + + let mem_requirements = unsafe { context.device().get_image_memory_requirements(image) }; + + let memory_type_index = find_memory_type( + context.memory_properties(), + mem_requirements.memory_type_bits, + vk::MemoryPropertyFlags::DEVICE_LOCAL, + ) + .ok_or_else(|| { + PixelForgeError::MemoryAllocation( + "No suitable memory type for layered DPB image".to_string(), + ) + })?; + + let alloc_info = vk::MemoryAllocateInfo::default() + .allocation_size(mem_requirements.size) + .memory_type_index(memory_type_index); + + let memory = unsafe { context.device().allocate_memory(&alloc_info, None) } + .map_err(|e| PixelForgeError::MemoryAllocation(e.to_string()))?; - Ok((dpb_images, dpb_image_memories, dpb_image_views)) + unsafe { context.device().bind_image_memory(image, memory, 0) } + .map_err(|e| PixelForgeError::MemoryAllocation(e.to_string()))?; + + // Create one view per array layer. + let mut dpb_image_views = Vec::with_capacity(count); + for layer in 0..count as u32 { + let view_create_info = vk::ImageViewCreateInfo::default() + .image(image) + .view_type(vk::ImageViewType::TYPE_2D) + .format(format) + .components(vk::ComponentMapping { + r: vk::ComponentSwizzle::IDENTITY, + g: vk::ComponentSwizzle::IDENTITY, + b: vk::ComponentSwizzle::IDENTITY, + a: vk::ComponentSwizzle::IDENTITY, + }) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: layer, + layer_count: 1, + }); + + let view = unsafe { context.device().create_image_view(&view_create_info, None) } + .map_err(|e| { + PixelForgeError::ResourceCreation(format!( + "layered DPB image view layer {}: {}", + layer, e + )) + })?; + dpb_image_views.push(view); + } + + Ok((vec![image], vec![memory], dpb_image_views)) + } else { + let mut dpb_images = Vec::with_capacity(count); + let mut dpb_image_memories = Vec::with_capacity(count); + let mut dpb_image_views = Vec::with_capacity(count); + + for _ in 0..count { + let (dpb_image, dpb_image_memory, dpb_image_view) = + create_image(context, width, height, format, true, profile_info)?; + dpb_images.push(dpb_image); + dpb_image_memories.push(dpb_image_memory); + dpb_image_views.push(dpb_image_view); + } + + Ok((dpb_images, dpb_image_memories, dpb_image_views)) + } } /// Map a bitstream buffer for persistent access. -pub fn map_bitstream_buffer( +pub(crate) fn map_bitstream_buffer( context: &VideoContext, memory: vk::DeviceMemory, size: usize, @@ -456,7 +643,7 @@ pub fn map_bitstream_buffer( } /// Parameters for uploading an image to the encoder's input image. -pub struct UploadParams { +pub(crate) struct UploadParams { /// The command buffer to use for the upload. pub upload_command_buffer: vk::CommandBuffer, /// The fence to use for synchronization. @@ -473,6 +660,8 @@ pub struct UploadParams { pub pixel_format: PixelFormat, /// The current layout of the input image. pub input_image_layout: vk::ImageLayout, + /// The queue to submit transfer operations to. + pub upload_queue: vk::Queue, } /// Upload an image to the encoder's input image via GPU-to-GPU copy. @@ -487,7 +676,7 @@ pub struct UploadParams { /// - Submitting the command buffer and waiting for completion /// /// Returns Ok(()) on success, or an error if any Vulkan operation fails. -pub fn upload_image_to_input( +pub(crate) fn upload_image_to_input( context: &crate::vulkan::VideoContext, params: &UploadParams, ) -> Result<()> { @@ -668,11 +857,7 @@ pub fn upload_image_to_input( let submit_info = vk::SubmitInfo::default() .command_buffers(std::slice::from_ref(¶ms.upload_command_buffer)); - let encode_queue = context.video_encode_queue().ok_or_else(|| { - PixelForgeError::NoSuitableDevice("No video encode queue available".to_string()) - })?; - - unsafe { device.queue_submit(encode_queue, &[submit_info], params.upload_fence) } + unsafe { device.queue_submit(params.upload_queue, &[submit_info], params.upload_fence) } .map_err(|e| PixelForgeError::CommandBuffer(e.to_string()))?; unsafe { device.wait_for_fences(&[params.upload_fence], true, u64::MAX) } @@ -683,3 +868,191 @@ pub fn upload_image_to_input( Ok(()) } + +/// Parameters for cleaning up shared encoder resources. +pub(crate) struct EncoderResources<'a> { + pub query_pool: vk::QueryPool, + pub upload_fence: vk::Fence, + pub encode_fence: vk::Fence, + pub command_pool: vk::CommandPool, + pub upload_command_pool: vk::CommandPool, + pub bitstream_buffer: vk::Buffer, + pub bitstream_buffer_memory: vk::DeviceMemory, + pub input_image: vk::Image, + pub input_image_memory: vk::DeviceMemory, + pub input_image_view: vk::ImageView, + pub dpb_images: &'a [vk::Image], + pub dpb_image_memories: &'a [vk::DeviceMemory], + pub dpb_image_views: &'a [vk::ImageView], + pub session: vk::VideoSessionKHR, + pub session_params: vk::VideoSessionParametersKHR, + pub session_memory: &'a [vk::DeviceMemory], +} + +/// Destroy all shared encoder resources. +/// +/// # Safety +/// +/// The device must be idle before calling this function. +pub(crate) unsafe fn destroy_encoder_resources( + device: &ash::Device, + video_queue_fn: &ash::khr::video_queue::Device, + res: &EncoderResources, +) { + device.destroy_query_pool(res.query_pool, None); + device.destroy_fence(res.upload_fence, None); + device.destroy_fence(res.encode_fence, None); + device.destroy_command_pool(res.command_pool, None); + if res.upload_command_pool != res.command_pool { + device.destroy_command_pool(res.upload_command_pool, None); + } + + device.unmap_memory(res.bitstream_buffer_memory); + device.destroy_buffer(res.bitstream_buffer, None); + device.free_memory(res.bitstream_buffer_memory, None); + + device.destroy_image_view(res.input_image_view, None); + device.destroy_image(res.input_image, None); + device.free_memory(res.input_image_memory, None); + + for view in res.dpb_image_views { + device.destroy_image_view(*view, None); + } + for image in res.dpb_images { + device.destroy_image(*image, None); + } + for memory in res.dpb_image_memories { + device.free_memory(*memory, None); + } + + (video_queue_fn.fp().destroy_video_session_parameters_khr)( + device.handle(), + res.session_params, + std::ptr::null(), + ); + (video_queue_fn.fp().destroy_video_session_khr)(device.handle(), res.session, std::ptr::null()); + + for memory in res.session_memory { + device.free_memory(*memory, None); + } +} + +/// Record DPB image barriers for encode. +/// +/// Transitions the setup DPB slot from UNDEFINED to VIDEO_ENCODE_DPB and +/// adds execution barriers for reference slot images. +/// +/// # Safety +/// +/// The command buffer must be in recording state. +pub(crate) unsafe fn record_dpb_barriers( + device: &ash::Device, + command_buffer: vk::CommandBuffer, + dpb_images: &[vk::Image], + use_layered_dpb: bool, + current_dpb_slot: u8, + reference_dpb_slots: &[u8], +) { + let dpb_image = if use_layered_dpb { + dpb_images[0] + } else { + dpb_images[current_dpb_slot as usize] + }; + let dpb_base_array_layer = if use_layered_dpb { + current_dpb_slot as u32 + } else { + 0 + }; + + let dpb_barrier = vk::ImageMemoryBarrier::default() + .old_layout(vk::ImageLayout::UNDEFINED) + .new_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(dpb_image) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: dpb_base_array_layer, + layer_count: 1, + }) + .src_access_mask(vk::AccessFlags::empty()) + .dst_access_mask(vk::AccessFlags::empty()); + + let mut all_barriers = vec![dpb_barrier]; + + for &ref_slot in reference_dpb_slots { + let (ref_image, ref_layer) = if use_layered_dpb { + (dpb_images[0], ref_slot as u32) + } else { + (dpb_images[ref_slot as usize], 0u32) + }; + all_barriers.push( + vk::ImageMemoryBarrier::default() + .old_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR) + .new_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(ref_image) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: ref_layer, + layer_count: 1, + }) + .src_access_mask(vk::AccessFlags::empty()) + .dst_access_mask(vk::AccessFlags::empty()), + ); + } + + device.cmd_pipeline_barrier( + command_buffer, + vk::PipelineStageFlags::TOP_OF_PIPE, + vk::PipelineStageFlags::BOTTOM_OF_PIPE, + vk::DependencyFlags::empty(), + &[], + &[], + &all_barriers, + ); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_gcd() { + assert_eq!(gcd(12, 8), 4); + assert_eq!(gcd(8, 12), 4); + assert_eq!(gcd(16, 16), 16); + assert_eq!(gcd(7, 3), 1); + assert_eq!(gcd(0, 5), 5); + assert_eq!(gcd(5, 0), 5); + } + + #[test] + fn test_lcm() { + assert_eq!(lcm(32, 64), 64); + assert_eq!(lcm(16, 12), 48); + assert_eq!(lcm(4, 6), 12); + assert_eq!(lcm(0, 5), 0); + assert_eq!(lcm(5, 0), 0); + assert_eq!(lcm(7, 7), 7); + } + + #[test] + fn test_align_up() { + assert_eq!(align_up(130, 64), 192); + assert_eq!(align_up(128, 64), 128); + assert_eq!(align_up(1, 64), 64); + assert_eq!(align_up(0, 64), 0); + assert_eq!(align_up(100, 1), 100); + assert_eq!(align_up(100, 0), 100); + // AMD-realistic case: align 320 to lcm(32, 64) = 64. + assert_eq!(align_up(320, lcm(32, 64)), 320); + // AMD-realistic case: align 130 to lcm(32, 16) = 32. + assert_eq!(align_up(130, lcm(32, 16)), 160); + } +} diff --git a/src/image.rs b/src/image.rs index 4d22819..6e08d5d 100644 --- a/src/image.rs +++ b/src/image.rs @@ -763,6 +763,8 @@ impl Drop for InputImage { fn drop(&mut self) { let device = self.context.device(); unsafe { + // Wait for all GPU work to finish before destroying resources. + let _ = device.device_wait_idle(); device.destroy_fence(self.fence, None); device.destroy_command_pool(self.command_pool, None); device.destroy_buffer(self.staging_buffer, None); diff --git a/src/vulkan.rs b/src/vulkan.rs index 1ebcfa2..34a76a7 100644 --- a/src/vulkan.rs +++ b/src/vulkan.rs @@ -182,21 +182,46 @@ impl VideoContext { .engine_version(vk::make_api_version(0, 1, 0, 0)) .api_version(vk::API_VERSION_1_3); + let mut enable_validation = builder.enable_validation; + if enable_validation { + let available_layers = unsafe { entry.enumerate_instance_layer_properties() } + .map_err(|e| PixelForgeError::InstanceCreation(e.to_string()))?; + let validation_layer_name = c"VK_LAYER_KHRONOS_validation"; + let has_validation_layer = available_layers.iter().any(|layer| { + let name = unsafe { CStr::from_ptr(layer.layer_name.as_ptr()) }; + name == validation_layer_name + }); + if !has_validation_layer { + warn!("Validation layer requested but not available"); + enable_validation = false; + } + } + let mut layer_names: Vec<*const c_char> = Vec::new(); - let validation_layer = - CString::new("VK_LAYER_KHRONOS_validation").expect("Invalid layer name"); - if builder.enable_validation { + let validation_layer = c"VK_LAYER_KHRONOS_validation"; + if enable_validation { layer_names.push(validation_layer.as_ptr()); } // Enable VK_EXT_validation_features if validation is enabled to allow configuration. - let validation_features_ext = - CString::new("VK_EXT_validation_features").expect("Invalid extension name"); - let instance_extensions: Vec<*const c_char> = if builder.enable_validation { - vec![validation_features_ext.as_ptr()] - } else { - vec![] - }; + let mut instance_extensions: Vec<*const c_char> = Vec::new(); + if enable_validation { + let validation_layer_name = c"VK_LAYER_KHRONOS_validation"; + let available_exts = unsafe { + entry.enumerate_instance_extension_properties(Some(validation_layer_name)) + } + .map_err(|e| PixelForgeError::InstanceCreation(e.to_string()))?; + let validation_features_name = c"VK_EXT_validation_features"; + let has_validation_features = available_exts.iter().any(|ext| { + let name = unsafe { CStr::from_ptr(ext.extension_name.as_ptr()) }; + name == validation_features_name + }); + if has_validation_features { + instance_extensions.push(validation_features_name.as_ptr()); + } else { + warn!("VK_EXT_validation_features requested but not available"); + } + } let create_info = vk::InstanceCreateInfo::default() .application_info(&app_info) From ba222b0b75e9330b403cef8cf7cab484193171bc Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Wed, 25 Feb 2026 11:40:45 +0100 Subject: [PATCH 2/7] Add setting for setting the budget of encoding. --- src/encoder/h264/encode.rs | 4 ++-- src/encoder/h265/encode.rs | 4 ++-- src/encoder/mod.rs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/encoder/h264/encode.rs b/src/encoder/h264/encode.rs index dcf57f7..9f2e856 100644 --- a/src/encoder/h264/encode.rs +++ b/src/encoder/h264/encode.rs @@ -535,8 +535,8 @@ impl H264Encoder { if rc_mode != vk::VideoEncodeRateControlModeFlagsKHR::DISABLED { rc_info = rc_info .layers(&rc_layers) - .virtual_buffer_size_in_ms(1000) - .initial_virtual_buffer_size_in_ms(1000); + .virtual_buffer_size_in_ms(self.config.virtual_buffer_size_ms) + .initial_virtual_buffer_size_in_ms(self.config.initial_virtual_buffer_size_ms); rc_info.p_next = &mut h264_rc_info as *mut _ as *mut std::ffi::c_void; } diff --git a/src/encoder/h265/encode.rs b/src/encoder/h265/encode.rs index 907ea27..ec85fd4 100644 --- a/src/encoder/h265/encode.rs +++ b/src/encoder/h265/encode.rs @@ -562,8 +562,8 @@ impl H265Encoder { if rc_mode != vk::VideoEncodeRateControlModeFlagsKHR::DISABLED { rc_info = rc_info .layers(&rc_layers) - .virtual_buffer_size_in_ms(1000) - .initial_virtual_buffer_size_in_ms(1000); + .virtual_buffer_size_in_ms(self.config.virtual_buffer_size_ms) + .initial_virtual_buffer_size_in_ms(self.config.initial_virtual_buffer_size_ms); rc_info.p_next = (&mut h265_rc_info as *mut vk::VideoEncodeH265RateControlInfoKHR).cast(); } diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index b3f4d64..45e22fb 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -171,6 +171,17 @@ pub struct EncodeConfig { pub b_frame_count: u32, /// Maximum number of reference frames. pub max_reference_frames: u32, + /// VBV/HRD virtual buffer size in milliseconds. + /// Controls how much the encoder can deviate from the target bitrate + /// on a per-frame basis. Smaller values produce more uniform frame + /// sizes. + pub virtual_buffer_size_ms: u32, + /// Initial VBV buffer fullness in milliseconds. + /// Controls how much budget the encoder has for IDR/I-frames. + /// Setting this to 0 constrains IDR frames to the same budget as + /// P-frames. Setting it equal to `virtual_buffer_size_ms` gives + /// IDR frames maximum headroom. + pub initial_virtual_buffer_size_ms: u32, } impl EncodeConfig { @@ -193,6 +204,8 @@ impl EncodeConfig { gop_size: DEFAULT_GOP_SIZE, b_frame_count: 0, // Start without B-frames for simplicity. max_reference_frames: DEFAULT_MAX_REFERENCE_FRAMES, + virtual_buffer_size_ms: 1000, + initial_virtual_buffer_size_ms: 1000, } } @@ -215,6 +228,8 @@ impl EncodeConfig { gop_size: DEFAULT_GOP_SIZE, b_frame_count: 0, // Start without B-frames for simplicity. max_reference_frames: DEFAULT_MAX_REFERENCE_FRAMES, + virtual_buffer_size_ms: 1000, + initial_virtual_buffer_size_ms: 1000, } } @@ -278,6 +293,21 @@ impl EncodeConfig { self.max_bitrate = bitrate; self } + + /// Set the VBV/HRD virtual buffer size in milliseconds. + /// Smaller values produce more uniform frame sizes at the cost of + /// quality variation during scene changes. + pub fn with_virtual_buffer_size_ms(mut self, ms: u32) -> Self { + self.virtual_buffer_size_ms = ms; + self + } + + /// Set the initial VBV buffer fullness in milliseconds. + /// Use 0 to tightly constrain IDR/I-frame sizes. + pub fn with_initial_virtual_buffer_size_ms(mut self, ms: u32) -> Self { + self.initial_virtual_buffer_size_ms = ms; + self + } } /// Encoded video packet. From 4d6176c745867e92454613ea3add0defb5c27388 Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Wed, 25 Feb 2026 19:26:30 +0100 Subject: [PATCH 3/7] Fix H264 YUV444 encoding. --- src/encoder/h264/encode.rs | 4 + src/encoder/h264/init.rs | 113 +++++++++++++----- src/encoder/h264/mod.rs | 2 + src/encoder/h265/encode.rs | 1 + src/encoder/h265/init.rs | 35 +++++- src/encoder/resources.rs | 238 ++++++++++++++++++++++++++++++++++++- 6 files changed, 357 insertions(+), 36 deletions(-) diff --git a/src/encoder/h264/encode.rs b/src/encoder/h264/encode.rs index 9f2e856..3cc19a4 100644 --- a/src/encoder/h264/encode.rs +++ b/src/encoder/h264/encode.rs @@ -88,6 +88,7 @@ impl H264Encoder { self.use_layered_dpb, self.current_dpb_slot, &ref_dpb_slots, + self.dpb_slot_active[self.current_dpb_slot as usize], ); } @@ -687,6 +688,9 @@ impl H264Encoder { query_result.offset, query_result.bytes_written ); + // Mark DPB slot as active. + self.dpb_slot_active[self.current_dpb_slot as usize] = true; + // Read back the bitstream data using the persistently mapped buffer pointer. // This avoids per-frame map/unmap overhead (the buffer is mapped once at init) // Note: The Vulkan encoder output already includes NAL start codes (Annex B format) diff --git a/src/encoder/h264/init.rs b/src/encoder/h264/init.rs index a762159..71e1384 100644 --- a/src/encoder/h264/init.rs +++ b/src/encoder/h264/init.rs @@ -3,9 +3,10 @@ use super::{H264Encoder, MB_SIZE}; use crate::encoder::dpb::{DecodedPictureBuffer, DecodedPictureBufferTrait, DpbConfig}; use crate::encoder::gop::GopStructure; use crate::encoder::resources::{ - align_up, allocate_session_memory, create_bitstream_buffer, create_command_resources, - create_dpb_images, create_image, get_video_format, lcm, make_codec_name, map_bitstream_buffer, - query_supported_video_formats, MIN_BITSTREAM_BUFFER_SIZE, + align_up, allocate_session_memory, clear_input_image, create_bitstream_buffer, + create_command_resources, create_dpb_images, create_image, get_video_format, lcm, + map_bitstream_buffer, query_supported_video_formats, ClearImageParams, + MIN_BITSTREAM_BUFFER_SIZE, }; use crate::encoder::PixelFormat; use crate::error::{PixelForgeError, Result}; @@ -137,6 +138,45 @@ impl H264Encoder { capabilities.picture_access_granularity.height, ); + // Query quality level properties to get the driver's preferred settings. + let video_encode_instance = + ash::khr::video_encode_queue::Instance::load(context.entry(), context.instance()); + let mut h264_quality_level_properties = + vk::VideoEncodeH264QualityLevelPropertiesKHR::default(); + let mut quality_level_properties = vk::VideoEncodeQualityLevelPropertiesKHR { + p_next: (&mut h264_quality_level_properties + as *mut vk::VideoEncodeH264QualityLevelPropertiesKHR) + .cast(), + ..Default::default() + }; + let quality_level_info = vk::PhysicalDeviceVideoEncodeQualityLevelInfoKHR::default() + .video_profile(&profile_info) + .quality_level(0); + let ql_result = unsafe { + (video_encode_instance + .fp() + .get_physical_device_video_encode_quality_level_properties_khr)( + context.physical_device(), + &quality_level_info, + &mut quality_level_properties, + ) + }; + let preferred_entropy_cabac = if ql_result == vk::Result::SUCCESS { + debug!( + "H.264 quality level 0: preferredStdEntropyCodingModeFlag={}, preferredMaxL0ReferenceCount={}, preferredMaxL1ReferenceCount={}", + h264_quality_level_properties.preferred_std_entropy_coding_mode_flag, + h264_quality_level_properties.preferred_max_l0_reference_count, + h264_quality_level_properties.preferred_max_l1_reference_count, + ); + h264_quality_level_properties.preferred_std_entropy_coding_mode_flag != 0 + } else { + debug!( + "Failed to query quality level properties: {:?}, defaulting to CABAC", + ql_result + ); + true + }; + let gran_w = capabilities.picture_access_granularity.width.max(1); let gran_h = capabilities.picture_access_granularity.height.max(1); let align_w = lcm(MB_SIZE, gran_w); @@ -145,8 +185,14 @@ impl H264Encoder { let mut aligned_width = align_up(width, align_w); let mut aligned_height = align_up(height, align_h); - aligned_width = aligned_width.max(capabilities.min_coded_extent.width); - aligned_height = aligned_height.max(capabilities.min_coded_extent.height); + aligned_width = align_up( + aligned_width.max(capabilities.min_coded_extent.width), + align_w, + ); + aligned_height = align_up( + aligned_height.max(capabilities.min_coded_extent.height), + align_h, + ); if aligned_width > capabilities.max_coded_extent.width || aligned_height > capabilities.max_coded_extent.height @@ -227,10 +273,8 @@ impl H264Encoder { ); // Create video session. - let std_header_version = vk::ExtensionProperties { - extension_name: make_codec_name(b"VK_STD_vulkan_video_codec_h264_encode"), - spec_version: vk::make_api_version(0, 1, 0, 0), - }; + // Use the STD header version reported by the driver capabilities. + let std_header_version = capabilities.std_header_version; // Calculate required DPB slots and active references. let max_dpb_slots_supported = capabilities.max_dpb_slots as usize; @@ -388,20 +432,6 @@ impl H264Encoder { crate::encoder::BitDepth::Ten => (2u8, 2u8), }; - let hrd_params = ash::vk::native::StdVideoH264HrdParameters { - cpb_cnt_minus1: 0, - bit_rate_scale: 0, - cpb_size_scale: 0, - bit_rate_value_minus1: [0; 32], - cpb_size_value_minus1: [0; 32], - cbr_flag: [0; 32], - initial_cpb_removal_delay_length_minus1: 23, - cpb_removal_delay_length_minus1: 15, - dpb_output_delay_length_minus1: 5, - time_offset_length: 24, - reserved1: 0, - }; - let mut vui_flags: ash::vk::native::StdVideoH264SpsVuiFlags = unsafe { std::mem::zeroed() }; vui_flags.set_aspect_ratio_info_present_flag(1); vui_flags.set_video_signal_type_present_flag(1); @@ -429,7 +459,7 @@ impl H264Encoder { chroma_sample_loc_type_top_field: 0, chroma_sample_loc_type_bottom_field: 0, reserved1: 0, - pHrdParameters: &hrd_params, + pHrdParameters: ptr::null(), }; let sps = ash::vk::native::StdVideoH264SequenceParameterSet { @@ -441,7 +471,11 @@ impl H264Encoder { bit_depth_luma_minus8, bit_depth_chroma_minus8, log2_max_frame_num_minus4: 4, - pic_order_cnt_type: ash::vk::native::StdVideoH264PocType_STD_VIDEO_H264_POC_TYPE_0, + pic_order_cnt_type: if config.b_frame_count > 0 { + ash::vk::native::StdVideoH264PocType_STD_VIDEO_H264_POC_TYPE_0 + } else { + ash::vk::native::StdVideoH264PocType_STD_VIDEO_H264_POC_TYPE_2 + }, offset_for_non_ref_pic: 0, offset_for_top_to_bottom_field: 0, log2_max_pic_order_cnt_lsb_minus4: 4, @@ -461,8 +495,14 @@ impl H264Encoder { }; let mut pps_flags: ash::vk::native::StdVideoH264PpsFlags = unsafe { std::mem::zeroed() }; - pps_flags.set_transform_8x8_mode_flag(0); - pps_flags.set_entropy_coding_mode_flag(1); + // Enable 8x8 transform for High profile and above (required by some + // drivers for High 4:4:4 Predictive SPS/PPS generation). + let transform_8x8 = profile_idc + >= ash::vk::native::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_HIGH; + pps_flags.set_transform_8x8_mode_flag(transform_8x8 as u32); + // Use the driver's preferred entropy coding mode from quality level properties. + // Some drivers (e.g., NVIDIA for H.264 High 4:4:4 Predictive) require CAVLC. + pps_flags.set_entropy_coding_mode_flag(preferred_entropy_cabac as u32); pps_flags.set_deblocking_filter_control_present_flag(1); // vk_video_samples sets chroma QP offsets to 6 for 4:4:4 unless lossless. @@ -590,6 +630,22 @@ impl H264Encoder { let upload_fence = cmd_resources.upload_fence; let encode_fence = cmd_resources.encode_fence; + // Clear the input image so padding between user dimensions and the + // aligned coded extent is zero-initialized. + clear_input_image( + &context, + &ClearImageParams { + command_buffer: upload_command_buffer, + fence: upload_fence, + queue: context.transfer_queue(), + image: input_image, + width: aligned_width, + height: aligned_height, + pixel_format: config.pixel_format, + bit_depth: config.bit_depth, + }, + )?; + // Create query pool. let mut h264_profile_info_query = vk::VideoEncodeH264ProfileInfoKHR::default().std_profile_idc(profile_idc); @@ -672,12 +728,13 @@ impl H264Encoder { input_image, input_image_memory, input_image_view, - input_image_layout: vk::ImageLayout::UNDEFINED, + input_image_layout: vk::ImageLayout::VIDEO_ENCODE_SRC_KHR, dpb_images, dpb_image_memories, dpb_image_views, dpb_slot_count, use_layered_dpb, + dpb_slot_active: vec![false; dpb_slot_count], current_dpb_slot: 0, l0_references: Vec::new(), active_reference_count: max_active_reference_pictures as u32, diff --git a/src/encoder/h264/mod.rs b/src/encoder/h264/mod.rs index 9991e1b..d2c69db 100644 --- a/src/encoder/h264/mod.rs +++ b/src/encoder/h264/mod.rs @@ -68,6 +68,8 @@ pub struct H264Encoder { dpb_slot_count: usize, /// Whether the DPB uses a single layered image (true) or separate images (false). use_layered_dpb: bool, + /// Tracks which DPB slots have been activated (used at least once). + dpb_slot_active: Vec, bitstream_buffer: vk::Buffer, bitstream_buffer_memory: vk::DeviceMemory, /// Persistently mapped pointer to the bitstream buffer (avoids per-frame map/unmap). diff --git a/src/encoder/h265/encode.rs b/src/encoder/h265/encode.rs index ec85fd4..ca442f6 100644 --- a/src/encoder/h265/encode.rs +++ b/src/encoder/h265/encode.rs @@ -64,6 +64,7 @@ impl H265Encoder { self.use_layered_dpb, self.current_dpb_slot, &ref_dpb_slots, + self.dpb_slot_active[self.current_dpb_slot as usize], ); } diff --git a/src/encoder/h265/init.rs b/src/encoder/h265/init.rs index 8b1fa2b..636b171 100644 --- a/src/encoder/h265/init.rs +++ b/src/encoder/h265/init.rs @@ -3,9 +3,10 @@ use super::H265Encoder; use crate::encoder::dpb::{DecodedPictureBuffer, DecodedPictureBufferTrait, DpbConfig}; use crate::encoder::gop::GopStructure; use crate::encoder::resources::{ - align_up, allocate_session_memory, create_bitstream_buffer, create_command_resources, - create_dpb_images, create_image, get_video_format, lcm, make_codec_name, map_bitstream_buffer, - query_supported_video_formats, MIN_BITSTREAM_BUFFER_SIZE, + align_up, allocate_session_memory, clear_input_image, create_bitstream_buffer, + create_command_resources, create_dpb_images, create_image, get_video_format, lcm, + make_codec_name, map_bitstream_buffer, query_supported_video_formats, ClearImageParams, + MIN_BITSTREAM_BUFFER_SIZE, }; use crate::encoder::{BitDepth, PixelFormat}; use crate::error::{PixelForgeError, Result}; @@ -126,8 +127,14 @@ impl H265Encoder { let mut aligned_width = align_up(width, align_w); let mut aligned_height = align_up(height, align_h); - aligned_width = aligned_width.max(capabilities.min_coded_extent.width); - aligned_height = aligned_height.max(capabilities.min_coded_extent.height); + aligned_width = align_up( + aligned_width.max(capabilities.min_coded_extent.width), + align_w, + ); + aligned_height = align_up( + aligned_height.max(capabilities.min_coded_extent.height), + align_h, + ); if aligned_width > capabilities.max_coded_extent.width || aligned_height > capabilities.max_coded_extent.height @@ -702,6 +709,22 @@ impl H265Encoder { let upload_fence = cmd_resources.upload_fence; let encode_fence = cmd_resources.encode_fence; + // Clear the input image so padding between user dimensions and the + // aligned coded extent is zero-initialized. + clear_input_image( + &context, + &ClearImageParams { + command_buffer: upload_command_buffer, + fence: upload_fence, + queue: context.transfer_queue(), + image: input_image, + width: aligned_width, + height: aligned_height, + pixel_format: config.pixel_format, + bit_depth: config.bit_depth, + }, + )?; + // Create query pool let mut h265_profile_info_query = vk::VideoEncodeH265ProfileInfoKHR::default().std_profile_idc(profile_idc); @@ -783,7 +806,7 @@ impl H265Encoder { input_image, input_image_memory, input_image_view, - input_image_layout: vk::ImageLayout::UNDEFINED, + input_image_layout: vk::ImageLayout::VIDEO_ENCODE_SRC_KHR, dpb_images, dpb_image_memories, dpb_image_views, diff --git a/src/encoder/resources.rs b/src/encoder/resources.rs index 8efde31..7b046aa 100644 --- a/src/encoder/resources.rs +++ b/src/encoder/resources.rs @@ -642,6 +642,229 @@ pub(crate) fn map_bitstream_buffer( Ok(ptr) } +/// Parameters for clearing the input image at initialization. +pub(crate) struct ClearImageParams { + pub command_buffer: vk::CommandBuffer, + pub fence: vk::Fence, + pub queue: vk::Queue, + pub image: vk::Image, + pub width: u32, + pub height: u32, + pub pixel_format: PixelFormat, + pub bit_depth: BitDepth, +} + +/// Clear the input image by filling it with zeros via a staging buffer. +/// +/// This must be called once after creating the input image to ensure +/// the padding region (between the user dimensions and the aligned coded +/// extent) contains defined values. Without this, the first frame's +/// padding is undefined, which can cause encoding artifacts on strict +/// drivers. +pub(crate) fn clear_input_image( + context: &VideoContext, + params: &ClearImageParams, +) -> Result<()> { + let device = context.device(); + let bytes_per_component: u32 = match params.bit_depth { + BitDepth::Eight => 1, + BitDepth::Ten => 2, + }; + + // Calculate per-plane sizes. + let plane0_size = params.width * params.height * bytes_per_component; + let plane1_size = match params.pixel_format { + // NV12: UV plane is half width, half height, 2 components per pixel. + PixelFormat::Yuv420 => (params.width / 2) * (params.height / 2) * 2 * bytes_per_component, + // NV24: UV plane is full width, full height, 2 components per pixel. + PixelFormat::Yuv444 => params.width * params.height * 2 * bytes_per_component, + _ => (params.width / 2) * (params.height / 2) * 2 * bytes_per_component, + }; + let total_size = (plane0_size + plane1_size) as vk::DeviceSize; + + // Create a staging buffer filled with zeros. + let buffer_create_info = vk::BufferCreateInfo::default() + .size(total_size) + .usage(vk::BufferUsageFlags::TRANSFER_SRC) + .sharing_mode(vk::SharingMode::EXCLUSIVE); + + let staging_buffer = unsafe { device.create_buffer(&buffer_create_info, None) } + .map_err(|e| PixelForgeError::ResourceCreation(format!("staging buffer: {}", e)))?; + + let mem_requirements = unsafe { device.get_buffer_memory_requirements(staging_buffer) }; + let memory_type_index = find_memory_type( + context.memory_properties(), + mem_requirements.memory_type_bits, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, + ) + .ok_or_else(|| { + PixelForgeError::MemoryAllocation("No suitable memory type for staging buffer".to_string()) + })?; + + let alloc_info = vk::MemoryAllocateInfo::default() + .allocation_size(mem_requirements.size) + .memory_type_index(memory_type_index); + + let staging_memory = unsafe { device.allocate_memory(&alloc_info, None) } + .map_err(|e| PixelForgeError::MemoryAllocation(e.to_string()))?; + + unsafe { device.bind_buffer_memory(staging_buffer, staging_memory, 0) } + .map_err(|e| PixelForgeError::MemoryAllocation(e.to_string()))?; + + // Map and zero-fill. + let data_ptr = + unsafe { device.map_memory(staging_memory, 0, total_size, vk::MemoryMapFlags::empty()) } + .map_err(|e| PixelForgeError::MemoryAllocation(format!("map staging buffer: {}", e)))?; + unsafe { ptr::write_bytes(data_ptr as *mut u8, 0, total_size as usize) }; + unsafe { device.unmap_memory(staging_memory) }; + + // Record commands. + unsafe { + device.reset_command_buffer(params.command_buffer, vk::CommandBufferResetFlags::empty()) + } + .map_err(|e| PixelForgeError::CommandBuffer(e.to_string()))?; + + let begin_info = + vk::CommandBufferBeginInfo::default().flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); + unsafe { device.begin_command_buffer(params.command_buffer, &begin_info) } + .map_err(|e| PixelForgeError::CommandBuffer(e.to_string()))?; + + // Transition image from UNDEFINED to TRANSFER_DST. + let barrier = vk::ImageMemoryBarrier::default() + .old_layout(vk::ImageLayout::UNDEFINED) + .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(params.image) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }) + .src_access_mask(vk::AccessFlags::empty()) + .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE); + + unsafe { + device.cmd_pipeline_barrier( + params.command_buffer, + vk::PipelineStageFlags::TOP_OF_PIPE, + vk::PipelineStageFlags::TRANSFER, + vk::DependencyFlags::empty(), + &[], + &[], + &[barrier], + ); + } + + // Copy from staging buffer to image planes. + let (uv_width, uv_height) = match params.pixel_format { + PixelFormat::Yuv420 => (params.width / 2, params.height / 2), + PixelFormat::Yuv444 => (params.width, params.height), + _ => (params.width / 2, params.height / 2), + }; + + let copy_regions = [ + vk::BufferImageCopy { + buffer_offset: 0, + buffer_row_length: 0, + buffer_image_height: 0, + image_subresource: vk::ImageSubresourceLayers { + aspect_mask: vk::ImageAspectFlags::PLANE_0, + mip_level: 0, + base_array_layer: 0, + layer_count: 1, + }, + image_offset: vk::Offset3D { x: 0, y: 0, z: 0 }, + image_extent: vk::Extent3D { + width: params.width, + height: params.height, + depth: 1, + }, + }, + vk::BufferImageCopy { + buffer_offset: plane0_size as vk::DeviceSize, + buffer_row_length: 0, + buffer_image_height: 0, + image_subresource: vk::ImageSubresourceLayers { + aspect_mask: vk::ImageAspectFlags::PLANE_1, + mip_level: 0, + base_array_layer: 0, + layer_count: 1, + }, + image_offset: vk::Offset3D { x: 0, y: 0, z: 0 }, + image_extent: vk::Extent3D { + width: uv_width, + height: uv_height, + depth: 1, + }, + }, + ]; + + unsafe { + device.cmd_copy_buffer_to_image( + params.command_buffer, + staging_buffer, + params.image, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + ©_regions, + ); + } + + // Transition image to VIDEO_ENCODE_SRC. + let barrier = vk::ImageMemoryBarrier::default() + .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) + .new_layout(vk::ImageLayout::VIDEO_ENCODE_SRC_KHR) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(params.image) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }) + .src_access_mask(vk::AccessFlags::TRANSFER_WRITE) + .dst_access_mask(vk::AccessFlags::empty()); + + unsafe { + device.cmd_pipeline_barrier( + params.command_buffer, + vk::PipelineStageFlags::TRANSFER, + vk::PipelineStageFlags::BOTTOM_OF_PIPE, + vk::DependencyFlags::empty(), + &[], + &[], + &[barrier], + ); + } + + unsafe { device.end_command_buffer(params.command_buffer) } + .map_err(|e| PixelForgeError::CommandBuffer(e.to_string()))?; + + // Submit and wait. + let submit_info = + vk::SubmitInfo::default().command_buffers(std::slice::from_ref(¶ms.command_buffer)); + unsafe { device.reset_fences(&[params.fence]) } + .map_err(|e| PixelForgeError::CommandBuffer(format!("reset fence: {}", e)))?; + unsafe { device.queue_submit(params.queue, &[submit_info], params.fence) } + .map_err(|e| PixelForgeError::CommandBuffer(format!("submit clear: {}", e)))?; + unsafe { device.wait_for_fences(&[params.fence], true, u64::MAX) } + .map_err(|e| PixelForgeError::CommandBuffer(format!("wait clear: {}", e)))?; + unsafe { device.reset_fences(&[params.fence]) } + .map_err(|e| PixelForgeError::CommandBuffer(format!("reset fence after clear: {}", e)))?; + + // Clean up staging buffer. + unsafe { + device.destroy_buffer(staging_buffer, None); + device.free_memory(staging_memory, None); + } + + Ok(()) +} + /// Parameters for uploading an image to the encoder's input image. pub(crate) struct UploadParams { /// The command buffer to use for the upload. @@ -893,7 +1116,8 @@ pub(crate) struct EncoderResources<'a> { /// /// # Safety /// -/// The device must be idle before calling this function. +/// All queues that may reference these resources (transfer and video encode) +/// must be idle before calling this function. pub(crate) unsafe fn destroy_encoder_resources( device: &ash::Device, video_queue_fn: &ash::khr::video_queue::Device, @@ -952,6 +1176,7 @@ pub(crate) unsafe fn record_dpb_barriers( use_layered_dpb: bool, current_dpb_slot: u8, reference_dpb_slots: &[u8], + setup_slot_active: bool, ) { let dpb_image = if use_layered_dpb { dpb_images[0] @@ -964,8 +1189,17 @@ pub(crate) unsafe fn record_dpb_barriers( 0 }; + // Use UNDEFINED only on first use of a DPB slot; after that it is already + // in VIDEO_ENCODE_DPB_KHR and transitioning from UNDEFINED would discard + // the contents, which is invalid/UB. + let setup_old_layout = if setup_slot_active { + vk::ImageLayout::VIDEO_ENCODE_DPB_KHR + } else { + vk::ImageLayout::UNDEFINED + }; + let dpb_barrier = vk::ImageMemoryBarrier::default() - .old_layout(vk::ImageLayout::UNDEFINED) + .old_layout(setup_old_layout) .new_layout(vk::ImageLayout::VIDEO_ENCODE_DPB_KHR) .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) From e41162836632229d969c2d257ddcb397dbbf8c3a Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Wed, 25 Feb 2026 19:37:37 +0100 Subject: [PATCH 4/7] Run cargo fmt. --- src/encoder/h264/init.rs | 4 ++-- src/encoder/resources.rs | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/encoder/h264/init.rs b/src/encoder/h264/init.rs index 71e1384..ef351a9 100644 --- a/src/encoder/h264/init.rs +++ b/src/encoder/h264/init.rs @@ -497,8 +497,8 @@ impl H264Encoder { let mut pps_flags: ash::vk::native::StdVideoH264PpsFlags = unsafe { std::mem::zeroed() }; // Enable 8x8 transform for High profile and above (required by some // drivers for High 4:4:4 Predictive SPS/PPS generation). - let transform_8x8 = profile_idc - >= ash::vk::native::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_HIGH; + let transform_8x8 = + profile_idc >= ash::vk::native::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_HIGH; pps_flags.set_transform_8x8_mode_flag(transform_8x8 as u32); // Use the driver's preferred entropy coding mode from quality level properties. // Some drivers (e.g., NVIDIA for H.264 High 4:4:4 Predictive) require CAVLC. diff --git a/src/encoder/resources.rs b/src/encoder/resources.rs index 7b046aa..0d82d3c 100644 --- a/src/encoder/resources.rs +++ b/src/encoder/resources.rs @@ -661,10 +661,7 @@ pub(crate) struct ClearImageParams { /// extent) contains defined values. Without this, the first frame's /// padding is undefined, which can cause encoding artifacts on strict /// drivers. -pub(crate) fn clear_input_image( - context: &VideoContext, - params: &ClearImageParams, -) -> Result<()> { +pub(crate) fn clear_input_image(context: &VideoContext, params: &ClearImageParams) -> Result<()> { let device = context.device(); let bytes_per_component: u32 = match params.bit_depth { BitDepth::Eight => 1, From d4a24bee1fa14bbb411c08c73326b4e774f2ef3c Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Wed, 25 Feb 2026 23:38:18 +0100 Subject: [PATCH 5/7] Update src/encoder/resources.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/encoder/resources.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encoder/resources.rs b/src/encoder/resources.rs index 0d82d3c..e87ddbd 100644 --- a/src/encoder/resources.rs +++ b/src/encoder/resources.rs @@ -22,7 +22,7 @@ pub(crate) fn lcm(a: u32, b: u32) -> u32 { if a == 0 || b == 0 { 0 } else { - a / gcd(a, b) * b + (a / gcd(a, b)).saturating_mul(b) } } From f060ccb7e059b95b2944fee96c8c60c9ebe47851 Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Wed, 25 Feb 2026 23:38:41 +0100 Subject: [PATCH 6/7] Update src/encoder/resources.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/encoder/resources.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/encoder/resources.rs b/src/encoder/resources.rs index e87ddbd..f824eff 100644 --- a/src/encoder/resources.rs +++ b/src/encoder/resources.rs @@ -671,10 +671,13 @@ pub(crate) fn clear_input_image(context: &VideoContext, params: &ClearImageParam // Calculate per-plane sizes. let plane0_size = params.width * params.height * bytes_per_component; let plane1_size = match params.pixel_format { - // NV12: UV plane is half width, half height, 2 components per pixel. + // YUV 4:2:0 (e.g., NV12): UV plane is half width, half height, 2 components per pixel. PixelFormat::Yuv420 => (params.width / 2) * (params.height / 2) * 2 * bytes_per_component, - // NV24: UV plane is full width, full height, 2 components per pixel. + // YUV 4:2:2: UV plane is half width, full height, 2 components per pixel. + PixelFormat::Yuv422 => (params.width / 2) * params.height * 2 * bytes_per_component, + // YUV 4:4:4 (e.g., NV24): UV plane is full width, full height, 2 components per pixel. PixelFormat::Yuv444 => params.width * params.height * 2 * bytes_per_component, + // Default to 4:2:0-style chroma for any other subsampled formats. _ => (params.width / 2) * (params.height / 2) * 2 * bytes_per_component, }; let total_size = (plane0_size + plane1_size) as vk::DeviceSize; From 56a330c58aa34ca22bd26be4c5e2907f38fb8ad1 Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Wed, 25 Feb 2026 23:41:40 +0100 Subject: [PATCH 7/7] Fix warning. --- src/encoder/resources.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/encoder/resources.rs b/src/encoder/resources.rs index f824eff..ab71254 100644 --- a/src/encoder/resources.rs +++ b/src/encoder/resources.rs @@ -677,8 +677,6 @@ pub(crate) fn clear_input_image(context: &VideoContext, params: &ClearImageParam PixelFormat::Yuv422 => (params.width / 2) * params.height * 2 * bytes_per_component, // YUV 4:4:4 (e.g., NV24): UV plane is full width, full height, 2 components per pixel. PixelFormat::Yuv444 => params.width * params.height * 2 * bytes_per_component, - // Default to 4:2:0-style chroma for any other subsampled formats. - _ => (params.width / 2) * (params.height / 2) * 2 * bytes_per_component, }; let total_size = (plane0_size + plane1_size) as vk::DeviceSize;