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:
1 parent
2ce3fc96e8
commit
26fb9817c4
11 files changed
+400
-297
No files matched your search
Generated
-1
@@ -7585,7 +7585,6 @@ dependencies = [
|
|||||||
"block",
|
"block",
|
||||||
"cbindgen",
|
"cbindgen",
|
||||||
"cocoa 0.26.0",
|
"cocoa 0.26.0",
|
||||||
"collections",
|
|
||||||
"core-foundation 0.10.0",
|
"core-foundation 0.10.0",
|
||||||
"core-video",
|
"core-video",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
|
|||||||
+252
-8
@@ -48,6 +48,7 @@ use crate::{
|
|||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use async_task::Runnable;
|
use async_task::Runnable;
|
||||||
|
use collections::FxHashMap;
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
|
#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
|
||||||
use image::RgbaImage;
|
use image::RgbaImage;
|
||||||
@@ -61,6 +62,7 @@ use seahash::SeaHasher;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::ops;
|
use std::ops;
|
||||||
@@ -1384,13 +1386,6 @@ pub enum AtlasKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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.
|
/// Returns the texture kind for this atlas key.
|
||||||
pub fn texture_kind(&self) -> AtlasTextureKind {
|
pub fn texture_kind(&self) -> AtlasTextureKind {
|
||||||
match self {
|
match self {
|
||||||
@@ -1429,9 +1424,10 @@ impl From<RenderImageParams> for AtlasKey {
|
|||||||
|
|
||||||
#[expect(missing_docs)]
|
#[expect(missing_docs)]
|
||||||
pub trait PlatformAtlas {
|
pub trait PlatformAtlas {
|
||||||
|
/// The builder runs with the atlas locked and must not re-enter the same atlas.
|
||||||
fn get_or_insert_with<'a>(
|
fn get_or_insert_with<'a>(
|
||||||
&self,
|
&self,
|
||||||
key: &AtlasKey,
|
key: AtlasKey,
|
||||||
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
|
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
|
||||||
) -> Result<Option<AtlasTile>>;
|
) -> Result<Option<AtlasTile>>;
|
||||||
fn remove(&self, key: &AtlasKey);
|
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)]
|
#[doc(hidden)]
|
||||||
pub struct AtlasTextureList<T> {
|
pub struct AtlasTextureList<T> {
|
||||||
pub textures: Vec<Option<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")))]
|
#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DevicePixels,
|
AnyWindowHandle, Bounds, DevicePixels, DispatchEventResult, GpuSpecs, HeadlessAtlas, Pixels,
|
||||||
DispatchEventResult, GpuSpecs, Pixels, PlatformAtlas, PlatformDisplay,
|
PlatformAtlas, PlatformDisplay, PlatformHeadlessRenderer, PlatformInput, PlatformInputHandler,
|
||||||
PlatformHeadlessRenderer, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
|
PlatformWindow, Point, PromptButton, RequestFrameOptions, Scene, Size, TestPlatform,
|
||||||
PromptButton, RequestFrameOptions, Scene, Size, TestPlatform, TextInputConfiguration,
|
TextInputConfiguration, TextInputStateChange, WindowAppearance, WindowBackgroundAppearance,
|
||||||
TextInputStateChange, TileId, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
|
WindowBounds, WindowControlArea, WindowInsets, WindowParams, WindowVisibility,
|
||||||
WindowControlArea, WindowInsets, WindowParams, WindowVisibility,
|
|
||||||
};
|
};
|
||||||
use collections::HashMap;
|
|
||||||
use gpui_util::ResultExt as _;
|
use gpui_util::ResultExt as _;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use image::RgbaImage;
|
use image::RgbaImage;
|
||||||
@@ -91,7 +89,7 @@ impl TestWindow {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
let sprite_atlas: Arc<dyn PlatformAtlas> = match &renderer {
|
let sprite_atlas: Arc<dyn PlatformAtlas> = match &renderer {
|
||||||
Some(r) => r.sprite_atlas(),
|
Some(r) => r.sprite_atlas(),
|
||||||
None => Arc::new(TestAtlas::new()),
|
None => Arc::new(HeadlessAtlas::default()),
|
||||||
};
|
};
|
||||||
Self(Rc::new(Mutex::new(TestWindowState {
|
Self(Rc::new(Mutex::new(TestWindowState {
|
||||||
bounds: params.bounds,
|
bounds: params.bounds,
|
||||||
@@ -591,72 +589,3 @@ impl PlatformWindow for TestWindow {
|
|||||||
None
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4671,7 +4671,7 @@ impl Window {
|
|||||||
if !raster_bounds.is_zero() {
|
if !raster_bounds.is_zero() {
|
||||||
let tile = self
|
let tile = self
|
||||||
.sprite_atlas
|
.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)?;
|
let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?;
|
||||||
Ok(Some((size, Cow::Owned(bytes))))
|
Ok(Some((size, Cow::Owned(bytes))))
|
||||||
})?
|
})?
|
||||||
@@ -4761,7 +4761,7 @@ impl Window {
|
|||||||
if !raster_bounds.is_zero() {
|
if !raster_bounds.is_zero() {
|
||||||
let tile = self
|
let tile = self
|
||||||
.sprite_atlas
|
.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)?;
|
let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?;
|
||||||
Ok(Some((size, Cow::Owned(bytes))))
|
Ok(Some((size, Cow::Owned(bytes))))
|
||||||
})?
|
})?
|
||||||
@@ -4814,7 +4814,7 @@ impl Window {
|
|||||||
|
|
||||||
let Some(tile) =
|
let Some(tile) =
|
||||||
self.sprite_atlas
|
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)?
|
let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(¶ms, data)?
|
||||||
else {
|
else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -4887,7 +4887,7 @@ impl Window {
|
|||||||
|
|
||||||
let tile = self
|
let tile = self
|
||||||
.sprite_atlas
|
.sprite_atlas
|
||||||
.get_or_insert_with(¶ms.into(), &mut || {
|
.get_or_insert_with(params.into(), &mut || {
|
||||||
Ok(Some((
|
Ok(Some((
|
||||||
data.size(frame_index),
|
data.size(frame_index),
|
||||||
Cow::Borrowed(
|
Cow::Borrowed(
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ gpui.workspace = true
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
block.workspace = true
|
block.workspace = true
|
||||||
cocoa.workspace = true
|
cocoa.workspace = true
|
||||||
collections.workspace = true
|
|
||||||
core-foundation.workspace = true
|
core-foundation.workspace = true
|
||||||
core-video.workspace = true
|
core-video.workspace = true
|
||||||
derive_more.workspace = true
|
derive_more.workspace = true
|
||||||
|
|||||||
@@ -1,74 +1,71 @@
|
|||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use collections::FxHashMap;
|
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
use etagere::BucketedAtlasAllocator;
|
use etagere::BucketedAtlasAllocator;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTextureList, AtlasTile, Bounds, DevicePixels,
|
AtlasBackend, AtlasKey, AtlasState, AtlasTextureId, AtlasTextureKind, AtlasTextureList,
|
||||||
PlatformAtlas, Point, Size,
|
AtlasTile, Bounds, DevicePixels, PlatformAtlas, Point, Size,
|
||||||
};
|
};
|
||||||
use metal::Device;
|
use metal::Device;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
pub struct MetalAtlas(Mutex<MetalAtlasState>);
|
pub struct MetalAtlas(Mutex<AtlasState<MetalAtlasTextures>>);
|
||||||
|
|
||||||
impl MetalAtlas {
|
impl MetalAtlas {
|
||||||
pub(crate) fn new(device: Device, is_apple_gpu: bool) -> Self {
|
pub(crate) fn new(device: Device, is_apple_gpu: bool) -> Self {
|
||||||
MetalAtlas(Mutex::new(MetalAtlasState {
|
MetalAtlas(Mutex::new(AtlasState::new(MetalAtlasTextures {
|
||||||
device: AssertSend(device),
|
device: AssertSend(device),
|
||||||
is_apple_gpu,
|
is_apple_gpu,
|
||||||
monochrome_textures: Default::default(),
|
monochrome_textures: Default::default(),
|
||||||
polychrome_textures: Default::default(),
|
polychrome_textures: Default::default(),
|
||||||
tiles_by_key: Default::default(),
|
})))
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn metal_texture(&self, id: AtlasTextureId) -> metal::Texture {
|
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>,
|
device: AssertSend<Device>,
|
||||||
is_apple_gpu: bool,
|
is_apple_gpu: bool,
|
||||||
monochrome_textures: AtlasTextureList<MetalAtlasTexture>,
|
monochrome_textures: AtlasTextureList<MetalAtlasTexture>,
|
||||||
polychrome_textures: AtlasTextureList<MetalAtlasTexture>,
|
polychrome_textures: AtlasTextureList<MetalAtlasTexture>,
|
||||||
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlatformAtlas for MetalAtlas {
|
impl PlatformAtlas for MetalAtlas {
|
||||||
fn get_or_insert_with<'a>(
|
fn get_or_insert_with<'a>(
|
||||||
&self,
|
&self,
|
||||||
key: &AtlasKey,
|
key: AtlasKey,
|
||||||
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
|
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
|
||||||
) -> Result<Option<AtlasTile>> {
|
) -> Result<Option<AtlasTile>> {
|
||||||
let mut lock = self.0.lock();
|
self.0.lock().get_or_insert_with(key, build)
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove(&self, key: &AtlasKey) {
|
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 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 id = tile.texture_id;
|
||||||
|
|
||||||
let textures = match id.kind {
|
let textures = match id.kind {
|
||||||
AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
|
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
|
||||||
AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
|
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
|
||||||
AtlasTextureKind::Subpixel => unreachable!(),
|
AtlasTextureKind::Subpixel => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,7 +89,7 @@ impl PlatformAtlas for MetalAtlas {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MetalAtlasState {
|
impl MetalAtlasTextures {
|
||||||
fn allocate(
|
fn allocate(
|
||||||
&mut self,
|
&mut self,
|
||||||
size: Size<DevicePixels>,
|
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
|
atlas
|
||||||
.get_or_insert_with(key, &mut || {
|
.get_or_insert_with(key, &mut || {
|
||||||
let byte_count = (size.width.0 as usize) * (size.height.0 as usize) * 4;
|
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_b = make_image_key(2, 0);
|
||||||
let key_c = make_image_key(3, 0);
|
let key_c = make_image_key(3, 0);
|
||||||
|
|
||||||
let tile_a = insert_tile(&atlas, &key_a, small);
|
let tile_a = insert_tile(&atlas, key_a.clone(), small);
|
||||||
let tile_b = insert_tile(&atlas, &key_b, small);
|
let tile_b = insert_tile(&atlas, key_b.clone(), small);
|
||||||
let tile_c = insert_tile(&atlas, &key_c, small);
|
let tile_c = insert_tile(&atlas, key_c.clone(), small);
|
||||||
|
|
||||||
assert_eq!(tile_a.texture_id, tile_b.texture_id);
|
assert_eq!(tile_a.texture_id, tile_b.texture_id);
|
||||||
assert_eq!(tile_b.texture_id, tile_c.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,
|
// Re-inserting A must allocate a fresh tile on a new texture,
|
||||||
// NOT return a stale tile referencing the deleted 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.
|
// The texture must actually exist — this would panic before the fix.
|
||||||
let _texture = atlas.metal_texture(tile_a2.texture_id);
|
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_a = make_image_key(2, 0);
|
||||||
let big_key_b = make_image_key(3, 0);
|
let big_key_b = make_image_key(3, 0);
|
||||||
|
|
||||||
let keeper_tile = insert_tile(&atlas, &keeper_key, small);
|
let keeper_tile = insert_tile(&atlas, keeper_key, small);
|
||||||
let tile_a = insert_tile(&atlas, &big_key_a, big);
|
let tile_a = insert_tile(&atlas, big_key_a.clone(), big);
|
||||||
assert_eq!(keeper_tile.texture_id, tile_a.texture_id);
|
assert_eq!(keeper_tile.texture_id, tile_a.texture_id);
|
||||||
|
|
||||||
atlas.remove(&big_key_a);
|
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);
|
assert_eq!(tile_b.texture_id, keeper_tile.texture_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,24 +2,22 @@
|
|||||||
//!
|
//!
|
||||||
//! A headless window has no compositor surface and no GPU: layout, text
|
//! A headless window has no compositor surface and no GPU: layout, text
|
||||||
//! shaping, and entity plumbing run normally, `draw` discards the scene, and
|
//! shaping, and entity plumbing run normally, `draw` discards the scene, and
|
||||||
//! the sprite atlas hands out tiles without uploading pixels (mirroring
|
//! the sprite atlas hands out tiles without uploading pixels (the same
|
||||||
//! GPUI's `TestWindow`/`TestAtlas`). This lets command-line tools drive real
|
//! `HeadlessAtlas` GPUI's `TestWindow` uses). This lets command-line tools
|
||||||
//! `Window`-based code paths without a display server.
|
//! drive real `Window`-based code paths without a display server.
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use collections::HashMap;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AtlasKey, AtlasTextureId, AtlasTile, Bounds, Capslock, DevicePixels, DispatchEventResult,
|
Bounds, Capslock, DispatchEventResult, DisplayId, GpuSpecs, HeadlessAtlas, Modifiers, Pixels,
|
||||||
DisplayId, GpuSpecs, Modifiers, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
|
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
|
||||||
PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions,
|
PromptButton, PromptLevel, RequestFrameOptions, Scene, Size, WindowAppearance,
|
||||||
Scene, Size, TileId, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
|
WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams, WindowVisibility,
|
||||||
WindowControlArea, WindowParams, WindowVisibility, px,
|
px,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -236,59 +234,3 @@ impl PlatformWindow for HeadlessWindow {
|
|||||||
None
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use collections::FxHashMap;
|
|
||||||
use etagere::{BucketedAtlasAllocator, size2};
|
use etagere::{BucketedAtlasAllocator, size2};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTextureList, AtlasTile, Bounds, DevicePixels,
|
AtlasBackend, AtlasKey, AtlasState, AtlasTextureId, AtlasTextureKind, AtlasTextureList,
|
||||||
PlatformAtlas, Point, Size,
|
AtlasTile, Bounds, DevicePixels, PlatformAtlas, Point, Size,
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{borrow::Cow, ops, sync::Arc};
|
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 {
|
struct PendingUpload {
|
||||||
id: AtlasTextureId,
|
id: AtlasTextureId,
|
||||||
@@ -29,13 +28,12 @@ struct PendingUpload {
|
|||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WgpuAtlasState {
|
struct WgpuAtlasTextures {
|
||||||
device: Arc<wgpu::Device>,
|
device: Arc<wgpu::Device>,
|
||||||
queue: Arc<wgpu::Queue>,
|
queue: Arc<wgpu::Queue>,
|
||||||
max_texture_size: u32,
|
max_texture_size: u32,
|
||||||
color_texture_format: wgpu::TextureFormat,
|
color_texture_format: wgpu::TextureFormat,
|
||||||
storage: WgpuAtlasStorage,
|
storage: WgpuAtlasStorage,
|
||||||
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
|
|
||||||
pending_uploads: Vec<PendingUpload>,
|
pending_uploads: Vec<PendingUpload>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,15 +48,14 @@ impl WgpuAtlas {
|
|||||||
color_texture_format: wgpu::TextureFormat,
|
color_texture_format: wgpu::TextureFormat,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let max_texture_size = device.limits().max_texture_dimension_2d;
|
let max_texture_size = device.limits().max_texture_dimension_2d;
|
||||||
WgpuAtlas(Mutex::new(WgpuAtlasState {
|
WgpuAtlas(Mutex::new(AtlasState::new(WgpuAtlasTextures {
|
||||||
device,
|
device,
|
||||||
queue,
|
queue,
|
||||||
max_texture_size,
|
max_texture_size,
|
||||||
color_texture_format,
|
color_texture_format,
|
||||||
storage: WgpuAtlasStorage::default(),
|
storage: WgpuAtlasStorage::default(),
|
||||||
tiles_by_key: Default::default(),
|
|
||||||
pending_uploads: Vec::new(),
|
pending_uploads: Vec::new(),
|
||||||
}))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_context(context: &WgpuContext) -> Self {
|
pub fn from_context(context: &WgpuContext) -> Self {
|
||||||
@@ -71,12 +68,12 @@ impl WgpuAtlas {
|
|||||||
|
|
||||||
pub fn before_frame(&self) {
|
pub fn before_frame(&self) {
|
||||||
let mut lock = self.0.lock();
|
let mut lock = self.0.lock();
|
||||||
lock.flush_uploads();
|
lock.backend.flush_uploads();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_texture_info(&self, id: AtlasTextureId) -> WgpuTextureInfo {
|
pub fn get_texture_info(&self, id: AtlasTextureId) -> WgpuTextureInfo {
|
||||||
let lock = self.0.lock();
|
let lock = self.0.lock();
|
||||||
let texture = &lock.storage[id];
|
let texture = &lock.backend.storage[id];
|
||||||
WgpuTextureInfo {
|
WgpuTextureInfo {
|
||||||
view: texture.view.clone(),
|
view: texture.view.clone(),
|
||||||
}
|
}
|
||||||
@@ -85,57 +82,54 @@ impl WgpuAtlas {
|
|||||||
/// Clears all cached textures and tiles, forcing them to be recreated.
|
/// Clears all cached textures and tiles, forcing them to be recreated.
|
||||||
/// Use this for incremental recovery when the device is still valid.
|
/// Use this for incremental recovery when the device is still valid.
|
||||||
pub fn clear(&self) {
|
pub fn clear(&self) {
|
||||||
let mut lock = self.0.lock();
|
self.0.lock().clear(|textures| {
|
||||||
lock.storage = WgpuAtlasStorage::default();
|
textures.storage = WgpuAtlasStorage::default();
|
||||||
lock.tiles_by_key.clear();
|
textures.pending_uploads.clear();
|
||||||
lock.pending_uploads.clear();
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles device lost by clearing all textures and cached tiles.
|
/// Handles device lost by clearing all textures and cached tiles.
|
||||||
/// The atlas will lazily recreate textures as needed on subsequent frames.
|
/// The atlas will lazily recreate textures as needed on subsequent frames.
|
||||||
pub fn handle_device_lost(&self, context: &WgpuContext) {
|
pub fn handle_device_lost(&self, context: &WgpuContext) {
|
||||||
let mut lock = self.0.lock();
|
self.0.lock().clear(|textures| {
|
||||||
lock.device = context.device.clone();
|
textures.device = context.device.clone();
|
||||||
lock.queue = context.queue.clone();
|
textures.queue = context.queue.clone();
|
||||||
lock.color_texture_format = context.color_texture_format();
|
textures.color_texture_format = context.color_texture_format();
|
||||||
lock.storage = WgpuAtlasStorage::default();
|
textures.storage = WgpuAtlasStorage::default();
|
||||||
lock.tiles_by_key.clear();
|
textures.pending_uploads.clear();
|
||||||
lock.pending_uploads.clear();
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlatformAtlas for WgpuAtlas {
|
impl PlatformAtlas for WgpuAtlas {
|
||||||
fn get_or_insert_with<'a>(
|
fn get_or_insert_with<'a>(
|
||||||
&self,
|
&self,
|
||||||
key: &AtlasKey,
|
key: AtlasKey,
|
||||||
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
|
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
|
||||||
) -> Result<Option<AtlasTile>> {
|
) -> Result<Option<AtlasTile>> {
|
||||||
let mut lock = self.0.lock();
|
self.0.lock().get_or_insert_with(key, build)
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove(&self, key: &AtlasKey) {
|
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 {
|
impl AtlasBackend for WgpuAtlasTextures {
|
||||||
return;
|
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 id = tile.texture_id;
|
||||||
|
let Some(texture_slot) = self.storage[id.kind].textures.get_mut(id.index as usize) else {
|
||||||
let Some(texture_slot) = lock.storage[id.kind].textures.get_mut(id.index as usize) else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -143,9 +137,9 @@ impl PlatformAtlas for WgpuAtlas {
|
|||||||
texture.allocator.deallocate(tile.tile_id.into());
|
texture.allocator.deallocate(tile.tile_id.into());
|
||||||
texture.decrement_ref_count();
|
texture.decrement_ref_count();
|
||||||
if texture.is_unreferenced() {
|
if texture.is_unreferenced() {
|
||||||
lock.pending_uploads
|
self.pending_uploads
|
||||||
.retain(|upload| upload.id != texture.id);
|
.retain(|upload| upload.id != texture.id);
|
||||||
lock.storage[id.kind]
|
self.storage[id.kind]
|
||||||
.free_list
|
.free_list
|
||||||
.push(texture.id.index as usize);
|
.push(texture.id.index as usize);
|
||||||
} else {
|
} else {
|
||||||
@@ -155,7 +149,7 @@ impl PlatformAtlas for WgpuAtlas {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WgpuAtlasState {
|
impl WgpuAtlasTextures {
|
||||||
fn allocate(
|
fn allocate(
|
||||||
&mut self,
|
&mut self,
|
||||||
size: Size<DevicePixels>,
|
size: Size<DevicePixels>,
|
||||||
@@ -456,7 +450,7 @@ mod tests {
|
|||||||
|
|
||||||
// Regression test: before the fix, this panicked in flush_uploads
|
// Regression test: before the fix, this panicked in flush_uploads
|
||||||
atlas
|
atlas
|
||||||
.get_or_insert_with(&key, &mut build)?
|
.get_or_insert_with(key.clone(), &mut build)?
|
||||||
.expect("tile should be created");
|
.expect("tile should be created");
|
||||||
atlas.remove(&key);
|
atlas.remove(&key);
|
||||||
atlas.before_frame();
|
atlas.before_frame();
|
||||||
@@ -483,7 +477,7 @@ mod tests {
|
|||||||
frame_index: 0,
|
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;
|
let byte_count = (size.width.0 as usize) * (size.height.0 as usize) * 4;
|
||||||
atlas
|
atlas
|
||||||
.get_or_insert_with(key, &mut || {
|
.get_or_insert_with(key, &mut || {
|
||||||
@@ -497,12 +491,12 @@ mod tests {
|
|||||||
let big_key_a = make_key(2);
|
let big_key_a = make_key(2);
|
||||||
let big_key_b = make_key(3);
|
let big_key_b = make_key(3);
|
||||||
|
|
||||||
let keeper_tile = insert(&keeper_key, small);
|
let keeper_tile = insert(keeper_key, small);
|
||||||
let tile_a = insert(&big_key_a, big);
|
let tile_a = insert(big_key_a.clone(), big);
|
||||||
assert_eq!(keeper_tile.texture_id, tile_a.texture_id);
|
assert_eq!(keeper_tile.texture_id, tile_a.texture_id);
|
||||||
|
|
||||||
atlas.remove(&big_key_a);
|
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);
|
assert_eq!(tile_b.texture_id, keeper_tile.texture_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use collections::FxHashMap;
|
|
||||||
use etagere::BucketedAtlasAllocator;
|
use etagere::BucketedAtlasAllocator;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use windows::Win32::Graphics::{
|
use windows::Win32::Graphics::{
|
||||||
@@ -10,19 +9,18 @@ use windows::Win32::Graphics::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTextureList, AtlasTile, Bounds, DevicePixels,
|
AtlasBackend, AtlasKey, AtlasState, AtlasTextureId, AtlasTextureKind, AtlasTextureList,
|
||||||
PlatformAtlas, Point, Size,
|
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: ID3D11Device,
|
||||||
device_context: ID3D11DeviceContext,
|
device_context: ID3D11DeviceContext,
|
||||||
monochrome_textures: AtlasTextureList<DirectXAtlasTexture>,
|
monochrome_textures: AtlasTextureList<DirectXAtlasTexture>,
|
||||||
polychrome_textures: AtlasTextureList<DirectXAtlasTexture>,
|
polychrome_textures: AtlasTextureList<DirectXAtlasTexture>,
|
||||||
subpixel_textures: AtlasTextureList<DirectXAtlasTexture>,
|
subpixel_textures: AtlasTextureList<DirectXAtlasTexture>,
|
||||||
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DirectXAtlasTexture {
|
struct DirectXAtlasTexture {
|
||||||
@@ -36,14 +34,13 @@ struct DirectXAtlasTexture {
|
|||||||
|
|
||||||
impl DirectXAtlas {
|
impl DirectXAtlas {
|
||||||
pub(crate) fn new(device: &ID3D11Device, device_context: &ID3D11DeviceContext) -> Self {
|
pub(crate) fn new(device: &ID3D11Device, device_context: &ID3D11DeviceContext) -> Self {
|
||||||
DirectXAtlas(Mutex::new(DirectXAtlasState {
|
DirectXAtlas(Mutex::new(AtlasState::new(DirectXAtlasTextures {
|
||||||
device: device.clone(),
|
device: device.clone(),
|
||||||
device_context: device_context.clone(),
|
device_context: device_context.clone(),
|
||||||
monochrome_textures: Default::default(),
|
monochrome_textures: Default::default(),
|
||||||
polychrome_textures: Default::default(),
|
polychrome_textures: Default::default(),
|
||||||
subpixel_textures: Default::default(),
|
subpixel_textures: Default::default(),
|
||||||
tiles_by_key: Default::default(),
|
})))
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_texture_view(
|
pub(crate) fn get_texture_view(
|
||||||
@@ -51,8 +48,8 @@ impl DirectXAtlas {
|
|||||||
id: AtlasTextureId,
|
id: AtlasTextureId,
|
||||||
) -> [Option<ID3D11ShaderResourceView>; 1] {
|
) -> [Option<ID3D11ShaderResourceView>; 1] {
|
||||||
let lock = self.0.lock();
|
let lock = self.0.lock();
|
||||||
let tex = lock.texture(id);
|
let texture = lock.backend.texture(id);
|
||||||
tex.view.clone()
|
texture.view.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn handle_device_lost(
|
pub(crate) fn handle_device_lost(
|
||||||
@@ -61,52 +58,54 @@ impl DirectXAtlas {
|
|||||||
device_context: &ID3D11DeviceContext,
|
device_context: &ID3D11DeviceContext,
|
||||||
) {
|
) {
|
||||||
let mut lock = self.0.lock();
|
let mut lock = self.0.lock();
|
||||||
lock.device = device.clone();
|
lock.clear(|textures| {
|
||||||
lock.device_context = device_context.clone();
|
textures.device = device.clone();
|
||||||
lock.monochrome_textures = AtlasTextureList::default();
|
textures.device_context = device_context.clone();
|
||||||
lock.polychrome_textures = AtlasTextureList::default();
|
textures.monochrome_textures = AtlasTextureList::default();
|
||||||
lock.subpixel_textures = AtlasTextureList::default();
|
textures.polychrome_textures = AtlasTextureList::default();
|
||||||
lock.tiles_by_key.clear();
|
textures.subpixel_textures = AtlasTextureList::default();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlatformAtlas for DirectXAtlas {
|
impl PlatformAtlas for DirectXAtlas {
|
||||||
fn get_or_insert_with<'a>(
|
fn get_or_insert_with<'a>(
|
||||||
&self,
|
&self,
|
||||||
key: &AtlasKey,
|
key: AtlasKey,
|
||||||
build: &mut dyn FnMut() -> anyhow::Result<
|
build: &mut dyn FnMut() -> anyhow::Result<
|
||||||
Option<(Size<DevicePixels>, std::borrow::Cow<'a, [u8]>)>,
|
Option<(Size<DevicePixels>, std::borrow::Cow<'a, [u8]>)>,
|
||||||
>,
|
>,
|
||||||
) -> anyhow::Result<Option<AtlasTile>> {
|
) -> anyhow::Result<Option<AtlasTile>> {
|
||||||
let mut lock = self.0.lock();
|
self.0.lock().get_or_insert_with(key, build)
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove(&self, key: &AtlasKey) {
|
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 {
|
impl AtlasBackend for DirectXAtlasTextures {
|
||||||
return;
|
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 id = tile.texture_id;
|
||||||
|
|
||||||
let textures = match id.kind {
|
let textures = match id.kind {
|
||||||
AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
|
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
|
||||||
AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
|
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
|
||||||
AtlasTextureKind::Subpixel => &mut lock.subpixel_textures,
|
AtlasTextureKind::Subpixel => &mut self.subpixel_textures,
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(texture_slot) = textures.textures.get_mut(id.index as usize) else {
|
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(
|
fn allocate(
|
||||||
&mut self,
|
&mut self,
|
||||||
size: Size<DevicePixels>,
|
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
|
atlas
|
||||||
.get_or_insert_with(key, &mut || {
|
.get_or_insert_with(key, &mut || {
|
||||||
let byte_count = (size.width.0 as usize) * (size.height.0 as usize) * 4;
|
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_a = make_image_key(2);
|
||||||
let big_key_b = make_image_key(3);
|
let big_key_b = make_image_key(3);
|
||||||
|
|
||||||
let keeper_tile = insert_tile(&atlas, &keeper_key, small);
|
let keeper_tile = insert_tile(&atlas, keeper_key, small);
|
||||||
let tile_a = insert_tile(&atlas, &big_key_a, big);
|
let tile_a = insert_tile(&atlas, big_key_a.clone(), big);
|
||||||
assert_eq!(keeper_tile.texture_id, tile_a.texture_id);
|
assert_eq!(keeper_tile.texture_id, tile_a.texture_id);
|
||||||
|
|
||||||
atlas.remove(&big_key_a);
|
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);
|
assert_eq!(tile_b.texture_id, keeper_tile.texture_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,7 +417,7 @@ impl DirectXRenderer {
|
|||||||
) -> Result<image::RgbaImage> {
|
) -> Result<image::RgbaImage> {
|
||||||
// A pending device-lost recovery (`skip_draws`) leaves the atlas holding
|
// A pending device-lost recovery (`skip_draws`) leaves the atlas holding
|
||||||
// tile references from the previous device; drawing before the forced
|
// 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!(
|
anyhow::ensure!(
|
||||||
!self.skip_draws,
|
!self.skip_draws,
|
||||||
"render_to_image unavailable while recovering from a lost device"
|
"render_to_image unavailable while recovering from a lost device"
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ pub struct WindowsWindowState {
|
|||||||
/// render. Used after a GPU device-lost recovery, where the next frame
|
/// render. Used after a GPU device-lost recovery, where the next frame
|
||||||
/// must both re-enable drawing (via `mark_drawable`) and bypass the GPUI
|
/// must both re-enable drawing (via `mark_drawable`) and bypass the GPUI
|
||||||
/// view cache (which would otherwise replay stale atlas tile references
|
/// 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
|
/// and when a forced render was requested while another draw was in
|
||||||
/// progress and had to be deferred.
|
/// progress and had to be deferred.
|
||||||
pub force_render_pending: Cell<bool>,
|
pub force_render_pending: Cell<bool>,
|
||||||
|
|||||||
Reference in new issue
Block a user