use crate::{ platform::AtlasTextureList, AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas, Point, Size, }; use anyhow::{anyhow, Result}; use collections::FxHashMap; use derive_more::{Deref, DerefMut}; use etagere::BucketedAtlasAllocator; use metal::Device; use parking_lot::Mutex; use std::borrow::Cow; pub(crate) struct MetalAtlas(Mutex); impl MetalAtlas { pub(crate) fn new(device: Device, path_sample_count: u32) -> Self { MetalAtlas(Mutex::new(MetalAtlasState { device: AssertSend(device), monochrome_textures: Default::default(), polychrome_textures: Default::default(), path_textures: Default::default(), tiles_by_key: Default::default(), path_sample_count, })) } pub(crate) fn metal_texture(&self, id: AtlasTextureId) -> metal::Texture { self.0.lock().texture(id).metal_texture.clone() } pub(crate) fn msaa_texture(&self, id: AtlasTextureId) -> Option { self.0.lock().texture(id).msaa_texture.clone() } pub(crate) fn allocate( &self, size: Size, texture_kind: AtlasTextureKind, ) -> Option { self.0.lock().allocate(size, texture_kind) } pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) { let mut lock = self.0.lock(); let textures = match texture_kind { AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, AtlasTextureKind::Path => &mut lock.path_textures, }; for texture in textures.iter_mut() { texture.clear(); } } } struct MetalAtlasState { device: AssertSend, monochrome_textures: AtlasTextureList, polychrome_textures: AtlasTextureList, path_textures: AtlasTextureList, tiles_by_key: FxHashMap, path_sample_count: u32, } impl PlatformAtlas for MetalAtlas { fn get_or_insert_with<'a>( &self, key: &AtlasKey, build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, ) -> Result> { let mut lock = self.0.lock(); if let Some(tile) = lock.tiles_by_key.get(key) { Ok(Some(tile.clone())) } else { let Some((size, bytes)) = build()? else { return Ok(None); }; let tile = lock .allocate(size, key.texture_kind()) .ok_or_else(|| anyhow!("failed to allocate"))?; let texture = lock.texture(tile.texture_id); texture.upload(tile.bounds, &bytes); lock.tiles_by_key.insert(key.clone(), tile.clone()); Ok(Some(tile)) } } fn remove(&self, key: &AtlasKey) { let mut lock = self.0.lock(); let Some(id) = lock.tiles_by_key.get(key).map(|v| v.texture_id) else { return; }; let textures = match id.kind { AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, AtlasTextureKind::Path => &mut lock.polychrome_textures, }; let Some(texture_slot) = textures .textures .iter_mut() .find(|texture| texture.as_ref().is_some_and(|v| v.id == id)) else { return; }; if let Some(mut texture) = texture_slot.take() { texture.decrement_ref_count(); if texture.is_unreferenced() { textures.free_list.push(id.index as usize); lock.tiles_by_key.remove(key); } else { *texture_slot = Some(texture); } } } } impl MetalAtlasState { fn allocate( &mut self, size: Size, texture_kind: AtlasTextureKind, ) -> Option { { let textures = match texture_kind { AtlasTextureKind::Monochrome => &mut self.monochrome_textures, AtlasTextureKind::Polychrome => &mut self.polychrome_textures, AtlasTextureKind::Path => &mut self.path_textures, }; if let Some(tile) = textures .iter_mut() .rev() .find_map(|texture| texture.allocate(size)) { return Some(tile); } } let texture = self.push_texture(size, texture_kind); texture.allocate(size) } fn push_texture( &mut self, min_size: Size, kind: AtlasTextureKind, ) -> &mut MetalAtlasTexture { const DEFAULT_ATLAS_SIZE: Size = Size { width: DevicePixels(1024), height: DevicePixels(1024), }; // Max texture size on all modern Apple GPUs. Anything bigger than that crashes in validateWithDevice. const MAX_ATLAS_SIZE: Size = Size { width: DevicePixels(16384), height: DevicePixels(16384), }; let size = min_size.min(&MAX_ATLAS_SIZE).max(&DEFAULT_ATLAS_SIZE); let texture_descriptor = metal::TextureDescriptor::new(); texture_descriptor.set_width(size.width.into()); texture_descriptor.set_height(size.height.into()); let pixel_format; let usage; match kind { AtlasTextureKind::Monochrome => { pixel_format = metal::MTLPixelFormat::A8Unorm; usage = metal::MTLTextureUsage::ShaderRead; } AtlasTextureKind::Polychrome => { pixel_format = metal::MTLPixelFormat::BGRA8Unorm; usage = metal::MTLTextureUsage::ShaderRead; } AtlasTextureKind::Path => { pixel_format = metal::MTLPixelFormat::R16Float; usage = metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead; } } texture_descriptor.set_pixel_format(pixel_format); texture_descriptor.set_usage(usage); let metal_texture = self.device.new_texture(&texture_descriptor); // We currently only enable MSAA for path textures. let msaa_texture = if self.path_sample_count > 1 && kind == AtlasTextureKind::Path { let mut descriptor = texture_descriptor.clone(); descriptor.set_texture_type(metal::MTLTextureType::D2Multisample); descriptor.set_storage_mode(metal::MTLStorageMode::Private); descriptor.set_sample_count(self.path_sample_count as _); let msaa_texture = self.device.new_texture(&descriptor); Some(msaa_texture) } else { None }; let texture_list = match kind { AtlasTextureKind::Monochrome => &mut self.monochrome_textures, AtlasTextureKind::Polychrome => &mut self.polychrome_textures, AtlasTextureKind::Path => &mut self.path_textures, }; let index = texture_list.free_list.pop(); let atlas_texture = MetalAtlasTexture { id: AtlasTextureId { index: index.unwrap_or(texture_list.textures.len()) as u32, kind, }, allocator: etagere::BucketedAtlasAllocator::new(size.into()), metal_texture: AssertSend(metal_texture), msaa_texture: AssertSend(msaa_texture), live_atlas_keys: 0, }; if let Some(ix) = index { texture_list.textures[ix] = Some(atlas_texture); texture_list.textures.get_mut(ix).unwrap().as_mut().unwrap() } else { texture_list.textures.push(Some(atlas_texture)); texture_list.textures.last_mut().unwrap().as_mut().unwrap() } } fn texture(&self, id: AtlasTextureId) -> &MetalAtlasTexture { let textures = match id.kind { crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, crate::AtlasTextureKind::Path => &self.path_textures, }; textures[id.index as usize].as_ref().unwrap() } } struct MetalAtlasTexture { id: AtlasTextureId, allocator: BucketedAtlasAllocator, metal_texture: AssertSend, msaa_texture: AssertSend>, live_atlas_keys: u32, } impl MetalAtlasTexture { fn clear(&mut self) { self.allocator.clear(); } fn allocate(&mut self, size: Size) -> Option { let allocation = self.allocator.allocate(size.into())?; let tile = AtlasTile { texture_id: self.id, tile_id: allocation.id.into(), bounds: Bounds { origin: allocation.rectangle.min.into(), size, }, padding: 0, }; self.live_atlas_keys += 1; Some(tile) } fn upload(&self, bounds: Bounds, bytes: &[u8]) { let region = metal::MTLRegion::new_2d( bounds.origin.x.into(), bounds.origin.y.into(), bounds.size.width.into(), bounds.size.height.into(), ); self.metal_texture.replace_region( region, 0, bytes.as_ptr() as *const _, bounds.size.width.to_bytes(self.bytes_per_pixel()) as u64, ); } fn bytes_per_pixel(&self) -> u8 { use metal::MTLPixelFormat::*; match self.metal_texture.pixel_format() { A8Unorm | R8Unorm => 1, RGBA8Unorm | BGRA8Unorm => 4, _ => unimplemented!(), } } fn decrement_ref_count(&mut self) { self.live_atlas_keys -= 1; } fn is_unreferenced(&mut self) -> bool { self.live_atlas_keys == 0 } } impl From> for etagere::Size { fn from(size: Size) -> Self { etagere::Size::new(size.width.into(), size.height.into()) } } impl From for Point { fn from(value: etagere::Point) -> Self { Point { x: DevicePixels::from(value.x), y: DevicePixels::from(value.y), } } } impl From for Size { fn from(size: etagere::Size) -> Self { Size { width: DevicePixels::from(size.width), height: DevicePixels::from(size.height), } } } impl From for Bounds { fn from(rectangle: etagere::Rectangle) -> Self { Bounds { origin: rectangle.min.into(), size: rectangle.size().into(), } } } #[derive(Deref, DerefMut)] struct AssertSend(T); unsafe impl Send for AssertSend {}