use anyhow::{Context as _, Result, ensure};
use derive_more::Deref;
use etagere::BucketedAtlasAllocator;
use gpui::{
AtlasBackend, AtlasKey, AtlasState, AtlasTextureId, AtlasTextureKind, AtlasTextureList,
AtlasTile, Bounds, DevicePixels, PlatformAtlas, Point, Size,
};
use objc2::{rc::Retained, runtime::ProtocolObject};
use objc2_metal::{
MTLDevice, MTLOrigin, MTLPixelFormat, MTLRegion, MTLSize, MTLStorageMode, MTLTexture,
MTLTextureDescriptor, MTLTextureUsage,
};
use parking_lot::Mutex;
use std::{borrow::Cow, ptr::NonNull};
pub struct MetalAtlas(Mutex<AtlasState<MetalAtlasTextures>>);
impl MetalAtlas {
pub(crate) fn new(
device: Retained<ProtocolObject<dyn MTLDevice>>,
supports_shared_storage: bool,
) -> Self {
MetalAtlas(Mutex::new(AtlasState::new(MetalAtlasTextures {
device,
supports_shared_storage,
monochrome_textures: AtlasTextureList::default(),
polychrome_textures: AtlasTextureList::default(),
})))
}
pub(crate) fn metal_texture(
&self,
id: AtlasTextureId,
) -> Option<Retained<ProtocolObject<dyn MTLTexture>>> {
Some(self.0.lock().backend.texture(id)?.metal_texture.clone())
}
}
struct MetalAtlasTextures {
device: Retained<ProtocolObject<dyn MTLDevice>>,
supports_shared_storage: bool,
monochrome_textures: AtlasTextureList<MetalAtlasTexture>,
polychrome_textures: AtlasTextureList<MetalAtlasTexture>,
}
impl PlatformAtlas for MetalAtlas {
fn get_or_insert_with<'a>(
&self,
key: AtlasKey,
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
) -> Result<Option<AtlasTile>> {
self.0.lock().get_or_insert_with(key, build)
}
fn remove(&self, key: &AtlasKey) {
self.0.lock().remove(key);
}
}
impl AtlasBackend for MetalAtlasTextures {
fn insert(
&mut self,
kind: AtlasTextureKind,
size: Size<DevicePixels>,
bytes: &[u8],
) -> Result<AtlasTile> {
let upload = AtlasUpload::new(kind, size, bytes)?;
let tile = self.allocate(size, kind)?;
let texture = self
.texture(tile.texture_id)
.context("looking up allocated atlas texture")?;
texture.upload(tile.bounds.origin, &upload)?;
Ok(tile)
}
fn remove(&mut self, tile: AtlasTile) {
let id = tile.texture_id;
let textures = match id.kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Subpixel => unreachable!(),
};
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.allocator.deallocate(tile.tile_id.into());
texture.decrement_ref_count();
if texture.is_unreferenced() {
textures.free_list.push(id.index as usize);
} else {
*texture_slot = Some(texture);
}
}
}
}
impl MetalAtlasTextures {
fn allocate(
&mut self,
size: Size<DevicePixels>,
texture_kind: AtlasTextureKind,
) -> Result<AtlasTile> {
{
let textures = match texture_kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Subpixel => unreachable!(),
};
if let Some(tile) = textures
.iter_mut()
.rev()
.find_map(|texture| texture.allocate(size))
{
return Ok(tile);
}
}
let texture = self.push_texture(size, texture_kind)?;
texture.allocate(size).context("allocating atlas tile")
}
fn push_texture(
&mut self,
min_size: Size<DevicePixels>,
kind: AtlasTextureKind,
) -> Result<&mut MetalAtlasTexture> {
const DEFAULT_ATLAS_SIZE: Size<DevicePixels> = Size {
width: DevicePixels(1024),
height: DevicePixels(1024),
};
const MAX_ATLAS_SIZE: Size<DevicePixels> = Size {
width: DevicePixels(16384),
height: DevicePixels(16384),
};
let size = min_size.min(&MAX_ATLAS_SIZE).max(&DEFAULT_ATLAS_SIZE);
let texture_descriptor = MTLTextureDescriptor::new();
unsafe {
texture_descriptor.setWidth(size.width.0 as usize);
texture_descriptor.setHeight(size.height.0 as usize);
}
let pixel_format;
let usage;
match kind {
AtlasTextureKind::Monochrome => {
pixel_format = MTLPixelFormat::A8Unorm;
usage = MTLTextureUsage::ShaderRead;
}
AtlasTextureKind::Polychrome => {
pixel_format = MTLPixelFormat::BGRA8Unorm;
usage = MTLTextureUsage::ShaderRead;
}
AtlasTextureKind::Subpixel => unreachable!(),
}
texture_descriptor.setPixelFormat(pixel_format);
texture_descriptor.setUsage(usage);
texture_descriptor.setStorageMode(if self.supports_shared_storage {
MTLStorageMode::Shared
} else {
MTLStorageMode::Managed
});
let metal_texture = self
.device
.newTextureWithDescriptor(&texture_descriptor)
.context("allocating atlas texture")?;
let texture_list = match kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Subpixel => unreachable!(),
};
let index = texture_list.free_list.pop().unwrap_or_else(|| {
let index = texture_list.textures.len();
texture_list.textures.push(None);
index
});
let atlas_texture = MetalAtlasTexture {
id: AtlasTextureId {
index: index as u32,
kind,
},
allocator: etagere::BucketedAtlasAllocator::new(size_to_etagere(size)),
metal_texture: AtlasTextureHandle(metal_texture),
live_atlas_keys: 0,
};
Ok(texture_list.textures[index].insert(atlas_texture))
}
fn texture(&self, id: AtlasTextureId) -> Option<&MetalAtlasTexture> {
let textures = match id.kind {
AtlasTextureKind::Monochrome => &self.monochrome_textures,
AtlasTextureKind::Polychrome => &self.polychrome_textures,
AtlasTextureKind::Subpixel => unreachable!(),
};
textures.textures.get(id.index as usize)?.as_ref()
}
}
struct MetalAtlasTexture {
id: AtlasTextureId,
allocator: BucketedAtlasAllocator,
metal_texture: AtlasTextureHandle,
live_atlas_keys: u32,
}
impl MetalAtlasTexture {
fn allocate(&mut self, size: Size<DevicePixels>) -> Option<AtlasTile> {
let allocation = self.allocator.allocate(size_to_etagere(size))?;
let tile = AtlasTile {
texture_id: self.id,
tile_id: allocation.id.into(),
bounds: Bounds {
origin: point_from_etagere(allocation.rectangle.min),
size,
},
padding: 0,
};
self.live_atlas_keys += 1;
Some(tile)
}
fn upload(&self, origin: Point<DevicePixels>, upload: &AtlasUpload<'_>) -> Result<()> {
ensure!(
self.id.kind == upload.kind,
"atlas upload kind does not match texture"
);
ensure!(
origin.x.0 >= 0
&& origin.y.0 >= 0
&& (origin.x.0 as usize)
.checked_add(upload.size.width.0 as usize)
.is_some_and(|end| end <= self.metal_texture.width())
&& (origin.y.0 as usize)
.checked_add(upload.size.height.0 as usize)
.is_some_and(|end| end <= self.metal_texture.height()),
"atlas upload is outside texture bounds"
);
let region = MTLRegion {
origin: MTLOrigin {
x: origin.x.0 as usize,
y: origin.y.0 as usize,
z: 0,
},
size: MTLSize {
width: upload.size.width.0 as usize,
height: upload.size.height.0 as usize,
depth: 1,
},
};
unsafe {
self.metal_texture
.replaceRegion_mipmapLevel_withBytes_bytesPerRow(
region,
0,
NonNull::from(upload.bytes).cast(),
upload.bytes_per_row,
);
}
Ok(())
}
fn decrement_ref_count(&mut self) {
self.live_atlas_keys -= 1;
}
fn is_unreferenced(&mut self) -> bool {
self.live_atlas_keys == 0
}
}
struct AtlasUpload<'a> {
kind: AtlasTextureKind,
size: Size<DevicePixels>,
bytes: &'a [u8],
bytes_per_row: usize,
}
impl<'a> AtlasUpload<'a> {
fn new(kind: AtlasTextureKind, size: Size<DevicePixels>, bytes: &'a [u8]) -> Result<Self> {
ensure!(
size.width.0 > 0 && size.height.0 > 0,
"atlas dimensions must be positive"
);
let bytes_per_pixel = match kind {
AtlasTextureKind::Monochrome => 1,
AtlasTextureKind::Polychrome => 4,
AtlasTextureKind::Subpixel => unreachable!(),
};
let bytes_per_row = (size.width.0 as usize)
.checked_mul(bytes_per_pixel)
.context("calculating atlas row byte count")?;
let byte_count = bytes_per_row
.checked_mul(size.height.0 as usize)
.context("calculating atlas byte count")?;
ensure!(
bytes.len() >= byte_count,
"atlas upload byte length is {}, expected at least {byte_count}",
bytes.len()
);
Ok(Self {
kind,
size,
bytes,
bytes_per_row,
})
}
}
fn size_to_etagere(size: Size<DevicePixels>) -> etagere::Size {
etagere::Size::new(size.width.into(), size.height.into())
}
fn point_from_etagere(value: etagere::Point) -> Point<DevicePixels> {
Point {
x: DevicePixels::from(value.x),
y: DevicePixels::from(value.y),
}
}
#[derive(Deref)]
struct AtlasTextureHandle(Retained<ProtocolObject<dyn MTLTexture>>);
unsafe impl Send for AtlasTextureHandle {}
#[cfg(test)]
mod tests {
use super::*;
use gpui::{PlatformAtlas, point, size};
use objc2_metal::{MTLCreateSystemDefaultDevice, MTLGPUFamily};
use std::borrow::Cow;
#[test]
fn objc2_atlas_upload_and_retained_texture_survive_eviction() {
objc2::rc::autoreleasepool(|_| {
let device =
objc2_metal::MTLCreateSystemDefaultDevice().expect("Metal device required");
let shared = device.supportsFamily(objc2_metal::MTLGPUFamily::Apple1);
let atlas = MetalAtlas::new(device, shared);
let size = Size {
width: DevicePixels(2),
height: DevicePixels(2),
};
let pixels = vec![13, 27, 61, 255].repeat(4);
let key = make_image_key(42, 0);
let tile = atlas
.get_or_insert_with(key.clone(), &mut || {
Ok(Some((size, Cow::Borrowed(pixels.as_slice()))))
})
.unwrap()
.unwrap();
let texture = atlas.metal_texture(tile.texture_id).unwrap();
atlas.remove(&key);
assert!(atlas.metal_texture(tile.texture_id).is_none());
drop(atlas);
let mut actual = vec![0u8; pixels.len()];
let region = MTLRegion {
origin: MTLOrigin {
x: tile.bounds.origin.x.0 as usize,
y: tile.bounds.origin.y.0 as usize,
z: 0,
},
size: MTLSize {
width: 2,
height: 2,
depth: 1,
},
};
unsafe {
texture.getBytes_bytesPerRow_fromRegion_mipmapLevel(
std::ptr::NonNull::new(actual.as_mut_ptr().cast()).unwrap(),
8,
region,
0,
);
}
assert_eq!(actual, pixels);
});
}
#[test]
fn test_remove_clears_stale_keys_from_tiles_by_key() {
let Some(atlas) = create_atlas() else {
return;
};
let small = Size {
width: DevicePixels(64),
height: DevicePixels(64),
};
let key_a = make_image_key(1, 0);
let key_b = make_image_key(2, 0);
let key_c = make_image_key(3, 0);
let tile_a = insert_tile(&atlas, key_a.clone(), small);
let tile_b = insert_tile(&atlas, key_b.clone(), small);
let tile_c = insert_tile(&atlas, key_c.clone(), small);
assert_eq!(tile_a.texture_id, tile_b.texture_id);
assert_eq!(tile_b.texture_id, tile_c.texture_id);
atlas.remove(&key_a);
atlas.remove(&key_b);
atlas.remove(&key_c);
let tile_a2 = insert_tile(&atlas, key_a, small);
assert_eq!(tile_a2.texture_id, tile_a.texture_id);
assert!(atlas.metal_texture(tile_a2.texture_id).is_some());
}
#[test]
fn test_metal_texture_is_none_after_last_tile_removed() {
let Some(atlas) = create_atlas() else {
return;
};
let key = make_image_key(1, 0);
let tile = insert_tile(
&atlas,
key.clone(),
Size {
width: DevicePixels(64),
height: DevicePixels(64),
},
);
assert!(atlas.metal_texture(tile.texture_id).is_some());
atlas.remove(&key);
assert!(atlas.metal_texture(tile.texture_id).is_none());
}
#[test]
fn test_remove_deallocates_tile_space_for_reuse() {
let Some(atlas) = create_atlas() else {
return;
};
let small = Size {
width: DevicePixels(64),
height: DevicePixels(64),
};
let big = Size {
width: DevicePixels(700),
height: DevicePixels(700),
};
let keeper_key = make_image_key(1, 0);
let big_key_a = make_image_key(2, 0);
let big_key_b = make_image_key(3, 0);
let keeper_tile = insert_tile(&atlas, keeper_key, small);
let tile_a = insert_tile(&atlas, big_key_a.clone(), big);
assert_eq!(keeper_tile.texture_id, tile_a.texture_id);
atlas.remove(&big_key_a);
let tile_b = insert_tile(&atlas, big_key_b, big);
assert_eq!(tile_b.texture_id, keeper_tile.texture_id);
}
#[test]
fn test_remove_nonexistent_key_is_noop() {
let Some(atlas) = create_atlas() else {
return;
};
let key = make_image_key(999, 0);
atlas.remove(&key);
}
#[test]
fn test_invalid_upload_does_not_allocate() {
let Some(atlas) = create_atlas() else {
return;
};
let mut state = atlas.0.lock();
for (kind, short_bytes, short_error) in [
(
AtlasTextureKind::Monochrome,
&[][..],
"atlas upload byte length is 0, expected at least 1",
),
(
AtlasTextureKind::Polychrome,
&[0][..],
"atlas upload byte length is 1, expected at least 4",
),
] {
for (width, height, bytes, expected_error) in [
(1, 1, short_bytes, short_error),
(0, 1, &[][..], "atlas dimensions must be positive"),
(1, 0, &[][..], "atlas dimensions must be positive"),
(i32::MIN, 1, &[][..], "atlas dimensions must be positive"),
(1, i32::MIN, &[][..], "atlas dimensions must be positive"),
] {
assert_eq!(
state
.backend
.insert(kind, size(DevicePixels(width), DevicePixels(height)), bytes)
.err()
.map(|error| error.to_string()),
Some(expected_error.to_owned())
);
for textures in [
&state.backend.monochrome_textures,
&state.backend.polychrome_textures,
] {
assert_eq!((textures.textures.len(), textures.free_list.len()), (0, 0));
}
}
}
}
#[test]
fn test_texture_upload_preserves_pixel_layout() -> Result<()> {
let Some(atlas) = create_atlas() else {
return Ok(());
};
let mut state = atlas.0.lock();
for (kind, bytes_per_pixel) in [
(AtlasTextureKind::Monochrome, 1),
(AtlasTextureKind::Polychrome, 4),
] {
let texture = state
.backend
.push_texture(size(DevicePixels(8), DevicePixels(8)), kind)?;
texture.upload(
Point::default(),
&AtlasUpload::new(
kind,
size(DevicePixels(8), DevicePixels(8)),
&vec![0; 8 * 8 * bytes_per_pixel],
)?,
)?;
let pixels = (1..=3 * 2 * bytes_per_pixel)
.map(|value| value as u8)
.collect::<Vec<_>>();
texture.upload(
point(DevicePixels(2), DevicePixels(3)),
&AtlasUpload::new(kind, size(DevicePixels(3), DevicePixels(2)), &pixels)?,
)?;
let mut actual = vec![0; 8 * 8 * bytes_per_pixel];
unsafe {
texture
.metal_texture
.getBytes_bytesPerRow_fromRegion_mipmapLevel(
NonNull::from(actual.as_mut_slice()).cast(),
8 * bytes_per_pixel,
MTLRegion {
origin: MTLOrigin { x: 0, y: 0, z: 0 },
size: MTLSize {
width: 8,
height: 8,
depth: 1,
},
},
0,
);
}
let mut expected = vec![0; actual.len()];
for row in 0..2 {
let start = ((row + 3) * 8 + 2) * bytes_per_pixel;
let source = row * 3 * bytes_per_pixel;
expected[start..start + 3 * bytes_per_pixel]
.copy_from_slice(&pixels[source..source + 3 * bytes_per_pixel]);
}
assert_eq!(actual, expected);
}
Ok(())
}
fn create_atlas() -> Option<MetalAtlas> {
let device = MTLCreateSystemDefaultDevice()?;
let supports_shared_storage =
cfg!(target_os = "ios") || device.supportsFamily(MTLGPUFamily::Apple1);
Some(MetalAtlas::new(device, supports_shared_storage))
}
fn make_image_key(image_id: usize, frame_index: usize) -> AtlasKey {
AtlasKey::Image(gpui::RenderImageParams {
image_id: gpui::ImageId(image_id),
frame_index,
})
}
fn insert_tile(atlas: &MetalAtlas, key: AtlasKey, size: Size<DevicePixels>) -> AtlasTile {
atlas
.get_or_insert_with(key, &mut || {
let byte_count = (size.width.0 as usize) * (size.height.0 as usize) * 4;
Ok(Some((size, Cow::Owned(vec![0u8; byte_count]))))
})
.expect("inserting atlas tile")
.expect("retrieving inserted atlas tile")
}
}
#[cfg(all(test, feature = "font-kit"))]
mod variation_diagnostics {
use super::*;
use gpui::{FontId, FontRun, PlatformTextSystem, RenderGlyphParams, font, point, px};
use objc2_metal::MTLResource;
use std::{collections::HashSet, time::Instant};
#[test]
#[ignore = "requires a Metal device; explicit atlas allocation diagnostic"]
fn static_axis_sweep_measures_live_tiles_and_allocated_pages() -> Result<()> {
let device = objc2_metal::MTLCreateSystemDefaultDevice()
.context("Metal device required for atlas diagnostic")?;
for quantized in [false, true] {
let atlas = MetalAtlas::new(device.clone(), true);
let fonts = crate::AppleTextSystem::new();
fonts.add_fonts(vec![Cow::Borrowed(include_bytes!(
"../../../../extensions/crates/xgpui-parley/tests/fonts/RobotoFlex.ttf"
))])?;
let mut keys = HashSet::new();
let mut requested = HashSet::<FontId>::new();
let mut previous = None;
let now = Instant::now();
let snapshot = |atlas: &MetalAtlas| {
let state = atlas.0.lock();
state
.backend
.monochrome_textures
.textures
.iter()
.flatten()
.fold((0usize, 0usize, 0usize), |(pages, bytes, tiles), page| {
(
pages + 1,
bytes + page.metal_texture.allocatedSize(),
tiles + page.live_atlas_keys as usize,
)
})
};
for cycle in 0..3 {
for w in 0..8 {
for d in 0..8 {
for s in 0..4 {
let values = [
("wght", 100.0 + w as f32 * 800.0 / 7.0, 100.0, 1000.0, 400.0),
("wdth", 50.0 + d as f32 * 100.0 / 7.0, 25.0, 151.0, 50.0),
("slnt", -10.0 + s as f32 * 10.0 / 3.0, -10.0, 0.0, 5.0),
];
let mut descriptor = font("Roboto Flex");
descriptor.variations = gpui::FontVariations::new(
values
.into_iter()
.map(|(tag, value, min, max, step)| {
Ok((
tag.parse()?,
if quantized {
(min + ((value - min) / step).round() * step)
.clamp(min, max)
} else {
value
},
))
})
.collect::<Result<Vec<_>>>()?,
)?;
let id = fonts.font_id(&descriptor)?;
requested.insert(id);
let text = "Hamburgefonts 012345";
let line = fonts.layout_line(
text,
px(28.0),
&[FontRun {
len: text.len(),
font_id: id,
}],
);
for run in line.runs {
for glyph in run.glyphs {
let params = RenderGlyphParams {
font_id: run.font_id,
glyph_id: glyph.id,
font_size: px(28.0),
scale_factor: 1.0,
subpixel_variant: point(0, 0),
is_emoji: false,
subpixel_rendering: false,
dilation: 0,
};
let key = AtlasKey::Glyph(params.clone());
if keys.insert(key.clone()) {
atlas.get_or_insert_with(key, &mut || {
let bounds = fonts.glyph_raster_bounds(¶ms)?;
if bounds.size.width.0 <= 0 || bounds.size.height.0 <= 0
{
return Ok(None);
}
let (size, bytes) =
fonts.rasterize_glyph(¶ms, bounds)?;
Ok(Some((size, Cow::Owned(bytes))))
})?;
}
}
}
}
}
}
let current = snapshot(&atlas);
println!(
"Metal quantized={quantized} cycle={cycle} requested={} pages={} allocated_bytes={} live_tiles={} elapsed_ms={:.2}",
requested.len(),
current.0,
current.1,
current.2,
now.elapsed().as_secs_f64() * 1000.0
);
if let Some(previous) = previous {
assert_eq!(current, previous);
}
previous = Some(current);
}
anyhow::ensure!(
keys.len() > requested.len(),
"variation instances unexpectedly collapsed during shaping"
);
let retained = {
let state = atlas.0.lock();
state
.backend
.monochrome_textures
.textures
.iter()
.flatten()
.next()
.context("expected atlas page")?
.metal_texture
.clone()
};
for key in keys {
atlas.remove(&key);
}
assert_eq!(snapshot(&atlas), (0, 0, 0));
println!(
"Metal after removal: active_pages=0 active_tiles=0 retained_texture_bytes={}",
retained.allocatedSize()
);
drop(retained);
}
Ok(())
}
}