1#![doc = include_str!("../README.md")]
2
3mod error;
4mod sync;
5
6#[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
7mod gl_bindings {
8 #![allow(unsafe_op_in_unsafe_fn)]
9
10 include!(concat!(env!("OUT_DIR"), "/gl_bindings.rs"));
11}
12
13pub mod raw_gl;
14
15#[cfg(feature = "surfman")]
16pub mod surfman_gl;
17
18use std::rc::Rc;
19
20use dpi::PhysicalSize;
21pub use error::{InteropError, UnsupportedReason};
22pub use sync::{ImplicitOnlySynchronizer, InteropSynchronizer, NoopSynchronizer, SyncMechanism};
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29#[non_exhaustive]
30pub enum InteropBackend {
31 Vulkan,
33 Metal,
35 Dx12,
38 Unknown,
41}
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub enum TextureOrigin {
51 TopLeft,
53 BottomLeft,
55}
56
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61#[non_exhaustive]
62pub enum NativeFrameKind {
63 GlFramebufferSource,
65 VulkanExternalImage,
67 MetalTextureRef,
69 Dx12SharedTexture,
71}
72
73#[derive(Clone, Debug, Eq, PartialEq)]
75pub enum CapabilityStatus {
76 Supported,
78 Unsupported(UnsupportedReason),
80}
81
82#[derive(Clone, Debug, Eq, PartialEq)]
88pub struct CapabilityMatrix {
89 pub host_backend: InteropBackend,
91 pub gl_framebuffer_source: CapabilityStatus,
93 pub vulkan_external_image: CapabilityStatus,
95 pub metal_texture_ref: CapabilityStatus,
97 pub dx12_shared_texture: CapabilityStatus,
99}
100
101#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct ProducerCapabilities {
104 pub supported_frames: Vec<NativeFrameKind>,
106}
107
108#[derive(Clone, Debug)]
113pub struct HostWgpuContext {
114 pub device: wgpu::Device,
116 pub queue: wgpu::Queue,
118 pub backend: InteropBackend,
120}
121
122impl HostWgpuContext {
123 pub fn new(device: wgpu::Device, queue: wgpu::Queue) -> Self {
124 Self {
125 backend: detect_backend(&device),
126 device,
127 queue,
128 }
129 }
130
131 pub fn capabilities(&self) -> CapabilityMatrix {
132 CapabilityMatrix::for_backend(self.backend)
133 }
134}
135
136#[derive(Clone, Copy, Debug)]
138pub struct ImportOptions {
139 pub allow_copy_fallback: bool,
142 pub normalize_origin: bool,
146 pub normalize_format: bool,
150}
151
152impl Default for ImportOptions {
153 fn default() -> Self {
154 Self {
155 allow_copy_fallback: false,
156 normalize_origin: true,
157 normalize_format: true,
158 }
159 }
160}
161
162#[derive(Debug)]
166pub struct ImportedTexture {
167 pub texture: wgpu::Texture,
170 pub format: wgpu::TextureFormat,
173 pub size: PhysicalSize<u32>,
175 pub origin: TextureOrigin,
179 pub generation: u64,
182 pub consumer_sync: SyncMechanism,
185}
186
187pub struct GlFramebufferSource {
188 size: PhysicalSize<u32>,
189 generation: u64,
190 producer_sync: SyncMechanism,
191 importer: Rc<dyn GlFramebufferSourceImpl>,
192}
193
194impl GlFramebufferSource {
195 pub fn size(&self) -> PhysicalSize<u32> {
196 self.size
197 }
198
199 pub fn generation(&self) -> u64 {
200 self.generation
201 }
202
203 pub fn producer_sync(&self) -> SyncMechanism {
204 self.producer_sync
205 }
206
207 pub fn new(
208 size: PhysicalSize<u32>,
209 generation: u64,
210 producer_sync: SyncMechanism,
211 importer: Rc<dyn GlFramebufferSourceImpl>,
212 ) -> Self {
213 Self {
214 size,
215 generation,
216 producer_sync,
217 importer,
218 }
219 }
220}
221
222#[derive(Clone, Copy, Debug)]
224pub struct VulkanExternalImage {
225 pub size: PhysicalSize<u32>,
226 pub format: wgpu::TextureFormat,
227 pub generation: u64,
228 pub producer_sync: SyncMechanism,
229}
230
231#[derive(Clone, Copy, Debug)]
237pub struct MetalTextureRef {
238 pub size: PhysicalSize<u32>,
239 pub format: wgpu::TextureFormat,
240 pub generation: u64,
241 pub producer_sync: SyncMechanism,
242 #[cfg(target_vendor = "apple")]
247 pub raw_metal_texture: *mut std::ffi::c_void,
248}
249
250#[derive(Clone, Copy, Debug)]
257pub struct Dx12SharedTexture {
258 pub size: PhysicalSize<u32>,
259 pub format: wgpu::TextureFormat,
260 pub generation: u64,
261 pub producer_sync: SyncMechanism,
262 #[cfg(target_os = "windows")]
268 pub handle: *mut std::ffi::c_void,
269}
270
271#[non_exhaustive]
281pub enum NativeFrame {
282 GlFramebufferSource(GlFramebufferSource),
284 VulkanExternalImage(VulkanExternalImage),
287 MetalTextureRef(MetalTextureRef),
289 Dx12SharedTexture(Dx12SharedTexture),
291}
292
293impl NativeFrame {
294 pub fn kind(&self) -> NativeFrameKind {
295 match self {
296 NativeFrame::GlFramebufferSource(_) => NativeFrameKind::GlFramebufferSource,
297 NativeFrame::VulkanExternalImage(_) => NativeFrameKind::VulkanExternalImage,
298 NativeFrame::MetalTextureRef(_) => NativeFrameKind::MetalTextureRef,
299 NativeFrame::Dx12SharedTexture(_) => NativeFrameKind::Dx12SharedTexture,
300 }
301 }
302
303 pub fn producer_sync(&self) -> SyncMechanism {
304 match self {
305 NativeFrame::GlFramebufferSource(frame) => frame.producer_sync(),
306 NativeFrame::VulkanExternalImage(frame) => frame.producer_sync,
307 NativeFrame::MetalTextureRef(frame) => frame.producer_sync,
308 NativeFrame::Dx12SharedTexture(frame) => frame.producer_sync,
309 }
310 }
311}
312
313pub trait FrameProducer {
319 fn capabilities(&self) -> ProducerCapabilities;
321 fn acquire_frame(&mut self) -> Result<NativeFrame, InteropError>;
324}
325
326pub trait TextureImporter {
328 fn import_frame(
335 &self,
336 frame: &NativeFrame,
337 options: &ImportOptions,
338 ) -> Result<ImportedTexture, InteropError>;
339}
340
341pub struct WgpuTextureImporter {
354 host: HostWgpuContext,
355 synchronizer: Box<dyn InteropSynchronizer>,
356}
357
358impl WgpuTextureImporter {
359 pub fn new(host: HostWgpuContext) -> Self {
361 Self {
362 host,
363 synchronizer: Box::new(ImplicitOnlySynchronizer),
364 }
365 }
366
367 pub fn with_synchronizer(
369 host: HostWgpuContext,
370 synchronizer: Box<dyn InteropSynchronizer>,
371 ) -> Self {
372 Self { host, synchronizer }
373 }
374
375 pub fn host(&self) -> &HostWgpuContext {
377 &self.host
378 }
379}
380
381impl TextureImporter for WgpuTextureImporter {
382 fn import_frame(
383 &self,
384 frame: &NativeFrame,
385 options: &ImportOptions,
386 ) -> Result<ImportedTexture, InteropError> {
387 self.synchronizer
388 .producer_complete(frame, frame.producer_sync())?;
389
390 let imported = match frame {
391 NativeFrame::GlFramebufferSource(frame_source) => {
392 frame_source
393 .importer
394 .import_into(frame_source, &self.host, options)
395 }
396 NativeFrame::VulkanExternalImage(_) => Err(InteropError::Unsupported(
397 UnsupportedReason::NativeImportNotYetImplemented,
398 )),
399 NativeFrame::MetalTextureRef(frame) => import_metal_texture_ref(frame, &self.host),
400 NativeFrame::Dx12SharedTexture(frame) => import_dx12_shared_texture(frame, &self.host),
401 }?;
402
403 self.synchronizer
404 .consumer_ready(&imported, imported.consumer_sync)?;
405 Ok(imported)
406 }
407}
408
409impl CapabilityMatrix {
410 pub fn for_backend(host_backend: InteropBackend) -> Self {
411 let gl_framebuffer_source = match host_backend {
412 InteropBackend::Vulkan | InteropBackend::Metal | InteropBackend::Dx12 => {
413 CapabilityStatus::Supported
414 }
415 InteropBackend::Unknown => {
416 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendUnavailable)
417 }
418 };
419
420 let vulkan_external_image = match host_backend {
421 InteropBackend::Vulkan => {
422 CapabilityStatus::Unsupported(UnsupportedReason::NativeImportNotYetImplemented)
423 }
424 InteropBackend::Metal | InteropBackend::Dx12 => {
425 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
426 }
427 InteropBackend::Unknown => {
428 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendUnavailable)
429 }
430 };
431
432 let metal_texture_ref = match host_backend {
433 InteropBackend::Metal => CapabilityStatus::Supported,
434 InteropBackend::Vulkan | InteropBackend::Dx12 => {
435 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
436 }
437 InteropBackend::Unknown => {
438 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendUnavailable)
439 }
440 };
441
442 let dx12_shared_texture = match host_backend {
443 InteropBackend::Dx12 => CapabilityStatus::Supported,
444 InteropBackend::Vulkan | InteropBackend::Metal => {
445 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
446 }
447 InteropBackend::Unknown => {
448 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendUnavailable)
449 }
450 };
451
452 Self {
453 host_backend,
454 gl_framebuffer_source,
455 vulkan_external_image,
456 metal_texture_ref,
457 dx12_shared_texture,
458 }
459 }
460}
461
462pub trait GlFramebufferSourceImpl {
463 fn import_into(
464 &self,
465 frame: &GlFramebufferSource,
466 host: &HostWgpuContext,
467 options: &ImportOptions,
468 ) -> Result<ImportedTexture, InteropError>;
469}
470
471fn import_metal_texture_ref(
472 #[cfg_attr(not(target_vendor = "apple"), allow(unused_variables))] frame: &MetalTextureRef,
473 #[cfg_attr(not(target_vendor = "apple"), allow(unused_variables))] host: &HostWgpuContext,
474) -> Result<ImportedTexture, InteropError> {
475 #[cfg(target_vendor = "apple")]
476 {
477 use foreign_types_shared::ForeignType;
478 use objc2::rc::Retained;
479 use objc2::runtime::AnyObject;
480
481 if frame.raw_metal_texture.is_null() {
482 return Err(InteropError::InvalidFrame("raw_metal_texture is null"));
483 }
484 if host.backend != InteropBackend::Metal {
485 return Err(InteropError::BackendMismatch {
486 expected: "Metal",
487 actual: "non-Metal",
488 });
489 }
490
491 let texture = unsafe {
492 let obj_ptr = frame.raw_metal_texture as *mut AnyObject;
495 let retained = Retained::retain(obj_ptr)
496 .ok_or_else(|| InteropError::Metal("failed to retain Metal texture".into()))?;
497 let raw_ptr = Retained::into_raw(retained) as *mut _;
498 let metal_texture = metal::Texture::from_ptr(raw_ptr);
499
500 let hal_texture = wgpu::hal::metal::Device::texture_from_raw(
503 metal_texture,
504 frame.format,
505 metal::MTLTextureType::D2,
506 0, 0, wgpu::hal::CopyExtent {
509 width: frame.size.width,
510 height: frame.size.height,
511 depth: 0,
512 },
513 );
514
515 host.device
516 .create_texture_from_hal::<wgpu::wgc::api::Metal>(
517 hal_texture,
518 &wgpu::TextureDescriptor {
519 label: Some("metal-texture-ref-import"),
520 size: wgpu::Extent3d {
521 width: frame.size.width,
522 height: frame.size.height,
523 depth_or_array_layers: 1,
524 },
525 format: frame.format,
526 dimension: wgpu::TextureDimension::D2,
527 mip_level_count: 1,
528 sample_count: 1,
529 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
530 view_formats: &[],
531 },
532 )
533 };
534
535 return Ok(ImportedTexture {
536 texture,
537 format: frame.format,
538 size: frame.size,
539 origin: TextureOrigin::TopLeft,
540 generation: frame.generation,
541 consumer_sync: frame.producer_sync,
542 });
543 }
544
545 #[cfg(not(target_vendor = "apple"))]
546 Err(InteropError::Unsupported(
547 UnsupportedReason::HostBackendMismatch,
548 ))
549}
550
551fn import_dx12_shared_texture(
552 #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] frame: &Dx12SharedTexture,
553 #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] host: &HostWgpuContext,
554) -> Result<ImportedTexture, InteropError> {
555 #[cfg(target_os = "windows")]
556 {
557 if host.backend != InteropBackend::Dx12 {
558 return Err(InteropError::BackendMismatch {
559 expected: "Dx12",
560 actual: "non-Dx12",
561 });
562 }
563
564 let texture = unsafe {
565 let hal_device = host.device.as_hal::<wgpu::wgc::api::Dx12>().ok_or(
566 InteropError::BackendMismatch {
567 expected: "Dx12",
568 actual: "non-Dx12",
569 },
570 )?;
571
572 let d3d_device = hal_device.raw_device().clone();
573 let mut resource: Option<windows::Win32::Graphics::Direct3D12::ID3D12Resource> = None;
574 d3d_device
575 .OpenSharedHandle(
576 windows::Win32::Foundation::HANDLE(frame.handle as *mut std::ffi::c_void),
577 &mut resource,
578 )
579 .map_err(|e| InteropError::Dx12(e.to_string()))?;
580 let resource = resource
581 .ok_or_else(|| InteropError::Dx12("OpenSharedHandle returned null".into()))?;
582
583 let hal_texture = wgpu_hal::dx12::Device::texture_from_raw(
584 resource,
585 frame.format,
586 wgpu::TextureDimension::D2,
587 wgpu::Extent3d {
588 width: frame.size.width,
589 height: frame.size.height,
590 depth_or_array_layers: 1,
591 },
592 1, 1, );
595
596 host.device.create_texture_from_hal::<wgpu::wgc::api::Dx12>(
597 hal_texture,
598 &wgpu::TextureDescriptor {
599 label: Some("dx12-shared-texture-import"),
600 size: wgpu::Extent3d {
601 width: frame.size.width,
602 height: frame.size.height,
603 depth_or_array_layers: 1,
604 },
605 format: frame.format,
606 dimension: wgpu::TextureDimension::D2,
607 mip_level_count: 1,
608 sample_count: 1,
609 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
610 view_formats: &[],
611 },
612 )
613 };
614
615 return Ok(ImportedTexture {
616 texture,
617 format: frame.format,
618 size: frame.size,
619 origin: TextureOrigin::TopLeft,
620 generation: frame.generation,
621 consumer_sync: frame.producer_sync,
622 });
623 }
624
625 #[cfg(not(target_os = "windows"))]
626 Err(InteropError::Unsupported(
627 UnsupportedReason::HostBackendMismatch,
628 ))
629}
630
631fn detect_backend(device: &wgpu::Device) -> InteropBackend {
632 unsafe {
633 #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
636 if device.as_hal::<wgpu::wgc::api::Vulkan>().is_some() {
637 return InteropBackend::Vulkan;
638 }
639
640 #[cfg(target_vendor = "apple")]
641 if device.as_hal::<wgpu::wgc::api::Metal>().is_some() {
642 return InteropBackend::Metal;
643 }
644
645 #[cfg(target_os = "windows")]
646 if device.as_hal::<wgpu::wgc::api::Dx12>().is_some() {
647 return InteropBackend::Dx12;
648 }
649 }
650
651 InteropBackend::Unknown
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657
658 #[test]
659 fn import_options_default_prefers_normalized_textures() {
660 let options = ImportOptions::default();
661
662 assert!(!options.allow_copy_fallback);
663 assert!(options.normalize_origin);
664 assert!(options.normalize_format);
665 }
666
667 #[test]
668 fn capability_matrix_tracks_backend_shape() {
669 let vulkan = CapabilityMatrix::for_backend(InteropBackend::Vulkan);
670 let metal = CapabilityMatrix::for_backend(InteropBackend::Metal);
671 let dx12 = CapabilityMatrix::for_backend(InteropBackend::Dx12);
672 let unknown = CapabilityMatrix::for_backend(InteropBackend::Unknown);
673
674 assert_eq!(vulkan.gl_framebuffer_source, CapabilityStatus::Supported);
675 assert_eq!(metal.gl_framebuffer_source, CapabilityStatus::Supported);
676 assert_eq!(dx12.gl_framebuffer_source, CapabilityStatus::Supported);
677 assert_eq!(
678 unknown.gl_framebuffer_source,
679 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendUnavailable)
680 );
681
682 assert_eq!(
683 vulkan.vulkan_external_image,
684 CapabilityStatus::Unsupported(UnsupportedReason::NativeImportNotYetImplemented)
685 );
686 assert_eq!(
687 metal.vulkan_external_image,
688 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
689 );
690
691 assert_eq!(metal.metal_texture_ref, CapabilityStatus::Supported);
692 assert_eq!(
693 vulkan.metal_texture_ref,
694 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
695 );
696
697 assert_eq!(dx12.dx12_shared_texture, CapabilityStatus::Supported);
698 assert_eq!(
699 vulkan.dx12_shared_texture,
700 CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
701 );
702 }
703
704 #[test]
705 fn implicit_synchronizer_accepts_implicit_flush() {
706 assert!(ImplicitOnlySynchronizer::validate(SyncMechanism::ImplicitGlFlush).is_ok());
707 }
708
709 #[test]
710 fn implicit_synchronizer_rejects_explicit_sync() {
711 assert!(matches!(
712 ImplicitOnlySynchronizer::validate(SyncMechanism::ExplicitExternalSemaphore),
713 Err(InteropError::UnsupportedSynchronization(
714 SyncMechanism::ExplicitExternalSemaphore
715 ))
716 ));
717 }
718}