fix(text): draw Fabric text with grayscale antialiasing on transparent composition surfaces#16341
Open
FaithfulAudio wants to merge 1 commit into
Open
Conversation
…t surfaces RenderText calls ID2D1RenderTarget::DrawTextLayout without setting a text antialias mode, so the draw inherits the device context default - ClearType subpixel antialiasing on a normal machine. The target it draws onto is transparent: ParagraphComponentView creates the drawing surface with DirectXAlphaMode::Premultiplied and clears it to ColorF(Black, 0.0f) when the view has no backgroundColor. ClearType computes independent R/G/B coverage and needs an opaque, known background to filter against. With none, that per-channel coverage lands in the surface colour channels and shows as dark or colour-tinted fringes along thin glyph stems once composited. D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE is the documented mode for transparent render targets. Saves the current mode, sets GRAYSCALE for the draw, and restores it - mirroring the GetAntialiasMode/SetAntialiasMode and GetDpi/SetDpi save/restore idioms already used in this function. Neither SetTextAntialiasMode nor D2D1_TEXT_ANTIALIAS_MODE appeared anywhere in the tree before this change. Addresses microsoft#16340.
Contributor
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
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.
Draw Fabric text with grayscale antialiasing instead of inheriting ClearType
Problem
Text drawn by the new-architecture (Fabric / Composition) text stack shows dark or colour-tinted
fringes along thin glyph stems — most visible on light backgrounds, on hairline strokes, on icon
fonts, and at fractional DPI. The text looks slightly "dirty" or double-struck rather than cleanly
antialiased.
Root cause
RenderTextinvnext/Microsoft.ReactNative/Fabric/Composition/TextDrawing.cppnever sets a textantialias mode before
ID2D1RenderTarget::DrawTextLayout, so the draw runs with whatever mode thedevice context happens to carry — in practice the system default, which is ClearType when the user
has ClearType enabled (the normal Windows setting).
ClearType is subpixel filtering: it computes separate R, G and B coverage and needs an opaque,
known background to filter against. This draw has no such background:
ParagraphComponentView::updateVisualBrushcreates the target surface asCreateDrawingSurfaceBrush(surfaceSize, DirectXPixelFormat::B8G8R8A8UIntNormalized, DirectXAlphaMode::Premultiplied)(
ParagraphComponentView.cpp, ~L367-370) — premultiplied alpha.ParagraphComponentView::DrawTextclears that surface toD2D1::ColorF(D2D1::ColorF::Black, 0.0f)— fully transparent black — whenever the view has no
backgroundColor(ParagraphComponentView.cpp, ~L521-523).over content this device context cannot sample.
Per-channel coverage computed against "nothing" survives into the surface's colour channels and
reads as coloured/dark fringing once composited.
D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALEis the modeintended for transparent render targets.
Scope note from a repo-wide search at
c69cf55f67f9b03f467502dac1007ac2d9ebe209: neitherSetTextAntialiasModenorD2D1_TEXT_ANTIALIAS_MODEoccurs anywhere in the tree, so no part ofRNW currently makes this choice explicitly.
Fix
Save the current text antialias mode, force
D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALEfor theDrawTextLayoutcall, and restore it afterwards.GetTextAntialiasMode/SetTextAntialiasModeare
ID2D1RenderTargetmembers (D2D 1.0), and the save/force/restore shape deliberately mirrorsthe two idioms already used a few lines above in the same function:
GetAntialiasMode/SetAntialiasMode/ restore around the selection-highlight rectangles (L142-160) andGetDpi/SetDpi/ restore around the whole body (L31-32, L175). Twelve added lines, one file,no signature or header changes.
Validation
Honest limits — nothing here was compiled or run:
yarn lint,yarn format:verify, clang-format and typecheck were not run.raw.githubusercontent.combytes ofTextDrawing.cppatc69cf55f67f9b03f467502dac1007ac2d9ebe209and verified only mechanically:git apply --checkagainst a pristine scratch copy (core.autocrlf false): exit 0.git apply: exit 0, producing a file with 0 CRLF sequences (source is LF-only:178 LF before, 190 after).
core.autocrlf true(what a normalcheckout produces, since
.gitattributesdeclares*.cpp text eol=crlf):git apply --checkexit 0 there too.is a hand count, not a clang-format run.
Reviewer-facing caveats, stated plainly because they are judgement calls rather than measurements:
<Text>with an opaquebackgroundColorcurrently gets ClearType and will now get grayscale — very slightly softer.I chose unconditional because the surface is premultiplied and composited by the visual tree
regardless of the clear colour, so ClearType is not reliably valid even in that case, and
because making it conditional would mean threading the view's background/opacity into
RenderText, which is called from four places (ParagraphComponentView,DebuggerUIIsland,TooltipService,ReactNativeIsland). If maintainers prefer the conditional version, say soand I will rework it.
grayscale on some premultiplied targets; if it does on the surfaces RNW uses, this change is a
no-op for pixels and its value is only determinism (no dependence on the user's font-smoothing
setting). I could not confirm either way without running the app, so I am not claiming a
measured pixel improvement.
ScrollViewComponentView.cpp(~L529, scrollbar arrow glyphs) andTextInput/WindowsTextInputComponentView.cpp(~L1870, the placeholder text — the editable textitself goes through RichEdit, not
DrawTextLayout). Kept out to keep this diff to onereviewable file; happy to extend it in this PR or a follow-up, whichever maintainers prefer.
Change file
change/react-native-windows-fix-text-cleartype-fringing.json,"type": "prerelease"(correct formain, whosevnext/package.jsonversion is0.0.0-canary.1057; the repo's beachball transformdowngrades
prereleasetopatchautomatically on released branches).Microsoft Reviewers: Open in CodeFlow