@@ -369,6 +369,8 @@ fn index_paragraph_records(
369369
370370#[ derive( Default ) ]
371371struct 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
28252844impl 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