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::Mp4Muxer;
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 = Mp4Muxer::open(request.output, &context, time_base, request.audio)
366        .map_err(HardwareEncodeOpenError::Failed)?;
367    Ok((
368        VulkanEncodeSession {
369            context,
370            time_base,
371            muxer,
372            stages: EncodeStageTimings::default(),
373            selection,
374            generation,
375        },
376        input,
377    ))
378}
379
380/// One renderer-owned NV12 plane target for the encode handoff.
381///
382/// `COPY_SRC` is what gives the image `VK_IMAGE_USAGE_TRANSFER_SRC`, which the
383/// raw copy into the encoder frame reads from.
384fn plane_texture(
385    device: &wgpu::Device,
386    label: &'static str,
387    width: u32,
388    height: u32,
389    format: wgpu::TextureFormat,
390) -> wgpu::Texture {
391    device.create_texture(&wgpu::TextureDescriptor {
392        label: Some(label),
393        size: wgpu::Extent3d {
394            width,
395            height,
396            depth_or_array_layers: 1,
397        },
398        mip_level_count: 1,
399        sample_count: 1,
400        dimension: wgpu::TextureDimension::D2,
401        format,
402        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
403        view_formats: &[],
404    })
405}
406
407/// One slot of the NV12 plane ring the renderer packs into.
408struct Nv12PlanePair {
409    luma: wgpu::Texture,
410    chroma: wgpu::Texture,
411}
412
413/// Opens `h264_vulkan` on `device` with an NV12 encoder-input pool.
414fn open_encoder(
415    device: &AVHWDeviceContext,
416    width: u32,
417    height: u32,
418    rate_control: alpha2_media::VideoRateControl,
419    frame_rate: (i32, i32),
420    color: ColorMetadata,
421) -> Result<
422    (
423        AVHWFramesContext,
424        AVCodecContext,
425        crate::NativeVideoEncoderSelection,
426    ),
427    EncodeError,
428> {
429    let encoder = AVCodec::find_encoder_by_name(c"h264_vulkan").ok_or_else(|| {
430        EncodeError::Adapter("this FFmpeg runtime has no h264_vulkan encoder".into())
431    })?;
432    let coded_width =
433        i32::try_from(width).map_err(|_| EncodeError::Adapter("width is out of range".into()))?;
434    let coded_height =
435        i32::try_from(height).map_err(|_| EncodeError::Adapter("height is out of range".into()))?;
436    let frames = vulkan_encode_frames(device, coded_width, coded_height)?;
437    let rate = u32::try_from(frame_rate.0)
438        .ok()
439        .zip(u32::try_from(frame_rate.1).ok())
440        .and_then(|(num, den)| alpha2_time::FrameRate::new(num, den).ok())
441        .ok_or_else(|| EncodeError::Adapter("frame rate is out of range".into()))?;
442    let open = |quantized: Option<&crate::rate_control::MechanismRequest>| {
443        let mut context = AVCodecContext::new(&encoder);
444        context.set_width(coded_width);
445        context.set_height(coded_height);
446        context.set_pix_fmt(ffi::AV_PIX_FMT_VULKAN);
447        // `ff_encode_preinit` copies the pool's `sw_format` into `sw_pix_fmt`, and rejects the open
448        // if a caller set a different one.
449        context.set_time_base(ra(frame_rate.1, frame_rate.0));
450        context.set_framerate(ra(frame_rate.0, frame_rate.1));
451        context.set_bit_rate(quantized.map_or_else(
452            || i64::try_from(rate_control.bitrate).unwrap_or(i64::MAX),
453            |quantized| quantized.bit_rate,
454        ));
455        context.set_gop_size(crate::capabilities::keyframe_interval(rate));
456        // No B pictures, by measurement: devices allow them (an RTX 4090 reports 7 L0 / 1 L1
457        // references) and FFmpeg emits them, but on the 4K showcase at a fixed rate two B frames
458        // cost 0.34-0.63 dB PSNR-Y against none across clips and GOP lengths.
459        context.set_max_b_frames(0);
460        context.set_hw_frames_ctx(frames.clone());
461        // MP4 carries SPS and PPS in `avcC`. The flag is read at open, so it must be set before it,
462        // or the parameter sets travel in-band only.
463        context.set_flags(
464            context.flags | i32::try_from(ffi::AV_CODEC_FLAG_GLOBAL_HEADER).unwrap_or(0),
465        );
466        // Without these the stream has no VUI and decoders guess the colour; the software sinks tag
467        // their streams through the same options.
468        let color = crate::convert::color_options(color);
469        let options = match quantized {
470            Some(quantized) => quantized.dictionary(color),
471            None => crate::rate_control::rate_control_options("h264_vulkan", rate_control, color),
472        };
473        let unused = alpha2_ffmpeg_bridge::open_codec_context(&mut context, options)
474            .map_err(|error| encode_adapter_error_at_open(&error))?;
475        crate::convert::reject_unused_encode_options(unused)?;
476        if context.extradata_size <= 0 {
477            return Err(EncodeError::Adapter(
478                "h264_vulkan opened without SPS/PPS extradata for the MP4 header".into(),
479            ));
480        }
481        Ok::<_, EncodeError>(context)
482    };
483    // A refused quantizer falls back to the rate control's own bitrate on the same encoder, and the
484    // selection says which one runs.
485    let (context, applied, quantizer_refused) =
486        match crate::rate_control::quantizer_request("h264_vulkan", rate_control) {
487            Some(Ok(quantized)) => match open(Some(&quantized)) {
488                Ok(context) => (context, crate::AppliedRateControl::Quantizer, None),
489                Err(error) => (
490                    open(None)?,
491                    crate::AppliedRateControl::Bitrate,
492                    Some(format!("{} refused: {error}", quantized.describe())),
493                ),
494            },
495            Some(Err(reason)) => (
496                open(None)?,
497                crate::AppliedRateControl::Bitrate,
498                Some(reason),
499            ),
500            None => (open(None)?, crate::AppliedRateControl::Bitrate, None),
501        };
502    if let Some(reason) = &quantizer_refused {
503        alpha2_os::diagnostic!(
504            "alpha2 native export: h264_vulkan runs the bitrate fallback: {reason}"
505        );
506    }
507    // No candidate list: this encoder is chosen from the device, and anything that could refuse it
508    // already has.
509    let selection = crate::NativeVideoEncoderSelection {
510        encoder_name: "h264_vulkan".to_owned(),
511        class: crate::NativeEncoderClass::Hardware,
512        hardware_accelerated: true,
513        rejected: Vec::new(),
514        rate_control: applied,
515        quantizer_refused,
516        software_threads: None,
517    };
518    Ok((frames, context, selection))
519}
520
521/// The encode worker's half of a Vulkan session: the codec and the muxer.
522pub(super) struct VulkanEncodeSession {
523    context: AVCodecContext,
524    time_base: AVRational,
525    muxer: Mp4Muxer,
526    stages: EncodeStageTimings,
527    selection: crate::NativeVideoEncoderSelection,
528    generation: u64,
529}
530
531/// The coordinator's half of a Vulkan session.
532///
533/// The codec moves to an encode worker; the copy cannot follow it, because
534/// the wgpu plane textures and the render signal both belong to the thread
535/// that rendered them. This is what stays behind: the copier and its plane
536/// ring, and a reference to the encoder-input pool, which is reference
537/// counted, so a frame taken here stays valid for as long as the encoder
538/// needs it even though the two halves are on different threads.
539///
540/// Fields drop in declaration order, and the copier comes first: dropping it
541/// waits on every in-flight copy, retaining the images, encoder frames, and
542/// device together if that wait times out, since wgpu cannot track these raw
543/// copies. Dropping the planes first would free them under a running copy.
544pub(super) struct VulkanEncodeInput {
545    copier: alpha2_native_gpu::VulkanEncodeCopier,
546    planes: Vec<Nv12PlanePair>,
547    frames: AVHWFramesContext,
548    size: (u32, u32),
549    time_base: AVRational,
550    color: ColorMetadata,
551    stats: EncoderPoolStats,
552    /// The distinct images the pool has handed out, which is how many it
553    /// allocated: it recycles an image only once its last reference drops.
554    images: HashSet<u64>,
555}
556
557/// One frame taken from a Vulkan encoder-input pool.
558pub(super) struct VulkanEncoderFrame(AVFrame);
559
560impl VulkanEncoderFrame {
561    pub(super) fn set_pts(&mut self, pts: i64) {
562        self.0.set_pts(pts);
563    }
564}
565
566impl VulkanEncodeInput {
567    pub(super) fn descriptor(&self, generation: u64) -> EncoderSurfaceDescriptor {
568        EncoderSurfaceDescriptor {
569            backend: HardwareEncodeBackend::Vulkan,
570            pool_generation: generation,
571            layout: EncoderInputLayout::Nv12,
572            visible: self.size,
573            color: self.color,
574        }
575    }
576
577    /// Takes one frame from the encoder-input pool.
578    ///
579    /// `FFmpeg`'s pool recycles the image behind a frame once the last
580    /// reference to it is dropped: the host's, the encoder's, and the
581    /// copier's, which keeps each frame it copied into until its slot comes
582    /// round again.
583    pub(super) fn acquire_frame(&mut self) -> Result<VulkanEncoderFrame, EncodeError> {
584        let mut frame = AVFrame::new();
585        self.frames
586            .get_buffer(&mut frame)
587            .map_err(encode_adapter_error)?;
588        let image = alpha2_ffmpeg_bridge::vulkan_frame_info(&frame)
589            .map_err(encode_adapter_error)?
590            .images[0];
591        self.images.insert(image);
592        self.stats.acquired = self.stats.acquired.saturating_add(1);
593        self.stats.allocated = u64::try_from(self.images.len()).unwrap_or(u64::MAX);
594        Ok(VulkanEncoderFrame(frame))
595    }
596
597    /// Waits until the plane pair the next copy reads is free. The render that
598    /// packs the planes is submitted before [`Self::submit`]'s copy, on the
599    /// same queue, which is all that orders the two.
600    pub(super) fn begin_frame(&mut self) -> Result<InputFrame, EncodeError> {
601        let slot = self.copier.await_slot().map_err(encode_adapter_error)?;
602        if slot >= self.planes.len() {
603            return Err(EncodeError::Adapter(format!(
604                "encode copier reported slot {slot}"
605            )));
606        }
607        Ok(InputFrame { slot })
608    }
609
610    pub(super) fn planes(&self, frame: &InputFrame) -> (&wgpu::Texture, &wgpu::Texture) {
611        let pair = &self.planes[frame.slot];
612        (&pair.luma, &pair.chroma)
613    }
614
615    /// Copies `frame`'s planes into `backing`. The copy signals the frame's
616    /// own timeline semaphore and records the value on the frame, so the
617    /// encoder waits for it on the GPU and this call does not have to.
618    // Taking the token is the contract: a begun frame is submitted once.
619    #[allow(clippy::needless_pass_by_value)]
620    pub(super) fn submit(
621        &mut self,
622        frame: InputFrame,
623        backing: &mut VulkanEncoderFrame,
624    ) -> Result<(), EncodeError> {
625        let InputFrame { slot } = frame;
626        let pair = &self.planes[slot];
627        let mut access = alpha2_ffmpeg_bridge::VulkanEncodeFrameAccess::begin(&mut backing.0)
628            .map_err(encode_adapter_error)?;
629        self.copier
630            .submit(&mut access, &pair.luma, &pair.chroma, self.size)
631            .map_err(encode_adapter_error)?;
632        self.stats.copy_operations = self.stats.copy_operations.saturating_add(2);
633        self.stats.copy_bytes = self
634            .stats
635            .copy_bytes
636            .saturating_add(EncoderInputLayout::Nv12.frame_bytes(self.size.0, self.size.1));
637        Ok(())
638    }
639
640    /// Presentation timestamp for the frame at `time`, in encoder ticks.
641    pub(super) fn timestamp(&self, time: MediaTime) -> i64 {
642        av_rescale_q(time.ticks(), tick_base(), self.time_base)
643    }
644
645    pub(super) const fn stats(&self) -> EncoderPoolStats {
646        self.stats
647    }
648
649    /// Waits on every copy's fence before the plane textures it reads drop.
650    pub(super) fn finish(self) -> Result<(), EncodeError> {
651        let Self { copier, planes, .. } = self;
652        copier.finish().map_err(encode_adapter_error)?;
653        drop(planes);
654        Ok(())
655    }
656}
657
658impl VulkanEncodeSession {
659    pub(super) const fn selection(&self) -> &crate::NativeVideoEncoderSelection {
660        &self.selection
661    }
662
663    pub(super) fn stage_timings(&self) -> EncodeStageTimings {
664        self.muxer.with_audio_timings(self.stages)
665    }
666
667    /// Submits `frame` and muxes whatever packets it produced.
668    pub(super) fn encode(
669        &mut self,
670        generation: u64,
671        frame: &VulkanEncoderFrame,
672    ) -> Result<(), EncodeError> {
673        if generation != self.generation {
674            return Err(EncodeError::Adapter(format!(
675                "encoder surface from pool {generation} reached session {}",
676                self.generation
677            )));
678        }
679        timed(&mut self.stages.video_send_frame, || {
680            self.context.send_frame(Some(&frame.0))
681        })
682        .map_err(encode_adapter_error)?;
683        self.drain()
684    }
685
686    pub(super) fn write_audio(
687        &mut self,
688        block: &alpha2_media::AudioBlock,
689    ) -> Result<(), EncodeError> {
690        self.muxer.write_audio(block, &mut self.stages)
691    }
692
693    /// Flushes the audio and the encoder, then closes the output. Flushing
694    /// and closing are one step because only a closed output renames the
695    /// partial file into place.
696    pub(super) fn finish(&mut self) -> Result<(), EncodeError> {
697        // Every audio packet must exist before the muxer decides the final interleave.
698        self.muxer.finish_audio()?;
699        crate::encode::beat_encode_call();
700        self.context
701            .send_frame(None)
702            .map_err(encode_adapter_error)?;
703        self.drain()?;
704        self.muxer.write_ready_audio(true, &mut self.stages)?;
705        self.muxer.close()
706    }
707
708    pub(super) fn abort(&mut self) {
709        self.muxer.abort();
710    }
711
712    fn drain(&mut self) -> Result<(), EncodeError> {
713        loop {
714            let received = timed(&mut self.stages.video_receive_packet, || {
715                self.context.receive_packet()
716            });
717            match received {
718                Ok(mut packet) => self.muxer.write_video(&mut packet, self.time_base)?,
719                Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => {
720                    return self.muxer.write_ready_audio(false, &mut self.stages);
721                }
722                Err(error) => return Err(encode_adapter_error(error)),
723            }
724        }
725    }
726}
727
728/// Builds the encoder-input pool.
729///
730/// `avcodec_get_hw_frames_parameters` is a decode-side API and answers
731/// `ENOENT` for an encoder, so the pool is described here. `FFmpeg`'s Vulkan
732/// encoder requests no image usage of its own, so the pool is given the one
733/// an export uses (see `vulkan_frames_add_usage`) before it is initialized.
734fn vulkan_encode_frames(
735    device: &AVHWDeviceContext,
736    width: i32,
737    height: i32,
738) -> Result<AVHWFramesContext, EncodeError> {
739    let mut frames = device.hwframe_ctx_alloc();
740    {
741        let data = frames.data();
742        data.format = ffi::AV_PIX_FMT_VULKAN;
743        data.sw_format = ffi::AV_PIX_FMT_NV12;
744        data.width = width;
745        data.height = height;
746        data.initial_pool_size = 0;
747    }
748    alpha2_ffmpeg_bridge::vulkan_frames_add_usage(
749        &mut frames,
750        alpha2_ffmpeg_bridge::VULKAN_ENCODE_INPUT_USAGE,
751    )
752    .map_err(encode_adapter_error)?;
753    frames.init().map_err(encode_adapter_error)?;
754    Ok(frames)
755}
756
757fn encode_adapter_error_at_open(error: &RsmpegError) -> EncodeError {
758    EncodeError::Adapter(format!("h264_vulkan session could not open: {error}"))
759}