use anyhow::{bail, Context, Result};
use bytes::Bytes;
use std::ffi::c_void;
use std::os::raw::c_int;
use std::ptr;
use super::tuning::{self, AmfRateControl};
use super::{AUTO_FROM_TARGET, EncodedPacket, Encoder, EncoderConfig};
#[cfg(test)]
use crate::frame::ColorMetadata;
use crate::frame::VideoFrame;
mod config;
mod ffi;
mod surface;
#[cfg(test)]
mod tests;
use self::config::*;
use self::ffi::*;
use 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> {
if config.codec != crate::frame::VideoCodec::Av1 {
anyhow::bail!(
"AMF encodes AV1 only today; for {:?} output use Intel QSV (Arc+) \
— native AMF H.264/H.265 is a follow-up",
config.codec
);
}
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,
}))
}
}