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
This commit is contained in:
Cameron Mcloughlin authored and GitHub committed 2026-09-16 23:23:23 +00:00
1 parent a001a76198
commit bbacc15a4f
2 files changed
+175 -12

No files matched your search

+52 -11
View File
@@ -1,7 +1,7 @@
@group(1) @binding(0) var t_instances: texture_2d<u32>;
// 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<function, InstanceCursor>) -> 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<vec4<f32>>(texel0);
let values1 = bitcast<vec4<f32>>(texel1);
let values2 = bitcast<vec4<f32>>(texel2);
let values3 = bitcast<vec4<f32>>(texel3);
let values4 = bitcast<vec4<f32>>(texel4);
let values5 = bitcast<vec4<f32>>(texel5);
let values6 = bitcast<vec4<f32>>(texel6);
let values7 = bitcast<vec4<f32>>(texel7);
let values8 = bitcast<vec4<f32>>(texel8);
let values9 = bitcast<vec4<f32>>(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, 2>(
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),
);
}
+123 -1
View File
@@ -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::<SubpixelSprite>(), 28 * 4);
assert_eq!(std::mem::size_of::<PolychromeSprite>(), 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(),
]
);
}
}