Repository: microsoft/terminal (language: cpp)

Issue:
Make certain that the cursor can invert/not-occlude/display the character under it
> I've been using WSL for a while and it's been great, but using nvim with this issue still pending is at times unusable. Other terminals are not having this issue (see ConEmu or Hyper for example). I hope this issue gets re-opened.

_Originally posted by @n1ghtmare in https://github.com/microsoft/terminal/issues/1203#issuecomment-801461436_

> I'm with @n1ghtmare on this one. Most syntax highlighting colors in Vim have a wide array of colors that very much clash with the cursor color when the cursor color isn't able to be swapped to a contrasted color dynamically. This has been demonstrated in the few screenshots I uploaded before. It affects everyone using terminal Vim or another terminal based text editor.
>
> I know @pianocomposer321 said to pick a better color for the cursor but this doesn't make a difference in the end. If he adjusted his screenshot to be over a comment instead of the  keyword then the comment's character under the cursor would be invisible. Also in his current screenshot the contrast is very low, it would certainly not be good enough for a video recording or giving a talk.
>
> As good as the MS terminal is, I still tend to use wsltty (which dynamically sets its cursor color based on contrast calculations) in my day to day because having a block cursor in terminal Vim is much better than the vintage cursor in the MS terminal. Not just from a legibility POV, but it allows you to configure Vim so that your cursor shape changes from the block cursor to a line cursor depending on if you're in normal or insert mode.

_Originally posted by @nickjj in https://github.com/microsoft/terminal/issues/1203#issuecomment-801475389_

Subscribers, thumbs-uppers from that comment: @krage, @saurik, @krage


Discussion / hints:
Is this blocked by #6193 or other issues? Or we can just implement the algorithm for calculating the contrasted color and call it a day
I KNEW THERE WAS AN EXISTING ISSUE!

Part of the trick here is that when the `Renderer` is drawing runs of text, it doesn't know where the cursor is. So it needs to be able to split the run with the cursor in it, so it can draw {text left of the cursor, text where the cursor is, text to the right of the cursor }.

Presumably, if there's a way of doing it without doing all the work in #6193, that'd be cool. I had all these ideas before in #6151 for different ways of specifying how the text at the cursor should look. Something like `"cursorTextColor": null|"textForeground"|"#rrggbb"`.
The thing I was toying with--which initially made me think that this would be _easy_--was drawing a D2D "fill effect" (that is: a command buffer that produces a flood fill) through the image/effect drawing pipeline with the XOR composition mode. It was promising, but it only worked where we weren't already drawing a background. Everything goes pear-shaped when you _are_ drawing a background color. :smile:
However, it looks like we can use the 2-pass cursor renderer @zadjii-msft wrote to backplate the color for a destination-inversion cursor.
![it puts the cursor on its skin](https://user-images.githubusercontent.com/189190/112690422-bd065e00-8e49-11eb-9eb9-073828b45b3f.gif)

Looks like a really good start, nice work. Could this be something that gets backported into the current stable release as a hotfix once you're happy with its performance and stability? It feels like such a huge feature that it transforms the entire experience for anyone who might be using Vim.
Imo, there are 3 possible solution to this:
 * calculate contrast color from the current foreground
 * use terminal background color
 * define background and foreground color for the cursor

Calculated contrast color may not have enough contrast to the cursor's background color (even visible in the post above (purple cursor + inverted color - blue) and it may be distracting for some users. I agree it should be the default behavior but also, there should be a possibility to use a custom cursor foreground color defined by user or system background as default, something like: Cursor color = (contrast color | system background | custom cursor foreground) in profile settings.

But I see that the main issue here is the cursor position between renders so thank you for your work @DHowett!
Well, what needs to happen now is for us to come up with a design that reconciles the current `cursorColor` with the proposed `cursorTextColor`, with the now possible `invertCursor` functionality. Once we've got a sensible way of expressing these options, it needs to make it into a PR, merge it, then selfhost it internally to make sure it feels right. That'll probably take right up to the next release _anyways_, so I'm not sure releasing as a hotfix would end up saving anyone any time 😄 
For those of you who appreciate long commentary,

```
// CURSOR INVERSION
// We're trying to invert the cursor and the character underneath it without redrawing the text (as
// doing so would break up the run if it were part of a ligature). To do that, we're going to try
// to XOR the content of the screen where the cursor would have been.
//
// This renderer, however, supports transparency. In fact, in its default configuration it will not
// have a background at all (it delegates background handling to somebody else.) You can't XOR what
// isn't there.
//
// To properly invert the cursor in such a configuration, then, we have to play some tricks. Examples
// are given below for two cursor types, but this applies to all of them.
//
// First, we'll draw a "backplate" in the user's requested background color (with the alpha channel
// set to 0xFF). (firstPass == true)
//
// EMPTY BOX  FILLED BOX
// =====      =====
// =   =      =====
// =   =      =====
// =   =      =====
// =====      =====
//
// Then, outside of _drawCursor, the glyph is drawn:
//
// EMPTY BOX  FILLED BOX
// ==A==      ==A==
// =A A=      =A=A=
// AAAAA      AAAAA
// A   A      A===A
// A===A      A===A
//
// Last, we'll draw the cursor again in all white and use that as the *mask* for XORing the already-
// drawn pixels. (firstPass == false) (# = mask, a = inverted A)
//
// EMPTY BOX  FILLED BOX
// ##a##      ##a##
// #A A#      #a#a#
// aAAAa      aaaaa
// a   a      a###a
// a###a      a###a
```
Just wanted to pipe up here for a second, specifically replying to @DHowett's last message. I always considered ligatures being drawn as their individual characters when the cursor is over them as a feature, not a bug. It's nice to be able to see what characters comprise the ligature if you need to, and have it display as a ligature otherwise.

That said, I can see it the other way too, but I'm just saying that if it was up to me I wouldn't jump through too many hoops to make ligatures display "correctly".
> Just wanted to pipe up here for a second, specifically replying to @DHowett's last message. I always considered ligatures being drawn as their individual characters when the cursor is over them as a feature, not a bug. It's nice to be able to see what characters comprise the ligature if you need to, and have it display as a ligature otherwise.
> 
> That said, I can see it the other way too, but I'm just saying that if it was up to me I wouldn't jump through too many hoops to make ligatures display "correctly".

Thanks for the input! Yeah... I waffled back and forth on this. Right now, given the architecture of our renderer, it's actually harder for us to split the ligature and re-strike the character :smile:

We condense full runs of text into as few rendering calls as possible, and we don't have support for splitting a run where we think the cursor is ... which is 100% something we need to fix later.
It took me an entire week to realize that what most people want is a _reverse video_ cursor (red on black text becomes black on red under the cursor), not a _literal XOR cursor_ (red on black text becomes cyan on white under the cursor).

You can guess which one I've been working on.

Anyway -- is that an acceptable stop-gap? Just to make sure it's visible.

We've got a plan now, at least, for how we'll implement reverse video.

Fix patch:
diff --git a/.github/actions/spelling/allow/names.txt b/.github/actions/spelling/allow/names.txt
index a157774a531..e58e630b21d 100644
--- a/.github/actions/spelling/allow/names.txt
+++ b/.github/actions/spelling/allow/names.txt
@@ -1,6 +1,6 @@
 Anup
-austdi
 arkthur
+austdi
 Ballmer
 bhoj
 Bhojwani
@@ -31,8 +31,8 @@ jerrysh
 Kaiyu
 kimwalisch
 KMehrain
-KODELIFE
 Kodelife
+KODELIFE
 Kourosh
 kowalczyk
 leonardder
@@ -61,6 +61,7 @@ oising
 oldnewthing
 opengl
 osgwiki
+Ottosson
 pabhojwa
 panos
 paulcam
@@ -88,8 +89,8 @@ Wirt
 Wojciech
 zadjii
 Zamor
-Zamora
 zamora
+Zamora
 zljubisic
 Zoey
 zorio
diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt
index 3f6e31b1072..3f3c4a691d5 100644
--- a/.github/actions/spelling/excludes.txt
+++ b/.github/actions/spelling/excludes.txt
@@ -112,6 +112,7 @@
 ^src/tools/RenderingTests/main.cpp$
 ^src/tools/texttests/fira\.txt$
 ^src/tools/U8U16Test/(?:fr|ru|zh)\.txt$
+^src/types/ColorFix.cpp
 ^src/types/ut_types/UtilsTests.cpp$
 ^tools/ReleaseEngineering/ServicingPipeline.ps1$
 ignore$
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index 8c4bfc3629c..f471a831251 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -43,6 +43,7 @@ antialiasing
 ANull
 anycpu
 APARTMENTTHREADED
+APCA
 APCs
 APIENTRY
 apiset
@@ -194,6 +195,8 @@ chh
 chk
 CHT
 Cic
+cielab
+Cielab
 Clcompile
 CLE
 cleartype
@@ -584,6 +587,7 @@ ENU
 ENUMLOGFONT
 ENUMLOGFONTEX
 enumranges
+EOK
 eplace
 EPres
 EQU
@@ -1290,6 +1294,8 @@ OEMFONT
 OEMFORMAT
 OEMs
 offboarded
+oklab
+Oklab
 OLEAUT
 OLECHAR
 onecore
@@ -1770,6 +1776,8 @@ srcsrv
 SRCSRVTRG
 srctool
 srect
+srgb
+Srgb
 srv
 srvinit
 srvpipe
diff --git a/NOTICE.md b/NOTICE.md
index e5046604574..fb48315d254 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -301,41 +301,28 @@ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 ```
 
-## ConEmu
-**Source**: [https://github.com/Maximus5/ConEmu](https://github.com/Maximus5/ConEmu)
+## Oklab
+**Source**: [https://bottosson.github.io/posts/oklab/](https://bottosson.github.io/posts/oklab/)
 
 ### License
 
 ```
-BSD 3-Clause License
-
-Copyright (c) 2009-2017, Maximus5 <ConEmu.Maximus5@gmail.com>
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
-  list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
-  this list of conditions and the following disclaimer in the documentation
-  and/or other materials provided with the distribution.
-
-* Neither the name of the copyright holder nor the names of its
-  contributors may be used to endorse or promote products derived from
-  this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+Copyright (c) 2020 Björn Ottosson
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
 ```
 
 # Microsoft Open Source
diff --git a/doc/color_nudging.html b/doc/color_nudging.html
new file mode 100644
index 00000000000..cfbbc4bd11e
--- /dev/null
+++ b/doc/color_nudging.html
@@ -0,0 +1,235 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <title>Perceptual Color Nudging</title>
+    <meta name="viewport" content="width=device-width,initial-scale=1">
+    <style>
+        html {
+            background-color: #0c0c0c;
+            color: #cccccc;
+            font-family: "Cascadia Code", "Cascadia Mono", monospace;
+        }
+
+        body {
+            display: flex;
+            margin: 0;
+            white-space: nowrap;
+            min-height: 100vh;
+        }
+
+        body>div {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            padding: 2rem;
+        }
+
+        form,
+        h2 {
+            margin: 1rem;
+        }
+
+        p,
+        pre {
+            margin: 0.5rem;
+        }
+
+        table {
+            border-collapse: collapse;
+        }
+
+        table td {
+            padding: 0.5rem;
+        }
+
+    </style>
+</head>
+
+<body>
+    <div style="flex: 2; align-items: flex-start; background-color: #0c0c0c">
+        <form>
+            <input id="background-color" name="background-color" type="color" value="#0c0c0c" />
+            <label for="background-color">background color</label>
+        </form>
+        <table>
+            <tr>
+                <td>Input</td>
+                <td>WCAG21:<br>APCA:</td>
+                <td id="stats-input"></td>
+            </tr>
+            <tr>
+                <td>&Delta;E2000<br>(ConEmu)</td>
+                <td>WCAG21:<br>APCA:</td>
+                <td id="stats-cielab"></td>
+            </tr>
+            <tr>
+                <td>&Delta;EOK</td>
+                <td>WCAG21:<br>APCA:</td>
+                <td id="stats-oklab"></td>
+            </tr>
+        </table>
+    </div>
+    <div id="input" style="flex: 1">
+        <h2>Input</h2>
+        <pre style="font-size: 8pt">&#29483;</pre>
+        <pre style="font-size: 10pt">&#29483;</pre>
+        <pre style="font-size: 12pt">&#29483;</pre>
+        <pre style="font-size: 14pt">&#29483;</pre>
+        <pre style="font-size: 16pt">&#29483;</pre>
+        <pre style="font-size: 32pt">&#29483;</pre>
+        <pre style="font-size: 64pt">&#29483;</pre>
+    </div>
+    <div id="cielab" style="flex: 1">
+        <h2>&Delta;E2000 (ConEmu)</h2>
+        <pre style="font-size: 8pt">&#29483;</pre>
+        <pre style="font-size: 10pt">&#29483;</pre>
+        <pre style="font-size: 12pt">&#29483;</pre>
+        <pre style="font-size: 14pt">&#29483;</pre>
+        <pre style="font-size: 16pt">&#29483;</pre>
+        <pre style="font-size: 32pt">&#29483;</pre>
+        <pre style="font-size: 64pt">&#29483;</pre>
+    </div>
+    <div id="oklab" style="flex: 1">
+        <h2>&Delta;EOK</h2>
+        <pre style="font-size: 8pt">&#29483;</pre>
+        <pre style="font-size: 10pt">&#29483;</pre>
+        <pre style="font-size: 12pt">&#29483;</pre>
+        <pre style="font-size: 14pt">&#29483;</pre>
+        <pre style="font-size: 16pt">&#29483;</pre>
+        <pre style="font-size: 32pt">&#29483;</pre>
+        <pre style="font-size: 64pt">&#29483;</pre>
+    </div>
+    <script type="module">
+        import Color from "https://cdn.jsdelivr.net/npm/colorjs.io@0.4.3/+esm";
+
+        window.Color = Color;
+
+        const input = document.getElementById("input");
+        const cielab = document.getElementById("cielab");
+        const oklab = document.getElementById("oklab");
+
+        const statsInput = document.getElementById("stats-input");
+        const statsCielab = document.getElementById("stats-cielab");
+        const statsOklab = document.getElementById("stats-oklab");
+
+        let backgroundColor = new Color("#0c0c0c");
+        let foregroundColor = new Color("#0c0c0c");
+        let foregroundColorRange = null;
+        let previousSecsIntegral = -1;
+
+        function saturate(val) {
+            return val < 0 ? 0 : val > 1 ? 1 : val;
+        }
+
+        function clipToSrgb(color) {
+            return color.to("srgb").toGamut({ method: "clip" });
+        }
+
+        function nudgeCielab(backgroundColor, foregroundColor) {
+            const backgroundCielab = backgroundColor.to("lab-d65");
+            const foregroundCielab = foregroundColor.to("lab-d65");
+
+            const de1 = Color.deltaE(foregroundColor, backgroundCielab, "2000");
+            if (de1 >= 12.0) {
+                return foregroundColor;
+            }
+
+            for (let i = 0; i <= 1; i++) {
+                const step = (i == 0) ? 5.0 : -5.0;
+                foregroundCielab.l += step;
+
+                while (((i == 0) && foregroundCielab.l <= 100) || (i == 1 && foregroundCielab.l >= 0)) {
+                    const de2 = Color.deltaE(foregroundCielab, backgroundCielab, "2000");
+                    if (de2 >= 20.0) {
+                        return clipToSrgb(foregroundCielab);
+                    }
+                    foregroundCielab.l += step;
+                }
+            }
+        }
+
+        function nudgeOklab(backgroundColor, foregroundColor) {
+            const backgroundOklab = backgroundColor.to("oklab");
+            const foregroundOklab = foregroundColor.to("oklab");
+            const deltaSquared = {
+                l: (backgroundOklab.l - foregroundOklab.l) ** 2,
+                a: (backgroundOklab.a - foregroundOklab.a) ** 2,
+                b: (backgroundOklab.b - foregroundOklab.b) ** 2,
+            };
+            const distance = deltaSquared.l + deltaSquared.a + deltaSquared.b;
+
+            if (distance >= 0.25) {
+                return foregroundColor;
+            }
+
+            let deltaL = Math.sqrt(0.25 - deltaSquared.a - deltaSquared.b);
+            if (foregroundOklab.l < backgroundOklab.l)
+            {
+                deltaL = -deltaL;
+            }
+
+            foregroundOklab.l = backgroundOklab.l + deltaL;
+            if (foregroundOklab.l < 0 || foregroundOklab.l > 1)
+            {
+                foregroundOklab.l = backgroundOklab.l - deltaL;
+            }
+
+            return clipToSrgb(foregroundOklab);
+        }
+
+        function contrastStringLevels(num, level0, level1) {
+            const str = num.toFixed(1);
+            if (num < level0) {
+                return `<span style="color:crimson">${str}</span>`;
+            }
+            if (num < level1) {
+                return `<span style="color:coral">${str}</span>`;
+            }
+            return str;
+        }
+
+        function contrastString(foregroundColor) {
+            const contrastWCAG21 = contrastStringLevels(foregroundColor.contrast(backgroundColor, "WCAG21"), 3, 4.5);
+            const contrastAPCA = contrastStringLevels(Math.abs(foregroundColor.contrast(backgroundColor, "APCA")), 45, 60);
+            return `${contrastWCAG21}<br/>${contrastAPCA}`;
+        }
+
+        function animate(time) {
+            const timeScale = time / 1000;
+            const secsIntegral = Math.trunc(timeScale);
+            const secsFractional = timeScale % 1;
+
+            if (previousSecsIntegral != secsIntegral) {
+                const foregroundColorTarget = new Color("srgb", backgroundColor.coords.map(c => saturate(c + Math.random() - 0.5)));
+                foregroundColorRange = foregroundColor.range(foregroundColorTarget, { space: "srgb" });
+                previousSecsIntegral = secsIntegral;
+            }
+
+            foregroundColor = foregroundColorRange(secsFractional);
+            input.style.color = foregroundColor.toString({ inGamut: false });
+
+            const foregroundCielabNudged = nudgeCielab(backgroundColor, foregroundColor);
+            const foregroundOklabNudged = nudgeOklab(backgroundColor, foregroundColor);
+
+            cielab.style.color = foregroundCielabNudged;
+            oklab.style.color = foregroundOklabNudged;
+
+            statsInput.innerHTML = contrastString(foregroundColor);
+            statsCielab.innerHTML = contrastString(foregroundCielabNudged);
+            statsOklab.innerHTML = contrastString(foregroundOklabNudged);
+
+            requestAnimationFrame(animate);
+        }
+        requestAnimationFrame(animate);
+
+        document.getElementById("background-color").addEventListener("input", event => {
+            backgroundColor = new Color(event.target.value);
+            document.documentElement.style.backgroundColor = backgroundColor;
+        }, false);
+
+        document.documentElement.style.backgroundColor = backgroundColor;
+    </script>
+</body>
+
+</html>
diff --git a/src/buffer/out/TextColor.cpp b/src/buffer/out/TextColor.cpp
index 7440cc03d36..2e2e6f9fa0b 100644
--- a/src/buffer/out/TextColor.cpp
+++ b/src/buffer/out/TextColor.cpp
@@ -64,7 +64,7 @@ bool TextColor::CanBeBrightened() const noexcept
 
 bool TextColor::IsLegacy() const noexcept
 {
-    return IsIndex16() || (IsIndex256() && _index < 16);
+    return (IsIndex16() || IsIndex256()) && _index < 16;
 }
 
 bool TextColor::IsIndex16() const noexcept
@@ -82,6 +82,11 @@ bool TextColor::IsDefault() const noexcept
     return _meta == ColorType::IsDefault;
 }
 
+bool TextColor::IsDefaultOrLegacy() const noexcept
+{
+    return _meta != ColorType::IsRgb && _index < 16;
+}
+
 bool TextColor::IsRgb() const noexcept
 {
     return _meta == ColorType::IsRgb;
diff --git a/src/buffer/out/TextColor.h b/src/buffer/out/TextColor.h
index b975028760c..0b71078fbf1 100644
--- a/src/buffer/out/TextColor.h
+++ b/src/buffer/out/TextColor.h
@@ -37,12 +37,14 @@ Revision History:
 #include "WexTestClass.h"
 #endif
 
+// The enum values being in this particular order allows the compiler to do some useful optimizations,
+// like simplifying `IsIndex16() || IsIndex256()` into a simple range check without branching.
 enum class ColorType : BYTE
 {
-    IsIndex256 = 0x0,
-    IsIndex16 = 0x1,
-    IsDefault = 0x2,
-    IsRgb = 0x3
+    IsDefault,
+    IsIndex16,
+    IsIndex256,
+    IsRgb
 };
 
 enum class ColorAlias : size_t
@@ -121,6 +123,7 @@ struct TextColor
     bool IsIndex16() const noexcept;
     bool IsIndex256() const noexcept;
     bool IsDefault() const noexcept;
+    bool IsDefaultOrLegacy() const noexcept;
     bool IsRgb() const noexcept;
 
     void SetColor(const COLORREF rgbColor) noexcept;
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index 99fa8453472..c6fa30d0026 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -180,7 +180,6 @@ void Terminal::UpdateAppearance(const ICoreAppearance& appearance)
     {
         _renderSettings.SetColorTableEntry(i, til::color{ appearance.GetColorTableEntry(i) });
     }
-    _renderSettings.MakeAdjustedColorArray();
 
     auto cursorShape = CursorType::VerticalBar;
     switch (appearance.CursorShape())
@@ -1325,8 +1324,6 @@ void Terminal::ApplyScheme(const Scheme& colorScheme)
 
     _renderSettings.SetColorTableEntry(TextColor::CURSOR_COLOR, til::color{ colorScheme.CursorColor });
 
-    _renderSettings.MakeAdjustedColorArray();
-
     // Tell the control that the scrollbar has somehow changed. Used as a
     // workaround to force the control to redraw any scrollbar marks whose color
     // may have changed.
diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp
index 9ecf9beac7e..c4959521fad 100644
--- a/src/renderer/atlas/BackendD3D.cpp
+++ b/src/renderer/atlas/BackendD3D.cpp
@@ -10,6 +10,7 @@
 #include <shader_vs.h>
 
 #include "dwrite.h"
+#include "../../types/inc/ColorFix.hpp"
 
 #if ATLAS_DEBUG_SHOW_DIRTY || ATLAS_DEBUG_COLORIZE_GLYPH_ATLAS
 #include "colorbrewer.h"
@@ -826,7 +827,7 @@ void BackendD3D::_flushQuads(const RenderingPayload& p)
 
     if (!_cursorRects.empty())
     {
-        _drawCursorForeground(p);
+        _drawCursorForeground();
     }
 
     // TODO: Shrink instances buffer
@@ -1627,9 +1628,19 @@ void BackendD3D::_drawCursorBackground(const RenderingPayload& p)
             static_cast<u16>(p.s->font->cellSize.x * (x1 - x0)),
             p.s->font->cellSize.y,
         };
-        const auto isInverted = cursorColor == 0xffffffff;
-        const auto background = isInverted ? bg ^ 0xc0c0c0 : cursorColor;
-        const auto foreground = isInverted ? 0 : bg;
+        auto background = cursorColor;
+        auto foreground = bg;
+
+        if (cursorColor == 0xffffffff)
+        {
+            background = bg ^ 0xffffff;
+            foreground = 0xffffffff;
+        }
+
+        // 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);
+
         auto& c0 = _cursorRects.emplace_back(position, size, background, foreground);
 
         switch (static_cast<CursorType>(p.s->cursor->cursorType))
@@ -1702,7 +1713,7 @@ void BackendD3D::_drawCursorBackground(const RenderingPayload& p)
     }
 }
 
-void BackendD3D::_drawCursorForeground(const RenderingPayload& p)
+void BackendD3D::_drawCursorForeground()
 {
     // NOTE: _appendQuad() may reallocate the _instances vector. It's important to iterate
     // by index, because pointers (or iterators) would get invalidated. It's also important
@@ -1779,7 +1790,7 @@ void BackendD3D::_drawCursorForeground(const RenderingPayload& p)
             {
                 // The _instances vector is _huge_ (easily up to 50k items) whereas only 1-2 items will actually overlap
                 // with the cursor. --> Make this loop more compact by putting as much as possible into a function call.
-                const auto added = _drawCursorForegroundSlowPath(p, c, i);
+                const auto added = _drawCursorForegroundSlowPath(c, i);
                 i += added;
                 instancesCount += added;
             }
@@ -1787,7 +1798,7 @@ void BackendD3D::_drawCursorForeground(const RenderingPayload& p)
     }
 }
 
-size_t BackendD3D::_drawCursorForegroundSlowPath(const RenderingPayload& p, const CursorRect& c, size_t offset)
+size_t BackendD3D::_drawCursorForegroundSlowPath(const CursorRect& c, size_t offset)
 {
     // We won't die from copying 24 bytes. It simplifies the code below especially in
     // respect to when/if we overwrite the _instances[offset] slot with a cutout.
@@ -1886,7 +1897,9 @@ size_t BackendD3D::_drawCursorForegroundSlowPath(const RenderingPayload& p, cons
         target.color = it.color;
     }
 
-    const auto cursorColor = p.s->cursor->cursorColor;
+    auto color = c.foreground == 0xffffffff ? it.color ^ 0xffffff : c.foreground;
+    color = ColorFix::GetPerceivableColor(color, c.background, 0.5f * 0.5f);
+
     // If the cursor covers the entire glyph (like, let's say, a full-box cursor with an ASCII character),
     // we don't append a new quad, but rather reuse the one that already exists (cutoutCount == 0).
     auto& target = cutoutCount ? _appendQuad() : _instances[offset];
@@ -1898,7 +1911,7 @@ size_t BackendD3D::_drawCursorForegroundSlowPath(const RenderingPayload& p, cons
     target.size.y = static_cast<u16>(intersectionB - intersectionT);
     target.texcoord.x = static_cast<u16>(it.texcoord.x + intersectionL - instanceL);
     target.texcoord.y = static_cast<u16>(it.texcoord.y + intersectionT - instanceT);
-    target.color = cursorColor == 0xffffffff ? it.color ^ 0xc0c0c0 : c.foreground;
+    target.color = color;
 
     return addedInstances;
 }
diff --git a/src/renderer/atlas/BackendD3D.h b/src/renderer/atlas/BackendD3D.h
index c5cf07e6553..f75ae954079 100644
--- a/src/renderer/atlas/BackendD3D.h
+++ b/src/renderer/atlas/BackendD3D.h
@@ -220,8 +220,8 @@ namespace Microsoft::Console::Render::Atlas
         void _splitDoubleHeightGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry);
         void _drawGridlines(const RenderingPayload& p, u16 y);
         void _drawCursorBackground(const RenderingPayload& p);
-        ATLAS_ATTR_COLD void _drawCursorForeground(const RenderingPayload& p);
-        ATLAS_ATTR_COLD size_t _drawCursorForegroundSlowPath(const RenderingPayload& p, const CursorRect& c, size_t offset);
+        ATLAS_ATTR_COLD void _drawCursorForeground();
+        ATLAS_ATTR_COLD size_t _drawCursorForegroundSlowPath(const CursorRect& c, size_t offset);
         void _drawSelection(const RenderingPayload& p);
         void _executeCustomShader(RenderingPayload& p);
 
diff --git a/src/renderer/base/RenderSettings.cpp b/src/renderer/base/RenderSettings.cpp
index 65a63a707dc..03c4795635a 100644
--- a/src/renderer/base/RenderSettings.cpp
+++ b/src/renderer/base/RenderSettings.cpp
@@ -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 };
-
 RenderSettings::RenderSettings() noexcept
 {
     InitializeColorTable(_colorTable);
@@ -71,42 +67,6 @@ void RenderSettings::ResetColorTable() noexcept
     InitializeColorTable({ _colorTable.data(), 16 });
 }
 
-// Routine Description:
-// - Creates the adjusted color array, which contains the possible foreground colors,
-//   adjusted for perceivability
-// - The adjusted color array is 2-d, and effectively maps a background and foreground
-//   color pair to the adjusted foreground for that color pair
-void RenderSettings::MakeAdjustedColorArray() noexcept
-{
-    // The color table has 16 colors, but the adjusted color table needs to be 19
-    // to include the default background, default foreground and bright default foreground colors
-    std::array<COLORREF, 19> colorTableWithDefaults;
-    std::copy_n(std::begin(_colorTable), 16, std::begin(colorTableWithDefaults));
-    colorTableWithDefaults[AdjustedFgIndex] = GetColorAlias(ColorAlias::DefaultForeground);
-    colorTableWithDefaults[AdjustedBgIndex] = GetColorAlias(ColorAlias::DefaultBackground);
-
-    // We need to use TextColor to calculate the bright default fg
-    TextColor defaultFg;
-    colorTableWithDefaults[AdjustedBrightFgIndex] = defaultFg.GetColor(_colorTable, GetColorAliasIndex(ColorAlias::DefaultForeground), true);
-
-    for (auto fgIndex = 0; fgIndex < 19; ++fgIndex)
-    {
-        const auto fg = til::at(colorTableWithDefaults, fgIndex);
-        for (auto bgIndex = 0; bgIndex < 19; ++bgIndex)
-        {
-            if (fgIndex == bgIndex)
-            {
-                _adjustedForegroundColors[bgIndex][fgIndex] = fg;
-            }
-            else
-            {
-                const auto bg = til::at(colorTableWithDefaults, bgIndex);
-                _adjustedForegroundColors[bgIndex][fgIndex] = ColorFix::GetPerceivableColor(fg, bg);
-            }
-        }
-    }
-}
-
 // Routine Description:
 // - Updates the given index in the color table to a new value.
 // Arguments:
@@ -196,74 +156,37 @@ std::pair<COLORREF, COLORREF> RenderSettings::GetAttributeColors(const TextAttri
     const auto dimFg = attr.IsFaint() || (_blinkShouldBeFaint && attr.IsBlinking());
     const auto swapFgAndBg = attr.IsReverseVideo() ^ GetRenderMode(Mode::ScreenReversed);
 
-    // We want to nudge the foreground color to make it more perceivable only for the
-    // default color pairs within the color table
-    if (Feature_AdjustIndistinguishableText::IsEnabled() &&
-        GetRenderMode(Mode::IndexedDistinguishableColors) &&
-        !dimFg &&
-        !attr.IsInvisible() &&
-        (fgTextColor.IsDefault() || fgTextColor.IsLegacy()) &&
-        (bgTextColor.IsDefault() || bgTextColor.IsLegacy()))
-    {
-        const auto bgIndex = bgTextColor.IsDefault() ? AdjustedBgIndex : bgTextColor.GetIndex();
-        auto fgIndex = fgTextColor.IsDefault() ? AdjustedFgIndex : fgTextColor.GetIndex();
-
-        if (brightenFg)
-        {
-            // There is a special case for intense here - we need to get the bright version of the foreground color
-            if (fgTextColor.IsIndex16() && (fgIndex < 8))
-            {
-                fgIndex += 8;
-            }
-            else if (fgTextColor.IsDefault())
-            {
-                fgIndex = AdjustedBrightFgIndex;
-            }
-        }
+    auto fg = fgTextColor.GetColor(_colorTable, defaultFgIndex, brightenFg);
+    auto bg = bgTextColor.GetColor(_colorTable, defaultBgIndex);
 
-        if (swapFgAndBg)
-        {
-            const auto fg = _adjustedForegroundColors[fgIndex][bgIndex];
-            const auto bg = fgTextColor.GetColor(_colorTable, defaultFgIndex, brightenFg);
-            return { fg, bg };
-        }
-        else
-        {
-            const auto fg = _adjustedForegroundColors[bgIndex][fgIndex];
-            const auto bg = bgTextColor.GetColor(_colorTable, defaultBgIndex);
-            return { fg, bg };
-        }
+    if (dimFg)
+    {
+        fg = (fg >> 1) & 0x7F7F7F; // Divide foreground color components by two.
     }
-    else
+    if (swapFgAndBg)
     {
-        auto fg = fgTextColor.GetColor(_colorTable, defaultFgIndex, brightenFg);
-        auto bg = bgTextColor.GetColor(_colorTable, defaultBgIndex);
-
-        if (dimFg)
-        {
-            fg = (fg >> 1) & 0x7F7F7F; // Divide foreground color components by two.
-        }
-        if (swapFgAndBg)
-        {
-            std::swap(fg, bg);
-        }
-        if (attr.IsInvisible())
-        {
-            fg = bg;
-        }
+        std::swap(fg, bg);
+    }
+    if (attr.IsInvisible())
+    {
+        fg = bg;
+    }
 
-        // We intentionally aren't _only_ checking for attr.IsInvisible here, because we also want to
-        // catch the cases where the fg was intentionally set to be the same as the bg. In either case,
-        // don't adjust the foreground.
-        if (Feature_AdjustIndistinguishableText::IsEnabled() &&
+    // We intentionally aren't _only_ checking for attr.IsInvisible here, because we also want to
+    // catch the cases where the fg was intentionally set to be the same as the bg. In either case,
+    // don't adjust the foreground.
+    if constexpr (Feature_AdjustIndistinguishableText::IsEnabled())
+    {
+        if (
+            _renderMode.any(Mode::IndexedDistinguishableColors, Mode::AlwaysDistinguishableColors) &&
             fg != bg &&
-            GetRenderMode(Mode::AlwaysDistinguishableColors))
+            (_renderMode.test(Mode::AlwaysDistinguishableColors) || (fgTextColor.IsDefaultOrLegacy() && bgTextColor.IsDefaultOrLegacy())))
         {
-            fg = ColorFix::GetPerceivableColor(fg, bg);
+            fg = ColorFix::GetPerceivableColor(fg, bg, 0.5f * 0.5f);
         }
-
-        return { fg, bg };
     }
+
+    return { fg, bg };
 }
 
 // Routine Description:
diff --git a/src/renderer/inc/RenderSettings.hpp b/src/renderer/inc/RenderSettings.hpp
index 897d8e099d4..4b6e7c3981c 100644
--- a/src/renderer/inc/RenderSettings.hpp
+++ b/src/renderer/inc/RenderSettings.hpp
@@ -33,7 +33,6 @@ namespace Microsoft::Console::Render
         bool GetRenderMode(const Mode mode) const noexcept;
         const std::array<COLORREF, TextColor::TABLE_SIZE>& GetColorTable() const noexcept;
         void ResetColorTable() noexcept;
-        void MakeAdjustedColorArray() noexcept;
         void SetColorTableEntry(const size_t tableIndex, const COLORREF color);
         COLORREF GetColorTableEntry(const size_t tableIndex) const;
         void SetColorAlias(const ColorAlias alias, const size_t tableIndex, const COLORREF color);
@@ -48,7 +47,6 @@ namespace Microsoft::Console::Render
         til::enumset<Mode> _renderMode{ Mode::BlinkAllowed, Mode::IntenseIsBright };
         std::array<COLORREF, TextColor::TABLE_SIZE> _colorTable;
         std::array<size_t, static_cast<size_t>(ColorAlias::ENUM_COUNT)> _colorAliasIndices;
-        std::array<std::array<COLORREF, 19>, 19> _adjustedForegroundColors;
         size_t _blinkCycle = 0;
         mutable bool _blinkIsInUse = false;
         bool _blinkShouldBeFaint = false;
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index 6a4d7b8d672..7c2c04705fd 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -2940,11 +2940,6 @@ bool AdaptDispatch::SetClipboard(const std::wstring_view content)
 bool AdaptDispatch::SetColorTableEntry(const size_t tableIndex, const DWORD dwColor)
 {
     _renderSettings.SetColorTableEntry(tableIndex, dwColor);
-    if (_renderSettings.GetRenderMode(RenderSettings::Mode::IndexedDistinguishableColors))
-    {
-        // Re-calculate the adjusted colors now that one of the entries has been changed
-        _renderSettings.MakeAdjustedColorArray();
-    }
 
     // If we're a conpty, always return false, so that we send the updated color
     //      value to the terminal. Still handle the sequence so apps that use
@@ -3010,11 +3005,6 @@ bool AdaptDispatch::AssignColor(const DispatchTypes::ColorItem item, const VTInt
     case DispatchTypes::ColorItem::NormalText:
         _renderSettings.SetColorAliasIndex(ColorAlias::DefaultForeground, fgIndex);
         _renderSettings.SetColorAliasIndex(ColorAlias::DefaultBackground, bgIndex);
-        if (_renderSettings.GetRenderMode(RenderSettings::Mode::IndexedDistinguishableColors))
-        {
-            // Re-calculate the adjusted colors now that these aliases have been changed
-            _renderSettings.MakeAdjustedColorArray();
-        }
         break;
     case DispatchTypes::ColorItem::WindowFrame:
         _renderSettings.SetColorAliasIndex(ColorAlias::FrameForeground, fgIndex);
diff --git a/src/types/ColorFix.cpp b/src/types/ColorFix.cpp
index 723d7059f68..eda47d396a2 100644
--- a/src/types/ColorFix.cpp
+++ b/src/types/ColorFix.cpp
@@ -1,231 +1,212 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT license.
 
-// A lot of code was taken from
-// https://github.com/Maximus5/ConEmu/blob/master/src/ConEmu/ColorFix.cpp
-// and then adjusted to fit our style guidelines
-
 #include "precomp.h"
-
 #include "inc/ColorFix.hpp"
 
-TIL_FAST_MATH_BEGIN
-
-static constexpr float gMinThreshold = 12.0f;
-static constexpr float gExpThreshold = 20.0f;
-static constexpr float gLStep = 5.0f;
+// This table contains a direct mapping from 8-bit sRGB to linear RGB.
+// It was generated using the following code:
+//   #include <charconv>
+//
+//   int main() {
+//       char buffer[32];
+//
+//       for (int i = 0; i < 256; ++i) {
+//           auto v = i / 255.0f;
+//           v = v <= 0.04045f ? v / 12.92f : powf((v + 0.055f) / 1.055f, 2.4f);
+//
+//           auto p = std::to_chars(&buffer[0], &buffer[32], v, std::chars_format::hex, -1).ptr;
+//           strcpy_s(p, &buffer[32] - p, "f,");
+//
+//           if (i % 16 == 0) printf("\n    ");
+//           printf("0x%-15s", &buffer[0]);
+//       }
+//
+//       return 0;
+//   }
+//
+// clang-format off
+static constexpr float srgbToRgbLUT[256]{
+    0x0.000000p+0f,  0x1.3e4568p-12f, 0x1.3e4568p-11f, 0x1.dd681cp-11f, 0x1.3e4568p-10f, 0x1.8dd6c2p-10f, 0x1.dd681cp-10f, 0x1.167cbap-9f,  0x1.3e4568p-9f,  0x1.660e16p-9f,  0x1.8dd6c2p-9f,  0x1.b6a31ap-9f,  0x1.e1e31ap-9f,  0x1.07c38cp-8f,  0x1.1fcc2cp-8f,  0x1.390ffap-8f,
+    0x1.53936ep-8f,  0x1.6f5adep-8f,  0x1.8c6a92p-8f,  0x1.aac6c2p-8f,  0x1.ca7382p-8f,  0x1.eb74e0p-8f,  0x1.06e76ap-7f,  0x1.18c2a4p-7f,  0x1.2b4e06p-7f,  0x1.3e8b7cp-7f,  0x1.527cd6p-7f,  0x1.6723eep-7f,  0x1.7c8292p-7f,  0x1.929a86p-7f,  0x1.a96d8ep-7f,  0x1.c0fd62p-7f,
+    0x1.d94bbep-7f,  0x1.f25a44p-7f,  0x1.061550p-6f,  0x1.135f3ep-6f,  0x1.210bb6p-6f,  0x1.2f1b8ap-6f,  0x1.3d8f84p-6f,  0x1.4c6866p-6f,  0x1.5ba6fap-6f,  0x1.6b4c02p-6f,  0x1.7b5840p-6f,  0x1.8bcc72p-6f,  0x1.9ca956p-6f,  0x1.adefaap-6f,  0x1.bfa020p-6f,  0x1.d1bb72p-6f,
+    0x1.e44258p-6f,  0x1.f73582p-6f,  0x1.054ad2p-5f,  0x1.0f31b8p-5f,  0x1.194fccp-5f,  0x1.23a55ep-5f,  0x1.2e32c6p-5f,  0x1.38f85cp-5f,  0x1.43f678p-5f,  0x1.4f2d64p-5f,  0x1.5a9d76p-5f,  0x1.664700p-5f,  0x1.722a56p-5f,  0x1.7e47c6p-5f,  0x1.8a9fa2p-5f,  0x1.973238p-5f,
+    0x1.a3ffdep-5f,  0x1.b108d4p-5f,  0x1.be4d6ep-5f,  0x1.cbcdfcp-5f,  0x1.d98ac8p-5f,  0x1.e7841ep-5f,  0x1.f5ba52p-5f,  0x1.0216cep-4f,  0x1.096f2ap-4f,  0x1.10e660p-4f,  0x1.187c94p-4f,  0x1.2031ecp-4f,  0x1.28068ap-4f,  0x1.2ffa94p-4f,  0x1.380e2cp-4f,  0x1.404176p-4f,
+    0x1.489496p-4f,  0x1.5107aep-4f,  0x1.599ae0p-4f,  0x1.624e50p-4f,  0x1.6b2224p-4f,  0x1.741676p-4f,  0x1.7d2b6ap-4f,  0x1.866124p-4f,  0x1.8fb7c4p-4f,  0x1.992f6cp-4f,  0x1.a2c83ep-4f,  0x1.ac8258p-4f,  0x1.b65ddep-4f,  0x1.c05aeep-4f,  0x1.ca79aap-4f,  0x1.d4ba32p-4f,
+    0x1.df1ca4p-4f,  0x1.e9a122p-4f,  0x1.f447d0p-4f,  0x1.ff10c2p-4f,  0x1.04fe0ep-3f,  0x1.0a8500p-3f,  0x1.101d46p-3f,  0x1.15c6f0p-3f,  0x1.1b820ap-3f,  0x1.214ea8p-3f,  0x1.272cd6p-3f,  0x1.2d1ca4p-3f,  0x1.331e20p-3f,  0x1.39315ap-3f,  0x1.3f5660p-3f,  0x1.458d46p-3f,
+    0x1.4bd612p-3f,  0x1.5230d8p-3f,  0x1.589da0p-3f,  0x1.5f1c82p-3f,  0x1.65ad88p-3f,  0x1.6c50c2p-3f,  0x1.73063cp-3f,  0x1.79ce06p-3f,  0x1.80a82cp-3f,  0x1.8794bep-3f,  0x1.8e93c8p-3f,  0x1.95a55cp-3f,  0x1.9cc984p-3f,  0x1.a4004ep-3f,  0x1.ab49ccp-3f,  0x1.b2a606p-3f,
+    0x1.ba1516p-3f,  0x1.c196f8p-3f,  0x1.c92bc0p-3f,  0x1.d0d37ep-3f,  0x1.d88e40p-3f,  0x1.e05c18p-3f,  0x1.e83d08p-3f,  0x1.f03120p-3f,  0x1.f83870p-3f,  0x1.002984p-2f,  0x1.044078p-2f,  0x1.08611ap-2f,  0x1.0c8b74p-2f,  0x1.10bf8ap-2f,  0x1.14fd64p-2f,  0x1.194506p-2f,
+    0x1.1d967ap-2f,  0x1.21f1c2p-2f,  0x1.2656e8p-2f,  0x1.2ac5f0p-2f,  0x1.2f3ee2p-2f,  0x1.33c1c4p-2f,  0x1.384e9cp-2f,  0x1.3ce56ep-2f,  0x1.418644p-2f,  0x1.463124p-2f,  0x1.4ae610p-2f,  0x1.4fa512p-2f,  0x1.546e30p-2f,  0x1.59416ep-2f,  0x1.5e1ed2p-2f,  0x1.630666p-2f,
+    0x1.67f830p-2f,  0x1.6cf430p-2f,  0x1.71fa6ep-2f,  0x1.770af2p-2f,  0x1.7c25c2p-2f,  0x1.814ae2p-2f,  0x1.867a5ap-2f,  0x1.8bb42ep-2f,  0x1.90f866p-2f,  0x1.964706p-2f,  0x1.9ba016p-2f,  0x1.a1039ap-2f,  0x1.a67198p-2f,  0x1.abea16p-2f,  0x1.b16d1ap-2f,  0x1.b6faa8p-2f,
+    0x1.bc92cap-2f,  0x1.c23582p-2f,  0x1.c7e2d6p-2f,  0x1.cd9acep-2f,  0x1.d35d6cp-2f,  0x1.d92abap-2f,  0x1.df02bap-2f,  0x1.e4e574p-2f,  0x1.ead2ecp-2f,  0x1.f0cb28p-2f,  0x1.f6ce2ep-2f,  0x1.fcdc04p-2f,  0x1.017a5ap-1f,  0x1.048c1cp-1f,  0x1.07a34ep-1f,  0x1.0abff2p-1f,
+    0x1.0de20cp-1f,  0x1.11099ep-1f,  0x1.1436acp-1f,  0x1.176936p-1f,  0x1.1aa140p-1f,  0x1.1ddecep-1f,  0x1.2121e2p-1f,  0x1.246a7cp-1f,  0x1.27b8a2p-1f,  0x1.2b0c56p-1f,  0x1.2e659ap-1f,  0x1.31c472p-1f,  0x1.3528dep-1f,  0x1.3892e2p-1f,  0x1.3c0282p-1f,  0x1.3f77bep-1f,
+    0x1.42f29cp-1f,  0x1.46731cp-1f,  0x1.49f940p-1f,  0x1.4d850cp-1f,  0x1.511682p-1f,  0x1.54ada6p-1f,  0x1.584a7ap-1f,  0x1.5bed00p-1f,  0x1.5f953cp-1f,  0x1.63432ep-1f,  0x1.66f6d8p-1f,  0x1.6ab040p-1f,  0x1.6e6f66p-1f,  0x1.72344cp-1f,  0x1.75fef8p-1f,  0x1.79cf6ap-1f,
+    0x1.7da5a4p-1f,  0x1.8181a8p-1f,  0x1.85637cp-1f,  0x1.894b20p-1f,  0x1.8d3896p-1f,  0x1.912be2p-1f,  0x1.952504p-1f,  0x1.992402p-1f,  0x1.9d28dcp-1f,  0x1.a13396p-1f,  0x1.a54430p-1f,  0x1.a95aaep-1f,  0x1.ad7714p-1f,  0x1.b19960p-1f,  0x1.b5c19ap-1f,  0x1.b9efc0p-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.e09b70p-1f,  0x1.e5055ep-1f,  0x1.e9755cp-1f,  0x1.edeb5cp-1f,  0x1.f26772p-1f,  0x1.f6e98cp-1f,  0x1.fb71c0p-1f,  0x1.000000p+0f,
+};
+// clang-format on
 
-static constexpr float rad006 = 0.104719755119659774f;
-static constexpr float rad025 = 0.436332312998582394f;
-static constexpr float rad030 = 0.523598775598298873f;
-static constexpr float rad060 = 1.047197551196597746f;
-static constexpr float rad063 = 1.099557428756427633f;
-static constexpr float rad180 = 3.141592653589793238f;
-static constexpr float rad275 = 4.799655442984406336f;
-static constexpr float rad360 = 6.283185307179586476f;
+TIL_FAST_MATH_BEGIN
 
-ColorFix::ColorFix(COLORREF color) noexcept
+constexpr float saturate(float f) noexcept
 {
-    rgb = color;
-    _ToLab();
+    return f < 0 ? 0 : (f > 1 ? 1 : f);
 }
 
-// Method Description:
-// - Helper function to calculate HPrime
-float ColorFix::_GetHPrimeFn(float x, float y) noexcept
+// FP32 is the very epitome of defined behavior.
+#pragma warning(suppress : 26497) // You can attempt to make 'cbrtf_est' constexpr unless it contains any undefined behavior (f.4).)
+__forceinline float cbrtf_est(float a) noexcept
 {
-    if (x == 0 && y == 0)
+    // We can't use std::bit_cast here, even though it exists exactly for this purpose.
+    // MSVC is dumb and introduces function calls to std::bit_cast<uint32_t, float>
+    // even at the highest optimization and inlining levels. What.
+    union FP32
     {
-        return 0;
-    }
-
-    const auto hueAngle = atan2(x, y);
-    return hueAngle >= 0 ? hueAngle : hueAngle + rad360;
+        uint32_t u;
+        float f;
+    };
+
+    // http://metamerist.com/cbrt/cbrt.htm showed a great estimator for the cube root:
+    //   float_as_uint32_t / 3 + 709921077
+    // 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?
+    const auto x = fp.f;
+
+    // One round of Newton's method. It follows the Wikipedia article at
+    //   https://en.wikipedia.org/wiki/Cube_root#Numerical_methods
+    // For `a`s in the range between 0 and 1, this results in a maximum error of
+    // less than 6.7e-4f, which is not good, but good enough for us, because
+    // we're not an image editor. The benefit is that it's really fast.
+    return (1.0f / 3.0f) * (a / (x * x) + (x + x)); // 1st iteration
 }
 
-// Method Description:
-// - Given 2 colors, computes the DeltaE value between them
-// Arguments:
-// - x1: the first color
-// - x2: the second color
-// Return Value:
-// - The DeltaE value between x1 and x2
-float ColorFix::_GetDeltaE(ColorFix x1, ColorFix x2) noexcept
+// This namespace contains functions and structures as defined by: https://bottosson.github.io/posts/oklab/
+// It has been released into the public domain and is also available under an MIT license.
+// The only change I made is to replace cbrtf() with cbrtf_est() to cut the CPU cost by a third.
+namespace oklab
 {
-    constexpr float kSubL = 1;
-    constexpr float kSubC = 1;
-    constexpr float kSubH = 1;
-
-    // Delta L Prime
-    const auto deltaLPrime = x2.L - x1.L;
-
-    // L Bar
-    const auto lBar = (x1.L + x2.L) / 2;
-
-    // C1 & C2
-    const auto c1 = sqrtf(powf(x1.A, 2) + powf(x1.B, 2));
-    const auto c2 = sqrtf(powf(x2.A, 2) + powf(x2.B, 2));
-
-    // C Bar
-    const auto cBar = (c1 + c2) / 2;
-
-    // A Prime 1
-    const auto aPrime1 = x1.A + (x1.A / 2) * (1 - sqrtf(powf(cBar, 7) / (powf(cBar, 7) + powf(25.0f, 7))));
-
-    // A Prime 2
-    const auto aPrime2 = x2.A + (x2.A / 2) * (1 - sqrtf(powf(cBar, 7) / (powf(cBar, 7) + powf(25.0f, 7))));
-
-    // C Prime 1
-    const auto cPrime1 = sqrtf(powf(aPrime1, 2) + powf(x1.B, 2));
-
-    // C Prime 2
-    const auto cPrime2 = sqrtf(powf(aPrime2, 2) + powf(x2.B, 2));
-
-    // C Bar Prime
-    const auto cBarPrime = (cPrime1 + cPrime2) / 2;
-
-    // Delta C Prime
-    const auto deltaCPrime = cPrime2 - cPrime1;
-
-    // S sub L
-    const auto sSubL = 1 + ((0.015f * powf(lBar - 50, 2)) / sqrtf(20 + powf(lBar - 50, 2)));
-
-    // S sub C
-    const auto sSubC = 1 + 0.045f * cBarPrime;
-
-    // h Prime 1
-    const auto hPrime1 = _GetHPrimeFn(x1.B, aPrime1);
-
-    // h Prime 2
-    const auto hPrime2 = _GetHPrimeFn(x2.B, aPrime2);
-
-    // Delta H Prime
-    const auto deltaHPrime = 0 == c1 || 0 == c2 ? 0 : 2 * sqrtf(cPrime1 * cPrime2) * sin(abs(hPrime1 - hPrime2) <= rad180 ? hPrime2 - hPrime1 : (hPrime2 <= hPrime1 ? hPrime2 - hPrime1 + rad360 : hPrime2 - hPrime1 - rad360) / 2);
-
-    // H Bar Prime
-    const auto hBarPrime = (abs(hPrime1 - hPrime2) > rad180) ? (hPrime1 + hPrime2 + rad360) / 2 : (hPrime1 + hPrime2) / 2;
+    struct Lab
+    {
+        float l;
+        float a;
+        float b;
+    };
 
-    // T
-    const auto t = 1 - 0.17f * cosf(hBarPrime - rad030) + 0.24f * cosf(2 * hBarPrime) + 0.32f * cosf(3 * hBarPrime + rad006) - 0.20f * cosf(4 * hBarPrime - rad063);
+    struct RGB
+    {
+        float r;
+        float g;
+        float b;
+    };
 
-    // S sub H
-    const auto sSubH = 1 + 0.015f * cBarPrime * t;
+    __forceinline Lab linear_srgb_to_oklab(const RGB& c) noexcept
+    {
+        const auto l = 0.4122214708f * c.r + 0.5363325363f * c.g + 0.0514459929f * c.b;
+        const auto m = 0.2119034982f * c.r + 0.6806995451f * c.g + 0.1073969566f * c.b;
+        const auto s = 0.0883024619f * c.r + 0.2817188376f * c.g + 0.6299787005f * c.b;
+
+        const auto l_ = cbrtf_est(l);
+        const auto m_ = cbrtf_est(m);
+        const auto s_ = cbrtf_est(s);
+
+        return {
+            0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_,
+            1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_,
+            0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_,
+        };
+    }
 
-    // R sub T
-    const auto rSubT = -2 * sqrtf(powf(cBarPrime, 7) / (powf(cBarPrime, 7) + powf(25.0f, 7))) * sin(rad060 * exp(-powf((hBarPrime - rad275) / rad025, 2)));
+    __forceinline RGB oklab_to_linear_srgb(const Lab& c) noexcept
+    {
+        const auto l_ = c.l + 0.3963377774f * c.a + 0.2158037573f * c.b;
+        const auto m_ = c.l - 0.1055613458f * c.a - 0.0638541728f * c.b;
+        const auto s_ = c.l - 0.0894841775f * c.a - 1.2914855480f * c.b;
+
+        const auto l = l_ * l_ * l_;
+        const auto m = m_ * m_ * m_;
+        const auto s = s_ * s_ * s_;
+
+        return {
+            +4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s,
+            -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s,
+            -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s,
+        };
+    }
+}
 
-    // Put it all together!
-    const auto lightness = deltaLPrime / (kSubL * sSubL);
-    const auto chroma = deltaCPrime / (kSubC * sSubC);
-    const auto hue = deltaHPrime / (kSubH * sSubH);
+#pragma warning(push)
+#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4).
+#pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2).
 
-    return sqrtf(powf(lightness, 2) + powf(chroma, 2) + powf(hue, 2) + rSubT * chroma * hue);
+__forceinline oklab::RGB colorrefToLinear(COLORREF c) noexcept
+{
+    const auto r = srgbToRgbLUT[(c >> 0) & 0xff];
+    const auto g = srgbToRgbLUT[(c >> 8) & 0xff];
+    const auto b = srgbToRgbLUT[(c >> 16) & 0xff];
+    return { r, g, b };
 }
 
-// Method Description:
-// - Populates our L, A, B values, based on our r, g, b values
-// - Converts a color in rgb format to a color in lab format
-// - Reference: http://www.easyrgb.com/index.php?X=MATH&H=01#text1
-void ColorFix::_ToLab() noexcept
+#pragma warning(pop)
+
+__forceinline COLORREF linearToColorref(const oklab::RGB& c) noexcept
 {
-    auto var_R = r / 255.0f;
-    auto var_G = g / 255.0f;
-    auto var_B = b / 255.0f;
-
-    var_R = var_R > 0.04045f ? powf(((var_R + 0.055f) / 1.055f), 2.4f) : var_R / 12.92f;
-    var_G = var_G > 0.04045f ? powf(((var_G + 0.055f) / 1.055f), 2.4f) : var_G / 12.92f;
-    var_B = var_B > 0.04045f ? powf(((var_B + 0.055f) / 1.055f), 2.4f) : var_B / 12.92f;
-
-    var_R = var_R * 100.0f;
-    var_G = var_G * 100.0f;
-    var_B = var_B * 100.0f;
-
-    //Observer. = 2 degrees, Illuminant = D65
-    const auto X = var_R * 0.4124f + var_G * 0.3576f + var_B * 0.1805f;
-    const auto Y = var_R * 0.2126f + var_G * 0.7152f + var_B * 0.0722f;
-    const auto Z = var_R * 0.0193f + var_G * 0.1192f + var_B * 0.9505f;
-
-    auto var_X = X / 95.047f; //ref_X =  95.047   (Observer= 2 degrees, Illuminant= D65)
-    auto var_Y = Y / 100.000f; //ref_Y = 100.000
-    auto var_Z = Z / 108.883f; //ref_Z = 108.883
-
-    var_X = var_X > 0.008856f ? powf(var_X, (1.0f / 3.0f)) : (7.787f * var_X) + (16.0f / 116.0f);
-    var_Y = var_Y > 0.008856f ? powf(var_Y, (1.0f / 3.0f)) : (7.787f * var_Y) + (16.0f / 116.0f);
-    var_Z = var_Z > 0.008856f ? powf(var_Z, (1.0f / 3.0f)) : (7.787f * var_Z) + (16.0f / 116.0f);
-
-    L = (116.0f * var_Y) - 16.0f;
-    A = 500.0f * (var_X - var_Y);
-    B = 200.0f * (var_Y - var_Z);
+    auto r = saturate(c.r);
+    auto g = saturate(c.g);
+    auto b = saturate(c.b);
+    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;
+    return lrintf(r) | (lrintf(g) << 8) | (lrintf(b) << 16);
 }
 
-// Method Description:
-// - Populates our r, g, b values, based on our L, A, B values
-// - Converts a color in lab format to a color in rgb format
-// - Reference: http://www.easyrgb.com/index.php?X=MATH&H=01#text1
-void ColorFix::_ToRGB()
+// This function changes `color` so that it is visually different
+// enough from `reference` that it's (much more easily) readable.
+// See /doc/color_nudging.html
+COLORREF ColorFix::GetPerceivableColor(COLORREF color, COLORREF reference, float minSquaredDistance) noexcept
 {
-    auto var_Y = (L + 16.0f) / 116.0f;
-    auto var_X = A / 500.0f + var_Y;
-    auto var_Z = var_Y - B / 200.0f;
-
-    var_Y = (powf(var_Y, 3) > 0.008856f) ? powf(var_Y, 3) : (var_Y - 16.0f / 116.0f) / 7.787f;
-    var_X = (powf(var_X, 3) > 0.008856f) ? powf(var_X, 3) : (var_X - 16.0f / 116.0f) / 7.787f;
-    var_Z = (powf(var_Z, 3) > 0.008856f) ? powf(var_Z, 3) : (var_Z - 16.0f / 116.0f) / 7.787f;
-
-    const auto X = 95.047f * var_X; //ref_X =  95.047     (Observer= 2 degrees, Illuminant= D65)
-    const auto Y = 100.000f * var_Y; //ref_Y = 100.000
-    const auto Z = 108.883f * var_Z; //ref_Z = 108.883
-
-    var_X = X / 100.0f; //X from 0 to  95.047      (Observer = 2 degrees, Illuminant = D65)
-    var_Y = Y / 100.0f; //Y from 0 to 100.000
-    var_Z = Z / 100.0f; //Z from 0 to 108.883
-
-    auto var_R = var_X * 3.2406f + var_Y * -1.5372f + var_Z * -0.4986f;
-    auto var_G = var_X * -0.9689f + var_Y * 1.8758f + var_Z * 0.0415f;
-    auto var_B = var_X * 0.0557f + var_Y * -0.2040f + var_Z * 1.0570f;
+    const auto referenceOklab = oklab::linear_srgb_to_oklab(colorrefToLinear(reference));
+    auto colorOklab = oklab::linear_srgb_to_oklab(colorrefToLinear(color));
+
+    // To determine whether the two colors are too close to each other we use the ΔEOK metric
+    // based on the Oklab color space. It's defined as the simple euclidean distance between.
+    auto dl = referenceOklab.l - colorOklab.l;
+    auto da = referenceOklab.a - colorOklab.a;
+    auto db = referenceOklab.b - colorOklab.b;
+    dl *= dl;
+    da *= da;
+    db *= db;
+
+    const auto distance = dl + da + db;
+    if (distance >= minSquaredDistance)
+    {
+        return color;
+    }
 
-    var_R = var_R > 0.0031308f ? 1.055f * powf(var_R, (1 / 2.4f)) - 0.055f : var_R = 12.92f * var_R;
-    var_G = var_G > 0.0031308f ? 1.055f * powf(var_G, (1 / 2.4f)) - 0.055f : var_G = 12.92f * var_G;
-    var_B = var_B > 0.0031308f ? 1.055f * powf(var_B, (1 / 2.4f)) - 0.055f : var_B = 12.92f * var_B;
+    // Thanks to ΔEOK being the euclidean distance we can immediately compute the
+    // minimum .l distance that's required for `distance` to be >= `minSquaredDistance`.
+    auto deltaL = sqrtf(minSquaredDistance - da - db);
 
-    r = (BYTE)std::clamp(var_R * 255.0f, 0.0f, 255.0f);
-    g = (BYTE)std::clamp(var_G * 255.0f, 0.0f, 255.0f);
-    b = (BYTE)std::clamp(var_B * 255.0f, 0.0f, 255.0f);
-}
+    // Try to retain the brightness relationship between `reference` and `color`.
+    // If `color` is darker than `reference`, we should first try to make it even darker.
+    if (colorOklab.l < referenceOklab.l)
+    {
+        deltaL = -deltaL;
+    }
 
-// Method Description:
-// - Given foreground and background colors, change the foreground color to
-//   make it more perceivable if necessary
-// - Arguments:
-// - fg: the foreground color
-// - bg: the background color
-// - Return Value:
-// - The foreground color after performing any necessary changes to make it more perceivable
-COLORREF ColorFix::GetPerceivableColor(COLORREF fg, COLORREF bg)
-{
-    const ColorFix backLab(bg);
-    ColorFix frontLab(fg);
-    const auto de1 = _GetDeltaE(frontLab, backLab);
-    if (de1 < gMinThreshold)
+    // This primitive way of adjusting the lightness will result in gamut clipping. That's really not good
+    // (it reduces the contrast significantly for some colors), but on the other hand, doing proper gamut
+    // mapping is annoying and expensive. I was unable to find an easy and cheap (!) algorithm that would
+    // change the chroma so that we're (mostly) back inside the gamut, but found none. I'm sure it's
+    // possible to come up with something, but I left that as a future improvement.
+    colorOklab.l = referenceOklab.l + deltaL;
+    if (colorOklab.l < 0 || colorOklab.l > 1)
     {
-        for (auto i = 0; i <= 1; i++)
-        {
-            const auto step = (i == 0) ? gLStep : -gLStep;
-            frontLab.L += step;
-
-            while (((i == 0) && (frontLab.L <= 100)) || ((i == 1) && (frontLab.L >= 0)))
-            {
-                const auto de2 = _GetDeltaE(frontLab, backLab);
-                if (de2 >= gExpThreshold)
-                {
-                    frontLab._ToRGB();
-                    return frontLab.rgb;
-                }
-                frontLab.L += step;
-            }
-        }
+        colorOklab.l = referenceOklab.l - deltaL;
     }
-    return frontLab.rgb;
+
+    return linearToColorref(oklab::oklab_to_linear_srgb(colorOklab)) | (color & 0xff000000);
 }
 
 TIL_FAST_MATH_END
diff --git a/src/types/inc/ColorFix.hpp b/src/types/inc/ColorFix.hpp
index a233d1268aa..02bdc0631b3 100644
--- a/src/types/inc/ColorFix.hpp
+++ b/src/types/inc/ColorFix.hpp
@@ -1,53 +1,13 @@
-/*++
-Copyright (c) Microsoft Corporation
-Licensed under the MIT license.
-
-Module Name:
-- ColorFix
-
-Abstract:
-- Implementation of perceptual color nudging, which allows the Terminal
-  to slightly shift the foreground color to make it more perceivable on
-  the current background (for cases where the foreground is very close
-  to being imperceivable on the background).
-
-Author(s):
-- Pankaj Bhojwani - Sep 2021
-
---*/
-
+// Copyright (c) Microsoft Corporation
+// Licensed under the MIT license.
+//
+// Implementation of perceptual color nudging, which allows the Terminal
+// to slightly shift the foreground color to make it more perceivable on
+// the current background (for cases where the foreground is very close
+// to being imperceivable on the background).
 #pragma once
 
-struct ColorFix
+namespace ColorFix
 {
-public:
-    ColorFix(COLORREF color) noexcept;
-
-    static COLORREF GetPerceivableColor(COLORREF fg, COLORREF bg);
-
-#pragma warning(push)
-    // CL will complain about the both nameless and anonymous struct.
-#pragma warning(disable : 4201)
-    // RGB
-    union
-    {
-        struct
-        {
-            BYTE r, g, b, dummy;
-        };
-        COLORREF rgb;
-    };
-
-    // Lab
-    struct
-    {
-        float L, A, B;
-    };
-#pragma warning(pop)
-
-private:
-    static float _GetHPrimeFn(float x, float y) noexcept;
-    static float _GetDeltaE(ColorFix x1, ColorFix x2) noexcept;
-    void _ToLab() noexcept;
-    void _ToRGB();
-};
+    COLORREF GetPerceivableColor(COLORREF color, COLORREF reference, float minSquaredDistance) noexcept;
+}


Test patch: