Skip to main content

alpha2_native/hwencode/
vulkan.rs

1//! The Vulkan backend: `h264_vulkan` on the renderer's shared device.
2//!
3//! Everything between the renderer and the encoder (the frames pool, its
4//! image usage, the number of images per frame, the queue family that owns
5//! them) is decided by `FFmpeg` and has to be observed rather than assumed.
6//! The probes observe it on whichever device they are handed, so the same
7//! answers can be read for `FFmpeg`'s own device and for the one wgpu created.
8
9use std::collections::HashSet;
10use std::ffi::CString;
11
12use alpha2_media::{ColorMetadata, EncodeError};
13use alpha2_time::MediaTime;
14use rsmpeg::avcodec::{AVCodec, AVCodecContext};
15use rsmpeg::avutil::{AVFrame, AVHWDeviceContext, AVHWFramesContext, AVRational, av_rescale_q, ra};
16use rsmpeg::error::RsmpegError;
17use rsmpeg::ffi;
18
19use super::{
20    EncoderInputHandoff, EncoderInputLayout, EncoderPoolStats, EncoderSurfaceDescriptor,
21    HardwareEncodeBackend, HardwareEncodeCapability, HardwareEncodeOpenError,
22    HardwareEncodeRequest, InputFrame,
23};
24use crate::NativeVideoCodec;
25use crate::convert::{encode_adapter_error, tick_base};
26use crate::encode::{EncodeStageTimings, timed};
27use crate::mux::Muxer;
28
29/// What one Vulkan encode session turned out to require and produce.
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct VulkanEncodeProbe {
32    /// `FFmpeg` encoder that opened.
33    pub encoder: String,
34    /// Coded size the pool allocated.
35    pub coded_size: (u32, u32),
36    /// Image usage the pool was created with: the one the encoder-input pool
37    /// asks for, which `FFmpeg` keeps as given. A flag the format cannot
38    /// support fails the pool or the session instead of disappearing here.
39    pub frames_usage: u32,
40    /// `VkImage` handles backing one frame. One means a single multi-planar
41    /// image and a per-plane copy; two would mean a plane per image.
42    pub images_per_frame: usize,
43    /// Initial layout `FFmpeg` left the frame in.
44    pub initial_layout: u32,
45    /// Queue family owning the frame, or `u32::MAX` for `VK_QUEUE_FAMILY_IGNORED`.
46    pub queue_family: u32,
47    /// Whether the frame exposes a timeline semaphore for the handoff.
48    pub has_timeline_semaphore: bool,
49    /// Packets drained from one submitted frame plus a flush.
50    pub packets: u32,
51    /// Bytes in those packets.
52    pub packet_bytes: u32,
53}
54
55/// Opens `h264_vulkan` against `device` and encodes one pool frame.
56///
57/// The frame is left as the pool allocated it: this answers whether the
58/// session can be created and driven on this device, not what the picture
59/// looks like.
60///
61/// # Errors
62/// Returns an adapter failure when the encoder is absent, the pool cannot be
63/// created, the session cannot open, or no packet is produced.
64pub fn probe_vulkan_encode(
65    device: &AVHWDeviceContext,
66    width: u32,
67    height: u32,
68) -> Result<VulkanEncodeProbe, EncodeError> {
69    let encoder = AVCodec::find_encoder_by_name(c"h264_vulkan").ok_or_else(|| {
70        EncodeError::Adapter("this FFmpeg runtime has no h264_vulkan encoder".into())
71    })?;
72    let coded_width =
73        i32::try_from(width).map_err(|_| EncodeError::Adapter("width is out of range".into()))?;
74    let coded_height =
75        i32::try_from(height).map_err(|_| EncodeError::Adapter("height is out of range".into()))?;
76
77    let mut frames = vulkan_encode_frames(device, coded_width, coded_height)?;
78    let frames_usage = alpha2_ffmpeg_bridge::vulkan_frames_usage(&frames);
79
80    let mut context = AVCodecContext::new(&encoder);
81    context.set_width(coded_width);
82    context.set_height(coded_height);
83    context.set_pix_fmt(ffi::AV_PIX_FMT_VULKAN);
84    // `ff_encode_preinit` copies the pool's `sw_format` into `sw_pix_fmt`, and rejects the open if
85    // a caller set a different one, so it is left unset.
86    context.set_time_base(ra(1, 30));
87    context.set_framerate(ra(30, 1));
88    context.set_bit_rate(12_000_000);
89    context.set_gop_size(12);
90    // A probe's coding settings are arbitrary; see the session for why exports have no B pictures.
91    context.set_max_b_frames(0);
92    context.set_hw_frames_ctx(frames.clone());
93    context
94        .open(None)
95        .map_err(|error| encode_adapter_error_at_open(&error))?;
96
97    let mut frame = AVFrame::new();
98    frames
99        .get_buffer(&mut frame)
100        .map_err(encode_adapter_error)?;
101    let descriptor =
102        alpha2_ffmpeg_bridge::vulkan_frame_info(&frame).map_err(encode_adapter_error)?;
103    frame.set_pts(0);
104
105    context
106        .send_frame(Some(&frame))
107        .map_err(encode_adapter_error)?;
108    context.send_frame(None).map_err(encode_adapter_error)?;
109    let mut packets = 0;
110    let mut packet_bytes = 0_u32;
111    loop {
112        match context.receive_packet() {
113            Ok(packet) => {
114                packets += 1;
115                packet_bytes = packet_bytes.saturating_add(packet.size.unsigned_abs());
116            }
117            Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => break,
118            Err(error) => return Err(encode_adapter_error(error)),
119        }
120    }
121    if packets == 0 {
122        return Err(EncodeError::Adapter(
123            "h264_vulkan opened but produced no packet".into(),
124        ));
125    }
126    Ok(VulkanEncodeProbe {
127        encoder: encoder.name().to_string_lossy().into_owned(),
128        coded_size: (
129            descriptor.coded_width.unsigned_abs(),
130            descriptor.coded_height.unsigned_abs(),
131        ),
132        frames_usage,
133        images_per_frame: descriptor.plane_count as usize,
134        initial_layout: descriptor.layouts[0],
135        queue_family: descriptor.queue_families[0],
136        has_timeline_semaphore: descriptor.semaphores[0] != 0,
137        packets,
138        packet_bytes,
139    })
140}
141
142/// Opens a Vulkan encoder with one rate-control request and encodes one
143/// pool frame, on a device `FFmpeg` creates for the purpose.
144///
145/// This is `probe-encoders`' view of a hardware-frames encoder: the pass
146/// condition is the same as for a software one: it opened, consumed every
147/// option it was given, and produced a packet. Whether the device supports a
148/// mode is `VkVideoEncodeCapabilitiesKHR::rateControlModes`, which `FFmpeg`
149/// checks at open, so an unsupported one fails here rather than later.
150pub(crate) fn probe_vulkan_rate_control(
151    encoder: &rsmpeg::avcodec::AVCodecRef<'static>,
152    width: u32,
153    height: u32,
154    request: &crate::rate_control::MechanismRequest,
155) -> Result<(u32, String), String> {
156    // The host's chosen device, passed by index: FFmpeg reads a name starting with a digit as an
157    // index, so `ALPHA2_VULKAN_DEVICE` cannot pass through unchanged. Otherwise the probe walks the
158    // indices until one accepts or refuses the mode, since only some devices have an encode queue.
159    let named = std::env::var("ALPHA2_VULKAN_DEVICE").ok();
160    let shared = alpha2_native_gpu::shared_device_index(named.as_deref());
161    let candidates = match (&named, shared) {
162        (Some(_), Ok(index)) => vec![index.to_string()],
163        (Some(_), Err(error)) => return Err(error.to_string()),
164        (None, shared) => {
165            let first = shared.ok();
166            first
167                .into_iter()
168                .chain((0..8).filter(|index| Some(*index) != first))
169                .map(|index| index.to_string())
170                .collect::<Vec<_>>()
171        }
172    };
173    let mut last = "no Vulkan device".to_owned();
174    for device in candidates {
175        match probe_vulkan_rate_control_on(encoder, &device, width, height, request) {
176            Ok(packets) => return Ok((packets, device)),
177            Err(VulkanProbeError::NoDevice(error)) => {
178                last = error;
179                break;
180            }
181            Err(VulkanProbeError::NoEncodeQueue(error)) if named.is_none() => last = error,
182            Err(VulkanProbeError::NoEncodeQueue(error) | VulkanProbeError::Refused(error)) => {
183                return Err(format!("on Vulkan device {device}: {error}"));
184            }
185        }
186    }
187    Err(last)
188}
189
190enum VulkanProbeError {
191    /// No device at this index: the walk is over.
192    NoDevice(String),
193    /// The device has no video-encode queue; another one might.
194    NoEncodeQueue(String),
195    /// The device got as far as the encoder, which refused.
196    Refused(String),
197}
198
199fn probe_vulkan_rate_control_on(
200    encoder: &rsmpeg::avcodec::AVCodecRef<'static>,
201    device_name: &str,
202    width: u32,
203    height: u32,
204    request: &crate::rate_control::MechanismRequest,
205) -> Result<u32, VulkanProbeError> {
206    let device_name = CString::new(device_name)
207        .map_err(|_| VulkanProbeError::NoDevice("device name contains a NUL byte".to_owned()))?;
208    // FFmpeg enables video-encode extensions on a device it creates only when asked; the export's
209    // shared device asks through wgpu instead.
210    let extensions = rsmpeg::avutil::AVDictionary::new(
211        c"device_extensions",
212        c"VK_KHR_video_queue+VK_KHR_video_encode_queue+VK_KHR_video_encode_h264+VK_KHR_video_encode_h265+VK_KHR_video_encode_av1+VK_KHR_video_maintenance1",
213        0,
214    );
215    let device = AVHWDeviceContext::create(
216        ffi::AV_HWDEVICE_TYPE_VULKAN,
217        Some(&device_name),
218        Some(&extensions),
219        0,
220    )
221    .map_err(|error| VulkanProbeError::NoDevice(format!("no Vulkan device: {error}")))?;
222    let refused = |error: String| VulkanProbeError::Refused(error);
223    let coded_width =
224        i32::try_from(width).map_err(|_| refused("width is out of range".to_owned()))?;
225    let coded_height =
226        i32::try_from(height).map_err(|_| refused("height is out of range".to_owned()))?;
227    let mut frames = vulkan_encode_frames(&device, coded_width, coded_height)
228        .map_err(|error| VulkanProbeError::NoEncodeQueue(error.to_string()))?;
229    let mut context = AVCodecContext::new(encoder);
230    context.set_width(coded_width);
231    context.set_height(coded_height);
232    context.set_pix_fmt(ffi::AV_PIX_FMT_VULKAN);
233    context.set_time_base(ra(1, 30));
234    context.set_framerate(ra(30, 1));
235    context.set_bit_rate(request.bit_rate);
236    context.set_gop_size(12);
237    context.set_max_b_frames(0);
238    context.set_hw_frames_ctx(frames.clone());
239    let unused = alpha2_ffmpeg_bridge::open_codec_context(
240        &mut context,
241        request.dictionary(crate::convert::color_options(ColorMetadata::EXPORT)),
242    )
243    .map_err(|error| {
244        // `ENOSYS` means the device has no encode queue at all, which says nothing about the mode.
245        let message = error.to_string();
246        if matches!(error, RsmpegError::AVError(code) if code == -38) {
247            VulkanProbeError::NoEncodeQueue(message)
248        } else {
249            refused(message)
250        }
251    })?;
252    crate::convert::reject_unused_encode_options(unused)
253        .map_err(|error| refused(error.to_string()))?;
254    let mut frame = AVFrame::new();
255    frames
256        .get_buffer(&mut frame)
257        .map_err(|error| refused(error.to_string()))?;
258    frame.set_pts(0);
259    context
260        .send_frame(Some(&frame))
261        .map_err(|error| refused(error.to_string()))?;
262    context
263        .send_frame(None)
264        .map_err(|error| refused(error.to_string()))?;
265    let mut packets = 0;
266    loop {
267        match context.receive_packet() {
268            Ok(_) => packets += 1,
269            Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => break,
270            Err(error) => return Err(refused(error.to_string())),
271        }
272    }
273    if packets == 0 {
274        return Err(refused("opened but produced no packet".to_owned()));
275    }
276    Ok(packets)
277}
278
279/// What this device's `h264_vulkan` accepts, from the device's own answers.
280pub(super) fn capability(
281    shared: &alpha2_native_gpu::SharedVulkanContext,
282) -> Result<HardwareEncodeCapability, String> {
283    if shared.encode_codecs().is_empty() {
284        return Err("this Vulkan device has no video-encode queue".to_owned());
285    }
286    let extent = shared
287        .encode_extent()
288        .ok_or_else(|| "this device did not report its H.264 encode extents".to_owned())?;
289    Ok(HardwareEncodeCapability {
290        backend: HardwareEncodeBackend::Vulkan,
291        encoder: "h264_vulkan",
292        codec: NativeVideoCodec::H264,
293        gpu_input_only: true,
294        input_layout: EncoderInputLayout::Nv12,
295        handoff: EncoderInputHandoff::GpuCopy,
296        shares_renderer_device: true,
297        minimum: extent.minimum,
298        maximum: extent.maximum,
299        granularity: extent.granularity,
300        max_p_picture_l0_references: Some(extent.max_p_picture_l0_references),
301        max_b_picture_l0_references: Some(extent.max_b_picture_l0_references),
302        max_l1_references: Some(extent.max_l1_references),
303        rate_control_modes: extent.rate_control_modes.clone(),
304        plane_ring_slots: alpha2_native_gpu::VulkanEncodeCopier::slots(),
305        // The copier keeps each copy's frame in its slot until the slot is reused.
306        handoff_retained_surfaces: alpha2_native_gpu::VulkanEncodeCopier::slots(),
307        // Measured: the pool never exceeded the host's leases plus the copier's slots.
308        codec_retained_surfaces: 0,
309    })
310}
311
312/// Opens `h264_vulkan` on the shared device, its input half, and the output.
313pub(super) fn open(
314    shared: &alpha2_native_gpu::SharedVulkanContext,
315    request: &HardwareEncodeRequest<'_>,
316    generation: u64,
317) -> Result<(VulkanEncodeSession, VulkanEncodeInput), HardwareEncodeOpenError> {
318    let refused = |error: EncodeError| HardwareEncodeOpenError::Refused(error.to_string());
319    let (width, height) = (request.width, request.height);
320    let frame_rate = super::session_frame_rate(request.frame_rate).map_err(refused)?;
321    let (frames, context, selection) = open_encoder(
322        &shared.ffmpeg_device(),
323        width,
324        height,
325        request.rate_control,
326        frame_rate,
327        request.color,
328    )
329    .map_err(refused)?;
330    let copier = alpha2_native_gpu::VulkanEncodeCopier::new(shared)
331        .map_err(|error| HardwareEncodeOpenError::Refused(format!("{error:?}")))?;
332    let gpu = shared.gpu();
333    // One pair per copier slot: the coordinator can run ahead of the encoder, so a single pair
334    // would let the next render overwrite planes a copy is still reading.
335    let planes = (0..alpha2_native_gpu::VulkanEncodeCopier::slots())
336        .map(|_| Nv12PlanePair {
337            luma: plane_texture(
338                gpu.device(),
339                "alpha2 NV12 luma",
340                width,
341                height,
342                wgpu::TextureFormat::R8Unorm,
343            ),
344            chroma: plane_texture(
345                gpu.device(),
346                "alpha2 NV12 chroma",
347                width / 2,
348                height / 2,
349                wgpu::TextureFormat::Rg8Unorm,
350            ),
351        })
352        .collect();
353    let time_base = ra(frame_rate.1, frame_rate.0);
354    let input = VulkanEncodeInput {
355        copier,
356        planes,
357        frames,
358        size: (width, height),
359        time_base,
360        color: request.color,
361        stats: EncoderPoolStats::default(),
362        images: HashSet::new(),
363    };
364    // From here on a failure is the export's rather than a refusal: the partial output exists.
365    let muxer = Muxer::open(
366        request.output,
367        request.container,
368        &context,
369        time_base,
370        request.audio,
371    )
372    .map_err(HardwareEncodeOpenError::Failed)?;
373    Ok((
374        VulkanEncodeSession {
375            context,
376            time_base,
377            muxer,
378            stages: EncodeStageTimings::default(),
379            selection,
380            generation,
381        },
382        input,
383    ))
384}
385
386/// One renderer-owned NV12 plane target for the encode handoff.
387///
388/// `COPY_SRC` is what gives the image `VK_IMAGE_USAGE_TRANSFER_SRC`, which the
389/// raw copy into the encoder frame reads from.
390fn plane_texture(
391    device: &wgpu::Device,
392    label: &'static str,
393    width: u32,
394    height: u32,
395    format: wgpu::TextureFormat,
396) -> wgpu::Texture {
397    device.create_texture(&wgpu::TextureDescriptor {
398        label: Some(label),
399        size: wgpu::Extent3d {
400            width,
401            height,
402            depth_or_array_layers: 1,
403        },
404        mip_level_count: 1,
405        sample_count: 1,
406        dimension: wgpu::TextureDimension::D2,
407        format,
408        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
409        view_formats: &[],
410    })
411}
412
413/// One slot of the NV12 plane ring the renderer packs into.
414struct Nv12PlanePair {
415    luma: wgpu::Texture,
416    chroma: wgpu::Texture,
417}
418
419/// Opens `h264_vulkan` on `device` with an NV12 encoder-input pool.
420fn open_encoder(
421    device: &AVHWDeviceContext,
422    width: u32,
423    height: u32,
424    rate_control: alpha2_media::VideoRateControl,
425    frame_rate: (i32, i32),
426    color: ColorMetadata,
427) -> Result<
428    (
429        AVHWFramesContext,
430        AVCodecContext,
431        crate::NativeVideoEncoderSelection,
432    ),
433    EncodeError,
434> {
435    let encoder = AVCodec::find_encoder_by_name(c"h264_vulkan").ok_or_else(|| {
436        EncodeError::Adapter("this FFmpeg runtime has no h264_vulkan encoder".into())
437    })?;
438    let coded_width =
439        i32::try_from(width).map_err(|_| EncodeError::Adapter("width is out of range".into()))?;
440    let coded_height =
441        i32::try_from(height).map_err(|_| EncodeError::Adapter("height is out of range".into()))?;
442    let frames = vulkan_encode_frames(device, coded_width, coded_height)?;
443    let rate = u32::try_from(frame_rate.0)
444        .ok()
445        .zip(u32::try_from(frame_rate.1).ok())
446        .and_then(|(num, den)| alpha2_time::FrameRate::new(num, den).ok())
447        .ok_or_else(|| EncodeError::Adapter("frame rate is out of range".into()))?;
448    let open = |quantized: Option<&crate::rate_control::MechanismRequest>| {
449        let mut context = AVCodecContext::new(&encoder);
450        context.set_width(coded_width);
451        context.set_height(coded_height);
452        context.set_pix_fmt(ffi::AV_PIX_FMT_VULKAN);
453        // `ff_encode_preinit` copies the pool's `sw_format` into `sw_pix_fmt`, and rejects the open
454        // if a caller set a different one.
455        context.set_time_base(ra(frame_rate.1, frame_rate.0));
456        context.set_framerate(ra(frame_rate.0, frame_rate.1));
457        context.set_bit_rate(quantized.map_or_else(
458            || i64::try_from(rate_control.bitrate).unwrap_or(i64::MAX),
459            |quantized| quantized.bit_rate,
460        ));
461        context.set_gop_size(crate::capabilities::keyframe_interval(rate));
462        // No B pictures, by measurement: devices allow them (an RTX 4090 reports 7 L0 / 1 L1
463        // references) and FFmpeg emits them, but on the 4K showcase at a fixed rate two B frames
464        // cost 0.34-0.63 dB PSNR-Y against none across clips and GOP lengths.
465        context.set_max_b_frames(0);
466        context.set_hw_frames_ctx(frames.clone());
467        // MP4 carries SPS and PPS in `avcC`. The flag is read at open, so it must be set before it,
468        // or the parameter sets travel in-band only.
469        context.set_flags(
470            context.flags | i32::try_from(ffi::AV_CODEC_FLAG_GLOBAL_HEADER).unwrap_or(0),
471        );
472        // Without these the stream has no VUI and decoders guess the colour; the software sinks tag
473        // their streams through the same options.
474        let color = crate::convert::color_options(color);
475        let options = match quantized {
476            Some(quantized) => quantized.dictionary(color),
477            None => crate::rate_control::rate_control_options("h264_vulkan", rate_control, color),
478        };
479        let unused = alpha2_ffmpeg_bridge::open_codec_context(&mut context, options)
480            .map_err(|error| encode_adapter_error_at_open(&error))?;
481        crate::convert::reject_unused_encode_options(unused)?;
482        if context.extradata_size <= 0 {
483            return Err(EncodeError::Adapter(
484                "h264_vulkan opened without SPS/PPS extradata for the MP4 header".into(),
485            ));
486        }
487        Ok::<_, EncodeError>(context)
488    };
489    // A refused quantizer falls back to the rate control's own bitrate on the same encoder, and the
490    // selection says which one runs.
491    let (context, applied, quantizer_refused) =
492        match crate::rate_control::quantizer_request("h264_vulkan", rate_control) {
493            Some(Ok(quantized)) => match open(Some(&quantized)) {
494                Ok(context) => (context, crate::AppliedRateControl::Quantizer, None),
495                Err(error) => (
496                    open(None)?,
497                    crate::AppliedRateControl::Bitrate,
498                    Some(format!("{} refused: {error}", quantized.describe())),
499                ),
500            },
501            Some(Err(reason)) => (
502                open(None)?,
503                crate::AppliedRateControl::Bitrate,
504                Some(reason),
505            ),
506            None => (open(None)?, crate::AppliedRateControl::Bitrate, None),
507        };
508    if let Some(reason) = &quantizer_refused {
509        alpha2_os::diagnostic!(
510            "alpha2 native export: h264_vulkan runs the bitrate fallback: {reason}"
511        );
512    }
513    // No candidate list: this encoder is chosen from the device, and anything that could refuse it
514    // already has.
515    let selection = crate::NativeVideoEncoderSelection {
516        encoder_name: "h264_vulkan".to_owned(),
517        class: crate::NativeEncoderClass::Hardware,
518        hardware_accelerated: true,
519        rejected: Vec::new(),
520        rate_control: applied,
521        quantizer_refused,
522        software_threads: None,
523    };
524    Ok((frames, context, selection))
525}
526
527/// The encode worker's half of a Vulkan session: the codec and the muxer.
528pub(super) struct VulkanEncodeSession {
529    context: AVCodecContext,
530    time_base: AVRational,
531    muxer: Muxer,
532    stages: EncodeStageTimings,
533    selection: crate::NativeVideoEncoderSelection,
534    generation: u64,
535}
536
537/// The coordinator's half of a Vulkan session.
538///
539/// The codec moves to an encode worker; the copy cannot follow it, because
540/// the wgpu plane textures and the render signal both belong to the thread
541/// that rendered them. This is what stays behind: the copier and its plane
542/// ring, and a reference to the encoder-input pool, which is reference
543/// counted, so a frame taken here stays valid for as long as the encoder
544/// needs it even though the two halves are on different threads.
545///
546/// Fields drop in declaration order, and the copier comes first: dropping it
547/// waits on every in-flight copy, retaining the images, encoder frames, and
548/// device together if that wait times out, since wgpu cannot track these raw
549/// copies. Dropping the planes first would free them under a running copy.
550pub(super) struct VulkanEncodeInput {
551    copier: alpha2_native_gpu::VulkanEncodeCopier,
552    planes: Vec<Nv12PlanePair>,
553    frames: AVHWFramesContext,
554    size: (u32, u32),
555    time_base: AVRational,
556    color: ColorMetadata,
557    stats: EncoderPoolStats,
558    /// The distinct images the pool has handed out, which is how many it
559    /// allocated: it recycles an image only once its last reference drops.
560    images: HashSet<u64>,
561}
562
563/// One frame taken from a Vulkan encoder-input pool.
564pub(super) struct VulkanEncoderFrame(AVFrame);
565
566impl VulkanEncoderFrame {
567    pub(super) fn set_pts(&mut self, pts: i64) {
568        self.0.set_pts(pts);
569    }
570}
571
572impl VulkanEncodeInput {
573    pub(super) fn descriptor(&self, generation: u64) -> EncoderSurfaceDescriptor {
574        EncoderSurfaceDescriptor {
575            backend: HardwareEncodeBackend::Vulkan,
576            pool_generation: generation,
577            layout: EncoderInputLayout::Nv12,
578            visible: self.size,
579            color: self.color,
580        }
581    }
582
583    /// Takes one frame from the encoder-input pool.
584    ///
585    /// `FFmpeg`'s pool recycles the image behind a frame once the last
586    /// reference to it is dropped: the host's, the encoder's, and the
587    /// copier's, which keeps each frame it copied into until its slot comes
588    /// round again.
589    pub(super) fn acquire_frame(&mut self) -> Result<VulkanEncoderFrame, EncodeError> {
590        let mut frame = AVFrame::new();
591        self.frames
592            .get_buffer(&mut frame)
593            .map_err(encode_adapter_error)?;
594        let image = alpha2_ffmpeg_bridge::vulkan_frame_info(&frame)
595            .map_err(encode_adapter_error)?
596            .images[0];
597        self.images.insert(image);
598        self.stats.acquired = self.stats.acquired.saturating_add(1);
599        self.stats.allocated = u64::try_from(self.images.len()).unwrap_or(u64::MAX);
600        Ok(VulkanEncoderFrame(frame))
601    }
602
603    /// Waits until the plane pair the next copy reads is free. The render that
604    /// packs the planes is submitted before [`Self::submit`]'s copy, on the
605    /// same queue, which is all that orders the two.
606    pub(super) fn begin_frame(&mut self) -> Result<InputFrame, EncodeError> {
607        let slot = self.copier.await_slot().map_err(encode_adapter_error)?;
608        if slot >= self.planes.len() {
609            return Err(EncodeError::Adapter(format!(
610                "encode copier reported slot {slot}"
611            )));
612        }
613        Ok(InputFrame { slot })
614    }
615
616    pub(super) fn planes(&self, frame: &InputFrame) -> (&wgpu::Texture, &wgpu::Texture) {
617        let pair = &self.planes[frame.slot];
618        (&pair.luma, &pair.chroma)
619    }
620
621    /// Copies `frame`'s planes into `backing`. The copy signals the frame's
622    /// own timeline semaphore and records the value on the frame, so the
623    /// encoder waits for it on the GPU and this call does not have to.
624    // Taking the token is the contract: a begun frame is submitted once.
625    #[allow(clippy::needless_pass_by_value)]
626    pub(super) fn submit(
627        &mut self,
628        frame: InputFrame,
629        backing: &mut VulkanEncoderFrame,
630    ) -> Result<(), EncodeError> {
631        let InputFrame { slot } = frame;
632        let pair = &self.planes[slot];
633        let mut access = alpha2_ffmpeg_bridge::VulkanEncodeFrameAccess::begin(&mut backing.0)
634            .map_err(encode_adapter_error)?;
635        self.copier
636            .submit(&mut access, &pair.luma, &pair.chroma, self.size)
637            .map_err(encode_adapter_error)?;
638        self.stats.copy_operations = self.stats.copy_operations.saturating_add(2);
639        self.stats.copy_bytes = self
640            .stats
641            .copy_bytes
642            .saturating_add(EncoderInputLayout::Nv12.frame_bytes(self.size.0, self.size.1));
643        Ok(())
644    }
645
646    /// Presentation timestamp for the frame at `time`, in encoder ticks.
647    pub(super) fn timestamp(&self, time: MediaTime) -> i64 {
648        av_rescale_q(time.ticks(), tick_base(), self.time_base)
649    }
650
651    pub(super) const fn stats(&self) -> EncoderPoolStats {
652        self.stats
653    }
654
655    /// Waits on every copy's fence before the plane textures it reads drop.
656    pub(super) fn finish(self) -> Result<(), EncodeError> {
657        let Self { copier, planes, .. } = self;
658        copier.finish().map_err(encode_adapter_error)?;
659        drop(planes);
660        Ok(())
661    }
662}
663
664impl VulkanEncodeSession {
665    pub(super) const fn selection(&self) -> &crate::NativeVideoEncoderSelection {
666        &self.selection
667    }
668
669    pub(super) fn stage_timings(&self) -> EncodeStageTimings {
670        self.muxer.with_audio_timings(self.stages)
671    }
672
673    /// Submits `frame` and muxes whatever packets it produced.
674    pub(super) fn encode(
675        &mut self,
676        generation: u64,
677        frame: &VulkanEncoderFrame,
678    ) -> Result<(), EncodeError> {
679        if generation != self.generation {
680            return Err(EncodeError::Adapter(format!(
681                "encoder surface from pool {generation} reached session {}",
682                self.generation
683            )));
684        }
685        timed(&mut self.stages.video_send_frame, || {
686            self.context.send_frame(Some(&frame.0))
687        })
688        .map_err(encode_adapter_error)?;
689        self.drain()
690    }
691
692    pub(super) fn write_audio(
693        &mut self,
694        block: &alpha2_media::AudioBlock,
695    ) -> Result<(), EncodeError> {
696        self.muxer.write_audio(block, &mut self.stages)
697    }
698
699    /// Flushes the audio and the encoder, then closes the output. Flushing
700    /// and closing are one step because only a closed output renames the
701    /// partial file into place.
702    pub(super) fn finish(&mut self) -> Result<(), EncodeError> {
703        // Every audio packet must exist before the muxer decides the final interleave.
704        self.muxer.finish_audio()?;
705        crate::encode::beat_encode_call();
706        self.context
707            .send_frame(None)
708            .map_err(encode_adapter_error)?;
709        self.drain()?;
710        self.muxer.write_ready_audio(true, &mut self.stages)?;
711        self.muxer.close()
712    }
713
714    pub(super) fn abort(&mut self) {
715        self.muxer.abort();
716    }
717
718    fn drain(&mut self) -> Result<(), EncodeError> {
719        loop {
720            let received = timed(&mut self.stages.video_receive_packet, || {
721                self.context.receive_packet()
722            });
723            match received {
724                Ok(mut packet) => self.muxer.write_video(&mut packet, self.time_base)?,
725                Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => {
726                    return self.muxer.write_ready_audio(false, &mut self.stages);
727                }
728                Err(error) => return Err(encode_adapter_error(error)),
729            }
730        }
731    }
732}
733
734/// Builds the encoder-input pool.
735///
736/// `avcodec_get_hw_frames_parameters` is a decode-side API and answers
737/// `ENOENT` for an encoder, so the pool is described here. `FFmpeg`'s Vulkan
738/// encoder requests no image usage of its own, so the pool is given the one
739/// an export uses (see `vulkan_frames_add_usage`) before it is initialized.
740fn vulkan_encode_frames(
741    device: &AVHWDeviceContext,
742    width: i32,
743    height: i32,
744) -> Result<AVHWFramesContext, EncodeError> {
745    let mut frames = device.hwframe_ctx_alloc();
746    {
747        let data = frames.data();
748        data.format = ffi::AV_PIX_FMT_VULKAN;
749        data.sw_format = ffi::AV_PIX_FMT_NV12;
750        data.width = width;
751        data.height = height;
752        data.initial_pool_size = 0;
753    }
754    alpha2_ffmpeg_bridge::vulkan_frames_add_usage(
755        &mut frames,
756        alpha2_ffmpeg_bridge::VULKAN_ENCODE_INPUT_USAGE,
757    )
758    .map_err(encode_adapter_error)?;
759    frames.init().map_err(encode_adapter_error)?;
760    Ok(frames)
761}
762
763fn encode_adapter_error_at_open(error: &RsmpegError) -> EncodeError {
764    EncodeError::Adapter(format!("h264_vulkan session could not open: {error}"))
765}