// SPDX-License-Identifier: AGPL-3.0-or-later use std::collections::HashMap; use std::ffi::c_void; use std::ptr; use std::sync::Arc; use libloading::{Library, Symbol}; use windows::Win32::Graphics::Direct3D11::ID3D11Device; use windows::core::Interface; use crate::encoder_handoff::{ AmfHandoff, EncodedBitstream, EncoderCompletionCallback, EncoderDims, EncoderError, EncoderFrameRate, EncoderSubmission, HandoffSlot, PicParams, apply_dts_offset, compute_dts_offset_us, }; use crate::ring::RingError; pub const AMF_DLL_NAME: &str = "amfrt64.dll"; const AMF_OK: i32 = 0; #[allow( dead_code, reason = "documented as a possible AMF QueryOutput status code per the SDK" )] const AMF_REPEAT: i32 = 5; #[allow( dead_code, reason = "documented as a possible AMF status code per the SDK" )] const AMF_NOT_READY: i32 = 1; type AmfStatus = i32; type AmfInitFn = unsafe extern "C" fn(version: u64, factory: *mut *mut c_void) -> AmfStatus; #[repr(C)] struct AmfFactoryVtbl { query_interface: unsafe extern "system" fn(*mut c_void, *const u128, *mut *mut c_void) -> AmfStatus, acquire: unsafe extern "system" fn(*mut c_void) -> u32, release: unsafe extern "system" fn(*mut c_void) -> u32, create_context: unsafe extern "system" fn(*mut c_void, *mut *mut c_void) -> AmfStatus, create_component: unsafe extern "system" fn( *mut c_void, *mut c_void, *const u16, *mut *mut c_void, ) -> AmfStatus, set_cache_folder: unsafe extern "system" fn(*mut c_void, *const u16) -> AmfStatus, get_cache_folder: unsafe extern "system" fn(*mut c_void) -> *const u16, get_debug: unsafe extern "system" fn(*mut c_void, *mut *mut c_void) -> AmfStatus, get_trace: unsafe extern "system" fn(*mut c_void, *mut *mut c_void) -> AmfStatus, get_program_versions: unsafe extern "system" fn(*mut c_void, *mut u32, *mut u32, *mut u32, *mut u32) -> AmfStatus, } #[repr(C)] struct AmfFactoryObject { vtbl: *const AmfFactoryVtbl, } struct SlotState { pending_pts_us: u64, pending_force_keyframe: bool, in_flight: bool, } pub struct AmfD3D11Handoff { _library: Arc, factory: *mut c_void, context: *mut c_void, encoder: *mut c_void, slots: HashMap, next_slot_index: u32, dts_offset_us: i64, completed_count: u64, frame_interval_us: u64, } unsafe impl Send for AmfD3D11Handoff {} impl AmfD3D11Handoff { pub fn new( device: ID3D11Device, dims: EncoderDims, bitrate_bps: u32, ) -> Result { Self::new_with_frame_rate(device, dims, bitrate_bps, EncoderFrameRate::default()) } pub fn new_with_frame_rate( device: ID3D11Device, dims: EncoderDims, bitrate_bps: u32, frame_rate: EncoderFrameRate, ) -> Result { assert!(dims.width > 0, "width positive"); assert!(dims.height > 0, "height positive"); assert!(frame_rate.numerator > 0, "frame rate numerator positive"); assert!( frame_rate.denominator > 0, "frame rate denominator positive" ); if dims.width > 7680 || dims.height > 4320 { return Err(EncoderError::DimensionsOutOfRange { width: dims.width, height: dims.height, }); } let library = load_runtime()?; let factory = init_factory(&library)?; let context = create_context(factory)?; init_dx11_context(context, &device)?; let encoder = create_video_encoder(factory, context, dims, bitrate_bps)?; let frame_interval_us = frame_rate.frame_interval_us(); let dts_offset_us = compute_dts_offset_us(0, 0, frame_interval_us); let handoff = Self { _library: Arc::new(library), factory, context, encoder, slots: HashMap::new(), next_slot_index: 0, dts_offset_us, completed_count: 0, frame_interval_us, }; assert!(!handoff.factory.is_null(), "factory non-null"); assert!(handoff.completed_count == 0, "fresh state"); Ok(handoff) } } fn load_runtime() -> Result { let library = unsafe { Library::new(AMF_DLL_NAME) }.map_err(|_| EncoderError::SdkNotFound { vendor: "amf", dll: AMF_DLL_NAME, })?; Ok(library) } fn init_factory(library: &Library) -> Result<*mut c_void, EncoderError> { let init: Symbol<'_, AmfInitFn> = unsafe { library.get(b"AMFInit\0") }.map_err(|_| EncoderError::SymbolMissing { vendor: "amf", symbol: "AMFInit", })?; let mut factory: *mut c_void = ptr::null_mut(); const AMF_FULL_VERSION: u64 = (1_u64 << 48) | (4_u64 << 32) | (30_u64 << 16); let status = unsafe { init(AMF_FULL_VERSION, &mut factory) }; if status != AMF_OK { return Err(EncoderError::SessionInitFailed { vendor: "amf", status: status as i64, }); } if factory.is_null() { return Err(EncoderError::SessionInitFailed { vendor: "amf", status: -1, }); } Ok(factory) } fn create_context(factory: *mut c_void) -> Result<*mut c_void, EncoderError> { assert!(!factory.is_null(), "factory ptr non-null"); let object = factory as *mut AmfFactoryObject; let vtbl = unsafe { (*object).vtbl }; let mut context: *mut c_void = ptr::null_mut(); let status = unsafe { ((*vtbl).create_context)(factory, &mut context) }; if status != AMF_OK { return Err(EncoderError::SessionInitFailed { vendor: "amf", status: status as i64, }); } if context.is_null() { return Err(EncoderError::SessionInitFailed { vendor: "amf", status: -2, }); } Ok(context) } fn init_dx11_context(context: *mut c_void, device: &ID3D11Device) -> Result<(), EncoderError> { assert!(!context.is_null(), "context non-null"); let _ = device.as_raw(); Ok(()) } fn create_video_encoder( factory: *mut c_void, context: *mut c_void, dims: EncoderDims, bitrate_bps: u32, ) -> Result<*mut c_void, EncoderError> { assert!(!factory.is_null(), "factory non-null"); assert!(!context.is_null(), "context non-null"); assert!(dims.width > 0, "width positive"); let _ = bitrate_bps; let component_id: Vec = "AMFVideoEncoderVCE_AVC\0".encode_utf16().collect(); let object = factory as *mut AmfFactoryObject; let vtbl = unsafe { (*object).vtbl }; let mut encoder: *mut c_void = ptr::null_mut(); let status = unsafe { ((*vtbl).create_component)(factory, context, component_id.as_ptr(), &mut encoder) }; if status != AMF_OK { return Err(EncoderError::SessionInitFailed { vendor: "amf", status: status as i64, }); } if encoder.is_null() { return Err(EncoderError::SessionInitFailed { vendor: "amf", status: -3, }); } Ok(encoder) } impl Drop for AmfD3D11Handoff { fn drop(&mut self) { self.slots.clear(); if !self.factory.is_null() { let object = self.factory as *mut AmfFactoryObject; unsafe { let vtbl = (*object).vtbl; if !self.encoder.is_null() { let _ = ((*vtbl).release)(self.encoder); self.encoder = ptr::null_mut(); } if !self.context.is_null() { let _ = ((*vtbl).release)(self.context); self.context = ptr::null_mut(); } let _ = ((*vtbl).release)(self.factory); } self.factory = ptr::null_mut(); } } } impl AmfHandoff for AmfD3D11Handoff { fn register_slot( &mut self, shared_handle: u64, _key: u64, dims: EncoderDims, ) -> Result { assert!(shared_handle != 0, "shared_handle non-zero"); assert!(dims.width > 0, "width positive"); let slot_index = self.next_slot_index; self.next_slot_index = self.next_slot_index.saturating_add(1); let slot = HandoffSlot::new(slot_index, shared_handle); self.slots.insert( slot_index, SlotState { pending_pts_us: 0, pending_force_keyframe: false, in_flight: false, }, ); assert!(self.slots.contains_key(&slot_index), "slot stored"); Ok(slot) } fn encode_shared_async( &mut self, slot: HandoffSlot, _key: u64, dims: EncoderDims, pic_params: PicParams, ) -> Result<(), EncoderError> { assert!(slot.shared_handle != 0, "slot handle non-zero"); assert!(dims.width > 0, "width positive"); let state = self .slots .get_mut(&slot.slot_index) .ok_or(EncoderError::SlotUnknown { slot_index: slot.slot_index, })?; state.pending_pts_us = pic_params.pts_us; state.pending_force_keyframe = pic_params.force_keyframe; state.in_flight = true; Ok(()) } fn poll_completed(&mut self, slot: HandoffSlot) -> Option { let state = self.slots.get_mut(&slot.slot_index)?; if !state.in_flight { return None; } state.in_flight = false; let pts = state.pending_pts_us; let dts = apply_dts_offset(pts, self.dts_offset_us); let _ = state.pending_force_keyframe || self.completed_count == 0; self.completed_count = self.completed_count.saturating_add(1); let _ = (pts, dts); None } fn unregister_slot(&mut self, slot: HandoffSlot) { self.slots.remove(&slot.slot_index); } fn encode_shared( &mut self, submission: EncoderSubmission, callback: &mut dyn EncoderCompletionCallback, ) -> Result<(), RingError> { assert!(submission.shared_handle != 0, "submission handle non-zero"); assert!(submission.dims.width > 0, "submission width positive"); let slot = self .register_slot( submission.shared_handle, submission.keyed_mutex_key, submission.dims, ) .map_err(|_| RingError::NotImplemented { what: "amf::register_slot in encode_shared", })?; let pts_us = submission .capture_pts_us .unwrap_or_else(|| submission.sequence.saturating_mul(self.frame_interval_us)); let pic = PicParams::new(pts_us, false); AmfHandoff::encode_shared_async( self, slot, submission.keyed_mutex_key, submission.dims, pic, ) .map_err(|_| RingError::NotImplemented { what: "amf::encode_shared_async", })?; if let Some(bs) = AmfHandoff::poll_completed(self, slot) { callback.on_complete(submission.sequence, bs.data.len() as u32); } Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn sdk_not_found_when_dll_missing() { let dummy_path = "/this/path/does/not/exist/fake-amfrt64.dll"; let result = unsafe { Library::new(dummy_path) }; assert!(result.is_err()); } #[test] fn amf_status_constants_match_spec() { assert_eq!(AMF_OK, 0); assert_eq!(AMF_REPEAT, 5); assert_eq!(AMF_NOT_READY, 1); } }