Skip to main content

oximedia_transcode/
hwaccel.rs

1//! Hardware acceleration configuration and simulation.
2//!
3//! Pure-Rust module providing backend enumeration, capability modelling,
4//! codec–encoder name mapping, and best-backend selection — no actual
5//! hardware calls are performed.
6
7use serde::{Deserialize, Serialize};
8
9// ─── HwAccelBackend ───────────────────────────────────────────────────────────
10
11/// Hardware acceleration backends supported by OxiMedia.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub enum HwAccelBackend {
14    /// Software-only encoding/decoding.
15    None,
16    /// Video Acceleration API (Linux, Intel/AMD/Nvidia via VA-API).
17    Vaapi,
18    /// NVIDIA NVENC/NVDEC (CUDA-based).
19    Nvenc,
20    /// Apple VideoToolbox (macOS / iOS).
21    Videotoolbox,
22    /// Intel Quick Sync Video.
23    Qsv,
24    /// AMD Advanced Media Framework.
25    Amf,
26    /// Direct3D 11 Video Acceleration (Windows).
27    D3d11Va,
28}
29
30impl HwAccelBackend {
31    /// Returns a stable string identifier for this backend.
32    #[must_use]
33    pub fn as_str(&self) -> &'static str {
34        match self {
35            Self::None => "none",
36            Self::Vaapi => "vaapi",
37            Self::Nvenc => "nvenc",
38            Self::Videotoolbox => "videotoolbox",
39            Self::Qsv => "qsv",
40            Self::Amf => "amf",
41            Self::D3d11Va => "d3d11va",
42        }
43    }
44
45    /// Returns all non-`None` backends.
46    #[must_use]
47    pub fn all_hw() -> &'static [HwAccelBackend] {
48        &[
49            Self::Vaapi,
50            Self::Nvenc,
51            Self::Videotoolbox,
52            Self::Qsv,
53            Self::Amf,
54            Self::D3d11Va,
55        ]
56    }
57}
58
59// ─── HwAccelCaps ─────────────────────────────────────────────────────────────
60
61/// Capabilities advertised by a hardware acceleration backend.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct HwAccelCaps {
64    /// Which backend these capabilities describe.
65    pub backend: HwAccelBackend,
66    /// Whether this backend can accelerate decoding.
67    pub can_decode: bool,
68    /// Whether this backend can accelerate encoding.
69    pub can_encode: bool,
70    /// Maximum resolution supported (width, height).
71    pub max_resolution: (u32, u32),
72    /// Codec names supported by this backend (e.g., "hevc", "av1").
73    pub supported_codecs: Vec<String>,
74    /// Maximum concurrent encode/decode sessions.
75    pub max_sessions: u8,
76}
77
78impl HwAccelCaps {
79    /// Returns `true` if this backend supports the given codec.
80    #[must_use]
81    pub fn supports_codec(&self, codec: &str) -> bool {
82        let codec_lower = codec.to_lowercase();
83        self.supported_codecs
84            .iter()
85            .any(|c| c.to_lowercase() == codec_lower)
86    }
87
88    /// Returns `true` if this backend supports the given resolution.
89    #[must_use]
90    pub fn supports_resolution(&self, width: u32, height: u32) -> bool {
91        width <= self.max_resolution.0 && height <= self.max_resolution.1
92    }
93}
94
95// ─── HwAccelConfig ────────────────────────────────────────────────────────────
96
97/// Runtime configuration for using a hardware acceleration backend.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct HwAccelConfig {
100    /// The backend to use.
101    pub backend: HwAccelBackend,
102    /// Device index (for multi-GPU setups).
103    pub device_index: u8,
104    /// Fall back to software encoding if the HW backend fails.
105    pub fallback_to_software: bool,
106}
107
108impl HwAccelConfig {
109    /// Creates a new config for the given backend.
110    #[must_use]
111    pub fn new(backend: HwAccelBackend) -> Self {
112        Self {
113            backend,
114            device_index: 0,
115            fallback_to_software: true,
116        }
117    }
118
119    /// Software-only configuration.
120    #[must_use]
121    pub fn software() -> Self {
122        Self::new(HwAccelBackend::None)
123    }
124
125    /// Sets the device index.
126    #[must_use]
127    pub fn with_device(mut self, index: u8) -> Self {
128        self.device_index = index;
129        self
130    }
131
132    /// Disables fallback to software.
133    #[must_use]
134    pub fn no_fallback(mut self) -> Self {
135        self.fallback_to_software = false;
136        self
137    }
138}
139
140// ─── HwCodecMapping ───────────────────────────────────────────────────────────
141
142/// Maps hardware backends and codecs to encoder/decoder program names.
143#[derive(Debug, Clone, Default)]
144pub struct HwCodecMapping;
145
146impl HwCodecMapping {
147    /// Creates a new mapping.
148    #[must_use]
149    pub fn new() -> Self {
150        Self
151    }
152
153    /// Returns the platform-specific encoder name for a backend+codec pair,
154    /// or `None` if the combination is not supported.
155    #[must_use]
156    pub fn get_encoder_name(backend: &HwAccelBackend, codec: &str) -> Option<&'static str> {
157        let codec_lower = codec.to_lowercase();
158        match backend {
159            HwAccelBackend::None => None,
160            HwAccelBackend::Vaapi => match codec_lower.as_str() {
161                "hevc" | "h265" => Some("hevc_vaapi"),
162                "h264" | "avc" => Some("h264_vaapi"),
163                "vp9" => Some("vp9_vaapi"),
164                "av1" => Some("av1_vaapi"),
165                "vp8" => Some("vp8_vaapi"),
166                "mjpeg" => Some("mjpeg_vaapi"),
167                _ => None,
168            },
169            HwAccelBackend::Nvenc => match codec_lower.as_str() {
170                "hevc" | "h265" => Some("hevc_nvenc"),
171                "h264" | "avc" => Some("h264_nvenc"),
172                "av1" => Some("av1_nvenc"),
173                _ => None,
174            },
175            HwAccelBackend::Videotoolbox => match codec_lower.as_str() {
176                "hevc" | "h265" => Some("hevc_videotoolbox"),
177                "h264" | "avc" => Some("h264_videotoolbox"),
178                _ => None,
179            },
180            HwAccelBackend::Qsv => match codec_lower.as_str() {
181                "hevc" | "h265" => Some("hevc_qsv"),
182                "h264" | "avc" => Some("h264_qsv"),
183                "vp9" => Some("vp9_qsv"),
184                "av1" => Some("av1_qsv"),
185                "mjpeg" => Some("mjpeg_qsv"),
186                _ => None,
187            },
188            HwAccelBackend::Amf => match codec_lower.as_str() {
189                "hevc" | "h265" => Some("hevc_amf"),
190                "h264" | "avc" => Some("h264_amf"),
191                "av1" => Some("av1_amf"),
192                _ => None,
193            },
194            HwAccelBackend::D3d11Va => match codec_lower.as_str() {
195                "hevc" | "h265" => Some("hevc_d3d11va"),
196                "h264" | "avc" => Some("h264_d3d11va"),
197                _ => None,
198            },
199        }
200    }
201
202    /// Convenience wrapper for owned types.
203    #[must_use]
204    pub fn encoder_name(backend: &HwAccelBackend, codec: &str) -> Option<&'static str> {
205        Self::get_encoder_name(backend, codec)
206    }
207}
208
209// ─── simulate_hw_caps ─────────────────────────────────────────────────────────
210
211/// Returns plausible simulated hardware capabilities for a given backend.
212///
213/// Intended for testing and simulation — no real device detection occurs.
214#[must_use]
215pub fn simulate_hw_caps(backend: HwAccelBackend) -> HwAccelCaps {
216    match backend {
217        HwAccelBackend::None => HwAccelCaps {
218            backend,
219            can_decode: true,
220            can_encode: true,
221            max_resolution: (7680, 4320),
222            supported_codecs: vec![
223                "h264".to_string(),
224                "hevc".to_string(),
225                "vp9".to_string(),
226                "av1".to_string(),
227                "vp8".to_string(),
228                "opus".to_string(),
229                "flac".to_string(),
230            ],
231            max_sessions: 32,
232        },
233        HwAccelBackend::Vaapi => HwAccelCaps {
234            backend,
235            can_decode: true,
236            can_encode: true,
237            max_resolution: (4096, 4096),
238            supported_codecs: vec![
239                "h264".to_string(),
240                "hevc".to_string(),
241                "vp9".to_string(),
242                "av1".to_string(),
243                "vp8".to_string(),
244                "mjpeg".to_string(),
245            ],
246            max_sessions: 8,
247        },
248        HwAccelBackend::Nvenc => HwAccelCaps {
249            backend,
250            can_decode: true,
251            can_encode: true,
252            max_resolution: (7680, 4320),
253            supported_codecs: vec!["h264".to_string(), "hevc".to_string(), "av1".to_string()],
254            max_sessions: 3,
255        },
256        HwAccelBackend::Videotoolbox => HwAccelCaps {
257            backend,
258            can_decode: true,
259            can_encode: true,
260            max_resolution: (4096, 2160),
261            supported_codecs: vec!["h264".to_string(), "hevc".to_string()],
262            max_sessions: 4,
263        },
264        HwAccelBackend::Qsv => HwAccelCaps {
265            backend,
266            can_decode: true,
267            can_encode: true,
268            max_resolution: (8192, 8192),
269            supported_codecs: vec![
270                "h264".to_string(),
271                "hevc".to_string(),
272                "vp9".to_string(),
273                "av1".to_string(),
274                "mjpeg".to_string(),
275            ],
276            max_sessions: 6,
277        },
278        HwAccelBackend::Amf => HwAccelCaps {
279            backend,
280            can_decode: true,
281            can_encode: true,
282            max_resolution: (7680, 4320),
283            supported_codecs: vec!["h264".to_string(), "hevc".to_string(), "av1".to_string()],
284            max_sessions: 4,
285        },
286        HwAccelBackend::D3d11Va => HwAccelCaps {
287            backend,
288            can_decode: true,
289            can_encode: false, // D3D11VA is decode-only
290            max_resolution: (4096, 2160),
291            supported_codecs: vec!["h264".to_string(), "hevc".to_string()],
292            max_sessions: 2,
293        },
294    }
295}
296
297// ─── LatencyMode ─────────────────────────────────────────────────────────────
298
299/// Latency mode for the transcoding pipeline.
300#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
301pub enum LatencyMode {
302    /// Offline / batch processing — optimise for throughput and quality.
303    Batch,
304    /// Low-latency mode with a bounded output delay.
305    LowLatency {
306        /// Maximum acceptable end-to-end delay in milliseconds.
307        max_delay_ms: u32,
308    },
309    /// Hard real-time mode — frames must arrive at wall-clock rate.
310    Realtime,
311}
312
313impl LatencyMode {
314    /// Returns `true` for real-time or near-real-time modes.
315    #[must_use]
316    pub fn is_live(&self) -> bool {
317        matches!(self, Self::LowLatency { .. } | Self::Realtime)
318    }
319}
320
321// ─── PipelineConfig ───────────────────────────────────────────────────────────
322
323/// Pipeline-level hardware and threading configuration.
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct PipelineHwConfig {
326    /// Hardware acceleration settings.
327    pub hw_accel: HwAccelConfig,
328    /// Number of worker threads.
329    pub thread_count: u8,
330    /// Number of frame/packet buffers in flight.
331    pub buffer_count: u8,
332    /// Latency mode.
333    pub latency_mode: LatencyMode,
334}
335
336impl PipelineHwConfig {
337    /// Creates a sensible default configuration using software encoding.
338    #[must_use]
339    pub fn default_software() -> Self {
340        Self {
341            hw_accel: HwAccelConfig::software(),
342            thread_count: 4,
343            buffer_count: 8,
344            latency_mode: LatencyMode::Batch,
345        }
346    }
347
348    /// Creates a low-latency configuration with the given backend.
349    #[must_use]
350    pub fn low_latency(backend: HwAccelBackend, max_delay_ms: u32) -> Self {
351        Self {
352            hw_accel: HwAccelConfig::new(backend),
353            thread_count: 2,
354            buffer_count: 4,
355            latency_mode: LatencyMode::LowLatency { max_delay_ms },
356        }
357    }
358}
359
360// ─── HwAccelSelector ─────────────────────────────────────────────────────────
361
362/// Selects the best available hardware backend for a given task.
363#[derive(Debug, Clone, Default)]
364pub struct HwAccelSelector;
365
366impl HwAccelSelector {
367    /// Creates a new selector.
368    #[must_use]
369    pub fn new() -> Self {
370        Self
371    }
372
373    /// Selects the best backend from `available` that supports `codec` at
374    /// `resolution`.
375    ///
376    /// Selection criterion: highest `max_sessions` among qualifying backends
377    /// that can encode and support the requested codec and resolution.
378    ///
379    /// Returns `None` if no suitable backend is found.
380    #[must_use]
381    pub fn select_best(
382        available: &[HwAccelCaps],
383        codec: &str,
384        resolution: (u32, u32),
385    ) -> Option<HwAccelConfig> {
386        let (width, height) = resolution;
387        available
388            .iter()
389            .filter(|caps| {
390                caps.can_encode
391                    && caps.supports_codec(codec)
392                    && caps.supports_resolution(width, height)
393                    && caps.backend != HwAccelBackend::None
394            })
395            .max_by_key(|caps| caps.max_sessions)
396            .map(|caps| HwAccelConfig::new(caps.backend))
397    }
398
399    /// Selects the best backend or falls back to software.
400    #[must_use]
401    pub fn select_best_or_software(
402        available: &[HwAccelCaps],
403        codec: &str,
404        resolution: (u32, u32),
405    ) -> HwAccelConfig {
406        Self::select_best(available, codec, resolution).unwrap_or_else(HwAccelConfig::software)
407    }
408}
409
410// ─── Tests ────────────────────────────────────────────────────────────────────
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415
416    // ── HwAccelBackend ────────────────────────────────────────────────────────
417
418    #[test]
419    fn test_backend_as_str() {
420        assert_eq!(HwAccelBackend::None.as_str(), "none");
421        assert_eq!(HwAccelBackend::Vaapi.as_str(), "vaapi");
422        assert_eq!(HwAccelBackend::Nvenc.as_str(), "nvenc");
423        assert_eq!(HwAccelBackend::Videotoolbox.as_str(), "videotoolbox");
424        assert_eq!(HwAccelBackend::Qsv.as_str(), "qsv");
425        assert_eq!(HwAccelBackend::Amf.as_str(), "amf");
426        assert_eq!(HwAccelBackend::D3d11Va.as_str(), "d3d11va");
427    }
428
429    #[test]
430    fn test_all_hw_contains_six_backends() {
431        assert_eq!(HwAccelBackend::all_hw().len(), 6);
432        assert!(!HwAccelBackend::all_hw().contains(&HwAccelBackend::None));
433    }
434
435    // ── HwCodecMapping ────────────────────────────────────────────────────────
436
437    #[test]
438    fn test_vaapi_hevc_encoder_name() {
439        assert_eq!(
440            HwCodecMapping::get_encoder_name(&HwAccelBackend::Vaapi, "hevc"),
441            Some("hevc_vaapi")
442        );
443    }
444
445    #[test]
446    fn test_nvenc_hevc_encoder_name() {
447        assert_eq!(
448            HwCodecMapping::get_encoder_name(&HwAccelBackend::Nvenc, "hevc"),
449            Some("hevc_nvenc")
450        );
451    }
452
453    #[test]
454    fn test_videotoolbox_hevc_encoder_name() {
455        assert_eq!(
456            HwCodecMapping::get_encoder_name(&HwAccelBackend::Videotoolbox, "hevc"),
457            Some("hevc_videotoolbox")
458        );
459    }
460
461    #[test]
462    fn test_qsv_h264_encoder_name() {
463        assert_eq!(
464            HwCodecMapping::get_encoder_name(&HwAccelBackend::Qsv, "h264"),
465            Some("h264_qsv")
466        );
467    }
468
469    #[test]
470    fn test_amf_av1_encoder_name() {
471        assert_eq!(
472            HwCodecMapping::get_encoder_name(&HwAccelBackend::Amf, "av1"),
473            Some("av1_amf")
474        );
475    }
476
477    #[test]
478    fn test_none_backend_returns_none() {
479        assert_eq!(
480            HwCodecMapping::get_encoder_name(&HwAccelBackend::None, "h264"),
481            None
482        );
483    }
484
485    #[test]
486    fn test_unsupported_codec_returns_none() {
487        assert_eq!(
488            HwCodecMapping::get_encoder_name(&HwAccelBackend::Nvenc, "vp9"),
489            None
490        );
491    }
492
493    // ── simulate_hw_caps ──────────────────────────────────────────────────────
494
495    #[test]
496    fn test_simulate_vaapi_can_encode_and_decode() {
497        let caps = simulate_hw_caps(HwAccelBackend::Vaapi);
498        assert!(caps.can_encode);
499        assert!(caps.can_decode);
500    }
501
502    #[test]
503    fn test_simulate_d3d11va_is_decode_only() {
504        let caps = simulate_hw_caps(HwAccelBackend::D3d11Va);
505        assert!(!caps.can_encode);
506        assert!(caps.can_decode);
507    }
508
509    #[test]
510    fn test_simulate_nvenc_supports_hevc() {
511        let caps = simulate_hw_caps(HwAccelBackend::Nvenc);
512        assert!(caps.supports_codec("hevc"));
513    }
514
515    #[test]
516    fn test_simulate_nvenc_does_not_support_vp9() {
517        let caps = simulate_hw_caps(HwAccelBackend::Nvenc);
518        assert!(!caps.supports_codec("vp9"));
519    }
520
521    #[test]
522    fn test_simulate_resolution_check() {
523        let caps = simulate_hw_caps(HwAccelBackend::Videotoolbox);
524        assert!(caps.supports_resolution(1920, 1080));
525        assert!(!caps.supports_resolution(7680, 4320)); // exceeds max
526    }
527
528    #[test]
529    fn test_simulate_none_is_unlimited() {
530        let caps = simulate_hw_caps(HwAccelBackend::None);
531        assert!(caps.supports_resolution(7680, 4320));
532    }
533
534    // ── HwAccelConfig ─────────────────────────────────────────────────────────
535
536    #[test]
537    fn test_hw_accel_config_new() {
538        let cfg = HwAccelConfig::new(HwAccelBackend::Vaapi);
539        assert_eq!(cfg.backend, HwAccelBackend::Vaapi);
540        assert_eq!(cfg.device_index, 0);
541        assert!(cfg.fallback_to_software);
542    }
543
544    #[test]
545    fn test_hw_accel_config_no_fallback() {
546        let cfg = HwAccelConfig::new(HwAccelBackend::Nvenc).no_fallback();
547        assert!(!cfg.fallback_to_software);
548    }
549
550    #[test]
551    fn test_hw_accel_config_with_device() {
552        let cfg = HwAccelConfig::new(HwAccelBackend::Nvenc).with_device(2);
553        assert_eq!(cfg.device_index, 2);
554    }
555
556    // ── LatencyMode ───────────────────────────────────────────────────────────
557
558    #[test]
559    fn test_latency_mode_is_live() {
560        assert!(!LatencyMode::Batch.is_live());
561        assert!(LatencyMode::LowLatency { max_delay_ms: 100 }.is_live());
562        assert!(LatencyMode::Realtime.is_live());
563    }
564
565    #[test]
566    fn test_latency_mode_equality() {
567        assert_eq!(
568            LatencyMode::LowLatency { max_delay_ms: 200 },
569            LatencyMode::LowLatency { max_delay_ms: 200 }
570        );
571        assert_ne!(
572            LatencyMode::LowLatency { max_delay_ms: 100 },
573            LatencyMode::LowLatency { max_delay_ms: 200 }
574        );
575    }
576
577    // ── HwAccelSelector ───────────────────────────────────────────────────────
578
579    #[test]
580    fn test_selector_picks_highest_sessions() {
581        let caps = vec![
582            simulate_hw_caps(HwAccelBackend::Vaapi), // max_sessions = 8
583            simulate_hw_caps(HwAccelBackend::Nvenc), // max_sessions = 3
584            simulate_hw_caps(HwAccelBackend::Qsv),   // max_sessions = 6
585        ];
586        let cfg = HwAccelSelector::select_best(&caps, "h264", (1920, 1080));
587        assert!(cfg.is_some());
588        assert_eq!(
589            cfg.expect("should have config").backend,
590            HwAccelBackend::Vaapi
591        );
592    }
593
594    #[test]
595    fn test_selector_filters_unsupported_codec() {
596        // D3D11VA doesn't support encode, so only Videotoolbox fits for hevc
597        let caps = vec![
598            simulate_hw_caps(HwAccelBackend::D3d11Va),
599            simulate_hw_caps(HwAccelBackend::Videotoolbox),
600        ];
601        let cfg = HwAccelSelector::select_best(&caps, "hevc", (1920, 1080));
602        assert!(cfg.is_some());
603        assert_eq!(
604            cfg.expect("should have config").backend,
605            HwAccelBackend::Videotoolbox
606        );
607    }
608
609    #[test]
610    fn test_selector_returns_none_when_no_match() {
611        let caps = vec![simulate_hw_caps(HwAccelBackend::D3d11Va)]; // decode only
612        let cfg = HwAccelSelector::select_best(&caps, "h264", (1920, 1080));
613        assert!(cfg.is_none());
614    }
615
616    #[test]
617    fn test_selector_fallback_to_software() {
618        let caps: Vec<HwAccelCaps> = vec![];
619        let cfg = HwAccelSelector::select_best_or_software(&caps, "h264", (1920, 1080));
620        assert_eq!(cfg.backend, HwAccelBackend::None);
621    }
622
623    #[test]
624    fn test_pipeline_hw_config_default_software() {
625        let cfg = PipelineHwConfig::default_software();
626        assert_eq!(cfg.hw_accel.backend, HwAccelBackend::None);
627        assert_eq!(cfg.latency_mode, LatencyMode::Batch);
628    }
629
630    #[test]
631    fn test_pipeline_hw_config_low_latency() {
632        let cfg = PipelineHwConfig::low_latency(HwAccelBackend::Nvenc, 50);
633        assert!(cfg.latency_mode.is_live());
634        assert_eq!(cfg.hw_accel.backend, HwAccelBackend::Nvenc);
635    }
636}