Skip to content

Commit fbecc87

Browse files
perf(glyph): skip preparation for unchanged paragraphs (#276)
1 parent b7118d0 commit fbecc87

3 files changed

Lines changed: 309 additions & 0 deletions

File tree

‎.agents/docs/attestations/2026-10-09-glyph-51c1436a.md‎

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎.agents/docs/packages/glyph.md‎

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/glyph/rust/shaper/src/engine/state.rs‎

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,8 @@ fn index_paragraph_records(
369369

370370
#[derive(Default)]
371371
struct ParagraphState {
372+
#[cfg(test)]
373+
preparation_count: usize,
372374
text: Staged<TextStage>,
373375
/// Whether the pending buffers are a byte-identical copy of the committed ones, so a
374376
/// mutation batch can skip re-seeding them.
@@ -777,6 +779,19 @@ impl TextEngine {
777779
.ok_or(EngineError::RootMissing)
778780
}
779781

782+
#[cfg(test)]
783+
fn planner_paragraph_preparation_count(
784+
&self,
785+
root: u32,
786+
paragraph: u32,
787+
) -> Result<usize, EngineError> {
788+
self.planners
789+
.get(&root)
790+
.and_then(|planner| planner.paragraph(paragraph))
791+
.map(|paragraph| paragraph.state.preparation_count)
792+
.ok_or(EngineError::RootMissing)
793+
}
794+
780795
pub fn root_count(&self) -> u32 {
781796
self.planners.len().try_into().unwrap_or(u32::MAX)
782797
}
@@ -1528,6 +1543,10 @@ impl TextEngine {
15281543
let paragraph = planner
15291544
.paragraph_mut(paragraph_id)
15301545
.ok_or(EngineError::InvalidRequest)?;
1546+
if spans.is_empty() && !paragraph.state.has_pending_preparation() {
1547+
paragraph.positioned_changed = false;
1548+
continue;
1549+
}
15311550
let (prefix_adopted, geometry_adopted) = if adopted.is_some() {
15321551
paragraph.state.speculative_match(text, styles, geometry)
15331552
} else {
@@ -2823,6 +2842,19 @@ impl PlannerState {
28232842
}
28242843

28252844
impl ParagraphState {
2845+
fn has_pending_preparation(&self) -> bool {
2846+
self.text.is_prepared()
2847+
|| self.styles.is_prepared()
2848+
|| self.unicode.is_prepared()
2849+
|| self.bidi.is_prepared()
2850+
|| self.shaping_runs.is_prepared()
2851+
|| self.shape.is_prepared()
2852+
|| self.clusters.is_prepared()
2853+
|| self.geometry.is_prepared()
2854+
|| self.flow_layout.is_prepared()
2855+
|| self.positioned.is_prepared()
2856+
}
2857+
28262858
/// Clears paragraph identity and committed/pending semantics while retaining every allocation.
28272859
#[inline(never)]
28282860
fn reset_for_reuse(&mut self) {
@@ -2949,6 +2981,10 @@ impl ParagraphState {
29492981
next_glyph_id: &mut u32,
29502982
next_content_revision: &mut u32,
29512983
) -> Result<bool, EngineError> {
2984+
#[cfg(test)]
2985+
{
2986+
self.preparation_count += 1;
2987+
}
29522988
self.prepare_text(text_mutations)?;
29532989
self.prepare_styles(style_mutations, |handle| {
29542990
font_stacks
@@ -6107,6 +6143,262 @@ mod tests {
61076143
assert_eq!(engine.dispose_root(4), Err(EngineError::RootMissing));
61086144
}
61096145

6146+
#[test]
6147+
fn unchanged_paragraphs_skip_real_shaper_preparation_across_lifecycle_transitions() {
6148+
const INTER: &[u8] =
6149+
include_bytes!("../../../../../../benches/fixtures/fonts/inter-v4.1/Inter-Regular.ttf");
6150+
const GLYPH_COUNT: u32 = 2937;
6151+
6152+
let mut shaper = ShaperRegistry::default();
6153+
let extents = vec![0; GLYPH_COUNT as usize * 8];
6154+
let availability = vec![0; (GLYPH_COUNT as usize).div_ceil(8)];
6155+
assert_eq!(
6156+
shaper.register_font(101, INTER, &extents, &availability, 0, 0),
6157+
crate::STATUS_OK
6158+
);
6159+
6160+
let mut engine = TextEngine::default();
6161+
engine
6162+
.register_codec(9, validated_codec(TechniqueId(1)))
6163+
.unwrap();
6164+
engine
6165+
.register_font_binding(20, 101, GLYPH_COUNT, render_binding(GLYPH_COUNT, 1))
6166+
.unwrap();
6167+
engine.register_font_stack(7, &[20]).unwrap();
6168+
engine.create_root(4).unwrap();
6169+
6170+
let lifecycle = paragraph_mutation_bytes(&[
6171+
(PARAGRAPH_MUTATION_UPSERT, 1, 1),
6172+
(PARAGRAPH_MUTATION_UPSERT, 2, 2),
6173+
(PARAGRAPH_MUTATION_UPSERT, 3, 3),
6174+
]);
6175+
let text = paragraph_text_mutation_bytes(&[
6176+
(1, 0, 0, &[0x61]),
6177+
(2, 0, 0, &[0x62]),
6178+
(3, 0, 0, &[0x63]),
6179+
]);
6180+
let styles = paragraph_root_style_bytes(&[(1, 1), (2, 1), (3, 1)]);
6181+
let mut initial = update(0, 0, 0);
6182+
initial.limits.max_paragraphs = 3;
6183+
initial.limits.max_clusters = 8;
6184+
initial.limits.max_lines = 8;
6185+
initial.paragraph_mutations =
6186+
parse_paragraph_mutations(&lifecycle, ENGINE_UPDATE_REQUEST_HEADER_SIZE, 3).unwrap();
6187+
initial.text_mutations =
6188+
parse_text_mutations(&text, ENGINE_UPDATE_REQUEST_HEADER_SIZE, 3).unwrap();
6189+
initial.style_mutations =
6190+
parse_style_mutations(&styles, ENGINE_UPDATE_REQUEST_HEADER_SIZE, 3).unwrap();
6191+
let prepared = engine
6192+
.prepare_update_with_shaper(&mut shaper, initial, 1)
6193+
.unwrap();
6194+
engine.commit_update(prepared).unwrap();
6195+
for paragraph_id in 1..=3 {
6196+
assert_eq!(
6197+
engine.planner_paragraph_preparation_count(4, paragraph_id),
6198+
Ok(1)
6199+
);
6200+
assert_eq!(
6201+
engine
6202+
.planners
6203+
.get(&4)
6204+
.unwrap()
6205+
.paragraph(paragraph_id)
6206+
.unwrap()
6207+
.state
6208+
.shape
6209+
.active()
6210+
.glyph_ids
6211+
.len(),
6212+
1
6213+
);
6214+
}
6215+
6216+
let sibling_text = paragraph_text_mutation_bytes(&[(2, 0, 1, &[0x79])]);
6217+
let mut sibling = update(1, 1, 1);
6218+
sibling.limits.max_paragraphs = 3;
6219+
sibling.limits.max_clusters = 8;
6220+
sibling.limits.max_lines = 8;
6221+
sibling.text_mutations =
6222+
parse_text_mutations(&sibling_text, ENGINE_UPDATE_REQUEST_HEADER_SIZE, 1).unwrap();
6223+
let prepared = engine
6224+
.prepare_update_with_shaper(&mut shaper, sibling, 2)
6225+
.unwrap();
6226+
engine.commit_update(prepared).unwrap();
6227+
assert_eq!(engine.planner_paragraph_preparation_count(4, 1), Ok(1));
6228+
assert_eq!(engine.planner_paragraph_preparation_count(4, 2), Ok(2));
6229+
assert_eq!(engine.planner_paragraph_preparation_count(4, 3), Ok(1));
6230+
assert_eq!(
6231+
engine
6232+
.planners
6233+
.get(&4)
6234+
.unwrap()
6235+
.paragraph(2)
6236+
.unwrap()
6237+
.state
6238+
.text
6239+
.committed()
6240+
.units,
6241+
[0x79]
6242+
);
6243+
6244+
let speculative_text = paragraph_text_mutation_bytes(&[(1, 0, 1, &[0x71])]);
6245+
let mut query = update(2, 2, 2);
6246+
query.limits.max_paragraphs = 3;
6247+
query.limits.max_clusters = 8;
6248+
query.limits.max_lines = 8;
6249+
query.text_mutations =
6250+
parse_text_mutations(&speculative_text, ENGINE_UPDATE_REQUEST_HEADER_SIZE, 1).unwrap();
6251+
engine
6252+
.measure_paragraph_with_shaper(&mut shaper, query, 1)
6253+
.unwrap();
6254+
assert_eq!(engine.planner_paragraph_preparation_count(4, 1), Ok(2));
6255+
assert!(
6256+
engine
6257+
.planners
6258+
.get(&4)
6259+
.unwrap()
6260+
.paragraph(1)
6261+
.unwrap()
6262+
.state
6263+
.has_pending_preparation()
6264+
);
6265+
assert_eq!(
6266+
engine
6267+
.planners
6268+
.get(&4)
6269+
.unwrap()
6270+
.paragraph(1)
6271+
.unwrap()
6272+
.state
6273+
.text
6274+
.committed()
6275+
.units,
6276+
[0x61]
6277+
);
6278+
assert_eq!(
6279+
engine
6280+
.planners
6281+
.get(&4)
6282+
.unwrap()
6283+
.paragraph(1)
6284+
.unwrap()
6285+
.state
6286+
.text
6287+
.pending()
6288+
.units,
6289+
[0x71]
6290+
);
6291+
6292+
let next_sibling_text = paragraph_text_mutation_bytes(&[(2, 0, 1, &[0x7a])]);
6293+
let mut next_sibling = update(2, 2, 2);
6294+
next_sibling.limits.max_paragraphs = 3;
6295+
next_sibling.limits.max_clusters = 8;
6296+
next_sibling.limits.max_lines = 8;
6297+
next_sibling.text_mutations =
6298+
parse_text_mutations(&next_sibling_text, ENGINE_UPDATE_REQUEST_HEADER_SIZE, 1).unwrap();
6299+
let prepared = engine
6300+
.prepare_update_with_shaper(&mut shaper, next_sibling, 3)
6301+
.unwrap();
6302+
engine.commit_update(prepared).unwrap();
6303+
assert_eq!(engine.planner_paragraph_preparation_count(4, 1), Ok(3));
6304+
assert_eq!(engine.planner_paragraph_preparation_count(4, 2), Ok(3));
6305+
assert_eq!(engine.planner_paragraph_preparation_count(4, 3), Ok(1));
6306+
assert_eq!(
6307+
engine
6308+
.planners
6309+
.get(&4)
6310+
.unwrap()
6311+
.paragraph(1)
6312+
.unwrap()
6313+
.state
6314+
.text
6315+
.committed()
6316+
.units,
6317+
[0x61]
6318+
);
6319+
6320+
let retry_text = paragraph_text_mutation_bytes(&[(3, 0, 1, &[0x64])]);
6321+
let mut retry_request = update(3, 3, 3);
6322+
retry_request.limits.max_paragraphs = 3;
6323+
retry_request.limits.max_clusters = 8;
6324+
retry_request.limits.max_lines = 8;
6325+
retry_request.text_mutations =
6326+
parse_text_mutations(&retry_text, ENGINE_UPDATE_REQUEST_HEADER_SIZE, 1).unwrap();
6327+
let prepared = engine
6328+
.prepare_update_with_shaper(&mut shaper, retry_request, 4)
6329+
.unwrap();
6330+
engine.abort_update(prepared).unwrap();
6331+
assert_eq!(engine.planner_paragraph_preparation_count(4, 1), Ok(3));
6332+
assert_eq!(engine.planner_paragraph_preparation_count(4, 2), Ok(3));
6333+
assert_eq!(engine.planner_paragraph_preparation_count(4, 3), Ok(2));
6334+
assert_eq!(
6335+
engine
6336+
.planners
6337+
.get(&4)
6338+
.unwrap()
6339+
.paragraph(3)
6340+
.unwrap()
6341+
.state
6342+
.text
6343+
.committed()
6344+
.units,
6345+
[0x63]
6346+
);
6347+
let prepared = engine
6348+
.prepare_update_with_shaper(&mut shaper, retry_request, 4)
6349+
.unwrap();
6350+
engine.commit_update(prepared).unwrap();
6351+
assert_eq!(engine.planner_paragraph_preparation_count(4, 1), Ok(3));
6352+
assert_eq!(engine.planner_paragraph_preparation_count(4, 2), Ok(3));
6353+
assert_eq!(engine.planner_paragraph_preparation_count(4, 3), Ok(3));
6354+
assert_eq!(
6355+
engine
6356+
.planners
6357+
.get(&4)
6358+
.unwrap()
6359+
.paragraph(3)
6360+
.unwrap()
6361+
.state
6362+
.text
6363+
.committed()
6364+
.units,
6365+
[0x64]
6366+
);
6367+
6368+
engine.dispose_root(4).unwrap();
6369+
engine.create_root(4).unwrap();
6370+
let recreated_lifecycle = paragraph_mutation_bytes(&[(PARAGRAPH_MUTATION_UPSERT, 4, 1)]);
6371+
let recreated_text = paragraph_text_mutation_bytes(&[(4, 0, 0, &[0x65])]);
6372+
let recreated_style = paragraph_root_style_bytes(&[(4, 1)]);
6373+
let mut recreated = update(0, 0, 0);
6374+
recreated.paragraph_mutations =
6375+
parse_paragraph_mutations(&recreated_lifecycle, ENGINE_UPDATE_REQUEST_HEADER_SIZE, 1)
6376+
.unwrap();
6377+
recreated.text_mutations =
6378+
parse_text_mutations(&recreated_text, ENGINE_UPDATE_REQUEST_HEADER_SIZE, 1).unwrap();
6379+
recreated.style_mutations =
6380+
parse_style_mutations(&recreated_style, ENGINE_UPDATE_REQUEST_HEADER_SIZE, 1).unwrap();
6381+
let prepared = engine
6382+
.prepare_update_with_shaper(&mut shaper, recreated, 5)
6383+
.unwrap();
6384+
engine.commit_update(prepared).unwrap();
6385+
assert_eq!(engine.planner_paragraph_preparation_count(4, 4), Ok(1));
6386+
assert_eq!(
6387+
engine
6388+
.planners
6389+
.get(&4)
6390+
.unwrap()
6391+
.paragraph(4)
6392+
.unwrap()
6393+
.state
6394+
.shape
6395+
.active()
6396+
.glyph_ids
6397+
.len(),
6398+
1
6399+
);
6400+
}
6401+
61106402
#[test]
61116403
fn update_rejects_a_capability_set_outside_the_registered_codec() {
61126404
let mut engine = TextEngine::default();

0 commit comments

Comments
 (0)