use crate::metal;
use crate::metal::*;
use objc2::{rc::Retained, runtime::ProtocolObject};
pub mod core_video;
mod coverage_cache;
use anyhow::{Result, ensure};
pub(crate) use coverage_cache::{CoverageCache, CoverageKey};
use gpui::{CustomDraw, DevicePixels, Size, size};
use objc2_metal::{MTLCommandBuffer, MTLDevice, MTLTexture};
#[derive(Clone)]
pub struct MetalCustomContext {
pub device: Device,
pub(crate) queue: CommandQueue,
recording: std::sync::Arc<std::sync::Mutex<PreparationRecording>>,
}
#[derive(Clone)]
pub struct MetalQueueIdentity(CommandQueue);
impl MetalQueueIdentity {
pub fn key(&self) -> u64 {
self.0.as_ptr() as usize as u64
}
}
struct PreparationRecording {
command: Option<CommandBuffer>,
thread: std::thread::ThreadId,
}
unsafe impl Send for PreparationRecording {}
impl MetalCustomContext {
pub fn new(device: Device, queue: CommandQueue) -> Self {
Self {
device,
queue,
recording: std::sync::Arc::new(std::sync::Mutex::new(PreparationRecording {
command: None,
thread: std::thread::current().id(),
})),
}
}
pub fn record(
&self,
encode: impl FnOnce(&CommandBuffer) -> Result<()>,
) -> Result<CommandBuffer> {
let mut recording = self
.recording
.try_lock()
.map_err(|_| anyhow::anyhow!("Metal preparation recording is busy or poisoned"))?;
ensure!(
recording.thread == std::thread::current().id(),
"Metal batch recording must run on its preparation thread"
);
if recording
.command
.as_ref()
.is_none_or(|command| command.status() != MTLCommandBufferStatus::NotEnqueued)
{
recording.command = Some(
self.queue
.commandBuffer()
.ok_or_else(|| anyhow::anyhow!("Metal command buffer allocation failed"))?
.to_owned(),
);
}
let command = recording.command.as_ref().unwrap();
encode(command)?;
Ok(command.clone())
}
pub fn objc2_device(&self) -> &ProtocolObject<dyn MTLDevice> {
&self.device
}
#[cfg(test)]
pub(crate) fn objc2_queue(&self) -> &ProtocolObject<dyn objc2_metal::MTLCommandQueue> {
&self.queue
}
pub fn queue_identity(&self) -> MetalQueueIdentity {
MetalQueueIdentity(self.queue.clone())
}
pub fn cache_key(&self) -> u64 {
self.queue.as_ptr() as usize as u64
}
pub unsafe fn adopt_texture(
&self,
texture: Retained<ProtocolObject<dyn MTLTexture>>,
commands: Vec<CommandBuffer>,
) -> Result<MetalImageOutput> {
ensure!(
texture.device().registryID() == self.device.registryID(),
"adopted texture belongs to another device"
);
ensure!(
texture.textureType() == MTLTextureType::Type2D
&& texture.sampleCount() == 1
&& texture.arrayLength() == 1
&& texture.usage().contains(MTLTextureUsage::ShaderRead)
&& matches!(
texture.pixelFormat(),
MTLPixelFormat::RGBA8Unorm
| MTLPixelFormat::BGRA8Unorm
| MTLPixelFormat::RGBA16Float
)
&& (1..=16384).contains(&texture.width())
&& (1..=16384).contains(&texture.height()),
"adopted texture must be a sampleable 2D RGBA8, BGRA8 or RGBA16Float texture of 1..=16384 pixels"
);
validate_commands(self, &commands)?;
Ok(MetalImageOutput {
output: MetalCustomOutput { texture, commands },
queue: self.queue.clone(),
})
}
}
pub struct MetalCustomOutput {
pub texture: Texture,
pub commands: Vec<CommandBuffer>,
}
pub struct MetalImageOutput {
output: MetalCustomOutput,
queue: CommandQueue,
}
unsafe impl Send for MetalImageOutput {}
impl MetalImageOutput {
pub fn is_submitted(&self) -> bool {
self.output.commands.iter().all(|command| {
matches!(
command.status(),
MTLCommandBufferStatus::Committed
| MTLCommandBufferStatus::Scheduled
| MTLCommandBufferStatus::Completed
)
})
}
pub fn reusable_for(&self, context: &MetalCustomContext) -> Result<Self> {
ensure!(
self.queue.as_ptr() == context.queue.as_ptr(),
"image output belongs to another queue"
);
ensure!(
self.output.texture.device().registryID() == context.device.registryID(),
"image output belongs to another device"
);
let mut commands = Vec::new();
for command in &self.output.commands {
if wide_command_pending(command.status())? {
commands.push(command.clone());
}
}
Ok(Self {
output: MetalCustomOutput {
texture: self.output.texture.clone(),
commands,
},
queue: self.queue.clone(),
})
}
}
#[derive(Clone)]
pub struct MetalImageStagingPool {
queue: CommandQueue,
buffers: std::sync::Arc<gpui::ImageStagingPool<Buffer>>,
}
impl MetalImageStagingPool {
pub fn stats(&self) -> gpui::ImageStagingPoolStats {
self.buffers.stats()
}
}
impl MetalCustomContext {
pub fn image_staging_pool(&self, capacity: usize) -> MetalImageStagingPool {
MetalImageStagingPool {
queue: self.queue.clone(),
buffers: gpui::ImageStagingPool::new(capacity),
}
}
pub fn stage_image_upload(&self, data: &gpui::ImageTextureData) -> Result<MetalImageOutput> {
objc2::rc::autoreleasepool(|_| self.create_image_output(data))
}
pub fn stage_image_upload_with_pool(
&self,
data: &gpui::ImageTextureData,
pool: &MetalImageStagingPool,
guard: Option<std::sync::Arc<dyn Send + Sync>>,
) -> Result<MetalImageOutput> {
objc2::rc::autoreleasepool(|_| self.create_pooled_image(data, pool, guard))
}
pub fn create_image_output(&self, data: &gpui::ImageTextureData) -> Result<MetalImageOutput> {
self.create_pooled_image(
data,
&self.image_staging_pool(data.staging_byte_len()),
None,
)
}
fn create_pooled_image(
&self,
data: &gpui::ImageTextureData,
pool: &MetalImageStagingPool,
guard: Option<std::sync::Arc<dyn Send + Sync>>,
) -> Result<MetalImageOutput> {
ensure!(
pool.queue.as_ptr() == self.queue.as_ptr(),
"foreign image staging pool"
);
ensure!(
self.queue.device().registryID() == self.device.registryID(),
"host queue belongs to another device"
);
let bytes = data.staging_byte_len();
let allocation = pool.buffers.checkout(bytes as u64, bytes, || {
Ok(metal::Buffer(
self.device
.newBufferWithLength_options(
bytes as usize,
MTLResourceOptions::StorageModeShared,
)
.expect("Metal buffer allocation failed"),
))
})?;
let desc = TextureDescriptor::new();
unsafe { desc.setWidth(data.width() as usize) };
unsafe { desc.setHeight(data.height() as usize) };
unsafe { desc.setMipmapLevelCount(data.levels().len() as usize) };
desc.setPixelFormat(match data.format() {
gpui::ImageTextureFormat::Rgba8Srgb => MTLPixelFormat::RGBA8Unorm,
gpui::ImageTextureFormat::LinearP3Rgba16Float
| gpui::ImageTextureFormat::LinearRec2020Rgba16Float => MTLPixelFormat::RGBA16Float,
});
desc.setStorageMode(MTLStorageMode::Private);
desc.setUsage(MTLTextureUsage::ShaderRead);
let texture = self
.device
.newTextureWithDescriptor(&desc)
.expect("Metal texture allocation failed");
let command = self
.queue
.commandBuffer()
.expect("Metal command buffer allocation failed")
.to_owned();
let encoder = command
.blitCommandEncoder()
.expect("Metal blit encoder allocation failed");
let buffer = allocation.resource();
let mapped = unsafe {
std::slice::from_raw_parts_mut(buffer.contents().cast::<u8>().as_ptr(), bytes)
};
let mut offset = 0usize;
for (level, pixels) in data.levels().iter().enumerate() {
let width = (data.width() >> level).max(1) as usize;
let height = (data.height() >> level).max(1) as usize;
let bpp = data.bytes_per_pixel();
let row = (width * bpp as usize).div_ceil(256) * 256;
for y in 0..height as usize {
mapped[offset + y * row as usize..offset + y * row as usize + width as usize * bpp]
.copy_from_slice(
&pixels[y * width as usize * bpp..(y + 1) * width as usize * bpp],
);
}
unsafe {
encoder.copyFromBuffer_sourceOffset_sourceBytesPerRow_sourceBytesPerImage_sourceSize_toTexture_destinationSlice_destinationLevel_destinationOrigin_options(buffer, offset as usize, row, row * height, MTLSize { width: width, height: height, depth: 1 }, &texture, 0, level as usize, MTLOrigin { x: 0, y: 0, z: 0 }, MTLBlitOption::empty())
};
offset += row as usize * height as usize;
}
encoder.endEncoding();
let completion = std::sync::Mutex::new(Some((allocation, guard)));
let block = block2::RcBlock::new(
move |_: std::ptr::NonNull<ProtocolObject<dyn MTLCommandBuffer>>| {
completion.lock().unwrap().take();
},
);
unsafe {
command.addCompletedHandler(block2::RcBlock::as_ptr(&block));
}
Ok(MetalImageOutput {
output: MetalCustomOutput {
texture,
commands: vec![command],
},
queue: self.queue.clone(),
})
}
}
pub struct MetalWideSdrOutput {
output: MetalCustomOutput,
queue: CommandQueue,
}
impl MetalWideSdrOutput {
pub fn from_objc2_linear_p3_premultiplied(
context: &MetalCustomContext,
texture: Retained<ProtocolObject<dyn MTLTexture>>,
commands: Vec<Retained<ProtocolObject<dyn MTLCommandBuffer>>>,
) -> Result<Self> {
Self::from_linear_p3_premultiplied(context, texture, commands)
}
pub fn from_linear_p3_premultiplied(
context: &MetalCustomContext,
texture: Texture,
commands: Vec<CommandBuffer>,
) -> Result<Self> {
let result = Self {
output: MetalCustomOutput { texture, commands },
queue: context.queue.clone(),
};
result.validate_context(context)?;
Ok(result)
}
pub fn reusable_for(&self, context: &MetalCustomContext) -> Result<Self> {
self.validate_resources(context)?;
let mut commands = Vec::new();
for command in &self.output.commands {
if wide_command_pending(command.status())? {
commands.push(command.clone());
}
}
Ok(Self {
output: MetalCustomOutput {
texture: self.output.texture.clone(),
commands,
},
queue: self.queue.clone(),
})
}
fn validate_resources(&self, context: &MetalCustomContext) -> Result<()> {
ensure!(
self.queue.as_ptr() == context.queue.as_ptr(),
"wide output belongs to another queue"
);
ensure!(
context.queue.device().registryID() == context.device.registryID(),
"host queue belongs to another device"
);
let t = &self.output.texture;
ensure!(
t.device().registryID() == context.device.registryID(),
"wide texture belongs to another device"
);
ensure!(
t.pixelFormat() == MTLPixelFormat::RGBA16Float
&& t.textureType() == MTLTextureType::Type2D
&& t.sampleCount() == 1
&& t.usage().contains(MTLTextureUsage::ShaderRead)
&& t.width() > 0
&& t.height() > 0
&& t.width() <= 16384
&& t.height() <= 16384,
"invalid wide texture format, usage, or extent"
);
Ok(())
}
pub(crate) fn validate_context(&self, context: &MetalCustomContext) -> Result<()> {
self.validate_resources(context)?;
validate_commands(context, &self.output.commands)
}
#[cfg(target_os = "macos")]
pub(crate) fn into_output(self) -> MetalCustomOutput {
self.output
}
}
fn wide_command_pending(status: MTLCommandBufferStatus) -> Result<bool> {
match status {
MTLCommandBufferStatus::NotEnqueued => Ok(true),
MTLCommandBufferStatus::Committed
| MTLCommandBufferStatus::Scheduled
| MTLCommandBufferStatus::Completed => Ok(false),
_ => anyhow::bail!("wide output command was externally enqueued or failed"),
}
}
pub(crate) struct PreparedMetalOutput {
pub output: MetalCustomOutput,
nv12: Option<MetalNv12Output>,
}
impl From<MetalCustomOutput> for PreparedMetalOutput {
fn from(output: MetalCustomOutput) -> Self {
Self { output, nv12: None }
}
}
impl std::ops::Deref for PreparedMetalOutput {
type Target = MetalCustomOutput;
fn deref(&self) -> &Self::Target {
&self.output
}
}
#[derive(Clone)]
pub(crate) struct MetalNv12Output {
y: Texture,
uv: Texture,
rows: [[f32; 4]; 3],
queue: CommandQueue,
}
impl MetalNv12Output {
fn reusable_for(&self, context: &MetalCustomContext) -> Result<Self> {
ensure!(
self.queue.as_ptr() == context.queue.as_ptr(),
"NV12 output belongs to another queue"
);
ensure!(
self.y.device().registryID() == context.device.registryID()
&& self.uv.device().registryID() == context.device.registryID(),
"NV12 planes belong to another device"
);
Ok(self.clone())
}
}
pub(crate) trait MetalSampleSource {
fn color_texture(&self) -> &TextureRef;
fn nv12_planes(&self) -> Option<&MetalNv12Output> {
None
}
}
impl MetalSampleSource for MetalCustomOutput {
fn color_texture(&self) -> &TextureRef {
&self.texture
}
}
impl MetalSampleSource for PreparedMetalOutput {
fn color_texture(&self) -> &TextureRef {
&self.output.texture
}
fn nv12_planes(&self) -> Option<&MetalNv12Output> {
self.nv12.as_ref()
}
}
pub(crate) struct CustomCompositor {
pipeline: RenderPipelineState,
pub(crate) masks: CoverageCache<Texture>,
pub(crate) mask_bytes: usize,
}
impl CustomCompositor {
pub fn new(device: &DeviceRef) -> Result<Self> {
let library = device
.newLibraryWithSource_options_error(
&objc2_foundation::NSString::from_str(include_str!("custom_draw.metal")),
Some(&CompileOptions::new()),
)
.map_err(anyhow::Error::msg)?;
let descriptor = RenderPipelineDescriptor::new();
let vertex = library
.newFunctionWithName(&objc2_foundation::NSString::from_str("custom_vertex"))
.ok_or_else(|| anyhow::anyhow!("Metal function missing"))
.map_err(anyhow::Error::msg)?;
let fragment = library
.newFunctionWithName(&objc2_foundation::NSString::from_str("custom_fragment"))
.ok_or_else(|| anyhow::anyhow!("Metal function missing"))
.map_err(anyhow::Error::msg)?;
descriptor.setVertexFunction(Some(&vertex));
descriptor.setFragmentFunction(Some(&fragment));
let color = unsafe { descriptor.colorAttachments().objectAtIndexedSubscript(0) };
color.setPixelFormat(MTLPixelFormat::BGRA8Unorm);
color.setBlendingEnabled(true);
color.setSourceRGBBlendFactor(MTLBlendFactor::SourceAlpha);
color.setDestinationRGBBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
color.setSourceAlphaBlendFactor(MTLBlendFactor::One);
color.setDestinationAlphaBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
let pipeline = device
.newRenderPipelineStateWithDescriptor_error(&descriptor)
.map_err(anyhow::Error::msg)?;
Ok(Self {
pipeline,
masks: Default::default(),
mask_bytes: 0,
})
}
pub fn extent(draw: &CustomDraw) -> Result<Size<DevicePixels>> {
ensure!(
draw.opacity.is_finite() && (0.0..=1.0).contains(&draw.opacity),
"invalid custom opacity"
);
ensure!(
[
draw.bounds.origin.x.0,
draw.bounds.origin.y.0,
draw.content_mask.bounds.origin.x.0,
draw.content_mask.bounds.origin.y.0,
draw.content_mask.bounds.size.width.0,
draw.content_mask.bounds.size.height.0,
draw.corner_radii.top_left.0,
draw.corner_radii.top_right.0,
draw.corner_radii.bottom_left.0,
draw.corner_radii.bottom_right.0
]
.iter()
.all(|v| v.is_finite()),
"non-finite custom geometry"
);
if let Some(source) = draw.content.image_source_size() {
ensure!(
draw.coverage.is_none()
&& draw.bounds.size.width.0.is_finite()
&& draw.bounds.size.height.0.is_finite()
&& draw.bounds.size.width.0 > 0.
&& draw.bounds.size.height.0 > 0.
&& source.width.0 > 0
&& source.height.0 > 0
&& source.width.0 <= 16384
&& source.height.0 <= 16384,
"invalid source-sized image preparation"
);
return Ok(source);
}
let width = draw.bounds.size.width.0.ceil();
let height = draw.bounds.size.height.0.ceil();
ensure!(
width.is_finite()
&& height.is_finite()
&& width > 0.
&& height > 0.
&& width <= 16384.
&& height <= 16384.,
"invalid custom texture extent"
);
Ok(size(
DevicePixels(width as i32),
DevicePixels(height as i32),
))
}
pub fn validate(
context: &MetalCustomContext,
draw: &CustomDraw,
output: Box<dyn std::any::Any>,
) -> Result<MetalCustomOutput> {
Self::validate_format(context, draw, output, MTLPixelFormat::BGRA8Unorm)
}
pub(crate) fn validate_format(
context: &MetalCustomContext,
draw: &CustomDraw,
output: Box<dyn std::any::Any>,
format: MTLPixelFormat,
) -> Result<MetalCustomOutput> {
let extent = Self::extent(draw)?;
let source_sized = output.is::<MetalImageOutput>();
let output = if source_sized {
let image = *output.downcast::<MetalImageOutput>().unwrap();
ensure!(
image.queue.as_ptr() == context.queue.as_ptr(),
"image output belongs to another queue"
);
Box::new(image.output) as Box<dyn std::any::Any>
} else {
output
};
let output = *output
.downcast::<MetalCustomOutput>()
.map_err(|_| anyhow::anyhow!("producer returned a non-Metal output"))?;
ensure!(
output.texture.device().registryID() == context.device.registryID(),
"custom texture belongs to another device"
);
if source_sized {
ensure!(
(output.texture.pixelFormat() == MTLPixelFormat::RGBA16Float)
== (draw.content.image_color_mode() != gpui::ImageColorMode::EncodedSrgb),
"image texture format does not match source color metadata"
);
}
ensure!(
(if source_sized {
matches!(
output.texture.pixelFormat(),
MTLPixelFormat::RGBA8Unorm
| MTLPixelFormat::BGRA8Unorm
| MTLPixelFormat::RGBA16Float
)
} else {
output.texture.pixelFormat() == format
}) && output.texture.textureType() == MTLTextureType::Type2D
&& output.texture.sampleCount() == 1
&& output.texture.usage().contains(MTLTextureUsage::ShaderRead)
&& (source_sized
|| (output.texture.width() == extent.width.0 as usize
&& output.texture.height() == extent.height.0 as usize)),
"invalid custom texture format, usage, or size"
);
validate_commands(context, &output.commands)?;
Ok(output)
}
pub(crate) fn validate_nv12(
context: &MetalCustomContext,
raw: Box<dyn std::any::Any>,
) -> Result<PreparedMetalOutput> {
let planes = *raw
.downcast::<MetalNv12Output>()
.map_err(|_| anyhow::anyhow!("expected native NV12 output"))?;
let planes = planes.reusable_for(context)?;
for (texture, format) in [
(&planes.y, MTLPixelFormat::R8Unorm),
(&planes.uv, MTLPixelFormat::RG8Unorm),
] {
ensure!(
texture.pixelFormat() == format
&& texture.textureType() == MTLTextureType::Type2D
&& texture.sampleCount() == 1
&& texture.usage().contains(MTLTextureUsage::ShaderRead),
"invalid NV12 texture view"
);
}
ensure!(
planes.y.width() == planes.uv.width() * 2
&& planes.y.height() == planes.uv.height() * 2,
"NV12 plane extents do not match"
);
Ok(PreparedMetalOutput {
output: MetalCustomOutput {
texture: planes.y.clone(),
commands: vec![],
},
nv12: Some(planes),
})
}
pub fn draw(
&self,
draw: &CustomDraw,
output: &impl MetalSampleSource,
coverage: Option<&TextureRef>,
native_source: bool,
viewport: Size<DevicePixels>,
encoder: &RenderCommandEncoderRef,
) {
let adjustments = draw.content.image_linear_adjustments();
let transform = adjustments
.map(|v| v.rows())
.unwrap_or_else(|| draw.content.image_color_transform());
let vignette = adjustments
.map(|v| v.vignette())
.unwrap_or([0., 0., 1., 1.]);
let linear = [
if adjustments.is_some() { 1. } else { 0. },
draw.content.image_color_mode().shader_value(),
if adjustments.is_some_and(|v| v.is_p3()) {
1.
} else {
0.
},
0.,
];
let planes = output.nv12_planes();
let yuv = planes.map(|p| p.rows).unwrap_or([[0.; 4]; 3]);
let params: [[f32; 4]; 12] = [
[
draw.bounds.origin.x.0,
draw.bounds.origin.y.0,
draw.bounds.size.width.0,
draw.bounds.size.height.0,
],
[
draw.content_mask.bounds.origin.x.0,
draw.content_mask.bounds.origin.y.0,
draw.content_mask.bounds.size.width.0,
draw.content_mask.bounds.size.height.0,
],
[
draw.corner_radii.top_left.0,
draw.corner_radii.top_right.0,
draw.corner_radii.bottom_right.0,
draw.corner_radii.bottom_left.0,
],
[
viewport.width.0 as f32,
viewport.height.0 as f32,
draw.opacity * draw.content.image_sample_opacity().clamp(0., 1.),
(if coverage.is_some() { 1. } else { 0. })
+ (if native_source { 2. } else { 0. })
+ (if planes.is_some() { 4. } else { 0. }),
],
transform[0],
transform[1],
transform[2],
yuv[0],
yuv[1],
yuv[2],
vignette,
linear,
];
encoder.setRenderPipelineState(&self.pipeline);
unsafe {
encoder.setVertexBytes_length_atIndex(
std::ptr::NonNull::new(params.as_ptr().cast::<std::ffi::c_void>() as *mut _)
.expect("non-null bytes"),
std::mem::size_of_val(¶ms) as usize,
0,
)
};
unsafe {
encoder.setFragmentBytes_length_atIndex(
std::ptr::NonNull::new(params.as_ptr().cast::<std::ffi::c_void>() as *mut _)
.expect("non-null bytes"),
std::mem::size_of_val(¶ms) as usize,
0,
)
};
unsafe { encoder.setFragmentTexture_atIndex(Some(output.color_texture()), 0) };
unsafe { encoder.setFragmentTexture_atIndex(coverage.or(Some(output.color_texture())), 1) };
unsafe {
encoder.setFragmentTexture_atIndex(
Some(
planes
.map(|p| p.uv.as_ref())
.unwrap_or(output.color_texture()),
),
2,
)
};
unsafe { encoder.drawPrimitives_vertexStart_vertexCount(MTLPrimitiveType::Triangle, 0, 6) };
}
}
fn validate_commands(context: &MetalCustomContext, commands: &[CommandBuffer]) -> Result<()> {
for command in commands {
let queue = command.commandQueue();
ensure!(
Retained::as_ptr(&queue) == Retained::as_ptr(&context.queue),
"custom command belongs to another queue"
);
ensure!(
command.status() == MTLCommandBufferStatus::NotEnqueued,
"custom command already submitted"
);
}
Ok(())
}
pub(crate) fn submit_custom_commands<'a>(
context: &MetalCustomContext,
outputs: impl IntoIterator<Item = &'a MetalCustomOutput>,
) -> Result<()> {
let mut seen = std::collections::HashSet::new();
let mut pending = Vec::new();
for output in outputs {
validate_commands(context, &output.commands)?;
for command in &output.commands {
if seen.insert(command.as_ptr() as usize) {
pending.push(command);
}
}
}
for command in pending {
command.commit();
}
Ok(())
}
#[cfg(test)]
mod submission_tests;
#[cfg(test)]
mod wide_output_tests;
pub(crate) fn texture(device: &DeviceRef, extent: Size<DevicePixels>) -> Result<Texture> {
ensure!(
extent.width.0 > 0
&& extent.height.0 > 0
&& extent.width.0 <= 16384
&& extent.height.0 <= 16384,
"invalid paint extent"
);
ensure!(
(extent.width.0 as usize) * (extent.height.0 as usize) * 4 <= 64 * 1024 * 1024,
"paint texture exceeds 64 MiB"
);
let desc = TextureDescriptor::new();
unsafe { desc.setWidth(extent.width.0 as usize) };
unsafe { desc.setHeight(extent.height.0 as usize) };
desc.setPixelFormat(MTLPixelFormat::BGRA8Unorm);
desc.setUsage(MTLTextureUsage::RenderTarget | MTLTextureUsage::ShaderRead);
desc.setStorageMode(MTLStorageMode::Private);
Ok(device
.newTextureWithDescriptor(&desc)
.expect("Metal texture allocation failed"))
}
pub(crate) fn coverage_scene(draw: &CustomDraw, extent: Size<DevicePixels>) -> Result<gpui::Scene> {
use gpui::*;
let mut scene = Scene::default();
let bounds = Bounds::new(
point(ScaledPixels(0.), ScaledPixels(0.)),
size(
ScaledPixels(extent.width.0 as f32),
ScaledPixels(extent.height.0 as f32),
),
);
let mask = ContentMask { bounds };
match draw.coverage.as_ref().expect("coverage") {
CustomCoverage::Border { widths, style } => {
ensure!(
[widths.top.0, widths.right.0, widths.bottom.0, widths.left.0]
.iter()
.all(|v| v.is_finite() && *v >= 0.),
"invalid border widths"
);
scene.insert_primitive(Quad {
bounds,
content_mask: mask,
background: transparent_black().into(),
border_color: white(),
border_widths: *widths,
corner_radii: draw.corner_radii,
border_style: *style,
..Default::default()
});
}
CustomCoverage::Path(path) => {
ensure!(path.vertices.len() <= 1_000_000, "coverage path too large");
let mut path = path.as_ref().clone();
let sx = bounds.size.width.0 / draw.bounds.size.width.0;
let sy = bounds.size.height.0 / draw.bounds.size.height.0;
for vertex in &mut path.vertices {
vertex.xy_position.x =
ScaledPixels((vertex.xy_position.x.0 - draw.bounds.origin.x.0) * sx);
vertex.xy_position.y =
ScaledPixels((vertex.xy_position.y.0 - draw.bounds.origin.y.0) * sy);
ensure!(
[
vertex.xy_position.x.0,
vertex.xy_position.y.0,
vertex.st_position.x,
vertex.st_position.y
]
.iter()
.all(|v| v.is_finite()),
"non-finite coverage path"
);
vertex.content_mask = mask;
}
path.bounds = bounds;
path.content_mask = mask;
path.color = white().into();
scene.insert_primitive(path);
}
}
scene.finish();
Ok(scene)
}