use crate::metal_atlas::MetalAtlas;
use anyhow::{Context as _, Result};
use block::ConcreteBlock;
use cocoa::{
base::{NO, YES},
foundation::{NSSize, NSUInteger},
quartzcore::AutoresizingMask,
};
use gpui::{
AtlasTextureId, Background, Bounds, ContentMask, DevicePixels, PaintSurface, Path, Point,
PrimitiveBatch, ScaledPixels, Scene, Size, point, size,
};
#[cfg(any(test, feature = "bench-support", feature = "test-support"))]
use image::RgbaImage;
use core_foundation::base::TCFType;
use core_video::{
metal_texture::CVMetalTextureGetTexture, metal_texture_cache::CVMetalTextureCache,
pixel_buffer::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
};
use foreign_types::{ForeignType, ForeignTypeRef};
use metal::{
CAMetalLayer, CommandQueue, MTLGPUFamily, MTLPixelFormat, MTLResourceOptions, NSRange,
};
use objc::{self, msg_send, sel, sel_impl};
use parking_lot::Mutex;
use std::{cell::Cell, ffi::c_void, mem, mem::MaybeUninit, ops::Range, ptr, slice, sync::Arc};
pub(crate) type PointF = gpui::Point<f32>;
#[cfg(not(feature = "runtime_shaders"))]
const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
#[cfg(feature = "runtime_shaders")]
const SHADERS_SOURCE_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal"));
const PATH_SAMPLE_COUNT: u32 = 4;
const INSTANCE_BUFFER_ALIGNMENT: usize = 256;
const MAX_INSTANCE_BUFFER_SIZE: usize = 256 * 1024 * 1024;
pub type Context = Arc<Mutex<InstanceBufferPool>>;
pub type Renderer = MetalRenderer;
pub unsafe fn new_renderer(
context: self::Context,
_native_window: *mut c_void,
_native_view: *mut c_void,
_bounds: gpui::Size<f32>,
transparent: bool,
) -> Renderer {
MetalRenderer::new(context, transparent)
}
pub struct InstanceBufferPool {
buffer_size: usize,
buffers: Vec<metal::Buffer>,
}
impl Default for InstanceBufferPool {
fn default() -> Self {
Self {
buffer_size: 2 * 1024 * 1024,
buffers: Vec::new(),
}
}
}
pub(crate) struct InstanceBuffer {
metal_buffer: metal::Buffer,
size: usize,
}
impl InstanceBufferPool {
pub(crate) fn reset(&mut self, buffer_size: usize) {
self.buffer_size = buffer_size;
self.buffers.clear();
}
pub(crate) fn acquire(
&mut self,
device: &metal::Device,
unified_memory: bool,
) -> InstanceBuffer {
let buffer = self.buffers.pop().unwrap_or_else(|| {
let options = if unified_memory {
MTLResourceOptions::StorageModeShared
| MTLResourceOptions::CPUCacheModeWriteCombined
} else {
MTLResourceOptions::StorageModeManaged
};
device.new_buffer(self.buffer_size as u64, options)
});
InstanceBuffer {
metal_buffer: buffer,
size: self.buffer_size,
}
}
pub(crate) fn release(&mut self, buffer: InstanceBuffer) {
if buffer.size == self.buffer_size {
self.buffers.push(buffer.metal_buffer)
}
}
}
pub struct MetalRenderer {
device: metal::Device,
layer: Option<metal::MetalLayer>,
is_apple_gpu: bool,
is_unified_memory: bool,
presents_with_transaction: bool,
opaque: bool,
command_queue: CommandQueue,
paths_rasterization_pipeline_state: metal::RenderPipelineState,
path_sprites_pipeline_state: metal::RenderPipelineState,
shadows_pipeline_state: metal::RenderPipelineState,
quads_pipeline_state: metal::RenderPipelineState,
underlines_pipeline_state: metal::RenderPipelineState,
monochrome_sprites_pipeline_state: metal::RenderPipelineState,
polychrome_sprites_pipeline_state: metal::RenderPipelineState,
surfaces_pipeline_state: metal::RenderPipelineState,
unit_vertices: metal::Buffer,
#[allow(clippy::arc_with_non_send_sync)]
instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>,
sprite_atlas: Arc<MetalAtlas>,
core_video_texture_cache: core_video::metal_texture_cache::CVMetalTextureCache,
path_intermediate_texture: Option<metal::Texture>,
path_intermediate_msaa_texture: Option<metal::Texture>,
path_sample_count: u32,
#[cfg(any(test, feature = "bench-support", feature = "test-support"))]
headless_render_target: Option<metal::Texture>,
}
#[repr(C)]
pub struct PathRasterizationVertex {
pub xy_position: Point<ScaledPixels>,
pub st_position: Point<f32>,
pub color: Background,
pub bounds: Bounds<ScaledPixels>,
}
impl MetalRenderer {
pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>, transparent: bool) -> Self {
let device = Self::create_device();
let layer = metal::MetalLayer::new();
layer.set_device(&device);
layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
layer.set_opaque(!transparent);
layer.set_maximum_drawable_count(3);
#[cfg(any(test, feature = "test-support"))]
layer.set_framebuffer_only(false);
unsafe {
let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
let _: () = msg_send![
&*layer,
setAutoresizingMask: AutoresizingMask::WIDTH_SIZABLE
| AutoresizingMask::HEIGHT_SIZABLE
];
}
Self::new_internal(device, Some(layer), !transparent, instance_buffer_pool)
}
#[cfg(any(test, feature = "bench-support", feature = "test-support"))]
pub fn new_headless(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>) -> Self {
let device = Self::create_device();
Self::new_internal(device, None, true, instance_buffer_pool)
}
fn create_device() -> metal::Device {
if let Some(d) = metal::Device::all()
.into_iter()
.min_by_key(|d| (d.is_removable(), !d.is_low_power()))
{
d
} else {
log::error!(
"Unable to enumerate Metal devices; attempting to use system default device"
);
metal::Device::system_default().unwrap_or_else(|| {
log::error!("unable to access a compatible graphics device");
std::process::exit(1);
})
}
}
fn new_internal(
device: metal::Device,
layer: Option<metal::MetalLayer>,
opaque: bool,
instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>,
) -> Self {
#[cfg(feature = "runtime_shaders")]
let library = device
.new_library_with_source(&SHADERS_SOURCE_FILE, &metal::CompileOptions::new())
.expect("error building metal library");
#[cfg(not(feature = "runtime_shaders"))]
let library = device
.new_library_with_data(SHADERS_METALLIB)
.expect("error building metal library");
fn to_float2_bits(point: PointF) -> u64 {
let mut output = point.y.to_bits() as u64;
output <<= 32;
output |= point.x.to_bits() as u64;
output
}
let is_unified_memory = device.has_unified_memory();
let is_apple_gpu = device.supports_family(MTLGPUFamily::Apple1);
let unit_vertices = [
to_float2_bits(point(0., 0.)),
to_float2_bits(point(1., 0.)),
to_float2_bits(point(0., 1.)),
to_float2_bits(point(0., 1.)),
to_float2_bits(point(1., 0.)),
to_float2_bits(point(1., 1.)),
];
let unit_vertices = device.new_buffer_with_data(
unit_vertices.as_ptr() as *const c_void,
mem::size_of_val(&unit_vertices) as u64,
if is_unified_memory {
MTLResourceOptions::StorageModeShared
| MTLResourceOptions::CPUCacheModeWriteCombined
} else {
MTLResourceOptions::StorageModeManaged
},
);
let paths_rasterization_pipeline_state = build_path_rasterization_pipeline_state(
&device,
&library,
"paths_rasterization",
"path_rasterization_vertex",
"path_rasterization_fragment",
MTLPixelFormat::BGRA8Unorm,
PATH_SAMPLE_COUNT,
);
let path_sprites_pipeline_state = build_path_sprite_pipeline_state(
&device,
&library,
"path_sprites",
"path_sprite_vertex",
"path_sprite_fragment",
MTLPixelFormat::BGRA8Unorm,
);
let shadows_pipeline_state = build_pipeline_state(
&device,
&library,
"shadows",
"shadow_vertex",
"shadow_fragment",
MTLPixelFormat::BGRA8Unorm,
);
let quads_pipeline_state = build_pipeline_state(
&device,
&library,
"quads",
"quad_vertex",
"quad_fragment",
MTLPixelFormat::BGRA8Unorm,
);
let underlines_pipeline_state = build_pipeline_state(
&device,
&library,
"underlines",
"underline_vertex",
"underline_fragment",
MTLPixelFormat::BGRA8Unorm,
);
let monochrome_sprites_pipeline_state = build_pipeline_state(
&device,
&library,
"monochrome_sprites",
"monochrome_sprite_vertex",
"monochrome_sprite_fragment",
MTLPixelFormat::BGRA8Unorm,
);
let polychrome_sprites_pipeline_state = build_pipeline_state(
&device,
&library,
"polychrome_sprites",
"polychrome_sprite_vertex",
"polychrome_sprite_fragment",
MTLPixelFormat::BGRA8Unorm,
);
let surfaces_pipeline_state = build_pipeline_state(
&device,
&library,
"surfaces",
"surface_vertex",
"surface_fragment",
MTLPixelFormat::BGRA8Unorm,
);
let command_queue = device.new_command_queue();
let sprite_atlas = Arc::new(MetalAtlas::new(device.clone(), is_apple_gpu));
let core_video_texture_cache =
CVMetalTextureCache::new(None, device.clone(), None).unwrap();
Self {
device,
layer,
presents_with_transaction: false,
is_apple_gpu,
is_unified_memory,
opaque,
command_queue,
paths_rasterization_pipeline_state,
path_sprites_pipeline_state,
shadows_pipeline_state,
quads_pipeline_state,
underlines_pipeline_state,
monochrome_sprites_pipeline_state,
polychrome_sprites_pipeline_state,
surfaces_pipeline_state,
unit_vertices,
instance_buffer_pool,
sprite_atlas,
core_video_texture_cache,
path_intermediate_texture: None,
path_intermediate_msaa_texture: None,
path_sample_count: PATH_SAMPLE_COUNT,
#[cfg(any(test, feature = "bench-support", feature = "test-support"))]
headless_render_target: None,
}
}
pub fn layer(&self) -> Option<&metal::MetalLayerRef> {
self.layer.as_ref().map(|l| l.as_ref())
}
pub fn layer_ptr(&self) -> *mut CAMetalLayer {
self.layer
.as_ref()
.map(|l| l.as_ptr())
.unwrap_or(ptr::null_mut())
}
pub fn sprite_atlas(&self) -> &Arc<MetalAtlas> {
&self.sprite_atlas
}
pub fn set_presents_with_transaction(&mut self, presents_with_transaction: bool) {
self.presents_with_transaction = presents_with_transaction;
if let Some(layer) = &self.layer {
layer.set_presents_with_transaction(presents_with_transaction);
}
}
pub fn update_drawable_size(&mut self, size: Size<DevicePixels>) {
if let Some(layer) = &self.layer {
let ns_size = NSSize {
width: size.width.0 as f64,
height: size.height.0 as f64,
};
unsafe {
let _: () = msg_send![
layer.as_ref(),
setDrawableSize: ns_size
];
}
}
self.update_path_intermediate_textures(size);
}
fn update_path_intermediate_textures(&mut self, size: Size<DevicePixels>) {
if size.width.0 <= 0 || size.height.0 <= 0 {
self.path_intermediate_texture = None;
self.path_intermediate_msaa_texture = None;
return;
}
let texture_descriptor = metal::TextureDescriptor::new();
texture_descriptor.set_width(size.width.0 as u64);
texture_descriptor.set_height(size.height.0 as u64);
texture_descriptor.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm);
texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
texture_descriptor
.set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead);
self.path_intermediate_texture = Some(self.device.new_texture(&texture_descriptor));
if self.path_sample_count > 1 {
let storage_mode = if self.is_apple_gpu {
metal::MTLStorageMode::Memoryless
} else {
metal::MTLStorageMode::Private
};
let msaa_descriptor = texture_descriptor;
msaa_descriptor.set_texture_type(metal::MTLTextureType::D2Multisample);
msaa_descriptor.set_storage_mode(storage_mode);
msaa_descriptor.set_sample_count(self.path_sample_count as _);
self.path_intermediate_msaa_texture = Some(self.device.new_texture(&msaa_descriptor));
} else {
self.path_intermediate_msaa_texture = None;
}
}
pub fn update_transparency(&mut self, transparent: bool) {
self.opaque = !transparent;
if let Some(layer) = &self.layer {
layer.set_opaque(!transparent);
}
}
pub fn destroy(&self) {
}
pub fn draw(&mut self, scene: &Scene) {
let layer = match &self.layer {
Some(l) => l.clone(),
None => {
log::error!(
"draw() called on headless renderer - use render_scene_to_image() instead"
);
return;
}
};
let viewport_size = layer.drawable_size();
let viewport_size: Size<DevicePixels> = size(
(viewport_size.width.ceil() as i32).into(),
(viewport_size.height.ceil() as i32).into(),
);
let drawable = if let Some(drawable) = layer.next_drawable() {
drawable
} else {
log::error!(
"failed to retrieve next drawable, drawable size: {:?}",
viewport_size
);
return;
};
let command_buffer = match self.render_frame(scene, drawable.texture(), viewport_size) {
Ok(command_buffer) => command_buffer,
Err(error) => {
log::error!("failed to render: {error:#}");
return;
}
};
if self.presents_with_transaction {
command_buffer.commit();
command_buffer.wait_until_scheduled();
drawable.present();
} else {
command_buffer.present_drawable(drawable);
command_buffer.commit();
}
}
fn render_frame(
&mut self,
scene: &Scene,
texture: &metal::TextureRef,
viewport_size: Size<DevicePixels>,
) -> Result<metal::CommandBuffer> {
let mut writer = InstanceBufferWriter::new(
&self.device,
&self.instance_buffer_pool,
self.is_unified_memory,
);
let instance_bindings = write_instances(scene, &mut writer).with_context(|| {
format!(
"scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
scene.paths.len(),
scene.shadows.len(),
scene.quads.len(),
scene.underlines.len(),
scene.monochrome_sprites.len(),
scene.polychrome_sprites.len(),
scene.surfaces.len(),
)
})?;
let command_buffer = self.draw_primitives_to_texture(
scene,
&instance_bindings,
&mut writer,
texture,
viewport_size,
)?;
let instance_buffer_pool = self.instance_buffer_pool.clone();
let instance_buffer = Cell::new(Some(writer.finish()));
let block = ConcreteBlock::new(move |_| {
if let Some(instance_buffer) = instance_buffer.take() {
instance_buffer_pool.lock().release(instance_buffer);
}
});
let block = block.copy();
command_buffer.add_completed_handler(&block);
Ok(command_buffer)
}
#[cfg(any(test, feature = "test-support"))]
pub fn render_to_image(&mut self, scene: &Scene) -> Result<RgbaImage> {
let layer = self
.layer
.clone()
.ok_or_else(|| anyhow::anyhow!("render_to_image requires a layer-backed renderer"))?;
let viewport_size = layer.drawable_size();
let viewport_size: Size<DevicePixels> = size(
(viewport_size.width.ceil() as i32).into(),
(viewport_size.height.ceil() as i32).into(),
);
let drawable = layer
.next_drawable()
.ok_or_else(|| anyhow::anyhow!("Failed to get drawable for render_to_image"))?;
let command_buffer = self.render_frame(scene, drawable.texture(), viewport_size)?;
command_buffer.commit();
command_buffer.wait_until_completed();
read_texture_to_image(drawable.texture())
}
#[cfg(any(test, feature = "bench-support", feature = "test-support"))]
pub fn render_scene_to_image(
&mut self,
scene: &Scene,
size: Size<DevicePixels>,
) -> Result<RgbaImage> {
if size.width.0 <= 0 || size.height.0 <= 0 {
anyhow::bail!("Invalid size for render_scene_to_image: {:?}", size);
}
objc2::rc::autoreleasepool(|_| {
self.update_path_intermediate_textures(size);
let texture_descriptor = metal::TextureDescriptor::new();
texture_descriptor.set_width(size.width.0 as u64);
texture_descriptor.set_height(size.height.0 as u64);
texture_descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
texture_descriptor.set_usage(
metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead,
);
texture_descriptor.set_storage_mode(metal::MTLStorageMode::Managed);
let target_texture = self.device.new_texture(&texture_descriptor);
let command_buffer = self.render_frame(scene, &target_texture, size)?;
if !self.is_unified_memory {
let blit = command_buffer.new_blit_command_encoder();
blit.synchronize_resource(&target_texture);
blit.end_encoding();
}
command_buffer.commit();
command_buffer.wait_until_completed();
read_texture_to_image(&target_texture)
})
}
#[cfg(any(test, feature = "bench-support", feature = "test-support"))]
pub fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()> {
if size.width.0 <= 0 || size.height.0 <= 0 {
anyhow::bail!("Invalid size for render_scene: {:?}", size);
}
objc2::rc::autoreleasepool(|_| {
self.update_path_intermediate_textures(size);
let needs_new_target = self.headless_render_target.as_ref().is_none_or(|texture| {
texture.width() != size.width.0 as u64 || texture.height() != size.height.0 as u64
});
if needs_new_target {
let texture_descriptor = metal::TextureDescriptor::new();
texture_descriptor.set_width(size.width.0 as u64);
texture_descriptor.set_height(size.height.0 as u64);
texture_descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
texture_descriptor.set_usage(
metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead,
);
texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
self.headless_render_target = Some(self.device.new_texture(&texture_descriptor));
}
let target_texture = self
.headless_render_target
.clone()
.expect("just ensured the render target exists");
let command_buffer = self.render_frame(scene, &target_texture, size)?;
command_buffer.commit();
Ok(())
})
}
fn draw_primitives_to_texture(
&mut self,
scene: &Scene,
instance_bindings: &InstanceBindings,
writer: &mut InstanceBufferWriter,
texture: &metal::TextureRef,
viewport_size: Size<DevicePixels>,
) -> Result<metal::CommandBuffer> {
let command_queue = self.command_queue.clone();
let command_buffer = command_queue.new_command_buffer();
let alpha = if self.opaque { 1. } else { 0. };
let mut command_encoder = new_command_encoder_for_texture(
command_buffer,
texture,
viewport_size,
Some(metal::MTLClearColor::new(0., 0., 0., alpha)),
);
for batch in scene.batches() {
match batch {
PrimitiveBatch::Shadows(range) => {
self.draw_shadows(range, instance_bindings, viewport_size, command_encoder)
}
PrimitiveBatch::Quads(range) => {
self.draw_quads(range, instance_bindings, viewport_size, command_encoder)
}
PrimitiveBatch::Paths(range) => {
let paths = &scene.paths[range];
command_encoder.end_encoding();
let did_draw = self.draw_paths_to_intermediate(
paths,
writer,
viewport_size,
command_buffer,
)?;
command_encoder = new_command_encoder_for_texture(
command_buffer,
texture,
viewport_size,
None,
);
if did_draw {
if let Err(error) = self.draw_paths_from_intermediate(
paths,
writer,
viewport_size,
command_encoder,
) {
command_encoder.end_encoding();
return Err(error);
}
}
}
PrimitiveBatch::Underlines(range) => {
self.draw_underlines(range, instance_bindings, viewport_size, command_encoder)
}
PrimitiveBatch::MonochromeSprites { texture_id, range } => self
.draw_monochrome_sprites(
texture_id,
range,
instance_bindings,
viewport_size,
command_encoder,
),
PrimitiveBatch::PolychromeSprites { texture_id, range } => self
.draw_polychrome_sprites(
texture_id,
range,
instance_bindings,
viewport_size,
command_encoder,
),
PrimitiveBatch::Surfaces(range) => self.draw_surfaces(
&scene.surfaces[range.clone()],
range.start,
instance_bindings,
viewport_size,
command_encoder,
),
PrimitiveBatch::SubpixelSprites { .. } => unreachable!(),
}
}
command_encoder.end_encoding();
Ok(command_buffer.to_owned())
}
fn draw_paths_to_intermediate(
&self,
paths: &[Path<ScaledPixels>],
writer: &mut InstanceBufferWriter,
viewport_size: Size<DevicePixels>,
command_buffer: &metal::CommandBufferRef,
) -> Result<bool> {
if paths.is_empty() {
return Ok(false);
}
let intermediate_texture = self
.path_intermediate_texture
.as_ref()
.context("missing path intermediate texture")?;
let mut vertices = Vec::new();
for path in paths {
vertices.extend(path.vertices.iter().map(|v| PathRasterizationVertex {
xy_position: v.xy_position,
st_position: v.st_position,
color: path.color,
bounds: path.bounds.intersect(&path.content_mask.bounds),
}));
}
let vertex_instance_bindings = writer.write(&vertices)?;
let render_pass_descriptor = metal::RenderPassDescriptor::new();
let color_attachment = render_pass_descriptor
.color_attachments()
.object_at(0)
.unwrap();
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 0.));
if let Some(msaa_texture) = &self.path_intermediate_msaa_texture {
color_attachment.set_texture(Some(msaa_texture));
color_attachment.set_resolve_texture(Some(intermediate_texture));
color_attachment.set_store_action(metal::MTLStoreAction::MultisampleResolve);
} else {
color_attachment.set_texture(Some(intermediate_texture));
color_attachment.set_store_action(metal::MTLStoreAction::Store);
}
let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state);
command_encoder.set_vertex_buffer(
PathRasterizationInputIndex::Vertices as u64,
Some(&vertex_instance_bindings.buffer),
vertex_instance_bindings.offset as u64,
);
command_encoder.set_vertex_bytes(
PathRasterizationInputIndex::ViewportSize as u64,
mem::size_of_val(&viewport_size) as u64,
&viewport_size as *const Size<DevicePixels> as *const _,
);
command_encoder.set_fragment_buffer(
PathRasterizationInputIndex::Vertices as u64,
Some(&vertex_instance_bindings.buffer),
vertex_instance_bindings.offset as u64,
);
command_encoder.draw_primitives(
metal::MTLPrimitiveType::Triangle,
0,
vertices.len() as u64,
);
command_encoder.end_encoding();
Ok(true)
}
fn draw_shadows(
&self,
shadows: Range<usize>,
instance_bindings: &InstanceBindings,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) {
if shadows.is_empty() {
return;
}
command_encoder.set_render_pipeline_state(&self.shadows_pipeline_state);
command_encoder.set_vertex_buffer(
ShadowInputIndex::Vertices as u64,
Some(&self.unit_vertices),
0,
);
command_encoder.set_vertex_buffer(
ShadowInputIndex::Shadows as u64,
Some(&instance_bindings.shadows.buffer),
instance_bindings.shadows.offset as u64,
);
command_encoder.set_fragment_buffer(
ShadowInputIndex::Shadows as u64,
Some(&instance_bindings.shadows.buffer),
instance_bindings.shadows.offset as u64,
);
command_encoder.set_vertex_bytes(
ShadowInputIndex::ViewportSize as u64,
mem::size_of_val(&viewport_size) as u64,
&viewport_size as *const Size<DevicePixels> as *const _,
);
command_encoder.draw_primitives_instanced_base_instance(
metal::MTLPrimitiveType::Triangle,
0,
6,
shadows.len() as u64,
shadows.start as u64,
);
}
fn draw_quads(
&self,
quads: Range<usize>,
instance_bindings: &InstanceBindings,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) {
if quads.is_empty() {
return;
}
command_encoder.set_render_pipeline_state(&self.quads_pipeline_state);
command_encoder.set_vertex_buffer(
QuadInputIndex::Vertices as u64,
Some(&self.unit_vertices),
0,
);
command_encoder.set_vertex_buffer(
QuadInputIndex::Quads as u64,
Some(&instance_bindings.quads.buffer),
instance_bindings.quads.offset as u64,
);
command_encoder.set_fragment_buffer(
QuadInputIndex::Quads as u64,
Some(&instance_bindings.quads.buffer),
instance_bindings.quads.offset as u64,
);
command_encoder.set_vertex_bytes(
QuadInputIndex::ViewportSize as u64,
mem::size_of_val(&viewport_size) as u64,
&viewport_size as *const Size<DevicePixels> as *const _,
);
command_encoder.draw_primitives_instanced_base_instance(
metal::MTLPrimitiveType::Triangle,
0,
6,
quads.len() as u64,
quads.start as u64,
);
}
fn draw_paths_from_intermediate(
&self,
paths: &[Path<ScaledPixels>],
writer: &mut InstanceBufferWriter,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> Result<()> {
let Some(first_path) = paths.first() else {
return Ok(());
};
let intermediate_texture = self
.path_intermediate_texture
.as_ref()
.context("missing path intermediate texture")?;
command_encoder.set_render_pipeline_state(&self.path_sprites_pipeline_state);
command_encoder.set_vertex_buffer(
SpriteInputIndex::Vertices as u64,
Some(&self.unit_vertices),
0,
);
command_encoder.set_vertex_bytes(
SpriteInputIndex::ViewportSize as u64,
mem::size_of_val(&viewport_size) as u64,
&viewport_size as *const Size<DevicePixels> as *const _,
);
command_encoder.set_fragment_texture(
SpriteInputIndex::AtlasTexture as u64,
Some(intermediate_texture),
);
let sprites;
if paths.last().unwrap().order == first_path.order {
sprites = paths
.iter()
.map(|path| PathSprite {
bounds: path.clipped_bounds(),
})
.collect();
} else {
let mut bounds = first_path.clipped_bounds();
for path in paths.iter().skip(1) {
bounds = bounds.union(&path.clipped_bounds());
}
sprites = vec![PathSprite { bounds }];
}
let sprite_instance_bindings = writer.write(&sprites)?;
command_encoder.set_vertex_buffer(
SpriteInputIndex::Sprites as u64,
Some(&sprite_instance_bindings.buffer),
sprite_instance_bindings.offset as u64,
);
command_encoder.draw_primitives_instanced(
metal::MTLPrimitiveType::Triangle,
0,
6,
sprites.len() as u64,
);
Ok(())
}
fn draw_underlines(
&self,
underlines: Range<usize>,
instance_bindings: &InstanceBindings,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) {
if underlines.is_empty() {
return;
}
command_encoder.set_render_pipeline_state(&self.underlines_pipeline_state);
command_encoder.set_vertex_buffer(
UnderlineInputIndex::Vertices as u64,
Some(&self.unit_vertices),
0,
);
command_encoder.set_vertex_buffer(
UnderlineInputIndex::Underlines as u64,
Some(&instance_bindings.underlines.buffer),
instance_bindings.underlines.offset as u64,
);
command_encoder.set_fragment_buffer(
UnderlineInputIndex::Underlines as u64,
Some(&instance_bindings.underlines.buffer),
instance_bindings.underlines.offset as u64,
);
command_encoder.set_vertex_bytes(
UnderlineInputIndex::ViewportSize as u64,
mem::size_of_val(&viewport_size) as u64,
&viewport_size as *const Size<DevicePixels> as *const _,
);
command_encoder.draw_primitives_instanced_base_instance(
metal::MTLPrimitiveType::Triangle,
0,
6,
underlines.len() as u64,
underlines.start as u64,
);
}
fn draw_monochrome_sprites(
&self,
texture_id: AtlasTextureId,
sprites: Range<usize>,
instance_bindings: &InstanceBindings,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) {
if sprites.is_empty() {
return;
}
let texture = self.sprite_atlas.metal_texture(texture_id);
let texture_size = size(
DevicePixels(texture.width() as i32),
DevicePixels(texture.height() as i32),
);
command_encoder.set_render_pipeline_state(&self.monochrome_sprites_pipeline_state);
command_encoder.set_vertex_buffer(
SpriteInputIndex::Vertices as u64,
Some(&self.unit_vertices),
0,
);
command_encoder.set_vertex_buffer(
SpriteInputIndex::Sprites as u64,
Some(&instance_bindings.monochrome_sprites.buffer),
instance_bindings.monochrome_sprites.offset as u64,
);
command_encoder.set_vertex_bytes(
SpriteInputIndex::ViewportSize as u64,
mem::size_of_val(&viewport_size) as u64,
&viewport_size as *const Size<DevicePixels> as *const _,
);
command_encoder.set_vertex_bytes(
SpriteInputIndex::AtlasTextureSize as u64,
mem::size_of_val(&texture_size) as u64,
&texture_size as *const Size<DevicePixels> as *const _,
);
command_encoder.set_fragment_buffer(
SpriteInputIndex::Sprites as u64,
Some(&instance_bindings.monochrome_sprites.buffer),
instance_bindings.monochrome_sprites.offset as u64,
);
command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
command_encoder.draw_primitives_instanced_base_instance(
metal::MTLPrimitiveType::Triangle,
0,
6,
sprites.len() as u64,
sprites.start as u64,
);
}
fn draw_polychrome_sprites(
&self,
texture_id: AtlasTextureId,
sprites: Range<usize>,
instance_bindings: &InstanceBindings,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) {
if sprites.is_empty() {
return;
}
let texture = self.sprite_atlas.metal_texture(texture_id);
let texture_size = size(
DevicePixels(texture.width() as i32),
DevicePixels(texture.height() as i32),
);
command_encoder.set_render_pipeline_state(&self.polychrome_sprites_pipeline_state);
command_encoder.set_vertex_buffer(
SpriteInputIndex::Vertices as u64,
Some(&self.unit_vertices),
0,
);
command_encoder.set_vertex_buffer(
SpriteInputIndex::Sprites as u64,
Some(&instance_bindings.polychrome_sprites.buffer),
instance_bindings.polychrome_sprites.offset as u64,
);
command_encoder.set_vertex_bytes(
SpriteInputIndex::ViewportSize as u64,
mem::size_of_val(&viewport_size) as u64,
&viewport_size as *const Size<DevicePixels> as *const _,
);
command_encoder.set_vertex_bytes(
SpriteInputIndex::AtlasTextureSize as u64,
mem::size_of_val(&texture_size) as u64,
&texture_size as *const Size<DevicePixels> as *const _,
);
command_encoder.set_fragment_buffer(
SpriteInputIndex::Sprites as u64,
Some(&instance_bindings.polychrome_sprites.buffer),
instance_bindings.polychrome_sprites.offset as u64,
);
command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
command_encoder.draw_primitives_instanced_base_instance(
metal::MTLPrimitiveType::Triangle,
0,
6,
sprites.len() as u64,
sprites.start as u64,
);
}
fn draw_surfaces(
&mut self,
surfaces: &[PaintSurface],
first_surface: usize,
instance_bindings: &InstanceBindings,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) {
if surfaces.is_empty() {
return;
}
command_encoder.set_render_pipeline_state(&self.surfaces_pipeline_state);
command_encoder.set_vertex_buffer(
SurfaceInputIndex::Vertices as u64,
Some(&self.unit_vertices),
0,
);
command_encoder.set_vertex_buffer(
SurfaceInputIndex::Surfaces as u64,
Some(&instance_bindings.surfaces.buffer),
instance_bindings.surfaces.offset as u64,
);
command_encoder.set_vertex_bytes(
SurfaceInputIndex::ViewportSize as u64,
mem::size_of_val(&viewport_size) as u64,
&viewport_size as *const Size<DevicePixels> as *const _,
);
for (index, surface) in surfaces.iter().enumerate() {
let texture_size = size(
DevicePixels::from(surface.image_buffer.get_width() as i32),
DevicePixels::from(surface.image_buffer.get_height() as i32),
);
assert_eq!(
surface.image_buffer.get_pixel_format(),
kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
);
let y_texture = self
.core_video_texture_cache
.create_texture_from_image(
surface.image_buffer.as_concrete_TypeRef(),
None,
MTLPixelFormat::R8Unorm,
surface.image_buffer.get_width_of_plane(0),
surface.image_buffer.get_height_of_plane(0),
0,
)
.unwrap();
let cb_cr_texture = self
.core_video_texture_cache
.create_texture_from_image(
surface.image_buffer.as_concrete_TypeRef(),
None,
MTLPixelFormat::RG8Unorm,
surface.image_buffer.get_width_of_plane(1),
surface.image_buffer.get_height_of_plane(1),
1,
)
.unwrap();
command_encoder.set_vertex_bytes(
SurfaceInputIndex::TextureSize as u64,
mem::size_of_val(&texture_size) as u64,
&texture_size as *const Size<DevicePixels> as *const _,
);
command_encoder.set_fragment_texture(SurfaceInputIndex::YTexture as u64, unsafe {
let texture = CVMetalTextureGetTexture(y_texture.as_concrete_TypeRef());
Some(metal::TextureRef::from_ptr(texture as *mut _))
});
command_encoder.set_fragment_texture(SurfaceInputIndex::CbCrTexture as u64, unsafe {
let texture = CVMetalTextureGetTexture(cb_cr_texture.as_concrete_TypeRef());
Some(metal::TextureRef::from_ptr(texture as *mut _))
});
command_encoder.draw_primitives_instanced_base_instance(
metal::MTLPrimitiveType::Triangle,
0,
6,
1,
(first_surface + index) as u64,
);
}
}
}
fn new_command_encoder_for_texture<'a>(
command_buffer: &'a metal::CommandBufferRef,
texture: &'a metal::TextureRef,
viewport_size: Size<DevicePixels>,
clear_color: Option<metal::MTLClearColor>,
) -> &'a metal::RenderCommandEncoderRef {
let render_pass_descriptor = metal::RenderPassDescriptor::new();
let color_attachment = render_pass_descriptor
.color_attachments()
.object_at(0)
.unwrap();
color_attachment.set_texture(Some(texture));
color_attachment.set_store_action(metal::MTLStoreAction::Store);
if let Some(clear_color) = clear_color {
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
color_attachment.set_clear_color(clear_color);
} else {
color_attachment.set_load_action(metal::MTLLoadAction::Load);
}
let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
command_encoder.set_viewport(metal::MTLViewport {
originX: 0.0,
originY: 0.0,
width: i32::from(viewport_size.width) as f64,
height: i32::from(viewport_size.height) as f64,
znear: 0.0,
zfar: 1.0,
});
command_encoder
}
#[cfg(any(test, feature = "bench-support", feature = "test-support"))]
fn read_texture_to_image(texture: &metal::TextureRef) -> Result<RgbaImage> {
let width = texture.width() as u32;
let height = texture.height() as u32;
let bytes_per_row = width as usize * 4;
let mut pixels = vec![0u8; height as usize * bytes_per_row];
let region = metal::MTLRegion {
origin: metal::MTLOrigin { x: 0, y: 0, z: 0 },
size: metal::MTLSize {
width: width as u64,
height: height as u64,
depth: 1,
},
};
texture.get_bytes(
pixels.as_mut_ptr() as *mut std::ffi::c_void,
bytes_per_row as u64,
region,
0,
);
for chunk in pixels.chunks_exact_mut(4) {
chunk.swap(0, 2);
}
RgbaImage::from_raw(width, height, pixels).context("failed to create RgbaImage from pixel data")
}
fn build_pipeline_state(
device: &metal::DeviceRef,
library: &metal::LibraryRef,
label: &str,
vertex_fn_name: &str,
fragment_fn_name: &str,
pixel_format: metal::MTLPixelFormat,
) -> metal::RenderPipelineState {
let vertex_fn = library
.get_function(vertex_fn_name, None)
.expect("error locating vertex function");
let fragment_fn = library
.get_function(fragment_fn_name, None)
.expect("error locating fragment function");
let descriptor = metal::RenderPipelineDescriptor::new();
descriptor.set_label(label);
descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
color_attachment.set_pixel_format(pixel_format);
color_attachment.set_blending_enabled(true);
color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add);
color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add);
color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::SourceAlpha);
color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One);
device
.new_render_pipeline_state(&descriptor)
.expect("could not create render pipeline state")
}
fn build_path_sprite_pipeline_state(
device: &metal::DeviceRef,
library: &metal::LibraryRef,
label: &str,
vertex_fn_name: &str,
fragment_fn_name: &str,
pixel_format: metal::MTLPixelFormat,
) -> metal::RenderPipelineState {
let vertex_fn = library
.get_function(vertex_fn_name, None)
.expect("error locating vertex function");
let fragment_fn = library
.get_function(fragment_fn_name, None)
.expect("error locating fragment function");
let descriptor = metal::RenderPipelineDescriptor::new();
descriptor.set_label(label);
descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
color_attachment.set_pixel_format(pixel_format);
color_attachment.set_blending_enabled(true);
color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add);
color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add);
color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::One);
color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One);
device
.new_render_pipeline_state(&descriptor)
.expect("could not create render pipeline state")
}
fn build_path_rasterization_pipeline_state(
device: &metal::DeviceRef,
library: &metal::LibraryRef,
label: &str,
vertex_fn_name: &str,
fragment_fn_name: &str,
pixel_format: metal::MTLPixelFormat,
path_sample_count: u32,
) -> metal::RenderPipelineState {
let vertex_fn = library
.get_function(vertex_fn_name, None)
.expect("error locating vertex function");
let fragment_fn = library
.get_function(fragment_fn_name, None)
.expect("error locating fragment function");
let descriptor = metal::RenderPipelineDescriptor::new();
descriptor.set_label(label);
descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
if path_sample_count > 1 {
descriptor.set_raster_sample_count(path_sample_count as _);
descriptor.set_alpha_to_coverage_enabled(false);
}
let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
color_attachment.set_pixel_format(pixel_format);
color_attachment.set_blending_enabled(true);
color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add);
color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add);
color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::One);
color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
device
.new_render_pipeline_state(&descriptor)
.expect("could not create render pipeline state")
}
#[derive(Clone)]
struct InstanceBinding {
buffer: metal::Buffer,
offset: usize,
}
struct InstanceBindings {
quads: InstanceBinding,
shadows: InstanceBinding,
underlines: InstanceBinding,
monochrome_sprites: InstanceBinding,
polychrome_sprites: InstanceBinding,
surfaces: InstanceBinding,
}
fn write_instances(scene: &Scene, writer: &mut InstanceBufferWriter) -> Result<InstanceBindings> {
Ok(InstanceBindings {
quads: writer.write(&scene.quads)?,
shadows: writer.write(&scene.shadows)?,
underlines: writer.write(&scene.underlines)?,
monochrome_sprites: writer.write(&scene.monochrome_sprites)?,
polychrome_sprites: writer.write(&scene.polychrome_sprites)?,
surfaces: writer.write_iter(scene.surfaces.iter().map(|surface| SurfaceBounds {
bounds: surface.bounds,
content_mask: surface.content_mask,
}))?,
})
}
struct InstanceBufferWriter {
device: metal::Device,
pool: Arc<Mutex<InstanceBufferPool>>,
unified_memory: bool,
filled: Vec<(InstanceBuffer, usize)>,
current: InstanceBuffer,
offset: usize,
}
impl InstanceBufferWriter {
fn new(
device: &metal::Device,
pool: &Arc<Mutex<InstanceBufferPool>>,
unified_memory: bool,
) -> Self {
let current = pool.lock().acquire(device, unified_memory);
Self {
device: device.clone(),
pool: pool.clone(),
unified_memory,
filled: Vec::new(),
current,
offset: 0,
}
}
fn allocate<T>(&mut self, count: usize) -> Result<(InstanceBinding, &mut [MaybeUninit<T>])> {
let size = mem::size_of::<T>() * count;
let mut offset = self.offset.next_multiple_of(INSTANCE_BUFFER_ALIGNMENT);
if offset + size > self.current.size {
self.grow(size)?;
offset = 0;
}
self.offset = offset + size;
let binding = InstanceBinding {
buffer: self.current.metal_buffer.clone(),
offset,
};
let values = unsafe {
let start = (self.current.metal_buffer.contents() as *mut u8).add(offset);
slice::from_raw_parts_mut(start.cast::<MaybeUninit<T>>(), count)
};
Ok((binding, values))
}
fn write<T>(&mut self, values: &[T]) -> Result<InstanceBinding> {
let (binding, destination) = self.allocate::<T>(values.len())?;
unsafe {
ptr::copy_nonoverlapping(
values.as_ptr(),
destination.as_mut_ptr().cast::<T>(),
values.len(),
);
}
Ok(binding)
}
fn write_iter<T>(
&mut self,
values: impl ExactSizeIterator<Item = T>,
) -> Result<InstanceBinding> {
let (binding, destination) = self.allocate::<T>(values.len())?;
for (slot, value) in destination.iter_mut().zip(values) {
slot.write(value);
}
Ok(binding)
}
fn grow(&mut self, required: usize) -> Result<()> {
let mut pool = self.pool.lock();
let buffer_size = (pool.buffer_size * 2)
.max(required.next_power_of_two())
.min(MAX_INSTANCE_BUFFER_SIZE);
anyhow::ensure!(
buffer_size >= required,
"instance buffer needs {required} bytes, above the maximum of {MAX_INSTANCE_BUFFER_SIZE}"
);
anyhow::ensure!(
buffer_size > self.current.size,
"frame instance data exceeds the {MAX_INSTANCE_BUFFER_SIZE}-byte maximum"
);
if buffer_size != pool.buffer_size {
log::info!("increased instance buffer size to {buffer_size}");
pool.reset(buffer_size);
}
let buffer = pool.acquire(&self.device, self.unified_memory);
drop(pool);
let filled = mem::replace(&mut self.current, buffer);
self.filled.push((filled, self.offset));
self.offset = 0;
Ok(())
}
fn finish(self) -> InstanceBuffer {
let Self {
unified_memory,
filled,
current,
offset,
..
} = self;
if !unified_memory {
for (buffer, written) in &filled {
if *written == 0 {
continue;
}
buffer.metal_buffer.did_modify_range(NSRange {
location: 0,
length: *written as NSUInteger,
});
}
if offset > 0 {
current.metal_buffer.did_modify_range(NSRange {
location: 0,
length: offset as NSUInteger,
});
}
}
drop(filled);
current
}
}
#[repr(C)]
enum ShadowInputIndex {
Vertices = 0,
Shadows = 1,
ViewportSize = 2,
}
#[repr(C)]
enum QuadInputIndex {
Vertices = 0,
Quads = 1,
ViewportSize = 2,
}
#[repr(C)]
enum UnderlineInputIndex {
Vertices = 0,
Underlines = 1,
ViewportSize = 2,
}
#[repr(C)]
enum SpriteInputIndex {
Vertices = 0,
Sprites = 1,
ViewportSize = 2,
AtlasTextureSize = 3,
AtlasTexture = 4,
}
#[repr(C)]
enum SurfaceInputIndex {
Vertices = 0,
Surfaces = 1,
ViewportSize = 2,
TextureSize = 3,
YTexture = 4,
CbCrTexture = 5,
}
#[repr(C)]
enum PathRasterizationInputIndex {
Vertices = 0,
ViewportSize = 1,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[repr(C)]
pub struct PathSprite {
pub bounds: Bounds<ScaledPixels>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[repr(C)]
pub struct SurfaceBounds {
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
}
#[cfg(any(test, feature = "bench-support", feature = "test-support"))]
pub struct MetalHeadlessRenderer {
renderer: MetalRenderer,
}
#[cfg(any(test, feature = "bench-support", feature = "test-support"))]
impl MetalHeadlessRenderer {
pub fn new() -> Self {
let instance_buffer_pool = Arc::new(Mutex::new(InstanceBufferPool::default()));
let renderer = MetalRenderer::new_headless(instance_buffer_pool);
Self { renderer }
}
}
#[cfg(any(test, feature = "bench-support", feature = "test-support"))]
impl gpui::PlatformHeadlessRenderer for MetalHeadlessRenderer {
fn render_scene_to_image(
&mut self,
scene: &Scene,
size: Size<DevicePixels>,
) -> anyhow::Result<image::RgbaImage> {
self.renderer.render_scene_to_image(scene, size)
}
fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> anyhow::Result<()> {
self.renderer.render_scene(scene, size)
}
fn sprite_atlas(&self) -> Arc<dyn gpui::PlatformAtlas> {
self.renderer.sprite_atlas().clone()
}
}