use std::ffi::c_void;
use std::fmt;
use std::ptr::NonNull;
use objc2_core_foundation::{CFGetTypeID, CFRetained};
use objc2_core_video::{
CVMetalTexture, CVMetalTextureCache, CVMetalTextureGetTexture, CVPixelBuffer,
CVPixelBufferGetHeight, CVPixelBufferGetPixelFormatType, CVPixelBufferGetTypeID,
CVPixelBufferGetWidth, kCVPixelFormatType_32BGRA, kCVReturnSuccess,
};
use objc2_metal::{MTLPixelFormat, MTLTextureType};
use wgpu::hal::api::Metal;
const TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8Unorm;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum MetalImportError {
Unsupported(String),
Failed(String),
}
impl fmt::Display for MetalImportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unsupported(reason) => write!(f, "CVPixelBuffer import unsupported: {reason}"),
Self::Failed(reason) => write!(f, "CVPixelBuffer import failed: {reason}"),
}
}
}
impl std::error::Error for MetalImportError {}
pub struct MetalImporter {
device: wgpu::Device,
cache: CFRetained<CVMetalTextureCache>,
}
unsafe impl Send for MetalImporter {}
impl fmt::Debug for MetalImporter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MetalImporter")
.field("device", &self.device)
.finish_non_exhaustive()
}
}
impl MetalImporter {
pub fn new(device: &wgpu::Device) -> Result<Self, MetalImportError> {
let hal_device = unsafe { device.as_hal::<Metal>() }.ok_or_else(|| {
MetalImportError::Unsupported("the device is not a Metal device".to_string())
})?;
let mut cache: *mut CVMetalTextureCache = std::ptr::null_mut();
let status = unsafe {
CVMetalTextureCache::create(
None,
None,
hal_device.raw_device(),
None,
NonNull::from(&mut cache),
)
};
drop(hal_device);
let cache = NonNull::new(cache)
.filter(|_| status == kCVReturnSuccess)
.ok_or_else(|| {
MetalImportError::Failed(format!("CVMetalTextureCacheCreate returned {status}"))
})?;
let cache = unsafe { CFRetained::from_raw(cache) };
Ok(Self {
device: device.clone(),
cache,
})
}
#[must_use]
pub fn device(&self) -> &wgpu::Device {
&self.device
}
pub unsafe fn import(
&self,
pixel_buffer: *mut c_void,
) -> Result<(wgpu::Texture, (u32, u32)), MetalImportError> {
let buffer = NonNull::new(pixel_buffer.cast::<CVPixelBuffer>())
.ok_or_else(|| MetalImportError::Failed("a null CVPixelBuffer".to_string()))?;
let buffer = unsafe { CFRetained::retain(buffer) };
if CFGetTypeID(Some(&buffer)) != CVPixelBufferGetTypeID() {
return Err(MetalImportError::Failed(
"the object is not a CVPixelBuffer".to_string(),
));
}
let format = CVPixelBufferGetPixelFormatType(&buffer);
if format != kCVPixelFormatType_32BGRA {
return Err(MetalImportError::Unsupported(format!(
"pixel format {} is not 32BGRA",
four_char_code(format)
)));
}
let max_dimension = self.device.limits().max_texture_dimension_2d;
let (width, height) = check_extent(
CVPixelBufferGetWidth(&buffer),
CVPixelBufferGetHeight(&buffer),
max_dimension,
)?;
self.cache.flush(0);
let mut cv_texture: *mut CVMetalTexture = std::ptr::null_mut();
let status = unsafe {
CVMetalTextureCache::create_texture_from_image(
None,
&self.cache,
&buffer,
None,
MTLPixelFormat::BGRA8Unorm,
width as usize,
height as usize,
0,
NonNull::from(&mut cv_texture),
)
};
let cv_texture = NonNull::new(cv_texture)
.filter(|_| status == kCVReturnSuccess)
.ok_or_else(|| {
MetalImportError::Failed(format!(
"CVMetalTextureCacheCreateTextureFromImage returned {status} (is the \
buffer IOSurface-backed?)"
))
})?;
let cv_texture = unsafe { CFRetained::from_raw(cv_texture) };
let raw = CVMetalTextureGetTexture(&cv_texture).ok_or_else(|| {
MetalImportError::Failed("CVMetalTextureGetTexture returned no texture".to_string())
})?;
let held = HeldFrame {
_texture: cv_texture,
_buffer: buffer,
};
let on_drop: wgpu::hal::DropCallback = Box::new(move || drop(held));
let hal_texture = unsafe {
wgpu::hal::metal::Device::texture_from_raw(
raw,
TEXTURE_FORMAT,
MTLTextureType::Type2D,
1,
1,
wgpu::hal::CopyExtent {
width,
height,
depth: 1,
},
Some(on_drop),
)
};
let descriptor = wgpu::TextureDescriptor {
label: Some("frust-gpu CVPixelBuffer"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TEXTURE_FORMAT,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
};
let texture = unsafe {
self.device.create_texture_from_hal::<Metal>(
hal_texture,
&descriptor,
wgpu::TextureUses::RESOURCE,
)
};
Ok((texture, (width, height)))
}
}
impl Drop for MetalImporter {
fn drop(&mut self) {
self.cache.flush(0);
}
}
struct HeldFrame {
_texture: CFRetained<CVMetalTexture>,
_buffer: CFRetained<CVPixelBuffer>,
}
unsafe impl Send for HeldFrame {}
unsafe impl Sync for HeldFrame {}
fn check_extent(
width: usize,
height: usize,
max_dimension: u32,
) -> Result<(u32, u32), MetalImportError> {
let fits = |edge: usize| {
u32::try_from(edge)
.ok()
.filter(|&edge| (1..=max_dimension).contains(&edge))
};
match (fits(width), fits(height)) {
(Some(width), Some(height)) => Ok((width, height)),
_ => Err(MetalImportError::Failed(format!(
"a {width}x{height} buffer is outside 1..={max_dimension}"
))),
}
}
fn four_char_code(code: u32) -> String {
let bytes = code.to_be_bytes();
if bytes.iter().all(|b| b.is_ascii_graphic() || *b == b' ') {
bytes.iter().map(|&b| char::from(b)).collect()
} else {
format!("{code:#010x}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_extent_within_the_limit_is_accepted() {
assert_eq!(check_extent(64, 32, 4096), Ok((64, 32)));
assert_eq!(check_extent(4096, 4096, 4096), Ok((4096, 4096)));
}
#[test]
fn empty_and_oversized_extents_are_refused() {
assert!(check_extent(0, 32, 4096).is_err());
assert!(check_extent(32, 0, 4096).is_err());
assert!(check_extent(4097, 32, 4096).is_err());
assert!(check_extent(usize::MAX, 32, 4096).is_err());
}
#[test]
fn pixel_formats_print_as_their_four_characters() {
assert_eq!(four_char_code(kCVPixelFormatType_32BGRA), "BGRA");
assert_eq!(four_char_code(0x3432_3076), "420v");
assert_eq!(four_char_code(0x0000_0020), "0x00000020");
}
}