From 5795c0695472ca1b8e384be68007b3be55ceffd4 Mon Sep 17 00:00:00 2001 From: Cameron Mcloughlin Date: Mon, 21 Sep 2026 19:24:38 +0000 Subject: [PATCH] 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 --- crates/gpui_wgpu/src/wgpu_atlas.rs | 48 ++++++++++++ crates/gpui_wgpu/src/wgpu_renderer.rs | 102 ++++++++++++++++++++------ 2 files changed, 128 insertions(+), 22 deletions(-) diff --git a/crates/gpui_wgpu/src/wgpu_atlas.rs b/crates/gpui_wgpu/src/wgpu_atlas.rs index 90731a33ee..72aa46ad1a 100644 --- a/crates/gpui_wgpu/src/wgpu_atlas.rs +++ b/crates/gpui_wgpu/src/wgpu_atlas.rs @@ -35,10 +35,13 @@ struct WgpuAtlasTextures { color_texture_format: wgpu::TextureFormat, storage: WgpuAtlasStorage, pending_uploads: Vec, + 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 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]; diff --git a/crates/gpui_wgpu/src/wgpu_renderer.rs b/crates/gpui_wgpu/src/wgpu_renderer.rs index ecb19e87da..d728711a64 100644 --- a/crates/gpui_wgpu/src/wgpu_renderer.rs +++ b/crates/gpui_wgpu/src/wgpu_renderer.rs @@ -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, 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, } +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, 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(instances: &[T]) -> &[u8] {