Use a scratch buffer to reduce allocations

This commit adds a new ShapeBuffer type, which contains some buffers tha
are commonly re-created during the shaping process. New APIs are added
that take this structure, and old APIs are turned into wrappers around
the new API.

The goal is to reduce the number of per-layout allocations that happen
in a typical layout call.
This commit is contained in:
John Nunley 2023-07-18 19:18:56 -07:00 • committed by John Nunley
parent d7214b5417
commit 64e469718b
4 changed files with 265 additions and 66 deletions

View file

@ -10,7 +10,7 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::{ use crate::{
Attrs, AttrsList, BidiParagraphs, BorrowedWithFontSystem, BufferLine, Color, FontSystem, Attrs, AttrsList, BidiParagraphs, BorrowedWithFontSystem, BufferLine, Color, FontSystem,
LayoutGlyph, LayoutLine, ShapeLine, Shaping, Wrap, LayoutGlyph, LayoutLine, ShapeBuffer, ShapeLine, Shaping, Wrap,
}; };
/// Current cursor location /// Current cursor location
@ -330,6 +330,9 @@ pub struct Buffer {
/// True if a redraw is requires. Set to false after processing /// True if a redraw is requires. Set to false after processing
redraw: bool, redraw: bool,
wrap: Wrap, wrap: Wrap,
/// Scratch buffer for shaping and laying out.
scratch: ShapeBuffer,
} }
impl Buffer { impl Buffer {
@ -354,6 +357,7 @@ impl Buffer {
scroll: 0, scroll: 0,
redraw: false, redraw: false,
wrap: Wrap::Word, wrap: Wrap::Word,
scratch: ShapeBuffer::default(),
} }
} }
@ -411,7 +415,13 @@ impl Buffer {
if line.shape_opt().is_none() { if line.shape_opt().is_none() {
reshaped += 1; reshaped += 1;
} }
let layout = line.layout(font_system, self.metrics.font_size, self.width, self.wrap); let layout = line.layout_in_buffer(
&mut self.scratch,
font_system,
self.metrics.font_size,
self.width,
self.wrap,
);
total_layout += layout.len() as i32; total_layout += layout.len() as i32;
} }
@ -439,7 +449,13 @@ impl Buffer {
if line.shape_opt().is_none() { if line.shape_opt().is_none() {
reshaped += 1; reshaped += 1;
} }
let layout = line.layout(font_system, self.metrics.font_size, self.width, self.wrap); let layout = line.layout_in_buffer(
&mut self.scratch,
font_system,
self.metrics.font_size,
self.width,
self.wrap,
);
if line_i == cursor.line { if line_i == cursor.line {
let layout_cursor = self.layout_cursor(&cursor); let layout_cursor = self.layout_cursor(&cursor);
layout_i += layout_cursor.layout as i32; layout_i += layout_cursor.layout as i32;

View file

@ -1,7 +1,7 @@
#[cfg(not(feature = "std"))] #[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec}; use alloc::{string::String, vec::Vec};
use crate::{Align, AttrsList, FontSystem, LayoutLine, ShapeLine, Shaping, Wrap}; use crate::{Align, AttrsList, FontSystem, LayoutLine, ShapeBuffer, ShapeLine, Shaping, Wrap};
/// A line (or paragraph) of text that is shaped and laid out /// A line (or paragraph) of text that is shaped and laid out
#[derive(Debug)] #[derive(Debug)]
@ -169,8 +169,18 @@ impl BufferLine {
/// Shape line, will cache results /// Shape line, will cache results
pub fn shape(&mut self, font_system: &mut FontSystem) -> &ShapeLine { pub fn shape(&mut self, font_system: &mut FontSystem) -> &ShapeLine {
self.shape_in_buffer(&mut ShapeBuffer::default(), font_system)
}
/// Shape a line using a pre-existing shape buffer.
pub fn shape_in_buffer(
&mut self,
scratch: &mut ShapeBuffer,
font_system: &mut FontSystem,
) -> &ShapeLine {
if self.shape_opt.is_none() { if self.shape_opt.is_none() {
self.shape_opt = Some(ShapeLine::new( self.shape_opt = Some(ShapeLine::new_in_buffer(
scratch,
font_system, font_system,
&self.text, &self.text,
&self.attrs_list, &self.attrs_list,
@ -204,6 +214,26 @@ impl BufferLine {
self.layout_opt.as_ref().expect("layout not found") self.layout_opt.as_ref().expect("layout not found")
} }
/// Layout a line using a pre-existing shape buffer.
pub fn layout_in_buffer(
&mut self,
scratch: &mut ShapeBuffer,
font_system: &mut FontSystem,
font_size: f32,
width: f32,
wrap: Wrap,
) -> &[LayoutLine] {
if self.layout_opt.is_none() {
self.wrap = wrap;
let align = self.align;
let shape = self.shape_in_buffer(scratch, font_system);
let mut layout = Vec::with_capacity(1);
shape.layout_to_buffer(scratch, font_size, width, wrap, align, &mut layout);
self.layout_opt = Some(layout);
}
self.layout_opt.as_ref().expect("layout not found")
}
/// Get line layout cache /// Get line layout cache
pub fn layout_opt(&self) -> &Option<Vec<LayoutLine>> { pub fn layout_opt(&self) -> &Option<Vec<LayoutLine>> {
&self.layout_opt &self.layout_opt

View file

@ -36,7 +36,7 @@ pub struct FontFallbackIter<'a> {
font_ids: &'a [fontdb::ID], font_ids: &'a [fontdb::ID],
default_families: &'a [&'a Family<'a>], default_families: &'a [&'a Family<'a>],
default_i: usize, default_i: usize,
scripts: Vec<Script>, scripts: &'a [Script],
script_i: (usize, usize), script_i: (usize, usize),
common_i: usize, common_i: usize,
other_i: usize, other_i: usize,
@ -48,7 +48,7 @@ impl<'a> FontFallbackIter<'a> {
font_system: &'a mut FontSystem, font_system: &'a mut FontSystem,
font_ids: &'a [fontdb::ID], font_ids: &'a [fontdb::ID],
default_families: &'a [&'a Family<'a>], default_families: &'a [&'a Family<'a>],
scripts: Vec<Script>, scripts: &'a [Script],
) -> Self { ) -> Self {
Self { Self {
font_system, font_system,

View file

@ -1,5 +1,7 @@
// SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-License-Identifier: MIT OR Apache-2.0
#![allow(clippy::too_many_arguments)]
#[cfg(not(feature = "std"))] #[cfg(not(feature = "std"))]
use alloc::vec::Vec; use alloc::vec::Vec;
use core::cmp::{max, min}; use core::cmp::{max, min};
@ -34,38 +36,62 @@ pub enum Shaping {
impl Shaping { impl Shaping {
fn run( fn run(
self, self,
scratch: &mut ShapeBuffer,
glyphs: &mut Vec<ShapeGlyph>,
font_system: &mut FontSystem, font_system: &mut FontSystem,
line: &str, line: &str,
attrs_list: &AttrsList, attrs_list: &AttrsList,
start_run: usize, start_run: usize,
end_run: usize, end_run: usize,
span_rtl: bool, span_rtl: bool,
) -> Vec<ShapeGlyph> { ) {
match self { match self {
#[cfg(feature = "swash")] #[cfg(feature = "swash")]
Self::Basic => shape_skip(font_system, line, attrs_list, start_run, end_run), Self::Basic => shape_skip(font_system, glyphs, line, attrs_list, start_run, end_run),
Self::Advanced => { Self::Advanced => shape_run(
shape_run(font_system, line, attrs_list, start_run, end_run, span_rtl) scratch,
} glyphs,
font_system,
line,
attrs_list,
start_run,
end_run,
span_rtl,
),
} }
} }
} }
/// A set of buffers containing allocations for shaped text.
#[derive(Default)]
pub struct ShapeBuffer {
/// Buffer for holding unicode text.
rustybuzz_buffer: Option<rustybuzz::UnicodeBuffer>,
/// Temporary buffers for scripts.
scripts: Vec<Script>,
/// Buffer for visual lines.
visual_lines: Vec<VisualLine>,
}
fn shape_fallback( fn shape_fallback(
scratch: &mut ShapeBuffer,
glyphs: &mut Vec<ShapeGlyph>,
font: &Font, font: &Font,
line: &str, line: &str,
attrs_list: &AttrsList, attrs_list: &AttrsList,
start_run: usize, start_run: usize,
end_run: usize, end_run: usize,
span_rtl: bool, span_rtl: bool,
) -> (Vec<ShapeGlyph>, Vec<usize>) { ) -> Vec<usize> {
let run = &line[start_run..end_run]; let run = &line[start_run..end_run];
let font_scale = font.rustybuzz().units_per_em() as f32; let font_scale = font.rustybuzz().units_per_em() as f32;
let ascent = font.rustybuzz().ascender() as f32 / font_scale; let ascent = font.rustybuzz().ascender() as f32 / font_scale;
let descent = -font.rustybuzz().descender() as f32 / font_scale; let descent = -font.rustybuzz().descender() as f32 / font_scale;
let mut buffer = rustybuzz::UnicodeBuffer::new(); let mut buffer = scratch.rustybuzz_buffer.take().unwrap_or_default();
buffer.set_direction(if span_rtl { buffer.set_direction(if span_rtl {
rustybuzz::Direction::RightToLeft rustybuzz::Direction::RightToLeft
} else { } else {
@ -82,7 +108,8 @@ fn shape_fallback(
let glyph_positions = glyph_buffer.glyph_positions(); let glyph_positions = glyph_buffer.glyph_positions();
let mut missing = Vec::new(); let mut missing = Vec::new();
let mut glyphs = Vec::with_capacity(glyph_infos.len()); glyphs.reserve(glyph_infos.len());
let glyph_start = glyphs.len();
for (info, pos) in glyph_infos.iter().zip(glyph_positions.iter()) { for (info, pos) in glyph_infos.iter().zip(glyph_positions.iter()) {
let x_advance = pos.x_advance as f32 / font_scale; let x_advance = pos.x_advance as f32 / font_scale;
let y_advance = pos.y_advance as f32 / font_scale; let y_advance = pos.y_advance as f32 / font_scale;
@ -115,7 +142,7 @@ fn shape_fallback(
// Adjust end of glyphs // Adjust end of glyphs
if rtl { if rtl {
for i in 1..glyphs.len() { for i in glyph_start + 1..glyphs.len() {
let next_start = glyphs[i - 1].start; let next_start = glyphs[i - 1].start;
let next_end = glyphs[i - 1].end; let next_end = glyphs[i - 1].end;
let prev = &mut glyphs[i]; let prev = &mut glyphs[i];
@ -126,7 +153,7 @@ fn shape_fallback(
} }
} }
} else { } else {
for i in (1..glyphs.len()).rev() { for i in (glyph_start + 1..glyphs.len()).rev() {
let next_start = glyphs[i].start; let next_start = glyphs[i].start;
let next_end = glyphs[i].end; let next_end = glyphs[i].end;
let prev = &mut glyphs[i - 1]; let prev = &mut glyphs[i - 1];
@ -138,19 +165,28 @@ fn shape_fallback(
} }
} }
(glyphs, missing) // Restore the buffer to save an allocation.
scratch.rustybuzz_buffer = Some(glyph_buffer.clear());
missing
} }
fn shape_run( fn shape_run(
scratch: &mut ShapeBuffer,
glyphs: &mut Vec<ShapeGlyph>,
font_system: &mut FontSystem, font_system: &mut FontSystem,
line: &str, line: &str,
attrs_list: &AttrsList, attrs_list: &AttrsList,
start_run: usize, start_run: usize,
end_run: usize, end_run: usize,
span_rtl: bool, span_rtl: bool,
) -> Vec<ShapeGlyph> { ) {
//TODO: use smallvec? // Re-use the previous script buffer if possible.
let mut scripts = Vec::new(); let mut scripts = {
let mut scripts = mem::take(&mut scratch.scripts);
scripts.clear();
scripts
};
for c in line[start_run..end_run].chars() { for c in line[start_run..end_run].chars() {
match c.script() { match c.script() {
Script::Common | Script::Inherited | Script::Latin | Script::Unknown => (), Script::Common | Script::Inherited | Script::Latin | Script::Unknown => (),
@ -162,19 +198,25 @@ fn shape_run(
} }
} }
log::trace!(" Run {:?}: '{}'", scripts, &line[start_run..end_run],); log::trace!(
" Run {:?}: '{}'",
&scratch.scripts,
&line[start_run..end_run],
);
let attrs = attrs_list.get_span(start_run); let attrs = attrs_list.get_span(start_run);
let fonts = font_system.get_font_matches(attrs); let fonts = font_system.get_font_matches(attrs);
let default_families = [&attrs.family]; let default_families = [&attrs.family];
let mut font_iter = FontFallbackIter::new(font_system, &fonts, &default_families, scripts); let mut font_iter = FontFallbackIter::new(font_system, &fonts, &default_families, &scripts);
let font = font_iter.next().expect("no default font found"); let font = font_iter.next().expect("no default font found");
let (mut glyphs, mut missing) = let glyph_start = glyphs.len();
shape_fallback(&font, line, attrs_list, start_run, end_run, span_rtl); let mut missing = shape_fallback(
scratch, glyphs, &font, line, attrs_list, start_run, end_run, span_rtl,
);
//TODO: improve performance! //TODO: improve performance!
while !missing.is_empty() { while !missing.is_empty() {
@ -187,8 +229,17 @@ fn shape_run(
"Evaluating fallback with font '{}'", "Evaluating fallback with font '{}'",
font_iter.face_name(font.id()) font_iter.face_name(font.id())
); );
let (mut fb_glyphs, fb_missing) = let mut fb_glyphs = Vec::new();
shape_fallback(&font, line, attrs_list, start_run, end_run, span_rtl); let fb_missing = shape_fallback(
scratch,
&mut fb_glyphs,
&font,
line,
attrs_list,
start_run,
end_run,
span_rtl,
);
// Insert all matching glyphs // Insert all matching glyphs
let mut fb_i = 0; let mut fb_i = 0;
@ -213,7 +264,7 @@ fn shape_run(
} }
// Find prior glyphs // Find prior glyphs
let mut i = 0; let mut i = glyph_start;
while i < glyphs.len() { while i < glyphs.len() {
if glyphs[i].start >= start && glyphs[i].end <= end { if glyphs[i].start >= start && glyphs[i].end <= end {
break; break;
@ -254,22 +305,24 @@ fn shape_run(
} }
*/ */
glyphs // Restore the scripts buffer.
scratch.scripts = scripts;
} }
#[cfg(feature = "swash")] #[cfg(feature = "swash")]
fn shape_skip( fn shape_skip(
font_system: &mut FontSystem, font_system: &mut FontSystem,
glyphs: &mut Vec<ShapeGlyph>,
line: &str, line: &str,
attrs_list: &AttrsList, attrs_list: &AttrsList,
start_run: usize, start_run: usize,
end_run: usize, end_run: usize,
) -> Vec<ShapeGlyph> { ) {
let attrs = attrs_list.get_span(start_run); let attrs = attrs_list.get_span(start_run);
let fonts = font_system.get_font_matches(attrs); let fonts = font_system.get_font_matches(attrs);
let default_families = [&attrs.family]; let default_families = [&attrs.family];
let mut font_iter = FontFallbackIter::new(font_system, &fonts, &default_families, Vec::new()); let mut font_iter = FontFallbackIter::new(font_system, &fonts, &default_families, &[]);
let font = font_iter.next().expect("no default font found"); let font = font_iter.next().expect("no default font found");
let font_id = font.id(); let font_id = font.id();
@ -282,29 +335,30 @@ fn shape_skip(
let ascent = metrics.ascent / f32::from(metrics.units_per_em); let ascent = metrics.ascent / f32::from(metrics.units_per_em);
let descent = metrics.descent / f32::from(metrics.units_per_em); let descent = metrics.descent / f32::from(metrics.units_per_em);
line[start_run..end_run] glyphs.extend(
.chars() line[start_run..end_run]
.enumerate() .chars()
.map(|(i, codepoint)| { .enumerate()
let glyph_id = charmap.map(codepoint); .map(|(i, codepoint)| {
let x_advance = glyph_metrics.advance_width(glyph_id); let glyph_id = charmap.map(codepoint);
let x_advance = glyph_metrics.advance_width(glyph_id);
ShapeGlyph { ShapeGlyph {
start: i, start: i,
end: i + 1, end: i + 1,
x_advance, x_advance,
y_advance: 0.0, y_advance: 0.0,
x_offset: 0.0, x_offset: 0.0,
y_offset: 0.0, y_offset: 0.0,
ascent, ascent,
descent, descent,
font_id, font_id,
glyph_id, glyph_id,
color_opt: attrs.color_opt, color_opt: attrs.color_opt,
metadata: attrs.metadata, metadata: attrs.metadata,
} }
}) }),
.collect() );
} }
/// A shaped glyph /// A shaped glyph
@ -369,6 +423,30 @@ impl ShapeWord {
level: unicode_bidi::Level, level: unicode_bidi::Level,
blank: bool, blank: bool,
shaping: Shaping, shaping: Shaping,
) -> Self {
Self::new_in_buffer(
&mut ShapeBuffer::default(),
font_system,
line,
attrs_list,
word_range,
level,
blank,
shaping,
)
}
/// Shape a word into a set of glyphs, using a scratch buffer.
#[allow(clippy::too_many_arguments)]
pub fn new_in_buffer(
scratch: &mut ShapeBuffer,
font_system: &mut FontSystem,
line: &str,
attrs_list: &AttrsList,
word_range: Range<usize>,
level: unicode_bidi::Level,
blank: bool,
shaping: Shaping,
) -> Self { ) -> Self {
let word = &line[word_range.clone()]; let word = &line[word_range.clone()];
@ -387,30 +465,32 @@ impl ShapeWord {
let start_egc = word_range.start + egc_i; let start_egc = word_range.start + egc_i;
let attrs_egc = attrs_list.get_span(start_egc); let attrs_egc = attrs_list.get_span(start_egc);
if !attrs.compatible(&attrs_egc) { if !attrs.compatible(&attrs_egc) {
//TODO: more efficient shaping.run(
glyphs.append(&mut shaping.run( scratch,
&mut glyphs,
font_system, font_system,
line, line,
attrs_list, attrs_list,
start_run, start_run,
start_egc, start_egc,
span_rtl, span_rtl,
)); );
start_run = start_egc; start_run = start_egc;
attrs = attrs_egc; attrs = attrs_egc;
} }
} }
if start_run < word_range.end { if start_run < word_range.end {
//TODO: more efficient shaping.run(
glyphs.append(&mut shaping.run( scratch,
&mut glyphs,
font_system, font_system,
line, line,
attrs_list, attrs_list,
start_run, start_run,
word_range.end, word_range.end,
span_rtl, span_rtl,
)); );
} }
let mut x_advance = 0.0; let mut x_advance = 0.0;
@ -445,6 +525,29 @@ impl ShapeSpan {
line_rtl: bool, line_rtl: bool,
level: unicode_bidi::Level, level: unicode_bidi::Level,
shaping: Shaping, shaping: Shaping,
) -> Self {
Self::new_in_buffer(
&mut ShapeBuffer::default(),
font_system,
line,
attrs_list,
span_range,
line_rtl,
level,
shaping,
)
}
/// Shape a span into a set of words, using a scratch buffer.
pub fn new_in_buffer(
scratch: &mut ShapeBuffer,
font_system: &mut FontSystem,
line: &str,
attrs_list: &AttrsList,
span_range: Range<usize>,
line_rtl: bool,
level: unicode_bidi::Level,
shaping: Shaping,
) -> Self { ) -> Self {
let span = &line[span_range.start..span_range.end]; let span = &line[span_range.start..span_range.end];
@ -468,7 +571,8 @@ impl ShapeSpan {
} }
} }
if start_word < start_lb { if start_word < start_lb {
words.push(ShapeWord::new( words.push(ShapeWord::new_in_buffer(
scratch,
font_system, font_system,
line, line,
attrs_list, attrs_list,
@ -481,7 +585,8 @@ impl ShapeSpan {
if start_lb < end_lb { if start_lb < end_lb {
for (i, c) in span[start_lb..end_lb].char_indices() { for (i, c) in span[start_lb..end_lb].char_indices() {
// assert!(c.is_whitespace()); // assert!(c.is_whitespace());
words.push(ShapeWord::new( words.push(ShapeWord::new_in_buffer(
scratch,
font_system, font_system,
line, line,
attrs_list, attrs_list,
@ -538,6 +643,27 @@ impl ShapeLine {
line: &str, line: &str,
attrs_list: &AttrsList, attrs_list: &AttrsList,
shaping: Shaping, shaping: Shaping,
) -> Self {
Self::new_in_buffer(
&mut ShapeBuffer::default(),
font_system,
line,
attrs_list,
shaping,
)
}
/// Shape a line into a set of spans, using a scratch buffer.
///
/// # Panics
///
/// Will panic if `line` contains more than one paragraph.
pub fn new_in_buffer(
scratch: &mut ShapeBuffer,
font_system: &mut FontSystem,
line: &str,
attrs_list: &AttrsList,
shaping: Shaping,
) -> Self { ) -> Self {
let mut spans = Vec::new(); let mut spans = Vec::new();
@ -558,6 +684,7 @@ impl ShapeLine {
// Each span is a set of characters with equal levels. // Each span is a set of characters with equal levels.
let mut start = line_range.start; let mut start = line_range.start;
let mut run_level = levels[start]; let mut run_level = levels[start];
spans.reserve(line_range.end - start + 1);
for (i, &new_level) in levels for (i, &new_level) in levels
.iter() .iter()
@ -567,7 +694,8 @@ impl ShapeLine {
{ {
if new_level != run_level { if new_level != run_level {
// End of the previous run, start of a new one. // End of the previous run, start of a new one.
spans.push(ShapeSpan::new( spans.push(ShapeSpan::new_in_buffer(
scratch,
font_system, font_system,
line, line,
attrs_list, attrs_list,
@ -580,7 +708,8 @@ impl ShapeLine {
run_level = new_level; run_level = new_level;
} }
} }
spans.push(ShapeSpan::new( spans.push(ShapeSpan::new_in_buffer(
scratch,
font_system, font_system,
line, line,
attrs_list, attrs_list,
@ -719,8 +848,27 @@ impl ShapeLine {
wrap: Wrap, wrap: Wrap,
align: Option<Align>, align: Option<Align>,
) -> Vec<LayoutLine> { ) -> Vec<LayoutLine> {
let mut layout_lines = Vec::with_capacity(1); let mut lines = Vec::with_capacity(1);
self.layout_to_buffer(
&mut ShapeBuffer::default(),
font_size,
line_width,
wrap,
align,
&mut lines,
);
lines
}
pub fn layout_to_buffer(
&self,
scratch: &mut ShapeBuffer,
font_size: f32,
line_width: f32,
wrap: Wrap,
align: Option<Align>,
layout_lines: &mut Vec<LayoutLine>,
) {
let align = align.unwrap_or({ let align = align.unwrap_or({
if self.rtl { if self.rtl {
Align::Right Align::Right
@ -735,7 +883,11 @@ impl ShapeLine {
// For each visual line a list of (span index, and range of words in that span) // For each visual line a list of (span index, and range of words in that span)
// Note that a BiDi visual line could have multiple spans or parts of them // Note that a BiDi visual line could have multiple spans or parts of them
// let mut vl_range_of_spans = Vec::with_capacity(1); // let mut vl_range_of_spans = Vec::with_capacity(1);
let mut visual_lines: Vec<VisualLine> = Vec::with_capacity(1); let mut visual_lines: Vec<VisualLine> = {
let mut visual_lines = mem::take(&mut scratch.visual_lines);
visual_lines.clear();
visual_lines
};
fn add_to_visual_line( fn add_to_visual_line(
vl: &mut VisualLine, vl: &mut VisualLine,
@ -1193,6 +1345,7 @@ impl ShapeLine {
}); });
} }
layout_lines // Restore the buffer to the scratch set to prevent reallocations.
scratch.visual_lines = visual_lines;
} }
} }