Fix infinite loop when a text run shapes to no glyphs - #22119
Merged
MrJul merged 3 commits intoSep 1, 2026
Conversation
A shaper hides the default ignorables it substitutes for line breaks behind the font's space glyph, and deletes them when the font has no such glyph. A run holding nothing but a line break then shapes to an empty glyph buffer while still owning its characters. - TextFormatter drops that run today, so the line covers no text and the layout loop never advances past it - measuring a wrapping TextBlock whose text starts with a newline never returns. - GlyphRun indexes its glyph list without checking it is non-empty, so hit-testing such a run throws IndexOutOfRangeException. Covers all three layers: GlyphRun hit-testing, the formatted line, and the TextLayout repro from the issue. The layout test bounds maxLines so a regression fails instead of hanging the test run. The Skia tests embed the headless platform's BareMinimum.ttf, which has four glyphs and no space, rather than a copy of it.
ShapeTogether split the shaped buffer by glyph count, so a run whose glyphs were all deleted by the shaper was skipped and its characters rolled into previousLength for a following run to absorb. When no such run follows, the characters are lost: the line reports a length of 0 and TextLayout formats the same line forever. Splitting by text length preserves the case the accumulation exists for - a run whose glyphs merged into a neighbouring cluster splits off no text either - and stops discarding characters that shaped to nothing. That makes zero-glyph glyph runs reachable for the first time, so: - GetDistanceFromCharacterHit, FindGlyphIndex and FindNearestCharacterHit handle an empty glyph list. There is no cluster to snap to and the run sits at a single position, so it degenerates to one zero-width cluster. - InkBounds answers empty directly instead of building a platform glyph run. TextLineImpl reads it for every shaped run in a line, and the Skia implementation creates an SKFont and measures glyph widths in its constructor - wasted on a run that marks nothing. Fixes AvaloniaUI#22004
3 tasks
Member
|
This also supersedes #21686, right? |
MrJul
reviewed
Sep 1, 2026
| new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height)); | ||
|
|
||
| public Rect InkBounds => PlatformImpl.Item.Bounds; | ||
| // A run with no glyphs marks nothing, so its ink bounds are empty by definition. Answering |
Member
There was a problem hiding this comment.
Other added comments genuinely help to understand the algorithm, but this one is a bit verbose for its purpose. Please reduce the clanker's comment here: "only allocate the platform impl if needed", or something along those lines.
|
You can test this PR using the following package version. |
Contributor
Author
Update: I had a deeper look at that issue, and it is not related. |
Gillibald
force-pushed
the
fix/runs-that-shape-to-no-glyphs
branch
from
September 1, 2026 10:22
41fd133 to
697b533
Compare
MrJul
enabled auto-merge
September 1, 2026 10:37
|
You can test this PR using the following package version. |
MrJul
pushed a commit
to MrJul/Avalonia
that referenced
this pull request
Sep 2, 2026
* Add failing tests for runs that shape to no glyphs A shaper hides the default ignorables it substitutes for line breaks behind the font's space glyph, and deletes them when the font has no such glyph. A run holding nothing but a line break then shapes to an empty glyph buffer while still owning its characters. - TextFormatter drops that run today, so the line covers no text and the layout loop never advances past it - measuring a wrapping TextBlock whose text starts with a newline never returns. - GlyphRun indexes its glyph list without checking it is non-empty, so hit-testing such a run throws IndexOutOfRangeException. Covers all three layers: GlyphRun hit-testing, the formatted line, and the TextLayout repro from the issue. The layout test bounds maxLines so a regression fails instead of hanging the test run. The Skia tests embed the headless platform's BareMinimum.ttf, which has four glyphs and no space, rather than a copy of it. * Keep text runs that shape to no glyphs ShapeTogether split the shaped buffer by glyph count, so a run whose glyphs were all deleted by the shaper was skipped and its characters rolled into previousLength for a following run to absorb. When no such run follows, the characters are lost: the line reports a length of 0 and TextLayout formats the same line forever. Splitting by text length preserves the case the accumulation exists for - a run whose glyphs merged into a neighbouring cluster splits off no text either - and stops discarding characters that shaped to nothing. That makes zero-glyph glyph runs reachable for the first time, so: - GetDistanceFromCharacterHit, FindGlyphIndex and FindNearestCharacterHit handle an empty glyph list. There is no cluster to snap to and the run sits at a single position, so it degenerates to one zero-width cluster. - InkBounds answers empty directly instead of building a platform glyph run. TextLineImpl reads it for every shaped run in a line, and the Skia implementation creates an SKFont and measures glyph widths in its constructor - wasted on a run that marks nothing. Fixes AvaloniaUI#22004 * Adjust GlyphRun.InkBounds xml comment
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does the pull request do?
Fixes an infinite loop and unbounded allocation in
TextLayoutwhen a text run shapes to no glyphs.What is the current behavior?
A shaper hides the default ignorables it substitutes for line breaks behind the font's space glyph, and deletes them when the font has none. A run holding nothing but a line break then shapes to an empty glyph buffer while still owning its characters.
TextFormatterImpl.ShapeTogethersplits the shaped buffer by glyph count, so it skips that run and rolls its characters intopreviousLengthfor a following run to absorb. When no such run follows they are lost: the line reports a length of 0 with no line break,TextLayout.CreateTextLinesnever advances_textSourceLength, and it formats the same line forever until the process runs out of memory.Line breaks are the common trigger, not the only one. Any run that is entirely default-ignorable takes the same path - with the headless
BareMinimum.ttf,U+200D,U+200BandU+FE0Feach shape to a run of length 1 with zero glyphs.GlyphRunalso indexes its glyph list without checking it is non-empty, so hit-testing a run with characters and no glyphs throwsIndexOutOfRangeException.What is the updated/expected behavior with this PR?
ShapeTogethersplits on text length, so a run that shapes to no glyphs keeps its characters and the layout advances. Text starting with a line break lays out as an empty first line followed by the wrapped text.GlyphRunhit-testing handles an empty glyph list, degenerating to a single zero-width cluster.GlyphRun.InkBoundsanswers empty without building a platform glyph run.TextLineImplreads it for every shaped run in a line, and the Skia implementation creates anSKFontand measures glyph widths in its constructor.Checklist
Breaking changes
None. No public API is added or changed.
Obsoletions / Deprecations
None.
Fixed issues
Fixes #22004
🤖 Generated with Claude Code