diff --git a/crates/gpui_macos/src/metal_atlas.rs b/crates/gpui_macos/src/metal_atlas.rs index eacd9407fe2e447abbd05dc8cdb2e9f7660cf3cf..e6b8443c520e1b47006104085fdc26a5415d85f6 100644 --- a/crates/gpui_macos/src/metal_atlas.rs +++ b/crates/gpui_macos/src/metal_atlas.rs @@ -61,7 +61,7 @@ impl PlatformAtlas for MetalAtlas { 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 { + let Some(id) = lock.tiles_by_key.remove(key).map(|v| v.texture_id) else { return; }; @@ -81,10 +81,8 @@ impl PlatformAtlas for MetalAtlas { 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); } @@ -271,3 +269,81 @@ fn point_from_etagere(value: etagere::Point) -> Point { struct AssertSend(T); unsafe impl Send for AssertSend {} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::PlatformAtlas; + use std::borrow::Cow; + + fn create_atlas() -> Option { + let device = metal::Device::system_default()?; + Some(MetalAtlas::new(device, true)) + } + + 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) -> 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("allocation should succeed") + .expect("callback returns Some") + } + + #[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, small); + let tile_b = insert_tile(&atlas, &key_b, small); + let tile_c = insert_tile(&atlas, &key_c, small); + + assert_eq!(tile_a.texture_id, tile_b.texture_id); + assert_eq!(tile_b.texture_id, tile_c.texture_id); + + // Remove A: texture still has B and C, so it stays. + // The key for A must be removed from tiles_by_key. + atlas.remove(&key_a); + + // Remove B: texture still has C. + atlas.remove(&key_b); + + // Remove C: texture becomes unreferenced and is deleted. + atlas.remove(&key_c); + + // Re-inserting A must allocate a fresh tile on a new texture, + // NOT return a stale tile referencing the deleted texture. + let tile_a2 = insert_tile(&atlas, &key_a, small); + + // The texture must actually exist — this would panic before the fix. + let _texture = atlas.metal_texture(tile_a2.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); + } +} diff --git a/crates/gpui_windows/src/directx_atlas.rs b/crates/gpui_windows/src/directx_atlas.rs index a2ded660ca232a32b2a609c6185a95433c803d9c..03acadb8607ed3e7d957e7faa73960724fa09888 100644 --- a/crates/gpui_windows/src/directx_atlas.rs +++ b/crates/gpui_windows/src/directx_atlas.rs @@ -116,7 +116,6 @@ impl PlatformAtlas for DirectXAtlas { texture.decrement_ref_count(); if texture.is_unreferenced() { textures.free_list.push(texture.id.index as usize); - lock.tiles_by_key.remove(key); } else { *texture_slot = Some(texture); }