From 0802a9d9c0f03846d6c838e232825ee845eb201e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 30 Mar 2021 21:07:19 +0200 Subject: [PATCH] Optimize: get glyph uv rects during layouts instead of in tesselation This allows them to be cached, saving around 20% total CPU. It also makes the code more nicely structured --- epaint/src/tessellator.rs | 51 +++++++++++++++++------------------- epaint/src/text/font.rs | 54 ++++++++++++++++++++++++++------------- epaint/src/text/fonts.rs | 6 +++++ epaint/src/text/galley.rs | 6 ++++- 4 files changed, 71 insertions(+), 46 deletions(-) diff --git a/epaint/src/tessellator.rs b/epaint/src/tessellator.rs index b1c6a337..9dde7154 100644 --- a/epaint/src/tessellator.rs +++ b/epaint/src/tessellator.rs @@ -634,36 +634,38 @@ impl Tessellator { out.reserve_triangles(num_chars * 2); out.reserve_vertices(num_chars * 4); - let tex_w = fonts.texture().width as f32; - let tex_h = fonts.texture().height as f32; + let inv_tex_w = 1.0 / fonts.texture().width as f32; + let inv_tex_h = 1.0 / fonts.texture().height as f32; let clip_rect = self.clip_rect.expand(2.0); // Some fudge to handle letters that are slightly larger than expected. - let font = &fonts[galley.text_style]; - let mut chars = galley.text.chars(); - for line in &galley.rows { - let line_min_y = pos.y + line.y_min; - let line_max_y = line_min_y + font.row_height(); - let is_line_visible = line_max_y >= clip_rect.min.y && line_min_y <= clip_rect.max.y; + for row in &galley.rows { + let row_min_y = pos.y + row.y_min; + let row_max_y = pos.y + row.y_max; + let is_line_visible = row_max_y >= clip_rect.min.y && row_min_y <= clip_rect.max.y; - for x_offset in line.x_offsets.iter().take(line.x_offsets.len() - 1) { - let c = chars.next().unwrap(); + if self.options.coarse_tessellation_culling && !is_line_visible { + // culling individual lines of text is important, since a single `Shape::Text` + // can span hundreds of lines. + continue; + } - if self.options.coarse_tessellation_culling && !is_line_visible { - // culling individual lines of text is important, since a single `Shape::Text` - // can span hundreds of lines. - continue; - } - - if let Some(glyph) = font.uv_rect(c) { - let mut left_top = pos + glyph.offset + vec2(*x_offset, line.y_min); - left_top.x = font.round_to_pixel(left_top.x); // Pixel-perfection. - left_top.y = font.round_to_pixel(left_top.y); // Pixel-perfection. + for (x_offset, uv_rect) in row.x_offsets.iter().zip(&row.uv_rects) { + if let Some(glyph) = uv_rect { + let mut left_top = pos + glyph.offset + vec2(*x_offset, row.y_min); + left_top.x = fonts.round_to_pixel(left_top.x); // Pixel-perfection. + left_top.y = fonts.round_to_pixel(left_top.y); // Pixel-perfection. let rect = Rect::from_min_max(left_top, left_top + glyph.size); let uv = Rect::from_min_max( - pos2(glyph.min.0 as f32 / tex_w, glyph.min.1 as f32 / tex_h), - pos2(glyph.max.0 as f32 / tex_w, glyph.max.1 as f32 / tex_h), + pos2( + glyph.min.0 as f32 * inv_tex_w, + glyph.min.1 as f32 * inv_tex_h, + ), + pos2( + glyph.max.0 as f32 * inv_tex_w, + glyph.max.1 as f32 * inv_tex_h, + ), ); if fake_italics { @@ -698,12 +700,7 @@ impl Tessellator { } } } - if line.ends_with_newline { - let newline = chars.next().unwrap(); - debug_assert_eq!(newline, '\n'); - } } - assert_eq!(chars.next(), None); } } diff --git a/epaint/src/text/font.rs b/epaint/src/text/font.rs index fd1f2b55..7137fd36 100644 --- a/epaint/src/text/font.rs +++ b/epaint/src/text/font.rs @@ -17,7 +17,7 @@ use emath::{vec2, Vec2}; // ---------------------------------------------------------------------------- -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct UvRect { /// X/Y offset for nice rendering (unit: points). pub offset: Vec2, @@ -220,7 +220,7 @@ impl Font { self.text_style } - #[inline] + #[inline(always)] pub fn round_to_pixel(&self, point: f32) -> f32 { (point * self.pixels_per_point).round() / self.pixels_per_point } @@ -310,6 +310,7 @@ impl Font { let x_offsets = self.layout_single_row_fragment(&text); let row = Row { x_offsets, + uv_rects: vec![], // will be filled in later y_min: 0.0, y_max: self.row_height(), ends_with_newline: false, @@ -322,8 +323,7 @@ impl Font { rows: vec![row], size, }; - galley.sanity_check(); - galley + self.finalize_galley(galley) } /// Always returns at least one row. @@ -389,6 +389,7 @@ impl Font { if text.is_empty() { rows.push(Row { x_offsets: vec![first_row_indentation], + uv_rects: vec![], y_min: cursor_y, y_max: cursor_y + row_height, ends_with_newline: false, @@ -396,6 +397,7 @@ impl Font { } else if text.ends_with('\n') { rows.push(Row { x_offsets: vec![0.0], + uv_rects: vec![], y_min: cursor_y, y_max: cursor_y + row_height, ends_with_newline: false, @@ -415,8 +417,7 @@ impl Font { rows, size, }; - galley.sanity_check(); - galley + self.finalize_galley(galley) } /// A paragraph is text with no line break character in it. @@ -431,6 +432,7 @@ impl Font { if text.is_empty() { return vec![Row { x_offsets: vec![first_row_indentation], + uv_rects: vec![], y_min: 0.0, y_max: self.row_height(), ends_with_newline: false, @@ -461,28 +463,26 @@ impl Font { { // Allow the first row to be completely empty, because we know there will be more space on the next row: assert_eq!(row_start_idx, 0); - let row = Row { + out_rows.push(Row { x_offsets: vec![first_row_indentation], + uv_rects: vec![], y_min: cursor_y, y_max: cursor_y + self.row_height(), ends_with_newline: false, - }; - row.sanity_check(); - out_rows.push(row); + }); cursor_y = self.round_to_pixel(cursor_y + self.row_height()); first_row_indentation = 0.0; // Continue all other rows as if there is no indentation } else if let Some(last_kept_index) = row_break_candidates.get() { - let row = Row { + out_rows.push(Row { x_offsets: full_x_offsets[row_start_idx..=last_kept_index + 1] .iter() .map(|x| first_row_indentation + x - row_start_x) .collect(), + uv_rects: vec![], // Will be filled in later! y_min: cursor_y, y_max: cursor_y + self.row_height(), ends_with_newline: false, - }; - row.sanity_check(); - out_rows.push(row); + }); row_start_idx = last_kept_index + 1; row_start_x = first_row_indentation + full_x_offsets[row_start_idx]; @@ -495,21 +495,39 @@ impl Font { } if row_start_idx + 1 < full_x_offsets.len() { - let row = Row { + out_rows.push(Row { x_offsets: full_x_offsets[row_start_idx..] .iter() .map(|x| first_row_indentation + x - row_start_x) .collect(), + uv_rects: vec![], // Will be filled in later! y_min: cursor_y, y_max: cursor_y + self.row_height(), ends_with_newline: false, - }; - row.sanity_check(); - out_rows.push(row); + }); } out_rows } + + fn finalize_galley(&self, mut galley: Galley) -> Galley { + let mut chars = galley.text.chars(); + for row in &mut galley.rows { + row.uv_rects.clear(); + row.uv_rects.reserve(row.char_count_excluding_newline()); + for _ in 0..row.char_count_excluding_newline() { + let c = chars.next().unwrap(); + row.uv_rects.push(self.uv_rect(c)); + } + if row.ends_with_newline { + let newline = chars.next().unwrap(); + assert_eq!(newline, '\n'); + } + } + assert_eq!(chars.next(), None); + galley.sanity_check(); + galley + } } /// Keeps track of good places to break a long row of text. diff --git a/epaint/src/text/fonts.rs b/epaint/src/text/fonts.rs index 6d36d0e5..139028fd 100644 --- a/epaint/src/text/fonts.rs +++ b/epaint/src/text/fonts.rs @@ -247,6 +247,7 @@ impl Fonts { } } + #[inline(always)] pub fn pixels_per_point(&self) -> f32 { self.pixels_per_point } @@ -255,6 +256,11 @@ impl Fonts { &self.definitions } + #[inline(always)] + pub fn round_to_pixel(&self, point: f32) -> f32 { + (point * self.pixels_per_point).round() / self.pixels_per_point + } + /// Call each frame to get the latest available font texture data. pub fn texture(&self) -> Arc { let atlas = self.atlas.lock(); diff --git a/epaint/src/text/galley.rs b/epaint/src/text/galley.rs index 7e72b678..00fa06b7 100644 --- a/epaint/src/text/galley.rs +++ b/epaint/src/text/galley.rs @@ -19,7 +19,7 @@ //! and the start of the second row. //! [`CCursor::prefer_next_row`] etc selects which. -use super::cursor::*; +use super::{cursor::*, font::UvRect}; use emath::{pos2, NumExt, Rect, Vec2}; /// A collection of text locked into place. @@ -52,6 +52,9 @@ pub struct Row { /// `x_offsets.len() + (ends_with_newline as usize) == text.chars().count() + 1` pub x_offsets: Vec, + /// Per-character. Used when rendering. + pub uv_rects: Vec>, + /// Top of the row, offset within the Galley. /// Unit: points. pub y_min: f32, @@ -71,6 +74,7 @@ pub struct Row { impl Row { pub fn sanity_check(&self) { assert!(!self.x_offsets.is_empty()); + assert!(self.x_offsets.len() == self.uv_rects.len() + 1); } /// Excludes the implicit `\n` after the `Row`, if any.