Skip to main content

alpha2_native_gpu/
vulkan.rs

1//! Vulkan interop: the device the renderer shares with `FFmpeg`'s decoder and
2//! encoder, the import of decoded frames as wgpu plane views with their layout
3//! transitions and timeline synchronization, the copy of rendered planes into
4//! encoder frames, and the device capability probes.
5
6use std::{
7    collections::VecDeque,
8    error::Error,
9    ffi::{CStr, c_void},
10    fmt,
11    ptr::NonNull,
12    sync::{
13        Arc, Mutex, Weak,
14        atomic::{AtomicBool, Ordering},
15    },
16};
17
18use alpha2_ffmpeg_bridge::{
19    BridgeError, VulkanDeviceInfo, VulkanFrameAccess, VulkanFrameAccessGuard, VulkanFrameLease,
20    VulkanQueueLock, create_vulkan_device_context,
21};
22use alpha2_gpu::{ExternalQueueLock, GpuContext};
23use ash::vk::{self, Handle};
24use rsmpeg::avutil::AVHWDeviceContext;
25use wgpu_hal::Instance as _;
26
27use crate::{NativeGpuIdentity, NativePlaneLayout};
28
29// Each helper that can fail returns 0 or the step that failed, and writes the platform status of
30// that step to `status` (`include/alpha2_native_gpu_status.h`).
31unsafe extern "C" {
32    fn alpha2_native_gpu_vulkan_probe(access: *mut VulkanFrameAccess, status: *mut i32) -> i32;
33    fn alpha2_native_gpu_vulkan_acquire_check(
34        access: *const VulkanFrameAccess,
35        status: *mut i32,
36    ) -> i32;
37    fn alpha2_native_gpu_vulkan_ticket_create(
38        device: u64,
39        allocator: u64,
40        queue_family: u32,
41        output: *mut *mut c_void,
42        status: *mut i32,
43    ) -> i32;
44    fn alpha2_native_gpu_vulkan_ticket_poll(ticket: *const c_void, status: *mut i32) -> i32;
45    fn alpha2_native_gpu_vulkan_ticket_reset(ticket: *mut c_void, status: *mut i32) -> i32;
46    fn alpha2_native_gpu_vulkan_acquire_submit(
47        access: *mut VulkanFrameAccess,
48        ticket: *mut c_void,
49        status: *mut i32,
50    ) -> i32;
51    fn alpha2_native_gpu_vulkan_release(
52        access: *mut VulkanFrameAccess,
53        ticket: *mut c_void,
54        status: *mut i32,
55    ) -> i32;
56    fn alpha2_native_gpu_vulkan_signal(
57        access: *mut VulkanFrameAccess,
58        semaphore: u64,
59        wait_value: u64,
60        signal_value: u64,
61        status: *mut i32,
62    ) -> i32;
63    fn alpha2_native_gpu_vulkan_import_destroy(ticket: *mut c_void);
64    fn alpha2_native_gpu_vulkan_import_retire(ticket: *mut c_void, timeout_ns: u64) -> i32;
65    #[cfg(test)]
66    fn alpha2_native_gpu_vulkan_retained_helpers() -> u64;
67    fn alpha2_native_gpu_vulkan_encode_copier_create(
68        device: u64,
69        queue_family: u32,
70        allocator: u64,
71        output: *mut *mut c_void,
72        status: *mut i32,
73    ) -> i32;
74    fn alpha2_native_gpu_vulkan_encode_copier_destroy(copier: *mut c_void, status: *mut i32)
75    -> i32;
76    fn alpha2_native_gpu_vulkan_encode_copier_slots() -> u32;
77    fn alpha2_native_gpu_vulkan_encode_copier_await_slot(
78        copier: *mut c_void,
79        slot: *mut u32,
80        status: *mut i32,
81    ) -> i32;
82    fn alpha2_native_gpu_vulkan_encode_copier_submit(
83        copier: *mut c_void,
84        access: *mut VulkanFrameAccess,
85        copy: *const VulkanEncodeCopy,
86        status: *mut i32,
87    ) -> i32;
88    fn alpha2_native_gpu_vulkan_set_wait_timeout(nanoseconds: u64);
89    fn alpha2_native_gpu_vulkan_wait_timeout() -> u64;
90}
91
92/// Sets how long the C helpers wait on a fence before giving up.
93pub(crate) fn set_wait_timeout(timeout: std::time::Duration) {
94    let nanoseconds = u64::try_from(timeout.as_nanos()).unwrap_or(u64::MAX);
95    // SAFETY: stores one integer the helpers read on each wait.
96    unsafe { alpha2_native_gpu_vulkan_set_wait_timeout(nanoseconds) };
97}
98
99/// How long the C helpers wait on a fence before giving up.
100fn wait_timeout() -> std::time::Duration {
101    // SAFETY: reads one integer.
102    std::time::Duration::from_nanos(unsafe { alpha2_native_gpu_vulkan_wait_timeout() })
103}
104
105/// The error for a Vulkan helper's `operation` that failed at `step` with
106/// the `VkResult` `status`.
107fn helper_error(operation: &'static str, step: i32, status: i32) -> BridgeError {
108    crate::status::helper_failure(
109        alpha2_ffmpeg_bridge::NativeGpuBackend::Vulkan,
110        operation,
111        step,
112        status,
113    )
114}
115
116/// Fails the Nth call of an import helper on this thread, as only a device
117/// fault would, so tests reach the branches that handle those failures.
118#[cfg(test)]
119pub(crate) mod inject {
120    use std::cell::Cell;
121
122    /// An import helper a test can fail.
123    #[derive(Clone, Copy, Debug)]
124    pub(crate) enum Hook {
125        AcquireSubmit,
126        TicketPoll,
127        TicketReset,
128        Release,
129    }
130
131    thread_local! {
132        static COUNTDOWNS: [Cell<u32>; 4] =
133            const { [Cell::new(0), Cell::new(0), Cell::new(0), Cell::new(0)] };
134        static STATUSES: [Cell<i32>; 4] =
135            const { [Cell::new(0), Cell::new(0), Cell::new(0), Cell::new(0)] };
136    }
137
138    /// Fails the `nth` (1-based) next call of `hook` on this thread with a
139    /// lost device.
140    pub(crate) fn fail_nth(hook: Hook, nth: u32) {
141        fail_nth_with(hook, nth, super::vk::Result::ERROR_DEVICE_LOST.as_raw());
142    }
143
144    /// Fails the `nth` (1-based) next call of `hook` on this thread with the
145    /// `VkResult` `status`.
146    pub(crate) fn fail_nth_with(hook: Hook, nth: u32, status: i32) {
147        COUNTDOWNS.with(|countdowns| countdowns[hook as usize].set(nth));
148        STATUSES.with(|statuses| statuses[hook as usize].set(status));
149    }
150
151    /// Whether this call of `hook` is the one to fail; a failure returns the
152    /// status the test chose at the step that call would have reached.
153    pub(super) fn fails(hook: Hook, step: i32, status: &mut i32) -> Option<i32> {
154        let due = COUNTDOWNS.with(|countdowns| {
155            let countdown = &countdowns[hook as usize];
156            match countdown.get() {
157                0 => false,
158                1 => {
159                    countdown.set(0);
160                    true
161                }
162                left => {
163                    countdown.set(left - 1);
164                    false
165                }
166            }
167        });
168        due.then(|| {
169            *status = STATUSES.with(|statuses| statuses[hook as usize].get());
170            step
171        })
172    }
173}
174
175/// An import's frame, locked, with the ticket whose commands move it.
176///
177/// A ticket's command pool, command buffers and fences are Vulkan objects the
178/// application must synchronize. Its lease uses them only through this, which
179/// holds the frame's lock (and with it the lock of the queue `FFmpeg` and the
180/// renderer share), so the lock of the one frame a ticket serves orders every
181/// use of it. The pool touches a ticket only before a lease takes it and after
182/// the lease's drop gives it back.
183struct LeaseAccess<'a> {
184    frame: VulkanFrameAccessGuard<'a>,
185    ticket: NonNull<c_void>,
186}
187
188/// Calls `alpha2_native_gpu_vulkan_acquire_submit`, or in tests fails first
189/// where [`inject`] asks.
190///
191/// # Safety
192/// As for the helper: `lease.ticket` was created for the frame's device and
193/// queue family, and `lease.frame` may record what the acquire submits.
194unsafe fn acquire_submit(lease: &mut LeaseAccess<'_>, status: &mut i32) -> i32 {
195    #[cfg(test)]
196    if let Some(step) = inject::fails(
197        inject::Hook::AcquireSubmit,
198        crate::interop::ALPHA2_GPU_STEP_QUEUE_SUBMIT,
199        status,
200    ) {
201        return step;
202    }
203    // SAFETY: the caller's contract; the guard holds the frame's and the queue's locks.
204    unsafe {
205        alpha2_native_gpu_vulkan_acquire_submit(
206            lease.frame.access_mut(),
207            lease.ticket.as_ptr(),
208            status,
209        )
210    }
211}
212
213/// Calls `alpha2_native_gpu_vulkan_release`, or in tests fails first where
214/// [`inject`] asks.
215///
216/// # Safety
217/// As for the helper: `lease.ticket` is the one the frame's acquire was
218/// submitted with, and `lease.frame` may record what the release submits.
219unsafe fn release(lease: &mut LeaseAccess<'_>, status: &mut i32) -> i32 {
220    #[cfg(test)]
221    if let Some(step) = inject::fails(
222        inject::Hook::Release,
223        crate::interop::ALPHA2_GPU_STEP_QUEUE_SUBMIT,
224        status,
225    ) {
226        return step;
227    }
228    // SAFETY: the caller's contract; the guard holds the frame's and the queue's locks.
229    unsafe {
230        alpha2_native_gpu_vulkan_release(lease.frame.access_mut(), lease.ticket.as_ptr(), status)
231    }
232}
233
234/// Calls `alpha2_native_gpu_vulkan_ticket_poll`, or in tests fails first
235/// where [`inject`] asks.
236///
237/// # Safety
238/// `ticket` is a live ticket the caller owns.
239unsafe fn ticket_poll(ticket: *const c_void, status: &mut i32) -> i32 {
240    #[cfg(test)]
241    if let Some(step) = inject::fails(
242        inject::Hook::TicketPoll,
243        crate::interop::ALPHA2_GPU_STEP_FENCE_STATUS,
244        status,
245    ) {
246        return step;
247    }
248    // SAFETY: the caller's contract.
249    unsafe { alpha2_native_gpu_vulkan_ticket_poll(ticket, status) }
250}
251
252/// Calls `alpha2_native_gpu_vulkan_ticket_reset`, or in tests fails first
253/// where [`inject`] asks.
254///
255/// # Safety
256/// `ticket` is a live ticket the caller owns, and nothing it submitted runs.
257unsafe fn ticket_reset(ticket: *mut c_void, status: &mut i32) -> i32 {
258    #[cfg(test)]
259    if let Some(step) = inject::fails(
260        inject::Hook::TicketReset,
261        crate::interop::ALPHA2_GPU_STEP_RESET_FENCE,
262        status,
263    ) {
264        return step;
265    }
266    // SAFETY: the caller's contract.
267    unsafe { alpha2_native_gpu_vulkan_ticket_reset(ticket, status) }
268}
269
270use crate::interop::Alpha2VulkanEncodeCopy as VulkanEncodeCopy;
271
272/// Reusable NV12 handoff from the renderer's plane textures to encoder frames.
273///
274/// Owns the command pool and the command-buffer ring, so a frame costs one
275/// queue submission and no CPU wait. One per export.
276///
277/// The copy is ordered after the render that packed the planes by being
278/// submitted after it on the same queue, not by waiting for a signal that
279/// render's submission raises: a submission `wgpu` rejects raises nothing,
280/// and a copy waiting for it would block the shared queue.
281pub struct VulkanEncodeCopier {
282    copier: *mut c_void,
283    /// The same lock `FFmpeg`'s own submissions and `GpuContext::submit` take.
284    /// This copy goes to the graphics queue wgpu uses, so it has to hold it.
285    queue_lock: alpha2_ffmpeg_bridge::VulkanQueueLock,
286    retained: Option<Box<RetainedEncodeWork>>,
287    close_failed: bool,
288}
289
290/// Raw Vulkan submissions are invisible to wgpu's resource tracking. Keep
291/// both ends of every copy and their device alive until its fence completes.
292struct RetainedEncodeWork {
293    copies: Vec<Option<EncodeCopyInputs>>,
294    _context: SharedVulkanContext,
295    #[cfg(test)]
296    lifetime_probe: Option<Arc<()>>,
297}
298
299struct EncodeCopyInputs {
300    _luma: wgpu::Texture,
301    _chroma: wgpu::Texture,
302    _frame: rsmpeg::avutil::AVFrame,
303}
304
305// SAFETY: the C object is only reached through `&mut self`, and the raw pointer is owned solely by
306// this value, which destroys it once.
307unsafe impl Send for VulkanEncodeCopier {}
308
309impl fmt::Debug for VulkanEncodeCopier {
310    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
311        formatter
312            .debug_struct("VulkanEncodeCopier")
313            .finish_non_exhaustive()
314    }
315}
316
317impl Drop for VulkanEncodeCopier {
318    fn drop(&mut self) {
319        if !self.copier.is_null() && !self.close_failed {
320            let _ = self.try_close();
321        }
322        if !self.copier.is_null() {
323            // The C helper did not finish: leaking only its handle would let Rust free what its
324            // commands still use, so keep the entire ownership graph, without waiting twice.
325            std::mem::forget(self.retained.take());
326        }
327    }
328}
329
330impl VulkanEncodeCopier {
331    /// Waits for outstanding copies and releases their resources.
332    ///
333    /// # Errors
334    /// On timeout or device loss, retains the pending GPU resources and device
335    /// rather than destroying images that raw Vulkan work may still use.
336    pub fn finish(mut self) -> Result<(), BridgeError> {
337        self.try_close()
338    }
339
340    fn try_close(&mut self) -> Result<(), BridgeError> {
341        let mut status = 0;
342        // SAFETY: created by the matching constructor and destroyed once; the C side waits for
343        // every in-flight copy before freeing anything.
344        let step =
345            unsafe { alpha2_native_gpu_vulkan_encode_copier_destroy(self.copier, &raw mut status) };
346        if step != 0 {
347            self.close_failed = true;
348            return Err(helper_error("encode copier teardown", step, status));
349        }
350        self.copier = std::ptr::null_mut();
351        // Keep the device until the queue and FFmpeg queue-lock references have dropped too;
352        // `retained` is deliberately the last owning field.
353        Ok(())
354    }
355}
356
357impl VulkanEncodeCopier {
358    /// Creates the handoff for one export on `context`'s device.
359    ///
360    /// # Errors
361    /// Returns an error when the command pool, command buffers or fences
362    /// cannot be created, or the device has no queue lock for its graphics
363    /// family.
364    pub fn new(context: &SharedVulkanContext) -> Result<Self, BridgeError> {
365        let gpu = context.gpu();
366        // SAFETY: this context is always Vulkan; the handle is only read.
367        let device = unsafe { gpu.device().as_hal::<wgpu_hal::api::Vulkan>() }
368            .map(|device| device.raw_device().handle().as_raw())
369            .ok_or(BridgeError::InvalidInput("renderer is not Vulkan"))?;
370        let mut copier = std::ptr::null_mut();
371        let mut status = 0;
372        // SAFETY: `device` is this renderer's live logical device and the output pointers are valid
373        // locals.
374        let step = unsafe {
375            alpha2_native_gpu_vulkan_encode_copier_create(
376                device,
377                0,
378                0,
379                &raw mut copier,
380                &raw mut status,
381            )
382        };
383        if step != 0 {
384            return Err(helper_error("encode copier creation", step, status));
385        }
386        Ok(Self {
387            copier,
388            queue_lock: alpha2_ffmpeg_bridge::VulkanQueueLock::new(context.ffmpeg_device(), 0)?,
389            retained: Some(Box::new(RetainedEncodeWork {
390                copies: (0..Self::slots()).map(|_| None).collect(),
391                _context: context.clone(),
392                #[cfg(test)]
393                lifetime_probe: None,
394            })),
395            close_failed: false,
396        })
397    }
398
399    /// Plane-texture pairs a caller must cycle through to feed this copier.
400    ///
401    /// One pair is only safe while the copy and the next render are ordered on
402    /// one queue by a caller that never runs ahead. A caller that hands the
403    /// encode to another thread does run ahead, and then the pairs have to be
404    /// a ring as deep as the command-buffer ring this copier already keeps.
405    #[must_use]
406    pub fn slots() -> usize {
407        // SAFETY: a constant read out of the shim; it touches no state.
408        unsafe { alpha2_native_gpu_vulkan_encode_copier_slots() as usize }
409    }
410
411    /// Waits until the slot the next copy will use is free, and names it.
412    ///
413    /// Call before recording the render that writes the plane textures, and
414    /// render into the pair this returns. The wait is on the fence of the copy
415    /// four submissions ago, so in a pipeline that is keeping up it is free.
416    ///
417    /// # Errors
418    /// Returns an error when the fence wait fails.
419    pub fn await_slot(&mut self) -> Result<usize, BridgeError> {
420        let (mut slot, mut status) = (0_u32, 0);
421        // SAFETY: the copier is live and the output pointers are valid locals.
422        let step = unsafe {
423            alpha2_native_gpu_vulkan_encode_copier_await_slot(
424                self.copier,
425                &raw mut slot,
426                &raw mut status,
427            )
428        };
429        if step != 0 {
430            return Err(helper_error("encode copy slot", step, status));
431        }
432        if let Some(retained) = self.retained.as_mut() {
433            retained.copies[slot as usize] = None;
434        }
435        Ok(slot as usize)
436    }
437
438    /// Copies a renderer-packed NV12 pair into an encoder frame's image.
439    ///
440    /// The submission that wrote these textures must already be on the
441    /// renderer's queue: the copy is ordered after it by queue order alone.
442    /// When that submission was rejected the copy still runs, over whatever
443    /// the planes held, and the frame is the caller's to fail.
444    ///
445    /// # Errors
446    /// Returns an error when the frame is not one multi-planar image on this
447    /// device, the textures are not Vulkan, or the submission fails.
448    pub fn submit(
449        &mut self,
450        frame: &mut alpha2_ffmpeg_bridge::VulkanEncodeFrameAccess<'_>,
451        luma: &wgpu::Texture,
452        chroma: &wgpu::Texture,
453        size: (u32, u32),
454    ) -> Result<(), BridgeError> {
455        let slot = self.await_slot()?;
456        let inputs = EncodeCopyInputs {
457            _luma: luma.clone(),
458            _chroma: chroma.clone(),
459            _frame: frame.retain_frame()?,
460        };
461        let kept = self
462            .retained
463            .as_mut()
464            .and_then(|work| work.copies.get_mut(slot))
465            .ok_or(BridgeError::InvalidInput("invalid encode copier slot"))?;
466        // SAFETY: both textures belong to this Vulkan device and the handles are only read; wgpu
467        // keeps owning and destroying them.
468        let luma_image = unsafe { luma.as_hal::<wgpu_hal::api::Vulkan>() }
469            .map(|texture| unsafe { texture.raw_handle() }.as_raw())
470            .ok_or(BridgeError::InvalidInput("luma plane is not Vulkan"))?;
471        // SAFETY: as above.
472        let chroma_image = unsafe { chroma.as_hal::<wgpu_hal::api::Vulkan>() }
473            .map(|texture| unsafe { texture.raw_handle() }.as_raw())
474            .ok_or(BridgeError::InvalidInput("chroma plane is not Vulkan"))?;
475        let copy = VulkanEncodeCopy {
476            luma_image,
477            chroma_image,
478            width: size.0,
479            height: size.1,
480        };
481        // The copy submits to the graphics queue wgpu and FFmpeg share, and every other submission
482        // to it holds this lock. The plane textures it reads are protected by the caller's slot
483        // ring rather than by queue order, because a caller whose encode runs on another thread can
484        // be several frames ahead by the time this executes.
485        let queue = self.queue_lock.lock();
486        let mut status = 0;
487        // SAFETY: the guard holds FFmpeg's frame lock for the whole call, and the shim validates
488        // every handle it reads out of the descriptor.
489        let step = unsafe {
490            alpha2_native_gpu_vulkan_encode_copier_submit(
491                self.copier,
492                frame.access_mut(),
493                &raw const copy,
494                &raw mut status,
495            )
496        };
497        drop(queue);
498        if step != 0 {
499            return Err(helper_error("encode copy", step, status));
500        }
501        *kept = Some(inputs);
502        frame.commit();
503        Ok(())
504    }
505}
506
507/// Checks that a decoded frame can be imported: moves its planes to
508/// `SHADER_READ_ONLY` and back in one submission on its queue, through views
509/// like an import's, and waits for it.
510///
511/// # Errors
512/// Returns an error when image-view creation, synchronization, or submission fails.
513pub fn probe_vulkan_submission(lease: &VulkanFrameLease) -> Result<(), BridgeError> {
514    let mut guard = lease.lock_access()?;
515    let mut status = 0;
516    // SAFETY: the bridge guard holds the FFmpeg frame and queue locks, and the C helper only uses
517    // the descriptor for this synchronous call. It changes only the timeline values, to the ones
518    // its submission signals, and only once it has submitted; the layouts it returns to are the
519    // frame's.
520    let step = unsafe { alpha2_native_gpu_vulkan_probe(guard.access_mut(), &raw mut status) };
521    // Written back on failure too: a submission whose wait timed out still signals when it runs,
522    // and the frame's next user must signal above it.
523    guard.commit();
524    if step != 0 {
525        return Err(helper_error("decode probe", step, status));
526    }
527    Ok(())
528}
529
530/// Coded extents and coding limits one video-encode profile has on a device.
531#[derive(Clone, Debug, Eq, PartialEq)]
532pub struct VideoEncodeExtent {
533    /// The H.264 profile these are the limits of: the one `h264_vulkan`
534    /// opens its session with on this device (`constrained_baseline`,
535    /// `main`, or `high`).
536    pub profile: &'static str,
537    /// Smallest accepted width and height, in samples.
538    pub minimum: (u32, u32),
539    /// Largest accepted width and height, in samples.
540    pub maximum: (u32, u32),
541    /// Granularity the coded extent is aligned up to before the limits apply:
542    /// `VkVideoEncodeCapabilitiesKHR::encodeInputPictureGranularity`, which
543    /// `FFmpeg` aligns its coded size to.
544    ///
545    /// This is why a 158x62 output is accepted where 64x48 is not: both are
546    /// below the minimum as written, but the first aligns up to the minimum
547    /// and the second does not. Comparing an output size against `minimum`
548    /// without aligning it first gets that case wrong in the strict direction.
549    pub granularity: (u32, u32),
550    /// `VkVideoEncodeH264CapabilitiesKHR::maxPPictureL0ReferenceCount`: how
551    /// many references a P picture may use.
552    pub max_p_picture_l0_references: u32,
553    /// `VkVideoEncodeH264CapabilitiesKHR::maxBPictureL0ReferenceCount`: the
554    /// forward references a B picture may use. Zero, together with
555    /// `max_l1_references`, means the device encodes no B pictures.
556    pub max_b_picture_l0_references: u32,
557    /// `VkVideoEncodeH264CapabilitiesKHR::maxL1ReferenceCount`: the backward
558    /// references a B picture may use.
559    pub max_l1_references: u32,
560    /// `VkVideoEncodeCapabilitiesKHR::rateControlModes`, as the mode names
561    /// the device reports: `cbr`, `vbr`, `disabled` (constant QP).
562    pub rate_control_modes: Vec<&'static str>,
563}
564
565impl VideoEncodeExtent {
566    /// Whether the device can encode B pictures at all.
567    #[must_use]
568    pub const fn supports_b_pictures(&self) -> bool {
569        self.max_b_picture_l0_references > 0 || self.max_l1_references > 0
570    }
571}
572
573impl VideoEncodeExtent {
574    /// Whether an output of this size reaches the encoder's coded limits.
575    #[must_use]
576    pub const fn accepts(&self, width: u32, height: u32) -> bool {
577        let coded = self.coded(width, height);
578        coded.0 >= self.minimum.0
579            && coded.1 >= self.minimum.1
580            && coded.0 <= self.maximum.0
581            && coded.1 <= self.maximum.1
582    }
583
584    /// The coded extent an output of this size is encoded at.
585    #[must_use]
586    pub const fn coded(&self, width: u32, height: u32) -> (u32, u32) {
587        (
588            align_up(width, self.granularity.0),
589            align_up(height, self.granularity.1),
590        )
591    }
592}
593
594const fn align_up(value: u32, granularity: u32) -> u32 {
595    if granularity <= 1 {
596        return value;
597    }
598    value.div_ceil(granularity).saturating_mul(granularity)
599}
600
601/// The H.264 profiles `FFmpeg`'s `h264_vulkan` tries for 8-bit 4:2:0 input,
602/// in its order. With no profile asked for, which nothing here does, it
603/// opens the session with the last one the device supports
604/// (`vulkan_encode_h264.c`, `init_profile`): `high` on every device that
605/// has it, not `main`.
606const H264_ENCODE_PROFILES: [(vk::native::StdVideoH264ProfileIdc, &str); 3] = [
607    (
608        vk::native::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_BASELINE,
609        "constrained_baseline",
610    ),
611    (
612        vk::native::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_MAIN,
613        "main",
614    ),
615    (
616        vk::native::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_HIGH,
617        "high",
618    ),
619];
620
621/// What `query` answers for the last of `H264_ENCODE_PROFILES` it answers
622/// at all: the profile `h264_vulkan` would open, and its answer.
623fn last_supported_profile<T>(
624    mut query: impl FnMut(vk::native::StdVideoH264ProfileIdc) -> Option<T>,
625) -> Option<(&'static str, T)> {
626    H264_ENCODE_PROFILES
627        .iter()
628        .filter_map(|&(idc, name)| query(idc).map(|answer| (name, answer)))
629        .last()
630}
631
632/// Queries the coded extents this device's 8-bit 4:2:0 H.264 encoder accepts,
633/// for the profile `h264_vulkan` opens on it.
634///
635/// Asked of the physical device rather than of `FFmpeg`, because `FFmpeg` only
636/// answers by failing to open a session at a size, after the fact, and a
637/// caller needs the answer before it exports.
638///
639/// Only call this for a device that exposes `VK_KHR_video_queue`,
640/// `VK_KHR_video_encode_queue`, and `VK_KHR_video_encode_h264`.
641fn h264_encode_extent(
642    entry: &ash::Entry,
643    instance: &ash::Instance,
644    physical: vk::PhysicalDevice,
645) -> Option<VideoEncodeExtent> {
646    let video = ash::khr::video_queue::Instance::new(entry, instance);
647    let (profile, mut extent) = last_supported_profile(|idc| {
648        let mut h264 = vk::VideoEncodeH264ProfileInfoKHR::default().std_profile_idc(idc);
649        let profile = vk::VideoProfileInfoKHR::default()
650            .video_codec_operation(vk::VideoCodecOperationFlagsKHR::ENCODE_H264)
651            .chroma_subsampling(vk::VideoChromaSubsamplingFlagsKHR::TYPE_420)
652            .luma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::TYPE_8)
653            .chroma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::TYPE_8)
654            .push_next(&mut h264);
655        let mut h264_capabilities = vk::VideoEncodeH264CapabilitiesKHR::default();
656        let mut encode = vk::VideoEncodeCapabilitiesKHR::default();
657        let mut capabilities = vk::VideoCapabilitiesKHR::default()
658            .push_next(&mut encode)
659            .push_next(&mut h264_capabilities);
660        // SAFETY: the profile chain lives for the call and the device is this instance's. The
661        // caller has checked that the device exposes `VK_KHR_video_queue`,
662        // `VK_KHR_video_encode_queue`, and `VK_KHR_video_encode_h264`; without them the loader
663        // dispatches to a null driver entry point. With them, a device that lacks this profile
664        // answers with an error rather than writing anything.
665        let result = unsafe {
666            (video.fp().get_physical_device_video_capabilities_khr)(
667                physical,
668                &raw const profile,
669                &raw mut capabilities,
670            )
671        };
672        if result != vk::Result::SUCCESS {
673            return None;
674        }
675        let coded = (capabilities.min_coded_extent, capabilities.max_coded_extent);
676        Some(encode_extent(coded, &encode, &h264_capabilities))
677    })?;
678    extent.profile = profile;
679    Some(extent)
680}
681
682/// The limits one profile's capabilities describe: `coded`, its minimum and
683/// maximum coded extents, and its encode and H.264 capabilities. `profile`
684/// is left for the caller to name.
685fn encode_extent(
686    (minimum, maximum): (vk::Extent2D, vk::Extent2D),
687    encode: &vk::VideoEncodeCapabilitiesKHR<'_>,
688    h264: &vk::VideoEncodeH264CapabilitiesKHR<'_>,
689) -> VideoEncodeExtent {
690    VideoEncodeExtent {
691        profile: "",
692        minimum: (minimum.width, minimum.height),
693        maximum: (maximum.width, maximum.height),
694        // What FFmpeg aligns the coded size to before it checks the limits (`vulkan_encode.c`), not
695        // `pictureAccessGranularity`: on Mesa ANV the two are 32 and 16.
696        granularity: (
697            encode.encode_input_picture_granularity.width.max(1),
698            encode.encode_input_picture_granularity.height.max(1),
699        ),
700        max_p_picture_l0_references: h264.max_p_picture_l0_reference_count,
701        max_b_picture_l0_references: h264.max_b_picture_l0_reference_count,
702        max_l1_references: h264.max_l1_reference_count,
703        rate_control_modes: [
704            (vk::VideoEncodeRateControlModeFlagsKHR::CBR, "cbr"),
705            (vk::VideoEncodeRateControlModeFlagsKHR::VBR, "vbr"),
706            (vk::VideoEncodeRateControlModeFlagsKHR::DISABLED, "disabled"),
707        ]
708        .into_iter()
709        .filter(|(flag, _)| encode.rate_control_modes.contains(*flag))
710        .map(|(_, name)| name)
711        .collect(),
712    }
713}
714
715/// A Vulkan layer's specification version.
716#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
717pub struct VulkanLayerVersion {
718    /// Major version.
719    pub major: u32,
720    /// Minor version.
721    pub minor: u32,
722    /// Patch version (the SDK release).
723    pub patch: u32,
724}
725
726impl VulkanLayerVersion {
727    /// The first Khronos validation layer `FFmpeg`'s video sessions survive.
728    ///
729    /// Older ones crash or hang inside the session setup, on the `pNext`
730    /// chain `FFmpeg` builds for it: a decode session as an encode one.
731    pub const VIDEO_SESSIONS: Self = Self {
732        major: 1,
733        minor: 4,
734        patch: 0,
735    };
736
737    /// Unpacks a `VK_MAKE_API_VERSION` value.
738    #[must_use]
739    pub const fn from_packed(version: u32) -> Self {
740        Self {
741            major: vk::api_version_major(version),
742            minor: vk::api_version_minor(version),
743            patch: vk::api_version_patch(version),
744        }
745    }
746}
747
748impl fmt::Display for VulkanLayerVersion {
749    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
750        write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
751    }
752}
753
754const KHRONOS_VALIDATION: &str = "VK_LAYER_KHRONOS_validation";
755
756/// The version of the Khronos validation layer the loader activated on
757/// `instance`, or `None` when it activated none.
758///
759/// Asked of the loader rather than worked out from the environment.
760/// `vkEnumerateDeviceLayerProperties` is deprecated for device layers, and
761/// what every loader returns from it instead is the instance's activated
762/// layer list: the layers the application enabled (wgpu enables this one
763/// when its validation flag is set, as in every debug build), the ones
764/// `VK_INSTANCE_LAYERS` and `VK_LOADER_LAYERS_ENABLE` add, and none that
765/// `VK_LOADER_LAYERS_DISABLE` takes away, each by that loader's own rules.
766/// Reimplementing those rules is easy to get wrong both ways: the loader's
767/// globs ignore case and have four forms only, `~explicit~` enables nothing,
768/// and a loader before 1.3.234 reads neither filter variable.
769fn activated_validation_layer(
770    instance: &ash::Instance,
771    physical: vk::PhysicalDevice,
772) -> Option<VulkanLayerVersion> {
773    // SAFETY: `physical` was enumerated from `instance`, which is alive for the call.
774    let layers = unsafe { instance.enumerate_device_layer_properties(physical) }.ok()?;
775    layers
776        .iter()
777        .find(|layer| {
778            layer
779                .layer_name_as_c_str()
780                .is_ok_and(|name| name.to_bytes() == KHRONOS_VALIDATION.as_bytes())
781        })
782        .map(|layer| VulkanLayerVersion::from_packed(layer.spec_version))
783}
784
785/// The Vulkan-only HAL instance the shared device is made on.
786fn hal_instance() -> Result<wgpu_hal::vulkan::Instance, SharedVulkanError> {
787    let hal_desc = wgpu_hal::InstanceDescriptor {
788        name: "alpha2 shared Vulkan decode",
789        flags: wgpu::InstanceFlags::from_env_or_default(),
790        memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
791        backend_options: wgpu::BackendOptions::from_env_or_default(),
792        telemetry: None,
793        display: None,
794    };
795    // SAFETY: the returned HAL instance owns its loader and Vulkan instance.
796    unsafe {
797        wgpu_hal::vulkan::Instance::init(&hal_desc)
798            .map_err(|error| SharedVulkanError(format!("Vulkan instance: {error}")))
799    }
800}
801
802/// Holds the shared graphics-queue lock for the renderer's submissions.
803///
804/// Private to this module and only ever handed to the [`GpuContext`] it is
805/// made for, which takes it around each queue call and releases it on the
806/// same thread, innermost hold first, before that call returns (its
807/// `QueueGuard` and `QueueLockSlot`). That pairing is the contract of the raw
808/// lock below; no other code can reach this value to break it.
809#[derive(Debug)]
810struct SharedQueueLock(VulkanQueueLock);
811
812impl ExternalQueueLock for SharedQueueLock {
813    fn lock(&self) {
814        // SAFETY: the context this is handed to releases each hold with one `unlock` on the same
815        // thread, in reverse order (see above).
816        unsafe { self.0.raw_lock() };
817    }
818
819    fn unlock(&self) {
820        // SAFETY: the context calls this only to release its own innermost hold, on the thread that
821        // took it (see above).
822        unsafe { self.0.raw_unlock() };
823    }
824}
825
826/// Failure while creating a logical Vulkan device shared by wgpu and `FFmpeg`.
827#[derive(Debug)]
828pub struct SharedVulkanError(String);
829
830impl fmt::Display for SharedVulkanError {
831    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
832        formatter.write_str(&self.0)
833    }
834}
835
836impl Error for SharedVulkanError {}
837
838/// An imported bi-planar 4:2:0 image and its two wgpu plane views.
839#[derive(Debug)]
840pub struct ImportedVulkanFrame {
841    /// Plane-zero luma view.
842    pub y: wgpu::TextureView,
843    /// Plane-one interleaved chroma view.
844    pub uv: wgpu::TextureView,
845    /// Sample layout of the imported planes.
846    pub layout: NativePlaneLayout,
847    /// Allocated image extent, which the visible rectangle is placed inside.
848    ///
849    /// This is the codec's padded coded size, not the display size, and it is
850    /// the extent presentation must normalize sample coordinates against.
851    pub coded_size: [u32; 2],
852    /// Submission/lifetime lease for the decoder surface.
853    pub lease: Arc<VulkanImportLease>,
854}
855
856/// Allocation counters of a context's import tickets.
857#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
858pub struct ImportTicketStats {
859    /// Tickets created: a command pool, two command buffers, and a fence each.
860    pub created: u64,
861    /// Imports served by a finished, reset ticket.
862    pub reused: u64,
863    /// Tickets reset and ready for an import.
864    pub idle: usize,
865    /// Tickets whose release may still be running.
866    pub pending: usize,
867    /// Tickets given up on because their device could not report them finished.
868    pub abandoned: u64,
869}
870
871/// One import ticket and the device and queue family it was made for.
872struct PooledTicket {
873    ticket: NonNull<c_void>,
874    device: u64,
875    queue_family: u32,
876}
877
878/// A returned ticket waits in `pending` until the fences of its acquire and release, whichever it
879/// submitted, are seen signalled, polled without blocking at the next import, and only then is
880/// reset and reused. A ticket whose device cannot say, or that is still running when too many wait,
881/// is never reused: it is destroyed after a bounded wait or, failing that, leaked, which is the
882/// safe loss. Those waits happen outside the pool's lock, and the tickets retired together (the
883/// pool's drop retires every pending one) share one wait timeout, so a device that is hung but not
884/// lost costs the timeout once, not once per ticket.
885#[derive(Default)]
886struct ImportTicketPool {
887    state: Mutex<ImportTicketPoolState>,
888}
889
890#[derive(Default)]
891struct ImportTicketPoolState {
892    idle: Vec<PooledTicket>,
893    pending: VecDeque<PooledTicket>,
894    created: u64,
895    reused: u64,
896    abandoned: u64,
897}
898
899// Frames in flight bound how many tickets an export uses at once; these bound what an unusual burst
900// leaves behind.
901const MAX_IDLE_IMPORT_TICKETS: usize = 8;
902const MAX_PENDING_IMPORT_TICKETS: usize = 32;
903
904// SAFETY: a ticket is only touched by the pool under its mutex, or by the one lease that took it;
905// Vulkan's own objects are externally synchronized by that single ownership.
906unsafe impl Send for ImportTicketPool {}
907// SAFETY: as for `Send`: every access goes through the mutex.
908unsafe impl Sync for ImportTicketPool {}
909
910impl ImportTicketPool {
911    fn take(
912        &self,
913        device: u64,
914        allocator: u64,
915        queue_family: u32,
916    ) -> Result<PooledTicket, BridgeError> {
917        let mut state = self
918            .state
919            .lock()
920            .unwrap_or_else(std::sync::PoisonError::into_inner);
921        state.reclaim();
922        if let Some(index) = state
923            .idle
924            .iter()
925            .position(|idle| idle.device == device && idle.queue_family == queue_family)
926        {
927            state.reused = state.reused.saturating_add(1);
928            return Ok(state.idle.swap_remove(index));
929        }
930        state.created = state.created.saturating_add(1);
931        drop(state);
932        let (mut raw, mut status) = (std::ptr::null_mut(), 0);
933        // SAFETY: `device` and `allocator` are the frame's own, read under its lock; the output
934        // ticket is uniquely owned by the caller.
935        let step = unsafe {
936            alpha2_native_gpu_vulkan_ticket_create(
937                device,
938                allocator,
939                queue_family,
940                &raw mut raw,
941                &raw mut status,
942            )
943        };
944        if step != 0 {
945            return Err(helper_error("import ticket creation", step, status));
946        }
947        NonNull::new(raw)
948            .map(|ticket| PooledTicket {
949                ticket,
950                device,
951                queue_family,
952            })
953            .ok_or(BridgeError::InvalidInput(
954                "Vulkan import ticket creation returned no ticket",
955            ))
956    }
957
958    /// Takes back a ticket, whatever it has submitted: each submission carries
959    /// a fence the pool polls before the ticket is reused.
960    fn give_back(&self, ticket: PooledTicket) {
961        let mut state = self
962            .state
963            .lock()
964            .unwrap_or_else(std::sync::PoisonError::into_inner);
965        state.pending.push_back(ticket);
966        if state.pending.len() > MAX_PENDING_IMPORT_TICKETS {
967            state.reclaim();
968        }
969        let excess = state
970            .pending
971            .len()
972            .saturating_sub(MAX_PENDING_IMPORT_TICKETS);
973        let oldest = state.pending.drain(..excess).collect::<Vec<_>>();
974        state.abandoned = state.abandoned.saturating_add(oldest.len() as u64);
975        // Imports of the device go on while these wait.
976        drop(state);
977        retire(oldest);
978    }
979
980    fn stats(&self) -> ImportTicketStats {
981        let state = self
982            .state
983            .lock()
984            .unwrap_or_else(std::sync::PoisonError::into_inner);
985        ImportTicketStats {
986            created: state.created,
987            reused: state.reused,
988            idle: state.idle.len(),
989            pending: state.pending.len(),
990            abandoned: state.abandoned,
991        }
992    }
993}
994
995impl ImportTicketPoolState {
996    /// Resets every pending ticket whose release has finished and makes it
997    /// idle, without waiting on any.
998    fn reclaim(&mut self) {
999        let mut index = 0;
1000        while index < self.pending.len() {
1001            let ticket = self.pending[index].ticket;
1002            let mut status = 0;
1003            // SAFETY: the pool owns every pending ticket.
1004            match unsafe { ticket_poll(ticket.as_ptr(), &mut status) } {
1005                0 => index += 1,
1006                1 => {
1007                    let Some(finished) = self.pending.remove(index) else {
1008                        break;
1009                    };
1010                    // SAFETY: nothing the ticket submitted is still running.
1011                    let reset = unsafe { ticket_reset(ticket.as_ptr(), &mut status) };
1012                    if reset != 0 {
1013                        self.abandoned = self.abandoned.saturating_add(1);
1014                    } else if self.idle.len() < MAX_IDLE_IMPORT_TICKETS {
1015                        self.idle.push(finished);
1016                    } else {
1017                        // SAFETY: finished and reset, so nothing uses it.
1018                        unsafe { alpha2_native_gpu_vulkan_import_destroy(ticket.as_ptr()) };
1019                    }
1020                }
1021                _ => {
1022                    // The device cannot say whether it finished: never reuse it, and leak it rather
1023                    // than free it running.
1024                    self.pending.remove(index);
1025                    self.abandoned = self.abandoned.saturating_add(1);
1026                }
1027            }
1028        }
1029    }
1030}
1031
1032impl Drop for ImportTicketPool {
1033    fn drop(&mut self) {
1034        let state = self
1035            .state
1036            .get_mut()
1037            .unwrap_or_else(std::sync::PoisonError::into_inner);
1038        for idle in state.idle.drain(..) {
1039            // SAFETY: idle tickets are reset and unused.
1040            unsafe { alpha2_native_gpu_vulkan_import_destroy(idle.ticket.as_ptr()) };
1041        }
1042        retire(state.pending.drain(..));
1043    }
1044}
1045
1046/// Destroys tickets the pool owns once what they submitted has finished, all within one wait
1047/// timeout from now; a ticket still unfinished when it runs out is leaked, not freed under the GPU.
1048fn retire(tickets: impl IntoIterator<Item = PooledTicket>) {
1049    let deadline = std::time::Instant::now().checked_add(wait_timeout());
1050    for pooled in tickets {
1051        let left = deadline.map_or(std::time::Duration::MAX, |deadline| {
1052            deadline.saturating_duration_since(std::time::Instant::now())
1053        });
1054        let nanoseconds = u64::try_from(left.as_nanos()).unwrap_or(u64::MAX);
1055        // SAFETY: the pool owns the ticket and gives it up here; the helper frees it only once its
1056        // fences have signalled.
1057        unsafe { alpha2_native_gpu_vulkan_import_retire(pooled.ticket.as_ptr(), nanoseconds) };
1058    }
1059}
1060
1061/// Owns the imported texture, `FFmpeg` frame, and native transition commands.
1062pub struct VulkanImportLease {
1063    frame: Arc<VulkanFrameLease>,
1064    _texture: wgpu::Texture,
1065    gpu: GpuContext,
1066    batch: Arc<VulkanSubmissionBatch>,
1067    tickets: Arc<ImportTicketPool>,
1068    /// Taken only by the drop, which gives it back to `tickets`; used only
1069    /// through [`Self::lock`].
1070    ticket: Option<PooledTicket>,
1071    staged_signals: Mutex<Vec<StagedSignal>>,
1072    queued: AtomicBool,
1073    acquired: AtomicBool,
1074    released: AtomicBool,
1075    retained_bytes: u64,
1076}
1077
1078#[derive(Clone, Copy, Debug)]
1079struct StagedSignal {
1080    semaphore: u64,
1081    acquired_value: u64,
1082    signal_value: u64,
1083}
1084
1085#[derive(Default)]
1086struct VulkanSubmissionBatch {
1087    pending: Mutex<Vec<Weak<VulkanImportLease>>>,
1088}
1089
1090impl VulkanSubmissionBatch {
1091    fn enqueue(&self, lease: &Arc<VulkanImportLease>) {
1092        if !lease.queued.swap(true, Ordering::AcqRel) {
1093            self.pending
1094                .lock()
1095                .unwrap_or_else(std::sync::PoisonError::into_inner)
1096                .push(Arc::downgrade(lease));
1097        }
1098    }
1099
1100    fn flush(&self) -> Result<(), BridgeError> {
1101        let leases = self
1102            .pending
1103            .lock()
1104            .unwrap_or_else(std::sync::PoisonError::into_inner)
1105            .drain(..)
1106            .filter_map(|lease| lease.upgrade())
1107            .filter(|lease| !lease.acquired.load(Ordering::Acquire))
1108            .collect::<Vec<_>>();
1109        if leases.is_empty() {
1110            return Ok(());
1111        }
1112        // Draining cleared the queue, so a lease that is still unacquired when this call fails has
1113        // to be re-enqueueable or it would never acquire.
1114        let result = flush_leases(&leases);
1115        if result.is_err() {
1116            for lease in &leases {
1117                if !lease.acquired.load(Ordering::Acquire) {
1118                    lease.queued.store(false, Ordering::Release);
1119                }
1120            }
1121        }
1122        result
1123    }
1124}
1125
1126/// Acquires every queued import for the renderer's next submission.
1127///
1128/// Each frame is acquired under its own lock, from the state it is in at that
1129/// moment: the barrier starts from its current layout, and the timeline wait
1130/// is on the value `FFmpeg` last signalled. Recording the barriers at import
1131/// and submitting them together under the first frame's lock would let a
1132/// decode in between (into a frame used as a reference) move a frame on, so
1133/// the batch would wait on a stale value and signal one `FFmpeg` had already
1134/// passed.
1135///
1136/// The signal the sampling submission will raise is staged under that same
1137/// lock, before `FFmpeg` can see the frame again. Staged after the lock is
1138/// released, it would leave a window in which a decode that uses the frame as
1139/// a reference reads the acquire's value and layout: it would move the image
1140/// to its DPB layout while the renderer was still to sample it
1141/// (VUID-vkCmdDraw-None-09600), and signal the next value on the frame's
1142/// timeline from the decode queue while the renderer's submission signals a
1143/// later one from its own. With two queues raising one timeline out of order,
1144/// a wait on it can stay unsatisfied, and `FFmpeg`, freeing a picture, waits
1145/// forever. Staged first, the frame's recorded value is the renderer's, so a
1146/// decode that uses it waits for the sampling to finish.
1147///
1148/// Only one frame lock is ever held, so this cannot deadlock against a decode
1149/// that locks several frames in its own order; the cost is one queue
1150/// submission per imported frame instead of one per batch.
1151fn flush_leases(leases: &[Arc<VulkanImportLease>]) -> Result<(), BridgeError> {
1152    for lease in leases {
1153        let mut access = lease.lock()?;
1154        // Decided under the frame's lock, so a lease two flushes reach is acquired once.
1155        if lease.acquired.load(Ordering::Acquire) {
1156            continue;
1157        }
1158        let mut status = 0;
1159        // SAFETY: the ticket was created for this retained frame at import, and the frame and queue
1160        // stay locked for the submission.
1161        let step = unsafe { acquire_submit(&mut access, &mut status) };
1162        if step != 0 {
1163            return Err(helper_error("import acquire", step, status));
1164        }
1165        lease.acquired.store(true, Ordering::Release);
1166        // The acquire is submitted whatever staging does next, so its state is committed either
1167        // way.
1168        access.frame.commit();
1169        // SAFETY: staging records, as the frame's timeline values, the signals it adds to the
1170        // renderer's next queue submission, which samples this frame and is made before anything
1171        // waits for them; the layouts stay the ones the acquire just recorded.
1172        lease.stage_submission_signal(unsafe { access.frame.access_mut() })?;
1173    }
1174    Ok(())
1175}
1176
1177// SAFETY: what a lease shares across threads is behind locks. The ticket's Vulkan objects are used
1178// only through `LeaseAccess`, under the lock of the frame it serves, which also holds the queue
1179// `FFmpeg` and the renderer share; the drop, which gives the ticket back, has the lease to itself.
1180// The staged signals are behind their mutex, the flags are atomics, and the frame and wgpu objects
1181// synchronize themselves.
1182unsafe impl Send for VulkanImportLease {}
1183// SAFETY: as for `Send`: `&self` reaches the ticket only through `Self::lock`.
1184unsafe impl Sync for VulkanImportLease {}
1185
1186impl fmt::Debug for VulkanImportLease {
1187    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1188        formatter
1189            .debug_struct("VulkanImportLease")
1190            .field("released", &self.released.load(Ordering::Acquire))
1191            .field("retained_bytes", &self.retained_bytes)
1192            .finish_non_exhaustive()
1193    }
1194}
1195
1196impl VulkanImportLease {
1197    /// Locks the frame for a use of the ticket: the only way `&self` reaches
1198    /// it (see [`LeaseAccess`]).
1199    fn lock(&self) -> Result<LeaseAccess<'_>, BridgeError> {
1200        let ticket = self
1201            .ticket
1202            .as_ref()
1203            .map(|pooled| pooled.ticket)
1204            .ok_or(BridgeError::InvalidInput("missing Vulkan import ticket"))?;
1205        Ok(LeaseAccess {
1206            frame: self.frame.lock_access()?,
1207            ticket,
1208        })
1209    }
1210
1211    /// Approximate decoded-surface bytes retained by this import.
1212    #[must_use]
1213    pub const fn retained_bytes(&self) -> u64 {
1214        self.retained_bytes
1215    }
1216
1217    /// Timeline signals this lease has already staged on the wgpu queue.
1218    ///
1219    /// Zero until the frame that samples this import stages its submission,
1220    /// and zero again once that submission completes and the lease is
1221    /// released. A nonzero count when the next frame begins staging would mean
1222    /// a signal from the previous frame is about to ride on this one.
1223    #[must_use]
1224    pub fn staged_signals(&self) -> usize {
1225        self.staged_signals
1226            .lock()
1227            .unwrap_or_else(std::sync::PoisonError::into_inner)
1228            .len()
1229    }
1230
1231    /// Queues this import for the next [`Self::flush_submission`], which
1232    /// acquires the frame and adds its timeline signals to the wgpu
1233    /// submission that samples it. Queuing an import twice is harmless.
1234    pub fn prepare_submission(self: &Arc<Self>) {
1235        self.batch.enqueue(self);
1236    }
1237
1238    /// Acquires every queued import of this context, one native queue
1239    /// submission per frame, each under that frame's lock, and stages the
1240    /// signals of the wgpu submission that samples them.
1241    ///
1242    /// # Errors
1243    /// Returns an error if a frame cannot be locked, its acquire fails, or its
1244    /// signals cannot be staged. Imports not yet acquired can then be queued
1245    /// again for a retry.
1246    pub fn flush_submission(&self) -> Result<(), BridgeError> {
1247        self.batch.flush()
1248    }
1249
1250    /// Stages the sampling submission's timeline signals and records them as
1251    /// the frame's values. `access` is the frame's lock, held since the acquire.
1252    fn stage_submission_signal(&self, access: &mut VulkanFrameAccess) -> Result<(), BridgeError> {
1253        let mut staged = self
1254            .staged_signals
1255            .lock()
1256            .unwrap_or_else(std::sync::PoisonError::into_inner);
1257        if !staged.is_empty() || self.released.load(Ordering::Acquire) {
1258            return Ok(());
1259        }
1260        // SAFETY: the queue was created from this context's Vulkan OpenDevice.
1261        let queue = unsafe { self.gpu.queue().as_hal::<wgpu_hal::api::Vulkan>() }
1262            .ok_or(BridgeError::InvalidInput("renderer queue is not Vulkan"))?;
1263        let descriptor = access.frame_mut();
1264        for index in 0..descriptor.images.len() {
1265            if descriptor.images[index] == 0 {
1266                continue;
1267            }
1268            let semaphore = descriptor.semaphores[index];
1269            let acquired_value = descriptor.semaphore_values[index];
1270            if semaphore == 0 {
1271                return Err(BridgeError::InvalidInput("frame has no timeline semaphore"));
1272            }
1273            if let Some(existing) = staged
1274                .iter_mut()
1275                .find(|existing| existing.semaphore == semaphore)
1276            {
1277                existing.acquired_value = existing.acquired_value.max(acquired_value);
1278                existing.signal_value = existing.acquired_value.saturating_add(1);
1279            } else {
1280                staged.push(StagedSignal {
1281                    semaphore,
1282                    acquired_value,
1283                    signal_value: acquired_value.saturating_add(1),
1284                });
1285            }
1286        }
1287        for signal in &*staged {
1288            queue.add_signal_semaphore(
1289                vk::Semaphore::from_raw(signal.semaphore),
1290                Some(signal.signal_value),
1291            );
1292            for index in 0..descriptor.images.len() {
1293                if descriptor.semaphores[index] == signal.semaphore {
1294                    descriptor.semaphore_values[index] = signal.signal_value;
1295                }
1296            }
1297        }
1298        Ok(())
1299    }
1300
1301    /// Records that the wgpu submission sampling this import was made.
1302    ///
1303    /// When the frame's signals were staged on that submission, the frame
1304    /// already records its value and layout, and nothing more is submitted.
1305    /// Otherwise the planes are returned to the layout `FFmpeg` had them in,
1306    /// in a submission of their own.
1307    ///
1308    /// # Errors
1309    /// Returns a bridge error if the frame can no longer be locked or submitted.
1310    pub fn submitted(&self) -> Result<(), BridgeError> {
1311        if self.released.load(Ordering::Acquire) {
1312            return Ok(());
1313        }
1314        if !self
1315            .staged_signals
1316            .lock()
1317            .unwrap_or_else(std::sync::PoisonError::into_inner)
1318            .is_empty()
1319        {
1320            self.released.store(true, Ordering::Release);
1321            return Ok(());
1322        }
1323        let mut access = self.lock()?;
1324        // Decided under the frame's lock, so a lease two callers reach is released once.
1325        if self.released.load(Ordering::Acquire) {
1326            return Ok(());
1327        }
1328        let mut status = 0;
1329        // SAFETY: ticket was created for this retained frame and remains owned by this lease until
1330        // queue completion drops it.
1331        let step = unsafe { release(&mut access, &mut status) };
1332        if step != 0 {
1333            return Err(helper_error("import release", step, status));
1334        }
1335        access.frame.commit();
1336        self.released.store(true, Ordering::Release);
1337        Ok(())
1338    }
1339}
1340
1341impl Drop for VulkanImportLease {
1342    /// Leaves the frame as `FFmpeg` can go on from, and gives the ticket back.
1343    ///
1344    /// An acquired lease that the renderer's sampling submission never
1345    /// released (its frame failed, or was dropped, after the acquire) still
1346    /// owes the frame what it promised. When its signal was staged, the frame
1347    /// records the value that submission was to raise, and a decode that used
1348    /// the frame since has queued a wait on it: taking the signal back off
1349    /// the queue alone would leave that wait, and every later batch on the
1350    /// decode queue, waiting forever. So under the frame's lock, which `FFmpeg`
1351    /// holds from reading the value to submitting its wait, a signal wgpu has
1352    /// not submitted yet is taken off its queue and raised by a submission of
1353    /// its own, after the acquire. A lease with nothing staged releases the
1354    /// frame from where it stands instead.
1355    ///
1356    /// Either way the ticket goes back to the pool, which reuses it once the
1357    /// fences of everything it submitted have signalled, the acquire's
1358    /// included: an acquire may still be waiting on the decoder when its
1359    /// lease is dropped, and freeing its command pool then would free commands
1360    /// the GPU has yet to run.
1361    fn drop(&mut self) {
1362        let Some(pooled) = self.ticket.take() else {
1363            return;
1364        };
1365        if self.acquired.load(Ordering::Acquire) && !self.released.load(Ordering::Acquire) {
1366            let staged = std::mem::take(
1367                self.staged_signals
1368                    .get_mut()
1369                    .unwrap_or_else(std::sync::PoisonError::into_inner),
1370            );
1371            if let Ok(frame) = self.frame.lock_access() {
1372                // The drop has the lease, and so its ticket, to itself.
1373                let mut lease = LeaseAccess {
1374                    frame,
1375                    ticket: pooled.ticket,
1376                };
1377                if staged.is_empty() {
1378                    let mut status = 0;
1379                    // SAFETY: the ticket is this frame's and the guard holds its lock.
1380                    if unsafe { release(&mut lease, &mut status) } == 0 {
1381                        lease.frame.commit();
1382                    }
1383                } else {
1384                    // SAFETY: the queue belongs to the shared Vulkan device.
1385                    let queue = unsafe { self.gpu.queue().as_hal::<wgpu_hal::api::Vulkan>() };
1386                    for signal in &staged {
1387                        let unsubmitted = queue.as_ref().is_some_and(|queue| {
1388                            queue.remove_signal_semaphore(vk::Semaphore::from_raw(signal.semaphore))
1389                        });
1390                        if unsubmitted {
1391                            let mut status = 0;
1392                            // SAFETY: the guard holds the frame's and the queue's locks; the
1393                            // semaphore is the frame's timeline, which reaches `acquired_value`
1394                            // with the acquire.
1395                            let _ = unsafe {
1396                                alpha2_native_gpu_vulkan_signal(
1397                                    lease.frame.access_mut(),
1398                                    signal.semaphore,
1399                                    signal.acquired_value,
1400                                    signal.signal_value,
1401                                    &raw mut status,
1402                                )
1403                            };
1404                        }
1405                    }
1406                }
1407            }
1408        }
1409        self.tickets.give_back(pooled);
1410    }
1411}
1412
1413/// Device features `FFmpeg`'s Vulkan code uses that wgpu does not enable.
1414///
1415/// `FFmpeg` enables these itself on a device it creates. On this one, which
1416/// wgpu creates, `FFmpeg` uses them whether or not they are on: every decode
1417/// and encode submission is `vkQueueSubmit2`, which needs `synchronization2`,
1418/// its YCbCr conversion needs `samplerYcbcrConversion`, and its
1419/// profile-independent images need `videoMaintenance1`.
1420#[derive(Clone, Copy, Debug, Default)]
1421struct FfmpegDeviceFeatures {
1422    synchronization2: bool,
1423    sampler_ycbcr_conversion: bool,
1424    video_maintenance1: bool,
1425}
1426
1427/// The [`FfmpegDeviceFeatures`] `physical` supports; `video_maintenance1` is
1428/// only asked for when its extension is being enabled.
1429fn ffmpeg_device_features(
1430    instance: &ash::Instance,
1431    physical: vk::PhysicalDevice,
1432    video_maintenance1: bool,
1433) -> FfmpegDeviceFeatures {
1434    let mut synchronization2 = vk::PhysicalDeviceSynchronization2Features::default();
1435    let mut ycbcr = vk::PhysicalDeviceSamplerYcbcrConversionFeatures::default();
1436    let mut maintenance = vk::PhysicalDeviceVideoMaintenance1FeaturesKHR::default();
1437    let mut features = vk::PhysicalDeviceFeatures2::default()
1438        .push_next(&mut synchronization2)
1439        .push_next(&mut ycbcr);
1440    if video_maintenance1 {
1441        features = features.push_next(&mut maintenance);
1442    }
1443    // SAFETY: `physical` was enumerated from `instance`, and every structure in the chain outlives
1444    // the call.
1445    unsafe { instance.get_physical_device_features2(physical, &mut features) };
1446    FfmpegDeviceFeatures {
1447        synchronization2: synchronization2.synchronization2 == vk::TRUE,
1448        sampler_ycbcr_conversion: ycbcr.sampler_ycbcr_conversion == vk::TRUE,
1449        video_maintenance1: video_maintenance1 && maintenance.video_maintenance1 == vk::TRUE,
1450    }
1451}
1452
1453impl FfmpegDeviceFeatures {
1454    /// Adds the supported features to the device's creation chain.
1455    ///
1456    /// wgpu builds its own feature structures after this callback and uses
1457    /// none of these three, so they are appended rather than merged. They
1458    /// live for the process: the chain is read after the callback returns.
1459    fn enable(self, create_info: &mut vk::DeviceCreateInfo<'_>) {
1460        let mut info = std::mem::take(create_info);
1461        if self.synchronization2 {
1462            info = info.push_next(Box::leak(Box::new(
1463                vk::PhysicalDeviceSynchronization2Features::default().synchronization2(true),
1464            )));
1465        }
1466        if self.sampler_ycbcr_conversion {
1467            info = info.push_next(Box::leak(Box::new(
1468                vk::PhysicalDeviceSamplerYcbcrConversionFeatures::default()
1469                    .sampler_ycbcr_conversion(true),
1470            )));
1471        }
1472        if self.video_maintenance1 {
1473            info = info.push_next(Box::leak(Box::new(
1474                vk::PhysicalDeviceVideoMaintenance1FeaturesKHR::default().video_maintenance1(true),
1475            )));
1476        }
1477        *create_info = info;
1478    }
1479}
1480
1481/// One Vulkan logical device adopted by both the renderer and `FFmpeg`.
1482#[derive(Clone)]
1483pub struct SharedVulkanContext {
1484    // Fields drop in order: the idle import tickets are Vulkan objects of the device, so they go
1485    // while it still exists.
1486    import_tickets: Arc<ImportTicketPool>,
1487    // Drop FFmpeg references before wgpu destroys the raw device.
1488    ffmpeg_device: AVHWDeviceContext,
1489    gpu: GpuContext,
1490    submission_batch: Arc<VulkanSubmissionBatch>,
1491    identity: NativeGpuIdentity,
1492    decode_codecs: Vec<&'static str>,
1493    encode_codecs: Vec<&'static str>,
1494    encode_extent: Option<VideoEncodeExtent>,
1495    validation_layer: Option<VulkanLayerVersion>,
1496}
1497
1498impl fmt::Debug for SharedVulkanContext {
1499    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1500        formatter
1501            .debug_struct("SharedVulkanContext")
1502            .field("identity", &self.identity)
1503            .field("decode_codecs", &self.decode_codecs)
1504            .field("encode_codecs", &self.encode_codecs)
1505            .finish_non_exhaustive()
1506    }
1507}
1508
1509impl SharedVulkanContext {
1510    /// Creates a Vulkan 1.3 device with wgpu, video-decode, and NV12 support.
1511    ///
1512    /// # Errors
1513    /// Returns a capability or initialization failure suitable for auto-mode fallback.
1514    #[allow(clippy::too_many_lines)]
1515    pub fn create() -> Result<Self, SharedVulkanError> {
1516        let _turn = alpha2_gpu::serialize_acquisition();
1517        let hal_instance = hal_instance()?;
1518        // SAFETY: adapter enumeration does not require a surface.
1519        let mut adapters = unsafe { hal_instance.enumerate_adapters(None) };
1520        let position = choose_adapter(
1521            &mut adapters,
1522            std::env::var("ALPHA2_VULKAN_DEVICE").ok().as_deref(),
1523        )?;
1524        let exposed = adapters.swap_remove(position);
1525        let physical = exposed.adapter.raw_physical_device();
1526        let shared_instance = exposed.adapter.shared_instance();
1527        let raw_instance = shared_instance.raw_instance().handle();
1528        let validation_layer = activated_validation_layer(shared_instance.raw_instance(), physical);
1529        // SAFETY: `physical` was enumerated from this instance.
1530        let queue_properties = unsafe {
1531            shared_instance
1532                .raw_instance()
1533                .get_physical_device_queue_family_properties(physical)
1534        };
1535        let graphics_family = u32::try_from(
1536            queue_properties
1537                .iter()
1538                .position(|family| {
1539                    family.queue_flags.contains(vk::QueueFlags::GRAPHICS) && family.queue_count > 0
1540                })
1541                .ok_or_else(|| SharedVulkanError("Vulkan adapter has no graphics queue".into()))?,
1542        )
1543        .map_err(|_| SharedVulkanError("graphics queue index is out of range".into()))?;
1544        if graphics_family != 0 {
1545            return Err(SharedVulkanError(
1546                "wgpu-hal shared-device path requires graphics queue family 0".into(),
1547            ));
1548        }
1549        let graphics_flags = queue_properties[0].queue_flags;
1550        if !graphics_flags.contains(vk::QueueFlags::COMPUTE | vk::QueueFlags::TRANSFER) {
1551            return Err(SharedVulkanError(
1552                "graphics queue family lacks FFmpeg compute/transfer capabilities".into(),
1553            ));
1554        }
1555        let decode_family = u32::try_from(
1556            queue_properties
1557                .iter()
1558                .position(|family| {
1559                    family
1560                        .queue_flags
1561                        .contains(vk::QueueFlags::VIDEO_DECODE_KHR)
1562                        && family.queue_count > 0
1563                })
1564                .ok_or_else(|| {
1565                    SharedVulkanError("Vulkan adapter has no video-decode queue".into())
1566                })?,
1567        )
1568        .map_err(|_| SharedVulkanError("video-decode queue index is out of range".into()))?;
1569        if decode_family == graphics_family {
1570            return Err(SharedVulkanError(
1571                "shared Vulkan device requires a decode queue separate from wgpu graphics".into(),
1572            ));
1573        }
1574        // The encode family is optional: a device that decodes but cannot encode still runs every
1575        // other path.
1576        let encode_family = queue_properties
1577            .iter()
1578            .position(|family| {
1579                family
1580                    .queue_flags
1581                    .contains(vk::QueueFlags::VIDEO_ENCODE_KHR)
1582                    && family.queue_count > 0
1583            })
1584            .and_then(|index| u32::try_from(index).ok());
1585        // SAFETY: `physical` was enumerated from this instance.
1586        let extension_properties = unsafe {
1587            shared_instance
1588                .raw_instance()
1589                .enumerate_device_extension_properties(physical)
1590        }
1591        .map_err(|error| SharedVulkanError(format!("Vulkan extensions: {error}")))?;
1592        let supports = |name: &CStr| {
1593            extension_properties.iter().any(|property| {
1594                property
1595                    .extension_name_as_c_str()
1596                    .is_ok_and(|candidate| candidate == name)
1597            })
1598        };
1599        let mut video_extensions = vec![
1600            ash::khr::video_queue::NAME,
1601            ash::khr::video_decode_queue::NAME,
1602        ];
1603        for extension in [
1604            ash::khr::video_decode_h264::NAME,
1605            ash::khr::video_decode_h265::NAME,
1606            ash::khr::video_decode_av1::NAME,
1607        ] {
1608            if supports(extension) {
1609                video_extensions.push(extension);
1610            }
1611        }
1612        if !video_extensions.iter().all(|extension| supports(extension))
1613            || !video_extensions.contains(&ash::khr::video_decode_h264::NAME)
1614        {
1615            return Err(SharedVulkanError(
1616                "Vulkan H.264 video decode is unavailable".into(),
1617            ));
1618        }
1619        // FFmpeg's Vulkan encoder refuses to open without both of these, and wgpu asks for neither,
1620        // so they are added to the device it creates. `video_maintenance1` is what lets the encoder
1621        // use a video session without pre-allocating its own DPB images.
1622        let encode_extensions = [
1623            ash::khr::video_encode_queue::NAME,
1624            ash::khr::video_maintenance1::NAME,
1625            ash::khr::video_encode_h264::NAME,
1626        ];
1627        let encode_family = encode_family.filter(|_| {
1628            encode_extensions
1629                .iter()
1630                .all(|extension| supports(extension))
1631        });
1632        // Asked only now that the device is known to expose the video queue and H.264 encode
1633        // extensions: the query is an extension entry point, and a driver without them has no
1634        // function behind the loader's trampoline. Asked here because `shared_instance` borrows the
1635        // adapter, which is moved into wgpu once the device exists.
1636        let h264_extent = encode_family.and_then(|_| {
1637            h264_encode_extent(
1638                shared_instance.entry(),
1639                shared_instance.raw_instance(),
1640                physical,
1641            )
1642        });
1643        if let Some(family) = encode_family {
1644            video_extensions.extend_from_slice(&encode_extensions);
1645            let _ = family;
1646        }
1647        let mut decode_caps = vk::VideoCodecOperationFlagsKHR::empty();
1648        let mut decode_codecs = Vec::new();
1649        for (extension, cap, label) in [
1650            (
1651                ash::khr::video_decode_h264::NAME,
1652                vk::VideoCodecOperationFlagsKHR::DECODE_H264,
1653                "h264",
1654            ),
1655            (
1656                ash::khr::video_decode_h265::NAME,
1657                vk::VideoCodecOperationFlagsKHR::DECODE_H265,
1658                "hevc",
1659            ),
1660            (
1661                ash::khr::video_decode_av1::NAME,
1662                vk::VideoCodecOperationFlagsKHR::DECODE_AV1,
1663                "av1",
1664            ),
1665        ] {
1666            if video_extensions.contains(&extension) {
1667                decode_caps |= cap;
1668                decode_codecs.push(label);
1669            }
1670        }
1671        // P010 is optional: an adapter without it still decodes 8-bit streams, and 10-bit sources
1672        // fall back to software on that device.
1673        let required_features = (exposed.features
1674            & (wgpu::Features::TIMESTAMP_QUERY
1675                | wgpu::Features::TEXTURE_FORMAT_P010
1676                | wgpu::Features::TEXTURE_FORMAT_16BIT_NORM))
1677            | wgpu::Features::TEXTURE_FORMAT_NV12;
1678        let required_limits = wgpu::Limits::downlevel_webgl2_defaults()
1679            .using_resolution(exposed.capabilities.limits.clone());
1680        let enabled_extensions = exposed
1681            .adapter
1682            .required_device_extensions(required_features);
1683        let core = exposed
1684            .adapter
1685            .physical_device_features(&enabled_extensions, required_features)
1686            .get_core();
1687        let ffmpeg_features = ffmpeg_device_features(
1688            shared_instance.raw_instance(),
1689            physical,
1690            video_extensions.contains(&ash::khr::video_maintenance1::NAME),
1691        );
1692        let extra_family = decode_family;
1693        let extra_encode_family =
1694            encode_family.filter(|family| *family != graphics_family && *family != decode_family);
1695        let extra_extensions = video_extensions.clone();
1696        // SAFETY: all added extensions and queues were queried from this adapter.
1697        let open = unsafe {
1698            exposed.adapter.open_with_callback(
1699                required_features,
1700                &required_limits,
1701                &wgpu::MemoryHints::Performance,
1702                Some(Box::new(move |args| {
1703                    ffmpeg_features.enable(args.create_info);
1704                    for extension in extra_extensions {
1705                        if !args.extensions.contains(&extension) {
1706                            args.extensions.push(extension);
1707                        }
1708                    }
1709                    if extra_family != 0 {
1710                        args.queue_create_infos.push(
1711                            vk::DeviceQueueCreateInfo::default()
1712                                .queue_family_index(extra_family)
1713                                .queue_priorities(&[1.0]),
1714                        );
1715                    }
1716                    if let Some(family) = extra_encode_family {
1717                        args.queue_create_infos.push(
1718                            vk::DeviceQueueCreateInfo::default()
1719                                .queue_family_index(family)
1720                                .queue_priorities(&[1.0]),
1721                        );
1722                    }
1723                })),
1724            )
1725        }
1726        .map_err(|error| SharedVulkanError(format!("shared Vulkan device: {error}")))?;
1727        let raw_device = open.device.raw_device().handle();
1728        let device_extensions = open
1729            .device
1730            .enabled_device_extensions()
1731            .iter()
1732            .map(|name| name.to_string_lossy().into_owned())
1733            .collect::<Vec<_>>();
1734        let instance_extensions = shared_instance
1735            .extensions()
1736            .iter()
1737            .map(|name| name.to_string_lossy().into_owned())
1738            .collect::<Vec<_>>();
1739        // SAFETY: the instance, physical device, and device are the ones wgpu just opened with
1740        // these queues and extensions, and `Self` drops the `FFmpeg` context before wgpu destroys
1741        // the device.
1742        let ffmpeg_device = unsafe {
1743            create_vulkan_device_context(&VulkanDeviceInfo {
1744                instance: raw_instance.as_raw(),
1745                physical_device: physical.as_raw(),
1746                device: raw_device.as_raw(),
1747                graphics_queue_family: 0,
1748                graphics_queue_count: 1,
1749                decode_queue_family: decode_family,
1750                decode_queue_count: 1,
1751                decode_video_caps: decode_caps.as_raw(),
1752                encode_queue_family: encode_family.unwrap_or(0),
1753                encode_queue_count: u32::from(encode_family.is_some()),
1754                encode_video_caps: encode_family
1755                    .map_or(0, |_| vk::VideoCodecOperationFlagsKHR::ENCODE_H264.as_raw()),
1756                core_features: [
1757                    core.shader_image_gather_extended,
1758                    core.fragment_stores_and_atomics,
1759                    core.shader_int64,
1760                    core.vertex_pipeline_stores_and_atomics,
1761                ],
1762                synchronization2: u32::from(ffmpeg_features.synchronization2),
1763                sampler_ycbcr_conversion: u32::from(ffmpeg_features.sampler_ycbcr_conversion),
1764                video_maintenance1: u32::from(ffmpeg_features.video_maintenance1),
1765                instance_extensions,
1766                device_extensions,
1767            })
1768        }
1769        .map_err(|error| SharedVulkanError(format!("FFmpeg Vulkan device: {error}")))?;
1770        // SAFETY: the adapter and instance originate from the same HAL object.
1771        let wgpu_instance =
1772            unsafe { wgpu::Instance::from_hal::<wgpu_hal::api::Vulkan>(hal_instance) };
1773        // SAFETY: `exposed` was enumerated from the HAL instance `wgpu_instance` now wraps.
1774        let adapter = unsafe { wgpu_instance.create_adapter_from_hal(exposed) };
1775        let descriptor = wgpu::DeviceDescriptor {
1776            label: Some("alpha2 shared Vulkan decode"),
1777            required_features,
1778            required_limits,
1779            ..Default::default()
1780        };
1781        // SAFETY: OpenDevice has the descriptor's exact features and limits.
1782        let (device, queue) = unsafe { adapter.create_device_from_hal(open, &descriptor) }
1783            .map_err(|error| SharedVulkanError(format!("wgpu adoption: {error}")))?;
1784        // wgpu and FFmpeg submit to the same graphics queue, which Vulkan requires be externally
1785        // synchronized, so the renderer takes the same lock FFmpeg takes around its own
1786        // submissions.
1787        let queue_lock: Arc<dyn ExternalQueueLock> = Arc::new(SharedQueueLock(
1788            VulkanQueueLock::new(ffmpeg_device.clone(), 0)
1789                .map_err(|error| SharedVulkanError(format!("FFmpeg Vulkan queue lock: {error}")))?,
1790        ));
1791        let gpu = GpuContext::from_parts_with_queue_lock(adapter, device, queue, Some(queue_lock));
1792        Ok(Self {
1793            ffmpeg_device,
1794            gpu,
1795            submission_batch: Arc::new(VulkanSubmissionBatch::default()),
1796            import_tickets: Arc::new(ImportTicketPool::default()),
1797            identity: NativeGpuIdentity {
1798                physical_device: physical.as_raw(),
1799                device: raw_device.as_raw(),
1800                epoch: 1,
1801            },
1802            decode_codecs,
1803            encode_extent: h264_extent,
1804            encode_codecs: if encode_family.is_some() {
1805                vec!["h264"]
1806            } else {
1807                Vec::new()
1808            },
1809            validation_layer,
1810        })
1811    }
1812
1813    /// Renderer context backed by the shared logical device.
1814    #[must_use]
1815    pub fn gpu(&self) -> GpuContext {
1816        self.gpu.clone()
1817    }
1818
1819    /// `FFmpeg` device reference for one decoder session.
1820    #[must_use]
1821    pub fn ffmpeg_device(&self) -> AVHWDeviceContext {
1822        self.ffmpeg_device.clone()
1823    }
1824
1825    /// Raw-device cache identity.
1826    #[must_use]
1827    pub const fn identity(&self) -> NativeGpuIdentity {
1828        self.identity
1829    }
1830
1831    /// Whether this device can present 10-bit decoded planes.
1832    #[must_use]
1833    pub fn supports_ten_bit(&self) -> bool {
1834        self.gpu
1835            .device()
1836            .features()
1837            .contains(wgpu::Features::TEXTURE_FORMAT_P010)
1838    }
1839
1840    /// Decode codecs (`h264`, `hevc`, `av1`) whose extensions the device was
1841    /// created with.
1842    #[must_use]
1843    pub fn decode_codecs(&self) -> &[&'static str] {
1844        &self.decode_codecs
1845    }
1846
1847    /// The Khronos validation layer loaded on this instance, and its version.
1848    ///
1849    /// Loaded means the loader activated it: wgpu enabled it (its validation
1850    /// flag, on in every debug build) or the environment injected it.
1851    #[must_use]
1852    pub const fn validation_layer(&self) -> Option<VulkanLayerVersion> {
1853        self.validation_layer
1854    }
1855
1856    /// Why `FFmpeg` must not open a video session on this device, or `None`
1857    /// when it may.
1858    ///
1859    /// A validation layer older than [`VulkanLayerVersion::VIDEO_SESSIONS`]
1860    /// crashes or hangs inside the session setup, and a decode session is
1861    /// set up as an encode one is: hardware decode and the GPU encoder both
1862    /// ask this, so neither opens one the other is kept from.
1863    #[must_use]
1864    pub fn video_session_hazard(&self) -> Option<String> {
1865        self.validation_layer
1866            .filter(|layer| *layer < VulkanLayerVersion::VIDEO_SESSIONS)
1867            .map(|layer| {
1868                format!(
1869                    "the loaded Vulkan validation layer is {layer}, and layers older than {} \
1870                     crash in FFmpeg's video session setup",
1871                    VulkanLayerVersion::VIDEO_SESSIONS
1872                )
1873            })
1874    }
1875
1876    /// Video-encode codecs this device created a queue and extensions for.
1877    ///
1878    /// Empty on an adapter with no encode queue, which every other path still
1879    /// runs on. `FFmpeg`'s Vulkan encoder refuses to open without the queue,
1880    /// so this is what decides whether that encoder is even a candidate.
1881    #[must_use]
1882    pub fn encode_codecs(&self) -> &[&'static str] {
1883        &self.encode_codecs
1884    }
1885
1886    /// Output sizes this device's H.264 encoder accepts.
1887    ///
1888    /// `h264_vulkan` refuses anything outside it, and an export that asks for
1889    /// one silently takes the readback path instead. The limit is the
1890    /// device's, not the encoder list's: it comes from
1891    /// `vkGetPhysicalDeviceVideoCapabilitiesKHR`, and on the adapter this was
1892    /// developed against the minimum is 160x64.
1893    ///
1894    /// `None` when the device has no encode queue, or when the query itself
1895    /// failed, in which case no size can be promised either way.
1896    #[must_use]
1897    pub const fn encode_extent(&self) -> Option<&VideoEncodeExtent> {
1898        self.encode_extent.as_ref()
1899    }
1900
1901    /// Allocation counters of the import tickets this device reuses.
1902    #[must_use]
1903    pub fn import_ticket_stats(&self) -> ImportTicketStats {
1904        self.import_tickets.stats()
1905    }
1906
1907    /// Imports one same-device `FFmpeg` NV12 or P010 surface as plane views.
1908    ///
1909    /// The acquire transition waits on `FFmpeg`'s latest timeline value without
1910    /// a CPU fence wait. The returned lease must receive `submitted()` directly
1911    /// after the wgpu submission that samples the views.
1912    ///
1913    /// # Errors
1914    /// Returns an error for a different device, unsupported image layout, or a
1915    /// failed Vulkan transition submission.
1916    #[allow(clippy::too_many_lines)]
1917    pub fn import_surface(
1918        &self,
1919        frame: Arc<VulkanFrameLease>,
1920    ) -> Result<ImportedVulkanFrame, BridgeError> {
1921        let info = frame.info()?;
1922        if info.device != self.identity.device
1923            || info.physical_device != self.identity.physical_device
1924        {
1925            return Err(BridgeError::InvalidInput(
1926                "decoded frame belongs to another Vulkan device",
1927            ));
1928        }
1929        let layout = match (info.software_format, info.formats[0]) {
1930            (rsmpeg::ffi::AV_PIX_FMT_NV12, format)
1931                if format
1932                    == vk::Format::G8_B8R8_2PLANE_420_UNORM
1933                        .as_raw()
1934                        .cast_unsigned() =>
1935            {
1936                NativePlaneLayout::Nv12
1937            }
1938            (rsmpeg::ffi::AV_PIX_FMT_P010LE, format)
1939                if format
1940                    == vk::Format::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16
1941                        .as_raw()
1942                        .cast_unsigned() =>
1943            {
1944                NativePlaneLayout::P010
1945            }
1946            _ => {
1947                return Err(BridgeError::InvalidInput(
1948                    "only sampled single-image NV12 or P010 is importable",
1949                ));
1950            }
1951        };
1952        if layout == NativePlaneLayout::P010
1953            && !self
1954                .gpu
1955                .device()
1956                .features()
1957                .contains(wgpu::Features::TEXTURE_FORMAT_P010)
1958        {
1959            return Err(BridgeError::InvalidInput(
1960                "shared Vulkan device does not expose P010 plane views",
1961            ));
1962        }
1963        if info.plane_count != 1 || !info.sampled || info.width <= 0 || info.height <= 0 {
1964            return Err(BridgeError::InvalidInput(
1965                "only sampled single-image NV12 or P010 is importable",
1966            ));
1967        }
1968        // The image comes from the frames context, so its extent is the padded coded size;
1969        // importing it as the display size would rescale every sample.
1970        if info.coded_width < info.width || info.coded_height < info.height {
1971            return Err(BridgeError::InvalidInput(
1972                "decoded frame is larger than its allocated image",
1973            ));
1974        }
1975        let coded_width = u32::try_from(info.coded_width)
1976            .map_err(|_| BridgeError::InvalidInput("invalid coded width"))?;
1977        let coded_height = u32::try_from(info.coded_height)
1978            .map_err(|_| BridgeError::InvalidInput("invalid coded height"))?;
1979        let access = frame.lock_access()?;
1980        let mut status = 0;
1981        // SAFETY: the guard holds FFmpeg's frame and graphics queue locks while C reads the frame's
1982        // state.
1983        let step =
1984            unsafe { alpha2_native_gpu_vulkan_acquire_check(access.access(), &raw mut status) };
1985        if step != 0 {
1986            return Err(helper_error("import check", step, status));
1987        }
1988        let (device, allocator, queue_family) = {
1989            let access = access.access();
1990            (
1991                access.frame().device,
1992                access.allocator(),
1993                access.queue_family(),
1994            )
1995        };
1996        drop(access);
1997        let ticket = self.import_tickets.take(device, allocator, queue_family)?;
1998        let (texture_format, luma_format, chroma_format) = match layout {
1999            NativePlaneLayout::P010 => (
2000                wgpu::TextureFormat::P010,
2001                wgpu::TextureFormat::R16Unorm,
2002                wgpu::TextureFormat::Rg16Unorm,
2003            ),
2004            _ => (
2005                wgpu::TextureFormat::NV12,
2006                wgpu::TextureFormat::R8Unorm,
2007                wgpu::TextureFormat::Rg8Unorm,
2008            ),
2009        };
2010        let hal_descriptor = wgpu_hal::TextureDescriptor {
2011            label: Some("alpha2 FFmpeg decode surface"),
2012            size: wgpu::Extent3d {
2013                width: coded_width,
2014                height: coded_height,
2015                depth_or_array_layers: 1,
2016            },
2017            mip_level_count: 1,
2018            sample_count: 1,
2019            dimension: wgpu::TextureDimension::D2,
2020            format: texture_format,
2021            usage: wgpu::TextureUses::RESOURCE,
2022            memory_flags: wgpu_hal::MemoryFlags::empty(),
2023            view_formats: Vec::new(),
2024        };
2025        // SAFETY: this context is always Vulkan and the frame identity check above established the
2026        // same logical device.
2027        let hal_device = unsafe { self.gpu.device().as_hal::<wgpu_hal::api::Vulkan>() }
2028            .ok_or(BridgeError::InvalidInput("renderer is not Vulkan"))?;
2029        let drop_owner = Arc::clone(&frame);
2030        // SAFETY: FFmpeg created the image with the validated descriptor and retains its allocation
2031        // through `drop_owner`; wgpu must not destroy it.
2032        let hal_texture = unsafe {
2033            hal_device.texture_from_raw(
2034                vk::Image::from_raw(info.images[0]),
2035                &hal_descriptor,
2036                Some(Box::new(move || drop(drop_owner))),
2037                wgpu_hal::vulkan::TextureMemory::External,
2038            )
2039        };
2040        let descriptor = wgpu::TextureDescriptor {
2041            label: Some("alpha2 FFmpeg decode surface"),
2042            size: hal_descriptor.size,
2043            mip_level_count: 1,
2044            sample_count: 1,
2045            dimension: wgpu::TextureDimension::D2,
2046            format: texture_format,
2047            usage: wgpu::TextureUsages::TEXTURE_BINDING,
2048            view_formats: &[],
2049        };
2050        // SAFETY: the batched acquire transitions the whole image to RESOURCE's shader-read layout
2051        // before its first wgpu submission, and the HAL texture came from this device.
2052        let texture = unsafe {
2053            self.gpu
2054                .device()
2055                .create_texture_from_hal::<wgpu_hal::api::Vulkan>(
2056                    hal_texture,
2057                    &descriptor,
2058                    wgpu::TextureUses::RESOURCE,
2059                )
2060        };
2061        let y = texture.create_view(&wgpu::TextureViewDescriptor {
2062            label: Some("alpha2 FFmpeg decode Y"),
2063            format: Some(luma_format),
2064            aspect: wgpu::TextureAspect::Plane0,
2065            ..Default::default()
2066        });
2067        let uv = texture.create_view(&wgpu::TextureViewDescriptor {
2068            label: Some("alpha2 FFmpeg decode UV"),
2069            format: Some(chroma_format),
2070            aspect: wgpu::TextureAspect::Plane1,
2071            ..Default::default()
2072        });
2073        let pixels = u64::from(coded_width).saturating_mul(u64::from(coded_height));
2074        let lease = Arc::new(VulkanImportLease {
2075            frame,
2076            _texture: texture,
2077            gpu: self.gpu.clone(),
2078            batch: Arc::clone(&self.submission_batch),
2079            tickets: Arc::clone(&self.import_tickets),
2080            ticket: Some(ticket),
2081            staged_signals: Mutex::new(Vec::new()),
2082            queued: AtomicBool::new(false),
2083            acquired: AtomicBool::new(false),
2084            released: AtomicBool::new(false),
2085            retained_bytes: pixels
2086                .saturating_mul(layout.bytes_per_pixel_numerator())
2087                .div_ceil(2),
2088        });
2089        Ok(ImportedVulkanFrame {
2090            y,
2091            uv,
2092            layout,
2093            coded_size: [coded_width, coded_height],
2094            lease,
2095        })
2096    }
2097}
2098
2099/// The adapter the shared device opens among `adapters`, which this sorts in
2100/// the order it chooses in ([`adapter_rank`]): the first that supports NV12
2101/// plane views and, with a `selector` (`ALPHA2_VULKAN_DEVICE`), whose name
2102/// contains it.
2103fn choose_adapter(
2104    adapters: &mut [wgpu_hal::ExposedAdapter<wgpu_hal::api::Vulkan>],
2105    selector: Option<&str>,
2106) -> Result<usize, SharedVulkanError> {
2107    let preference = wgpu::PowerPreference::from_env();
2108    adapters.sort_by_key(|adapter| adapter_rank(adapter.info.device_type, preference));
2109    adapters
2110        .iter()
2111        .position(|adapter| {
2112            selector.is_none_or(|needle| adapter.info.name.contains(needle))
2113                && adapter
2114                    .features
2115                    .contains(wgpu::Features::TEXTURE_FORMAT_NV12)
2116        })
2117        .ok_or_else(|| {
2118            SharedVulkanError(selector.map_or_else(
2119                || "no Vulkan adapter supports NV12 plane views".to_owned(),
2120                |needle| {
2121                    format!(
2122                        "no Vulkan adapter whose name contains ALPHA2_VULKAN_DEVICE={needle:?} \
2123                         supports NV12 plane views"
2124                    )
2125                },
2126            ))
2127        })
2128}
2129
2130/// Where the device [`SharedVulkanContext::create`] opens is in the
2131/// loader's physical-device order, for `selector` as `ALPHA2_VULKAN_DEVICE`
2132/// names it (`None` unset): the index `FFmpeg` takes for a Vulkan device of
2133/// its own (`av_hwdevice_ctx_create`'s `device`). A probe that opens its own
2134/// device passes this, so it opens the device an export renders, decodes, and
2135/// encodes on. `FFmpeg` reads a name that starts with a digit as an index,
2136/// so passing the variable on would pick another device, or none.
2137///
2138/// # Errors
2139/// Returns why no device is chosen: no Vulkan instance, or no adapter the
2140/// shared device could open.
2141pub fn shared_device_index(selector: Option<&str>) -> Result<usize, SharedVulkanError> {
2142    let _turn = alpha2_gpu::serialize_acquisition();
2143    let hal_instance = hal_instance()?;
2144    // SAFETY: adapter enumeration does not require a surface.
2145    let mut adapters = unsafe { hal_instance.enumerate_adapters(None) };
2146    let position = choose_adapter(&mut adapters, selector)?;
2147    let chosen = &adapters[position];
2148    let physical = chosen.adapter.raw_physical_device();
2149    // SAFETY: the instance is alive for the call; enumeration has no other precondition.
2150    let order = unsafe {
2151        chosen
2152            .adapter
2153            .shared_instance()
2154            .raw_instance()
2155            .enumerate_physical_devices()
2156    }
2157    .map_err(|error| SharedVulkanError(format!("Vulkan device enumeration: {error}")))?;
2158    order
2159        .iter()
2160        .position(|device| *device == physical)
2161        .ok_or_else(|| SharedVulkanError("the chosen adapter is not in the device list".into()))
2162}
2163
2164/// Where an adapter of `device_type` comes in the shared device's choice,
2165/// lowest first: a discrete GPU before an integrated one, unless
2166/// `WGPU_POWER_PREF` asks for low power, which puts the integrated one first,
2167/// as `wgpu` orders the renderer's adapters. The shared device is the
2168/// renderer's on Linux, so ignoring the variable here would ignore it for
2169/// every export. `ALPHA2_VULKAN_DEVICE` still picks by name among them.
2170fn adapter_rank(device_type: wgpu::DeviceType, preference: Option<wgpu::PowerPreference>) -> u8 {
2171    let low_power = preference == Some(wgpu::PowerPreference::LowPower);
2172    match device_type {
2173        wgpu::DeviceType::IntegratedGpu if low_power => 0,
2174        wgpu::DeviceType::DiscreteGpu if low_power => 1,
2175        wgpu::DeviceType::DiscreteGpu => 0,
2176        wgpu::DeviceType::IntegratedGpu => 1,
2177        _ => 2,
2178    }
2179}
2180
2181#[cfg(test)]
2182#[path = "vulkan_failure_tests.rs"]
2183mod failure_tests;
2184
2185#[cfg(test)]
2186#[path = "vulkan_tests.rs"]
2187mod tests;