1#![doc = include_str!("../README.md")]
2
3mod error;
4mod sync;
5
6#[cfg(target_os = "windows")]
7mod sync_dx12;
8#[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
9mod sync_vulkan;
10#[cfg(target_vendor = "apple")]
11mod sync_metal;
12
13#[cfg(target_os = "linux")]
14mod vulkan_dmabuf;
15
16#[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
17mod gl_bindings {
18 #![allow(unsafe_op_in_unsafe_fn)]
19
20 include!(concat!(env!("OUT_DIR"), "/gl_bindings.rs"));
21}
22
23pub mod raw_gl;
24
25#[cfg(feature = "surfman")]
26pub mod surfman_gl;
27
28use std::rc::Rc;
29
30use dpi::PhysicalSize;
31pub use error::{InteropError, UnsupportedReason};
32pub use sync::{ImplicitOnlySynchronizer, InteropSynchronizer, NoopSynchronizer, SyncMechanism};
33
34#[cfg(target_os = "windows")]
35pub use sync_dx12::Dx12FenceSynchronizer;
36#[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
37pub use sync_vulkan::VulkanSemaphoreSynchronizer;
38#[cfg(target_vendor = "apple")]
39pub use sync_metal::MetalSharedEventSynchronizer;
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46#[non_exhaustive]
47pub enum InteropBackend {
48 Vulkan,
50 Metal,
52 Dx12,
56 Unknown,
59}
60
61#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub enum TextureOrigin {
69 TopLeft,
71 BottomLeft,
73}
74
75#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79#[non_exhaustive]
80pub enum NativeFrameKind {
81 GlFramebufferSource,
83 VulkanExternalImage,
85 MetalTextureRef,
87 Dx12SharedTexture,
89}
90
91#[derive(Clone, Debug, Eq, PartialEq)]
93pub enum CapabilityStatus {
94 Supported,
96 Unsupported(UnsupportedReason),
98}
99
100#[derive(Clone, Debug, Eq, PartialEq)]
106pub struct CapabilityMatrix {
107 pub host_backend: InteropBackend,
109 pub gl_framebuffer_source: CapabilityStatus,
111 pub vulkan_external_image: CapabilityStatus,
113 pub metal_texture_ref: CapabilityStatus,
115 pub dx12_shared_texture: CapabilityStatus,
117}
118
119#[derive(Clone, Debug, Eq, PartialEq)]
121pub struct ProducerCapabilities {
122 pub supported_frames: Vec<NativeFrameKind>,
124}
125
126#[derive(Clone, Debug)]
131pub struct HostWgpuContext {
132 pub device: wgpu::Device,
134 pub queue: wgpu::Queue,
136 pub backend: InteropBackend,
138}
139
140impl HostWgpuContext {
141 pub fn new(device: wgpu::Device, queue: wgpu::Queue) -> Self {
142 Self {
143 backend: detect_backend(&device),
144 device,
145 queue,
146 }
147 }
148
149 pub fn capabilities(&self) -> CapabilityMatrix {
150 CapabilityMatrix::for_backend(self.backend)
151 }
152}
153
154#[derive(Clone, Copy, Debug)]
156pub struct ImportOptions {
157 pub allow_copy_fallback: bool,
160 pub normalize_origin: bool,
164 pub normalize_format: bool,
168}
169
170impl Default for ImportOptions {
171 fn default() -> Self {
172 Self {
173 allow_copy_fallback: false,
174 normalize_origin: true,
175 normalize_format: true,
176 }
177 }
178}
179
180#[derive(Debug)]
184pub struct ImportedTexture {
185 pub texture: wgpu::Texture,
188 pub format: wgpu::TextureFormat,
191 pub size: PhysicalSize<u32>,
193 pub origin: TextureOrigin,
197 pub generation: u64,
200 pub consumer_sync: SyncMechanism,
203}
204
205pub struct GlFramebufferSource {
206 size: PhysicalSize<u32>,
207 generation: u64,
208 producer_sync: SyncMechanism,
209 importer: Rc<dyn GlFramebufferSourceImpl>,
210}
211
212impl GlFramebufferSource {
213 pub fn size(&self) -> PhysicalSize<u32> {
214 self.size
215 }
216
217 pub fn generation(&self) -> u64 {
218 self.generation
219 }
220
221 pub fn producer_sync(&self) -> SyncMechanism {
222 self.producer_sync
223 }
224
225 pub fn new(
226 size: PhysicalSize<u32>,
227 generation: u64,
228 producer_sync: SyncMechanism,
229 importer: Rc<dyn GlFramebufferSourceImpl>,
230 ) -> Self {
231 Self {
232 size,
233 generation,
234 producer_sync,
235 importer,
236 }
237 }
238}
239
240#[derive(Clone, Copy, Debug)]
253pub struct VulkanExternalImage {
254 pub size: PhysicalSize<u32>,
255 pub format: wgpu::TextureFormat,
256 pub generation: u64,
257 pub producer_sync: SyncMechanism,
258 pub dmabuf_fd: i32,
260 pub dmabuf_offset: u64,
263 pub dmabuf_stride: u64,
266 pub drm_modifier: u64,
269 pub wait_semaphore_fd: Option<i32>,
274}
275
276#[derive(Clone, Copy, Debug)]
282pub struct MetalTextureRef {
283 pub size: PhysicalSize<u32>,
284 pub format: wgpu::TextureFormat,
285 pub generation: u64,
286 pub producer_sync: SyncMechanism,
287 #[cfg(target_vendor = "apple")]
292 pub raw_metal_texture: *mut std::ffi::c_void,
293}
294
295#[derive(Clone, Copy, Debug)]
302pub struct Dx12SharedTexture {
303 pub size: PhysicalSize<u32>,
304 pub format: wgpu::TextureFormat,
305 pub generation: u64,
306 pub producer_sync: SyncMechanism,
307 pub fence_value: u64,
317 #[cfg(target_os = "windows")]
323 pub handle: *mut std::ffi::c_void,
324}
325
326#[non_exhaustive]
336pub enum NativeFrame {
337 GlFramebufferSource(GlFramebufferSource),
339 VulkanExternalImage(VulkanExternalImage),
342 MetalTextureRef(MetalTextureRef),
344 Dx12SharedTexture(Dx12SharedTexture),
346}
347
348impl NativeFrame {
349 pub fn kind(&self) -> NativeFrameKind {
350 match self {
351 NativeFrame::GlFramebufferSource(_) => NativeFrameKind::GlFramebufferSource,
352 NativeFrame::VulkanExternalImage(_) => NativeFrameKind::VulkanExternalImage,
353 NativeFrame::MetalTextureRef(_) => NativeFrameKind::MetalTextureRef,
354 NativeFrame::Dx12SharedTexture(_) => NativeFrameKind::Dx12SharedTexture,
355 }
356 }
357
358 pub fn producer_sync(&self) -> SyncMechanism {
359 match self {
360 NativeFrame::GlFramebufferSource(frame) => frame.producer_sync(),
361 NativeFrame::VulkanExternalImage(frame) => frame.producer_sync,
362 NativeFrame::MetalTextureRef(frame) => frame.producer_sync,
363 NativeFrame::Dx12SharedTexture(frame) => frame.producer_sync,
364 }
365 }
366}
367
368pub trait FrameProducer {
374 fn capabilities(&self) -> ProducerCapabilities;
376 fn acquire_frame(&mut self) -> Result<NativeFrame, InteropError>;
379}
380
381pub trait TextureImporter {
383 fn import_frame(
390 &self,
391 frame: &NativeFrame,
392 options: &ImportOptions,
393 ) -> Result<ImportedTexture, InteropError>;
394}
395
396pub struct WgpuTextureImporter {
409 host: HostWgpuContext,
410 synchronizer: Box<dyn InteropSynchronizer>,
411}
412
413impl WgpuTextureImporter {
414 pub fn new(host: HostWgpuContext) -> Self {
416 Self {
417 host,
418 synchronizer: Box::new(ImplicitOnlySynchronizer),
419 }
420 }
421
422 pub fn with_synchronizer(
424 host: HostWgpuContext,
425 synchronizer: Box<dyn InteropSynchronizer>,
426 ) -> Self {
427 Self { host, synchronizer }
428 }
429
430 pub fn host(&self) -> &HostWgpuContext {
432 &self.host
433 }
434}
435
436impl TextureImporter for WgpuTextureImporter {
437 fn import_frame(
438 &self,
439 frame: &NativeFrame,
440 options: &ImportOptions,
441 ) -> Result<ImportedTexture, InteropError> {
442 self.synchronizer
443 .producer_complete(frame, frame.producer_sync())?;
444
445 let imported = match frame {
446 NativeFrame::GlFramebufferSource(frame_source) => {
447 frame_source
448 .importer
449 .import_into(frame_source, &self.host, options)
450 }
451 NativeFrame::VulkanExternalImage(frame) => import_vulkan_external_image(frame, &self.host),
452 NativeFrame::MetalTextureRef(frame) => import_metal_texture_ref(frame, &self.host),
453 NativeFrame::Dx12SharedTexture(frame) => import_dx12_shared_texture(frame, &self.host),
454 }?;
455
456 self.synchronizer
457 .consumer_ready(&imported, imported.consumer_sync)?;
458 Ok(imported)
459 }
460}
461
462impl CapabilityMatrix {
463 pub fn for_backend(host_backend: InteropBackend) -> Self {
464 let gl_framebuffer_source = match host_backend {
465 InteropBackend::Vulkan | InteropBackend::Metal | InteropBackend::Dx12 => {
466 CapabilityStatus::Supported
467 }
468 InteropBackend::Unknown => {
469 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendUnavailable)
470 }
471 };
472
473 let vulkan_external_image = match host_backend {
474 InteropBackend::Vulkan => {
475 #[cfg(target_os = "linux")]
476 {
477 CapabilityStatus::Supported
478 }
479 #[cfg(not(target_os = "linux"))]
480 {
481 CapabilityStatus::Unsupported(UnsupportedReason::PlatformNotImplemented)
484 }
485 }
486 InteropBackend::Metal | InteropBackend::Dx12 => {
487 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
488 }
489 InteropBackend::Unknown => {
490 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendUnavailable)
491 }
492 };
493
494 let metal_texture_ref = match host_backend {
495 InteropBackend::Metal => CapabilityStatus::Supported,
496 InteropBackend::Vulkan | InteropBackend::Dx12 => {
497 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
498 }
499 InteropBackend::Unknown => {
500 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendUnavailable)
501 }
502 };
503
504 let dx12_shared_texture = match host_backend {
505 InteropBackend::Dx12 => CapabilityStatus::Supported,
506 InteropBackend::Vulkan | InteropBackend::Metal => {
507 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
508 }
509 InteropBackend::Unknown => {
510 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendUnavailable)
511 }
512 };
513
514 Self {
515 host_backend,
516 gl_framebuffer_source,
517 vulkan_external_image,
518 metal_texture_ref,
519 dx12_shared_texture,
520 }
521 }
522}
523
524pub trait GlFramebufferSourceImpl {
525 fn import_into(
526 &self,
527 frame: &GlFramebufferSource,
528 host: &HostWgpuContext,
529 options: &ImportOptions,
530 ) -> Result<ImportedTexture, InteropError>;
531}
532
533fn import_vulkan_external_image(
534 #[cfg_attr(not(target_os = "linux"), allow(unused_variables))] frame: &VulkanExternalImage,
535 #[cfg_attr(not(target_os = "linux"), allow(unused_variables))] host: &HostWgpuContext,
536) -> Result<ImportedTexture, InteropError> {
537 #[cfg(target_os = "linux")]
538 {
539 let texture = vulkan_dmabuf::import_vulkan_external_image(frame, host)?;
540 return Ok(ImportedTexture {
541 texture,
542 format: frame.format,
543 size: frame.size,
544 origin: TextureOrigin::TopLeft,
545 generation: frame.generation,
546 consumer_sync: frame.producer_sync,
547 });
548 }
549
550 #[cfg(not(target_os = "linux"))]
551 Err(InteropError::Unsupported(
552 UnsupportedReason::NativeImportNotYetImplemented,
553 ))
554}
555
556fn import_metal_texture_ref(
557 #[cfg_attr(not(target_vendor = "apple"), allow(unused_variables))] frame: &MetalTextureRef,
558 #[cfg_attr(not(target_vendor = "apple"), allow(unused_variables))] host: &HostWgpuContext,
559) -> Result<ImportedTexture, InteropError> {
560 #[cfg(target_vendor = "apple")]
561 {
562 use foreign_types_shared::ForeignType;
563 use objc2::rc::Retained;
564 use objc2::runtime::AnyObject;
565
566 if frame.raw_metal_texture.is_null() {
567 return Err(InteropError::InvalidFrame("raw_metal_texture is null"));
568 }
569 if host.backend != InteropBackend::Metal {
570 return Err(InteropError::BackendMismatch {
571 expected: "Metal",
572 actual: "non-Metal",
573 });
574 }
575
576 let texture = unsafe {
577 let obj_ptr = frame.raw_metal_texture as *mut AnyObject;
580 let retained = Retained::retain(obj_ptr)
581 .ok_or_else(|| InteropError::Metal("failed to retain Metal texture".into()))?;
582 let raw_ptr = Retained::into_raw(retained) as *mut _;
583 let metal_texture = metal::Texture::from_ptr(raw_ptr);
584
585 let hal_texture = wgpu::hal::metal::Device::texture_from_raw(
588 metal_texture,
589 frame.format,
590 metal::MTLTextureType::D2,
591 0, 0, wgpu::hal::CopyExtent {
594 width: frame.size.width,
595 height: frame.size.height,
596 depth: 0,
597 },
598 );
599
600 host.device
601 .create_texture_from_hal::<wgpu::wgc::api::Metal>(
602 hal_texture,
603 &wgpu::TextureDescriptor {
604 label: Some("metal-texture-ref-import"),
605 size: wgpu::Extent3d {
606 width: frame.size.width,
607 height: frame.size.height,
608 depth_or_array_layers: 1,
609 },
610 format: frame.format,
611 dimension: wgpu::TextureDimension::D2,
612 mip_level_count: 1,
613 sample_count: 1,
614 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
615 view_formats: &[],
616 },
617 )
618 };
619
620 return Ok(ImportedTexture {
621 texture,
622 format: frame.format,
623 size: frame.size,
624 origin: TextureOrigin::TopLeft,
625 generation: frame.generation,
626 consumer_sync: frame.producer_sync,
627 });
628 }
629
630 #[cfg(not(target_vendor = "apple"))]
631 Err(InteropError::Unsupported(
632 UnsupportedReason::HostBackendMismatch,
633 ))
634}
635
636fn import_dx12_shared_texture(
637 #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] frame: &Dx12SharedTexture,
638 #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] host: &HostWgpuContext,
639) -> Result<ImportedTexture, InteropError> {
640 #[cfg(target_os = "windows")]
641 {
642 if host.backend != InteropBackend::Dx12 {
643 return Err(InteropError::BackendMismatch {
644 expected: "Dx12",
645 actual: "non-Dx12",
646 });
647 }
648
649 let texture = unsafe {
650 let hal_device = host.device.as_hal::<wgpu::wgc::api::Dx12>().ok_or(
651 InteropError::BackendMismatch {
652 expected: "Dx12",
653 actual: "non-Dx12",
654 },
655 )?;
656
657 let d3d_device = hal_device.raw_device().clone();
658 let mut resource: Option<windows::Win32::Graphics::Direct3D12::ID3D12Resource> = None;
659 d3d_device
660 .OpenSharedHandle(
661 windows::Win32::Foundation::HANDLE(frame.handle as *mut std::ffi::c_void),
662 &mut resource,
663 )
664 .map_err(|e| InteropError::Dx12(e.to_string()))?;
665 let resource = resource
666 .ok_or_else(|| InteropError::Dx12("OpenSharedHandle returned null".into()))?;
667
668 let hal_texture = wgpu_hal::dx12::Device::texture_from_raw(
669 resource,
670 frame.format,
671 wgpu::TextureDimension::D2,
672 wgpu::Extent3d {
673 width: frame.size.width,
674 height: frame.size.height,
675 depth_or_array_layers: 1,
676 },
677 1, 1, );
680
681 host.device.create_texture_from_hal::<wgpu::wgc::api::Dx12>(
682 hal_texture,
683 &wgpu::TextureDescriptor {
684 label: Some("dx12-shared-texture-import"),
685 size: wgpu::Extent3d {
686 width: frame.size.width,
687 height: frame.size.height,
688 depth_or_array_layers: 1,
689 },
690 format: frame.format,
691 dimension: wgpu::TextureDimension::D2,
692 mip_level_count: 1,
693 sample_count: 1,
694 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
695 view_formats: &[],
696 },
697 )
698 };
699
700 return Ok(ImportedTexture {
701 texture,
702 format: frame.format,
703 size: frame.size,
704 origin: TextureOrigin::TopLeft,
705 generation: frame.generation,
706 consumer_sync: frame.producer_sync,
707 });
708 }
709
710 #[cfg(not(target_os = "windows"))]
711 Err(InteropError::Unsupported(
712 UnsupportedReason::HostBackendMismatch,
713 ))
714}
715
716pub fn backend_name(device: &wgpu::Device) -> &'static str {
722 match detect_backend(device) {
723 InteropBackend::Vulkan => "Vulkan",
724 InteropBackend::Metal => "Metal",
725 InteropBackend::Dx12 => "DirectX 12",
726 InteropBackend::Unknown => "Unknown",
727 }
728}
729
730pub fn print_wgpu_backend(device: &wgpu::Device) {
735 eprintln!("[wgpu] backend: {}", backend_name(device));
736}
737
738fn detect_backend(device: &wgpu::Device) -> InteropBackend {
739 unsafe {
740 #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
743 if device.as_hal::<wgpu::wgc::api::Vulkan>().is_some() {
744 return InteropBackend::Vulkan;
745 }
746
747 #[cfg(target_vendor = "apple")]
748 if device.as_hal::<wgpu::wgc::api::Metal>().is_some() {
749 return InteropBackend::Metal;
750 }
751
752 #[cfg(target_os = "windows")]
753 if device.as_hal::<wgpu::wgc::api::Dx12>().is_some() {
754 return InteropBackend::Dx12;
755 }
756 }
757
758 InteropBackend::Unknown
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764
765 #[test]
766 fn import_options_default_prefers_normalized_textures() {
767 let options = ImportOptions::default();
768
769 assert!(!options.allow_copy_fallback);
770 assert!(options.normalize_origin);
771 assert!(options.normalize_format);
772 }
773
774 #[test]
775 fn capability_matrix_tracks_backend_shape() {
776 let vulkan = CapabilityMatrix::for_backend(InteropBackend::Vulkan);
777 let metal = CapabilityMatrix::for_backend(InteropBackend::Metal);
778 let dx12 = CapabilityMatrix::for_backend(InteropBackend::Dx12);
779 let unknown = CapabilityMatrix::for_backend(InteropBackend::Unknown);
780
781 assert_eq!(vulkan.gl_framebuffer_source, CapabilityStatus::Supported);
782 assert_eq!(metal.gl_framebuffer_source, CapabilityStatus::Supported);
783 assert_eq!(dx12.gl_framebuffer_source, CapabilityStatus::Supported);
784 assert_eq!(
785 unknown.gl_framebuffer_source,
786 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendUnavailable)
787 );
788
789 #[cfg(target_os = "linux")]
790 assert_eq!(vulkan.vulkan_external_image, CapabilityStatus::Supported);
791 #[cfg(not(target_os = "linux"))]
792 assert_eq!(
793 vulkan.vulkan_external_image,
794 CapabilityStatus::Unsupported(UnsupportedReason::PlatformNotImplemented)
795 );
796 assert_eq!(
797 metal.vulkan_external_image,
798 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
799 );
800
801 assert_eq!(metal.metal_texture_ref, CapabilityStatus::Supported);
802 assert_eq!(
803 vulkan.metal_texture_ref,
804 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
805 );
806
807 assert_eq!(dx12.dx12_shared_texture, CapabilityStatus::Supported);
808 assert_eq!(
809 vulkan.dx12_shared_texture,
810 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
811 );
812 }
813
814 #[test]
815 fn implicit_synchronizer_accepts_implicit_flush() {
816 assert!(ImplicitOnlySynchronizer::validate(SyncMechanism::ImplicitGlFlush).is_ok());
817 }
818
819 #[test]
820 fn implicit_synchronizer_rejects_explicit_sync() {
821 assert!(matches!(
822 ImplicitOnlySynchronizer::validate(SyncMechanism::ExplicitExternalSemaphore),
823 Err(InteropError::UnsupportedSynchronization(
824 SyncMechanism::ExplicitExternalSemaphore
825 ))
826 ));
827 }
828}