Skip to content

[Windows][Impeller] Opt-in switch to disable subpixel glyph positioning for small CJK text (GDI-classic rendering mode) #196024

Open

Description

@SuperAiPm

Splitting this out of #194931 as requested by @gaaclarke in #194931 (comment). The framework-side default-locale fix is tracked by PR #195997; this issue is the second item: an opt-in rendering-mode switch for small CJK text on Windows.

Problem

At 96 DPI / 100% scaling (still ~52% of desktops per the Steam Hardware Survey, Jan 2026), 14px CJK text rendered by Flutter/Impeller reads visibly softer than GDI/DirectWrite output in the same apps. We ship an enterprise IM client where users read dense 14px CJK for hours; our design team cannot sign off on text weights at this size, and every side-by-side against native Windows apps reads as lower quality.

What we measured (grayscale on both sides, so chroma is out of the picture)

Same face (Microsoft YaHei), same 14px, ink-pixel statistics over one full line — from my measurements in #194931:

advance (px) ink mass inked px solid (L<64) fringe (L≥64)
GDI / DirectWrite, ANTIALIASED_QUALITY 196 743 1020 59.1% 40.9%
GDI / DirectWrite, ClearType converted to gray (luma) 196 858 1310 43.7% 56.3%
Flutter 3.47 / Impeller, YaHei 200 811 1276 41.4% 58.6%

Flutter's grayscale output has almost exactly the coverage profile of ClearType converted to gray (41.4% vs 43.7% solid) and is far from GDI's real grayscale path (59.1%). A 1px CJK stem becomes one near-solid column under GDI's grid-fitting; Flutter spreads the same stem over two gray columns.

Root cause per @Rusino (#194931 (comment))

  • This is not missing hinting: Impeller already requests SkFontHinting::kSlight, and Skia's DirectWrite scaler keeps grid fitting on for every level except kNone.
  • The difference from GDI comes from:
    1. the rendering mode Skia picks — NATURAL / NATURAL_SYMMETRIC for fonts with a gasp table, while GDI uses classic mode; and
    2. setSubpixel(true) in the Impeller glyph atlas, which places glyphs at fractional x positions and spreads 1px stems over two columns.
  • Rusino's suggested first experiment: "An opt-in Windows switch that turns off subpixel positioning for small CJK text (and possibly asks for a GDI-classic rendering mode)."

The ask

An opt-in switch on the Windows embedder / engine, grayscale only, default off, e.g.:

  • disable subpixel (fractional-x) glyph positioning for small text, and/or
  • request a GDI-classic rendering mode from the DirectWrite scaler.

Zero change to defaults, so no risk to existing apps. Even vertical stem snapping alone would close most of the remaining gap for low-DPI CJK.

Scope notes (deliberately out)

Environment

Windows 10 22H2 (10.0.19045), Intel HD Graphics 630, 1920x1080 @ 100% (FontSmoothing=2, ClearType on), Flutter 3.47.1 stable, engine 11d79658c444, Impeller OpenGLESSDF.

Happy to supply captures, the pixel-diff harness (~150-line Flutter app + ~60-line diff script), and validation on any branch.

Activity

  1. added
    in triagePresently being triaged by the triage team
    engineflutter/engine related. See also e: labels.
    e: impellerImpeller rendering backend issues and features requests
    c: new featureNothing broken; request for a new capability
    c: proposalA detailed proposal for a change to Flutter
    and removed
    in triagePresently being triaged by the triage team
    on Nov 5, 2026
  2. puneetkukreja98 commented on Nov 5, 2026

    @puneetkukreja98
    Contributor

    Routing this to team-engine for further investigation.

    As previously discussed on that #194931 (comment).

  3. SuperAiPm commented on Nov 10, 2026

    @SuperAiPm
    Author

    Friendly ping @gaaclarke — following up on the issue you asked me to file
    (#194931 (comment)).

    It was routed to team-engine on Nov 5 but hasn't been picked up since.
    Where does this stand relative to #195997? If an opt-in hinted-text switch
    is something team-engine wants to evaluate, I'm glad to help with
    measurements or local testing — my measurements so far are in the OP.

  4. added
    P2Important issues not at the top of the work list
    on Nov 10, 2026
  5. SuperAiPm commented on Nov 13, 2026

    @SuperAiPm
    Author

    Hi @gaaclarke — now that the locale-fallback fix (#195997, ec93269) has
    landed, the font-selection half of this is resolved on master. Thanks for
    that. The remaining ask here is the separate hinting/positioning switch,
    which you asked me to file as its own issue. Is the next step a design doc
    from your side, or would you prefer I prototype it behind a flag first?
    Happy to do either — just want to avoid sitting here idle.

  6. gaaclarke commented on Nov 13, 2026

    @gaaclarke
    Member

    hey @SuperAiPm this issue doesn't live in a vacuum it's prioritized against all the other work we have to do. Switches while easy to implement often have a higher maintenance cost. If you want to post a PR we can take a look but know that just because it works technically there are other variables to consider when approving it or not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listc: new featureNothing broken; request for a new capabilityc: proposalA detailed proposal for a change to Fluttere: impellerImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.team-engineOwned by Engine team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions