use crate::util::ffmpeg_utils::av_err2str;
use ffmpeg_sys_next::{
av_buffer_unref, av_dict_free, av_dict_parse_string, av_hwdevice_ctx_create,
av_hwdevice_ctx_create_derived, av_hwdevice_find_type_by_name, av_hwdevice_get_type_name,
av_hwdevice_iterate_types, avcodec_get_hw_config, avfilter_get_by_name, AVBufferRef, AVCodec,
AVDictionary, AVHWDeviceType, AVERROR, AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX, EINVAL, ENOMEM,
};
use log::{error, warn};
use std::ffi::{CStr, CString};
use std::ptr::{null, null_mut};
use std::sync::{Arc, Mutex, OnceLock};
#[derive(Clone, Debug)]
pub struct HWAccelInfo {
pub name: String,
pub hw_device_type: AVHWDeviceType,
}
pub fn get_hwaccels() -> Vec<HWAccelInfo> {
let mut hwaccels = Vec::new();
let mut device_type = AVHWDeviceType::AV_HWDEVICE_TYPE_NONE;
loop {
device_type = unsafe { av_hwdevice_iterate_types(device_type) };
if device_type == AVHWDeviceType::AV_HWDEVICE_TYPE_NONE {
break;
}
let name = unsafe {
let name = av_hwdevice_get_type_name(device_type);
match CStr::from_ptr(name).to_str() {
Ok(name) => name.to_string(),
Err(_) => "unknown name".to_string(),
}
};
hwaccels.push(HWAccelInfo {
name,
hw_device_type: device_type,
});
}
hwaccels
}
static HW_DEVICES: OnceLock<Mutex<Vec<HWDevice>>> = OnceLock::new();
#[cfg(test)]
pub(crate) static HW_REGISTRY_TEST_LOCK: Mutex<()> = Mutex::new(());
static INIT_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
fn init_lock() -> &'static Mutex<()> {
INIT_LOCK.get_or_init(|| Mutex::new(()))
}
static FILTER_HW_DEVICE: OnceLock<Mutex<Option<HWDevice>>> = OnceLock::new();
pub(crate) fn new_hw_devices() -> Mutex<Vec<HWDevice>> {
Mutex::new(Vec::new())
}
pub(crate) fn init_filter_hw_device(hw_device: &str) -> i32 {
if FILTER_HW_DEVICE.get().is_some() {
warn!("Only one filter device can be used.");
return 0;
}
match hw_device_init_from_string(hw_device) {
(0, Some(dev)) => {
FILTER_HW_DEVICE.set(Mutex::new(Some(dev))).ok();
0
}
(_, _) => {
error!("Invalid filter device {}", hw_device);
FILTER_HW_DEVICE.set(Mutex::new(None)).ok();
AVERROR(EINVAL)
}
}
}
#[repr(i32)]
#[derive(Copy, Clone, PartialEq)]
pub enum HWAccelID {
HwaccelNone = 0,
HwaccelAuto,
HwaccelGeneric,
}
#[derive(Debug)]
pub(crate) struct HWDevice {
pub(crate) name: String,
pub(crate) device_type: AVHWDeviceType,
device: Arc<OwnedDeviceRef>,
pub(crate) init_arg: Option<String>,
}
#[derive(Debug)]
struct OwnedDeviceRef(*mut AVBufferRef);
impl Drop for OwnedDeviceRef {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe {
av_buffer_unref(&mut self.0);
}
}
}
}
unsafe impl Send for OwnedDeviceRef {}
unsafe impl Sync for OwnedDeviceRef {}
impl HWDevice {
fn new(
name: String,
device_type: AVHWDeviceType,
device_ref: *mut AVBufferRef,
init_arg: Option<String>,
) -> Self {
HWDevice {
name,
device_type,
device: Arc::new(OwnedDeviceRef(device_ref)),
init_arg,
}
}
pub(crate) fn device_ref(&self) -> *mut AVBufferRef {
self.device.0
}
}
impl Clone for HWDevice {
fn clone(&self) -> Self {
HWDevice {
name: self.name.clone(),
device_type: self.device_type,
device: Arc::clone(&self.device),
init_arg: self.init_arg.clone(),
}
}
}
unsafe impl Send for HWDevice {}
pub(crate) unsafe fn hw_device_free_all() {
if let Some(slot) = FILTER_HW_DEVICE.get() {
if let Ok(mut slot) = slot.lock() {
slot.take();
}
}
if let Some(hw_devices) = HW_DEVICES.get() {
match hw_devices.lock() {
Ok(mut devices_guard) => {
devices_guard.clear();
}
Err(e) => {
error!("Failed to lock hardware device list: {}", e);
}
}
}
}
pub(crate) fn hw_device_for_filter() -> Option<HWDevice> {
if let Some(slot) = FILTER_HW_DEVICE.get() {
let slot = slot.lock().unwrap();
if let Some(dev) = slot.as_ref() {
return Some(dev.clone());
}
}
let devices = HW_DEVICES.get_or_init(new_hw_devices);
let devices = devices.lock().unwrap();
if !devices.is_empty() {
let dev = devices.last();
match dev {
None => {}
Some(dev) => {
if devices.len() > 1 {
unsafe {
let type_name = av_hwdevice_get_type_name(dev.device_type);
let type_name = CStr::from_ptr(type_name).to_str();
if let Ok(type_name) = type_name {
warn!("There are {} hardware devices. device {} of type {type_name} is picked for filters by default. Set hardware device explicitly with the filter_hw_device option if device {} is not usable for filters.",
devices.len(),dev.name,
dev.name,);
}
}
}
return Some(dev.clone());
}
}
}
None
}
pub(crate) fn hw_device_match_by_codec(codec: *const AVCodec) -> Option<HWDevice> {
let mut i = 0;
loop {
let config = unsafe { avcodec_get_hw_config(codec, i) };
if config.is_null() {
return None;
}
unsafe {
if (*config).methods as u32 & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX as u32 == 0 {
i += 1;
continue;
}
if let Some(dev) = hw_device_get_by_type((*config).device_type) {
return Some(dev.clone());
}
}
i += 1;
}
}
pub(crate) fn hw_device_get_by_type(device_type: AVHWDeviceType) -> Option<HWDevice> {
let mut found = None;
let devices = HW_DEVICES.get_or_init(new_hw_devices);
let devices = devices.lock().unwrap();
for device in devices.iter() {
if device.device_type == device_type {
if found.is_some() {
return None;
}
found = Some(device.clone());
}
}
found
}
fn split_device_type(arg: &str) -> (&str, &str) {
let k = arg.find([':', '=', '@']).unwrap_or(arg.len());
(&arg[..k], &arg[k..])
}
fn split_device_and_options(p: &str) -> (Option<&str>, Option<&str>) {
let rest = p.strip_prefix(':').unwrap_or(p);
match rest.find(',') {
Some(comma_pos) => (
(comma_pos > 0).then(|| &rest[..comma_pos]),
Some(&rest[comma_pos + 1..]),
),
None => (if rest.is_empty() { None } else { Some(rest) }, None),
}
}
struct DictGuard(*mut AVDictionary);
impl Drop for DictGuard {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { av_dict_free(&mut self.0) };
}
}
}
pub(crate) fn hw_device_init_from_string(arg: &str) -> (i32, Option<HWDevice>) {
let _init_guard = init_lock()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut device_ref = null_mut();
let (type_str, mut p) = split_device_type(arg);
let Ok(type_name) = CString::new(type_str) else {
error!("Device creation failed: type:{type_str} can't convert to CString");
return (AVERROR(ENOMEM), None);
};
let device_type = unsafe { av_hwdevice_find_type_by_name(type_name.as_ptr()) };
if device_type == AVHWDeviceType::AV_HWDEVICE_TYPE_NONE {
error!("Invalid device specification \"{arg}\": unknown device type");
return (AVERROR(EINVAL), None);
}
if let Some(existing) = reuse_by_init_arg_move_to_back(arg) {
return (0, Some(existing));
}
let name = if p.starts_with('=') {
let name_end = p[1..].find([':', '@', ',']).unwrap_or(p.len() - 1);
let name = Some(p[1..=name_end].to_string());
if hw_device_get_by_name(&name.clone().unwrap()).is_some() {
error!("Invalid device specification \"{arg}\": named device already exists");
return (AVERROR(EINVAL), None);
}
let new_p_index = 1 + name_end;
p = &p[new_p_index..];
name
} else {
hw_device_default_name(device_type)
};
if p.is_empty() {
let err =
unsafe { av_hwdevice_ctx_create(&mut device_ref, device_type, null(), null_mut(), 0) };
if err < 0 {
error!("Device creation failed: {err}.");
unsafe {
av_buffer_unref(&mut device_ref);
}
return (err, None);
}
} else if p.starts_with(':') {
let (device_name, options_str) = split_device_and_options(p);
let mut options = DictGuard(null_mut());
if let Some(v) = options_str {
unsafe {
let Ok(v_cstr) = CString::new(v) else {
error!("Device creation failed: option:{v} can't convert to CString");
av_buffer_unref(&mut device_ref);
return (AVERROR(EINVAL), None);
};
let eq_cstr = CString::new("=").unwrap();
let comma_cstr = CString::new(",").unwrap();
let err = av_dict_parse_string(
&mut options.0,
v_cstr.as_ptr(),
eq_cstr.as_ptr(),
comma_cstr.as_ptr(),
0,
);
if err < 0 {
error!("Invalid device specification \"{arg}\": failed to parse options");
av_buffer_unref(&mut device_ref);
return (AVERROR(EINVAL), None);
}
}
}
let err = unsafe {
match device_name {
None => av_hwdevice_ctx_create(&mut device_ref, device_type, null(), options.0, 0),
Some(device_name) => {
let Ok(device_name_cstr) = CString::new(device_name) else {
error!("Device creation failed: device_name:{device_name} can't convert to CString");
av_buffer_unref(&mut device_ref);
return (AVERROR(EINVAL), None);
};
av_hwdevice_ctx_create(
&mut device_ref,
device_type,
device_name_cstr.as_ptr(),
options.0,
0,
)
}
}
};
if err < 0 {
error!("Device creation failed: {err}.");
unsafe {
av_buffer_unref(&mut device_ref);
}
return (err, None);
}
} else if let Some(src_name) = p.strip_prefix('@') {
let Some(src_device) = hw_device_get_by_name(src_name) else {
error!("Invalid device specification \"{arg}\": invalid source device name");
unsafe {
av_buffer_unref(&mut device_ref);
}
return (AVERROR(EINVAL), None);
};
let err = unsafe {
av_hwdevice_ctx_create_derived(&mut device_ref, device_type, src_device.device_ref(), 0)
};
if err < 0 {
error!("Device creation failed: {err}.");
unsafe {
av_buffer_unref(&mut device_ref);
}
return (err, None);
}
} else if let Some(v) = p.strip_prefix(',') {
unsafe {
let mut options = DictGuard(null_mut());
let Ok(v_cstr) = CString::new(v) else {
error!("Device creation failed: option:{v} can't convert to CString");
av_buffer_unref(&mut device_ref);
return (AVERROR(EINVAL), None);
};
let eq_cstr = CString::new("=").unwrap();
let comma_cstr = CString::new(",").unwrap();
let mut err = av_dict_parse_string(
&mut options.0,
v_cstr.as_ptr(),
eq_cstr.as_ptr(),
comma_cstr.as_ptr(),
0,
);
if err < 0 {
error!("Invalid device specification \"{arg}\": failed to parse options");
av_buffer_unref(&mut device_ref);
return (AVERROR(EINVAL), None);
}
err = av_hwdevice_ctx_create(&mut device_ref, device_type, null(), options.0, 0);
if err < 0 {
error!("Device creation failed: {err}.");
av_buffer_unref(&mut device_ref);
return (err, None);
}
}
} else {
error!("Invalid device specification \"{arg}\": parse error");
return (AVERROR(EINVAL), None);
}
let dev = HWDevice::new(
name.unwrap(),
device_type,
device_ref,
Some(arg.to_string()),
);
add_hw_device(dev.clone());
(0, Some(dev))
}
pub(crate) fn hw_device_init_from_type(
device_type: AVHWDeviceType,
device: Option<String>,
) -> (i32, Option<HWDevice>) {
let _init_guard = init_lock()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let init_arg = type_init_arg(device_type, device.as_deref());
if let Some(init_arg) = init_arg.as_deref() {
if let Some(existing) = reuse_by_init_arg_move_to_back(init_arg) {
return (0, Some(existing));
}
}
let name = hw_device_default_name(device_type);
if name.is_none() {
return (AVERROR(ENOMEM), None);
}
let mut device_ref = null_mut();
let err = match device.as_deref() {
None => unsafe {
av_hwdevice_ctx_create(&mut device_ref, device_type, null(), null_mut(), 0)
},
Some(device) => {
let Ok(device_cstr) = CString::new(device) else {
return (AVERROR(EINVAL), None);
};
unsafe {
av_hwdevice_ctx_create(
&mut device_ref,
device_type,
device_cstr.as_ptr(),
null_mut(),
0,
)
}
}
};
if err < 0 {
error!("Device creation failed: {}.", err);
unsafe {
av_buffer_unref(&mut device_ref);
}
return (err, None);
}
let dev = register_from_type_device(name.unwrap(), device_type, device_ref, device.as_deref());
(0, Some(dev))
}
fn register_from_type_device(
name: String,
device_type: AVHWDeviceType,
device_ref: *mut AVBufferRef,
device: Option<&str>,
) -> HWDevice {
let dev = HWDevice::new(
name,
device_type,
device_ref,
type_init_arg(device_type, device),
);
add_hw_device(dev.clone());
dev
}
fn type_init_arg(device_type: AVHWDeviceType, device: Option<&str>) -> Option<String> {
let type_name = unsafe { av_hwdevice_get_type_name(device_type) };
if type_name.is_null() {
return None;
}
let type_name = unsafe { CStr::from_ptr(type_name) }.to_str().ok()?;
Some(match device {
None => format!(":{type_name}"),
Some(device) => format!(":{type_name}:{device}"),
})
}
pub(crate) fn hw_device_default_name(device_type: AVHWDeviceType) -> Option<String> {
let type_name = unsafe { av_hwdevice_get_type_name(device_type) };
if type_name.is_null() {
return None;
}
let type_name = unsafe { CStr::from_ptr(type_name) }.to_str().ok()?;
let index_limit = 1000;
for index in 0..index_limit {
let name = format!("{}{}", type_name, index);
if hw_device_get_by_name(&name).is_none() {
return Some(name);
}
}
None
}
pub(crate) fn hw_device_get_by_name(name: &str) -> Option<HWDevice> {
let devices = HW_DEVICES.get_or_init(new_hw_devices);
let devices = devices.lock().unwrap();
for device in devices.iter() {
if device.name == name {
return Some(device.clone());
}
}
None
}
fn reuse_by_init_arg_move_to_back(arg: &str) -> Option<HWDevice> {
let devices = HW_DEVICES.get_or_init(new_hw_devices);
let mut devices = devices.lock().unwrap();
reuse_move_to_back(&mut devices, arg)
}
fn reuse_move_to_back(devices: &mut Vec<HWDevice>, arg: &str) -> Option<HWDevice> {
let idx = devices
.iter()
.position(|device| device.init_arg.as_deref() == Some(arg))?;
let device = devices.remove(idx);
devices.push(device.clone());
Some(device)
}
const HW_DEVICES_CAP: usize = 32;
fn add_hw_device(device: HWDevice) {
let devices = HW_DEVICES.get_or_init(new_hw_devices);
let mut devices = devices.lock().unwrap();
devices.push(device);
while devices.len() > HW_DEVICES_CAP {
drop(devices.remove(0));
}
}
#[derive(Clone, Debug)]
pub struct GpuFilterBackend {
pub name: String,
pub device_type: AVHWDeviceType,
pub device_available: bool,
pub device_error: Option<String>,
pub filters: Vec<GpuFilterAvailability>,
}
#[derive(Clone, Debug)]
pub struct GpuFilterAvailability {
pub name: &'static str,
pub present_in_build: bool,
}
fn known_filters_for(device_type: AVHWDeviceType) -> &'static [&'static str] {
match device_type {
AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA => &[
"scale_cuda",
"overlay_cuda",
"yadif_cuda",
"bwdif_cuda",
"chromakey_cuda",
"colorspace_cuda",
"bilateral_cuda",
"thumbnail_cuda",
"hwupload_cuda",
],
AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI => &[
"scale_vaapi",
"deinterlace_vaapi",
"denoise_vaapi",
"procamp_vaapi",
"sharpness_vaapi",
"tonemap_vaapi",
"overlay_vaapi",
"transpose_vaapi",
],
AVHWDeviceType::AV_HWDEVICE_TYPE_QSV => {
&["scale_qsv", "vpp_qsv", "overlay_qsv", "deinterlace_qsv"]
}
AVHWDeviceType::AV_HWDEVICE_TYPE_VULKAN => &[
"scale_vulkan",
"gblur_vulkan",
"avgblur_vulkan",
"chromaber_vulkan",
"overlay_vulkan",
"flip_vulkan",
"hflip_vulkan",
"vflip_vulkan",
"transpose_vulkan",
"nlmeans_vulkan",
"bwdif_vulkan",
"blend_vulkan",
"xfade_vulkan",
"libplacebo",
],
AVHWDeviceType::AV_HWDEVICE_TYPE_OPENCL => &[
"program_opencl",
"avgblur_opencl",
"boxblur_opencl",
"overlay_opencl",
"tonemap_opencl",
"unsharp_opencl",
"nlmeans_opencl",
"xfade_opencl",
],
_ => &[],
}
}
pub fn is_filter_available(name: &str) -> bool {
let Ok(name_cstr) = CString::new(name) else {
return false;
};
!unsafe { avfilter_get_by_name(name_cstr.as_ptr()) }.is_null()
}
pub fn get_gpu_filter_backends() -> Vec<GpuFilterBackend> {
let mut backends = Vec::new();
let mut device_type = AVHWDeviceType::AV_HWDEVICE_TYPE_NONE;
loop {
device_type = unsafe { av_hwdevice_iterate_types(device_type) };
if device_type == AVHWDeviceType::AV_HWDEVICE_TYPE_NONE {
break;
}
let name = unsafe {
let name = av_hwdevice_get_type_name(device_type);
if name.is_null() {
continue;
}
match CStr::from_ptr(name).to_str() {
Ok(name) => name.to_string(),
Err(_) => "unknown name".to_string(),
}
};
let (device_available, device_error) = probe_hw_device(device_type);
let filters = known_filters_for(device_type)
.iter()
.map(|filter_name| GpuFilterAvailability {
name: filter_name,
present_in_build: is_filter_available(filter_name),
})
.collect();
backends.push(GpuFilterBackend {
name,
device_type,
device_available,
device_error,
filters,
});
}
backends
}
fn probe_hw_device(device_type: AVHWDeviceType) -> (bool, Option<String>) {
let mut device_ref: *mut AVBufferRef = null_mut();
let err =
unsafe { av_hwdevice_ctx_create(&mut device_ref, device_type, null(), null_mut(), 0) };
if err < 0 {
unsafe { av_buffer_unref(&mut device_ref) };
(false, Some(av_err2str(err)))
} else {
unsafe { av_buffer_unref(&mut device_ref) };
(true, None)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct RegistrySnapshot(Vec<HWDevice>);
impl RegistrySnapshot {
fn take() -> Self {
let registry = HW_DEVICES.get_or_init(new_hw_devices);
RegistrySnapshot(std::mem::take(
&mut *registry
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
))
}
}
impl Drop for RegistrySnapshot {
fn drop(&mut self) {
let registry = HW_DEVICES.get_or_init(new_hw_devices);
*registry
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = std::mem::take(&mut self.0);
}
}
#[test]
fn test_get_hwaccels() {
let hwaccels = get_hwaccels();
println!("{:?}", hwaccels);
}
#[test]
fn test_is_filter_available() {
assert!(is_filter_available("scale"));
assert!(!is_filter_available("definitely_not_a_filter_xyz"));
assert!(!is_filter_available("bad\0name"));
}
fn dev(name: &str, init_arg: Option<&str>) -> HWDevice {
dev_with_ref(name, init_arg, null_mut())
}
fn dev_with_ref(name: &str, init_arg: Option<&str>, device_ref: *mut AVBufferRef) -> HWDevice {
HWDevice::new(
name.to_string(),
AVHWDeviceType::AV_HWDEVICE_TYPE_NONE,
device_ref,
init_arg.map(str::to_string),
)
}
fn sentinel_buffer() -> *mut AVBufferRef {
let buf = unsafe { ffmpeg_sys_next::av_buffer_alloc(1) };
assert!(!buf.is_null(), "av_buffer_alloc failed");
buf
}
fn payload(buf: *mut AVBufferRef) -> *mut u8 {
unsafe { (*buf).data }
}
#[test]
fn reuse_only_matches_the_exact_init_spec() {
let mut devices = vec![
dev("vaapi0", Some("vaapi:/dev/dri/renderD128")),
dev("vaapi1", Some("vaapi:/dev/dri/renderD129")),
dev("cuda0", None),
];
assert_eq!(
reuse_move_to_back(&mut devices, "vaapi:/dev/dri/renderD128")
.map(|d| d.name.clone())
.as_deref(),
Some("vaapi0"),
"an identical spec must reuse its device"
);
assert!(
reuse_move_to_back(&mut devices, "vaapi:/dev/dri/renderD130").is_none(),
"a different spec must not reuse an unrelated device"
);
assert!(
reuse_move_to_back(&mut devices, "cuda0").is_none(),
"a device with no init spec (init_arg None) must never be reused by spec"
);
assert!(
reuse_move_to_back(&mut Vec::new(), "vaapi:/dev/dri/renderD128").is_none(),
"an empty list reuses nothing"
);
}
#[test]
fn reuse_moves_the_device_to_the_back_for_default_filter_selection() {
let mut devices = vec![dev("A", Some("spec-a")), dev("B", Some("spec-b"))];
assert_eq!(
devices.last().map(|d| d.name.as_str()),
Some("B"),
"precondition: B was initialized last"
);
let reused = reuse_move_to_back(&mut devices, "spec-a").expect("A must be reused");
assert_eq!(reused.name, "A");
assert_eq!(
devices.iter().map(|d| d.name.as_str()).collect::<Vec<_>>(),
vec!["B", "A"],
"reusing A must move it to the back so it is the default filter device"
);
assert_eq!(
devices.len(),
2,
"reuse must not add a duplicate entry (no leaked context)"
);
}
#[test]
fn from_type_keys_cannot_collide_with_from_string_specs() {
let vaapi = AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI;
let device_key = type_init_arg(vaapi, Some("/dev/dri/renderD128"))
.expect("vaapi always has a type name");
assert_eq!(device_key, ":vaapi:/dev/dri/renderD128");
assert_eq!(type_init_arg(vaapi, None).as_deref(), Some(":vaapi"));
assert_eq!(
type_init_arg(AVHWDeviceType::AV_HWDEVICE_TYPE_NONE, None),
None,
"a type with no name gets no key (it cannot be created anyway)"
);
let mut devices = vec![dev("vaapi0", Some("vaapi:/dev/dri/renderD128"))];
assert!(reuse_move_to_back(&mut devices, &device_key).is_none());
let mut devices = vec![dev("vaapi0", Some(&device_key))];
assert!(reuse_move_to_back(&mut devices, "vaapi:/dev/dri/renderD128").is_none());
}
#[test]
fn from_type_reuse_matches_the_exact_type_and_device_request() {
let vaapi = AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI;
let d128 = type_init_arg(vaapi, Some("/dev/dri/renderD128")).unwrap();
let d129 = type_init_arg(vaapi, Some("/dev/dri/renderD129")).unwrap();
let type_only = type_init_arg(vaapi, None).unwrap();
let ref_a = sentinel_buffer();
let ref_b = sentinel_buffer();
let payload_a = payload(ref_a);
let payload_b = payload(ref_b);
let mut devices = vec![
dev_with_ref("vaapi0", Some(&d128), ref_a),
dev_with_ref("vaapi1", Some(&type_only), ref_b),
];
let reused = reuse_move_to_back(&mut devices, &d128).expect("same request must reuse");
assert_eq!(reused.name, "vaapi0");
assert_eq!(
payload(reused.device_ref()),
payload_a,
"reuse must hand back the SAME registered context"
);
assert_eq!(devices.len(), 2, "reuse must not register a new entry");
assert!(
reuse_move_to_back(&mut devices, &d129).is_none(),
"a different device must not reuse another device's context"
);
let reused = reuse_move_to_back(&mut devices, &type_only)
.expect("type-only must reuse the type-only entry");
assert_eq!(reused.name, "vaapi1");
assert_eq!(payload(reused.device_ref()), payload_b);
assert_eq!(
devices.last().map(|d| d.name.as_str()),
Some("vaapi1"),
"from_type reuse must mirror from_string's move-to-back so the reused \
device stays the default filter device"
);
}
#[test]
fn from_type_failed_creation_registers_nothing() {
let _registry = HW_REGISTRY_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let before = HW_DEVICES
.get()
.map(|m| m.lock().unwrap().len())
.unwrap_or(0);
let (err, dev) = hw_device_init_from_type(
AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI,
Some("/definitely/not/a/device/node".to_string()),
);
assert!(err < 0, "creating a device on a bogus node must fail");
assert!(dev.is_none());
let after = HW_DEVICES
.get()
.map(|m| m.lock().unwrap().len())
.unwrap_or(0);
assert_eq!(
before, after,
"a failed from_type creation must not register a device"
);
}
#[test]
fn test_get_gpu_filter_backends_does_not_register_devices() {
let _registry = HW_REGISTRY_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let devices_before = HW_DEVICES
.get()
.map(|m| m.lock().unwrap().len())
.unwrap_or(0);
let backends = get_gpu_filter_backends();
for backend in &backends {
println!(
"{}: device_available={} error={:?} filters_in_build={}/{}",
backend.name,
backend.device_available,
backend.device_error,
backend
.filters
.iter()
.filter(|f| f.present_in_build)
.count(),
backend.filters.len(),
);
}
let devices_after = HW_DEVICES
.get()
.map(|m| m.lock().unwrap().len())
.unwrap_or(0);
assert_eq!(
devices_before, devices_after,
"probing must not register devices in HW_DEVICES"
);
}
#[test]
fn split_plain_type() {
assert_eq!(split_device_type("cuda"), ("cuda", ""));
}
#[test]
fn split_type_with_device_ordinal() {
assert_eq!(split_device_type("cuda:0"), ("cuda", ":0"));
}
#[test]
fn split_type_with_name_and_source() {
assert_eq!(split_device_type("vaapi=va@src"), ("vaapi", "=va@src"));
}
#[test]
fn device_tail_plain_ordinal() {
assert_eq!(split_device_and_options(":0"), (Some("0"), None));
}
#[test]
fn device_tail_with_options() {
assert_eq!(
split_device_and_options(":/dev/dri/renderD128,k=v"),
(Some("/dev/dri/renderD128"), Some("k=v"))
);
}
#[test]
fn device_tail_options_only() {
assert_eq!(split_device_and_options(":,k=v"), (None, Some("k=v")));
}
#[test]
fn device_tail_empty() {
assert_eq!(split_device_and_options(":"), (None, None));
}
#[test]
fn from_type_production_wiring_reuses_across_calls() {
let _registry = HW_REGISTRY_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _restore = RegistrySnapshot::take();
let registry = HW_DEVICES.get_or_init(new_hw_devices);
let vaapi = AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI;
let dev_path = "/ez-ffmpeg-tests/wiring-pin-sentinel";
let key = type_init_arg(vaapi, Some(dev_path)).unwrap();
let sentinel = sentinel_buffer();
let sentinel_payload = payload(sentinel);
add_hw_device(HWDevice::new(
"wiring-pin".to_string(),
vaapi,
sentinel,
Some(key),
));
for round in 1..=2 {
let (err, dev) = hw_device_init_from_type(vaapi, Some(dev_path.to_string()));
assert_eq!(
err, 0,
"round {round}: the registered (type, device) request must \
succeed via the production reuse lookup, not attempt \
creation of the nonexistent node"
);
let dev = dev.expect("reuse returns the registered device");
assert_eq!(
payload(dev.device_ref()),
sentinel_payload,
"round {round}: from_type must hand back the SAME registered \
context"
);
assert_eq!(dev.name, "wiring-pin");
}
let len = registry
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len();
assert_eq!(len, 1, "reuse must never grow the registry");
}
#[test]
fn from_type_registration_records_the_canonical_reuse_key() {
let _registry = HW_REGISTRY_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _restore = RegistrySnapshot::take();
let vaapi = AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI;
let dev_path = "/ez-ffmpeg-tests/registration-key-sentinel";
let registered = register_from_type_device(
"regkey-pin".to_string(),
vaapi,
sentinel_buffer(),
Some(dev_path),
);
let expected_key = type_init_arg(vaapi, Some(dev_path)).unwrap();
assert_eq!(
registered.init_arg.as_deref(),
Some(expected_key.as_str()),
"from_type's registration must record the canonical reuse key"
);
let reused =
reuse_by_init_arg_move_to_back(&expected_key).expect("the registered key must match");
assert_eq!(reused.name, "regkey-pin");
assert_eq!(
payload(reused.device_ref()),
payload(registered.device_ref())
);
}
#[test]
fn registry_evicts_least_recently_used_past_the_cap() {
let _registry = HW_REGISTRY_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _restore = RegistrySnapshot::take();
let registry = HW_DEVICES.get_or_init(new_hw_devices);
for i in 0..HW_DEVICES_CAP {
add_hw_device(dev_with_ref(
&format!("d{i}"),
Some(&format!("spec-{i}")),
sentinel_buffer(),
));
}
assert!(reuse_by_init_arg_move_to_back("spec-0").is_some());
add_hw_device(dev_with_ref("fresh", Some("spec-fresh"), sentinel_buffer()));
let (len, names): (usize, Vec<String>) = {
let devices = registry
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
(
devices.len(),
devices.iter().map(|d| d.name.clone()).collect(),
)
};
assert_eq!(len, HW_DEVICES_CAP, "length pinned at the cap");
assert!(
names.iter().any(|n| n == "d0"),
"the just-reused entry must survive eviction (LRU, not FIFO)"
);
assert!(
!names.iter().any(|n| n == "d1"),
"the least-recently-used entry must be the one evicted"
);
assert!(
names.iter().any(|n| n == "fresh"),
"the new entry must be registered"
);
}
}