Repository: microsoft/terminal (language: cpp)

Issue:
Add support for Double-Height and Double-Width text to Windows Terminal
# Description of the new feature/enhancement

At least Windows 11 ships with a conhost version which supports `DECDHL` and `DECDWL`. I'm used to telling people who are asking for a better VT support something like "Hey, check out the shiny Windows Terminal. It supports quite some escape sequences that the console host does not, and you can even set it as default terminal app." Now I have to add "... unless you're looking for double height/width text where you explicitly have to use the good ol' console." This feels rather odd 😄

# Proposed technical implementation details (optional)

Consider adding this feature to Windows Terminal. 
However, from what I've read, the current architecture of conpty doesn't seem to be ready yet. So, I suppose a lot of refactoring would be necessary to get to the preconditions 😧

Related: #7865, #8664
cc @j4james 


Discussion / hints:
There are two aspects to this:

1. We need to get the DX renderer to support it, and that may be a good first step, because that can actually be tested in conhost now without involving conpty (thanks to #10770). The main reason I've been putting that off is because the double-width attributes touch on aspects of the DX renderer that are fundamentally broken, and it annoys me every time I look at it. Those issues aside, it may not be that much work, but I'm no expert on DX.

2. Once the renderer is in place, we need to get it working over conpty. I had a hacky idea for that which shouldn't be that complicated, but I'm not certain it would work, and in the ideal world this would all be solved by the conpty passthrough mode (#1173). Since that's actually starting to look like it may be a real possibility, I'd rather not waste time on the hacky solution, and instead concentrate on the things that'll help make passthrough more feasible.

So I probably should take a stab at part 1 at some point, because we're going to need that either way. I'm less certain about part 2, but that's not to say it won't happen. At the moment I'm working on a bunch of other things, though, and I'm not the fastest of coders, but there's no reason why someone else couldn't take it on.

Yep, I pretty much have nothing to add. If you'd like to take a stab at it, we'd love the help 😄 

Fix patch:
diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp
index 91046f34d43..983b4cd082b 100644
--- a/src/renderer/atlas/AtlasEngine.cpp
+++ b/src/renderer/atlas/AtlasEngine.cpp
@@ -325,7 +325,7 @@ CATCH_RETURN()
     return S_OK;
 }
 
-[[nodiscard]] HRESULT AtlasEngine::PrepareLineTransform(const LineRendition lineRendition, const size_t targetRow, const size_t viewportLeft) noexcept
+[[nodiscard]] HRESULT AtlasEngine::PrepareLineTransform(const LineRendition lineRendition, const til::CoordType targetRow, const til::CoordType viewportLeft) noexcept
 {
     return S_OK;
 }
diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h
index 5b6b9db3d39..b5ab7f54739 100644
--- a/src/renderer/atlas/AtlasEngine.h
+++ b/src/renderer/atlas/AtlasEngine.h
@@ -41,7 +41,7 @@ namespace Microsoft::Console::Render
         [[nodiscard]] HRESULT NotifyNewText(const std::wstring_view newText) noexcept override;
         [[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override;
         [[nodiscard]] HRESULT ResetLineTransform() noexcept override;
-        [[nodiscard]] HRESULT PrepareLineTransform(LineRendition lineRendition, size_t targetRow, size_t viewportLeft) noexcept override;
+        [[nodiscard]] HRESULT PrepareLineTransform(LineRendition lineRendition, til::CoordType targetRow, til::CoordType viewportLeft) noexcept override;
         [[nodiscard]] HRESULT PaintBackground() noexcept override;
         [[nodiscard]] HRESULT PaintBufferLine(gsl::span<const Cluster> clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept override;
         [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF color, size_t cchLine, til::point coordTarget) noexcept override;
diff --git a/src/renderer/base/RenderEngineBase.cpp b/src/renderer/base/RenderEngineBase.cpp
index 1bff8739162..988aaf6690b 100644
--- a/src/renderer/base/RenderEngineBase.cpp
+++ b/src/renderer/base/RenderEngineBase.cpp
@@ -53,8 +53,8 @@ HRESULT RenderEngineBase::ResetLineTransform() noexcept
 }
 
 HRESULT RenderEngineBase::PrepareLineTransform(const LineRendition /*lineRendition*/,
-                                               const size_t /*targetRow*/,
-                                               const size_t /*viewportLeft*/) noexcept
+                                               const til::CoordType /*targetRow*/,
+                                               const til::CoordType /*viewportLeft*/) noexcept
 {
     return S_FALSE;
 }
diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp
index 540e166ad99..83e09c7ddbb 100644
--- a/src/renderer/dx/DxRenderer.cpp
+++ b/src/renderer/dx/DxRenderer.cpp
@@ -2343,8 +2343,8 @@ CATCH_RETURN();
 // Return Value:
 // - S_OK if successful. S_FALSE if already set. E_FAIL if there was an error.
 [[nodiscard]] HRESULT DxEngine::PrepareLineTransform(const LineRendition lineRendition,
-                                                     const size_t targetRow,
-                                                     const size_t viewportLeft) noexcept
+                                                     const til::CoordType targetRow,
+                                                     const til::CoordType viewportLeft) noexcept
 {
     auto lineTransform = D2D1::Matrix3x2F{ 0, 0, 0, 0, 0, 0 };
     const auto fontSize = _fontRenderData->GlyphCell();
diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp
index 7a6e2b55de4..d3242d2a98f 100644
--- a/src/renderer/dx/DxRenderer.hpp
+++ b/src/renderer/dx/DxRenderer.hpp
@@ -97,8 +97,8 @@ namespace Microsoft::Console::Render
 
         [[nodiscard]] HRESULT ResetLineTransform() noexcept override;
         [[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition,
-                                                   const size_t targetRow,
-                                                   const size_t viewportLeft) noexcept override;
+                                                   const til::CoordType targetRow,
+                                                   const til::CoordType viewportLeft) noexcept override;
 
         [[nodiscard]] HRESULT PaintBackground() noexcept override;
         [[nodiscard]] HRESULT PaintBufferLine(const gsl::span<const Cluster> clusters,
diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp
index 5800055ef4b..0eeb0486225 100644
--- a/src/renderer/gdi/gdirenderer.hpp
+++ b/src/renderer/gdi/gdirenderer.hpp
@@ -43,8 +43,8 @@ namespace Microsoft::Console::Render
 
         [[nodiscard]] HRESULT ResetLineTransform() noexcept override;
         [[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition,
-                                                   const size_t targetRow,
-                                                   const size_t viewportLeft) noexcept override;
+                                                   const til::CoordType targetRow,
+                                                   const til::CoordType viewportLeft) noexcept override;
 
         [[nodiscard]] HRESULT PaintBackground() noexcept override;
         [[nodiscard]] HRESULT PaintBufferLine(const gsl::span<const Cluster> clusters,
diff --git a/src/renderer/gdi/state.cpp b/src/renderer/gdi/state.cpp
index f2da4bc0f6f..4baa6180820 100644
--- a/src/renderer/gdi/state.cpp
+++ b/src/renderer/gdi/state.cpp
@@ -210,8 +210,8 @@ GdiEngine::~GdiEngine()
 // Return Value:
 // - S_OK if successful. S_FALSE if already set. E_FAIL if there was an error.
 [[nodiscard]] HRESULT GdiEngine::PrepareLineTransform(const LineRendition lineRendition,
-                                                      const size_t targetRow,
-                                                      const size_t viewportLeft) noexcept
+                                                      const til::CoordType targetRow,
+                                                      const til::CoordType viewportLeft) noexcept
 {
     XFORM lineTransform = {};
     // The X delta is to account for the horizontal viewport offset.
diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp
index dc641bb3861..6fb29d7f83f 100644
--- a/src/renderer/inc/IRenderEngine.hpp
+++ b/src/renderer/inc/IRenderEngine.hpp
@@ -72,7 +72,7 @@ namespace Microsoft::Console::Render
         [[nodiscard]] virtual HRESULT NotifyNewText(const std::wstring_view newText) noexcept = 0;
         [[nodiscard]] virtual HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept = 0;
         [[nodiscard]] virtual HRESULT ResetLineTransform() noexcept = 0;
-        [[nodiscard]] virtual HRESULT PrepareLineTransform(LineRendition lineRendition, size_t targetRow, size_t viewportLeft) noexcept = 0;
+        [[nodiscard]] virtual HRESULT PrepareLineTransform(LineRendition lineRendition, til::CoordType targetRow, til::CoordType viewportLeft) noexcept = 0;
         [[nodiscard]] virtual HRESULT PaintBackground() noexcept = 0;
         [[nodiscard]] virtual HRESULT PaintBufferLine(gsl::span<const Cluster> clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept = 0;
         [[nodiscard]] virtual HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF color, size_t cchLine, til::point coordTarget) noexcept = 0;
diff --git a/src/renderer/inc/RenderEngineBase.hpp b/src/renderer/inc/RenderEngineBase.hpp
index 28a29a5a721..eea90051021 100644
--- a/src/renderer/inc/RenderEngineBase.hpp
+++ b/src/renderer/inc/RenderEngineBase.hpp
@@ -38,8 +38,8 @@ namespace Microsoft::Console::Render
 
         [[nodiscard]] HRESULT ResetLineTransform() noexcept override;
         [[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition,
-                                                   const size_t targetRow,
-                                                   const size_t viewportLeft) noexcept override;
+                                                   const til::CoordType targetRow,
+                                                   const til::CoordType viewportLeft) noexcept override;
 
         [[nodiscard]] virtual bool RequiresContinuousRedraw() noexcept override;
 
diff --git a/src/renderer/vt/paint.cpp b/src/renderer/vt/paint.cpp
index 414ba6df402..db824f2cb24 100644
--- a/src/renderer/vt/paint.cpp
+++ b/src/renderer/vt/paint.cpp
@@ -25,6 +25,10 @@ using namespace Microsoft::Console::Types;
         return S_FALSE;
     }
 
+    // If we're using line renditions, and this is a full screen paint, we can
+    // potentially stop using them at the end of this frame.
+    _stopUsingLineRenditions = _usingLineRenditions && _AllIsInvalid();
+
     // If there's nothing to do, quick return
     auto somethingToDo = _invalidMap.any() ||
                          _scrollDelta != til::point{ 0, 0 } ||
@@ -75,6 +79,14 @@ using namespace Microsoft::Console::Types;
     }
     _circled = false;
 
+    // If _stopUsingLineRenditions is still true at the end of the frame, that
+    // means we've refreshed the entire viewport with every line being single
+    // width, so we can safely stop using them from now on.
+    if (_stopUsingLineRenditions)
+    {
+        _usingLineRenditions = false;
+    }
+
     // If we deferred a cursor movement during the frame, make sure we put the
     //      cursor in the right place before we end the frame.
     if (_deferredCursorPos != INVALID_COORDS)
@@ -100,6 +112,45 @@ using namespace Microsoft::Console::Types;
     return S_FALSE;
 }
 
+[[nodiscard]] HRESULT VtEngine::ResetLineTransform() noexcept
+{
+    return S_FALSE;
+}
+
+[[nodiscard]] HRESULT VtEngine::PrepareLineTransform(const LineRendition lineRendition,
+                                                     const til::CoordType targetRow,
+                                                     const til::CoordType /*viewportLeft*/) noexcept
+{
+    // We don't want to waste bandwidth writing out line rendition attributes
+    // until we know they're in use. But once they are in use, we have to keep
+    // applying them on every line until we know they definitely aren't being
+    // used anymore (we check that at the end of any fullscreen paint).
+    if (lineRendition != LineRendition::SingleWidth)
+    {
+        _stopUsingLineRenditions = false;
+        _usingLineRenditions = true;
+    }
+    // One simple optimization is that we can skip sending the line attributes
+    // when _quickReturn is true. That indicates that we're writing out a single
+    // character, which should preclude there being a rendition switch.
+    if (_usingLineRenditions && !_quickReturn)
+    {
+        RETURN_IF_FAILED(_MoveCursor({ _lastText.x, targetRow }));
+        switch (lineRendition)
+        {
+        case LineRendition::SingleWidth:
+            return _Write("\x1b#5");
+        case LineRendition::DoubleWidth:
+            return _Write("\x1b#6");
+        case LineRendition::DoubleHeightTop:
+            return _Write("\x1b#3");
+        case LineRendition::DoubleHeightBottom:
+            return _Write("\x1b#4");
+        }
+    }
+    return S_OK;
+}
+
 // Routine Description:
 // - Paints the background of the invalid area of the frame.
 // Arguments:
diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp
index f00d34a7efe..872f419a004 100644
--- a/src/renderer/vt/state.cpp
+++ b/src/renderer/vt/state.cpp
@@ -29,6 +29,8 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe,
                    const Viewport initialViewport) :
     RenderEngineBase(),
     _hFile(std::move(pipe)),
+    _usingLineRenditions(false),
+    _stopUsingLineRenditions(false),
     _lastTextAttributes(INVALID_COLOR, INVALID_COLOR),
     _lastViewport(initialViewport),
     _pool(til::pmr::get_default_resource()),
diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp
index 4d9e624e919..aee2428bb1d 100644
--- a/src/renderer/vt/vtrenderer.hpp
+++ b/src/renderer/vt/vtrenderer.hpp
@@ -58,6 +58,8 @@ namespace Microsoft::Console::Render
         [[nodiscard]] HRESULT InvalidateSelection(const std::vector<til::rect>& rectangles) noexcept override;
         [[nodiscard]] HRESULT InvalidateAll() noexcept override;
         [[nodiscard]] HRESULT InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept override;
+        [[nodiscard]] HRESULT ResetLineTransform() noexcept override;
+        [[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition, const til::CoordType targetRow, const til::CoordType viewportLeft) noexcept override;
         [[nodiscard]] HRESULT PaintBackground() noexcept override;
         [[nodiscard]] HRESULT PaintBufferLine(gsl::span<const Cluster> clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept override;
         [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF color, size_t cchLine, til::point coordTarget) noexcept override;
@@ -96,6 +98,8 @@ namespace Microsoft::Console::Render
         std::string _formatBuffer;
         std::string _conversionBuffer;
 
+        bool _usingLineRenditions;
+        bool _stopUsingLineRenditions;
         TextAttribute _lastTextAttributes;
 
         std::function<void(bool)> _pfnSetLookingForDSR;


Test patch: