use metal::{
Buffer, CommandQueue, CompileOptions, Device, MTLBlendFactor, MTLBlendOperation,
MTLPixelFormat, MTLPrimitiveType, MTLRegion, MTLResourceOptions, MTLTextureUsage,
MTLVertexFormat, MTLVertexStepFunction, RenderCommandEncoderRef,
RenderPipelineDescriptor, RenderPipelineState, Texture, TextureDescriptor,
VertexDescriptor,
};
use parking_lot::{Condvar, Mutex};
use rustc_hash::FxHashMap;
use std::sync::Arc;
use super::atlas::{AtlasSlot, GlyphKey, RasterizedGlyph};
use super::cell::{CellBg, CellText, GridUniforms};
use crate::context::metal::MetalContext;
use crate::renderer::image_cache::atlas::AtlasAllocator;
const FRAMES_IN_FLIGHT: usize = 3;
pub const FRAMES_IN_FLIGHT_PUB: usize = FRAMES_IN_FLIGHT;
pub type FramePermits = Arc<(Mutex<usize>, Condvar)>;
pub fn new_frame_permits() -> FramePermits {
Arc::new((Mutex::new(FRAMES_IN_FLIGHT), Condvar::new()))
}
pub fn acquire_frame_permit(p: &FramePermits) {
let (m, c) = &**p;
let mut g = m.lock();
while *g == 0 {
c.wait(&mut g);
}
*g -= 1;
}
pub fn release_frame_permit(p: &FramePermits) {
let (m, c) = &**p;
let mut g = m.lock();
*g += 1;
c.notify_one();
}
const CURSOR_ROW_SLOTS: usize = 2;
const ATLAS_SIZE: u16 = 2048;
const ATLAS_MAX_SIZE: u16 = 8192;
pub struct MetalGlyphAtlas {
pub(crate) texture: Texture,
allocator: AtlasAllocator,
slots: FxHashMap<GlyphKey, AtlasSlot>,
bytes_per_pixel: u32,
format: MTLPixelFormat,
label: &'static str,
}
impl MetalGlyphAtlas {
pub fn new_grayscale(device: &Device) -> Self {
Self::new(device, MTLPixelFormat::R8Unorm, 1, "grid.atlas_grayscale")
}
pub fn new_color(device: &Device) -> Self {
Self::new(device, MTLPixelFormat::RGBA8Unorm, 4, "grid.atlas_color")
}
fn new(
device: &Device,
format: MTLPixelFormat,
bytes_per_pixel: u32,
label: &'static str,
) -> Self {
let texture = create_atlas_texture(device, format, ATLAS_SIZE, label);
Self {
texture,
allocator: AtlasAllocator::new(ATLAS_SIZE, ATLAS_SIZE),
slots: FxHashMap::default(),
bytes_per_pixel,
format,
label,
}
}
pub fn grow(&mut self, device: &Device, queue: &CommandQueue) -> bool {
let (old_w, old_h) = self.allocator.dimensions();
if old_w >= ATLAS_MAX_SIZE {
return false;
}
let new_size = old_w.saturating_mul(2).min(ATLAS_MAX_SIZE);
if new_size <= old_w {
return false;
}
let new_texture = create_atlas_texture(device, self.format, new_size, self.label);
let cmd_buffer = queue.new_command_buffer();
let blit = cmd_buffer.new_blit_command_encoder();
blit.copy_from_texture(
&self.texture,
0,
0,
metal::MTLOrigin { x: 0, y: 0, z: 0 },
metal::MTLSize {
width: old_w as u64,
height: old_h as u64,
depth: 1,
},
&new_texture,
0,
0,
metal::MTLOrigin { x: 0, y: 0, z: 0 },
);
blit.end_encoding();
cmd_buffer.commit();
cmd_buffer.wait_until_completed();
self.texture = new_texture;
self.allocator.grow_to(new_size, new_size);
true
}
#[inline]
pub fn lookup(&self, key: GlyphKey) -> Option<AtlasSlot> {
self.slots.get(&key).copied()
}
pub fn insert(
&mut self,
key: GlyphKey,
glyph: RasterizedGlyph<'_>,
) -> Option<AtlasSlot> {
if glyph.width == 0 || glyph.height == 0 {
let slot = AtlasSlot {
x: 0,
y: 0,
w: 0,
h: 0,
bearing_x: glyph.bearing_x,
bearing_y: glyph.bearing_y,
};
self.slots.insert(key, slot);
return Some(slot);
}
let (x, y) = self.allocator.allocate(glyph.width, glyph.height)?;
let slot = AtlasSlot {
x,
y,
w: glyph.width,
h: glyph.height,
bearing_x: glyph.bearing_x,
bearing_y: glyph.bearing_y,
};
self.slots.insert(key, slot);
let region = MTLRegion {
origin: metal::MTLOrigin {
x: x as u64,
y: y as u64,
z: 0,
},
size: metal::MTLSize {
width: glyph.width as u64,
height: glyph.height as u64,
depth: 1,
},
};
self.texture.replace_region(
region,
0,
glyph.bytes.as_ptr() as *const std::ffi::c_void,
(glyph.width as u64) * (self.bytes_per_pixel as u64),
);
Some(slot)
}
#[allow(dead_code)]
pub fn clear(&mut self) {
self.allocator.clear();
self.slots.clear();
}
}
fn create_atlas_texture(
device: &Device,
format: MTLPixelFormat,
size: u16,
label: &str,
) -> Texture {
let descriptor = TextureDescriptor::new();
descriptor.set_width(size as u64);
descriptor.set_height(size as u64);
descriptor.set_pixel_format(format);
descriptor.set_storage_mode(if device.has_unified_memory() {
metal::MTLStorageMode::Shared
} else {
metal::MTLStorageMode::Managed
});
descriptor.set_usage(MTLTextureUsage::ShaderRead);
let texture = device.new_texture(&descriptor);
texture.set_label(label);
texture
}
pub struct MetalGridRenderer {
device: Device,
command_queue: CommandQueue,
cols: u32,
rows: u32,
bg_cpu: Vec<CellBg>,
bg_buffers: [Buffer; FRAMES_IN_FLIGHT],
bg_dirty: [bool; FRAMES_IN_FLIGHT],
fg_rows: Vec<Vec<CellText>>,
fg_buffers: [Buffer; FRAMES_IN_FLIGHT],
fg_capacity: [usize; FRAMES_IN_FLIGHT],
bg_pipeline: RenderPipelineState,
text_pipeline: RenderPipelineState,
fg_staging: Vec<CellText>,
fg_live_count: [u32; FRAMES_IN_FLIGHT],
fg_dirty: [bool; FRAMES_IN_FLIGHT],
atlas_grayscale: MetalGlyphAtlas,
atlas_color: MetalGlyphAtlas,
needs_full_rebuild: bool,
}
impl MetalGridRenderer {
pub fn new(ctx: &MetalContext, cols: u32, rows: u32) -> Self {
let device = ctx.device.to_owned();
let command_queue = ctx.command_queue.to_owned();
let bg_buffers = std::array::from_fn(|_| alloc_bg_buffer(&device, cols, rows));
let initial_fg_capacity = (cols as usize) * (rows as usize).max(1);
let fg_buffers =
std::array::from_fn(|_| alloc_fg_buffer(&device, initial_fg_capacity));
let fg_capacity = [initial_fg_capacity; FRAMES_IN_FLIGHT];
let bg_pipeline = build_bg_pipeline(&device);
let text_pipeline = build_text_pipeline(&device);
let atlas_grayscale = MetalGlyphAtlas::new_grayscale(&device);
let atlas_color = MetalGlyphAtlas::new_color(&device);
let bg_cpu_len = (cols as usize) * (rows as usize);
let bg_cpu = vec![CellBg::TRANSPARENT; bg_cpu_len];
Self {
device,
command_queue,
cols,
rows,
bg_cpu,
bg_buffers,
bg_dirty: [true; FRAMES_IN_FLIGHT],
fg_rows: init_fg_rows(rows),
fg_buffers,
fg_capacity,
bg_pipeline,
text_pipeline,
fg_staging: Vec::new(),
fg_live_count: [0; FRAMES_IN_FLIGHT],
fg_dirty: [true; FRAMES_IN_FLIGHT],
atlas_grayscale,
atlas_color,
needs_full_rebuild: true,
}
}
#[inline]
pub fn needs_full_rebuild(&self) -> bool {
self.needs_full_rebuild
}
#[inline]
pub fn mark_full_rebuild_done(&mut self) {
self.needs_full_rebuild = false;
}
pub fn lookup_glyph(&self, key: GlyphKey) -> Option<AtlasSlot> {
self.atlas_grayscale.lookup(key)
}
pub fn insert_glyph(
&mut self,
key: GlyphKey,
glyph: RasterizedGlyph<'_>,
) -> Option<AtlasSlot> {
if let Some(slot) = self.atlas_grayscale.insert(key, glyph) {
return Some(slot);
}
if self.atlas_grayscale.grow(&self.device, &self.command_queue) {
self.atlas_grayscale.insert(key, glyph)
} else {
None
}
}
pub fn lookup_glyph_color(&self, key: GlyphKey) -> Option<AtlasSlot> {
self.atlas_color.lookup(key)
}
pub fn insert_glyph_color(
&mut self,
key: GlyphKey,
glyph: RasterizedGlyph<'_>,
) -> Option<AtlasSlot> {
if let Some(slot) = self.atlas_color.insert(key, glyph) {
return Some(slot);
}
if self.atlas_color.grow(&self.device, &self.command_queue) {
self.atlas_color.insert(key, glyph)
} else {
None
}
}
pub fn resize(&mut self, cols: u32, rows: u32) {
if cols == self.cols && rows == self.rows {
return;
}
self.cols = cols;
self.rows = rows;
self.bg_cpu = vec![CellBg::TRANSPARENT; (cols as usize) * (rows as usize)];
self.bg_buffers =
std::array::from_fn(|_| alloc_bg_buffer(&self.device, cols, rows));
self.fg_rows = init_fg_rows(rows);
let initial_fg_capacity = (cols as usize) * (rows as usize).max(1);
self.fg_buffers =
std::array::from_fn(|_| alloc_fg_buffer(&self.device, initial_fg_capacity));
self.fg_capacity = [initial_fg_capacity; FRAMES_IN_FLIGHT];
self.needs_full_rebuild = true;
self.bg_dirty = [true; FRAMES_IN_FLIGHT];
self.fg_dirty = [true; FRAMES_IN_FLIGHT];
self.fg_live_count = [0; FRAMES_IN_FLIGHT];
}
pub fn write_row(&mut self, row: u32, bg: &[CellBg], fg: &[CellText]) {
let idx = (row as usize) + 1;
if let Some(slot) = self.fg_rows.get_mut(idx) {
slot.clear();
slot.extend_from_slice(fg);
self.fg_dirty = [true; FRAMES_IN_FLIGHT];
}
if row >= self.rows {
return;
}
let row_start = (row as usize) * (self.cols as usize);
let row_len = (self.cols as usize).min(bg.len());
let dst = &mut self.bg_cpu[row_start..row_start + self.cols as usize];
dst[..row_len].copy_from_slice(&bg[..row_len]);
for slot in &mut dst[row_len..] {
*slot = CellBg::TRANSPARENT;
}
self.bg_dirty = [true; FRAMES_IN_FLIGHT];
}
pub fn clear_row(&mut self, row: u32) {
let idx = (row as usize) + 1;
if let Some(slot) = self.fg_rows.get_mut(idx) {
if !slot.is_empty() {
self.fg_dirty = [true; FRAMES_IN_FLIGHT];
}
slot.clear();
}
if row >= self.rows {
return;
}
let row_start = (row as usize) * (self.cols as usize);
let dst = &mut self.bg_cpu[row_start..row_start + self.cols as usize];
let needs_flush = dst.iter().any(|c| *c != CellBg::TRANSPARENT);
for slot in dst {
*slot = CellBg::TRANSPARENT;
}
if needs_flush {
self.bg_dirty = [true; FRAMES_IN_FLIGHT];
}
}
pub fn set_block_cursor(&mut self, cells: &[CellText]) {
if let Some(slot) = self.fg_rows.first_mut() {
if slot.is_empty() && cells.is_empty() {
return;
}
slot.clear();
slot.extend_from_slice(cells);
self.fg_dirty = [true; FRAMES_IN_FLIGHT];
}
}
pub fn set_non_block_cursor(&mut self, cells: &[CellText]) {
let idx = self.fg_rows.len().saturating_sub(1);
if let Some(slot) = self.fg_rows.get_mut(idx) {
if slot.is_empty() && cells.is_empty() {
return;
}
slot.clear();
slot.extend_from_slice(cells);
self.fg_dirty = [true; FRAMES_IN_FLIGHT];
}
}
pub fn clear_cursor(&mut self) {
let mut changed = false;
if let Some(slot) = self.fg_rows.first_mut() {
if !slot.is_empty() {
slot.clear();
changed = true;
}
}
let last = self.fg_rows.len().saturating_sub(1);
if last > 0 {
if let Some(slot) = self.fg_rows.get_mut(last) {
if !slot.is_empty() {
slot.clear();
changed = true;
}
}
}
if changed {
self.fg_dirty = [true; FRAMES_IN_FLIGHT];
}
}
pub fn render_bg(
&mut self,
encoder: &RenderCommandEncoderRef,
frame: usize,
uniforms: &GridUniforms,
) {
if self.bg_dirty[frame] {
let bytes = bytemuck::cast_slice::<CellBg, u8>(&self.bg_cpu);
unsafe {
let dst = self.bg_buffers[frame].contents() as *mut u8;
std::ptr::copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len());
}
self.bg_dirty[frame] = false;
}
let uniforms_bytes = bytemuck::bytes_of(uniforms);
encoder.set_render_pipeline_state(&self.bg_pipeline);
encoder.set_vertex_bytes(
0,
uniforms_bytes.len() as u64,
uniforms_bytes.as_ptr() as *const std::ffi::c_void,
);
encoder.set_fragment_bytes(
0,
uniforms_bytes.len() as u64,
uniforms_bytes.as_ptr() as *const std::ffi::c_void,
);
encoder.set_fragment_buffer(1, Some(&self.bg_buffers[frame]), 0);
encoder.draw_primitives(MTLPrimitiveType::Triangle, 0, 3);
}
pub fn render_text(
&mut self,
encoder: &RenderCommandEncoderRef,
frame: usize,
uniforms: &GridUniforms,
) {
if self.fg_dirty[frame] {
self.fg_staging.clear();
for row in &self.fg_rows {
self.fg_staging.extend_from_slice(row);
}
if self.fg_staging.len() > self.fg_capacity[frame] {
let new_cap = self.fg_staging.len().next_power_of_two();
self.fg_buffers[frame] = alloc_fg_buffer(&self.device, new_cap);
self.fg_capacity[frame] = new_cap;
}
let fg_bytes = bytemuck::cast_slice::<CellText, u8>(&self.fg_staging);
unsafe {
let dst = self.fg_buffers[frame].contents() as *mut u8;
std::ptr::copy_nonoverlapping(fg_bytes.as_ptr(), dst, fg_bytes.len());
}
self.fg_live_count[frame] = self.fg_staging.len() as u32;
self.fg_dirty[frame] = false;
}
let instance_count = self.fg_live_count[frame] as usize;
if instance_count == 0 {
return;
}
let uniforms_bytes = bytemuck::bytes_of(uniforms);
encoder.set_render_pipeline_state(&self.text_pipeline);
encoder.set_vertex_buffer(0, Some(&self.fg_buffers[frame]), 0);
encoder.set_vertex_bytes(
1,
uniforms_bytes.len() as u64,
uniforms_bytes.as_ptr() as *const std::ffi::c_void,
);
encoder.set_fragment_texture(0, Some(&self.atlas_grayscale.texture));
encoder.set_fragment_texture(1, Some(&self.atlas_color.texture));
encoder.draw_primitives_instanced(
MTLPrimitiveType::TriangleStrip,
0,
4,
instance_count as u64,
);
}
}
fn build_text_pipeline(device: &Device) -> RenderPipelineState {
let shader_source = include_str!("shaders/grid.metal");
let library = device
.new_library_with_source(shader_source, &CompileOptions::new())
.expect("grid.metal failed to compile (text)");
let vertex_fn = library
.get_function("grid_text_vertex", None)
.expect("grid_text_vertex not found");
let fragment_fn = library
.get_function("grid_text_fragment", None)
.expect("grid_text_fragment not found");
let vd = VertexDescriptor::new();
let attrs = vd.attributes();
let a = attrs.object_at(0).unwrap();
a.set_format(MTLVertexFormat::UInt2);
a.set_buffer_index(0);
a.set_offset(0);
let a = attrs.object_at(1).unwrap();
a.set_format(MTLVertexFormat::UInt2);
a.set_buffer_index(0);
a.set_offset(8);
let a = attrs.object_at(2).unwrap();
a.set_format(MTLVertexFormat::Short2);
a.set_buffer_index(0);
a.set_offset(16);
let a = attrs.object_at(3).unwrap();
a.set_format(MTLVertexFormat::UShort2);
a.set_buffer_index(0);
a.set_offset(20);
let a = attrs.object_at(4).unwrap();
a.set_format(MTLVertexFormat::UChar4);
a.set_buffer_index(0);
a.set_offset(24);
let a = attrs.object_at(5).unwrap();
a.set_format(MTLVertexFormat::UChar);
a.set_buffer_index(0);
a.set_offset(28);
let a = attrs.object_at(6).unwrap();
a.set_format(MTLVertexFormat::UChar);
a.set_buffer_index(0);
a.set_offset(29);
let layout = vd.layouts().object_at(0).unwrap();
layout.set_stride(std::mem::size_of::<CellText>() as u64);
layout.set_step_function(MTLVertexStepFunction::PerInstance);
layout.set_step_rate(1);
let descriptor = RenderPipelineDescriptor::new();
descriptor.set_label("grid.text");
descriptor.set_vertex_function(Some(&vertex_fn));
descriptor.set_fragment_function(Some(&fragment_fn));
descriptor.set_vertex_descriptor(Some(vd));
let color = descriptor
.color_attachments()
.object_at(0)
.expect("color attachment 0 missing");
color.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
color.set_blending_enabled(true);
color.set_source_rgb_blend_factor(MTLBlendFactor::One);
color.set_destination_rgb_blend_factor(MTLBlendFactor::OneMinusSourceAlpha);
color.set_rgb_blend_operation(MTLBlendOperation::Add);
color.set_source_alpha_blend_factor(MTLBlendFactor::One);
color.set_destination_alpha_blend_factor(MTLBlendFactor::OneMinusSourceAlpha);
color.set_alpha_blend_operation(MTLBlendOperation::Add);
device
.new_render_pipeline_state(&descriptor)
.expect("grid.text pipeline state creation failed")
}
fn build_bg_pipeline(device: &Device) -> RenderPipelineState {
let shader_source = include_str!("shaders/grid.metal");
let library = device
.new_library_with_source(shader_source, &CompileOptions::new())
.expect("grid.metal failed to compile");
let vertex_fn = library
.get_function("grid_bg_vertex", None)
.expect("grid_bg_vertex not found");
let fragment_fn = library
.get_function("grid_bg_fragment", None)
.expect("grid_bg_fragment not found");
let descriptor = RenderPipelineDescriptor::new();
descriptor.set_label("grid.bg");
descriptor.set_vertex_function(Some(&vertex_fn));
descriptor.set_fragment_function(Some(&fragment_fn));
let color = descriptor
.color_attachments()
.object_at(0)
.expect("color attachment 0 missing");
color.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
color.set_blending_enabled(true);
color.set_source_rgb_blend_factor(MTLBlendFactor::SourceAlpha);
color.set_destination_rgb_blend_factor(MTLBlendFactor::OneMinusSourceAlpha);
color.set_rgb_blend_operation(MTLBlendOperation::Add);
color.set_source_alpha_blend_factor(MTLBlendFactor::One);
color.set_destination_alpha_blend_factor(MTLBlendFactor::OneMinusSourceAlpha);
color.set_alpha_blend_operation(MTLBlendOperation::Add);
device
.new_render_pipeline_state(&descriptor)
.expect("grid.bg pipeline state creation failed")
}
fn alloc_bg_buffer(device: &Device, cols: u32, rows: u32) -> Buffer {
let size = (cols as u64)
.saturating_mul(rows as u64)
.saturating_mul(std::mem::size_of::<CellBg>() as u64)
.max(std::mem::size_of::<CellBg>() as u64);
device.new_buffer(size, MTLResourceOptions::StorageModeShared)
}
fn alloc_fg_buffer(device: &Device, capacity: usize) -> Buffer {
let size = (capacity as u64)
.saturating_mul(std::mem::size_of::<CellText>() as u64)
.max(std::mem::size_of::<CellText>() as u64);
device.new_buffer(size, MTLResourceOptions::StorageModeShared)
}
fn init_fg_rows(rows: u32) -> Vec<Vec<CellText>> {
(0..(rows as usize + CURSOR_ROW_SLOTS))
.map(|_| Vec::new())
.collect()
}