Skip to main content

alpha2_native/hwencode/
mod.rs

1//! Hardware video encode from renderer-packed planes.
2//!
3//! The contract every GPU-resident export backend implements, opened on the renderer's device and
4//! split across two threads:
5//!
6//! - [`HardwareEncodeInput`] (coordinator): hands out encoder surfaces, names the plane textures
7//!   the renderer packs into, and moves those planes into the surface on the GPU.
8//! - [`HardwareEncodeSession`] (encode worker): submits surfaces to the encoder and muxes the
9//!   packets, with audio, into a transactional output file.
10//!
11//! A surface moves `acquire -> begin -> (render) -> submit -> encode` and is reused only after
12//! every GPU access and codec reference is released. The backend enforces this; dropping the host's
13//! last handle does not mean nothing still reads the image.
14// Platforms without a backend compile the contract with nothing behind it, so the host
15// loop is the same everywhere.
16#![cfg_attr(
17    not(any(target_os = "linux", target_os = "macos", target_os = "windows")),
18    allow(dead_code, unused_variables, clippy::needless_pass_by_value)
19)]
20
21#[cfg(target_os = "windows")]
22mod d3d12va;
23#[cfg(target_os = "macos")]
24mod videotoolbox;
25#[cfg(target_os = "linux")]
26mod vulkan;
27
28use std::sync::atomic::{AtomicU64, Ordering};
29
30use alpha2_media::{ColorMetadata, EncodeError, VideoRateControl};
31use alpha2_time::{FrameRate, MediaTime};
32
33#[cfg(all(test, target_os = "windows"))]
34pub(crate) use d3d12va::encode_no_rate_control_frames;
35#[cfg(target_os = "windows")]
36pub(crate) use d3d12va::probe_d3d12va_rate_control;
37#[cfg(target_os = "windows")]
38pub use d3d12va::{
39    D3D12EncodeProbe, D3D12RateControlOpen, D3D12RateControlRunner, D3D12TestEncodeRunner,
40    D3D12VA_TEST_ENCODE_SIZE, d3d12va_encoder, open_d3d12va_rate_control, probe_d3d12va_encode,
41    set_d3d12va_rate_control_runner, set_d3d12va_test_encode_runner,
42};
43#[cfg(target_os = "macos")]
44pub use videotoolbox::{VideoToolboxEncodeProbe, probe_videotoolbox_encode};
45#[cfg(target_os = "linux")]
46pub(crate) use vulkan::probe_vulkan_rate_control;
47#[cfg(target_os = "linux")]
48pub use vulkan::{VulkanEncodeProbe, probe_vulkan_encode};
49
50use crate::{EncodeStageTimings, NativeVideoCodec, NativeVideoEncoderSelection};
51
52/// The native API a hardware encode session drives.
53#[derive(Copy, Clone, Debug, Eq, PartialEq)]
54pub enum HardwareEncodeBackend {
55    /// `FFmpeg`'s Vulkan video encoders on the renderer's Vulkan device.
56    Vulkan,
57    /// `FFmpeg`'s `VideoToolbox` encoders reading `IOSurface`s the renderer's `Metal` device draws.
58    VideoToolbox,
59    /// `FFmpeg`'s D3D12 Video encoders on the renderer's D3D12 device.
60    D3D12Va,
61}
62
63impl HardwareEncodeBackend {
64    /// Stable identifier used by capability reports and export results.
65    #[must_use]
66    pub const fn as_str(self) -> &'static str {
67        match self {
68            Self::Vulkan => "vulkan",
69            Self::VideoToolbox => "videotoolbox",
70            Self::D3D12Va => "d3d12va",
71        }
72    }
73}
74
75/// How the pixels of an encoder-input surface are laid out.
76#[derive(Copy, Clone, Debug, Eq, PartialEq)]
77pub enum EncoderInputLayout {
78    /// 8-bit 4:2:0: a full-size luma plane and a half-size interleaved chroma plane.
79    Nv12,
80}
81
82impl EncoderInputLayout {
83    /// Stable identifier used by capability reports.
84    #[must_use]
85    pub const fn as_str(self) -> &'static str {
86        match self {
87            Self::Nv12 => "nv12",
88        }
89    }
90
91    /// Bytes one visible frame of this layout holds, without padding.
92    #[must_use]
93    pub const fn frame_bytes(self, width: u32, height: u32) -> u64 {
94        match self {
95            Self::Nv12 => {
96                let luma = width as u64 * height as u64;
97                luma + luma / 2
98            }
99        }
100    }
101}
102
103/// How a packed frame reaches the encoder's surface.
104#[derive(Copy, Clone, Debug, Eq, PartialEq)]
105pub enum EncoderInputHandoff {
106    /// The renderer packs into its own plane textures and a GPU copy moves them into the surface.
107    GpuCopy,
108    /// The renderer packs straight into the surface's planes, with no ring and no copy.
109    DirectTarget,
110}
111
112impl EncoderInputHandoff {
113    /// Stable identifier used by capability reports.
114    #[must_use]
115    pub const fn as_str(self) -> &'static str {
116        match self {
117            Self::GpuCopy => "gpu-copy",
118            Self::DirectTarget => "direct-target",
119        }
120    }
121}
122
123/// What one hardware encoder accepts on this device, read from the device.
124#[derive(Clone, Debug, Eq, PartialEq)]
125pub struct HardwareEncodeCapability {
126    /// The native API the session drives.
127    pub backend: HardwareEncodeBackend,
128    /// The `FFmpeg` encoder a session opens.
129    pub encoder: &'static str,
130    /// The codec it encodes.
131    pub codec: NativeVideoCodec,
132    /// Whether the encoder takes GPU frames only. Pinning such an encoder selects this path; one
133    /// the readback path can also feed (`h264_videotoolbox`) can be pinned on either.
134    pub gpu_input_only: bool,
135    /// The surface layout it takes.
136    pub input_layout: EncoderInputLayout,
137    /// How a packed frame reaches its surface.
138    pub handoff: EncoderInputHandoff,
139    /// Whether the encoder runs on the renderer's own device. Backends needing a cross-device copy
140    /// are not offered as GPU-resident.
141    pub shares_renderer_device: bool,
142    /// Smallest accepted coded width and height.
143    pub minimum: (u32, u32),
144    /// Largest accepted coded width and height.
145    pub maximum: (u32, u32),
146    /// Granularity the output size is aligned up to before the limits apply.
147    pub granularity: (u32, u32),
148    /// References a P picture may use, when the device reports it.
149    pub max_p_picture_l0_references: Option<u32>,
150    /// Forward references a B picture may use; zero with `max_l1_references` zero means no B
151    /// pictures. `None` when not reported.
152    pub max_b_picture_l0_references: Option<u32>,
153    /// Backward references a B picture may use, when the device reports it.
154    pub max_l1_references: Option<u32>,
155    /// The rate-control modes the device reports: `cbr`, `vbr`, `disabled` (constant
156    /// quantizer), and on D3D12 `qvbr`.
157    pub rate_control_modes: Vec<&'static str>,
158    /// Plane-texture pairs the renderer packs into, allocated for the whole session. Zero when it
159    /// packs into the surfaces themselves.
160    pub plane_ring_slots: usize,
161    /// Surfaces the handoff may still hold after the host released them (a copying backend keeps
162    /// each until its slot comes round). Allocated on top of the host's, so budgets count them.
163    pub handoff_retained_surfaces: usize,
164    /// Surfaces the encoder holds after accepting them, until consumed. Also on top of the host's.
165    pub codec_retained_surfaces: usize,
166    /// The default encode channel depth (`ALPHA2_ENCODE_QUEUE_DEPTH` overrides): the smallest depth
167    /// within 3 % of the best measured per-frame time on this backend.
168    pub default_channel_depth: usize,
169}
170
171impl HardwareEncodeCapability {
172    /// Every surface the pool may hold beyond those the host has leased.
173    #[must_use]
174    pub const fn retained_surfaces(&self) -> usize {
175        self.handoff_retained_surfaces
176            .saturating_add(self.codec_retained_surfaces)
177    }
178}
179
180impl HardwareEncodeCapability {
181    /// The coded extent an output of this size is encoded at.
182    #[must_use]
183    pub const fn coded(&self, width: u32, height: u32) -> (u32, u32) {
184        (
185            align_up(width, self.granularity.0),
186            align_up(height, self.granularity.1),
187        )
188    }
189
190    /// Whether an output of this size can be encoded, and if not, why.
191    ///
192    /// # Errors
193    /// Names the limit the size misses.
194    pub fn accepts(&self, width: u32, height: u32) -> Result<(), String> {
195        if !width.is_multiple_of(2) || !height.is_multiple_of(2) {
196            return Err(format!(
197                "a {width}x{height} output has no 4:2:0 chroma plane"
198            ));
199        }
200        let coded = self.coded(width, height);
201        if coded.0 < self.minimum.0
202            || coded.1 < self.minimum.1
203            || coded.0 > self.maximum.0
204            || coded.1 > self.maximum.1
205        {
206            return Err(format!(
207                "a {width}x{height} output codes as {}x{}, outside {}'s {}x{}..{}x{}",
208                coded.0,
209                coded.1,
210                self.encoder,
211                self.minimum.0,
212                self.minimum.1,
213                self.maximum.0,
214                self.maximum.1
215            ));
216        }
217        Ok(())
218    }
219}
220
221const fn align_up(value: u32, granularity: u32) -> u32 {
222    if granularity <= 1 {
223        return value;
224    }
225    value.div_ceil(granularity).saturating_mul(granularity)
226}
227
228/// The renderer's device, as a hardware encoder can use it.
229#[derive(Clone)]
230pub struct HardwareEncodeDevice {
231    #[cfg(target_os = "linux")]
232    vulkan: alpha2_native_gpu::SharedVulkanContext,
233    #[cfg(target_os = "macos")]
234    metal: alpha2_native_gpu::SharedMetalContext,
235    #[cfg(target_os = "windows")]
236    d3d12: alpha2_native_gpu::SharedD3D12Context,
237}
238
239impl std::fmt::Debug for HardwareEncodeDevice {
240    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        formatter
242            .debug_struct("HardwareEncodeDevice")
243            .finish_non_exhaustive()
244    }
245}
246
247/// One export's encoder settings and output.
248#[derive(Clone, Debug)]
249pub struct HardwareEncodeRequest<'a> {
250    /// Output width, which must be even.
251    pub width: u32,
252    /// Output height, which must be even.
253    pub height: u32,
254    /// The codec to encode, one [`HardwareEncodeDevice::capability`] accepts.
255    pub codec: NativeVideoCodec,
256    /// The constant frame rate the stream is timed in.
257    pub frame_rate: FrameRate,
258    /// The resolved rate control.
259    pub rate_control: VideoRateControl,
260    /// The color the pack encodes and the stream is tagged with.
261    pub color: ColorMetadata,
262    /// Where the file goes and how an existing file is handled; a partial file sits
263    /// beside it until it finishes.
264    pub output: &'a crate::OutputTarget,
265    /// The file format, which must offer `codec` and the audio codec.
266    pub container: alpha2_media::Container,
267    /// The audio stream muxed beside the video, if any.
268    pub audio: Option<crate::AudioStreamConfig>,
269}
270
271/// A session's frame rate as `FFmpeg` takes it. Terms past `i32` are refused, as in the readback
272/// sinks, so the export falls back and fails there for the same reason.
273pub(crate) fn session_frame_rate(rate: FrameRate) -> Result<(i32, i32), EncodeError> {
274    match (
275        i32::try_from(rate.numerator()),
276        i32::try_from(rate.denominator()),
277    ) {
278        (Ok(numerator), Ok(denominator)) => Ok((numerator, denominator)),
279        _ => Err(EncodeError::Adapter(format!(
280            "frame rate {}/{} is out of range",
281            rate.numerator(),
282            rate.denominator()
283        ))),
284    }
285}
286
287/// A refusal of `encoder` to open, read from `error` and the lines it logged since `mark`; busy
288/// when its status says no session is free.
289#[cfg(any(target_os = "linux", target_os = "macos"))]
290fn open_refusal(
291    encoder: &str,
292    mark: alpha2_ffmpeg_bridge::LogMark,
293    error: &EncodeError,
294) -> HardwareEncodeOpenError {
295    let lines = alpha2_ffmpeg_bridge::log_since(mark, &[encoder]);
296    let error = error.to_string();
297    let reason = format!("{error}{}", alpha2_ffmpeg_bridge::log_detail(&lines));
298    match crate::capabilities::classify_open_failure(encoder, &error, &lines) {
299        alpha2_media::OpenFailureKind::Busy => HardwareEncodeOpenError::Busy(reason),
300        _ => HardwareEncodeOpenError::Refused(reason),
301    }
302}
303
304/// Why a hardware session did not open.
305#[derive(Debug)]
306pub enum HardwareEncodeOpenError {
307    /// The encoder refused before anything was written; another path can still take the export.
308    Refused(String),
309    /// The device has no session free (another export holds them), before anything was written.
310    /// Another path can still take the export, or this one once the other export ends.
311    Busy(String),
312    /// The output was being created when this failed; the export has failed.
313    Failed(EncodeError),
314}
315
316impl std::fmt::Display for HardwareEncodeOpenError {
317    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318        match self {
319            Self::Refused(reason) | Self::Busy(reason) => formatter.write_str(reason),
320            Self::Failed(error) => write!(formatter, "{error}"),
321        }
322    }
323}
324
325impl HardwareEncodeDevice {
326    /// The shared Vulkan device the renderer runs on.
327    #[cfg(target_os = "linux")]
328    #[must_use]
329    pub const fn vulkan(context: alpha2_native_gpu::SharedVulkanContext) -> Self {
330        Self { vulkan: context }
331    }
332
333    /// The `Metal` device the renderer runs on.
334    #[cfg(target_os = "macos")]
335    #[must_use]
336    pub const fn metal(context: alpha2_native_gpu::SharedMetalContext) -> Self {
337        Self { metal: context }
338    }
339
340    /// The shared D3D12 device the renderer runs on.
341    #[cfg(target_os = "windows")]
342    #[must_use]
343    pub const fn d3d12(context: alpha2_native_gpu::SharedD3D12Context) -> Self {
344        Self { d3d12: context }
345    }
346
347    /// What this device's hardware encoder accepts for `codec`.
348    ///
349    /// Windows and Linux ask the device per codec; macOS has one hardware encoder, for H.264.
350    ///
351    /// # Errors
352    /// Says why this device has no usable hardware encoder for `codec`.
353    #[cfg_attr(
354        not(any(target_os = "linux", target_os = "macos", target_os = "windows")),
355        allow(clippy::unused_self)
356    )]
357    pub fn capability(&self, codec: NativeVideoCodec) -> Result<HardwareEncodeCapability, String> {
358        #[cfg(target_os = "windows")]
359        return d3d12va::capability(&self.d3d12, codec);
360        #[cfg(target_os = "linux")]
361        return vulkan::capability(&self.vulkan, codec);
362        #[cfg(target_os = "macos")]
363        {
364            let capability = self.preferred_capability()?;
365            if capability.codec == codec {
366                Ok(capability)
367            } else {
368                Err(format!(
369                    "{} encodes {}, not {}",
370                    capability.encoder,
371                    capability.codec.as_str(),
372                    codec.as_str()
373                ))
374            }
375        }
376        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
377        Err("this platform has no hardware encode backend yet".to_owned())
378    }
379
380    /// The capability a report leads with: the first of H.264, HEVC and AV1 the device encodes
381    /// (Windows, Linux), or the one encoder on macOS.
382    ///
383    /// # Errors
384    /// Says why this device has no usable hardware encoder at all.
385    #[cfg_attr(
386        not(any(target_os = "linux", target_os = "macos", target_os = "windows")),
387        allow(clippy::unused_self)
388    )]
389    pub fn preferred_capability(&self) -> Result<HardwareEncodeCapability, String> {
390        #[cfg(target_os = "linux")]
391        return vulkan::preferred_capability(&self.vulkan);
392        #[cfg(target_os = "macos")]
393        return videotoolbox::capability(&self.metal);
394        #[cfg(target_os = "windows")]
395        return d3d12va::preferred_capability(&self.d3d12);
396        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
397        Err("this platform has no hardware encode backend yet".to_owned())
398    }
399
400    /// Opens a session writing `request.output` and its input half.
401    ///
402    /// # Errors
403    /// [`HardwareEncodeOpenError::Refused`] before any output exists,
404    /// [`HardwareEncodeOpenError::Failed`] once creating it went wrong.
405    #[cfg_attr(
406        not(any(target_os = "linux", target_os = "macos", target_os = "windows")),
407        allow(clippy::unused_self)
408    )]
409    pub fn open(
410        &self,
411        request: &HardwareEncodeRequest<'_>,
412    ) -> Result<(HardwareEncodeSession, HardwareEncodeInput), HardwareEncodeOpenError> {
413        let generation = NEXT_POOL_GENERATION.fetch_add(1, Ordering::Relaxed);
414        // The D3D12VA backend checks the codec it is asked for itself.
415        #[cfg(any(target_os = "linux", target_os = "macos"))]
416        self.capability(request.codec)
417            .map_err(HardwareEncodeOpenError::Refused)?;
418        #[cfg(target_os = "linux")]
419        {
420            let (session, input) = vulkan::open(&self.vulkan, request, generation)?;
421            Ok((
422                HardwareEncodeSession(SessionBackend::Vulkan(session)),
423                HardwareEncodeInput {
424                    backend: InputBackend::Vulkan(input),
425                    generation,
426                },
427            ))
428        }
429        #[cfg(target_os = "macos")]
430        {
431            let (session, input) = videotoolbox::open(&self.metal, request, generation)?;
432            Ok((
433                HardwareEncodeSession(SessionBackend::VideoToolbox(session)),
434                HardwareEncodeInput {
435                    backend: InputBackend::VideoToolbox(input),
436                    generation,
437                },
438            ))
439        }
440        #[cfg(target_os = "windows")]
441        {
442            let (session, input) = d3d12va::open(&self.d3d12, request, generation)?;
443            Ok((
444                HardwareEncodeSession(SessionBackend::D3D12(session)),
445                HardwareEncodeInput {
446                    backend: InputBackend::D3D12(input),
447                    generation,
448                },
449            ))
450        }
451        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
452        {
453            let _ = (request, generation);
454            Err(HardwareEncodeOpenError::Refused(
455                "this platform has no hardware encode backend yet".to_owned(),
456            ))
457        }
458    }
459}
460
461/// Tags each pool so an old surface (after a cancel, resize or device loss) can
462/// never reach a new session.
463static NEXT_POOL_GENERATION: AtomicU64 = AtomicU64::new(1);
464
465/// What one encoder-input surface is, independent of its backing.
466#[derive(Copy, Clone, Debug, Eq, PartialEq)]
467pub struct EncoderSurfaceDescriptor {
468    /// The backend whose pool allocated it.
469    pub backend: HardwareEncodeBackend,
470    /// The session's pool; see [`HardwareEncodeInput::acquire_surface`].
471    pub pool_generation: u64,
472    /// Its pixel layout.
473    pub layout: EncoderInputLayout,
474    /// The picture's size.
475    pub visible: (u32, u32),
476    /// The color its pixels are encoded in.
477    pub color: ColorMetadata,
478}
479
480/// One encoder-input surface on loan from its session's pool.
481///
482/// Dropping it returns the backend's reference; the backend still decides
483/// when the backing is reusable.
484pub struct EncoderSurface {
485    descriptor: EncoderSurfaceDescriptor,
486    backing: SurfaceBacking,
487}
488
489enum SurfaceBacking {
490    #[cfg(target_os = "linux")]
491    Vulkan(vulkan::VulkanEncoderFrame),
492    #[cfg(target_os = "macos")]
493    VideoToolbox(videotoolbox::VideoToolboxEncoderFrame),
494    #[cfg(target_os = "windows")]
495    D3D12(d3d12va::D3D12EncoderFrame),
496    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
497    Unsupported(std::convert::Infallible),
498}
499
500impl EncoderSurface {
501    /// What this surface is.
502    #[must_use]
503    pub const fn descriptor(&self) -> &EncoderSurfaceDescriptor {
504        &self.descriptor
505    }
506}
507
508/// A begun frame whose planes belong to the renderer until [`HardwareEncodeInput::submit`].
509#[must_use = "a begun frame must be submitted, or its planes stay claimed"]
510pub struct InputFrame {
511    /// The plane pair of the copier's ring the frame is packed into.
512    #[cfg(any(target_os = "linux", target_os = "windows"))]
513    slot: usize,
514    /// The surface whose own planes the frame is packed into.
515    #[cfg(target_os = "macos")]
516    pixel_buffer: usize,
517}
518
519/// Surface-pool counters for one session.
520#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
521pub struct EncoderPoolStats {
522    /// Surfaces handed out.
523    pub acquired: u64,
524    /// Distinct backings allocated. Beyond the host's frames, this counts what the encoder and GPU
525    /// copy still referenced when the next surface was requested.
526    pub allocated: u64,
527    /// Handoffs into a surface: a plane copy each on a copying backend.
528    pub copy_operations: u64,
529    /// The bytes those handoffs moved.
530    pub copy_bytes: u64,
531}
532
533/// The coordinator's half of a hardware session.
534pub struct HardwareEncodeInput {
535    backend: InputBackend,
536    generation: u64,
537}
538
539enum InputBackend {
540    #[cfg(target_os = "linux")]
541    Vulkan(vulkan::VulkanEncodeInput),
542    #[cfg(target_os = "macos")]
543    VideoToolbox(videotoolbox::VideoToolboxEncodeInput),
544    #[cfg(target_os = "windows")]
545    D3D12(d3d12va::D3D12EncodeInput),
546    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
547    Unsupported(std::convert::Infallible),
548}
549
550impl HardwareEncodeInput {
551    /// Takes one surface from the session's pool.
552    ///
553    /// The pool allocates on demand, so its size is the peak outstanding; the caller bounds that.
554    ///
555    /// # Errors
556    /// Returns an adapter failure when the pool cannot allocate.
557    pub fn acquire_surface(&mut self) -> Result<EncoderSurface, EncodeError> {
558        match &mut self.backend {
559            #[cfg(target_os = "linux")]
560            InputBackend::Vulkan(input) => {
561                let frame = input.acquire_frame()?;
562                Ok(EncoderSurface {
563                    descriptor: input.descriptor(self.generation),
564                    backing: SurfaceBacking::Vulkan(frame),
565                })
566            }
567            #[cfg(target_os = "macos")]
568            InputBackend::VideoToolbox(input) => {
569                let frame = input.acquire_frame()?;
570                Ok(EncoderSurface {
571                    descriptor: input.descriptor(self.generation),
572                    backing: SurfaceBacking::VideoToolbox(frame),
573                })
574            }
575            #[cfg(target_os = "windows")]
576            InputBackend::D3D12(input) => {
577                let frame = input.acquire_frame()?;
578                Ok(EncoderSurface {
579                    descriptor: input.descriptor(self.generation),
580                    backing: SurfaceBacking::D3D12(frame),
581                })
582            }
583            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
584            InputBackend::Unsupported(never) => match *never {},
585        }
586    }
587
588    /// Begins a frame bound for `surface`, waiting until its planes are free. Call
589    /// it before recording the pack render into [`Self::planes`], and submit that
590    /// render before [`Self::submit`].
591    ///
592    /// # Errors
593    /// Returns an error for a surface from another pool, or when the wait fails.
594    pub fn begin_frame(&mut self, surface: &EncoderSurface) -> Result<InputFrame, EncodeError> {
595        self.check(surface)?;
596        match &mut self.backend {
597            #[cfg(target_os = "linux")]
598            InputBackend::Vulkan(input) => input.begin_frame(),
599            #[cfg(target_os = "macos")]
600            InputBackend::VideoToolbox(input) => match &surface.backing {
601                SurfaceBacking::VideoToolbox(backing) => input.begin_frame(backing),
602            },
603            #[cfg(target_os = "windows")]
604            InputBackend::D3D12(input) => input.begin_frame(),
605            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
606            InputBackend::Unsupported(never) => match *never {},
607        }
608    }
609
610    /// The luma and interleaved chroma textures `frame` is packed into.
611    #[must_use]
612    pub fn planes(&self, frame: &InputFrame) -> (&wgpu::Texture, &wgpu::Texture) {
613        match &self.backend {
614            #[cfg(target_os = "linux")]
615            InputBackend::Vulkan(input) => input.planes(frame),
616            #[cfg(target_os = "macos")]
617            InputBackend::VideoToolbox(input) => input.planes(frame),
618            #[cfg(target_os = "windows")]
619            InputBackend::D3D12(input) => input.planes(frame),
620            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
621            InputBackend::Unsupported(never) => match *never {},
622        }
623    }
624
625    /// Moves `frame`'s packed planes into `surface` on the GPU; the
626    /// encoder waits for it on the GPU.
627    ///
628    /// # Errors
629    /// Returns an error for a surface from another pool, or when the handoff cannot be submitted.
630    pub fn submit(
631        &mut self,
632        frame: InputFrame,
633        surface: &mut EncoderSurface,
634    ) -> Result<(), EncodeError> {
635        self.check(surface)?;
636        match (&mut self.backend, &mut surface.backing) {
637            #[cfg(target_os = "linux")]
638            (InputBackend::Vulkan(input), SurfaceBacking::Vulkan(backing)) => {
639                input.submit(frame, backing)
640            }
641            #[cfg(target_os = "macos")]
642            (InputBackend::VideoToolbox(input), SurfaceBacking::VideoToolbox(backing)) => {
643                input.submit(frame, backing)
644            }
645            #[cfg(target_os = "windows")]
646            (InputBackend::D3D12(input), SurfaceBacking::D3D12(backing)) => {
647                input.submit(frame, backing)
648            }
649            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
650            (InputBackend::Unsupported(never), _) => match *never {},
651        }
652    }
653
654    /// Stamps `surface` with its presentation time.
655    pub fn set_time(&self, surface: &mut EncoderSurface, time: MediaTime) {
656        match (&self.backend, &mut surface.backing) {
657            #[cfg(target_os = "linux")]
658            (InputBackend::Vulkan(input), SurfaceBacking::Vulkan(backing)) => {
659                backing.set_pts(input.timestamp(time));
660            }
661            #[cfg(target_os = "macos")]
662            (InputBackend::VideoToolbox(input), SurfaceBacking::VideoToolbox(backing)) => {
663                backing.set_pts(input.timestamp(time));
664            }
665            #[cfg(target_os = "windows")]
666            (InputBackend::D3D12(input), SurfaceBacking::D3D12(backing)) => {
667                backing.set_pts(input.timestamp(time));
668            }
669            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
670            (InputBackend::Unsupported(never), _) => match *never {},
671        }
672    }
673
674    /// This session's pool counters so far.
675    #[must_use]
676    pub fn stats(&self) -> EncoderPoolStats {
677        match &self.backend {
678            #[cfg(target_os = "linux")]
679            InputBackend::Vulkan(input) => input.stats(),
680            #[cfg(target_os = "macos")]
681            InputBackend::VideoToolbox(input) => input.stats(),
682            #[cfg(target_os = "windows")]
683            InputBackend::D3D12(input) => input.stats(),
684            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
685            InputBackend::Unsupported(never) => match *never {},
686        }
687    }
688
689    /// Waits for every in-flight handoff and releases the input resources.
690    ///
691    /// # Errors
692    /// On timeout or device loss, resources the GPU may still use are kept rather than freed.
693    pub fn finish(self) -> Result<(), EncodeError> {
694        match self.backend {
695            #[cfg(target_os = "linux")]
696            InputBackend::Vulkan(input) => input.finish(),
697            #[cfg(target_os = "macos")]
698            InputBackend::VideoToolbox(input) => input.finish(),
699            #[cfg(target_os = "windows")]
700            InputBackend::D3D12(input) => input.finish(),
701            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
702            InputBackend::Unsupported(never) => match never {},
703        }
704    }
705
706    fn check(&self, surface: &EncoderSurface) -> Result<(), EncodeError> {
707        if surface.descriptor.pool_generation == self.generation {
708            Ok(())
709        } else {
710            Err(EncodeError::Adapter(format!(
711                "encoder surface from pool {} reached pool {}",
712                surface.descriptor.pool_generation, self.generation
713            )))
714        }
715    }
716}
717
718/// The encode worker's half of a hardware session.
719pub struct HardwareEncodeSession(SessionBackend);
720
721enum SessionBackend {
722    #[cfg(target_os = "linux")]
723    Vulkan(vulkan::VulkanEncodeSession),
724    #[cfg(target_os = "macos")]
725    VideoToolbox(videotoolbox::VideoToolboxEncodeSession),
726    #[cfg(target_os = "windows")]
727    D3D12(d3d12va::D3D12EncodeSession),
728    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
729    Unsupported(std::convert::Infallible),
730}
731
732impl HardwareEncodeSession {
733    /// The encoder this session opened, reported like every other export path.
734    #[must_use]
735    pub fn selection(&self) -> &NativeVideoEncoderSelection {
736        match &self.0 {
737            #[cfg(target_os = "linux")]
738            SessionBackend::Vulkan(session) => session.selection(),
739            #[cfg(target_os = "macos")]
740            SessionBackend::VideoToolbox(session) => session.selection(),
741            #[cfg(target_os = "windows")]
742            SessionBackend::D3D12(session) => session.selection(),
743            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
744            SessionBackend::Unsupported(never) => match *never {},
745        }
746    }
747
748    /// Where this session's time has gone, split like the software sinks.
749    #[must_use]
750    pub fn stage_timings(&self) -> EncodeStageTimings {
751        match &self.0 {
752            #[cfg(target_os = "linux")]
753            SessionBackend::Vulkan(session) => session.stage_timings(),
754            #[cfg(target_os = "macos")]
755            SessionBackend::VideoToolbox(session) => session.stage_timings(),
756            #[cfg(target_os = "windows")]
757            SessionBackend::D3D12(session) => session.stage_timings(),
758            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
759            SessionBackend::Unsupported(never) => match *never {},
760        }
761    }
762
763    /// Submits `surface` and muxes whatever packets it produced.
764    ///
765    /// # Errors
766    /// Returns an adapter failure when the encoder rejects the frame or packets cannot be written.
767    pub fn encode(&mut self, surface: &EncoderSurface) -> Result<(), EncodeError> {
768        match (&mut self.0, &surface.backing) {
769            #[cfg(target_os = "linux")]
770            (SessionBackend::Vulkan(session), SurfaceBacking::Vulkan(frame)) => {
771                session.encode(surface.descriptor.pool_generation, frame)
772            }
773            #[cfg(target_os = "macos")]
774            (SessionBackend::VideoToolbox(session), SurfaceBacking::VideoToolbox(frame)) => {
775                session.encode(surface.descriptor.pool_generation, frame)
776            }
777            #[cfg(target_os = "windows")]
778            (SessionBackend::D3D12(session), SurfaceBacking::D3D12(frame)) => {
779                session.encode(surface.descriptor.pool_generation, frame)
780            }
781            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
782            (SessionBackend::Unsupported(never), _) => match *never {},
783        }
784    }
785
786    /// Buffers one audio block for the audio stream.
787    ///
788    /// # Errors
789    /// Returns an adapter failure for a format change, a non-monotonic block, or a failed encode.
790    pub fn write_audio(&mut self, block: &alpha2_media::AudioBlock) -> Result<(), EncodeError> {
791        match &mut self.0 {
792            #[cfg(target_os = "linux")]
793            SessionBackend::Vulkan(session) => session.write_audio(block),
794            #[cfg(target_os = "macos")]
795            SessionBackend::VideoToolbox(session) => session.write_audio(block),
796            #[cfg(target_os = "windows")]
797            SessionBackend::D3D12(session) => session.write_audio(block),
798            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
799            SessionBackend::Unsupported(never) => match *never {},
800        }
801    }
802
803    /// Flushes video and audio, writes the trailer, and publishes the output.
804    ///
805    /// # Errors
806    /// Returns an adapter failure from the flush or trailer; [`EncodeError::Publish`] when the
807    /// finished file was kept beside its target; [`EncodeError::Cancelled`] when `cancel`
808    /// stopped the publish retries.
809    pub fn finish(&mut self, cancel: &alpha2_media::CancelToken) -> Result<(), EncodeError> {
810        match &mut self.0 {
811            #[cfg(target_os = "linux")]
812            SessionBackend::Vulkan(session) => session.finish(cancel),
813            #[cfg(target_os = "macos")]
814            SessionBackend::VideoToolbox(session) => session.finish(cancel),
815            #[cfg(target_os = "windows")]
816            SessionBackend::D3D12(session) => session.finish(cancel),
817            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
818            SessionBackend::Unsupported(never) => match *never {},
819        }
820    }
821
822    /// How the finished file was published; `None` until [`Self::finish`] has published it.
823    #[must_use]
824    pub fn publish_report(&self) -> Option<crate::PublishReport> {
825        match &self.0 {
826            #[cfg(target_os = "linux")]
827            SessionBackend::Vulkan(session) => session.publish_report(),
828            #[cfg(target_os = "macos")]
829            SessionBackend::VideoToolbox(session) => session.publish_report(),
830            #[cfg(target_os = "windows")]
831            SessionBackend::D3D12(session) => session.publish_report(),
832            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
833            SessionBackend::Unsupported(never) => match *never {},
834        }
835    }
836
837    /// Discards the partial output.
838    pub fn abort(&mut self) {
839        match &mut self.0 {
840            #[cfg(target_os = "linux")]
841            SessionBackend::Vulkan(session) => session.abort(),
842            #[cfg(target_os = "macos")]
843            SessionBackend::VideoToolbox(session) => session.abort(),
844            #[cfg(target_os = "windows")]
845            SessionBackend::D3D12(session) => session.abort(),
846            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
847            SessionBackend::Unsupported(never) => match *never {},
848        }
849    }
850}