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 audio stream, into a
13//!   transactional output file.
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 file goes; a partial file sits beside it until it finishes.
277    pub output: &'a Path,
278    /// The file format written there, which must offer `codec` and the audio
279    /// codec.
280    pub container: alpha2_media::Container,
281    /// The audio stream muxed beside the video, if any.
282    pub audio: Option<crate::AudioStreamConfig>,
283}
284
285/// A session's frame rate as `FFmpeg` takes it, numerator and denominator.
286/// Either one past `i32` is refused, as the readback sinks refuse it, so the
287/// export falls back to them and is refused there for the same reason
288/// instead of the session quietly running at another rate.
289pub(crate) fn session_frame_rate(rate: FrameRate) -> Result<(i32, i32), EncodeError> {
290    match (
291        i32::try_from(rate.numerator()),
292        i32::try_from(rate.denominator()),
293    ) {
294        (Ok(numerator), Ok(denominator)) => Ok((numerator, denominator)),
295        _ => Err(EncodeError::Adapter(format!(
296            "frame rate {}/{} is out of range",
297            rate.numerator(),
298            rate.denominator()
299        ))),
300    }
301}
302
303/// Why a hardware session did not open.
304#[derive(Debug)]
305pub enum HardwareEncodeOpenError {
306    /// The encoder refused before anything was written: another path can
307    /// still take the export.
308    Refused(String),
309    /// The output was being created when this failed; the export has failed.
310    Failed(EncodeError),
311}
312
313impl std::fmt::Display for HardwareEncodeOpenError {
314    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315        match self {
316            Self::Refused(reason) => formatter.write_str(reason),
317            Self::Failed(error) => write!(formatter, "{error}"),
318        }
319    }
320}
321
322impl HardwareEncodeDevice {
323    /// The shared Vulkan device the renderer runs on.
324    #[cfg(target_os = "linux")]
325    #[must_use]
326    pub const fn vulkan(context: alpha2_native_gpu::SharedVulkanContext) -> Self {
327        Self { vulkan: context }
328    }
329
330    /// The `Metal` device the renderer runs on.
331    #[cfg(target_os = "macos")]
332    #[must_use]
333    pub const fn metal(context: alpha2_native_gpu::SharedMetalContext) -> Self {
334        Self { metal: context }
335    }
336
337    /// The shared D3D12 device the renderer runs on.
338    #[cfg(target_os = "windows")]
339    #[must_use]
340    pub const fn d3d12(context: alpha2_native_gpu::SharedD3D12Context) -> Self {
341        Self { d3d12: context }
342    }
343
344    /// What this device's hardware encoder accepts for `codec`.
345    ///
346    /// A device can encode one codec and not another, so each export asks
347    /// about its own. Linux and macOS have one hardware encoder, for H.264.
348    ///
349    /// # Errors
350    /// Says why this device has no usable hardware encoder for `codec`.
351    #[cfg_attr(
352        not(any(target_os = "linux", target_os = "macos", target_os = "windows")),
353        allow(clippy::unused_self)
354    )]
355    pub fn capability(&self, codec: NativeVideoCodec) -> Result<HardwareEncodeCapability, String> {
356        #[cfg(target_os = "windows")]
357        return d3d12va::capability(&self.d3d12, codec);
358        #[cfg(any(target_os = "linux", target_os = "macos"))]
359        {
360            let capability = self.preferred_capability()?;
361            if capability.codec == codec {
362                Ok(capability)
363            } else {
364                Err(format!(
365                    "{} encodes {}, not {}",
366                    capability.encoder,
367                    capability.codec.as_str(),
368                    codec.as_str()
369                ))
370            }
371        }
372        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
373        Err("this platform has no hardware encode backend yet".to_owned())
374    }
375
376    /// The capability a report leads with: on Windows the first of H.264,
377    /// HEVC and AV1 this device encodes, elsewhere the one encoder there is.
378    ///
379    /// # Errors
380    /// Says why this device has no usable hardware encoder at all.
381    #[cfg_attr(
382        not(any(target_os = "linux", target_os = "macos", target_os = "windows")),
383        allow(clippy::unused_self)
384    )]
385    pub fn preferred_capability(&self) -> Result<HardwareEncodeCapability, String> {
386        #[cfg(target_os = "linux")]
387        return vulkan::capability(&self.vulkan);
388        #[cfg(target_os = "macos")]
389        return videotoolbox::capability(&self.metal);
390        #[cfg(target_os = "windows")]
391        return d3d12va::preferred_capability(&self.d3d12);
392        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
393        Err("this platform has no hardware encode backend yet".to_owned())
394    }
395
396    /// Opens a session writing `request.output` and its input half.
397    ///
398    /// Everything that can refuse does so before the output file exists.
399    ///
400    /// # Errors
401    /// [`HardwareEncodeOpenError::Refused`] before any output exists,
402    /// [`HardwareEncodeOpenError::Failed`] once creating it went wrong.
403    #[cfg_attr(
404        not(any(target_os = "linux", target_os = "macos", target_os = "windows")),
405        allow(clippy::unused_self)
406    )]
407    pub fn open(
408        &self,
409        request: &HardwareEncodeRequest<'_>,
410    ) -> Result<(HardwareEncodeSession, HardwareEncodeInput), HardwareEncodeOpenError> {
411        let generation = NEXT_POOL_GENERATION.fetch_add(1, Ordering::Relaxed);
412        // These backends encode one codec; the D3D12VA one checks the codec it is asked for itself.
413        #[cfg(any(target_os = "linux", target_os = "macos"))]
414        self.capability(request.codec)
415            .map_err(HardwareEncodeOpenError::Refused)?;
416        #[cfg(target_os = "linux")]
417        {
418            let (session, input) = vulkan::open(&self.vulkan, request, generation)?;
419            Ok((
420                HardwareEncodeSession(SessionBackend::Vulkan(session)),
421                HardwareEncodeInput {
422                    backend: InputBackend::Vulkan(input),
423                    generation,
424                },
425            ))
426        }
427        #[cfg(target_os = "macos")]
428        {
429            let (session, input) = videotoolbox::open(&self.metal, request, generation)?;
430            Ok((
431                HardwareEncodeSession(SessionBackend::VideoToolbox(session)),
432                HardwareEncodeInput {
433                    backend: InputBackend::VideoToolbox(input),
434                    generation,
435                },
436            ))
437        }
438        #[cfg(target_os = "windows")]
439        {
440            let (session, input) = d3d12va::open(&self.d3d12, request, generation)?;
441            Ok((
442                HardwareEncodeSession(SessionBackend::D3D12(session)),
443                HardwareEncodeInput {
444                    backend: InputBackend::D3D12(input),
445                    generation,
446                },
447            ))
448        }
449        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
450        {
451            let _ = (request, generation);
452            Err(HardwareEncodeOpenError::Refused(
453                "this platform has no hardware encode backend yet".to_owned(),
454            ))
455        }
456    }
457}
458
459/// Identifies the pool a surface came from, so a surface can never be
460/// written or encoded by a session other than its own: after a cancel, a
461/// resize, or a device loss, an old surface must not reach a new pool.
462static NEXT_POOL_GENERATION: AtomicU64 = AtomicU64::new(1);
463
464/// What one encoder-input surface is, independent of its backing.
465#[derive(Copy, Clone, Debug, Eq, PartialEq)]
466pub struct EncoderSurfaceDescriptor {
467    /// The backend whose pool allocated it.
468    pub backend: HardwareEncodeBackend,
469    /// The session's pool; see [`HardwareEncodeInput::acquire_surface`].
470    pub pool_generation: u64,
471    /// Its pixel layout.
472    pub layout: EncoderInputLayout,
473    /// The picture's size.
474    pub visible: (u32, u32),
475    /// The color its pixels are encoded in.
476    pub color: ColorMetadata,
477}
478
479/// One encoder-input surface on loan from its session's pool.
480///
481/// Holding it keeps the backing alive; dropping it gives the backend's
482/// reference back. The backend still decides when the backing is reusable.
483pub struct EncoderSurface {
484    descriptor: EncoderSurfaceDescriptor,
485    backing: SurfaceBacking,
486}
487
488enum SurfaceBacking {
489    #[cfg(target_os = "linux")]
490    Vulkan(vulkan::VulkanEncoderFrame),
491    #[cfg(target_os = "macos")]
492    VideoToolbox(videotoolbox::VideoToolboxEncoderFrame),
493    #[cfg(target_os = "windows")]
494    D3D12(d3d12va::D3D12EncoderFrame),
495    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
496    Unsupported(std::convert::Infallible),
497}
498
499impl EncoderSurface {
500    /// What this surface is.
501    #[must_use]
502    pub const fn descriptor(&self) -> &EncoderSurfaceDescriptor {
503        &self.descriptor
504    }
505}
506
507/// A frame the coordinator has begun: its planes are the renderer's to write
508/// until [`HardwareEncodeInput::submit`] takes it.
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 the pool allocated to hand them out. Beyond the
525    /// frames the host holds, this counts what the encoder and the GPU copy
526    /// still referenced when the next surface was asked for.
527    pub allocated: u64,
528    /// Handoffs into a surface: a plane copy each on a copying backend.
529    pub copy_operations: u64,
530    /// The bytes those handoffs moved.
531    pub copy_bytes: u64,
532}
533
534/// The coordinator's half of a hardware session.
535pub struct HardwareEncodeInput {
536    backend: InputBackend,
537    generation: u64,
538}
539
540enum InputBackend {
541    #[cfg(target_os = "linux")]
542    Vulkan(vulkan::VulkanEncodeInput),
543    #[cfg(target_os = "macos")]
544    VideoToolbox(videotoolbox::VideoToolboxEncodeInput),
545    #[cfg(target_os = "windows")]
546    D3D12(d3d12va::D3D12EncodeInput),
547    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
548    Unsupported(std::convert::Infallible),
549}
550
551impl HardwareEncodeInput {
552    /// Takes one surface from the session's pool.
553    ///
554    /// The pool allocates on demand, so the number of backings it ever holds
555    /// is the peak number outstanding; the caller bounds that.
556    ///
557    /// # Errors
558    /// Returns an adapter failure when the pool cannot allocate.
559    pub fn acquire_surface(&mut self) -> Result<EncoderSurface, EncodeError> {
560        match &mut self.backend {
561            #[cfg(target_os = "linux")]
562            InputBackend::Vulkan(input) => {
563                let frame = input.acquire_frame()?;
564                Ok(EncoderSurface {
565                    descriptor: input.descriptor(self.generation),
566                    backing: SurfaceBacking::Vulkan(frame),
567                })
568            }
569            #[cfg(target_os = "macos")]
570            InputBackend::VideoToolbox(input) => {
571                let frame = input.acquire_frame()?;
572                Ok(EncoderSurface {
573                    descriptor: input.descriptor(self.generation),
574                    backing: SurfaceBacking::VideoToolbox(frame),
575                })
576            }
577            #[cfg(target_os = "windows")]
578            InputBackend::D3D12(input) => {
579                let frame = input.acquire_frame()?;
580                Ok(EncoderSurface {
581                    descriptor: input.descriptor(self.generation),
582                    backing: SurfaceBacking::D3D12(frame),
583                })
584            }
585            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
586            InputBackend::Unsupported(never) => match *never {},
587        }
588    }
589
590    /// Begins a frame bound for `surface`: waits until the planes it will be
591    /// packed into are free. Call it before recording the render that packs
592    /// the frame, render into [`Self::planes`], and submit that render before
593    /// [`Self::submit`].
594    ///
595    /// # Errors
596    /// Returns an error for a surface from another pool, or when the wait
597    /// fails.
598    pub fn begin_frame(&mut self, surface: &EncoderSurface) -> Result<InputFrame, EncodeError> {
599        self.check(surface)?;
600        match &mut self.backend {
601            #[cfg(target_os = "linux")]
602            InputBackend::Vulkan(input) => input.begin_frame(),
603            #[cfg(target_os = "macos")]
604            InputBackend::VideoToolbox(input) => match &surface.backing {
605                SurfaceBacking::VideoToolbox(backing) => input.begin_frame(backing),
606            },
607            #[cfg(target_os = "windows")]
608            InputBackend::D3D12(input) => input.begin_frame(),
609            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
610            InputBackend::Unsupported(never) => match *never {},
611        }
612    }
613
614    /// The luma and interleaved chroma textures `frame` is packed into.
615    #[must_use]
616    pub fn planes(&self, frame: &InputFrame) -> (&wgpu::Texture, &wgpu::Texture) {
617        match &self.backend {
618            #[cfg(target_os = "linux")]
619            InputBackend::Vulkan(input) => input.planes(frame),
620            #[cfg(target_os = "macos")]
621            InputBackend::VideoToolbox(input) => input.planes(frame),
622            #[cfg(target_os = "windows")]
623            InputBackend::D3D12(input) => input.planes(frame),
624            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
625            InputBackend::Unsupported(never) => match *never {},
626        }
627    }
628
629    /// Moves `frame`'s packed planes into `surface` on the GPU. The encoder
630    /// waits for that on the GPU too, so this does not.
631    ///
632    /// # Errors
633    /// Returns an error for a surface from another pool, or when the handoff
634    /// cannot be submitted.
635    pub fn submit(
636        &mut self,
637        frame: InputFrame,
638        surface: &mut EncoderSurface,
639    ) -> Result<(), EncodeError> {
640        self.check(surface)?;
641        match (&mut self.backend, &mut surface.backing) {
642            #[cfg(target_os = "linux")]
643            (InputBackend::Vulkan(input), SurfaceBacking::Vulkan(backing)) => {
644                input.submit(frame, backing)
645            }
646            #[cfg(target_os = "macos")]
647            (InputBackend::VideoToolbox(input), SurfaceBacking::VideoToolbox(backing)) => {
648                input.submit(frame, backing)
649            }
650            #[cfg(target_os = "windows")]
651            (InputBackend::D3D12(input), SurfaceBacking::D3D12(backing)) => {
652                input.submit(frame, backing)
653            }
654            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
655            (InputBackend::Unsupported(never), _) => match *never {},
656        }
657    }
658
659    /// Stamps `surface` with its presentation time.
660    pub fn set_time(&self, surface: &mut EncoderSurface, time: MediaTime) {
661        match (&self.backend, &mut surface.backing) {
662            #[cfg(target_os = "linux")]
663            (InputBackend::Vulkan(input), SurfaceBacking::Vulkan(backing)) => {
664                backing.set_pts(input.timestamp(time));
665            }
666            #[cfg(target_os = "macos")]
667            (InputBackend::VideoToolbox(input), SurfaceBacking::VideoToolbox(backing)) => {
668                backing.set_pts(input.timestamp(time));
669            }
670            #[cfg(target_os = "windows")]
671            (InputBackend::D3D12(input), SurfaceBacking::D3D12(backing)) => {
672                backing.set_pts(input.timestamp(time));
673            }
674            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
675            (InputBackend::Unsupported(never), _) => match *never {},
676        }
677    }
678
679    /// This session's pool counters so far.
680    #[must_use]
681    pub fn stats(&self) -> EncoderPoolStats {
682        match &self.backend {
683            #[cfg(target_os = "linux")]
684            InputBackend::Vulkan(input) => input.stats(),
685            #[cfg(target_os = "macos")]
686            InputBackend::VideoToolbox(input) => input.stats(),
687            #[cfg(target_os = "windows")]
688            InputBackend::D3D12(input) => input.stats(),
689            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
690            InputBackend::Unsupported(never) => match *never {},
691        }
692    }
693
694    /// Waits for every in-flight handoff and releases the input resources.
695    ///
696    /// # Errors
697    /// On timeout or device loss the backend keeps what the GPU may still use
698    /// rather than freeing it, and says so.
699    pub fn finish(self) -> Result<(), EncodeError> {
700        match self.backend {
701            #[cfg(target_os = "linux")]
702            InputBackend::Vulkan(input) => input.finish(),
703            #[cfg(target_os = "macos")]
704            InputBackend::VideoToolbox(input) => input.finish(),
705            #[cfg(target_os = "windows")]
706            InputBackend::D3D12(input) => input.finish(),
707            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
708            InputBackend::Unsupported(never) => match never {},
709        }
710    }
711
712    fn check(&self, surface: &EncoderSurface) -> Result<(), EncodeError> {
713        if surface.descriptor.pool_generation == self.generation {
714            Ok(())
715        } else {
716            Err(EncodeError::Adapter(format!(
717                "encoder surface from pool {} reached pool {}",
718                surface.descriptor.pool_generation, self.generation
719            )))
720        }
721    }
722}
723
724/// The encode worker's half of a hardware session.
725pub struct HardwareEncodeSession(SessionBackend);
726
727enum SessionBackend {
728    #[cfg(target_os = "linux")]
729    Vulkan(vulkan::VulkanEncodeSession),
730    #[cfg(target_os = "macos")]
731    VideoToolbox(videotoolbox::VideoToolboxEncodeSession),
732    #[cfg(target_os = "windows")]
733    D3D12(d3d12va::D3D12EncodeSession),
734    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
735    Unsupported(std::convert::Infallible),
736}
737
738impl HardwareEncodeSession {
739    /// The encoder this session opened, reported the way every other export
740    /// path reports the encoder it opened.
741    #[must_use]
742    pub fn selection(&self) -> &NativeVideoEncoderSelection {
743        match &self.0 {
744            #[cfg(target_os = "linux")]
745            SessionBackend::Vulkan(session) => session.selection(),
746            #[cfg(target_os = "macos")]
747            SessionBackend::VideoToolbox(session) => session.selection(),
748            #[cfg(target_os = "windows")]
749            SessionBackend::D3D12(session) => session.selection(),
750            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
751            SessionBackend::Unsupported(never) => match *never {},
752        }
753    }
754
755    /// Where this session's time has gone, split the way the software sinks
756    /// split theirs.
757    #[must_use]
758    pub fn stage_timings(&self) -> EncodeStageTimings {
759        match &self.0 {
760            #[cfg(target_os = "linux")]
761            SessionBackend::Vulkan(session) => session.stage_timings(),
762            #[cfg(target_os = "macos")]
763            SessionBackend::VideoToolbox(session) => session.stage_timings(),
764            #[cfg(target_os = "windows")]
765            SessionBackend::D3D12(session) => session.stage_timings(),
766            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
767            SessionBackend::Unsupported(never) => match *never {},
768        }
769    }
770
771    /// Submits `surface` and muxes whatever packets it produced.
772    ///
773    /// # Errors
774    /// Returns an adapter failure when the encoder rejects the frame or its
775    /// packets cannot be written.
776    pub fn encode(&mut self, surface: &EncoderSurface) -> Result<(), EncodeError> {
777        match (&mut self.0, &surface.backing) {
778            #[cfg(target_os = "linux")]
779            (SessionBackend::Vulkan(session), SurfaceBacking::Vulkan(frame)) => {
780                session.encode(surface.descriptor.pool_generation, frame)
781            }
782            #[cfg(target_os = "macos")]
783            (SessionBackend::VideoToolbox(session), SurfaceBacking::VideoToolbox(frame)) => {
784                session.encode(surface.descriptor.pool_generation, frame)
785            }
786            #[cfg(target_os = "windows")]
787            (SessionBackend::D3D12(session), SurfaceBacking::D3D12(frame)) => {
788                session.encode(surface.descriptor.pool_generation, frame)
789            }
790            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
791            (SessionBackend::Unsupported(never), _) => match *never {},
792        }
793    }
794
795    /// Buffers one audio block for the audio stream.
796    ///
797    /// # Errors
798    /// Returns an adapter failure for a format change, a non-monotonic block,
799    /// or a failed audio encode.
800    pub fn write_audio(&mut self, block: &alpha2_media::AudioBlock) -> Result<(), EncodeError> {
801        match &mut self.0 {
802            #[cfg(target_os = "linux")]
803            SessionBackend::Vulkan(session) => session.write_audio(block),
804            #[cfg(target_os = "macos")]
805            SessionBackend::VideoToolbox(session) => session.write_audio(block),
806            #[cfg(target_os = "windows")]
807            SessionBackend::D3D12(session) => session.write_audio(block),
808            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
809            SessionBackend::Unsupported(never) => match *never {},
810        }
811    }
812
813    /// Flushes the encoder and the audio, writes the trailer, and renames the
814    /// output into place. Only this makes the output visible.
815    ///
816    /// # Errors
817    /// Returns an adapter failure from the flush, the trailer, or the rename.
818    pub fn finish(&mut self) -> Result<(), EncodeError> {
819        match &mut self.0 {
820            #[cfg(target_os = "linux")]
821            SessionBackend::Vulkan(session) => session.finish(),
822            #[cfg(target_os = "macos")]
823            SessionBackend::VideoToolbox(session) => session.finish(),
824            #[cfg(target_os = "windows")]
825            SessionBackend::D3D12(session) => session.finish(),
826            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
827            SessionBackend::Unsupported(never) => match *never {},
828        }
829    }
830
831    /// Discards the partial output.
832    pub fn abort(&mut self) {
833        match &mut self.0 {
834            #[cfg(target_os = "linux")]
835            SessionBackend::Vulkan(session) => session.abort(),
836            #[cfg(target_os = "macos")]
837            SessionBackend::VideoToolbox(session) => session.abort(),
838            #[cfg(target_os = "windows")]
839            SessionBackend::D3D12(session) => session.abort(),
840            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
841            SessionBackend::Unsupported(never) => match *never {},
842        }
843    }
844}