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 = hw_device_type_name(device_type)
.unwrap_or("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 OwnedDeviceRef {
fn create(
device_type: AVHWDeviceType,
device: Option<&CStr>,
opts: Option<&DictGuard>,
) -> Result<Self, i32> {
let mut device_ref = null_mut();
unsafe {
let err = av_hwdevice_ctx_create(
&mut device_ref,
device_type,
device.map_or(null(), CStr::as_ptr),
opts.map_or(null_mut(), |guard| guard.0),
0,
);
if err < 0 {
av_buffer_unref(&mut device_ref);
return Err(err);
}
}
Ok(Self(device_ref))
}
fn derive_from(device_type: AVHWDeviceType, src: &HWDevice) -> Result<Self, i32> {
let mut device_ref = null_mut();
unsafe {
let err =
av_hwdevice_ctx_create_derived(&mut device_ref, device_type, src.device_ref(), 0);
if err < 0 {
av_buffer_unref(&mut device_ref);
return Err(err);
}
}
Ok(Self(device_ref))
}
}
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: OwnedDeviceRef,
init_arg: Option<String>,
) -> Self {
HWDevice {
name,
device_type,
device: Arc::new(device),
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) fn hw_device_free_all() {
if let Some(slot) = FILTER_HW_DEVICE.get() {
slot.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
}
if let Some(hw_devices) = HW_DEVICES.get() {
hw_devices
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
}
}
#[derive(Clone, Copy)]
struct HwDeviceTypeName(&'static CStr);
impl HwDeviceTypeName {
fn lookup(device_type: AVHWDeviceType) -> Option<Self> {
unsafe {
let name = av_hwdevice_get_type_name(device_type);
if name.is_null() {
None
} else {
Some(Self(CStr::from_ptr(name)))
}
}
}
fn to_str(self) -> Option<&'static str> {
self.0.to_str().ok()
}
}
pub(crate) fn hw_device_type_name(device_type: AVHWDeviceType) -> Option<&'static str> {
HwDeviceTypeName::lookup(device_type)?.to_str()
}
pub(crate) fn hw_device_for_filter() -> Option<HWDevice> {
if let Some(slot) = FILTER_HW_DEVICE.get() {
let slot = slot
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(dev) = slot.as_ref() {
return Some(dev.clone());
}
}
let devices = HW_DEVICES.get_or_init(new_hw_devices);
let guard = devices
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (picked, advisory) = pick_filter_default(&guard);
drop(guard);
if let (Some(dev), Some(count)) = (&picked, advisory) {
let type_name = hw_device_type_name(dev.device_type).unwrap_or("unknown");
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.",
count,dev.name,
dev.name,);
}
picked
}
fn pick_filter_default(devices: &[HWDevice]) -> (Option<HWDevice>, Option<usize>) {
let picked = devices
.iter()
.rev()
.find(|dev| dev.device_type != AVHWDeviceType::AV_HWDEVICE_TYPE_NONE)
.cloned();
let advisory = (picked.is_some() && devices.len() > 1).then_some(devices.len());
(picked, advisory)
}
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_or_else(std::sync::PoisonError::into_inner);
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),
}
}
#[derive(Debug)]
struct DictGuard(*mut AVDictionary);
impl Drop for DictGuard {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { av_dict_free(&mut self.0) };
}
}
}
impl DictGuard {
fn parse(arg: &str, options: &str) -> Result<DictGuard, InitFailure> {
let mut guard = DictGuard(null_mut());
let Ok(options_cstr) = CString::new(options) else {
return Err(InitFailure {
code: AVERROR(EINVAL),
log: Some(format!(
"Device creation failed: option:{options} can't convert to CString"
)),
});
};
let eq_cstr = CString::new("=").unwrap();
let comma_cstr = CString::new(",").unwrap();
let err = unsafe {
av_dict_parse_string(
&mut guard.0,
options_cstr.as_ptr(),
eq_cstr.as_ptr(),
comma_cstr.as_ptr(),
0,
)
};
if err < 0 {
return Err(InitFailure {
code: AVERROR(EINVAL),
log: Some(format!(
"Invalid device specification \"{arg}\": failed to parse options"
)),
});
}
Ok(guard)
}
}
#[derive(Debug)]
struct InitFailure {
code: i32,
log: Option<String>,
}
fn settle(result: Result<HWDevice, InitFailure>) -> (i32, Option<HWDevice>) {
match result {
Ok(dev) => (0, Some(dev)),
Err(failure) => {
if let Some(message) = failure.log {
error!("{message}");
}
(failure.code, None)
}
}
}
pub(crate) fn hw_device_init_from_string(arg: &str) -> (i32, Option<HWDevice>) {
let result = {
let _init_guard = init_lock()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
hw_device_init_from_string_locked(arg)
};
settle(result)
}
fn hw_device_init_from_string_locked(arg: &str) -> Result<HWDevice, InitFailure> {
let (type_str, mut p) = split_device_type(arg);
let Ok(type_name) = CString::new(type_str) else {
return Err(InitFailure {
code: AVERROR(ENOMEM),
log: Some(format!(
"Device creation failed: type:{type_str} can't convert to CString"
)),
});
};
let device_type = unsafe { av_hwdevice_find_type_by_name(type_name.as_ptr()) };
if device_type == AVHWDeviceType::AV_HWDEVICE_TYPE_NONE {
return Err(InitFailure {
code: AVERROR(EINVAL),
log: Some(format!(
"Invalid device specification \"{arg}\": unknown device type"
)),
});
}
if let Some(existing) = reuse_by_init_arg_move_to_back(arg) {
return Ok(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() {
return Err(InitFailure {
code: AVERROR(EINVAL),
log: Some(format!(
"Invalid device specification \"{arg}\": named device already exists"
)),
});
}
let new_p_index = 1 + name_end;
p = &p[new_p_index..];
name
} else {
hw_device_default_name(device_type)
};
let device = if p.is_empty() {
OwnedDeviceRef::create(device_type, None, None).map_err(|err| InitFailure {
code: err,
log: Some(format!("Device creation failed: {err}.")),
})?
} else if p.starts_with(':') {
let (device_name, options_str) = split_device_and_options(p);
let options = match options_str {
Some(v) => Some(DictGuard::parse(arg, v)?),
None => None,
};
let device_name_cstr = match device_name {
None => None,
Some(device_name) => {
let Ok(device_name_cstr) = CString::new(device_name) else {
return Err(InitFailure {
code: AVERROR(EINVAL),
log: Some(format!(
"Device creation failed: device_name:{device_name} can't convert to CString"
)),
});
};
Some(device_name_cstr)
}
};
OwnedDeviceRef::create(device_type, device_name_cstr.as_deref(), options.as_ref()).map_err(
|err| InitFailure {
code: err,
log: Some(format!("Device creation failed: {err}.")),
},
)?
} else if let Some(src_name) = p.strip_prefix('@') {
let Some(src_device) = hw_device_get_by_name(src_name) else {
return Err(InitFailure {
code: AVERROR(EINVAL),
log: Some(format!(
"Invalid device specification \"{arg}\": invalid source device name"
)),
});
};
OwnedDeviceRef::derive_from(device_type, &src_device).map_err(|err| InitFailure {
code: err,
log: Some(format!("Device creation failed: {err}.")),
})?
} else if let Some(v) = p.strip_prefix(',') {
let options = DictGuard::parse(arg, v)?;
OwnedDeviceRef::create(device_type, None, Some(&options)).map_err(|err| InitFailure {
code: err,
log: Some(format!("Device creation failed: {err}.")),
})?
} else {
return Err(InitFailure {
code: AVERROR(EINVAL),
log: Some(format!(
"Invalid device specification \"{arg}\": parse error"
)),
});
};
let dev = HWDevice::new(name.unwrap(), device_type, device, Some(arg.to_string()));
add_hw_device(dev.clone());
Ok(dev)
}
pub(crate) fn hw_device_init_from_type(
device_type: AVHWDeviceType,
device: Option<String>,
) -> (i32, Option<HWDevice>) {
let result = {
let _init_guard = init_lock()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
hw_device_init_from_type_locked(device_type, device)
};
settle(result)
}
fn hw_device_init_from_type_locked(
device_type: AVHWDeviceType,
device: Option<String>,
) -> Result<HWDevice, InitFailure> {
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 Ok(existing);
}
}
let name = hw_device_default_name(device_type);
if name.is_none() {
return Err(InitFailure {
code: AVERROR(ENOMEM),
log: None,
});
}
let device_cstr = match device.as_deref() {
None => None,
Some(device) => {
let Ok(device_cstr) = CString::new(device) else {
return Err(InitFailure {
code: AVERROR(EINVAL),
log: None,
});
};
Some(device_cstr)
}
};
let owned =
OwnedDeviceRef::create(device_type, device_cstr.as_deref(), None).map_err(|err| {
InitFailure {
code: err,
log: Some(format!("Device creation failed: {err}.")),
}
})?;
Ok(register_from_type_device(
name.unwrap(),
device_type,
owned,
device.as_deref(),
))
}
fn register_from_type_device(
name: String,
device_type: AVHWDeviceType,
device: OwnedDeviceRef,
requested_device: Option<&str>,
) -> HWDevice {
let dev = HWDevice::new(
name,
device_type,
device,
type_init_arg(device_type, requested_device),
);
add_hw_device(dev.clone());
dev
}
fn type_init_arg(device_type: AVHWDeviceType, device: Option<&str>) -> Option<String> {
let type_name = hw_device_type_name(device_type)?;
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 = hw_device_type_name(device_type)?;
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_or_else(std::sync::PoisonError::into_inner);
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_or_else(std::sync::PoisonError::into_inner);
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_or_else(std::sync::PoisonError::into_inner);
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 Some(type_name) = HwDeviceTypeName::lookup(device_type) else {
continue;
};
let name = type_name.to_str().unwrap_or("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>) {
match OwnedDeviceRef::create(device_type, None, None) {
Err(err) => (false, Some(av_err2str(err))),
Ok(owned) => {
drop(owned);
(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"));
}
#[test]
fn type_name_lookup_rejects_null_and_resolves_known_types() {
assert!(HwDeviceTypeName::lookup(AVHWDeviceType::AV_HWDEVICE_TYPE_NONE).is_none());
assert_eq!(
hw_device_type_name(AVHWDeviceType::AV_HWDEVICE_TYPE_NONE),
None
);
let name = HwDeviceTypeName::lookup(AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI)
.expect("vaapi is always in libavutil's static name table")
.to_str();
assert_eq!(name, Some("vaapi"));
assert_eq!(
hw_device_type_name(AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI),
Some("vaapi")
);
}
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,
OwnedDeviceRef(device_ref),
init_arg.map(str::to_string),
)
}
fn typed_dev_with_ref(
name: &str,
device_type: AVHWDeviceType,
device_ref: *mut AVBufferRef,
) -> HWDevice {
HWDevice::new(
name.to_string(),
device_type,
OwnedDeviceRef(device_ref),
None,
)
}
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,
OwnedDeviceRef(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,
OwnedDeviceRef(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"
);
}
#[test]
fn filter_default_skips_typeless_devices() {
let _registry = HW_REGISTRY_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _restore = RegistrySnapshot::take();
add_hw_device(dev_with_ref("s0", None, sentinel_buffer()));
add_hw_device(dev_with_ref("s1", None, sentinel_buffer()));
assert!(
hw_device_for_filter().is_none(),
"a TYPE_NONE device must never be the filter default"
);
}
#[test]
fn filter_default_picks_the_newest_concretely_typed_device() {
let _registry = HW_REGISTRY_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _restore = RegistrySnapshot::take();
add_hw_device(typed_dev_with_ref(
"older-cuda",
AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA,
sentinel_buffer(),
));
add_hw_device(typed_dev_with_ref(
"newer-vaapi",
AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI,
sentinel_buffer(),
));
add_hw_device(dev_with_ref("sentinel", None, sentinel_buffer()));
let picked = hw_device_for_filter()
.expect("a concretely typed device must be picked over sentinels");
assert_eq!(picked.name, "newer-vaapi");
}
#[test]
fn pick_filter_default_advises_only_on_a_multi_device_pick() {
let devices = vec![
typed_dev_with_ref(
"older-cuda",
AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA,
sentinel_buffer(),
),
typed_dev_with_ref(
"newer-vaapi",
AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI,
sentinel_buffer(),
),
];
let (picked, advisory) = pick_filter_default(&devices);
assert_eq!(
picked.map(|d| d.name).as_deref(),
Some("newer-vaapi"),
"the newest concretely-typed entry wins"
);
assert_eq!(advisory, Some(2), "two devices -> advisory with the count");
let devices = vec![typed_dev_with_ref(
"solo-cuda",
AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA,
sentinel_buffer(),
)];
let (picked, advisory) = pick_filter_default(&devices);
assert_eq!(picked.map(|d| d.name).as_deref(), Some("solo-cuda"));
assert_eq!(advisory, None, "a single device needs no advisory");
let devices = vec![
dev_with_ref("s0", None, sentinel_buffer()),
dev_with_ref("s1", None, sentinel_buffer()),
];
let (picked, advisory) = pick_filter_default(&devices);
assert!(picked.is_none(), "TYPE_NONE sentinels are never picked");
assert_eq!(advisory, None, "no pick -> no advisory");
}
#[test]
fn registry_lock_poison_does_not_panic_accessors() {
let _registry = HW_REGISTRY_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _restore = RegistrySnapshot::take();
struct ClearPoison;
impl Drop for ClearPoison {
fn drop(&mut self) {
HW_DEVICES.get().unwrap().clear_poison();
}
}
let _clear = ClearPoison;
let _ = std::thread::spawn(|| {
let _guard = HW_DEVICES.get_or_init(new_hw_devices).lock().unwrap();
panic!("poison the registry lock");
})
.join();
assert!(
HW_DEVICES.get().unwrap().is_poisoned(),
"precondition: the registry mutex is poisoned"
);
assert!(hw_device_get_by_name("ez-ffmpeg-tests/no-such-device").is_none());
assert!(hw_device_get_by_type(AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI).is_none());
assert!(
hw_device_for_filter().is_none(),
"an empty (snapshot-cleared) registry has no filter default"
);
add_hw_device(dev_with_ref("poison-sentinel", None, sentinel_buffer()));
let len = HW_DEVICES
.get()
.unwrap()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len();
assert_eq!(len, 1, "add_hw_device must append on a poisoned registry");
}
#[test]
fn locked_init_body_defers_the_failure_log_to_the_wrapper() {
let _registry = HW_REGISTRY_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _restore = RegistrySnapshot::take();
let failure = hw_device_init_from_string_locked("ezffmpeg_bogus_type:0")
.expect_err("an unknown device type must fail");
assert_eq!(failure.code, AVERROR(EINVAL));
assert_eq!(
failure.log.as_deref(),
Some("Invalid device specification \"ezffmpeg_bogus_type:0\": unknown device type"),
"the deferred message must be byte-identical to the historical log"
);
let (code, dev) = hw_device_init_from_string("ezffmpeg_bogus_type:0");
assert_eq!(code, AVERROR(EINVAL));
assert!(dev.is_none());
let len = HW_DEVICES
.get()
.unwrap()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len();
assert_eq!(len, 0, "a failed init must not register a device");
}
#[test]
fn dict_guard_parse_rejects_malformed_options_and_accepts_valid() {
let guard = DictGuard::parse("spec", "k=v,k2=v2").expect("well-formed options must parse");
assert!(!guard.0.is_null(), "a parsed non-empty dict is non-null");
let failure = DictGuard::parse("spec", "bad\0option")
.expect_err("an interior NUL cannot become a C string");
assert_eq!(failure.code, AVERROR(EINVAL));
assert_eq!(
failure.log.as_deref(),
Some("Device creation failed: option:bad\0option can't convert to CString")
);
let failure = DictGuard::parse("spec", "novalue")
.expect_err("an option without '=' must fail to parse");
assert_eq!(failure.code, AVERROR(EINVAL));
assert_eq!(
failure.log.as_deref(),
Some("Invalid device specification \"spec\": failed to parse options")
);
}
}