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
This commit is contained in:
Emil Ernerfeldt 2021-03-30 21:07:19 +02:00
parent 22cd1a8e10
commit 0802a9d9c0
4 changed files with 71 additions and 46 deletions

View file

@ -634,36 +634,38 @@ impl Tessellator {
out.reserve_triangles(num_chars * 2); out.reserve_triangles(num_chars * 2);
out.reserve_vertices(num_chars * 4); out.reserve_vertices(num_chars * 4);
let tex_w = fonts.texture().width as f32; let inv_tex_w = 1.0 / fonts.texture().width as f32;
let tex_h = fonts.texture().height 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 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]; for row in &galley.rows {
let mut chars = galley.text.chars(); let row_min_y = pos.y + row.y_min;
for line in &galley.rows { let row_max_y = pos.y + row.y_max;
let line_min_y = pos.y + line.y_min; let is_line_visible = row_max_y >= clip_rect.min.y && row_min_y <= clip_rect.max.y;
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 x_offset in line.x_offsets.iter().take(line.x_offsets.len() - 1) { if self.options.coarse_tessellation_culling && !is_line_visible {
let c = chars.next().unwrap(); // 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 { for (x_offset, uv_rect) in row.x_offsets.iter().zip(&row.uv_rects) {
// culling individual lines of text is important, since a single `Shape::Text` if let Some(glyph) = uv_rect {
// can span hundreds of lines. let mut left_top = pos + glyph.offset + vec2(*x_offset, row.y_min);
continue; left_top.x = fonts.round_to_pixel(left_top.x); // Pixel-perfection.
} left_top.y = fonts.round_to_pixel(left_top.y); // Pixel-perfection.
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.
let rect = Rect::from_min_max(left_top, left_top + glyph.size); let rect = Rect::from_min_max(left_top, left_top + glyph.size);
let uv = Rect::from_min_max( let uv = Rect::from_min_max(
pos2(glyph.min.0 as f32 / tex_w, glyph.min.1 as f32 / tex_h), pos2(
pos2(glyph.max.0 as f32 / tex_w, glyph.max.1 as f32 / tex_h), 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 { 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);
} }
} }

View file

@ -17,7 +17,7 @@ use emath::{vec2, Vec2};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct UvRect { pub struct UvRect {
/// X/Y offset for nice rendering (unit: points). /// X/Y offset for nice rendering (unit: points).
pub offset: Vec2, pub offset: Vec2,
@ -220,7 +220,7 @@ impl Font {
self.text_style self.text_style
} }
#[inline] #[inline(always)]
pub fn round_to_pixel(&self, point: f32) -> f32 { pub fn round_to_pixel(&self, point: f32) -> f32 {
(point * self.pixels_per_point).round() / self.pixels_per_point (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 x_offsets = self.layout_single_row_fragment(&text);
let row = Row { let row = Row {
x_offsets, x_offsets,
uv_rects: vec![], // will be filled in later
y_min: 0.0, y_min: 0.0,
y_max: self.row_height(), y_max: self.row_height(),
ends_with_newline: false, ends_with_newline: false,
@ -322,8 +323,7 @@ impl Font {
rows: vec![row], rows: vec![row],
size, size,
}; };
galley.sanity_check(); self.finalize_galley(galley)
galley
} }
/// Always returns at least one row. /// Always returns at least one row.
@ -389,6 +389,7 @@ impl Font {
if text.is_empty() { if text.is_empty() {
rows.push(Row { rows.push(Row {
x_offsets: vec![first_row_indentation], x_offsets: vec![first_row_indentation],
uv_rects: vec![],
y_min: cursor_y, y_min: cursor_y,
y_max: cursor_y + row_height, y_max: cursor_y + row_height,
ends_with_newline: false, ends_with_newline: false,
@ -396,6 +397,7 @@ impl Font {
} else if text.ends_with('\n') { } else if text.ends_with('\n') {
rows.push(Row { rows.push(Row {
x_offsets: vec![0.0], x_offsets: vec![0.0],
uv_rects: vec![],
y_min: cursor_y, y_min: cursor_y,
y_max: cursor_y + row_height, y_max: cursor_y + row_height,
ends_with_newline: false, ends_with_newline: false,
@ -415,8 +417,7 @@ impl Font {
rows, rows,
size, size,
}; };
galley.sanity_check(); self.finalize_galley(galley)
galley
} }
/// A paragraph is text with no line break character in it. /// A paragraph is text with no line break character in it.
@ -431,6 +432,7 @@ impl Font {
if text.is_empty() { if text.is_empty() {
return vec![Row { return vec![Row {
x_offsets: vec![first_row_indentation], x_offsets: vec![first_row_indentation],
uv_rects: vec![],
y_min: 0.0, y_min: 0.0,
y_max: self.row_height(), y_max: self.row_height(),
ends_with_newline: false, 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: // 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); assert_eq!(row_start_idx, 0);
let row = Row { out_rows.push(Row {
x_offsets: vec![first_row_indentation], x_offsets: vec![first_row_indentation],
uv_rects: vec![],
y_min: cursor_y, y_min: cursor_y,
y_max: cursor_y + self.row_height(), y_max: cursor_y + self.row_height(),
ends_with_newline: false, ends_with_newline: false,
}; });
row.sanity_check();
out_rows.push(row);
cursor_y = self.round_to_pixel(cursor_y + self.row_height()); 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 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() { } 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] x_offsets: full_x_offsets[row_start_idx..=last_kept_index + 1]
.iter() .iter()
.map(|x| first_row_indentation + x - row_start_x) .map(|x| first_row_indentation + x - row_start_x)
.collect(), .collect(),
uv_rects: vec![], // Will be filled in later!
y_min: cursor_y, y_min: cursor_y,
y_max: cursor_y + self.row_height(), y_max: cursor_y + self.row_height(),
ends_with_newline: false, ends_with_newline: false,
}; });
row.sanity_check();
out_rows.push(row);
row_start_idx = last_kept_index + 1; row_start_idx = last_kept_index + 1;
row_start_x = first_row_indentation + full_x_offsets[row_start_idx]; 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() { if row_start_idx + 1 < full_x_offsets.len() {
let row = Row { out_rows.push(Row {
x_offsets: full_x_offsets[row_start_idx..] x_offsets: full_x_offsets[row_start_idx..]
.iter() .iter()
.map(|x| first_row_indentation + x - row_start_x) .map(|x| first_row_indentation + x - row_start_x)
.collect(), .collect(),
uv_rects: vec![], // Will be filled in later!
y_min: cursor_y, y_min: cursor_y,
y_max: cursor_y + self.row_height(), y_max: cursor_y + self.row_height(),
ends_with_newline: false, ends_with_newline: false,
}; });
row.sanity_check();
out_rows.push(row);
} }
out_rows 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. /// Keeps track of good places to break a long row of text.

View file

@ -247,6 +247,7 @@ impl Fonts {
} }
} }
#[inline(always)]
pub fn pixels_per_point(&self) -> f32 { pub fn pixels_per_point(&self) -> f32 {
self.pixels_per_point self.pixels_per_point
} }
@ -255,6 +256,11 @@ impl Fonts {
&self.definitions &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. /// Call each frame to get the latest available font texture data.
pub fn texture(&self) -> Arc<Texture> { pub fn texture(&self) -> Arc<Texture> {
let atlas = self.atlas.lock(); let atlas = self.atlas.lock();

View file

@ -19,7 +19,7 @@
//! and the start of the second row. //! and the start of the second row.
//! [`CCursor::prefer_next_row`] etc selects which. //! [`CCursor::prefer_next_row`] etc selects which.
use super::cursor::*; use super::{cursor::*, font::UvRect};
use emath::{pos2, NumExt, Rect, Vec2}; use emath::{pos2, NumExt, Rect, Vec2};
/// A collection of text locked into place. /// 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` /// `x_offsets.len() + (ends_with_newline as usize) == text.chars().count() + 1`
pub x_offsets: Vec<f32>, pub x_offsets: Vec<f32>,
/// Per-character. Used when rendering.
pub uv_rects: Vec<Option<UvRect>>,
/// Top of the row, offset within the Galley. /// Top of the row, offset within the Galley.
/// Unit: points. /// Unit: points.
pub y_min: f32, pub y_min: f32,
@ -71,6 +74,7 @@ pub struct Row {
impl Row { impl Row {
pub fn sanity_check(&self) { pub fn sanity_check(&self) {
assert!(!self.x_offsets.is_empty()); 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. /// Excludes the implicit `\n` after the `Row`, if any.