Lots of claude-driven performance work.

This commit is contained in:
2026-03-23 00:24:55 -04:00
parent 497af9151d
commit e90f09bf3e
14 changed files with 1820 additions and 394 deletions

View File

@@ -7,8 +7,8 @@ use cosmic_text::{
};
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use ruin_ui::{
BoxShadowKind, ClipRegion, Color, DisplayItem, Point, PreparedImage, PreparedText, Rect,
RoundedRect, SceneSnapshot, ShadowRect, UiSize,
BoxShadowKind, ClipRegion, Color, DisplayItem, GlyphInstance, Point, PreparedImage,
PreparedText, Rect, RoundedRect, SceneSnapshot, ShadowRect, UiSize,
};
use tracing::trace;
use wgpu::util::DeviceExt;
@@ -107,6 +107,14 @@ struct CachedImageTexture {
bind_group: wgpu::BindGroup,
}
#[derive(Default)]
struct AtlasTextPerfStats {
/// Total glyphs in all text nodes in the scene.
glyphs_total: u32,
/// Glyphs skipped because their line is outside the clip rect.
glyphs_clip_culled: u32,
}
#[allow(dead_code)]
struct GlyphAtlas {
texture: wgpu::Texture,
@@ -740,7 +748,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
let text_prepare_start = std::time::Instant::now();
let mut uploaded_images = Vec::new();
let mut uploaded_texts = Vec::new();
let uploaded_atlas_text = self.prepare_uploaded_atlas_text(scene);
let mut atlas_perf = AtlasTextPerfStats::default();
let uploaded_atlas_text = self.prepare_uploaded_atlas_text(scene, &mut atlas_perf);
let mut clip_stack = Vec::new();
let mut active_clip = ActiveClip::default();
for item in &scene.items {
@@ -836,6 +845,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
.as_ref()
.map_or(0_u32, |text| text.vertex_count),
fallback_text_batches = uploaded_texts.len(),
atlas_glyphs_total = atlas_perf.glyphs_total,
atlas_glyphs_clip_culled = atlas_perf.glyphs_clip_culled,
text_prepare_ms = text_prepare_ms,
render_ms = render_start.elapsed().as_secs_f64() * 1_000.0,
"rendered scene"
@@ -927,8 +938,9 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
continue;
}
let local_x = (glyph.position.x - text.origin.x).round() as i32;
let local_y = (glyph.position.y - text.origin.y).round() as i32;
// Glyph positions are LOCAL (origin-relative); no subtraction needed.
let local_x = glyph.position.x.round() as i32;
let local_y = glyph.position.y.round() as i32;
glyphs.push(PreparedGlyphBitmap {
rect: PixelRect {
left: local_x + image.placement.left,
@@ -937,7 +949,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
bottom: local_y - image.placement.top + height,
},
cache_key,
color: glyph.color,
color: resolve_glyph_color(glyph.color, text.default_color),
});
}
glyphs
@@ -978,7 +990,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
bottom: physical.y - image.placement.top + height,
},
content: image.content,
color: glyph.color_opt.map_or(text.color, color_from_cosmic),
color: glyph.color_opt.map_or(text.default_color, color_from_cosmic),
data: image.data.clone(),
});
}
@@ -986,7 +998,11 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
glyphs
}
fn prepare_uploaded_atlas_text(&mut self, scene: &SceneSnapshot) -> Option<UploadedAtlasText> {
fn prepare_uploaded_atlas_text(
&mut self,
scene: &SceneSnapshot,
perf: &mut AtlasTextPerfStats,
) -> Option<UploadedAtlasText> {
let mut vertices = Vec::new();
let mut clip_stack = Vec::new();
let mut active_clip = ActiveClip::default();
@@ -1020,22 +1036,34 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
}
let clip_rect = logical_clip_rect.map(rect_to_pixel_rect);
for glyph in &text.glyphs {
// Only iterate glyphs whose lines fall within the clip rect.
// For large text nodes (e.g. full Cargo.lock) this reduces
// iteration from O(all_glyphs) to O(visible_glyphs).
let all_count = text.glyphs.len();
let visible = clip_visible_glyphs(text, logical_clip_rect);
perf.glyphs_total += all_count as u32;
perf.glyphs_clip_culled += (all_count - visible.len()) as u32;
for glyph in visible {
let Some(cache_key) = glyph.cache_key else {
continue;
};
let Some(atlas_glyph) = self.ensure_atlas_glyph(cache_key, glyph.color)
let resolved_color = resolve_glyph_color(glyph.color, text.default_color);
let Some(atlas_glyph) = self.ensure_atlas_glyph(cache_key, resolved_color)
else {
continue;
};
// Glyph positions are LOCAL; add text.origin for absolute screen coords.
let abs_x = (text.origin.x + glyph.position.x).round() as i32;
let abs_y = (text.origin.y + glyph.position.y).round() as i32;
let glyph_rect = PixelRect {
left: glyph.position.x.round() as i32 + atlas_glyph.placement_left,
top: glyph.position.y.round() as i32 - atlas_glyph.placement_top,
right: glyph.position.x.round() as i32
left: abs_x + atlas_glyph.placement_left,
top: abs_y - atlas_glyph.placement_top,
right: abs_x
+ atlas_glyph.placement_left
+ atlas_glyph.atlas_rect.width as i32,
bottom: glyph.position.y.round() as i32 - atlas_glyph.placement_top
bottom: abs_y - atlas_glyph.placement_top
+ atlas_glyph.atlas_rect.height as i32,
};
push_glyph_vertices(
@@ -1044,7 +1072,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
atlas_glyph.atlas_rect,
clip_rect,
scene.logical_size,
glyph.color,
resolved_color,
active_clip,
);
}
@@ -1439,6 +1467,12 @@ fn create_glyph_atlas(
}
}
/// Resolve `Color::SENTINEL` to `default_color`; return other colors unchanged.
#[inline]
fn resolve_glyph_color(color: Color, default_color: Color) -> Color {
if color == Color::SENTINEL { default_color } else { color }
}
fn color_from_cosmic(color: cosmic_text::Color) -> Color {
Color::rgba(color.r(), color.g(), color.b(), color.a())
}
@@ -2256,6 +2290,40 @@ fn clip_textured_rect(
Some((clipped, (clipped_u0, clipped_v0, clipped_u1, clipped_v1)))
}
/// Returns the sub-slice of `text.glyphs` whose lines overlap `clip`.
///
/// Glyphs are stored in text-layout order (top-to-bottom, left-to-right).
/// We binary-search `text.lines` for the first and last visible line, then
/// return only those glyph indices. This turns O(all_glyphs) iteration into
/// O(log(lines) + visible_glyphs) — critical for large text nodes in scroll
/// boxes where only a few lines are on screen at once.
fn clip_visible_glyphs<'a>(text: &'a PreparedText, clip: Option<Rect>) -> &'a [GlyphInstance] {
let Some(clip) = clip else {
return &text.glyphs;
};
let lines = &text.lines;
if lines.is_empty() {
return &text.glyphs;
}
// Convert clip bounds to local (origin-relative) Y coordinates.
let local_top = clip.origin.y - text.origin.y;
let local_bottom = local_top + clip.size.height;
// First line whose bottom edge reaches or passes the clip top.
let start = lines.partition_point(|l| l.rect.origin.y + l.rect.size.height < local_top);
// First line whose top edge is strictly past the clip bottom.
let end = lines.partition_point(|l| l.rect.origin.y <= local_bottom);
if start >= end || start >= lines.len() {
return &text.glyphs[0..0];
}
let glyph_start = lines[start].glyph_start;
let glyph_end = lines[end - 1].glyph_end;
&text.glyphs[glyph_start..glyph_end.min(text.glyphs.len())]
}
fn text_texture_key(text: &PreparedText) -> TextTextureKey {
TextTextureKey {
text: text.text.clone(),
@@ -2264,13 +2332,14 @@ fn text_texture_key(text: &PreparedText) -> TextTextureKey {
.map(|bounds| (bounds.width.to_bits(), bounds.height.to_bits())),
font_size_bits: text.font_size.to_bits(),
line_height_bits: text.line_height.to_bits(),
color: (text.color.r, text.color.g, text.color.b, text.color.a),
color: (text.default_color.r, text.default_color.g, text.default_color.b, text.default_color.a),
glyphs: text
.glyphs
.iter()
.map(|glyph| TextTextureGlyph {
local_x_bits: (glyph.position.x - text.origin.x).to_bits(),
local_y_bits: (glyph.position.y - text.origin.y).to_bits(),
// Glyph positions are already LOCAL; no subtraction needed.
local_x_bits: glyph.position.x.to_bits(),
local_y_bits: glyph.position.y.to_bits(),
advance_bits: glyph.advance.to_bits(),
color: (glyph.color.r, glyph.color.g, glyph.color.b, glyph.color.a),
cache_key: glyph.cache_key,
@@ -2301,19 +2370,13 @@ mod tests {
Rect::new(10.0, 10.0, 20.0, 20.0),
Color::rgb(0x44, 0x55, 0x66),
);
scene.push_text(PreparedText {
element_id: None,
text: "ignored".into(),
origin: Point::new(4.0, 8.0),
bounds: None,
font_size: 16.0,
line_height: 18.0,
color: Color::rgb(0xFF, 0xFF, 0xFF),
selectable: true,
selection_style: TextSelectionStyle::DEFAULT,
lines: Vec::new(),
glyphs: Vec::new(),
});
scene.push_text(PreparedText::monospace(
"ignored",
Point::new(4.0, 8.0),
16.0,
8.0,
Color::rgb(0xFF, 0xFF, 0xFF),
));
let vertices = build_vertices(&scene);
assert_eq!(vertices.len(), 12);
@@ -2330,34 +2393,22 @@ mod tests {
#[test]
fn text_texture_key_ignores_absolute_origin() {
let first = PreparedText {
element_id: None,
text: "cache me".into(),
origin: Point::new(20.0, 30.0),
bounds: Some(UiSize::new(120.0, 48.0)),
font_size: 16.0,
line_height: 20.0,
color: Color::rgb(0xEE, 0xEE, 0xEE),
selectable: true,
selection_style: TextSelectionStyle::DEFAULT,
lines: Vec::new(),
glyphs: vec![GlyphInstance {
position: Point::new(24.0, 44.0),
advance: 8.0,
color: Color::rgb(0xEE, 0xEE, 0xEE),
cache_key: None,
text_start: 0,
text_end: 1,
}],
};
let second = PreparedText {
origin: Point::new(60.0, 90.0),
glyphs: vec![GlyphInstance {
position: Point::new(64.0, 104.0),
..first.glyphs[0].clone()
}],
..first.clone()
};
// Two PreparedTexts with the same content but different origins must
// produce the same TextTextureKey (the key stores local glyph offsets).
let first = PreparedText::monospace(
"x",
Point::new(20.0, 30.0),
16.0,
8.0,
Color::rgb(0xEE, 0xEE, 0xEE),
);
let second = PreparedText::monospace(
"x",
Point::new(60.0, 90.0),
16.0,
8.0,
Color::rgb(0xEE, 0xEE, 0xEE),
);
assert_eq!(text_texture_key(&first), text_texture_key(&second));
}