Repository: microsoft/terminal (language: cpp)

Issue:
Gaps between block elements when using Direct2D
### Windows Terminal version

Current main

### Windows build number

10.0.19045.4291

### Other Software

_No response_

### Steps to reproduce

- System-wide scale and layout (display settings): 100%
- WT PowerShell profile settings:
  - Font face: "Cascadia Mono"
  - Font size: "12.2"
  - Line height: "1.2"
  - Font weight: "Normal"
  - Builtin Glyphs: "On"
  - ![image](https://github.com/microsoft/terminal/assets/11535558/c5d3dcab-d3f3-4928-bb64-b5106f9c4eec)
  - Graphics API: "Direct2D"
  - ![image](https://github.com/microsoft/terminal/assets/11535558/92c17826-e955-4155-8c98-57ecbd565c40)

- Type "▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄" at the pwsh prompt
  ```pwsh
  "▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
  ```


### Expected Behavior

[Block elements](https://unicode.org/charts/PDF/U2580.pdf) are fused into a solid bar.

### Actual Behavior

![image](https://github.com/microsoft/terminal/assets/11535558/41b56b19-9f19-4300-a76c-f1022d65926f)



Discussion / hints:
Hi I'm an AI powered bot that finds similar issues based off the issue title.

Please view the issues below to see if they solve your problem, and if the issue describes your problem please consider closing this one and thumbs upping the other issue to help us prioritize it. Thank you!

### Open similar issues:

- [Box drawing characters are not antialiased (#6161)](https://github.com/microsoft/terminal/issues/6161),  similarity score: 0.78

### Closed similar issues:
- [Box drawing characters contain gaps when viewed in Windows Terminal (#14654)](https://github.com/microsoft/terminal/issues/14654),  similarity score: 0.79
- [Box-drawing characters in the Fira Mono font are not connected in the latest preview version of Terminal (#13064)](https://github.com/microsoft/terminal/issues/13064),  similarity score: 0.77
- [Aliased unicode box drawing characters (#13311)](https://github.com/microsoft/terminal/issues/13311),  similarity score: 0.77
- [Unicode box-drawing characters are not rendered in Consolas when font.size < 8 (#12678)](https://github.com/microsoft/terminal/issues/12678),  similarity score: 0.76

> Note: You can give me feedback by thumbs upping or thumbs downing this comment.
It seems that despite the fact that Direct2D provides graphics acceleration, builtin glyphs are not activated in this mode. If this is intended, then it would probably be worth rephrasing the "Builtin Glyphs On/Off" option description, specifically indicating the need for "Direct3D".

https://github.com/microsoft/terminal/blob/49e4eea60f737b46b8aeda505f4693df8a9d44a6/src/renderer/atlas/AtlasEngine.h#L130-L140

https://github.com/microsoft/terminal/blob/49e4eea60f737b46b8aeda505f4693df8a9d44a6/src/renderer/atlas/AtlasEngine.r.cpp#L281-L299
Right... I wrote that "This feature only works when GPU Acceleration is available." text before I added the Graphics API setting (at which time that statement was mostly true), but now that doesn't work anymore of course. The question is, which one is best?
* This feature is only available when the Direct3D Graphics API is used.
* This feature is unavailable when the Direct2D Graphics API is used.
* This feature is unavailable when Direct2D is used for text rendering.
* _something else?_
It seems to me that since the "Builtin Glyphs" option is completely dependent on the "Graphics API" option, it makes sense to make it a sub-option of the "Graphics API" option that becomes available when "Direct3D 11" is selected.

I don't think the "Builtin Glyphs" option should necessarily be per-profile, as a global option it is quite appropriate for me.
Oof that's a tricky one. Cause I'd expect it on the appearance page, but the renderer option doesn't really need to be on the appearance page. That's definitely an advance option. 

Maybe when d2d is enabled, we could disable the builtin glyphs menu item, and add a message box with a deeplink to the advanced page... but that sounds overly complicated. 

I guess making it a sub-item of the renderer entry is probably the most elegant solution
I suspect most people will keep it on the default option "Automatic", in which case Direct2D is only used if the system lacks a GPU (or it's a very old GPU, but that's fairly rare nowadays). This is commonly the case when using RDP to remote into a server or VM.

The reason I put the "Builtin Glyphs" option on the profile appearance page is because the font setting is per-profile and so I guess the builtin-glyph option (which overwrites glyphs in the font) should be per-profile too.

The problem is that the settings UI doesn't really have any idea about this "Automatic" API choice. It could be hooked up of course, but I think that's somewhat annoying to do (different DLLs, with different components, on different threads, and it may change at any given moment = needs event hooks). The upside on the other hand is potentially fairly small, because >95% of users are using the Direct3D renderer anyway.

Unless Dustin/Mike think about this differently, I think I'll only change the sub-text on the setting for now. If more people run into this issue (even just a few), I'll try to improve it by hooking up the render thread to the settings UI, or something similar.
 I'd love those specific feature toggles to get grayed out when the feature is unavailable for any reason. We could give additional context as to why they're grayed out with a ⓘ button treatment to the grayed-out toggles.

![image](https://github.com/microsoft/terminal/assets/55626797/3a556b4a-ef4c-4deb-9ebf-a888161ca436)

Unanimous team consent: It's just easier to add builtin glyph support to d2d rather than finagle the SUI to support this

Fix patch:
diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp
index 28295aeadd2..b5a89f6c09d 100644
--- a/src/renderer/atlas/AtlasEngine.cpp
+++ b/src/renderer/atlas/AtlasEngine.cpp
@@ -74,9 +74,8 @@ try
         _handleSettingsUpdate();
     }
 
-    if (ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION || _hackTriggerRedrawAll)
+    if constexpr (ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION)
     {
-        _hackTriggerRedrawAll = false;
         _api.invalidatedRows = invalidatedRowsAll;
         _api.scrollOffset = 0;
     }
@@ -703,8 +702,6 @@ void AtlasEngine::_recreateFontDependentResources()
             _api.textFormatAxes[i] = { fontAxisValues.data(), fontAxisValues.size() };
         }
     }
-
-    _hackWantsBuiltinGlyphs = _p.s->font->builtinGlyphs && !_hackIsBackendD2D;
 }
 
 void AtlasEngine::_recreateCellCountDependentResources()
@@ -765,18 +762,13 @@ void AtlasEngine::_flushBufferLine()
     // This would seriously blow us up otherwise.
     Expects(_api.bufferLineColumn.size() == _api.bufferLine.size() + 1);
 
+    const auto builtinGlyphs = _p.s->font->builtinGlyphs;
     const auto beg = _api.bufferLine.data();
     const auto len = _api.bufferLine.size();
     size_t segmentBeg = 0;
     size_t segmentEnd = 0;
     bool custom = false;
 
-    if (!_hackWantsBuiltinGlyphs)
-    {
-        _mapRegularText(0, len);
-        return;
-    }
-
     while (segmentBeg < len)
     {
         segmentEnd = segmentBeg;
@@ -789,7 +781,7 @@ void AtlasEngine::_flushBufferLine()
                 codepoint = til::combine_surrogates(codepoint, beg[i++]);
             }
 
-            const auto c = BuiltinGlyphs::IsBuiltinGlyph(codepoint) || BuiltinGlyphs::IsSoftFontChar(codepoint);
+            const auto c = (builtinGlyphs && BuiltinGlyphs::IsBuiltinGlyph(codepoint)) || BuiltinGlyphs::IsSoftFontChar(codepoint);
             if (custom != c)
             {
                 break;
diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h
index 1f26644ebe6..ccb4da9fb4e 100644
--- a/src/renderer/atlas/AtlasEngine.h
+++ b/src/renderer/atlas/AtlasEngine.h
@@ -127,18 +127,6 @@ namespace Microsoft::Console::Render::Atlas
         std::unique_ptr<IBackend> _b;
         RenderingPayload _p;
 
-        // _p.s->font->builtinGlyphs is the setting which decides whether we should map box drawing glyphs to
-        // our own builtin versions. There's just one problem: BackendD2D doesn't have this functionality.
-        // But since AtlasEngine shapes the text before it's handed to the backends, it would need to know
-        // whether BackendD2D is in use, before BackendD2D even exists. These two flags solve the issue
-        // by triggering a complete, immediate redraw whenever the backend type changes.
-        //
-        // The proper solution is to move text shaping into the backends.
-        // Someone just needs to write a generic "TextBuffer to DWRITE_GLYPH_RUN" function.
-        bool _hackIsBackendD2D = false;
-        bool _hackWantsBuiltinGlyphs = true;
-        bool _hackTriggerRedrawAll = false;
-
         struct ApiState
         {
             GenerationalSettings s = DirtyGenerationalSettings();
diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp
index a0dbdcc5470..3591abe7905 100644
--- a/src/renderer/atlas/AtlasEngine.r.cpp
+++ b/src/renderer/atlas/AtlasEngine.r.cpp
@@ -77,7 +77,7 @@ CATCH_RETURN()
 
 [[nodiscard]] bool AtlasEngine::RequiresContinuousRedraw() noexcept
 {
-    return ATLAS_DEBUG_CONTINUOUS_REDRAW || (_b && _b->RequiresContinuousRedraw()) || _hackTriggerRedrawAll;
+    return ATLAS_DEBUG_CONTINUOUS_REDRAW || (_b && _b->RequiresContinuousRedraw());
 }
 
 void AtlasEngine::WaitUntilCanRender() noexcept
@@ -282,21 +282,15 @@ void AtlasEngine::_recreateBackend()
     {
     case GraphicsAPI::Direct2D:
         _b = std::make_unique<BackendD2D>();
-        _hackIsBackendD2D = true;
         break;
     default:
         _b = std::make_unique<BackendD3D>(_p);
-        _hackIsBackendD2D = false;
         break;
     }
 
     // This ensures that the backends redraw their entire viewports whenever a new swap chain is created,
     // EVEN IF we got called when no actual settings changed (i.e. rendering failure, etc.).
     _p.MarkAllAsDirty();
-
-    const auto hackWantsBuiltinGlyphs = _p.s->font->builtinGlyphs && !_hackIsBackendD2D;
-    _hackTriggerRedrawAll = _hackWantsBuiltinGlyphs != hackWantsBuiltinGlyphs;
-    _hackWantsBuiltinGlyphs = hackWantsBuiltinGlyphs;
 }
 
 void AtlasEngine::_handleSwapChainUpdate()
diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp
index 5a663b49d04..cf7bd8a2bec 100644
--- a/src/renderer/atlas/BackendD2D.cpp
+++ b/src/renderer/atlas/BackendD2D.cpp
@@ -4,6 +4,8 @@
 #include "pch.h"
 #include "BackendD2D.h"
 
+#include <til/unicode.h>
+
 #if ATLAS_DEBUG_SHOW_DIRTY
 #include "colorbrewer.h"
 #endif
@@ -94,11 +96,15 @@ void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p)
                 .dpiY = static_cast<f32>(p.s->font->dpi),
             };
             // ID2D1RenderTarget and ID2D1DeviceContext are the same and I'm tired of pretending they're not.
-            THROW_IF_FAILED(p.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, reinterpret_cast<ID2D1RenderTarget**>(_renderTarget.addressof())));
-            _renderTarget.try_query_to(_renderTarget4.addressof());
+            THROW_IF_FAILED(p.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, reinterpret_cast<ID2D1RenderTarget**>(_renderTarget.put())));
 
             _renderTarget->SetUnitMode(D2D1_UNIT_MODE_PIXELS);
-            _renderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
+
+            _renderTarget.try_query_to(_renderTarget4.put());
+            if (_renderTarget4)
+            {
+                THROW_IF_FAILED(_renderTarget4->CreateSpriteBatch(_builtinGlyphBatch.put()));
+            }
         }
         {
             static constexpr D2D1_COLOR_F color{};
@@ -108,18 +114,15 @@ void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p)
         }
     }
 
-    if (!_dottedStrokeStyle)
-    {
-        static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM };
-        static constexpr FLOAT dashes[2]{ 1, 1 };
-        THROW_IF_FAILED(p.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _dottedStrokeStyle.addressof()));
-    }
-
     if (renderTargetChanged || fontChanged)
     {
         const auto dpi = static_cast<f32>(p.s->font->dpi);
         _renderTarget->SetDpi(dpi, dpi);
         _renderTarget->SetTextAntialiasMode(static_cast<D2D1_TEXT_ANTIALIAS_MODE>(p.s->font->antialiasingMode));
+
+        _builtinGlyphsRenderTarget.reset();
+        _builtinGlyphsBitmap.reset();
+        _builtinGlyphsRenderTargetActive = false;
     }
 
     if (renderTargetChanged || fontChanged || cellCountChanged)
@@ -199,6 +202,12 @@ void BackendD2D::_drawText(RenderingPayload& p)
 
         for (const auto& m : row->mappings)
         {
+            if (!m.fontFace)
+            {
+                baselineX = _drawBuiltinGlyphs(p, row, m, baselineY, baselineX);
+                continue;
+            }
+
             const auto colorsBegin = row->colors.begin();
             auto it = colorsBegin + m.glyphsFrom;
             const auto end = colorsBegin + m.glyphsTo;
@@ -228,42 +237,39 @@ void BackendD2D::_drawText(RenderingPayload& p)
                     baselineY,
                 };
 
-                if (glyphRun.fontFace)
+                D2D1_RECT_F bounds = GlyphRunEmptyBounds;
+                wil::com_ptr<IDWriteColorGlyphRunEnumerator1> enumerator;
+
+                if (p.s->font->colorGlyphs)
                 {
-                    D2D1_RECT_F bounds = GlyphRunEmptyBounds;
-                    wil::com_ptr<IDWriteColorGlyphRunEnumerator1> enumerator;
+                    enumerator = TranslateColorGlyphRun(p.dwriteFactory4.get(), baselineOrigin, &glyphRun);
+                }
 
-                    if (p.s->font->colorGlyphs)
+                if (enumerator)
+                {
+                    while (ColorGlyphRunMoveNext(enumerator.get()))
                     {
-                        enumerator = TranslateColorGlyphRun(p.dwriteFactory4.get(), baselineOrigin, &glyphRun);
+                        const auto colorGlyphRun = ColorGlyphRunGetCurrentRun(enumerator.get());
+                        ColorGlyphRunDraw(_renderTarget4.get(), _emojiBrush.get(), brush, colorGlyphRun);
+                        ColorGlyphRunAccumulateBounds(_renderTarget.get(), colorGlyphRun, bounds);
                     }
+                }
+                else
+                {
+                    _renderTarget->DrawGlyphRun(baselineOrigin, &glyphRun, brush, DWRITE_MEASURING_MODE_NATURAL);
+                    GlyphRunAccumulateBounds(_renderTarget.get(), baselineOrigin, &glyphRun, bounds);
+                }
 
-                    if (enumerator)
-                    {
-                        while (ColorGlyphRunMoveNext(enumerator.get()))
-                        {
-                            const auto colorGlyphRun = ColorGlyphRunGetCurrentRun(enumerator.get());
-                            ColorGlyphRunDraw(_renderTarget4.get(), _emojiBrush.get(), brush, colorGlyphRun);
-                            ColorGlyphRunAccumulateBounds(_renderTarget.get(), colorGlyphRun, bounds);
-                        }
-                    }
-                    else
+                if (bounds.top < bounds.bottom)
+                {
+                    // Since we used SetUnitMode(D2D1_UNIT_MODE_PIXELS), bounds.top/bottom is in pixels already and requires no conversion/rounding.
+                    if (row->lineRendition != LineRendition::DoubleHeightTop)
                     {
-                        _renderTarget->DrawGlyphRun(baselineOrigin, &glyphRun, brush, DWRITE_MEASURING_MODE_NATURAL);
-                        GlyphRunAccumulateBounds(_renderTarget.get(), baselineOrigin, &glyphRun, bounds);
+                        row->dirtyBottom = std::max(row->dirtyBottom, static_cast<i32>(lrintf(bounds.bottom)));
                     }
-
-                    if (bounds.top < bounds.bottom)
+                    if (row->lineRendition != LineRendition::DoubleHeightBottom)
                     {
-                        // Since we used SetUnitMode(D2D1_UNIT_MODE_PIXELS), bounds.top/bottom is in pixels already and requires no conversion/rounding.
-                        if (row->lineRendition != LineRendition::DoubleHeightTop)
-                        {
-                            row->dirtyBottom = std::max(row->dirtyBottom, static_cast<i32>(lrintf(bounds.bottom)));
-                        }
-                        if (row->lineRendition != LineRendition::DoubleHeightBottom)
-                        {
-                            row->dirtyTop = std::min(row->dirtyTop, static_cast<i32>(lrintf(bounds.top)));
-                        }
+                        row->dirtyTop = std::min(row->dirtyTop, static_cast<i32>(lrintf(bounds.top)));
                     }
                 }
 
@@ -274,6 +280,8 @@ void BackendD2D::_drawText(RenderingPayload& p)
             }
         }
 
+        _flushBuiltinGlyphs();
+
         if (!row->gridLineRanges.empty())
         {
             _drawGridlineRow(p, row, y);
@@ -300,6 +308,160 @@ void BackendD2D::_drawText(RenderingPayload& p)
     }
 }
 
+f32 BackendD2D::_drawBuiltinGlyphs(const RenderingPayload& p, const ShapedRow* row, const FontMapping& m, f32 baselineY, f32 baselineX)
+{
+    const f32 cellTop = baselineY - p.s->font->baseline;
+    const f32 cellBottom = cellTop + p.s->font->cellSize.y;
+    const f32 cellWidth = p.s->font->cellSize.x;
+
+    _prepareBuiltinGlyphRenderTarget(p);
+
+    for (size_t i = m.glyphsFrom; i < m.glyphsTo; ++i)
+    {
+        // This code runs when fontFace == nullptr. This is only the case for builtin glyphs which then use the glyphIndices
+        // to store UTF16 code points. In other words, this doesn't accidentally corrupt any actual glyph indices.
+        u32 ch = row->glyphIndices[i];
+        if (til::is_leading_surrogate(ch))
+        {
+            i += 1;
+            ch = til::combine_surrogates(ch, row->glyphIndices[i]);
+        }
+
+        // If we don't have support for ID2D1SpriteBatch we don't support builtin glyphs.
+        // But we do still need to account for the glyphAdvances, which is why we can't just skip everything.
+        // It's very unlikely for a target device to not support ID2D1SpriteBatch as it's very old at this point.
+        if (_builtinGlyphBatch)
+        {
+            if (const auto off = BuiltinGlyphs::GetBitmapCellIndex(ch); off >= 0)
+            {
+                const D2D1_RECT_F dst{ baselineX, cellTop, baselineX + cellWidth, cellBottom };
+                const auto src = _prepareBuiltinGlyph(p, ch, off);
+                const auto color = colorFromU32(row->colors[i]);
+                THROW_IF_FAILED(_builtinGlyphBatch->AddSprites(1, &dst, &src, &color, nullptr, sizeof(D2D1_RECT_F), sizeof(D2D1_RECT_U), sizeof(D2D1_COLOR_F), sizeof(D2D1_MATRIX_3X2_F)));
+            }
+        }
+
+        baselineX += row->glyphAdvances[i];
+    }
+
+    return baselineX;
+}
+
+void BackendD2D::_prepareBuiltinGlyphRenderTarget(const RenderingPayload& p)
+{
+    // If we don't have support for ID2D1SpriteBatch none of the related members will be initialized or used.
+    // We can just early-return in that case.
+    if (!_builtinGlyphBatch)
+    {
+        return;
+    }
+
+    // If the render target is already created, all of the below has already been done in a previous frame.
+    // Once the relevant settings change for some reason (primarily the font->cellSize), then _handleSettingsUpdate()
+    // will reset the render target which will cause us to skip this condition and re-initialize it below.
+    if (_builtinGlyphsRenderTarget)
+    {
+        return;
+    }
+
+    const auto cellWidth = static_cast<u32>(p.s->font->cellSize.x);
+    const auto cellHeight = static_cast<u32>(p.s->font->cellSize.y);
+    const auto cellArea = cellWidth * cellHeight;
+    const auto area = cellArea * BuiltinGlyphs::TotalCharCount;
+
+    // This block of code calculates the size of a power-of-2 texture that has an area larger than the given `area`.
+    // For instance, for an area of 985x1946 = 1916810 it would result in a u/v of 2048x1024 (area = 2097152).
+    // We throw the "v" in this case away, because we don't really need power-of-2 textures here,
+    // but you can find the complete code over in BackendD3D. If someone deleted it in the meantime:
+    //   const auto index = bitness_of_area_minus_1 - std::countl_zero(area - 1); // aka: _BitScanReverse
+    //   const auto u = 1u << ((index + 2) / 2);
+    //   const auto v = 1u << ((index + 1) / 2);
+    unsigned long index;
+    _BitScanReverse(&index, area - 1);
+    const auto potWidth = 1u << ((index + 2) / 2);
+
+    const auto cellCountU = potWidth / cellWidth;
+    const auto cellCountV = (BuiltinGlyphs::TotalCharCount + cellCountU - 1) / cellCountU;
+    const auto u = cellCountU * cellWidth;
+    const auto v = cellCountV * cellHeight;
+
+    const D2D1_SIZE_F sizeF{ static_cast<f32>(u), static_cast<f32>(v) };
+    const D2D1_SIZE_U sizeU{ gsl::narrow_cast<UINT32>(u), gsl::narrow_cast<UINT32>(v) };
+    static constexpr D2D1_PIXEL_FORMAT format{ DXGI_FORMAT_A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED };
+    wil::com_ptr<ID2D1BitmapRenderTarget> target;
+    THROW_IF_FAILED(_renderTarget->CreateCompatibleRenderTarget(&sizeF, &sizeU, &format, D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE, target.addressof()));
+
+    THROW_IF_FAILED(target->GetBitmap(_builtinGlyphsBitmap.put()));
+    _builtinGlyphsRenderTarget = target.query<ID2D1DeviceContext>();
+    _builtinGlyphsBitmapCellCountU = cellCountU;
+    _builtinGlyphsRenderTargetActive = false;
+    memset(&_builtinGlyphsReady[0], 0, sizeof(_builtinGlyphsReady));
+}
+
+D2D1_RECT_U BackendD2D::_prepareBuiltinGlyph(const RenderingPayload& p, char32_t ch, u32 off)
+{
+    const u32 w = p.s->font->cellSize.x;
+    const u32 h = p.s->font->cellSize.y;
+    const u32 l = (off % _builtinGlyphsBitmapCellCountU) * w;
+    const u32 t = (off / _builtinGlyphsBitmapCellCountU) * h;
+    D2D1_RECT_U rectU{ l, t, l + w, t + h };
+
+    // Check if we previously cached this glyph already.
+    if (_builtinGlyphsReady[off])
+    {
+        return rectU;
+    }
+
+    static constexpr D2D1_COLOR_F shadeColorMap[] = {
+        { 1, 1, 1, 0.25f }, // Shape_Filled025
+        { 1, 1, 1, 0.50f }, // Shape_Filled050
+        { 1, 1, 1, 0.75f }, // Shape_Filled075
+        { 1, 1, 1, 1.00f }, // Shape_Filled100
+    };
+
+    if (!_builtinGlyphsRenderTargetActive)
+    {
+        _builtinGlyphsRenderTarget->BeginDraw();
+        _builtinGlyphsRenderTargetActive = true;
+    }
+
+    const auto brush = _brushWithColor(0xffffffff);
+    const D2D1_RECT_F rectF{
+        static_cast<f32>(rectU.left),
+        static_cast<f32>(rectU.top),
+        static_cast<f32>(rectU.right),
+        static_cast<f32>(rectU.bottom),
+    };
+    BuiltinGlyphs::DrawBuiltinGlyph(p.d2dFactory.get(), _builtinGlyphsRenderTarget.get(), brush, shadeColorMap, rectF, ch);
+
+    _builtinGlyphsReady[off] = true;
+    return rectU;
+}
+
+void BackendD2D::_flushBuiltinGlyphs()
+{
+    // If we don't have support for ID2D1SpriteBatch none of the related members will be initialized or used.
+    // We can just early-return in that case.
+    if (!_builtinGlyphBatch)
+    {
+        return;
+    }
+
+    if (_builtinGlyphsRenderTargetActive)
+    {
+        THROW_IF_FAILED(_builtinGlyphsRenderTarget->EndDraw());
+        _builtinGlyphsRenderTargetActive = false;
+    }
+
+    if (const auto count = _builtinGlyphBatch->GetSpriteCount(); count > 0)
+    {
+        _renderTarget4->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
+        _renderTarget4->DrawSpriteBatch(_builtinGlyphBatch.get(), 0, count, _builtinGlyphsBitmap.get(), D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR, D2D1_SPRITE_OPTIONS_NONE);
+        _renderTarget4->SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
+        _builtinGlyphBatch->Clear();
+    }
+}
+
 f32 BackendD2D::_drawTextPrepareLineRendition(const RenderingPayload& p, const ShapedRow* row, f32 baselineY) const noexcept
 {
     const auto lineRendition = row->lineRendition;
@@ -410,44 +572,118 @@ f32r BackendD2D::_getGlyphRunDesignBounds(const DWRITE_GLYPH_RUN& glyphRun, f32
 
 void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* row, u16 y)
 {
-    const auto widthShift = gsl::narrow_cast<u8>(row->lineRendition != LineRendition::SingleWidth);
-    const auto cellSize = p.s->font->cellSize;
-    const auto rowTop = gsl::narrow_cast<i16>(cellSize.y * y);
-    const auto rowBottom = gsl::narrow_cast<i16>(rowTop + cellSize.y);
-    const auto textCellCenter = row->lineRendition == LineRendition::DoubleHeightTop ? rowBottom : rowTop;
+    const auto cellWidth = static_cast<f32>(p.s->font->cellSize.x);
+    const auto cellHeight = static_cast<f32>(p.s->font->cellSize.y);
+    const auto rowTop = cellHeight * y;
+    const auto rowBottom = rowTop + cellHeight;
+    const auto cellCenter = row->lineRendition == LineRendition::DoubleHeightTop ? rowBottom : rowTop;
+    const auto scaleHorizontal = row->lineRendition != LineRendition::SingleWidth ? 0.5f : 1.0f;
+    const auto scaledCellWidth = cellWidth * scaleHorizontal;
 
     const auto appendVerticalLines = [&](const GridLineRange& r, FontDecorationPosition pos) {
-        const auto from = r.from >> widthShift;
-        const auto to = r.to >> widthShift;
+        const auto from = r.from * scaledCellWidth;
+        const auto to = r.to * scaledCellWidth;
+        auto x = from + pos.position;
 
-        auto posX = from * cellSize.x + pos.position;
-        const auto end = to * cellSize.x;
-
-        D2D1_POINT_2F point0{ 0, static_cast<f32>(textCellCenter) };
-        D2D1_POINT_2F point1{ 0, static_cast<f32>(textCellCenter + cellSize.y) };
+        D2D1_POINT_2F point0{ 0, cellCenter };
+        D2D1_POINT_2F point1{ 0, cellCenter + cellHeight };
         const auto brush = _brushWithColor(r.gridlineColor);
         const f32 w = pos.height;
         const f32 hw = w * 0.5f;
 
-        for (; posX < end; posX += cellSize.x)
+        for (; x < to; x += cellWidth)
         {
-            const auto centerX = posX + hw;
+            const auto centerX = x + hw;
             point0.x = centerX;
             point1.x = centerX;
             _renderTarget->DrawLine(point0, point1, brush, w, nullptr);
         }
     };
     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 from = r.from * scaledCellWidth;
+        const auto to = r.to * scaledCellWidth;
 
         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 };
-        const D2D1_POINT_2F point1{ static_cast<f32>(to * cellSize.x), centerY };
+        const f32 centerY = cellCenter + pos.position + w * 0.5f;
+        const D2D1_POINT_2F point0{ from, centerY };
+        const D2D1_POINT_2F point1{ to, centerY };
         _renderTarget->DrawLine(point0, point1, brush, w, strokeStyle);
     };
+    const auto appendCurlyLine = [&](const GridLineRange& r) {
+        const auto& font = *p.s->font;
+
+        const auto duTop = static_cast<f32>(font.doubleUnderline[0].position);
+        const auto duBottom = static_cast<f32>(font.doubleUnderline[1].position);
+        // The double-underline height is also our target line width.
+        const auto duHeight = static_cast<f32>(font.doubleUnderline[0].height);
+
+        // This gives it the same position and height as our double-underline. There's no particular reason for that, apart from
+        // it being simple to implement and robust against more peculiar fonts with unusually large/small descenders, etc.
+        // We still need to ensure though that it doesn't clip out of the cellHeight at the bottom, which is why `position` has a min().
+        const auto height = std::max(3.0f, duBottom + duHeight - duTop);
+        const auto position = std::min(duTop, cellHeight - height - duHeight);
+
+        // The amplitude of the wave needs to account for the stroke width, so that the final height including
+        // antialiasing isn't larger than our target `height`. That's why we calculate `(height - duHeight)`.
+        //
+        // In other words, Direct2D draws strokes centered on the path. This also means that (for instance)
+        // for a line width of 1px, we need to ensure that the amplitude passes through the center of a pixel.
+        // Because once the path gets stroked, it'll occupy half a pixel on either side of the path.
+        // This results in a "crisp" look. That's why we do `round(amp + half) - half`.
+        const auto halfLineWidth = 0.5f * duHeight;
+        const auto amplitude = roundf((height - duHeight) * 0.5f + halfLineWidth) - halfLineWidth;
+        // While the amplitude needs to account for the stroke width, the vertical center of the wave needs
+        // to be at an integer pixel position of course. Otherwise, the wave won't be vertically symmetric.
+        const auto center = cellCenter + position + amplitude + halfLineWidth;
+
+        const auto top = center - 2.0f * amplitude;
+        const auto bottom = center + 2.0f * amplitude;
+        const auto step = 0.5f * height;
+        const auto period = 4.0f * step;
+
+        const auto from = r.from * scaledCellWidth;
+        const auto to = r.to * scaledCellWidth;
+        // Align the start of the wave to the nearest preceding period boundary.
+        // This ensures that the wave is continuous across color and cell changes.
+        auto x = floorf(from / period) * period;
+
+        wil::com_ptr<ID2D1PathGeometry> geometry;
+        THROW_IF_FAILED(p.d2dFactory->CreatePathGeometry(geometry.addressof()));
+
+        wil::com_ptr<ID2D1GeometrySink> sink;
+        THROW_IF_FAILED(geometry->Open(sink.addressof()));
+
+        // This adds complete periods of the wave until we reach the end of the range.
+        sink->BeginFigure({ x, center }, D2D1_FIGURE_BEGIN_HOLLOW);
+        for (D2D1_QUADRATIC_BEZIER_SEGMENT segment; x < to;)
+        {
+            x += step;
+            segment.point1.x = x;
+            segment.point1.y = top;
+            x += step;
+            segment.point2.x = x;
+            segment.point2.y = center;
+            sink->AddQuadraticBezier(&segment);
+
+            x += step;
+            segment.point1.x = x;
+            segment.point1.y = bottom;
+            x += step;
+            segment.point2.x = x;
+            segment.point2.y = center;
+            sink->AddQuadraticBezier(&segment);
+        }
+        sink->EndFigure(D2D1_FIGURE_END_OPEN);
+
+        THROW_IF_FAILED(sink->Close());
+
+        const auto brush = _brushWithColor(r.underlineColor);
+        const D2D1_RECT_F clipRect{ from, rowTop, to, rowBottom };
+        _renderTarget->PushAxisAlignedClip(&clipRect, D2D1_ANTIALIAS_MODE_ALIASED);
+        _renderTarget->DrawGeometry(geometry.get(), brush, duHeight, nullptr);
+        _renderTarget->PopAxisAlignedClip();
+    };
 
     for (const auto& r : row->gridLineRanges)
     {
@@ -481,8 +717,28 @@ void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* ro
         }
         else if (r.lines.any(GridLines::DottedUnderline, GridLines::HyperlinkUnderline))
         {
+            if (!_dottedStrokeStyle)
+            {
+                static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM };
+                static constexpr FLOAT dashes[2]{ 1, 1 };
+                THROW_IF_FAILED(p.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _dottedStrokeStyle.addressof()));
+            }
             appendHorizontalLine(r, p.s->font->underline, _dottedStrokeStyle.get(), r.underlineColor);
         }
+        else if (r.lines.test(GridLines::DashedUnderline))
+        {
+            if (!_dashedStrokeStyle)
+            {
+                static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM };
+                static constexpr FLOAT dashes[2]{ 2, 2 };
+                THROW_IF_FAILED(p.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _dashedStrokeStyle.addressof()));
+            }
+            appendHorizontalLine(r, p.s->font->underline, _dashedStrokeStyle.get(), r.underlineColor);
+        }
+        else if (r.lines.test(GridLines::CurlyUnderline))
+        {
+            appendCurlyLine(r);
+        }
         else if (r.lines.test(GridLines::DoubleUnderline))
         {
             for (const auto pos : p.s->font->doubleUnderline)
diff --git a/src/renderer/atlas/BackendD2D.h b/src/renderer/atlas/BackendD2D.h
index e6993d60603..4206390ea52 100644
--- a/src/renderer/atlas/BackendD2D.h
+++ b/src/renderer/atlas/BackendD2D.h
@@ -3,9 +3,8 @@
 
 #pragma once
 
-#include <til/flat_set.h>
-
 #include "Backend.h"
+#include "BuiltinGlyphs.h"
 
 namespace Microsoft::Console::Render::Atlas
 {
@@ -19,6 +18,10 @@ namespace Microsoft::Console::Render::Atlas
         ATLAS_ATTR_COLD void _handleSettingsUpdate(const RenderingPayload& p);
         void _drawBackground(const RenderingPayload& p);
         void _drawText(RenderingPayload& p);
+        ATLAS_ATTR_COLD f32 _drawBuiltinGlyphs(const RenderingPayload& p, const ShapedRow* row, const FontMapping& m, f32 baselineY, f32 baselineX);
+        void _prepareBuiltinGlyphRenderTarget(const RenderingPayload& p);
+        D2D1_RECT_U _prepareBuiltinGlyph(const RenderingPayload& p, char32_t ch, u32 off);
+        void _flushBuiltinGlyphs();
         ATLAS_ATTR_COLD f32 _drawTextPrepareLineRendition(const RenderingPayload& p, const ShapedRow* row, f32 baselineY) const noexcept;
         ATLAS_ATTR_COLD void _drawTextResetLineRendition(const ShapedRow* row) const noexcept;
         ATLAS_ATTR_COLD f32r _getGlyphRunDesignBounds(const DWRITE_GLYPH_RUN& glyphRun, f32 baselineX, f32 baselineY);
@@ -37,10 +40,18 @@ namespace Microsoft::Console::Render::Atlas
         wil::com_ptr<ID2D1DeviceContext> _renderTarget;
         wil::com_ptr<ID2D1DeviceContext4> _renderTarget4; // Optional. Supported since Windows 10 14393.
         wil::com_ptr<ID2D1StrokeStyle> _dottedStrokeStyle;
+        wil::com_ptr<ID2D1StrokeStyle> _dashedStrokeStyle;
         wil::com_ptr<ID2D1Bitmap> _backgroundBitmap;
         wil::com_ptr<ID2D1BitmapBrush> _backgroundBrush;
         til::generation_t _backgroundBitmapGeneration;
 
+        wil::com_ptr<ID2D1DeviceContext> _builtinGlyphsRenderTarget;
+        wil::com_ptr<ID2D1Bitmap> _builtinGlyphsBitmap;
+        wil::com_ptr<ID2D1SpriteBatch> _builtinGlyphBatch;
+        u32 _builtinGlyphsBitmapCellCountU = 0;
+        bool _builtinGlyphsRenderTargetActive = false;
+        bool _builtinGlyphsReady[BuiltinGlyphs::TotalCharCount]{};
+
         wil::com_ptr<ID2D1Bitmap> _cursorBitmap;
         til::size _cursorBitmapSize; // in columns/rows
 
diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp
index fe07271c16a..d85d6e0365f 100644
--- a/src/renderer/atlas/BackendD3D.cpp
+++ b/src/renderer/atlas/BackendD3D.cpp
@@ -295,20 +295,20 @@ void BackendD3D::_updateFontDependents(const RenderingPayload& p)
     // baseline of curlyline is at the middle of singly underline. When there's
     // limited space to draw a curlyline, we apply a limit on the peak height.
     {
-        const auto cellHeight = static_cast<f32>(font.cellSize.y);
-        const auto duTop = static_cast<f32>(font.doubleUnderline[0].position);
-        const auto duBottom = static_cast<f32>(font.doubleUnderline[1].position);
-        const auto duHeight = static_cast<f32>(font.doubleUnderline[0].height);
+        const int cellHeight = font.cellSize.y;
+        const int duTop = font.doubleUnderline[0].position;
+        const int duBottom = font.doubleUnderline[1].position;
+        const int duHeight = font.doubleUnderline[0].height;
 
         // This gives it the same position and height as our double-underline. There's no particular reason for that, apart from
         // it being simple to implement and robust against more peculiar fonts with unusually large/small descenders, etc.
-        // We still need to ensure though that it doesn't clip out of the cellHeight at the bottom.
-        const auto height = std::max(3.0f, duBottom + duHeight - duTop);
-        const auto top = std::min(duTop, floorf(cellHeight - height - duHeight));
+        // We still need to ensure though that it doesn't clip out of the cellHeight at the bottom, which is why `position` has a min().
+        const auto height = std::max(3, duBottom + duHeight - duTop);
+        const auto position = std::min(duTop, cellHeight - height - duHeight);
 
         _curlyLineHalfHeight = height * 0.5f;
-        _curlyUnderline.position = gsl::narrow_cast<u16>(lrintf(top));
-        _curlyUnderline.height = gsl::narrow_cast<u16>(lrintf(height));
+        _curlyUnderline.position = gsl::narrow_cast<u16>(position);
+        _curlyUnderline.height = gsl::narrow_cast<u16>(height);
     }
 
     DWrite_GetRenderParams(p.dwriteFactory.get(), &_gamma, &_cleartypeEnhancedContrast, &_grayscaleEnhancedContrast, _textRenderingParams.put());
@@ -1509,7 +1509,19 @@ BackendD3D::AtlasGlyphEntry* BackendD3D::_drawBuiltinGlyph(const RenderingPayloa
     }
     else
     {
-        BuiltinGlyphs::DrawBuiltinGlyph(p.d2dFactory.get(), _d2dRenderTarget.get(), _brush.get(), r, glyphIndex);
+        // This code works in tandem with SHADING_TYPE_TEXT_BUILTIN_GLYPH in our pixel shader.
+        // Unless someone removed it, it should have a lengthy comment visually explaining
+        // what each of the 3 RGB components do. The short version is:
+        //   R: stretch the checkerboard pattern (Shape_Filled050) horizontally
+        //   G: invert the pixels
+        //   B: overrides the above and fills it
+        static constexpr D2D1_COLOR_F shadeColorMap[] = {
+            { 1, 0, 0, 1 }, // Shape_Filled025
+            { 0, 0, 0, 1 }, // Shape_Filled050
+            { 1, 1, 0, 1 }, // Shape_Filled075
+            { 1, 1, 1, 1 }, // Shape_Filled100
+        };
+        BuiltinGlyphs::DrawBuiltinGlyph(p.d2dFactory.get(), _d2dRenderTarget.get(), _brush.get(), shadeColorMap, r, glyphIndex);
         shadingType = ShadingType::TextBuiltinGlyph;
     }
 
diff --git a/src/renderer/atlas/BuiltinGlyphs.cpp b/src/renderer/atlas/BuiltinGlyphs.cpp
index c46bfc01fc0..92d87bc0b45 100644
--- a/src/renderer/atlas/BuiltinGlyphs.cpp
+++ b/src/renderer/atlas/BuiltinGlyphs.cpp
@@ -135,8 +135,6 @@ inline constexpr f32 Pos_Lut[][2] = {
     /* Pos_11_12       */ { 11.0f / 12.0f, 0.0f },
 };
 
-static constexpr char32_t BoxDrawing_FirstChar = 0x2500;
-static constexpr u32 BoxDrawing_CharCount = 0xA0;
 static constexpr Instruction BoxDrawing[BoxDrawing_CharCount][InstructionsPerGlyph] = {
     // U+2500 ─ BOX DRAWINGS LIGHT HORIZONTAL
     {
@@ -964,8 +962,6 @@ static constexpr Instruction BoxDrawing[BoxDrawing_CharCount][InstructionsPerGly
     },
 };
 
-static constexpr char32_t Powerline_FirstChar = 0xE0B0;
-static constexpr u32 Powerline_CharCount = 0x10;
 static constexpr Instruction Powerline[Powerline_CharCount][InstructionsPerGlyph] = {
     // U+E0B0 Right triangle solid
     {
@@ -1071,7 +1067,20 @@ static const Instruction* GetInstructions(char32_t codepoint) noexcept
     return nullptr;
 }
 
-void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_RECT_F& rect, char32_t codepoint)
+i32 BuiltinGlyphs::GetBitmapCellIndex(char32_t codepoint) noexcept
+{
+    if (BoxDrawing_IsMapped(codepoint))
+    {
+        return codepoint - BoxDrawing_FirstChar;
+    }
+    if (Powerline_IsMapped(codepoint))
+    {
+        return codepoint - Powerline_FirstChar + BoxDrawing_CharCount;
+    }
+    return -1;
+}
+
+void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_COLOR_F (&shadeColorMap)[4], const D2D1_RECT_F& rect, char32_t codepoint)
 {
     renderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED);
     const auto restoreD2D = wil::scope_exit([&]() {
@@ -1122,15 +1131,18 @@ void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext*
         const auto lineOffsetX = isHollowRect || isLineX ? lineWidthHalf : 0.0f;
         const auto lineOffsetY = isHollowRect || isLineY ? lineWidthHalf : 0.0f;
 
-        begX = roundf(begX - lineOffsetX) + lineOffsetX;
-        begY = roundf(begY - lineOffsetY) + lineOffsetY;
-        endX = roundf(endX + lineOffsetX) - lineOffsetX;
-        endY = roundf(endY + lineOffsetY) - lineOffsetY;
-
-        const auto begXabs = begX + rectX;
-        const auto begYabs = begY + rectY;
-        const auto endXabs = endX + rectX;
-        const auto endYabs = endY + rectY;
+        // Direct2D draws strokes centered on the path. In order to make them pixel-perfect we need to round the
+        // coordinates to whole pixels, but offset by half the stroke width (= the radius of the stroke).
+        //
+        // All floats up to this point will be highly "consistent" between different `rect`s of identical size and
+        // different shapes, because the above calculations work with only a small set of constant floats.
+        // However, the addition of a potentially fractional begX/Y with a highly variable `rect` position is different.
+        // Rounding beg/endX/Y first ensures that we continue to get a consistent behavior between calls.
+        // This is particularly noticeable at smaller font sizes, where the line width is just a pixel or two.
+        const auto begXabs = rectX + roundf(begX - lineOffsetX) + lineOffsetX;
+        const auto begYabs = rectY + roundf(begY - lineOffsetY) + lineOffsetY;
+        const auto endXabs = rectX + roundf(endX + lineOffsetX) - lineOffsetX;
+        const auto endYabs = rectY + roundf(endY + lineOffsetY) - lineOffsetY;
 
         switch (shape)
         {
@@ -1139,21 +1151,8 @@ void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext*
         case Shape_Filled075:
         case Shape_Filled100:
         {
-            // This code works in tandem with SHADING_TYPE_TEXT_BUILTIN_GLYPH in our pixel shader.
-            // Unless someone removed it, it should have a lengthy comment visually explaining
-            // what each of the 3 RGB components do. The short version is:
-            //   R: stretch the checkerboard pattern (Shape_Filled050) horizontally
-            //   G: invert the pixels
-            //   B: overrides the above and fills it
-            static constexpr D2D1_COLOR_F colors[] = {
-                { 1, 0, 0, 1 }, // Shape_Filled025
-                { 0, 0, 0, 1 }, // Shape_Filled050
-                { 1, 1, 0, 1 }, // Shape_Filled075
-                { 1, 1, 1, 1 }, // Shape_Filled100
-            };
-
             const auto brushColor = brush->GetColor();
-            brush->SetColor(&colors[shape]);
+            brush->SetColor(&shadeColorMap[shape]);
 
             const D2D1_RECT_F r{ begXabs, begYabs, endXabs, endYabs };
             renderTarget->FillRectangle(&r, brush);
@@ -1183,13 +1182,13 @@ void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext*
         }
         case Shape_FilledEllipsis:
         {
-            const D2D1_ELLIPSE e{ { begXabs, begYabs }, endX, endY };
+            const D2D1_ELLIPSE e{ { rectX + begX, rectY + begY }, endX, endY };
             renderTarget->FillEllipse(&e, brush);
             break;
         }
         case Shape_EmptyEllipsis:
         {
-            const D2D1_ELLIPSE e{ { begXabs, begYabs }, endX, endY };
+            const D2D1_ELLIPSE e{ { rectX + begX, rectY + begY }, endX, endY };
             renderTarget->DrawEllipse(&e, brush, lineWidth, nullptr);
             break;
         }
diff --git a/src/renderer/atlas/BuiltinGlyphs.h b/src/renderer/atlas/BuiltinGlyphs.h
index f399653893c..b6cce3a3974 100644
--- a/src/renderer/atlas/BuiltinGlyphs.h
+++ b/src/renderer/atlas/BuiltinGlyphs.h
@@ -8,7 +8,17 @@
 namespace Microsoft::Console::Render::Atlas::BuiltinGlyphs
 {
     bool IsBuiltinGlyph(char32_t codepoint) noexcept;
-    void DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_RECT_F& rect, char32_t codepoint);
+    void DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_COLOR_F (&shadeColorMap)[4], const D2D1_RECT_F& rect, char32_t codepoint);
+
+    inline constexpr char32_t BoxDrawing_FirstChar = 0x2500;
+    inline constexpr u32 BoxDrawing_CharCount = 0xA0;
+
+    inline constexpr char32_t Powerline_FirstChar = 0xE0B0;
+    inline constexpr u32 Powerline_CharCount = 0x10;
+
+    inline constexpr u32 TotalCharCount = BoxDrawing_CharCount + Powerline_CharCount;
+
+    i32 GetBitmapCellIndex(char32_t codepoint) noexcept;
 
     // This is just an extra. It's not actually implemented as part of BuiltinGlyphs.cpp.
     constexpr bool IsSoftFontChar(char32_t ch) noexcept
diff --git a/src/tools/RenderingTests/main.cpp b/src/tools/RenderingTests/main.cpp
index 45dfcf66ea7..2d0ef8612a4 100644
--- a/src/tools/RenderingTests/main.cpp
+++ b/src/tools/RenderingTests/main.cpp
@@ -108,15 +108,15 @@ static void printfUTF16(_In_z_ _Printf_format_string_ wchar_t const* const forma
 
 static void wait()
 {
-    printUTF16(L"\x1B[9999;1HPress any key to continue...");
+    printUTF16(L"\x1b[9999;1HPress any key to continue...");
     _getch();
 }
 
 static void clear()
 {
     printUTF16(
-        L"\x1B[H" // move cursor to 0,0
-        L"\x1B[2J" // clear screen
+        L"\x1b[H" // move cursor to 0,0
+        L"\x1b[2J" // clear screen
     );
 }
 
@@ -166,7 +166,7 @@ int main()
             for (const auto& t : consoleAttributeTests)
             {
                 const auto length = static_cast<DWORD>(wcslen(t.text));
-                printfUTF16(L"\x1B[%d;5H%s", row + 1, t.text);
+                printfUTF16(L"\x1b[%d;5H%s", row + 1, t.text);
 
                 WORD attributes[32];
                 std::fill_n(&attributes[0], length, static_cast<WORD>(FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | t.attribute));
@@ -190,16 +190,16 @@ int main()
                 { L"overlined", 53 },
             };
 
-            printfUTF16(L"\x1B[3;39HANSI escape SGR:");
+            printfUTF16(L"\x1b[3;39HANSI escape SGR:");
 
             int row = 5;
             for (const auto& t : basicSGR)
             {
-                printfUTF16(L"\x1B[%d;39H\x1b[%dm%s\x1b[m", row, t.attribute, t.text);
+                printfUTF16(L"\x1b[%d;39H\x1b[%dm%s\x1b[m", row, t.attribute, t.text);
                 row += 2;
             }
 
-            printfUTF16(L"\x1B[%d;39H\x1b]8;;https://example.com\x1b\\hyperlink\x1b]8;;\x1b\\", row);
+            printfUTF16(L"\x1b[%d;39H\x1b]8;;https://example.com\x1b\\hyperlink\x1b]8;;\x1b\\", row);
         }
 
         {
@@ -211,18 +211,18 @@ int main()
                 { L"dashed", 5 },
             };
 
-            printfUTF16(L"\x1B[3;63HStyled Underlines:");
+            printfUTF16(L"\x1b[3;63HStyled Underlines:");
 
             int row = 5;
             for (const auto& t : styledUnderlines)
             {
-                printfUTF16(L"\x1B[%d;63H\x1b[4:%dm", row, t.attribute);
+                printfUTF16(L"\x1b[%d;63H\x1b[4:%dm", row, t.attribute);
 
                 const auto len = wcslen(t.text);
                 for (size_t i = 0; i < len; ++i)
                 {
                     const auto color = colorbrewer::pastel1[i % std::size(colorbrewer::pastel1)];
-                    printfUTF16(L"\x1B[58:2::%d:%d:%dm%c", (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, t.text[i]);
+                    printfUTF16(L"\x1b[58:2::%d:%d:%dm%c", (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, t.text[i]);
                 }
 
                 printfUTF16(L"\x1b[m");
@@ -236,19 +236,19 @@ int main()
 
     {
         printUTF16(
-            L"\x1B[3;5HDECDWL Double Width \U0001FAE0 \x1B[45;92mA\u0353\u0353\x1B[m B\u036F\u036F"
-            L"\x1B[4;3H\x1b#6DECDWL Double Width         \U0001FAE0 \x1B[45;92mA\u0353\u0353\x1B[m B\u036F\u036F"
-            L"\x1B[7;5HDECDHL Double Height \U0001F952\U0001F6C1 A\u0353\u0353 \x1B[45;92mB\u036F\u036F\x1B[m \x1B[45;92mX\u0353\u0353\x1B[m Y\u036F\u036F"
-            L"\x1B[8;3H\x1b#3DECDHL Double Height Top    \U0001F952 A\u0353\u0353 \x1B[45;92mB\u036F\u036F\x1B[m"
-            L"\x1B[9;3H\x1b#4DECDHL Double Height Bottom \U0001F6C1 \x1B[45;92mX\u0353\u0353\x1B[m Y\u036F\u036F"
-            L"\x1B[13;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m        \x1b[4munderline\x1b[m        \x1b[7mreverse\x1b[m"
-            L"\x1B[15;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m"
-            L"\x1B[17;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m        \x1b[4munderline\x1b[m        \x1b[7mreverse\x1b[m"
-            L"\x1B[19;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m"
-            L"\x1B[21;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m        \x1b[4munderline\x1b[m        \x1b[7mreverse\x1b[m"
-            L"\x1B[22;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m        \x1b[4munderline\x1b[m        \x1b[7mreverse\x1b[m"
-            L"\x1B[24;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m"
-            L"\x1B[25;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m");
+            L"\x1b[3;5HDECDWL Double Width \U0001FAE0 \x1b[45;92mA\u0353\u0353\x1b[m B\u036F\u036F"
+            L"\x1b[4;3H\x1b#6DECDWL Double Width         \U0001FAE0 \x1b[45;92mA\u0353\u0353\x1b[m B\u036F\u036F"
+            L"\x1b[7;5HDECDHL Double Height \U0001F952\U0001F6C1 A\u0353\u0353 \x1b[45;92mB\u036F\u036F\x1b[m \x1b[45;92mX\u0353\u0353\x1b[m Y\u036F\u036F"
+            L"\x1b[8;3H\x1b#3DECDHL Double Height Top    \U0001F952 A\u0353\u0353 \x1b[45;92mB\u036F\u036F\x1b[m"
+            L"\x1b[9;3H\x1b#4DECDHL Double Height Bottom \U0001F6C1 \x1b[45;92mX\u0353\u0353\x1b[m Y\u036F\u036F"
+            L"\x1b[12;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[3;4:3;58:2::255:0:0mita\x1b[58:2::0:255:0mlic\x1b[m        \x1b[4munderline\x1b[m        \x1b[7mreverse\x1b[m"
+            L"\x1b[14;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m"
+            L"\x1b[16;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[3;4:3;58:2::255:0:0mita\x1b[58:2::0:255:0mlic\x1b[m        \x1b[4munderline\x1b[m        \x1b[7mreverse\x1b[m"
+            L"\x1b[18;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m"
+            L"\x1b[20;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[3;4:3;58:2::255:0:0mita\x1b[58:2::0:255:0mlic\x1b[m        \x1b[4munderline\x1b[m        \x1b[7mreverse\x1b[m"
+            L"\x1b[21;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[3;4:3;58:2::255:0:0mita\x1b[58:2::0:255:0mlic\x1b[m        \x1b[4munderline\x1b[m        \x1b[7mreverse\x1b[m"
+            L"\x1b[23;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m"
+            L"\x1b[24;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m");
 
         static constexpr WORD attributes[]{
             FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_GRID_HORIZONTAL,
@@ -264,7 +264,7 @@ int main()
         DWORD numberOfAttrsWritten;
         DWORD offset = 0;
 
-        for (const auto r : { 12, 14, 16, 18, 20, 21, 23, 24 })
+        for (const auto r : { 11, 13, 15, 17, 19, 20, 22, 23 })
         {
             COORD coord;
             coord.X = r > 14 ? 2 : 4;
@@ -338,14 +338,14 @@ int main()
 
 #define DRCS_SEQUENCE L"\x1b( @#\x1b(A"
         printUTF16(
-            L"\x1B[3;5HDECDLD and DRCS test - it should show \"WT\" in a single cell"
-            L"\x1B[5;5HRegular: " DRCS_SEQUENCE L""
-            L"\x1B[7;3H\x1b#6DECDWL: " DRCS_SEQUENCE L""
-            L"\x1B[9;3H\x1b#3DECDHL: " DRCS_SEQUENCE L""
-            L"\x1B[10;3H\x1b#4DECDHL: " DRCS_SEQUENCE L""
+            L"\x1b[3;5HDECDLD and DRCS test - it should show \"WT\" in a single cell"
+            L"\x1b[5;5HRegular: " DRCS_SEQUENCE L""
+            L"\x1b[7;3H\x1b#6DECDWL: " DRCS_SEQUENCE L""
+            L"\x1b[9;3H\x1b#3DECDHL: " DRCS_SEQUENCE L""
+            L"\x1b[10;3H\x1b#4DECDHL: " DRCS_SEQUENCE L""
             // We map soft fonts into the private use area starting at U+EF20. This test ensures
             // that we correctly map actual fallback glyphs mixed into the DRCS glyphs.
-            L"\x1B[12;5HUnicode Fallback: \uE000\uE001" DRCS_SEQUENCE L"\uE003\uE004");
+            L"\x1b[12;5HUnicode Fallback: \uE000\uE001" DRCS_SEQUENCE L"\uE003\uE004");
 #undef DRCS_SEQUENCE
 
         wait();


Test patch: