Skip to main content

oximedia_transcode/hw_accel/
mod.rs

1//! Hardware acceleration detection and configuration.
2//!
3//! This module provides two complementary APIs:
4//!
5//! ## Legacy API (type-centric)
6//!
7//! [`HwAccelType`], [`HwAccelConfig`], [`HwEncoder`], [`HwFeature`] and
8//! [`detect_available_hw_accel`] return a `Vec<HwAccelType>`.  These types
9//! are retained for backward compatibility.
10//!
11//! ## Capability API (device-centric, platform-probing)
12//!
13//! [`HwAccelCapabilities`], [`HwAccelDevice`], [`HwKind`], and
14//! [`detect_hw_accel_caps`] return a richer, platform-probed capability
15//! set.  Use [`detect_hw_accel_with_probe`] for unit-testable code and
16//! [`MockProbe`] for tests.
17//!
18//! ## Platform Support Matrix
19//!
20//! | Platform | Backend | Codecs (Decode) | Codecs (Encode) |
21//! |----------|---------|-----------------|-----------------|
22//! | macOS (M1/M2) | VideoToolbox | H.264, HEVC | H.264, HEVC |
23//! | macOS (M3/M4) | VideoToolbox | H.264, HEVC, AV1 | H.264, HEVC |
24//! | macOS (Intel) | VideoToolbox | H.264, HEVC | H.264, HEVC |
25//! | Linux (Intel) | VAAPI | H.264, HEVC, AV1 | H.264, HEVC |
26//! | Linux (AMD) | VAAPI | H.264, HEVC, AV1 | H.264, HEVC |
27//! | Linux (NVIDIA) | VAAPI | H.264, HEVC | — |
28//! | Windows | — | — (planned DXVA2) | — |
29//! | WASM | — | — | — |
30//!
31//! Note: `libva` is not linked (Pure Rust policy). Linux detection reads
32//! `/sys/class/drm/` vendor IDs and applies a static codec-support matrix.
33//!
34//! ## Extending the Detector
35//!
36//! Implement [`HwProbe`] to inject custom detection logic or mock hardware in tests:
37//!
38//! ```rust,no_run
39//! use oximedia_transcode::{
40//!     detect_hw_accel_with_probe, HwAccelCapabilities, HwProbe,
41//! };
42//!
43//! struct MyProbe;
44//! impl HwProbe for MyProbe {
45//!     fn probe(&self) -> HwAccelCapabilities {
46//!         HwAccelCapabilities::none()
47//!     }
48//! }
49//!
50//! let caps = detect_hw_accel_with_probe(&MyProbe);
51//! assert!(caps.is_empty());
52//! ```
53
54pub mod capabilities;
55pub mod probe;
56
57#[cfg(target_os = "macos")]
58pub(crate) mod macos;
59
60#[cfg(target_os = "linux")]
61pub(crate) mod linux;
62
63// ─── Re-exports from capability layer ────────────────────────────────────────
64
65pub use capabilities::{HwAccelCapabilities, HwAccelDevice, HwKind};
66pub use probe::{HwProbe, MockProbe, SystemProbe};
67
68// ─── OnceLock cache for the new capability API ───────────────────────────────
69
70use std::sync::OnceLock;
71
72static HW_CAPS: OnceLock<HwAccelCapabilities> = OnceLock::new();
73
74/// Probe and return hardware acceleration capabilities for this process.
75///
76/// The result is computed once on the first call and cached for the process
77/// lifetime.  Subsequent calls return the cached value with no overhead.
78///
79/// Use [`detect_hw_accel_with_probe`] for unit tests where you need to inject
80/// a [`MockProbe`] instead of running the real system probe.
81#[must_use]
82pub fn detect_hw_accel_caps() -> &'static HwAccelCapabilities {
83    HW_CAPS.get_or_init(|| SystemProbe.probe())
84}
85
86/// Run a probe and return the result without touching the process-wide cache.
87///
88/// Useful in tests:
89///
90/// ```
91/// use oximedia_transcode::{
92///     detect_hw_accel_with_probe, HwAccelCapabilities, MockProbe,
93/// };
94///
95/// let probe = MockProbe(HwAccelCapabilities::none());
96/// let caps = detect_hw_accel_with_probe(&probe);
97/// assert!(caps.is_empty());
98/// ```
99#[must_use]
100pub fn detect_hw_accel_with_probe(probe: &dyn HwProbe) -> HwAccelCapabilities {
101    probe.probe()
102}
103
104// ─── Legacy API (preserved for backward compatibility) ────────────────────────
105
106use serde::{Deserialize, Serialize};
107
108/// Hardware acceleration types supported.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110pub enum HwAccelType {
111    /// No hardware acceleration.
112    None,
113    /// NVIDIA NVENC (CUDA).
114    Nvenc,
115    /// Intel Quick Sync Video.
116    Qsv,
117    /// AMD VCE/VCN.
118    Amd,
119    /// Apple `VideoToolbox`.
120    VideoToolbox,
121    /// Vulkan acceleration.
122    Vulkan,
123    /// Direct3D 11.
124    D3d11,
125    /// VAAPI (Linux).
126    Vaapi,
127    /// VDPAU (Linux, legacy).
128    Vdpau,
129}
130
131/// Hardware encoder information.
132#[derive(Debug, Clone)]
133pub struct HwEncoder {
134    /// Encoder type.
135    pub accel_type: HwAccelType,
136    /// Codec name.
137    pub codec: String,
138    /// Encoder name.
139    pub encoder_name: String,
140    /// Whether the encoder is available.
141    pub available: bool,
142    /// Maximum resolution supported.
143    pub max_resolution: (u32, u32),
144    /// Supported features.
145    pub features: Vec<HwFeature>,
146}
147
148/// Hardware encoder features.
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum HwFeature {
151    /// Supports 10-bit encoding.
152    TenBit,
153    /// Supports HDR encoding.
154    Hdr,
155    /// Supports B-frames.
156    BFrames,
157    /// Supports look-ahead.
158    Lookahead,
159    /// Supports temporal AQ.
160    TemporalAq,
161    /// Supports spatial AQ.
162    SpatialAq,
163    /// Supports weighted prediction.
164    WeightedPred,
165    /// Supports custom quantization matrices.
166    CustomQuant,
167}
168
169/// Hardware acceleration configuration.
170#[derive(Debug, Clone)]
171pub struct HwAccelConfig {
172    /// Preferred acceleration type.
173    pub preferred_type: HwAccelType,
174    /// Fallback to software if hardware unavailable.
175    pub allow_fallback: bool,
176    /// Use hardware for decoding.
177    pub decode: bool,
178    /// Use hardware for encoding.
179    pub encode: bool,
180    /// Device ID to use (for multi-GPU systems).
181    pub device_id: Option<u32>,
182}
183
184impl Default for HwAccelConfig {
185    fn default() -> Self {
186        Self {
187            preferred_type: HwAccelType::None,
188            allow_fallback: true,
189            decode: false,
190            encode: false,
191            device_id: None,
192        }
193    }
194}
195
196impl HwAccelConfig {
197    /// Creates a new hardware acceleration config.
198    #[must_use]
199    pub fn new(accel_type: HwAccelType) -> Self {
200        Self {
201            preferred_type: accel_type,
202            allow_fallback: true,
203            decode: true,
204            encode: true,
205            device_id: None,
206        }
207    }
208
209    /// Sets whether to allow fallback to software.
210    #[must_use]
211    pub fn allow_fallback(mut self, allow: bool) -> Self {
212        self.allow_fallback = allow;
213        self
214    }
215
216    /// Sets whether to use hardware for decoding.
217    #[must_use]
218    pub fn decode(mut self, enable: bool) -> Self {
219        self.decode = enable;
220        self
221    }
222
223    /// Sets whether to use hardware for encoding.
224    #[must_use]
225    pub fn encode(mut self, enable: bool) -> Self {
226        self.encode = enable;
227        self
228    }
229
230    /// Sets the device ID.
231    #[must_use]
232    pub fn device_id(mut self, id: u32) -> Self {
233        self.device_id = Some(id);
234        self
235    }
236}
237
238impl HwAccelType {
239    /// Gets the platform name.
240    #[must_use]
241    pub fn platform_name(self) -> &'static str {
242        match self {
243            Self::None => "software",
244            Self::Nvenc => "NVIDIA NVENC",
245            Self::Qsv => "Intel Quick Sync",
246            Self::Amd => "AMD VCE/VCN",
247            Self::VideoToolbox => "Apple VideoToolbox",
248            Self::Vulkan => "Vulkan",
249            Self::D3d11 => "Direct3D 11",
250            Self::Vaapi => "VAAPI",
251            Self::Vdpau => "VDPAU",
252        }
253    }
254
255    /// Checks if this acceleration type is available on the current platform.
256    #[must_use]
257    pub fn is_available(self) -> bool {
258        match self {
259            Self::None => true,
260            Self::Nvenc => detect_nvenc(),
261            Self::Qsv => detect_qsv(),
262            Self::Amd => detect_amd(),
263            Self::VideoToolbox => detect_videotoolbox(),
264            Self::Vulkan => detect_vulkan(),
265            Self::D3d11 => detect_d3d11(),
266            Self::Vaapi => detect_vaapi(),
267            Self::Vdpau => detect_vdpau(),
268        }
269    }
270
271    /// Gets supported codecs for this acceleration type.
272    #[must_use]
273    pub fn supported_codecs(self) -> Vec<&'static str> {
274        match self {
275            Self::None => vec!["h264", "vp8", "vp9", "av1", "theora"],
276            Self::Nvenc => vec!["h264", "h265", "av1"],
277            Self::Qsv => vec!["h264", "h265", "vp9", "av1"],
278            Self::Amd => vec!["h264", "h265", "av1"],
279            Self::VideoToolbox => vec!["h264", "h265"],
280            Self::Vulkan => vec!["h264", "h265"],
281            Self::D3d11 => vec!["h264", "h265", "vp9"],
282            Self::Vaapi => vec!["h264", "h265", "vp8", "vp9", "av1"],
283            Self::Vdpau => vec!["h264", "h265"],
284        }
285    }
286
287    /// Gets the encoder name for a given codec.
288    #[must_use]
289    pub fn encoder_name(self, codec: &str) -> Option<String> {
290        match self {
291            Self::None => Some(codec.to_string()),
292            Self::Nvenc => match codec {
293                "h264" => Some("h264_nvenc".to_string()),
294                "h265" => Some("hevc_nvenc".to_string()),
295                "av1" => Some("av1_nvenc".to_string()),
296                _ => None,
297            },
298            Self::Qsv => match codec {
299                "h264" => Some("h264_qsv".to_string()),
300                "h265" => Some("hevc_qsv".to_string()),
301                "vp9" => Some("vp9_qsv".to_string()),
302                "av1" => Some("av1_qsv".to_string()),
303                _ => None,
304            },
305            Self::Amd => match codec {
306                "h264" => Some("h264_amf".to_string()),
307                "h265" => Some("hevc_amf".to_string()),
308                "av1" => Some("av1_amf".to_string()),
309                _ => None,
310            },
311            Self::VideoToolbox => match codec {
312                "h264" => Some("h264_videotoolbox".to_string()),
313                "h265" => Some("hevc_videotoolbox".to_string()),
314                _ => None,
315            },
316            Self::Vulkan => match codec {
317                "h264" => Some("h264_vulkan".to_string()),
318                "h265" => Some("hevc_vulkan".to_string()),
319                _ => None,
320            },
321            Self::D3d11 => match codec {
322                "h264" => Some("h264_d3d11va".to_string()),
323                "h265" => Some("hevc_d3d11va".to_string()),
324                "vp9" => Some("vp9_d3d11va".to_string()),
325                _ => None,
326            },
327            Self::Vaapi => match codec {
328                "h264" => Some("h264_vaapi".to_string()),
329                "h265" => Some("hevc_vaapi".to_string()),
330                "vp8" => Some("vp8_vaapi".to_string()),
331                "vp9" => Some("vp9_vaapi".to_string()),
332                "av1" => Some("av1_vaapi".to_string()),
333                _ => None,
334            },
335            Self::Vdpau => match codec {
336                "h264" => Some("h264_vdpau".to_string()),
337                "h265" => Some("hevc_vdpau".to_string()),
338                _ => None,
339            },
340        }
341    }
342}
343
344/// Detects available hardware acceleration on the system (legacy API).
345///
346/// Returns a `Vec<HwAccelType>` starting with [`HwAccelType::None`].
347/// For richer per-device information, prefer [`detect_hw_accel_caps`].
348#[must_use]
349pub fn detect_available_hw_accel() -> Vec<HwAccelType> {
350    let mut available = vec![HwAccelType::None];
351
352    for accel_type in &[
353        HwAccelType::Nvenc,
354        HwAccelType::Qsv,
355        HwAccelType::Amd,
356        HwAccelType::VideoToolbox,
357        HwAccelType::Vulkan,
358        HwAccelType::D3d11,
359        HwAccelType::Vaapi,
360        HwAccelType::Vdpau,
361    ] {
362        if accel_type.is_available() {
363            available.push(*accel_type);
364        }
365    }
366
367    available
368}
369
370/// Detects the best hardware acceleration for a given codec (legacy API).
371#[must_use]
372pub fn detect_best_hw_accel_for_codec(codec: &str) -> Option<HwAccelType> {
373    detect_available_hw_accel()
374        .into_iter()
375        .find(|&accel_type| accel_type.supported_codecs().contains(&codec))
376}
377
378// ─── Platform-specific detection functions (legacy) ───────────────────────────
379
380/// Checks whether NVIDIA NVENC is available.
381#[cfg(target_os = "linux")]
382fn detect_nvenc() -> bool {
383    std::path::Path::new("/dev/nvidia0").exists()
384        && std::path::Path::new("/dev/nvidia-modeset").exists()
385}
386
387#[cfg(not(target_os = "linux"))]
388fn detect_nvenc() -> bool {
389    false
390}
391
392/// Detects Linux VAAPI by probing the DRI render node.
393#[cfg(target_os = "linux")]
394fn detect_vaapi() -> bool {
395    use std::path::Path;
396
397    if !Path::new("/dev/dri/renderD128").exists() {
398        return false;
399    }
400
401    if !Path::new("/dev/dri/card0").exists() {
402        if !Path::new("/dev/dri/renderD129").exists() {
403            return false;
404        }
405    }
406
407    let driver_paths = [
408        "/usr/lib/dri/i965_drv_video.so",
409        "/usr/lib/dri/iHD_drv_video.so",
410        "/usr/lib/dri/radeonsi_drv_video.so",
411        "/usr/lib/dri/nouveau_drv_video.so",
412        "/usr/lib/x86_64-linux-gnu/dri/i965_drv_video.so",
413        "/usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so",
414        "/usr/lib/x86_64-linux-gnu/dri/radeonsi_drv_video.so",
415    ];
416    for p in &driver_paths {
417        if Path::new(p).exists() {
418            return true;
419        }
420    }
421
422    true
423}
424
425#[cfg(not(target_os = "linux"))]
426fn detect_vaapi() -> bool {
427    false
428}
429
430/// Detects VDPAU on Linux.
431#[cfg(target_os = "linux")]
432fn detect_vdpau() -> bool {
433    use std::path::Path;
434    if !Path::new("/dev/dri/renderD128").exists() {
435        return false;
436    }
437    let vdpau_paths = [
438        "/usr/lib/vdpau/libvdpau_nvidia.so",
439        "/usr/lib/vdpau/libvdpau_r600.so",
440        "/usr/lib/vdpau/libvdpau_radeonsi.so",
441        "/usr/lib/x86_64-linux-gnu/vdpau/libvdpau_nvidia.so",
442        "/usr/lib/x86_64-linux-gnu/vdpau/libvdpau_radeonsi.so",
443    ];
444    vdpau_paths.iter().any(|p| Path::new(p).exists())
445}
446
447#[cfg(not(target_os = "linux"))]
448fn detect_vdpau() -> bool {
449    false
450}
451
452/// Detects Intel Quick Sync Video.
453#[cfg(target_os = "linux")]
454fn detect_qsv() -> bool {
455    use std::path::Path;
456    if !Path::new("/dev/dri/renderD128").exists() {
457        return false;
458    }
459    let intel_paths = [
460        "/usr/lib/dri/i965_drv_video.so",
461        "/usr/lib/dri/iHD_drv_video.so",
462        "/usr/lib/x86_64-linux-gnu/dri/i965_drv_video.so",
463        "/usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so",
464    ];
465    intel_paths.iter().any(|p| Path::new(p).exists())
466}
467
468#[cfg(target_os = "windows")]
469fn detect_qsv() -> bool {
470    false
471}
472
473#[cfg(not(any(target_os = "linux", target_os = "windows")))]
474fn detect_qsv() -> bool {
475    false
476}
477
478/// Detects AMD VCE/VCN hardware video encoder.
479#[cfg(target_os = "linux")]
480fn detect_amd() -> bool {
481    use std::path::Path;
482    if !Path::new("/dev/dri/renderD128").exists() {
483        return false;
484    }
485    let amd_paths = [
486        "/usr/lib/dri/radeonsi_drv_video.so",
487        "/usr/lib/x86_64-linux-gnu/dri/radeonsi_drv_video.so",
488    ];
489    amd_paths.iter().any(|p| Path::new(p).exists())
490}
491
492#[cfg(target_os = "windows")]
493fn detect_amd() -> bool {
494    false
495}
496
497#[cfg(not(any(target_os = "linux", target_os = "windows")))]
498fn detect_amd() -> bool {
499    false
500}
501
502/// Detects Apple VideoToolbox on macOS.
503#[cfg(target_os = "macos")]
504fn detect_videotoolbox() -> bool {
505    let framework_paths = [
506        "/System/Library/Frameworks/VideoToolbox.framework",
507        "/System/Library/Frameworks/CoreMedia.framework",
508    ];
509    framework_paths
510        .iter()
511        .all(|p| std::path::Path::new(p).exists())
512}
513
514#[cfg(not(target_os = "macos"))]
515fn detect_videotoolbox() -> bool {
516    false
517}
518
519/// Detects Direct3D 11 video acceleration.
520#[cfg(target_os = "windows")]
521fn detect_d3d11() -> bool {
522    true
523}
524
525#[cfg(not(target_os = "windows"))]
526fn detect_d3d11() -> bool {
527    false
528}
529
530/// Detects Vulkan video extension support.
531fn detect_vulkan() -> bool {
532    #[cfg(target_os = "linux")]
533    {
534        let vulkan_icd_paths = [
535            "/usr/share/vulkan/icd.d",
536            "/etc/vulkan/icd.d",
537            "/usr/local/share/vulkan/icd.d",
538        ];
539        vulkan_icd_paths
540            .iter()
541            .any(|p| std::path::Path::new(p).exists())
542    }
543    #[cfg(not(target_os = "linux"))]
544    {
545        false
546    }
547}
548
549impl HwEncoder {
550    /// Creates a new hardware encoder info.
551    #[must_use]
552    pub fn new(
553        accel_type: HwAccelType,
554        codec: impl Into<String>,
555        encoder_name: impl Into<String>,
556    ) -> Self {
557        Self {
558            accel_type,
559            codec: codec.into(),
560            encoder_name: encoder_name.into(),
561            available: false,
562            max_resolution: (7680, 4320),
563            features: Vec::new(),
564        }
565    }
566
567    /// Sets whether the encoder is available.
568    #[must_use]
569    pub fn available(mut self, available: bool) -> Self {
570        self.available = available;
571        self
572    }
573
574    /// Sets the maximum resolution.
575    #[must_use]
576    pub fn max_resolution(mut self, width: u32, height: u32) -> Self {
577        self.max_resolution = (width, height);
578        self
579    }
580
581    /// Adds a supported feature.
582    #[must_use]
583    pub fn with_feature(mut self, feature: HwFeature) -> Self {
584        self.features.push(feature);
585        self
586    }
587
588    /// Checks if a feature is supported.
589    #[must_use]
590    pub fn supports_feature(&self, feature: HwFeature) -> bool {
591        self.features.contains(&feature)
592    }
593}
594
595/// Gets information about all available hardware encoders.
596#[must_use]
597pub fn get_available_encoders() -> Vec<HwEncoder> {
598    let mut encoders = Vec::new();
599
600    for accel_type in detect_available_hw_accel() {
601        for codec in accel_type.supported_codecs() {
602            if let Some(encoder_name) = accel_type.encoder_name(codec) {
603                let encoder = HwEncoder::new(accel_type, codec, encoder_name).available(true);
604                encoders.push(encoder);
605            }
606        }
607    }
608
609    encoders
610}
611
612// ─── Tests ────────────────────────────────────────────────────────────────────
613
614#[cfg(test)]
615mod tests {
616    use super::*;
617
618    #[test]
619    fn test_hw_accel_type_platform_name() {
620        assert_eq!(HwAccelType::Nvenc.platform_name(), "NVIDIA NVENC");
621        assert_eq!(HwAccelType::Qsv.platform_name(), "Intel Quick Sync");
622        assert_eq!(HwAccelType::Vaapi.platform_name(), "VAAPI");
623    }
624
625    #[test]
626    fn test_hw_accel_supported_codecs() {
627        let codecs = HwAccelType::Nvenc.supported_codecs();
628        assert!(codecs.contains(&"h264"));
629        assert!(codecs.contains(&"h265"));
630    }
631
632    #[test]
633    fn test_hw_accel_encoder_name() {
634        assert_eq!(
635            HwAccelType::Nvenc.encoder_name("h264"),
636            Some("h264_nvenc".to_string())
637        );
638        assert_eq!(
639            HwAccelType::Qsv.encoder_name("h265"),
640            Some("hevc_qsv".to_string())
641        );
642    }
643
644    #[test]
645    fn test_hw_accel_config() {
646        let config = HwAccelConfig::new(HwAccelType::Nvenc)
647            .allow_fallback(false)
648            .decode(true)
649            .encode(true)
650            .device_id(0);
651
652        assert_eq!(config.preferred_type, HwAccelType::Nvenc);
653        assert!(!config.allow_fallback);
654        assert!(config.decode);
655        assert!(config.encode);
656        assert_eq!(config.device_id, Some(0));
657    }
658
659    #[test]
660    fn test_detect_available_hw_accel() {
661        let available = detect_available_hw_accel();
662        assert!(available.contains(&HwAccelType::None)); // Always available
663    }
664
665    #[test]
666    fn test_hw_encoder_creation() {
667        let encoder = HwEncoder::new(HwAccelType::Nvenc, "h264", "h264_nvenc")
668            .available(true)
669            .max_resolution(3840, 2160)
670            .with_feature(HwFeature::TenBit)
671            .with_feature(HwFeature::Lookahead);
672
673        assert_eq!(encoder.accel_type, HwAccelType::Nvenc);
674        assert_eq!(encoder.codec, "h264");
675        assert!(encoder.available);
676        assert_eq!(encoder.max_resolution, (3840, 2160));
677        assert!(encoder.supports_feature(HwFeature::TenBit));
678        assert!(encoder.supports_feature(HwFeature::Lookahead));
679        assert!(!encoder.supports_feature(HwFeature::Hdr));
680    }
681
682    // ── New capability API tests ──────────────────────────────────────────────
683
684    #[test]
685    fn test_mock_probe_empty_returns_empty_caps() {
686        let probe = MockProbe(HwAccelCapabilities::none());
687        let caps = detect_hw_accel_with_probe(&probe);
688        assert!(caps.is_empty());
689    }
690
691    #[test]
692    fn test_mock_probe_with_device_is_non_empty() {
693        use super::capabilities::HwAccelDevice;
694        let device = HwAccelDevice {
695            kind: HwKind::VideoToolbox,
696            driver: None,
697            render_node: None,
698            supported_codecs: vec!["h264".to_string(), "hevc".to_string()],
699            max_width: 8192,
700            max_height: 4320,
701            supports_hdr: true,
702        };
703        let caps = HwAccelCapabilities {
704            devices: vec![device],
705        };
706        let probe = MockProbe(caps);
707        let result = detect_hw_accel_with_probe(&probe);
708        assert!(!result.is_empty());
709        assert_eq!(result.devices.len(), 1);
710        assert!(result.devices[0].supports_codec("hevc"));
711    }
712
713    #[test]
714    fn test_hw_accel_capabilities_none_is_empty() {
715        let caps = HwAccelCapabilities::none();
716        assert!(caps.is_empty());
717        assert!(caps.device_for_codec("h264").is_none());
718    }
719
720    #[test]
721    fn test_hw_kind_debug() {
722        let kind = HwKind::Vaapi;
723        assert!(format!("{kind:?}").contains("Vaapi"));
724    }
725}