Skip to main content

wgpu_native_texture_interop/
lib.rs

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/// The wgpu graphics backend in use on the host device.
42///
43/// Detected automatically by [`HostWgpuContext::new`] via `as_hal`. Used to
44/// drive [`CapabilityMatrix::for_backend`].
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46#[non_exhaustive]
47pub enum InteropBackend {
48    /// Vulkan backend (Linux, Android, Windows with `wgpu::Backends::VULKAN`).
49    Vulkan,
50    /// Metal backend (macOS, iOS).
51    Metal,
52    /// Direct3D 12 backend (Windows). GL→DX12 import is supported on
53    /// ANGLE-backed surfman via the shared D3D11 NT-handle path
54    /// (`surfman_gl::windows_dx12_shared`).
55    Dx12,
56    /// Backend could not be detected. All import paths will report
57    /// [`CapabilityStatus::Unsupported`].
58    Unknown,
59}
60
61/// Which corner of the texture holds row 0 of the image.
62///
63/// GL renders with the origin at the bottom-left; most compositors expect
64/// top-left. The import paths in this crate Y-flip during blit so that all
65/// returned textures have [`TextureOrigin::TopLeft`] when
66/// [`ImportOptions::normalize_origin`] is `true` (the default).
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub enum TextureOrigin {
69    /// Row 0 is the top row. The standard convention for wgpu/Vulkan/Metal.
70    TopLeft,
71    /// Row 0 is the bottom row. Raw GL output before Y-flip normalization.
72    BottomLeft,
73}
74
75/// Discriminant for [`NativeFrame`] variants, without carrying the frame data.
76///
77/// Returned by [`NativeFrame::kind`] and used in [`ProducerCapabilities`].
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79#[non_exhaustive]
80pub enum NativeFrameKind {
81    /// A GL framebuffer that will be imported via the platform-specific path.
82    GlFramebufferSource,
83    /// A Vulkan external image (Linux DMABUF import).
84    VulkanExternalImage,
85    /// A Metal texture reference (macOS/iOS).
86    MetalTextureRef,
87    /// A D3D12 shared texture (Windows).
88    Dx12SharedTexture,
89}
90
91/// Whether a particular interop capability is available on this device.
92#[derive(Clone, Debug, Eq, PartialEq)]
93pub enum CapabilityStatus {
94    /// The capability is available and `import_frame` should succeed.
95    Supported,
96    /// The capability is not available for the given reason.
97    Unsupported(UnsupportedReason),
98}
99
100/// Reports which frame types can be imported on the current device and backend.
101///
102/// Obtain via [`HostWgpuContext::capabilities`] or
103/// [`CapabilityMatrix::for_backend`]. Use this before attempting an import to
104/// give the user an early, descriptive error rather than a runtime failure.
105#[derive(Clone, Debug, Eq, PartialEq)]
106pub struct CapabilityMatrix {
107    /// The backend detected on the host wgpu device.
108    pub host_backend: InteropBackend,
109    /// GL framebuffer import (the primary path — Linux Vulkan, Apple Metal).
110    pub gl_framebuffer_source: CapabilityStatus,
111    /// Direct DMABUF→Vulkan import (Linux only).
112    pub vulkan_external_image: CapabilityStatus,
113    /// Direct Metal texture reference import (Apple platforms only).
114    pub metal_texture_ref: CapabilityStatus,
115    /// D3D12 shared texture import (Windows only).
116    pub dx12_shared_texture: CapabilityStatus,
117}
118
119/// The set of [`NativeFrameKind`]s a [`FrameProducer`] is able to emit.
120#[derive(Clone, Debug, Eq, PartialEq)]
121pub struct ProducerCapabilities {
122    /// Frame kinds this producer can supply.
123    pub supported_frames: Vec<NativeFrameKind>,
124}
125
126/// Wraps a `wgpu::Device` and `wgpu::Queue` together with the detected backend.
127///
128/// Pass one of these to [`WgpuTextureImporter::new`] or directly to the
129/// platform-specific import functions.
130#[derive(Clone, Debug)]
131pub struct HostWgpuContext {
132    /// The wgpu device that will own imported textures.
133    pub device: wgpu::Device,
134    /// The queue associated with `device`.
135    pub queue: wgpu::Queue,
136    /// The graphics backend detected on `device` at construction time.
137    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/// Options that control how [`WgpuTextureImporter`] processes each frame.
155#[derive(Clone, Copy, Debug)]
156pub struct ImportOptions {
157    /// If `true`, fall back to a CPU-side copy when the zero-copy path is
158    /// unavailable. Currently unused — reserved for future use.
159    pub allow_copy_fallback: bool,
160    /// If `true` (default), the importer runs a GPU blit/shader pass to
161    /// flip the texture to [`TextureOrigin::TopLeft`]. Set to `false` only
162    /// if you want the raw GL bottom-left orientation.
163    pub normalize_origin: bool,
164    /// If `true` (default), the importer converts BGRA output (Apple) to
165    /// RGBA so that all returned textures have a consistent
166    /// `Rgba8Unorm` format.
167    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/// A successfully imported wgpu texture, ready for use in a render pipeline.
181///
182/// Returned by [`TextureImporter::import_frame`].
183#[derive(Debug)]
184pub struct ImportedTexture {
185    /// The imported wgpu texture. Bind this as a texture resource in your
186    /// render pipeline.
187    pub texture: wgpu::Texture,
188    /// The pixel format of `texture`. `Rgba8Unorm` when
189    /// [`ImportOptions::normalize_format`] is `true` (the default).
190    pub format: wgpu::TextureFormat,
191    /// Dimensions of `texture` in physical pixels.
192    pub size: PhysicalSize<u32>,
193    /// Whether row 0 of `texture` is the top or bottom of the image.
194    /// [`TextureOrigin::TopLeft`] when [`ImportOptions::normalize_origin`]
195    /// is `true` (the default).
196    pub origin: TextureOrigin,
197    /// Monotonically increasing counter that the producer increments each
198    /// time new content is rendered. Use this to skip redundant re-imports.
199    pub generation: u64,
200    /// The synchronization mechanism the consumer should use after reading
201    /// `texture`. Passed to [`InteropSynchronizer::consumer_ready`].
202    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/// A frame backed by a Linux DMABUF imported via Vulkan
241/// `VK_KHR_external_memory_fd` + `VK_EXT_image_drm_format_modifier`.
242///
243/// The producer (e.g. WPE) hands the consumer a DMABUF fd, DRM format
244/// modifier, and per-plane offset/stride; the importer wraps it as a
245/// `wgpu::Texture` on the host's wgpu Vulkan device. Single-plane RGBA is
246/// the common case; multi-plane formats are not yet supported.
247///
248/// The `dmabuf_fd` and `wait_semaphore_fd` are **consumed** by the importer
249/// — Vulkan's `vkImportMemoryFdKHR` and `vkImportSemaphoreFdKHR` take
250/// ownership of the descriptors and the driver closes them. The producer
251/// must not close its copy after handoff.
252#[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    /// DMABUF file descriptor of the producer's allocated image. Linux only.
259    pub dmabuf_fd: i32,
260    /// Offset in bytes into the dmabuf where the image data starts. `0` for
261    /// most allocators.
262    pub dmabuf_offset: u64,
263    /// Row stride in bytes. The producer's allocator reports this (often
264    /// `width * bytes_per_pixel` rounded up to alignment).
265    pub dmabuf_stride: u64,
266    /// DRM format modifier reported by the producer. `0`
267    /// (`DRM_FORMAT_MOD_LINEAR`) for linear-tiled buffers.
268    pub drm_modifier: u64,
269    /// Optional fd to a `VkSemaphore` payload (`OPAQUE_FD`) the producer
270    /// signals after rendering. Pair with
271    /// [`VulkanSemaphoreSynchronizer`](crate::VulkanSemaphoreSynchronizer)
272    /// to gate consumer submits on the signal.
273    pub wait_semaphore_fd: Option<i32>,
274}
275
276/// A frame backed by a `MTLTexture` from a Metal producer.
277///
278/// The producer is responsible for ensuring the texture remains valid for the
279/// duration of the import call. Ownership is **not** transferred; the importer
280/// wraps the texture without retaining it via Objective-C ARC.
281#[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    /// Raw `MTLTexture *` pointer. Must be non-null. Apple platforms only.
288    ///
289    /// The caller retains ownership and must ensure the texture outlives this
290    /// struct. The importer does not call `retain` or `release` on the pointer.
291    #[cfg(target_vendor = "apple")]
292    pub raw_metal_texture: *mut std::ffi::c_void,
293}
294
295/// A frame backed by a D3D12 resource shared via a DXGI NT handle.
296///
297/// Obtain the handle by calling `IDXGIResource1::CreateSharedHandle` on your
298/// `ID3D12Resource`. The importer opens its own D3D12 reference via
299/// `ID3D12Device::OpenSharedHandle`; **you are responsible for closing your
300/// copy** of the handle after constructing this struct.
301#[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    /// Fence value the producer signalled at on its `ID3D11Fence` /
308    /// `ID3D12Fence` (opened from
309    /// [`Dx12FenceSynchronizer::shared_handle`](crate::Dx12FenceSynchronizer::shared_handle)).
310    /// The synchronizer waits for this value on the wgpu D3D12 queue
311    /// before the next consumer submit.
312    ///
313    /// Only meaningful when `producer_sync == SyncMechanism::ExplicitFence`.
314    /// Set to `0` for the keyed-mutex / no-fence path; the synchronizer
315    /// treats `0` as "no wait recorded for this frame".
316    pub fence_value: u64,
317    /// NT `HANDLE` from `IDXGIResource1::CreateSharedHandle`. Windows only.
318    ///
319    /// The importer opens its own reference via `OpenSharedHandle`. Close
320    /// your copy of the handle once this struct has been passed to
321    /// [`TextureImporter::import_frame`].
322    #[cfg(target_os = "windows")]
323    pub handle: *mut std::ffi::c_void,
324}
325
326/// A frame produced by a [`FrameProducer`], ready to be imported by a
327/// [`TextureImporter`].
328///
329/// A frame produced by a [`FrameProducer`], ready to be imported by a
330/// [`TextureImporter`].
331///
332/// All four variants have working import implementations.
333/// `VulkanExternalImage` is Linux-only; the others are gated on their
334/// respective platforms.
335#[non_exhaustive]
336pub enum NativeFrame {
337    /// A GL framebuffer — the primary, fully-implemented path.
338    GlFramebufferSource(GlFramebufferSource),
339    /// A Linux DMABUF imported via Vulkan
340    /// `VK_KHR_external_memory_fd` + `VK_EXT_image_drm_format_modifier`.
341    VulkanExternalImage(VulkanExternalImage),
342    /// A Metal texture reference. Implemented via IOSurface interop.
343    MetalTextureRef(MetalTextureRef),
344    /// A D3D12 shared texture. Implemented via shared NT handle interop.
345    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
368/// Produces [`NativeFrame`]s for a [`TextureImporter`] to consume.
369///
370/// Implement this for your GL/Vulkan/Metal renderer to feed frames into the
371/// interop pipeline. See [`raw_gl::producer::RawGlFrameProducer`] for a
372/// ready-made implementation that wraps any GL context.
373pub trait FrameProducer {
374    /// Returns what frame kinds this producer can emit.
375    fn capabilities(&self) -> ProducerCapabilities;
376    /// Acquire the next frame from the producer. The returned [`NativeFrame`]
377    /// should be passed immediately to [`TextureImporter::import_frame`].
378    fn acquire_frame(&mut self) -> Result<NativeFrame, InteropError>;
379}
380
381/// Imports a [`NativeFrame`] into a `wgpu::Texture`.
382pub trait TextureImporter {
383    /// Import `frame` into a [`wgpu::Texture`] owned by the host device.
384    ///
385    /// Returns [`InteropError::Unsupported`] if the frame kind is not
386    /// supported on the current platform/backend. Check
387    /// [`HostWgpuContext::capabilities`] first to get a descriptive error
388    /// before calling this.
389    fn import_frame(
390        &self,
391        frame: &NativeFrame,
392        options: &ImportOptions,
393    ) -> Result<ImportedTexture, InteropError>;
394}
395
396/// The main entry point for importing frames into wgpu textures.
397///
398/// Create one per wgpu device and reuse it across frames.
399///
400/// ```ignore
401/// let host = HostWgpuContext::new(device, queue);
402/// let importer = WgpuTextureImporter::new(host);
403/// // each frame:
404/// let frame = producer.acquire_frame()?;
405/// let imported = importer.import_frame(&frame, &ImportOptions::default())?;
406/// // use imported.texture in your render pipeline
407/// ```
408pub struct WgpuTextureImporter {
409    host: HostWgpuContext,
410    synchronizer: Box<dyn InteropSynchronizer>,
411}
412
413impl WgpuTextureImporter {
414    /// Create a new importer with the default [`ImplicitOnlySynchronizer`].
415    pub fn new(host: HostWgpuContext) -> Self {
416        Self {
417            host,
418            synchronizer: Box::new(ImplicitOnlySynchronizer),
419        }
420    }
421
422    /// Create a new importer with a custom [`InteropSynchronizer`].
423    pub fn with_synchronizer(
424        host: HostWgpuContext,
425        synchronizer: Box<dyn InteropSynchronizer>,
426    ) -> Self {
427        Self { host, synchronizer }
428    }
429
430    /// Returns the underlying [`HostWgpuContext`].
431    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                    // The wgpu Vulkan backend works on Linux/Android/Windows,
482                    // but DMABUF imports are Linux-specific.
483                    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            // Retain the caller's MTLTexture so that wgpu can take ownership
578            // of the reference we hand it without invalidating the caller's copy.
579            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            // texture_from_raw is a free associated function — matches the
586            // signature used in raw_gl/metal.rs.
587            let hal_texture = wgpu::hal::metal::Device::texture_from_raw(
588                metal_texture,
589                frame.format,
590                metal::MTLTextureType::D2,
591                0, // array_layers
592                0, // mip_levels
593                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, // mip_level_count
678                1, // sample_count
679            );
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
716/// Returns a human-readable name of the wgpu backend powering `device`.
717///
718/// Useful for `eprintln!`/log lines on startup so the active graphics API is
719/// visible without rebuilding with `RUST_LOG=wgpu=debug`. Returns `"Unknown"`
720/// when the backend can't be detected (e.g. no `as_hal` impl matches).
721pub 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
730/// Logs the active wgpu backend to stderr.
731///
732/// Equivalent to `eprintln!("[wgpu] backend: {}", backend_name(device))`.
733/// Prefer [`backend_name`] when you want to route the value to a logger.
734pub 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        // wgpu::wgc::api::Vulkan is only compiled in when the hal `vulkan` cfg
741        // is set — i.e. Linux, Android, and Windows (not macOS).
742        #[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}