-
Notifications
You must be signed in to change notification settings - Fork 8.5k
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
Use Oklab for text and cursor contrast adjustments #15283
Conversation
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Here's a (temporary) preview of the interactive |
@@ -0,0 +1,235 @@ | |||
<!DOCTYPE html> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -64,7 +64,7 @@ bool TextColor::CanBeBrightened() const noexcept | |||
|
|||
bool TextColor::IsLegacy() const noexcept | |||
{ | |||
return IsIndex16() || (IsIndex256() && _index < 16); | |||
return (IsIndex16() || IsIndex256()) && _index < 16; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This allows for better compiler optimizations, because Index16 and Index256 are two neighboring values in the enum. The compiler optimizes the || away entirely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: This is the sort of scenario where I'd be inclined to add a static_assert
checking that those two enums are in fact neighboring values, and a comment explaining why that matters. That way if someone ever messes with that enum in the future, they'll be notified that they'll be losing this optimization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is a enum class, there are no implicit operators to turn them into integers. I don't really want to write the rather verbose code required to do static_assert
checks, so I've instead opted to add a comment to the declaration instead, which points this out.
@@ -82,6 +82,11 @@ bool TextColor::IsDefault() const noexcept | |||
return _meta == ColorType::IsDefault; | |||
} | |||
|
|||
bool TextColor::IsDefaultOrIndex16() const noexcept | |||
{ | |||
return _meta != ColorType::IsRgb && _index < 16; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ColorType::IsDefault
has a _index
of 0 at all times as mandated by the constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be named IsDefaultOrLegacy
? It's not only matching IsIndex16
- it's also matching IsIndex256
with an index < 16 - which is exactly what IsLegacy
covers.
IsDefault, | ||
IsIndex16, | ||
IsIndex256, | ||
IsRgb |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The more consistent ordering from "less available colors" to "more" allows for better compiler optimizations, as mentioned above, and also IMO make more sense subjectively speaking.
if (cursorColor == 0xffffffff) | ||
{ | ||
background = bg ^ 0xffffff; | ||
foreground = 0xffffffff; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the first inverted cursor handling block.
|
||
// The legacy console used to invert colors by just doing `bg ^ 0xc0c0c0`. This resulted | ||
// in a minimum squared distance of just 0.029195 across all possible color combinations. | ||
background = ColorFix::GetPerceivableColor(background, bg, 0.25f * 0.25f); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The minimum distance given to GetPerceivableColor
is the squared distance. That way we don't need to do any sqrt()
inside it (which is very slow relatively speaking). The background of the cursor gets half the min. distance the text gets, because I noticed that the background color really just doesn't need it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the 0.029195 number in the comment valid for Oklab?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see you added that number in another commit of this PR, so I presume it is valid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, you could use it as an argument to that function.
BTW I've just rechecked it and the average ΔEOK of the previous XOR method is 0.432123. It's just that the minimum is 0.170865 (which results in 0.029195 if squared). It might be worth it to increase the 0.25f
a bit, but that looked a little weird at times, because it resulted in a way too strong contrast. Or maybe I should combine the XOR and Oklab methods together? Argh I'm not sure what's best.
@@ -11,10 +11,6 @@ | |||
using namespace Microsoft::Console::Render; | |||
using Microsoft::Console::Utils::InitializeColorTable; | |||
|
|||
static constexpr size_t AdjustedFgIndex{ 16 }; | |||
static constexpr size_t AdjustedBgIndex{ 17 }; | |||
static constexpr size_t AdjustedBrightFgIndex{ 18 }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suppress white space changes if you haven't done so already. It'll effectively remove almost all changes in this file.
r = (r > 0.0031308f ? (1.055f * powf(r, 1.0f / 2.4f) - 0.055f) : 12.92f * r) * 255.0f; | ||
g = (g > 0.0031308f ? (1.055f * powf(g, 1.0f / 2.4f) - 0.055f) : 12.92f * g) * 255.0f; | ||
b = (b > 0.0031308f ? (1.055f * powf(b, 1.0f / 2.4f) - 0.055f) : 12.92f * b) * 255.0f; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fabian Giesen (ryg) has also written some great linear -> sRGB routines and published them here: https://gist.github.com/rygorous/2203834
But I felt like that's a bit too soon for now just like my other SIMD optimizations for this. I'll work towards that in a follow-up PR, because it requires me to write NEON intrinsics and I'm not feeling comfortable doing that without extra validation.
@@ -64,7 +64,7 @@ bool TextColor::CanBeBrightened() const noexcept | |||
|
|||
bool TextColor::IsLegacy() const noexcept | |||
{ | |||
return IsIndex16() || (IsIndex256() && _index < 16); | |||
return (IsIndex16() || IsIndex256()) && _index < 16; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: This is the sort of scenario where I'd be inclined to add a static_assert
checking that those two enums are in fact neighboring values, and a comment explaining why that matters. That way if someone ever messes with that enum in the future, they'll be notified that they'll be losing this optimization.
@@ -82,6 +82,11 @@ bool TextColor::IsDefault() const noexcept | |||
return _meta == ColorType::IsDefault; | |||
} | |||
|
|||
bool TextColor::IsDefaultOrIndex16() const noexcept | |||
{ | |||
return _meta != ColorType::IsRgb && _index < 16; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be named IsDefaultOrLegacy
? It's not only matching IsIndex16
- it's also matching IsIndex256
with an index < 16 - which is exactly what IsLegacy
covers.
src/renderer/base/RenderSettings.cpp
Outdated
if ( | ||
_renderMode.any(Mode::IndexedDistinguishableColors, Mode::AlwaysDistinguishableColors) && | ||
fg != bg && | ||
GetRenderMode(Mode::AlwaysDistinguishableColors)) | ||
(_renderMode.test(Mode::AlwaysDistinguishableColors) || (fgTextColor.IsDefaultOrIndex16() && bgTextColor.IsDefaultOrIndex16()))) | ||
{ | ||
fg = ColorFix::GetPerceivableColor(fg, bg); | ||
fg = ColorFix::GetPerceivableColor(fg, bg, 0.5f * 0.5f); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that there's a slight change in behavior here from the previous implementation (I think). When you don't have the AlwaysDistinguishableColors
mode set, it would previously not have applied to dimmed colors, but it will do now (assuming I'm reading this correctly). I'm not sure whether anyone would care either way, but I thought it worth mentioning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wow, I totally forgot to mention that in this PR!
I did it intentionally. Since GetPerceivableColor
could be considered a very important accessibility feature, dim colors need to be just as visible as non-dim colors are for people relying on this feature. Since the new GetPerceivableColor
function is fairly "accurate" at not changing the lightness more than necessary, the result still looks quite okay to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is insane but also amazingly effective
0x1.7da5a4p-1f, 0x1.8181a8p-1f, 0x1.85637cp-1f, 0x1.894b2p-1f, 0x1.8d3896p-1f, 0x1.912be2p-1f, 0x1.952504p-1f, 0x1.992402p-1f, 0x1.9d28dcp-1f, 0x1.a13396p-1f, 0x1.a5443p-1f, 0x1.a95aaep-1f, 0x1.ad7714p-1f, 0x1.b1996p-1f, 0x1.b5c19ap-1f, 0x1.b9efcp-1f, | ||
0x1.be23d8p-1f, 0x1.c25ddcp-1f, 0x1.c69ddep-1f, 0x1.cae3d2p-1f, 0x1.cf2fc6p-1f, 0x1.d381acp-1f, 0x1.d7d99ap-1f, 0x1.dc377ep-1f, 0x1.e09b7p-1f, 0x1.e5055ep-1f, 0x1.e9755cp-1f, 0x1.edeb5cp-1f, 0x1.f26772p-1f, 0x1.f6e98cp-1f, 0x1.fb71cp-1f, 0x1p+0f, | ||
}; | ||
// clang-format on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for the record, this whole table looks insane and no human can actually read that.
but it works
@msftbot make sure @PankajBhojwani signs off /rip msftbot |
void _ToLab() noexcept; | ||
void _ToRGB(); | ||
}; | ||
COLORREF GetPerceivableColor(COLORREF color, COLORREF reference, float minSquaredDistance) noexcept; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no til
:(
} | ||
|
||
// The legacy console used to invert colors by just doing `bg ^ 0xc0c0c0`. This resulted | ||
// in a minimum squared distance of just 0.029195 across all possible color combinations. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so this ensures that all cursor foregrounds on all colors are visible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should! ...in theory at least.
// It's similar to the well known "fast inverse square root" trick. Lots of numbers around 709921077 perform | ||
// at least equally well to 709921077, and it is unknown how and why 709921077 was chosen specifically. | ||
FP32 fp{ .f = a }; // evil floating point bit level hacking | ||
fp.u = fp.u / 3 + 709921077; // what the fuck? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lmao you guys
Oklab by Björn Ottosson is a color space that has less irregularities than the CIELAB color space used for ΔE2000. The distance metric for Oklab (ΔEOK) is defined by CSS4 as the simple euclidian distance. This allows us to drastically simplify the code needed to determine a color that has enough contrast. The new implementation still lacks proper gamut mapping, but that's another and less important issue. I also made it so that text with the dim attribute gets adjusted just like regular text, since this is an accessibility feature after all. The new code is so much faster than the old code (12-125x) that I dropped any caching code we had. While this increases the CPU overhead when printing lots of indexed colors, the code is way less complex now. "Increases" in this case however means something in the order of 15-60ns per color change (as measured on my CPU). It's possible to further improve the performance using explicit SIMD instructions, but I've left that as a future improvement, since that will make the code quite a bit more verbose and I didn't want to hinder the initial review. Finally, these new routines are also used for ensuring that the AtlasEngine cursors remains visible at all times. Closes #9610 * When `adjustIndistinguishableColors` is enabled colors are distinguishable ✅ * An inverted cursor on top of a `#7f7f7f` foreground & background is still visible ✅ * A colored cursor on top of a background with identical color is still visible ✅ * Cursors on a transparent background are visible ✅ (cherry picked from commit 4dd9493) Service-Card-Id: 89215984 Service-Version: 1.17
Oklab by Björn Ottosson is a color space that has less irregularities
than the CIELAB color space used for ΔE2000. The distance metric for
Oklab (ΔEOK) is defined by CSS4 as the simple euclidian distance.
This allows us to drastically simplify the code needed to determine
a color that has enough contrast. The new implementation still lacks
proper gamut mapping, but that's another and less important issue.
I also made it so that text with the dim attribute gets adjusted just
like regular text, since this is an accessibility feature after all.
The new code is so much faster than the old code (12-125x) that I
dropped any caching code we had. While this increases the CPU overhead
when printing lots of indexed colors, the code is way less complex now.
"Increases" in this case however means something in the order of 15-60ns
per color change (as measured on my CPU). It's possible to further
improve the performance using explicit SIMD instructions, but I've
left that as a future improvement, since that will make the code quite
a bit more verbose and I didn't want to hinder the initial review.
Finally, these new routines are also used for ensuring that the
AtlasEngine cursors remains visible at all times.
Closes #9610
Validation Steps Performed
adjustIndistinguishableColors
is enabledcolors are distinguishable ✅
#7f7f7f
foreground & backgroundis still visible ✅
is still visible ✅