diff --git a/core/src/layout_diagnostics.rs b/core/src/layout_diagnostics.rs index 7f22d98..f986803 100644 --- a/core/src/layout_diagnostics.rs +++ b/core/src/layout_diagnostics.rs @@ -54,6 +54,7 @@ pub(crate) enum Counter { TextRenders, TextShapeHits, TextShapes, + TextBreaks, GlyphPlacements, } @@ -90,6 +91,7 @@ impl Counter { "text renders", "text shape hits", "text shapes", + "text line breaks", "glyph placements", ]; } @@ -102,6 +104,7 @@ pub(crate) enum TimerKind { IncrementalLayout, TextRender, TextShape, + TextBreak, GlyphPlacement, } @@ -115,6 +118,7 @@ impl TimerKind { "incremental layout", "text render", "text shape", + "text line break", "glyph placement", ]; } diff --git a/core/src/primitive/text.rs b/core/src/primitive/text.rs index f83419e..8281225 100644 --- a/core/src/primitive/text.rs +++ b/core/src/primitive/text.rs @@ -22,30 +22,31 @@ pub struct TextData { pub layout_ctx: LayoutContext, scale_ctx: ScaleContext, pub atlas: GlyphAtlas, - spare: VecDeque, + spare: VecDeque, } -/// One shaping of some text, and the glyphs placed from it. A buffer holds -/// the one it is drawn as; these are the ones it had before, kept because a -/// container measures a child by drawing it in a box it may not keep, and so -/// comes back to widths it has already asked for. -struct Shaping { - /// A shaping is a function of these three and nothing else, so no widget - /// or buffer identity is involved and two texts of the same words share - /// an answer. +/// The glyphs of one text at one width. A buffer holds the ones it is drawn +/// as; these are the ones it had before, kept because a container measures a +/// child by drawing it in a box it may not keep, and so comes back to widths +/// it has already asked for. +struct Placed { + /// Where the glyphs land is a function of these three and nothing else, + /// so no widget or buffer identity is involved and two texts of the same + /// words share an answer. text: String, key: LayoutKey, - layout: Layout, - placed: Option, + glyphs: RenderedText, } -/// How many to keep. Bounding the whole cache rather than each buffer is -/// what makes this a fixed cost instead of one a tree of ten thousand texts -/// pays ten thousand times; the re-asks come from laying out one subtree, so -/// they are close together and few are needed. On `tests/revision_cost.rs` -/// under `SWEEP=1`, the case that cannot hit across frames, 32 is not enough -/// (6.6 ms) and 64 is (4.4 ms). -const SPARE_SHAPINGS: usize = 128; +/// How many to keep. Bounding the whole store rather than each buffer is what +/// makes this a fixed cost instead of one a tree of ten thousand texts pays +/// ten thousand times; the re-asks come from laying out one subtree, so they +/// are close together and few are needed. Instructions over 500 resize frames +/// of `tests/revision_cost.rs`, both the repeating widths and the sweep that +/// cannot hit across frames: 13.7B at 32, 12.1B at 64, 10.4B and 12.1B at 128, +/// and nothing past that -- so 128, which is no worse in the case that never +/// repeats and better in the one that does. +const SPARE_PLACED: usize = 128; impl Default for TextData { fn default() -> Self { @@ -188,21 +189,31 @@ impl TextBuffer { diag::bump(Counter::TextShapeHits); return; } - let kept = data.take_shaping(&self.text, &layout_key); - if let Some(key) = self.layout_key.take() { - data.keep_shaping(Shaping { + let same_shaping = self + .layout_key + .as_ref() + .is_some_and(|key| key.attrs == *attrs); + let old_key = self.layout_key.replace(layout_key); + // The glyphs it holds are of the width it held, which the layout may + // well come back to. + if let Some(key) = old_key + && let Some(glyphs) = self.placed.take() + { + data.keep_placed(Placed { text: self.text.clone(), key, - layout: std::mem::replace(&mut self.layout, Layout::new()), - placed: self.placed.take(), + glyphs, }); } - if let Some(shaping) = kept { + // Only the line breaking depends on the width: the shaped runs under + // it are a function of the text and the attrs, and parley re-breaks + // them in place. So a new width is a break, not a shaping. + if same_shaping { #[cfg(feature = "layout-diagnostics")] - diag::bump(Counter::TextShapeHits); - self.layout = shaping.layout; - self.placed = shaping.placed; - self.layout_key = Some(shaping.key); + diag::bump(Counter::TextBreaks); + #[cfg(feature = "layout-diagnostics")] + let _break = diag::timer(TimerKind::TextBreak); + self.break_lines(width); return; } #[cfg(feature = "layout-diagnostics")] @@ -219,10 +230,13 @@ impl TextBuffer { ))); builder.push_default(StyleProperty::Brush(attrs.color)); builder.build_into(&mut self.layout, &self.text); + self.break_lines(width); + } + + fn break_lines(&mut self, width: Option) { self.layout.break_all_lines(width); self.layout .align(Alignment::Start, AlignmentOptions::default()); - self.layout_key = Some(layout_key); } } @@ -334,21 +348,21 @@ pub struct RenderedText { } impl TextData { - /// The shaping for this text at this width, taken out of what is kept. - fn take_shaping(&mut self, text: &str, key: &LayoutKey) -> Option { + /// The glyphs of this text at this width, taken out of what is kept. + fn take_placed(&mut self, text: &str, key: &LayoutKey) -> Option { // From the newest, since a re-ask is usually of something recent. let at = self .spare .iter() .rposition(|spare| spare.key == *key && spare.text == text)?; - self.spare.remove(at) + self.spare.remove(at).map(|spare| spare.glyphs) } - fn keep_shaping(&mut self, shaping: Shaping) { - if self.spare.len() >= SPARE_SHAPINGS { + fn keep_placed(&mut self, placed: Placed) { + if self.spare.len() >= SPARE_PLACED { self.spare.pop_front(); } - self.spare.push_back(shaping); + self.spare.push_back(placed); } pub fn render<'b>( @@ -362,7 +376,13 @@ impl TextData { #[cfg(feature = "layout-diagnostics")] let _render = diag::timer(TimerKind::TextRender); buffer.shape(self, attrs, width); - let placed = match buffer.placed.take() { + // Only asked for when the buffer no longer holds them: taking one out + // of the store to then drop it would throw an answer away. + let placed = buffer.placed.take().or_else(|| { + let key = buffer.layout_key.as_ref()?; + self.take_placed(&buffer.text, key) + }); + let placed = match placed { Some(placed) => placed, None => { #[cfg(feature = "layout-diagnostics")]