grafting 0.6.0

Native texture interop helpers for importing offscreen producer frames into host-owned wgpu textures
Documentation
// Copyright 2026 Mark Alan Boykin
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// SPDX-License-Identifier: MPL-2.0

//! IOSurface-to-wgpu import via Metal (Apple platforms).
//!
//! Imports an IOSurface into a `wgpu::Texture` by:
//! 1. Creating a Metal texture backed by the IOSurface
//! 2. Wrapping it as a wgpu HAL texture
//! 3. Normalizing from BGRA8Unorm (Metal native) to RGBA8Unorm with Y-flip

use dpi::PhysicalSize;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_io_surface::IOSurfaceRef;
use objc2_metal::{
    MTLDevice, MTLPixelFormat, MTLTexture, MTLTextureDescriptor, MTLTextureType, MTLTextureUsage,
};

#[cfg(all(
    feature = "wgpu-28",
    not(feature = "wgpu-29"),
    not(feature = "wgpu-30")
))]
use foreign_types_shared::ForeignType;

use crate::InteropError;

use super::texture_normalizer::ImportedTextureNormalizer;

/// Caches the normalization pipeline for repeated IOSurface imports.
///
/// Create once and reuse across frames to avoid repeated shader compilation.
pub struct MetalImporter {
    normalizer: ImportedTextureNormalizer,
}

impl MetalImporter {
    pub fn new(device: &wgpu::Device) -> Self {
        Self {
            normalizer: ImportedTextureNormalizer::new(device),
        }
    }

    /// Import an IOSurface into a normalized `wgpu::Texture`.
    ///
    /// The returned texture is Rgba8Unorm with top-left origin.
    pub fn import(
        &self,
        iosurface: &IOSurfaceRef,
        size: PhysicalSize<u32>,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
    ) -> Result<wgpu::Texture, InteropError> {
        let metal_texture = self.create_metal_texture(device, iosurface, size)?;
        let hal_texture = self.wrap_as_wgpu_hal(device, metal_texture, size)?;
        Ok(self.normalizer.normalize(device, queue, &hal_texture, size))
    }

    fn create_metal_texture(
        &self,
        wgpu_device: &wgpu::Device,
        iosurface: &IOSurfaceRef,
        size: PhysicalSize<u32>,
    ) -> Result<Retained<ProtocolObject<dyn MTLTexture>>, InteropError> {
        unsafe {
            let metal_device = wgpu_device.as_hal::<wgpu::wgc::api::Metal>().ok_or(
                InteropError::BackendMismatch {
                    expected: "Metal",
                    actual: "non-Metal",
                },
            )?;

            #[cfg(all(
                feature = "wgpu-28",
                not(feature = "wgpu-29"),
                not(feature = "wgpu-30")
            ))]
            let device_raw = Retained::retain(
                metal_device
                    .raw_device()
                    .as_ptr()
                    .cast::<ProtocolObject<dyn MTLDevice>>(),
            )
            .ok_or_else(|| InteropError::Metal("failed to retain Metal device".into()))?;
            #[cfg(any(feature = "wgpu-29", feature = "wgpu-30"))]
            let device_raw = metal_device.raw_device().clone();

            let descriptor = MTLTextureDescriptor::new();
            descriptor.setDepth(1);
            descriptor.setSampleCount(1);
            descriptor.setWidth(size.width as usize);
            descriptor.setHeight(size.height as usize);
            descriptor.setMipmapLevelCount(1);
            descriptor.setUsage(MTLTextureUsage::ShaderRead);
            descriptor.setPixelFormat(MTLPixelFormat::BGRA8Unorm);
            descriptor.setTextureType(MTLTextureType::Type2D);

            device_raw
                .newTextureWithDescriptor_iosurface_plane(&descriptor, iosurface, 0)
                .ok_or_else(|| {
                    InteropError::Metal("failed to create Metal texture from IOSurface".to_string())
                })
        }
    }

    fn wrap_as_wgpu_hal(
        &self,
        wgpu_device: &wgpu::Device,
        metal_texture: Retained<ProtocolObject<dyn MTLTexture>>,
        size: PhysicalSize<u32>,
    ) -> Result<wgpu::Texture, InteropError> {
        unsafe {
            let copy_size = wgpu::hal::CopyExtent {
                width: size.width,
                height: size.height,
                depth: 1,
            };
            // hal 30 added a `drop_callback` parameter; None preserves the
            // pre-30 behavior (the retained MTLTexture keeps the surface
            // alive, nothing extra to run on drop).
            #[cfg(feature = "wgpu-30")]
            let hal_texture = wgpu::hal::metal::Device::texture_from_raw(
                metal_texture,
                wgpu::TextureFormat::Bgra8Unorm,
                MTLTextureType::Type2D,
                1,
                1,
                copy_size,
                None, // drop_callback
            );
            #[cfg(all(feature = "wgpu-29", not(feature = "wgpu-30")))]
            let hal_texture = wgpu::hal::metal::Device::texture_from_raw(
                metal_texture,
                wgpu::TextureFormat::Bgra8Unorm,
                MTLTextureType::Type2D,
                1,
                1,
                copy_size,
            );
            #[cfg(all(
                feature = "wgpu-28",
                not(feature = "wgpu-29"),
                not(feature = "wgpu-30")
            ))]
            let hal_texture = wgpu::hal::metal::Device::texture_from_raw(
                metal::Texture::from_ptr(Retained::into_raw(metal_texture).cast()),
                wgpu::TextureFormat::Bgra8Unorm,
                metal::MTLTextureType::D2,
                1,
                1,
                copy_size,
            );

            Ok(
                crate::wgpu_compat::create_texture_from_hal::<wgpu_hal::api::Metal>(
                    wgpu_device,
                    hal_texture,
                    &wgpu::TextureDescriptor {
                        label: Some("iosurface-metal-import"),
                        size: wgpu::Extent3d {
                            width: size.width,
                            height: size.height,
                            depth_or_array_layers: 1,
                        },
                        mip_level_count: 1,
                        sample_count: 1,
                        dimension: wgpu::TextureDimension::D2,
                        format: wgpu::TextureFormat::Bgra8Unorm,
                        usage: wgpu::TextureUsages::TEXTURE_BINDING
                            | wgpu::TextureUsages::RENDER_ATTACHMENT,
                        view_formats: &[],
                    },
                    wgpu::TextureUses::RESOURCE,
                ),
            )
        }
    }
}