Repository: microsoft/terminal (language: cpp)

Issue:
Background color can exceed the height of the last line
### Windows Terminal version

1.21.1772.0

### Windows build number

10.0.22631.0

### Other Software

_No response_

### Steps to reproduce

I am experiencing this because I use Oh My Posh, but this can be reproduced without a custom prompt:

- Open PowerShell
- Maximize the terminal window
- Press enter until the prompt reaches the bottom of the screen
- Write some text with colored background on the last line:
  ```powershell
  Write-Host -BackgroundColor Blue Hello && Start-Sleep 10
  ```
  You may also try to add `-NoNewline` to `Write-Host` if this doesn't reproduce, but I hope it does 😅 

### Expected Behavior

The background color should not exceed the height of the last line. This works fine when the window is not maximized:

![image](https://github.com/user-attachments/assets/c5e54142-248a-4e17-9a96-4d633b21fd24)

Note the height of the last line is the same as the cursor height.

### Actual Behavior

When maximized, the background color exceeds the height of the last line, it is extended a few pixels below the vertical span of the line:

![image](https://github.com/user-attachments/assets/97f287bf-55ba-4496-a334-87e1808c27a8)

Compare the height of the blue block to the cursor height.

Here is what happens after the sleep is over:

![image](https://github.com/user-attachments/assets/65e63b05-4846-4532-9dfc-13531cb21cd9)

And writing some text makes a part of the blue bar disappear:

![image](https://github.com/user-attachments/assets/1a659be5-f28e-4ed9-ba96-810f7fb89bc9)

Here's what I experience with Oh My Posh:

![image](https://github.com/user-attachments/assets/1f776597-765b-40da-ac6d-166301e53d14)

---

I *suppose* this is due to the fact that maximizing the window prevents Windows Terminal from ensuring the window height is a multiple of the line size. I first thought this rendering quirk would be quickly noticed, but it's been a while and I'm surprised I couldn't find a similar issue, so here we go.

PS: I was able to reproduce this with other fonts as well.


Discussion / hints:
Ah, yes, this! @lhecker was telling me about this.

You're probably using the Direct2D renderer (by choice or by hardware support, or maybe because you're remoting in!)

When we prepare the background, we render all the colors into a bitmap and then scale it to cover the entire terminal surface. The terminal surface is actually a little larger than the sum of the cells present in the terminal, so there's some excess that gets filled.

He notes:

> - D2D
>    - Margins are filled with background color ❌
>       - They're filled with the neighboring's cell background color for convenience, as D2D doesn't support D3D11_TEXTURE_ADDRESS_BORDER
Ooh, yes I'm using RDP, and setting the renderer to D3D 11 explicitly fixed the issue.

Thanks! 🙂 

(I'm closing this since there's a fix, but feel free to reopen if you think a fix in D2D is necessary)

Fix patch:
diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp
index 8dc330dfe0c..8a7861ecd9b 100644
--- a/src/renderer/atlas/BackendD2D.cpp
+++ b/src/renderer/atlas/BackendD2D.cpp
@@ -80,6 +80,7 @@ void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p)
     const auto renderTargetChanged = !_renderTarget;
     const auto fontChanged = _fontGeneration != p.s->font.generation();
     const auto cursorChanged = _cursorGeneration != p.s->cursor.generation();
+    const auto backgroundColorChanged = _miscGeneration != p.s->misc.generation();
     const auto cellCountChanged = _viewportCellCount != p.s->viewportCellCount;
 
     if (renderTargetChanged)
@@ -125,7 +126,7 @@ void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p)
         _builtinGlyphsRenderTargetActive = false;
     }
 
-    if (renderTargetChanged || fontChanged || cellCountChanged)
+    if (renderTargetChanged || fontChanged || cellCountChanged || backgroundColorChanged)
     {
         const D2D1_BITMAP_PROPERTIES props{
             .pixelFormat = { DXGI_FORMAT_R8G8B8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED },
@@ -133,18 +134,40 @@ void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p)
             .dpiY = static_cast<f32>(p.s->font->dpi),
         };
         const D2D1_SIZE_U size{
-            p.s->viewportCellCount.x,
-            p.s->viewportCellCount.y,
+            p.s->viewportCellCount.x + 2u,
+            p.s->viewportCellCount.y + 2u,
         };
         const D2D1_MATRIX_3X2_F transform{
             .m11 = static_cast<f32>(p.s->font->cellSize.x),
             .m22 = static_cast<f32>(p.s->font->cellSize.y),
+            /* Brushes are transformed relative to the render target, not the rect into which they are painted. */
+            .dx = -static_cast<f32>(p.s->font->cellSize.x),
+            .dy = -static_cast<f32>(p.s->font->cellSize.y),
         };
-        THROW_IF_FAILED(_renderTarget->CreateBitmap(size, nullptr, 0, &props, _backgroundBitmap.put()));
+
+        /*
+         We're allocating a bitmap that is one pixel wider on every side than the viewport so that we can fill in the gutter
+         with the background color. D2D doesn't have an equivalent to D3D11_TEXTURE_ADDRESS_BORDER, which we use in the D3D
+         backend to ensure the colors don't bleed off the edges.
+
+         XXXXXXXXXXXXXXXX <- background color
+         X+------------+X
+         X| viewport   |X
+         X|            |X
+         X|            |X
+         X+------------+X
+         XXXXXXXXXXXXXXXX
+
+         The translation in `transform` ensures that we render it off the top left of the render target.
+        */
+        auto backgroundFill = std::make_unique_for_overwrite<u32[]>(static_cast<size_t>(size.width) * size.height);
+        std::fill_n(backgroundFill.get(), size.width * size.height, u32ColorPremultiply(p.s->misc->backgroundColor));
+
+        THROW_IF_FAILED(_renderTarget->CreateBitmap(size, backgroundFill.get(), size.width * sizeof(u32), &props, _backgroundBitmap.put()));
         THROW_IF_FAILED(_renderTarget->CreateBitmapBrush(_backgroundBitmap.get(), _backgroundBrush.put()));
         _backgroundBrush->SetInterpolationMode(D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR);
-        _backgroundBrush->SetExtendModeX(D2D1_EXTEND_MODE_MIRROR);
-        _backgroundBrush->SetExtendModeY(D2D1_EXTEND_MODE_MIRROR);
+        _backgroundBrush->SetExtendModeX(D2D1_EXTEND_MODE_CLAMP);
+        _backgroundBrush->SetExtendModeY(D2D1_EXTEND_MODE_CLAMP);
         _backgroundBrush->SetTransform(&transform);
         _backgroundBitmapGeneration = {};
     }
@@ -158,6 +181,7 @@ void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p)
     _generation = p.s.generation();
     _fontGeneration = p.s->font.generation();
     _cursorGeneration = p.s->cursor.generation();
+    _miscGeneration = p.s->misc.generation();
     _viewportCellCount = p.s->viewportCellCount;
 }
 
@@ -165,7 +189,13 @@ void BackendD2D::_drawBackground(const RenderingPayload& p)
 {
     if (_backgroundBitmapGeneration != p.colorBitmapGenerations[0])
     {
-        THROW_IF_FAILED(_backgroundBitmap->CopyFromMemory(nullptr, p.backgroundBitmap.data(), gsl::narrow_cast<UINT32>(p.colorBitmapRowStride * sizeof(u32))));
+        const D2D1_RECT_U rect{
+            1u,
+            1u,
+            1u + p.s->viewportCellCount.x,
+            1u + p.s->viewportCellCount.y,
+        };
+        THROW_IF_FAILED(_backgroundBitmap->CopyFromMemory(&rect, p.backgroundBitmap.data(), gsl::narrow_cast<UINT32>(p.colorBitmapRowStride * sizeof(u32))));
         _backgroundBitmapGeneration = p.colorBitmapGenerations[0];
     }
 
diff --git a/src/renderer/atlas/BackendD2D.h b/src/renderer/atlas/BackendD2D.h
index c5f7dfbe7d2..da4991bc417 100644
--- a/src/renderer/atlas/BackendD2D.h
+++ b/src/renderer/atlas/BackendD2D.h
@@ -67,6 +67,7 @@ namespace Microsoft::Console::Render::Atlas
         til::generation_t _generation;
         til::generation_t _fontGeneration;
         til::generation_t _cursorGeneration;
+        til::generation_t _miscGeneration;
         u16x2 _viewportCellCount{};
 
 #if ATLAS_DEBUG_SHOW_DIRTY


Test patch: