From bbacc15a4fcdb540abd3a3bef94ed004ff38fcd3 Mon Sep 17 00:00:00 2001 From: Cameron Mcloughlin Date: Wed, 16 Sep 2026 23:23:23 +0000 Subject: [PATCH] gpui_web: Flatten webgl texel loading (#64337) The webGL backend uses textures to upload quads to the GPU, since it doesn't have storage buffers. Each quad is stored in 10 texels, and we previously fetched each texel 4 times. Although texel fetches are cached, the extra bookkeeping optimized badly, and lead to bad performance. This change replaces the `read_word` calls with `fetch_texel_instance`, and manually unrolls this loop. This gave the following frame time improvements scrolling through delta web: - desktop, 9950x3d iGPU, 1440p, firefox: ~3.3x faster - mobile, Pixel 10 Pro, firefox: ~23x faster --- Release Notes: - [GPUI] Improved performance of webGL quad upload, around a ~3x frame time reduction in typical cases --- crates/gpui_wgpu/src/shaders_webgl.wgsl | 63 +++++++++--- crates/gpui_wgpu/src/wgpu_renderer.rs | 124 +++++++++++++++++++++++- 2 files changed, 175 insertions(+), 12 deletions(-) diff --git a/crates/gpui_wgpu/src/shaders_webgl.wgsl b/crates/gpui_wgpu/src/shaders_webgl.wgsl index 0c9c217a69..bf5f629cf9 100644 --- a/crates/gpui_wgpu/src/shaders_webgl.wgsl +++ b/crates/gpui_wgpu/src/shaders_webgl.wgsl @@ -1,7 +1,7 @@ @group(1) @binding(0) var t_instances: texture_2d; -// Each texel of `t_instances` packs four 32-bit words of instance data. Records -// are read strictly front to back, so all readers share a cursor that keeps the +// Each texel of `t_instances` packs four 32-bit words of instance data. Most +// records are read strictly front to back through a cursor that keeps the // most recently fetched texel and only touches the texture again when the next // word crosses a texel boundary. This fetches each texel exactly once per // record load instead of once per word. The `read_*` functions must therefore @@ -136,16 +136,57 @@ fn read_transformation(cursor: ptr) -> TransformationM } fn load_quad(instance_id: u32) -> Quad { - var cursor = instance_cursor(instance_id * 40u); + // Keep this fixed-layout decoder explicit. Some WebGL shader compilers fail + // to optimize the cursor's branches and dynamic vector indexing for quads. + let first_texel_index = instance_id * 10u; + let width = textureDimensions(t_instances).x; + let texel0 = fetch_instance_texel(first_texel_index, width); + let texel1 = fetch_instance_texel(first_texel_index + 1u, width); + let texel2 = fetch_instance_texel(first_texel_index + 2u, width); + let texel3 = fetch_instance_texel(first_texel_index + 3u, width); + let texel4 = fetch_instance_texel(first_texel_index + 4u, width); + let texel5 = fetch_instance_texel(first_texel_index + 5u, width); + let texel6 = fetch_instance_texel(first_texel_index + 6u, width); + let texel7 = fetch_instance_texel(first_texel_index + 7u, width); + let texel8 = fetch_instance_texel(first_texel_index + 8u, width); + let texel9 = fetch_instance_texel(first_texel_index + 9u, width); + + let values0 = bitcast>(texel0); + let values1 = bitcast>(texel1); + let values2 = bitcast>(texel2); + let values3 = bitcast>(texel3); + let values4 = bitcast>(texel4); + let values5 = bitcast>(texel5); + let values6 = bitcast>(texel6); + let values7 = bitcast>(texel7); + let values8 = bitcast>(texel8); + let values9 = bitcast>(texel9); + return Quad( - read_word(&cursor), - read_word(&cursor), - read_bounds(&cursor), - read_bounds(&cursor), - read_background(&cursor), - read_hsla(&cursor), - read_corners(&cursor), - read_edges(&cursor), + texel0.x, + texel0.y, + Bounds(values0.zw, values1.xy), + Bounds(values1.zw, values2.xy), + Background( + texel2.z, + texel2.w, + Hsla(values3.x, values3.y, values3.z, values3.w), + values4.x, + array( + LinearColorStop( + Hsla(values4.y, values4.z, values4.w, values5.x), + values5.y, + ), + LinearColorStop( + Hsla(values5.z, values5.w, values6.x, values6.y), + values6.z, + ), + ), + texel6.w, + ), + Hsla(values7.x, values7.y, values7.z, values7.w), + Corners(values8.x, values8.y, values8.z, values8.w), + Edges(values9.x, values9.y, values9.z, values9.w), ); } diff --git a/crates/gpui_wgpu/src/wgpu_renderer.rs b/crates/gpui_wgpu/src/wgpu_renderer.rs index b0cd40a767..ecb19e87da 100644 --- a/crates/gpui_wgpu/src/wgpu_renderer.rs +++ b/crates/gpui_wgpu/src/wgpu_renderer.rs @@ -2201,7 +2201,11 @@ impl RenderingParameters { #[cfg(test)] mod tests { use super::*; - use gpui::{MonochromeSprite, PolychromeSprite, Quad, Shadow, SubpixelSprite, Underline}; + use gpui::{ + BorderStyle, ColorSpace, ContentMask, Corners, Edges, Hsla, MonochromeSprite, + PolychromeSprite, Quad, Shadow, SubpixelSprite, Underline, linear_color_stop, + linear_gradient, + }; #[test] fn webgl_shader_is_valid_wgsl_without_storage_buffers() { @@ -2240,4 +2244,122 @@ mod tests { assert_eq!(std::mem::size_of::(), 28 * 4); assert_eq!(std::mem::size_of::(), 24 * 4); } + + #[test] + fn webgl_quad_layout_matches_fixed_decoder() { + let quad = Quad { + order: 41, + border_style: BorderStyle::Dashed, + bounds: Bounds { + origin: Point { + x: 1.0.into(), + y: 2.0.into(), + }, + size: Size { + width: 3.0.into(), + height: 4.0.into(), + }, + }, + content_mask: ContentMask { + bounds: Bounds { + origin: Point { + x: 5.0.into(), + y: 6.0.into(), + }, + size: Size { + width: 7.0.into(), + height: 8.0.into(), + }, + }, + }, + background: linear_gradient( + 11.0, + linear_color_stop( + Hsla { + h: 12.0, + s: 13.0, + l: 14.0, + a: 15.0, + }, + 16.0, + ), + linear_color_stop( + Hsla { + h: 17.0, + s: 18.0, + l: 19.0, + a: 20.0, + }, + 21.0, + ), + ) + .color_space(ColorSpace::Oklab), + border_color: Hsla { + h: 22.0, + s: 23.0, + l: 24.0, + a: 25.0, + }, + corner_radii: Corners { + top_left: 26.0.into(), + top_right: 27.0.into(), + bottom_right: 28.0.into(), + bottom_left: 29.0.into(), + }, + border_widths: Edges { + top: 30.0.into(), + right: 31.0.into(), + bottom: 32.0.into(), + left: 33.0.into(), + }, + }; + + let bytes = unsafe { WgpuRenderer::instance_bytes(std::slice::from_ref(&quad)) }; + let words: &[u32] = bytemuck::cast_slice(bytes); + assert_eq!( + words, + &[ + 41, + 1, + 1.0_f32.to_bits(), + 2.0_f32.to_bits(), + 3.0_f32.to_bits(), + 4.0_f32.to_bits(), + 5.0_f32.to_bits(), + 6.0_f32.to_bits(), + 7.0_f32.to_bits(), + 8.0_f32.to_bits(), + 1, + 1, + 0, + 0, + 0, + 0, + 11.0_f32.to_bits(), + 12.0_f32.to_bits(), + 13.0_f32.to_bits(), + 14.0_f32.to_bits(), + 15.0_f32.to_bits(), + 16.0_f32.to_bits(), + 17.0_f32.to_bits(), + 18.0_f32.to_bits(), + 19.0_f32.to_bits(), + 20.0_f32.to_bits(), + 21.0_f32.to_bits(), + 0, + 22.0_f32.to_bits(), + 23.0_f32.to_bits(), + 24.0_f32.to_bits(), + 25.0_f32.to_bits(), + 26.0_f32.to_bits(), + 27.0_f32.to_bits(), + 28.0_f32.to_bits(), + 29.0_f32.to_bits(), + 30.0_f32.to_bits(), + 31.0_f32.to_bits(), + 32.0_f32.to_bits(), + 33.0_f32.to_bits(), + ] + ); + } }