Skip to main content

alpha2_native/hwencode/
mod.rs

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