use anyhow::{Context, Result, bail};
use bytes::Bytes;
use std::ffi::c_void;
use std::os::raw::c_int;
use std::ptr;
use super::tuning::{self, AmfQualityPreset, AmfRateControl};
use super::{AUTO_FROM_TARGET, EncodedPacket, Encoder, EncoderConfig};
#[cfg(test)]
use crate::frame::ColorMetadata;
use crate::frame::{PixelFormat, TransferFn, VideoFrame};
type AmfResult = i32;
const AMF_OK: AmfResult = 0;
#[allow(dead_code)]
const AMF_FAIL: AmfResult = 1;
const AMF_NEED_MORE_INPUT: AmfResult = 2022;
const AMF_REPEAT: AmfResult = 2023;
const AMF_EOF: AmfResult = 2024;
const AMF_INPUT_FULL: AmfResult = 2020;
const AMF_VERSION: u64 = amf_make_version(1, 4, 30, 0);
const fn amf_make_version(major: u64, minor: u64, sub_major: u64, sub_minor: u64) -> u64 {
(major << 48) | (minor << 32) | (sub_major << 16) | sub_minor
}
const AMF_MEMORY_HOST: i32 = 1;
const AMF_SURFACE_NV12: i32 = 1;
const AMF_SURFACE_P010: i32 = 10;
const AMF_PLANE_Y: i32 = 2;
const AMF_PLANE_UV: i32 = 3;
const AMF_VARIANT_INT64: i32 = 2;
const AMF_RC_CQP: i64 = 1;
const AMF_RC_QUALITY_VBR: i64 = 5;
const AMF_OUTPUT_FRAME_TYPE_KEY: i64 = 0;
const AMF_OUTPUT_FRAME_TYPE_INTRA_ONLY: i64 = 1;
const AMF_USAGE_TRANSCODING: i64 = 0;
const AMF_OUTPUT_MODE_FRAME: i64 = 0;
const AMF_AV1_COLOR_BIT_DEPTH_8: i64 = 1;
const AMF_AV1_COLOR_BIT_DEPTH_10: i64 = 2;
const RING_SIZE: usize = 4;
const INPUT_FULL_MAX_RETRIES: u32 = 16;
const INPUT_FULL_BACKOFF_MS_INITIAL: u64 = 1;
const INPUT_FULL_BACKOFF_MS_MAX: u64 = 16;
#[repr(C)]
#[derive(Clone, Copy)]
struct AmfVariant {
ty: i32,
_pad: i32,
value: [u8; 24],
}
const _: () = {
assert!(
std::mem::size_of::<AmfVariant>() == 32,
"AmfVariant must be 32 bytes"
);
assert!(
std::mem::offset_of!(AmfVariant, value) == 8,
"AmfVariant value payload must start at offset 8"
);
};
const _: () = assert!(AMF_SURFACE_NV12 == 1);
const _: () = assert!(AMF_SURFACE_P010 == 10);
const _: () = assert!(AMF_AV1_COLOR_BIT_DEPTH_8 == 1);
const _: () = assert!(AMF_AV1_COLOR_BIT_DEPTH_10 == 2);
const _: () = assert!(amf_color_bit_depth_for(PixelFormat::Yuv420p10le) == 2);
const _: () = assert!(amf_color_bit_depth_for(PixelFormat::Yuv420p) == 1);
impl AmfVariant {
fn int64(v: i64) -> Self {
let mut value = [0u8; 24];
value[..8].copy_from_slice(&v.to_le_bytes());
Self {
ty: AMF_VARIANT_INT64,
_pad: 0,
value,
}
}
#[allow(dead_code)]
fn as_int64(&self) -> Option<i64> {
if self.ty == AMF_VARIANT_INT64 {
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&self.value[..8]);
Some(i64::from_le_bytes(bytes))
} else {
None
}
}
}
type QueryInterfaceFn = unsafe extern "C" fn(*mut c_void, *const c_void, *mut *mut c_void) -> i64;
type AcquireFn = unsafe extern "C" fn(*mut c_void) -> i64;
type ReleaseFn = unsafe extern "C" fn(*mut c_void) -> i64;
#[repr(C)]
struct AmfFactoryVtbl {
create_context: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> AmfResult,
create_component:
unsafe extern "C" fn(*mut c_void, *mut c_void, *const u16, *mut *mut c_void) -> AmfResult,
set_cache_folder: unsafe extern "C" fn(*mut c_void, *const u16) -> AmfResult,
get_cache_folder: unsafe extern "C" fn(*mut c_void) -> *const u16,
get_debug: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> AmfResult,
get_trace: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> AmfResult,
get_programs: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> AmfResult,
}
#[repr(C)]
struct AmfFactoryObj {
vtbl: *const AmfFactoryVtbl,
}
#[repr(C)]
struct AmfContextVtbl {
query_interface: QueryInterfaceFn,
acquire: AcquireFn,
release: ReleaseFn,
terminate: unsafe extern "C" fn(*mut c_void) -> AmfResult,
init_dx11: unsafe extern "C" fn(*mut c_void, *mut c_void, i32) -> AmfResult,
get_dx11_device: unsafe extern "C" fn(*mut c_void, i32) -> *mut c_void,
lock_dx11: unsafe extern "C" fn(*mut c_void) -> AmfResult,
unlock_dx11: unsafe extern "C" fn(*mut c_void) -> AmfResult,
init_opencl: unsafe extern "C" fn(*mut c_void, *mut c_void) -> AmfResult,
get_opencl_context: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
get_opencl_command_queue: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
get_opencl_device_id: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
convert_to_opencl: unsafe extern "C" fn(*mut c_void, *mut c_void) -> AmfResult,
lock_opencl: unsafe extern "C" fn(*mut c_void) -> AmfResult,
unlock_opencl: unsafe extern "C" fn(*mut c_void) -> AmfResult,
init_opengl:
unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *mut c_void) -> AmfResult,
get_opengl_context: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
get_opengl_drawable: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
convert_to_opengl: unsafe extern "C" fn(*mut c_void, *mut c_void) -> AmfResult,
lock_opengl: unsafe extern "C" fn(*mut c_void) -> AmfResult,
unlock_opengl: unsafe extern "C" fn(*mut c_void) -> AmfResult,
init_vulkan: unsafe extern "C" fn(*mut c_void, *mut c_void) -> AmfResult,
get_vulkan_device: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
lock_vulkan: unsafe extern "C" fn(*mut c_void) -> AmfResult,
unlock_vulkan: unsafe extern "C" fn(*mut c_void) -> AmfResult,
alloc_buffer: unsafe extern "C" fn(*mut c_void, i32, usize, *mut *mut c_void) -> AmfResult,
alloc_surface: unsafe extern "C" fn(
*mut c_void,
i32, // memory type
i32, // surface format
i32, // width
i32, // height
*mut *mut c_void,
) -> AmfResult,
create_surface_from_host_native: unsafe extern "C" fn(
*mut c_void,
i32,
i32,
i32,
i32,
i32,
*mut c_void,
*mut *mut c_void,
*mut c_void,
) -> AmfResult,
}
#[repr(C)]
struct AmfContextObj {
vtbl: *const AmfContextVtbl,
}
#[repr(C)]
struct AmfComponentVtbl {
query_interface: QueryInterfaceFn,
acquire: AcquireFn,
release: ReleaseFn,
set_property: unsafe extern "C" fn(*mut c_void, *const u16, AmfVariant) -> AmfResult,
get_property: unsafe extern "C" fn(*mut c_void, *const u16, *mut AmfVariant) -> AmfResult,
init: unsafe extern "C" fn(*mut c_void, i32, i32, i32) -> AmfResult,
reinit: unsafe extern "C" fn(*mut c_void, i32, i32) -> AmfResult,
terminate: unsafe extern "C" fn(*mut c_void) -> AmfResult,
drain: unsafe extern "C" fn(*mut c_void) -> AmfResult,
flush: unsafe extern "C" fn(*mut c_void) -> AmfResult,
submit_input: unsafe extern "C" fn(*mut c_void, *mut c_void) -> AmfResult,
query_output: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> AmfResult,
get_context: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
set_output_data_allocator_cb: unsafe extern "C" fn(*mut c_void, *mut c_void) -> AmfResult,
get_caps: unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> AmfResult,
optimize: unsafe extern "C" fn(*mut c_void, *mut c_void) -> AmfResult,
}
#[repr(C)]
struct AmfComponentObj {
vtbl: *const AmfComponentVtbl,
}
#[repr(C)]
struct AmfSurfaceVtbl {
query_interface: QueryInterfaceFn,
acquire: AcquireFn,
release: ReleaseFn,
set_property: unsafe extern "C" fn(*mut c_void, *const u16, AmfVariant) -> AmfResult,
get_property: unsafe extern "C" fn(*mut c_void, *const u16, *mut AmfVariant) -> AmfResult,
duplicate: unsafe extern "C" fn(*mut c_void, i32, *mut *mut c_void) -> AmfResult,
get_pts: unsafe extern "C" fn(*mut c_void) -> i64,
set_pts: unsafe extern "C" fn(*mut c_void, i64),
get_duration: unsafe extern "C" fn(*mut c_void) -> i64,
set_duration: unsafe extern "C" fn(*mut c_void, i64),
get_planes_count: unsafe extern "C" fn(*mut c_void) -> usize,
get_plane_at: unsafe extern "C" fn(*mut c_void, usize) -> *mut c_void,
get_plane: unsafe extern "C" fn(*mut c_void, i32) -> *mut c_void,
}
#[repr(C)]
struct AmfSurfaceObj {
vtbl: *const AmfSurfaceVtbl,
}
#[repr(C)]
struct AmfPlaneVtbl {
query_interface: QueryInterfaceFn,
acquire: AcquireFn,
release: ReleaseFn,
get_type: unsafe extern "C" fn(*mut c_void) -> i32,
get_native: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
get_pixel_size_in_bytes: unsafe extern "C" fn(*mut c_void) -> i32,
get_offset_x: unsafe extern "C" fn(*mut c_void) -> i32,
get_offset_y: unsafe extern "C" fn(*mut c_void) -> i32,
get_width: unsafe extern "C" fn(*mut c_void) -> i32,
get_height: unsafe extern "C" fn(*mut c_void) -> i32,
get_h_pitch: unsafe extern "C" fn(*mut c_void) -> i32,
get_v_pitch: unsafe extern "C" fn(*mut c_void) -> i32,
}
#[repr(C)]
struct AmfPlaneObj {
vtbl: *const AmfPlaneVtbl,
}
#[repr(C)]
struct AmfBufferVtbl {
query_interface: QueryInterfaceFn,
acquire: AcquireFn,
release: ReleaseFn,
set_property: unsafe extern "C" fn(*mut c_void, *const u16, AmfVariant) -> AmfResult,
get_property: unsafe extern "C" fn(*mut c_void, *const u16, *mut AmfVariant) -> AmfResult,
duplicate: unsafe extern "C" fn(*mut c_void, i32, *mut *mut c_void) -> AmfResult,
get_pts: unsafe extern "C" fn(*mut c_void) -> i64,
set_pts: unsafe extern "C" fn(*mut c_void, i64),
get_duration: unsafe extern "C" fn(*mut c_void) -> i64,
set_duration: unsafe extern "C" fn(*mut c_void, i64),
get_native: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
get_size: unsafe extern "C" fn(*mut c_void) -> usize,
}
#[repr(C)]
struct AmfBufferObj {
vtbl: *const AmfBufferVtbl,
}
const AMF_IID_BUFFER: [u8; 16] = [
0xbe, 0x5d, 0xd7, 0xb1, 0x6c, 0x0e, 0x4c, 0x43, 0xb7, 0x28, 0x02, 0x85, 0x98, 0x37, 0x85, 0x7d,
];
type FnAmfInit = unsafe extern "C" fn(u64, *mut *mut c_void) -> AmfResult;
fn wide(s: &str) -> Vec<u16> {
let mut out: Vec<u16> = s.encode_utf16().collect();
out.push(0);
out
}
fn prop(s: &str) -> Vec<u16> {
wide(s)
}
fn amf_surface_format_for(fmt: PixelFormat) -> Result<i32> {
match fmt {
PixelFormat::Yuv420p => Ok(AMF_SURFACE_NV12),
PixelFormat::Yuv420p10le => Ok(AMF_SURFACE_P010),
other => bail!("AMF AV1 expects Yuv420p or Yuv420p10le, got {other:?}"),
}
}
const fn amf_color_bit_depth_for(fmt: PixelFormat) -> i64 {
match fmt {
PixelFormat::Yuv420p10le => AMF_AV1_COLOR_BIT_DEPTH_10,
_ => AMF_AV1_COLOR_BIT_DEPTH_8,
}
}
fn transfer_to_h273(tf: TransferFn) -> i64 {
match tf {
TransferFn::Bt709 => 1,
TransferFn::Bt470Bg => 4,
TransferFn::Linear => 8,
TransferFn::St2084 => 16,
TransferFn::AribStdB67 => 18,
TransferFn::Unspecified => 1,
}
}
struct SurfaceGuard {
surface: *mut c_void,
owned: bool,
}
impl SurfaceGuard {
fn new(surface: *mut c_void) -> Self {
Self {
surface,
owned: true,
}
}
fn transfer_to_encoder(&mut self) {
self.owned = false;
}
fn as_ptr(&self) -> *mut c_void {
self.surface
}
}
impl Drop for SurfaceGuard {
fn drop(&mut self) {
if self.owned && !self.surface.is_null() {
unsafe {
let obj = self.surface as *mut AmfSurfaceObj;
let vt = &*(*obj).vtbl;
(vt.release)(self.surface);
}
}
}
}
struct AmfSession {
encoder: *mut c_void,
context: *mut c_void,
#[allow(dead_code)]
factory: *mut c_void,
width: u32,
height: u32,
pts_timescale: u64,
surface_format: i32,
}
unsafe impl Send for AmfSession {}
impl Drop for AmfSession {
fn drop(&mut self) {
unsafe {
if !self.encoder.is_null() {
let obj = self.encoder as *mut AmfComponentObj;
let vt = &*(*obj).vtbl;
let _ = (vt.terminate)(self.encoder);
let _ = (vt.release)(self.encoder);
}
if !self.context.is_null() {
let obj = self.context as *mut AmfContextObj;
let vt = &*(*obj).vtbl;
let _ = (vt.terminate)(self.context);
let _ = (vt.release)(self.context);
}
}
}
}
pub struct AmfEncoder {
config: EncoderConfig,
session: Option<AmfSession>,
encoded_packets: Vec<EncodedPacket>,
packet_cursor: usize,
flushed: bool,
frame_counter: u32,
ring_idx: usize,
_runtime_lib: libloading::Library,
}
impl AmfEncoder {
pub fn new(config: EncoderConfig, gpu_index: u32) -> Result<Self> {
let runtime_lib = unsafe { libloading::Library::new("libamfrt64.so.1") }
.or_else(|_| unsafe { libloading::Library::new("libamfrt64.so") })
.or_else(|_| unsafe { libloading::Library::new("amfrt64.dll") })
.context("loading AMF runtime library (AMD driver not present?)")?;
unsafe {
let amf_init: libloading::Symbol<FnAmfInit> =
runtime_lib.get(b"AMFInit").context("AMFInit symbol")?;
let mut factory: *mut c_void = ptr::null_mut();
let rc = amf_init(AMF_VERSION, &mut factory);
if rc != AMF_OK || factory.is_null() {
bail!("AMFInit failed: {rc}");
}
let mut context: *mut c_void = ptr::null_mut();
let factory_obj = factory as *mut AmfFactoryObj;
let factory_vt = &*(*factory_obj).vtbl;
let rc = (factory_vt.create_context)(factory, &mut context);
if rc != AMF_OK || context.is_null() {
bail!("AMFFactory::CreateContext failed: {rc}");
}
if gpu_index != 0 {
tracing::warn!(
gpu_index,
"AMF init picks adapter 0 unconditionally; \
multi-AMD hosts may need external adapter routing"
);
}
let context_obj = context as *mut AmfContextObj;
let context_vt = &*(*context_obj).vtbl;
let rc_dx11 = (context_vt.init_dx11)(context, ptr::null_mut(), 0);
if rc_dx11 != AMF_OK {
let rc_vk = (context_vt.init_vulkan)(context, ptr::null_mut());
if rc_vk != AMF_OK {
(context_vt.release)(context);
bail!("AMFContext::InitDX11 ({rc_dx11}) and InitVulkan ({rc_vk}) both failed");
}
}
let component_id = wide("AMFVideoEncoderVCN_AV1");
let mut encoder: *mut c_void = ptr::null_mut();
let rc = (factory_vt.create_component)(
factory,
context,
component_id.as_ptr(),
&mut encoder,
);
if rc != AMF_OK || encoder.is_null() {
(context_vt.terminate)(context);
(context_vt.release)(context);
bail!(
"AMFFactory::CreateComponent(AMFVideoEncoderVCN_AV1) failed: {rc} — RDNA3+ GPU required"
);
}
let encoder_obj = encoder as *mut AmfComponentObj;
let encoder_vt = &*(*encoder_obj).vtbl;
let tp =
tuning::amf_av1_params(config.target, config.tier, config.width, config.height);
let q_intra = if config.quality == AUTO_FROM_TARGET {
tp.q_index_intra
} else {
((config.quality as u32 * 4).min(255)) as u8
};
let q_inter = q_intra.saturating_add(8);
set_int_property(encoder, encoder_vt, "Av1Usage", AMF_USAGE_TRANSCODING)?;
set_int_property(
encoder,
encoder_vt,
"Av1RateControlMethod",
if config.constant_qp {
AMF_RC_CQP
} else {
match tp.rc_mode {
AmfRateControl::Cqp => AMF_RC_CQP,
AmfRateControl::QualityVbr => AMF_RC_QUALITY_VBR,
}
},
)?;
set_int_property(
encoder,
encoder_vt,
"Av1QualityPreset",
amf_quality_preset_i64(tp.quality_preset),
)?;
set_int_property(encoder, encoder_vt, "Av1QIndexIntra", q_intra as i64)?;
set_int_property(encoder, encoder_vt, "Av1QIndexInter", q_inter as i64)?;
if tp.rc_mode == AmfRateControl::QualityVbr {
set_int_property(
encoder,
encoder_vt,
"Av1QvbrQualityLevel",
tp.qvbr_quality as i64,
)?;
}
set_int_property(
encoder,
encoder_vt,
"Av1GOPSize",
config.keyframe_interval as i64,
)?;
set_int_property(encoder, encoder_vt, "Av1AQMode", tp.aq_mode as i64)?;
set_int_property(
encoder,
encoder_vt,
"Av1TilesPerFrame",
tp.tiles_per_frame as i64,
)?;
set_int_property(encoder, encoder_vt, "Av1OutputMode", AMF_OUTPUT_MODE_FRAME)?;
let surface_fmt = amf_surface_format_for(config.pixel_format)?;
let color_bit_depth = amf_color_bit_depth_for(config.pixel_format);
set_int_property(encoder, encoder_vt, "Av1ColorBitDepth", color_bit_depth)?;
let cm = &config.color_metadata;
set_int_property(
encoder,
encoder_vt,
"Av1OutColorPrimaries",
cm.colour_primaries as i64,
)?;
set_int_property(
encoder,
encoder_vt,
"Av1OutColorTransferChar",
transfer_to_h273(cm.transfer),
)?;
set_int_property(
encoder,
encoder_vt,
"Av1OutColorMatrixCoeff",
cm.matrix_coefficients as i64,
)?;
set_int_property(
encoder,
encoder_vt,
"Av1OutColorRange",
if cm.full_range { 1 } else { 0 },
)?;
tracing::info!(
width = config.width,
height = config.height,
target = ?config.target,
tier = ?config.tier,
q_index_intra = q_intra,
q_index_inter = q_inter,
qvbr_quality = tp.qvbr_quality,
rc_mode = ?tp.rc_mode,
quality_preset = ?tp.quality_preset,
tiles_per_frame = tp.tiles_per_frame,
ring_size = RING_SIZE,
"AMF AV1 tuning applied"
);
let rc = (encoder_vt.init)(
encoder,
surface_fmt,
config.width as i32,
config.height as i32,
);
if rc != AMF_OK {
(encoder_vt.release)(encoder);
(context_vt.terminate)(context);
(context_vt.release)(context);
bail!(
"AMFComponent::Init(AV1, {fmt}, {w}x{h}) failed: {rc} \
(surface format dispatched for {pf:?})",
fmt = surface_fmt,
w = config.width,
h = config.height,
pf = config.pixel_format,
);
}
let session = AmfSession {
encoder,
context,
factory,
width: config.width,
height: config.height,
pts_timescale: (10_000_000.0f64 / config.frame_rate).round() as u64,
surface_format: surface_fmt,
};
tracing::info!(
width = config.width,
height = config.height,
gpu = gpu_index,
"AMF AV1 encoder ready"
);
Ok(Self {
config,
session: Some(session),
encoded_packets: Vec::new(),
packet_cursor: 0,
flushed: false,
frame_counter: 0,
ring_idx: 0,
_runtime_lib: runtime_lib,
})
}
}
fn encode_one(&mut self, frame: &VideoFrame) -> Result<()> {
let session = self
.session
.as_ref()
.ok_or_else(|| anyhow::anyhow!("encode_one called after session drop"))?;
let encoder_ptr = session.encoder;
let snap = SessionSnapshot {
encoder: session.encoder,
context: session.context,
width: session.width,
height: session.height,
pts_timescale: session.pts_timescale,
surface_format: session.surface_format,
};
let force_key = self
.frame_counter
.is_multiple_of(self.effective_keyframe_interval());
let packets = &mut self.encoded_packets;
let ring_slot = self.ring_idx;
let outcome = unsafe {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let raw_surface = upload_frame_static(&snap, frame)?;
let mut guard = SurfaceGuard::new(raw_surface);
if force_key {
let surface_obj = guard.as_ptr() as *mut AmfSurfaceObj;
let surface_vt = &*(*surface_obj).vtbl;
let key = AmfVariant::int64(1);
let name = prop("Av1ForceKeyFrame");
(surface_vt.set_property)(guard.as_ptr(), name.as_ptr(), key);
}
submit_with_backpressure(packets, encoder_ptr, &mut guard)?;
drain_until_hungry_raw(packets, encoder_ptr)?;
Ok::<(), anyhow::Error>(())
}));
match result {
Ok(inner) => inner,
Err(_panic) => {
bail!("panic in AMF encode path — aborting rather than unwinding across FFI")
}
}
};
outcome?;
self.frame_counter += 1;
self.ring_idx = (ring_slot + 1) % RING_SIZE;
Ok(())
}
fn effective_keyframe_interval(&self) -> u32 {
if self.config.keyframe_interval == 0 {
240
} else {
self.config.keyframe_interval
}
}
fn flush_drain(&mut self) -> Result<()> {
let encoder_ptr = match &self.session {
Some(s) => s.encoder,
None => return Ok(()),
};
let packets = &mut self.encoded_packets;
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
let encoder_obj = encoder_ptr as *mut AmfComponentObj;
let encoder_vt = &*(*encoder_obj).vtbl;
let rc = (encoder_vt.drain)(encoder_ptr);
if rc != AMF_OK && rc != AMF_REPEAT {
bail!("AMF Drain failed: {rc}");
}
drain_until_hungry_raw(packets, encoder_ptr)?;
Ok::<(), anyhow::Error>(())
}));
match result {
Ok(inner) => inner,
Err(_panic) => {
bail!("panic in AMF flush path — aborting rather than unwinding across FFI")
}
}
}
#[allow(dead_code)]
fn _suppress_unused_c_int() -> c_int {
0
}
}
impl Encoder for AmfEncoder {
fn send_frame(&mut self, frame: &VideoFrame) -> Result<()> {
if frame.format != self.config.pixel_format {
bail!(
"AMF session was initialized with {:?} input but frame is {:?}",
self.config.pixel_format,
frame.format
);
}
self.encode_one(frame)
}
fn flush(&mut self) -> Result<()> {
if !self.flushed {
self.flush_drain()?;
self.flushed = true;
}
Ok(())
}
fn receive_packet(&mut self) -> Result<Option<EncodedPacket>> {
if self.packet_cursor < self.encoded_packets.len() {
let pkt = self.encoded_packets[self.packet_cursor].clone();
self.packet_cursor += 1;
Ok(Some(pkt))
} else {
Ok(None)
}
}
}
unsafe fn submit_with_backpressure(
packets: &mut Vec<EncodedPacket>,
encoder: *mut c_void,
guard: &mut SurfaceGuard,
) -> Result<()> {
unsafe {
let encoder_obj = encoder as *mut AmfComponentObj;
let encoder_vt = &*(*encoder_obj).vtbl;
let mut backoff_ms = INPUT_FULL_BACKOFF_MS_INITIAL;
for attempt in 0..=INPUT_FULL_MAX_RETRIES {
let rc = (encoder_vt.submit_input)(encoder, guard.as_ptr());
match rc {
AMF_OK | AMF_NEED_MORE_INPUT => {
let surface_obj = guard.as_ptr() as *mut AmfSurfaceObj;
let surface_vt = &*(*surface_obj).vtbl;
(surface_vt.release)(guard.as_ptr());
guard.transfer_to_encoder();
return Ok(());
}
AMF_INPUT_FULL | AMF_REPEAT => {
if attempt == INPUT_FULL_MAX_RETRIES {
tracing::warn!(
status = rc,
attempts = attempt + 1,
"AMF SubmitInput backpressure exceeded retry budget — \
surface still caller-owned, releasing via guard"
);
bail!(
"AMF SubmitInput stuck at {rc} after {} attempts",
attempt + 1
);
}
drain_until_hungry_raw(packets, encoder)?;
if attempt > 0 {
std::thread::sleep(std::time::Duration::from_millis(backoff_ms));
backoff_ms = (backoff_ms * 2).min(INPUT_FULL_BACKOFF_MS_MAX);
}
continue;
}
other => {
tracing::warn!(
status = other,
"AMF SubmitInput hard failure — surface still caller-owned, \
releasing via guard"
);
bail!("AMF SubmitInput failed: {other}");
}
}
}
unreachable!("submit_with_backpressure loop invariant violated")
}
}
unsafe fn drain_until_hungry_raw(
packets: &mut Vec<EncodedPacket>,
encoder: *mut c_void,
) -> Result<()> {
unsafe {
loop {
let encoder_obj = encoder as *mut AmfComponentObj;
let encoder_vt = &*(*encoder_obj).vtbl;
let mut data: *mut c_void = ptr::null_mut();
let rc = (encoder_vt.query_output)(encoder, &mut data);
match rc {
AMF_OK => {
if data.is_null() {
continue;
}
if let Some(pkt) = buffer_to_packet(data)? {
packets.push(pkt);
}
let obj = data as *mut AmfBufferObj;
((*(*obj).vtbl).release)(data);
}
AMF_REPEAT => return Ok(()),
AMF_EOF => return Ok(()),
AMF_NEED_MORE_INPUT => return Ok(()),
other => bail!("AMF QueryOutput failed: {other}"),
}
}
}
}
unsafe fn buffer_to_packet(data: *mut c_void) -> Result<Option<EncodedPacket>> {
unsafe {
let data_obj = data as *mut AmfBufferObj;
let data_vt = &*(*data_obj).vtbl;
let mut buffer: *mut c_void = ptr::null_mut();
let qi_rc =
(data_vt.query_interface)(data, AMF_IID_BUFFER.as_ptr() as *const c_void, &mut buffer);
if qi_rc != 0 || buffer.is_null() {
bail!("AMFData::QueryInterface(AMFBuffer) failed: {qi_rc}");
}
let buffer_obj = buffer as *mut AmfBufferObj;
let buffer_vt = &*(*buffer_obj).vtbl;
let size = (buffer_vt.get_size)(buffer_obj as *mut c_void);
let native = (buffer_vt.get_native)(buffer_obj as *mut c_void) as *const u8;
if size == 0 || native.is_null() {
(buffer_vt.release)(buffer_obj as *mut c_void);
return Ok(None);
}
let slice = std::slice::from_raw_parts(native, size);
let data_bytes = Bytes::copy_from_slice(slice);
let pts_ticks = (buffer_vt.get_pts)(buffer_obj as *mut c_void) as u64;
let prop_name = prop("Av1OutputFrameType");
let mut var: AmfVariant = AmfVariant {
ty: 0,
_pad: 0,
value: [0; 24],
};
let is_keyframe =
if (buffer_vt.get_property)(buffer_obj as *mut c_void, prop_name.as_ptr(), &mut var)
== AMF_OK
&& var.ty == AMF_VARIANT_INT64
{
let mut v_bytes = [0u8; 8];
v_bytes.copy_from_slice(&var.value[..8]);
let v = i64::from_le_bytes(v_bytes);
v == AMF_OUTPUT_FRAME_TYPE_KEY || v == AMF_OUTPUT_FRAME_TYPE_INTRA_ONLY
} else {
false
};
(buffer_vt.release)(buffer_obj as *mut c_void);
Ok(Some(EncodedPacket {
data: data_bytes,
pts: pts_ticks,
is_keyframe,
}))
}
}
fn amf_quality_preset_i64(preset: AmfQualityPreset) -> i64 {
match preset {
AmfQualityPreset::HighQuality => 10,
AmfQualityPreset::Quality => 30,
AmfQualityPreset::Balanced => 50,
AmfQualityPreset::Speed => 70,
}
}
unsafe fn set_int_property(
obj: *mut c_void,
vt: &AmfComponentVtbl,
name: &str,
value: i64,
) -> Result<()> {
unsafe {
let wname = wide(name);
let rc = (vt.set_property)(obj, wname.as_ptr(), AmfVariant::int64(value));
if rc != AMF_OK {
bail!("AMF SetProperty({}, {}) failed: {rc}", name, value);
}
Ok(())
}
}
#[derive(Clone, Copy)]
struct SessionSnapshot {
encoder: *mut c_void,
context: *mut c_void,
width: u32,
height: u32,
pts_timescale: u64,
surface_format: i32,
}
unsafe fn upload_frame_static(snap: &SessionSnapshot, frame: &VideoFrame) -> Result<*mut c_void> {
let _ = snap.encoder; unsafe {
let context_obj = snap.context as *mut AmfContextObj;
let context_vt = &*(*context_obj).vtbl;
let mut surface: *mut c_void = ptr::null_mut();
let rc = (context_vt.alloc_surface)(
snap.context,
AMF_MEMORY_HOST,
snap.surface_format,
snap.width as i32,
snap.height as i32,
&mut surface,
);
if rc != AMF_OK || surface.is_null() {
bail!(
"AMFContext::AllocSurface({}x{} fmt={}) failed: {rc}",
snap.width,
snap.height,
snap.surface_format,
);
}
let surface_obj = surface as *mut AmfSurfaceObj;
let surface_vt = &*(*surface_obj).vtbl;
let y_plane = (surface_vt.get_plane)(surface, AMF_PLANE_Y);
let uv_plane = (surface_vt.get_plane)(surface, AMF_PLANE_UV);
if y_plane.is_null() || uv_plane.is_null() {
(surface_vt.release)(surface);
bail!(
"AMF surface (fmt={}) missing Y or UV plane",
snap.surface_format
);
}
let upload_result = match snap.surface_format {
AMF_SURFACE_NV12 => copy_yuv420p_to_nv12_surface(
surface,
surface_vt,
y_plane,
uv_plane,
snap.width,
snap.height,
frame,
),
AMF_SURFACE_P010 => copy_yuv420p10le_to_p010_surface(
surface,
surface_vt,
y_plane,
uv_plane,
snap.width,
snap.height,
frame,
),
other => {
(surface_vt.release)(surface);
bail!("AMF surface format {other} not supported by uploader");
}
};
upload_result?;
(surface_vt.set_pts)(surface, (frame.pts * snap.pts_timescale) as i64);
Ok(surface)
}
}
unsafe fn copy_yuv420p_to_nv12_surface(
surface: *mut c_void,
surface_vt: &AmfSurfaceVtbl,
y_plane: *mut c_void,
uv_plane: *mut c_void,
width: u32,
height: u32,
frame: &VideoFrame,
) -> Result<()> {
unsafe {
let w = width as usize;
let h = height as usize;
let y_size = w * h;
let cw = w.div_ceil(2);
let ch = h.div_ceil(2);
let uv_size = cw * ch;
if frame.data.len() < y_size + 2 * uv_size {
(surface_vt.release)(surface);
bail!(
"frame data too small for {}x{} YUV420p: need {} bytes, got {}",
w,
h,
y_size + 2 * uv_size,
frame.data.len()
);
}
let y_plane_obj = y_plane as *mut AmfPlaneObj;
let y_vt = &*(*y_plane_obj).vtbl;
let y_dst = (y_vt.get_native)(y_plane) as *mut u8;
let y_pitch = (y_vt.get_h_pitch)(y_plane) as usize;
if y_dst.is_null() {
(surface_vt.release)(surface);
bail!("AMF Y plane native pointer is null — surface not host-mapped?");
}
for row in 0..h {
let src = frame.data.as_ptr().add(row * w);
let dst = y_dst.add(row * y_pitch);
ptr::copy_nonoverlapping(src, dst, w);
}
let uv_plane_obj = uv_plane as *mut AmfPlaneObj;
let uv_vt = &*(*uv_plane_obj).vtbl;
let uv_dst = (uv_vt.get_native)(uv_plane) as *mut u8;
let uv_pitch = (uv_vt.get_h_pitch)(uv_plane) as usize;
if uv_dst.is_null() {
(surface_vt.release)(surface);
bail!("AMF UV plane native pointer is null — surface not host-mapped?");
}
let u_src_base = frame.data.as_ptr().add(y_size);
let v_src_base = u_src_base.add(uv_size);
for row in 0..ch {
let u_src = u_src_base.add(row * cw);
let v_src = v_src_base.add(row * cw);
let dst_row = uv_dst.add(row * uv_pitch);
for col in 0..cw {
*dst_row.add(col * 2) = *u_src.add(col);
*dst_row.add(col * 2 + 1) = *v_src.add(col);
}
}
Ok(())
}
}
unsafe fn copy_yuv420p10le_to_p010_surface(
surface: *mut c_void,
surface_vt: &AmfSurfaceVtbl,
y_plane: *mut c_void,
uv_plane: *mut c_void,
width: u32,
height: u32,
frame: &VideoFrame,
) -> Result<()> {
unsafe {
let w = width as usize;
let h = height as usize;
let cw = w.div_ceil(2);
let ch = h.div_ceil(2);
let y_bytes = w * h * 2;
let uv_bytes = cw * ch * 2;
if frame.data.len() < y_bytes + 2 * uv_bytes {
(surface_vt.release)(surface);
bail!(
"frame data too small for {}x{} Yuv420p10le: need {} bytes, got {}",
w,
h,
y_bytes + 2 * uv_bytes,
frame.data.len()
);
}
let y_plane_obj = y_plane as *mut AmfPlaneObj;
let y_vt = &*(*y_plane_obj).vtbl;
let y_dst = (y_vt.get_native)(y_plane) as *mut u8;
let y_pitch_bytes = (y_vt.get_h_pitch)(y_plane) as usize;
if y_dst.is_null() {
(surface_vt.release)(surface);
bail!("AMF P010 Y plane native pointer is null");
}
let src_ptr = frame.data.as_ptr();
for row in 0..h {
let src_row = src_ptr.add(row * w * 2) as *const u16;
let dst_row = y_dst.add(row * y_pitch_bytes) as *mut u16;
for col in 0..w {
let sample = (*src_row.add(col)) & 0x03FF;
*dst_row.add(col) = sample << 6;
}
}
let uv_plane_obj = uv_plane as *mut AmfPlaneObj;
let uv_vt = &*(*uv_plane_obj).vtbl;
let uv_dst = (uv_vt.get_native)(uv_plane) as *mut u8;
let uv_pitch_bytes = (uv_vt.get_h_pitch)(uv_plane) as usize;
if uv_dst.is_null() {
(surface_vt.release)(surface);
bail!("AMF P010 UV plane native pointer is null");
}
let u_src_base = src_ptr.add(y_bytes);
let v_src_base = u_src_base.add(uv_bytes);
for row in 0..ch {
let u_src = u_src_base.add(row * cw * 2) as *const u16;
let v_src = v_src_base.add(row * cw * 2) as *const u16;
let dst_row = uv_dst.add(row * uv_pitch_bytes) as *mut u16;
for col in 0..cw {
let u = (*u_src.add(col)) & 0x03FF;
let v = (*v_src.add(col)) & 0x03FF;
*dst_row.add(col * 2) = u << 6;
*dst_row.add(col * 2 + 1) = v << 6;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
thread_local! {
static MOCK_SUBMIT_RESULTS: RefCell<Vec<AmfResult>> = const { RefCell::new(Vec::new()) };
static MOCK_QUERY_RESULTS: RefCell<Vec<AmfResult>> = const { RefCell::new(Vec::new()) };
static MOCK_SUBMIT_CALLS: AtomicUsize = const { AtomicUsize::new(0) };
static MOCK_QUERY_CALLS: AtomicUsize = const { AtomicUsize::new(0) };
static MOCK_SURFACE_REFCOUNT: AtomicI64 = const { AtomicI64::new(0) };
static MOCK_SUBMIT_POINTERS: RefCell<Vec<*mut c_void>> = const { RefCell::new(Vec::new()) };
}
fn mock_reset() {
MOCK_SUBMIT_RESULTS.with(|v| v.borrow_mut().clear());
MOCK_QUERY_RESULTS.with(|v| v.borrow_mut().clear());
MOCK_SUBMIT_POINTERS.with(|v| v.borrow_mut().clear());
MOCK_SUBMIT_CALLS.with(|c| c.store(0, Ordering::SeqCst));
MOCK_QUERY_CALLS.with(|c| c.store(0, Ordering::SeqCst));
MOCK_SURFACE_REFCOUNT.with(|c| c.store(1, Ordering::SeqCst));
}
fn set_submit_sequence(results: &[AmfResult]) {
MOCK_SUBMIT_RESULTS.with(|v| *v.borrow_mut() = results.to_vec());
}
fn set_query_sequence(results: &[AmfResult]) {
MOCK_QUERY_RESULTS.with(|v| *v.borrow_mut() = results.to_vec());
}
fn submit_call_count() -> usize {
MOCK_SUBMIT_CALLS.with(|c| c.load(Ordering::SeqCst))
}
fn query_call_count() -> usize {
MOCK_QUERY_CALLS.with(|c| c.load(Ordering::SeqCst))
}
fn surface_refcount() -> i64 {
MOCK_SURFACE_REFCOUNT.with(|c| c.load(Ordering::SeqCst))
}
fn submit_pointer_at(idx: usize) -> Option<*mut c_void> {
MOCK_SUBMIT_POINTERS.with(|v| v.borrow().get(idx).copied())
}
unsafe extern "C" fn mock_qi(_: *mut c_void, _: *const c_void, _: *mut *mut c_void) -> i64 {
0
}
unsafe extern "C" fn mock_acquire(_: *mut c_void) -> i64 {
1
}
unsafe extern "C" fn mock_release_component(_: *mut c_void) -> i64 {
1
}
unsafe extern "C" fn mock_set_property(
_: *mut c_void,
_: *const u16,
_: AmfVariant,
) -> AmfResult {
AMF_OK
}
unsafe extern "C" fn mock_get_property(
_: *mut c_void,
_: *const u16,
_: *mut AmfVariant,
) -> AmfResult {
AMF_OK
}
unsafe extern "C" fn mock_init(_: *mut c_void, _: i32, _: i32, _: i32) -> AmfResult {
AMF_OK
}
unsafe extern "C" fn mock_reinit(_: *mut c_void, _: i32, _: i32) -> AmfResult {
AMF_OK
}
unsafe extern "C" fn mock_terminate(_: *mut c_void) -> AmfResult {
AMF_OK
}
unsafe extern "C" fn mock_drain(_: *mut c_void) -> AmfResult {
AMF_OK
}
unsafe extern "C" fn mock_flush(_: *mut c_void) -> AmfResult {
AMF_OK
}
unsafe extern "C" fn mock_submit_input(_: *mut c_void, surface: *mut c_void) -> AmfResult {
MOCK_SUBMIT_POINTERS.with(|v| v.borrow_mut().push(surface));
let idx = MOCK_SUBMIT_CALLS.with(|c| c.fetch_add(1, Ordering::SeqCst));
MOCK_SUBMIT_RESULTS.with(|v| {
let v = v.borrow();
v.get(idx).copied().unwrap_or(AMF_OK)
})
}
unsafe extern "C" fn mock_query_output(_: *mut c_void, data: *mut *mut c_void) -> AmfResult {
let idx = MOCK_QUERY_CALLS.with(|c| c.fetch_add(1, Ordering::SeqCst));
let rc = MOCK_QUERY_RESULTS.with(|v| {
let v = v.borrow();
v.get(idx).copied().unwrap_or(AMF_REPEAT)
});
if rc == AMF_OK {
unsafe {
*data = ptr::null_mut();
}
}
rc
}
unsafe extern "C" fn mock_get_context(_: *mut c_void) -> *mut c_void {
ptr::null_mut()
}
unsafe extern "C" fn mock_set_output_cb(_: *mut c_void, _: *mut c_void) -> AmfResult {
AMF_OK
}
unsafe extern "C" fn mock_get_caps(_: *mut c_void, _: *mut *mut c_void) -> AmfResult {
AMF_OK
}
unsafe extern "C" fn mock_optimize(_: *mut c_void, _: *mut c_void) -> AmfResult {
AMF_OK
}
unsafe extern "C" fn mock_surface_release(_: *mut c_void) -> i64 {
let prev = MOCK_SURFACE_REFCOUNT.with(|c| c.fetch_sub(1, Ordering::SeqCst));
assert!(
prev > 0,
"surface Release when refcount already zero (UAF indicator)"
);
prev - 1
}
unsafe extern "C" fn mock_surface_set_property(
_: *mut c_void,
_: *const u16,
_: AmfVariant,
) -> AmfResult {
AMF_OK
}
unsafe extern "C" fn mock_surface_get_property(
_: *mut c_void,
_: *const u16,
_: *mut AmfVariant,
) -> AmfResult {
AMF_OK
}
unsafe extern "C" fn mock_surface_duplicate(
_: *mut c_void,
_: i32,
_: *mut *mut c_void,
) -> AmfResult {
AMF_OK
}
unsafe extern "C" fn mock_surface_get_pts(_: *mut c_void) -> i64 {
0
}
unsafe extern "C" fn mock_surface_set_pts(_: *mut c_void, _: i64) {}
unsafe extern "C" fn mock_surface_get_duration(_: *mut c_void) -> i64 {
0
}
unsafe extern "C" fn mock_surface_set_duration(_: *mut c_void, _: i64) {}
unsafe extern "C" fn mock_surface_get_planes_count(_: *mut c_void) -> usize {
2
}
unsafe extern "C" fn mock_surface_get_plane_at(_: *mut c_void, _: usize) -> *mut c_void {
ptr::null_mut()
}
unsafe extern "C" fn mock_surface_get_plane(_: *mut c_void, _: i32) -> *mut c_void {
ptr::null_mut()
}
static MOCK_SURFACE_VTBL: AmfSurfaceVtbl = AmfSurfaceVtbl {
query_interface: mock_qi,
acquire: mock_acquire,
release: mock_surface_release,
set_property: mock_surface_set_property,
get_property: mock_surface_get_property,
duplicate: mock_surface_duplicate,
get_pts: mock_surface_get_pts,
set_pts: mock_surface_set_pts,
get_duration: mock_surface_get_duration,
set_duration: mock_surface_set_duration,
get_planes_count: mock_surface_get_planes_count,
get_plane_at: mock_surface_get_plane_at,
get_plane: mock_surface_get_plane,
};
static MOCK_COMPONENT_VTBL: AmfComponentVtbl = AmfComponentVtbl {
query_interface: mock_qi,
acquire: mock_acquire,
release: mock_release_component,
set_property: mock_set_property,
get_property: mock_get_property,
init: mock_init,
reinit: mock_reinit,
terminate: mock_terminate,
drain: mock_drain,
flush: mock_flush,
submit_input: mock_submit_input,
query_output: mock_query_output,
get_context: mock_get_context,
set_output_data_allocator_cb: mock_set_output_cb,
get_caps: mock_get_caps,
optimize: mock_optimize,
};
fn make_mock_pair() -> (Box<AmfSurfaceObj>, Box<AmfComponentObj>) {
let surface = Box::new(AmfSurfaceObj {
vtbl: &MOCK_SURFACE_VTBL,
});
let component = Box::new(AmfComponentObj {
vtbl: &MOCK_COMPONENT_VTBL,
});
(surface, component)
}
#[test]
fn test_amf_input_full_does_not_release_surface_before_retry() {
mock_reset();
set_submit_sequence(&[AMF_INPUT_FULL, AMF_OK]);
set_query_sequence(&[AMF_REPEAT]);
let (mut surface, mut component) = make_mock_pair();
let surface_ptr: *mut c_void = surface.as_mut() as *mut _ as *mut c_void;
let component_ptr: *mut c_void = component.as_mut() as *mut _ as *mut c_void;
let mut guard = SurfaceGuard::new(surface_ptr);
let mut packets = Vec::new();
let result = unsafe { submit_with_backpressure(&mut packets, component_ptr, &mut guard) };
assert!(
result.is_ok(),
"submit_with_backpressure failed: {result:?}"
);
assert_eq!(
submit_call_count(),
2,
"SubmitInput must retry exactly once on INPUT_FULL before success"
);
assert_eq!(
submit_pointer_at(0),
Some(surface_ptr),
"first submit must pass the original surface pointer"
);
assert_eq!(
submit_pointer_at(1),
Some(surface_ptr),
"retry submit must pass the SAME surface pointer — anything else would be a UAF tell"
);
assert_eq!(
surface_refcount(),
0,
"surface refcount must reach exactly 0 after success (no leak, no double-release)"
);
drop(guard);
assert_eq!(surface_refcount(), 0, "Drop after transfer must be a no-op");
}
#[test]
fn test_amf_need_more_input_returns_no_packet() {
mock_reset();
set_query_sequence(&[AMF_NEED_MORE_INPUT]);
let (_, mut component) = make_mock_pair();
let component_ptr: *mut c_void = component.as_mut() as *mut _ as *mut c_void;
let mut packets = Vec::new();
let result = unsafe { drain_until_hungry_raw(&mut packets, component_ptr) };
assert!(
result.is_ok(),
"AMF_NEED_MORE_INPUT on drain must be Ok (no packet yet), got {result:?}"
);
assert_eq!(packets.len(), 0, "no packets should be emitted");
assert_eq!(
query_call_count(),
1,
"drain should have returned after the single NEED_MORE_INPUT"
);
}
#[test]
fn test_amf_eof_ends_drain_cleanly() {
mock_reset();
set_query_sequence(&[AMF_EOF]);
let (_, mut component) = make_mock_pair();
let component_ptr: *mut c_void = component.as_mut() as *mut _ as *mut c_void;
let mut packets = Vec::new();
let result = unsafe { drain_until_hungry_raw(&mut packets, component_ptr) };
assert!(
result.is_ok(),
"AMF_EOF on drain must end the flush loop cleanly, got {result:?}"
);
assert_eq!(packets.len(), 0, "no packets at EOF");
assert_eq!(
query_call_count(),
1,
"drain should return on the first EOF"
);
}
#[test]
fn test_amf_ring_buffer_index_cycles() {
let mut idx = 0usize;
let mut seen = Vec::new();
for _ in 0..(RING_SIZE * 3) {
seen.push(idx);
idx = (idx + 1) % RING_SIZE;
}
assert_eq!(
seen,
vec![0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3],
"ring index must cycle through 0..RING_SIZE"
);
}
#[test]
fn test_amf_ring_size_is_four() {
assert_eq!(
RING_SIZE, 4,
"RING_SIZE must match Squad-5's NVENC default of 4"
);
}
#[test]
fn test_amf_repeat_on_submit_retries_same_surface() {
mock_reset();
set_submit_sequence(&[AMF_REPEAT, AMF_OK]);
set_query_sequence(&[AMF_REPEAT]);
let (mut surface, mut component) = make_mock_pair();
let surface_ptr: *mut c_void = surface.as_mut() as *mut _ as *mut c_void;
let component_ptr: *mut c_void = component.as_mut() as *mut _ as *mut c_void;
let mut guard = SurfaceGuard::new(surface_ptr);
let mut packets = Vec::new();
let result = unsafe { submit_with_backpressure(&mut packets, component_ptr, &mut guard) };
assert!(result.is_ok(), "AMF_REPEAT retry must succeed");
assert_eq!(submit_call_count(), 2);
assert_eq!(submit_pointer_at(1), Some(surface_ptr));
assert_eq!(surface_refcount(), 0);
drop(guard);
}
#[test]
fn test_amf_submit_hard_error_releases_through_guard() {
mock_reset();
set_submit_sequence(&[AMF_FAIL]);
set_query_sequence(&[AMF_REPEAT]);
let (mut surface, mut component) = make_mock_pair();
let surface_ptr: *mut c_void = surface.as_mut() as *mut _ as *mut c_void;
let component_ptr: *mut c_void = component.as_mut() as *mut _ as *mut c_void;
let mut packets = Vec::new();
{
let mut guard = SurfaceGuard::new(surface_ptr);
let result =
unsafe { submit_with_backpressure(&mut packets, component_ptr, &mut guard) };
assert!(result.is_err(), "hard error must propagate as Err");
}
assert_eq!(
surface_refcount(),
0,
"hard-error path must release exactly once via the guard's Drop"
);
}
#[test]
fn test_amf_submit_bounded_retry_budget() {
mock_reset();
let saturated: Vec<AmfResult> = (0..(INPUT_FULL_MAX_RETRIES as usize + 2))
.map(|_| AMF_INPUT_FULL)
.collect();
set_submit_sequence(&saturated);
let drains: Vec<AmfResult> = (0..(INPUT_FULL_MAX_RETRIES as usize + 2))
.map(|_| AMF_REPEAT)
.collect();
set_query_sequence(&drains);
let (mut surface, mut component) = make_mock_pair();
let surface_ptr: *mut c_void = surface.as_mut() as *mut _ as *mut c_void;
let component_ptr: *mut c_void = component.as_mut() as *mut _ as *mut c_void;
let mut packets = Vec::new();
{
let mut guard = SurfaceGuard::new(surface_ptr);
let result =
unsafe { submit_with_backpressure(&mut packets, component_ptr, &mut guard) };
assert!(
result.is_err(),
"stuck backpressure must eventually bail (not spin)"
);
assert_eq!(
submit_call_count() as u32,
INPUT_FULL_MAX_RETRIES + 1,
"retry count must match INPUT_FULL_MAX_RETRIES + 1 (initial + retries)"
);
}
assert_eq!(
surface_refcount(),
0,
"bounded-retry failure must still release cleanly via guard"
);
}
#[test]
fn test_amf_variant_int64_layout() {
let v = AmfVariant::int64(0x0123_4567_89ab_cdef);
assert_eq!(v.ty, AMF_VARIANT_INT64);
assert_eq!(v._pad, 0);
assert_eq!(
v.as_int64(),
Some(0x0123_4567_89ab_cdef),
"int64 round-trip must match"
);
let expected = 0x0123_4567_89ab_cdefi64.to_le_bytes();
assert_eq!(
&v.value[..8],
&expected,
"int64 payload must be LE-encoded into value[0..8]"
);
assert_eq!(std::mem::size_of::<AmfVariant>(), 32);
assert_eq!(std::mem::offset_of!(AmfVariant, value), 8);
}
#[test]
fn test_amf_iid_buffer_byte_order() {
assert_eq!(&AMF_IID_BUFFER[0..4], &0xb1d75dbeu32.to_le_bytes());
assert_eq!(&AMF_IID_BUFFER[4..6], &0x0e6cu16.to_le_bytes());
assert_eq!(&AMF_IID_BUFFER[6..8], &0x434cu16.to_le_bytes());
assert_eq!(
&AMF_IID_BUFFER[8..16],
&[0xb7, 0x28, 0x02, 0x85, 0x98, 0x37, 0x85, 0x7d]
);
}
#[test]
fn test_amf_quality_preset_mapping_exhaustive() {
assert_eq!(amf_quality_preset_i64(AmfQualityPreset::HighQuality), 10);
assert_eq!(amf_quality_preset_i64(AmfQualityPreset::Quality), 30);
assert_eq!(amf_quality_preset_i64(AmfQualityPreset::Balanced), 50);
assert_eq!(amf_quality_preset_i64(AmfQualityPreset::Speed), 70);
}
#[test]
fn test_amf_surface_format_dispatch() {
assert_eq!(
amf_surface_format_for(PixelFormat::Yuv420p).unwrap(),
AMF_SURFACE_NV12,
"8-bit → NV12"
);
assert_eq!(
amf_surface_format_for(PixelFormat::Yuv420p10le).unwrap(),
AMF_SURFACE_P010,
"10-bit → P010"
);
assert!(amf_surface_format_for(PixelFormat::Yuv422p).is_err());
assert!(amf_surface_format_for(PixelFormat::Rgb24).is_err());
assert!(amf_surface_format_for(PixelFormat::Yuv444p10le).is_err());
}
#[test]
fn test_amf_color_bit_depth_dispatch() {
assert_eq!(amf_color_bit_depth_for(PixelFormat::Yuv420p), 1);
assert_eq!(amf_color_bit_depth_for(PixelFormat::Yuv420p10le), 2);
}
#[test]
fn test_amf_transfer_to_h273_codes() {
assert_eq!(transfer_to_h273(TransferFn::Bt709), 1);
assert_eq!(transfer_to_h273(TransferFn::St2084), 16);
assert_eq!(transfer_to_h273(TransferFn::AribStdB67), 18);
assert_eq!(transfer_to_h273(TransferFn::Linear), 8);
assert_eq!(transfer_to_h273(TransferFn::Bt470Bg), 4);
assert_eq!(transfer_to_h273(TransferFn::Unspecified), 1);
}
#[test]
fn test_amf_hdr10_set_property_sequence() {
thread_local! {
static RECORDED: std::cell::RefCell<Vec<(String, i64)>> =
const { std::cell::RefCell::new(Vec::new()) };
}
unsafe extern "C" fn record_set_property(
_: *mut c_void,
name: *const u16,
v: AmfVariant,
) -> AmfResult {
unsafe {
let mut len = 0usize;
while *name.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(name, len);
let s = String::from_utf16_lossy(slice);
let value = v.as_int64().unwrap_or(0);
RECORDED.with(|r| r.borrow_mut().push((s, value)));
}
AMF_OK
}
static REC_VTBL: AmfComponentVtbl = AmfComponentVtbl {
query_interface: mock_qi,
acquire: mock_acquire,
release: mock_release_component,
set_property: record_set_property,
get_property: mock_get_property,
init: mock_init,
reinit: mock_reinit,
terminate: mock_terminate,
drain: mock_drain,
flush: mock_flush,
submit_input: mock_submit_input,
query_output: mock_query_output,
get_context: mock_get_context,
set_output_data_allocator_cb: mock_set_output_cb,
get_caps: mock_get_caps,
optimize: mock_optimize,
};
let mut component = Box::new(AmfComponentObj { vtbl: &REC_VTBL });
let component_ptr: *mut c_void = component.as_mut() as *mut _ as *mut c_void;
let vt: &AmfComponentVtbl = unsafe { &*(*(component_ptr as *mut AmfComponentObj)).vtbl };
let cm = ColorMetadata {
transfer: TransferFn::St2084,
matrix_coefficients: 9, colour_primaries: 9, full_range: true,
mastering_display: None,
content_light_level: None,
};
unsafe {
set_int_property(
component_ptr,
vt,
"Av1ColorBitDepth",
amf_color_bit_depth_for(PixelFormat::Yuv420p10le),
)
.unwrap();
set_int_property(
component_ptr,
vt,
"Av1OutColorPrimaries",
cm.colour_primaries as i64,
)
.unwrap();
set_int_property(
component_ptr,
vt,
"Av1OutColorTransferChar",
transfer_to_h273(cm.transfer),
)
.unwrap();
set_int_property(
component_ptr,
vt,
"Av1OutColorMatrixCoeff",
cm.matrix_coefficients as i64,
)
.unwrap();
set_int_property(component_ptr, vt, "Av1OutColorRange", cm.full_range as i64).unwrap();
}
let recorded: Vec<(String, i64)> = RECORDED.with(|r| r.borrow().clone());
let lookup = |name: &str| -> i64 {
recorded
.iter()
.find(|(n, _)| n == name)
.expect("property recorded")
.1
};
assert_eq!(
lookup("Av1ColorBitDepth"),
2,
"10-bit enum is value 2, not 10"
);
assert_eq!(lookup("Av1OutColorPrimaries"), 9, "BT.2020");
assert_eq!(lookup("Av1OutColorTransferChar"), 16, "ST 2084 / PQ");
assert_eq!(lookup("Av1OutColorMatrixCoeff"), 9, "BT.2020 NCL");
assert_eq!(lookup("Av1OutColorRange"), 1, "full range");
}
}