Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

renderer/metal: blend rendered text in linear RGB space (aka. Gamma Correction) #4686

Conversation

aljoscha
Copy link

@aljoscha aljoscha commented Jan 6, 2025

Touches #2125

Before, we were blending rendered text onto the background in sRGB
space, which is linear in perceived color/brightness but not linear in
physical terms.

Correct alpha blending requires decoding from sRGB space into linear RGB
space, performing the blending, and then encoding back to sRGB space for
presenting. See here for a very good article on the whole topic of gamma
and how it relates to sRGB and text rendering:
https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#antialiasing

In this demonstation, I do the decoding "by hand" in the shader, and
rely on Metal do re-encode as sRGB, by setting the right pixelFormat on
the colorAttachment.

One thing to note is that correct alpha blending in linear space makes
the rendered fonts look even thinner than they already are. When Kitty
introduced alpha blending in linear space, they introduced a
configuration that allows adjusting the gamma/contrast, which will
thicken up the fonts again. Fonts being thinner when doing correct
blending is also something that the article linked above explains, and
you can confirm this behavior for youself by running through an example
alpha blending calculation by hand, on pen and paper, say.

Just letting you know, I'm not at all wedded to this PR. 🕊️ I got interested in the topic because font rendering on Ghostty looked somewhat "flimsy" to me, so I dug down into the rabbit hole of sRGB, gamma correction, etc. If you have other plans or someone is already working on this I can close the PR. If you're keen to help me shepherd this in, though, I'd be very happy to respond to any comments/suggestions!

TODO/Notes before merging:

  • Add config that allows thickening fonts
  • Check performance, maybe introduce a LUT for doing the
    conversions/decode. Which benchmarks should I be running?
  • Follow up: I've only looked at Metal, but the Linux/OpenGL renderer
    might have the same issues. I could work on that one next.

Screenshots

You might have to zoom in/pixel peep on these.

This shows how fonts look even thinner now, as the above article also describes:
Screenshot 2025-01-06 at 14 49 48

We can see there's no more dark color artifacts around the edged:
Screenshot 2025-01-06 at 14 49 10

And here I demonstrate how we could thicken the fonts again by adding adjustable "gain" (name totally TBD) to the rendered font's alpha:
Screenshot 2025-01-06 at 14 54 22

Here's a yet more thorough comparison. We have browsers up top (Firefox, Safari, Chrome), of which I'd say Firefox does not blend in linear space but Safari and Chrome do. And I'd say Safari looks best to my eyes. In the bottom row we have Ghostty main and Terminal, which blend in sRGB space (without gamma correction), and then Kitty and this branch on the bottom right which do blend in linear space (with gamma correction). Here it also really helps to click through to the screenshot and zoom in. But the changes are most obvious when going in there with Digital Colour Meter.
Screenshot 2025-01-07 at 13 52 19

DO NOT MERGE, right now this is a draft for demonstration.

Before, we were blending rendered text onto the background in sRGB
space, which is linear in perceived color/brightness but _not_ linear in
physical terms.

Correct alpha blending requires decoding from sRGB space into linear RGB
space, performing the blending, and then encoding back to sRGB space for
presenting. See here for a very good article on the whole topic of gamma
and how it relates to sRGB and text rendering:
https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#antialiasing

In this demonstation, I do the decoding "by hand" in the shader, and
rely on Metal do re-encode as sRGB, by setting the right pixelFormat on
the colorAttachment.

One thing to note is that correct alpha blending in linear space makes
the rendered fonts look even thinner than they already are. When Kitty
introduced alpha blending in linear space, they introduced a
configuration that allows adjusting the gamma/contrast, which will
thicken up the fonts again. Fonts being thinner when doing correct
blending is also something that the article linked above explains, and
you can confirm this behavior for youself by running through an example
alpha blending calculation by hand, on pen and paper, say.

TODO/Notes before merging:

 - Add config that allows thickening fonts
 - Check performance, maybe introduce a LUT for doing the
   conversions/decode
 - Follow up: I've only looked at Metal, but the Linux/OpenGL renderer
   probably has the same issues. I could work on that one next.
@aljoscha aljoscha mentioned this pull request Jan 6, 2025
@aljoscha aljoscha changed the title renderer/metal: blend rendered cells/text in linear RGB space renderer/metal: blend rendered text in linear RGB space (aka. Gamma Correction) Jan 6, 2025
@aljoscha aljoscha marked this pull request as ready for review January 7, 2025 12:59
@qwerasd205
Copy link
Collaborator

There's nothing wrong with this implementation, however it isn't "correct" for macOS apps. After thorough investigation and a very helpful hint from @lunacookies, I've determined that these days what Apple seemingly does for their native apps (e.g. Terminal.app, TextEdit), is that they simply ignore gamma correction, but they avoid having particularly glaring fringing thanks to the fact that they render in the display's color space, which is typically a wide gamut color space more or less equal to Display P3. By rendering in a wide gamut color space, fully saturated sRGB colors are no longer at the very edges of the space, so blending issues are reduced quite a bit with no special extra handling required.

In experimenting with trying to exactly replicate Terminal.app's rendering (which I have successfully achieved now), I've already got a branch locally which has most of the code required for doing things that way, so I'll probably clean it up a bit and PR it some time tomorrow. I intend to have things implemented in a way that can be generalized to the OpenGL renderer and Linux, and which will allow the user to configure their desired alpha blending, whether they want "native" style rendering or proper gamma correction (or possibly a special third option I cooked up, which might give the best of both worlds).

Sorry to take this from you 😅 -- this PR is definitely on the right track if just doing proper gamma corrected blending was what was needed, but it's not exactly, and I already have most of the details of what is necessary worked out on my local branch.

@aljoscha
Copy link
Author

aljoscha commented Jan 8, 2025

Sorry to take this from you 😅 -- this PR is definitely on the right track if just doing proper gamma corrected blending was what was needed, but it's not exactly, and I already have most of the details of what is necessary worked out on my local branch.

No problem at all! ☺️ I'm glad you're working on it, you seem to have a good handle on things in general! Looking forward to your fix.

Closing now...

@aljoscha aljoscha closed this Jan 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants