Skip to main content

wgpu_native_texture_interop/
lib.rs

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/// The wgpu graphics backend in use on the host device.
25///
26/// Detected automatically by [`HostWgpuContext::new`] via `as_hal`. Used to
27/// drive [`CapabilityMatrix::for_backend`].
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29#[non_exhaustive]
30pub enum InteropBackend {
31    /// Vulkan backend (Linux, Android, Windows with `wgpu::Backends::VULKAN`).
32    Vulkan,
33    /// Metal backend (macOS, iOS).
34    Metal,
35    /// Direct3D 12 backend (Windows default). GL→DX12 import is not yet
36    /// implemented; use `wgpu::Backends::VULKAN` on Windows for GL interop.
37    Dx12,
38    /// Backend could not be detected. All import paths will report
39    /// [`CapabilityStatus::Unsupported`].
40    Unknown,
41}
42
43/// Which corner of the texture holds row 0 of the image.
44///
45/// GL renders with the origin at the bottom-left; most compositors expect
46/// top-left. The import paths in this crate Y-flip during blit so that all
47/// returned textures have [`TextureOrigin::TopLeft`] when
48/// [`ImportOptions::normalize_origin`] is `true` (the default).
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub enum TextureOrigin {
51    /// Row 0 is the top row. The standard convention for wgpu/Vulkan/Metal.
52    TopLeft,
53    /// Row 0 is the bottom row. Raw GL output before Y-flip normalization.
54    BottomLeft,
55}
56
57/// Discriminant for [`NativeFrame`] variants, without carrying the frame data.
58///
59/// Returned by [`NativeFrame::kind`] and used in [`ProducerCapabilities`].
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61#[non_exhaustive]
62pub enum NativeFrameKind {
63    /// A GL framebuffer that will be imported via the platform-specific path.
64    GlFramebufferSource,
65    /// A Vulkan external image. Import not yet implemented.
66    VulkanExternalImage,
67    /// A Metal texture reference. Import not yet implemented.
68    MetalTextureRef,
69    /// A D3D12 shared texture. Import not yet implemented.
70    Dx12SharedTexture,
71}
72
73/// Whether a particular interop capability is available on this device.
74#[derive(Clone, Debug, Eq, PartialEq)]
75pub enum CapabilityStatus {
76    /// The capability is available and `import_frame` should succeed.
77    Supported,
78    /// The capability is not available for the given reason.
79    Unsupported(UnsupportedReason),
80}
81
82/// Reports which frame types can be imported on the current device and backend.
83///
84/// Obtain via [`HostWgpuContext::capabilities`] or
85/// [`CapabilityMatrix::for_backend`]. Use this before attempting an import to
86/// give the user an early, descriptive error rather than a runtime failure.
87#[derive(Clone, Debug, Eq, PartialEq)]
88pub struct CapabilityMatrix {
89    /// The backend detected on the host wgpu device.
90    pub host_backend: InteropBackend,
91    /// GL framebuffer import (the primary path — Linux Vulkan, Apple Metal).
92    pub gl_framebuffer_source: CapabilityStatus,
93    /// Direct Vulkan external image import. Not yet implemented.
94    pub vulkan_external_image: CapabilityStatus,
95    /// Direct Metal texture reference import. Not yet implemented.
96    pub metal_texture_ref: CapabilityStatus,
97    /// D3D12 shared texture import. Not yet implemented.
98    pub dx12_shared_texture: CapabilityStatus,
99}
100
101/// The set of [`NativeFrameKind`]s a [`FrameProducer`] is able to emit.
102#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct ProducerCapabilities {
104    /// Frame kinds this producer can supply.
105    pub supported_frames: Vec<NativeFrameKind>,
106}
107
108/// Wraps a `wgpu::Device` and `wgpu::Queue` together with the detected backend.
109///
110/// Pass one of these to [`WgpuTextureImporter::new`] or directly to the
111/// platform-specific import functions.
112#[derive(Clone, Debug)]
113pub struct HostWgpuContext {
114    /// The wgpu device that will own imported textures.
115    pub device: wgpu::Device,
116    /// The queue associated with `device`.
117    pub queue: wgpu::Queue,
118    /// The graphics backend detected on `device` at construction time.
119    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/// Options that control how [`WgpuTextureImporter`] processes each frame.
137#[derive(Clone, Copy, Debug)]
138pub struct ImportOptions {
139    /// If `true`, fall back to a CPU-side copy when the zero-copy path is
140    /// unavailable. Currently unused — reserved for future use.
141    pub allow_copy_fallback: bool,
142    /// If `true` (default), the importer runs a GPU blit/shader pass to
143    /// flip the texture to [`TextureOrigin::TopLeft`]. Set to `false` only
144    /// if you want the raw GL bottom-left orientation.
145    pub normalize_origin: bool,
146    /// If `true` (default), the importer converts BGRA output (Apple) to
147    /// RGBA so that all returned textures have a consistent
148    /// `Rgba8Unorm` format.
149    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/// A successfully imported wgpu texture, ready for use in a render pipeline.
163///
164/// Returned by [`TextureImporter::import_frame`].
165#[derive(Debug)]
166pub struct ImportedTexture {
167    /// The imported wgpu texture. Bind this as a texture resource in your
168    /// render pipeline.
169    pub texture: wgpu::Texture,
170    /// The pixel format of `texture`. `Rgba8Unorm` when
171    /// [`ImportOptions::normalize_format`] is `true` (the default).
172    pub format: wgpu::TextureFormat,
173    /// Dimensions of `texture` in physical pixels.
174    pub size: PhysicalSize<u32>,
175    /// Whether row 0 of `texture` is the top or bottom of the image.
176    /// [`TextureOrigin::TopLeft`] when [`ImportOptions::normalize_origin`]
177    /// is `true` (the default).
178    pub origin: TextureOrigin,
179    /// Monotonically increasing counter that the producer increments each
180    /// time new content is rendered. Use this to skip redundant re-imports.
181    pub generation: u64,
182    /// The synchronization mechanism the consumer should use after reading
183    /// `texture`. Passed to [`InteropSynchronizer::consumer_ready`].
184    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/// Metadata for a Vulkan external image frame. **Import not yet implemented.**
223#[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/// A frame backed by a `MTLTexture` from a Metal producer.
232///
233/// The producer is responsible for ensuring the texture remains valid for the
234/// duration of the import call. Ownership is **not** transferred; the importer
235/// wraps the texture without retaining it via Objective-C ARC.
236#[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    /// Raw `MTLTexture *` pointer. Must be non-null. Apple platforms only.
243    ///
244    /// The caller retains ownership and must ensure the texture outlives this
245    /// struct. The importer does not call `retain` or `release` on the pointer.
246    #[cfg(target_vendor = "apple")]
247    pub raw_metal_texture: *mut std::ffi::c_void,
248}
249
250/// A frame backed by a D3D12 resource shared via a DXGI NT handle.
251///
252/// Obtain the handle by calling `IDXGIResource1::CreateSharedHandle` on your
253/// `ID3D12Resource`. The importer opens its own D3D12 reference via
254/// `ID3D12Device::OpenSharedHandle`; **you are responsible for closing your
255/// copy** of the handle after constructing this struct.
256#[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    /// NT `HANDLE` from `IDXGIResource1::CreateSharedHandle`. Windows only.
263    ///
264    /// The importer opens its own reference via `OpenSharedHandle`. Close
265    /// your copy of the handle once this struct has been passed to
266    /// [`TextureImporter::import_frame`].
267    #[cfg(target_os = "windows")]
268    pub handle: *mut std::ffi::c_void,
269}
270
271/// A frame produced by a [`FrameProducer`], ready to be imported by a
272/// [`TextureImporter`].
273///
274/// A frame produced by a [`FrameProducer`], ready to be imported by a
275/// [`TextureImporter`].
276///
277/// `GlFramebufferSource`, `MetalTextureRef`, and `Dx12SharedTexture` have
278/// complete import implementations. `VulkanExternalImage` is defined for API
279/// stability but its import path is not yet implemented.
280#[non_exhaustive]
281pub enum NativeFrame {
282    /// A GL framebuffer — the primary, fully-implemented path.
283    GlFramebufferSource(GlFramebufferSource),
284    /// A Vulkan external image. Not yet implemented — returns
285    /// [`UnsupportedReason::NativeImportNotYetImplemented`].
286    VulkanExternalImage(VulkanExternalImage),
287    /// A Metal texture reference. Fully implemented via IOSurface interop.
288    MetalTextureRef(MetalTextureRef),
289    /// A D3D12 shared texture. Fully implemented via shared handle interop.
290    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
313/// Produces [`NativeFrame`]s for a [`TextureImporter`] to consume.
314///
315/// Implement this for your GL/Vulkan/Metal renderer to feed frames into the
316/// interop pipeline. See [`raw_gl::producer::RawGlFrameProducer`] for a
317/// ready-made implementation that wraps any GL context.
318pub trait FrameProducer {
319    /// Returns what frame kinds this producer can emit.
320    fn capabilities(&self) -> ProducerCapabilities;
321    /// Acquire the next frame from the producer. The returned [`NativeFrame`]
322    /// should be passed immediately to [`TextureImporter::import_frame`].
323    fn acquire_frame(&mut self) -> Result<NativeFrame, InteropError>;
324}
325
326/// Imports a [`NativeFrame`] into a `wgpu::Texture`.
327pub trait TextureImporter {
328    /// Import `frame` into a [`wgpu::Texture`] owned by the host device.
329    ///
330    /// Returns [`InteropError::Unsupported`] if the frame kind is not
331    /// supported on the current platform/backend. Check
332    /// [`HostWgpuContext::capabilities`] first to get a descriptive error
333    /// before calling this.
334    fn import_frame(
335        &self,
336        frame: &NativeFrame,
337        options: &ImportOptions,
338    ) -> Result<ImportedTexture, InteropError>;
339}
340
341/// The main entry point for importing frames into wgpu textures.
342///
343/// Create one per wgpu device and reuse it across frames.
344///
345/// ```ignore
346/// let host = HostWgpuContext::new(device, queue);
347/// let importer = WgpuTextureImporter::new(host);
348/// // each frame:
349/// let frame = producer.acquire_frame()?;
350/// let imported = importer.import_frame(&frame, &ImportOptions::default())?;
351/// // use imported.texture in your render pipeline
352/// ```
353pub struct WgpuTextureImporter {
354    host: HostWgpuContext,
355    synchronizer: Box<dyn InteropSynchronizer>,
356}
357
358impl WgpuTextureImporter {
359    /// Create a new importer with the default [`ImplicitOnlySynchronizer`].
360    pub fn new(host: HostWgpuContext) -> Self {
361        Self {
362            host,
363            synchronizer: Box::new(ImplicitOnlySynchronizer),
364        }
365    }
366
367    /// Create a new importer with a custom [`InteropSynchronizer`].
368    pub fn with_synchronizer(
369        host: HostWgpuContext,
370        synchronizer: Box<dyn InteropSynchronizer>,
371    ) -> Self {
372        Self { host, synchronizer }
373    }
374
375    /// Returns the underlying [`HostWgpuContext`].
376    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            // Retain the caller's MTLTexture so that wgpu can take ownership
493            // of the reference we hand it without invalidating the caller's copy.
494            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            // texture_from_raw is a free associated function — matches the
501            // signature used in raw_gl/metal.rs.
502            let hal_texture = wgpu::hal::metal::Device::texture_from_raw(
503                metal_texture,
504                frame.format,
505                metal::MTLTextureType::D2,
506                0, // array_layers
507                0, // mip_levels
508                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, // mip_level_count
593                1, // sample_count
594            );
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        // wgpu::wgc::api::Vulkan is only compiled in when the hal `vulkan` cfg
634        // is set — i.e. Linux, Android, and Windows (not macOS).
635        #[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}