gpui: Improve PlatformAtlas::get_or_insert_with (#64331)

Stumbled across this during a recent review:

At all of the call sites in prod code, we would clone, then into, then
pass by ref, and then immediately clone again in `get_or_insert_with`
when the texture was not found.

Instead, changed the API to now take an owned `AtlasKey` to make better
use of the cloning.

As a second step, went ahead and added a generic `AtlasState`, as I
noticed all atlases shared some similar logic around keeping track of
the tiles. With this restructing, we reuse some code across different
backends, but more importantly, get rid of an additional hashmap lookup
by instead using the `Entry` API.

Release Notes:

- N/A
- [GPUI] N/A
This commit is contained in:
Finn Evers authored and GitHub committed 2026-09-17 08:24:25 +00:00
1 parent 2ce3fc96e8
commit 26fb9817c4
11 files changed
+400 -297

No files matched your search

Generated
-1
View File
@@ -7585,7 +7585,6 @@ dependencies = [
"block",
"cbindgen",
"cocoa 0.26.0",
"collections",
"core-foundation 0.10.0",
"core-video",
"derive_more",
+252 -8
View File
@@ -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<RenderImageParams> 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<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
) -> Result<Option<AtlasTile>>;
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<DevicePixels>,
bytes: &[u8],
) -> Result<AtlasTile>;
fn remove(&mut self, tile: AtlasTile);
}
#[doc(hidden)]
pub struct AtlasState<Backend> {
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
pub backend: Backend,
}
impl<Backend> AtlasState<Backend> {
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<Backend: Default> Default for AtlasState<Backend> {
fn default() -> Self {
Self::new(Backend::default())
}
}
impl<Backend: AtlasBackend> AtlasState<Backend> {
pub fn get_or_insert_with<'a>(
&mut self,
key: AtlasKey,
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
) -> Result<Option<AtlasTile>> {
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<AtlasState<HeadlessAtlasBackend>>);
#[doc(hidden)]
#[derive(Default)]
pub struct HeadlessAtlasBackend {
next_id: u32,
}
impl AtlasBackend for HeadlessAtlasBackend {
fn insert(
&mut self,
kind: AtlasTextureKind,
size: Size<DevicePixels>,
_bytes: &[u8],
) -> Result<AtlasTile> {
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<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);
}
#[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<T> {
pub textures: Vec<Option<T>>,
@@ -3030,6 +3156,124 @@ mod image_tests {
}
}
#[cfg(test)]
mod atlas_tests {
use super::*;
const TILE_SIZE: Size<DevicePixels> = Size {
width: DevicePixels(1),
height: DevicePixels(1),
};
#[derive(Default)]
struct RecordingAtlasBackend {
insert_calls: u32,
fail_next_insert: bool,
removed_tiles: Vec<AtlasTile>,
}
impl AtlasBackend for RecordingAtlasBackend {
fn insert(
&mut self,
kind: AtlasTextureKind,
size: Size<DevicePixels>,
_bytes: &[u8],
) -> Result<AtlasTile> {
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<Option<(Size<DevicePixels>, 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::*;
+6 -77
View File
@@ -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<dyn PlatformAtlas> = 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<AtlasKey, AtlasTile>,
}
pub(crate) struct TestAtlas(Mutex<TestAtlasState>);
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<crate::DevicePixels>, std::borrow::Cow<'a, [u8]>)>,
>,
) -> anyhow::Result<Option<crate::AtlasTile>> {
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)
}
}
+4 -4
View File
@@ -4671,7 +4671,7 @@ impl Window {
if !raster_bounds.is_zero() {
let tile = self
.sprite_atlas
.get_or_insert_with(&params.clone().into(), &mut || {
.get_or_insert_with(params.clone().into(), &mut || {
let (size, bytes) = self.text_system().rasterize_glyph(&params)?;
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(&params.clone().into(), &mut || {
.get_or_insert_with(params.clone().into(), &mut || {
let (size, bytes) = self.text_system().rasterize_glyph(&params)?;
Ok(Some((size, Cow::Owned(bytes))))
})?
@@ -4814,7 +4814,7 @@ impl Window {
let Some(tile) =
self.sprite_atlas
.get_or_insert_with(&params.clone().into(), &mut || {
.get_or_insert_with(params.clone().into(), &mut || {
let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(&params, data)?
else {
return Ok(None);
@@ -4887,7 +4887,7 @@ impl Window {
let tile = self
.sprite_atlas
.get_or_insert_with(&params.into(), &mut || {
.get_or_insert_with(params.into(), &mut || {
Ok(Some((
data.size(frame_index),
Cow::Borrowed(
-1
View File
@@ -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
+38 -41
View File
@@ -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<MetalAtlasState>);
pub struct MetalAtlas(Mutex<AtlasState<MetalAtlasTextures>>);
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<Device>,
is_apple_gpu: bool,
monochrome_textures: AtlasTextureList<MetalAtlasTexture>,
polychrome_textures: AtlasTextureList<MetalAtlasTexture>,
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
}
impl PlatformAtlas for MetalAtlas {
fn get_or_insert_with<'a>(
&self,
key: &AtlasKey,
key: AtlasKey,
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
) -> Result<Option<AtlasTile>> {
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<DevicePixels>,
bytes: &[u8],
) -> Result<AtlasTile> {
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<DevicePixels>,
@@ -290,7 +287,7 @@ mod tests {
})
}
fn insert_tile(atlas: &MetalAtlas, key: &AtlasKey, size: Size<DevicePixels>) -> AtlasTile {
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;
@@ -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);
}
+8 -66
View File
@@ -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<HeadlessAtlasState>);
#[derive(Default)]
struct HeadlessAtlasState {
next_id: u32,
tiles: HashMap<AtlasKey, AtlasTile>,
}
impl PlatformAtlas for HeadlessAtlas {
fn get_or_insert_with<'a>(
&self,
key: &AtlasKey,
build: &mut dyn FnMut() -> anyhow::Result<
Option<(Size<DevicePixels>, std::borrow::Cow<'a, [u8]>)>,
>,
) -> anyhow::Result<Option<AtlasTile>> {
{
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);
}
}
+46 -52
View File
@@ -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<DevicePixels> {
}
}
pub struct WgpuAtlas(Mutex<WgpuAtlasState>);
pub struct WgpuAtlas(Mutex<AtlasState<WgpuAtlasTextures>>);
struct PendingUpload {
id: AtlasTextureId,
@@ -29,13 +28,12 @@ struct PendingUpload {
data: Vec<u8>,
}
struct WgpuAtlasState {
struct WgpuAtlasTextures {
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
max_texture_size: u32,
color_texture_format: wgpu::TextureFormat,
storage: WgpuAtlasStorage,
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
pending_uploads: Vec<PendingUpload>,
}
@@ -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<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
) -> Result<Option<AtlasTile>> {
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<DevicePixels>,
bytes: &[u8],
) -> Result<AtlasTile> {
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<DevicePixels>,
@@ -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<DevicePixels>| {
let insert = |key: AtlasKey, size: Size<DevicePixels>| {
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(())
}
+44 -45
View File
@@ -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<DirectXAtlasState>);
pub(crate) struct DirectXAtlas(Mutex<AtlasState<DirectXAtlasTextures>>);
struct DirectXAtlasState {
struct DirectXAtlasTextures {
device: ID3D11Device,
device_context: ID3D11DeviceContext,
monochrome_textures: AtlasTextureList<DirectXAtlasTexture>,
polychrome_textures: AtlasTextureList<DirectXAtlasTexture>,
subpixel_textures: AtlasTextureList<DirectXAtlasTexture>,
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
}
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<ID3D11ShaderResourceView>; 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<DevicePixels>, std::borrow::Cow<'a, [u8]>)>,
>,
) -> anyhow::Result<Option<AtlasTile>> {
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<DevicePixels>,
bytes: &[u8],
) -> anyhow::Result<AtlasTile> {
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<DevicePixels>,
@@ -379,7 +378,7 @@ mod tests {
})
}
fn insert_tile(atlas: &DirectXAtlas, key: &AtlasKey, size: Size<DevicePixels>) -> AtlasTile {
fn insert_tile(atlas: &DirectXAtlas, 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;
@@ -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);
}
}
+1 -1
View File
@@ -417,7 +417,7 @@ impl DirectXRenderer {
) -> Result<image::RgbaImage> {
// 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"
+1 -1
View File
@@ -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<bool>,