Repository: microsoft/terminal (language: cpp)

Issue:
Add support for Kitty/VTE extended underlines (4:x) [SGR]
_depends on #4321_

> 
> 
> Re underline:
> 
> The Kitty terminal emulator came up with the awesome idea of supporting [curly and colored underlines](https://sw.kovidgoyal.net/kitty/protocol-extensions.html#colored-and-styled-underlines), with the obvious intent of supporting user-friendly spell checking in terminal-based text editors. The choice of the escape sequence was coordinated between Kitty and VTE. The feature was then implemented so far at least in [Kitty](https://github.com/kovidgoyal/kitty/issues/226), [VTE](https://bugzilla.gnome.org/show_bug.cgi?id=721761), [Mintty](https://github.com/mintty/mintty/commit/7f12598a2cc2ded4fdb695fede237efe45785273), [Hterm](https://groups.google.com/a/chromium.org/forum/#!topic/chromium-hterm/ncWSu-hV7r8), probably a few more as well, and feature requests are filed to even more, including [iTerm2](https://gitlab.com/gnachman/iterm2/issues/6382), [Konsole](https://bugs.kde.org/show_bug.cgi?id=387811), [xterm.js](https://github.com/xtermjs/xterm.js/issues/1145). Some have even added dotted and dashed underlines, too.
> 
> It would be lovely if you also considered these extensions.
> 
> (A bit of technical info: With truecolor support I assume you already have like 25 bits for the foreground and background color each (in order to be able to store the 256 legacy palette values as well as "default", in addition to 24 bit RGB). At least this is how we do in VTE. And we didn't want to waste another 25 bits for this rarely used feature. So we approximate truecolor underline colors to 4+5+4 bits of R, G, B, respectively. This way all the color information of a charcell fits in an int64.)
> _from egmontkob in #2916_

> 
> 
> For what it's worth I've extended this a little, and my programs support the following subparameters:
> 
>   1. single
>   2. double
>   3. (short wavelength) curly
>   4. (closely spaced) dotted (8 dots per cell)
>   5. (short) dashed (4 eighth-width dashes per cell)
>   6. long dashed (2 quarter-width dashes per cell)
>   7. extra long dashed (1 half-width dash per cell)
>   8. medium spaced dotted (4 dots per cell)
>   9. widely spaced dotted (2 dots per cell)
>   10. long wavelength curly
>
> _from @jdebp in #2916_


Discussion / hints:
I added this to xterm.js last week, currently the sequences get ignored by conpty, we need them to pass through. https://github.com/xtermjs/xterm.js/pull/3921
I suspect even the premptive flushing of #13462 wouldn't fix this for conpty consumers either. We'd need to actually specifically note this category of sequence and then return false (or just, handle ourselves). Though, handling ourselves might be fairly engineering expensive (parsing, storing, re-rendering to conpty, rendering in dx/atlas), so maybe it does make sense to include the no-op passthrough for a smaller conpty package update. 
FYI, passthrough for sequences like this is not workable, even with flushing. Testing with a simple `printf` may briefly give the impression that it's working, but any reasonably complicated application is bound to fail eventually.

To understand why, have a look at the debug tab while scrolling through a file in vim. Notice how it appears to redraw the entire screen every time you scroll down the page? That's not vim doing the redrawing - that's conpty. Vim is just using something like a linefeed combined with scroll margins.

So what happens when vim decides to output some wavy underlines? They may appear to work the first time the screen is rendered, but as soon as you scroll, it'll be conpty that does the redrawing. And since conpty knows nothing of those attributes, it's just going to obliterate them.

Bottom line: Unless conpty stores and forwards the extended attributes, they're not going to work.
Yep, these need to be stored in conpty's model, as opposed to OSC 133 and the like which just need pass-through and don't need to be emitted again on redraw
Technically OSC 133 can break in a similar manner - it's just less likely in typical usage. The only way we can guarantee perfect fidelity with passthrough is if we're doing it for everything, with no conpty buffering at all (i.e. the passthrough mode of #1173). So our choices are either buffer everything and then rerender to conpty, or buffer nothing and pass everything through directly. Mixing the two is always going to be flaky.
Any status on this one?
Nope, but you can "Subscribe" for updates by clicking the "Subscribe" button on the right side of the screen! That will also help avoid sending e-mail to everyone who already has subscribed :smile:

Fix patch:
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index 4339b17eb04..edcd290f256 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -343,6 +343,7 @@ CTRLVOLUME
 Ctxt
 CUF
 cupxy
+curlyline
 CURRENTFONT
 currentmode
 CURRENTPAGE
@@ -578,6 +579,7 @@ elems
 emacs
 EMPTYBOX
 enabledelayedexpansion
+ENDCAP
 endptr
 endregion
 ENTIREBUFFER
@@ -826,6 +828,7 @@ hostlib
 HPA
 hpcon
 HPCON
+hpen
 hpj
 HPR
 HProvider
@@ -1010,6 +1013,7 @@ LOBYTE
 localappdata
 locsrc
 Loewen
+LOGBRUSH
 LOGFONT
 LOGFONTA
 LOGFONTW
diff --git a/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp b/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp
index 12223e94a63..1637686fd0e 100644
--- a/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp
+++ b/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp
@@ -57,7 +57,7 @@ namespace
         HRESULT InvalidateCircling(_Out_ bool* /*pForcePaint*/) noexcept { return S_OK; }
         HRESULT PaintBackground() noexcept { return S_OK; }
         HRESULT PaintBufferLine(std::span<const Cluster> /*clusters*/, til::point /*coord*/, bool /*fTrimLeft*/, bool /*lineWrapped*/) noexcept { return S_OK; }
-        HRESULT PaintBufferGridLines(GridLineSet /*lines*/, COLORREF /*color*/, size_t /*cchLine*/, til::point /*coordTarget*/) noexcept { return S_OK; }
+        HRESULT PaintBufferGridLines(GridLineSet /*lines*/, COLORREF /*gridlineColor*/, COLORREF /*underlineColor*/, size_t /*cchLine*/, til::point /*coordTarget*/) noexcept { return S_OK; }
         HRESULT PaintSelection(const til::rect& /*rect*/) noexcept { return S_OK; }
         HRESULT PaintCursor(const CursorOptions& /*options*/) noexcept { return S_OK; }
         HRESULT UpdateDrawingBrushes(const TextAttribute& /*textAttributes*/, const RenderSettings& /*renderSettings*/, gsl::not_null<IRenderData*> /*pData*/, bool /*usingSoftFont*/, bool /*isSettingDefaultBrushes*/) noexcept { return S_OK; }
diff --git a/src/interactivity/onecore/BgfxEngine.cpp b/src/interactivity/onecore/BgfxEngine.cpp
index ce6b903ffd1..d508603d5f9 100644
--- a/src/interactivity/onecore/BgfxEngine.cpp
+++ b/src/interactivity/onecore/BgfxEngine.cpp
@@ -147,9 +147,10 @@ CATCH_RETURN()
     CATCH_RETURN()
 }
 
-[[nodiscard]] HRESULT BgfxEngine::PaintBufferGridLines(GridLineSet const /*lines*/,
-                                                       COLORREF const /*color*/,
-                                                       size_t const /*cchLine*/,
+[[nodiscard]] HRESULT BgfxEngine::PaintBufferGridLines(const GridLineSet /*lines*/,
+                                                       const COLORREF /*gridlineColor*/,
+                                                       const COLORREF /*underlineColor*/,
+                                                       const size_t /*cchLine*/,
                                                        const til::point /*coordTarget*/) noexcept
 {
     return S_OK;
diff --git a/src/interactivity/onecore/BgfxEngine.hpp b/src/interactivity/onecore/BgfxEngine.hpp
index 31fa49c8522..e9759ce0306 100644
--- a/src/interactivity/onecore/BgfxEngine.hpp
+++ b/src/interactivity/onecore/BgfxEngine.hpp
@@ -51,7 +51,7 @@ namespace Microsoft::Console::Render
                                               const til::point coord,
                                               const bool trimLeft,
                                               const bool lineWrapped) noexcept override;
-        [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet const lines, COLORREF const color, size_t const cchLine, til::point const coordTarget) noexcept override;
+        [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override;
         [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override;
 
         [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override;
diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp
index e46bb250835..7438b93543e 100644
--- a/src/renderer/atlas/AtlasEngine.cpp
+++ b/src/renderer/atlas/AtlasEngine.cpp
@@ -375,7 +375,7 @@ try
 }
 CATCH_RETURN()
 
-[[nodiscard]] HRESULT AtlasEngine::PaintBufferGridLines(const GridLineSet lines, const COLORREF color, const size_t cchLine, const til::point coordTarget) noexcept
+[[nodiscard]] HRESULT AtlasEngine::PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept
 try
 {
     const auto shift = gsl::narrow_cast<u8>(_api.lineRendition != LineRendition::SingleWidth);
@@ -383,8 +383,9 @@ try
     const auto y = gsl::narrow_cast<u16>(clamp<til::CoordType>(coordTarget.y, 0, _p.s->viewportCellCount.y));
     const auto from = gsl::narrow_cast<u16>(clamp<til::CoordType>(x << shift, 0, _p.s->viewportCellCount.x - 1));
     const auto to = gsl::narrow_cast<u16>(clamp<size_t>((x + cchLine) << shift, from, _p.s->viewportCellCount.x));
-    const auto fg = gsl::narrow_cast<u32>(color) | 0xff000000;
-    _p.rows[y]->gridLineRanges.emplace_back(lines, fg, from, to);
+    const auto glColor = gsl::narrow_cast<u32>(gridlineColor) | 0xff000000;
+    const auto ulColor = gsl::narrow_cast<u32>(underlineColor) | 0xff000000;
+    _p.rows[y]->gridLineRanges.emplace_back(lines, glColor, ulColor, from, to);
     return S_OK;
 }
 CATCH_RETURN()
diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h
index b135a8989f8..4ec09a9e931 100644
--- a/src/renderer/atlas/AtlasEngine.h
+++ b/src/renderer/atlas/AtlasEngine.h
@@ -43,7 +43,7 @@ namespace Microsoft::Console::Render::Atlas
         [[nodiscard]] HRESULT PrepareLineTransform(LineRendition lineRendition, til::CoordType targetRow, til::CoordType viewportLeft) noexcept override;
         [[nodiscard]] HRESULT PaintBackground() noexcept override;
         [[nodiscard]] HRESULT PaintBufferLine(std::span<const Cluster> clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept override;
-        [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF color, size_t cchLine, til::point coordTarget) noexcept override;
+        [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override;
         [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override;
         [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override;
         [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, gsl::not_null<IRenderData*> pData, bool usingSoftFont, bool isSettingDefaultBrushes) noexcept override;
diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp
index 301738954aa..ea4ea0923eb 100644
--- a/src/renderer/atlas/BackendD2D.cpp
+++ b/src/renderer/atlas/BackendD2D.cpp
@@ -409,7 +409,7 @@ void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* ro
 
         D2D1_POINT_2F point0{ 0, static_cast<f32>(textCellCenter) };
         D2D1_POINT_2F point1{ 0, static_cast<f32>(textCellCenter + cellSize.y) };
-        const auto brush = _brushWithColor(r.color);
+        const auto brush = _brushWithColor(r.gridlineColor);
         const f32 w = pos.height;
         const f32 hw = w * 0.5f;
 
@@ -421,11 +421,11 @@ void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* ro
             _renderTarget->DrawLine(point0, point1, brush, w, nullptr);
         }
     };
-    const auto appendHorizontalLine = [&](const GridLineRange& r, FontDecorationPosition pos, ID2D1StrokeStyle* strokeStyle) {
+    const auto appendHorizontalLine = [&](const GridLineRange& r, FontDecorationPosition pos, ID2D1StrokeStyle* strokeStyle, const u32 color) {
         const auto from = r.from >> widthShift;
         const auto to = r.to >> widthShift;
 
-        const auto brush = _brushWithColor(r.color);
+        const auto brush = _brushWithColor(color);
         const f32 w = pos.height;
         const f32 centerY = textCellCenter + pos.position + w * 0.5f;
         const D2D1_POINT_2F point0{ static_cast<f32>(from * cellSize.x), centerY };
@@ -448,32 +448,32 @@ void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* ro
         }
         if (r.lines.test(GridLines::Top))
         {
-            appendHorizontalLine(r, p.s->font->gridTop, nullptr);
+            appendHorizontalLine(r, p.s->font->gridTop, nullptr, r.gridlineColor);
         }
         if (r.lines.test(GridLines::Bottom))
         {
-            appendHorizontalLine(r, p.s->font->gridBottom, nullptr);
+            appendHorizontalLine(r, p.s->font->gridBottom, nullptr, r.gridlineColor);
+        }
+        if (r.lines.test(GridLines::Strikethrough))
+        {
+            appendHorizontalLine(r, p.s->font->strikethrough, nullptr, r.gridlineColor);
         }
 
         if (r.lines.test(GridLines::Underline))
         {
-            appendHorizontalLine(r, p.s->font->underline, nullptr);
+            appendHorizontalLine(r, p.s->font->underline, nullptr, r.underlineColor);
         }
-        if (r.lines.test(GridLines::HyperlinkUnderline))
+        else if (r.lines.any(GridLines::DottedUnderline, GridLines::HyperlinkUnderline))
         {
-            appendHorizontalLine(r, p.s->font->underline, _dottedStrokeStyle.get());
+            appendHorizontalLine(r, p.s->font->underline, _dottedStrokeStyle.get(), r.underlineColor);
         }
-        if (r.lines.test(GridLines::DoubleUnderline))
+        else if (r.lines.test(GridLines::DoubleUnderline))
         {
             for (const auto pos : p.s->font->doubleUnderline)
             {
-                appendHorizontalLine(r, pos, nullptr);
+                appendHorizontalLine(r, pos, nullptr, r.underlineColor);
             }
         }
-        if (r.lines.test(GridLines::Strikethrough))
-        {
-            appendHorizontalLine(r, p.s->font->strikethrough, nullptr);
-        }
     }
 }
 
diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp
index 846567ca613..ce630dda716 100644
--- a/src/renderer/atlas/BackendD3D.cpp
+++ b/src/renderer/atlas/BackendD3D.cpp
@@ -90,7 +90,8 @@ BackendD3D::BackendD3D(const RenderingPayload& p)
     {
         static constexpr D3D11_INPUT_ELEMENT_DESC layout[]{
             { "SV_Position", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
-            { "shadingType", 0, DXGI_FORMAT_R32_UINT, 1, offsetof(QuadInstance, shadingType), D3D11_INPUT_PER_INSTANCE_DATA, 1 },
+            { "shadingType", 0, DXGI_FORMAT_R16_UINT, 1, offsetof(QuadInstance, shadingType), D3D11_INPUT_PER_INSTANCE_DATA, 1 },
+            { "renditionScale", 0, DXGI_FORMAT_R8G8_UINT, 1, offsetof(QuadInstance, renditionScale), D3D11_INPUT_PER_INSTANCE_DATA, 1 },
             { "position", 0, DXGI_FORMAT_R16G16_SINT, 1, offsetof(QuadInstance, position), D3D11_INPUT_PER_INSTANCE_DATA, 1 },
             { "size", 0, DXGI_FORMAT_R16G16_UINT, 1, offsetof(QuadInstance, size), D3D11_INPUT_PER_INSTANCE_DATA, 1 },
             { "texcoord", 0, DXGI_FORMAT_R16G16_UINT, 1, offsetof(QuadInstance, texcoord), D3D11_INPUT_PER_INSTANCE_DATA, 1 },
@@ -305,6 +306,37 @@ void BackendD3D::_updateFontDependents(const RenderingPayload& p)
 {
     const auto& font = *p.s->font;
 
+    // The max height of Curly line peak in `em` units.
+    const auto maxCurlyLinePeakHeightEm = 0.075f;
+    // We aim for atleast 1px height, but since we draw 1px smaller curly line,
+    // we aim for 2px height as a result.
+    const auto minCurlyLinePeakHeight = 2.0f;
+
+    // Curlyline uses the gap between cell bottom and singly underline position
+    // as the height of the wave's peak. The baseline for curly-line is at the
+    // middle of singly underline. The gap could be too big, so we also apply
+    // a limit on the peak height.
+    const auto strokeHalfWidth = font.underline.height / 2.0f;
+    const auto underlineMidY = font.underline.position + strokeHalfWidth;
+    const auto cellBottomGap = font.cellSize.y - underlineMidY - strokeHalfWidth;
+    const auto maxCurlyLinePeakHeight = maxCurlyLinePeakHeightEm * font.fontSize;
+    auto curlyLinePeakHeight = std::min(cellBottomGap, maxCurlyLinePeakHeight);
+
+    // When it's too small to be curly, make it straight.
+    if (curlyLinePeakHeight < minCurlyLinePeakHeight)
+    {
+        curlyLinePeakHeight = 0;
+    }
+
+    // We draw a smaller curly line (-1px) to avoid clipping due to the rounding.
+    _curlyLineDrawPeakHeight = std::max(0.0f, curlyLinePeakHeight - 1.0f);
+
+    const auto curlyUnderlinePos = font.underline.position - curlyLinePeakHeight;
+    const auto curlyUnderlineWidth = 2.0f * (curlyLinePeakHeight + strokeHalfWidth);
+    const auto curlyUnderlinePosU16 = gsl::narrow_cast<u16>(lrintf(curlyUnderlinePos));
+    const auto curlyUnderlineWidthU16 = gsl::narrow_cast<u16>(lrintf(curlyUnderlineWidth));
+    _curlyUnderline = { curlyUnderlinePosU16, curlyUnderlineWidthU16 };
+
     DWrite_GetRenderParams(p.dwriteFactory.get(), &_gamma, &_cleartypeEnhancedContrast, &_grayscaleEnhancedContrast, _textRenderingParams.put());
     // Clearing the atlas requires BeginDraw(), which is expensive. Defer this until we need Direct2D anyways.
     _fontChangedResetGlyphAtlas = true;
@@ -539,6 +571,9 @@ void BackendD3D::_recreateConstBuffer(const RenderingPayload& p) const
         DWrite_GetGammaRatios(_gamma, data.gammaRatios);
         data.enhancedContrast = p.s->font->antialiasingMode == AntialiasingMode::ClearType ? _cleartypeEnhancedContrast : _grayscaleEnhancedContrast;
         data.underlineWidth = p.s->font->underline.height;
+        data.curlyLineWaveFreq = 2.0f * 3.14f / p.s->font->cellSize.x;
+        data.curlyLinePeakHeight = _curlyLineDrawPeakHeight;
+        data.curlyLineCellOffset = p.s->font->underline.position + p.s->font->underline.height / 2.0f;
         p.deviceContext->UpdateSubresource(_psConstantBuffer.get(), 0, nullptr, &data, 0, 0);
     }
 }
@@ -1020,7 +1055,7 @@ void BackendD3D::_drawText(RenderingPayload& p)
                     goto drawGlyphRetry;
                 }
 
-                if (glyphEntry.data.GetShadingType() != ShadingType::Default)
+                if (glyphEntry.data.shadingType != ShadingType::Default)
                 {
                     auto l = static_cast<til::CoordType>(lrintf((baselineX + row->glyphOffsets[x].advanceOffset) * scaleX));
                     auto t = static_cast<til::CoordType>(lrintf((baselineY - row->glyphOffsets[x].ascenderOffset) * scaleY));
@@ -1032,7 +1067,7 @@ void BackendD3D::_drawText(RenderingPayload& p)
                     row->dirtyBottom = std::max(row->dirtyBottom, t + glyphEntry.data.size.y);
 
                     _appendQuad() = {
-                        .shadingType = glyphEntry.data.GetShadingType(),
+                        .shadingType = glyphEntry.data.shadingType,
                         .position = { static_cast<i16>(l), static_cast<i16>(t) },
                         .size = glyphEntry.data.size,
                         .texcoord = glyphEntry.data.texcoord,
@@ -1454,7 +1489,7 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryI
     const auto triggerRight = _ligatureOverhangTriggerRight * horizontalScale;
     const auto overlapSplit = rect.w >= p.s->font->cellSize.x && (bl <= triggerLeft || br >= triggerRight);
 
-    glyphEntry.data.shadingType = static_cast<u16>(isColorGlyph ? ShadingType::TextPassthrough : _textShadingType);
+    glyphEntry.data.shadingType = isColorGlyph ? ShadingType::TextPassthrough : _textShadingType;
     glyphEntry.data.overlapSplit = overlapSplit;
     glyphEntry.data.offset.x = bl;
     glyphEntry.data.offset.y = bt;
@@ -1523,7 +1558,7 @@ bool BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFa
     _drawSoftFontGlyphInBitmap(p, glyphEntry);
     _d2dRenderTarget->DrawBitmap(_softFontBitmap.get(), &dest, 1, interpolation, nullptr, nullptr);
 
-    glyphEntry.data.shadingType = static_cast<u16>(ShadingType::TextGrayscale);
+    glyphEntry.data.shadingType = ShadingType::TextGrayscale;
     glyphEntry.data.overlapSplit = 0;
     glyphEntry.data.offset.x = 0;
     glyphEntry.data.offset.y = -baseline;
@@ -1627,11 +1662,11 @@ void BackendD3D::_splitDoubleHeightGlyph(const RenderingPayload& p, const AtlasF
     // double-height row. This effectively turns the other (unneeded) side into whitespace.
     if (!top.data.size.y)
     {
-        top.data.shadingType = static_cast<u16>(ShadingType::Default);
+        top.data.shadingType = ShadingType::Default;
     }
     if (!bottom.data.size.y)
     {
-        bottom.data.shadingType = static_cast<u16>(ShadingType::Default);
+        bottom.data.shadingType = ShadingType::Default;
     }
 }
 
@@ -1643,8 +1678,6 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y)
     const auto verticalShift = static_cast<u8>(row->lineRendition >= LineRendition::DoubleHeightTop);
 
     const auto cellSize = p.s->font->cellSize;
-    const auto dottedLineType = horizontalShift ? ShadingType::DottedLineWide : ShadingType::DottedLine;
-
     const auto rowTop = static_cast<i16>(cellSize.y * y);
     const auto rowBottom = static_cast<i16>(rowTop + cellSize.y);
 
@@ -1671,11 +1704,11 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y)
                 .shadingType = ShadingType::SolidLine,
                 .position = { static_cast<i16>(posX), rowTop },
                 .size = { width, p.s->font->cellSize.y },
-                .color = r.color,
+                .color = r.gridlineColor,
             };
         }
     };
-    const auto appendHorizontalLine = [&](const GridLineRange& r, FontDecorationPosition pos, ShadingType shadingType) {
+    const auto appendHorizontalLine = [&](const GridLineRange& r, FontDecorationPosition pos, ShadingType shadingType, const u32 color) {
         const auto offset = pos.position << verticalShift;
         const auto height = static_cast<u16>(pos.height << verticalShift);
 
@@ -1691,9 +1724,10 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y)
         {
             _appendQuad() = {
                 .shadingType = shadingType,
+                .renditionScale = { static_cast<u8>(1 << horizontalShift), static_cast<u8>(1 << verticalShift) },
                 .position = { left, static_cast<i16>(rt) },
                 .size = { width, static_cast<u16>(rb - rt) },
-                .color = r.color,
+                .color = color,
             };
         }
     };
@@ -1713,32 +1747,40 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y)
         }
         if (r.lines.test(GridLines::Top))
         {
-            appendHorizontalLine(r, p.s->font->gridTop, ShadingType::SolidLine);
+            appendHorizontalLine(r, p.s->font->gridTop, ShadingType::SolidLine, r.gridlineColor);
         }
         if (r.lines.test(GridLines::Bottom))
         {
-            appendHorizontalLine(r, p.s->font->gridBottom, ShadingType::SolidLine);
+            appendHorizontalLine(r, p.s->font->gridBottom, ShadingType::SolidLine, r.gridlineColor);
+        }
+        if (r.lines.test(GridLines::Strikethrough))
+        {
+            appendHorizontalLine(r, p.s->font->strikethrough, ShadingType::SolidLine, r.gridlineColor);
         }
 
         if (r.lines.test(GridLines::Underline))
         {
-            appendHorizontalLine(r, p.s->font->underline, ShadingType::SolidLine);
+            appendHorizontalLine(r, p.s->font->underline, ShadingType::SolidLine, r.underlineColor);
         }
-        if (r.lines.test(GridLines::HyperlinkUnderline))
+        else if (r.lines.any(GridLines::DottedUnderline, GridLines::HyperlinkUnderline))
         {
-            appendHorizontalLine(r, p.s->font->underline, dottedLineType);
+            appendHorizontalLine(r, p.s->font->underline, ShadingType::DottedLine, r.underlineColor);
         }
-        if (r.lines.test(GridLines::DoubleUnderline))
+        else if (r.lines.test(GridLines::DashedUnderline))
+        {
+            appendHorizontalLine(r, p.s->font->underline, ShadingType::DashedLine, r.underlineColor);
+        }
+        else if (r.lines.test(GridLines::CurlyUnderline))
+        {
+            appendHorizontalLine(r, _curlyUnderline, ShadingType::CurlyLine, r.underlineColor);
+        }
+        else if (r.lines.test(GridLines::DoubleUnderline))
         {
             for (const auto pos : p.s->font->doubleUnderline)
             {
-                appendHorizontalLine(r, pos, ShadingType::SolidLine);
+                appendHorizontalLine(r, pos, ShadingType::SolidLine, r.underlineColor);
             }
         }
-        if (r.lines.test(GridLines::Strikethrough))
-        {
-            appendHorizontalLine(r, p.s->font->strikethrough, ShadingType::SolidLine);
-        }
     }
 }
 
@@ -2038,6 +2080,8 @@ size_t BackendD3D::_drawCursorForegroundSlowPath(const CursorRect& c, size_t off
         auto& target = _instances[offset + i];
 
         target.shadingType = it.shadingType;
+        target.renditionScale.x = it.renditionScale.x;
+        target.renditionScale.y = it.renditionScale.y;
         target.position.x = static_cast<i16>(cutout.left);
         target.position.y = static_cast<i16>(cutout.top);
         target.size.x = static_cast<u16>(cutout.right - cutout.left);
@@ -2055,6 +2099,8 @@ size_t BackendD3D::_drawCursorForegroundSlowPath(const CursorRect& c, size_t off
     auto& target = cutoutCount ? _appendQuad() : _instances[offset];
 
     target.shadingType = it.shadingType;
+    target.renditionScale.x = it.renditionScale.x;
+    target.renditionScale.y = it.renditionScale.y;
     target.position.x = static_cast<i16>(intersectionL);
     target.position.y = static_cast<i16>(intersectionT);
     target.size.x = static_cast<u16>(intersectionR - intersectionL);
diff --git a/src/renderer/atlas/BackendD3D.h b/src/renderer/atlas/BackendD3D.h
index 14f4ca2943e..0befe8fe342 100644
--- a/src/renderer/atlas/BackendD3D.h
+++ b/src/renderer/atlas/BackendD3D.h
@@ -42,6 +42,9 @@ namespace Microsoft::Console::Render::Atlas
             alignas(sizeof(f32x4)) f32 gammaRatios[4]{};
             alignas(sizeof(f32)) f32 enhancedContrast = 0;
             alignas(sizeof(f32)) f32 underlineWidth = 0;
+            alignas(sizeof(f32)) f32 curlyLinePeakHeight = 0;
+            alignas(sizeof(f32)) f32 curlyLineWaveFreq = 0;
+            alignas(sizeof(f32)) f32 curlyLineCellOffset = 0;
 #pragma warning(suppress : 4324) // 'PSConstBuffer': structure was padded due to alignment specifier
         };
 
@@ -55,7 +58,7 @@ namespace Microsoft::Console::Render::Atlas
 #pragma warning(suppress : 4324) // 'CustomConstBuffer': structure was padded due to alignment specifier
         };
 
-        enum class ShadingType : u32
+        enum class ShadingType : u16
         {
             Default = 0,
             Background = 0,
@@ -66,12 +69,13 @@ namespace Microsoft::Console::Render::Atlas
             TextClearType = 2,
             TextPassthrough = 3,
             DottedLine = 4,
-            DottedLineWide = 5,
+            DashedLine = 5,
+            CurlyLine = 6,
             // All items starting here will be drawing as a solid RGBA color
-            SolidLine = 6,
+            SolidLine = 7,
 
-            Cursor = 7,
-            Selection = 8,
+            Cursor = 8,
+            Selection = 9,
 
             TextDrawingFirst = TextGrayscale,
             TextDrawingLast = SolidLine,
@@ -86,7 +90,8 @@ namespace Microsoft::Console::Render::Atlas
             // impact on performance and power draw. If (when?) displays with >32k resolution make their
             // appearance in the future, this should be changed to f32x2. But if you do so, please change
             // all other occurrences of i16x2 positions/offsets throughout the class to keep it consistent.
-            alignas(u32) ShadingType shadingType;
+            alignas(u16) ShadingType shadingType;
+            alignas(u16) u8x2 renditionScale;
             alignas(u32) i16x2 position;
             alignas(u32) u16x2 size;
             alignas(u32) u16x2 texcoord;
@@ -95,16 +100,11 @@ namespace Microsoft::Console::Render::Atlas
 
         struct alignas(u32) AtlasGlyphEntryData
         {
-            u16 shadingType;
+            ShadingType shadingType;
             u16 overlapSplit;
             i16x2 offset;
             u16x2 size;
             u16x2 texcoord;
-
-            constexpr ShadingType GetShadingType() const noexcept
-            {
-                return static_cast<ShadingType>(shadingType);
-            }
         };
 
         // NOTE: Don't initialize any members in this struct. This ensures that no
@@ -291,6 +291,9 @@ namespace Microsoft::Console::Render::Atlas
         // The bounding rect of _cursorRects in pixels.
         til::rect _cursorPosition;
 
+        f32 _curlyLineDrawPeakHeight = 0;
+        FontDecorationPosition _curlyUnderline;
+
         bool _requiresContinuousRedraw = false;
 
 #if ATLAS_DEBUG_SHOW_DIRTY
diff --git a/src/renderer/atlas/common.h b/src/renderer/atlas/common.h
index bb46ff61547..7d3d0b982ed 100644
--- a/src/renderer/atlas/common.h
+++ b/src/renderer/atlas/common.h
@@ -125,6 +125,7 @@ namespace Microsoft::Console::Render::Atlas
     };
 
     using u8 = uint8_t;
+    using u8x2 = vec2<u8>;
 
     using u16 = uint16_t;
     using u16x2 = vec2<u16>;
@@ -426,7 +427,8 @@ namespace Microsoft::Console::Render::Atlas
     struct GridLineRange
     {
         GridLineSet lines;
-        u32 color = 0;
+        u32 gridlineColor = 0;
+        u32 underlineColor = 0;
         u16 from = 0;
         u16 to = 0;
     };
diff --git a/src/renderer/atlas/shader_common.hlsl b/src/renderer/atlas/shader_common.hlsl
index 957b17ac6ad..d712f081f28 100644
--- a/src/renderer/atlas/shader_common.hlsl
+++ b/src/renderer/atlas/shader_common.hlsl
@@ -7,13 +7,15 @@
 #define SHADING_TYPE_TEXT_CLEARTYPE     2
 #define SHADING_TYPE_TEXT_PASSTHROUGH   3
 #define SHADING_TYPE_DOTTED_LINE        4
-#define SHADING_TYPE_DOTTED_LINE_WIDE   5
+#define SHADING_TYPE_DASHED_LINE        5
+#define SHADING_TYPE_CURLY_LINE         6
 // clang-format on
 
 struct VSData
 {
     float2 vertex : SV_Position;
     uint shadingType : shadingType;
+    uint2 renditionScale : renditionScale;
     int2 position : position;
     uint2 size : size;
     uint2 texcoord : texcoord;
@@ -25,6 +27,7 @@ struct PSData
     float4 position : SV_Position;
     float2 texcoord : texcoord;
     nointerpolation uint shadingType : shadingType;
+    nointerpolation uint2 renditionScale : renditionScale;
     nointerpolation float4 color : color;
 };
 
diff --git a/src/renderer/atlas/shader_ps.hlsl b/src/renderer/atlas/shader_ps.hlsl
index 2cb92947ce2..e19ba955fe5 100644
--- a/src/renderer/atlas/shader_ps.hlsl
+++ b/src/renderer/atlas/shader_ps.hlsl
@@ -12,6 +12,9 @@ cbuffer ConstBuffer : register(b0)
     float4 gammaRatios;
     float enhancedContrast;
     float underlineWidth;
+    float curlyLinePeakHeight;
+    float curlyLineWaveFreq;
+    float curlyLineCellOffset;
 }
 
 Texture2D<float4> background : register(t0);
@@ -73,18 +76,36 @@ Output main(PSData data) : SV_Target
     }
     case SHADING_TYPE_DOTTED_LINE:
     {
-        const bool on = frac(data.position.x / (2.0f * underlineWidth)) < 0.5f;
+        const bool on = frac(data.position.x / (2.0f * underlineWidth * data.renditionScale.x)) < 0.5f;
         color = on * premultiplyColor(data.color);
         weights = color.aaaa;
         break;
     }
-    case SHADING_TYPE_DOTTED_LINE_WIDE:
+    case SHADING_TYPE_DASHED_LINE:
     {
-        const bool on = frac(data.position.x / (4.0f * underlineWidth)) < 0.5f;
+        const bool on = frac(data.position.x / (backgroundCellSize.x * data.renditionScale.x)) < 0.5f;
         color = on * premultiplyColor(data.color);
         weights = color.aaaa;
         break;
     }
+    case SHADING_TYPE_CURLY_LINE:
+    {
+        uint cellRow = floor(data.position.y / backgroundCellSize.y);
+        // Use the previous cell when drawing 'Double Height' curly line.
+        cellRow -= data.renditionScale.y - 1;
+        const float cellTop = cellRow * backgroundCellSize.y;
+        const float centerY = cellTop + curlyLineCellOffset * data.renditionScale.y;
+        const float strokeWidthHalf = underlineWidth * data.renditionScale.y / 2.0f;
+        const float amp = curlyLinePeakHeight * data.renditionScale.y;
+        const float freq = curlyLineWaveFreq / data.renditionScale.x;
+
+        const float s = sin(data.position.x * freq);
+        const float d = abs(centerY - (s * amp) - data.position.y);
+        const float a = 1 - saturate(d - strokeWidthHalf);
+        color = a * premultiplyColor(data.color);
+        weights = color.aaaa;
+        break;
+    }
     default:
     {
         color = premultiplyColor(data.color);
diff --git a/src/renderer/atlas/shader_vs.hlsl b/src/renderer/atlas/shader_vs.hlsl
index 49b9030b156..eb96fcf0e45 100644
--- a/src/renderer/atlas/shader_vs.hlsl
+++ b/src/renderer/atlas/shader_vs.hlsl
@@ -15,6 +15,7 @@ PSData main(VSData data)
     PSData output;
     output.color = data.color;
     output.shadingType = data.shadingType;
+    output.renditionScale = data.renditionScale;
     // positionScale is expected to be float2(2.0f / sizeInPixel.x, -2.0f / sizeInPixel.y). Together with the
     // addition below this will transform our "position" from pixel into normalized device coordinate (NDC) space.
     output.position.xy = (data.position + data.vertex.xy * data.size) * positionScale + float2(-1.0f, 1.0f);
diff --git a/src/renderer/base/RenderSettings.cpp b/src/renderer/base/RenderSettings.cpp
index 03c4795635a..f885957796a 100644
--- a/src/renderer/base/RenderSettings.cpp
+++ b/src/renderer/base/RenderSettings.cpp
@@ -212,6 +212,47 @@ std::pair<COLORREF, COLORREF> RenderSettings::GetAttributeColorsWithAlpha(const
     return { fg, bg };
 }
 
+// Routine Description:
+// - Calculates the RGB underline color of a given text attribute, using the
+//   current color table configuration and active render settings.
+// - Returns the current foreground color when the underline color isn't set.
+// Arguments:
+// - attr - The TextAttribute to retrieve the underline color from.
+// Return Value:
+// - The color value of the attribute's underline.
+COLORREF RenderSettings::GetAttributeUnderlineColor(const TextAttribute& attr) const noexcept
+{
+    const auto [fg, bg] = GetAttributeColors(attr);
+    const auto ulTextColor = attr.GetUnderlineColor();
+    if (ulTextColor.IsDefault())
+    {
+        return fg;
+    }
+
+    const auto defaultUlIndex = GetColorAliasIndex(ColorAlias::DefaultForeground);
+    auto ul = ulTextColor.GetColor(_colorTable, defaultUlIndex, true);
+    if (attr.IsInvisible())
+    {
+        ul = bg;
+    }
+
+    // We intentionally aren't _only_ checking for attr.IsInvisible here, because we also want to
+    // catch the cases where the ul was intentionally set to be the same as the bg. In either case,
+    // don't adjust the underline color.
+    if constexpr (Feature_AdjustIndistinguishableText::IsEnabled())
+    {
+        if (
+            ul != bg &&
+            (_renderMode.test(Mode::AlwaysDistinguishableColors) ||
+             (_renderMode.test(Mode::IndexedDistinguishableColors) && ulTextColor.IsDefaultOrLegacy() && attr.GetBackground().IsDefaultOrLegacy())))
+        {
+            ul = ColorFix::GetPerceivableColor(ul, bg, 0.5f * 0.5f);
+        }
+    }
+
+    return ul;
+}
+
 // Routine Description:
 // - Increments the position in the blink cycle, toggling the blink rendition
 //   state on every second call, potentially triggering a redraw of the given
diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp
index 917c5ba05f9..2b328ee07f9 100644
--- a/src/renderer/base/renderer.cpp
+++ b/src/renderer/base/renderer.cpp
@@ -955,13 +955,21 @@ GridLineSet Renderer::s_GetGridlines(const TextAttribute& textAttribute) noexcep
     {
     case UnderlineStyle::NoUnderline:
         break;
+    case UnderlineStyle::SinglyUnderlined:
+        lines.set(GridLines::Underline);
+        break;
     case UnderlineStyle::DoublyUnderlined:
         lines.set(GridLines::DoubleUnderline);
         break;
-    case UnderlineStyle::SinglyUnderlined:
     case UnderlineStyle::CurlyUnderlined:
+        lines.set(GridLines::CurlyUnderline);
+        break;
     case UnderlineStyle::DottedUnderlined:
+        lines.set(GridLines::DottedUnderline);
+        break;
     case UnderlineStyle::DashedUnderlined:
+        lines.set(GridLines::DashedUnderline);
+        break;
     default:
         lines.set(GridLines::Underline);
         break;
@@ -1002,10 +1010,11 @@ void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngin
     // Return early if there are no lines to paint.
     if (lines.any())
     {
-        // Get the current foreground color to render the lines.
-        const auto rgb = _renderSettings.GetAttributeColors(textAttribute).first;
+        // Get the current foreground and underline colors to render the lines.
+        const auto fg = _renderSettings.GetAttributeColors(textAttribute).first;
+        const auto underlineColor = _renderSettings.GetAttributeUnderlineColor(textAttribute);
         // Draw the lines
-        LOG_IF_FAILED(pEngine->PaintBufferGridLines(lines, rgb, cchLine, coordTarget));
+        LOG_IF_FAILED(pEngine->PaintBufferGridLines(lines, fg, underlineColor, cchLine, coordTarget));
     }
 }
 
diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp
index f74bf528dc1..064d219f72a 100644
--- a/src/renderer/dx/DxRenderer.cpp
+++ b/src/renderer/dx/DxRenderer.cpp
@@ -1696,14 +1696,16 @@ CATCH_RETURN()
 // - Paints lines around cells (draws in pieces of the grid)
 // Arguments:
 // - lines - Which grid lines (top, left, bottom, right) to draw
-// - color - The color to use for drawing the lines
+// - gridlineColor - The color to use for drawing the gridlines
+// - underlineColor - The color to use for drawing the underlines
 // - cchLine - Length of the line to draw in character cells
 // - coordTarget - The X,Y character position in the grid where we should start drawing
 //               - We will draw rightward (+X) from here
 // Return Value:
 // - S_OK or relevant DirectX error
 [[nodiscard]] HRESULT DxEngine::PaintBufferGridLines(const GridLineSet lines,
-                                                     COLORREF const color,
+                                                     const COLORREF gridlineColor,
+                                                     const COLORREF underlineColor,
                                                      const size_t cchLine,
                                                      const til::point coordTarget) noexcept
 try
@@ -1711,8 +1713,6 @@ try
     const auto existingColor = _d2dBrushForeground->GetColor();
     const auto restoreBrushOnExit = wil::scope_exit([&]() noexcept { _d2dBrushForeground->SetColor(existingColor); });
 
-    _d2dBrushForeground->SetColor(_ColorFFromColorRef(color | 0xff000000));
-
     const auto font = _fontRenderData->GlyphCell().to_d2d_size();
     const D2D_POINT_2F target = { coordTarget.x * font.width, coordTarget.y * font.height };
     const auto fullRunWidth = font.width * gsl::narrow_cast<unsigned>(cchLine);
@@ -1721,10 +1721,12 @@ try
         _d2dDeviceContext->DrawLine({ x0, y0 }, { x1, y1 }, _d2dBrushForeground.Get(), strokeWidth, _strokeStyle.Get());
     };
 
-    const auto DrawHyperlinkLine = [=](const auto x0, const auto y0, const auto x1, const auto y1, const auto strokeWidth) noexcept {
+    const auto DrawDottedLine = [=](const auto x0, const auto y0, const auto x1, const auto y1, const auto strokeWidth) noexcept {
         _d2dDeviceContext->DrawLine({ x0, y0 }, { x1, y1 }, _d2dBrushForeground.Get(), strokeWidth, _dashStrokeStyle.Get());
     };
 
+    _d2dBrushForeground->SetColor(_ColorFFromColorRef(gridlineColor | 0xff000000));
+
     // NOTE: Line coordinates are centered within the line, so they need to be
     // offset by half the stroke width. For the start coordinate we add half
     // the stroke width, and for the end coordinate we subtract half the width.
@@ -1773,10 +1775,22 @@ try
         }
     }
 
+    if (lines.test(GridLines::Strikethrough))
+    {
+        const auto halfStrikethroughWidth = lineMetrics.strikethroughWidth / 2.0f;
+        const auto startX = target.x + halfStrikethroughWidth;
+        const auto endX = target.x + fullRunWidth - halfStrikethroughWidth;
+        const auto y = target.y + lineMetrics.strikethroughOffset;
+
+        DrawLine(startX, y, endX, y, lineMetrics.strikethroughWidth);
+    }
+
+    _d2dBrushForeground->SetColor(_ColorFFromColorRef(underlineColor | 0xff000000));
+
     // In the case of the underline and strikethrough offsets, the stroke width
     // is already accounted for, so they don't require further adjustments.
 
-    if (lines.any(GridLines::Underline, GridLines::DoubleUnderline, GridLines::HyperlinkUnderline))
+    if (lines.any(GridLines::Underline, GridLines::DoubleUnderline, GridLines::DottedUnderline, GridLines::HyperlinkUnderline))
     {
         const auto halfUnderlineWidth = lineMetrics.underlineWidth / 2.0f;
         const auto startX = target.x + halfUnderlineWidth;
@@ -1788,9 +1802,9 @@ try
             DrawLine(startX, y, endX, y, lineMetrics.underlineWidth);
         }
 
-        if (lines.test(GridLines::HyperlinkUnderline))
+        if (lines.any(GridLines::DottedUnderline, GridLines::HyperlinkUnderline))
         {
-            DrawHyperlinkLine(startX, y, endX, y, lineMetrics.underlineWidth);
+            DrawDottedLine(startX, y, endX, y, lineMetrics.underlineWidth);
         }
 
         if (lines.test(GridLines::DoubleUnderline))
@@ -1801,16 +1815,6 @@ try
         }
     }
 
-    if (lines.test(GridLines::Strikethrough))
-    {
-        const auto halfStrikethroughWidth = lineMetrics.strikethroughWidth / 2.0f;
-        const auto startX = target.x + halfStrikethroughWidth;
-        const auto endX = target.x + fullRunWidth - halfStrikethroughWidth;
-        const auto y = target.y + lineMetrics.strikethroughOffset;
-
-        DrawLine(startX, y, endX, y, lineMetrics.strikethroughWidth);
-    }
-
     return S_OK;
 }
 CATCH_RETURN()
diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp
index bfb11205a05..877b3f1adfa 100644
--- a/src/renderer/dx/DxRenderer.hpp
+++ b/src/renderer/dx/DxRenderer.hpp
@@ -106,7 +106,7 @@ namespace Microsoft::Console::Render
                                               const bool fTrimLeft,
                                               const bool lineWrapped) noexcept override;
 
-        [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet const lines, COLORREF const color, size_t const cchLine, til::point const coordTarget) noexcept override;
+        [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override;
         [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override;
 
         [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override;
diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp
index a1ab2290a97..2cc5b4dc14c 100644
--- a/src/renderer/gdi/gdirenderer.hpp
+++ b/src/renderer/gdi/gdirenderer.hpp
@@ -52,7 +52,8 @@ namespace Microsoft::Console::Render
                                               const bool trimLeft,
                                               const bool lineWrapped) noexcept override;
         [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines,
-                                                   const COLORREF color,
+                                                   const COLORREF gridlineColor,
+                                                   const COLORREF underlineColor,
                                                    const size_t cchLine,
                                                    const til::point coordTarget) noexcept override;
         [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override;
@@ -120,6 +121,7 @@ namespace Microsoft::Console::Render
             int underlineWidth;
             int strikethroughOffset;
             int strikethroughWidth;
+            int curlylinePeakHeight;
         };
 
         LineMetrics _lineMetrics;
diff --git a/src/renderer/gdi/paint.cpp b/src/renderer/gdi/paint.cpp
index 17dc9bddd82..9722703d105 100644
--- a/src/renderer/gdi/paint.cpp
+++ b/src/renderer/gdi/paint.cpp
@@ -509,27 +509,24 @@ bool GdiEngine::FontHasWesternScript(HDC hdc)
 // - Draws up to one line worth of grid lines on top of characters.
 // Arguments:
 // - lines - Enum defining which edges of the rectangle to draw
-// - color - The color to use for drawing the edges.
+// - gridlineColor - The color to use for drawing the gridlines.
+// - underlineColor - The color to use for drawing the underlines.
 // - cchLine - How many characters we should draw the grid lines along (left to right in a row)
 // - coordTarget - The starting X/Y position of the first character to draw on.
 // Return Value:
 // - S_OK or suitable GDI HRESULT error or E_FAIL for GDI errors in functions that don't reliably return a specific error code.
-[[nodiscard]] HRESULT GdiEngine::PaintBufferGridLines(const GridLineSet lines, const COLORREF color, const size_t cchLine, const til::point coordTarget) noexcept
+[[nodiscard]] HRESULT GdiEngine::PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept
 {
     LOG_IF_FAILED(_FlushBufferLines());
 
     // Convert the target from characters to pixels.
     const auto ptTarget = coordTarget * _GetFontSize();
-    // Set the brush color as requested and save the previous brush to restore at the end.
-    wil::unique_hbrush hbr(CreateSolidBrush(color));
-    RETURN_HR_IF_NULL(E_FAIL, hbr.get());
-
-    wil::unique_hbrush hbrPrev(SelectBrush(_hdcMemoryContext, hbr.get()));
-    RETURN_HR_IF_NULL(E_FAIL, hbrPrev.get());
-    hbr.release(); // If SelectBrush was successful, GDI owns the brush. Release for now.
 
-    // On exit, be sure we try to put the brush back how it was originally.
-    auto restoreBrushOnExit = wil::scope_exit([&] { hbr.reset(SelectBrush(_hdcMemoryContext, hbrPrev.get())); });
+    // Create a brush with the gridline color, and apply it.
+    wil::unique_hbrush hbr(CreateSolidBrush(gridlineColor));
+    RETURN_HR_IF_NULL(E_FAIL, hbr.get());
+    const auto prevBrush = wil::SelectObject(_hdcMemoryContext, hbr.get());
+    RETURN_HR_IF_NULL(E_FAIL, prevBrush.get());
 
     // Get the font size so we know the size of the rectangle lines we'll be inscribing.
     const auto fontWidth = _GetFontSize().width;
@@ -539,6 +536,31 @@ bool GdiEngine::FontHasWesternScript(HDC hdc)
     const auto DrawLine = [=](const auto x, const auto y, const auto w, const auto h) {
         return PatBlt(_hdcMemoryContext, x, y, w, h, PATCOPY);
     };
+    const auto DrawStrokedLine = [&](const auto x, const auto y, const auto w) {
+        RETURN_HR_IF(E_FAIL, !MoveToEx(_hdcMemoryContext, x, y, nullptr));
+        RETURN_HR_IF(E_FAIL, !LineTo(_hdcMemoryContext, x + w, y));
+        return S_OK;
+    };
+    const auto DrawCurlyLine = [&](const auto x, const auto y, const auto cCurlyLines) {
+        const auto curlyLineWidth = fontWidth;
+        const auto curlyLineHalfWidth = lrintf(curlyLineWidth / 2.0f);
+        const auto controlPointHeight = gsl::narrow_cast<long>(std::floor(3.5f * _lineMetrics.curlylinePeakHeight));
+        // Each curlyLine requires 3 `POINT`s
+        const auto cPoints = gsl::narrow<DWORD>(3 * cCurlyLines);
+        std::vector<POINT> points;
+        points.reserve(cPoints);
+        auto start = x;
+        for (auto i = 0u; i < cCurlyLines; i++)
+        {
+            points.emplace_back(start + curlyLineHalfWidth, y - controlPointHeight);
+            points.emplace_back(start + curlyLineHalfWidth, y + controlPointHeight);
+            points.emplace_back(start + curlyLineWidth, y);
+            start += curlyLineWidth;
+        }
+        RETURN_HR_IF(E_FAIL, !MoveToEx(_hdcMemoryContext, x, y, nullptr));
+        RETURN_HR_IF(E_FAIL, !PolyBezierTo(_hdcMemoryContext, points.data(), cPoints));
+        return S_OK;
+    };
 
     if (lines.test(GridLines::Left))
     {
@@ -574,22 +596,51 @@ bool GdiEngine::FontHasWesternScript(HDC hdc)
         RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.gridlineWidth));
     }
 
-    if (lines.any(GridLines::Underline, GridLines::DoubleUnderline))
+    if (lines.test(GridLines::Strikethrough))
     {
-        const auto y = ptTarget.y + _lineMetrics.underlineOffset;
-        RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.underlineWidth));
+        const auto y = ptTarget.y + _lineMetrics.strikethroughOffset;
+        RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.strikethroughWidth));
+    }
 
-        if (lines.test(GridLines::DoubleUnderline))
-        {
-            const auto y2 = ptTarget.y + _lineMetrics.underlineOffset2;
-            RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y2, widthOfAllCells, _lineMetrics.underlineWidth));
-        }
+    // Create a pen matching the underline style.
+    DWORD underlinePenType = PS_SOLID;
+    if (lines.test(GridLines::DottedUnderline))
+    {
+        underlinePenType = PS_DOT;
+    }
+    else if (lines.test(GridLines::DashedUnderline))
+    {
+        underlinePenType = PS_DASH;
     }
+    const LOGBRUSH brushProp{ .lbStyle = BS_SOLID, .lbColor = underlineColor };
+    wil::unique_hpen hpen(ExtCreatePen(underlinePenType | PS_GEOMETRIC | PS_ENDCAP_FLAT, _lineMetrics.underlineWidth, &brushProp, 0, nullptr));
 
-    if (lines.test(GridLines::Strikethrough))
+    // Apply the pen.
+    const auto prevPen = wil::SelectObject(_hdcMemoryContext, hpen.get());
+    RETURN_HR_IF_NULL(E_FAIL, prevPen.get());
+
+    const auto underlineMidY = std::lround(ptTarget.y + _lineMetrics.underlineOffset + _lineMetrics.underlineWidth / 2.0f);
+    if (lines.test(GridLines::Underline))
     {
-        const auto y = ptTarget.y + _lineMetrics.strikethroughOffset;
-        RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.strikethroughWidth));
+        return DrawStrokedLine(ptTarget.x, underlineMidY, widthOfAllCells);
+    }
+    else if (lines.test(GridLines::DoubleUnderline))
+    {
+        const auto doubleUnderlineBottomLineMidY = std::lround(ptTarget.y + _lineMetrics.underlineOffset2 + _lineMetrics.underlineWidth / 2.0f);
+        RETURN_IF_FAILED(DrawStrokedLine(ptTarget.x, underlineMidY, widthOfAllCells));
+        return DrawStrokedLine(ptTarget.x, doubleUnderlineBottomLineMidY, widthOfAllCells);
+    }
+    else if (lines.test(GridLines::CurlyUnderline))
+    {
+        return DrawCurlyLine(ptTarget.x, underlineMidY, cchLine);
+    }
+    else if (lines.test(GridLines::DottedUnderline))
+    {
+        return DrawStrokedLine(ptTarget.x, underlineMidY, widthOfAllCells);
+    }
+    else if (lines.test(GridLines::DashedUnderline))
+    {
+        return DrawStrokedLine(ptTarget.x, underlineMidY, widthOfAllCells);
     }
 
     return S_OK;
diff --git a/src/renderer/gdi/state.cpp b/src/renderer/gdi/state.cpp
index 556f2df02fb..099c2f43f78 100644
--- a/src/renderer/gdi/state.cpp
+++ b/src/renderer/gdi/state.cpp
@@ -11,6 +11,15 @@
 
 using namespace Microsoft::Console::Render;
 
+namespace
+{
+    // The max height of Curly line peak in `em` units.
+    constexpr auto MaxCurlyLinePeakHeightEm = 0.075f;
+
+    // The min height of Curly line peak.
+    constexpr auto MinCurlyLinePeakHeight = 2.0f;
+}
+
 // Routine Description:
 // - Creates a new GDI-based rendering engine
 // - NOTE: Will throw if initialization failure. Caller must catch.
@@ -397,6 +406,31 @@ GdiEngine::~GdiEngine()
         _lineMetrics.underlineOffset2 = _lineMetrics.underlineOffset - _lineMetrics.gridlineWidth;
     }
 
+    // Curly line doesn't render properly below 1px stroke width. Make it a straight line.
+    if (_lineMetrics.underlineWidth < 1)
+    {
+        _lineMetrics.curlylinePeakHeight = 0;
+    }
+    else
+    {
+        // Curlyline uses the gap between cell bottom and singly underline
+        // position as the height of the wave's peak. The baseline for curly
+        // line is at the middle of singly underline. The gap could be too big,
+        // so we also apply a limit on the peak height.
+        const auto strokeHalfWidth = _lineMetrics.underlineWidth / 2.0f;
+        const auto underlineMidY = _lineMetrics.underlineOffset + strokeHalfWidth;
+        const auto cellBottomGap = Font.GetSize().height - underlineMidY - strokeHalfWidth;
+        const auto maxCurlyLinePeakHeight = MaxCurlyLinePeakHeightEm * fontSize;
+        auto curlyLinePeakHeight = std::min(cellBottomGap, maxCurlyLinePeakHeight);
+
+        // When it's too small to be curly, make it a straight line.
+        if (curlyLinePeakHeight < MinCurlyLinePeakHeight)
+        {
+            curlyLinePeakHeight = 0.0f;
+        }
+        _lineMetrics.curlylinePeakHeight = gsl::narrow_cast<int>(std::floor(curlyLinePeakHeight));
+    }
+
     // Now find the size of a 0 in this current font and save it for conversions done later.
     _coordFontLast = Font.GetSize();
 
diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp
index 46221ae911d..afcaa5aff59 100644
--- a/src/renderer/inc/IRenderEngine.hpp
+++ b/src/renderer/inc/IRenderEngine.hpp
@@ -41,6 +41,9 @@ namespace Microsoft::Console::Render
         Right,
         Underline,
         DoubleUnderline,
+        CurlyUnderline,
+        DottedUnderline,
+        DashedUnderline,
         Strikethrough,
         HyperlinkUnderline
     };
@@ -73,7 +76,7 @@ namespace Microsoft::Console::Render
         [[nodiscard]] virtual HRESULT PrepareLineTransform(LineRendition lineRendition, til::CoordType targetRow, til::CoordType viewportLeft) noexcept = 0;
         [[nodiscard]] virtual HRESULT PaintBackground() noexcept = 0;
         [[nodiscard]] virtual HRESULT PaintBufferLine(std::span<const Cluster> clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept = 0;
-        [[nodiscard]] virtual HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF color, size_t cchLine, til::point coordTarget) noexcept = 0;
+        [[nodiscard]] virtual HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF gridlineColor, COLORREF underlineColor, size_t cchLine, til::point coordTarget) noexcept = 0;
         [[nodiscard]] virtual HRESULT PaintSelection(const til::rect& rect) noexcept = 0;
         [[nodiscard]] virtual HRESULT PaintCursor(const CursorOptions& options) noexcept = 0;
         [[nodiscard]] virtual HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, gsl::not_null<IRenderData*> pData, bool usingSoftFont, bool isSettingDefaultBrushes) noexcept = 0;
diff --git a/src/renderer/inc/RenderSettings.hpp b/src/renderer/inc/RenderSettings.hpp
index 4b6e7c3981c..c836bdde848 100644
--- a/src/renderer/inc/RenderSettings.hpp
+++ b/src/renderer/inc/RenderSettings.hpp
@@ -41,6 +41,7 @@ namespace Microsoft::Console::Render
         size_t GetColorAliasIndex(const ColorAlias alias) const noexcept;
         std::pair<COLORREF, COLORREF> GetAttributeColors(const TextAttribute& attr) const noexcept;
         std::pair<COLORREF, COLORREF> GetAttributeColorsWithAlpha(const TextAttribute& attr) const noexcept;
+        COLORREF GetAttributeUnderlineColor(const TextAttribute& attr) const noexcept;
         void ToggleBlinkRendition(class Renderer& renderer) noexcept;
 
     private:
diff --git a/src/renderer/uia/UiaRenderer.cpp b/src/renderer/uia/UiaRenderer.cpp
index e58c227c561..ba09384580b 100644
--- a/src/renderer/uia/UiaRenderer.cpp
+++ b/src/renderer/uia/UiaRenderer.cpp
@@ -347,14 +347,16 @@ void UiaEngine::WaitUntilCanRender() noexcept
 //  For UIA, this doesn't mean anything. So do nothing.
 // Arguments:
 // - lines - <unused>
-// - color - <unused>
+// - gridlineColor - <unused>
+// - underlineColor - <unused>
 // - cchLine - <unused>
 // - coordTarget - <unused>
 // Return Value:
 // - S_FALSE
-[[nodiscard]] HRESULT UiaEngine::PaintBufferGridLines(GridLineSet const /*lines*/,
-                                                      COLORREF const /*color*/,
-                                                      size_t const /*cchLine*/,
+[[nodiscard]] HRESULT UiaEngine::PaintBufferGridLines(const GridLineSet /*lines*/,
+                                                      const COLORREF /*gridlineColor*/,
+                                                      const COLORREF /*underlineColor*/,
+                                                      const size_t /*cchLine*/,
                                                       const til::point /*coordTarget*/) noexcept
 {
     return S_FALSE;
diff --git a/src/renderer/uia/UiaRenderer.hpp b/src/renderer/uia/UiaRenderer.hpp
index 5e486b7e0e1..4625ca155cb 100644
--- a/src/renderer/uia/UiaRenderer.hpp
+++ b/src/renderer/uia/UiaRenderer.hpp
@@ -49,7 +49,7 @@ namespace Microsoft::Console::Render
         [[nodiscard]] HRESULT NotifyNewText(const std::wstring_view newText) noexcept override;
         [[nodiscard]] HRESULT PaintBackground() noexcept override;
         [[nodiscard]] HRESULT PaintBufferLine(const std::span<const Cluster> clusters, const til::point coord, const bool fTrimLeft, const bool lineWrapped) noexcept override;
-        [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF color, const size_t cchLine, const til::point coordTarget) noexcept override;
+        [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override;
         [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override;
         [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override;
         [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, const gsl::not_null<IRenderData*> pData, const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override;
diff --git a/src/renderer/vt/paint.cpp b/src/renderer/vt/paint.cpp
index 801390085c0..99f8853a98f 100644
--- a/src/renderer/vt/paint.cpp
+++ b/src/renderer/vt/paint.cpp
@@ -187,13 +187,15 @@ using namespace Microsoft::Console::Types;
 // - Draws up to one line worth of grid lines on top of characters.
 // Arguments:
 // - lines - Enum defining which edges of the rectangle to draw
-// - color - The color to use for drawing the edges.
+// - gridlineColor - The color to use for drawing the gridlines.
+// - underlineColor - The color to use for drawing the underlines.
 // - cchLine - How many characters we should draw the grid lines along (left to right in a row)
 // - coordTarget - The starting X/Y position of the first character to draw on.
 // Return Value:
 // - S_OK
 [[nodiscard]] HRESULT VtEngine::PaintBufferGridLines(const GridLineSet /*lines*/,
-                                                     const COLORREF /*color*/,
+                                                     const COLORREF /*gridlineColor*/,
+                                                     const COLORREF /*underlineColor*/,
                                                      const size_t /*cchLine*/,
                                                      const til::point /*coordTarget*/) noexcept
 {
diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp
index 15de6e0e760..100e04c549d 100644
--- a/src/renderer/vt/vtrenderer.hpp
+++ b/src/renderer/vt/vtrenderer.hpp
@@ -62,7 +62,7 @@ namespace Microsoft::Console::Render
         [[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition, const til::CoordType targetRow, const til::CoordType viewportLeft) noexcept override;
         [[nodiscard]] HRESULT PaintBackground() noexcept override;
         [[nodiscard]] HRESULT PaintBufferLine(std::span<const Cluster> clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept override;
-        [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF color, size_t cchLine, til::point coordTarget) noexcept override;
+        [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override;
         [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override;
         [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override;
         [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept override;
diff --git a/src/renderer/wddmcon/WddmConRenderer.cpp b/src/renderer/wddmcon/WddmConRenderer.cpp
index 378b1dfd086..fda8ae6c21b 100644
--- a/src/renderer/wddmcon/WddmConRenderer.cpp
+++ b/src/renderer/wddmcon/WddmConRenderer.cpp
@@ -284,9 +284,10 @@ CATCH_RETURN()
     CATCH_RETURN();
 }
 
-[[nodiscard]] HRESULT WddmConEngine::PaintBufferGridLines(GridLineSet const /*lines*/,
-                                                          COLORREF const /*color*/,
-                                                          size_t const /*cchLine*/,
+[[nodiscard]] HRESULT WddmConEngine::PaintBufferGridLines(const GridLineSet /*lines*/,
+                                                          const COLORREF /*gridlineColor*/,
+                                                          const COLORREF /*underlineColor*/,
+                                                          const size_t /*cchLine*/,
                                                           const til::point /*coordTarget*/) noexcept
 {
     return S_OK;
diff --git a/src/renderer/wddmcon/WddmConRenderer.hpp b/src/renderer/wddmcon/WddmConRenderer.hpp
index a658ef51180..954dc226946 100644
--- a/src/renderer/wddmcon/WddmConRenderer.hpp
+++ b/src/renderer/wddmcon/WddmConRenderer.hpp
@@ -44,7 +44,7 @@ namespace Microsoft::Console::Render
                                               const til::point coord,
                                               const bool trimLeft,
                                               const bool lineWrapped) noexcept override;
-        [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet const lines, COLORREF const color, size_t const cchLine, til::point const coordTarget) noexcept override;
+        [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override;
         [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override;
 
         [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override;
diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp
index af5ebb431ea..419acf1ff0b 100644
--- a/src/types/UiaTextRangeBase.cpp
+++ b/src/types/UiaTextRangeBase.cpp
@@ -405,16 +405,22 @@ std::optional<bool> UiaTextRangeBase::_verifyAttr(TEXTATTRIBUTEID attributeId, V
         THROW_HR_IF(E_INVALIDARG, val.vt != VT_I4);
 
         // The underline style is stored as a TextDecorationLineStyle.
-        // However, The text buffer doesn't have that many different styles for being underlined.
-        // Instead, we only have single and double underlined.
+        // However, The text buffer doesn't have all the different styles for being underlined.
+        // Instead, we only use a subset of them.
         switch (val.lVal)
         {
         case TextDecorationLineStyle_None:
             return !attr.IsUnderlined();
+        case TextDecorationLineStyle_Single:
+            return attr.GetUnderlineStyle() == UnderlineStyle::SinglyUnderlined;
         case TextDecorationLineStyle_Double:
             return attr.GetUnderlineStyle() == UnderlineStyle::DoublyUnderlined;
-        case TextDecorationLineStyle_Single: // singly underlined and extended styles are treated the same
-            return attr.IsUnderlined() && attr.GetUnderlineStyle() != UnderlineStyle::DoublyUnderlined;
+        case TextDecorationLineStyle_Wavy:
+            return attr.GetUnderlineStyle() == UnderlineStyle::CurlyUnderlined;
+        case TextDecorationLineStyle_Dot:
+            return attr.GetUnderlineStyle() == UnderlineStyle::DottedUnderlined;
+        case TextDecorationLineStyle_Dash:
+            return attr.GetUnderlineStyle() == UnderlineStyle::DashedUnderlined;
         default:
             return std::nullopt;
         }
@@ -697,18 +703,26 @@ bool UiaTextRangeBase::_initializeAttrQuery(TEXTATTRIBUTEID attributeId, VARIANT
         const auto style = attr.GetUnderlineStyle();
         switch (style)
         {
+        case UnderlineStyle::NoUnderline:
+            pRetVal->lVal = TextDecorationLineStyle_None;
+            return true;
         case UnderlineStyle::SinglyUnderlined:
             pRetVal->lVal = TextDecorationLineStyle_Single;
             return true;
         case UnderlineStyle::DoublyUnderlined:
             pRetVal->lVal = TextDecorationLineStyle_Double;
             return true;
-        case UnderlineStyle::NoUnderline:
-            pRetVal->lVal = TextDecorationLineStyle_None;
+        case UnderlineStyle::CurlyUnderlined:
+            pRetVal->lVal = TextDecorationLineStyle_Wavy;
+            return true;
+        case UnderlineStyle::DottedUnderlined:
+            pRetVal->lVal = TextDecorationLineStyle_Dot;
+            return true;
+        case UnderlineStyle::DashedUnderlined:
+            pRetVal->lVal = TextDecorationLineStyle_Dash;
             return true;
+        // Out of range styles are treated as singly underlined.
         default:
-            // TODO: Handle other underline styles once they're supported in the graphic renderer.
-            // For now, extended styles are treated (and rendered) as single underline.
             pRetVal->lVal = TextDecorationLineStyle_Single;
             return true;
         }


Test patch: