use crate::{EngineSurface, SurfaceDesc, SurfaceFormat, SurfaceSlot, Unsupported};
use anyhow::{Context as _, Result};
use gpui_apple::custom_draw::{MetalCustomContext, MetalCustomOutput, MetalWideSdrOutput};
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_core_foundation::{CFDictionary, CFNumber, CFRetained, CFString};
use objc2_io_surface::{
IOSurfaceRef, kIOSurfaceBytesPerElement, kIOSurfaceHeight, kIOSurfacePixelFormat,
kIOSurfaceWidth,
};
use objc2_metal::{
MTLCommandBuffer, MTLCommandEncoder, MTLDevice, MTLLoadAction, MTLPixelFormat,
MTLRenderPassDescriptor, MTLSharedEvent, MTLStoreAction, MTLTexture, MTLTextureDescriptor,
MTLTextureUsage,
};
use std::any::Any;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
#[cfg(feature = "wgpu")]
mod wgpu_target;
#[cfg(feature = "wgpu")]
pub use wgpu_target::WgpuSurfaceTarget;
#[cfg(test)]
mod tests;
#[derive(Clone)]
pub struct SyncPoint {
event: Retained<ProtocolObject<dyn MTLSharedEvent>>,
value: u64,
submitted: bool,
seen: Arc<AtomicBool>,
}
unsafe impl Send for SyncPoint {}
unsafe impl Sync for SyncPoint {}
impl std::fmt::Debug for SyncPoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"SyncPoint({}{})",
self.value,
if self.submitted { ", submitted" } else { "" }
)
}
}
impl SyncPoint {
pub fn new(event: Retained<ProtocolObject<dyn MTLSharedEvent>>, value: u64) -> Self {
Self {
event,
value,
submitted: false,
seen: Arc::new(AtomicBool::new(false)),
}
}
pub unsafe fn submitted(
event: Retained<ProtocolObject<dyn MTLSharedEvent>>,
value: u64,
) -> Self {
Self {
submitted: true,
..Self::new(event, value)
}
}
pub fn value(&self) -> u64 {
self.value
}
pub fn is_gpu_wait(&self) -> bool {
self.submitted
}
pub(crate) fn reached(&self) -> bool {
if self.seen.load(Ordering::Acquire) {
return true;
}
let reached = self.event.signaledValue() >= self.value;
if reached {
self.seen.store(true, Ordering::Release);
}
reached
}
pub(crate) fn ready(&self) -> bool {
self.submitted || self.reached()
}
pub(crate) fn wait_reached(&self, timeout: Duration) -> bool {
self.reached()
|| (self
.event
.waitUntilSignaledValue_timeoutMS(self.value, timeout.as_millis() as u64)
&& self.reached())
}
}
pub(crate) fn metal_pixel_format(format: SurfaceFormat) -> MTLPixelFormat {
match format {
SurfaceFormat::Bgra8Srgb => MTLPixelFormat::BGRA8Unorm,
SurfaceFormat::Rgba16FloatLinearP3 => MTLPixelFormat::RGBA16Float,
}
}
type HostTexture = (u64, Retained<ProtocolObject<dyn MTLTexture>>);
#[derive(Debug)]
pub(crate) struct SlotResources {
surface: CFRetained<IOSurfaceRef>,
host: Mutex<Option<HostTexture>>,
}
unsafe impl Send for SlotResources {}
unsafe impl Sync for SlotResources {}
impl SlotResources {
pub(crate) fn release_engine(&self) {}
fn new(desc: &SurfaceDesc) -> Result<Self> {
let keys: [&CFString; 4] = unsafe {
[
kIOSurfaceWidth,
kIOSurfaceHeight,
kIOSurfaceBytesPerElement,
kIOSurfacePixelFormat,
]
};
let pixel_format = match desc.format {
SurfaceFormat::Bgra8Srgb => *b"BGRA",
SurfaceFormat::Rgba16FloatLinearP3 => *b"RGhA",
};
let values = [
CFNumber::new_i32(desc.width as i32),
CFNumber::new_i32(desc.height as i32),
CFNumber::new_i32(desc.format.bytes_per_pixel() as i32),
CFNumber::new_i32(i32::from_be_bytes(pixel_format)),
];
let values: Vec<&CFNumber> = values.iter().map(|value| &**value).collect();
let properties = CFDictionary::from_slices(&keys, &values);
let surface = unsafe { IOSurfaceRef::new(properties.as_opaque()) }
.context("IOSurface allocation failed")?;
Ok(Self {
surface,
host: Mutex::new(None),
})
}
fn host_texture(
&self,
device: &ProtocolObject<dyn MTLDevice>,
desc: &SurfaceDesc,
) -> Result<Retained<ProtocolObject<dyn MTLTexture>>> {
let registry = device.registryID();
let mut cached = self.host.lock().unwrap();
if let Some((id, texture)) = cached.as_ref()
&& *id == registry
{
return Ok(texture.clone());
}
let texture = surface_texture(
device,
&self.surface,
desc,
MTLTextureUsage::ShaderRead | MTLTextureUsage::RenderTarget,
)?;
*cached = Some((registry, texture.clone()));
Ok(texture)
}
}
pub(crate) fn surface_texture(
device: &ProtocolObject<dyn MTLDevice>,
surface: &IOSurfaceRef,
desc: &SurfaceDesc,
usage: MTLTextureUsage,
) -> Result<Retained<ProtocolObject<dyn MTLTexture>>> {
let descriptor = unsafe {
MTLTextureDescriptor::texture2DDescriptorWithPixelFormat_width_height_mipmapped(
metal_pixel_format(desc.format),
desc.width as usize,
desc.height as usize,
false,
)
};
descriptor.setUsage(usage);
device
.newTextureWithDescriptor_iosurface_plane(&descriptor, surface, 0)
.context("IOSurface-backed Metal texture creation failed")
}
impl EngineSurface {
pub fn new(desc: SurfaceDesc) -> Result<Self> {
desc.validate()?;
let slots = (0..desc.slots)
.map(|_| SlotResources::new(&desc).map(Arc::new))
.collect::<Result<Vec<_>>>()
.map_err(|error| Unsupported(format!("{error:#}")))?;
Self::from_slots(desc, slots)
}
}
impl SurfaceSlot {
pub fn iosurface(&self) -> &IOSurfaceRef {
&self.resources.surface
}
}
pub(crate) fn retire_completed(_slots: &[Arc<SlotResources>]) {}
pub(crate) fn prepare_frame(
desc: &SurfaceDesc,
resources: &SlotResources,
sync: &SyncPoint,
color: gpui::ImageColorMode,
backend: &dyn Any,
) -> Result<Box<dyn Any>> {
let context = backend
.downcast_ref::<MetalCustomContext>()
.ok_or_else(|| Unsupported("engine surfaces require the Metal compositor".into()))?;
let device = context.objc2_device();
let texture = resources.host_texture(device, desc)?;
let commands = if sync.reached() {
Vec::new()
} else {
anyhow::ensure!(sync.submitted, "engine frame selected before it was ready");
let event = sync.event.clone();
let value = sync.value;
let target = texture.clone();
vec![context.record(move |command| {
command.encodeWaitForEvent_value(ProtocolObject::from_ref(&*event), value);
let pass = MTLRenderPassDescriptor::new();
let attachment = unsafe { pass.colorAttachments().objectAtIndexedSubscript(0) };
attachment.setTexture(Some(&target));
attachment.setLoadAction(MTLLoadAction::Load);
attachment.setStoreAction(MTLStoreAction::Store);
command
.renderCommandEncoderWithDescriptor(&pass)
.context("Metal render encoder allocation failed")?
.endEncoding();
Ok(())
})?]
};
Ok(match (desc.format, color) {
(SurfaceFormat::Bgra8Srgb, _) => Box::new(MetalCustomOutput { texture, commands }),
(SurfaceFormat::Rgba16FloatLinearP3, gpui::ImageColorMode::LinearP3Wide) => Box::new(
MetalWideSdrOutput::from_objc2_linear_p3_premultiplied(context, texture, commands)?,
),
(SurfaceFormat::Rgba16FloatLinearP3, _) => {
Box::new(unsafe { context.adopt_texture(texture, commands) }?)
}
})
}