Skip to main content

wgpu_native_texture_interop/
sync_vulkan.rs

1//! Vulkan external semaphore synchronizer for cross-API texture handoff.
2//!
3//! Pairs a wgpu Vulkan consumer with a producer that signals a `VkSemaphore`
4//! after rendering. Designed for the WPE DMABUF protocol on Linux, where
5//! each frame carries an `OPAQUE_FD`-handle-typed semaphore fd.
6//!
7//! Per frame:
8//!
9//! 1. The producer hands the consumer a frame whose
10//!    [`VulkanExternalImage::wait_semaphore_fd`] is `Some(fd)`.
11//! 2. `producer_complete` imports the fd into a persistent `VkSemaphore`
12//!    using `vkImportSemaphoreFdKHR` with [`vk::SemaphoreImportFlags::TEMPORARY`].
13//! 3. A standalone "pure wait" `vkQueueSubmit` (wait semaphore, no command
14//!    buffers) is issued on the wgpu Vulkan queue. Subsequent wgpu submits
15//!    are gated on the producer's signal.
16//!
17//! After the wait, the temporary import is consumed and the persistent
18//! semaphore returns to its prior unsignalled state, ready for the next
19//! frame. The fd ownership transfers to the driver on import per the
20//! Vulkan spec — the producer must not close it after handoff.
21
22use std::sync::Mutex;
23
24use ash::vk;
25
26use crate::{
27    HostWgpuContext, ImportedTexture, InteropBackend, InteropError, InteropSynchronizer,
28    NativeFrame, SyncMechanism,
29};
30
31/// Synchronizer that uses an external `VkSemaphore` fd per frame to gate
32/// consumer submits on producer rendering completion.
33pub struct VulkanSemaphoreSynchronizer {
34    vk_device: ash::Device,
35    vk_queue: vk::Queue,
36    persistent_semaphore: vk::Semaphore,
37    external_semaphore_fd: ash::khr::external_semaphore_fd::Device,
38    // The wait submit and import are mutually-exclusive across threads to
39    // prevent concurrent imports of the temporary payload on the same
40    // semaphore handle.
41    submit_lock: Mutex<()>,
42}
43
44unsafe impl Send for VulkanSemaphoreSynchronizer {}
45unsafe impl Sync for VulkanSemaphoreSynchronizer {}
46
47impl VulkanSemaphoreSynchronizer {
48    /// Create a synchronizer bound to the host's wgpu Vulkan device + queue.
49    ///
50    /// Returns [`InteropError::BackendMismatch`] if `host.backend` is not
51    /// [`InteropBackend::Vulkan`].
52    pub fn new(host: &HostWgpuContext) -> Result<Self, InteropError> {
53        if host.backend != InteropBackend::Vulkan {
54            return Err(InteropError::BackendMismatch {
55                expected: "Vulkan",
56                actual: "non-Vulkan",
57            });
58        }
59
60        unsafe {
61            let hal_device = host.device.as_hal::<wgpu::wgc::api::Vulkan>().ok_or(
62                InteropError::BackendMismatch {
63                    expected: "Vulkan",
64                    actual: "non-Vulkan",
65                },
66            )?;
67            let vk_device = hal_device.raw_device().clone();
68            let vk_instance = hal_device.shared_instance().raw_instance().clone();
69            drop(hal_device);
70
71            let hal_queue = host.queue.as_hal::<wgpu::wgc::api::Vulkan>().ok_or(
72                InteropError::BackendMismatch {
73                    expected: "Vulkan",
74                    actual: "non-Vulkan",
75                },
76            )?;
77            let vk_queue = hal_queue.as_raw();
78            drop(hal_queue);
79
80            let semaphore = vk_device
81                .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)
82                .map_err(|err| {
83                    InteropError::Vulkan(format!("create_semaphore: {}", err))
84                })?;
85
86            let external_semaphore_fd =
87                ash::khr::external_semaphore_fd::Device::new(&vk_instance, &vk_device);
88
89            Ok(Self {
90                vk_device,
91                vk_queue,
92                persistent_semaphore: semaphore,
93                external_semaphore_fd,
94                submit_lock: Mutex::new(()),
95            })
96        }
97    }
98}
99
100impl InteropSynchronizer for VulkanSemaphoreSynchronizer {
101    fn producer_complete(
102        &self,
103        frame: &NativeFrame,
104        mechanism: SyncMechanism,
105    ) -> Result<(), InteropError> {
106        match mechanism {
107            SyncMechanism::ExplicitExternalSemaphore => {
108                let semaphore_fd = match frame {
109                    NativeFrame::VulkanExternalImage(vk_frame) => vk_frame.wait_semaphore_fd,
110                    _ => {
111                        return Err(InteropError::Vulkan(
112                            "ExplicitExternalSemaphore requires a VulkanExternalImage frame".into(),
113                        ));
114                    }
115                };
116                let Some(fd) = semaphore_fd else {
117                    return Err(InteropError::Vulkan(
118                        "frame.wait_semaphore_fd is None but mechanism is ExplicitExternalSemaphore"
119                            .into(),
120                    ));
121                };
122
123                let _guard = self.submit_lock.lock().map_err(|_| {
124                    InteropError::Vulkan("submit_lock poisoned".into())
125                })?;
126
127                unsafe {
128                    self.external_semaphore_fd
129                        .import_semaphore_fd(
130                            &vk::ImportSemaphoreFdInfoKHR::default()
131                                .semaphore(self.persistent_semaphore)
132                                .flags(vk::SemaphoreImportFlags::TEMPORARY)
133                                .handle_type(vk::ExternalSemaphoreHandleTypeFlags::OPAQUE_FD)
134                                .fd(fd),
135                        )
136                        .map_err(|err| {
137                            InteropError::Vulkan(format!("import_semaphore_fd: {}", err))
138                        })?;
139
140                    let wait_semaphores = [self.persistent_semaphore];
141                    let wait_stages = [vk::PipelineStageFlags::FRAGMENT_SHADER];
142                    let submit_info = vk::SubmitInfo::default()
143                        .wait_semaphores(&wait_semaphores)
144                        .wait_dst_stage_mask(&wait_stages);
145
146                    self.vk_device
147                        .queue_submit(self.vk_queue, &[submit_info], vk::Fence::null())
148                        .map_err(|err| InteropError::Vulkan(format!("queue_submit: {}", err)))?;
149                }
150                Ok(())
151            }
152            SyncMechanism::None | SyncMechanism::ImplicitGlFlush => Ok(()),
153            other => Err(InteropError::UnsupportedSynchronization(other)),
154        }
155    }
156
157    fn consumer_ready(
158        &self,
159        _texture: &ImportedTexture,
160        mechanism: SyncMechanism,
161    ) -> Result<(), InteropError> {
162        match mechanism {
163            SyncMechanism::None
164            | SyncMechanism::ImplicitGlFlush
165            | SyncMechanism::ExplicitExternalSemaphore => Ok(()),
166            other => Err(InteropError::UnsupportedSynchronization(other)),
167        }
168    }
169}
170
171impl Drop for VulkanSemaphoreSynchronizer {
172    fn drop(&mut self) {
173        unsafe {
174            self.vk_device
175                .destroy_semaphore(self.persistent_semaphore, None);
176        }
177    }
178}