1use std::{
7 collections::VecDeque,
8 error::Error,
9 ffi::{CStr, c_void},
10 fmt,
11 ptr::NonNull,
12 sync::{
13 Arc, Mutex, Weak,
14 atomic::{AtomicBool, Ordering},
15 },
16};
17
18use alpha2_ffmpeg_bridge::{
19 BridgeError, VulkanDeviceInfo, VulkanFrameAccess, VulkanFrameAccessGuard, VulkanFrameLease,
20 VulkanQueueLock, create_vulkan_device_context,
21};
22use alpha2_gpu::{ExternalQueueLock, GpuContext};
23use ash::vk::{self, Handle};
24use rsmpeg::avutil::AVHWDeviceContext;
25use wgpu_hal::Instance as _;
26
27use crate::{NativeGpuIdentity, NativePlaneLayout};
28
29unsafe extern "C" {
32 fn alpha2_native_gpu_vulkan_probe(access: *mut VulkanFrameAccess, status: *mut i32) -> i32;
33 fn alpha2_native_gpu_vulkan_acquire_check(
34 access: *const VulkanFrameAccess,
35 status: *mut i32,
36 ) -> i32;
37 fn alpha2_native_gpu_vulkan_ticket_create(
38 device: u64,
39 allocator: u64,
40 queue_family: u32,
41 output: *mut *mut c_void,
42 status: *mut i32,
43 ) -> i32;
44 fn alpha2_native_gpu_vulkan_ticket_poll(ticket: *const c_void, status: *mut i32) -> i32;
45 fn alpha2_native_gpu_vulkan_ticket_reset(ticket: *mut c_void, status: *mut i32) -> i32;
46 fn alpha2_native_gpu_vulkan_acquire_submit(
47 access: *mut VulkanFrameAccess,
48 ticket: *mut c_void,
49 status: *mut i32,
50 ) -> i32;
51 fn alpha2_native_gpu_vulkan_release(
52 access: *mut VulkanFrameAccess,
53 ticket: *mut c_void,
54 status: *mut i32,
55 ) -> i32;
56 fn alpha2_native_gpu_vulkan_signal(
57 access: *mut VulkanFrameAccess,
58 semaphore: u64,
59 wait_value: u64,
60 signal_value: u64,
61 status: *mut i32,
62 ) -> i32;
63 fn alpha2_native_gpu_vulkan_import_destroy(ticket: *mut c_void);
64 fn alpha2_native_gpu_vulkan_import_retire(ticket: *mut c_void, timeout_ns: u64) -> i32;
65 #[cfg(test)]
66 fn alpha2_native_gpu_vulkan_retained_helpers() -> u64;
67 fn alpha2_native_gpu_vulkan_encode_copier_create(
68 device: u64,
69 queue_family: u32,
70 allocator: u64,
71 output: *mut *mut c_void,
72 status: *mut i32,
73 ) -> i32;
74 fn alpha2_native_gpu_vulkan_encode_copier_destroy(copier: *mut c_void, status: *mut i32)
75 -> i32;
76 fn alpha2_native_gpu_vulkan_encode_copier_slots() -> u32;
77 fn alpha2_native_gpu_vulkan_encode_copier_await_slot(
78 copier: *mut c_void,
79 slot: *mut u32,
80 status: *mut i32,
81 ) -> i32;
82 fn alpha2_native_gpu_vulkan_encode_copier_submit(
83 copier: *mut c_void,
84 access: *mut VulkanFrameAccess,
85 copy: *const VulkanEncodeCopy,
86 status: *mut i32,
87 ) -> i32;
88 fn alpha2_native_gpu_vulkan_set_wait_timeout(nanoseconds: u64);
89 fn alpha2_native_gpu_vulkan_wait_timeout() -> u64;
90}
91
92pub(crate) fn set_wait_timeout(timeout: std::time::Duration) {
94 let nanoseconds = u64::try_from(timeout.as_nanos()).unwrap_or(u64::MAX);
95 unsafe { alpha2_native_gpu_vulkan_set_wait_timeout(nanoseconds) };
97}
98
99fn wait_timeout() -> std::time::Duration {
101 std::time::Duration::from_nanos(unsafe { alpha2_native_gpu_vulkan_wait_timeout() })
103}
104
105fn helper_error(operation: &'static str, step: i32, status: i32) -> BridgeError {
108 crate::status::helper_failure(
109 alpha2_ffmpeg_bridge::NativeGpuBackend::Vulkan,
110 operation,
111 step,
112 status,
113 )
114}
115
116#[cfg(test)]
119pub(crate) mod inject {
120 use std::cell::Cell;
121
122 #[derive(Clone, Copy, Debug)]
124 pub(crate) enum Hook {
125 AcquireSubmit,
126 TicketPoll,
127 TicketReset,
128 Release,
129 }
130
131 thread_local! {
132 static COUNTDOWNS: [Cell<u32>; 4] =
133 const { [Cell::new(0), Cell::new(0), Cell::new(0), Cell::new(0)] };
134 static STATUSES: [Cell<i32>; 4] =
135 const { [Cell::new(0), Cell::new(0), Cell::new(0), Cell::new(0)] };
136 }
137
138 pub(crate) fn fail_nth(hook: Hook, nth: u32) {
141 fail_nth_with(hook, nth, super::vk::Result::ERROR_DEVICE_LOST.as_raw());
142 }
143
144 pub(crate) fn fail_nth_with(hook: Hook, nth: u32, status: i32) {
147 COUNTDOWNS.with(|countdowns| countdowns[hook as usize].set(nth));
148 STATUSES.with(|statuses| statuses[hook as usize].set(status));
149 }
150
151 pub(super) fn fails(hook: Hook, step: i32, status: &mut i32) -> Option<i32> {
154 let due = COUNTDOWNS.with(|countdowns| {
155 let countdown = &countdowns[hook as usize];
156 match countdown.get() {
157 0 => false,
158 1 => {
159 countdown.set(0);
160 true
161 }
162 left => {
163 countdown.set(left - 1);
164 false
165 }
166 }
167 });
168 due.then(|| {
169 *status = STATUSES.with(|statuses| statuses[hook as usize].get());
170 step
171 })
172 }
173}
174
175struct LeaseAccess<'a> {
184 frame: VulkanFrameAccessGuard<'a>,
185 ticket: NonNull<c_void>,
186}
187
188unsafe fn acquire_submit(lease: &mut LeaseAccess<'_>, status: &mut i32) -> i32 {
195 #[cfg(test)]
196 if let Some(step) = inject::fails(
197 inject::Hook::AcquireSubmit,
198 crate::interop::ALPHA2_GPU_STEP_QUEUE_SUBMIT,
199 status,
200 ) {
201 return step;
202 }
203 unsafe {
205 alpha2_native_gpu_vulkan_acquire_submit(
206 lease.frame.access_mut(),
207 lease.ticket.as_ptr(),
208 status,
209 )
210 }
211}
212
213unsafe 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 unsafe {
230 alpha2_native_gpu_vulkan_release(lease.frame.access_mut(), lease.ticket.as_ptr(), status)
231 }
232}
233
234unsafe fn ticket_poll(ticket: *const c_void, status: &mut i32) -> i32 {
240 #[cfg(test)]
241 if let Some(step) = inject::fails(
242 inject::Hook::TicketPoll,
243 crate::interop::ALPHA2_GPU_STEP_FENCE_STATUS,
244 status,
245 ) {
246 return step;
247 }
248 unsafe { alpha2_native_gpu_vulkan_ticket_poll(ticket, status) }
250}
251
252unsafe fn ticket_reset(ticket: *mut c_void, status: &mut i32) -> i32 {
258 #[cfg(test)]
259 if let Some(step) = inject::fails(
260 inject::Hook::TicketReset,
261 crate::interop::ALPHA2_GPU_STEP_RESET_FENCE,
262 status,
263 ) {
264 return step;
265 }
266 unsafe { alpha2_native_gpu_vulkan_ticket_reset(ticket, status) }
268}
269
270use crate::interop::Alpha2VulkanEncodeCopy as VulkanEncodeCopy;
271
272pub struct VulkanEncodeCopier {
282 copier: *mut c_void,
283 queue_lock: alpha2_ffmpeg_bridge::VulkanQueueLock,
286 retained: Option<Box<RetainedEncodeWork>>,
287 close_failed: bool,
288}
289
290struct RetainedEncodeWork {
293 copies: Vec<Option<EncodeCopyInputs>>,
294 _context: SharedVulkanContext,
295 #[cfg(test)]
296 lifetime_probe: Option<Arc<()>>,
297}
298
299struct EncodeCopyInputs {
300 _luma: wgpu::Texture,
301 _chroma: wgpu::Texture,
302 _frame: rsmpeg::avutil::AVFrame,
303}
304
305unsafe impl Send for VulkanEncodeCopier {}
308
309impl fmt::Debug for VulkanEncodeCopier {
310 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
311 formatter
312 .debug_struct("VulkanEncodeCopier")
313 .finish_non_exhaustive()
314 }
315}
316
317impl Drop for VulkanEncodeCopier {
318 fn drop(&mut self) {
319 if !self.copier.is_null() && !self.close_failed {
320 let _ = self.try_close();
321 }
322 if !self.copier.is_null() {
323 std::mem::forget(self.retained.take());
326 }
327 }
328}
329
330impl VulkanEncodeCopier {
331 pub fn finish(mut self) -> Result<(), BridgeError> {
337 self.try_close()
338 }
339
340 fn try_close(&mut self) -> Result<(), BridgeError> {
341 let mut status = 0;
342 let step =
345 unsafe { alpha2_native_gpu_vulkan_encode_copier_destroy(self.copier, &raw mut status) };
346 if step != 0 {
347 self.close_failed = true;
348 return Err(helper_error("encode copier teardown", step, status));
349 }
350 self.copier = std::ptr::null_mut();
351 Ok(())
354 }
355}
356
357impl VulkanEncodeCopier {
358 pub fn new(context: &SharedVulkanContext) -> Result<Self, BridgeError> {
365 let gpu = context.gpu();
366 let device = unsafe { gpu.device().as_hal::<wgpu_hal::api::Vulkan>() }
368 .map(|device| device.raw_device().handle().as_raw())
369 .ok_or(BridgeError::InvalidInput("renderer is not Vulkan"))?;
370 let mut copier = std::ptr::null_mut();
371 let mut status = 0;
372 let step = unsafe {
375 alpha2_native_gpu_vulkan_encode_copier_create(
376 device,
377 0,
378 0,
379 &raw mut copier,
380 &raw mut status,
381 )
382 };
383 if step != 0 {
384 return Err(helper_error("encode copier creation", step, status));
385 }
386 Ok(Self {
387 copier,
388 queue_lock: alpha2_ffmpeg_bridge::VulkanQueueLock::new(context.ffmpeg_device(), 0)?,
389 retained: Some(Box::new(RetainedEncodeWork {
390 copies: (0..Self::slots()).map(|_| None).collect(),
391 _context: context.clone(),
392 #[cfg(test)]
393 lifetime_probe: None,
394 })),
395 close_failed: false,
396 })
397 }
398
399 #[must_use]
406 pub fn slots() -> usize {
407 unsafe { alpha2_native_gpu_vulkan_encode_copier_slots() as usize }
409 }
410
411 pub fn await_slot(&mut self) -> Result<usize, BridgeError> {
420 let (mut slot, mut status) = (0_u32, 0);
421 let step = unsafe {
423 alpha2_native_gpu_vulkan_encode_copier_await_slot(
424 self.copier,
425 &raw mut slot,
426 &raw mut status,
427 )
428 };
429 if step != 0 {
430 return Err(helper_error("encode copy slot", step, status));
431 }
432 if let Some(retained) = self.retained.as_mut() {
433 retained.copies[slot as usize] = None;
434 }
435 Ok(slot as usize)
436 }
437
438 pub fn submit(
449 &mut self,
450 frame: &mut alpha2_ffmpeg_bridge::VulkanEncodeFrameAccess<'_>,
451 luma: &wgpu::Texture,
452 chroma: &wgpu::Texture,
453 size: (u32, u32),
454 ) -> Result<(), BridgeError> {
455 let slot = self.await_slot()?;
456 let inputs = EncodeCopyInputs {
457 _luma: luma.clone(),
458 _chroma: chroma.clone(),
459 _frame: frame.retain_frame()?,
460 };
461 let kept = self
462 .retained
463 .as_mut()
464 .and_then(|work| work.copies.get_mut(slot))
465 .ok_or(BridgeError::InvalidInput("invalid encode copier slot"))?;
466 let luma_image = unsafe { luma.as_hal::<wgpu_hal::api::Vulkan>() }
469 .map(|texture| unsafe { texture.raw_handle() }.as_raw())
470 .ok_or(BridgeError::InvalidInput("luma plane is not Vulkan"))?;
471 let chroma_image = unsafe { chroma.as_hal::<wgpu_hal::api::Vulkan>() }
473 .map(|texture| unsafe { texture.raw_handle() }.as_raw())
474 .ok_or(BridgeError::InvalidInput("chroma plane is not Vulkan"))?;
475 let copy = VulkanEncodeCopy {
476 luma_image,
477 chroma_image,
478 width: size.0,
479 height: size.1,
480 };
481 let queue = self.queue_lock.lock();
486 let mut status = 0;
487 let step = unsafe {
490 alpha2_native_gpu_vulkan_encode_copier_submit(
491 self.copier,
492 frame.access_mut(),
493 &raw const copy,
494 &raw mut status,
495 )
496 };
497 drop(queue);
498 if step != 0 {
499 return Err(helper_error("encode copy", step, status));
500 }
501 *kept = Some(inputs);
502 frame.commit();
503 Ok(())
504 }
505}
506
507pub fn probe_vulkan_submission(lease: &VulkanFrameLease) -> Result<(), BridgeError> {
514 let mut guard = lease.lock_access()?;
515 let mut status = 0;
516 let step = unsafe { alpha2_native_gpu_vulkan_probe(guard.access_mut(), &raw mut status) };
521 guard.commit();
524 if step != 0 {
525 return Err(helper_error("decode probe", step, status));
526 }
527 Ok(())
528}
529
530#[derive(Clone, Debug, Eq, PartialEq)]
532pub struct VideoEncodeExtent {
533 pub profile: &'static str,
537 pub minimum: (u32, u32),
539 pub maximum: (u32, u32),
541 pub granularity: (u32, u32),
550 pub max_p_picture_l0_references: u32,
553 pub max_b_picture_l0_references: u32,
557 pub max_l1_references: u32,
560 pub rate_control_modes: Vec<&'static str>,
563}
564
565impl VideoEncodeExtent {
566 #[must_use]
568 pub const fn supports_b_pictures(&self) -> bool {
569 self.max_b_picture_l0_references > 0 || self.max_l1_references > 0
570 }
571}
572
573impl VideoEncodeExtent {
574 #[must_use]
576 pub const fn accepts(&self, width: u32, height: u32) -> bool {
577 let coded = self.coded(width, height);
578 coded.0 >= self.minimum.0
579 && coded.1 >= self.minimum.1
580 && coded.0 <= self.maximum.0
581 && coded.1 <= self.maximum.1
582 }
583
584 #[must_use]
586 pub const fn coded(&self, width: u32, height: u32) -> (u32, u32) {
587 (
588 align_up(width, self.granularity.0),
589 align_up(height, self.granularity.1),
590 )
591 }
592}
593
594const fn align_up(value: u32, granularity: u32) -> u32 {
595 if granularity <= 1 {
596 return value;
597 }
598 value.div_ceil(granularity).saturating_mul(granularity)
599}
600
601const H264_ENCODE_PROFILES: [(vk::native::StdVideoH264ProfileIdc, &str); 3] = [
607 (
608 vk::native::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_BASELINE,
609 "constrained_baseline",
610 ),
611 (
612 vk::native::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_MAIN,
613 "main",
614 ),
615 (
616 vk::native::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_HIGH,
617 "high",
618 ),
619];
620
621fn last_supported_profile<T>(
624 mut query: impl FnMut(vk::native::StdVideoH264ProfileIdc) -> Option<T>,
625) -> Option<(&'static str, T)> {
626 H264_ENCODE_PROFILES
627 .iter()
628 .filter_map(|&(idc, name)| query(idc).map(|answer| (name, answer)))
629 .last()
630}
631
632fn h264_encode_extent(
642 entry: &ash::Entry,
643 instance: &ash::Instance,
644 physical: vk::PhysicalDevice,
645) -> Option<VideoEncodeExtent> {
646 let video = ash::khr::video_queue::Instance::new(entry, instance);
647 let (profile, mut extent) = last_supported_profile(|idc| {
648 let mut h264 = vk::VideoEncodeH264ProfileInfoKHR::default().std_profile_idc(idc);
649 let profile = vk::VideoProfileInfoKHR::default()
650 .video_codec_operation(vk::VideoCodecOperationFlagsKHR::ENCODE_H264)
651 .chroma_subsampling(vk::VideoChromaSubsamplingFlagsKHR::TYPE_420)
652 .luma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::TYPE_8)
653 .chroma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::TYPE_8)
654 .push_next(&mut h264);
655 let mut h264_capabilities = vk::VideoEncodeH264CapabilitiesKHR::default();
656 let mut encode = vk::VideoEncodeCapabilitiesKHR::default();
657 let mut capabilities = vk::VideoCapabilitiesKHR::default()
658 .push_next(&mut encode)
659 .push_next(&mut h264_capabilities);
660 let result = unsafe {
666 (video.fp().get_physical_device_video_capabilities_khr)(
667 physical,
668 &raw const profile,
669 &raw mut capabilities,
670 )
671 };
672 if result != vk::Result::SUCCESS {
673 return None;
674 }
675 let coded = (capabilities.min_coded_extent, capabilities.max_coded_extent);
676 Some(encode_extent(coded, &encode, &h264_capabilities))
677 })?;
678 extent.profile = profile;
679 Some(extent)
680}
681
682fn encode_extent(
686 (minimum, maximum): (vk::Extent2D, vk::Extent2D),
687 encode: &vk::VideoEncodeCapabilitiesKHR<'_>,
688 h264: &vk::VideoEncodeH264CapabilitiesKHR<'_>,
689) -> VideoEncodeExtent {
690 VideoEncodeExtent {
691 profile: "",
692 minimum: (minimum.width, minimum.height),
693 maximum: (maximum.width, maximum.height),
694 granularity: (
697 encode.encode_input_picture_granularity.width.max(1),
698 encode.encode_input_picture_granularity.height.max(1),
699 ),
700 max_p_picture_l0_references: h264.max_p_picture_l0_reference_count,
701 max_b_picture_l0_references: h264.max_b_picture_l0_reference_count,
702 max_l1_references: h264.max_l1_reference_count,
703 rate_control_modes: [
704 (vk::VideoEncodeRateControlModeFlagsKHR::CBR, "cbr"),
705 (vk::VideoEncodeRateControlModeFlagsKHR::VBR, "vbr"),
706 (vk::VideoEncodeRateControlModeFlagsKHR::DISABLED, "disabled"),
707 ]
708 .into_iter()
709 .filter(|(flag, _)| encode.rate_control_modes.contains(*flag))
710 .map(|(_, name)| name)
711 .collect(),
712 }
713}
714
715#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
717pub struct VulkanLayerVersion {
718 pub major: u32,
720 pub minor: u32,
722 pub patch: u32,
724}
725
726impl VulkanLayerVersion {
727 pub const VIDEO_SESSIONS: Self = Self {
732 major: 1,
733 minor: 4,
734 patch: 0,
735 };
736
737 #[must_use]
739 pub const fn from_packed(version: u32) -> Self {
740 Self {
741 major: vk::api_version_major(version),
742 minor: vk::api_version_minor(version),
743 patch: vk::api_version_patch(version),
744 }
745 }
746}
747
748impl fmt::Display for VulkanLayerVersion {
749 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
750 write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
751 }
752}
753
754const KHRONOS_VALIDATION: &str = "VK_LAYER_KHRONOS_validation";
755
756fn activated_validation_layer(
770 instance: &ash::Instance,
771 physical: vk::PhysicalDevice,
772) -> Option<VulkanLayerVersion> {
773 let layers = unsafe { instance.enumerate_device_layer_properties(physical) }.ok()?;
775 layers
776 .iter()
777 .find(|layer| {
778 layer
779 .layer_name_as_c_str()
780 .is_ok_and(|name| name.to_bytes() == KHRONOS_VALIDATION.as_bytes())
781 })
782 .map(|layer| VulkanLayerVersion::from_packed(layer.spec_version))
783}
784
785fn hal_instance() -> Result<wgpu_hal::vulkan::Instance, SharedVulkanError> {
787 let hal_desc = wgpu_hal::InstanceDescriptor {
788 name: "alpha2 shared Vulkan decode",
789 flags: wgpu::InstanceFlags::from_env_or_default(),
790 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
791 backend_options: wgpu::BackendOptions::from_env_or_default(),
792 telemetry: None,
793 display: None,
794 };
795 unsafe {
797 wgpu_hal::vulkan::Instance::init(&hal_desc)
798 .map_err(|error| SharedVulkanError(format!("Vulkan instance: {error}")))
799 }
800}
801
802#[derive(Debug)]
810struct SharedQueueLock(VulkanQueueLock);
811
812impl ExternalQueueLock for SharedQueueLock {
813 fn lock(&self) {
814 unsafe { self.0.raw_lock() };
817 }
818
819 fn unlock(&self) {
820 unsafe { self.0.raw_unlock() };
823 }
824}
825
826#[derive(Debug)]
828pub struct SharedVulkanError(String);
829
830impl fmt::Display for SharedVulkanError {
831 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
832 formatter.write_str(&self.0)
833 }
834}
835
836impl Error for SharedVulkanError {}
837
838#[derive(Debug)]
840pub struct ImportedVulkanFrame {
841 pub y: wgpu::TextureView,
843 pub uv: wgpu::TextureView,
845 pub layout: NativePlaneLayout,
847 pub coded_size: [u32; 2],
852 pub lease: Arc<VulkanImportLease>,
854}
855
856#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
858pub struct ImportTicketStats {
859 pub created: u64,
861 pub reused: u64,
863 pub idle: usize,
865 pub pending: usize,
867 pub abandoned: u64,
869}
870
871struct PooledTicket {
873 ticket: NonNull<c_void>,
874 device: u64,
875 queue_family: u32,
876}
877
878#[derive(Default)]
886struct ImportTicketPool {
887 state: Mutex<ImportTicketPoolState>,
888}
889
890#[derive(Default)]
891struct ImportTicketPoolState {
892 idle: Vec<PooledTicket>,
893 pending: VecDeque<PooledTicket>,
894 created: u64,
895 reused: u64,
896 abandoned: u64,
897}
898
899const MAX_IDLE_IMPORT_TICKETS: usize = 8;
902const MAX_PENDING_IMPORT_TICKETS: usize = 32;
903
904unsafe impl Send for ImportTicketPool {}
907unsafe impl Sync for ImportTicketPool {}
909
910impl ImportTicketPool {
911 fn take(
912 &self,
913 device: u64,
914 allocator: u64,
915 queue_family: u32,
916 ) -> Result<PooledTicket, BridgeError> {
917 let mut state = self
918 .state
919 .lock()
920 .unwrap_or_else(std::sync::PoisonError::into_inner);
921 state.reclaim();
922 if let Some(index) = state
923 .idle
924 .iter()
925 .position(|idle| idle.device == device && idle.queue_family == queue_family)
926 {
927 state.reused = state.reused.saturating_add(1);
928 return Ok(state.idle.swap_remove(index));
929 }
930 state.created = state.created.saturating_add(1);
931 drop(state);
932 let (mut raw, mut status) = (std::ptr::null_mut(), 0);
933 let step = unsafe {
936 alpha2_native_gpu_vulkan_ticket_create(
937 device,
938 allocator,
939 queue_family,
940 &raw mut raw,
941 &raw mut status,
942 )
943 };
944 if step != 0 {
945 return Err(helper_error("import ticket creation", step, status));
946 }
947 NonNull::new(raw)
948 .map(|ticket| PooledTicket {
949 ticket,
950 device,
951 queue_family,
952 })
953 .ok_or(BridgeError::InvalidInput(
954 "Vulkan import ticket creation returned no ticket",
955 ))
956 }
957
958 fn give_back(&self, ticket: PooledTicket) {
961 let mut state = self
962 .state
963 .lock()
964 .unwrap_or_else(std::sync::PoisonError::into_inner);
965 state.pending.push_back(ticket);
966 if state.pending.len() > MAX_PENDING_IMPORT_TICKETS {
967 state.reclaim();
968 }
969 let excess = state
970 .pending
971 .len()
972 .saturating_sub(MAX_PENDING_IMPORT_TICKETS);
973 let oldest = state.pending.drain(..excess).collect::<Vec<_>>();
974 state.abandoned = state.abandoned.saturating_add(oldest.len() as u64);
975 drop(state);
977 retire(oldest);
978 }
979
980 fn stats(&self) -> ImportTicketStats {
981 let state = self
982 .state
983 .lock()
984 .unwrap_or_else(std::sync::PoisonError::into_inner);
985 ImportTicketStats {
986 created: state.created,
987 reused: state.reused,
988 idle: state.idle.len(),
989 pending: state.pending.len(),
990 abandoned: state.abandoned,
991 }
992 }
993}
994
995impl ImportTicketPoolState {
996 fn reclaim(&mut self) {
999 let mut index = 0;
1000 while index < self.pending.len() {
1001 let ticket = self.pending[index].ticket;
1002 let mut status = 0;
1003 match unsafe { ticket_poll(ticket.as_ptr(), &mut status) } {
1005 0 => index += 1,
1006 1 => {
1007 let Some(finished) = self.pending.remove(index) else {
1008 break;
1009 };
1010 let reset = unsafe { ticket_reset(ticket.as_ptr(), &mut status) };
1012 if reset != 0 {
1013 self.abandoned = self.abandoned.saturating_add(1);
1014 } else if self.idle.len() < MAX_IDLE_IMPORT_TICKETS {
1015 self.idle.push(finished);
1016 } else {
1017 unsafe { alpha2_native_gpu_vulkan_import_destroy(ticket.as_ptr()) };
1019 }
1020 }
1021 _ => {
1022 self.pending.remove(index);
1025 self.abandoned = self.abandoned.saturating_add(1);
1026 }
1027 }
1028 }
1029 }
1030}
1031
1032impl Drop for ImportTicketPool {
1033 fn drop(&mut self) {
1034 let state = self
1035 .state
1036 .get_mut()
1037 .unwrap_or_else(std::sync::PoisonError::into_inner);
1038 for idle in state.idle.drain(..) {
1039 unsafe { alpha2_native_gpu_vulkan_import_destroy(idle.ticket.as_ptr()) };
1041 }
1042 retire(state.pending.drain(..));
1043 }
1044}
1045
1046fn retire(tickets: impl IntoIterator<Item = PooledTicket>) {
1049 let deadline = std::time::Instant::now().checked_add(wait_timeout());
1050 for pooled in tickets {
1051 let left = deadline.map_or(std::time::Duration::MAX, |deadline| {
1052 deadline.saturating_duration_since(std::time::Instant::now())
1053 });
1054 let nanoseconds = u64::try_from(left.as_nanos()).unwrap_or(u64::MAX);
1055 unsafe { alpha2_native_gpu_vulkan_import_retire(pooled.ticket.as_ptr(), nanoseconds) };
1058 }
1059}
1060
1061pub struct VulkanImportLease {
1063 frame: Arc<VulkanFrameLease>,
1064 _texture: wgpu::Texture,
1065 gpu: GpuContext,
1066 batch: Arc<VulkanSubmissionBatch>,
1067 tickets: Arc<ImportTicketPool>,
1068 ticket: Option<PooledTicket>,
1071 staged_signals: Mutex<Vec<StagedSignal>>,
1072 queued: AtomicBool,
1073 acquired: AtomicBool,
1074 released: AtomicBool,
1075 retained_bytes: u64,
1076}
1077
1078#[derive(Clone, Copy, Debug)]
1079struct StagedSignal {
1080 semaphore: u64,
1081 acquired_value: u64,
1082 signal_value: u64,
1083}
1084
1085#[derive(Default)]
1086struct VulkanSubmissionBatch {
1087 pending: Mutex<Vec<Weak<VulkanImportLease>>>,
1088}
1089
1090impl VulkanSubmissionBatch {
1091 fn enqueue(&self, lease: &Arc<VulkanImportLease>) {
1092 if !lease.queued.swap(true, Ordering::AcqRel) {
1093 self.pending
1094 .lock()
1095 .unwrap_or_else(std::sync::PoisonError::into_inner)
1096 .push(Arc::downgrade(lease));
1097 }
1098 }
1099
1100 fn flush(&self) -> Result<(), BridgeError> {
1101 let leases = self
1102 .pending
1103 .lock()
1104 .unwrap_or_else(std::sync::PoisonError::into_inner)
1105 .drain(..)
1106 .filter_map(|lease| lease.upgrade())
1107 .filter(|lease| !lease.acquired.load(Ordering::Acquire))
1108 .collect::<Vec<_>>();
1109 if leases.is_empty() {
1110 return Ok(());
1111 }
1112 let result = flush_leases(&leases);
1115 if result.is_err() {
1116 for lease in &leases {
1117 if !lease.acquired.load(Ordering::Acquire) {
1118 lease.queued.store(false, Ordering::Release);
1119 }
1120 }
1121 }
1122 result
1123 }
1124}
1125
1126fn flush_leases(leases: &[Arc<VulkanImportLease>]) -> Result<(), BridgeError> {
1152 for lease in leases {
1153 let mut access = lease.lock()?;
1154 if lease.acquired.load(Ordering::Acquire) {
1156 continue;
1157 }
1158 let mut status = 0;
1159 let step = unsafe { acquire_submit(&mut access, &mut status) };
1162 if step != 0 {
1163 return Err(helper_error("import acquire", step, status));
1164 }
1165 lease.acquired.store(true, Ordering::Release);
1166 access.frame.commit();
1169 lease.stage_submission_signal(unsafe { access.frame.access_mut() })?;
1173 }
1174 Ok(())
1175}
1176
1177unsafe impl Send for VulkanImportLease {}
1183unsafe impl Sync for VulkanImportLease {}
1185
1186impl fmt::Debug for VulkanImportLease {
1187 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1188 formatter
1189 .debug_struct("VulkanImportLease")
1190 .field("released", &self.released.load(Ordering::Acquire))
1191 .field("retained_bytes", &self.retained_bytes)
1192 .finish_non_exhaustive()
1193 }
1194}
1195
1196impl VulkanImportLease {
1197 fn lock(&self) -> Result<LeaseAccess<'_>, BridgeError> {
1200 let ticket = self
1201 .ticket
1202 .as_ref()
1203 .map(|pooled| pooled.ticket)
1204 .ok_or(BridgeError::InvalidInput("missing Vulkan import ticket"))?;
1205 Ok(LeaseAccess {
1206 frame: self.frame.lock_access()?,
1207 ticket,
1208 })
1209 }
1210
1211 #[must_use]
1213 pub const fn retained_bytes(&self) -> u64 {
1214 self.retained_bytes
1215 }
1216
1217 #[must_use]
1224 pub fn staged_signals(&self) -> usize {
1225 self.staged_signals
1226 .lock()
1227 .unwrap_or_else(std::sync::PoisonError::into_inner)
1228 .len()
1229 }
1230
1231 pub fn prepare_submission(self: &Arc<Self>) {
1235 self.batch.enqueue(self);
1236 }
1237
1238 pub fn flush_submission(&self) -> Result<(), BridgeError> {
1247 self.batch.flush()
1248 }
1249
1250 fn stage_submission_signal(&self, access: &mut VulkanFrameAccess) -> Result<(), BridgeError> {
1253 let mut staged = self
1254 .staged_signals
1255 .lock()
1256 .unwrap_or_else(std::sync::PoisonError::into_inner);
1257 if !staged.is_empty() || self.released.load(Ordering::Acquire) {
1258 return Ok(());
1259 }
1260 let queue = unsafe { self.gpu.queue().as_hal::<wgpu_hal::api::Vulkan>() }
1262 .ok_or(BridgeError::InvalidInput("renderer queue is not Vulkan"))?;
1263 let descriptor = access.frame_mut();
1264 for index in 0..descriptor.images.len() {
1265 if descriptor.images[index] == 0 {
1266 continue;
1267 }
1268 let semaphore = descriptor.semaphores[index];
1269 let acquired_value = descriptor.semaphore_values[index];
1270 if semaphore == 0 {
1271 return Err(BridgeError::InvalidInput("frame has no timeline semaphore"));
1272 }
1273 if let Some(existing) = staged
1274 .iter_mut()
1275 .find(|existing| existing.semaphore == semaphore)
1276 {
1277 existing.acquired_value = existing.acquired_value.max(acquired_value);
1278 existing.signal_value = existing.acquired_value.saturating_add(1);
1279 } else {
1280 staged.push(StagedSignal {
1281 semaphore,
1282 acquired_value,
1283 signal_value: acquired_value.saturating_add(1),
1284 });
1285 }
1286 }
1287 for signal in &*staged {
1288 queue.add_signal_semaphore(
1289 vk::Semaphore::from_raw(signal.semaphore),
1290 Some(signal.signal_value),
1291 );
1292 for index in 0..descriptor.images.len() {
1293 if descriptor.semaphores[index] == signal.semaphore {
1294 descriptor.semaphore_values[index] = signal.signal_value;
1295 }
1296 }
1297 }
1298 Ok(())
1299 }
1300
1301 pub fn submitted(&self) -> Result<(), BridgeError> {
1311 if self.released.load(Ordering::Acquire) {
1312 return Ok(());
1313 }
1314 if !self
1315 .staged_signals
1316 .lock()
1317 .unwrap_or_else(std::sync::PoisonError::into_inner)
1318 .is_empty()
1319 {
1320 self.released.store(true, Ordering::Release);
1321 return Ok(());
1322 }
1323 let mut access = self.lock()?;
1324 if self.released.load(Ordering::Acquire) {
1326 return Ok(());
1327 }
1328 let mut status = 0;
1329 let step = unsafe { release(&mut access, &mut status) };
1332 if step != 0 {
1333 return Err(helper_error("import release", step, status));
1334 }
1335 access.frame.commit();
1336 self.released.store(true, Ordering::Release);
1337 Ok(())
1338 }
1339}
1340
1341impl Drop for VulkanImportLease {
1342 fn drop(&mut self) {
1362 let Some(pooled) = self.ticket.take() else {
1363 return;
1364 };
1365 if self.acquired.load(Ordering::Acquire) && !self.released.load(Ordering::Acquire) {
1366 let staged = std::mem::take(
1367 self.staged_signals
1368 .get_mut()
1369 .unwrap_or_else(std::sync::PoisonError::into_inner),
1370 );
1371 if let Ok(frame) = self.frame.lock_access() {
1372 let mut lease = LeaseAccess {
1374 frame,
1375 ticket: pooled.ticket,
1376 };
1377 if staged.is_empty() {
1378 let mut status = 0;
1379 if unsafe { release(&mut lease, &mut status) } == 0 {
1381 lease.frame.commit();
1382 }
1383 } else {
1384 let queue = unsafe { self.gpu.queue().as_hal::<wgpu_hal::api::Vulkan>() };
1386 for signal in &staged {
1387 let unsubmitted = queue.as_ref().is_some_and(|queue| {
1388 queue.remove_signal_semaphore(vk::Semaphore::from_raw(signal.semaphore))
1389 });
1390 if unsubmitted {
1391 let mut status = 0;
1392 let _ = unsafe {
1396 alpha2_native_gpu_vulkan_signal(
1397 lease.frame.access_mut(),
1398 signal.semaphore,
1399 signal.acquired_value,
1400 signal.signal_value,
1401 &raw mut status,
1402 )
1403 };
1404 }
1405 }
1406 }
1407 }
1408 }
1409 self.tickets.give_back(pooled);
1410 }
1411}
1412
1413#[derive(Clone, Copy, Debug, Default)]
1421struct FfmpegDeviceFeatures {
1422 synchronization2: bool,
1423 sampler_ycbcr_conversion: bool,
1424 video_maintenance1: bool,
1425}
1426
1427fn ffmpeg_device_features(
1430 instance: &ash::Instance,
1431 physical: vk::PhysicalDevice,
1432 video_maintenance1: bool,
1433) -> FfmpegDeviceFeatures {
1434 let mut synchronization2 = vk::PhysicalDeviceSynchronization2Features::default();
1435 let mut ycbcr = vk::PhysicalDeviceSamplerYcbcrConversionFeatures::default();
1436 let mut maintenance = vk::PhysicalDeviceVideoMaintenance1FeaturesKHR::default();
1437 let mut features = vk::PhysicalDeviceFeatures2::default()
1438 .push_next(&mut synchronization2)
1439 .push_next(&mut ycbcr);
1440 if video_maintenance1 {
1441 features = features.push_next(&mut maintenance);
1442 }
1443 unsafe { instance.get_physical_device_features2(physical, &mut features) };
1446 FfmpegDeviceFeatures {
1447 synchronization2: synchronization2.synchronization2 == vk::TRUE,
1448 sampler_ycbcr_conversion: ycbcr.sampler_ycbcr_conversion == vk::TRUE,
1449 video_maintenance1: video_maintenance1 && maintenance.video_maintenance1 == vk::TRUE,
1450 }
1451}
1452
1453impl FfmpegDeviceFeatures {
1454 fn enable(self, create_info: &mut vk::DeviceCreateInfo<'_>) {
1460 let mut info = std::mem::take(create_info);
1461 if self.synchronization2 {
1462 info = info.push_next(Box::leak(Box::new(
1463 vk::PhysicalDeviceSynchronization2Features::default().synchronization2(true),
1464 )));
1465 }
1466 if self.sampler_ycbcr_conversion {
1467 info = info.push_next(Box::leak(Box::new(
1468 vk::PhysicalDeviceSamplerYcbcrConversionFeatures::default()
1469 .sampler_ycbcr_conversion(true),
1470 )));
1471 }
1472 if self.video_maintenance1 {
1473 info = info.push_next(Box::leak(Box::new(
1474 vk::PhysicalDeviceVideoMaintenance1FeaturesKHR::default().video_maintenance1(true),
1475 )));
1476 }
1477 *create_info = info;
1478 }
1479}
1480
1481#[derive(Clone)]
1483pub struct SharedVulkanContext {
1484 import_tickets: Arc<ImportTicketPool>,
1487 ffmpeg_device: AVHWDeviceContext,
1489 gpu: GpuContext,
1490 submission_batch: Arc<VulkanSubmissionBatch>,
1491 identity: NativeGpuIdentity,
1492 decode_codecs: Vec<&'static str>,
1493 encode_codecs: Vec<&'static str>,
1494 encode_extent: Option<VideoEncodeExtent>,
1495 validation_layer: Option<VulkanLayerVersion>,
1496}
1497
1498impl fmt::Debug for SharedVulkanContext {
1499 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1500 formatter
1501 .debug_struct("SharedVulkanContext")
1502 .field("identity", &self.identity)
1503 .field("decode_codecs", &self.decode_codecs)
1504 .field("encode_codecs", &self.encode_codecs)
1505 .finish_non_exhaustive()
1506 }
1507}
1508
1509impl SharedVulkanContext {
1510 #[allow(clippy::too_many_lines)]
1515 pub fn create() -> Result<Self, SharedVulkanError> {
1516 let _turn = alpha2_gpu::serialize_acquisition();
1517 let hal_instance = hal_instance()?;
1518 let mut adapters = unsafe { hal_instance.enumerate_adapters(None) };
1520 let position = choose_adapter(
1521 &mut adapters,
1522 std::env::var("ALPHA2_VULKAN_DEVICE").ok().as_deref(),
1523 )?;
1524 let exposed = adapters.swap_remove(position);
1525 let physical = exposed.adapter.raw_physical_device();
1526 let shared_instance = exposed.adapter.shared_instance();
1527 let raw_instance = shared_instance.raw_instance().handle();
1528 let validation_layer = activated_validation_layer(shared_instance.raw_instance(), physical);
1529 let queue_properties = unsafe {
1531 shared_instance
1532 .raw_instance()
1533 .get_physical_device_queue_family_properties(physical)
1534 };
1535 let graphics_family = u32::try_from(
1536 queue_properties
1537 .iter()
1538 .position(|family| {
1539 family.queue_flags.contains(vk::QueueFlags::GRAPHICS) && family.queue_count > 0
1540 })
1541 .ok_or_else(|| SharedVulkanError("Vulkan adapter has no graphics queue".into()))?,
1542 )
1543 .map_err(|_| SharedVulkanError("graphics queue index is out of range".into()))?;
1544 if graphics_family != 0 {
1545 return Err(SharedVulkanError(
1546 "wgpu-hal shared-device path requires graphics queue family 0".into(),
1547 ));
1548 }
1549 let graphics_flags = queue_properties[0].queue_flags;
1550 if !graphics_flags.contains(vk::QueueFlags::COMPUTE | vk::QueueFlags::TRANSFER) {
1551 return Err(SharedVulkanError(
1552 "graphics queue family lacks FFmpeg compute/transfer capabilities".into(),
1553 ));
1554 }
1555 let decode_family = u32::try_from(
1556 queue_properties
1557 .iter()
1558 .position(|family| {
1559 family
1560 .queue_flags
1561 .contains(vk::QueueFlags::VIDEO_DECODE_KHR)
1562 && family.queue_count > 0
1563 })
1564 .ok_or_else(|| {
1565 SharedVulkanError("Vulkan adapter has no video-decode queue".into())
1566 })?,
1567 )
1568 .map_err(|_| SharedVulkanError("video-decode queue index is out of range".into()))?;
1569 if decode_family == graphics_family {
1570 return Err(SharedVulkanError(
1571 "shared Vulkan device requires a decode queue separate from wgpu graphics".into(),
1572 ));
1573 }
1574 let encode_family = queue_properties
1577 .iter()
1578 .position(|family| {
1579 family
1580 .queue_flags
1581 .contains(vk::QueueFlags::VIDEO_ENCODE_KHR)
1582 && family.queue_count > 0
1583 })
1584 .and_then(|index| u32::try_from(index).ok());
1585 let extension_properties = unsafe {
1587 shared_instance
1588 .raw_instance()
1589 .enumerate_device_extension_properties(physical)
1590 }
1591 .map_err(|error| SharedVulkanError(format!("Vulkan extensions: {error}")))?;
1592 let supports = |name: &CStr| {
1593 extension_properties.iter().any(|property| {
1594 property
1595 .extension_name_as_c_str()
1596 .is_ok_and(|candidate| candidate == name)
1597 })
1598 };
1599 let mut video_extensions = vec![
1600 ash::khr::video_queue::NAME,
1601 ash::khr::video_decode_queue::NAME,
1602 ];
1603 for extension in [
1604 ash::khr::video_decode_h264::NAME,
1605 ash::khr::video_decode_h265::NAME,
1606 ash::khr::video_decode_av1::NAME,
1607 ] {
1608 if supports(extension) {
1609 video_extensions.push(extension);
1610 }
1611 }
1612 if !video_extensions.iter().all(|extension| supports(extension))
1613 || !video_extensions.contains(&ash::khr::video_decode_h264::NAME)
1614 {
1615 return Err(SharedVulkanError(
1616 "Vulkan H.264 video decode is unavailable".into(),
1617 ));
1618 }
1619 let encode_extensions = [
1623 ash::khr::video_encode_queue::NAME,
1624 ash::khr::video_maintenance1::NAME,
1625 ash::khr::video_encode_h264::NAME,
1626 ];
1627 let encode_family = encode_family.filter(|_| {
1628 encode_extensions
1629 .iter()
1630 .all(|extension| supports(extension))
1631 });
1632 let h264_extent = encode_family.and_then(|_| {
1637 h264_encode_extent(
1638 shared_instance.entry(),
1639 shared_instance.raw_instance(),
1640 physical,
1641 )
1642 });
1643 if let Some(family) = encode_family {
1644 video_extensions.extend_from_slice(&encode_extensions);
1645 let _ = family;
1646 }
1647 let mut decode_caps = vk::VideoCodecOperationFlagsKHR::empty();
1648 let mut decode_codecs = Vec::new();
1649 for (extension, cap, label) in [
1650 (
1651 ash::khr::video_decode_h264::NAME,
1652 vk::VideoCodecOperationFlagsKHR::DECODE_H264,
1653 "h264",
1654 ),
1655 (
1656 ash::khr::video_decode_h265::NAME,
1657 vk::VideoCodecOperationFlagsKHR::DECODE_H265,
1658 "hevc",
1659 ),
1660 (
1661 ash::khr::video_decode_av1::NAME,
1662 vk::VideoCodecOperationFlagsKHR::DECODE_AV1,
1663 "av1",
1664 ),
1665 ] {
1666 if video_extensions.contains(&extension) {
1667 decode_caps |= cap;
1668 decode_codecs.push(label);
1669 }
1670 }
1671 let required_features = (exposed.features
1674 & (wgpu::Features::TIMESTAMP_QUERY
1675 | wgpu::Features::TEXTURE_FORMAT_P010
1676 | wgpu::Features::TEXTURE_FORMAT_16BIT_NORM))
1677 | wgpu::Features::TEXTURE_FORMAT_NV12;
1678 let required_limits = wgpu::Limits::downlevel_webgl2_defaults()
1679 .using_resolution(exposed.capabilities.limits.clone());
1680 let enabled_extensions = exposed
1681 .adapter
1682 .required_device_extensions(required_features);
1683 let core = exposed
1684 .adapter
1685 .physical_device_features(&enabled_extensions, required_features)
1686 .get_core();
1687 let ffmpeg_features = ffmpeg_device_features(
1688 shared_instance.raw_instance(),
1689 physical,
1690 video_extensions.contains(&ash::khr::video_maintenance1::NAME),
1691 );
1692 let extra_family = decode_family;
1693 let extra_encode_family =
1694 encode_family.filter(|family| *family != graphics_family && *family != decode_family);
1695 let extra_extensions = video_extensions.clone();
1696 let open = unsafe {
1698 exposed.adapter.open_with_callback(
1699 required_features,
1700 &required_limits,
1701 &wgpu::MemoryHints::Performance,
1702 Some(Box::new(move |args| {
1703 ffmpeg_features.enable(args.create_info);
1704 for extension in extra_extensions {
1705 if !args.extensions.contains(&extension) {
1706 args.extensions.push(extension);
1707 }
1708 }
1709 if extra_family != 0 {
1710 args.queue_create_infos.push(
1711 vk::DeviceQueueCreateInfo::default()
1712 .queue_family_index(extra_family)
1713 .queue_priorities(&[1.0]),
1714 );
1715 }
1716 if let Some(family) = extra_encode_family {
1717 args.queue_create_infos.push(
1718 vk::DeviceQueueCreateInfo::default()
1719 .queue_family_index(family)
1720 .queue_priorities(&[1.0]),
1721 );
1722 }
1723 })),
1724 )
1725 }
1726 .map_err(|error| SharedVulkanError(format!("shared Vulkan device: {error}")))?;
1727 let raw_device = open.device.raw_device().handle();
1728 let device_extensions = open
1729 .device
1730 .enabled_device_extensions()
1731 .iter()
1732 .map(|name| name.to_string_lossy().into_owned())
1733 .collect::<Vec<_>>();
1734 let instance_extensions = shared_instance
1735 .extensions()
1736 .iter()
1737 .map(|name| name.to_string_lossy().into_owned())
1738 .collect::<Vec<_>>();
1739 let ffmpeg_device = unsafe {
1743 create_vulkan_device_context(&VulkanDeviceInfo {
1744 instance: raw_instance.as_raw(),
1745 physical_device: physical.as_raw(),
1746 device: raw_device.as_raw(),
1747 graphics_queue_family: 0,
1748 graphics_queue_count: 1,
1749 decode_queue_family: decode_family,
1750 decode_queue_count: 1,
1751 decode_video_caps: decode_caps.as_raw(),
1752 encode_queue_family: encode_family.unwrap_or(0),
1753 encode_queue_count: u32::from(encode_family.is_some()),
1754 encode_video_caps: encode_family
1755 .map_or(0, |_| vk::VideoCodecOperationFlagsKHR::ENCODE_H264.as_raw()),
1756 core_features: [
1757 core.shader_image_gather_extended,
1758 core.fragment_stores_and_atomics,
1759 core.shader_int64,
1760 core.vertex_pipeline_stores_and_atomics,
1761 ],
1762 synchronization2: u32::from(ffmpeg_features.synchronization2),
1763 sampler_ycbcr_conversion: u32::from(ffmpeg_features.sampler_ycbcr_conversion),
1764 video_maintenance1: u32::from(ffmpeg_features.video_maintenance1),
1765 instance_extensions,
1766 device_extensions,
1767 })
1768 }
1769 .map_err(|error| SharedVulkanError(format!("FFmpeg Vulkan device: {error}")))?;
1770 let wgpu_instance =
1772 unsafe { wgpu::Instance::from_hal::<wgpu_hal::api::Vulkan>(hal_instance) };
1773 let adapter = unsafe { wgpu_instance.create_adapter_from_hal(exposed) };
1775 let descriptor = wgpu::DeviceDescriptor {
1776 label: Some("alpha2 shared Vulkan decode"),
1777 required_features,
1778 required_limits,
1779 ..Default::default()
1780 };
1781 let (device, queue) = unsafe { adapter.create_device_from_hal(open, &descriptor) }
1783 .map_err(|error| SharedVulkanError(format!("wgpu adoption: {error}")))?;
1784 let queue_lock: Arc<dyn ExternalQueueLock> = Arc::new(SharedQueueLock(
1788 VulkanQueueLock::new(ffmpeg_device.clone(), 0)
1789 .map_err(|error| SharedVulkanError(format!("FFmpeg Vulkan queue lock: {error}")))?,
1790 ));
1791 let gpu = GpuContext::from_parts_with_queue_lock(adapter, device, queue, Some(queue_lock));
1792 Ok(Self {
1793 ffmpeg_device,
1794 gpu,
1795 submission_batch: Arc::new(VulkanSubmissionBatch::default()),
1796 import_tickets: Arc::new(ImportTicketPool::default()),
1797 identity: NativeGpuIdentity {
1798 physical_device: physical.as_raw(),
1799 device: raw_device.as_raw(),
1800 epoch: 1,
1801 },
1802 decode_codecs,
1803 encode_extent: h264_extent,
1804 encode_codecs: if encode_family.is_some() {
1805 vec!["h264"]
1806 } else {
1807 Vec::new()
1808 },
1809 validation_layer,
1810 })
1811 }
1812
1813 #[must_use]
1815 pub fn gpu(&self) -> GpuContext {
1816 self.gpu.clone()
1817 }
1818
1819 #[must_use]
1821 pub fn ffmpeg_device(&self) -> AVHWDeviceContext {
1822 self.ffmpeg_device.clone()
1823 }
1824
1825 #[must_use]
1827 pub const fn identity(&self) -> NativeGpuIdentity {
1828 self.identity
1829 }
1830
1831 #[must_use]
1833 pub fn supports_ten_bit(&self) -> bool {
1834 self.gpu
1835 .device()
1836 .features()
1837 .contains(wgpu::Features::TEXTURE_FORMAT_P010)
1838 }
1839
1840 #[must_use]
1843 pub fn decode_codecs(&self) -> &[&'static str] {
1844 &self.decode_codecs
1845 }
1846
1847 #[must_use]
1852 pub const fn validation_layer(&self) -> Option<VulkanLayerVersion> {
1853 self.validation_layer
1854 }
1855
1856 #[must_use]
1864 pub fn video_session_hazard(&self) -> Option<String> {
1865 self.validation_layer
1866 .filter(|layer| *layer < VulkanLayerVersion::VIDEO_SESSIONS)
1867 .map(|layer| {
1868 format!(
1869 "the loaded Vulkan validation layer is {layer}, and layers older than {} \
1870 crash in FFmpeg's video session setup",
1871 VulkanLayerVersion::VIDEO_SESSIONS
1872 )
1873 })
1874 }
1875
1876 #[must_use]
1882 pub fn encode_codecs(&self) -> &[&'static str] {
1883 &self.encode_codecs
1884 }
1885
1886 #[must_use]
1897 pub const fn encode_extent(&self) -> Option<&VideoEncodeExtent> {
1898 self.encode_extent.as_ref()
1899 }
1900
1901 #[must_use]
1903 pub fn import_ticket_stats(&self) -> ImportTicketStats {
1904 self.import_tickets.stats()
1905 }
1906
1907 #[allow(clippy::too_many_lines)]
1917 pub fn import_surface(
1918 &self,
1919 frame: Arc<VulkanFrameLease>,
1920 ) -> Result<ImportedVulkanFrame, BridgeError> {
1921 let info = frame.info()?;
1922 if info.device != self.identity.device
1923 || info.physical_device != self.identity.physical_device
1924 {
1925 return Err(BridgeError::InvalidInput(
1926 "decoded frame belongs to another Vulkan device",
1927 ));
1928 }
1929 let layout = match (info.software_format, info.formats[0]) {
1930 (rsmpeg::ffi::AV_PIX_FMT_NV12, format)
1931 if format
1932 == vk::Format::G8_B8R8_2PLANE_420_UNORM
1933 .as_raw()
1934 .cast_unsigned() =>
1935 {
1936 NativePlaneLayout::Nv12
1937 }
1938 (rsmpeg::ffi::AV_PIX_FMT_P010LE, format)
1939 if format
1940 == vk::Format::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16
1941 .as_raw()
1942 .cast_unsigned() =>
1943 {
1944 NativePlaneLayout::P010
1945 }
1946 _ => {
1947 return Err(BridgeError::InvalidInput(
1948 "only sampled single-image NV12 or P010 is importable",
1949 ));
1950 }
1951 };
1952 if layout == NativePlaneLayout::P010
1953 && !self
1954 .gpu
1955 .device()
1956 .features()
1957 .contains(wgpu::Features::TEXTURE_FORMAT_P010)
1958 {
1959 return Err(BridgeError::InvalidInput(
1960 "shared Vulkan device does not expose P010 plane views",
1961 ));
1962 }
1963 if info.plane_count != 1 || !info.sampled || info.width <= 0 || info.height <= 0 {
1964 return Err(BridgeError::InvalidInput(
1965 "only sampled single-image NV12 or P010 is importable",
1966 ));
1967 }
1968 if info.coded_width < info.width || info.coded_height < info.height {
1971 return Err(BridgeError::InvalidInput(
1972 "decoded frame is larger than its allocated image",
1973 ));
1974 }
1975 let coded_width = u32::try_from(info.coded_width)
1976 .map_err(|_| BridgeError::InvalidInput("invalid coded width"))?;
1977 let coded_height = u32::try_from(info.coded_height)
1978 .map_err(|_| BridgeError::InvalidInput("invalid coded height"))?;
1979 let access = frame.lock_access()?;
1980 let mut status = 0;
1981 let step =
1984 unsafe { alpha2_native_gpu_vulkan_acquire_check(access.access(), &raw mut status) };
1985 if step != 0 {
1986 return Err(helper_error("import check", step, status));
1987 }
1988 let (device, allocator, queue_family) = {
1989 let access = access.access();
1990 (
1991 access.frame().device,
1992 access.allocator(),
1993 access.queue_family(),
1994 )
1995 };
1996 drop(access);
1997 let ticket = self.import_tickets.take(device, allocator, queue_family)?;
1998 let (texture_format, luma_format, chroma_format) = match layout {
1999 NativePlaneLayout::P010 => (
2000 wgpu::TextureFormat::P010,
2001 wgpu::TextureFormat::R16Unorm,
2002 wgpu::TextureFormat::Rg16Unorm,
2003 ),
2004 _ => (
2005 wgpu::TextureFormat::NV12,
2006 wgpu::TextureFormat::R8Unorm,
2007 wgpu::TextureFormat::Rg8Unorm,
2008 ),
2009 };
2010 let hal_descriptor = wgpu_hal::TextureDescriptor {
2011 label: Some("alpha2 FFmpeg decode surface"),
2012 size: wgpu::Extent3d {
2013 width: coded_width,
2014 height: coded_height,
2015 depth_or_array_layers: 1,
2016 },
2017 mip_level_count: 1,
2018 sample_count: 1,
2019 dimension: wgpu::TextureDimension::D2,
2020 format: texture_format,
2021 usage: wgpu::TextureUses::RESOURCE,
2022 memory_flags: wgpu_hal::MemoryFlags::empty(),
2023 view_formats: Vec::new(),
2024 };
2025 let hal_device = unsafe { self.gpu.device().as_hal::<wgpu_hal::api::Vulkan>() }
2028 .ok_or(BridgeError::InvalidInput("renderer is not Vulkan"))?;
2029 let drop_owner = Arc::clone(&frame);
2030 let hal_texture = unsafe {
2033 hal_device.texture_from_raw(
2034 vk::Image::from_raw(info.images[0]),
2035 &hal_descriptor,
2036 Some(Box::new(move || drop(drop_owner))),
2037 wgpu_hal::vulkan::TextureMemory::External,
2038 )
2039 };
2040 let descriptor = wgpu::TextureDescriptor {
2041 label: Some("alpha2 FFmpeg decode surface"),
2042 size: hal_descriptor.size,
2043 mip_level_count: 1,
2044 sample_count: 1,
2045 dimension: wgpu::TextureDimension::D2,
2046 format: texture_format,
2047 usage: wgpu::TextureUsages::TEXTURE_BINDING,
2048 view_formats: &[],
2049 };
2050 let texture = unsafe {
2053 self.gpu
2054 .device()
2055 .create_texture_from_hal::<wgpu_hal::api::Vulkan>(
2056 hal_texture,
2057 &descriptor,
2058 wgpu::TextureUses::RESOURCE,
2059 )
2060 };
2061 let y = texture.create_view(&wgpu::TextureViewDescriptor {
2062 label: Some("alpha2 FFmpeg decode Y"),
2063 format: Some(luma_format),
2064 aspect: wgpu::TextureAspect::Plane0,
2065 ..Default::default()
2066 });
2067 let uv = texture.create_view(&wgpu::TextureViewDescriptor {
2068 label: Some("alpha2 FFmpeg decode UV"),
2069 format: Some(chroma_format),
2070 aspect: wgpu::TextureAspect::Plane1,
2071 ..Default::default()
2072 });
2073 let pixels = u64::from(coded_width).saturating_mul(u64::from(coded_height));
2074 let lease = Arc::new(VulkanImportLease {
2075 frame,
2076 _texture: texture,
2077 gpu: self.gpu.clone(),
2078 batch: Arc::clone(&self.submission_batch),
2079 tickets: Arc::clone(&self.import_tickets),
2080 ticket: Some(ticket),
2081 staged_signals: Mutex::new(Vec::new()),
2082 queued: AtomicBool::new(false),
2083 acquired: AtomicBool::new(false),
2084 released: AtomicBool::new(false),
2085 retained_bytes: pixels
2086 .saturating_mul(layout.bytes_per_pixel_numerator())
2087 .div_ceil(2),
2088 });
2089 Ok(ImportedVulkanFrame {
2090 y,
2091 uv,
2092 layout,
2093 coded_size: [coded_width, coded_height],
2094 lease,
2095 })
2096 }
2097}
2098
2099fn choose_adapter(
2104 adapters: &mut [wgpu_hal::ExposedAdapter<wgpu_hal::api::Vulkan>],
2105 selector: Option<&str>,
2106) -> Result<usize, SharedVulkanError> {
2107 let preference = wgpu::PowerPreference::from_env();
2108 adapters.sort_by_key(|adapter| adapter_rank(adapter.info.device_type, preference));
2109 adapters
2110 .iter()
2111 .position(|adapter| {
2112 selector.is_none_or(|needle| adapter.info.name.contains(needle))
2113 && adapter
2114 .features
2115 .contains(wgpu::Features::TEXTURE_FORMAT_NV12)
2116 })
2117 .ok_or_else(|| {
2118 SharedVulkanError(selector.map_or_else(
2119 || "no Vulkan adapter supports NV12 plane views".to_owned(),
2120 |needle| {
2121 format!(
2122 "no Vulkan adapter whose name contains ALPHA2_VULKAN_DEVICE={needle:?} \
2123 supports NV12 plane views"
2124 )
2125 },
2126 ))
2127 })
2128}
2129
2130pub fn shared_device_index(selector: Option<&str>) -> Result<usize, SharedVulkanError> {
2142 let _turn = alpha2_gpu::serialize_acquisition();
2143 let hal_instance = hal_instance()?;
2144 let mut adapters = unsafe { hal_instance.enumerate_adapters(None) };
2146 let position = choose_adapter(&mut adapters, selector)?;
2147 let chosen = &adapters[position];
2148 let physical = chosen.adapter.raw_physical_device();
2149 let order = unsafe {
2151 chosen
2152 .adapter
2153 .shared_instance()
2154 .raw_instance()
2155 .enumerate_physical_devices()
2156 }
2157 .map_err(|error| SharedVulkanError(format!("Vulkan device enumeration: {error}")))?;
2158 order
2159 .iter()
2160 .position(|device| *device == physical)
2161 .ok_or_else(|| SharedVulkanError("the chosen adapter is not in the device list".into()))
2162}
2163
2164fn adapter_rank(device_type: wgpu::DeviceType, preference: Option<wgpu::PowerPreference>) -> u8 {
2171 let low_power = preference == Some(wgpu::PowerPreference::LowPower);
2172 match device_type {
2173 wgpu::DeviceType::IntegratedGpu if low_power => 0,
2174 wgpu::DeviceType::DiscreteGpu if low_power => 1,
2175 wgpu::DeviceType::DiscreteGpu => 0,
2176 wgpu::DeviceType::IntegratedGpu => 1,
2177 _ => 2,
2178 }
2179}
2180
2181#[cfg(test)]
2182#[path = "vulkan_failure_tests.rs"]
2183mod failure_tests;
2184
2185#[cfg(test)]
2186#[path = "vulkan_tests.rs"]
2187mod tests;