#![doc = include_str!("../README.md")]
mod error;
mod sync;
#[cfg(target_os = "windows")]
mod sync_dx12;
#[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
mod sync_vulkan;
#[cfg(target_vendor = "apple")]
mod sync_metal;
#[cfg(target_os = "linux")]
mod vulkan_dmabuf;
#[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
mod gl_bindings {
#![allow(unsafe_op_in_unsafe_fn)]
include!(concat!(env!("OUT_DIR"), "/gl_bindings.rs"));
}
pub mod raw_gl;
#[cfg(feature = "surfman")]
pub mod surfman_gl;
use std::rc::Rc;
use dpi::PhysicalSize;
pub use error::{InteropError, UnsupportedReason};
pub use sync::{ImplicitOnlySynchronizer, InteropSynchronizer, NoopSynchronizer, SyncMechanism};
#[cfg(target_os = "windows")]
pub use sync_dx12::Dx12FenceSynchronizer;
#[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
pub use sync_vulkan::VulkanSemaphoreSynchronizer;
#[cfg(target_vendor = "apple")]
pub use sync_metal::MetalSharedEventSynchronizer;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum InteropBackend {
Vulkan,
Metal,
Dx12,
Unknown,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TextureOrigin {
TopLeft,
BottomLeft,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum NativeFrameKind {
GlFramebufferSource,
VulkanExternalImage,
MetalTextureRef,
Dx12SharedTexture,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CapabilityStatus {
Supported,
Unsupported(UnsupportedReason),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CapabilityMatrix {
pub host_backend: InteropBackend,
pub gl_framebuffer_source: CapabilityStatus,
pub vulkan_external_image: CapabilityStatus,
pub metal_texture_ref: CapabilityStatus,
pub dx12_shared_texture: CapabilityStatus,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProducerCapabilities {
pub supported_frames: Vec<NativeFrameKind>,
}
#[derive(Clone, Debug)]
pub struct HostWgpuContext {
pub device: wgpu::Device,
pub queue: wgpu::Queue,
pub backend: InteropBackend,
}
impl HostWgpuContext {
pub fn new(device: wgpu::Device, queue: wgpu::Queue) -> Self {
Self {
backend: detect_backend(&device),
device,
queue,
}
}
pub fn capabilities(&self) -> CapabilityMatrix {
CapabilityMatrix::for_backend(self.backend)
}
}
#[derive(Clone, Copy, Debug)]
pub struct ImportOptions {
pub allow_copy_fallback: bool,
pub normalize_origin: bool,
pub normalize_format: bool,
}
impl Default for ImportOptions {
fn default() -> Self {
Self {
allow_copy_fallback: false,
normalize_origin: true,
normalize_format: true,
}
}
}
#[derive(Debug)]
pub struct ImportedTexture {
pub texture: wgpu::Texture,
pub format: wgpu::TextureFormat,
pub size: PhysicalSize<u32>,
pub origin: TextureOrigin,
pub generation: u64,
pub consumer_sync: SyncMechanism,
}
pub struct GlFramebufferSource {
size: PhysicalSize<u32>,
generation: u64,
producer_sync: SyncMechanism,
importer: Rc<dyn GlFramebufferSourceImpl>,
}
impl GlFramebufferSource {
pub fn size(&self) -> PhysicalSize<u32> {
self.size
}
pub fn generation(&self) -> u64 {
self.generation
}
pub fn producer_sync(&self) -> SyncMechanism {
self.producer_sync
}
pub fn new(
size: PhysicalSize<u32>,
generation: u64,
producer_sync: SyncMechanism,
importer: Rc<dyn GlFramebufferSourceImpl>,
) -> Self {
Self {
size,
generation,
producer_sync,
importer,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct VulkanExternalImage {
pub size: PhysicalSize<u32>,
pub format: wgpu::TextureFormat,
pub generation: u64,
pub producer_sync: SyncMechanism,
pub dmabuf_fd: i32,
pub dmabuf_offset: u64,
pub dmabuf_stride: u64,
pub drm_modifier: u64,
pub wait_semaphore_fd: Option<i32>,
}
#[derive(Clone, Copy, Debug)]
pub struct MetalTextureRef {
pub size: PhysicalSize<u32>,
pub format: wgpu::TextureFormat,
pub generation: u64,
pub producer_sync: SyncMechanism,
#[cfg(target_vendor = "apple")]
pub raw_metal_texture: *mut std::ffi::c_void,
}
#[derive(Clone, Copy, Debug)]
pub struct Dx12SharedTexture {
pub size: PhysicalSize<u32>,
pub format: wgpu::TextureFormat,
pub generation: u64,
pub producer_sync: SyncMechanism,
pub fence_value: u64,
#[cfg(target_os = "windows")]
pub handle: *mut std::ffi::c_void,
}
#[non_exhaustive]
pub enum NativeFrame {
GlFramebufferSource(GlFramebufferSource),
VulkanExternalImage(VulkanExternalImage),
MetalTextureRef(MetalTextureRef),
Dx12SharedTexture(Dx12SharedTexture),
}
impl NativeFrame {
pub fn kind(&self) -> NativeFrameKind {
match self {
NativeFrame::GlFramebufferSource(_) => NativeFrameKind::GlFramebufferSource,
NativeFrame::VulkanExternalImage(_) => NativeFrameKind::VulkanExternalImage,
NativeFrame::MetalTextureRef(_) => NativeFrameKind::MetalTextureRef,
NativeFrame::Dx12SharedTexture(_) => NativeFrameKind::Dx12SharedTexture,
}
}
pub fn producer_sync(&self) -> SyncMechanism {
match self {
NativeFrame::GlFramebufferSource(frame) => frame.producer_sync(),
NativeFrame::VulkanExternalImage(frame) => frame.producer_sync,
NativeFrame::MetalTextureRef(frame) => frame.producer_sync,
NativeFrame::Dx12SharedTexture(frame) => frame.producer_sync,
}
}
}
pub trait FrameProducer {
fn capabilities(&self) -> ProducerCapabilities;
fn acquire_frame(&mut self) -> Result<NativeFrame, InteropError>;
}
pub trait TextureImporter {
fn import_frame(
&self,
frame: &NativeFrame,
options: &ImportOptions,
) -> Result<ImportedTexture, InteropError>;
}
pub struct WgpuTextureImporter {
host: HostWgpuContext,
synchronizer: Box<dyn InteropSynchronizer>,
}
impl WgpuTextureImporter {
pub fn new(host: HostWgpuContext) -> Self {
Self {
host,
synchronizer: Box::new(ImplicitOnlySynchronizer),
}
}
pub fn with_synchronizer(
host: HostWgpuContext,
synchronizer: Box<dyn InteropSynchronizer>,
) -> Self {
Self { host, synchronizer }
}
pub fn host(&self) -> &HostWgpuContext {
&self.host
}
}
impl TextureImporter for WgpuTextureImporter {
fn import_frame(
&self,
frame: &NativeFrame,
options: &ImportOptions,
) -> Result<ImportedTexture, InteropError> {
self.synchronizer
.producer_complete(frame, frame.producer_sync())?;
let imported = match frame {
NativeFrame::GlFramebufferSource(frame_source) => {
frame_source
.importer
.import_into(frame_source, &self.host, options)
}
NativeFrame::VulkanExternalImage(frame) => import_vulkan_external_image(frame, &self.host),
NativeFrame::MetalTextureRef(frame) => import_metal_texture_ref(frame, &self.host),
NativeFrame::Dx12SharedTexture(frame) => import_dx12_shared_texture(frame, &self.host),
}?;
self.synchronizer
.consumer_ready(&imported, imported.consumer_sync)?;
Ok(imported)
}
}
impl CapabilityMatrix {
pub fn for_backend(host_backend: InteropBackend) -> Self {
let gl_framebuffer_source = match host_backend {
InteropBackend::Vulkan | InteropBackend::Metal | InteropBackend::Dx12 => {
CapabilityStatus::Supported
}
InteropBackend::Unknown => {
CapabilityStatus::Unsupported(UnsupportedReason::HostBackendUnavailable)
}
};
let vulkan_external_image = match host_backend {
InteropBackend::Vulkan => {
#[cfg(target_os = "linux")]
{
CapabilityStatus::Supported
}
#[cfg(not(target_os = "linux"))]
{
CapabilityStatus::Unsupported(UnsupportedReason::PlatformNotImplemented)
}
}
InteropBackend::Metal | InteropBackend::Dx12 => {
CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
}
InteropBackend::Unknown => {
CapabilityStatus::Unsupported(UnsupportedReason::HostBackendUnavailable)
}
};
let metal_texture_ref = match host_backend {
InteropBackend::Metal => CapabilityStatus::Supported,
InteropBackend::Vulkan | InteropBackend::Dx12 => {
CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
}
InteropBackend::Unknown => {
CapabilityStatus::Unsupported(UnsupportedReason::HostBackendUnavailable)
}
};
let dx12_shared_texture = match host_backend {
InteropBackend::Dx12 => CapabilityStatus::Supported,
InteropBackend::Vulkan | InteropBackend::Metal => {
CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
}
InteropBackend::Unknown => {
CapabilityStatus::Unsupported(UnsupportedReason::HostBackendUnavailable)
}
};
Self {
host_backend,
gl_framebuffer_source,
vulkan_external_image,
metal_texture_ref,
dx12_shared_texture,
}
}
}
pub trait GlFramebufferSourceImpl {
fn import_into(
&self,
frame: &GlFramebufferSource,
host: &HostWgpuContext,
options: &ImportOptions,
) -> Result<ImportedTexture, InteropError>;
}
fn import_vulkan_external_image(
#[cfg_attr(not(target_os = "linux"), allow(unused_variables))] frame: &VulkanExternalImage,
#[cfg_attr(not(target_os = "linux"), allow(unused_variables))] host: &HostWgpuContext,
) -> Result<ImportedTexture, InteropError> {
#[cfg(target_os = "linux")]
{
let texture = vulkan_dmabuf::import_vulkan_external_image(frame, host)?;
return Ok(ImportedTexture {
texture,
format: frame.format,
size: frame.size,
origin: TextureOrigin::TopLeft,
generation: frame.generation,
consumer_sync: frame.producer_sync,
});
}
#[cfg(not(target_os = "linux"))]
Err(InteropError::Unsupported(
UnsupportedReason::NativeImportNotYetImplemented,
))
}
fn import_metal_texture_ref(
#[cfg_attr(not(target_vendor = "apple"), allow(unused_variables))] frame: &MetalTextureRef,
#[cfg_attr(not(target_vendor = "apple"), allow(unused_variables))] host: &HostWgpuContext,
) -> Result<ImportedTexture, InteropError> {
#[cfg(target_vendor = "apple")]
{
use foreign_types_shared::ForeignType;
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
if frame.raw_metal_texture.is_null() {
return Err(InteropError::InvalidFrame("raw_metal_texture is null"));
}
if host.backend != InteropBackend::Metal {
return Err(InteropError::BackendMismatch {
expected: "Metal",
actual: "non-Metal",
});
}
let texture = unsafe {
let obj_ptr = frame.raw_metal_texture as *mut AnyObject;
let retained = Retained::retain(obj_ptr)
.ok_or_else(|| InteropError::Metal("failed to retain Metal texture".into()))?;
let raw_ptr = Retained::into_raw(retained) as *mut _;
let metal_texture = metal::Texture::from_ptr(raw_ptr);
let hal_texture = wgpu::hal::metal::Device::texture_from_raw(
metal_texture,
frame.format,
metal::MTLTextureType::D2,
0, 0, wgpu::hal::CopyExtent {
width: frame.size.width,
height: frame.size.height,
depth: 0,
},
);
host.device
.create_texture_from_hal::<wgpu::wgc::api::Metal>(
hal_texture,
&wgpu::TextureDescriptor {
label: Some("metal-texture-ref-import"),
size: wgpu::Extent3d {
width: frame.size.width,
height: frame.size.height,
depth_or_array_layers: 1,
},
format: frame.format,
dimension: wgpu::TextureDimension::D2,
mip_level_count: 1,
sample_count: 1,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
},
)
};
return Ok(ImportedTexture {
texture,
format: frame.format,
size: frame.size,
origin: TextureOrigin::TopLeft,
generation: frame.generation,
consumer_sync: frame.producer_sync,
});
}
#[cfg(not(target_vendor = "apple"))]
Err(InteropError::Unsupported(
UnsupportedReason::HostBackendMismatch,
))
}
fn import_dx12_shared_texture(
#[cfg_attr(not(target_os = "windows"), allow(unused_variables))] frame: &Dx12SharedTexture,
#[cfg_attr(not(target_os = "windows"), allow(unused_variables))] host: &HostWgpuContext,
) -> Result<ImportedTexture, InteropError> {
#[cfg(target_os = "windows")]
{
if host.backend != InteropBackend::Dx12 {
return Err(InteropError::BackendMismatch {
expected: "Dx12",
actual: "non-Dx12",
});
}
let texture = unsafe {
let hal_device = host.device.as_hal::<wgpu::wgc::api::Dx12>().ok_or(
InteropError::BackendMismatch {
expected: "Dx12",
actual: "non-Dx12",
},
)?;
let d3d_device = hal_device.raw_device().clone();
let mut resource: Option<windows::Win32::Graphics::Direct3D12::ID3D12Resource> = None;
d3d_device
.OpenSharedHandle(
windows::Win32::Foundation::HANDLE(frame.handle as *mut std::ffi::c_void),
&mut resource,
)
.map_err(|e| InteropError::Dx12(e.to_string()))?;
let resource = resource
.ok_or_else(|| InteropError::Dx12("OpenSharedHandle returned null".into()))?;
let hal_texture = wgpu_hal::dx12::Device::texture_from_raw(
resource,
frame.format,
wgpu::TextureDimension::D2,
wgpu::Extent3d {
width: frame.size.width,
height: frame.size.height,
depth_or_array_layers: 1,
},
1, 1, );
host.device.create_texture_from_hal::<wgpu::wgc::api::Dx12>(
hal_texture,
&wgpu::TextureDescriptor {
label: Some("dx12-shared-texture-import"),
size: wgpu::Extent3d {
width: frame.size.width,
height: frame.size.height,
depth_or_array_layers: 1,
},
format: frame.format,
dimension: wgpu::TextureDimension::D2,
mip_level_count: 1,
sample_count: 1,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
},
)
};
return Ok(ImportedTexture {
texture,
format: frame.format,
size: frame.size,
origin: TextureOrigin::TopLeft,
generation: frame.generation,
consumer_sync: frame.producer_sync,
});
}
#[cfg(not(target_os = "windows"))]
Err(InteropError::Unsupported(
UnsupportedReason::HostBackendMismatch,
))
}
pub fn backend_name(device: &wgpu::Device) -> &'static str {
match detect_backend(device) {
InteropBackend::Vulkan => "Vulkan",
InteropBackend::Metal => "Metal",
InteropBackend::Dx12 => "DirectX 12",
InteropBackend::Unknown => "Unknown",
}
}
pub fn print_wgpu_backend(device: &wgpu::Device) {
eprintln!("[wgpu] backend: {}", backend_name(device));
}
fn detect_backend(device: &wgpu::Device) -> InteropBackend {
unsafe {
#[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
if device.as_hal::<wgpu::wgc::api::Vulkan>().is_some() {
return InteropBackend::Vulkan;
}
#[cfg(target_vendor = "apple")]
if device.as_hal::<wgpu::wgc::api::Metal>().is_some() {
return InteropBackend::Metal;
}
#[cfg(target_os = "windows")]
if device.as_hal::<wgpu::wgc::api::Dx12>().is_some() {
return InteropBackend::Dx12;
}
}
InteropBackend::Unknown
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn import_options_default_prefers_normalized_textures() {
let options = ImportOptions::default();
assert!(!options.allow_copy_fallback);
assert!(options.normalize_origin);
assert!(options.normalize_format);
}
#[test]
fn capability_matrix_tracks_backend_shape() {
let vulkan = CapabilityMatrix::for_backend(InteropBackend::Vulkan);
let metal = CapabilityMatrix::for_backend(InteropBackend::Metal);
let dx12 = CapabilityMatrix::for_backend(InteropBackend::Dx12);
let unknown = CapabilityMatrix::for_backend(InteropBackend::Unknown);
assert_eq!(vulkan.gl_framebuffer_source, CapabilityStatus::Supported);
assert_eq!(metal.gl_framebuffer_source, CapabilityStatus::Supported);
assert_eq!(dx12.gl_framebuffer_source, CapabilityStatus::Supported);
assert_eq!(
unknown.gl_framebuffer_source,
CapabilityStatus::Unsupported(UnsupportedReason::HostBackendUnavailable)
);
#[cfg(target_os = "linux")]
assert_eq!(vulkan.vulkan_external_image, CapabilityStatus::Supported);
#[cfg(not(target_os = "linux"))]
assert_eq!(
vulkan.vulkan_external_image,
CapabilityStatus::Unsupported(UnsupportedReason::PlatformNotImplemented)
);
assert_eq!(
metal.vulkan_external_image,
CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
);
assert_eq!(metal.metal_texture_ref, CapabilityStatus::Supported);
assert_eq!(
vulkan.metal_texture_ref,
CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
);
assert_eq!(dx12.dx12_shared_texture, CapabilityStatus::Supported);
assert_eq!(
vulkan.dx12_shared_texture,
CapabilityStatus::Unsupported(UnsupportedReason::HostBackendMismatch)
);
}
#[test]
fn implicit_synchronizer_accepts_implicit_flush() {
assert!(ImplicitOnlySynchronizer::validate(SyncMechanism::ImplicitGlFlush).is_ok());
}
#[test]
fn implicit_synchronizer_rejects_explicit_sync() {
assert!(matches!(
ImplicitOnlySynchronizer::validate(SyncMechanism::ExplicitExternalSemaphore),
Err(InteropError::UnsupportedSynchronization(
SyncMechanism::ExplicitExternalSemaphore
))
));
}
}