Lots of claude-driven performance work.
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user