wgpu: Cache atlas bind groups (#64434)

Previously, we created texture atlas bind groups every frame (once per
sprite group). This showed up in delta web mobile profiling.

This PR reuses them across frames, which saved ~5% frame time on the
delta web thread view on a pixel 10 pro.

---

Release Notes:

- [GPUI]: Improved performance on wgpu by caching atlas bind groups
This commit is contained in:
Cameron Mcloughlin authored and GitHub committed 2026-09-21 19:24:38 +00:00
1 parent 329142ebfc
commit 5795c06954
2 files changed
+128 -22

No files matched your search

+48
View File
@@ -35,10 +35,13 @@ struct WgpuAtlasTextures {
color_texture_format: wgpu::TextureFormat,
storage: WgpuAtlasStorage,
pending_uploads: Vec<PendingUpload>,
next_texture_generation: u64,
}
pub struct WgpuTextureInfo {
pub view: wgpu::TextureView,
/// Distinguishes new textures that reuse an [`AtlasTextureId`].
pub generation: u64,
}
impl WgpuAtlas {
@@ -55,6 +58,7 @@ impl WgpuAtlas {
color_texture_format,
storage: WgpuAtlasStorage::default(),
pending_uploads: Vec::new(),
next_texture_generation: 0,
})))
}
@@ -76,6 +80,7 @@ impl WgpuAtlas {
let texture = &lock.backend.storage[id];
WgpuTextureInfo {
view: texture.view.clone(),
generation: texture.generation,
}
}
@@ -211,12 +216,15 @@ impl WgpuAtlasTextures {
let texture_list = &mut self.storage[kind];
let index = texture_list.free_list.pop();
let generation = self.next_texture_generation;
self.next_texture_generation = self.next_texture_generation.wrapping_add(1);
let atlas_texture = WgpuAtlasTexture {
id: AtlasTextureId {
index: index.unwrap_or(texture_list.textures.len()) as u32,
kind,
},
generation,
allocator: BucketedAtlasAllocator::new(device_size_to_etagere(size)),
format,
texture,
@@ -339,6 +347,7 @@ impl ops::Index<AtlasTextureId> for WgpuAtlasStorage {
struct WgpuAtlasTexture {
id: AtlasTextureId,
generation: u64,
allocator: BucketedAtlasAllocator,
texture: wgpu::Texture,
view: wgpu::TextureView,
@@ -501,6 +510,45 @@ mod tests {
Ok(())
}
#[test]
fn reused_texture_id_has_new_generation() -> anyhow::Result<()> {
let (device, queue) = test_device_and_queue()?;
let atlas = WgpuAtlas::new(device, queue, wgpu::TextureFormat::Bgra8Unorm);
let size = Size {
width: DevicePixels(700),
height: DevicePixels(700),
};
let make_key = |image_id| {
AtlasKey::Image(RenderImageParams {
image_id: ImageId(image_id),
frame_index: 0,
})
};
let insert = |key: AtlasKey| {
atlas
.get_or_insert_with(key, &mut || {
Ok(Some((
size,
Cow::Owned(vec![0; size.width.0 as usize * size.height.0 as usize * 4]),
)))
})
.expect("allocation should succeed")
.expect("callback returns Some")
};
let first_key = make_key(1);
let first_tile = insert(first_key.clone());
let first_generation = atlas.get_texture_info(first_tile.texture_id).generation;
atlas.remove(&first_key);
let second_tile = insert(make_key(2));
let second_generation = atlas.get_texture_info(second_tile.texture_id).generation;
assert_eq!(second_tile.texture_id, first_tile.texture_id);
assert_ne!(second_generation, first_generation);
Ok(())
}
#[test]
fn swizzle_upload_data_preserves_bgra_uploads() {
let input = vec![0x10, 0x20, 0x30, 0x40];
+80 -22
View File
@@ -1,6 +1,7 @@
use crate::{CompositorGpuHint, WgpuAtlas, WgpuContext};
use anyhow::{Context as _, Result};
use bytemuck::{Pod, Zeroable};
use collections::FxHashMap;
use gpui::{
AtlasTextureId, Background, Bounds, DevicePixels, GpuSpecs, Path, Point, PrimitiveBatch,
ScaledPixels, Scene, Size, get_gamma_correction_ratios,
@@ -8,6 +9,7 @@ use gpui::{
use log::warn;
#[cfg(not(target_family = "wasm"))]
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use smallvec::SmallVec;
use std::cell::RefCell;
use std::num::NonZeroU64;
use std::ops::Range;
@@ -184,6 +186,7 @@ struct WgpuResources {
pipelines: WgpuPipelines,
bind_group_layouts: WgpuBindGroupLayouts,
atlas_sampler: wgpu::Sampler,
atlas_texture_bind_groups: FxHashMap<AtlasTextureId, CachedTextureBindGroup>,
globals_buffer: wgpu::Buffer,
globals_bind_group: wgpu::BindGroup,
path_globals_bind_group: wgpu::BindGroup,
@@ -194,6 +197,11 @@ struct WgpuResources {
path_msaa_view: Option<wgpu::TextureView>,
}
struct CachedTextureBindGroup {
texture_generation: u64,
bind_group: wgpu::BindGroup,
}
impl WgpuResources {
fn invalidate_intermediate_textures(&mut self) {
self.path_intermediate_texture = None;
@@ -568,6 +576,7 @@ impl WgpuRenderer {
pipelines,
bind_group_layouts,
atlas_sampler,
atlas_texture_bind_groups: FxHashMap::default(),
globals_buffer,
globals_bind_group,
path_globals_bind_group,
@@ -1418,6 +1427,7 @@ impl WgpuRenderer {
scene.polychrome_sprites.len(),
)
})?;
self.prepare_texture_bind_groups(scene);
let mut encoder =
self.resources()
@@ -1498,13 +1508,15 @@ impl WgpuRenderer {
instance_range(range),
&mut pass,
),
PrimitiveBatch::MonochromeSprites { texture_id, range } => self.draw_sprites(
&instance_bindings.monochrome_sprites,
texture_id,
&self.resources().pipelines.mono_sprites,
instance_range(range),
&mut pass,
),
PrimitiveBatch::MonochromeSprites { texture_id, range } => {
self.draw_sprites(
&instance_bindings.monochrome_sprites,
texture_id,
&self.resources().pipelines.mono_sprites,
instance_range(range),
&mut pass,
)?;
}
PrimitiveBatch::SubpixelSprites { texture_id, range } => {
let resources = self.resources();
self.draw_sprites(
@@ -1517,15 +1529,17 @@ impl WgpuRenderer {
.unwrap_or(&resources.pipelines.mono_sprites),
instance_range(range),
&mut pass,
);
)?;
}
PrimitiveBatch::PolychromeSprites { texture_id, range } => {
self.draw_sprites(
&instance_bindings.polychrome_sprites,
texture_id,
&self.resources().pipelines.poly_sprites,
instance_range(range),
&mut pass,
)?;
}
PrimitiveBatch::PolychromeSprites { texture_id, range } => self.draw_sprites(
&instance_bindings.polychrome_sprites,
texture_id,
&self.resources().pipelines.poly_sprites,
instance_range(range),
&mut pass,
),
// Surfaces are macOS-only for video playback and are not
// implemented by the WGPU renderer.
PrimitiveBatch::Surfaces(_surfaces) => {}
@@ -1602,6 +1616,47 @@ impl WgpuRenderer {
})
}
fn prepare_texture_bind_groups(&mut self, scene: &Scene) {
let mut texture_ids = SmallVec::<[AtlasTextureId; 8]>::new();
for batch in scene.batches() {
let texture_id = match batch {
PrimitiveBatch::MonochromeSprites { texture_id, .. }
| PrimitiveBatch::SubpixelSprites { texture_id, .. }
| PrimitiveBatch::PolychromeSprites { texture_id, .. } => texture_id,
_ => continue,
};
if !texture_ids.contains(&texture_id) {
texture_ids.push(texture_id);
}
}
self.resources_mut()
.atlas_texture_bind_groups
.retain(|texture_id, _| texture_ids.contains(texture_id));
for texture_id in texture_ids {
let texture_info = self.atlas.get_texture_info(texture_id);
let is_current = self
.resources()
.atlas_texture_bind_groups
.get(&texture_id)
.is_some_and(|cached| cached.texture_generation == texture_info.generation);
if is_current {
continue;
}
let bind_group =
self.create_texture_bind_group("atlas_texture_bind_group", &texture_info.view);
self.resources_mut().atlas_texture_bind_groups.insert(
texture_id,
CachedTextureBindGroup {
texture_generation: texture_info.generation,
bind_group,
},
);
}
}
fn draw_instances(
&self,
instances: &InstanceBinding,
@@ -1628,22 +1683,25 @@ impl WgpuRenderer {
pipeline: &wgpu::RenderPipeline,
range: Range<u32>,
pass: &mut wgpu::RenderPass<'_>,
) {
) -> Result<()> {
if range.is_empty() {
return;
return Ok(());
}
let texture_info = self.atlas.get_texture_info(texture_id);
let texture =
self.create_texture_bind_group("atlas_texture_bind_group", &texture_info.view);
let resources = self.resources();
let texture = resources
.atlas_texture_bind_groups
.get(&texture_id)
.context("missing atlas texture bind group")?;
pass.set_pipeline(pipeline);
pass.set_bind_group(0, &self.resources().globals_bind_group, &[]);
pass.set_bind_group(0, &resources.globals_bind_group, &[]);
pass.set_bind_group(1, &sprite_instances.bind_group, &[]);
pass.set_bind_group(2, &texture, &[]);
pass.set_bind_group(2, &texture.bind_group, &[]);
pass.draw(
0..4,
sprite_instances.first_instance + range.start
..sprite_instances.first_instance + range.end,
);
Ok(())
}
unsafe fn instance_bytes<T>(instances: &[T]) -> &[u8] {