Skip to main content

alpha2_native_gpu/
vulkan.rs

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