Skip to content

Fix inconsistent macOS text stroke weights by supporting light/dark glyph separation in glyph atlas#186074

Merged
auto-submit[bot] merged 7 commits into
flutter:masterfrom
b-luk:lightglyphs
May 7, 2026
Merged

Fix inconsistent macOS text stroke weights by supporting light/dark glyph separation in glyph atlas#186074
auto-submit[bot] merged 7 commits into
flutter:masterfrom
b-luk:lightglyphs

Conversation

@b-luk

@b-luk b-luk commented May 5, 2026

Copy link
Copy Markdown
Contributor

This PR addresses #185790 where text rendered using the Impeller backend on macOS exhibits inconsistent stroke weights, particularly with light text on dark backgrounds.

Cause

Apple's CoreText applies different font smoothing and antialiasing depending on whether text is light or dark. Visually, dark text on a light background looks heavier than light text on a dark background. CoreText tries to account for this and make light/dark text at the same size have a similar visual weight by automatically dilating light text and eroding dark text.

Prior to this PR, our (non-color) glyph atlases are based on drawing a black glyph using Skia, which uses CoreText as its underlying text renderer on macOS. So our glyphs all are eroded by CoreText, based on the incorrect assumption that they are always displayed as dark text. As a result, our light glyphs end up wispy and inconsistently stroked. Light text already appears optically lighter weight, and then CoreText erodes them even further based on incorrectly rendering them with dark-text weighting.

Fix

  • On macOS, update GlyphProperties to calculate whether a glyph is "light" based on the luminance of its color. This is stored in a new is_light property. This ensures that the same character rendered with a light color and a dark color will result in two separate entries in the glyph atlas.
  • Render light glyphs as white during atlas generation to trigger the correct CoreText smoothing for light glyphs.
    • Dark glyphs retain the existing behavior and are rendered by having Skia render a black glyph on an A8 bitmap.
    • Light glyphs must be rendered as white on a full-color bitmap. This is because when Skia renders to an A8 bitmap (like for the dark glyph case), it is hard coded to render the glyph as black on white (from here). So for light glyphs, render them into an RGBA bitmap and then convert it to an A8 bitmap to be copied into the glyph atlas.

Additionally, this PR refactors the usage of std::optional<GlyphProperties> to just be GlyphProperties. A nullopt value was treated exactly the same as a default GlyphProperties: color is black and stroke is null. So the optionality didn't add anything. It required a lot of unnecessary and confusing branching that made the code harder to follow, especially with this added is_light property.

Fixes #185790

This PR makes this change only for macOS. We can also consider whether to do this on iOS. iOS also uses Apple's CoreText, so the same erosion issues with dark text apply on iOS. However, all modern iOS devices are hi-dpi displays which use UI scaling. This mitigates most of the visual inconsistencies that we see on macOS when running apps on a non-UI-upscaled display. We would have to weigh whether the negative performance impact of this change is worth it for iOS. Whether or not to expand this to iOS is out of scope for this PR.

Newly added AiksTest.CanRenderTextFrameWithThinLightAndDarkColors golden test

This shows the golden screenshot before this change (top half) and after this change (bottom half). Light text is now heavier weight and more consistently stroked. Dark text is unchanged.
image

Flutter "New Gallery" app screenshots

All of these screenshots compare:

  • Top row: Dark mode
  • Bottom row: Light mode
  • Left: Before this PR, rendering with Impeller
  • Middle: After this PR, rendering with Impeller
  • Right: Rendering with Skia. It is a not a goal to replicate Skia's rendering exactly. But Skia's rendering is provided here for comparison.

Home screen

  • In both dark and light mode, light text in the "Reply" card is now thicker.
  • In dark mode, light text like in the "Shrine" card and in the category titles (Material, Cupertino, Styles & Other) are thicker. In light mode, this text is dark and is unaffected.
image

"About Flutter Gallery" modal

  • In dark mode, the text is now thicker and more consistently stroked.
  • Light mode is unaffected.
image

Settings screen, normal text scaling

  • In dark mode, the text is now thicker and more consistently stroked.
  • Light mode is unaffected.
image

Settings screen, small text scaling

  • In dark mode, wispy-ness of small text is even more apparent in the old version.
  • Light mode is unaffected.
image

Settings screen, large text scaling

  • In dark mode, large text is very readable even in the old version. In the new version it is noticeably thicker, but whether it looks better is more subjective than in the examples with smaller and thinner text.
  • Light mode is unaffected.
image

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label May 5, 2026
@github-actions github-actions Bot added a: text input Entering text in a text field or keyboard related problems engine flutter/engine related. See also e: labels. e: impeller Impeller rendering backend issues and features requests labels May 5, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements support for light and dark glyph variations in the Impeller text renderer by adding an is_light property to GlyphProperties. This property is determined by color luminance and is now part of the glyph atlas cache key. The implementation includes a manual RGBA to A8 conversion in the Skia backend and refactors several interfaces to treat GlyphProperties as a required rather than optional parameter. Review feedback recommends using SkBitmap::extractAlpha for more robust alpha channel extraction, expanding the macOS-specific logic to all Apple platforms, and addressing the accuracy of the linear luminance calculation.

Comment thread engine/src/flutter/impeller/typographer/backends/skia/typographer_context_skia.cc Outdated
Comment thread engine/src/flutter/impeller/typographer/font_glyph_pair.h Outdated
Comment thread engine/src/flutter/impeller/typographer/font_glyph_pair.h Outdated
@github-actions github-actions Bot removed the CICD Run CI/CD label May 5, 2026
@b-luk

b-luk commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the Impeller text rendering pipeline to distinguish between light and dark text glyphs, specifically for macOS compatibility. It replaces optional glyph properties with a mandatory struct and introduces an is_light flag based on luminance. The Skia backend now supports RGBA8888 alpha atlases when light glyphs are present. Review feedback suggests ensuring the is_light property is set for all non-color frames, including stroked text, to prevent rendering issues on macOS.

Comment thread engine/src/flutter/impeller/display_list/dl_dispatcher.cc
Comment thread engine/src/flutter/impeller/entity/contents/text_contents.cc

@gaaclarke gaaclarke left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work. Testing looks good, approach looks good. I have some feedback and questions about the layout of the code.

Comment on lines +69 to +74
@@ -70,6 +70,8 @@ void TextContents::SetTextProperties(
// Alpha is always applied when rendering, remove it here so
// we do not double-apply the alpha.
properties_.color = color.WithAlpha(1.0);
} else {
properties_.SetIsLight(color);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of weird that this one concept is stored across 2 fields. Would it make more sense to store color as a std::variant<DlColor, Tone>, so it's either a color or light/dark?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also like the idea of using a std::variant to indicate that the color is not used if is_light mode is selected.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. This was a pretty involved change, so PTAL

Comment thread engine/src/flutter/impeller/typographer/backends/skia/typographer_context_skia.cc Outdated
Comment thread engine/src/flutter/impeller/typographer/backends/skia/typographer_context_skia.cc Outdated
Comment thread engine/src/flutter/impeller/typographer/backends/skia/typographer_context_skia.cc Outdated
Comment thread engine/src/flutter/impeller/typographer/backends/skia/typographer_context_skia.cc Outdated
@flutter-dashboard

Copy link
Copy Markdown

Golden file changes have been found for this pull request. Click here to view and triage (e.g. because this is an intentional change).

If you are still iterating on this change and are not ready to resolve the images on the Flutter Gold dashboard, consider marking this PR as a draft pull request above. You will still be able to view image results on the dashboard, commenting will be silenced, and the check will not try to resolve itself until marked ready for review.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Changes reported for pull request #186074 at sha 0351205

@flutter-dashboard flutter-dashboard Bot added the will affect goldens Changes to golden files label May 5, 2026
@b-luk

b-luk commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

Two problems I noticed from the Google testing and golden results:

  1. The check to apply this only for macos is incorrect. FML_OS_MACOSX is defined and set for both macos and iOS. I'll need to use #if FML_OS_MACOSX && !FML_OS_IOS to only apply this to iOS.

  2. There's something funky going on when light text is scaled. I'm looking into this.

@github-actions github-actions Bot removed the CICD Run CI/CD label May 5, 2026
@b-luk

b-luk commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

Two problems I noticed from the Google testing and golden results:

  1. The check to apply this only for macos is incorrect. FML_OS_MACOSX is defined and set for both macos and iOS. I'll need to use #if FML_OS_MACOSX && !FML_OS_IOS to only apply this to iOS.
  2. There's something funky going on when light text is scaled. I'm looking into this.
  1. I updated the macOS check.
  2. It turns out that I can't use extractAlpha (which was based on an earlier Gemini suggestion). SkBitmap::extractAlpha does not guarantee that the pixels are contiguous, and in many cases will end up with padding at the end of each row. I changed this to SkBitmap::readPixels instead, which lets us allocate its memory beforehand to not have row padding. The logic is more than 1 line now, so I put it in a helper function.

@jason-simmons jason-simmons left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM

Comment on lines +69 to +74
@@ -70,6 +70,8 @@ void TextContents::SetTextProperties(
// Alpha is always applied when rendering, remove it here so
// we do not double-apply the alpha.
properties_.color = color.WithAlpha(1.0);
} else {
properties_.SetIsLight(color);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also like the idea of using a std::variant to indicate that the color is not used if is_light mode is selected.

Comment thread engine/src/flutter/impeller/typographer/backends/skia/typographer_context_skia.cc Outdated
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label May 6, 2026
@b-luk b-luk requested review from gaaclarke and jason-simmons May 6, 2026 00:28
@flutter-dashboard

Copy link
Copy Markdown

Golden file changes are available for triage from new commit, Click here to view.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Changes reported for pull request #186074 at sha f54a853

jason-simmons
jason-simmons previously approved these changes May 6, 2026
@b-luk b-luk added the autosubmit Merge PR when tree becomes green via auto submit App label May 6, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label May 6, 2026
@auto-submit

auto-submit Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/186074, because - The status or check suite Google testing has failed. Please fix the issues identified (or deflake) before re-applying this label.

@b-luk b-luk added the autosubmit Merge PR when tree becomes green via auto submit App label May 7, 2026
@auto-submit auto-submit Bot added this pull request to the merge queue May 7, 2026
Merged via the queue into flutter:master with commit 3da669f May 7, 2026
207 of 208 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label May 7, 2026
@b-luk b-luk deleted the lightglyphs branch May 7, 2026 18:58
abdulawalarif pushed a commit to abdulawalarif/flutter that referenced this pull request May 12, 2026
…ter#186262)

Fix `EmbedderTest.CanRenderTextWithImpellerMetal` test breakage.

This broke due to flutter#186074. That
PR modified how light text is rendered on macOS, but did not update this
test's golden image.

This updates the image.

It also changes the test to explicitly render dark text on a light
background and light text on a dark background. Previously it rendered
white text on a transparent background.

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

If this change needs to override an active code freeze, provide a
comment explaining why. The code freeze workflow can be overridden by
code reviewers. See pinned issues for any active code freezes with
guidance.

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Jason Simmons <jsimmons@google.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: text input Entering text in a text field or keyboard related problems CICD Run CI/CD e: impeller Impeller rendering backend issues and features requests engine flutter/engine related. See also e: labels. will affect goldens Changes to golden files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MacOS text on Impeller has inconsistent looking stroke weights

3 participants