Repository: microsoft/terminal (language: cpp)

Issue:
OpenConsole crash when pasting emoji 
<!-- 
🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨

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.19041.630]
Windows Terminal version (if applicable):

Any other software?
tcsh (native Windows version)

# Steps to reproduce

download tcsh-x64.exe from https://github.com/amoldeshpande/tcsh/releases/tag/20210103.10 

Launch tcsh.  Doesn't have to be in terminal.  Paste any emoji into it.  It will crash conhost.exe on Windows 10 2004 and OpenConsole.exe if you launch within Windows Terminal

# Expected behavior

Should paste an emoji

# Actual behavior


Edited: repro steps with tcsh. Cannnot repro it with my test program anymore




Discussion / hints:
Irrespective of whether emojis are expected to work in these conditions, it seems very wrong to me that an error condition here would cause the entire process to terminate. I'm convinced that half of the `FAIL_FAST` calls in the code are unintentional, and the original author was likely expecting an error to be returned, or an exception to be thrown (much like all the places we're mistakenly using `gsl::narrow` instead of `gsl::narrow_cast`).
I cannot reproduce the crash when right-click pasting 😀 (U+1F600) or 👨‍👩‍👧‍👦 (U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466) to OpenConsole.exe copied from Windows Terminal 1.4.3243.0, with the demo program built for x86, on Windows 10 version 20H2 (19042.685), where the setting "Beta: Use Unicode UTF-8 for worldwide language support" is off.

I don't see why SetFileApisToANSI would even matter here; doesn't it only affect the encoding of file names, which the demo program does not use?
@KalleOlaviNiemitalo I think you need to paste more than 100 characters (the length of the `INPUT_RECORD` buffer that was passed to `ReadConsoleInput`) in order to trigger the error. I could reproduce it by pasting the following sequence:

😀💘💥👍🎅🦄🍄🌎🚖⚓🚀⌚💣😀💘💥👍🎅🦄🍄🌎🚖⚓🚀⌚💣😀💘💥👍🎅🦄🍄🌎🚖⚓🚀⌚💣😀💘💥👍🎅🦄🍄🌎🚖⚓🚀⌚💣😀💘💥👍🎅🦄🍄🌎🚖⚓🚀⌚💣

I have been using a single 😁  to crash it.  
In that case I can't reproduce it either. What's the commit hash of the revision that you're testing with? And in case it makes any difference, what are the build details - x86 or x64, debug or release? Also, what keyboard locale are you using?
I just realised that the crash that I was seeing was actually in the `DirectReadData::Notify` method rather than the `_DoGetConsoleInput` function, but the code is essentially identical.

This is in `DirectReadData::Notify`:
https://github.com/microsoft/terminal/blob/d09fdd61cbb11b7ef2ccdd4820349ffe898ad583/src/host/readDataDirect.cpp#L168-L184

And this is in `_DoGetConsoleInput`:
https://github.com/microsoft/terminal/blob/d09fdd61cbb11b7ef2ccdd4820349ffe898ad583/src/host/directio.cpp#L226-L242

Either of those will crash if the number of `readEvents` is more than 1 greater than the `_eventReadCount`, and that is easy to achieve with a multibyte codepage. I can't see how the crash could be triggered with a single character, though, unless `ReadConsoleInput` was called with a much smaller record buffer.
If I understand correctly, InputBuffer::_ReadBuffer tries to predict how many UTF-16 input events fit in the specified number of OEM input events, but it assumes the OEM encoding uses at most two bytes per character: <https://github.com/microsoft/terminal/blob/d09fdd61cbb11b7ef2ccdd4820349ffe898ad583/src/host/inputBuffer.cpp#L394-L405>

According to <https://github.com/microsoft/terminal/issues/7589#issuecomment-712492563>, the estimated number of bytes per character even depends on the font.

Then, SplitToOem translates the UTF-16 input events to OEM input events. If the configured code page is UTF-8, then it uses CESU-8, which may need three bytes per UTF-16 code unit. _DoGetConsoleInput cannot return the excess events to its caller and does not know how to put them back to the input buffer, either.

If InputBuffer::_ReadBuffer supported UTF-8, then that would avoid the crash, I think. However, the resulting input events would still have CESU-8 rather than UTF-8, unless that is converted in some other layer that I cannot find now.

Also, I wonder if the StoreReadPartialByteSequence call should perhaps be skipped if IsPeek is true.
dangit, I can't reproduce it any more with my simple piece of code.

The more involved repro consists of  running code in this branch https://github.com/amoldeshpande/tcsh/tree/unicode under Windows Terminal or OpenConsole.exe and pasting the emoji.  This is 100% still repro-able with font of "MesloLGM NF" . Have not tried any other fonts IIRC with this shell.

(Nerd Fonts from here https://github.com/ryanoasis/nerd-fonts/releases) 

In answer to a previous question, I used an x64 debug build with standard US-EN  keyboard layout.  

I will keep trying to make my simple repro crash again meanwhile.
My guess is that there was a mode change involved somewhere (i.e. `SetConsoleMode`) that persisted from another test case or app. Then when you restarted OpenConsole, the modes went back to the defaults, so the error couldn't be reproduced.
~~~Here's a link to the shell executable https://1drv.ms/u/s!AopJdbqzqHFho-J1E-4rzg4DiYBqBg?e=g5xZEZ    launching this in Windows Terminal and pasting an emoji should crash OpenConsole.    Font can be default, I think.~~~

Editing to update repro steps:

download tcsh-x64.exe from https://github.com/amoldeshpande/tcsh/releases/tag/20210103.10 

Launch tcsh.  Doesn't have to be in terminal.  Paste any emoji into it.  It will crash conhost.exe on Windows 10 2004 and OpenConsole.exe if you launch within Windows Terminal

Okay since it looks like there's a repro and a pretty well identified piece of code responsible, I'm yanking triage. Looks like we didn't end up getting to this in 1.7, so I'm just gonna throw in with 2.0. Thanks everyone!
Hey uh, is anyone still hitting this? I can't get this to repro anymore as of `main`, in Terminal or in conhost, in cmd or powershell, in codepage 437, 936 or 65001. Maybe this got fixed in the last 9 months?
I can't even compile main because of missing `wil/nt_result_macros.h` or something, so I have no idea.  I have terminal `1.9.1942.0`  on Windows 10 and it still crashes 🤷
You need to update your submodules. `git submodule update`. :smile:
I can still reproduce a crash on a recentish build of OpenConsole (90ff261c35a1b3e50267576f5cbdc66733731101), but as I mentioned above, my crash is in `DirectReadData::Notify`, which isn't exactly the same as the OP.

To reproduce, I'm using this little test app in a conhost cmd shell:

```
#include <Windows.h>

int main(int argc, char** argv)
{
    HANDLE ih = GetStdHandle(STD_INPUT_HANDLE);

    DWORD mode;
    GetConsoleMode(ih, &mode);
    SetConsoleMode(ih, mode | ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT);

    SetConsoleCP(CP_UTF8);
    SetConsoleOutputCP(CP_UTF8);
    SetFileApisToANSI();

    while (true)
    {
        INPUT_RECORD inputRecord[100];
        DWORD inputCount;
        ReadConsoleInputA(ih, inputRecord, 100, &inputCount);
    }
}
```

And once it's running I paste a bunch of emojis like this:
😀💘💥👍🎅🦄🍄🌎🚖⚓🚀⌚💣😀💘💥👍🎅🦄🍄🌎🚖⚓🚀⌚💣😀💘💥👍🎅🦄🍄🌎🚖⚓🚀⌚💣😀💘💥👍🎅🦄🍄🌎🚖⚓🚀⌚💣😀💘💥👍🎅🦄🍄🌎🚖⚓🚀⌚💣
after an hour of compiling it now claims I don't have a .NET SDK installed.  I'm tired of this crap. You can close the bug if you like. The repro I gave is pretty simple (you can use any current release of tcsh) and if it doesn't crash, consider me satisfied.
Hmmmmmm. So it sounds like we kinda need to do a `SplitToOem` in `InputBuffer::Read`, before we even move the records to the `Notify` call. Or something like a pre-emptive scan in `Read`, like checking how many events each event will be translated into. I wonder how easy this would be to write a test for...
Okay so I think `InputBuffer::Read` is only called in two places:
* `directio.cpp:189`
* `readDataDirect.cpp:135`

Both do the same thing. If called with `!unicode`, then they will call `SplitToOem` on the events read from the buffer, to expand them out to chars. So I think it's okay to pre-emptively trim the buffer in the `!unicode` case inside of `IB::Read`, because we know the caller will be expanding them. 

Wow those two functions are really basically the same code aren't they...

Fix patch:
diff --git a/src/host/dbcs.cpp b/src/host/dbcs.cpp
index 0048f5cdc4a..bed34bfdca0 100644
--- a/src/host/dbcs.cpp
+++ b/src/host/dbcs.cpp
@@ -177,77 +177,3 @@ BOOL IsAvailableEastAsianCodePage(const UINT uiCodePage)
         return false;
     }
 }
-
-_Ret_range_(0, cbAnsi)
-    ULONG TranslateUnicodeToOem(_In_reads_(cchUnicode) PCWCHAR pwchUnicode,
-                                const ULONG cchUnicode,
-                                _Out_writes_bytes_(cbAnsi) PCHAR pchAnsi,
-                                const ULONG cbAnsi,
-                                _Out_ std::unique_ptr<IInputEvent>& partialEvent)
-{
-    const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
-    const auto TmpUni = new (std::nothrow) WCHAR[cchUnicode];
-    if (TmpUni == nullptr)
-    {
-        return 0;
-    }
-
-    memcpy(TmpUni, pwchUnicode, cchUnicode * sizeof(WCHAR));
-
-    CHAR AsciiDbcs[2];
-    AsciiDbcs[1] = 0;
-
-    ULONG i, j;
-    for (i = 0, j = 0; i < cchUnicode && j < cbAnsi; i++, j++)
-    {
-        if (IsGlyphFullWidth(TmpUni[i]))
-        {
-            const auto NumBytes = sizeof(AsciiDbcs);
-            ConvertToOem(gci.CP, &TmpUni[i], 1, (LPSTR)&AsciiDbcs[0], NumBytes);
-            if (IsDBCSLeadByteConsole(AsciiDbcs[0], &gci.CPInfo))
-            {
-                if (j < cbAnsi - 1)
-                { // -1 is safe DBCS in buffer
-                    pchAnsi[j] = AsciiDbcs[0];
-                    j++;
-                    pchAnsi[j] = AsciiDbcs[1];
-                    AsciiDbcs[1] = 0;
-                }
-                else
-                {
-                    pchAnsi[j] = AsciiDbcs[0];
-                    break;
-                }
-            }
-            else
-            {
-                pchAnsi[j] = AsciiDbcs[0];
-                AsciiDbcs[1] = 0;
-            }
-        }
-        else
-        {
-            ConvertToOem(gci.CP, &TmpUni[i], 1, &pchAnsi[j], 1);
-        }
-    }
-
-    if (AsciiDbcs[1])
-    {
-        try
-        {
-            auto keyEvent = std::make_unique<KeyEvent>();
-            if (keyEvent.get())
-            {
-                keyEvent->SetCharData(AsciiDbcs[1]);
-                partialEvent.reset(static_cast<IInputEvent* const>(keyEvent.release()));
-            }
-        }
-        catch (...)
-        {
-            LOG_HR(wil::ResultFromCaughtException());
-        }
-    }
-
-    delete[] TmpUni;
-    return j;
-}
diff --git a/src/host/dbcs.h b/src/host/dbcs.h
index fd4737e7a9c..db651eadef9 100644
--- a/src/host/dbcs.h
+++ b/src/host/dbcs.h
@@ -29,10 +29,3 @@ bool IsDBCSLeadByteConsole(const CHAR ch, const CPINFO* const pCPInfo);
 BYTE CodePageToCharSet(const UINT uiCodePage);
 
 BOOL IsAvailableEastAsianCodePage(const UINT uiCodePage);
-
-_Ret_range_(0, cbAnsi)
-    ULONG TranslateUnicodeToOem(_In_reads_(cchUnicode) PCWCHAR pwchUnicode,
-                                const ULONG cchUnicode,
-                                _Out_writes_bytes_(cbAnsi) PCHAR pchAnsi,
-                                const ULONG cbAnsi,
-                                _Out_ std::unique_ptr<IInputEvent>& partialEvent);
diff --git a/src/host/directio.cpp b/src/host/directio.cpp
index c5e69e454ad..96661c60b70 100644
--- a/src/host/directio.cpp
+++ b/src/host/directio.cpp
@@ -169,75 +169,21 @@ void EventsToUnicode(_Inout_ std::deque<std::unique_ptr<IInputEvent>>& inEvents,
         LockConsole();
         auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
 
-        std::deque<std::unique_ptr<IInputEvent>> partialEvents;
-        if (!IsUnicode)
-        {
-            if (inputBuffer.IsReadPartialByteSequenceAvailable())
-            {
-                partialEvents.push_back(inputBuffer.FetchReadPartialByteSequence(IsPeek));
-            }
-        }
-
-        size_t amountToRead;
-        if (FAILED(SizeTSub(eventReadCount, partialEvents.size(), &amountToRead)))
-        {
-            return STATUS_INTEGER_OVERFLOW;
-        }
-        std::deque<std::unique_ptr<IInputEvent>> readEvents;
-        auto Status = inputBuffer.Read(readEvents,
-                                       amountToRead,
-                                       IsPeek,
-                                       true,
-                                       IsUnicode,
-                                       false);
+        const auto Status = inputBuffer.Read(outEvents,
+                                             eventReadCount,
+                                             IsPeek,
+                                             true,
+                                             IsUnicode,
+                                             false);
 
         if (CONSOLE_STATUS_WAIT == Status)
         {
-            FAIL_FAST_IF(!(readEvents.empty()));
             // If we're told to wait until later, move all of our context
             // to the read data object and send it back up to the server.
             waiter = std::make_unique<DirectReadData>(&inputBuffer,
                                                       &readHandleState,
                                                       eventReadCount,
-                                                      std::move(partialEvents));
-        }
-        else if (SUCCEEDED_NTSTATUS(Status))
-        {
-            // split key events to oem chars if necessary
-            if (!IsUnicode)
-            {
-                try
-                {
-                    SplitToOem(readEvents);
-                }
-                CATCH_LOG();
-            }
-
-            // combine partial and readEvents
-            while (!partialEvents.empty())
-            {
-                readEvents.push_front(std::move(partialEvents.back()));
-                partialEvents.pop_back();
-            }
-
-            // move events over
-            for (size_t i = 0; i < eventReadCount; ++i)
-            {
-                if (readEvents.empty())
-                {
-                    break;
-                }
-                outEvents.push_back(std::move(readEvents.front()));
-                readEvents.pop_front();
-            }
-
-            // store partial event if necessary
-            if (!readEvents.empty())
-            {
-                inputBuffer.StoreReadPartialByteSequence(std::move(readEvents.front()));
-                readEvents.pop_front();
-                FAIL_FAST_IF(!(readEvents.empty()));
-            }
+                                                      std::move(outEvents));
         }
         return Status;
     }
diff --git a/src/host/inputBuffer.cpp b/src/host/inputBuffer.cpp
index 2427d4c19ed..215b49ceb69 100644
--- a/src/host/inputBuffer.cpp
+++ b/src/host/inputBuffer.cpp
@@ -3,12 +3,13 @@
 
 #include "precomp.h"
 #include "inputBuffer.hpp"
-#include "dbcs.h"
+
 #include "stream.h"
 #include "../types/inc/GlyphWidth.hpp"
 
-#include <functional>
+#include <til/bytes.h>
 
+#include "misc.h"
 #include "../interactivity/inc/ServiceLocator.hpp"
 
 #define INPUT_BUFFER_DEFAULT_INPUT_MODE (ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_ECHO_INPUT | ENABLE_MOUSE_INPUT)
@@ -36,59 +37,199 @@ InputBuffer::InputBuffer() :
     fInComposition = false;
 }
 
-// Routine Description:
-// - This routine frees the resources associated with an input buffer.
-// Arguments:
-// - None
-// Return Value:
-InputBuffer::~InputBuffer() = default;
+// Transfer as many `wchar_t`s from source over to the `char`/`wchar_t` buffer `target`. After it returns,
+// the start of the `source` and `target` slices will be offset by as many bytes as have been copied
+// over, so that if you call this function again it'll continue copying from wherever it left off.
+//
+// It performs the necessary `WideCharToMultiByte` conversion if `isUnicode` is `false`.
+// Since not all converted `char`s might fit into `target` it'll cache the remainder. The next
+// time this function is called those cached `char`s will then be the first to be copied over.
+void InputBuffer::Consume(bool isUnicode, std::wstring_view& source, std::span<char>& target)
+{
+    // `_cachedTextReaderA` might still contain target data from a previous invocation.
+    // `ConsumeCached` calls `_switchReadingMode` for us.
+    ConsumeCached(isUnicode, target);
 
-// Routine Description:
-// - checks if any partial char data is available for reading operation
-// Arguments:
-// - None
-// Return Value:
-// - true if partial char data is available, false otherwise
-bool InputBuffer::IsReadPartialByteSequenceAvailable()
+    if (source.empty() || target.empty())
+    {
+        return;
+    }
+
+    if (isUnicode)
+    {
+        // The above block should either leave `target` or `_cachedTextReaderW` empty (or both).
+        // If we're here, `_cachedTextReaderW` should be empty.
+        assert(_cachedTextReaderW.empty());
+
+        til::bytes_transfer(target, source);
+    }
+    else
+    {
+        // The above block should either leave `target` or `_cachedTextReaderA` empty (or both).
+        // If we're here, `_cachedTextReaderA` should be empty.
+        assert(_cachedTextReaderA.empty());
+
+        const auto cp = ServiceLocator::LocateGlobals().getConsoleInformation().CP;
+
+        // Fast path: Batch convert all data in case the user provided buffer is large enough.
+        {
+            const auto wideLength = gsl::narrow<ULONG>(source.size());
+            const auto narrowLength = gsl::narrow<ULONG>(target.size());
+
+            const auto length = WideCharToMultiByte(cp, 0, source.data(), wideLength, target.data(), narrowLength, nullptr, nullptr);
+            if (length > 0)
+            {
+                source = {};
+                til::bytes_advance(target, gsl::narrow_cast<size_t>(length));
+                return;
+            }
+
+            const auto error = GetLastError();
+            THROW_HR_IF(HRESULT_FROM_WIN32(error), error != ERROR_INSUFFICIENT_BUFFER);
+        }
+
+        // Slow path: Character-wise conversion otherwise. We do this in order to only
+        // consume as many characters from `source` as necessary to fill `target`.
+        {
+            size_t read = 0;
+
+            for (const auto& wch : source)
+            {
+                char buffer[8];
+                const auto length = WideCharToMultiByte(cp, 0, &wch, 1, &buffer[0], sizeof(buffer), nullptr, nullptr);
+                THROW_LAST_ERROR_IF(length <= 0);
+
+                std::string_view slice{ &buffer[0], gsl::narrow_cast<size_t>(length) };
+                til::bytes_transfer(target, slice);
+
+                ++read;
+
+                if (!slice.empty())
+                {
+                    _cachedTextA = slice;
+                    _cachedTextReaderA = _cachedTextA;
+                    break;
+                }
+            }
+
+            source = source.substr(read);
+        }
+    }
+}
+
+// Same as `Consume`, but without any `source` characters.
+void InputBuffer::ConsumeCached(bool isUnicode, std::span<char>& target)
 {
-    return _readPartialByteSequence.get() != nullptr;
+    _switchReadingMode(isUnicode ? ReadingMode::StringW : ReadingMode::StringA);
+
+    if (isUnicode)
+    {
+        if (!_cachedTextReaderW.empty())
+        {
+            til::bytes_transfer(target, _cachedTextReaderW);
+
+            if (_cachedTextReaderW.empty())
+            {
+                // This is just so that we release memory eagerly.
+                _cachedTextW = std::wstring{};
+            }
+        }
+    }
+    else
+    {
+        if (!_cachedTextReaderA.empty())
+        {
+            til::bytes_transfer(target, _cachedTextReaderA);
+
+            if (_cachedTextReaderA.empty())
+            {
+                // This is just so that we release memory eagerly.
+                _cachedTextA = std::string{};
+            }
+        }
+    }
 }
 
-// Routine Description:
-// - reads any read partial char data available
-// Arguments:
-// - peek - if true, data will not be removed after being fetched
-// Return Value:
-// - the partial char data. may be nullptr if no data is available
-std::unique_ptr<IInputEvent> InputBuffer::FetchReadPartialByteSequence(_In_ bool peek)
+void InputBuffer::Cache(std::wstring_view source)
+{
+    const auto off = _cachedTextW.empty() ? 0 : _cachedTextReaderW.data() - _cachedTextW.data();
+    _cachedTextW.append(source);
+    _cachedTextReaderW = std::wstring_view{ _cachedTextW }.substr(off);
+}
+
+// Moves up to `count`, previously cached events into `target`.
+size_t InputBuffer::ConsumeCached(bool isUnicode, size_t count, InputEventQueue& target)
 {
-    if (!IsReadPartialByteSequenceAvailable())
+    _switchReadingMode(isUnicode ? ReadingMode::InputEventsW : ReadingMode::InputEventsA);
+
+    size_t i = 0;
+
+    while (i < count && !_cachedInputEvents.empty())
     {
-        return std::unique_ptr<IInputEvent>();
+        target.push_back(std::move(_cachedInputEvents.front()));
+        _cachedInputEvents.pop_front();
+        i++;
     }
 
-    if (peek)
+    return i;
+}
+
+// Copies up to `count`, previously cached events into `target`.
+size_t InputBuffer::PeekCached(bool isUnicode, size_t count, InputEventQueue& target)
+{
+    _switchReadingMode(isUnicode ? ReadingMode::InputEventsW : ReadingMode::InputEventsA);
+
+    size_t i = 0;
+
+    for (const auto& e : _cachedInputEvents)
     {
-        return IInputEvent::Create(_readPartialByteSequence->ToInputRecord());
+        if (i >= count)
+        {
+            break;
+        }
+
+        target.push_back(IInputEvent::Create(e->ToInputRecord()));
+        i++;
     }
-    else
+
+    return i;
+}
+
+// Trims `source` to have a size below or equal to `expectedSourceSize` by
+// storing any extra events in `_cachedInputEvents` for later retrieval.
+void InputBuffer::Cache(bool isUnicode, InputEventQueue& source, size_t expectedSourceSize)
+{
+    _switchReadingMode(isUnicode ? ReadingMode::InputEventsW : ReadingMode::InputEventsA);
+
+    if (source.size() > expectedSourceSize)
     {
-        std::unique_ptr<IInputEvent> outEvent;
-        outEvent.swap(_readPartialByteSequence);
-        return outEvent;
+        _cachedInputEvents.insert(
+            _cachedInputEvents.end(),
+            std::make_move_iterator(source.begin() + expectedSourceSize),
+            std::make_move_iterator(source.end()));
+        source.resize(expectedSourceSize);
     }
 }
 
-// Routine Description:
-// - stores partial read char data for a later read. will overwrite
-// any previously stored data.
-// Arguments:
-// - event - The event to store
-// Return Value:
-// - None
-void InputBuffer::StoreReadPartialByteSequence(std::unique_ptr<IInputEvent> event)
+void InputBuffer::_switchReadingMode(ReadingMode mode)
+{
+    if (_readingMode != mode)
+    {
+        _switchReadingModeSlowPath(mode);
+    }
+}
+
+void InputBuffer::_switchReadingModeSlowPath(ReadingMode mode)
 {
-    _readPartialByteSequence.swap(event);
+    _cachedTextA = std::string{};
+    _cachedTextReaderA = {};
+
+    _cachedTextW = std::wstring{};
+    _cachedTextReaderW = {};
+
+    _cachedInputEvents = std::deque<std::unique_ptr<IInputEvent>>{};
+
+    _readingMode = mode;
 }
 
 // Routine Description:
@@ -261,47 +402,105 @@ void InputBuffer::PassThroughWin32MouseRequest(bool enable)
                                          const bool WaitForData,
                                          const bool Unicode,
                                          const bool Stream)
+try
 {
-    try
+    assert(OutEvents.empty());
+
+    const auto cp = ServiceLocator::LocateGlobals().getConsoleInformation().CP;
+
+    if (Peek)
+    {
+        PeekCached(Unicode, AmountToRead, OutEvents);
+    }
+    else
+    {
+        ConsumeCached(Unicode, AmountToRead, OutEvents);
+    }
+
+    auto it = _storage.begin();
+    const auto end = _storage.end();
+
+    while (it != end && OutEvents.size() < AmountToRead)
     {
-        if (_storage.empty())
+        auto event = IInputEvent::Create((*it)->ToInputRecord());
+
+        if (event->EventType() == InputEventType::KeyEvent)
         {
-            if (!WaitForData)
+            const auto keyEvent = static_cast<KeyEvent*>(event.get());
+            WORD repeat = 1;
+
+            // for stream reads we need to split any key events that have been coalesced
+            if (Stream)
             {
-                return STATUS_SUCCESS;
+                repeat = keyEvent->GetRepeatCount();
+                keyEvent->SetRepeatCount(1);
             }
-            return CONSOLE_STATUS_WAIT;
-        }
 
-        // read from buffer
-        std::deque<std::unique_ptr<IInputEvent>> events;
-        size_t eventsRead;
-        bool resetWaitEvent;
-        _ReadBuffer(events,
-                    AmountToRead,
-                    eventsRead,
-                    Peek,
-                    resetWaitEvent,
-                    Unicode,
-                    Stream);
-
-        // copy events to outEvents
-        while (!events.empty())
-        {
-            OutEvents.push_back(std::move(events.front()));
-            events.pop_front();
-        }
+            if (Unicode)
+            {
+                do
+                {
+                    OutEvents.push_back(std::make_unique<KeyEvent>(*keyEvent));
+                    repeat--;
+                } while (repeat > 0 && OutEvents.size() < AmountToRead);
+            }
+            else
+            {
+                const auto wch = keyEvent->GetCharData();
+
+                char buffer[8];
+                const auto length = WideCharToMultiByte(cp, 0, &wch, 1, &buffer[0], sizeof(buffer), nullptr, nullptr);
+                THROW_LAST_ERROR_IF(length <= 0);
+
+                const std::string_view str{ &buffer[0], gsl::narrow_cast<size_t>(length) };
+
+                do
+                {
+                    for (const auto& ch : str)
+                    {
+                        auto tempEvent = std::make_unique<KeyEvent>(*keyEvent);
+                        tempEvent->SetCharData(ch);
+                        OutEvents.push_back(std::move(tempEvent));
+                    }
+                    repeat--;
+                } while (repeat > 0 && OutEvents.size() < AmountToRead);
+            }
 
-        if (resetWaitEvent)
+            if (repeat && !Peek)
+            {
+                const auto originalKeyEvent = static_cast<KeyEvent*>((*it).get());
+                originalKeyEvent->SetRepeatCount(repeat);
+                break;
+            }
+        }
+        else
         {
-            ServiceLocator::LocateGlobals().hInputEvent.ResetEvent();
+            OutEvents.push_back(std::move(event));
         }
-        return STATUS_SUCCESS;
+
+        ++it;
     }
-    catch (...)
+
+    if (!Peek)
+    {
+        _storage.erase(_storage.begin(), it);
+    }
+
+    Cache(Unicode, OutEvents, AmountToRead);
+
+    if (OutEvents.empty())
+    {
+        return WaitForData ? CONSOLE_STATUS_WAIT : STATUS_SUCCESS;
+    }
+    if (_storage.empty())
     {
-        return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
+        ServiceLocator::LocateGlobals().hInputEvent.ResetEvent();
     }
+    return STATUS_SUCCESS;
+}
+catch (...)
+{
+    return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
 }
 
 // Routine Description:
@@ -349,130 +548,6 @@ void InputBuffer::PassThroughWin32MouseRequest(bool enable)
     return Status;
 }
 
-// Routine Description:
-// - This routine reads from a buffer. It does the buffer manipulation.
-// Arguments:
-// - outEvents - where read events are placed
-// - readCount - amount of events to read
-// - eventsRead - where to store number of events read
-// - peek - if true , don't remove data from buffer, just copy it.
-// - resetWaitEvent - on exit, true if buffer became empty.
-// - unicode - true if read should be done in unicode mode
-// - streamRead - true if read should unpack KeyEvents that have a >1 repeat count. readCount must be 1 if streamRead is true.
-// Return Value:
-// - <none>
-// Note:
-// - The console lock must be held when calling this routine.
-void InputBuffer::_ReadBuffer(_Out_ std::deque<std::unique_ptr<IInputEvent>>& outEvents,
-                              const size_t readCount,
-                              _Out_ size_t& eventsRead,
-                              const bool peek,
-                              _Out_ bool& resetWaitEvent,
-                              const bool unicode,
-                              const bool streamRead)
-{
-    // when stream reading, the previous behavior was to only allow reading of a single
-    // event at a time.
-    FAIL_FAST_IF(streamRead && readCount != 1);
-
-    resetWaitEvent = false;
-
-    std::deque<std::unique_ptr<IInputEvent>> readEvents;
-    // we need another var to keep track of how many we've read
-    // because dbcs records count for two when we aren't doing a
-    // unicode read but the eventsRead count should return the number
-    // of events actually put into outRecords.
-    size_t virtualReadCount = 0;
-
-    while (!_storage.empty() && virtualReadCount < readCount)
-    {
-        auto performNormalRead = true;
-        // for stream reads we need to split any key events that have been coalesced
-        if (streamRead)
-        {
-            if (_storage.front()->EventType() == InputEventType::KeyEvent)
-            {
-                const auto pKeyEvent = static_cast<KeyEvent* const>(_storage.front().get());
-                if (pKeyEvent->GetRepeatCount() > 1)
-                {
-                    // split the key event
-                    auto streamKeyEvent = std::make_unique<KeyEvent>(*pKeyEvent);
-                    streamKeyEvent->SetRepeatCount(1);
-                    readEvents.push_back(std::move(streamKeyEvent));
-                    pKeyEvent->SetRepeatCount(pKeyEvent->GetRepeatCount() - 1);
-                    performNormalRead = false;
-                }
-            }
-        }
-
-        if (performNormalRead)
-        {
-            readEvents.push_back(std::move(_storage.front()));
-            _storage.pop_front();
-        }
-
-        ++virtualReadCount;
-        if (!unicode)
-        {
-            if (readEvents.back()->EventType() == InputEventType::KeyEvent)
-            {
-                const auto pKeyEvent = static_cast<const KeyEvent* const>(readEvents.back().get());
-                if (IsGlyphFullWidth(pKeyEvent->GetCharData()))
-                {
-                    ++virtualReadCount;
-                }
-            }
-        }
-    }
-
-    // the amount of events that were actually read
-    eventsRead = readEvents.size();
-
-    // copy the events back if we were supposed to peek
-    if (peek)
-    {
-        if (streamRead)
-        {
-            // we need to check and see if the event was split from a coalesced key event
-            // or if it was unrelated to the current front event in storage
-            if (!readEvents.empty() &&
-                !_storage.empty() &&
-                readEvents.back()->EventType() == InputEventType::KeyEvent &&
-                _storage.front()->EventType() == InputEventType::KeyEvent &&
-                _CanCoalesce(static_cast<const KeyEvent&>(*readEvents.back()),
-                             static_cast<const KeyEvent&>(*_storage.front())))
-            {
-                auto& keyEvent = static_cast<KeyEvent&>(*_storage.front());
-                keyEvent.SetRepeatCount(keyEvent.GetRepeatCount() + 1);
-            }
-            else
-            {
-                _storage.push_front(IInputEvent::Create(readEvents.back()->ToInputRecord()));
-            }
-        }
-        else
-        {
-            for (auto it = readEvents.crbegin(); it != readEvents.crend(); ++it)
-            {
-                _storage.push_front(IInputEvent::Create((*it)->ToInputRecord()));
-            }
-        }
-    }
-
-    // move events read to proper deque
-    while (!readEvents.empty())
-    {
-        outEvents.push_back(std::move(readEvents.front()));
-        readEvents.pop_front();
-    }
-
-    // signal if we emptied the buffer
-    if (_storage.empty())
-    {
-        resetWaitEvent = true;
-    }
-}
-
 // Routine Description:
 // -  Writes events to the beginning of the input buffer.
 // Arguments:
diff --git a/src/host/inputBuffer.hpp b/src/host/inputBuffer.hpp
index 5c003faf03c..f5103ea9f98 100644
--- a/src/host/inputBuffer.hpp
+++ b/src/host/inputBuffer.hpp
@@ -41,12 +41,15 @@ class InputBuffer final : public ConsoleObjectHeader
     bool fInComposition; // specifies if there's an ongoing text composition
 
     InputBuffer();
-    ~InputBuffer();
 
-    // storage API for partial dbcs bytes being read from the buffer
-    bool IsReadPartialByteSequenceAvailable();
-    std::unique_ptr<IInputEvent> FetchReadPartialByteSequence(_In_ bool peek);
-    void StoreReadPartialByteSequence(std::unique_ptr<IInputEvent> event);
+    // String oriented APIs
+    void Consume(bool isUnicode, std::wstring_view& source, std::span<char>& target);
+    void ConsumeCached(bool isUnicode, std::span<char>& target);
+    void Cache(std::wstring_view source);
+    // INPUT_RECORD oriented APIs
+    size_t ConsumeCached(bool isUnicode, size_t count, InputEventQueue& target);
+    size_t PeekCached(bool isUnicode, size_t count, InputEventQueue& target);
+    void Cache(bool isUnicode, InputEventQueue& source, size_t expectedSourceSize);
 
     // storage API for partial dbcs bytes being written to the buffer
     bool IsWritePartialByteSequenceAvailable();
@@ -84,8 +87,22 @@ class InputBuffer final : public ConsoleObjectHeader
     void PassThroughWin32MouseRequest(bool enable);
 
 private:
+    enum class ReadingMode : uint8_t
+    {
+        StringA,
+        StringW,
+        InputEventsA,
+        InputEventsW,
+    };
+
+    std::string _cachedTextA;
+    std::string_view _cachedTextReaderA;
+    std::wstring _cachedTextW;
+    std::wstring_view _cachedTextReaderW;
+    std::deque<std::unique_ptr<IInputEvent>> _cachedInputEvents;
+    ReadingMode _readingMode = ReadingMode::StringA;
+
     std::deque<std::unique_ptr<IInputEvent>> _storage;
-    std::unique_ptr<IInputEvent> _readPartialByteSequence;
     std::unique_ptr<IInputEvent> _writePartialByteSequence;
     Microsoft::Console::VirtualTerminal::TerminalInput _termInput;
     Microsoft::Console::Render::VtEngine* _pTtyConnection;
@@ -96,13 +113,8 @@ class InputBuffer final : public ConsoleObjectHeader
     // Otherwise, we should be calling them.
     bool _vtInputShouldSuppress{ false };
 
-    void _ReadBuffer(_Out_ std::deque<std::unique_ptr<IInputEvent>>& outEvents,
-                     const size_t readCount,
-                     _Out_ size_t& eventsRead,
-                     const bool peek,
-                     _Out_ bool& resetWaitEvent,
-                     const bool unicode,
-                     const bool streamRead);
+    void _switchReadingMode(ReadingMode mode);
+    void _switchReadingModeSlowPath(ReadingMode mode);
 
     void _WriteBuffer(_Inout_ std::deque<std::unique_ptr<IInputEvent>>& inRecords,
                       _Out_ size_t& eventsWritten,
diff --git a/src/host/misc.cpp b/src/host/misc.cpp
index 671da86d7a6..531ec2a9a63 100644
--- a/src/host/misc.cpp
+++ b/src/host/misc.cpp
@@ -2,13 +2,12 @@
 // Licensed under the MIT license.
 
 #include "precomp.h"
-
 #include "misc.h"
-#include "dbcs.h"
 
-#include "../types/inc/convert.hpp"
-#include "../types/inc/GlyphWidth.hpp"
+#include <til/unicode.h>
 
+#include "dbcs.h"
+#include "../types/inc/GlyphWidth.hpp"
 #include "../interactivity/inc/ServiceLocator.hpp"
 
 #pragma hdrstop
@@ -182,47 +181,6 @@ BOOL CheckBisectProcessW(const SCREEN_INFORMATION& ScreenInfo,
     }
 }
 
-// Routine Description:
-// - Converts all key events in the deque to the oem char data and adds
-// them back to events.
-// Arguments:
-// - events - on input the IInputEvents to convert. on output, the
-// converted input events
-// Note: may throw on error
-void SplitToOem(std::deque<std::unique_ptr<IInputEvent>>& events)
-{
-    const auto cp = ServiceLocator::LocateGlobals().getConsoleInformation().CP;
-    std::deque<std::unique_ptr<IInputEvent>> convertedEvents;
-
-    for (auto& currentEvent : events)
-    {
-        if (currentEvent->EventType() == InputEventType::KeyEvent)
-        {
-            const auto pKeyEvent = static_cast<const KeyEvent* const>(currentEvent.get());
-            const auto wch = pKeyEvent->GetCharData();
-
-            char buffer[8];
-            const auto length = WideCharToMultiByte(cp, 0, &wch, 1, &buffer[0], sizeof(buffer), nullptr, nullptr);
-            THROW_LAST_ERROR_IF(length <= 0);
-
-            const std::string_view str{ &buffer[0], gsl::narrow_cast<size_t>(length) };
-
-            for (const auto& ch : str)
-            {
-                auto tempEvent = std::make_unique<KeyEvent>(*pKeyEvent);
-                tempEvent->SetCharData(ch);
-                convertedEvents.push_back(std::move(tempEvent));
-            }
-        }
-        else
-        {
-            convertedEvents.push_back(std::move(currentEvent));
-        }
-    }
-
-    events = std::move(convertedEvents);
-}
-
 // Routine Description:
 // - Converts unicode characters to ANSI given a destination codepage
 // Arguments:
diff --git a/src/host/misc.h b/src/host/misc.h
index 6269353f5c0..ca90ac23ac8 100644
--- a/src/host/misc.h
+++ b/src/host/misc.h
@@ -44,8 +44,6 @@ int ConvertToOem(const UINT uiCodePage,
                  _Out_writes_(cchTarget) CHAR* const pchTarget,
                  const UINT cchTarget) noexcept;
 
-void SplitToOem(std::deque<std::unique_ptr<IInputEvent>>& events);
-
 int ConvertInputToUnicode(const UINT uiCodePage,
                           _In_reads_(cchSource) const CHAR* const pchSource,
                           const UINT cchSource,
diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp
index 0ea9e73cb54..15f6e49d310 100644
--- a/src/host/readDataCooked.cpp
+++ b/src/host/readDataCooked.cpp
@@ -47,7 +47,7 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer,
                                    _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData,
                                    SCREEN_INFORMATION& screenInfo,
                                    _In_ size_t UserBufferSize,
-                                   _In_ PWCHAR UserBuffer,
+                                   _In_ char* UserBuffer,
                                    _In_ ULONG CtrlWakeupMask,
                                    _In_ const std::wstring_view exeName,
                                    _In_ const std::string_view initialData,
@@ -62,7 +62,7 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer,
     _exeName{ exeName },
     _pdwNumBytes{ nullptr },
 
-    _commandHistory{ CommandHistory::s_Find((HANDLE)pClientProcess) },
+    _commandHistory{ CommandHistory::s_Find(pClientProcess) },
     _controlKeyState{ 0 },
     _ctrlWakeupMask{ CtrlWakeupMask },
     _visibleCharCount{ 0 },
@@ -881,10 +881,10 @@ size_t COOKED_READ_DATA::SavePromptToUserBuffer(const size_t cch)
         try
         {
             const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
-            const auto wstr = ConvertToW(gci.CP, { reinterpret_cast<char*>(_userBuffer), cch });
-            const auto copyAmount = std::min(wstr.size(), _userBufferSize / sizeof(wchar_t));
-            std::copy_n(wstr.begin(), copyAmount, _userBuffer);
-            return copyAmount * sizeof(wchar_t);
+            const auto wstr = ConvertToW(gci.CP, { _userBuffer, cch });
+            const auto copyAmount = std::min(wstr.size() * sizeof(wchar_t), _userBufferSize);
+            std::copy_n(reinterpret_cast<const char*>(wstr.data()), copyAmount, _userBuffer);
+            return copyAmount;
         }
         CATCH_LOG();
     }
@@ -1003,211 +1003,55 @@ void COOKED_READ_DATA::SavePendingInput(const size_t index, const bool multiline
 // - Status code that indicates success, out of memory, etc.
 [[nodiscard]] NTSTATUS COOKED_READ_DATA::_handlePostCharInputLoop(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) noexcept
 {
+    std::span writer{ _userBuffer, _userBufferSize };
+    std::wstring_view input{ _backupLimit, _bytesRead / sizeof(wchar_t) };
     DWORD LineCount = 1;
 
     if (_echoInput)
     {
-        // Figure out where real string ends (at carriage return or end of buffer).
-        auto StringPtr = _backupLimit;
-        auto StringLength = _bytesRead / sizeof(WCHAR);
-        auto FoundCR = false;
-        for (size_t i = 0; i < StringLength; i++)
-        {
-            if (*StringPtr++ == UNICODE_CARRIAGERETURN)
-            {
-                StringLength = i;
-                FoundCR = true;
-                break;
-            }
-        }
-
-        if (FoundCR)
+        const auto idx = input.find(UNICODE_CARRIAGERETURN);
+        if (idx != decltype(input)::npos)
         {
             if (_commandHistory)
             {
-                // add to command line recall list if we have a history list.
                 auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
-                LOG_IF_FAILED(_commandHistory->Add({ _backupLimit, StringLength },
-                                                   WI_IsFlagSet(gci.Flags, CONSOLE_HISTORY_NODUP)));
+                LOG_IF_FAILED(_commandHistory->Add({ _backupLimit, idx }, WI_IsFlagSet(gci.Flags, CONSOLE_HISTORY_NODUP)));
             }
 
-            Tracing::s_TraceCookedRead(_clientProcess,
-                                       _backupLimit,
-                                       base::saturated_cast<ULONG>(StringLength));
-
-            // check for alias
+            Tracing::s_TraceCookedRead(_clientProcess, _backupLimit, base::saturated_cast<ULONG>(idx));
             ProcessAliases(LineCount);
-        }
-    }
 
-    auto fAddDbcsLead = false;
-    size_t NumBytes = 0;
-    // at this point, a->NumBytes contains the number of bytes in
-    // the UNICODE string read.  UserBufferSize contains the converted
-    // size of the app's buffer.
-    if (_bytesRead > _userBufferSize || LineCount > 1)
-    {
-        if (LineCount > 1)
-        {
-            PWSTR Tmp;
-            if (!isUnicode)
+            if (LineCount > 1)
             {
-                if (_pInputBuffer->IsReadPartialByteSequenceAvailable())
-                {
-                    fAddDbcsLead = true;
-                    auto event = GetInputBuffer()->FetchReadPartialByteSequence(false);
-                    const auto pKeyEvent = static_cast<const KeyEvent* const>(event.get());
-                    *_userBuffer = static_cast<char>(pKeyEvent->GetCharData());
-                    _userBuffer++;
-                    _userBufferSize -= sizeof(wchar_t);
-                }
-
-                NumBytes = 0;
-                for (Tmp = _backupLimit;
-                     *Tmp != UNICODE_LINEFEED && _userBufferSize / sizeof(WCHAR) > NumBytes;
-                     Tmp++)
-                {
-                    NumBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1;
-                }
+                input = input.substr(0, idx + 1);
             }
-
-            // clang-format off
-#pragma prefast(suppress: __WARNING_BUFFER_OVERFLOW, "LineCount > 1 means there's a UNICODE_LINEFEED")
-            // clang-format on
-            for (Tmp = _backupLimit; *Tmp != UNICODE_LINEFEED; Tmp++)
-            {
-                FAIL_FAST_IF(!(Tmp < (_backupLimit + _bytesRead)));
-            }
-
-            numBytes = (ULONG)(Tmp - _backupLimit + 1) * sizeof(*Tmp);
-        }
-        else
-        {
-            if (!isUnicode)
-            {
-                PWSTR Tmp;
-
-                if (_pInputBuffer->IsReadPartialByteSequenceAvailable())
-                {
-                    fAddDbcsLead = true;
-                    auto event = GetInputBuffer()->FetchReadPartialByteSequence(false);
-                    const auto pKeyEvent = static_cast<const KeyEvent* const>(event.get());
-                    *_userBuffer = static_cast<char>(pKeyEvent->GetCharData());
-                    _userBuffer++;
-                    _userBufferSize -= sizeof(wchar_t);
-                }
-                NumBytes = 0;
-                auto NumToWrite = _bytesRead;
-                for (Tmp = _backupLimit;
-                     NumToWrite && _userBufferSize / sizeof(WCHAR) > NumBytes;
-                     Tmp++, NumToWrite -= sizeof(WCHAR))
-                {
-                    NumBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1;
-                }
-            }
-            numBytes = _userBufferSize;
         }
+    }
 
-        __analysis_assume(numBytes <= _userBufferSize);
-        memmove(_userBuffer, _backupLimit, numBytes);
+    GetInputBuffer()->Consume(isUnicode, input, writer);
 
-        const auto pInputReadHandleData = GetInputReadHandleData();
-        const std::wstring_view pending{ _backupLimit + (numBytes / sizeof(wchar_t)), (_bytesRead - numBytes) / sizeof(wchar_t) };
+    if (!input.empty())
+    {
         if (LineCount > 1)
         {
-            pInputReadHandleData->SaveMultilinePendingInput(pending);
+            GetInputReadHandleData()->SaveMultilinePendingInput(input);
         }
         else
         {
-            pInputReadHandleData->SavePendingInput(pending);
+            GetInputReadHandleData()->SavePendingInput(input);
         }
     }
-    else
-    {
-        if (!isUnicode)
-        {
-            PWSTR Tmp;
 
-            if (_pInputBuffer->IsReadPartialByteSequenceAvailable())
-            {
-                fAddDbcsLead = true;
-                auto event = GetInputBuffer()->FetchReadPartialByteSequence(false);
-                const auto pKeyEvent = static_cast<const KeyEvent* const>(event.get());
-                *_userBuffer = static_cast<char>(pKeyEvent->GetCharData());
-                _userBuffer++;
-                _userBufferSize -= sizeof(wchar_t);
-
-                if (_userBufferSize == 0)
-                {
-                    numBytes = 1;
-                    return STATUS_SUCCESS;
-                }
-            }
-            NumBytes = 0;
-            auto NumToWrite = _bytesRead;
-            for (Tmp = _backupLimit;
-                 NumToWrite && _userBufferSize / sizeof(WCHAR) > NumBytes;
-                 Tmp++, NumToWrite -= sizeof(WCHAR))
-            {
-                NumBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1;
-            }
-        }
-
-        numBytes = _bytesRead;
-
-        if (numBytes > _userBufferSize)
-        {
-            return STATUS_BUFFER_OVERFLOW;
-        }
-
-        memmove(_userBuffer, _backupLimit, numBytes);
-    }
+    numBytes = _userBufferSize - writer.size();
     controlKeyState = _controlKeyState;
-
-    if (!isUnicode)
-    {
-        // if ansi, translate string.
-        std::unique_ptr<char[]> tempBuffer;
-        try
-        {
-            tempBuffer = std::make_unique<char[]>(NumBytes);
-        }
-        catch (...)
-        {
-            return STATUS_NO_MEMORY;
-        }
-
-        std::unique_ptr<IInputEvent> partialEvent;
-        numBytes = TranslateUnicodeToOem(_userBuffer,
-                                         gsl::narrow<ULONG>(numBytes / sizeof(wchar_t)),
-                                         tempBuffer.get(),
-                                         gsl::narrow<ULONG>(NumBytes),
-                                         partialEvent);
-
-        if (partialEvent.get())
-        {
-            GetInputBuffer()->StoreReadPartialByteSequence(std::move(partialEvent));
-        }
-
-        if (numBytes > _userBufferSize)
-        {
-            return STATUS_BUFFER_OVERFLOW;
-        }
-
-        memmove(_userBuffer, tempBuffer.get(), numBytes);
-        if (fAddDbcsLead)
-        {
-            numBytes++;
-        }
-    }
     return STATUS_SUCCESS;
 }
 
 void COOKED_READ_DATA::MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer)
 {
     // See the comment in WaitBlock.cpp for more information.
-    if (_userBuffer == reinterpret_cast<const wchar_t*>(oldBuffer))
+    if (_userBuffer == oldBuffer)
     {
-        _userBuffer = reinterpret_cast<wchar_t*>(newBuffer);
+        _userBuffer = static_cast<char*>(newBuffer);
     }
 }
diff --git a/src/host/readDataCooked.hpp b/src/host/readDataCooked.hpp
index 23096e42b51..a263303e45d 100644
--- a/src/host/readDataCooked.hpp
+++ b/src/host/readDataCooked.hpp
@@ -37,7 +37,7 @@ class COOKED_READ_DATA final : public ReadData
                      _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData,
                      SCREEN_INFORMATION& screenInfo,
                      _In_ size_t UserBufferSize,
-                     _In_ PWCHAR UserBuffer,
+                     _In_ char* UserBuffer,
                      _In_ ULONG CtrlWakeupMask,
                      _In_ const std::wstring_view exeName,
                      _In_ const std::string_view initialData,
@@ -129,7 +129,7 @@ class COOKED_READ_DATA final : public ReadData
     wchar_t* _backupLimit;
 
     size_t _userBufferSize; // doubled size in ansi case
-    wchar_t* _userBuffer;
+    char* _userBuffer;
 
     size_t* _pdwNumBytes;
 
diff --git a/src/host/readDataDirect.cpp b/src/host/readDataDirect.cpp
index 21045a9ff4a..c4f58cf3c64 100644
--- a/src/host/readDataDirect.cpp
+++ b/src/host/readDataDirect.cpp
@@ -10,9 +10,7 @@
 
 // Routine Description:
 // - Constructs direct read data class to hold context across sessions
-// generally when there's not enough data to return. Also used a bit
-// internally to just pass some information along the stack
-// (regardless of wait necessity).
+// generally when there's not enough data to return.
 // Arguments:
 // - pInputBuffer - Buffer that data will be read from.
 // - pInputReadHandleData - Context stored across calls from the same
@@ -27,17 +25,10 @@ DirectReadData::DirectReadData(_In_ InputBuffer* const pInputBuffer,
                                const size_t eventReadCount,
                                _In_ std::deque<std::unique_ptr<IInputEvent>> partialEvents) :
     ReadData(pInputBuffer, pInputReadHandleData),
-    _eventReadCount{ eventReadCount },
-    _partialEvents{ std::move(partialEvents) },
-    _outEvents{}
+    _eventReadCount{ eventReadCount }
 {
 }
 
-// Routine Description:
-// - Destructs a read data class.
-// - Decrements count of readers waiting on the given handle.
-DirectReadData::~DirectReadData() = default;
-
 // Routine Description:
 // - This routine is called to complete a direct read that blocked in
 //   ReadInputBuffer. The context of the read was saved in the DirectReadData
@@ -68,18 +59,17 @@ bool DirectReadData::Notify(const WaitTerminationReason TerminationReason,
                             _Out_ size_t* const pNumBytes,
                             _Out_ DWORD* const pControlKeyState,
                             _Out_ void* const pOutputData)
+try
 {
     FAIL_FAST_IF_NULL(pOutputData);
-
     FAIL_FAST_IF(_pInputReadHandleData->GetReadCount() == 0);
 
-    const auto& gci = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation();
-    FAIL_FAST_IF(!gci.IsConsoleLocked());
+    assert(Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked());
 
     *pReplyStatus = STATUS_SUCCESS;
     *pControlKeyState = 0;
     *pNumBytes = 0;
-    auto retVal = true;
+
     std::deque<std::unique_ptr<IInputEvent>> readEvents;
 
     // If ctrl-c or ctrl-break was seen, ignore it.
@@ -88,105 +78,58 @@ bool DirectReadData::Notify(const WaitTerminationReason TerminationReason,
         return false;
     }
 
-    // check if a partial byte is already stored that we should read
-    if (!fIsUnicode &&
-        _pInputBuffer->IsReadPartialByteSequenceAvailable() &&
-        _eventReadCount == 1)
-    {
-        _partialEvents.push_back(_pInputBuffer->FetchReadPartialByteSequence(false));
-    }
-
     // See if called by CsrDestroyProcess or CsrDestroyThread
     // via ConsoleNotifyWaitBlock. If so, just decrement the ReadCount and return.
     if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::ThreadDying))
     {
         *pReplyStatus = STATUS_THREAD_IS_TERMINATING;
+        return true;
     }
+
     // We must see if we were woken up because the handle is being
     // closed. If so, we decrement the read count. If it goes to
     // zero, we wake up the close thread. Otherwise, we wake up any
     // other thread waiting for data.
-    else if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing))
+    if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing))
     {
         *pReplyStatus = STATUS_ALERTED;
+        return true;
     }
-    else
+
+    // if we get to here, this routine was called either by the input
+    // thread or a write routine.  both of these callers grab the
+    // current console lock.
+
+    size_t amountToRead;
+    if (FAILED(SizeTSub(_eventReadCount, _outEvents.size(), &amountToRead)))
     {
-        // if we get to here, this routine was called either by the input
-        // thread or a write routine.  both of these callers grab the
-        // current console lock.
-
-        // calculate how many events we need to read
-        size_t amountAlreadyRead;
-        if (FAILED(SizeTAdd(_partialEvents.size(), _outEvents.size(), &amountAlreadyRead)))
-        {
-            *pReplyStatus = STATUS_INTEGER_OVERFLOW;
-            return retVal;
-        }
-        size_t amountToRead;
-        if (FAILED(SizeTSub(_eventReadCount, amountAlreadyRead, &amountToRead)))
-        {
-            *pReplyStatus = STATUS_INTEGER_OVERFLOW;
-            return retVal;
-        }
-
-        *pReplyStatus = _pInputBuffer->Read(readEvents,
-                                            amountToRead,
-                                            false,
-                                            false,
-                                            fIsUnicode,
-                                            false);
-
-        if (*pReplyStatus == CONSOLE_STATUS_WAIT)
-        {
-            retVal = false;
-        }
+        *pReplyStatus = STATUS_INTEGER_OVERFLOW;
+        return true;
     }
 
-    if (*pReplyStatus != CONSOLE_STATUS_WAIT)
+    *pReplyStatus = _pInputBuffer->Read(_outEvents,
+                                        amountToRead,
+                                        false,
+                                        false,
+                                        fIsUnicode,
+                                        false);
+
+    if (*pReplyStatus == CONSOLE_STATUS_WAIT)
     {
-        // split key events to oem chars if necessary
-        if (*pReplyStatus == STATUS_SUCCESS && !fIsUnicode)
-        {
-            try
-            {
-                SplitToOem(readEvents);
-            }
-            CATCH_LOG();
-        }
-
-        // combine partial and whole events
-        while (!_partialEvents.empty())
-        {
-            readEvents.push_front(std::move(_partialEvents.back()));
-            _partialEvents.pop_back();
-        }
-
-        // move read events to out storage
-        for (size_t i = 0; i < _eventReadCount; ++i)
-        {
-            if (readEvents.empty())
-            {
-                break;
-            }
-            _outEvents.push_back(std::move(readEvents.front()));
-            readEvents.pop_front();
-        }
-
-        // store partial event if necessary
-        if (!readEvents.empty())
-        {
-            _pInputBuffer->StoreReadPartialByteSequence(std::move(readEvents.front()));
-            readEvents.pop_front();
-            FAIL_FAST_IF(!(readEvents.empty()));
-        }
-
-        // move events to pOutputData
-        const auto pOutputDeque = reinterpret_cast<std::deque<std::unique_ptr<IInputEvent>>* const>(pOutputData);
-        *pNumBytes = _outEvents.size() * sizeof(INPUT_RECORD);
-        pOutputDeque->swap(_outEvents);
+        return false;
     }
-    return retVal;
+
+    // move events to pOutputData
+    const auto pOutputDeque = static_cast<std::deque<std::unique_ptr<IInputEvent>>* const>(pOutputData);
+    *pNumBytes = _outEvents.size() * sizeof(INPUT_RECORD);
+    *pOutputDeque = std::move(_outEvents);
+
+    return true;
+}
+catch (...)
+{
+    *pReplyStatus = wil::StatusFromCaughtException();
+    return true;
 }
 
 void DirectReadData::MigrateUserBuffersOnTransitionToBackgroundWait(const void* /*oldBuffer*/, void* /*newBuffer*/)
diff --git a/src/host/readDataDirect.hpp b/src/host/readDataDirect.hpp
index d1ad2e50334..820ddd59819 100644
--- a/src/host/readDataDirect.hpp
+++ b/src/host/readDataDirect.hpp
@@ -38,8 +38,6 @@ class DirectReadData final : public ReadData
 
     DirectReadData(DirectReadData&&) = default;
 
-    ~DirectReadData() override;
-
     void MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) override;
     bool Notify(const WaitTerminationReason TerminationReason,
                 const bool fIsUnicode,
@@ -50,6 +48,5 @@ class DirectReadData final : public ReadData
 
 private:
     const size_t _eventReadCount;
-    std::deque<std::unique_ptr<IInputEvent>> _partialEvents;
     std::deque<std::unique_ptr<IInputEvent>> _outEvents;
 };
diff --git a/src/host/readDataRaw.cpp b/src/host/readDataRaw.cpp
index 56fb91a28f9..9351382c074 100644
--- a/src/host/readDataRaw.cpp
+++ b/src/host/readDataRaw.cpp
@@ -31,7 +31,6 @@ RAW_READ_DATA::RAW_READ_DATA(_In_ InputBuffer* const pInputBuffer,
     _BufferSize{ BufferSize },
     _BufPtr{ THROW_HR_IF_NULL(E_INVALIDARG, BufPtr) }
 {
-    THROW_HR_IF(E_INVALIDARG, _BufferSize % sizeof(wchar_t) != 0);
     THROW_HR_IF(E_INVALIDARG, _BufferSize == 0);
 }
 
@@ -82,146 +81,50 @@ bool RAW_READ_DATA::Notify(const WaitTerminationReason TerminationReason,
     *pControlKeyState = 0;
 
     *pNumBytes = 0;
-    size_t NumBytes = 0;
-
-    PWCHAR lpBuffer;
-    auto RetVal = true;
-    auto fAddDbcsLead = false;
-    auto fSkipFinally = false;
 
     // If a ctrl-c is seen, don't terminate read. If ctrl-break is seen, terminate read.
     if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::CtrlC))
     {
         return false;
     }
-    else if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::CtrlBreak))
+
+    if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::CtrlBreak))
     {
         *pReplyStatus = STATUS_ALERTED;
+        return true;
     }
+
     // See if we were called because the thread that owns this wait block is exiting.
-    else if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::ThreadDying))
+    if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::ThreadDying))
     {
         *pReplyStatus = STATUS_THREAD_IS_TERMINATING;
+        return true;
     }
+
     // We must see if we were woken up because the handle is being
     // closed. If so, we decrement the read count. If it goes to zero,
     // we wake up the close thread. Otherwise, we wake up any other
     // thread waiting for data.
-    else if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing))
+    if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing))
     {
         *pReplyStatus = STATUS_ALERTED;
-    }
-    else
-    {
-        // If we get to here, this routine was called either by the input
-        // thread or a write routine. Both of these callers grab the current
-        // console lock.
-
-        lpBuffer = _BufPtr;
-
-        if (!fIsUnicode && _pInputBuffer->IsReadPartialByteSequenceAvailable())
-        {
-            auto event = _pInputBuffer->FetchReadPartialByteSequence(false);
-            const auto pKeyEvent = static_cast<const KeyEvent* const>(event.get());
-            *lpBuffer = static_cast<char>(pKeyEvent->GetCharData());
-            _BufferSize -= sizeof(wchar_t);
-            *pReplyStatus = STATUS_SUCCESS;
-            fAddDbcsLead = true;
-
-            if (_BufferSize == 0)
-            {
-                *pNumBytes = 1;
-                RetVal = false;
-                fSkipFinally = true;
-            }
-        }
-        else
-        {
-            // This call to GetChar may block.
-            *pReplyStatus = GetChar(_pInputBuffer,
-                                    lpBuffer,
-                                    true,
-                                    nullptr,
-                                    nullptr,
-                                    nullptr);
-        }
-
-        if (FAILED_NTSTATUS(*pReplyStatus) || fSkipFinally)
-        {
-            if (*pReplyStatus == CONSOLE_STATUS_WAIT)
-            {
-                RetVal = false;
-            }
-        }
-        else
-        {
-            NumBytes += IsGlyphFullWidth(*lpBuffer) ? 2 : 1;
-            lpBuffer++;
-            *pNumBytes += sizeof(WCHAR);
-            while (*pNumBytes < _BufferSize)
-            {
-                // This call to GetChar won't block.
-                *pReplyStatus = GetChar(_pInputBuffer,
-                                        lpBuffer,
-                                        false,
-                                        nullptr,
-                                        nullptr,
-                                        nullptr);
-                if (FAILED_NTSTATUS(*pReplyStatus))
-                {
-                    *pReplyStatus = STATUS_SUCCESS;
-                    break;
-                }
-                NumBytes += IsGlyphFullWidth(*lpBuffer) ? 2 : 1;
-                lpBuffer++;
-                *pNumBytes += sizeof(WCHAR);
-            }
-        }
+        return true;
     }
 
-    // If the read was completed (status != wait), free the raw read data.
-    if (*pReplyStatus != CONSOLE_STATUS_WAIT &&
-        !fSkipFinally &&
-        !fIsUnicode)
-    {
-        // It's ansi, so translate the string.
-        std::unique_ptr<char[]> tempBuffer;
-        try
-        {
-            tempBuffer = std::make_unique<char[]>(NumBytes);
-        }
-        catch (...)
-        {
-            return true;
-        }
+    // If we get to here, this routine was called either by the input
+    // thread or a write routine. Both of these callers grab the current
+    // console lock.
 
-        lpBuffer = _BufPtr;
-        std::unique_ptr<IInputEvent> partialEvent;
-
-        *pNumBytes = TranslateUnicodeToOem(lpBuffer,
-                                           gsl::narrow<ULONG>(*pNumBytes / sizeof(wchar_t)),
-                                           tempBuffer.get(),
-                                           gsl::narrow<ULONG>(NumBytes),
-                                           partialEvent);
-        if (partialEvent.get())
-        {
-            _pInputBuffer->StoreReadPartialByteSequence(std::move(partialEvent));
-        }
-
-        memmove(lpBuffer, tempBuffer.get(), *pNumBytes);
-        if (fAddDbcsLead)
-        {
-            (*pNumBytes)++;
-        }
-    }
-    return RetVal;
+    std::span buffer{ reinterpret_cast<char*>(_BufPtr), _BufferSize };
+    *pReplyStatus = ReadCharacterInput(*_pInputBuffer, buffer, *pNumBytes, *_pInputReadHandleData, fIsUnicode);
+    return *pReplyStatus != CONSOLE_STATUS_WAIT;
 }
 
 void RAW_READ_DATA::MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer)
 {
     // See the comment in WaitBlock.cpp for more information.
-    if (_BufPtr == reinterpret_cast<const wchar_t*>(oldBuffer))
+    if (_BufPtr == static_cast<const wchar_t*>(oldBuffer))
     {
-        _BufPtr = reinterpret_cast<wchar_t*>(newBuffer);
+        _BufPtr = static_cast<wchar_t*>(newBuffer);
     }
 }
diff --git a/src/host/stream.cpp b/src/host/stream.cpp
index 8df36f82f52..92c76b89ad0 100644
--- a/src/host/stream.cpp
+++ b/src/host/stream.cpp
@@ -6,7 +6,6 @@
 #include "_stream.h"
 #include "stream.h"
 
-#include "dbcs.h"
 #include "handle.h"
 #include "misc.h"
 #include "readDataRaw.hpp"
@@ -285,151 +284,38 @@ til::CoordType RetrieveNumberOfSpaces(_In_ til::CoordType sOriginalCursorPositio
                                                 size_t& bytesRead,
                                                 INPUT_READ_HANDLE_DATA& readHandleState,
                                                 const bool unicode)
+try
 {
-    // TODO: MSFT: 18047766 - Correct this method to not play byte counting games.
-    auto fAddDbcsLead = FALSE;
-    size_t NumToWrite = 0;
-    size_t NumToBytes = 0;
-    auto pBuffer = reinterpret_cast<wchar_t*>(buffer.data());
-    auto bufferRemaining = buffer.size_bytes();
     bytesRead = 0;
 
-    if (buffer.size_bytes() < sizeof(wchar_t))
-    {
-        return STATUS_BUFFER_TOO_SMALL;
-    }
-
-    const auto pending = readHandleState.GetPendingInput();
-    auto pendingBytes = pending.size() * sizeof(wchar_t);
-    auto Tmp = pending.cbegin();
+    auto pending = readHandleState.GetPendingInput();
 
     if (readHandleState.IsMultilineInput())
     {
-        if (!unicode)
+        const auto idx = pending.find(UNICODE_LINEFEED);
+        if (idx != decltype(pending)::npos)
         {
-            if (inputBuffer.IsReadPartialByteSequenceAvailable())
-            {
-                auto event = inputBuffer.FetchReadPartialByteSequence(false);
-                const auto pKeyEvent = static_cast<const KeyEvent* const>(event.get());
-                *pBuffer = static_cast<char>(pKeyEvent->GetCharData());
-                ++pBuffer;
-                bufferRemaining -= sizeof(wchar_t);
-                pendingBytes -= sizeof(wchar_t);
-                fAddDbcsLead = TRUE;
-            }
-
-            if (pendingBytes == 0 || bufferRemaining == 0)
-            {
-                readHandleState.CompletePending();
-                bytesRead = 1;
-                return STATUS_SUCCESS;
-            }
-            else
-            {
-                for (NumToWrite = 0, Tmp = pending.cbegin(), NumToBytes = 0;
-                     NumToBytes < pendingBytes &&
-                     NumToBytes < bufferRemaining / sizeof(wchar_t) &&
-                     *Tmp != UNICODE_LINEFEED;
-                     Tmp++, NumToWrite += sizeof(wchar_t))
-                {
-                    NumToBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1;
-                }
-            }
-        }
-
-        NumToWrite = 0;
-        Tmp = pending.cbegin();
-        while (NumToWrite < pendingBytes &&
-               *Tmp != UNICODE_LINEFEED)
-        {
-            ++Tmp;
-            NumToWrite += sizeof(wchar_t);
-        }
-
-        NumToWrite += sizeof(wchar_t);
-        if (NumToWrite > bufferRemaining)
-        {
-            NumToWrite = bufferRemaining;
+            // +1 to include the newline.
+            pending = pending.substr(0, idx + 1);
         }
     }
-    else
-    {
-        if (!unicode)
-        {
-            if (inputBuffer.IsReadPartialByteSequenceAvailable())
-            {
-                auto event = inputBuffer.FetchReadPartialByteSequence(false);
-                const auto pKeyEvent = static_cast<const KeyEvent* const>(event.get());
-                *pBuffer = static_cast<char>(pKeyEvent->GetCharData());
-                ++pBuffer;
-                bufferRemaining -= sizeof(wchar_t);
-                pendingBytes -= sizeof(wchar_t);
-                fAddDbcsLead = TRUE;
-            }
-
-            if (pendingBytes == 0)
-            {
-                readHandleState.CompletePending();
-                bytesRead = 1;
-                return STATUS_SUCCESS;
-            }
-            else
-            {
-                for (NumToWrite = 0, Tmp = pending.cbegin(), NumToBytes = 0;
-                     NumToBytes < pendingBytes && NumToBytes < bufferRemaining / sizeof(wchar_t);
-                     Tmp++, NumToWrite += sizeof(wchar_t))
-                {
-                    NumToBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1;
-                }
-            }
-        }
 
-        NumToWrite = (bufferRemaining < pendingBytes) ? bufferRemaining : pendingBytes;
-    }
+    std::span writer{ buffer };
+    inputBuffer.Consume(unicode, pending, writer);
 
-    memmove(pBuffer, pending.data(), NumToWrite);
-    pendingBytes -= NumToWrite;
-    if (pendingBytes != 0)
-    {
-        std::wstring_view remainingPending{ pending.data() + (NumToWrite / sizeof(wchar_t)), pendingBytes / sizeof(wchar_t) };
-        readHandleState.UpdatePending(remainingPending);
-    }
-    else
+    if (pending.empty())
     {
         readHandleState.CompletePending();
     }
-
-    if (!unicode)
+    else
     {
-        // if ansi, translate string.  we allocated the capture buffer
-        // large enough to handle the translated string.
-        auto tempBuffer = std::make_unique<char[]>(NumToBytes);
-        std::unique_ptr<IInputEvent> partialEvent;
-
-        NumToWrite = TranslateUnicodeToOem(pBuffer,
-                                           gsl::narrow<ULONG>(NumToWrite / sizeof(wchar_t)),
-                                           tempBuffer.get(),
-                                           gsl::narrow<ULONG>(NumToBytes),
-                                           partialEvent);
-        if (partialEvent.get())
-        {
-            inputBuffer.StoreReadPartialByteSequence(std::move(partialEvent));
-        }
-
-        // clang-format off
-#pragma prefast(suppress: __WARNING_POTENTIAL_BUFFER_OVERFLOW_HIGH_PRIORITY, "This access is fine but prefast can't follow it, evidently")
-        // clang-format on
-        memmove(pBuffer, tempBuffer.get(), NumToWrite);
-
-        if (fAddDbcsLead)
-        {
-            NumToWrite++;
-        }
+        readHandleState.UpdatePending(pending);
     }
 
-    bytesRead = NumToWrite;
+    bytesRead = buffer.size() - writer.size();
     return STATUS_SUCCESS;
 }
+NT_CATCH_RETURN()
 
 // Routine Description:
 // - read in characters until the buffer is full or return is read.
@@ -478,7 +364,7 @@ til::CoordType RetrieveNumberOfSpaces(_In_ til::CoordType sOriginalCursorPositio
                                                                  &readHandleState, // pInputReadHandleData
                                                                  screenInfo, // pScreenInfo
                                                                  buffer.size_bytes(), // UserBufferSize
-                                                                 reinterpret_cast<wchar_t*>(buffer.data()), // UserBuffer
+                                                                 buffer.data(), // UserBuffer
                                                                  ctrlWakeupMask, // CtrlWakeupMask
                                                                  exeName, // exe name
                                                                  initialData,
@@ -521,140 +407,51 @@ til::CoordType RetrieveNumberOfSpaces(_In_ til::CoordType sOriginalCursorPositio
 // populated.
 // - STATUS_SUCCESS on success
 // - Other NTSTATUS codes as necessary
-[[nodiscard]] static NTSTATUS _ReadCharacterInput(InputBuffer& inputBuffer,
-                                                  std::span<char> buffer,
-                                                  size_t& bytesRead,
-                                                  INPUT_READ_HANDLE_DATA& readHandleState,
-                                                  const bool unicode,
-                                                  std::unique_ptr<IWaitRoutine>& waiter)
+[[nodiscard]] NTSTATUS ReadCharacterInput(InputBuffer& inputBuffer,
+                                          std::span<char> buffer,
+                                          size_t& bytesRead,
+                                          INPUT_READ_HANDLE_DATA& readHandleState,
+                                          const bool unicode)
+try
 {
-    size_t NumToWrite = 0;
-    auto addDbcsLead = false;
-    auto Status = STATUS_SUCCESS;
-    auto pBuffer = reinterpret_cast<wchar_t*>(buffer.data());
-    auto bufferRemaining = buffer.size_bytes();
+    UNREFERENCED_PARAMETER(readHandleState);
+
     bytesRead = 0;
 
-    if (buffer.size() < 1)
+    const auto charSize = unicode ? sizeof(wchar_t) : sizeof(char);
+    std::span writer{ buffer };
+
+    if (writer.size() < charSize)
     {
         return STATUS_BUFFER_TOO_SMALL;
     }
 
-    if (bytesRead < bufferRemaining)
-    {
-        auto pwchBufferTmp = pBuffer;
-
-        NumToWrite = 0;
+    inputBuffer.ConsumeCached(unicode, writer);
 
-        if (!unicode && inputBuffer.IsReadPartialByteSequenceAvailable())
-        {
-            auto event = inputBuffer.FetchReadPartialByteSequence(false);
-            const auto pKeyEvent = static_cast<const KeyEvent* const>(event.get());
-            *pBuffer = static_cast<char>(pKeyEvent->GetCharData());
-            ++pBuffer;
-            bufferRemaining -= sizeof(wchar_t);
-            addDbcsLead = true;
-
-            if (bufferRemaining == 0)
-            {
-                bytesRead = 1;
-                return STATUS_SUCCESS;
-            }
-        }
-        else
-        {
-            Status = GetChar(&inputBuffer,
-                             pBuffer,
-                             true,
-                             nullptr,
-                             nullptr,
-                             nullptr);
-        }
+    // We don't need to wait for input if `ConsumeCached` read something already, which is
+    // indicated by the writer having been advanced (= it's shorter than the original buffer).
+    auto wait = writer.size() == buffer.size();
+    auto status = STATUS_SUCCESS;
 
-        if (Status == CONSOLE_STATUS_WAIT)
-        {
-            waiter = std::make_unique<RAW_READ_DATA>(&inputBuffer,
-                                                     &readHandleState,
-                                                     gsl::narrow<ULONG>(buffer.size_bytes()),
-                                                     reinterpret_cast<wchar_t*>(buffer.data()));
-        }
-
-        if (FAILED_NTSTATUS(Status))
-        {
-            bytesRead = 0;
-            return Status;
-        }
-
-        if (!addDbcsLead)
-        {
-            bytesRead += IsGlyphFullWidth(*pBuffer) ? 2 : 1;
-            NumToWrite += sizeof(wchar_t);
-            pBuffer++;
-        }
-
-        while (NumToWrite < static_cast<ULONG>(bufferRemaining))
+    while (writer.size() >= charSize)
+    {
+        wchar_t wch;
+        status = GetChar(&inputBuffer, &wch, wait, nullptr, nullptr, nullptr);
+        if (!NT_SUCCESS(status))
         {
-            Status = GetChar(&inputBuffer,
-                             pBuffer,
-                             false,
-                             nullptr,
-                             nullptr,
-                             nullptr);
-            if (FAILED_NTSTATUS(Status))
-            {
-                break;
-            }
-            bytesRead += IsGlyphFullWidth(*pBuffer) ? 2 : 1;
-            NumToWrite += sizeof(wchar_t);
-            pBuffer++;
+            break;
         }
 
-        // if ansi, translate string.  we allocated the capture buffer large enough to handle the translated string.
-        if (!unicode)
-        {
-            std::unique_ptr<char[]> tempBuffer;
-            try
-            {
-                tempBuffer = std::make_unique<char[]>(bytesRead);
-            }
-            catch (...)
-            {
-                return STATUS_NO_MEMORY;
-            }
-
-            pBuffer = pwchBufferTmp;
-            std::unique_ptr<IInputEvent> partialEvent;
-
-            bytesRead = TranslateUnicodeToOem(pBuffer,
-                                              gsl::narrow<ULONG>(NumToWrite / sizeof(wchar_t)),
-                                              tempBuffer.get(),
-                                              gsl::narrow<ULONG>(bytesRead),
-                                              partialEvent);
-
-            if (partialEvent.get())
-            {
-                inputBuffer.StoreReadPartialByteSequence(std::move(partialEvent));
-            }
+        std::wstring_view wchView{ &wch, 1 };
+        inputBuffer.Consume(unicode, wchView, writer);
 
-#pragma prefast(suppress : 26053 26015, "PREfast claims read overflow. *pReadByteCount is the exact size of tempBuffer as allocated above.")
-            memmove(pBuffer, tempBuffer.get(), bytesRead);
-
-            if (addDbcsLead)
-            {
-                ++bytesRead;
-            }
-        }
-        else
-        {
-            // We always return the byte count for A & W modes, so in
-            // the Unicode case where we didn't translate back, set
-            // the return to the byte count that we assembled while
-            // pulling characters from the internal buffers.
-            bytesRead = NumToWrite;
-        }
+        wait = false;
     }
-    return STATUS_SUCCESS;
+
+    bytesRead = buffer.size() - writer.size();
+    return status;
 }
+NT_CATCH_RETURN()
 
 // Routine Description:
 // - This routine reads in characters for stream input and does the
@@ -730,12 +527,16 @@ til::CoordType RetrieveNumberOfSpaces(_In_ til::CoordType sOriginalCursorPositio
         }
         else
         {
-            return _ReadCharacterInput(inputBuffer,
-                                       buffer,
-                                       bytesRead,
-                                       readHandleState,
-                                       unicode,
-                                       waiter);
+            const auto status = ReadCharacterInput(inputBuffer,
+                                                   buffer,
+                                                   bytesRead,
+                                                   readHandleState,
+                                                   unicode);
+            if (status == CONSOLE_STATUS_WAIT)
+            {
+                waiter = std::make_unique<RAW_READ_DATA>(&inputBuffer, &readHandleState, gsl::narrow<ULONG>(buffer.size()), reinterpret_cast<wchar_t*>(buffer.data()));
+            }
+            return status;
         }
     }
     CATCH_RETURN();
diff --git a/src/host/stream.h b/src/host/stream.h
index 7ecc0595f23..b4c37d82a45 100644
--- a/src/host/stream.h
+++ b/src/host/stream.h
@@ -29,6 +29,12 @@ Revision History:
                                _Out_opt_ bool* const pPopupKeys,
                                _Out_opt_ DWORD* const pdwKeyState) noexcept;
 
+[[nodiscard]] NTSTATUS ReadCharacterInput(InputBuffer& inputBuffer,
+                                          std::span<char> buffer,
+                                          size_t& bytesRead,
+                                          INPUT_READ_HANDLE_DATA& readHandleState,
+                                          const bool unicode);
+
 // Routine Description:
 // - This routine returns the total number of screen spaces the characters up to the specified character take up.
 til::CoordType RetrieveTotalNumberOfSpaces(const til::CoordType sOriginalCursorPositionX,
diff --git a/src/host/ut_host/CommandLineTests.cpp b/src/host/ut_host/CommandLineTests.cpp
index 3248ee4d298..b003c37b5f2 100644
--- a/src/host/ut_host/CommandLineTests.cpp
+++ b/src/host/ut_host/CommandLineTests.cpp
@@ -77,7 +77,7 @@ class CommandLineTests
                             const size_t cchBuffer)
     {
         cookedReadData._commandHistory = pHistory;
-        cookedReadData._userBuffer = pBuffer;
+        cookedReadData._userBuffer = reinterpret_cast<char*>(pBuffer);
         cookedReadData._userBufferSize = cchBuffer * sizeof(wchar_t);
         cookedReadData._bufferSize = cchBuffer * sizeof(wchar_t);
         cookedReadData._backupLimit = pBuffer;
diff --git a/src/host/ut_host/Host.UnitTests.vcxproj b/src/host/ut_host/Host.UnitTests.vcxproj
index a20cf4d5ac0..31f0f8e5a95 100644
--- a/src/host/ut_host/Host.UnitTests.vcxproj
+++ b/src/host/ut_host/Host.UnitTests.vcxproj
@@ -35,7 +35,6 @@
     <ClCompile Include="UtilsTests.cpp" />
     <ClCompile Include="Utf8ToWideCharParserTests.cpp" />
     <ClCompile Include="InputBufferTests.cpp" />
-    <ClCompile Include="ReadWaitTests.cpp" />
     <ClCompile Include="ViewportTests.cpp" />
     <ClCompile Include="VtIoTests.cpp" />
     <ClCompile Include="VtRendererTests.cpp" />
diff --git a/src/host/ut_host/Host.UnitTests.vcxproj.filters b/src/host/ut_host/Host.UnitTests.vcxproj.filters
index 6e56f956f9f..2dcabcef1d4 100644
--- a/src/host/ut_host/Host.UnitTests.vcxproj.filters
+++ b/src/host/ut_host/Host.UnitTests.vcxproj.filters
@@ -54,9 +54,6 @@
     <ClCompile Include="InputBufferTests.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
-    <ClCompile Include="ReadWaitTests.cpp">
-      <Filter>Source Files</Filter>
-    </ClCompile>
     <ClCompile Include="ConsoleArgumentsTests.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
diff --git a/src/host/ut_host/InputBufferTests.cpp b/src/host/ut_host/InputBufferTests.cpp
index 2c72bc36c60..904ca21f8ca 100644
--- a/src/host/ut_host/InputBufferTests.cpp
+++ b/src/host/ut_host/InputBufferTests.cpp
@@ -367,7 +367,7 @@ class InputBufferTests
 
     TEST_METHOD(EmptyingBufferDuringReadSetsResetWaitEvent)
     {
-        Log::Comment(L"ResetWaitEvent should be true if a read to the buffer completely empties it");
+        Log::Comment(L"hInputEvent should be reset if a read to the buffer completely empties it");
 
         InputBuffer inputBuffer;
 
@@ -381,48 +381,62 @@ class InputBufferTests
         }
         VERIFY_IS_GREATER_THAN(inputBuffer.Write(inEvents), 0u);
 
-        // read one record, make sure ResetWaitEvent isn't set
+        auto& waitEvent = ServiceLocator::LocateGlobals().hInputEvent;
+        waitEvent.SetEvent();
+
+        // read one record, hInputEvent should still be signaled
         std::deque<std::unique_ptr<IInputEvent>> outEvents;
-        size_t eventsRead = 0;
-        auto resetWaitEvent = false;
-        inputBuffer._ReadBuffer(outEvents,
-                                1,
-                                eventsRead,
-                                false,
-                                resetWaitEvent,
-                                true,
-                                false);
-        VERIFY_ARE_EQUAL(eventsRead, 1u);
-        VERIFY_IS_FALSE(!!resetWaitEvent);
-
-        // read the rest, resetWaitEvent should be set to true
+        VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents,
+                                           1,
+                                           false,
+                                           false,
+                                           true,
+                                           false));
+        VERIFY_ARE_EQUAL(outEvents.size(), 1u);
+        VERIFY_IS_TRUE(waitEvent.is_signaled());
+
+        // read the rest, hInputEvent should be reset
+        waitEvent.SetEvent();
         outEvents.clear();
-        inputBuffer._ReadBuffer(outEvents,
-                                RECORD_INSERT_COUNT - 1,
-                                eventsRead,
-                                false,
-                                resetWaitEvent,
-                                true,
-                                false);
-        VERIFY_ARE_EQUAL(eventsRead, RECORD_INSERT_COUNT - 1);
-        VERIFY_IS_TRUE(!!resetWaitEvent);
+        VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents,
+                                           RECORD_INSERT_COUNT - 1,
+                                           false,
+                                           false,
+                                           true,
+                                           false));
+        VERIFY_ARE_EQUAL(outEvents.size(), RECORD_INSERT_COUNT - 1);
+        VERIFY_IS_FALSE(waitEvent.is_signaled());
     }
 
     TEST_METHOD(ReadingDbcsCharsPadsOutputArray)
     {
         Log::Comment(L"During a non-unicode read, the input buffer should count twice for each dbcs key event");
 
+        auto& codepage = ServiceLocator::LocateGlobals().getConsoleInformation().CP;
+        const auto restoreCP = wil::scope_exit([&, previous = codepage]() {
+            codepage = previous;
+        });
+
+        codepage = CP_JAPANESE;
+
         // write a mouse event, key event, dbcs key event, mouse event
         InputBuffer inputBuffer;
-        const unsigned int recordInsertCount = 4;
-        INPUT_RECORD inRecords[recordInsertCount];
+
+        std::array<INPUT_RECORD, 4> inRecords{};
         inRecords[0].EventType = MOUSE_EVENT;
         inRecords[1] = MakeKeyEvent(TRUE, 1, L'A', 0, L'A', 0);
         inRecords[2] = MakeKeyEvent(TRUE, 1, 0x3042, 0, 0x3042, 0); // U+3042 hiragana A
         inRecords[3].EventType = MOUSE_EVENT;
 
+        std::array<INPUT_RECORD, 5> outRecordsExpected{};
+        outRecordsExpected[0].EventType = MOUSE_EVENT;
+        outRecordsExpected[1] = MakeKeyEvent(TRUE, 1, L'A', 0, L'A', 0);
+        outRecordsExpected[2] = MakeKeyEvent(TRUE, 1, 0x3042, 0, 0x82, 0);
+        outRecordsExpected[3] = MakeKeyEvent(TRUE, 1, 0x3042, 0, 0xa0, 0);
+        outRecordsExpected[4].EventType = MOUSE_EVENT;
+
         std::deque<std::unique_ptr<IInputEvent>> inEvents;
-        for (size_t i = 0; i < recordInsertCount; ++i)
+        for (size_t i = 0; i < inRecords.size(); ++i)
         {
             inEvents.push_back(IInputEvent::Create(inRecords[i]));
         }
@@ -432,22 +446,16 @@ class InputBufferTests
 
         // read them out non-unicode style and compare
         std::deque<std::unique_ptr<IInputEvent>> outEvents;
-        size_t eventsRead = 0;
-        auto resetWaitEvent = false;
-        inputBuffer._ReadBuffer(outEvents,
-                                recordInsertCount,
-                                eventsRead,
-                                false,
-                                resetWaitEvent,
-                                false,
-                                false);
-        // the dbcs record should have counted for two elements in
-        // the array, making it so that we get less events read
-        VERIFY_ARE_EQUAL(eventsRead, recordInsertCount - 1);
-        VERIFY_ARE_EQUAL(eventsRead, outEvents.size());
-        for (size_t i = 0; i < eventsRead; ++i)
+        VERIFY_NT_SUCCESS(inputBuffer.Read(outEvents,
+                                           outRecordsExpected.size(),
+                                           false,
+                                           false,
+                                           false,
+                                           false));
+        VERIFY_ARE_EQUAL(outEvents.size(), outRecordsExpected.size());
+        for (size_t i = 0; i < outEvents.size(); ++i)
         {
-            VERIFY_ARE_EQUAL(outEvents[i]->ToInputRecord(), inRecords[i]);
+            VERIFY_ARE_EQUAL(outEvents[i]->ToInputRecord(), outRecordsExpected[i]);
         }
     }
 
diff --git a/src/host/ut_host/ReadWaitTests.cpp b/src/host/ut_host/ReadWaitTests.cpp
deleted file mode 100644
index 44c76579e14..00000000000
--- a/src/host/ut_host/ReadWaitTests.cpp
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT license.
-
-#include "precomp.h"
-#include "WexTestClass.h"
-#include "../../inc/consoletaeftemplates.hpp"
-
-#include "misc.h"
-#include "dbcs.h"
-#include "../../types/inc/IInputEvent.hpp"
-
-#include "../interactivity/inc/ServiceLocator.hpp"
-
-#include <deque>
-#include <memory>
-
-using namespace WEX::Logging;
-using Microsoft::Console::Interactivity::ServiceLocator;
-
-class InputRecordConversionTests
-{
-    TEST_CLASS(InputRecordConversionTests);
-
-    static const size_t INPUT_RECORD_COUNT = 10;
-    UINT savedCodepage = 0;
-
-    TEST_CLASS_SETUP(ClassSetup)
-    {
-        auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
-        savedCodepage = gci.CP;
-        gci.CP = CP_JAPANESE;
-        VERIFY_IS_TRUE(!!GetCPInfo(gci.CP, &gci.CPInfo));
-        return true;
-    }
-
-    TEST_CLASS_CLEANUP(ClassCleanup)
-    {
-        auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
-        gci.CP = savedCodepage;
-        VERIFY_IS_TRUE(!!GetCPInfo(gci.CP, &gci.CPInfo));
-        return true;
-    }
-
-    TEST_METHOD(SplitToOemLeavesNonKeyEventsAlone)
-    {
-        Log::Comment(L"nothing should happen to input events that aren't key events");
-
-        std::deque<std::unique_ptr<IInputEvent>> inEvents;
-        INPUT_RECORD inRecords[INPUT_RECORD_COUNT] = { 0 };
-        for (size_t i = 0; i < INPUT_RECORD_COUNT; ++i)
-        {
-            inRecords[i].EventType = MOUSE_EVENT;
-            inRecords[i].Event.MouseEvent.dwMousePosition.X = static_cast<SHORT>(i);
-            inRecords[i].Event.MouseEvent.dwMousePosition.Y = static_cast<SHORT>(i * 2);
-            inEvents.push_back(IInputEvent::Create(inRecords[i]));
-        }
-
-        SplitToOem(inEvents);
-        VERIFY_ARE_EQUAL(INPUT_RECORD_COUNT, inEvents.size());
-
-        for (size_t i = 0; i < INPUT_RECORD_COUNT; ++i)
-        {
-            VERIFY_ARE_EQUAL(inRecords[i], inEvents[i]->ToInputRecord());
-        }
-    }
-
-    TEST_METHOD(SplitToOemLeavesNonDbcsCharsAlone)
-    {
-        Log::Comment(L"non-dbcs chars shouldn't be split");
-
-        std::deque<std::unique_ptr<IInputEvent>> inEvents;
-        INPUT_RECORD inRecords[INPUT_RECORD_COUNT] = { 0 };
-        for (size_t i = 0; i < INPUT_RECORD_COUNT; ++i)
-        {
-            inRecords[i].EventType = KEY_EVENT;
-            inRecords[i].Event.KeyEvent.uChar.UnicodeChar = static_cast<wchar_t>(L'a' + i);
-            inEvents.push_back(IInputEvent::Create(inRecords[i]));
-        }
-
-        SplitToOem(inEvents);
-        VERIFY_ARE_EQUAL(INPUT_RECORD_COUNT, inEvents.size());
-
-        for (size_t i = 0; i < INPUT_RECORD_COUNT; ++i)
-        {
-            VERIFY_ARE_EQUAL(inRecords[i], inEvents[i]->ToInputRecord());
-        }
-    }
-
-    TEST_METHOD(SplitToOemSplitsDbcsChars)
-    {
-        Log::Comment(L"dbcs chars should be split");
-
-        const auto codepage = ServiceLocator::LocateGlobals().getConsoleInformation().CP;
-
-        INPUT_RECORD inRecords[INPUT_RECORD_COUNT * 2] = { 0 };
-        std::deque<std::unique_ptr<IInputEvent>> inEvents;
-        // U+3042 hiragana letter A
-        wchar_t hiraganaA = 0x3042;
-        wchar_t inChars[INPUT_RECORD_COUNT];
-        for (size_t i = 0; i < INPUT_RECORD_COUNT; ++i)
-        {
-            auto currentChar = static_cast<wchar_t>(hiraganaA + (i * 2));
-            inRecords[i].EventType = KEY_EVENT;
-            inRecords[i].Event.KeyEvent.uChar.UnicodeChar = currentChar;
-            inChars[i] = currentChar;
-            inEvents.push_back(IInputEvent::Create(inRecords[i]));
-        }
-
-        SplitToOem(inEvents);
-        VERIFY_ARE_EQUAL(INPUT_RECORD_COUNT * 2, inEvents.size());
-
-        // create the data to compare the output to
-        char dbcsChars[INPUT_RECORD_COUNT * 2] = { 0 };
-        auto writtenBytes = WideCharToMultiByte(codepage,
-                                                0,
-                                                inChars,
-                                                INPUT_RECORD_COUNT,
-                                                dbcsChars,
-                                                INPUT_RECORD_COUNT * 2,
-                                                nullptr,
-                                                FALSE);
-        VERIFY_ARE_EQUAL(writtenBytes, static_cast<int>(INPUT_RECORD_COUNT * 2));
-        for (size_t i = 0; i < INPUT_RECORD_COUNT * 2; ++i)
-        {
-            const auto pKeyEvent = static_cast<const KeyEvent* const>(inEvents[i].get());
-            VERIFY_ARE_EQUAL(static_cast<char>(pKeyEvent->GetCharData()), dbcsChars[i]);
-        }
-    }
-};
diff --git a/src/inc/til/bytes.h b/src/inc/til/bytes.h
new file mode 100644
index 00000000000..593a0cd435f
--- /dev/null
+++ b/src/inc/til/bytes.h
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+#include "type_traits.h"
+
+namespace til
+{
+    template<ContiguousBytes Target>
+    constexpr void bytes_advance(Target& target, size_t count)
+    {
+        if (count > target.size())
+        {
+            throw std::length_error{ "insufficient space left" };
+        }
+
+        target = { target.data() + count, target.size() - count };
+    }
+
+    template<TriviallyCopyable T, ContiguousBytes Target>
+    constexpr bool bytes_can_put(const Target& target)
+    {
+        return target.size() >= sizeof(T);
+    }
+
+    template<TriviallyCopyable T, ContiguousBytes Target>
+    constexpr void bytes_put(Target& target, const T& value)
+    {
+        using TargetType = typename Target::value_type;
+        constexpr auto size = sizeof(value);
+
+        if (size > target.size())
+        {
+            throw std::length_error{ "insufficient space left" };
+        }
+
+        std::copy_n(reinterpret_cast<const TargetType*>(&value), size, target.data());
+        target = { target.data() + size, target.size() - size };
+    }
+
+    template<ContiguousBytes Target, ContiguousView Source>
+    requires TriviallyCopyable<typename Source::value_type>
+    constexpr void bytes_transfer(Target& target, Source& source)
+    {
+        using TargetType = typename Target::value_type;
+        constexpr auto elementSize = sizeof(typename Source::value_type);
+
+        const auto sourceCount = std::min(source.size(), target.size() / elementSize);
+        const auto targetCount = sourceCount * elementSize;
+
+        std::copy_n(reinterpret_cast<const TargetType*>(source.data()), targetCount, target.data());
+
+        target = { target.data() + targetCount, target.size() - targetCount };
+        source = { source.data() + sourceCount, source.size() - sourceCount };
+    }
+}
diff --git a/src/inc/til/type_traits.h b/src/inc/til/type_traits.h
new file mode 100644
index 00000000000..a4a10173199
--- /dev/null
+++ b/src/inc/til/type_traits.h
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+namespace til
+{
+    namespace details
+    {
+        template<typename T>
+        struct is_contiguous_view : std::false_type
+        {
+        };
+        template<typename U, std::size_t E>
+        struct is_contiguous_view<std::span<U, E>> : std::true_type
+        {
+        };
+        template<typename U, typename V>
+        struct is_contiguous_view<std::basic_string_view<U, V>> : std::true_type
+        {
+        };
+
+        template<typename T>
+        struct is_byte : std::false_type
+        {
+        };
+        template<>
+        struct is_byte<char> : std::true_type
+        {
+        };
+        template<>
+        struct is_byte<std::byte> : std::true_type
+        {
+        };
+    }
+
+    template<typename T>
+    concept Byte = details::is_byte<std::remove_cv_t<T>>::value;
+
+    template<typename T>
+    concept ContiguousView = details::is_contiguous_view<std::remove_cv_t<T>>::value;
+
+    template<typename T>
+    concept ContiguousBytes = ContiguousView<T> && Byte<typename T::value_type>;
+
+    template<typename T>
+    concept TriviallyCopyable = std::is_trivially_copyable_v<T>;
+}
diff --git a/src/server/ApiDispatchers.cpp b/src/server/ApiDispatchers.cpp
index 0558b43ac78..6b16a2bde05 100644
--- a/src/server/ApiDispatchers.cpp
+++ b/src/server/ApiDispatchers.cpp
@@ -270,13 +270,8 @@
     // Get output parameter buffer.
     PVOID pvBuffer;
     ULONG cbBufferSize;
-    // TODO: This is dumb. We should find out how much we need, not guess.
-    // If the request is not in Unicode mode, we must allocate an output buffer that is twice as big as the actual caller buffer.
-    RETURN_IF_FAILED(m->GetAugmentedOutputBuffer((a->Unicode != FALSE) ? 1 : 2,
-                                                 &pvBuffer,
-                                                 &cbBufferSize));
+    RETURN_IF_FAILED(m->GetOutputBuffer(&pvBuffer, &cbBufferSize));
 
-    // TODO: This is also rather strange and will also probably make more sense if we stop guessing that we need 2x buffer to convert.
     // This might need to go on the other side of the fence (inside host) because the server doesn't know what we're going to do with initial num bytes.
     // (This restriction exists because it's going to copy initial into the final buffer, but we don't know that.)
     RETURN_HR_IF(E_INVALIDARG, a->InitialNumBytes > cbBufferSize);
diff --git a/src/types/inc/IInputEvent.hpp b/src/types/inc/IInputEvent.hpp
index edda9846a55..50626cdda7e 100644
--- a/src/types/inc/IInputEvent.hpp
+++ b/src/types/inc/IInputEvent.hpp
@@ -64,6 +64,8 @@ class IInputEvent
 
 inline IInputEvent::~IInputEvent() = default;
 
+using InputEventQueue = std::deque<std::unique_ptr<IInputEvent>>;
+
 #ifdef UNIT_TESTING
 std::wostream& operator<<(std::wostream& stream, const IInputEvent* pEvent);
 #endif


Test patch: