Repository: microsoft/terminal (language: cpp)

Issue:
Extreme sluggishness and CPU usage when using Atlas inside a VM
### Windows Terminal version

1.13.10983.0

### Windows build number

10.0.19044.1645

### Other Software

None

### Steps to reproduce

- Use Terminal inside a typical VM
- Switch to the experimental Atlas engine
- Observe CPU load
- Try to type and experience sluggishness and even higher CPU load etc.  There's a decent amount of lag and output from long commands tends to stutter into view; it's not very smooth

### Expected Behavior

Was expecting that Atlas, in default/easy cases, would perform better than the old engine.

Looking ahead: IF atlas becomes the default and IF Terminal replaces all old cmd windows, then using this inside VMs is going to be a non-starter.

### Actual Behavior

Extreme CPU usage and general sluggishness. Witness the firepower of this fully operational battle... I mean barely affordable Azure VM displaying nothing but the flashing cursor inside a blank Terminal window:

![atlas-perf](https://user-images.githubusercontent.com/7989986/167744410-1576f18e-27e4-4b72-a4d4-457db9d059b9.png)
![atlas-dxdiag](https://user-images.githubusercontent.com/7989986/167744490-c0cf7d48-d8e6-4c6b-a441-7e1391e253b1.png)




Discussion / hints:
Hmmmm....

I wonder if this is because we don't do differential rendering with the Atlas engine, so we're making the RDC server diff and compress at >=60fps.

@lhecker One of the main reasons we wanted to use `Present1` with dirty regions was to give the graphics stack--including the remote desktop server--as much information as it needs to be successful. Is that totally off the table for Atlas? Over remote desktop, full region presentation might be so expensive that it's worthwhile for us to burn some CPU up front in Terminal.

> Witness the firepower of this fully operational battle... I mean barely affordable Azure VM

😁 
I think this is `19041.1620.vb_release_svc_prod3.220311-1803`.
I've created a similar VM using TDP and I have a RDP latency of >200ms:

![image](https://user-images.githubusercontent.com/2256941/168333956-da1d7e94-8c2d-4889-ba04-ee1342936ad9.png)

![image](https://user-images.githubusercontent.com/2256941/168334074-2f4ef8f5-8aca-43a7-9642-302646d386ad.png)

I could not replicate the extreme sluggishness. The CPU load however is at around 14% compared to 4% for DxEngine. Additionally the perceived framerate while printing text is much better with DxEngine.
I investigated this over the last few hours. My findings:
* Scroll rects/offsets, etc. don't help
* Device creation with `D3D_DRIVER_TYPE_HARDWARE` works, but there's no difference to `D3D_DRIVER_TYPE_WARP`, which indicates that there's no GPU. This makes sense, since it's a VM and Intel Xeon CPUs don't have iGPUs.
* `AtlasEngine` uses "dependent texture lookups" which I believe is killing the VM. I modified the shader to just draw a screen full of the cursor texture and that uses 40% "GPU", whereas the full shader with twice as many texture loads uses exactly twice that (basically using 80% CPU while rendering). Direct2D sends text straight over RDP and renders it on the client side.

Proposed solutions:
* Meet and greet with the RDP folks to figure out if there's _maybe_ a solution, but probably not. [Direct2D docs](https://docs.microsoft.com/en-us/windows/win32/direct2d/direct2d-overview#high-performance-with-maximum-availability):
  > Content that is rendered by using Direct2D can also be displayed remotely by using the Remote Desktop Protocol (RDP) infrastructure in the Windows 7 operating system.
* Dynamic resolution scaling during scrolling: We simply draw 4x less pixels while the screen is scrolling and drop the GPU load from 80% down to 20%. Example: https://docs.microsoft.com/en-us/windows/uwp/gaming/multisampling--scaling--and-overlay-swap-chains
  Most importantly however, this doesn't really solve the lag during typing...
* Implement a Direct2D-based engine inside AtlasEngine, which has worse support, but works well over RDP

In the meantime I believe that the `AtlasEngine` is unusable on Azure VMs without dedicated GPU.
@jessey-git Do you have VS Code installed in your VM? Does it's builtin terminal stutter just as bad for you? In my testing it does...
(Alternatively any other Terminal based on [xterm.js](https://xtermjs.org/), or an IDE made by Jetbrains, or any other software which isn't based on Direct2D.)

---

Some thoughts for the team:
We have this problem now, where the new renderer significantly improves performance on any GPU conceived in the last 10 years, but is significantly worse on any hardware older than that and those without a GPU at all (like your server), because they fall back to a pure software-GPU called WARP. How do we now reconcile the wishes for performance by the "many" who have long had such "new" hardware with the fundamental needs of a working terminal by the "few" who don't have such hardware? (Words in quotes are generalizations I'm making. I don't have the actual stats for these at hand. 😅)
My idea is that we could just implement a "poor man's DxEngine" inside AtlasEngine. One that supports all basic functionality, but is not the main-goal of development. It would work well on such problematic hardware, but I wouldn't necessarily implement double-height/width text or sixels, etc. Basically I'd focus on basic text rendering when no GPU is available only, with added support for all basic text attributes, color and selection. Thoughts?
@lhecker Yeah, that sucks too. Here's a comparison of what it looks like when holding down a key between Terminal DxEngine and VS Code's integrated terminal window:

[Terminal DxEngine Holding key]
![Terminal_DxEngine_HoldingKey](https://user-images.githubusercontent.com/7989986/168497922-b074ea9e-eb48-462f-a37b-516b256ab954.png)

[VS Code Integrated Holding key - nearly 4x higher]
![Code_Integrated_HoldingKey](https://user-images.githubusercontent.com/7989986/168497907-b28e1248-b371-4be6-ac53-3d638a397ada.png)


> My idea is that we could just implement a "poor man's DxEngine" inside AtlasEngine.

Honestly that just sounds like "keep the DxEngine" which is a lot easier ;)
> Honestly that just sounds like "keep the DxEngine" which is a lot easier ;)

We'd have to switch between the two engines, if you connect via RDP to your PC.
Also, we can reuse the DirectWrite parsing in AtlasEngine and deduplicate the code.

Fix patch:
diff --git a/src/renderer/atlas/AtlasEngine.api.cpp b/src/renderer/atlas/AtlasEngine.api.cpp
index b738d5ece4b..4a9bbe390ae 100644
--- a/src/renderer/atlas/AtlasEngine.api.cpp
+++ b/src/renderer/atlas/AtlasEngine.api.cpp
@@ -606,7 +606,8 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
     // Point sizes are commonly treated at a 72 DPI scale
     // (including by OpenType), whereas DirectWrite uses 96 DPI.
     // Since we want the height in px we multiply by the display's DPI.
-    const auto fontSizeInPx = std::roundf(requestedSize.Y / 72.0f * _api.dpi);
+    const auto fontSizeInDIP = requestedSize.Y / 72.0f * 96.0f;
+    const auto fontSizeInPx = requestedSize.Y / 72.0f * _api.dpi;
 
     const auto designUnitsPerPx = fontSizeInPx / static_cast<float>(metrics.designUnitsPerEm);
     const auto ascent = static_cast<float>(metrics.ascent) * designUnitsPerPx;
@@ -687,7 +688,7 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
 
         fontMetrics->fontCollection = std::move(fontCollection);
         fontMetrics->fontName = std::move(fontName);
-        fontMetrics->fontSizeInDIP = fontSizeInPx / static_cast<float>(_api.dpi) * 96.0f;
+        fontMetrics->fontSizeInDIP = fontSizeInDIP;
         fontMetrics->baselineInDIP = baseline / static_cast<float>(_api.dpi) * 96.0f;
         fontMetrics->advanceScale = cellWidth / advanceWidth;
         fontMetrics->cellSize = { cellWidth, cellHeight };
diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp
index fea31c0fb2d..92555aec0d6 100644
--- a/src/renderer/atlas/AtlasEngine.cpp
+++ b/src/renderer/atlas/AtlasEngine.cpp
@@ -110,6 +110,7 @@ try
         {
             _r.selectionColor = _api.selectionColor;
             WI_SetFlag(_r.invalidations, RenderInvalidations::ConstBuffer);
+            WI_ClearFlag(_api.invalidations, ApiInvalidations::Settings);
         }
 
         // Equivalent to InvalidateAll().
@@ -160,101 +161,91 @@ try
     }
 #endif
 
-    if (_api.invalidatedRows == invalidatedRowsAll)
+    if constexpr (debugGlyphGenerationPerformance)
+    {
+        _r.glyphs = {};
+        _r.tileAllocator = TileAllocator{ _api.fontMetrics.cellSize, _api.sizeInPixel };
+    }
+    if constexpr (debugTextParsingPerformance)
     {
-        // Skip all the partial updates, since we redraw everything anyways.
-        _api.invalidatedCursorArea = invalidatedAreaNone;
-        _api.invalidatedRows = { 0, _api.cellCount.y };
+        _api.invalidatedRows = invalidatedRowsAll;
         _api.scrollOffset = 0;
     }
-    else
+
+    // Clamp invalidation rects into valid value ranges.
     {
-        // Clamp invalidation rects into valid value ranges.
-        {
-            _api.invalidatedCursorArea.left = std::min(_api.invalidatedCursorArea.left, _api.cellCount.x);
-            _api.invalidatedCursorArea.top = std::min(_api.invalidatedCursorArea.top, _api.cellCount.y);
-            _api.invalidatedCursorArea.right = clamp(_api.invalidatedCursorArea.right, _api.invalidatedCursorArea.left, _api.cellCount.x);
-            _api.invalidatedCursorArea.bottom = clamp(_api.invalidatedCursorArea.bottom, _api.invalidatedCursorArea.top, _api.cellCount.y);
-        }
-        {
-            _api.invalidatedRows.x = std::min(_api.invalidatedRows.x, _api.cellCount.y);
-            _api.invalidatedRows.y = clamp(_api.invalidatedRows.y, _api.invalidatedRows.x, _api.cellCount.y);
-        }
+        _api.invalidatedCursorArea.left = std::min(_api.invalidatedCursorArea.left, _api.cellCount.x);
+        _api.invalidatedCursorArea.top = std::min(_api.invalidatedCursorArea.top, _api.cellCount.y);
+        _api.invalidatedCursorArea.right = clamp(_api.invalidatedCursorArea.right, _api.invalidatedCursorArea.left, _api.cellCount.x);
+        _api.invalidatedCursorArea.bottom = clamp(_api.invalidatedCursorArea.bottom, _api.invalidatedCursorArea.top, _api.cellCount.y);
+    }
+    {
+        _api.invalidatedRows.x = std::min(_api.invalidatedRows.x, _api.cellCount.y);
+        _api.invalidatedRows.y = clamp(_api.invalidatedRows.y, _api.invalidatedRows.x, _api.cellCount.y);
+    }
+    {
+        const auto limit = gsl::narrow_cast<i16>(_api.cellCount.y & 0x7fff);
+        _api.scrollOffset = gsl::narrow_cast<i16>(clamp<int>(_api.scrollOffset, -limit, limit));
+    }
+
+    // Scroll the buffer by the given offset and mark the newly uncovered rows as "invalid".
+    if (_api.scrollOffset != 0)
+    {
+        const auto nothingInvalid = _api.invalidatedRows.x == _api.invalidatedRows.y;
+        const auto offset = static_cast<ptrdiff_t>(_api.scrollOffset) * _api.cellCount.x;
+
+        if (_api.scrollOffset < 0)
         {
-            const auto limit = gsl::narrow_cast<i16>(_api.cellCount.y & 0x7fff);
-            _api.scrollOffset = gsl::narrow_cast<i16>(clamp<int>(_api.scrollOffset, -limit, limit));
+            // Scroll up (for instance when new text is being written at the end of the buffer).
+            const u16 endRow = _api.cellCount.y + _api.scrollOffset;
+            _api.invalidatedRows.x = nothingInvalid ? endRow : std::min<u16>(_api.invalidatedRows.x, endRow);
+            _api.invalidatedRows.y = _api.cellCount.y;
+
+            // scrollOffset/offset = -1
+            // +----------+    +----------+
+            // |          |    | xxxxxxxxx|         + dst  < beg
+            // | xxxxxxxxx| -> |xxxxxxx   |  + src  |      < beg - offset
+            // |xxxxxxx   |    |          |  |      v
+            // +----------+    +----------+  v             < end
+            {
+                const auto beg = _r.cells.begin();
+                const auto end = _r.cells.end();
+                std::move(beg - offset, end, beg);
+            }
+            {
+                const auto beg = _r.cellGlyphMapping.begin();
+                const auto end = _r.cellGlyphMapping.end();
+                std::move(beg - offset, end, beg);
+            }
         }
-
-        // Scroll the buffer by the given offset and mark the newly uncovered rows as "invalid".
-        if (_api.scrollOffset != 0)
+        else
         {
-            const auto nothingInvalid = _api.invalidatedRows.x == _api.invalidatedRows.y;
-            const auto offset = static_cast<ptrdiff_t>(_api.scrollOffset) * _api.cellCount.x;
-            auto count = _r.cells.size();
-
-            if (_api.scrollOffset < 0)
+            // Scroll down.
+            _api.invalidatedRows.x = 0;
+            _api.invalidatedRows.y = nothingInvalid ? _api.scrollOffset : std::max<u16>(_api.invalidatedRows.y, _api.scrollOffset);
+
+            // scrollOffset/offset = 1
+            // +----------+    +----------+
+            // | xxxxxxxxx|    |          |  + src         < beg
+            // |xxxxxxx   | -> | xxxxxxxxx|  |      ^
+            // |          |    |xxxxxxx   |  v      |      < end - offset
+            // +----------+    +----------+         + dst  < end
             {
-                // Scroll up (for instance when new text is being written at the end of the buffer).
-                const u16 endRow = _api.cellCount.y + _api.scrollOffset;
-                _api.invalidatedRows.x = nothingInvalid ? endRow : std::min<u16>(_api.invalidatedRows.x, endRow);
-                _api.invalidatedRows.y = _api.cellCount.y;
-
-                // scrollOffset/offset = -1
-                // +----------+    +----------+
-                // |          |    | xxxxxxxxx|         + dst  < beg
-                // | xxxxxxxxx| -> |xxxxxxx   |  + src  |      < beg - offset
-                // |xxxxxxx   |    |          |  |      v
-                // +----------+    +----------+  v             < end
-                {
-                    const auto beg = _r.cells.begin();
-                    const auto end = beg + count;
-                    std::move(beg - offset, end, beg);
-                }
-                {
-                    const auto beg = _r.cellGlyphMapping.begin();
-                    const auto end = beg + count;
-                    std::move(beg - offset, end, beg);
-                }
+                const auto beg = _r.cells.begin();
+                const auto end = _r.cells.end();
+                std::move_backward(beg, end - offset, end);
             }
-            else
             {
-                // Scroll down.
-                _api.invalidatedRows.x = 0;
-                _api.invalidatedRows.y = nothingInvalid ? _api.scrollOffset : std::max<u16>(_api.invalidatedRows.y, _api.scrollOffset);
-
-                // scrollOffset/offset = 1
-                // +----------+    +----------+
-                // | xxxxxxxxx|    |          |  + src         < beg
-                // |xxxxxxx   | -> | xxxxxxxxx|  |      ^
-                // |          |    |xxxxxxx   |  v      |      < end - offset
-                // +----------+    +----------+         + dst  < end
-                {
-                    const auto beg = _r.cells.begin();
-                    const auto end = beg + count;
-                    std::move_backward(beg, end - offset, end);
-                }
-                {
-                    const auto beg = _r.cellGlyphMapping.begin();
-                    const auto end = beg + count;
-                    std::move_backward(beg, end - offset, end);
-                }
+                const auto beg = _r.cellGlyphMapping.begin();
+                const auto end = _r.cellGlyphMapping.end();
+                std::move_backward(beg, end - offset, end);
             }
         }
     }
 
-    if constexpr (debugGlyphGenerationPerformance)
-    {
-        _r.glyphs = {};
-        _r.tileAllocator = TileAllocator{ _api.fontMetrics.cellSize, _api.sizeInPixel };
-    }
-    if constexpr (debugTextParsingPerformance)
-    {
-        _api.dirtyRect = til::rect{ 0, 0, _api.cellCount.x, _api.cellCount.y };
-    }
-    else
-    {
-        _api.dirtyRect = til::rect{ 0, _api.invalidatedRows.x, _api.cellCount.x, _api.invalidatedRows.y };
-    }
+    _api.dirtyRect = til::rect{ 0, _api.invalidatedRows.x, _api.cellCount.x, _api.invalidatedRows.y };
+    _r.dirtyRect = _api.dirtyRect;
+    _r.scrollOffset = _api.scrollOffset;
 
     // This is an important block of code for our TileHashMap.
     // We only process glyphs within the dirtyRect, but glyphs outside of the
@@ -317,7 +308,10 @@ CATCH_RETURN()
 
 void AtlasEngine::WaitUntilCanRender() noexcept
 {
-    if constexpr (!debugGeneralPerformance)
+    // IDXGISwapChain2::GetFrameLatencyWaitableObject returns an auto-reset event.
+    // Once we've waited on the event, waiting on it again will block until the timeout elapses.
+    // _r.waitForPresentation guards against this.
+    if (!debugGeneralPerformance && std::exchange(_r.waitForPresentation, false))
     {
         WaitForSingleObjectEx(_r.frameLatencyWaitableObject.get(), 100, true);
 #ifndef NDEBUG
@@ -434,6 +428,7 @@ try
         rect.narrow_bottom<u16>(),
     };
     _setCellFlags(u16rect, CellFlags::Selected, CellFlags::Selected);
+    _r.dirtyRect |= rect;
     return S_OK;
 }
 CATCH_RETURN()
@@ -460,9 +455,10 @@ try
     }
 
     // Clear the previous cursor
-    if (_api.invalidatedCursorArea.non_empty())
+    if (const auto r = _api.invalidatedCursorArea; r.non_empty())
     {
-        _setCellFlags(_api.invalidatedCursorArea, CellFlags::Cursor, CellFlags::None);
+        _setCellFlags(r, CellFlags::Cursor, CellFlags::None);
+        _r.dirtyRect |= til::rect{ r.left, r.top, r.right, r.bottom };
     }
 
     if (options.isOn)
@@ -476,6 +472,7 @@ try
         const auto right = gsl::narrow_cast<uint16_t>(clamp(x + cursorWidth, 0, _r.cellCount.x - 0));
         const auto bottom = gsl::narrow_cast<uint16_t>(y + 1);
         _setCellFlags({ x, y, right, bottom }, CellFlags::Cursor, CellFlags::Cursor);
+        _r.dirtyRect |= til::rect{ x, y, right, bottom };
     }
 
     return S_OK;
@@ -636,6 +633,16 @@ void AtlasEngine::_createResources()
         _r.deviceContext = deviceContext.query<ID3D11DeviceContext1>();
     }
 
+    {
+        wil::com_ptr<IDXGIAdapter1> dxgiAdapter;
+        THROW_IF_FAILED(_r.device.query<IDXGIObject>()->GetParent(__uuidof(dxgiAdapter), dxgiAdapter.put_void()));
+        THROW_IF_FAILED(dxgiAdapter->GetParent(__uuidof(_r.dxgiFactory), _r.dxgiFactory.put_void()));
+
+        DXGI_ADAPTER_DESC1 desc;
+        THROW_IF_FAILED(dxgiAdapter->GetDesc1(&desc));
+        _r.d2dMode = debugForceD2DMode || WI_IsAnyFlagSet(desc.Flags, DXGI_ADAPTER_FLAG_REMOTE | DXGI_ADAPTER_FLAG_SOFTWARE);
+    }
+
 #ifndef NDEBUG
     // D3D debug messages
     if (deviceFlags & D3D11_CREATE_DEVICE_DEBUG)
@@ -648,17 +655,20 @@ void AtlasEngine::_createResources()
     }
 #endif // NDEBUG
 
-    // Our constant buffer will never get resized
+    if (!_r.d2dMode)
     {
-        D3D11_BUFFER_DESC desc{};
-        desc.ByteWidth = sizeof(ConstBuffer);
-        desc.Usage = D3D11_USAGE_DEFAULT;
-        desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
-        THROW_IF_FAILED(_r.device->CreateBuffer(&desc, nullptr, _r.constantBuffer.put()));
-    }
+        // Our constant buffer will never get resized
+        {
+            D3D11_BUFFER_DESC desc{};
+            desc.ByteWidth = sizeof(ConstBuffer);
+            desc.Usage = D3D11_USAGE_DEFAULT;
+            desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
+            THROW_IF_FAILED(_r.device->CreateBuffer(&desc, nullptr, _r.constantBuffer.put()));
+        }
 
-    THROW_IF_FAILED(_r.device->CreateVertexShader(&shader_vs[0], sizeof(shader_vs), nullptr, _r.vertexShader.put()));
-    THROW_IF_FAILED(_r.device->CreatePixelShader(&shader_ps[0], sizeof(shader_ps), nullptr, _r.pixelShader.put()));
+        THROW_IF_FAILED(_r.device->CreateVertexShader(&shader_vs[0], sizeof(shader_vs), nullptr, _r.vertexShader.put()));
+        THROW_IF_FAILED(_r.device->CreatePixelShader(&shader_ps[0], sizeof(shader_ps), nullptr, _r.pixelShader.put()));
+    }
 
     WI_ClearFlag(_api.invalidations, ApiInvalidations::Device);
     WI_SetAllFlags(_api.invalidations, ApiInvalidations::SwapChain);
@@ -673,6 +683,10 @@ void AtlasEngine::_releaseSwapChain()
     //   no views are bound to pipeline state), and then call Flush on the immediate context.
     if (_r.swapChain && _r.deviceContext)
     {
+        if (_r.d2dMode)
+        {
+            _r.d2dRenderTarget.reset();
+        }
         _r.frameLatencyWaitableObject.reset();
         _r.swapChain.reset();
         _r.renderTargetView.reset();
@@ -694,9 +708,9 @@ void AtlasEngine::_createSwapChain()
         desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
         desc.SampleDesc.Count = 1;
         desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
-        desc.BufferCount = 2; // TODO: 3?
+        desc.BufferCount = 2;
         desc.Scaling = DXGI_SCALING_NONE;
-        desc.SwapEffect = _sr.isWindows10OrGreater ? DXGI_SWAP_EFFECT_FLIP_DISCARD : DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
+        desc.SwapEffect = _sr.isWindows10OrGreater && !_r.d2dMode ? DXGI_SWAP_EFFECT_FLIP_DISCARD : DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
         // * HWND swap chains can't do alpha.
         // * If our background is opaque we can enable "independent" flips by setting DXGI_SWAP_EFFECT_FLIP_DISCARD and DXGI_ALPHA_MODE_IGNORE.
         //   As our swap chain won't have to compose with DWM anymore it reduces the display latency dramatically.
@@ -734,6 +748,7 @@ void AtlasEngine::_createSwapChain()
     // See documentation for IDXGISwapChain2::GetFrameLatencyWaitableObject method:
     // > For every frame it renders, the app should wait on this handle before starting any rendering operations.
     // > Note that this requirement includes the first frame the app renders with the swap chain.
+    _r.waitForPresentation = true;
     WaitUntilCanRender();
 
     if (_api.swapChainChangedCallback)
@@ -754,43 +769,29 @@ void AtlasEngine::_recreateSizeDependentResources()
     // ResizeBuffer() docs:
     //   Before you call ResizeBuffers, ensure that the application releases all references [...].
     //   You can use ID3D11DeviceContext::ClearState to ensure that all [internal] references are released.
-    if (_r.renderTargetView)
+    // The _r.cells check exists simply to prevent us from calling ResizeBuffers() on startup (i.e. when `_r` is empty).
+    if (_r.cells)
     {
+        if (_r.d2dMode)
+        {
+            _r.d2dRenderTarget.reset();
+        }
         _r.renderTargetView.reset();
         _r.deviceContext->ClearState();
         _r.deviceContext->Flush();
-        THROW_IF_FAILED(_r.swapChain->ResizeBuffers(0, _api.sizeInPixel.x, _api.sizeInPixel.y, DXGI_FORMAT_UNKNOWN, DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT));
+        THROW_IF_FAILED(_r.swapChain->ResizeBuffers(0, _api.sizeInPixel.x, _api.sizeInPixel.y, DXGI_FORMAT_UNKNOWN, debugGeneralPerformance ? 0 : DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT));
     }
 
-    // The RenderTargetView is later used with OMSetRenderTargets
-    // to tell D3D where stuff is supposed to be rendered at.
-    {
-        wil::com_ptr<ID3D11Texture2D> buffer;
-        THROW_IF_FAILED(_r.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), buffer.put_void()));
-        THROW_IF_FAILED(_r.device->CreateRenderTargetView(buffer.get(), nullptr, _r.renderTargetView.put()));
-    }
-
-    // Tell D3D which parts of the render target will be visible.
-    // Everything outside of the viewport will be black.
-    //
-    // In the future this should cover the entire _api.sizeInPixel.x/_api.sizeInPixel.y.
-    // The pixel shader should draw the remaining content in the configured background color.
-    {
-        D3D11_VIEWPORT viewport{};
-        viewport.Width = static_cast<float>(_api.sizeInPixel.x);
-        viewport.Height = static_cast<float>(_api.sizeInPixel.y);
-        _r.deviceContext->RSSetViewports(1, &viewport);
-    }
+    const auto totalCellCount = static_cast<size_t>(_api.cellCount.x) * static_cast<size_t>(_api.cellCount.y);
+    const auto resize = _api.cellCount != _r.cellCount;
 
-    if (_api.cellCount != _r.cellCount)
+    if (resize)
     {
-        const auto totalCellCount = static_cast<size_t>(_api.cellCount.x) * static_cast<size_t>(_api.cellCount.y);
         // Let's guess that every cell consists of a surrogate pair.
         const auto projectedTextSize = static_cast<size_t>(_api.cellCount.x) * 2;
         // IDWriteTextAnalyzer::GetGlyphs says:
         //   The recommended estimate for the per-glyph output buffers is (3 * textLength / 2 + 16).
-        // We already set the textLength to twice the cell count.
-        const auto projectedGlyphSize = 3 * projectedTextSize + 16;
+        const auto projectedGlyphSize = 3 * projectedTextSize / 2 + 16;
 
         // This buffer is a bit larger than the others (multiple MB).
         // Prevent a memory usage spike, by first deallocating and then allocating.
@@ -818,21 +819,44 @@ void AtlasEngine::_recreateSizeDependentResources()
         _api.glyphProps = Buffer<DWRITE_SHAPING_GLYPH_PROPERTIES>{ projectedGlyphSize };
         _api.glyphAdvances = Buffer<f32>{ projectedGlyphSize };
         _api.glyphOffsets = Buffer<DWRITE_GLYPH_OFFSET>{ projectedGlyphSize };
-
-        D3D11_BUFFER_DESC desc;
-        desc.ByteWidth = gsl::narrow<u32>(totalCellCount * sizeof(Cell)); // totalCellCount can theoretically be UINT32_MAX!
-        desc.Usage = D3D11_USAGE_DYNAMIC;
-        desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
-        desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
-        desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;
-        desc.StructureByteStride = sizeof(Cell);
-        THROW_IF_FAILED(_r.device->CreateBuffer(&desc, nullptr, _r.cellBuffer.put()));
-        THROW_IF_FAILED(_r.device->CreateShaderResourceView(_r.cellBuffer.get(), nullptr, _r.cellView.put()));
     }
 
-    // We have called _r.deviceContext->ClearState() in the beginning and lost all D3D state.
-    // This forces us to set up everything up from scratch again.
-    _setShaderResources();
+    if (!_r.d2dMode)
+    {
+        // The RenderTargetView is later used with OMSetRenderTargets
+        // to tell D3D where stuff is supposed to be rendered at.
+        {
+            wil::com_ptr<ID3D11Texture2D> buffer;
+            THROW_IF_FAILED(_r.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), buffer.put_void()));
+            THROW_IF_FAILED(_r.device->CreateRenderTargetView(buffer.get(), nullptr, _r.renderTargetView.put()));
+        }
+
+        // Tell D3D which parts of the render target will be visible.
+        // Everything outside of the viewport will be black.
+        {
+            D3D11_VIEWPORT viewport{};
+            viewport.Width = static_cast<float>(_api.sizeInPixel.x);
+            viewport.Height = static_cast<float>(_api.sizeInPixel.y);
+            _r.deviceContext->RSSetViewports(1, &viewport);
+        }
+
+        if (resize)
+        {
+            D3D11_BUFFER_DESC desc;
+            desc.ByteWidth = gsl::narrow<u32>(totalCellCount * sizeof(Cell)); // totalCellCount can theoretically be UINT32_MAX!
+            desc.Usage = D3D11_USAGE_DYNAMIC;
+            desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
+            desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
+            desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;
+            desc.StructureByteStride = sizeof(Cell);
+            THROW_IF_FAILED(_r.device->CreateBuffer(&desc, nullptr, _r.cellBuffer.put()));
+            THROW_IF_FAILED(_r.device->CreateShaderResourceView(_r.cellBuffer.get(), nullptr, _r.cellView.put()));
+        }
+
+        // We have called _r.deviceContext->ClearState() in the beginning and lost all D3D state.
+        // This forces us to set up everything up from scratch again.
+        _setShaderResources();
+    }
 
     WI_ClearFlag(_api.invalidations, ApiInvalidations::Size);
     WI_SetAllFlags(_r.invalidations, RenderInvalidations::ConstBuffer);
@@ -1387,7 +1411,7 @@ bool AtlasEngine::_emplaceGlyph(IDWriteFontFace* fontFace, size_t bufferPos1, si
         }
 
         it = _r.glyphs.insert(std::move(key), std::move(value));
-        _r.glyphQueue.emplace_back(&it->first, &it->second);
+        _r.glyphQueue.emplace_back(it);
     }
 
     const auto valueData = it->second.data();
diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h
index 78c3a02df4a..347dab8083d 100644
--- a/src/renderer/atlas/AtlasEngine.h
+++ b/src/renderer/atlas/AtlasEngine.h
@@ -99,8 +99,10 @@ namespace Microsoft::Console::Render
     friend constexpr type operator~(type v) noexcept { return static_cast<type>(~static_cast<underlying>(v)); }                                             \
     friend constexpr type operator|(type lhs, type rhs) noexcept { return static_cast<type>(static_cast<underlying>(lhs) | static_cast<underlying>(rhs)); } \
     friend constexpr type operator&(type lhs, type rhs) noexcept { return static_cast<type>(static_cast<underlying>(lhs) & static_cast<underlying>(rhs)); } \
+    friend constexpr type operator^(type lhs, type rhs) noexcept { return static_cast<type>(static_cast<underlying>(lhs) ^ static_cast<underlying>(rhs)); } \
     friend constexpr void operator|=(type& lhs, type rhs) noexcept { lhs = lhs | rhs; }                                                                     \
-    friend constexpr void operator&=(type& lhs, type rhs) noexcept { lhs = lhs & rhs; }
+    friend constexpr void operator&=(type& lhs, type rhs) noexcept { lhs = lhs & rhs; }                                                                     \
+    friend constexpr void operator^=(type& lhs, type rhs) noexcept { lhs = lhs ^ rhs; }
 
         template<typename T>
         struct vec2
@@ -132,7 +134,7 @@ namespace Microsoft::Console::Render
 
             ATLAS_POD_OPS(rect)
 
-            constexpr bool non_empty() noexcept
+            constexpr bool non_empty() const noexcept
             {
                 return (left < right) & (top < bottom);
             }
@@ -347,7 +349,8 @@ namespace Microsoft::Console::Render
 
             SmallObjectOptimizer& operator=(SmallObjectOptimizer&& other) noexcept
             {
-                return *new (this) SmallObjectOptimizer(other);
+                std::destroy_at(this);
+                return *std::construct_at(this, std::move(other));
             }
 
             ~SmallObjectOptimizer()
@@ -507,6 +510,21 @@ namespace Microsoft::Console::Render
             }
         };
 
+        struct CachedGlyphLayout
+        {
+            wil::com_ptr<IDWriteTextLayout> textLayout;
+            f32x2 halfSize;
+            f32x2 offset;
+            f32x2 scale;
+            D2D1_DRAW_TEXT_OPTIONS options = D2D1_DRAW_TEXT_OPTIONS_NONE;
+            bool scalingRequired = false;
+
+            explicit operator bool() const noexcept;
+            void reset() noexcept;
+            void applyScaling(ID2D1RenderTarget* d2dRenderTarget, D2D1_POINT_2F origin) const noexcept;
+            void undoScaling(ID2D1RenderTarget* d2dRenderTarget) const noexcept;
+        };
+
         struct AtlasValueData
         {
             CellFlags flags = CellFlags::None;
@@ -530,6 +548,8 @@ namespace Microsoft::Console::Render
                 return _data.data();
             }
 
+            CachedGlyphLayout cachedLayout;
+
         private:
             SmallObjectOptimizer<AtlasValueData> _data;
 
@@ -539,12 +559,6 @@ namespace Microsoft::Console::Render
             }
         };
 
-        struct AtlasQueueItem
-        {
-            const AtlasKey* key;
-            const AtlasValue* value;
-        };
-
         struct AtlasKeyHasher
         {
             using is_transparent = int;
@@ -899,9 +913,24 @@ namespace Microsoft::Console::Render
         void _updateConstantBuffer() const noexcept;
         void _adjustAtlasSize();
         void _processGlyphQueue();
-        void _drawGlyph(const AtlasQueueItem& item) const;
-        void _drawCursor();
-
+        void _drawGlyph(const TileHashMap::iterator& it) const;
+        CachedGlyphLayout _getCachedGlyphLayout(const wchar_t* chars, u16 charsLength, u16 cellCount, IDWriteTextFormat* textFormat, bool coloredGlyph) const;
+        void _drawCursor(u16r rect, u32 color, bool clear);
+        ID2D1Brush* _brushWithColor(u32 color);
+        void _d2dPresent();
+        void _d2dCreateRenderTarget();
+        void _d2dDrawDirtyArea();
+        u16 _d2dDrawGlyph(const TileHashMap::iterator& it, u16x2 coord, u32 color);
+        void _d2dDrawLine(u16r rect, u16 pos, u16 width, u32 color, ID2D1StrokeStyle* strokeStyle = nullptr);
+        void _d2dFillRectangle(u16r rect, u32 color);
+        void _d2dCellFlagRendererCursor(u16r rect, u32 color);
+        void _d2dCellFlagRendererSelected(u16r rect, u32 color);
+        void _d2dCellFlagRendererUnderline(u16r rect, u32 color);
+        void _d2dCellFlagRendererUnderlineDotted(u16r rect, u32 color);
+        void _d2dCellFlagRendererUnderlineDouble(u16r rect, u32 color);
+        void _d2dCellFlagRendererStrikethrough(u16r rect, u32 color);
+
+        static constexpr bool debugForceD2DMode = false;
         static constexpr bool debugGlyphGenerationPerformance = false;
         static constexpr bool debugTextParsingPerformance = false || debugGlyphGenerationPerformance;
         static constexpr bool debugGeneralPerformance = false || debugTextParsingPerformance;
@@ -931,6 +960,9 @@ namespace Microsoft::Console::Render
 
         struct Resources
         {
+            // DXGI resources
+            wil::com_ptr<IDXGIFactory1> dxgiFactory;
+
             // D3D resources
             wil::com_ptr<ID3D11Device> device;
             wil::com_ptr<ID3D11DeviceContext1> deviceContext;
@@ -947,10 +979,11 @@ namespace Microsoft::Console::Render
             wil::com_ptr<ID3D11Texture2D> atlasBuffer;
             wil::com_ptr<ID3D11ShaderResourceView> atlasView;
             wil::com_ptr<ID2D1RenderTarget> d2dRenderTarget;
-            wil::com_ptr<ID2D1Brush> brush;
+            wil::com_ptr<ID2D1SolidColorBrush> brush;
             wil::com_ptr<IDWriteTextFormat> textFormats[2][2];
             Buffer<DWRITE_FONT_AXIS_VALUE> textFormatAxes[2][2];
             wil::com_ptr<IDWriteTypography> typography;
+            wil::com_ptr<ID2D1StrokeStyle> dottedStrokeStyle;
 
             Buffer<Cell, 32> cells; // invalidated by ApiInvalidations::Size
             Buffer<TileHashMap::iterator> cellGlyphMapping; // invalidated by ApiInvalidations::Size
@@ -963,17 +996,24 @@ namespace Microsoft::Console::Render
             u16x2 atlasSizeInPixel; // invalidated by ApiInvalidations::Font
             TileHashMap glyphs;
             TileAllocator tileAllocator;
-            std::vector<AtlasQueueItem> glyphQueue;
+            std::vector<TileHashMap::iterator> glyphQueue;
 
             f32 gamma = 0;
             f32 cleartypeEnhancedContrast = 0;
             f32 grayscaleEnhancedContrast = 0;
             u32 backgroundColor = 0xff000000;
             u32 selectionColor = 0x7fffffff;
+            u32 brushColor = 0xffffffff;
 
             CachedCursorOptions cursorOptions;
             RenderInvalidations invalidations = RenderInvalidations::None;
 
+            til::rect previousDirtyRectInPx;
+            til::rect dirtyRect;
+            i16 scrollOffset = 0;
+            bool d2dMode = false;
+            bool waitForPresentation = false;
+
 #ifndef NDEBUG
             // See documentation for IDXGISwapChain2::GetFrameLatencyWaitableObject method:
             // > For every frame it renders, the app should wait on this handle before starting any rendering operations.
diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp
index 75f9e9cf930..5bc00a0225e 100644
--- a/src/renderer/atlas/AtlasEngine.r.cpp
+++ b/src/renderer/atlas/AtlasEngine.r.cpp
@@ -44,6 +44,15 @@ constexpr bool isInInversionList(const std::array<wchar_t, N>& ranges, wchar_t n
     return (idx & 1) != 0;
 }
 
+constexpr D2D1_COLOR_F colorFromU32(uint32_t rgba)
+{
+    const auto r = static_cast<float>((rgba >> 0) & 0xff) / 255.0f;
+    const auto g = static_cast<float>((rgba >> 8) & 0xff) / 255.0f;
+    const auto b = static_cast<float>((rgba >> 16) & 0xff) / 255.0f;
+    const auto a = static_cast<float>((rgba >> 24) & 0xff) / 255.0f;
+    return { r, g, b, a };
+}
+
 using namespace Microsoft::Console::Render;
 
 #pragma region IRenderEngine
@@ -53,55 +62,51 @@ using namespace Microsoft::Console::Render;
 [[nodiscard]] HRESULT AtlasEngine::Present() noexcept
 try
 {
-    _adjustAtlasSize();
-    _processGlyphQueue();
+    // See documentation for IDXGISwapChain2::GetFrameLatencyWaitableObject method:
+    // > For every frame it renders, the app should wait on this handle before starting any rendering operations.
+    // > Note that this requirement includes the first frame the app renders with the swap chain.
+    assert(debugGeneralPerformance || _r.frameLatencyWaitableObjectUsed);
 
-    if (WI_IsFlagSet(_r.invalidations, RenderInvalidations::Cursor))
+    if (_r.d2dMode) [[unlikely]]
     {
-        _drawCursor();
-        WI_ClearFlag(_r.invalidations, RenderInvalidations::Cursor);
+        _d2dPresent();
     }
-
-    // The values the constant buffer depends on are potentially updated after BeginPaint().
-    if (WI_IsFlagSet(_r.invalidations, RenderInvalidations::ConstBuffer))
+    else
     {
-        _updateConstantBuffer();
-        WI_ClearFlag(_r.invalidations, RenderInvalidations::ConstBuffer);
-    }
+        _adjustAtlasSize();
+        _processGlyphQueue();
 
-    {
-#pragma warning(suppress : 26494) // Variable 'mapped' is uninitialized. Always initialize an object (type.5).
-        D3D11_MAPPED_SUBRESOURCE mapped;
-        THROW_IF_FAILED(_r.deviceContext->Map(_r.cellBuffer.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped));
-        assert(mapped.RowPitch >= _r.cells.size() * sizeof(Cell));
-        memcpy(mapped.pData, _r.cells.data(), _r.cells.size() * sizeof(Cell));
-        _r.deviceContext->Unmap(_r.cellBuffer.get(), 0);
-    }
+        // The values the constant buffer depends on are potentially updated after BeginPaint().
+        if (WI_IsFlagSet(_r.invalidations, RenderInvalidations::ConstBuffer))
+        {
+            _updateConstantBuffer();
+            WI_ClearFlag(_r.invalidations, RenderInvalidations::ConstBuffer);
+        }
 
-    // After Present calls, the back buffer needs to explicitly be
-    // re-bound to the D3D11 immediate context before it can be used again.
-    _r.deviceContext->OMSetRenderTargets(1, _r.renderTargetView.addressof(), nullptr);
-    _r.deviceContext->Draw(3, 0);
+        {
+#pragma warning(suppress : 26494) // Variable 'mapped' is uninitialized. Always initialize an object (type.5).
+            D3D11_MAPPED_SUBRESOURCE mapped;
+            THROW_IF_FAILED(_r.deviceContext->Map(_r.cellBuffer.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped));
+            assert(mapped.RowPitch >= _r.cells.size() * sizeof(Cell));
+            memcpy(mapped.pData, _r.cells.data(), _r.cells.size() * sizeof(Cell));
+            _r.deviceContext->Unmap(_r.cellBuffer.get(), 0);
+        }
 
-    // See documentation for IDXGISwapChain2::GetFrameLatencyWaitableObject method:
-    // > For every frame it renders, the app should wait on this handle before starting any rendering operations.
-    // > Note that this requirement includes the first frame the app renders with the swap chain.
-    assert(debugGeneralPerformance || _r.frameLatencyWaitableObjectUsed);
+        // After Present calls, the back buffer needs to explicitly be
+        // re-bound to the D3D11 immediate context before it can be used again.
+        _r.deviceContext->OMSetRenderTargets(1, _r.renderTargetView.addressof(), nullptr);
+        _r.deviceContext->Draw(3, 0);
 
-    // > IDXGISwapChain::Present: Partial Presentation (using a dirty rects or scroll) is not supported
-    // > for SwapChains created with DXGI_SWAP_EFFECT_DISCARD or DXGI_SWAP_EFFECT_FLIP_DISCARD.
-    // ---> No need to call IDXGISwapChain1::Present1.
-    //      TODO: Would IDXGISwapChain1::Present1 and its dirty rects have benefits for remote desktop?
-    THROW_IF_FAILED(_r.swapChain->Present(1, 0));
+        // > IDXGISwapChain::Present: Partial Presentation (using a dirty rects or scroll) is not supported
+        // > for SwapChains created with DXGI_SWAP_EFFECT_DISCARD or DXGI_SWAP_EFFECT_FLIP_DISCARD.
+        // ---> No need to call IDXGISwapChain1::Present1.
+        THROW_IF_FAILED(_r.swapChain->Present(1, 0));
+        _r.waitForPresentation = true;
+    }
 
-    // On some GPUs with tile based deferred rendering (TBDR) architectures, binding
-    // RenderTargets that already have contents in them (from previous rendering) incurs a
-    // cost for having to copy the RenderTarget contents back into tile memory for rendering.
-    //
-    // On Windows 10 with DXGI_SWAP_EFFECT_FLIP_DISCARD we get this for free.
-    if (!_sr.isWindows10OrGreater)
+    if (!_r.dxgiFactory->IsCurrent())
     {
-        _r.deviceContext->DiscardView(_r.renderTargetView.get());
+        WI_SetFlag(_api.invalidations, ApiInvalidations::Device);
     }
 
     return S_OK;
@@ -205,13 +210,8 @@ void AtlasEngine::_adjustAtlasSize()
         _r.deviceContext->CopySubresourceRegion1(atlasBuffer.get(), 0, 0, 0, 0, _r.atlasBuffer.get(), 0, &box, D3D11_COPY_NO_OVERWRITE);
     }
 
-    _r.atlasSizeInPixel = requiredSize;
-    _r.atlasBuffer = std::move(atlasBuffer);
-    _r.atlasView = std::move(atlasView);
-    _setShaderResources();
-
     {
-        const auto surface = _r.atlasBuffer.query<IDXGISurface>();
+        const auto surface = atlasBuffer.query<IDXGISurface>();
 
         wil::com_ptr<IDWriteRenderingParams1> renderingParams;
         DWrite_GetRenderParams(_sr.dwriteFactory.get(), &_r.gamma, &_r.cleartypeEnhancedContrast, &_r.grayscaleEnhancedContrast, renderingParams.addressof());
@@ -234,55 +234,53 @@ void AtlasEngine::_adjustAtlasSize()
     }
     {
         static constexpr D2D1_COLOR_F color{ 1, 1, 1, 1 };
-        wil::com_ptr<ID2D1SolidColorBrush> brush;
-        THROW_IF_FAILED(_r.d2dRenderTarget->CreateSolidColorBrush(&color, nullptr, brush.addressof()));
-        _r.brush = brush.query<ID2D1Brush>();
+        THROW_IF_FAILED(_r.d2dRenderTarget->CreateSolidColorBrush(&color, nullptr, _r.brush.put()));
+        _r.brushColor = 0xffffffff;
     }
 
+    _r.atlasSizeInPixel = requiredSize;
+    _r.atlasBuffer = std::move(atlasBuffer);
+    _r.atlasView = std::move(atlasView);
+    _setShaderResources();
+
     WI_SetAllFlags(_r.invalidations, RenderInvalidations::ConstBuffer);
     WI_SetFlagIf(_r.invalidations, RenderInvalidations::Cursor, !copyFromExisting);
 }
 
 void AtlasEngine::_processGlyphQueue()
 {
-    if (_r.glyphQueue.empty())
+    if (_r.glyphQueue.empty() && WI_IsFlagClear(_r.invalidations, RenderInvalidations::Cursor))
     {
         return;
     }
 
     _r.d2dRenderTarget->BeginDraw();
-    for (const auto& pair : _r.glyphQueue)
+
+    if (WI_IsFlagSet(_r.invalidations, RenderInvalidations::Cursor))
     {
-        _drawGlyph(pair);
+        _drawCursor({ 0, 0, 1, 1 }, 0xffffffff, true);
+        WI_ClearFlag(_r.invalidations, RenderInvalidations::Cursor);
     }
-    THROW_IF_FAILED(_r.d2dRenderTarget->EndDraw());
 
+    for (const auto& it : _r.glyphQueue)
+    {
+        _drawGlyph(it);
+    }
     _r.glyphQueue.clear();
+
+    THROW_IF_FAILED(_r.d2dRenderTarget->EndDraw());
 }
 
-void AtlasEngine::_drawGlyph(const AtlasQueueItem& item) const
+void AtlasEngine::_drawGlyph(const TileHashMap::iterator& it) const
 {
-    const auto key = item.key->data();
-    const auto value = item.value->data();
+    const auto key = it->first.data();
+    const auto value = it->second.data();
     const auto coords = &value->coords[0];
     const auto charsLength = key->charCount;
-    const auto cellCount = static_cast<u32>(key->attributes.cellCount);
+    const auto cellCount = key->attributes.cellCount;
     const auto textFormat = _getTextFormat(key->attributes.bold, key->attributes.italic);
     const auto coloredGlyph = WI_IsFlagSet(value->flags, CellFlags::ColoredGlyph);
-    const f32x2 layoutBox{ cellCount * _r.cellSizeDIP.x, _r.cellSizeDIP.y };
-
-    // See D2DFactory::DrawText
-    wil::com_ptr<IDWriteTextLayout> textLayout;
-    THROW_IF_FAILED(_sr.dwriteFactory->CreateTextLayout(&key->chars[0], charsLength, textFormat, layoutBox.x, layoutBox.y, textLayout.addressof()));
-    if (_r.typography)
-    {
-        textLayout->SetTypography(_r.typography.get(), { 0, charsLength });
-    }
-
-    auto options = D2D1_DRAW_TEXT_OPTIONS_NONE;
-    // D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT enables a bunch of internal machinery
-    // which doesn't have to run if we know we can't use it anyways in the shader.
-    WI_SetFlagIf(options, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT, coloredGlyph);
+    const auto cachedLayout = _getCachedGlyphLayout(&key->chars[0], charsLength, cellCount, textFormat, coloredGlyph);
 
     // Colored glyphs cannot be drawn in linear gamma.
     // That's why we're simply alpha-blending them in the shader.
@@ -293,11 +291,53 @@ void AtlasEngine::_drawGlyph(const AtlasQueueItem& item) const
         _r.d2dRenderTarget->SetTextAntialiasMode(coloredGlyph ? D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE : D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE);
     }
 
+    for (u16 i = 0; i < cellCount; ++i)
+    {
+        const auto coord = coords[i];
+
+        D2D1_RECT_F rect;
+        rect.left = static_cast<float>(coord.x) * _r.dipPerPixel;
+        rect.top = static_cast<float>(coord.y) * _r.dipPerPixel;
+        rect.right = rect.left + _r.cellSizeDIP.x;
+        rect.bottom = rect.top + _r.cellSizeDIP.y;
+
+        D2D1_POINT_2F origin;
+        origin.x = rect.left - i * _r.cellSizeDIP.x;
+        origin.y = rect.top;
+
+        _r.d2dRenderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED);
+        _r.d2dRenderTarget->Clear();
+
+        cachedLayout.applyScaling(_r.d2dRenderTarget.get(), origin);
+
+        // Now that we're done using origin to calculate the center point for our transformation
+        // we can use it for its intended purpose to slightly shift the glyph around.
+        origin.x += cachedLayout.offset.x;
+        origin.y += cachedLayout.offset.y;
+        _r.d2dRenderTarget->DrawTextLayout(origin, cachedLayout.textLayout.get(), _r.brush.get(), cachedLayout.options);
+
+        cachedLayout.undoScaling(_r.d2dRenderTarget.get());
+
+        _r.d2dRenderTarget->PopAxisAlignedClip();
+    }
+}
+
+AtlasEngine::CachedGlyphLayout AtlasEngine::_getCachedGlyphLayout(const wchar_t* chars, u16 charsLength, u16 cellCount, IDWriteTextFormat* textFormat, bool coloredGlyph) const
+{
+    const f32x2 layoutBox{ cellCount * _r.cellSizeDIP.x, _r.cellSizeDIP.y };
     const f32x2 halfSize{ layoutBox.x * 0.5f, layoutBox.y * 0.5f };
     bool scalingRequired = false;
     f32x2 offset{ 0, 0 };
     f32x2 scale{ 1, 1 };
 
+    // See D2DFactory::DrawText
+    wil::com_ptr<IDWriteTextLayout> textLayout;
+    THROW_IF_FAILED(_sr.dwriteFactory->CreateTextLayout(chars, charsLength, textFormat, layoutBox.x, layoutBox.y, textLayout.addressof()));
+    if (_r.typography)
+    {
+        textLayout->SetTypography(_r.typography.get(), { 0, charsLength });
+    }
+
     // Block Element and Box Drawing characters need to be handled separately,
     // because unlike regular ones they're supposed to fill the entire layout box.
     //
@@ -319,14 +359,14 @@ void AtlasEngine::_drawGlyph(const AtlasQueueItem& item) const
         // clang-format on
     };
 
-    if (charsLength == 1 && isInInversionList(blockCharacters, key->chars[0]))
+    if (charsLength == 1 && isInInversionList(blockCharacters, chars[0]))
     {
         wil::com_ptr<IDWriteFontCollection> fontCollection;
         THROW_IF_FAILED(textFormat->GetFontCollection(fontCollection.addressof()));
         const auto baseWeight = textFormat->GetFontWeight();
         const auto baseStyle = textFormat->GetFontStyle();
 
-        TextAnalysisSource analysisSource{ &key->chars[0], 1 };
+        TextAnalysisSource analysisSource{ chars, 1 };
         UINT32 mappedLength = 0;
         wil::com_ptr<IDWriteFont> mappedFont;
         FLOAT mappedScale = 0;
@@ -351,7 +391,7 @@ void AtlasEngine::_drawGlyph(const AtlasQueueItem& item) const
             DWRITE_FONT_METRICS metrics;
             fontFace->GetMetrics(&metrics);
 
-            const u32 codePoint = key->chars[0];
+            const u32 codePoint = chars[0];
             u16 glyphIndex;
             THROW_IF_FAILED(fontFace->GetGlyphIndicesW(&codePoint, 1, &glyphIndex));
 
@@ -470,6 +510,10 @@ void AtlasEngine::_drawGlyph(const AtlasQueueItem& item) const
         offset.y += (baselineFixed - baseline) / scale.y;
     }
 
+    auto options = D2D1_DRAW_TEXT_OPTIONS_NONE;
+    // D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT enables a bunch of internal machinery
+    // which doesn't have to run if we know we can't use it anyways in the shader.
+    WI_SetFlagIf(options, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT, coloredGlyph);
     // !!! IMPORTANT !!!
     // DirectWrite/2D snaps the baseline to whole pixels, which is something we technically
     // want (it makes text look crisp), but fails in weird ways if `scalingRequired` is true.
@@ -481,57 +525,17 @@ void AtlasEngine::_drawGlyph(const AtlasQueueItem& item) const
     // where every single "=" might be blatantly misaligned vertically (same for any box drawings).
     WI_SetFlagIf(options, D2D1_DRAW_TEXT_OPTIONS_NO_SNAP, scalingRequired);
 
-    const f32x2 inverseScale{ 1.0f - scale.x, 1.0f - scale.y };
-
-    for (u32 i = 0; i < cellCount; ++i)
-    {
-        const auto coord = coords[i];
-
-        D2D1_RECT_F rect;
-        rect.left = static_cast<float>(coord.x) * _r.dipPerPixel;
-        rect.top = static_cast<float>(coord.y) * _r.dipPerPixel;
-        rect.right = rect.left + _r.cellSizeDIP.x;
-        rect.bottom = rect.top + _r.cellSizeDIP.y;
-
-        D2D1_POINT_2F origin;
-        origin.x = rect.left - i * _r.cellSizeDIP.x;
-        origin.y = rect.top;
-
-        {
-            _r.d2dRenderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED);
-            _r.d2dRenderTarget->Clear();
-        }
-        if (scalingRequired)
-        {
-            const D2D1_MATRIX_3X2_F transform{
-                scale.x,
-                0,
-                0,
-                scale.y,
-                (origin.x + halfSize.x) * inverseScale.x,
-                (origin.y + halfSize.y) * inverseScale.y,
-            };
-            _r.d2dRenderTarget->SetTransform(&transform);
-        }
-        {
-            // Now that we're done using origin to calculate the center point for our transformation
-            // we can use it for its intended purpose to slightly shift the glyph around.
-            origin.x += offset.x;
-            origin.y += offset.y;
-            _r.d2dRenderTarget->DrawTextLayout(origin, textLayout.get(), _r.brush.get(), options);
-        }
-        if (scalingRequired)
-        {
-            static constexpr D2D1_MATRIX_3X2_F identity{ 1, 0, 0, 1, 0, 0 };
-            _r.d2dRenderTarget->SetTransform(&identity);
-        }
-        {
-            _r.d2dRenderTarget->PopAxisAlignedClip();
-        }
-    }
+    return CachedGlyphLayout{
+        .textLayout = textLayout,
+        .halfSize = halfSize,
+        .offset = offset,
+        .scale = scale,
+        .options = options,
+        .scalingRequired = scalingRequired,
+    };
 }
 
-void AtlasEngine::_drawCursor()
+void AtlasEngine::_drawCursor(u16r rect, u32 color, bool clear)
 {
     // lineWidth is in D2D's DIPs. For instance if we have a 150-200% zoom scale we want to draw a 2px wide line.
     // At 150% scale lineWidth thus needs to be 1.33333... because at a zoom scale of 1.5 this results in a 2px wide line.
@@ -540,21 +544,21 @@ void AtlasEngine::_drawCursor()
 
     // `clip` is the rectangle within our texture atlas that's reserved for our cursor texture, ...
     D2D1_RECT_F clip;
-    clip.left = 0.0f;
-    clip.top = 0.0f;
-    clip.right = _r.cellSizeDIP.x;
-    clip.bottom = _r.cellSizeDIP.y;
+    clip.left = static_cast<float>(rect.left) * _r.cellSizeDIP.x;
+    clip.top = static_cast<float>(rect.top) * _r.cellSizeDIP.y;
+    clip.right = static_cast<float>(rect.right) * _r.cellSizeDIP.x;
+    clip.bottom = static_cast<float>(rect.bottom) * _r.cellSizeDIP.y;
 
     // ... whereas `rect` is just the visible (= usually white) portion of our cursor.
-    auto rect = clip;
+    auto box = clip;
 
     switch (cursorType)
     {
     case CursorType::Legacy:
-        rect.top = _r.cellSizeDIP.y * static_cast<float>(100 - _r.cursorOptions.heightPercentage) / 100.0f;
+        box.top = box.bottom - _r.cellSizeDIP.y * static_cast<float>(_r.cursorOptions.heightPercentage) / 100.0f;
         break;
     case CursorType::VerticalBar:
-        rect.right = lineWidth;
+        box.right = box.left + lineWidth;
         break;
     case CursorType::EmptyBox:
     {
@@ -562,42 +566,463 @@ void AtlasEngine::_drawCursor()
         // coordinates in such a way that the line border extends half the width to each side.
         // --> Our coordinates have to be 0.5 DIP off in order to draw a 2px line on a 200% scaling.
         const auto halfWidth = lineWidth / 2.0f;
-        rect.left = halfWidth;
-        rect.top = halfWidth;
-        rect.right -= halfWidth;
-        rect.bottom -= halfWidth;
+        box.left += halfWidth;
+        box.top += halfWidth;
+        box.right -= halfWidth;
+        box.bottom -= halfWidth;
         break;
     }
     case CursorType::Underscore:
     case CursorType::DoubleUnderscore:
-        rect.top = _r.cellSizeDIP.y - lineWidth;
+        box.top = box.bottom - lineWidth;
         break;
     default:
         break;
     }
 
-    _r.d2dRenderTarget->BeginDraw();
+    const auto brush = _brushWithColor(color);
+
     // We need to clip the area we draw in to ensure we don't
     // accidentally draw into any neighboring texture atlas tiles.
     _r.d2dRenderTarget->PushAxisAlignedClip(&clip, D2D1_ANTIALIAS_MODE_ALIASED);
-    _r.d2dRenderTarget->Clear();
+
+    if (clear)
+    {
+        _r.d2dRenderTarget->Clear();
+    }
 
     if (cursorType == CursorType::EmptyBox)
     {
-        _r.d2dRenderTarget->DrawRectangle(&rect, _r.brush.get(), lineWidth);
+        _r.d2dRenderTarget->DrawRectangle(&box, brush, lineWidth);
     }
     else
     {
-        _r.d2dRenderTarget->FillRectangle(&rect, _r.brush.get());
+        _r.d2dRenderTarget->FillRectangle(&box, brush);
     }
 
     if (cursorType == CursorType::DoubleUnderscore)
     {
-        rect.top -= 2.0f;
-        rect.bottom -= 2.0f;
-        _r.d2dRenderTarget->FillRectangle(&rect, _r.brush.get());
+        const auto offset = lineWidth * 2.0f;
+        box.top -= offset;
+        box.bottom -= offset;
+        _r.d2dRenderTarget->FillRectangle(&box, brush);
     }
 
     _r.d2dRenderTarget->PopAxisAlignedClip();
+}
+
+ID2D1Brush* AtlasEngine::_brushWithColor(u32 color)
+{
+    if (_r.brushColor != color)
+    {
+        const auto d2dColor = colorFromU32(color);
+        THROW_IF_FAILED(_r.d2dRenderTarget->CreateSolidColorBrush(&d2dColor, nullptr, _r.brush.put()));
+        _r.brushColor = color;
+    }
+    return _r.brush.get();
+}
+
+AtlasEngine::CachedGlyphLayout::operator bool() const noexcept
+{
+    return static_cast<bool>(textLayout);
+}
+
+void AtlasEngine::CachedGlyphLayout::reset() noexcept
+{
+    textLayout.reset();
+}
+
+void AtlasEngine::CachedGlyphLayout::applyScaling(ID2D1RenderTarget* d2dRenderTarget, D2D1_POINT_2F origin) const noexcept
+{
+    __assume(d2dRenderTarget != nullptr);
+
+    if (scalingRequired)
+    {
+        const D2D1_MATRIX_3X2_F transform{
+            scale.x,
+            0,
+            0,
+            scale.y,
+            (origin.x + halfSize.x) * (1.0f - scale.x),
+            (origin.y + halfSize.y) * (1.0f - scale.y),
+        };
+        d2dRenderTarget->SetTransform(&transform);
+    }
+}
+
+void AtlasEngine::CachedGlyphLayout::undoScaling(ID2D1RenderTarget* d2dRenderTarget) const noexcept
+{
+    __assume(d2dRenderTarget != nullptr);
+
+    if (scalingRequired)
+    {
+        static constexpr D2D1_MATRIX_3X2_F identity{ 1, 0, 0, 1, 0, 0 };
+        d2dRenderTarget->SetTransform(&identity);
+    }
+}
+
+void AtlasEngine::_d2dPresent()
+{
+    const til::rect fullRect{ 0, 0, _r.cellCount.x, _r.cellCount.y };
+
+    // A change in the selection or background color (etc.) forces a full redraw.
+    if (WI_IsFlagSet(_r.invalidations, RenderInvalidations::ConstBuffer))
+    {
+        _r.dirtyRect = fullRect;
+    }
+
+    if (!_r.dirtyRect)
+    {
+        return;
+    }
+
+    auto dirtyRectInPx = _r.dirtyRect;
+    dirtyRectInPx.left *= _r.fontMetrics.cellSize.x;
+    dirtyRectInPx.top *= _r.fontMetrics.cellSize.y;
+    dirtyRectInPx.right *= _r.fontMetrics.cellSize.x;
+    dirtyRectInPx.bottom *= _r.fontMetrics.cellSize.y;
+
+    if (const auto intersection = _r.previousDirtyRectInPx & dirtyRectInPx)
+    {
+        wil::com_ptr<ID3D11Resource> backBuffer;
+        wil::com_ptr<ID3D11Resource> frontBuffer;
+        THROW_IF_FAILED(_r.swapChain->GetBuffer(0, __uuidof(backBuffer), backBuffer.put_void()));
+        THROW_IF_FAILED(_r.swapChain->GetBuffer(1, __uuidof(frontBuffer), frontBuffer.put_void()));
+
+        D3D11_BOX intersectBox;
+        intersectBox.left = intersection.left;
+        intersectBox.top = intersection.top;
+        intersectBox.front = 0;
+        intersectBox.right = intersection.right;
+        intersectBox.bottom = intersection.bottom;
+        intersectBox.back = 1;
+        _r.deviceContext->CopySubresourceRegion1(backBuffer.get(), 0, intersection.left, intersection.top, 0, frontBuffer.get(), 0, &intersectBox, 0);
+    }
+
+    _d2dCreateRenderTarget();
+    _d2dDrawDirtyArea();
+
+    // See documentation for IDXGISwapChain2::GetFrameLatencyWaitableObject method:
+    // > For every frame it renders, the app should wait on this handle before starting any rendering operations.
+    // > Note that this requirement includes the first frame the app renders with the swap chain.
+    assert(debugGeneralPerformance || _r.frameLatencyWaitableObjectUsed);
+
+    if (_r.dirtyRect != fullRect)
+    {
+        RECT scrollRect{};
+        POINT scrollOffset{};
+        DXGI_PRESENT_PARAMETERS params{
+            .DirtyRectsCount = 1,
+            .pDirtyRects = dirtyRectInPx.as_win32_rect(),
+        };
+
+        if (_r.scrollOffset)
+        {
+            scrollRect = {
+                0,
+                std::max<til::CoordType>(0, _r.scrollOffset),
+                _r.cellCount.x,
+                _r.cellCount.y + std::min<til::CoordType>(0, _r.scrollOffset),
+            };
+            scrollOffset = {
+                0,
+                _r.scrollOffset,
+            };
+
+            scrollRect.top *= _r.fontMetrics.cellSize.y;
+            scrollRect.right *= _r.fontMetrics.cellSize.x;
+            scrollRect.bottom *= _r.fontMetrics.cellSize.y;
+
+            scrollOffset.y *= _r.fontMetrics.cellSize.y;
+
+            params.pScrollRect = &scrollRect;
+            params.pScrollOffset = &scrollOffset;
+        }
+
+        THROW_IF_FAILED(_r.swapChain->Present1(1, 0, &params));
+    }
+    else
+    {
+        THROW_IF_FAILED(_r.swapChain->Present(1, 0));
+    }
+
+    _r.glyphQueue.clear();
+    _r.previousDirtyRectInPx = dirtyRectInPx;
+    _r.waitForPresentation = true;
+    WI_ClearAllFlags(_r.invalidations, RenderInvalidations::Cursor | RenderInvalidations::ConstBuffer);
+}
+
+void AtlasEngine::_d2dCreateRenderTarget()
+{
+    if (_r.d2dRenderTarget)
+    {
+        return;
+    }
+
+    {
+        wil::com_ptr<ID3D11Texture2D> buffer;
+        THROW_IF_FAILED(_r.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), buffer.put_void()));
+
+        const auto surface = buffer.query<IDXGISurface>();
+
+        D2D1_RENDER_TARGET_PROPERTIES props{};
+        props.type = D2D1_RENDER_TARGET_TYPE_DEFAULT;
+        props.pixelFormat = { DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED };
+        props.dpiX = static_cast<float>(_r.dpi);
+        props.dpiY = static_cast<float>(_r.dpi);
+        THROW_IF_FAILED(_sr.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, _r.d2dRenderTarget.put()));
+
+        // In case _api.realizedAntialiasingMode is D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE we'll
+        // continuously adjust it in AtlasEngine::_drawGlyph. See _drawGlyph.
+        _r.d2dRenderTarget->SetTextAntialiasMode(static_cast<D2D1_TEXT_ANTIALIAS_MODE>(_api.realizedAntialiasingMode));
+    }
+    {
+        static constexpr D2D1_COLOR_F color{ 1, 1, 1, 1 };
+        THROW_IF_FAILED(_r.d2dRenderTarget->CreateSolidColorBrush(&color, nullptr, _r.brush.put()));
+        _r.brushColor = 0xffffffff;
+    }
+}
+
+void AtlasEngine::_d2dDrawDirtyArea()
+{
+    struct CellFlagHandler
+    {
+        CellFlags filter;
+        decltype(&AtlasEngine::_d2dCellFlagRendererCursor) func;
+    };
+
+    static constexpr std::array cellFlagHandlers{
+        // Ordered by lowest to highest "layer".
+        // The selection for instance is drawn on top of underlines, not under them.
+        CellFlagHandler{ CellFlags::Underline, &AtlasEngine::_d2dCellFlagRendererUnderline },
+        CellFlagHandler{ CellFlags::UnderlineDotted, &AtlasEngine::_d2dCellFlagRendererUnderlineDotted },
+        CellFlagHandler{ CellFlags::UnderlineDouble, &AtlasEngine::_d2dCellFlagRendererUnderlineDouble },
+        CellFlagHandler{ CellFlags::Strikethrough, &AtlasEngine::_d2dCellFlagRendererStrikethrough },
+        CellFlagHandler{ CellFlags::Cursor, &AtlasEngine::_d2dCellFlagRendererCursor },
+        CellFlagHandler{ CellFlags::Selected, &AtlasEngine::_d2dCellFlagRendererSelected },
+    };
+
+    auto left = gsl::narrow<u16>(_r.dirtyRect.left);
+    auto top = gsl::narrow<u16>(_r.dirtyRect.top);
+    auto right = gsl::narrow<u16>(_r.dirtyRect.right);
+    auto bottom = gsl::narrow<u16>(_r.dirtyRect.bottom);
+    if constexpr (debugGlyphGenerationPerformance)
+    {
+        left = 0;
+        top = 0;
+        right = _r.cellCount.x;
+        bottom = _r.cellCount.y;
+    }
+
+    _r.d2dRenderTarget->BeginDraw();
+
+    if (WI_IsFlagSet(_r.invalidations, RenderInvalidations::ConstBuffer))
+    {
+        _r.d2dRenderTarget->Clear(colorFromU32(_r.backgroundColor));
+    }
+
+    for (u16 y = top; y < bottom; ++y)
+    {
+        const Cell* cells = _getCell(0, y);
+        const TileHashMap::iterator* cellGlyphMappings = _getCellGlyphMapping(0, y);
+
+        // left/right might intersect a wide glyph. We have to extend left/right
+        // to include the entire glyph so that we can properly render it.
+        // Since a series of identical narrow glyphs (2 spaces for instance) are stored in cellGlyphMappings
+        // just like a single wide glyph (2 references to the same glyph in a row), the only way for us to
+        // know where wide glyphs begin and end is to iterate the entire row and use the stored `cellCount`.
+        u16 beg = 0;
+        for (;;)
+        {
+            const auto cellCount = cellGlyphMappings[beg]->first.data()->attributes.cellCount;
+            const auto begNext = gsl::narrow_cast<u16>(beg + cellCount);
+
+            if (begNext > left)
+            {
+                break;
+            }
+
+            beg = begNext;
+        }
+        auto end = beg;
+        for (;;)
+        {
+            const auto cellCount = cellGlyphMappings[end]->first.data()->attributes.cellCount;
+            end += cellCount;
+
+            if (end >= right)
+            {
+                break;
+            }
+        }
+
+        // Draw background.
+        {
+            auto x1 = beg;
+            auto x2 = gsl::narrow_cast<u16>(x1 + 1);
+            auto currentColor = cells[x1].color.y;
+
+            for (; x2 < end; ++x2)
+            {
+                const auto color = cells[x2].color.y;
+
+                if (currentColor != color)
+                {
+                    const u16r rect{ x1, y, x2, gsl::narrow_cast<u16>(y + 1) };
+                    _d2dFillRectangle(rect, currentColor);
+                    x1 = x2;
+                    currentColor = color;
+                }
+            }
+
+            {
+                const u16r rect{ x1, y, x2, gsl::narrow_cast<u16>(y + 1) };
+                _d2dFillRectangle(rect, currentColor);
+            }
+        }
+
+        // Draw text.
+        for (auto x = beg; x < end;)
+        {
+            const auto& it = cellGlyphMappings[x];
+            const u16x2 coord{ x, y };
+            const auto color = cells[x].color.x;
+            x += _d2dDrawGlyph(it, coord, color);
+        }
+
+        // Draw underlines, cursors, selections, etc.
+        for (const auto& handler : cellFlagHandlers)
+        {
+            auto x1 = beg;
+            auto currentFlags = CellFlags::None;
+
+            for (auto x2 = beg; x2 < end; ++x2)
+            {
+                const auto flags = cells[x2].flags & handler.filter;
+
+                if (currentFlags != flags)
+                {
+                    if (currentFlags != CellFlags::None)
+                    {
+                        const u16r rect{ x1, y, x2, gsl::narrow_cast<u16>(y + 1) };
+                        const auto color = cells[x1].color.x;
+                        (this->*handler.func)(rect, color);
+                    }
+
+                    x1 = x2;
+                    currentFlags = flags;
+                }
+            }
+
+            if (currentFlags != CellFlags::None)
+            {
+                const u16r rect{ x1, y, right, gsl::narrow_cast<u16>(y + 1) };
+                const auto color = cells[x1].color.x;
+                (this->*handler.func)(rect, color);
+            }
+        }
+    }
+
     THROW_IF_FAILED(_r.d2dRenderTarget->EndDraw());
 }
+
+// See _drawGlyph() for reference.
+AtlasEngine::u16 AtlasEngine::_d2dDrawGlyph(const TileHashMap::iterator& it, const u16x2 coord, const u32 color)
+{
+    const auto key = it->first.data();
+    const auto value = it->second.data();
+    const auto charsLength = key->charCount;
+    const auto cellCount = key->attributes.cellCount;
+    const auto textFormat = _getTextFormat(key->attributes.bold, key->attributes.italic);
+    const auto coloredGlyph = WI_IsFlagSet(value->flags, CellFlags::ColoredGlyph);
+
+    auto& cachedLayout = it->second.cachedLayout;
+    if (!cachedLayout)
+    {
+        cachedLayout = _getCachedGlyphLayout(&key->chars[0], charsLength, cellCount, textFormat, coloredGlyph);
+    }
+
+    D2D1_RECT_F rect;
+    rect.left = static_cast<float>(coord.x) * _r.cellSizeDIP.x;
+    rect.top = static_cast<float>(coord.y) * _r.cellSizeDIP.y;
+    rect.right = static_cast<float>(coord.x + cellCount) * _r.cellSizeDIP.x;
+    rect.bottom = rect.top + _r.cellSizeDIP.y;
+
+    D2D1_POINT_2F origin;
+    origin.x = rect.left;
+    origin.y = rect.top;
+
+    _r.d2dRenderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED);
+
+    cachedLayout.applyScaling(_r.d2dRenderTarget.get(), origin);
+
+    origin.x += cachedLayout.offset.x;
+    origin.y += cachedLayout.offset.y;
+    _r.d2dRenderTarget->DrawTextLayout(origin, cachedLayout.textLayout.get(), _brushWithColor(color), cachedLayout.options);
+
+    cachedLayout.undoScaling(_r.d2dRenderTarget.get());
+
+    _r.d2dRenderTarget->PopAxisAlignedClip();
+
+    return cellCount;
+}
+
+void AtlasEngine::_d2dDrawLine(u16r rect, u16 pos, u16 width, u32 color, ID2D1StrokeStyle* strokeStyle)
+{
+    const auto w = static_cast<float>(width) * _r.dipPerPixel;
+    const auto y1 = static_cast<float>(rect.top) * _r.cellSizeDIP.y + static_cast<float>(pos) * _r.dipPerPixel + w * 0.5f;
+    const auto x1 = static_cast<float>(rect.left) * _r.cellSizeDIP.x;
+    const auto x2 = static_cast<float>(rect.right) * _r.cellSizeDIP.x;
+    const auto brush = _brushWithColor(color);
+    _r.d2dRenderTarget->DrawLine({ x1, y1 }, { x2, y1 }, brush, w, strokeStyle);
+}
+
+void AtlasEngine::_d2dFillRectangle(u16r rect, u32 color)
+{
+    const D2D1_RECT_F r{
+        .left = static_cast<float>(rect.left) * _r.cellSizeDIP.x,
+        .top = static_cast<float>(rect.top) * _r.cellSizeDIP.y,
+        .right = static_cast<float>(rect.right) * _r.cellSizeDIP.x,
+        .bottom = static_cast<float>(rect.bottom) * _r.cellSizeDIP.y,
+    };
+    const auto brush = _brushWithColor(color);
+    _r.d2dRenderTarget->FillRectangle(r, brush);
+}
+
+void AtlasEngine::_d2dCellFlagRendererCursor(u16r rect, u32 color)
+{
+    _drawCursor(rect, _r.cursorOptions.cursorColor, false);
+}
+
+void AtlasEngine::_d2dCellFlagRendererSelected(u16r rect, u32 color)
+{
+    _d2dFillRectangle(rect, _r.selectionColor);
+}
+
+void AtlasEngine::_d2dCellFlagRendererUnderline(u16r rect, u32 color)
+{
+    _d2dDrawLine(rect, _r.fontMetrics.underlinePos, _r.fontMetrics.underlineWidth, color);
+}
+
+void AtlasEngine::_d2dCellFlagRendererUnderlineDotted(u16r rect, u32 color)
+{
+    if (!_r.dottedStrokeStyle)
+    {
+        static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM };
+        static constexpr FLOAT dashes[2]{ 1, 2 };
+        THROW_IF_FAILED(_sr.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _r.dottedStrokeStyle.addressof()));
+    }
+
+    _d2dDrawLine(rect, _r.fontMetrics.underlinePos, _r.fontMetrics.underlineWidth, color, _r.dottedStrokeStyle.get());
+}
+
+void AtlasEngine::_d2dCellFlagRendererUnderlineDouble(u16r rect, u32 color)
+{
+    _d2dDrawLine(rect, _r.fontMetrics.doubleUnderlinePos.x, _r.fontMetrics.thinLineWidth, color);
+    _d2dDrawLine(rect, _r.fontMetrics.doubleUnderlinePos.y, _r.fontMetrics.thinLineWidth, color);
+}
+
+void AtlasEngine::_d2dCellFlagRendererStrikethrough(u16r rect, u32 color)
+{
+    _d2dDrawLine(rect, _r.fontMetrics.strikethroughPos, _r.fontMetrics.strikethroughWidth, color);
+}


Test patch: