Fix inconsistent macOS text stroke weights by supporting light/dark glyph separation in glyph atlas#186074
Conversation
There was a problem hiding this comment.
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.
|
/gemini review |
There was a problem hiding this comment.
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.
gaaclarke
left a comment
There was a problem hiding this comment.
Awesome work. Testing looks good, approach looks good. I have some feedback and questions about the layout of the code.
| @@ -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); | |||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
I also like the idea of using a std::variant to indicate that the color is not used if is_light mode is selected.
There was a problem hiding this comment.
Done. This was a pretty involved change, so PTAL
|
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 Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
|
Two problems I noticed from the Google testing and golden results:
|
|
| @@ -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); | |||
There was a problem hiding this comment.
I also like the idea of using a std::variant to indicate that the color is not used if is_light mode is selected.
|
Golden file changes are available for triage from new commit, Click here to view. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
|
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. |
…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>
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
GlyphPropertiesto calculate whether a glyph is "light" based on the luminance of its color. This is stored in a newis_lightproperty. 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.Additionally, this PR refactors the usage of
std::optional<GlyphProperties>to just beGlyphProperties. A nullopt value was treated exactly the same as a defaultGlyphProperties: 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 addedis_lightproperty.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.CanRenderTextFrameWithThinLightAndDarkColorsgolden testThis 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.

Flutter "New Gallery" app screenshots
All of these screenshots compare:
Home screen
"About Flutter Gallery" modal
Settings screen, normal text scaling
Settings screen, small text scaling
Settings screen, large text scaling
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-assistbot 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.