Repository: microsoft/terminal (language: cpp)

Issue:
Alternate screen shouldn't write to scrollback or normal screen
<!-- 
🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨

I ACKNOWLEDGE THE FOLLOWING BEFORE PROCEEDING:
1. If I delete this entire template and go my own path, the core team may close my issue without further explanation or engagement.
2. If I list multiple bugs/concerns in this one issue, the core team may close my issue without further explanation or engagement.
3. If I write an issue that has many duplicates, the core team may close my issue without further explanation or engagement (and without necessarily spending time to find the exact duplicate ID number).
4. If I leave the title incomplete when filing the issue, the core team may close my issue without further explanation or engagement.
5. If I file something completely blank in the body, the core team may close my issue without further explanation or engagement.

All good? Then proceed!
-->

<!--
This bug tracker is monitored by Windows Terminal development team and other technical folks.

**Important: When reporting BSODs or security issues, DO NOT attach memory dumps, logs, or traces to Github issues**.
Instead, send dumps/traces to secure@microsoft.com, referencing this GitHub issue.

If this is an application crash, please also provide a Feedback Hub submission link so we can find your diagnostic data on the backend. Use the category "Apps > Windows Terminal (Preview)" and choose "Share My Feedback" after submission to get the link.

Please use this form and describe your issue, concisely but precisely, with as much detail as possible.

-->

# Environment

```none
Windows build number: 10.0.18362.0
Windows Terminal version (if applicable): 0.6.2951.0
```

# Epic

Scrollback handling should be just like in VTE because that's the best (ahem ahem) :rofl: 

# Steps to reproduce 1

- `ssh` to a Linux box, and execute these:

      seq -f 'normal %.f' 100
      echo -ne '\e[?1049h' # switch to alt screen
      seq -f 'alt %.f' 100
      echo -ne '\e[?1049l' # switch back to normal screen

- scroll back and examine the contents

# Expected behavior 1

Outputs of these two `seq`s shouldn't interleave

# Actual behavior 1

Output consists of `normal 1` to `normal 74` (exact number depends on the window height), followed by `alt 1` to `alt 74`, and finally `normal 75` to `normal 100`. (`alt 75` onwards are gone.)

# Steps to reproduce 2

- `ssh` to a Linux box.
- produce a few pages of output
- start a fullscreen app such as `mc`
- resize the window in various ways (maybe even back and forth to smaller and then bigger)
- quit `mc`
- examine the output, including the scrollback

# Expected behavior 2

This should look okay (see below).

# Actual behavior 2

The contents seem quite garbled. Most of the time you'll see remains of `mc`'s user interface in the scrollback buffer; sometimes even on the normal screen (this latter is harder to reproduce, and perhaps only happens with solid background colors).

---

# VTE rocks

The way this works amazingly in gnome-terminal is:

The alternate screen is pretty much only used by fullscreen apps that control the entire UI and repaint on window resize. Therefore it's unrelated to the normal screen and the scrollback. Data never travels between the (normal screen + scrollback) combo and the alternate screen. The scrollback buffer belongs to the normal screen only. Everything scrolled out from the alternate screen is lost for good.

On resize, the (normal screen + scrollback) is resized as a single unit, for the resizing operation it doesn't matter where the boundary is.

The (normal screen + scrollback) combo is resized the same way, regardless of whether it's the active one or the alternate screen is shown. Stress-test this: produce output on the normal screen, switch to the alternate one (e.g. `mc`), resize, quit `mc`. The normal screen's contents will be updated to the new width just as if you haven't switched to the alternate screen.

There's plenty of further tiny details to this story, e.g. how to vertically position the contents, some of which are documented in [VTE's doc/rewrap.txt](https://gitlab.gnome.org/GNOME/vte/blob/master/doc/rewrap.txt).

Of course there's no specification anywhere that all this has to behave like this. It's just what I believe makes the most sense. Obviously my opinion is quite biased since this was my first big contribution to VTE. :) Luckily I only had to do the rewrapping; the separation of the (normal screen + scrollback) vs. alternate screen was already as desired.



Discussion / hints:
This is actually one of the unfortunate circumstances of the pseudoconsole API.

conhost implements the alt buffer reasonably well (we think): it turns off scrollback, reflow, and so on.
When it remotes the contents of the screen as VT over pseudoconsole we're not _also_ sending the alt buffer state down the wire (#381).

Fix patch:
diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp
index 274bde52ebf..b2bc461e124 100644
--- a/src/buffer/out/textBuffer.cpp
+++ b/src/buffer/out/textBuffer.cpp
@@ -562,7 +562,7 @@ bool TextBuffer::IncrementCircularBuffer(const bool inVtMode)
     // to the logical position 0 in the window (cursor coordinates and all other coordinates).
     if (_isActiveBuffer)
     {
-        _renderer.TriggerCircling();
+        _renderer.TriggerFlush(true);
     }
 
     // Prune hyperlinks to delete obsolete references
diff --git a/src/cascadia/TerminalCore/ITerminalApi.hpp b/src/cascadia/TerminalCore/ITerminalApi.hpp
index 3aebe7ea1c8..7f41e0ce71f 100644
--- a/src/cascadia/TerminalCore/ITerminalApi.hpp
+++ b/src/cascadia/TerminalCore/ITerminalApi.hpp
@@ -69,6 +69,9 @@ namespace Microsoft::Terminal::Core
         virtual void PushGraphicsRendition(const ::Microsoft::Console::VirtualTerminal::VTParameters options) = 0;
         virtual void PopGraphicsRendition() = 0;
 
+        virtual void UseAlternateScreenBuffer() = 0;
+        virtual void UseMainScreenBuffer() = 0;
+
     protected:
         ITerminalApi() = default;
     };
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index 5db2cff6b9e..41951c72acf 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -40,6 +40,7 @@ Terminal::Terminal() :
     _mutableViewport{ Viewport::Empty() },
     _title{},
     _pfnWriteInput{ nullptr },
+    _altBuffer{ nullptr },
     _scrollOffset{ 0 },
     _snapOnInput{ true },
     _altGrAliasing{ true },
@@ -85,7 +86,7 @@ void Terminal::Create(COORD viewportSize, SHORT scrollbackLines, Renderer& rende
                             Utils::ClampToShortMax(viewportSize.Y + scrollbackLines, 1) };
     const TextAttribute attr{};
     const UINT cursorSize = 12;
-    _buffer = std::make_unique<TextBuffer>(bufferSize, attr, cursorSize, true, renderer);
+    _mainBuffer = std::make_unique<TextBuffer>(bufferSize, attr, cursorSize, true, renderer);
 }
 
 // Method Description:
@@ -147,21 +148,14 @@ void Terminal::UpdateSettings(ICoreSettings settings)
     // size is smaller than where the mutable viewport currently is, we'll want
     // to make sure to rotate the buffer contents upwards, so the mutable viewport
     // remains at the bottom of the buffer.
-    if (_buffer)
+
+    // Regenerate the pattern tree for the new buffer size
+    if (_mainBuffer)
     {
         // Clear the patterns first
-        _buffer->ClearPatternRecognizers();
-        if (settings.DetectURLs())
-        {
-            // Add regex pattern recognizers to the buffer
-            // For now, we only add the URI regex pattern
-            _hyperlinkPatternId = _buffer->AddPatternRecognizer(linkPattern);
-            UpdatePatternsUnderLock();
-        }
-        else
-        {
-            ClearPatternTree();
-        }
+        _mainBuffer->ClearPatternRecognizers();
+        _detectURLs = settings.DetectURLs();
+        _updateUrlDetection();
     }
 }
 
@@ -213,9 +207,12 @@ void Terminal::UpdateAppearance(const ICoreAppearance& appearance)
         break;
     }
 
-    if (_buffer)
+    // We're checking if the main buffer exists here, but then setting the
+    // appearance of the active one. If the main buffer exists, then at least
+    // one buffer exists and _activeBuffer() will work
+    if (_mainBuffer)
     {
-        _buffer->GetCursor().SetStyle(appearance.CursorHeight(), cursorShape);
+        _activeBuffer().GetCursor().SetStyle(appearance.CursorHeight(), cursorShape);
     }
 
     _defaultCursorShape = cursorShape;
@@ -252,9 +249,9 @@ void Terminal::UpdateAppearance(const ICoreAppearance& appearance)
     const bool originalOffsetWasZero = _scrollOffset == 0;
 
     // skip any drawing updates that might occur until we swap _buffer with the new buffer or if we exit early.
-    _buffer->GetCursor().StartDeferDrawing();
-    // we're capturing _buffer by reference here because when we exit, we want to EndDefer on the current active buffer.
-    auto endDefer = wil::scope_exit([&]() noexcept { _buffer->GetCursor().EndDeferDrawing(); });
+    _mainBuffer->GetCursor().StartDeferDrawing();
+    // we're capturing `this` here because when we exit, we want to EndDefer on the (newly created) active buffer.
+    auto endDefer = wil::scope_exit([this]() noexcept { _mainBuffer->GetCursor().EndDeferDrawing(); });
 
     // First allocate a new text buffer to take the place of the current one.
     std::unique_ptr<TextBuffer> newTextBuffer;
@@ -265,13 +262,14 @@ void Terminal::UpdateAppearance(const ICoreAppearance& appearance)
         // but after the resize, we'll want to make sure that the new buffer's
         // current attributes (the ones used for printing new text) match the
         // old buffer's.
-        const auto oldBufferAttributes = _buffer->GetCurrentAttributes();
+        const auto oldBufferAttributes = _mainBuffer->GetCurrentAttributes();
         newTextBuffer = std::make_unique<TextBuffer>(bufferSize,
                                                      TextAttribute{},
                                                      0, // temporarily set size to 0 so it won't render.
-                                                     _buffer->IsActiveBuffer(),
-                                                     _buffer->GetRenderer());
+                                                     _mainBuffer->IsActiveBuffer(),
+                                                     _mainBuffer->GetRenderer());
 
+        // start defer drawing on the new buffer
         newTextBuffer->GetCursor().StartDeferDrawing();
 
         // Build a PositionInformation to track the position of both the top of
@@ -289,7 +287,7 @@ void Terminal::UpdateAppearance(const ICoreAppearance& appearance)
         oldRows.visibleViewportTop = newVisibleTop;
 
         const std::optional<short> oldViewStart{ oldViewportTop };
-        RETURN_IF_FAILED(TextBuffer::Reflow(*_buffer.get(),
+        RETURN_IF_FAILED(TextBuffer::Reflow(*_mainBuffer.get(),
                                             *newTextBuffer.get(),
                                             _mutableViewport,
                                             { oldRows }));
@@ -405,7 +403,7 @@ void Terminal::UpdateAppearance(const ICoreAppearance& appearance)
 
     _mutableViewport = Viewport::FromDimensions({ 0, proposedTop }, viewportSize);
 
-    _buffer.swap(newTextBuffer);
+    _mainBuffer.swap(newTextBuffer);
 
     // GH#3494: Maintain scrollbar position during resize
     // Make sure that we don't scroll past the mutableViewport at the bottom of the buffer
@@ -417,10 +415,52 @@ void Terminal::UpdateAppearance(const ICoreAppearance& appearance)
     // before, and shouldn't be now either.
     _scrollOffset = originalOffsetWasZero ? 0 : static_cast<int>(::base::ClampSub(_mutableViewport.Top(), newVisibleTop));
 
+    // Now that we've finished the hard work of resizing the main buffer and
+    // getting the viewport back into the right spot, we need to ALSO resize the
+    // alt buffer, if one exists. Fortunately, this is easy. We don't need to
+    // worry about the viewport and scrollback at all! The alt buffer never has
+    // any scrollback, so we just need to resize it and presto, we're done.
+    if (_inAltBuffer())
+    {
+        _altBuffer->GetCursor().StartDeferDrawing();
+        // we're capturing `this` here because when we exit, we want to EndDefer on the (newly created) active buffer.
+        auto endDefer = wil::scope_exit([this]() noexcept { _altBuffer->GetCursor().EndDeferDrawing(); });
+
+        // First allocate a new text buffer to take the place of the current one.
+        std::unique_ptr<TextBuffer> newTextBuffer;
+        try
+        {
+            // GH#3848 - Stash away the current attributes
+            const auto oldBufferAttributes = _altBuffer->GetCurrentAttributes();
+            newTextBuffer = std::make_unique<TextBuffer>(bufferSize,
+                                                         TextAttribute{},
+                                                         0, // temporarily set size to 0 so it won't render.
+                                                         true,
+                                                         _mainBuffer->GetRenderer());
+
+            // start defer drawing on the new buffer
+            newTextBuffer->GetCursor().StartDeferDrawing();
+
+            // We don't need any fancy position information. We're just gonna
+            // resize the buffer, it's gonna be in exactly the place it is now.
+            // There's no scrollback to worry about!
+
+            RETURN_IF_FAILED(TextBuffer::Reflow(*_altBuffer.get(),
+                                                *newTextBuffer.get(),
+                                                _GetMutableViewport(),
+                                                std::nullopt));
+
+            // Restore the active text attributes
+            newTextBuffer->SetCurrentAttributes(oldBufferAttributes);
+            _altBuffer.swap(newTextBuffer);
+        }
+        CATCH_RETURN();
+    }
+
     // GH#5029 - make sure to InvalidateAll here, so that we'll paint the entire visible viewport.
     try
     {
-        _buffer->TriggerRedrawAll();
+        _activeBuffer().TriggerRedrawAll();
     }
     CATCH_LOG();
     _NotifyScrollEvent();
@@ -432,7 +472,7 @@ void Terminal::Write(std::wstring_view stringView)
 {
     auto lock = LockForWriting();
 
-    auto& cursor = _buffer->GetCursor();
+    auto& cursor = _activeBuffer().GetCursor();
     const til::point cursorPosBefore{ cursor.GetPosition() };
 
     _stateMachine->ProcessString(stringView);
@@ -502,10 +542,10 @@ bool Terminal::IsTrackingMouseInput() const noexcept
 // - The position
 std::wstring Terminal::GetHyperlinkAtPosition(const COORD position)
 {
-    auto attr = _buffer->GetCellDataAt(_ConvertToBufferCell(position))->TextAttr();
+    auto attr = _activeBuffer().GetCellDataAt(_ConvertToBufferCell(position))->TextAttr();
     if (attr.IsHyperlink())
     {
-        auto uri = _buffer->GetHyperlinkUriFromId(attr.GetHyperlinkId());
+        auto uri = _activeBuffer().GetHyperlinkUriFromId(attr.GetHyperlinkId());
         return uri;
     }
     // also look through our known pattern locations in our pattern interval tree
@@ -516,8 +556,8 @@ std::wstring Terminal::GetHyperlinkAtPosition(const COORD position)
         const auto end = result->stop;
         std::wstring uri;
 
-        const auto startIter = _buffer->GetCellDataAt(_ConvertToBufferCell(start.to_win32_coord()));
-        const auto endIter = _buffer->GetCellDataAt(_ConvertToBufferCell(end.to_win32_coord()));
+        const auto startIter = _activeBuffer().GetCellDataAt(_ConvertToBufferCell(start.to_win32_coord()));
+        const auto endIter = _activeBuffer().GetCellDataAt(_ConvertToBufferCell(end.to_win32_coord()));
         for (auto iter = startIter; iter != endIter; ++iter)
         {
             uri += iter->Chars();
@@ -535,7 +575,7 @@ std::wstring Terminal::GetHyperlinkAtPosition(const COORD position)
 // - The hyperlink ID
 uint16_t Terminal::GetHyperlinkIdAtPosition(const COORD position)
 {
-    return _buffer->GetCellDataAt(_ConvertToBufferCell(position))->TextAttr().GetHyperlinkId();
+    return _activeBuffer().GetCellDataAt(_ConvertToBufferCell(position))->TextAttr().GetHyperlinkId();
 }
 
 // Method description:
@@ -732,26 +772,26 @@ void Terminal::_InvalidateFromCoords(const COORD start, const COORD end)
     if (start.Y == end.Y)
     {
         SMALL_RECT region{ start.X, start.Y, end.X, end.Y };
-        _buffer->TriggerRedraw(Viewport::FromInclusive(region));
+        _activeBuffer().TriggerRedraw(Viewport::FromInclusive(region));
     }
     else
     {
-        const auto rowSize = gsl::narrow<SHORT>(_buffer->GetRowByOffset(0).size());
+        const auto rowSize = gsl::narrow<SHORT>(_activeBuffer().GetRowByOffset(0).size());
 
         // invalidate the first line
         SMALL_RECT region{ start.X, start.Y, gsl::narrow<short>(rowSize - 1), gsl::narrow<short>(start.Y) };
-        _buffer->TriggerRedraw(Viewport::FromInclusive(region));
+        _activeBuffer().TriggerRedraw(Viewport::FromInclusive(region));
 
         if ((end.Y - start.Y) > 1)
         {
             // invalidate the lines in between the first and last line
             region = SMALL_RECT{ 0, start.Y + 1, gsl::narrow<short>(rowSize - 1), gsl::narrow<short>(end.Y - 1) };
-            _buffer->TriggerRedraw(Viewport::FromInclusive(region));
+            _activeBuffer().TriggerRedraw(Viewport::FromInclusive(region));
         }
 
         // invalidate the last line
         region = SMALL_RECT{ 0, end.Y, end.X, end.Y };
-        _buffer->TriggerRedraw(Viewport::FromInclusive(region));
+        _activeBuffer().TriggerRedraw(Viewport::FromInclusive(region));
     }
 }
 
@@ -897,34 +937,37 @@ WORD Terminal::_TakeVirtualKeyFromLastKeyEvent(const WORD scanCode) noexcept
 
 Viewport Terminal::_GetMutableViewport() const noexcept
 {
-    return _mutableViewport;
+    return _inAltBuffer() ? Viewport::FromDimensions(_mutableViewport.Dimensions()) :
+                            _mutableViewport;
 }
 
 short Terminal::GetBufferHeight() const noexcept
 {
-    return _mutableViewport.BottomExclusive();
+    return _GetMutableViewport().BottomExclusive();
 }
 
 // ViewStartIndex is also the length of the scrollback
 int Terminal::ViewStartIndex() const noexcept
 {
-    return _mutableViewport.Top();
+    return _inAltBuffer() ? 0 : _mutableViewport.Top();
 }
 
 int Terminal::ViewEndIndex() const noexcept
 {
-    return _mutableViewport.BottomInclusive();
+    return _inAltBuffer() ? _mutableViewport.Height() - 1 : _mutableViewport.BottomInclusive();
 }
 
 // _VisibleStartIndex is the first visible line of the buffer
 int Terminal::_VisibleStartIndex() const noexcept
 {
-    return std::max(0, ViewStartIndex() - _scrollOffset);
+    return _inAltBuffer() ? ViewStartIndex() :
+                            std::max(0, ViewStartIndex() - _scrollOffset);
 }
 
 int Terminal::_VisibleEndIndex() const noexcept
 {
-    return std::max(0, ViewEndIndex() - _scrollOffset);
+    return _inAltBuffer() ? ViewEndIndex() :
+                            std::max(0, ViewEndIndex() - _scrollOffset);
 }
 
 Viewport Terminal::_GetVisibleViewport() const noexcept
@@ -944,7 +987,7 @@ Viewport Terminal::_GetVisibleViewport() const noexcept
 //       I had to make a bunch of hacks to get Japanese and emoji to work-ish.
 void Terminal::_WriteBuffer(const std::wstring_view& stringView)
 {
-    auto& cursor = _buffer->GetCursor();
+    auto& cursor = _activeBuffer().GetCursor();
 
     // Defer the cursor drawing while we are iterating the string, for a better performance.
     // We can not waste time displaying a cursor event when we know more text is coming right behind it.
@@ -963,8 +1006,8 @@ void Terminal::_WriteBuffer(const std::wstring_view& stringView)
         // from the stringView to form a single code point.
         const auto isSurrogate = wch >= 0xD800 && wch <= 0xDFFF;
         const auto view = stringView.substr(i, isSurrogate ? 2 : 1);
-        const OutputCellIterator it{ view, _buffer->GetCurrentAttributes() };
-        const auto end = _buffer->Write(it);
+        const OutputCellIterator it{ view, _activeBuffer().GetCurrentAttributes() };
+        const auto end = _activeBuffer().Write(it);
         const auto cellDistance = end.GetCellDistance(it);
         const auto inputDistance = end.GetInputDistance(it);
 
@@ -1006,7 +1049,7 @@ void Terminal::_WriteBuffer(const std::wstring_view& stringView)
     // Notify UIA of new text.
     // It's important to do this here instead of in TextBuffer, because here you have access to the entire line of text,
     // whereas TextBuffer writes it one character at a time via the OutputCellIterator.
-    _buffer->TriggerNewTextNotification(stringView);
+    _activeBuffer().TriggerNewTextNotification(stringView);
 
     cursor.EndDeferDrawing();
 }
@@ -1015,8 +1058,8 @@ void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
 {
 #pragma warning(suppress : 26496) // cpp core checks wants this const but it's modified below.
     auto proposedCursorPosition = proposedPosition;
-    auto& cursor = _buffer->GetCursor();
-    const Viewport bufferSize = _buffer->GetSize();
+    auto& cursor = _activeBuffer().GetCursor();
+    const Viewport bufferSize = _activeBuffer().GetSize();
 
     // If we're about to scroll past the bottom of the buffer, instead cycle the
     // buffer.
@@ -1026,7 +1069,7 @@ void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
     {
         for (auto dy = 0; dy < newRows; dy++)
         {
-            _buffer->IncrementCircularBuffer();
+            _activeBuffer().IncrementCircularBuffer();
             proposedCursorPosition.Y--;
             rowsPushedOffTopOfBuffer++;
 
@@ -1069,7 +1112,8 @@ void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
     if (scrollAmount > 0)
     {
         const auto newViewTop = std::max(0, proposedCursorPosition.Y - (_mutableViewport.Height() - 1));
-        if (newViewTop != _mutableViewport.Top())
+        // In the alt buffer, we never need to adjust _mutableViewport, which is the viewport of the main buffer.
+        if (!_inAltBuffer() && newViewTop != _mutableViewport.Top())
         {
             _mutableViewport = Viewport::FromDimensions({ 0, gsl::narrow<short>(newViewTop) },
                                                         _mutableViewport.Dimensions());
@@ -1079,7 +1123,7 @@ void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
 
     // If the viewport moved, or we circled the buffer, we might need to update
     // our _scrollOffset
-    if (updatedViewport || newRows != 0)
+    if (!_inAltBuffer() && (updatedViewport || newRows != 0))
     {
         const auto oldScrollOffset = _scrollOffset;
 
@@ -1093,7 +1137,7 @@ void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
         // Clamp the range to make sure that we don't scroll way off the top of the buffer
         _scrollOffset = std::clamp(_scrollOffset,
                                    0,
-                                   _buffer->GetSize().Height() - _mutableViewport.Height());
+                                   _activeBuffer().GetSize().Height() - _mutableViewport.Height());
 
         // If the new scroll offset is different, then we'll still want to raise a scroll event
         updatedViewport = updatedViewport || (oldScrollOffset != _scrollOffset);
@@ -1111,12 +1155,17 @@ void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
         // That didn't change the viewport and therefore the TriggerScroll(void)
         // method can't detect the delta on its own.
         COORD delta{ 0, gsl::narrow_cast<short>(-rowsPushedOffTopOfBuffer) };
-        _buffer->TriggerScroll(delta);
+        _activeBuffer().TriggerScroll(delta);
     }
 }
 
 void Terminal::UserScrollViewport(const int viewTop)
 {
+    if (_inAltBuffer())
+    {
+        return;
+    }
+
     // we're going to modify state here that the renderer could be reading.
     auto lock = LockForWriting();
 
@@ -1130,7 +1179,7 @@ void Terminal::UserScrollViewport(const int viewTop)
     // We can use the void variant of TriggerScroll here because
     // we adjusted the viewport so it can detect the difference
     // from the previous frame drawn.
-    _buffer->TriggerScroll();
+    _activeBuffer().TriggerScroll();
 }
 
 int Terminal::GetScrollOffset() noexcept
@@ -1229,12 +1278,12 @@ void Microsoft::Terminal::Core::Terminal::TaskbarProgressChangedCallback(std::fu
 void Terminal::SetCursorOn(const bool isOn)
 {
     auto lock = LockForWriting();
-    _buffer->GetCursor().SetIsOn(isOn);
+    _activeBuffer().GetCursor().SetIsOn(isOn);
 }
 
 bool Terminal::IsCursorBlinkingAllowed() const noexcept
 {
-    const auto& cursor = _buffer->GetCursor();
+    const auto& cursor = _activeBuffer().GetCursor();
     return cursor.IsBlinkingAllowed();
 }
 
@@ -1246,7 +1295,7 @@ bool Terminal::IsCursorBlinkingAllowed() const noexcept
 void Terminal::UpdatePatternsUnderLock() noexcept
 {
     auto oldTree = _patternIntervalTree;
-    _patternIntervalTree = _buffer->GetPatterns(_VisibleStartIndex(), _VisibleEndIndex());
+    _patternIntervalTree = _activeBuffer().GetPatterns(_VisibleStartIndex(), _VisibleEndIndex());
     _InvalidatePatternTree(oldTree);
     _InvalidatePatternTree(_patternIntervalTree);
 }
@@ -1343,3 +1392,28 @@ void Terminal::ApplyScheme(const Scheme& colorScheme)
 
     _renderSettings.MakeAdjustedColorArray();
 }
+
+bool Terminal::_inAltBuffer() const noexcept
+{
+    return _altBuffer != nullptr;
+}
+
+TextBuffer& Terminal::_activeBuffer() const noexcept
+{
+    return _inAltBuffer() ? *_altBuffer : *_mainBuffer;
+}
+
+void Terminal::_updateUrlDetection()
+{
+    if (_detectURLs)
+    {
+        // Add regex pattern recognizers to the buffer
+        // For now, we only add the URI regex pattern
+        _hyperlinkPatternId = _activeBuffer().AddPatternRecognizer(linkPattern);
+        UpdatePatternsUnderLock();
+    }
+    else
+    {
+        ClearPatternTree();
+    }
+}
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index d094115d4de..4fd6a20ded6 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -138,6 +138,8 @@ class Microsoft::Terminal::Core::Terminal final :
     void PushGraphicsRendition(const ::Microsoft::Console::VirtualTerminal::VTParameters options) override;
     void PopGraphicsRendition() override;
 
+    void UseAlternateScreenBuffer() override;
+    void UseMainScreenBuffer() override;
 #pragma endregion
 
 #pragma region ITerminalInput
@@ -319,11 +321,11 @@ class Microsoft::Terminal::Core::Terminal final :
     SelectionExpansion _multiClickSelectionMode;
 #pragma endregion
 
-    // TODO: These members are not shared by an alt-buffer. They should be
-    //      encapsulated, such that a Terminal can have both a main and alt buffer.
-    std::unique_ptr<TextBuffer> _buffer;
+    std::unique_ptr<TextBuffer> _mainBuffer;
+    std::unique_ptr<TextBuffer> _altBuffer;
     Microsoft::Console::Types::Viewport _mutableViewport;
     SHORT _scrollbackLines;
+    bool _detectURLs{ false };
 
     // _scrollOffset is the number of lines above the viewport that are currently visible
     // If _scrollOffset is 0, then the visible region of the buffer is the viewport.
@@ -375,6 +377,10 @@ class Microsoft::Terminal::Core::Terminal final :
 
     void _NotifyTerminalCursorPositionChanged() noexcept;
 
+    bool _inAltBuffer() const noexcept;
+    TextBuffer& _activeBuffer() const noexcept;
+    void _updateUrlDetection();
+
 #pragma region TextSelection
     // These methods are defined in TerminalSelection.cpp
     std::vector<SMALL_RECT> _GetSelectionRects() const noexcept;
diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp
index 5cee876e51d..36fae0efd42 100644
--- a/src/cascadia/TerminalCore/TerminalApi.cpp
+++ b/src/cascadia/TerminalCore/TerminalApi.cpp
@@ -18,7 +18,7 @@ void Terminal::PrintString(std::wstring_view stringView)
 
 TextAttribute Terminal::GetTextAttributes() const
 {
-    return _buffer->GetCurrentAttributes();
+    return _activeBuffer().GetCurrentAttributes();
 }
 
 bool Terminal::ReturnResponse(std::wstring_view responseString)
@@ -33,12 +33,12 @@ bool Terminal::ReturnResponse(std::wstring_view responseString)
 
 void Terminal::SetTextAttributes(const TextAttribute& attrs)
 {
-    _buffer->SetCurrentAttributes(attrs);
+    _activeBuffer().SetCurrentAttributes(attrs);
 }
 
 Viewport Terminal::GetBufferSize()
 {
-    return _buffer->GetSize();
+    return _activeBuffer().GetSize();
 }
 
 void Terminal::SetCursorPosition(short x, short y)
@@ -49,12 +49,12 @@ void Terminal::SetCursorPosition(short x, short y)
     const short absoluteY = viewOrigin.Y + y;
     COORD newPos{ absoluteX, absoluteY };
     viewport.Clamp(newPos);
-    _buffer->GetCursor().SetPosition(newPos);
+    _activeBuffer().GetCursor().SetPosition(newPos);
 }
 
 COORD Terminal::GetCursorPosition()
 {
-    const auto absoluteCursorPos = _buffer->GetCursor().GetPosition();
+    const auto absoluteCursorPos = _activeBuffer().GetCursor().GetPosition();
     const auto viewport = _GetMutableViewport();
     const auto viewOrigin = viewport.Origin();
     const short relativeX = absoluteCursorPos.X - viewOrigin.X;
@@ -73,11 +73,11 @@ COORD Terminal::GetCursorPosition()
 // - <none>
 void Terminal::CursorLineFeed(const bool withReturn)
 {
-    auto cursorPos = _buffer->GetCursor().GetPosition();
+    auto cursorPos = _activeBuffer().GetCursor().GetPosition();
 
     // since we explicitly just moved down a row, clear the wrap status on the
     // row we just came from
-    _buffer->GetRowByOffset(cursorPos.Y).SetWrapForced(false);
+    _activeBuffer().GetRowByOffset(cursorPos.Y).SetWrapForced(false);
 
     cursorPos.Y++;
     if (withReturn)
@@ -101,10 +101,12 @@ void Terminal::DeleteCharacter(const size_t count)
 {
     SHORT dist;
     THROW_IF_FAILED(SizeTToShort(count, &dist));
-    const auto cursorPos = _buffer->GetCursor().GetPosition();
+    const auto cursorPos = _activeBuffer().GetCursor().GetPosition();
     const auto copyToPos = cursorPos;
     const COORD copyFromPos{ cursorPos.X + dist, cursorPos.Y };
-    const auto sourceWidth = _mutableViewport.RightExclusive() - copyFromPos.X;
+    const auto viewport = _GetMutableViewport();
+
+    const auto sourceWidth = viewport.RightExclusive() - copyFromPos.X;
     SHORT width;
     THROW_IF_FAILED(UIntToShort(sourceWidth, &width));
 
@@ -121,8 +123,8 @@ void Terminal::DeleteCharacter(const size_t count)
     // Iterate over the source cell data and copy it over to the target
     do
     {
-        const auto data = OutputCell(*(_buffer->GetCellDataAt(sourcePos)));
-        _buffer->Write(OutputCellIterator({ &data, 1 }), targetPos);
+        const auto data = OutputCell(*(_activeBuffer().GetCellDataAt(sourcePos)));
+        _activeBuffer().Write(OutputCellIterator({ &data, 1 }), targetPos);
     } while (source.WalkInBounds(sourcePos, walkDirection) && target.WalkInBounds(targetPos, walkDirection));
 }
 
@@ -143,10 +145,11 @@ void Terminal::InsertCharacter(const size_t count)
     // TODO: GitHub issue #2163
     SHORT dist;
     THROW_IF_FAILED(SizeTToShort(count, &dist));
-    const auto cursorPos = _buffer->GetCursor().GetPosition();
+    const auto cursorPos = _activeBuffer().GetCursor().GetPosition();
     const auto copyFromPos = cursorPos;
     const COORD copyToPos{ cursorPos.X + dist, cursorPos.Y };
-    const auto sourceWidth = _mutableViewport.RightExclusive() - copyFromPos.X;
+    const auto viewport = _GetMutableViewport();
+    const auto sourceWidth = viewport.RightExclusive() - copyFromPos.X;
     SHORT width;
     THROW_IF_FAILED(UIntToShort(sourceWidth, &width));
 
@@ -164,21 +167,21 @@ void Terminal::InsertCharacter(const size_t count)
     // Iterate over the source cell data and copy it over to the target
     do
     {
-        const auto data = OutputCell(*(_buffer->GetCellDataAt(sourcePos)));
-        _buffer->Write(OutputCellIterator({ &data, 1 }), targetPos);
+        const auto data = OutputCell(*(_activeBuffer().GetCellDataAt(sourcePos)));
+        _activeBuffer().Write(OutputCellIterator({ &data, 1 }), targetPos);
     } while (source.WalkInBounds(sourcePos, walkDirection) && target.WalkInBounds(targetPos, walkDirection));
-    const auto eraseIter = OutputCellIterator(UNICODE_SPACE, _buffer->GetCurrentAttributes(), dist);
-    _buffer->Write(eraseIter, cursorPos);
+    const auto eraseIter = OutputCellIterator(UNICODE_SPACE, _activeBuffer().GetCurrentAttributes(), dist);
+    _activeBuffer().Write(eraseIter, cursorPos);
 }
 
 void Terminal::EraseCharacters(const size_t numChars)
 {
-    const auto absoluteCursorPos = _buffer->GetCursor().GetPosition();
+    const auto absoluteCursorPos = _activeBuffer().GetCursor().GetPosition();
     const auto viewport = _GetMutableViewport();
     const short distanceToRight = viewport.RightExclusive() - absoluteCursorPos.X;
     const short fillLimit = std::min(static_cast<short>(numChars), distanceToRight);
-    const auto eraseIter = OutputCellIterator(UNICODE_SPACE, _buffer->GetCurrentAttributes(), fillLimit);
-    _buffer->Write(eraseIter, absoluteCursorPos);
+    const auto eraseIter = OutputCellIterator(UNICODE_SPACE, _activeBuffer().GetCurrentAttributes(), fillLimit);
+    _activeBuffer().Write(eraseIter, absoluteCursorPos);
 }
 
 // Method description:
@@ -193,7 +196,7 @@ void Terminal::EraseCharacters(const size_t numChars)
 // - true if succeeded, false otherwise
 bool Terminal::EraseInLine(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::EraseType eraseType)
 {
-    const auto cursorPos = _buffer->GetCursor().GetPosition();
+    const auto cursorPos = _activeBuffer().GetCursor().GetPosition();
     const auto viewport = _GetMutableViewport();
     COORD startPos = { 0 };
     startPos.Y = cursorPos.Y;
@@ -218,10 +221,10 @@ bool Terminal::EraseInLine(const ::Microsoft::Console::VirtualTerminal::Dispatch
         return false;
     }
 
-    const auto eraseIter = OutputCellIterator(UNICODE_SPACE, _buffer->GetCurrentAttributes(), nlength);
+    const auto eraseIter = OutputCellIterator(UNICODE_SPACE, _activeBuffer().GetCurrentAttributes(), nlength);
 
     // Explicitly turn off end-of-line wrap-flag-setting when erasing cells.
-    _buffer->Write(eraseIter, startPos, false);
+    _activeBuffer().Write(eraseIter, startPos, false);
     return true;
 }
 
@@ -236,22 +239,33 @@ bool Terminal::EraseInLine(const ::Microsoft::Console::VirtualTerminal::Dispatch
 bool Terminal::EraseInDisplay(const DispatchTypes::EraseType eraseType)
 {
     // Store the relative cursor position so we can restore it later after we move the viewport
-    const auto cursorPos = _buffer->GetCursor().GetPosition();
+    const auto cursorPos = _activeBuffer().GetCursor().GetPosition();
 #pragma warning(suppress : 26496) // This is written by ConvertToOrigin, cpp core checks is wrong saying it should be const.
     auto relativeCursor = cursorPos;
-    _mutableViewport.ConvertToOrigin(&relativeCursor);
+    const auto viewport = _GetMutableViewport();
+
+    viewport.ConvertToOrigin(&relativeCursor);
 
     // Initialize the new location of the viewport
     // the top and bottom parameters are determined by the eraseType
     SMALL_RECT newWin;
-    newWin.Left = _mutableViewport.Left();
-    newWin.Right = _mutableViewport.RightExclusive();
+    newWin.Left = viewport.Left();
+    newWin.Right = viewport.RightExclusive();
 
     if (eraseType == DispatchTypes::EraseType::All)
     {
+        // If we're in the alt buffer, take a shortcut. Just increment the buffer enough to cycle the whole thing out and start fresh. This
+        if (_inAltBuffer())
+        {
+            for (auto i = 0; i < viewport.Height(); i++)
+            {
+                _activeBuffer().IncrementCircularBuffer();
+            }
+            return true;
+        }
         // In this case, we simply move the viewport down, effectively pushing whatever text was on the screen into the scrollback
         // and thus 'erasing' the text visible to the user
-        const auto coordLastChar = _buffer->GetLastNonSpaceCharacter(_mutableViewport);
+        const auto coordLastChar = _activeBuffer().GetLastNonSpaceCharacter(viewport);
         if (coordLastChar.X == 0 && coordLastChar.Y == 0)
         {
             // Nothing to clear, just return
@@ -261,38 +275,38 @@ bool Terminal::EraseInDisplay(const DispatchTypes::EraseType eraseType)
         short sNewTop = coordLastChar.Y + 1;
 
         // Increment the circular buffer only if the new location of the viewport would be 'below' the buffer
-        const short delta = (sNewTop + _mutableViewport.Height()) - (_buffer->GetSize().Height());
+        const short delta = (sNewTop + viewport.Height()) - (_activeBuffer().GetSize().Height());
         for (auto i = 0; i < delta; i++)
         {
-            _buffer->IncrementCircularBuffer();
+            _activeBuffer().IncrementCircularBuffer();
             sNewTop--;
         }
 
         newWin.Top = sNewTop;
-        newWin.Bottom = sNewTop + _mutableViewport.Height();
+        newWin.Bottom = sNewTop + viewport.Height();
     }
     else if (eraseType == DispatchTypes::EraseType::Scrollback)
     {
         // We only want to erase the scrollback, and leave everything else on the screen as it is
         // so we grab the text in the viewport and rotate it up to the top of the buffer
         COORD scrollFromPos{ 0, 0 };
-        _mutableViewport.ConvertFromOrigin(&scrollFromPos);
-        _buffer->ScrollRows(scrollFromPos.Y, _mutableViewport.Height(), -scrollFromPos.Y);
+        viewport.ConvertFromOrigin(&scrollFromPos);
+        _activeBuffer().ScrollRows(scrollFromPos.Y, viewport.Height(), -scrollFromPos.Y);
 
         // Since we only did a rotation, the text that was in the scrollback is now _below_ where we are going to move the viewport
         // and we have to make sure we erase that text
-        const auto eraseStart = _mutableViewport.Height();
-        const auto eraseEnd = _buffer->GetLastNonSpaceCharacter(_mutableViewport).Y;
+        const auto eraseStart = viewport.Height();
+        const auto eraseEnd = _activeBuffer().GetLastNonSpaceCharacter(viewport).Y;
         for (SHORT i = eraseStart; i <= eraseEnd; i++)
         {
-            _buffer->GetRowByOffset(i).Reset(_buffer->GetCurrentAttributes());
+            _activeBuffer().GetRowByOffset(i).Reset(_activeBuffer().GetCurrentAttributes());
         }
 
         // Reset the scroll offset now because there's nothing for the user to 'scroll' to
         _scrollOffset = 0;
 
         newWin.Top = 0;
-        newWin.Bottom = _mutableViewport.Height();
+        newWin.Bottom = viewport.Height();
     }
     else
     {
@@ -350,7 +364,7 @@ void Terminal::SetColorTableEntry(const size_t tableIndex, const COLORREF color)
     }
 
     // Repaint everything - the colors might have changed
-    _buffer->TriggerRedrawAll();
+    _activeBuffer().TriggerRedrawAll();
 }
 
 // Method Description:
@@ -412,8 +426,8 @@ void Terminal::SetCursorStyle(const DispatchTypes::CursorStyle cursorStyle)
         return;
     }
 
-    _buffer->GetCursor().SetType(finalCursorType);
-    _buffer->GetCursor().SetBlinkingAllowed(shouldBlink);
+    _activeBuffer().GetCursor().SetType(finalCursorType);
+    _activeBuffer().GetCursor().SetBlinkingAllowed(shouldBlink);
 }
 
 void Terminal::SetInputMode(const TerminalInput::Mode mode, const bool enabled)
@@ -426,7 +440,7 @@ void Terminal::SetRenderMode(const RenderSettings::Mode mode, const bool enabled
     _renderSettings.SetRenderMode(mode, enabled);
 
     // Repaint everything - the colors will have changed
-    _buffer->TriggerRedrawAll();
+    _activeBuffer().TriggerRedrawAll();
 }
 
 void Terminal::EnableXtermBracketedPasteMode(const bool enabled)
@@ -447,12 +461,12 @@ bool Terminal::IsVtInputEnabled() const
 
 void Terminal::SetCursorVisibility(const bool visible)
 {
-    _buffer->GetCursor().SetIsVisible(visible);
+    _activeBuffer().GetCursor().SetIsVisible(visible);
 }
 
 void Terminal::EnableCursorBlinking(const bool enable)
 {
-    _buffer->GetCursor().SetBlinkingAllowed(enable);
+    _activeBuffer().GetCursor().SetBlinkingAllowed(enable);
 
     // GH#2642 - From what we've gathered from other terminals, when blinking is
     // disabled, the cursor should remain On always, and have the visibility
@@ -460,7 +474,7 @@ void Terminal::EnableCursorBlinking(const bool enable)
     // to disable blinking, the cursor stays stuck On. At this point, only the
     // cursor visibility property controls whether the user can see it or not.
     // (Yes, the cursor can be On and NOT Visible)
-    _buffer->GetCursor().SetIsOn(true);
+    _activeBuffer().GetCursor().SetIsOn(true);
 }
 
 void Terminal::CopyToClipboard(std::wstring_view content)
@@ -477,11 +491,11 @@ void Terminal::CopyToClipboard(std::wstring_view content)
 // - <none>
 void Terminal::AddHyperlink(std::wstring_view uri, std::wstring_view params)
 {
-    auto attr = _buffer->GetCurrentAttributes();
-    const auto id = _buffer->GetHyperlinkId(uri, params);
+    auto attr = _activeBuffer().GetCurrentAttributes();
+    const auto id = _activeBuffer().GetHyperlinkId(uri, params);
     attr.SetHyperlinkId(id);
-    _buffer->SetCurrentAttributes(attr);
-    _buffer->AddHyperlinkToMap(uri, id);
+    _activeBuffer().SetCurrentAttributes(attr);
+    _activeBuffer().AddHyperlinkToMap(uri, id);
 }
 
 // Method Description:
@@ -490,9 +504,9 @@ void Terminal::AddHyperlink(std::wstring_view uri, std::wstring_view params)
 // - <none>
 void Terminal::EndHyperlink()
 {
-    auto attr = _buffer->GetCurrentAttributes();
+    auto attr = _activeBuffer().GetCurrentAttributes();
     attr.SetHyperlinkId(0);
-    _buffer->SetCurrentAttributes(attr);
+    _activeBuffer().SetCurrentAttributes(attr);
 }
 
 // Method Description:
@@ -565,7 +579,7 @@ std::wstring_view Terminal::GetWorkingDirectory()
 // - <none>
 void Terminal::PushGraphicsRendition(const VTParameters options)
 {
-    _sgrStack.Push(_buffer->GetCurrentAttributes(), options);
+    _sgrStack.Push(_activeBuffer().GetCurrentAttributes(), options);
 }
 
 // Method Description:
@@ -577,6 +591,96 @@ void Terminal::PushGraphicsRendition(const VTParameters options)
 // - <none>
 void Terminal::PopGraphicsRendition()
 {
-    const TextAttribute current = _buffer->GetCurrentAttributes();
-    _buffer->SetCurrentAttributes(_sgrStack.Pop(current));
+    const TextAttribute current = _activeBuffer().GetCurrentAttributes();
+    _activeBuffer().SetCurrentAttributes(_sgrStack.Pop(current));
+}
+
+void Terminal::UseAlternateScreenBuffer()
+{
+    // the new alt buffer is exactly the size of the viewport.
+    const COORD bufferSize{ _mutableViewport.Dimensions() };
+    const auto cursorSize = _mainBuffer->GetCursor().GetSize();
+
+    ClearSelection();
+    _mainBuffer->ClearPatternRecognizers();
+
+    // Create a new alt buffer
+    _altBuffer = std::make_unique<TextBuffer>(bufferSize,
+                                              TextAttribute{},
+                                              cursorSize,
+                                              true,
+                                              _mainBuffer->GetRenderer());
+    _mainBuffer->SetAsActiveBuffer(false);
+
+    // Copy our cursor state to the new buffer's cursor
+    {
+        // Update the alt buffer's cursor style, visibility, and position to match our own.
+        auto& myCursor = _mainBuffer->GetCursor();
+        auto& tgtCursor = _altBuffer->GetCursor();
+        tgtCursor.SetStyle(myCursor.GetSize(), myCursor.GetType());
+        tgtCursor.SetIsVisible(myCursor.IsVisible());
+        tgtCursor.SetBlinkingAllowed(myCursor.IsBlinkingAllowed());
+
+        // The new position should match the viewport-relative position of the main buffer.
+        auto tgtCursorPos = myCursor.GetPosition();
+        tgtCursorPos.Y -= _mutableViewport.Top();
+        tgtCursor.SetPosition(tgtCursorPos);
+    }
+
+    // update all the hyperlinks on the screen
+    _updateUrlDetection();
+
+    // Update scrollbars
+    _NotifyScrollEvent();
+
+    // redraw the screen
+    try
+    {
+        _activeBuffer().TriggerRedrawAll();
+    }
+    CATCH_LOG();
+}
+void Terminal::UseMainScreenBuffer()
+{
+    // Short-circuit: do nothing.
+    if (!_inAltBuffer())
+    {
+        return;
+    }
+
+    ClearSelection();
+
+    // Copy our cursor state back to the main buffer's cursor
+    {
+        // Update the alt buffer's cursor style, visibility, and position to match our own.
+        auto& myCursor = _altBuffer->GetCursor();
+        auto& tgtCursor = _mainBuffer->GetCursor();
+        tgtCursor.SetStyle(myCursor.GetSize(), myCursor.GetType());
+        tgtCursor.SetIsVisible(myCursor.IsVisible());
+        tgtCursor.SetBlinkingAllowed(myCursor.IsBlinkingAllowed());
+
+        // The new position should match the viewport-relative position of the main buffer.
+        // This is the equal and opposite effect of what we did in UseAlternateScreenBuffer
+        auto tgtCursorPos = myCursor.GetPosition();
+        tgtCursorPos.Y += _mutableViewport.Top();
+        tgtCursor.SetPosition(tgtCursorPos);
+    }
+
+    _mainBuffer->SetAsActiveBuffer(true);
+    // destroy the alt buffer
+    _altBuffer = nullptr;
+
+    // update all the hyperlinks on the screen
+    _mainBuffer->ClearPatternRecognizers();
+    _updateUrlDetection();
+
+    // Update scrollbars
+    _NotifyScrollEvent();
+
+    // redraw the screen
+    try
+    {
+        _activeBuffer().TriggerRedrawAll();
+    }
+    CATCH_LOG();
 }
diff --git a/src/cascadia/TerminalCore/TerminalDispatch.cpp b/src/cascadia/TerminalCore/TerminalDispatch.cpp
index 1747185b64a..689e29de93a 100644
--- a/src/cascadia/TerminalCore/TerminalDispatch.cpp
+++ b/src/cascadia/TerminalCore/TerminalDispatch.cpp
@@ -658,6 +658,9 @@ bool TerminalDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param,
     case DispatchTypes::ModeParams::W32IM_Win32InputMode:
         success = EnableWin32InputMode(enable);
         break;
+    case DispatchTypes::ModeParams::ASB_AlternateScreenBuffer:
+        success = enable ? UseAlternateScreenBuffer() : UseMainScreenBuffer();
+        break;
     default:
         // If no functions to call, overall dispatch was a failure.
         success = false;
@@ -786,3 +789,84 @@ bool TerminalDispatch::HardReset()
 
     return true;
 }
+
+// Routine Description:
+// - DECSC - Saves the current "cursor state" into a memory buffer. This
+//   includes the cursor position, origin mode, graphic rendition, and
+//   active character set.
+// Arguments:
+// - <none>
+// Return Value:
+// - True.
+bool TerminalDispatch::CursorSaveState()
+{
+    // TODO GH#3849: When de-duplicating this, the AdaptDispatch version of this
+    // is more elaborate.
+    const auto attributes = _terminalApi.GetTextAttributes();
+    COORD coordCursor = _terminalApi.GetCursorPosition();
+    // The cursor is given to us by the API as relative to current viewport top.
+
+    // VT is also 1 based, not 0 based, so correct by 1.
+    auto& savedCursorState = _savedCursorState.at(_usingAltBuffer);
+    savedCursorState.Column = coordCursor.X + 1;
+    savedCursorState.Row = coordCursor.Y + 1;
+    savedCursorState.Attributes = attributes;
+
+    return true;
+}
+
+// Routine Description:
+// - DECRC - Restores a saved "cursor state" from the DECSC command back into
+//   the console state. This includes the cursor position, origin mode, graphic
+//   rendition, and active character set.
+// Arguments:
+// - <none>
+// Return Value:
+// - True.
+bool TerminalDispatch::CursorRestoreState()
+{
+    // TODO GH#3849: When de-duplicating this, the AdaptDispatch version of this
+    // is more elaborate.
+    auto& savedCursorState = _savedCursorState.at(_usingAltBuffer);
+
+    auto row = savedCursorState.Row;
+    const auto col = savedCursorState.Column;
+
+    // The saved coordinates are always absolute, so we need reset the origin mode temporarily.
+    CursorPosition(row, col);
+
+    // Restore text attributes.
+    _terminalApi.SetTextAttributes(savedCursorState.Attributes);
+
+    return true;
+}
+
+// - ASBSET - Creates and swaps to the alternate screen buffer. In virtual terminals, there exists both a "main"
+//     screen buffer and an alternate. ASBSET creates a new alternate, and switches to it. If there is an already
+//     existing alternate, it is discarded.
+// Arguments:
+// - None
+// Return Value:
+// - True.
+bool TerminalDispatch::UseAlternateScreenBuffer()
+{
+    CursorSaveState();
+    _terminalApi.UseAlternateScreenBuffer();
+    _usingAltBuffer = true;
+    return true;
+}
+
+// Routine Description:
+// - ASBRST - From the alternate buffer, returns to the main screen buffer.
+//     From the main screen buffer, does nothing. The alternate is discarded.
+// Arguments:
+// - None
+// Return Value:
+// - True.
+bool TerminalDispatch::UseMainScreenBuffer()
+{
+    _terminalApi.UseMainScreenBuffer();
+    _usingAltBuffer = false;
+    CursorRestoreState();
+    return true;
+}
diff --git a/src/cascadia/TerminalCore/TerminalDispatch.hpp b/src/cascadia/TerminalCore/TerminalDispatch.hpp
index cfec1aaadfe..42188bc4ecf 100644
--- a/src/cascadia/TerminalCore/TerminalDispatch.hpp
+++ b/src/cascadia/TerminalCore/TerminalDispatch.hpp
@@ -39,6 +39,9 @@ class TerminalDispatch : public Microsoft::Console::VirtualTerminal::TermDispatc
     bool CarriageReturn() override;
     bool SetWindowTitle(std::wstring_view title) override;
 
+    bool UseAlternateScreenBuffer() override; // ASBSET
+    bool UseMainScreenBuffer() override; // ASBRST
+
     bool HorizontalTabSet() override; // HTS
     bool ForwardTab(const size_t numTabs) override; // CHT, HT
     bool BackwardsTab(const size_t numTabs) override; // CBT
@@ -82,12 +85,31 @@ class TerminalDispatch : public Microsoft::Console::VirtualTerminal::TermDispatc
 
     bool DoConEmuAction(const std::wstring_view string) override;
 
+    bool CursorSaveState() override;
+    bool CursorRestoreState() override;
+
 private:
     ::Microsoft::Terminal::Core::ITerminalApi& _terminalApi;
 
+    // Dramatically simplified version of AdaptDispatch::CursorState
+    struct CursorState
+    {
+        unsigned int Row = 1;
+        unsigned int Column = 1;
+        TextAttribute Attributes = {};
+    };
+
     std::vector<bool> _tabStopColumns;
     bool _initDefaultTabStops = true;
 
+    // We have two instances of the saved cursor state, because we need
+    // one for the main buffer (at index 0), and another for the alt buffer
+    // (at index 1). The _usingAltBuffer property keeps tracks of which
+    // buffer is active, so can be used as an index into this array to
+    // obtain the saved state that should be currently active.
+    std::array<CursorState, 2> _savedCursorState;
+    bool _usingAltBuffer = false;
+
     size_t _SetRgbColorsHelper(const ::Microsoft::Console::VirtualTerminal::VTParameters options,
                                TextAttribute& attr,
                                const bool isForeground);
diff --git a/src/cascadia/TerminalCore/TerminalSelection.cpp b/src/cascadia/TerminalCore/TerminalSelection.cpp
index c0b20b64d23..2a29b109eb0 100644
--- a/src/cascadia/TerminalCore/TerminalSelection.cpp
+++ b/src/cascadia/TerminalCore/TerminalSelection.cpp
@@ -54,7 +54,7 @@ std::vector<SMALL_RECT> Terminal::_GetSelectionRects() const noexcept
 
     try
     {
-        return _buffer->GetTextRects(_selection->start, _selection->end, _blockSelection, false);
+        return _activeBuffer().GetTextRects(_selection->start, _selection->end, _blockSelection, false);
     }
     CATCH_LOG();
     return result;
@@ -182,7 +182,7 @@ void Terminal::SetSelectionEnd(const COORD viewportPos, std::optional<SelectionE
 // - the new start/end for a selection
 std::pair<COORD, COORD> Terminal::_PivotSelection(const COORD targetPos, bool& targetStart) const
 {
-    if (targetStart = _buffer->GetSize().CompareInBounds(targetPos, _selection->pivot) <= 0)
+    if (targetStart = _activeBuffer().GetSize().CompareInBounds(targetPos, _selection->pivot) <= 0)
     {
         // target is before pivot
         // treat target as start
@@ -207,7 +207,7 @@ std::pair<COORD, COORD> Terminal::_ExpandSelectionAnchors(std::pair<COORD, COORD
     COORD start = anchors.first;
     COORD end = anchors.second;
 
-    const auto bufferSize = _buffer->GetSize();
+    const auto bufferSize = _activeBuffer().GetSize();
     switch (_multiClickSelectionMode)
     {
     case SelectionExpansion::Line:
@@ -215,8 +215,8 @@ std::pair<COORD, COORD> Terminal::_ExpandSelectionAnchors(std::pair<COORD, COORD
         end = { bufferSize.RightInclusive(), end.Y };
         break;
     case SelectionExpansion::Word:
-        start = _buffer->GetWordStart(start, _wordDelimiters);
-        end = _buffer->GetWordEnd(end, _wordDelimiters);
+        start = _activeBuffer().GetWordStart(start, _wordDelimiters);
+        end = _activeBuffer().GetWordEnd(end, _wordDelimiters);
         break;
     case SelectionExpansion::Char:
     default:
@@ -325,7 +325,7 @@ void Terminal::UpdateSelection(SelectionDirection direction, SelectionExpansion
             _scrollOffset -= amtBelowView;
         }
         _NotifyScrollEvent();
-        _buffer->TriggerScroll();
+        _activeBuffer().TriggerScroll();
     }
 }
 
@@ -334,22 +334,22 @@ void Terminal::_MoveByChar(SelectionDirection direction, COORD& pos)
     switch (direction)
     {
     case SelectionDirection::Left:
-        _buffer->GetSize().DecrementInBounds(pos);
-        pos = _buffer->GetGlyphStart(til::point{ pos }).to_win32_coord();
+        _activeBuffer().GetSize().DecrementInBounds(pos);
+        pos = _activeBuffer().GetGlyphStart(til::point{ pos }).to_win32_coord();
         break;
     case SelectionDirection::Right:
-        _buffer->GetSize().IncrementInBounds(pos);
-        pos = _buffer->GetGlyphEnd(til::point{ pos }).to_win32_coord();
+        _activeBuffer().GetSize().IncrementInBounds(pos);
+        pos = _activeBuffer().GetGlyphEnd(til::point{ pos }).to_win32_coord();
         break;
     case SelectionDirection::Up:
     {
-        const auto bufferSize{ _buffer->GetSize() };
+        const auto bufferSize{ _activeBuffer().GetSize() };
         pos = { pos.X, std::clamp(base::ClampSub<short, short>(pos.Y, 1).RawValue(), bufferSize.Top(), bufferSize.BottomInclusive()) };
         break;
     }
     case SelectionDirection::Down:
     {
-        const auto bufferSize{ _buffer->GetSize() };
+        const auto bufferSize{ _activeBuffer().GetSize() };
         pos = { pos.X, std::clamp(base::ClampAdd<short, short>(pos.Y, 1).RawValue(), bufferSize.Top(), bufferSize.BottomInclusive()) };
         break;
     }
@@ -361,19 +361,19 @@ void Terminal::_MoveByWord(SelectionDirection direction, COORD& pos)
     switch (direction)
     {
     case SelectionDirection::Left:
-        const auto wordStartPos{ _buffer->GetWordStart(pos, _wordDelimiters) };
-        if (_buffer->GetSize().CompareInBounds(_selection->pivot, pos) < 0)
+        const auto wordStartPos{ _activeBuffer().GetWordStart(pos, _wordDelimiters) };
+        if (_activeBuffer().GetSize().CompareInBounds(_selection->pivot, pos) < 0)
         {
             // If we're moving towards the pivot, move one more cell
             pos = wordStartPos;
-            _buffer->GetSize().DecrementInBounds(pos);
+            _activeBuffer().GetSize().DecrementInBounds(pos);
         }
         else if (wordStartPos == pos)
         {
             // already at the beginning of the current word,
             // move to the beginning of the previous word
-            _buffer->GetSize().DecrementInBounds(pos);
-            pos = _buffer->GetWordStart(pos, _wordDelimiters);
+            _activeBuffer().GetSize().DecrementInBounds(pos);
+            pos = _activeBuffer().GetWordStart(pos, _wordDelimiters);
         }
         else
         {
@@ -382,19 +382,19 @@ void Terminal::_MoveByWord(SelectionDirection direction, COORD& pos)
         }
         break;
     case SelectionDirection::Right:
-        const auto wordEndPos{ _buffer->GetWordEnd(pos, _wordDelimiters) };
-        if (_buffer->GetSize().CompareInBounds(pos, _selection->pivot) < 0)
+        const auto wordEndPos{ _activeBuffer().GetWordEnd(pos, _wordDelimiters) };
+        if (_activeBuffer().GetSize().CompareInBounds(pos, _selection->pivot) < 0)
         {
             // If we're moving towards the pivot, move one more cell
-            pos = _buffer->GetWordEnd(pos, _wordDelimiters);
-            _buffer->GetSize().IncrementInBounds(pos);
+            pos = _activeBuffer().GetWordEnd(pos, _wordDelimiters);
+            _activeBuffer().GetSize().IncrementInBounds(pos);
         }
         else if (wordEndPos == pos)
         {
             // already at the end of the current word,
             // move to the end of the next word
-            _buffer->GetSize().IncrementInBounds(pos);
-            pos = _buffer->GetWordEnd(pos, _wordDelimiters);
+            _activeBuffer().GetSize().IncrementInBounds(pos);
+            pos = _activeBuffer().GetWordEnd(pos, _wordDelimiters);
         }
         else
         {
@@ -404,18 +404,18 @@ void Terminal::_MoveByWord(SelectionDirection direction, COORD& pos)
         break;
     case SelectionDirection::Up:
         _MoveByChar(direction, pos);
-        pos = _buffer->GetWordStart(pos, _wordDelimiters);
+        pos = _activeBuffer().GetWordStart(pos, _wordDelimiters);
         break;
     case SelectionDirection::Down:
         _MoveByChar(direction, pos);
-        pos = _buffer->GetWordEnd(pos, _wordDelimiters);
+        pos = _activeBuffer().GetWordEnd(pos, _wordDelimiters);
         break;
     }
 }
 
 void Terminal::_MoveByViewport(SelectionDirection direction, COORD& pos)
 {
-    const auto bufferSize{ _buffer->GetSize() };
+    const auto bufferSize{ _activeBuffer().GetSize() };
     switch (direction)
     {
     case SelectionDirection::Left:
@@ -444,7 +444,7 @@ void Terminal::_MoveByViewport(SelectionDirection direction, COORD& pos)
 
 void Terminal::_MoveByBuffer(SelectionDirection direction, COORD& pos)
 {
-    const auto bufferSize{ _buffer->GetSize() };
+    const auto bufferSize{ _activeBuffer().GetSize() };
     switch (direction)
     {
     case SelectionDirection::Left:
@@ -489,7 +489,7 @@ const TextBuffer::TextAndColor Terminal::RetrieveSelectedTextFromBuffer(bool sin
     const auto includeCRLF = !singleLine || _blockSelection;
     const auto trimTrailingWhitespace = !singleLine && (!_blockSelection || _trimBlockSelection);
     const auto formatWrappedRows = _blockSelection;
-    return _buffer->GetText(includeCRLF, trimTrailingWhitespace, selectionRects, GetAttributeColors, formatWrappedRows);
+    return _activeBuffer().GetText(includeCRLF, trimTrailingWhitespace, selectionRects, GetAttributeColors, formatWrappedRows);
 }
 
 // Method Description:
@@ -502,7 +502,7 @@ COORD Terminal::_ConvertToBufferCell(const COORD viewportPos) const
 {
     const auto yPos = base::ClampedNumeric<short>(_VisibleStartIndex()) + viewportPos.Y;
     COORD bufferPos = { viewportPos.X, yPos };
-    _buffer->GetSize().Clamp(bufferPos);
+    _activeBuffer().GetSize().Clamp(bufferPos);
     return bufferPos;
 }
 
diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp
index 212551ae21a..d7d24b5e415 100644
--- a/src/cascadia/TerminalCore/terminalrenderdata.cpp
+++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp
@@ -25,7 +25,7 @@ COORD Terminal::GetTextBufferEndPosition() const noexcept
 
 const TextBuffer& Terminal::GetTextBuffer() noexcept
 {
-    return *_buffer;
+    return _activeBuffer();
 }
 
 const FontInfo& Terminal::GetFontInfo() noexcept
@@ -40,19 +40,19 @@ void Terminal::SetFontInfo(const FontInfo& fontInfo)
 
 COORD Terminal::GetCursorPosition() const noexcept
 {
-    const auto& cursor = _buffer->GetCursor();
+    const auto& cursor = _activeBuffer().GetCursor();
     return cursor.GetPosition();
 }
 
 bool Terminal::IsCursorVisible() const noexcept
 {
-    const auto& cursor = _buffer->GetCursor();
+    const auto& cursor = _activeBuffer().GetCursor();
     return cursor.IsVisible() && !cursor.IsPopupShown();
 }
 
 bool Terminal::IsCursorOn() const noexcept
 {
-    const auto& cursor = _buffer->GetCursor();
+    const auto& cursor = _activeBuffer().GetCursor();
     return cursor.IsOn();
 }
 
@@ -63,18 +63,18 @@ ULONG Terminal::GetCursorPixelWidth() const noexcept
 
 ULONG Terminal::GetCursorHeight() const noexcept
 {
-    return _buffer->GetCursor().GetSize();
+    return _activeBuffer().GetCursor().GetSize();
 }
 
 CursorType Terminal::GetCursorStyle() const noexcept
 {
-    return _buffer->GetCursor().GetType();
+    return _activeBuffer().GetCursor().GetType();
 }
 
 bool Terminal::IsCursorDoubleWidth() const
 {
-    const auto position = _buffer->GetCursor().GetPosition();
-    TextBufferTextIterator it(TextBufferCellIterator(*_buffer, position));
+    const auto position = _activeBuffer().GetCursor().GetPosition();
+    TextBufferTextIterator it(TextBufferCellIterator(_activeBuffer(), position));
     return IsGlyphFullWidth(*it);
 }
 
@@ -90,12 +90,12 @@ const bool Terminal::IsGridLineDrawingAllowed() noexcept
 
 const std::wstring Microsoft::Terminal::Core::Terminal::GetHyperlinkUri(uint16_t id) const noexcept
 {
-    return _buffer->GetHyperlinkUriFromId(id);
+    return _activeBuffer().GetHyperlinkUriFromId(id);
 }
 
 const std::wstring Microsoft::Terminal::Core::Terminal::GetHyperlinkCustomId(uint16_t id) const noexcept
 {
-    return _buffer->GetCustomIdFromId(id);
+    return _activeBuffer().GetCustomIdFromId(id);
 }
 
 // Method Description:
@@ -174,7 +174,7 @@ void Terminal::SelectNewRegion(const COORD coordStart, const COORD coordEnd)
 
     if (notifyScrollChange)
     {
-        _buffer->TriggerScroll();
+        _activeBuffer().TriggerScroll();
         _NotifyScrollEvent();
     }
 
@@ -227,5 +227,5 @@ const bool Terminal::IsUiaDataInitialized() const noexcept
     // when a screen reader requests it. However, the terminal might not be fully
     // initialized yet. So we use this to check if any crucial components of
     // UiaData are not yet initialized.
-    return !!_buffer;
+    return !!_mainBuffer;
 }
diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp
index 17c35ef3e4b..082c2d80707 100644
--- a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp
+++ b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp
@@ -222,6 +222,9 @@ class TerminalCoreUnitTests::ConptyRoundtripTests final
 
     TEST_METHOD(ClearBufferSignal);
 
+    TEST_METHOD(SimpleAltBufferTest);
+    TEST_METHOD(AltBufferToAltBufferTest);
+
 private:
     bool _writeCallback(const char* const pch, size_t const cch);
     void _flushFirstFrame();
@@ -322,7 +325,7 @@ void ConptyRoundtripTests::_clearConpty()
 
     // After we resize, make sure to get the new textBuffers
     return { &ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetTextBuffer(),
-             term->_buffer.get() };
+             term->_mainBuffer.get() };
 }
 
 void ConptyRoundtripTests::ConptyOutputTestCanary()
@@ -344,7 +347,7 @@ void ConptyRoundtripTests::SimpleWriteOutputTest()
     auto& gci = g.getConsoleInformation();
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
 
@@ -367,7 +370,7 @@ void ConptyRoundtripTests::WriteTwoLinesUsesNewline()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
 
@@ -402,7 +405,7 @@ void ConptyRoundtripTests::WriteAFewSimpleLines()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
 
@@ -443,7 +446,7 @@ void ConptyRoundtripTests::TestWrappingALongString()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
     _checkConptyOutput = false;
@@ -494,7 +497,7 @@ void ConptyRoundtripTests::TestAdvancedWrapping()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
     const auto initialTermView = term->GetViewport();
 
     _flushFirstFrame();
@@ -559,7 +562,7 @@ void ConptyRoundtripTests::TestExactWrappingWithoutSpaces()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     const auto initialTermView = term->GetViewport();
 
@@ -621,7 +624,7 @@ void ConptyRoundtripTests::TestExactWrappingWithSpaces()
     auto& hostSm = si.GetStateMachine();
 
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
     const auto initialTermView = term->GetViewport();
 
     _flushFirstFrame();
@@ -683,7 +686,7 @@ void ConptyRoundtripTests::MoveCursorAtEOL()
     auto& hostSm = si.GetStateMachine();
 
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
     _flushFirstFrame();
 
     Log::Comment(NoThrowString().Format(
@@ -762,7 +765,7 @@ void ConptyRoundtripTests::TestResizeHeight()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
     auto* hostTb = &si.GetTextBuffer();
-    auto* termTb = term->_buffer.get();
+    auto* termTb = term->_mainBuffer.get();
     const auto initialHostView = si.GetViewport();
     const auto initialTermView = term->GetViewport();
     const auto initialTerminalBufferHeight = term->GetTextBuffer().GetSize().Height();
@@ -962,7 +965,7 @@ void ConptyRoundtripTests::PassthroughCursorShapeImmediately()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
 
@@ -989,7 +992,7 @@ void ConptyRoundtripTests::PassthroughClearScrollback()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
 
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
 
@@ -1066,7 +1069,7 @@ void ConptyRoundtripTests::PassthroughClearAll()
     auto& gci = g.getConsoleInformation();
     auto& si = gci.GetActiveOutputBuffer();
     auto* hostTb = &si.GetTextBuffer();
-    auto* termTb = term->_buffer.get();
+    auto* termTb = term->_mainBuffer.get();
 
     auto& sm = si.GetStateMachine();
 
@@ -1153,7 +1156,7 @@ void ConptyRoundtripTests::PassthroughHardReset()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
 
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
 
@@ -1217,7 +1220,7 @@ void ConptyRoundtripTests::OutputWrappedLinesAtTopOfBuffer()
     auto& si = gci.GetActiveOutputBuffer();
     auto& sm = si.GetStateMachine();
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
 
@@ -1265,7 +1268,7 @@ void ConptyRoundtripTests::OutputWrappedLinesAtBottomOfBuffer()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
 
@@ -1398,7 +1401,7 @@ void ConptyRoundtripTests::ScrollWithChangesInMiddle()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
 
@@ -1499,7 +1502,7 @@ void ConptyRoundtripTests::ScrollWithMargins()
     auto& hostSm = si.GetStateMachine();
 
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
     const auto initialTermView = term->GetViewport();
 
     Log::Comment(L"Flush first frame.");
@@ -1743,7 +1746,7 @@ void ConptyRoundtripTests::DontWrapMoveCursorInSingleFrame()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
 
@@ -1836,7 +1839,7 @@ void ConptyRoundtripTests::ClearHostTrickeryTest()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
 
@@ -1944,7 +1947,7 @@ void ConptyRoundtripTests::OverstrikeAtBottomOfBuffer()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
 
@@ -2022,7 +2025,7 @@ void ConptyRoundtripTests::MarginsWithStatusLine()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
 
@@ -2113,7 +2116,7 @@ void ConptyRoundtripTests::OutputWrappedLineWithSpace()
     auto& si = gci.GetActiveOutputBuffer();
     auto& sm = si.GetStateMachine();
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
 
@@ -2179,7 +2182,7 @@ void ConptyRoundtripTests::OutputWrappedLineWithSpaceAtBottomOfBuffer()
     auto& si = gci.GetActiveOutputBuffer();
     auto& sm = si.GetStateMachine();
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
 
@@ -2334,8 +2337,6 @@ void ConptyRoundtripTests::BreakLinesOnCursorMovement()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
 
-    auto& termTb = *term->_buffer;
-
     _flushFirstFrame();
 
     // Any of the cursor movements that use a LF will actually hard break the
@@ -2475,8 +2476,9 @@ void ConptyRoundtripTests::BreakLinesOnCursorMovement()
     VERIFY_SUCCEEDED(renderer.PaintFrame());
 
     Log::Comment(L"========== Checking the terminal buffer state ==========");
-
-    verifyBuffer(termTb, til::rect{ term->_mutableViewport.ToInclusive() });
+    // GH#3492: Now that we support the alt buffer, make sure to validate the
+    // _alt buffer's_ contents.
+    verifyBuffer(*term->_altBuffer, til::rect{ term->_mutableViewport.ToInclusive() });
 }
 
 void ConptyRoundtripTests::TestCursorInDeferredEOLPositionOnNewLineWithSpaces()
@@ -2488,7 +2490,7 @@ void ConptyRoundtripTests::TestCursorInDeferredEOLPositionOnNewLineWithSpaces()
     auto& hostSm = si.GetStateMachine();
 
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
     const auto termView = term->GetViewport();
 
     _flushFirstFrame();
@@ -2549,7 +2551,7 @@ void ConptyRoundtripTests::ResizeRepaintVimExeBuffer()
     auto& si = gci.GetActiveOutputBuffer();
     auto& sm = si.GetStateMachine();
     auto* hostTb = &si.GetTextBuffer();
-    auto* termTb = term->_buffer.get();
+    auto* termTb = term->_mainBuffer.get();
 
     _flushFirstFrame();
 
@@ -2678,7 +2680,7 @@ void ConptyRoundtripTests::ClsAndClearHostClearsScrollbackTest()
     auto& gci = g.getConsoleInformation();
     auto& si = gci.GetActiveOutputBuffer();
     auto* hostTb = &si.GetTextBuffer();
-    auto* termTb = term->_buffer.get();
+    auto* termTb = term->_mainBuffer.get();
 
     auto& sm = si.GetStateMachine();
 
@@ -2833,7 +2835,7 @@ void ConptyRoundtripTests::TestResizeWithCookedRead()
     auto& gci = g.getConsoleInformation();
     auto& si = gci.GetActiveOutputBuffer();
     auto* hostTb = &si.GetTextBuffer();
-    auto* termTb = term->_buffer.get();
+    auto* termTb = term->_mainBuffer.get();
 
     _flushFirstFrame();
 
@@ -2894,7 +2896,7 @@ void ConptyRoundtripTests::ResizeInitializeBufferWithDefaultAttrs()
     auto& si = gci.GetActiveOutputBuffer();
     auto& sm = si.GetStateMachine();
     auto* hostTb = &si.GetTextBuffer();
-    auto* termTb = term->_buffer.get();
+    auto* termTb = term->_mainBuffer.get();
 
     _flushFirstFrame();
 
@@ -3021,7 +3023,7 @@ void ConptyRoundtripTests::NewLinesAtBottomWithBackground()
     auto& si = gci.GetActiveOutputBuffer();
     auto& sm = si.GetStateMachine();
     auto* hostTb = &si.GetTextBuffer();
-    auto* termTb = term->_buffer.get();
+    auto* termTb = term->_mainBuffer.get();
 
     _flushFirstFrame();
 
@@ -3191,7 +3193,7 @@ void ConptyRoundtripTests::WrapNewLineAtBottom()
     auto& si = gci.GetActiveOutputBuffer();
     auto& sm = si.GetStateMachine();
     auto* hostTb = &si.GetTextBuffer();
-    auto* termTb = term->_buffer.get();
+    auto* termTb = term->_mainBuffer.get();
 
     _flushFirstFrame();
 
@@ -3349,7 +3351,7 @@ void ConptyRoundtripTests::WrapNewLineAtBottomLikeMSYS()
     auto& si = gci.GetActiveOutputBuffer();
     auto& sm = si.GetStateMachine();
     auto* hostTb = &si.GetTextBuffer();
-    auto* termTb = term->_buffer.get();
+    auto* termTb = term->_mainBuffer.get();
 
     _flushFirstFrame();
 
@@ -3533,7 +3535,7 @@ void ConptyRoundtripTests::DeleteWrappedWord()
     auto& si = gci.GetActiveOutputBuffer();
     auto& sm = si.GetStateMachine();
     auto* hostTb = &si.GetTextBuffer();
-    auto* termTb = term->_buffer.get();
+    auto* termTb = term->_mainBuffer.get();
 
     _flushFirstFrame();
 
@@ -3625,7 +3627,7 @@ void ConptyRoundtripTests::HyperlinkIdConsistency()
     auto& si = gci.GetActiveOutputBuffer();
     auto& hostSm = si.GetStateMachine();
     auto& hostTb = si.GetTextBuffer();
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
 
     _flushFirstFrame();
 
@@ -3690,7 +3692,7 @@ void ConptyRoundtripTests::ClearBufferSignal()
     auto& si = gci.GetActiveOutputBuffer();
     auto& sm = si.GetStateMachine();
     auto* hostTb = &si.GetTextBuffer();
-    auto* termTb = term->_buffer.get();
+    auto* termTb = term->_mainBuffer.get();
 
     _flushFirstFrame();
 
@@ -3705,12 +3707,11 @@ void ConptyRoundtripTests::ClearBufferSignal()
     // B's are in red-on-yellow
 
     sm.ProcessString(L"\x1b[?25l");
-    sm.ProcessString(L"\x1b[?34;42m");
+    sm.ProcessString(L"\x1b[34;42m");
     sm.ProcessString(std::wstring(50, L'A'));
     sm.ProcessString(L" ");
-    sm.ProcessString(L"\x1b[?31;43m");
+    sm.ProcessString(L"\x1b[31;43m");
     sm.ProcessString(std::wstring(50, L'B'));
-    sm.ProcessString(L"\x1b[?m");
     sm.ProcessString(L"\x1b[?25h");
 
     auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& viewport, const bool before) {
@@ -3752,3 +3753,318 @@ void ConptyRoundtripTests::ClearBufferSignal()
     Log::Comment(L"========== Checking the terminal buffer state (after) ==========");
     verifyBuffer(*termTb, til::rect{ term->_mutableViewport.ToInclusive() }, false);
 }
+
+void ConptyRoundtripTests::SimpleAltBufferTest()
+{
+    Log::Comment(L"A test for entering and exiting the alt buffer, via conpty. "
+                 L"Ensures cursor is in the right place, and contents are "
+                 L"restored accordingly.");
+    auto& g = ServiceLocator::LocateGlobals();
+    auto& renderer = *g.pRender;
+    auto& gci = g.getConsoleInformation();
+    auto& si = gci.GetActiveOutputBuffer();
+    auto& sm = si.GetStateMachine();
+    auto* hostTb = &si.GetTextBuffer();
+    auto* termTb = term->_mainBuffer.get();
+
+    _flushFirstFrame();
+
+    _checkConptyOutput = false;
+    _logConpty = true;
+
+    // Print two lines of text:
+    // |AAAAAAAA            | <break>
+    // |BBBBBBBB_           | <break>
+    // (cursor on the '_')
+    // A's are in blue-on-green,
+    // B's are in red-on-yellow
+
+    // A one line version: (more or less)
+    //
+    // printf "\x1b[2J\x1b[H" ; printf
+    // "\x1b[?25l\x1b[34;42mAAAAA\n\x1b[31;43mBBBBB\x1b[?25h" ; sleep 2 ; printf
+    // "\x1b[?1049h" ; sleep 2 ; printf "CCCCC" ; sleep 2 ; printf "\x1b[?1049l"
+    // ; sleep 2
+
+    sm.ProcessString(L"\x1b[?25l");
+    sm.ProcessString(L"\x1b[34;42m");
+    sm.ProcessString(std::wstring(50, L'A'));
+    sm.ProcessString(L"\n");
+    sm.ProcessString(L"\x1b[31;43m");
+    sm.ProcessString(std::wstring(50, L'B'));
+    sm.ProcessString(L"\x1b[?25h");
+
+    // Four frames here:
+    // * After text is printed to main buffer
+    // * after we go to the alt buffer
+    // * print more to the alt buffer
+    // * after we go back to the buffer
+
+    enum class Frame : int
+    {
+        InMainBufferBefore = 0,
+        InAltBufferBefore,
+        InAltBufferAfter,
+        InMainBufferAfter
+    };
+
+    auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& viewport, const Frame frame) {
+        const short width = viewport.narrow_width<short>();
+        auto iter0 = tb.GetCellDataAt({ 0, 0 });
+        auto iter1 = tb.GetCellDataAt({ 0, 1 });
+        switch (frame)
+        {
+        case Frame::InMainBufferBefore:
+        case Frame::InMainBufferAfter:
+        {
+            TestUtils::VerifySpanOfText(L"A", iter0, 0, 50);
+            TestUtils::VerifySpanOfText(L" ", iter0, 0, static_cast<size_t>(width - 50));
+
+            TestUtils::VerifySpanOfText(L"B", iter1, 0, 50);
+            TestUtils::VerifySpanOfText(L" ", iter1, 0, static_cast<size_t>(width - 50));
+            COORD expectedCursor{ 50, 1 };
+            VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
+            break;
+        }
+        case Frame::InAltBufferBefore:
+        {
+            TestUtils::VerifySpanOfText(L" ", iter0, 0, width);
+            TestUtils::VerifySpanOfText(L" ", iter1, 0, width);
+
+            COORD expectedCursor{ 50, 1 };
+            VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
+            break;
+        }
+        case Frame::InAltBufferAfter:
+        {
+            TestUtils::VerifySpanOfText(L" ", iter0, 0, width);
+            TestUtils::VerifySpanOfText(L" ", iter1, 0, 50u);
+            TestUtils::VerifySpanOfText(L"C", iter1, 0, 5u);
+            TestUtils::VerifySpanOfText(L" ", iter1, 0, static_cast<size_t>(width - 55));
+
+            COORD expectedCursor{ 55, 1 };
+            VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
+            break;
+        }
+        }
+    };
+
+    Log::Comment(L"========== Checking the host buffer state (InMainBufferBefore) ==========");
+    verifyBuffer(*hostTb, til::rect{ si.GetViewport().ToInclusive() }, Frame::InMainBufferBefore);
+
+    Log::Comment(L"Painting the frame");
+    VERIFY_SUCCEEDED(renderer.PaintFrame());
+    Log::Comment(L"========== Checking the terminal buffer state (InMainBufferBefore) ==========");
+    VERIFY_ARE_EQUAL(0, term->_GetMutableViewport().Top());
+    verifyBuffer(*termTb, til::rect{ term->_GetMutableViewport().ToInclusive() }, Frame::InMainBufferBefore);
+
+    Log::Comment(L"========== Switch to the alt buffer ==========");
+
+    gci.LockConsole(); // Lock must be taken to manipulate alt/main buffer state.
+    auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
+    sm.ProcessString(L"\x1b[?1049h");
+
+    Log::Comment(L"Painting the frame");
+    VERIFY_SUCCEEDED(renderer.PaintFrame());
+
+    auto& siAlt = gci.GetActiveOutputBuffer();
+    auto* hostAltTb = &siAlt.GetTextBuffer();
+    auto* termAltTb = &term->_activeBuffer();
+
+    VERIFY_IS_TRUE(term->_inAltBuffer());
+    VERIFY_ARE_NOT_EQUAL(termAltTb, termTb);
+
+    Log::Comment(L"========== Checking the host buffer state (InAltBufferBefore) ==========");
+    verifyBuffer(*hostAltTb, til::rect{ siAlt.GetViewport().ToInclusive() }, Frame::InAltBufferBefore);
+
+    Log::Comment(L"Painting the frame");
+    VERIFY_SUCCEEDED(renderer.PaintFrame());
+    Log::Comment(L"========== Checking the terminal buffer state (InAltBufferBefore) ==========");
+    VERIFY_ARE_EQUAL(0, term->_GetMutableViewport().Top());
+    verifyBuffer(*termAltTb, til::rect{ term->_GetMutableViewport().ToInclusive() }, Frame::InAltBufferBefore);
+
+    Log::Comment(L"========== Add some text to the alt buffer ==========");
+
+    sm.ProcessString(L"CCCCC");
+    Log::Comment(L"========== Checking the host buffer state (InAltBufferAfter) ==========");
+    verifyBuffer(*hostAltTb, til::rect{ siAlt.GetViewport().ToInclusive() }, Frame::InAltBufferAfter);
+    Log::Comment(L"Painting the frame");
+    VERIFY_SUCCEEDED(renderer.PaintFrame());
+
+    Log::Comment(L"========== Checking the terminal buffer state (InAltBufferAfter) ==========");
+    VERIFY_ARE_EQUAL(0, term->_GetMutableViewport().Top());
+    verifyBuffer(*termAltTb, til::rect{ term->_GetMutableViewport().ToInclusive() }, Frame::InAltBufferAfter);
+
+    Log::Comment(L"========== Back to the main buffer ==========");
+
+    sm.ProcessString(L"\x1b[?1049l");
+    Log::Comment(L"========== Checking the host buffer state (InMainBufferAfter) ==========");
+    verifyBuffer(*hostTb, til::rect{ si.GetViewport().ToInclusive() }, Frame::InMainBufferAfter);
+    Log::Comment(L"Painting the frame");
+    VERIFY_SUCCEEDED(renderer.PaintFrame());
+
+    Log::Comment(L"========== Checking the terminal buffer state (InMainBufferAfter) ==========");
+    VERIFY_ARE_EQUAL(0, term->_GetMutableViewport().Top());
+    verifyBuffer(*termTb, til::rect{ term->_GetMutableViewport().ToInclusive() }, Frame::InMainBufferAfter);
+}
+
+void ConptyRoundtripTests::AltBufferToAltBufferTest()
+{
+    Log::Comment(L"When we request the alt buffer when we're already in the alt buffer, we should still clear it out and replace it.");
+    auto& g = ServiceLocator::LocateGlobals();
+    auto& renderer = *g.pRender;
+    auto& gci = g.getConsoleInformation();
+    auto& si = gci.GetActiveOutputBuffer();
+    auto& sm = si.GetStateMachine();
+    auto* hostTb = &si.GetTextBuffer();
+    auto* termTb = term->_mainBuffer.get();
+
+    _flushFirstFrame();
+
+    _checkConptyOutput = false;
+    _logConpty = true;
+
+    // Print two lines of text:
+    // |AAAAAAAA            | <break>
+    // |BBBBBBBB_           | <break>
+    // (cursor on the '_')
+    // A's are in blue-on-green,
+    // B's are in red-on-yellow
+
+    // A one line version: (more or less)
+    //
+    // printf "\x1b[2J\x1b[H" ; printf
+    // "\x1b[?25l\x1b[34;42mAAAAA\n\x1b[31;43mBBBBB\x1b[?25h" ; sleep 2 ; printf
+    // "\x1b[?1049h" ; sleep 2 ; printf "CCCCC" ; sleep 2 ; printf "\x1b[?1049h"
+    // ; sleep 2
+
+    sm.ProcessString(L"\x1b[?25l");
+    sm.ProcessString(L"\x1b[34;42m");
+    sm.ProcessString(std::wstring(50, L'A'));
+    sm.ProcessString(L"\n");
+    sm.ProcessString(L"\x1b[31;43m");
+    sm.ProcessString(std::wstring(50, L'B'));
+    sm.ProcessString(L"\x1b[?25h");
+
+    // Four frames here:
+    // * After text is printed to main buffer
+    // * after we go to the alt buffer
+    // * print more to the alt buffer
+    // * after we go back to the buffer
+
+    enum class Frame : int
+    {
+        InMainBufferBefore = 0,
+        InAltBufferBefore,
+        InAltBufferAfter,
+        StillInAltBuffer
+    };
+
+    auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& viewport, const Frame frame) {
+        const short width = viewport.narrow_width<short>();
+        auto iter0 = tb.GetCellDataAt({ 0, 0 });
+        auto iter1 = tb.GetCellDataAt({ 0, 1 });
+        switch (frame)
+        {
+        case Frame::InMainBufferBefore:
+        {
+            TestUtils::VerifySpanOfText(L"A", iter0, 0, 50);
+            TestUtils::VerifySpanOfText(L" ", iter0, 0, static_cast<size_t>(width - 50));
+
+            TestUtils::VerifySpanOfText(L"B", iter1, 0, 50);
+            TestUtils::VerifySpanOfText(L" ", iter1, 0, static_cast<size_t>(width - 50));
+            COORD expectedCursor{ 50, 1 };
+            VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
+            break;
+        }
+        case Frame::InAltBufferBefore:
+        {
+            TestUtils::VerifySpanOfText(L" ", iter0, 0, width);
+            TestUtils::VerifySpanOfText(L" ", iter1, 0, width);
+
+            COORD expectedCursor{ 50, 1 };
+            VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
+            break;
+        }
+        case Frame::InAltBufferAfter:
+        {
+            TestUtils::VerifySpanOfText(L" ", iter0, 0, width);
+            TestUtils::VerifySpanOfText(L" ", iter1, 0, 50u);
+            TestUtils::VerifySpanOfText(L"C", iter1, 0, 5u);
+            TestUtils::VerifySpanOfText(L" ", iter1, 0, static_cast<size_t>(width - 55));
+
+            COORD expectedCursor{ 55, 1 };
+            VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
+            break;
+        }
+        case Frame::StillInAltBuffer:
+        {
+            TestUtils::VerifySpanOfText(L" ", iter0, 0, width);
+            TestUtils::VerifySpanOfText(L" ", iter1, 0, width);
+
+            COORD expectedCursor{ 55, 1 };
+            VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
+            break;
+        }
+        }
+    };
+
+    Log::Comment(L"========== Checking the host buffer state (InMainBufferBefore) ==========");
+    verifyBuffer(*hostTb, til::rect{ si.GetViewport().ToInclusive() }, Frame::InMainBufferBefore);
+
+    Log::Comment(L"Painting the frame");
+    VERIFY_SUCCEEDED(renderer.PaintFrame());
+    Log::Comment(L"========== Checking the terminal buffer state (InMainBufferBefore) ==========");
+    verifyBuffer(*termTb, til::rect{ term->_GetMutableViewport().ToInclusive() }, Frame::InMainBufferBefore);
+
+    Log::Comment(L"========== Switch to the alt buffer ==========");
+
+    gci.LockConsole(); // Lock must be taken to manipulate alt/main buffer state.
+    auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
+    sm.ProcessString(L"\x1b[?1049h");
+
+    Log::Comment(L"Painting the frame");
+    VERIFY_SUCCEEDED(renderer.PaintFrame());
+
+    auto* siAlt = &gci.GetActiveOutputBuffer();
+    auto* hostAltTb = &siAlt->GetTextBuffer();
+    auto* termAltTb = &term->_activeBuffer();
+
+    VERIFY_IS_TRUE(term->_inAltBuffer());
+    VERIFY_ARE_NOT_EQUAL(termAltTb, termTb);
+
+    Log::Comment(L"========== Checking the host buffer state (InAltBufferBefore) ==========");
+    verifyBuffer(*hostAltTb, til::rect{ siAlt->GetViewport().ToInclusive() }, Frame::InAltBufferBefore);
+
+    Log::Comment(L"Painting the frame");
+    VERIFY_SUCCEEDED(renderer.PaintFrame());
+    Log::Comment(L"========== Checking the terminal buffer state (InAltBufferBefore) ==========");
+    verifyBuffer(*termAltTb, til::rect{ term->_GetMutableViewport().ToInclusive() }, Frame::InAltBufferBefore);
+
+    Log::Comment(L"========== Add some text to the alt buffer ==========");
+
+    sm.ProcessString(L"CCCCC");
+    Log::Comment(L"========== Checking the host buffer state (InAltBufferAfter) ==========");
+    verifyBuffer(*hostAltTb, til::rect{ siAlt->GetViewport().ToInclusive() }, Frame::InAltBufferAfter);
+    Log::Comment(L"Painting the frame");
+    VERIFY_SUCCEEDED(renderer.PaintFrame());
+
+    Log::Comment(L"========== Checking the terminal buffer state (InAltBufferAfter) ==========");
+    verifyBuffer(*termAltTb, til::rect{ term->_GetMutableViewport().ToInclusive() }, Frame::InAltBufferAfter);
+
+    Log::Comment(L"========== Stay in the alt buffer, what happens? ==========");
+
+    sm.ProcessString(L"\x1b[?1049h");
+    Log::Comment(L"Painting the frame");
+    VERIFY_SUCCEEDED(renderer.PaintFrame());
+
+    siAlt = &gci.GetActiveOutputBuffer();
+    hostAltTb = &siAlt->GetTextBuffer();
+    termAltTb = &term->_activeBuffer();
+
+    Log::Comment(L"========== Checking the host buffer state (StillInAltBuffer) ==========");
+    verifyBuffer(*hostAltTb, til::rect{ siAlt->GetViewport().ToInclusive() }, Frame::StillInAltBuffer);
+
+    Log::Comment(L"========== Checking the terminal buffer state (StillInAltBuffer) ==========");
+    verifyBuffer(*termAltTb, til::rect{ term->_GetMutableViewport().ToInclusive() }, Frame::StillInAltBuffer);
+}
diff --git a/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp b/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp
index 38b89d69bdc..81fad12a2d8 100644
--- a/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp
+++ b/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp
@@ -157,7 +157,7 @@ void ScrollTest::TestNotifyScrolling()
     Log::Comment(L"Watch out - this test takes a while to run, and won't "
                  L"output anything unless in encounters an error. This is expected.");
 
-    auto& termTb = *_term->_buffer;
+    auto& termTb = *_term->_mainBuffer;
     auto& termSm = *_term->_stateMachine;
 
     const auto totalBufferSize = termTb.GetSize().Height();
diff --git a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp
index 362535da374..9acfeb739dd 100644
--- a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp
+++ b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp
@@ -138,29 +138,29 @@ void TerminalApiTest::CursorVisibility()
     DummyRenderer renderer{ &term };
     term.Create({ 100, 100 }, 0, renderer);
 
-    VERIFY_IS_TRUE(term._buffer->GetCursor().IsVisible());
-    VERIFY_IS_TRUE(term._buffer->GetCursor().IsOn());
-    VERIFY_IS_TRUE(term._buffer->GetCursor().IsBlinkingAllowed());
+    VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsVisible());
+    VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsOn());
+    VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinkingAllowed());
 
     term.SetCursorOn(false);
-    VERIFY_IS_TRUE(term._buffer->GetCursor().IsVisible());
-    VERIFY_IS_FALSE(term._buffer->GetCursor().IsOn());
-    VERIFY_IS_TRUE(term._buffer->GetCursor().IsBlinkingAllowed());
+    VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsVisible());
+    VERIFY_IS_FALSE(term._mainBuffer->GetCursor().IsOn());
+    VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinkingAllowed());
 
     term.SetCursorOn(true);
-    VERIFY_IS_TRUE(term._buffer->GetCursor().IsVisible());
-    VERIFY_IS_TRUE(term._buffer->GetCursor().IsOn());
-    VERIFY_IS_TRUE(term._buffer->GetCursor().IsBlinkingAllowed());
+    VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsVisible());
+    VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsOn());
+    VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinkingAllowed());
 
     term.SetCursorVisibility(false);
-    VERIFY_IS_FALSE(term._buffer->GetCursor().IsVisible());
-    VERIFY_IS_TRUE(term._buffer->GetCursor().IsOn());
-    VERIFY_IS_TRUE(term._buffer->GetCursor().IsBlinkingAllowed());
+    VERIFY_IS_FALSE(term._mainBuffer->GetCursor().IsVisible());
+    VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsOn());
+    VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinkingAllowed());
 
     term.SetCursorOn(false);
-    VERIFY_IS_FALSE(term._buffer->GetCursor().IsVisible());
-    VERIFY_IS_FALSE(term._buffer->GetCursor().IsOn());
-    VERIFY_IS_TRUE(term._buffer->GetCursor().IsBlinkingAllowed());
+    VERIFY_IS_FALSE(term._mainBuffer->GetCursor().IsVisible());
+    VERIFY_IS_FALSE(term._mainBuffer->GetCursor().IsOn());
+    VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinkingAllowed());
 }
 
 void TerminalApiTest::CursorVisibilityViaStateMachine()
@@ -170,7 +170,7 @@ void TerminalApiTest::CursorVisibilityViaStateMachine()
     DummyRenderer renderer{ &term };
     term.Create({ 100, 100 }, 0, renderer);
 
-    auto& tbi = *(term._buffer);
+    auto& tbi = *(term._mainBuffer);
     auto& stateMachine = *(term._stateMachine);
     auto& cursor = tbi.GetCursor();
 
@@ -222,7 +222,7 @@ void TerminalApiTest::CheckDoubleWidthCursor()
     DummyRenderer renderer{ &term };
     term.Create({ 100, 100 }, 0, renderer);
 
-    auto& tbi = *(term._buffer);
+    auto& tbi = *(term._mainBuffer);
     auto& stateMachine = *(term._stateMachine);
     auto& cursor = tbi.GetCursor();
 
@@ -266,7 +266,7 @@ void TerminalCoreUnitTests::TerminalApiTest::AddHyperlink()
     DummyRenderer renderer{ &term };
     term.Create({ 100, 100 }, 0, renderer);
 
-    auto& tbi = *(term._buffer);
+    auto& tbi = *(term._mainBuffer);
     auto& stateMachine = *(term._stateMachine);
 
     // Process the opening osc 8 sequence
@@ -292,7 +292,7 @@ void TerminalCoreUnitTests::TerminalApiTest::AddHyperlinkCustomId()
     DummyRenderer renderer{ &term };
     term.Create({ 100, 100 }, 0, renderer);
 
-    auto& tbi = *(term._buffer);
+    auto& tbi = *(term._mainBuffer);
     auto& stateMachine = *(term._stateMachine);
 
     // Process the opening osc 8 sequence
@@ -320,7 +320,7 @@ void TerminalCoreUnitTests::TerminalApiTest::AddHyperlinkCustomIdDifferentUri()
     DummyRenderer renderer{ &term };
     term.Create({ 100, 100 }, 0, renderer);
 
-    auto& tbi = *(term._buffer);
+    auto& tbi = *(term._mainBuffer);
     auto& stateMachine = *(term._stateMachine);
 
     // Process the opening osc 8 sequence
diff --git a/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp b/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp
index 1684fa244b0..aac133fd308 100644
--- a/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp
+++ b/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp
@@ -79,7 +79,7 @@ class TerminalCoreUnitTests::TerminalBufferTests final
 
 void TerminalBufferTests::TestSimpleBufferWriting()
 {
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
     auto& termSm = *term->_stateMachine;
     const auto initialView = term->GetViewport();
 
@@ -98,7 +98,7 @@ void TerminalBufferTests::TestSimpleBufferWriting()
 
 void TerminalBufferTests::TestWrappingCharByChar()
 {
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
     auto& termSm = *term->_stateMachine;
     const auto initialView = term->GetViewport();
     auto& cursor = termTb.GetCursor();
@@ -137,7 +137,7 @@ void TerminalBufferTests::TestWrappingCharByChar()
 
 void TerminalBufferTests::TestWrappingALongString()
 {
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
     auto& termSm = *term->_stateMachine;
     const auto initialView = term->GetViewport();
     auto& cursor = termTb.GetCursor();
@@ -171,7 +171,7 @@ void TerminalBufferTests::TestWrappingALongString()
 
 void TerminalBufferTests::DontSnapToOutputTest()
 {
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
     auto& termSm = *term->_stateMachine;
     const auto initialView = term->GetViewport();
 
@@ -253,7 +253,7 @@ void TerminalBufferTests::DontSnapToOutputTest()
 
 void TerminalBufferTests::_SetTabStops(std::list<short> columns, bool replace)
 {
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
     auto& termSm = *term->_stateMachine;
     auto& cursor = termTb.GetCursor();
 
@@ -275,7 +275,7 @@ void TerminalBufferTests::_SetTabStops(std::list<short> columns, bool replace)
 std::list<short> TerminalBufferTests::_GetTabStops()
 {
     std::list<short> columns;
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
     auto& termSm = *term->_stateMachine;
     const auto initialView = term->GetViewport();
     const auto lastColumn = initialView.RightInclusive();
@@ -321,7 +321,7 @@ void TerminalBufferTests::TestResetClearTabStops()
 
 void TerminalBufferTests::TestAddTabStop()
 {
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
     auto& termSm = *term->_stateMachine;
     auto& cursor = termTb.GetCursor();
 
@@ -366,7 +366,7 @@ void TerminalBufferTests::TestAddTabStop()
 
 void TerminalBufferTests::TestClearTabStop()
 {
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
     auto& termSm = *term->_stateMachine;
     auto& cursor = termTb.GetCursor();
 
@@ -475,7 +475,7 @@ void TerminalBufferTests::TestClearTabStop()
 
 void TerminalBufferTests::TestGetForwardTab()
 {
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
     auto& termSm = *term->_stateMachine;
     const auto initialView = term->GetViewport();
     auto& cursor = termTb.GetCursor();
@@ -545,7 +545,7 @@ void TerminalBufferTests::TestGetForwardTab()
 
 void TerminalBufferTests::TestGetReverseTab()
 {
-    auto& termTb = *term->_buffer;
+    auto& termTb = *term->_mainBuffer;
     auto& termSm = *term->_stateMachine;
     auto& cursor = termTb.GetCursor();
 
diff --git a/src/host/VtIo.cpp b/src/host/VtIo.cpp
index 6863ef080a6..14271a4fb95 100644
--- a/src/host/VtIo.cpp
+++ b/src/host/VtIo.cpp
@@ -380,6 +380,16 @@ bool VtIo::IsUsingVt() const
     return hr;
 }
 
+[[nodiscard]] HRESULT VtIo::SwitchScreenBuffer(const bool useAltBuffer)
+{
+    HRESULT hr = S_OK;
+    if (_pVtRenderEngine)
+    {
+        hr = _pVtRenderEngine->SwitchScreenBuffer(useAltBuffer);
+    }
+    return hr;
+}
+
 void VtIo::CloseInput()
 {
     // This will release the lock when it goes out of scope
diff --git a/src/host/VtIo.hpp b/src/host/VtIo.hpp
index 6c204d6cf44..3abba39c184 100644
--- a/src/host/VtIo.hpp
+++ b/src/host/VtIo.hpp
@@ -36,6 +36,8 @@ namespace Microsoft::Console::VirtualTerminal
         [[nodiscard]] HRESULT SuppressResizeRepaint();
         [[nodiscard]] HRESULT SetCursorPosition(const COORD coordCursor);
 
+        [[nodiscard]] HRESULT SwitchScreenBuffer(const bool useAltBuffer);
+
         void CloseInput();
         void CloseOutput();
 
diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp
index 49dea7ba74b..351e877ecd6 100644
--- a/src/host/screenInfo.cpp
+++ b/src/host/screenInfo.cpp
@@ -1942,6 +1942,15 @@ const SCREEN_INFORMATION& SCREEN_INFORMATION::GetMainBuffer() const
             s_RemoveScreenBuffer(psiOldAltBuffer); // this will also delete the old alt buffer
         }
 
+        // GH#381: When we switch into the alt buffer:
+        //  * flush the current frame, to clear out anything that we prepared for this buffer.
+        //  * Emit a ?1049h/l to the remote side, to let them know that we've switched buffers.
+        if (gci.IsInVtIoMode() && ServiceLocator::LocateGlobals().pRender)
+        {
+            ServiceLocator::LocateGlobals().pRender->TriggerFlush(false);
+            LOG_IF_FAILED(gci.GetVtIo()->SwitchScreenBuffer(true));
+        }
+
         ::SetActiveScreenBuffer(*psiNewAltBuffer);
 
         // Kind of a hack until we have proper signal channels: If the client app wants window size events, send one for
@@ -1972,6 +1981,16 @@ void SCREEN_INFORMATION::UseMainScreenBuffer()
             psiMain->ProcessResizeWindow(&(psiMain->_rcAltSavedClientNew), &(psiMain->_rcAltSavedClientOld));
             psiMain->_fAltWindowChanged = false;
         }
+
+        // GH#381: When we switch into the main buffer:
+        //  * flush the current frame, to clear out anything that we prepared for this buffer.
+        //  * Emit a ?1049h/l to the remote side, to let them know that we've switched buffers.
+        if (gci.IsInVtIoMode() && ServiceLocator::LocateGlobals().pRender)
+        {
+            ServiceLocator::LocateGlobals().pRender->TriggerFlush(false);
+            LOG_IF_FAILED(gci.GetVtIo()->SwitchScreenBuffer(false));
+        }
+
         ::SetActiveScreenBuffer(*psiMain);
         psiMain->UpdateScrollBars(); // The alt had disabled scrollbars, re-enable them
 
diff --git a/src/interactivity/onecore/BgfxEngine.cpp b/src/interactivity/onecore/BgfxEngine.cpp
index b3bdd606651..5bc8d3123db 100644
--- a/src/interactivity/onecore/BgfxEngine.cpp
+++ b/src/interactivity/onecore/BgfxEngine.cpp
@@ -63,12 +63,6 @@ BgfxEngine::BgfxEngine(PVOID SharedViewBase, LONG DisplayHeight, LONG DisplayWid
     return S_OK;
 }
 
-[[nodiscard]] HRESULT BgfxEngine::InvalidateCircling(_Out_ bool* const pForcePaint) noexcept
-{
-    *pForcePaint = false;
-    return S_FALSE;
-}
-
 [[nodiscard]] HRESULT BgfxEngine::PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept
 {
     *pForcePaint = false;
diff --git a/src/interactivity/onecore/BgfxEngine.hpp b/src/interactivity/onecore/BgfxEngine.hpp
index 634a46fe412..032066b3e05 100644
--- a/src/interactivity/onecore/BgfxEngine.hpp
+++ b/src/interactivity/onecore/BgfxEngine.hpp
@@ -39,7 +39,6 @@ namespace Microsoft::Console::Render
         [[nodiscard]] HRESULT InvalidateSelection(const std::vector<SMALL_RECT>& rectangles) noexcept override;
         [[nodiscard]] HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override;
         [[nodiscard]] HRESULT InvalidateAll() noexcept override;
-        [[nodiscard]] HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept override;
         [[nodiscard]] HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override;
 
         [[nodiscard]] HRESULT StartPaint() noexcept override;
diff --git a/src/renderer/atlas/AtlasEngine.api.cpp b/src/renderer/atlas/AtlasEngine.api.cpp
index 7c49fb99675..525729dc309 100644
--- a/src/renderer/atlas/AtlasEngine.api.cpp
+++ b/src/renderer/atlas/AtlasEngine.api.cpp
@@ -130,7 +130,7 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2<T>& out) noexcept
     return S_OK;
 }
 
-[[nodiscard]] HRESULT AtlasEngine::InvalidateCircling(_Out_ bool* const pForcePaint) noexcept
+[[nodiscard]] HRESULT AtlasEngine::InvalidateFlush(_In_ const bool /*circled*/, _Out_ bool* const pForcePaint) noexcept
 {
     RETURN_HR_IF_NULL(E_INVALIDARG, pForcePaint);
     *pForcePaint = false;
diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h
index 1645544daba..ad3cc244184 100644
--- a/src/renderer/atlas/AtlasEngine.h
+++ b/src/renderer/atlas/AtlasEngine.h
@@ -33,7 +33,7 @@ namespace Microsoft::Console::Render
         [[nodiscard]] HRESULT InvalidateSelection(const std::vector<SMALL_RECT>& rectangles) noexcept override;
         [[nodiscard]] HRESULT InvalidateScroll(const COORD* pcoordDelta) noexcept override;
         [[nodiscard]] HRESULT InvalidateAll() noexcept override;
-        [[nodiscard]] HRESULT InvalidateCircling(_Out_ bool* pForcePaint) noexcept override;
+        [[nodiscard]] HRESULT InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept override;
         [[nodiscard]] HRESULT InvalidateTitle(std::wstring_view proposedTitle) noexcept override;
         [[nodiscard]] HRESULT NotifyNewText(const std::wstring_view newText) noexcept override;
         [[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override;
diff --git a/src/renderer/base/RenderEngineBase.cpp b/src/renderer/base/RenderEngineBase.cpp
index ca506b9d2ab..cf588ffe634 100644
--- a/src/renderer/base/RenderEngineBase.cpp
+++ b/src/renderer/base/RenderEngineBase.cpp
@@ -82,3 +82,19 @@ void RenderEngineBase::WaitUntilCanRender() noexcept
     // Throttle the render loop a bit by default (~60 FPS), improving throughput.
     Sleep(8);
 }
+
+// Routine Description:
+// - Notifies us that we're about to circle the buffer, giving us a chance to
+//   force a repaint before the buffer contents are lost.
+// - The default implementation of flush, is to do nothing for most renderers.
+// Arguments:
+// - circled - ignored
+// - pForcePaint - Always filled with false
+// Return Value:
+// - S_FALSE because we don't use this.
+[[nodiscard]] HRESULT RenderEngineBase::InvalidateFlush(_In_ const bool /*circled*/, _Out_ bool* const pForcePaint) noexcept
+{
+    RETURN_HR_IF_NULL(E_INVALIDARG, pForcePaint);
+    *pForcePaint = false;
+    return S_FALSE;
+}
diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp
index e720708b9e2..a880fbc0bd7 100644
--- a/src/renderer/base/renderer.cpp
+++ b/src/renderer/base/renderer.cpp
@@ -456,14 +456,14 @@ void Renderer::TriggerScroll(const COORD* const pcoordDelta)
 // - <none>
 // Return Value:
 // - <none>
-void Renderer::TriggerCircling()
+void Renderer::TriggerFlush(const bool circling)
 {
     const auto rects = _GetSelectionRects();
 
     FOREACH_ENGINE(pEngine)
     {
         bool fEngineRequestsRepaint = false;
-        HRESULT hr = pEngine->InvalidateCircling(&fEngineRequestsRepaint);
+        HRESULT hr = pEngine->InvalidateFlush(circling, &fEngineRequestsRepaint);
         LOG_IF_FAILED(hr);
 
         LOG_IF_FAILED(pEngine->InvalidateSelection(rects));
diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp
index 0a84c6b6e0b..e47d5a21450 100644
--- a/src/renderer/base/renderer.hpp
+++ b/src/renderer/base/renderer.hpp
@@ -51,7 +51,7 @@ namespace Microsoft::Console::Render
         void TriggerScroll();
         void TriggerScroll(const COORD* const pcoordDelta);
 
-        void TriggerCircling();
+        void TriggerFlush(const bool circling);
         void TriggerTitleChange();
 
         void TriggerNewTextNotification(const std::wstring_view newText);
diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp
index 265160cc983..42a3ea7e90f 100644
--- a/src/renderer/dx/DxRenderer.cpp
+++ b/src/renderer/dx/DxRenderer.cpp
@@ -1200,20 +1200,6 @@ try
 }
 CATCH_RETURN();
 
-// Routine Description:
-// - This currently has no effect in this renderer.
-// Arguments:
-// - pForcePaint - Always filled with false
-// Return Value:
-// - S_FALSE because we don't use this.
-[[nodiscard]] HRESULT DxEngine::InvalidateCircling(_Out_ bool* const pForcePaint) noexcept
-{
-    RETURN_HR_IF_NULL(E_INVALIDARG, pForcePaint);
-
-    *pForcePaint = false;
-    return S_FALSE;
-}
-
 // Routine Description:
 // - Gets the area in pixels of the surface we are targeting
 // Arguments:
diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp
index 7cfe9834abb..5d86fa648ae 100644
--- a/src/renderer/dx/DxRenderer.hpp
+++ b/src/renderer/dx/DxRenderer.hpp
@@ -79,7 +79,6 @@ namespace Microsoft::Console::Render
         [[nodiscard]] HRESULT InvalidateSelection(const std::vector<SMALL_RECT>& rectangles) noexcept override;
         [[nodiscard]] HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override;
         [[nodiscard]] HRESULT InvalidateAll() noexcept override;
-        [[nodiscard]] HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept override;
         [[nodiscard]] HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override;
 
         [[nodiscard]] HRESULT StartPaint() noexcept override;
diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp
index 8b0cab05b4d..e2d98ff939a 100644
--- a/src/renderer/gdi/gdirenderer.hpp
+++ b/src/renderer/gdi/gdirenderer.hpp
@@ -33,7 +33,6 @@ namespace Microsoft::Console::Render
         [[nodiscard]] HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override;
         [[nodiscard]] HRESULT InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept override;
         [[nodiscard]] HRESULT InvalidateAll() noexcept override;
-        [[nodiscard]] HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept override;
         [[nodiscard]] HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override;
 
         [[nodiscard]] HRESULT StartPaint() noexcept override;
diff --git a/src/renderer/gdi/invalidate.cpp b/src/renderer/gdi/invalidate.cpp
index ca77b03cd12..c55e21d2144 100644
--- a/src/renderer/gdi/invalidate.cpp
+++ b/src/renderer/gdi/invalidate.cpp
@@ -109,21 +109,6 @@ HRESULT GdiEngine::InvalidateAll() noexcept
     RETURN_HR(InvalidateSystem(&rc));
 }
 
-// Method Description:
-// - Notifies us that we're about to circle the buffer, giving us a chance to
-//      force a repaint before the buffer contents are lost. The GDI renderer
-//      doesn't care if we lose text - we're only painting visible text anyways,
-//      so we return false.
-// Arguments:
-// - Receives a bool indicating if we should force the repaint.
-// Return Value:
-// - S_FALSE - we succeeded, but the result was false.
-HRESULT GdiEngine::InvalidateCircling(_Out_ bool* const pForcePaint) noexcept
-{
-    *pForcePaint = false;
-    return S_FALSE;
-}
-
 // Method Description:
 // - Notifies us that we're about to be torn down. This gives us a last chance
 //      to force a repaint before the buffer contents are lost. The GDI renderer
diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp
index 3d13595aabc..3bd45e7e734 100644
--- a/src/renderer/inc/IRenderEngine.hpp
+++ b/src/renderer/inc/IRenderEngine.hpp
@@ -67,7 +67,7 @@ namespace Microsoft::Console::Render
         [[nodiscard]] virtual HRESULT InvalidateSelection(const std::vector<SMALL_RECT>& rectangles) noexcept = 0;
         [[nodiscard]] virtual HRESULT InvalidateScroll(const COORD* pcoordDelta) noexcept = 0;
         [[nodiscard]] virtual HRESULT InvalidateAll() noexcept = 0;
-        [[nodiscard]] virtual HRESULT InvalidateCircling(_Out_ bool* pForcePaint) noexcept = 0;
+        [[nodiscard]] virtual HRESULT InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept = 0;
         [[nodiscard]] virtual HRESULT InvalidateTitle(std::wstring_view proposedTitle) noexcept = 0;
         [[nodiscard]] virtual HRESULT NotifyNewText(const std::wstring_view newText) noexcept = 0;
         [[nodiscard]] virtual HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept = 0;
diff --git a/src/renderer/inc/RenderEngineBase.hpp b/src/renderer/inc/RenderEngineBase.hpp
index 452dd0cc39f..25998933381 100644
--- a/src/renderer/inc/RenderEngineBase.hpp
+++ b/src/renderer/inc/RenderEngineBase.hpp
@@ -53,6 +53,8 @@ namespace Microsoft::Console::Render
 
         [[nodiscard]] virtual bool RequiresContinuousRedraw() noexcept override;
 
+        [[nodiscard]] HRESULT InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept override;
+
         void WaitUntilCanRender() noexcept override;
 
     protected:
diff --git a/src/renderer/uia/UiaRenderer.cpp b/src/renderer/uia/UiaRenderer.cpp
index 1b31b6f4916..2d83d970dbc 100644
--- a/src/renderer/uia/UiaRenderer.cpp
+++ b/src/renderer/uia/UiaRenderer.cpp
@@ -168,20 +168,6 @@ CATCH_RETURN();
     return S_OK;
 }
 
-// Routine Description:
-// - This currently has no effect in this renderer.
-// Arguments:
-// - pForcePaint - Always filled with false
-// Return Value:
-// - S_FALSE because we don't use this.
-[[nodiscard]] HRESULT UiaEngine::InvalidateCircling(_Out_ bool* const pForcePaint) noexcept
-{
-    RETURN_HR_IF_NULL(E_INVALIDARG, pForcePaint);
-
-    *pForcePaint = false;
-    return S_FALSE;
-}
-
 [[nodiscard]] HRESULT UiaEngine::NotifyNewText(const std::wstring_view newText) noexcept
 try
 {
diff --git a/src/renderer/uia/UiaRenderer.hpp b/src/renderer/uia/UiaRenderer.hpp
index 8f1603aeac6..c56f1cc1084 100644
--- a/src/renderer/uia/UiaRenderer.hpp
+++ b/src/renderer/uia/UiaRenderer.hpp
@@ -46,7 +46,6 @@ namespace Microsoft::Console::Render
         [[nodiscard]] HRESULT InvalidateSelection(const std::vector<SMALL_RECT>& rectangles) noexcept override;
         [[nodiscard]] HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override;
         [[nodiscard]] HRESULT InvalidateAll() noexcept override;
-        [[nodiscard]] HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept override;
         [[nodiscard]] HRESULT NotifyNewText(const std::wstring_view newText) noexcept override;
         [[nodiscard]] HRESULT PaintBackground() noexcept override;
         [[nodiscard]] HRESULT PaintBufferLine(gsl::span<const Cluster> const clusters, const COORD coord, const bool fTrimLeft, const bool lineWrapped) noexcept override;
diff --git a/src/renderer/vt/VtSequences.cpp b/src/renderer/vt/VtSequences.cpp
index 58bb3f2f3bb..05fd6f95184 100644
--- a/src/renderer/vt/VtSequences.cpp
+++ b/src/renderer/vt/VtSequences.cpp
@@ -434,6 +434,17 @@ using namespace Microsoft::Console::Render;
     return _Write("\x1b[?9001h");
 }
 
+// Method Description:
+// - Send a sequence to the connected terminal to switch to the alternate or main screen buffer.
+// Arguments:
+// - useAltBuffer: if true, switch to the alt buffer, otherwise to the main buffer.
+// Return Value:
+// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
+[[nodiscard]] HRESULT VtEngine::_SwitchScreenBuffer(const bool useAltBuffer) noexcept
+{
+    return _Write(useAltBuffer ? "\x1b[?1049h" : "\x1b[?1049l");
+}
+
 // Method Description:
 // - Formats and writes a sequence to add a hyperlink to the terminal buffer
 // Arguments:
diff --git a/src/renderer/vt/invalidate.cpp b/src/renderer/vt/invalidate.cpp
index 5132f8bec0b..86e21637845 100644
--- a/src/renderer/vt/invalidate.cpp
+++ b/src/renderer/vt/invalidate.cpp
@@ -105,7 +105,7 @@ CATCH_RETURN();
 // - Receives a bool indicating if we should force the repaint.
 // Return Value:
 // - S_OK
-[[nodiscard]] HRESULT VtEngine::InvalidateCircling(_Out_ bool* const pForcePaint) noexcept
+[[nodiscard]] HRESULT VtEngine::InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept
 {
     // If we're in the middle of a resize request, don't try to immediately start a frame.
     if (_inResizeRequest)
@@ -118,7 +118,7 @@ CATCH_RETURN();
 
         // Keep track of the fact that we circled, we'll need to do some work on
         //      end paint to specifically handle this.
-        _circled = true;
+        _circled = circled;
     }
 
     _trace.TraceTriggerCircling(*pForcePaint);
diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp
index 2f9d83decda..1f3cf516461 100644
--- a/src/renderer/vt/state.cpp
+++ b/src/renderer/vt/state.cpp
@@ -527,3 +527,10 @@ HRESULT VtEngine::RequestWin32Input() noexcept
     RETURN_IF_FAILED(_Flush());
     return S_OK;
 }
+
+HRESULT VtEngine::SwitchScreenBuffer(const bool useAltBuffer) noexcept
+{
+    RETURN_IF_FAILED(_SwitchScreenBuffer(useAltBuffer));
+    RETURN_IF_FAILED(_Flush());
+    return S_OK;
+}
diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp
index ee2a1cd720d..0d1e36bb94b 100644
--- a/src/renderer/vt/vtrenderer.hpp
+++ b/src/renderer/vt/vtrenderer.hpp
@@ -56,7 +56,7 @@ namespace Microsoft::Console::Render
         [[nodiscard]] HRESULT InvalidateSystem(const RECT* prcDirtyClient) noexcept override;
         [[nodiscard]] HRESULT InvalidateSelection(const std::vector<SMALL_RECT>& rectangles) noexcept override;
         [[nodiscard]] HRESULT InvalidateAll() noexcept override;
-        [[nodiscard]] HRESULT InvalidateCircling(_Out_ bool* pForcePaint) noexcept override;
+        [[nodiscard]] HRESULT InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept override;
         [[nodiscard]] HRESULT PaintBackground() noexcept override;
         [[nodiscard]] HRESULT PaintBufferLine(gsl::span<const Cluster> clusters, COORD coord, bool fTrimLeft, bool lineWrapped) noexcept override;
         [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF color, size_t cchLine, COORD coordTarget) noexcept override;
@@ -85,6 +85,7 @@ namespace Microsoft::Console::Render
         void SetTerminalCursorTextPosition(const COORD coordCursor) noexcept;
         [[nodiscard]] virtual HRESULT ManuallyClearScrollback() noexcept;
         [[nodiscard]] HRESULT RequestWin32Input() noexcept;
+        [[nodiscard]] HRESULT SwitchScreenBuffer(const bool useAltBuffer) noexcept;
 
     protected:
         wil::unique_hfile _hFile;
@@ -196,6 +197,7 @@ namespace Microsoft::Console::Render
         [[nodiscard]] HRESULT _ListenForDSR() noexcept;
 
         [[nodiscard]] HRESULT _RequestWin32Input() noexcept;
+        [[nodiscard]] HRESULT _SwitchScreenBuffer(const bool useAltBuffer) noexcept;
 
         [[nodiscard]] virtual HRESULT _MoveCursor(const COORD coord) noexcept = 0;
         [[nodiscard]] HRESULT _RgbUpdateDrawingBrushes(const TextAttribute& textAttributes) noexcept;
diff --git a/src/renderer/wddmcon/WddmConRenderer.cpp b/src/renderer/wddmcon/WddmConRenderer.cpp
index 16f1cda9b3e..5a1c6f5abf9 100644
--- a/src/renderer/wddmcon/WddmConRenderer.cpp
+++ b/src/renderer/wddmcon/WddmConRenderer.cpp
@@ -191,12 +191,6 @@ bool WddmConEngine::IsInitialized()
     return S_OK;
 }
 
-[[nodiscard]] HRESULT WddmConEngine::InvalidateCircling(_Out_ bool* const pForcePaint) noexcept
-{
-    *pForcePaint = false;
-    return S_FALSE;
-}
-
 [[nodiscard]] HRESULT WddmConEngine::PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept
 {
     *pForcePaint = false;
diff --git a/src/renderer/wddmcon/WddmConRenderer.hpp b/src/renderer/wddmcon/WddmConRenderer.hpp
index 4a8bf83ef47..58f935b97a3 100644
--- a/src/renderer/wddmcon/WddmConRenderer.hpp
+++ b/src/renderer/wddmcon/WddmConRenderer.hpp
@@ -31,7 +31,6 @@ namespace Microsoft::Console::Render
         [[nodiscard]] HRESULT InvalidateSelection(const std::vector<SMALL_RECT>& rectangles) noexcept override;
         [[nodiscard]] HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override;
         [[nodiscard]] HRESULT InvalidateAll() noexcept override;
-        [[nodiscard]] HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept override;
         [[nodiscard]] HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override;
 
         [[nodiscard]] HRESULT StartPaint() noexcept override;


Test patch: