Perf profiling and some low hanging perf improvements in text rendering
This commit is contained in:
@@ -169,22 +169,12 @@ struct UploadedAtlasText {
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
struct TextTextureKey {
|
||||
text: String,
|
||||
layout_key: u64,
|
||||
bounds: Option<(i32, i32)>,
|
||||
clip: Option<(i32, i32, i32, i32)>,
|
||||
font_size_bits: u32,
|
||||
line_height_bits: u32,
|
||||
color: (u8, u8, u8, u8),
|
||||
glyphs: Vec<TextTextureGlyph>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
struct TextTextureGlyph {
|
||||
local_x_bits: u32,
|
||||
local_y_bits: u32,
|
||||
advance_bits: u32,
|
||||
color: (u8, u8, u8, u8),
|
||||
cache_key: Option<CacheKey>,
|
||||
}
|
||||
|
||||
const VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 11] = [
|
||||
@@ -315,6 +305,21 @@ pub enum RenderError {
|
||||
Validation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct RendererFrameStats {
|
||||
pub scene_version: u64,
|
||||
pub scene_items: usize,
|
||||
pub quad_vertices: usize,
|
||||
pub image_batches: usize,
|
||||
pub atlas_text_vertices: u32,
|
||||
pub fallback_text_batches: usize,
|
||||
pub fallback_text_vertices: u32,
|
||||
pub atlas_glyphs_total: u32,
|
||||
pub atlas_glyphs_clip_culled: u32,
|
||||
pub text_prepare_ms: f64,
|
||||
pub render_ms: f64,
|
||||
}
|
||||
|
||||
pub struct WgpuSceneRenderer {
|
||||
surface: wgpu::Surface<'static>,
|
||||
device: wgpu::Device,
|
||||
@@ -334,6 +339,7 @@ pub struct WgpuSceneRenderer {
|
||||
glyph_atlas: GlyphAtlas,
|
||||
scale_factor: f32,
|
||||
base_color: Color,
|
||||
last_frame_stats: RendererFrameStats,
|
||||
}
|
||||
|
||||
const MAX_TEXT_CACHE_ENTRIES: usize = 64;
|
||||
@@ -729,6 +735,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
glyph_atlas,
|
||||
scale_factor: 1.0,
|
||||
base_color,
|
||||
last_frame_stats: RendererFrameStats::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -758,6 +765,10 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn last_frame_stats(&self) -> RendererFrameStats {
|
||||
self.last_frame_stats
|
||||
}
|
||||
|
||||
pub fn render(&mut self, scene: &SceneSnapshot) -> Result<(), RenderError> {
|
||||
self.render_with_logical_size(scene, scene.logical_size)
|
||||
}
|
||||
@@ -885,19 +896,34 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
}
|
||||
self.queue.submit([encoder.finish()]);
|
||||
frame.present();
|
||||
trace!(
|
||||
target: "ruin_ui_renderer_wgpu::perf",
|
||||
scene_version = scene.version,
|
||||
quad_vertices = vertices.len(),
|
||||
image_batches = uploaded_images.len(),
|
||||
atlas_text_vertices = uploaded_atlas_text
|
||||
self.last_frame_stats = RendererFrameStats {
|
||||
scene_version: scene.version,
|
||||
scene_items: scene.items.len(),
|
||||
quad_vertices: vertices.len(),
|
||||
image_batches: uploaded_images.len(),
|
||||
atlas_text_vertices: uploaded_atlas_text
|
||||
.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,
|
||||
fallback_text_batches: uploaded_texts.len(),
|
||||
fallback_text_vertices: uploaded_texts.iter().map(|text| text.vertex_count).sum(),
|
||||
atlas_glyphs_total: atlas_perf.glyphs_total,
|
||||
atlas_glyphs_clip_culled: atlas_perf.glyphs_clip_culled,
|
||||
text_prepare_ms,
|
||||
render_ms: render_start.elapsed().as_secs_f64() * 1_000.0,
|
||||
};
|
||||
trace!(
|
||||
target: "ruin_ui_renderer_wgpu::perf",
|
||||
scene_version = self.last_frame_stats.scene_version,
|
||||
scene_items = self.last_frame_stats.scene_items,
|
||||
quad_vertices = self.last_frame_stats.quad_vertices,
|
||||
image_batches = self.last_frame_stats.image_batches,
|
||||
atlas_text_vertices = self.last_frame_stats.atlas_text_vertices,
|
||||
fallback_text_batches = self.last_frame_stats.fallback_text_batches,
|
||||
fallback_text_vertices = self.last_frame_stats.fallback_text_vertices,
|
||||
atlas_glyphs_total = self.last_frame_stats.atlas_glyphs_total,
|
||||
atlas_glyphs_clip_culled = self.last_frame_stats.atlas_glyphs_clip_culled,
|
||||
text_prepare_ms = self.last_frame_stats.text_prepare_ms,
|
||||
render_ms = self.last_frame_stats.render_ms,
|
||||
"rendered scene"
|
||||
);
|
||||
Ok(())
|
||||
@@ -908,10 +934,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
text: &PreparedText,
|
||||
raster_clip: Option<Rect>,
|
||||
) -> Option<RasterizedText> {
|
||||
if (self.scale_factor - 1.0).abs() <= f32::EPSILON
|
||||
&& text.glyphs.iter().all(|glyph| glyph.cache_key.is_some())
|
||||
{
|
||||
return self.rasterize_prepared_text(text, raster_clip);
|
||||
if text.glyphs.iter().all(|glyph| glyph.cache_key.is_some()) {
|
||||
return self.rasterize_prepared_text(text, raster_clip, self.scale_factor);
|
||||
}
|
||||
|
||||
let clip = raster_clip.map(|clip| scale_rect_to_pixel_rect(clip, self.scale_factor));
|
||||
@@ -953,16 +977,18 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
&mut self,
|
||||
text: &PreparedText,
|
||||
raster_clip: Option<Rect>,
|
||||
scale_factor: f32,
|
||||
) -> Option<RasterizedText> {
|
||||
let glyphs = self.collect_prepared_glyphs(text);
|
||||
let clip = raster_clip.map(|clip| scale_rect_to_pixel_rect(clip, scale_factor));
|
||||
let glyphs = self.collect_prepared_glyphs(text, raster_clip, clip, scale_factor);
|
||||
let clip = raster_clip
|
||||
.map(rect_to_pixel_rect)
|
||||
.map(|clip| scale_rect_to_pixel_rect(clip, scale_factor))
|
||||
.or_else(|| {
|
||||
text.bounds.map(|bounds| PixelRect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: bounds.width.ceil() as i32,
|
||||
bottom: bounds.height.ceil() as i32,
|
||||
right: (bounds.width * scale_factor).ceil() as i32,
|
||||
bottom: (bounds.height * scale_factor).ceil() as i32,
|
||||
})
|
||||
})
|
||||
.or_else(|| glyph_union_prepared(&glyphs))?;
|
||||
@@ -983,17 +1009,41 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
}
|
||||
|
||||
Some(RasterizedText {
|
||||
origin_offset: Point::new(clip.left as f32, clip.top as f32),
|
||||
draw_size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
||||
origin_offset: Point::new(
|
||||
clip.left as f32 / scale_factor,
|
||||
clip.top as f32 / scale_factor,
|
||||
),
|
||||
draw_size: UiSize::new(
|
||||
clip.width() as f32 / scale_factor,
|
||||
clip.height() as f32 / scale_factor,
|
||||
),
|
||||
texture_size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
||||
pixels,
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_prepared_glyphs(&mut self, text: &PreparedText) -> Vec<PreparedGlyphBitmap> {
|
||||
let mut glyphs = Vec::with_capacity(text.glyphs.len());
|
||||
for glyph in &text.glyphs {
|
||||
let Some(cache_key) = glyph.cache_key else {
|
||||
fn collect_prepared_glyphs(
|
||||
&mut self,
|
||||
text: &PreparedText,
|
||||
logical_clip: Option<Rect>,
|
||||
pixel_clip: Option<PixelRect>,
|
||||
scale_factor: f32,
|
||||
) -> Vec<PreparedGlyphBitmap> {
|
||||
let absolute_clip = logical_clip.map(|clip| {
|
||||
Rect::new(
|
||||
text.origin.x + clip.origin.x,
|
||||
text.origin.y + clip.origin.y,
|
||||
clip.size.width,
|
||||
clip.size.height,
|
||||
)
|
||||
});
|
||||
let visible_glyphs = clip_visible_glyphs(text, absolute_clip);
|
||||
let mut glyphs = Vec::with_capacity(visible_glyphs.len());
|
||||
for glyph in visible_glyphs {
|
||||
let Some(cache_key) = glyph
|
||||
.cache_key
|
||||
.map(|cache_key| scale_glyph_cache_key(cache_key, scale_factor))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(image) = self
|
||||
@@ -1010,15 +1060,19 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
}
|
||||
|
||||
// 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;
|
||||
let local_x = (glyph.position.x * scale_factor).round() as i32;
|
||||
let local_y = (glyph.position.y * scale_factor).round() as i32;
|
||||
let rect = PixelRect {
|
||||
left: local_x + image.placement.left,
|
||||
top: local_y - image.placement.top,
|
||||
right: local_x + image.placement.left + width,
|
||||
bottom: local_y - image.placement.top + height,
|
||||
};
|
||||
if pixel_clip.is_some_and(|clip| !pixel_rects_intersect(rect, clip)) {
|
||||
continue;
|
||||
}
|
||||
glyphs.push(PreparedGlyphBitmap {
|
||||
rect: PixelRect {
|
||||
left: local_x + image.placement.left,
|
||||
top: local_y - image.placement.top,
|
||||
right: local_x + image.placement.left + width,
|
||||
bottom: local_y - image.placement.top + height,
|
||||
},
|
||||
rect,
|
||||
cache_key,
|
||||
color: resolve_glyph_color(glyph.color, text.default_color),
|
||||
});
|
||||
@@ -1046,6 +1100,13 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
|
||||
let mut glyphs = Vec::new();
|
||||
for run in buffer.layout_runs() {
|
||||
if let Some(clip) = raster_clip {
|
||||
let line_top = run.line_top.floor() as i32;
|
||||
let line_bottom = (run.line_top + run.line_height).ceil() as i32;
|
||||
if line_bottom < clip.top || line_top > clip.bottom {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
for glyph in run.glyphs {
|
||||
let physical = glyph.physical((0.0, run.line_y), 1.0);
|
||||
let Some(image) = self
|
||||
@@ -2246,15 +2307,6 @@ fn expand_rect(rect: Rect, inset: f32) -> Rect {
|
||||
)
|
||||
}
|
||||
|
||||
fn rect_to_pixel_rect(rect: Rect) -> PixelRect {
|
||||
PixelRect {
|
||||
left: rect.origin.x.floor() as i32,
|
||||
top: rect.origin.y.floor() as i32,
|
||||
right: (rect.origin.x + rect.size.width).ceil() as i32,
|
||||
bottom: (rect.origin.y + rect.size.height).ceil() as i32,
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_rect_to_pixel_rect(rect: Rect, scale: f32) -> PixelRect {
|
||||
PixelRect {
|
||||
left: (rect.origin.x * scale).floor() as i32,
|
||||
@@ -2464,7 +2516,7 @@ fn clip_visible_glyphs(text: &PreparedText, clip: Option<Rect>) -> &[GlyphInstan
|
||||
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);
|
||||
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);
|
||||
|
||||
@@ -2483,7 +2535,7 @@ fn text_texture_key(
|
||||
scale_factor: f32,
|
||||
) -> TextTextureKey {
|
||||
TextTextureKey {
|
||||
text: text.text.clone(),
|
||||
layout_key: text.layout_cache_key(),
|
||||
bounds: text.bounds.map(|bounds| {
|
||||
(
|
||||
(bounds.width * scale_factor).ceil() as i32,
|
||||
@@ -2500,18 +2552,6 @@ fn text_texture_key(
|
||||
text.default_color.b,
|
||||
text.default_color.a,
|
||||
),
|
||||
glyphs: text
|
||||
.glyphs
|
||||
.iter()
|
||||
.map(|glyph| TextTextureGlyph {
|
||||
// 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,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2531,11 +2571,14 @@ fn scale_glyph_cache_key(cache_key: CacheKey, scale_factor: f32) -> CacheKey {
|
||||
mod tests {
|
||||
use super::{
|
||||
ActiveClip, AtlasRect, GlyphVertexContext, PixelRect, blend_rgba, build_text_vertices,
|
||||
build_vertices, clip_rect_array, push_clip_state, push_glyph_vertices, rounded_clip_arrays,
|
||||
scale_glyph_cache_key, text_texture_key,
|
||||
build_vertices, clip_rect_array, clip_visible_glyphs, push_clip_state, push_glyph_vertices,
|
||||
rounded_clip_arrays, scale_glyph_cache_key, text_texture_key,
|
||||
};
|
||||
use cosmic_text::{CacheKey, CacheKeyFlags, SubpixelBin, fontdb};
|
||||
use ruin_ui::{ClipRegion, Color, Point, PreparedText, Rect, SceneSnapshot, UiSize};
|
||||
use ruin_ui::{
|
||||
ClipRegion, Color, Point, PreparedText, Rect, SceneSnapshot, TextStyle, TextSystem,
|
||||
TextWrap, UiSize,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn quad_scenes_expand_to_six_vertices_per_quad() {
|
||||
@@ -2613,6 +2656,20 @@ mod tests {
|
||||
assert_ne!(first, different_pixel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clip_visible_glyphs_returns_only_lines_intersecting_clip() {
|
||||
let mut text_system = TextSystem::new();
|
||||
let style = TextStyle::new(16.0, Color::rgb(0xEE, 0xEE, 0xEE))
|
||||
.with_wrap(TextWrap::None)
|
||||
.with_line_height(20.0);
|
||||
let text = text_system.prepare("abcd\nefgh", Point::new(10.0, 20.0), &style);
|
||||
let visible = clip_visible_glyphs(&text, Some(Rect::new(10.0, 40.0, 100.0, 20.0)));
|
||||
|
||||
assert_eq!(visible.len(), 4);
|
||||
assert_eq!(visible[0].text_start, 5);
|
||||
assert_eq!(visible[3].text_end, 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_glyph_cache_key_scales_font_size_only() {
|
||||
let key = CacheKey {
|
||||
|
||||
Reference in New Issue
Block a user