diff --git a/Cargo.lock b/Cargo.lock index 0c13be8493..dd64ee4f0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7585,7 +7585,6 @@ dependencies = [ "block", "cbindgen", "cocoa 0.26.0", - "collections", "core-foundation 0.10.0", "core-video", "derive_more", diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index ccf97a29f9..c03653ed94 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -48,6 +48,7 @@ use crate::{ use anyhow::bail; use anyhow::{Context as _, Result}; use async_task::Runnable; +use collections::FxHashMap; use futures::channel::oneshot; #[cfg(any(test, feature = "test-support", feature = "bench-support"))] use image::RgbaImage; @@ -61,6 +62,7 @@ use seahash::SeaHasher; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use std::borrow::Cow; +use std::collections::hash_map::Entry; use std::hash::{Hash, Hasher}; use std::io::Cursor; use std::ops; @@ -1384,13 +1386,6 @@ pub enum AtlasKey { } impl AtlasKey { - #[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) - )] /// Returns the texture kind for this atlas key. pub fn texture_kind(&self) -> AtlasTextureKind { match self { @@ -1429,9 +1424,10 @@ impl From for AtlasKey { #[expect(missing_docs)] pub trait PlatformAtlas { + /// The builder runs with the atlas locked and must not re-enter the same atlas. fn get_or_insert_with<'a>( &self, - key: &AtlasKey, + key: AtlasKey, build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, ) -> Result>; fn remove(&self, key: &AtlasKey); @@ -1442,6 +1438,136 @@ pub trait PlatformAtlas { } } +#[doc(hidden)] +pub trait AtlasBackend { + fn insert( + &mut self, + kind: AtlasTextureKind, + size: Size, + bytes: &[u8], + ) -> Result; + + fn remove(&mut self, tile: AtlasTile); +} + +#[doc(hidden)] +pub struct AtlasState { + tiles_by_key: FxHashMap, + pub backend: Backend, +} + +impl AtlasState { + pub fn new(backend: Backend) -> Self { + Self { + tiles_by_key: FxHashMap::default(), + backend, + } + } + + pub fn contains(&self, key: &AtlasKey) -> bool { + self.tiles_by_key.contains_key(key) + } + + pub fn clear(&mut self, reset_backend: impl FnOnce(&mut Backend)) { + self.tiles_by_key.clear(); + reset_backend(&mut self.backend); + } +} + +impl Default for AtlasState { + fn default() -> Self { + Self::new(Backend::default()) + } +} + +impl AtlasState { + pub fn get_or_insert_with<'a>( + &mut self, + key: AtlasKey, + build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, + ) -> Result> { + match self.tiles_by_key.entry(key) { + Entry::Occupied(entry) => Ok(Some(*entry.get())), + Entry::Vacant(entry) => { + profiling::scope!("new tile"); + let Some((size, bytes)) = build()? else { + return Ok(None); + }; + let tile = self + .backend + .insert(entry.key().texture_kind(), size, &bytes)?; + entry.insert(tile); + Ok(Some(tile)) + } + } + } + + pub fn remove(&mut self, key: &AtlasKey) { + if let Some(tile) = self.tiles_by_key.remove(key) { + self.backend.remove(tile); + } + } +} + +/// A sprite atlas for windows without a GPU. It hands out uniquely identified +/// tiles without uploading any pixels, so glyph, SVG, and image painting can +/// run to completion in tests and headless platforms. +#[derive(Default)] +pub struct HeadlessAtlas(parking_lot::Mutex>); + +#[doc(hidden)] +#[derive(Default)] +pub struct HeadlessAtlasBackend { + next_id: u32, +} + +impl AtlasBackend for HeadlessAtlasBackend { + fn insert( + &mut self, + kind: AtlasTextureKind, + size: Size, + _bytes: &[u8], + ) -> Result { + self.next_id += 1; + let texture_id = self.next_id; + self.next_id += 1; + let tile_id = self.next_id; + Ok(AtlasTile { + texture_id: AtlasTextureId { + index: texture_id, + kind, + }, + tile_id: TileId(tile_id), + padding: 0, + bounds: Bounds { + origin: Point::default(), + size, + }, + }) + } + + fn remove(&mut self, _tile: AtlasTile) {} +} + +impl PlatformAtlas for HeadlessAtlas { + fn get_or_insert_with<'a>( + &self, + key: AtlasKey, + build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, + ) -> Result> { + self.0.lock().get_or_insert_with(key, build) + } + + fn remove(&self, key: &AtlasKey) { + self.0.lock().remove(key); + } + + #[cfg(any(test, feature = "test-support", feature = "bench-support"))] + fn contains(&self, key: &AtlasKey) -> bool { + self.0.lock().contains(key) + } +} + #[doc(hidden)] pub struct AtlasTextureList { pub textures: Vec>, @@ -3030,6 +3156,124 @@ mod image_tests { } } +#[cfg(test)] +mod atlas_tests { + use super::*; + + const TILE_SIZE: Size = Size { + width: DevicePixels(1), + height: DevicePixels(1), + }; + + #[derive(Default)] + struct RecordingAtlasBackend { + insert_calls: u32, + fail_next_insert: bool, + removed_tiles: Vec, + } + + impl AtlasBackend for RecordingAtlasBackend { + fn insert( + &mut self, + kind: AtlasTextureKind, + size: Size, + _bytes: &[u8], + ) -> Result { + self.insert_calls += 1; + if std::mem::take(&mut self.fail_next_insert) { + anyhow::bail!("backend failed"); + } + Ok(AtlasTile { + texture_id: AtlasTextureId { index: 0, kind }, + tile_id: TileId(self.insert_calls), + padding: 0, + bounds: Bounds { + origin: Point::default(), + size, + }, + }) + } + + fn remove(&mut self, tile: AtlasTile) { + self.removed_tiles.push(tile); + } + } + + fn image_key(image_id: usize) -> AtlasKey { + AtlasKey::Image(RenderImageParams { + image_id: crate::ImageId(image_id), + frame_index: 0, + }) + } + + fn build_tile() -> Result, Cow<'static, [u8]>)>> { + Ok(Some((TILE_SIZE, Cow::Borrowed(&[0, 0, 0, 255])))) + } + + #[test] + fn only_successful_inserts_are_cached() -> Result<()> { + let mut state = AtlasState::new(RecordingAtlasBackend::default()); + let key = image_key(1); + + assert_eq!( + state.get_or_insert_with(key.clone(), &mut || Ok(None))?, + None + ); + state + .get_or_insert_with(key.clone(), &mut || anyhow::bail!("builder failed")) + .expect_err("builder error should propagate"); + assert!(!state.contains(&key)); + assert_eq!(state.backend.insert_calls, 0); + + state.backend.fail_next_insert = true; + state + .get_or_insert_with(key.clone(), &mut build_tile) + .expect_err("backend error should propagate"); + assert!(!state.contains(&key)); + assert_eq!(state.backend.insert_calls, 1); + + let tile = state + .get_or_insert_with(key.clone(), &mut build_tile)? + .context("builder should produce a tile")?; + assert_eq!(tile.texture_id.kind, key.texture_kind()); + assert_eq!( + state.get_or_insert_with(key.clone(), &mut || { + anyhow::bail!("cache hit must not call the builder") + })?, + Some(tile) + ); + assert!(state.contains(&key)); + assert_eq!(state.backend.insert_calls, 2); + Ok(()) + } + + #[test] + fn remove_and_clear_invalidate_keys() -> Result<()> { + let mut state = AtlasState::new(RecordingAtlasBackend::default()); + let key = image_key(1); + let other_key = image_key(2); + let tile = state + .get_or_insert_with(key.clone(), &mut build_tile)? + .context("builder should produce a tile")?; + state + .get_or_insert_with(other_key.clone(), &mut build_tile)? + .context("builder should produce another tile")?; + + state.remove(&key); + state.remove(&key); + assert!(!state.contains(&key)); + assert!(state.contains(&other_key)); + assert_eq!(state.backend.removed_tiles, vec![tile]); + + let mut reset_calls = 0; + state.clear(|_| reset_calls += 1); + assert_eq!(reset_calls, 1); + assert!(!state.contains(&other_key)); + assert_eq!(state.backend.removed_tiles, vec![tile]); + Ok(()) + } +} + #[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))] mod tests { use super::*; diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index ddce59adf3..14fb8896b2 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -1,12 +1,10 @@ use crate::{ - AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DevicePixels, - DispatchEventResult, GpuSpecs, Pixels, PlatformAtlas, PlatformDisplay, - PlatformHeadlessRenderer, PlatformInput, PlatformInputHandler, PlatformWindow, Point, - PromptButton, RequestFrameOptions, Scene, Size, TestPlatform, TextInputConfiguration, - TextInputStateChange, TileId, WindowAppearance, WindowBackgroundAppearance, WindowBounds, - WindowControlArea, WindowInsets, WindowParams, WindowVisibility, + AnyWindowHandle, Bounds, DevicePixels, DispatchEventResult, GpuSpecs, HeadlessAtlas, Pixels, + PlatformAtlas, PlatformDisplay, PlatformHeadlessRenderer, PlatformInput, PlatformInputHandler, + PlatformWindow, Point, PromptButton, RequestFrameOptions, Scene, Size, TestPlatform, + TextInputConfiguration, TextInputStateChange, WindowAppearance, WindowBackgroundAppearance, + WindowBounds, WindowControlArea, WindowInsets, WindowParams, WindowVisibility, }; -use collections::HashMap; use gpui_util::ResultExt as _; #[cfg(any(test, feature = "test-support"))] use image::RgbaImage; @@ -91,7 +89,7 @@ impl TestWindow { ) -> Self { let sprite_atlas: Arc = match &renderer { Some(r) => r.sprite_atlas(), - None => Arc::new(TestAtlas::new()), + None => Arc::new(HeadlessAtlas::default()), }; Self(Rc::new(Mutex::new(TestWindowState { bounds: params.bounds, @@ -591,72 +589,3 @@ impl PlatformWindow for TestWindow { None } } - -pub(crate) struct TestAtlasState { - next_id: u32, - tiles: HashMap, -} - -pub(crate) struct TestAtlas(Mutex); - -impl TestAtlas { - pub fn new() -> Self { - TestAtlas(Mutex::new(TestAtlasState { - next_id: 0, - tiles: HashMap::default(), - })) - } -} - -impl PlatformAtlas for TestAtlas { - fn get_or_insert_with<'a>( - &self, - key: &crate::AtlasKey, - build: &mut dyn FnMut() -> anyhow::Result< - Option<(Size, std::borrow::Cow<'a, [u8]>)>, - >, - ) -> anyhow::Result> { - let mut state = self.0.lock(); - if let Some(&tile) = state.tiles.get(key) { - return Ok(Some(tile)); - } - drop(state); - - let Some((size, _)) = build()? else { - return Ok(None); - }; - - let mut state = self.0.lock(); - state.next_id += 1; - let texture_id = state.next_id; - state.next_id += 1; - let tile_id = state.next_id; - - state.tiles.insert( - key.clone(), - crate::AtlasTile { - texture_id: AtlasTextureId { - index: texture_id, - kind: crate::AtlasTextureKind::Monochrome, - }, - tile_id: TileId(tile_id), - padding: 0, - bounds: crate::Bounds { - origin: Point::default(), - size, - }, - }, - ); - - Ok(Some(state.tiles[key])) - } - - fn remove(&self, key: &AtlasKey) { - let mut state = self.0.lock(); - state.tiles.remove(key); - } - - fn contains(&self, key: &AtlasKey) -> bool { - self.0.lock().tiles.contains_key(key) - } -} diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index e18e27b8b0..6794bb3c81 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4671,7 +4671,7 @@ impl Window { if !raster_bounds.is_zero() { let tile = self .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { + .get_or_insert_with(params.clone().into(), &mut || { let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; Ok(Some((size, Cow::Owned(bytes)))) })? @@ -4761,7 +4761,7 @@ impl Window { if !raster_bounds.is_zero() { let tile = self .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { + .get_or_insert_with(params.clone().into(), &mut || { let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; Ok(Some((size, Cow::Owned(bytes)))) })? @@ -4814,7 +4814,7 @@ impl Window { let Some(tile) = self.sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { + .get_or_insert_with(params.clone().into(), &mut || { let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(¶ms, data)? else { return Ok(None); @@ -4887,7 +4887,7 @@ impl Window { let tile = self .sprite_atlas - .get_or_insert_with(¶ms.into(), &mut || { + .get_or_insert_with(params.into(), &mut || { Ok(Some(( data.size(frame_index), Cow::Borrowed( diff --git a/crates/gpui_apple/Cargo.toml b/crates/gpui_apple/Cargo.toml index d88611fb24..87fec14653 100644 --- a/crates/gpui_apple/Cargo.toml +++ b/crates/gpui_apple/Cargo.toml @@ -24,7 +24,6 @@ gpui.workspace = true anyhow.workspace = true block.workspace = true cocoa.workspace = true -collections.workspace = true core-foundation.workspace = true core-video.workspace = true derive_more.workspace = true diff --git a/crates/gpui_apple/src/metal_atlas.rs b/crates/gpui_apple/src/metal_atlas.rs index 9cfd445ba8..faba138809 100644 --- a/crates/gpui_apple/src/metal_atlas.rs +++ b/crates/gpui_apple/src/metal_atlas.rs @@ -1,74 +1,71 @@ use anyhow::{Context as _, Result}; -use collections::FxHashMap; use derive_more::{Deref, DerefMut}; use etagere::BucketedAtlasAllocator; use gpui::{ - AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTextureList, AtlasTile, Bounds, DevicePixels, - PlatformAtlas, Point, Size, + AtlasBackend, AtlasKey, AtlasState, AtlasTextureId, AtlasTextureKind, AtlasTextureList, + AtlasTile, Bounds, DevicePixels, PlatformAtlas, Point, Size, }; use metal::Device; use parking_lot::Mutex; use std::borrow::Cow; -pub struct MetalAtlas(Mutex); +pub struct MetalAtlas(Mutex>); impl MetalAtlas { pub(crate) fn new(device: Device, is_apple_gpu: bool) -> Self { - MetalAtlas(Mutex::new(MetalAtlasState { + MetalAtlas(Mutex::new(AtlasState::new(MetalAtlasTextures { device: AssertSend(device), is_apple_gpu, monochrome_textures: Default::default(), polychrome_textures: Default::default(), - tiles_by_key: Default::default(), - })) + }))) } pub(crate) fn metal_texture(&self, id: AtlasTextureId) -> metal::Texture { - self.0.lock().texture(id).metal_texture.clone() + self.0.lock().backend.texture(id).metal_texture.clone() } } -struct MetalAtlasState { +struct MetalAtlasTextures { device: AssertSend, is_apple_gpu: bool, monochrome_textures: AtlasTextureList, polychrome_textures: AtlasTextureList, - tiles_by_key: FxHashMap, } impl PlatformAtlas for MetalAtlas { fn get_or_insert_with<'a>( &self, - key: &AtlasKey, + 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)) - } else { - let Some((size, bytes)) = build()? else { - return Ok(None); - }; - let tile = lock - .allocate(size, key.texture_kind()) - .context("failed to allocate")?; - let texture = lock.texture(tile.texture_id); - texture.upload(tile.bounds, &bytes); - lock.tiles_by_key.insert(key.clone(), tile); - Ok(Some(tile)) - } + self.0.lock().get_or_insert_with(key, build) } fn remove(&self, key: &AtlasKey) { - let mut lock = self.0.lock(); - let Some(tile) = lock.tiles_by_key.remove(key) else { - return; - }; + self.0.lock().remove(key); + } +} + +impl AtlasBackend for MetalAtlasTextures { + fn insert( + &mut self, + kind: AtlasTextureKind, + size: Size, + bytes: &[u8], + ) -> Result { + let tile = self.allocate(size, kind).context("failed to allocate")?; + let texture = self.texture(tile.texture_id); + texture.upload(tile.bounds, bytes); + Ok(tile) + } + + fn remove(&mut self, tile: AtlasTile) { let id = tile.texture_id; let textures = match id.kind { - AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, - AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, + AtlasTextureKind::Monochrome => &mut self.monochrome_textures, + AtlasTextureKind::Polychrome => &mut self.polychrome_textures, AtlasTextureKind::Subpixel => unreachable!(), }; @@ -92,7 +89,7 @@ impl PlatformAtlas for MetalAtlas { } } -impl MetalAtlasState { +impl MetalAtlasTextures { fn allocate( &mut self, size: Size, @@ -290,7 +287,7 @@ mod tests { }) } - fn insert_tile(atlas: &MetalAtlas, key: &AtlasKey, size: Size) -> AtlasTile { + 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; @@ -315,9 +312,9 @@ mod tests { 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); + 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); @@ -334,7 +331,7 @@ mod tests { // 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); + 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); @@ -359,12 +356,12 @@ mod tests { 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, big); + 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); + let tile_b = insert_tile(&atlas, big_key_b, big); assert_eq!(tile_b.texture_id, keeper_tile.texture_id); } diff --git a/crates/gpui_linux/src/linux/headless/window.rs b/crates/gpui_linux/src/linux/headless/window.rs index 42575c385c..35096d922b 100644 --- a/crates/gpui_linux/src/linux/headless/window.rs +++ b/crates/gpui_linux/src/linux/headless/window.rs @@ -2,24 +2,22 @@ //! //! A headless window has no compositor surface and no GPU: layout, text //! shaping, and entity plumbing run normally, `draw` discards the scene, and -//! the sprite atlas hands out tiles without uploading pixels (mirroring -//! GPUI's `TestWindow`/`TestAtlas`). This lets command-line tools drive real -//! `Window`-based code paths without a display server. +//! the sprite atlas hands out tiles without uploading pixels (the same +//! `HeadlessAtlas` GPUI's `TestWindow` uses). This lets command-line tools +//! drive real `Window`-based code paths without a display server. use std::cell::RefCell; use std::rc::Rc; use std::sync::Arc; -use collections::HashMap; -use parking_lot::Mutex; use uuid::Uuid; use gpui::{ - AtlasKey, AtlasTextureId, AtlasTile, Bounds, Capslock, DevicePixels, DispatchEventResult, - DisplayId, GpuSpecs, Modifiers, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, - PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, - Scene, Size, TileId, WindowAppearance, WindowBackgroundAppearance, WindowBounds, - WindowControlArea, WindowParams, WindowVisibility, px, + Bounds, Capslock, DispatchEventResult, DisplayId, GpuSpecs, HeadlessAtlas, Modifiers, Pixels, + PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, + PromptButton, PromptLevel, RequestFrameOptions, Scene, Size, WindowAppearance, + WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams, WindowVisibility, + px, }; #[derive(Debug)] @@ -236,59 +234,3 @@ impl PlatformWindow for HeadlessWindow { None } } - -/// Allocates atlas tiles without uploading pixels, so glyph and sprite -/// painting completes headlessly. -#[derive(Default)] -struct HeadlessAtlas(Mutex); - -#[derive(Default)] -struct HeadlessAtlasState { - next_id: u32, - tiles: HashMap, -} - -impl PlatformAtlas for HeadlessAtlas { - fn get_or_insert_with<'a>( - &self, - key: &AtlasKey, - build: &mut dyn FnMut() -> anyhow::Result< - Option<(Size, std::borrow::Cow<'a, [u8]>)>, - >, - ) -> anyhow::Result> { - { - let state = self.0.lock(); - if let Some(&tile) = state.tiles.get(key) { - return Ok(Some(tile)); - } - } - - let Some((size, _)) = build()? else { - return Ok(None); - }; - - let mut state = self.0.lock(); - state.next_id += 1; - let texture_id = state.next_id; - state.next_id += 1; - let tile_id = state.next_id; - let tile = AtlasTile { - texture_id: AtlasTextureId { - index: texture_id, - kind: key.texture_kind(), - }, - tile_id: TileId(tile_id), - padding: 0, - bounds: Bounds { - origin: Point::default(), - size, - }, - }; - state.tiles.insert(key.clone(), tile); - Ok(Some(tile)) - } - - fn remove(&self, key: &AtlasKey) { - self.0.lock().tiles.remove(key); - } -} diff --git a/crates/gpui_wgpu/src/wgpu_atlas.rs b/crates/gpui_wgpu/src/wgpu_atlas.rs index 3cf71ac98c..90731a33ee 100644 --- a/crates/gpui_wgpu/src/wgpu_atlas.rs +++ b/crates/gpui_wgpu/src/wgpu_atlas.rs @@ -1,9 +1,8 @@ use anyhow::{Context as _, Result}; -use collections::FxHashMap; use etagere::{BucketedAtlasAllocator, size2}; use gpui::{ - AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTextureList, AtlasTile, Bounds, DevicePixels, - PlatformAtlas, Point, Size, + AtlasBackend, AtlasKey, AtlasState, AtlasTextureId, AtlasTextureKind, AtlasTextureList, + AtlasTile, Bounds, DevicePixels, PlatformAtlas, Point, Size, }; use parking_lot::Mutex; use std::{borrow::Cow, ops, sync::Arc}; @@ -21,7 +20,7 @@ fn etagere_point_to_device(point: etagere::Point) -> Point { } } -pub struct WgpuAtlas(Mutex); +pub struct WgpuAtlas(Mutex>); struct PendingUpload { id: AtlasTextureId, @@ -29,13 +28,12 @@ struct PendingUpload { data: Vec, } -struct WgpuAtlasState { +struct WgpuAtlasTextures { device: Arc, queue: Arc, max_texture_size: u32, color_texture_format: wgpu::TextureFormat, storage: WgpuAtlasStorage, - tiles_by_key: FxHashMap, pending_uploads: Vec, } @@ -50,15 +48,14 @@ impl WgpuAtlas { color_texture_format: wgpu::TextureFormat, ) -> Self { let max_texture_size = device.limits().max_texture_dimension_2d; - WgpuAtlas(Mutex::new(WgpuAtlasState { + WgpuAtlas(Mutex::new(AtlasState::new(WgpuAtlasTextures { device, queue, max_texture_size, color_texture_format, storage: WgpuAtlasStorage::default(), - tiles_by_key: Default::default(), pending_uploads: Vec::new(), - })) + }))) } pub fn from_context(context: &WgpuContext) -> Self { @@ -71,12 +68,12 @@ impl WgpuAtlas { pub fn before_frame(&self) { let mut lock = self.0.lock(); - lock.flush_uploads(); + lock.backend.flush_uploads(); } pub fn get_texture_info(&self, id: AtlasTextureId) -> WgpuTextureInfo { let lock = self.0.lock(); - let texture = &lock.storage[id]; + let texture = &lock.backend.storage[id]; WgpuTextureInfo { view: texture.view.clone(), } @@ -85,57 +82,54 @@ impl WgpuAtlas { /// Clears all cached textures and tiles, forcing them to be recreated. /// Use this for incremental recovery when the device is still valid. pub fn clear(&self) { - let mut lock = self.0.lock(); - lock.storage = WgpuAtlasStorage::default(); - lock.tiles_by_key.clear(); - lock.pending_uploads.clear(); + self.0.lock().clear(|textures| { + textures.storage = WgpuAtlasStorage::default(); + textures.pending_uploads.clear(); + }); } /// Handles device lost by clearing all textures and cached tiles. /// The atlas will lazily recreate textures as needed on subsequent frames. pub fn handle_device_lost(&self, context: &WgpuContext) { - let mut lock = self.0.lock(); - lock.device = context.device.clone(); - lock.queue = context.queue.clone(); - lock.color_texture_format = context.color_texture_format(); - lock.storage = WgpuAtlasStorage::default(); - lock.tiles_by_key.clear(); - lock.pending_uploads.clear(); + self.0.lock().clear(|textures| { + textures.device = context.device.clone(); + textures.queue = context.queue.clone(); + textures.color_texture_format = context.color_texture_format(); + textures.storage = WgpuAtlasStorage::default(); + textures.pending_uploads.clear(); + }); } } impl PlatformAtlas for WgpuAtlas { fn get_or_insert_with<'a>( &self, - key: &AtlasKey, + 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)) - } else { - profiling::scope!("new tile"); - let Some((size, bytes)) = build()? else { - return Ok(None); - }; - let tile = lock - .allocate(size, key.texture_kind()) - .context("failed to allocate")?; - lock.upload_texture(tile.texture_id, tile.bounds, &bytes); - lock.tiles_by_key.insert(key.clone(), tile); - Ok(Some(tile)) - } + self.0.lock().get_or_insert_with(key, build) } fn remove(&self, key: &AtlasKey) { - let mut lock = self.0.lock(); + self.0.lock().remove(key); + } +} - let Some(tile) = lock.tiles_by_key.remove(key) else { - return; - }; +impl AtlasBackend for WgpuAtlasTextures { + fn insert( + &mut self, + kind: AtlasTextureKind, + size: Size, + bytes: &[u8], + ) -> Result { + let tile = self.allocate(size, kind).context("failed to allocate")?; + self.upload_texture(tile.texture_id, tile.bounds, bytes); + Ok(tile) + } + + fn remove(&mut self, tile: AtlasTile) { let id = tile.texture_id; - - let Some(texture_slot) = lock.storage[id.kind].textures.get_mut(id.index as usize) else { + let Some(texture_slot) = self.storage[id.kind].textures.get_mut(id.index as usize) else { return; }; @@ -143,9 +137,9 @@ impl PlatformAtlas for WgpuAtlas { texture.allocator.deallocate(tile.tile_id.into()); texture.decrement_ref_count(); if texture.is_unreferenced() { - lock.pending_uploads + self.pending_uploads .retain(|upload| upload.id != texture.id); - lock.storage[id.kind] + self.storage[id.kind] .free_list .push(texture.id.index as usize); } else { @@ -155,7 +149,7 @@ impl PlatformAtlas for WgpuAtlas { } } -impl WgpuAtlasState { +impl WgpuAtlasTextures { fn allocate( &mut self, size: Size, @@ -456,7 +450,7 @@ mod tests { // Regression test: before the fix, this panicked in flush_uploads atlas - .get_or_insert_with(&key, &mut build)? + .get_or_insert_with(key.clone(), &mut build)? .expect("tile should be created"); atlas.remove(&key); atlas.before_frame(); @@ -483,7 +477,7 @@ mod tests { frame_index: 0, }) }; - let insert = |key: &AtlasKey, size: Size| { + let insert = |key: AtlasKey, size: Size| { let byte_count = (size.width.0 as usize) * (size.height.0 as usize) * 4; atlas .get_or_insert_with(key, &mut || { @@ -497,12 +491,12 @@ mod tests { let big_key_a = make_key(2); let big_key_b = make_key(3); - let keeper_tile = insert(&keeper_key, small); - let tile_a = insert(&big_key_a, big); + let keeper_tile = insert(keeper_key, small); + let tile_a = insert(big_key_a.clone(), big); assert_eq!(keeper_tile.texture_id, tile_a.texture_id); atlas.remove(&big_key_a); - let tile_b = insert(&big_key_b, big); + let tile_b = insert(big_key_b, big); assert_eq!(tile_b.texture_id, keeper_tile.texture_id); Ok(()) } diff --git a/crates/gpui_windows/src/directx_atlas.rs b/crates/gpui_windows/src/directx_atlas.rs index 62386997f5..a4ebd3c9b0 100644 --- a/crates/gpui_windows/src/directx_atlas.rs +++ b/crates/gpui_windows/src/directx_atlas.rs @@ -1,4 +1,3 @@ -use collections::FxHashMap; use etagere::BucketedAtlasAllocator; use parking_lot::Mutex; use windows::Win32::Graphics::{ @@ -10,19 +9,18 @@ use windows::Win32::Graphics::{ }; use gpui::{ - AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTextureList, AtlasTile, Bounds, DevicePixels, - PlatformAtlas, Point, Size, + AtlasBackend, AtlasKey, AtlasState, AtlasTextureId, AtlasTextureKind, AtlasTextureList, + AtlasTile, Bounds, DevicePixels, PlatformAtlas, Point, Size, }; -pub(crate) struct DirectXAtlas(Mutex); +pub(crate) struct DirectXAtlas(Mutex>); -struct DirectXAtlasState { +struct DirectXAtlasTextures { device: ID3D11Device, device_context: ID3D11DeviceContext, monochrome_textures: AtlasTextureList, polychrome_textures: AtlasTextureList, subpixel_textures: AtlasTextureList, - tiles_by_key: FxHashMap, } struct DirectXAtlasTexture { @@ -36,14 +34,13 @@ struct DirectXAtlasTexture { impl DirectXAtlas { pub(crate) fn new(device: &ID3D11Device, device_context: &ID3D11DeviceContext) -> Self { - DirectXAtlas(Mutex::new(DirectXAtlasState { + DirectXAtlas(Mutex::new(AtlasState::new(DirectXAtlasTextures { device: device.clone(), device_context: device_context.clone(), monochrome_textures: Default::default(), polychrome_textures: Default::default(), subpixel_textures: Default::default(), - tiles_by_key: Default::default(), - })) + }))) } pub(crate) fn get_texture_view( @@ -51,8 +48,8 @@ impl DirectXAtlas { id: AtlasTextureId, ) -> [Option; 1] { let lock = self.0.lock(); - let tex = lock.texture(id); - tex.view.clone() + let texture = lock.backend.texture(id); + texture.view.clone() } pub(crate) fn handle_device_lost( @@ -61,52 +58,54 @@ impl DirectXAtlas { device_context: &ID3D11DeviceContext, ) { let mut lock = self.0.lock(); - lock.device = device.clone(); - lock.device_context = device_context.clone(); - lock.monochrome_textures = AtlasTextureList::default(); - lock.polychrome_textures = AtlasTextureList::default(); - lock.subpixel_textures = AtlasTextureList::default(); - lock.tiles_by_key.clear(); + lock.clear(|textures| { + textures.device = device.clone(); + textures.device_context = device_context.clone(); + textures.monochrome_textures = AtlasTextureList::default(); + textures.polychrome_textures = AtlasTextureList::default(); + textures.subpixel_textures = AtlasTextureList::default(); + }); } } impl PlatformAtlas for DirectXAtlas { fn get_or_insert_with<'a>( &self, - key: &AtlasKey, + key: AtlasKey, build: &mut dyn FnMut() -> anyhow::Result< Option<(Size, std::borrow::Cow<'a, [u8]>)>, >, ) -> anyhow::Result> { - let mut lock = self.0.lock(); - if let Some(tile) = lock.tiles_by_key.get(key) { - Ok(Some(*tile)) - } else { - let Some((size, bytes)) = build()? else { - return Ok(None); - }; - let tile = lock - .allocate(size, key.texture_kind()) - .ok_or_else(|| anyhow::anyhow!("failed to allocate"))?; - let texture = lock.texture(tile.texture_id); - texture.upload(&lock.device_context, tile.bounds, &bytes); - lock.tiles_by_key.insert(key.clone(), tile); - Ok(Some(tile)) - } + self.0.lock().get_or_insert_with(key, build) } fn remove(&self, key: &AtlasKey) { - let mut lock = self.0.lock(); + self.0.lock().remove(key); + } +} - let Some(tile) = lock.tiles_by_key.remove(key) else { - return; - }; +impl AtlasBackend for DirectXAtlasTextures { + fn insert( + &mut self, + kind: AtlasTextureKind, + size: Size, + bytes: &[u8], + ) -> anyhow::Result { + let tile = self + .allocate(size, kind) + .ok_or_else(|| anyhow::anyhow!("failed to allocate"))?; + let texture = self.texture(tile.texture_id); + texture.upload(&self.device_context, tile.bounds, bytes); + Ok(tile) + } + + fn remove(&mut self, tile: AtlasTile) { let id = tile.texture_id; let textures = match id.kind { - AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, - AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, - AtlasTextureKind::Subpixel => &mut lock.subpixel_textures, + AtlasTextureKind::Monochrome => &mut self.monochrome_textures, + AtlasTextureKind::Polychrome => &mut self.polychrome_textures, + AtlasTextureKind::Subpixel => &mut self.subpixel_textures, }; let Some(texture_slot) = textures.textures.get_mut(id.index as usize) else { @@ -125,7 +124,7 @@ impl PlatformAtlas for DirectXAtlas { } } -impl DirectXAtlasState { +impl DirectXAtlasTextures { fn allocate( &mut self, size: Size, @@ -379,7 +378,7 @@ mod tests { }) } - fn insert_tile(atlas: &DirectXAtlas, key: &AtlasKey, size: Size) -> AtlasTile { + fn insert_tile(atlas: &DirectXAtlas, 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; @@ -408,13 +407,13 @@ mod tests { let big_key_a = make_image_key(2); let big_key_b = make_image_key(3); - let keeper_tile = insert_tile(&atlas, &keeper_key, small); - let tile_a = insert_tile(&atlas, &big_key_a, big); + 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); + let tile_b = insert_tile(&atlas, big_key_b, big); assert_eq!(tile_b.texture_id, keeper_tile.texture_id); } } diff --git a/crates/gpui_windows/src/directx_renderer.rs b/crates/gpui_windows/src/directx_renderer.rs index 8590407e61..3f53556e1d 100644 --- a/crates/gpui_windows/src/directx_renderer.rs +++ b/crates/gpui_windows/src/directx_renderer.rs @@ -417,7 +417,7 @@ impl DirectXRenderer { ) -> Result { // A pending device-lost recovery (`skip_draws`) leaves the atlas holding // tile references from the previous device; drawing before the forced - // re-render rebuilds them panics in `DirectXAtlasState::texture`. + // re-render rebuilds them panics in `DirectXAtlasTextures::texture`. anyhow::ensure!( !self.skip_draws, "render_to_image unavailable while recovering from a lost device" diff --git a/crates/gpui_windows/src/window.rs b/crates/gpui_windows/src/window.rs index d6b02d78b4..73f56d7a54 100644 --- a/crates/gpui_windows/src/window.rs +++ b/crates/gpui_windows/src/window.rs @@ -68,7 +68,7 @@ pub struct WindowsWindowState { /// render. Used after a GPU device-lost recovery, where the next frame /// must both re-enable drawing (via `mark_drawable`) and bypass the GPUI /// view cache (which would otherwise replay stale atlas tile references - /// from the previous frame and panic in `DirectXAtlasState::texture`), + /// from the previous frame and panic in `DirectXAtlasTextures::texture`), /// and when a forced render was requested while another draw was in /// progress and had to be deferred. pub force_render_pending: Cell,