Skip to main content

oximedia_transcode/
hw_accel.rs

1//! Hardware acceleration detection and configuration.
2
3use serde::{Deserialize, Serialize};
4
5/// Hardware acceleration types supported.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum HwAccelType {
8    /// No hardware acceleration.
9    None,
10    /// NVIDIA NVENC (CUDA).
11    Nvenc,
12    /// Intel Quick Sync Video.
13    Qsv,
14    /// AMD VCE/VCN.
15    Amd,
16    /// Apple `VideoToolbox`.
17    VideoToolbox,
18    /// Vulkan acceleration.
19    Vulkan,
20    /// Direct3D 11.
21    D3d11,
22    /// VAAPI (Linux).
23    Vaapi,
24    /// VDPAU (Linux, legacy).
25    Vdpau,
26}
27
28/// Hardware encoder information.
29#[derive(Debug, Clone)]
30pub struct HwEncoder {
31    /// Encoder type.
32    pub accel_type: HwAccelType,
33    /// Codec name.
34    pub codec: String,
35    /// Encoder name.
36    pub encoder_name: String,
37    /// Whether the encoder is available.
38    pub available: bool,
39    /// Maximum resolution supported.
40    pub max_resolution: (u32, u32),
41    /// Supported features.
42    pub features: Vec<HwFeature>,
43}
44
45/// Hardware encoder features.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum HwFeature {
48    /// Supports 10-bit encoding.
49    TenBit,
50    /// Supports HDR encoding.
51    Hdr,
52    /// Supports B-frames.
53    BFrames,
54    /// Supports look-ahead.
55    Lookahead,
56    /// Supports temporal AQ.
57    TemporalAq,
58    /// Supports spatial AQ.
59    SpatialAq,
60    /// Supports weighted prediction.
61    WeightedPred,
62    /// Supports custom quantization matrices.
63    CustomQuant,
64}
65
66/// Hardware acceleration configuration.
67#[derive(Debug, Clone)]
68pub struct HwAccelConfig {
69    /// Preferred acceleration type.
70    pub preferred_type: HwAccelType,
71    /// Fallback to software if hardware unavailable.
72    pub allow_fallback: bool,
73    /// Use hardware for decoding.
74    pub decode: bool,
75    /// Use hardware for encoding.
76    pub encode: bool,
77    /// Device ID to use (for multi-GPU systems).
78    pub device_id: Option<u32>,
79}
80
81impl Default for HwAccelConfig {
82    fn default() -> Self {
83        Self {
84            preferred_type: HwAccelType::None,
85            allow_fallback: true,
86            decode: false,
87            encode: false,
88            device_id: None,
89        }
90    }
91}
92
93impl HwAccelConfig {
94    /// Creates a new hardware acceleration config.
95    #[must_use]
96    pub fn new(accel_type: HwAccelType) -> Self {
97        Self {
98            preferred_type: accel_type,
99            allow_fallback: true,
100            decode: true,
101            encode: true,
102            device_id: None,
103        }
104    }
105
106    /// Sets whether to allow fallback to software.
107    #[must_use]
108    pub fn allow_fallback(mut self, allow: bool) -> Self {
109        self.allow_fallback = allow;
110        self
111    }
112
113    /// Sets whether to use hardware for decoding.
114    #[must_use]
115    pub fn decode(mut self, enable: bool) -> Self {
116        self.decode = enable;
117        self
118    }
119
120    /// Sets whether to use hardware for encoding.
121    #[must_use]
122    pub fn encode(mut self, enable: bool) -> Self {
123        self.encode = enable;
124        self
125    }
126
127    /// Sets the device ID.
128    #[must_use]
129    pub fn device_id(mut self, id: u32) -> Self {
130        self.device_id = Some(id);
131        self
132    }
133}
134
135impl HwAccelType {
136    /// Gets the platform name.
137    #[must_use]
138    pub fn platform_name(self) -> &'static str {
139        match self {
140            Self::None => "software",
141            Self::Nvenc => "NVIDIA NVENC",
142            Self::Qsv => "Intel Quick Sync",
143            Self::Amd => "AMD VCE/VCN",
144            Self::VideoToolbox => "Apple VideoToolbox",
145            Self::Vulkan => "Vulkan",
146            Self::D3d11 => "Direct3D 11",
147            Self::Vaapi => "VAAPI",
148            Self::Vdpau => "VDPAU",
149        }
150    }
151
152    /// Checks if this acceleration type is available on the current platform.
153    #[must_use]
154    pub fn is_available(self) -> bool {
155        // Placeholder - would perform actual detection
156        match self {
157            Self::None => true,
158            Self::Nvenc => detect_nvenc(),
159            Self::Qsv => detect_qsv(),
160            Self::Amd => detect_amd(),
161            Self::VideoToolbox => detect_videotoolbox(),
162            Self::Vulkan => detect_vulkan(),
163            Self::D3d11 => detect_d3d11(),
164            Self::Vaapi => detect_vaapi(),
165            Self::Vdpau => detect_vdpau(),
166        }
167    }
168
169    /// Gets supported codecs for this acceleration type.
170    #[must_use]
171    pub fn supported_codecs(self) -> Vec<&'static str> {
172        match self {
173            Self::None => vec!["h264", "vp8", "vp9", "av1", "theora"],
174            Self::Nvenc => vec!["h264", "h265", "av1"],
175            Self::Qsv => vec!["h264", "h265", "vp9", "av1"],
176            Self::Amd => vec!["h264", "h265", "av1"],
177            Self::VideoToolbox => vec!["h264", "h265"],
178            Self::Vulkan => vec!["h264", "h265"],
179            Self::D3d11 => vec!["h264", "h265", "vp9"],
180            Self::Vaapi => vec!["h264", "h265", "vp8", "vp9", "av1"],
181            Self::Vdpau => vec!["h264", "h265"],
182        }
183    }
184
185    /// Gets the encoder name for a given codec.
186    #[must_use]
187    pub fn encoder_name(self, codec: &str) -> Option<String> {
188        match self {
189            Self::None => Some(codec.to_string()),
190            Self::Nvenc => match codec {
191                "h264" => Some("h264_nvenc".to_string()),
192                "h265" => Some("hevc_nvenc".to_string()),
193                "av1" => Some("av1_nvenc".to_string()),
194                _ => None,
195            },
196            Self::Qsv => match codec {
197                "h264" => Some("h264_qsv".to_string()),
198                "h265" => Some("hevc_qsv".to_string()),
199                "vp9" => Some("vp9_qsv".to_string()),
200                "av1" => Some("av1_qsv".to_string()),
201                _ => None,
202            },
203            Self::Amd => match codec {
204                "h264" => Some("h264_amf".to_string()),
205                "h265" => Some("hevc_amf".to_string()),
206                "av1" => Some("av1_amf".to_string()),
207                _ => None,
208            },
209            Self::VideoToolbox => match codec {
210                "h264" => Some("h264_videotoolbox".to_string()),
211                "h265" => Some("hevc_videotoolbox".to_string()),
212                _ => None,
213            },
214            Self::Vulkan => match codec {
215                "h264" => Some("h264_vulkan".to_string()),
216                "h265" => Some("hevc_vulkan".to_string()),
217                _ => None,
218            },
219            Self::D3d11 => match codec {
220                "h264" => Some("h264_d3d11va".to_string()),
221                "h265" => Some("hevc_d3d11va".to_string()),
222                "vp9" => Some("vp9_d3d11va".to_string()),
223                _ => None,
224            },
225            Self::Vaapi => match codec {
226                "h264" => Some("h264_vaapi".to_string()),
227                "h265" => Some("hevc_vaapi".to_string()),
228                "vp8" => Some("vp8_vaapi".to_string()),
229                "vp9" => Some("vp9_vaapi".to_string()),
230                "av1" => Some("av1_vaapi".to_string()),
231                _ => None,
232            },
233            Self::Vdpau => match codec {
234                "h264" => Some("h264_vdpau".to_string()),
235                "h265" => Some("hevc_vdpau".to_string()),
236                _ => None,
237            },
238        }
239    }
240}
241
242/// Detects available hardware acceleration on the system.
243#[must_use]
244pub fn detect_available_hw_accel() -> Vec<HwAccelType> {
245    let mut available = vec![HwAccelType::None];
246
247    for accel_type in &[
248        HwAccelType::Nvenc,
249        HwAccelType::Qsv,
250        HwAccelType::Amd,
251        HwAccelType::VideoToolbox,
252        HwAccelType::Vulkan,
253        HwAccelType::D3d11,
254        HwAccelType::Vaapi,
255        HwAccelType::Vdpau,
256    ] {
257        if accel_type.is_available() {
258            available.push(*accel_type);
259        }
260    }
261
262    available
263}
264
265/// Detects the best hardware acceleration for a given codec.
266#[must_use]
267pub fn detect_best_hw_accel_for_codec(codec: &str) -> Option<HwAccelType> {
268    detect_available_hw_accel()
269        .into_iter()
270        .find(|&accel_type| accel_type.supported_codecs().contains(&codec))
271}
272
273// ─── Platform-specific detection functions ────────────────────────────────────
274
275/// Checks whether NVIDIA NVENC is available.
276///
277/// On Linux: presence of `/dev/nvidia0` device node.
278/// On other platforms: always false (device presence cannot be probed without
279/// the CUDA SDK, which conflicts with the Pure-Rust policy).
280#[cfg(target_os = "linux")]
281fn detect_nvenc() -> bool {
282    // Require both the NVIDIA device node and the render DRI node to
283    // exclude false positives on systems with NVIDIA kernel modules but no
284    // NVENC-capable encoder (e.g., some Tegra boards).
285    std::path::Path::new("/dev/nvidia0").exists()
286        && std::path::Path::new("/dev/nvidia-modeset").exists()
287}
288
289#[cfg(not(target_os = "linux"))]
290fn detect_nvenc() -> bool {
291    false
292}
293
294/// Detects Linux VAAPI by probing the DRI render node and looking for at least
295/// one codec entry in the `/sys/class/drm/` hierarchy that indicates hardware
296/// video decoding support.
297///
298/// The detection strategy (pure Rust, no C FFI):
299/// 1. `/dev/dri/renderD128` must exist (primary GPU render node).
300/// 2. `/sys/kernel/debug/dri/128/state` *or* at least one
301///    `/sys/class/drm/card*/*/decode` entry exists — we accept the render
302///    node alone as sufficient signal because many systems do not expose the
303///    debug FS entry.
304///
305/// This is conservative but avoids false negatives on typical desktop Linux
306/// (Mesa/Intel/AMD) and false positives on ARM boards with no media engine.
307#[cfg(target_os = "linux")]
308fn detect_vaapi() -> bool {
309    use std::path::Path;
310
311    // Primary render node must exist.
312    if !Path::new("/dev/dri/renderD128").exists() {
313        return false;
314    }
315
316    // Check for any secondary render node (renderD129…) or the primary only.
317    // Additionally probe /dev/dri/card0 as confirmation of a full DRM stack.
318    if !Path::new("/dev/dri/card0").exists() {
319        // Attempt renderD129 as fallback (dual-GPU systems).
320        if !Path::new("/dev/dri/renderD129").exists() {
321            return false;
322        }
323    }
324
325    // Check that at least one DRI driver library exists.
326    // Common paths on major distros:
327    let driver_paths = [
328        "/usr/lib/dri/i965_drv_video.so",
329        "/usr/lib/dri/iHD_drv_video.so",
330        "/usr/lib/dri/radeonsi_drv_video.so",
331        "/usr/lib/dri/nouveau_drv_video.so",
332        "/usr/lib/x86_64-linux-gnu/dri/i965_drv_video.so",
333        "/usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so",
334        "/usr/lib/x86_64-linux-gnu/dri/radeonsi_drv_video.so",
335    ];
336    for p in &driver_paths {
337        if Path::new(p).exists() {
338            return true;
339        }
340    }
341
342    // Even without a driver library we have the render node — report available
343    // so that the caller can attempt initialisation and get a proper API error.
344    true
345}
346
347#[cfg(not(target_os = "linux"))]
348fn detect_vaapi() -> bool {
349    false
350}
351
352/// Detects VDPAU on Linux by looking for the vendor library directory.
353#[cfg(target_os = "linux")]
354fn detect_vdpau() -> bool {
355    use std::path::Path;
356    // VDPAU requires the render node and at least one vendor library.
357    if !Path::new("/dev/dri/renderD128").exists() {
358        return false;
359    }
360    let vdpau_paths = [
361        "/usr/lib/vdpau/libvdpau_nvidia.so",
362        "/usr/lib/vdpau/libvdpau_r600.so",
363        "/usr/lib/vdpau/libvdpau_radeonsi.so",
364        "/usr/lib/x86_64-linux-gnu/vdpau/libvdpau_nvidia.so",
365        "/usr/lib/x86_64-linux-gnu/vdpau/libvdpau_radeonsi.so",
366    ];
367    vdpau_paths.iter().any(|p| Path::new(p).exists())
368}
369
370#[cfg(not(target_os = "linux"))]
371fn detect_vdpau() -> bool {
372    false
373}
374
375/// Detects Intel Quick Sync Video.
376///
377/// On Linux: same render node as VAAPI with Intel-specific drivers.
378/// On Windows: registry-based detection is not Pure-Rust compatible; we
379/// conservatively return false.
380#[cfg(target_os = "linux")]
381fn detect_qsv() -> bool {
382    use std::path::Path;
383    if !Path::new("/dev/dri/renderD128").exists() {
384        return false;
385    }
386    // Intel-specific VAAPI drivers signal QSV availability.
387    let intel_paths = [
388        "/usr/lib/dri/i965_drv_video.so",
389        "/usr/lib/dri/iHD_drv_video.so",
390        "/usr/lib/x86_64-linux-gnu/dri/i965_drv_video.so",
391        "/usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so",
392    ];
393    intel_paths.iter().any(|p| Path::new(p).exists())
394}
395
396#[cfg(target_os = "windows")]
397fn detect_qsv() -> bool {
398    // QSV on Windows requires the Intel Media SDK.  Detecting it without
399    // Win32 API calls (which require unsafe) is not possible in Pure Rust.
400    false
401}
402
403#[cfg(not(any(target_os = "linux", target_os = "windows")))]
404fn detect_qsv() -> bool {
405    false
406}
407
408/// Detects AMD VCE/VCN hardware video encoder.
409///
410/// On Linux: detected via the AMD-specific VAAPI driver.
411#[cfg(target_os = "linux")]
412fn detect_amd() -> bool {
413    use std::path::Path;
414    if !Path::new("/dev/dri/renderD128").exists() {
415        return false;
416    }
417    let amd_paths = [
418        "/usr/lib/dri/radeonsi_drv_video.so",
419        "/usr/lib/x86_64-linux-gnu/dri/radeonsi_drv_video.so",
420    ];
421    amd_paths.iter().any(|p| Path::new(p).exists())
422}
423
424#[cfg(target_os = "windows")]
425fn detect_amd() -> bool {
426    false
427}
428
429#[cfg(not(any(target_os = "linux", target_os = "windows")))]
430fn detect_amd() -> bool {
431    false
432}
433
434/// Detects Apple VideoToolbox on macOS.
435///
436/// VideoToolbox is available on all macOS 10.8+ systems.  We confirm its
437/// presence by checking for the framework bundle on disk without using any
438/// Objective-C or C FFI calls.
439#[cfg(target_os = "macos")]
440fn detect_videotoolbox() -> bool {
441    // The VideoToolbox framework is always present on macOS 10.8+.
442    // Verify the framework directory exists as a belt-and-suspenders check.
443    let framework_paths = [
444        "/System/Library/Frameworks/VideoToolbox.framework",
445        "/System/Library/Frameworks/CoreMedia.framework",
446    ];
447    framework_paths
448        .iter()
449        .all(|p| std::path::Path::new(p).exists())
450}
451
452#[cfg(not(target_os = "macos"))]
453fn detect_videotoolbox() -> bool {
454    false
455}
456
457/// Detects Direct3D 11 video acceleration.
458///
459/// D3D11 is available on Windows 7+ but probing requires Win32 API calls.
460/// We conservatively mark it available on Windows without deeper probing.
461#[cfg(target_os = "windows")]
462fn detect_d3d11() -> bool {
463    // D3D11 ships with Windows 7+ — safe to assume available.
464    true
465}
466
467#[cfg(not(target_os = "windows"))]
468fn detect_d3d11() -> bool {
469    false
470}
471
472/// Detects Vulkan video extension support.
473///
474/// A pure-Rust Vulkan instance creation would require the `ash` or `vulkano`
475/// crates which conflict with the Pure-Rust policy when linked against the
476/// Vulkan loader (a C shared library).  We therefore probe only the presence
477/// of the Vulkan ICD manifest directory.
478fn detect_vulkan() -> bool {
479    #[cfg(target_os = "linux")]
480    {
481        let vulkan_icd_paths = [
482            "/usr/share/vulkan/icd.d",
483            "/etc/vulkan/icd.d",
484            "/usr/local/share/vulkan/icd.d",
485        ];
486        vulkan_icd_paths
487            .iter()
488            .any(|p| std::path::Path::new(p).exists())
489    }
490    #[cfg(not(target_os = "linux"))]
491    {
492        false
493    }
494}
495
496impl HwEncoder {
497    /// Creates a new hardware encoder info.
498    #[must_use]
499    pub fn new(
500        accel_type: HwAccelType,
501        codec: impl Into<String>,
502        encoder_name: impl Into<String>,
503    ) -> Self {
504        Self {
505            accel_type,
506            codec: codec.into(),
507            encoder_name: encoder_name.into(),
508            available: false,
509            max_resolution: (7680, 4320), // 8K
510            features: Vec::new(),
511        }
512    }
513
514    /// Sets whether the encoder is available.
515    #[must_use]
516    pub fn available(mut self, available: bool) -> Self {
517        self.available = available;
518        self
519    }
520
521    /// Sets the maximum resolution.
522    #[must_use]
523    pub fn max_resolution(mut self, width: u32, height: u32) -> Self {
524        self.max_resolution = (width, height);
525        self
526    }
527
528    /// Adds a supported feature.
529    #[must_use]
530    pub fn with_feature(mut self, feature: HwFeature) -> Self {
531        self.features.push(feature);
532        self
533    }
534
535    /// Checks if a feature is supported.
536    #[must_use]
537    pub fn supports_feature(&self, feature: HwFeature) -> bool {
538        self.features.contains(&feature)
539    }
540}
541
542/// Gets information about all available hardware encoders.
543#[must_use]
544pub fn get_available_encoders() -> Vec<HwEncoder> {
545    let mut encoders = Vec::new();
546
547    for accel_type in detect_available_hw_accel() {
548        for codec in accel_type.supported_codecs() {
549            if let Some(encoder_name) = accel_type.encoder_name(codec) {
550                let encoder = HwEncoder::new(accel_type, codec, encoder_name).available(true);
551                encoders.push(encoder);
552            }
553        }
554    }
555
556    encoders
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    #[test]
564    fn test_hw_accel_type_platform_name() {
565        assert_eq!(HwAccelType::Nvenc.platform_name(), "NVIDIA NVENC");
566        assert_eq!(HwAccelType::Qsv.platform_name(), "Intel Quick Sync");
567        assert_eq!(HwAccelType::Vaapi.platform_name(), "VAAPI");
568    }
569
570    #[test]
571    fn test_hw_accel_supported_codecs() {
572        let codecs = HwAccelType::Nvenc.supported_codecs();
573        assert!(codecs.contains(&"h264"));
574        assert!(codecs.contains(&"h265"));
575    }
576
577    #[test]
578    fn test_hw_accel_encoder_name() {
579        assert_eq!(
580            HwAccelType::Nvenc.encoder_name("h264"),
581            Some("h264_nvenc".to_string())
582        );
583        assert_eq!(
584            HwAccelType::Qsv.encoder_name("h265"),
585            Some("hevc_qsv".to_string())
586        );
587    }
588
589    #[test]
590    fn test_hw_accel_config() {
591        let config = HwAccelConfig::new(HwAccelType::Nvenc)
592            .allow_fallback(false)
593            .decode(true)
594            .encode(true)
595            .device_id(0);
596
597        assert_eq!(config.preferred_type, HwAccelType::Nvenc);
598        assert!(!config.allow_fallback);
599        assert!(config.decode);
600        assert!(config.encode);
601        assert_eq!(config.device_id, Some(0));
602    }
603
604    #[test]
605    fn test_detect_available_hw_accel() {
606        let available = detect_available_hw_accel();
607        assert!(available.contains(&HwAccelType::None)); // Always available
608    }
609
610    #[test]
611    fn test_hw_encoder_creation() {
612        let encoder = HwEncoder::new(HwAccelType::Nvenc, "h264", "h264_nvenc")
613            .available(true)
614            .max_resolution(3840, 2160)
615            .with_feature(HwFeature::TenBit)
616            .with_feature(HwFeature::Lookahead);
617
618        assert_eq!(encoder.accel_type, HwAccelType::Nvenc);
619        assert_eq!(encoder.codec, "h264");
620        assert!(encoder.available);
621        assert_eq!(encoder.max_resolution, (3840, 2160));
622        assert!(encoder.supports_feature(HwFeature::TenBit));
623        assert!(encoder.supports_feature(HwFeature::Lookahead));
624        assert!(!encoder.supports_feature(HwFeature::Hdr));
625    }
626}