use std::ptr::NonNull;
use objc2_core_foundation::CFRetained;
use objc2_core_video::{
CVMetalTexture, CVMetalTextureCache, CVMetalTextureGetTexture, CVPixelBuffer, CVPixelBufferGetHeightOfPlane,
CVPixelBufferGetPixelFormatType, CVPixelBufferGetWidthOfPlane, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, kCVPixelFormatType_420YpCbCr8Planar,
};
use objc2_metal::{MTLPixelFormat, MTLTextureType};
use super::source::{Layout, Source};
use crate::frame::macos::PixelBuffer;
use crate::{Color, Error, Size};
fn err(message: impl std::fmt::Display) -> Error {
Error::Render(anyhow::anyhow!("{message}"))
}
struct Keepalive(#[expect(dead_code, reason = "held for its release, never read")] CFRetained<CVMetalTexture>);
unsafe impl Send for Keepalive {}
unsafe impl Sync for Keepalive {}
pub(super) struct Import {
cache: CFRetained<CVMetalTextureCache>,
}
unsafe impl Send for Import {}
impl Import {
pub fn new(device: &wgpu::Device) -> Result<Self, Error> {
let metal = unsafe { device.as_hal::<wgpu::hal::api::Metal>() }
.ok_or_else(|| err("wgpu device is not a Metal device"))?;
let mut ptr: *mut CVMetalTextureCache = std::ptr::null_mut();
let status = unsafe {
CVMetalTextureCache::create(
None,
None,
metal.raw_device(),
None,
NonNull::new(&mut ptr).expect("stack pointer is non-null"),
)
};
drop(metal);
let cache = NonNull::new(ptr)
.filter(|_| status == 0)
.map(|ptr| unsafe { CFRetained::from_raw(ptr) })
.ok_or_else(|| err(format!("CVMetalTextureCacheCreate failed: {status}")))?;
Ok(Self { cache })
}
pub fn import(&mut self, device: &wgpu::Device, buffer: &PixelBuffer) -> Result<Source, Error> {
let size = Size::new(buffer.width(), buffer.height());
let format = CVPixelBufferGetPixelFormatType(buffer.buffer());
let (layout, range_is_full) = match format {
f if f == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange => (Layout::Nv12, false),
f if f == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange => (Layout::Nv12, true),
f if f == kCVPixelFormatType_420YpCbCr8Planar => (Layout::I420, false),
f => return Err(err(format!("cannot import pixel format {f:#x}"))),
};
let color = buffer
.color()
.unwrap_or_else(|| Color::infer(size).with_range(!range_is_full));
let (plane0, plane1, plane2) = match layout {
Layout::Nv12 => {
let y = self.plane(device, buffer, 0, MTLPixelFormat::R8Unorm, wgpu::TextureFormat::R8Unorm)?;
let uv = self.plane(
device,
buffer,
1,
MTLPixelFormat::RG8Unorm,
wgpu::TextureFormat::Rg8Unorm,
)?;
(y, uv.clone(), uv)
}
Layout::I420 => (
self.plane(device, buffer, 0, MTLPixelFormat::R8Unorm, wgpu::TextureFormat::R8Unorm)?,
self.plane(device, buffer, 1, MTLPixelFormat::R8Unorm, wgpu::TextureFormat::R8Unorm)?,
self.plane(device, buffer, 2, MTLPixelFormat::R8Unorm, wgpu::TextureFormat::R8Unorm)?,
),
};
self.cache.flush(0);
Ok(Source {
layout,
color,
plane0,
plane1,
plane2,
})
}
fn plane(
&self,
device: &wgpu::Device,
buffer: &PixelBuffer,
index: usize,
metal: MTLPixelFormat,
format: wgpu::TextureFormat,
) -> Result<wgpu::TextureView, Error> {
let image: &CVPixelBuffer = buffer.buffer();
let width = CVPixelBufferGetWidthOfPlane(image, index);
let height = CVPixelBufferGetHeightOfPlane(image, index);
if width == 0 || height == 0 {
return Err(err(format!("pixel buffer has no plane {index}")));
}
let mut ptr: *mut CVMetalTexture = std::ptr::null_mut();
let status = unsafe {
CVMetalTextureCache::create_texture_from_image(
None,
&self.cache,
image,
None,
metal,
width,
height,
index,
NonNull::new(&mut ptr).expect("stack pointer is non-null"),
)
};
let texture = NonNull::new(ptr)
.filter(|_| status == 0)
.map(|ptr| unsafe { CFRetained::from_raw(ptr) })
.ok_or_else(|| err(format!("CVMetalTextureCacheCreateTextureFromImage failed: {status}")))?;
let raw =
CVMetalTextureGetTexture(&texture).ok_or_else(|| err("CVMetalTextureGetTexture returned no texture"))?;
let keepalive = Keepalive(texture);
let extent = wgpu::Extent3d {
width: width as u32,
height: height as u32,
depth_or_array_layers: 1,
};
let descriptor = wgpu::TextureDescriptor {
label: Some("moq-video imported plane"),
size: extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
};
let texture = unsafe {
let hal = wgpu::hal::metal::Device::texture_from_raw(
raw,
format,
MTLTextureType::Type2D,
1,
1,
wgpu::hal::CopyExtent {
width: extent.width,
height: extent.height,
depth: 1,
},
Some(Box::new(move || drop(keepalive))),
);
device.create_texture_from_hal::<wgpu::hal::api::Metal>(hal, &descriptor, wgpu::TextureUses::RESOURCE)
};
Ok(texture.create_view(&Default::default()))
}
}