Skip to content

render: no way to pass Chrome a launch flag; on Windows text renders with ClearType fringes without --disable-lcd-text #7661

Open

Description

@jasonc00person

Problem

There is no way to hand the render's Chrome an extra launch flag. buildChromeArgs in packages/engine/src/services/browserManager.ts assembles the whole list, and nothing a project or a user sets (CLI flag, environment variable, hyperframes.json) reaches it. Only the binary can be swapped (HYPERFRAMES_BROWSER_PATH).

The case that needed one: on Windows, small text on an opaque layer renders with ClearType subpixel antialiasing, so every stem carries a blue edge on one side and a red-brown edge on the other. Video frames are scaled, composited and watched on other screens, so display-specific subpixel AA is never wanted in them.

Measured on Windows 11 (ClearType on, 125 % scaling, Chrome 152.0.7977.30), on a 22 px mono label on a solid card, as a red-versus-blue edge split on the glyphs (0 = neutral):

build plain with --disable-lcd-text
0.8.105 0.004
0.9.40 0.500 0.003

In our runs it starts at 0.8.112. A solid card that is not on its own compositing layer already read 0.406 on 0.8.105, so the older builds were not immune either. Text over transparency, and anything under will-change: transform, is unaffected. macOS is unaffected. It is not specific to this flag set: a plain puppeteer launch on the same machine fringes the body text of a captured web page the same way (0.42 to 0.48, and 0.000 to 0.002 with the flag, with no glyph or line moving), so hyperframes capture is probably affected too, though we did not measure it.

Proposed solution

Two parts, either one would have solved it:

  1. Add --disable-lcd-text to buildChromeArgs. The engine's own motion blur fixture already launches with it (packages/engine/src/services/__fixtures__/motionBlur/blurPage.ts). With the flag on, the only other change we saw is that rotated text rasterises slightly crisper.
  2. An escape hatch for flags in general, appended at the end of buildChromeArgs and counted in the browser launch fingerprint, for example:
HYPERFRAMES_CHROME_ARGS="--disable-lcd-text --some-other-flag" npx hyperframes render ...
npx hyperframes render --chrome-arg=--disable-lcd-text ...

or a render.chromeArgs array in hyperframes.json. It should reach render, snapshot, check and capture alike, since they should draw the same pixels.

Alternatives considered

  • CSS on the composition. will-change: transform on the root moves text off the LCD path but resamples embedded captures; opacity: 0.999 on the root shifts moving overlays by a sub-pixel. Neither is a safe global fix.
  • No existing render flag changes it.
  • What we run today: a Node preload (NODE_OPTIONS=--require ./lcd-off.cjs) that wraps child_process.spawn and friends and appends the flag when the spawned program is a Chrome. It works on macOS and on Windows (Git Bash, PowerShell, cmd), but it patches a built-in to reach a launch call the CLI owns, and every script that renders has to remember to load it.

Additional context

Checked against 0.9.42: buildChromeArgs has no user-supplied arguments and no --disable-lcd-text. Happy to test a build on the Windows machine that measured this.

Activity

  1. added
    enhancementNew feature or request
    triage/needs-designEvidence is adequate; desired behavior or scope needs a maintainer decision.
    on Nov 14, 2026
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

    enhancementNew feature or requesttriage/needs-designEvidence is adequate; desired behavior or scope needs a maintainer decision.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions