Skip to content

fix(text-editor): keep huge text from corrupting the glyph atlas - #908

Merged
hm21 merged 2 commits into
stablefrom
fix/large-text-glyph-atlas
Oct 26, 2026
Merged

hm21 merged 2 commits into
stablefrom
fix/large-text-glyph-atlas

Conversation

@hm21

@hm21 hm21 commented Oct 26, 2026

Copy link
Copy Markdown
Owner

Description

Problem. Scaling a text layer up very large, or zooming the canvas onto big text, could leave text across the whole app garbled until the app restarted: letters missing or replaced by pieces of other letters, editor icons turning into blocks. Impeller rasterizes every glyph into one atlas shared by all text, including glyphs it then draws as paths (above 250 device pixels). The editor applies a text layer's scale as its font size, and Impeller only caps the canvas scale at 48×, not the font size, so the glyphs of a big layer reached thousands of pixels. When growing the atlas failed, Impeller did not roll back its bookkeeping, and every glyph added after that was drawn from the wrong place. With 14.19.0 a long text layer broke the atlas from about 7.5× and a short one between 14× and 20×. Zooming the canvas 5× onto a 192 pt outlined text broke it as well.

Approach. Text above 64 pixels keeps its layout at its real size and only draws its glyphs from copies set in a 5.25 pixel font, scaled up onto the place each run has in that layout.

  • The real TextPainter still decides everything else. Size, line breaks, background, hit testing and captures are unchanged. A smaller layout would not match: SkParagraph rounds every line height to a whole unit, so a small layout cannot reproduce the real line positions.
  • Each run is anchored on the baseline SkParagraph actually draws, which is top + floor(ascent + 0.5). The reported LineMetrics.baseline uses the rounded line height instead, and anchoring there shifted large text by up to 4 px.
  • Why 5.25 px. It is just above 250 / 48, so the copies reach Impeller's 48× cap where Impeller starts drawing paths. Below that, the atlas still holds each glyph at the size it shows. Above it, every size shares the same entries of about 250 px, so resizing or zooming no longer adds a new set of glyphs per frame. With 8 px copies, pinch-scaling outlined text corrupted glyphs already in the atlas, such as the editor's bar icons, on both Metal and Vulkan and without any log.
  • Emoji are separate runs, copied at 32 px. Apple Color Emoji is about a quarter wider for its size below ~20 px. Emoji go into the separate color atlas, which never affects other text.
  • Shadows of scaled text are cast from silhouettes, the existing outline path. A text shadow's radius-to-sigma conversion is not linear, so a shrunk shadow would come out softer.
  • A gradient is spread again per run, in that run's own coordinates.
  • Some text keeps drawing from its own font: justified text, a line limit, placeholders and mask filters.

Not covered.

  • The inline text field still draws at its real size while a huge layer is being edited.
  • Emoji layers are unchanged.
  • The underlying engine behavior remains: path-drawn glyphs still enter the atlas, and a failed atlas update is not rolled back.

Testing.

  • Devices: tested on a real Samsung Galaxy S26 (Impeller Vulkan) and the iOS simulator (Impeller Metal), in a test app that drives ProImageEditor. Each case was run with 14.19.0 and with this branch.
Case 14.19.0 This branch
Long outlined text scaled to 40× (S26) 86 atlas failures 0
Same, then new text on the next page (simulator) garbled clean
192 pt outlined text, canvas zoomed 5× (simulator) 51 atlas failures, bar icons and labels gone 0, bars intact
Pinch-scaling outlined text 5× and 9× (S26, simulator) clean clean
  • Pixel comparison, 14.19.0 against this branch: the canvas is identical, with at most 0.011 % of pixels differing by more than 32 levels. The cases cover scale 4–12, zoom up to 5×, outline, shadow, background and emoji, on both devices.
  • New tests:
    • scaled_text_lines_test compares the ink of the copies with the real TextPainter across alignments, wrapping, line breaks, mixed sizes, strokes and gradients. It catches the reported-baseline mistake. It also covers emoji segmentation and canDraw.
    • rounded_background_text_large_text_test covers the wiring: the threshold, box shrinking, silhouette shadows and the justified fallback.
  • flutter analyze is clean, and all 1265 tests pass.

Related Issue: Closes #none

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)
  • 🛠️ Bug fix (non-breaking change which fixes an issue)
  • ❌ Breaking change (fix or feature that would cause existing functionality to change)
  • 🧹 Code refactor
  • ✅ Build configuration change
  • 📝 Documentation
  • 🗑️ Chore

Impeller rasterizes every glyph into one atlas shared by all text, also
the glyphs it then draws as paths, and a text layer's scale is applied
as its font size, so a layer scaled up very large needed glyphs
thousands of pixels big. Once growing the atlas failed, every glyph
added later was drawn from the wrong place, garbling text across the
whole app until it restarted.

Text above 64 pixels now keeps its layout at its real size and draws
only its glyphs from copies set in a 5.25 pixel font, scaled up onto
the place each run has in the real layout. Emoji runs are copied at 32
pixels, where color emoji keep their proportions.
@hm21 hm21 self-assigned this Oct 26, 2026
Each copied run was painted at the negative of its anchor while its
gradient was spread in the run's own coordinates. A shader does not move
with the offset a paragraph is painted at, so the gradient of large text
slid by the copy's baseline: a vertical gradient came out almost
inverted, a diagonal one shifted. The run is now painted at its origin
with the anchor folded into the canvas transform, and the gradient test
also runs down and across the lines.

Also shortens the changelog entry.
@hm21
hm21 marked this pull request as ready for review October 26, 2026 14:29
@hm21
hm21 merged commit e5ce9ca into stable Oct 26, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant