terminal/src/host/ut_host/ApiRoutinesTests.cpp

881 lines
41 KiB
C++
Raw Normal View History

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "WexTestClass.h"
#include "../../inc/consoletaeftemplates.hpp"
#include "CommonState.hpp"
#include "ApiRoutines.h"
#include "getset.h"
#include "dbcs.h"
#include "misc.h"
#include "../interactivity/inc/ServiceLocator.hpp"
using namespace Microsoft::Console::Types;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using Microsoft::Console::Interactivity::ServiceLocator;
class ApiRoutinesTests
{
TEST_CLASS(ApiRoutinesTests);
std::unique_ptr<CommonState> m_state;
ApiRoutines _Routines;
IApiRoutines* _pApiRoutines = &_Routines;
TEST_METHOD_SETUP(MethodSetup)
{
m_state = std::make_unique<CommonState>();
m_state->PrepareGlobalFont();
m_state->PrepareGlobalScreenBuffer();
m_state->PrepareGlobalInputBuffer();
return true;
}
TEST_METHOD_CLEANUP(MethodCleanup)
{
m_state->CleanupGlobalInputBuffer();
m_state->CleanupGlobalScreenBuffer();
m_state->CleanupGlobalFont();
m_state.reset(nullptr);
return true;
}
BOOL _fPrevInsertMode;
void PrepVerifySetConsoleInputModeImpl(const ULONG ulOriginalInputMode)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.Flags = 0;
gci.pInputBuffer->InputMode = ulOriginalInputMode & ~(ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION | ENABLE_INSERT_MODE | ENABLE_EXTENDED_FLAGS);
gci.SetInsertMode(WI_IsFlagSet(ulOriginalInputMode, ENABLE_INSERT_MODE));
WI_UpdateFlag(gci.Flags, CONSOLE_QUICK_EDIT_MODE, WI_IsFlagSet(ulOriginalInputMode, ENABLE_QUICK_EDIT_MODE));
WI_UpdateFlag(gci.Flags, CONSOLE_AUTO_POSITION, WI_IsFlagSet(ulOriginalInputMode, ENABLE_AUTO_POSITION));
// Set cursor DB to on so we can verify that it turned off when the Insert Mode changes.
gci.GetActiveOutputBuffer().SetCursorDBMode(true);
// Record the insert mode at this time to see if it changed.
_fPrevInsertMode = gci.GetInsertMode();
}
void VerifySetConsoleInputModeImpl(const HRESULT hrExpected,
const ULONG ulNewMode)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
InputBuffer* const pii = gci.pInputBuffer;
// The expected mode set in the buffer is the mode given minus the flags that are stored in different fields.
ULONG ulModeExpected = ulNewMode;
WI_ClearAllFlags(ulModeExpected, (ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION | ENABLE_INSERT_MODE | ENABLE_EXTENDED_FLAGS));
bool const fQuickEditExpected = WI_IsFlagSet(ulNewMode, ENABLE_QUICK_EDIT_MODE);
bool const fAutoPositionExpected = WI_IsFlagSet(ulNewMode, ENABLE_AUTO_POSITION);
bool const fInsertModeExpected = WI_IsFlagSet(ulNewMode, ENABLE_INSERT_MODE);
// If the insert mode changed, we expect the cursor to have turned off.
bool const fCursorDBModeExpected = ((!!_fPrevInsertMode) == fInsertModeExpected);
// Call the API
HRESULT const hrActual = _pApiRoutines->SetConsoleInputModeImpl(*pii, ulNewMode);
// Now do verifications of final state.
VERIFY_ARE_EQUAL(hrExpected, hrActual);
VERIFY_ARE_EQUAL(ulModeExpected, pii->InputMode);
VERIFY_ARE_EQUAL(fQuickEditExpected, WI_IsFlagSet(gci.Flags, CONSOLE_QUICK_EDIT_MODE));
VERIFY_ARE_EQUAL(fAutoPositionExpected, WI_IsFlagSet(gci.Flags, CONSOLE_AUTO_POSITION));
VERIFY_ARE_EQUAL(!!fInsertModeExpected, !!gci.GetInsertMode());
VERIFY_ARE_EQUAL(fCursorDBModeExpected, gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor().IsDouble());
}
TEST_METHOD(ApiSetConsoleInputModeImplValidNonExtended)
{
Log::Comment(L"Set some perfectly valid, non-extended flags.");
PrepVerifySetConsoleInputModeImpl(0);
Log::Comment(L"Success code should result from setting valid flags.");
Log::Comment(L"Flags should be set exactly as given.");
VerifySetConsoleInputModeImpl(S_OK, ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT);
}
TEST_METHOD(ApiSetConsoleInputModeImplValidExtended)
{
Log::Comment(L"Set some perfectly valid, extended flags.");
PrepVerifySetConsoleInputModeImpl(0);
Log::Comment(L"Success code should result from setting valid flags.");
Log::Comment(L"Flags should be set exactly as given.");
VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS | ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION);
}
TEST_METHOD(ApiSetConsoleInputModeImplExtendedTurnOff)
{
Log::Comment(L"Try to turn off extended flags.");
PrepVerifySetConsoleInputModeImpl(ENABLE_EXTENDED_FLAGS | ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION);
Log::Comment(L"Success code should result from setting valid flags.");
Log::Comment(L"Flags should be set exactly as given.");
VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS);
}
TEST_METHOD(ApiSetConsoleInputModeImplInvalid)
{
Log::Comment(L"Set some invalid flags.");
PrepVerifySetConsoleInputModeImpl(0);
Log::Comment(L"Should get invalid argument code because we set invalid flags.");
Log::Comment(L"Flags should be set anyway despite invalid code.");
VerifySetConsoleInputModeImpl(E_INVALIDARG, 0x8000000);
}
TEST_METHOD(ApiSetConsoleInputModeImplInsertNoCookedRead)
{
Log::Comment(L"Turn on insert mode without cooked read data.");
PrepVerifySetConsoleInputModeImpl(0);
Log::Comment(L"Success code should result from setting valid flags.");
Log::Comment(L"Flags should be set exactly as given.");
VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS | ENABLE_INSERT_MODE);
Log::Comment(L"Turn back off and verify.");
PrepVerifySetConsoleInputModeImpl(0);
VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS);
}
TEST_METHOD(ApiSetConsoleInputModeImplInsertCookedRead)
{
Log::Comment(L"Turn on insert mode with cooked read data.");
m_state->PrepareReadHandle();
auto cleanupReadHandle = wil::scope_exit([&]() { m_state->CleanupReadHandle(); });
m_state->PrepareCookedReadData();
auto cleanupCookedRead = wil::scope_exit([&]() { m_state->CleanupCookedReadData(); });
PrepVerifySetConsoleInputModeImpl(0);
Log::Comment(L"Success code should result from setting valid flags.");
Log::Comment(L"Flags should be set exactly as given.");
VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS | ENABLE_INSERT_MODE);
Log::Comment(L"Turn back off and verify.");
PrepVerifySetConsoleInputModeImpl(0);
VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS);
}
TEST_METHOD(ApiSetConsoleInputModeImplEchoOnLineOff)
{
Log::Comment(L"Set ECHO on with LINE off. It's invalid, but it should get set anyway and return an error code.");
PrepVerifySetConsoleInputModeImpl(0);
Log::Comment(L"Setting ECHO without LINE should return an invalid argument code.");
Log::Comment(L"Input mode should be set anyway despite FAILED return code.");
VerifySetConsoleInputModeImpl(E_INVALIDARG, ENABLE_ECHO_INPUT);
}
TEST_METHOD(ApiSetConsoleInputModeExtendedFlagBehaviors)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
Log::Comment(L"Verify that we can set various extended flags even without the ENABLE_EXTENDED_FLAGS flag.");
PrepVerifySetConsoleInputModeImpl(0);
VerifySetConsoleInputModeImpl(S_OK, ENABLE_INSERT_MODE);
PrepVerifySetConsoleInputModeImpl(0);
VerifySetConsoleInputModeImpl(S_OK, ENABLE_QUICK_EDIT_MODE);
PrepVerifySetConsoleInputModeImpl(0);
VerifySetConsoleInputModeImpl(S_OK, ENABLE_AUTO_POSITION);
Log::Comment(L"Verify that we cannot unset various extended flags without the ENABLE_EXTENDED_FLAGS flag.");
PrepVerifySetConsoleInputModeImpl(ENABLE_INSERT_MODE | ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION);
InputBuffer* const pii = gci.pInputBuffer;
HRESULT const hr = _pApiRoutines->SetConsoleInputModeImpl(*pii, 0);
VERIFY_ARE_EQUAL(S_OK, hr);
VERIFY_ARE_EQUAL(true, !!gci.GetInsertMode());
VERIFY_ARE_EQUAL(true, WI_IsFlagSet(gci.Flags, CONSOLE_QUICK_EDIT_MODE));
VERIFY_ARE_EQUAL(true, WI_IsFlagSet(gci.Flags, CONSOLE_AUTO_POSITION));
}
TEST_METHOD(ApiSetConsoleInputModeImplPSReadlineScenario)
{
Log::Comment(L"Set Powershell PSReadline expected modes.");
PrepVerifySetConsoleInputModeImpl(0x1F7);
Log::Comment(L"Should return an invalid argument code because ECHO is set without LINE.");
Log::Comment(L"Input mode should be set anyway despite FAILED return code.");
VerifySetConsoleInputModeImpl(E_INVALIDARG, 0x1E4);
}
TEST_METHOD(ApiGetConsoleTitleA)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.SetTitle(L"Test window title.");
Eliminate more transient allocations: Titles and invalid rectangles and bitmap runs and utf8 conversions (#8621) ## References * See also #8617 ## PR Checklist * [x] Supports #3075 * [x] I work here. * [x] Manual test. ## Detailed Description of the Pull Request / Additional comments ### Window Title Generation Every time the renderer checks the title, it's doing two bad things that I've fixed: 1. It's assembling the prefix to the full title doing a concatenation. No one ever gets just the prefix ever after it is set besides the concat. So instead of storing prefix and the title, I store the assembled prefix + title and the bare title. 2. A copy must be made because it was returning `std::wstring` instead of `std::wstring&`. Now it returns the ref. ### Dirty Area Return Every time the renderer checks the dirty area, which is sometimes multiple times per pass (regular text printing, again for selection, etc.), a vector is created off the heap to return the rectangles. The consumers only ever iterate this data. Now we return a span over a rectangle or rectangles that the engine must store itself. 1. For some renderers, it's always a constant 1 element. They update that 1 element when dirty is queried and return it in the span with a span size of 1. 2. For other renderers with more complex behavior, they're already holding a cached vector of rectangles. Now it's effectively giving out the ref to those in the span for iteration. ### Bitmap Runs The `til::bitmap` used a `std::optional<std::vector<til::rectangle>>` inside itself to cache its runs and would clear the optional when the runs became invalidated. Unfortunately doing `.reset()` to clear the optional will destroy the underlying vector and have it release its memory. We know it's about to get reallocated again, so we're just going to make it a `std::pmr::vector` and give it a memory pool. The alternative solution here was to use a `bool` and `std::vector<til::rectangle>` and just flag when the vector was invalid, but that was honestly more code changes and I love excuses to try out PMR now. Also, instead of returning the ref to the vector... I'm just returning a span now. Everyone just iterates it anyway, may as well not share the implementation detail. ### UTF-8 conversions When testing with Terminal and looking at the `conhost.exe`'s PTY renderer, it spends a TON of allocation time on converting all the UTF-16 stuff inside to UTF-8 before it sends it out the PTY. This was because `ConvertToA` was allocating a string inside itself and returning it just to have it freed after printing and looping back around again... as a PTY does. The change here is to use `til::u16u8` that accepts a buffer out parameter so the caller can just hold onto it. ## Validation Steps Performed - [x] `big.txt` in conhost.exe (GDI renderer) - [x] `big.txt` in Terminal (DX, PTY renderer) - [x] Ensure WDDM and BGFX build under Razzle with this change.
2021-02-16 21:52:33 +01:00
const auto title = gci.GetTitle();
int const iBytesNeeded = WideCharToMultiByte(gci.OutputCP,
0,
Eliminate more transient allocations: Titles and invalid rectangles and bitmap runs and utf8 conversions (#8621) ## References * See also #8617 ## PR Checklist * [x] Supports #3075 * [x] I work here. * [x] Manual test. ## Detailed Description of the Pull Request / Additional comments ### Window Title Generation Every time the renderer checks the title, it's doing two bad things that I've fixed: 1. It's assembling the prefix to the full title doing a concatenation. No one ever gets just the prefix ever after it is set besides the concat. So instead of storing prefix and the title, I store the assembled prefix + title and the bare title. 2. A copy must be made because it was returning `std::wstring` instead of `std::wstring&`. Now it returns the ref. ### Dirty Area Return Every time the renderer checks the dirty area, which is sometimes multiple times per pass (regular text printing, again for selection, etc.), a vector is created off the heap to return the rectangles. The consumers only ever iterate this data. Now we return a span over a rectangle or rectangles that the engine must store itself. 1. For some renderers, it's always a constant 1 element. They update that 1 element when dirty is queried and return it in the span with a span size of 1. 2. For other renderers with more complex behavior, they're already holding a cached vector of rectangles. Now it's effectively giving out the ref to those in the span for iteration. ### Bitmap Runs The `til::bitmap` used a `std::optional<std::vector<til::rectangle>>` inside itself to cache its runs and would clear the optional when the runs became invalidated. Unfortunately doing `.reset()` to clear the optional will destroy the underlying vector and have it release its memory. We know it's about to get reallocated again, so we're just going to make it a `std::pmr::vector` and give it a memory pool. The alternative solution here was to use a `bool` and `std::vector<til::rectangle>` and just flag when the vector was invalid, but that was honestly more code changes and I love excuses to try out PMR now. Also, instead of returning the ref to the vector... I'm just returning a span now. Everyone just iterates it anyway, may as well not share the implementation detail. ### UTF-8 conversions When testing with Terminal and looking at the `conhost.exe`'s PTY renderer, it spends a TON of allocation time on converting all the UTF-16 stuff inside to UTF-8 before it sends it out the PTY. This was because `ConvertToA` was allocating a string inside itself and returning it just to have it freed after printing and looping back around again... as a PTY does. The change here is to use `til::u16u8` that accepts a buffer out parameter so the caller can just hold onto it. ## Validation Steps Performed - [x] `big.txt` in conhost.exe (GDI renderer) - [x] `big.txt` in Terminal (DX, PTY renderer) - [x] Ensure WDDM and BGFX build under Razzle with this change.
2021-02-16 21:52:33 +01:00
title.data(),
gsl::narrow_cast<int>(title.size()),
nullptr,
0,
nullptr,
nullptr);
wistd::unique_ptr<char[]> pszExpected = wil::make_unique_nothrow<char[]>(iBytesNeeded);
VERIFY_IS_NOT_NULL(pszExpected);
VERIFY_WIN32_BOOL_SUCCEEDED(WideCharToMultiByte(gci.OutputCP,
0,
Eliminate more transient allocations: Titles and invalid rectangles and bitmap runs and utf8 conversions (#8621) ## References * See also #8617 ## PR Checklist * [x] Supports #3075 * [x] I work here. * [x] Manual test. ## Detailed Description of the Pull Request / Additional comments ### Window Title Generation Every time the renderer checks the title, it's doing two bad things that I've fixed: 1. It's assembling the prefix to the full title doing a concatenation. No one ever gets just the prefix ever after it is set besides the concat. So instead of storing prefix and the title, I store the assembled prefix + title and the bare title. 2. A copy must be made because it was returning `std::wstring` instead of `std::wstring&`. Now it returns the ref. ### Dirty Area Return Every time the renderer checks the dirty area, which is sometimes multiple times per pass (regular text printing, again for selection, etc.), a vector is created off the heap to return the rectangles. The consumers only ever iterate this data. Now we return a span over a rectangle or rectangles that the engine must store itself. 1. For some renderers, it's always a constant 1 element. They update that 1 element when dirty is queried and return it in the span with a span size of 1. 2. For other renderers with more complex behavior, they're already holding a cached vector of rectangles. Now it's effectively giving out the ref to those in the span for iteration. ### Bitmap Runs The `til::bitmap` used a `std::optional<std::vector<til::rectangle>>` inside itself to cache its runs and would clear the optional when the runs became invalidated. Unfortunately doing `.reset()` to clear the optional will destroy the underlying vector and have it release its memory. We know it's about to get reallocated again, so we're just going to make it a `std::pmr::vector` and give it a memory pool. The alternative solution here was to use a `bool` and `std::vector<til::rectangle>` and just flag when the vector was invalid, but that was honestly more code changes and I love excuses to try out PMR now. Also, instead of returning the ref to the vector... I'm just returning a span now. Everyone just iterates it anyway, may as well not share the implementation detail. ### UTF-8 conversions When testing with Terminal and looking at the `conhost.exe`'s PTY renderer, it spends a TON of allocation time on converting all the UTF-16 stuff inside to UTF-8 before it sends it out the PTY. This was because `ConvertToA` was allocating a string inside itself and returning it just to have it freed after printing and looping back around again... as a PTY does. The change here is to use `til::u16u8` that accepts a buffer out parameter so the caller can just hold onto it. ## Validation Steps Performed - [x] `big.txt` in conhost.exe (GDI renderer) - [x] `big.txt` in Terminal (DX, PTY renderer) - [x] Ensure WDDM and BGFX build under Razzle with this change.
2021-02-16 21:52:33 +01:00
title.data(),
gsl::narrow_cast<int>(title.size()),
pszExpected.get(),
iBytesNeeded,
nullptr,
nullptr));
char pszTitle[MAX_PATH]; // most applications use MAX_PATH
size_t cchWritten = 0;
size_t cchNeeded = 0;
VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleTitleAImpl(gsl::span<char>(pszTitle, ARRAYSIZE(pszTitle)), cchWritten, cchNeeded));
VERIFY_ARE_NOT_EQUAL(0u, cchWritten);
// NOTE: W version of API returns string length. A version of API returns buffer length (string + null).
VERIFY_ARE_EQUAL(gci.GetTitle().length() + 1, cchWritten);
VERIFY_ARE_EQUAL(gci.GetTitle().length(), cchNeeded);
VERIFY_ARE_EQUAL(WEX::Common::String(pszExpected.get()), WEX::Common::String(pszTitle));
}
TEST_METHOD(ApiGetConsoleTitleW)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.SetTitle(L"Test window title.");
wchar_t pwszTitle[MAX_PATH]; // most applications use MAX_PATH
size_t cchWritten = 0;
size_t cchNeeded = 0;
VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleTitleWImpl(gsl::span<wchar_t>(pwszTitle, ARRAYSIZE(pwszTitle)), cchWritten, cchNeeded));
VERIFY_ARE_NOT_EQUAL(0u, cchWritten);
Eliminate more transient allocations: Titles and invalid rectangles and bitmap runs and utf8 conversions (#8621) ## References * See also #8617 ## PR Checklist * [x] Supports #3075 * [x] I work here. * [x] Manual test. ## Detailed Description of the Pull Request / Additional comments ### Window Title Generation Every time the renderer checks the title, it's doing two bad things that I've fixed: 1. It's assembling the prefix to the full title doing a concatenation. No one ever gets just the prefix ever after it is set besides the concat. So instead of storing prefix and the title, I store the assembled prefix + title and the bare title. 2. A copy must be made because it was returning `std::wstring` instead of `std::wstring&`. Now it returns the ref. ### Dirty Area Return Every time the renderer checks the dirty area, which is sometimes multiple times per pass (regular text printing, again for selection, etc.), a vector is created off the heap to return the rectangles. The consumers only ever iterate this data. Now we return a span over a rectangle or rectangles that the engine must store itself. 1. For some renderers, it's always a constant 1 element. They update that 1 element when dirty is queried and return it in the span with a span size of 1. 2. For other renderers with more complex behavior, they're already holding a cached vector of rectangles. Now it's effectively giving out the ref to those in the span for iteration. ### Bitmap Runs The `til::bitmap` used a `std::optional<std::vector<til::rectangle>>` inside itself to cache its runs and would clear the optional when the runs became invalidated. Unfortunately doing `.reset()` to clear the optional will destroy the underlying vector and have it release its memory. We know it's about to get reallocated again, so we're just going to make it a `std::pmr::vector` and give it a memory pool. The alternative solution here was to use a `bool` and `std::vector<til::rectangle>` and just flag when the vector was invalid, but that was honestly more code changes and I love excuses to try out PMR now. Also, instead of returning the ref to the vector... I'm just returning a span now. Everyone just iterates it anyway, may as well not share the implementation detail. ### UTF-8 conversions When testing with Terminal and looking at the `conhost.exe`'s PTY renderer, it spends a TON of allocation time on converting all the UTF-16 stuff inside to UTF-8 before it sends it out the PTY. This was because `ConvertToA` was allocating a string inside itself and returning it just to have it freed after printing and looping back around again... as a PTY does. The change here is to use `til::u16u8` that accepts a buffer out parameter so the caller can just hold onto it. ## Validation Steps Performed - [x] `big.txt` in conhost.exe (GDI renderer) - [x] `big.txt` in Terminal (DX, PTY renderer) - [x] Ensure WDDM and BGFX build under Razzle with this change.
2021-02-16 21:52:33 +01:00
const auto title = gci.GetTitle();
// NOTE: W version of API returns string length. A version of API returns buffer length (string + null).
Eliminate more transient allocations: Titles and invalid rectangles and bitmap runs and utf8 conversions (#8621) ## References * See also #8617 ## PR Checklist * [x] Supports #3075 * [x] I work here. * [x] Manual test. ## Detailed Description of the Pull Request / Additional comments ### Window Title Generation Every time the renderer checks the title, it's doing two bad things that I've fixed: 1. It's assembling the prefix to the full title doing a concatenation. No one ever gets just the prefix ever after it is set besides the concat. So instead of storing prefix and the title, I store the assembled prefix + title and the bare title. 2. A copy must be made because it was returning `std::wstring` instead of `std::wstring&`. Now it returns the ref. ### Dirty Area Return Every time the renderer checks the dirty area, which is sometimes multiple times per pass (regular text printing, again for selection, etc.), a vector is created off the heap to return the rectangles. The consumers only ever iterate this data. Now we return a span over a rectangle or rectangles that the engine must store itself. 1. For some renderers, it's always a constant 1 element. They update that 1 element when dirty is queried and return it in the span with a span size of 1. 2. For other renderers with more complex behavior, they're already holding a cached vector of rectangles. Now it's effectively giving out the ref to those in the span for iteration. ### Bitmap Runs The `til::bitmap` used a `std::optional<std::vector<til::rectangle>>` inside itself to cache its runs and would clear the optional when the runs became invalidated. Unfortunately doing `.reset()` to clear the optional will destroy the underlying vector and have it release its memory. We know it's about to get reallocated again, so we're just going to make it a `std::pmr::vector` and give it a memory pool. The alternative solution here was to use a `bool` and `std::vector<til::rectangle>` and just flag when the vector was invalid, but that was honestly more code changes and I love excuses to try out PMR now. Also, instead of returning the ref to the vector... I'm just returning a span now. Everyone just iterates it anyway, may as well not share the implementation detail. ### UTF-8 conversions When testing with Terminal and looking at the `conhost.exe`'s PTY renderer, it spends a TON of allocation time on converting all the UTF-16 stuff inside to UTF-8 before it sends it out the PTY. This was because `ConvertToA` was allocating a string inside itself and returning it just to have it freed after printing and looping back around again... as a PTY does. The change here is to use `til::u16u8` that accepts a buffer out parameter so the caller can just hold onto it. ## Validation Steps Performed - [x] `big.txt` in conhost.exe (GDI renderer) - [x] `big.txt` in Terminal (DX, PTY renderer) - [x] Ensure WDDM and BGFX build under Razzle with this change.
2021-02-16 21:52:33 +01:00
VERIFY_ARE_EQUAL(title.length(), cchWritten);
VERIFY_ARE_EQUAL(title.length(), cchNeeded);
VERIFY_ARE_EQUAL(WEX::Common::String(title.data(), gsl::narrow_cast<int>(title.size())), WEX::Common::String(pwszTitle));
}
TEST_METHOD(ApiGetConsoleOriginalTitleA)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.SetOriginalTitle(L"Test original window title.");
Eliminate more transient allocations: Titles and invalid rectangles and bitmap runs and utf8 conversions (#8621) ## References * See also #8617 ## PR Checklist * [x] Supports #3075 * [x] I work here. * [x] Manual test. ## Detailed Description of the Pull Request / Additional comments ### Window Title Generation Every time the renderer checks the title, it's doing two bad things that I've fixed: 1. It's assembling the prefix to the full title doing a concatenation. No one ever gets just the prefix ever after it is set besides the concat. So instead of storing prefix and the title, I store the assembled prefix + title and the bare title. 2. A copy must be made because it was returning `std::wstring` instead of `std::wstring&`. Now it returns the ref. ### Dirty Area Return Every time the renderer checks the dirty area, which is sometimes multiple times per pass (regular text printing, again for selection, etc.), a vector is created off the heap to return the rectangles. The consumers only ever iterate this data. Now we return a span over a rectangle or rectangles that the engine must store itself. 1. For some renderers, it's always a constant 1 element. They update that 1 element when dirty is queried and return it in the span with a span size of 1. 2. For other renderers with more complex behavior, they're already holding a cached vector of rectangles. Now it's effectively giving out the ref to those in the span for iteration. ### Bitmap Runs The `til::bitmap` used a `std::optional<std::vector<til::rectangle>>` inside itself to cache its runs and would clear the optional when the runs became invalidated. Unfortunately doing `.reset()` to clear the optional will destroy the underlying vector and have it release its memory. We know it's about to get reallocated again, so we're just going to make it a `std::pmr::vector` and give it a memory pool. The alternative solution here was to use a `bool` and `std::vector<til::rectangle>` and just flag when the vector was invalid, but that was honestly more code changes and I love excuses to try out PMR now. Also, instead of returning the ref to the vector... I'm just returning a span now. Everyone just iterates it anyway, may as well not share the implementation detail. ### UTF-8 conversions When testing with Terminal and looking at the `conhost.exe`'s PTY renderer, it spends a TON of allocation time on converting all the UTF-16 stuff inside to UTF-8 before it sends it out the PTY. This was because `ConvertToA` was allocating a string inside itself and returning it just to have it freed after printing and looping back around again... as a PTY does. The change here is to use `til::u16u8` that accepts a buffer out parameter so the caller can just hold onto it. ## Validation Steps Performed - [x] `big.txt` in conhost.exe (GDI renderer) - [x] `big.txt` in Terminal (DX, PTY renderer) - [x] Ensure WDDM and BGFX build under Razzle with this change.
2021-02-16 21:52:33 +01:00
const auto originalTitle = gci.GetOriginalTitle();
int const iBytesNeeded = WideCharToMultiByte(gci.OutputCP,
0,
Eliminate more transient allocations: Titles and invalid rectangles and bitmap runs and utf8 conversions (#8621) ## References * See also #8617 ## PR Checklist * [x] Supports #3075 * [x] I work here. * [x] Manual test. ## Detailed Description of the Pull Request / Additional comments ### Window Title Generation Every time the renderer checks the title, it's doing two bad things that I've fixed: 1. It's assembling the prefix to the full title doing a concatenation. No one ever gets just the prefix ever after it is set besides the concat. So instead of storing prefix and the title, I store the assembled prefix + title and the bare title. 2. A copy must be made because it was returning `std::wstring` instead of `std::wstring&`. Now it returns the ref. ### Dirty Area Return Every time the renderer checks the dirty area, which is sometimes multiple times per pass (regular text printing, again for selection, etc.), a vector is created off the heap to return the rectangles. The consumers only ever iterate this data. Now we return a span over a rectangle or rectangles that the engine must store itself. 1. For some renderers, it's always a constant 1 element. They update that 1 element when dirty is queried and return it in the span with a span size of 1. 2. For other renderers with more complex behavior, they're already holding a cached vector of rectangles. Now it's effectively giving out the ref to those in the span for iteration. ### Bitmap Runs The `til::bitmap` used a `std::optional<std::vector<til::rectangle>>` inside itself to cache its runs and would clear the optional when the runs became invalidated. Unfortunately doing `.reset()` to clear the optional will destroy the underlying vector and have it release its memory. We know it's about to get reallocated again, so we're just going to make it a `std::pmr::vector` and give it a memory pool. The alternative solution here was to use a `bool` and `std::vector<til::rectangle>` and just flag when the vector was invalid, but that was honestly more code changes and I love excuses to try out PMR now. Also, instead of returning the ref to the vector... I'm just returning a span now. Everyone just iterates it anyway, may as well not share the implementation detail. ### UTF-8 conversions When testing with Terminal and looking at the `conhost.exe`'s PTY renderer, it spends a TON of allocation time on converting all the UTF-16 stuff inside to UTF-8 before it sends it out the PTY. This was because `ConvertToA` was allocating a string inside itself and returning it just to have it freed after printing and looping back around again... as a PTY does. The change here is to use `til::u16u8` that accepts a buffer out parameter so the caller can just hold onto it. ## Validation Steps Performed - [x] `big.txt` in conhost.exe (GDI renderer) - [x] `big.txt` in Terminal (DX, PTY renderer) - [x] Ensure WDDM and BGFX build under Razzle with this change.
2021-02-16 21:52:33 +01:00
originalTitle.data(),
gsl::narrow_cast<int>(originalTitle.size()),
nullptr,
0,
nullptr,
nullptr);
wistd::unique_ptr<char[]> pszExpected = wil::make_unique_nothrow<char[]>(iBytesNeeded + 1);
VERIFY_IS_NOT_NULL(pszExpected);
VERIFY_WIN32_BOOL_SUCCEEDED(WideCharToMultiByte(gci.OutputCP,
0,
Eliminate more transient allocations: Titles and invalid rectangles and bitmap runs and utf8 conversions (#8621) ## References * See also #8617 ## PR Checklist * [x] Supports #3075 * [x] I work here. * [x] Manual test. ## Detailed Description of the Pull Request / Additional comments ### Window Title Generation Every time the renderer checks the title, it's doing two bad things that I've fixed: 1. It's assembling the prefix to the full title doing a concatenation. No one ever gets just the prefix ever after it is set besides the concat. So instead of storing prefix and the title, I store the assembled prefix + title and the bare title. 2. A copy must be made because it was returning `std::wstring` instead of `std::wstring&`. Now it returns the ref. ### Dirty Area Return Every time the renderer checks the dirty area, which is sometimes multiple times per pass (regular text printing, again for selection, etc.), a vector is created off the heap to return the rectangles. The consumers only ever iterate this data. Now we return a span over a rectangle or rectangles that the engine must store itself. 1. For some renderers, it's always a constant 1 element. They update that 1 element when dirty is queried and return it in the span with a span size of 1. 2. For other renderers with more complex behavior, they're already holding a cached vector of rectangles. Now it's effectively giving out the ref to those in the span for iteration. ### Bitmap Runs The `til::bitmap` used a `std::optional<std::vector<til::rectangle>>` inside itself to cache its runs and would clear the optional when the runs became invalidated. Unfortunately doing `.reset()` to clear the optional will destroy the underlying vector and have it release its memory. We know it's about to get reallocated again, so we're just going to make it a `std::pmr::vector` and give it a memory pool. The alternative solution here was to use a `bool` and `std::vector<til::rectangle>` and just flag when the vector was invalid, but that was honestly more code changes and I love excuses to try out PMR now. Also, instead of returning the ref to the vector... I'm just returning a span now. Everyone just iterates it anyway, may as well not share the implementation detail. ### UTF-8 conversions When testing with Terminal and looking at the `conhost.exe`'s PTY renderer, it spends a TON of allocation time on converting all the UTF-16 stuff inside to UTF-8 before it sends it out the PTY. This was because `ConvertToA` was allocating a string inside itself and returning it just to have it freed after printing and looping back around again... as a PTY does. The change here is to use `til::u16u8` that accepts a buffer out parameter so the caller can just hold onto it. ## Validation Steps Performed - [x] `big.txt` in conhost.exe (GDI renderer) - [x] `big.txt` in Terminal (DX, PTY renderer) - [x] Ensure WDDM and BGFX build under Razzle with this change.
2021-02-16 21:52:33 +01:00
originalTitle.data(),
gsl::narrow_cast<int>(originalTitle.size()),
pszExpected.get(),
iBytesNeeded,
nullptr,
nullptr));
// Make sure we terminate the expected title -- WC2MB does not add the \0 if we use the size variant
pszExpected[iBytesNeeded] = '\0';
char pszTitle[MAX_PATH]; // most applications use MAX_PATH
size_t cchWritten = 0;
size_t cchNeeded = 0;
VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleOriginalTitleAImpl(gsl::span<char>(pszTitle, ARRAYSIZE(pszTitle)), cchWritten, cchNeeded));
VERIFY_ARE_NOT_EQUAL(0u, cchWritten);
// NOTE: W version of API returns string length. A version of API returns buffer length (string + null).
VERIFY_ARE_EQUAL(gci.GetOriginalTitle().length() + 1, cchWritten);
VERIFY_ARE_EQUAL(gci.GetOriginalTitle().length(), cchNeeded);
VERIFY_ARE_EQUAL(WEX::Common::String(pszExpected.get()), WEX::Common::String(pszTitle));
}
TEST_METHOD(ApiGetConsoleOriginalTitleW)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.SetOriginalTitle(L"Test original window title.");
wchar_t pwszTitle[MAX_PATH]; // most applications use MAX_PATH
size_t cchWritten = 0;
size_t cchNeeded = 0;
VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleOriginalTitleWImpl(gsl::span<wchar_t>(pwszTitle, ARRAYSIZE(pwszTitle)), cchWritten, cchNeeded));
VERIFY_ARE_NOT_EQUAL(0u, cchWritten);
Eliminate more transient allocations: Titles and invalid rectangles and bitmap runs and utf8 conversions (#8621) ## References * See also #8617 ## PR Checklist * [x] Supports #3075 * [x] I work here. * [x] Manual test. ## Detailed Description of the Pull Request / Additional comments ### Window Title Generation Every time the renderer checks the title, it's doing two bad things that I've fixed: 1. It's assembling the prefix to the full title doing a concatenation. No one ever gets just the prefix ever after it is set besides the concat. So instead of storing prefix and the title, I store the assembled prefix + title and the bare title. 2. A copy must be made because it was returning `std::wstring` instead of `std::wstring&`. Now it returns the ref. ### Dirty Area Return Every time the renderer checks the dirty area, which is sometimes multiple times per pass (regular text printing, again for selection, etc.), a vector is created off the heap to return the rectangles. The consumers only ever iterate this data. Now we return a span over a rectangle or rectangles that the engine must store itself. 1. For some renderers, it's always a constant 1 element. They update that 1 element when dirty is queried and return it in the span with a span size of 1. 2. For other renderers with more complex behavior, they're already holding a cached vector of rectangles. Now it's effectively giving out the ref to those in the span for iteration. ### Bitmap Runs The `til::bitmap` used a `std::optional<std::vector<til::rectangle>>` inside itself to cache its runs and would clear the optional when the runs became invalidated. Unfortunately doing `.reset()` to clear the optional will destroy the underlying vector and have it release its memory. We know it's about to get reallocated again, so we're just going to make it a `std::pmr::vector` and give it a memory pool. The alternative solution here was to use a `bool` and `std::vector<til::rectangle>` and just flag when the vector was invalid, but that was honestly more code changes and I love excuses to try out PMR now. Also, instead of returning the ref to the vector... I'm just returning a span now. Everyone just iterates it anyway, may as well not share the implementation detail. ### UTF-8 conversions When testing with Terminal and looking at the `conhost.exe`'s PTY renderer, it spends a TON of allocation time on converting all the UTF-16 stuff inside to UTF-8 before it sends it out the PTY. This was because `ConvertToA` was allocating a string inside itself and returning it just to have it freed after printing and looping back around again... as a PTY does. The change here is to use `til::u16u8` that accepts a buffer out parameter so the caller can just hold onto it. ## Validation Steps Performed - [x] `big.txt` in conhost.exe (GDI renderer) - [x] `big.txt` in Terminal (DX, PTY renderer) - [x] Ensure WDDM and BGFX build under Razzle with this change.
2021-02-16 21:52:33 +01:00
const auto originalTitle = gci.GetOriginalTitle();
// NOTE: W version of API returns string length. A version of API returns buffer length (string + null).
Eliminate more transient allocations: Titles and invalid rectangles and bitmap runs and utf8 conversions (#8621) ## References * See also #8617 ## PR Checklist * [x] Supports #3075 * [x] I work here. * [x] Manual test. ## Detailed Description of the Pull Request / Additional comments ### Window Title Generation Every time the renderer checks the title, it's doing two bad things that I've fixed: 1. It's assembling the prefix to the full title doing a concatenation. No one ever gets just the prefix ever after it is set besides the concat. So instead of storing prefix and the title, I store the assembled prefix + title and the bare title. 2. A copy must be made because it was returning `std::wstring` instead of `std::wstring&`. Now it returns the ref. ### Dirty Area Return Every time the renderer checks the dirty area, which is sometimes multiple times per pass (regular text printing, again for selection, etc.), a vector is created off the heap to return the rectangles. The consumers only ever iterate this data. Now we return a span over a rectangle or rectangles that the engine must store itself. 1. For some renderers, it's always a constant 1 element. They update that 1 element when dirty is queried and return it in the span with a span size of 1. 2. For other renderers with more complex behavior, they're already holding a cached vector of rectangles. Now it's effectively giving out the ref to those in the span for iteration. ### Bitmap Runs The `til::bitmap` used a `std::optional<std::vector<til::rectangle>>` inside itself to cache its runs and would clear the optional when the runs became invalidated. Unfortunately doing `.reset()` to clear the optional will destroy the underlying vector and have it release its memory. We know it's about to get reallocated again, so we're just going to make it a `std::pmr::vector` and give it a memory pool. The alternative solution here was to use a `bool` and `std::vector<til::rectangle>` and just flag when the vector was invalid, but that was honestly more code changes and I love excuses to try out PMR now. Also, instead of returning the ref to the vector... I'm just returning a span now. Everyone just iterates it anyway, may as well not share the implementation detail. ### UTF-8 conversions When testing with Terminal and looking at the `conhost.exe`'s PTY renderer, it spends a TON of allocation time on converting all the UTF-16 stuff inside to UTF-8 before it sends it out the PTY. This was because `ConvertToA` was allocating a string inside itself and returning it just to have it freed after printing and looping back around again... as a PTY does. The change here is to use `til::u16u8` that accepts a buffer out parameter so the caller can just hold onto it. ## Validation Steps Performed - [x] `big.txt` in conhost.exe (GDI renderer) - [x] `big.txt` in Terminal (DX, PTY renderer) - [x] Ensure WDDM and BGFX build under Razzle with this change.
2021-02-16 21:52:33 +01:00
VERIFY_ARE_EQUAL(originalTitle.length(), cchWritten);
VERIFY_ARE_EQUAL(originalTitle.length(), cchNeeded);
VERIFY_ARE_EQUAL(WEX::Common::String(originalTitle.data(), gsl::narrow_cast<int>(originalTitle.size())), WEX::Common::String(pwszTitle));
}
static void s_AdjustOutputWait(const bool fShouldBlock)
{
WI_SetFlagIf(ServiceLocator::LocateGlobals().getConsoleInformation().Flags, CONSOLE_SELECTING, fShouldBlock);
WI_ClearFlagIf(ServiceLocator::LocateGlobals().getConsoleInformation().Flags, CONSOLE_SELECTING, !fShouldBlock);
}
TEST_METHOD(ApiWriteConsoleA)
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:fInduceWait", L"{false, true}")
TEST_METHOD_PROPERTY(L"Data:dwCodePage", L"{437, 932, 65001}")
TEST_METHOD_PROPERTY(L"Data:dwIncrement", L"{0, 1, 2}")
END_TEST_METHOD_PROPERTIES();
bool fInduceWait;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"fInduceWait", fInduceWait), L"Get whether or not we should exercise this function off a wait state.");
DWORD dwCodePage;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"dwCodePage", dwCodePage), L"Get the codepage for the test. Check a single byte, a double byte, and UTF-8.");
DWORD dwIncrement;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"dwIncrement", dwIncrement),
L"Get how many chars we should feed in at a time. This validates lead bytes and bytes held across calls.");
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer();
gci.LockConsole();
auto Unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
// Ensure global state is updated for our codepage.
gci.OutputCP = dwCodePage;
SetConsoleCPInfo(TRUE);
PCSTR pszTestText;
switch (dwCodePage)
{
case CP_USA: // US English ANSI
pszTestText = "Test Text";
break;
case CP_JAPANESE: // Japanese Shift-JIS
pszTestText = "J\x82\xa0\x82\xa2";
break;
case CP_UTF8:
pszTestText = "Test \xe3\x82\xab Text";
break;
default:
VERIFY_FAIL(L"Test is not ready for this codepage.");
return;
}
size_t cchTestText = strlen(pszTestText);
// Set our increment value for the loop.
// 0 represents the special case of feeding the whole string in at at time.
// Otherwise, we try different segment sizes to ensure preservation across calls
// for appropriate handling of DBCS and UTF-8 sequences.
const size_t cchIncrement = dwIncrement == 0 ? cchTestText : dwIncrement;
for (size_t i = 0; i < cchTestText; i += cchIncrement)
{
Log::Comment(WEX::Common::String().Format(L"Iteration %d of loop with increment %d", i, cchIncrement));
if (fInduceWait)
{
Log::Comment(L"Blocking global output state to induce waits.");
s_AdjustOutputWait(true);
}
size_t cchRead = 0;
std::unique_ptr<IWaitRoutine> waiter;
// The increment is either the specified length or the remaining text in the string (if that is smaller).
const size_t cchWriteLength = std::min(cchIncrement, cchTestText - i);
// Run the test method
Reintroduce a color compatibility hack, but only for PowerShells (#6810) There is going to be a very long tail of applications that will explicitly request VT SGR 40/37 when what they really want is to SetConsoleTextAttribute() with a black background/white foreground. Instead of making those applications look bad (and therefore making us look bad, because we're releasing this as an update to something that "looks good" already), we're introducing this compatibility quirk. Before the color reckoning in #6698 + #6506, *every* color was subject to being spontaneously and erroneously turned into the default color. Now, only the 16-color palette value that matches the active console background/foreground color will be destroyed, and only when received from specific applications. Removal will be tracked by #6807. Michael and I discussed what layer this quirk really belonged in. I originally believed it would be sufficient to detect a background color that matched the legacy default background, but @j4james provided an example of where that wouldn't work out (powershell setting the foreground color to white/gray). In addition, it was too heavyhanded: it re-broke black backgrounds for every application. Michael thought that it should live in the server, as a small VT parser that righted the wrongs coming directly out of the application. On further investigation, however, I realized that we'd need to push more information up into the server (so that it could make the decision about which VT was wrong and which was right) than should be strictly necessary. The host knows which colors are right and wrong, and it gets final say in what ends up in the buffer. Because of that, I chose to push the quirk state down through WriteConsole to DoWriteConsole and toggle state on the SCREEN_INFORMATION that indicates whether the colors coming out of the application are to be distrusted. This quirk _only applies to pwsh.exe and powershell.exe._ NOTE: This doesn't work for PowerShell the .NET Global tool, because it is run as an assembly through dotnet.exe. I have no opinion on how to fix this, or whether it is worth fixing. VALIDATION ---------- I configured my terminals to have an incredibly garish color scheme to show exactly what's going to happen as a result of this. The _default terminal background_ is purple or red, and the foreground green. I've printed out a heap of test colors to see how black interacts with them. Pull request #6810 contains the images generated from this test. The only color lines that change are the ones where black as a background or white as a foreground is selected out of the 16-color palette explicitly. Reverse video still works fine (because black is in the foreground!), and it's even possible to represent "black on default" and reverse it into "default on black", despite the black in question having been `40`. Fixes #6767.
2020-07-11 00:25:39 +02:00
const HRESULT hr = _pApiRoutines->WriteConsoleAImpl(si, { pszTestText + i, cchWriteLength }, cchRead, false, waiter);
VERIFY_ARE_EQUAL(S_OK, hr, L"Successful result code from writing.");
if (!fInduceWait)
{
VERIFY_IS_NULL(waiter.get(), L"We should have no waiter for this case.");
VERIFY_ARE_EQUAL(cchWriteLength, cchRead, L"We should have the same character count back as 'written' that we gave in.");
}
else
{
VERIFY_IS_NOT_NULL(waiter.get(), L"We should have a waiter for this case.");
// The cchRead is irrelevant at this point as it's not going to be returned until we're off the wait.
Log::Comment(L"Unblocking global output state so the wait can be serviced.");
s_AdjustOutputWait(false);
Log::Comment(L"Dispatching the wait.");
NTSTATUS Status = STATUS_SUCCESS;
size_t dwNumBytes = 0;
DWORD dwControlKeyState = 0; // unused but matches the pattern for read.
void* pOutputData = nullptr; // unused for writes but used for read.
const BOOL bNotifyResult = waiter->Notify(WaitTerminationReason::NoReason, FALSE, &Status, &dwNumBytes, &dwControlKeyState, &pOutputData);
VERIFY_IS_TRUE(!!bNotifyResult, L"Wait completion on notify should be successful.");
VERIFY_ARE_EQUAL(STATUS_SUCCESS, Status, L"We should have a successful return code to pass to the caller.");
const size_t dwBytesExpected = cchWriteLength;
VERIFY_ARE_EQUAL(dwBytesExpected, dwNumBytes, L"We should have the byte length of the string we put in as the returned value.");
}
}
}
TEST_METHOD(ApiWriteConsoleW)
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:fInduceWait", L"{false, true}")
END_TEST_METHOD_PROPERTIES();
bool fInduceWait;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"fInduceWait", fInduceWait), L"Get whether or not we should exercise this function off a wait state.");
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer();
gci.LockConsole();
auto Unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
const std::wstring testText(L"Test text");
if (fInduceWait)
{
Log::Comment(L"Blocking global output state to induce waits.");
s_AdjustOutputWait(true);
}
size_t cchRead = 0;
std::unique_ptr<IWaitRoutine> waiter;
Reintroduce a color compatibility hack, but only for PowerShells (#6810) There is going to be a very long tail of applications that will explicitly request VT SGR 40/37 when what they really want is to SetConsoleTextAttribute() with a black background/white foreground. Instead of making those applications look bad (and therefore making us look bad, because we're releasing this as an update to something that "looks good" already), we're introducing this compatibility quirk. Before the color reckoning in #6698 + #6506, *every* color was subject to being spontaneously and erroneously turned into the default color. Now, only the 16-color palette value that matches the active console background/foreground color will be destroyed, and only when received from specific applications. Removal will be tracked by #6807. Michael and I discussed what layer this quirk really belonged in. I originally believed it would be sufficient to detect a background color that matched the legacy default background, but @j4james provided an example of where that wouldn't work out (powershell setting the foreground color to white/gray). In addition, it was too heavyhanded: it re-broke black backgrounds for every application. Michael thought that it should live in the server, as a small VT parser that righted the wrongs coming directly out of the application. On further investigation, however, I realized that we'd need to push more information up into the server (so that it could make the decision about which VT was wrong and which was right) than should be strictly necessary. The host knows which colors are right and wrong, and it gets final say in what ends up in the buffer. Because of that, I chose to push the quirk state down through WriteConsole to DoWriteConsole and toggle state on the SCREEN_INFORMATION that indicates whether the colors coming out of the application are to be distrusted. This quirk _only applies to pwsh.exe and powershell.exe._ NOTE: This doesn't work for PowerShell the .NET Global tool, because it is run as an assembly through dotnet.exe. I have no opinion on how to fix this, or whether it is worth fixing. VALIDATION ---------- I configured my terminals to have an incredibly garish color scheme to show exactly what's going to happen as a result of this. The _default terminal background_ is purple or red, and the foreground green. I've printed out a heap of test colors to see how black interacts with them. Pull request #6810 contains the images generated from this test. The only color lines that change are the ones where black as a background or white as a foreground is selected out of the 16-color palette explicitly. Reverse video still works fine (because black is in the foreground!), and it's even possible to represent "black on default" and reverse it into "default on black", despite the black in question having been `40`. Fixes #6767.
2020-07-11 00:25:39 +02:00
const HRESULT hr = _pApiRoutines->WriteConsoleWImpl(si, testText, cchRead, false, waiter);
VERIFY_ARE_EQUAL(S_OK, hr, L"Successful result code from writing.");
if (!fInduceWait)
{
VERIFY_IS_NULL(waiter.get(), L"We should have no waiter for this case.");
VERIFY_ARE_EQUAL(testText.size(), cchRead, L"We should have the same character count back as 'written' that we gave in.");
}
else
{
VERIFY_IS_NOT_NULL(waiter.get(), L"We should have a waiter for this case.");
// The cchRead is irrelevant at this point as it's not going to be returned until we're off the wait.
Log::Comment(L"Unblocking global output state so the wait can be serviced.");
s_AdjustOutputWait(false);
Log::Comment(L"Dispatching the wait.");
NTSTATUS Status = STATUS_SUCCESS;
size_t dwNumBytes = 0;
DWORD dwControlKeyState = 0; // unused but matches the pattern for read.
void* pOutputData = nullptr; // unused for writes but used for read.
const BOOL bNotifyResult = waiter->Notify(WaitTerminationReason::NoReason, TRUE, &Status, &dwNumBytes, &dwControlKeyState, &pOutputData);
VERIFY_IS_TRUE(!!bNotifyResult, L"Wait completion on notify should be successful.");
VERIFY_ARE_EQUAL(STATUS_SUCCESS, Status, L"We should have a successful return code to pass to the caller.");
const size_t dwBytesExpected = testText.size() * sizeof(wchar_t);
VERIFY_ARE_EQUAL(dwBytesExpected, dwNumBytes, L"We should have the byte length of the string we put in as the returned value.");
}
}
void ValidateScreen(SCREEN_INFORMATION& si,
const CHAR_INFO background,
const CHAR_INFO fill,
const COORD delta,
const std::optional<Viewport> clip)
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& activeSi = si.GetActiveBuffer();
auto bufferSize = activeSi.GetBufferSize();
// Find the background area viewport by taking the size, translating it by the delta, then cropping it back to the buffer size.
Viewport backgroundArea = Viewport::Offset(bufferSize, delta);
bufferSize.Clamp(backgroundArea);
auto it = activeSi.GetCellDataAt({ 0, 0 }); // We're going to walk the whole thing. Start in the top left corner.
while (it)
{
if (backgroundArea.IsInBounds(it._pos) ||
(clip.has_value() && !clip.value().IsInBounds(it._pos)))
{
auto cellInfo = gci.AsCharInfo(*it);
VERIFY_ARE_EQUAL(background, cellInfo);
}
else
{
VERIFY_ARE_EQUAL(fill, gci.AsCharInfo(*it));
}
it++;
}
}
void ValidateComplexScreen(SCREEN_INFORMATION& si,
const CHAR_INFO background,
const CHAR_INFO fill,
const CHAR_INFO scroll,
const Viewport scrollArea,
const COORD destPoint,
const std::optional<Viewport> clip)
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& activeSi = si.GetActiveBuffer();
auto bufferSize = activeSi.GetBufferSize();
// Find the delta by comparing the scroll area to the destination point
COORD delta;
delta.X = destPoint.X - scrollArea.Left();
delta.Y = destPoint.Y - scrollArea.Top();
// Find the area where the scroll text should have gone by taking the scrolled area by the delta
Viewport scrolledDestination = Viewport::Offset(scrollArea, delta);
bufferSize.Clamp(scrolledDestination);
auto it = activeSi.GetCellDataAt({ 0, 0 }); // We're going to walk the whole thing. Start in the top left corner.
while (it)
{
// If there's no clip rectangle...
if (!clip.has_value())
{
// Three states.
// 1. We filled the background with something (background CHAR_INFO)
// 2. We filled another smaller area with a different something (scroll CHAR_INFO)
// 3. We moved #2 by delta and the uncovered area was filled with a third something (fill CHAR_INFO)
// If it's in the scrolled destination, it's the value that just got moved.
if (scrolledDestination.IsInBounds(it._pos))
{
VERIFY_ARE_EQUAL(scroll, gci.AsCharInfo(*it));
}
// Otherwise, if it's not in the destination but it was in the source, assume it got filled in.
else if (scrollArea.IsInBounds(it._pos))
{
VERIFY_ARE_EQUAL(fill, gci.AsCharInfo(*it));
}
// Lastly if it's not in either spot, it should have our background CHAR_INFO
else
{
VERIFY_ARE_EQUAL(background, gci.AsCharInfo(*it));
}
}
// If there is a clip rectangle...
else
{
const auto unboxedClip = clip.value();
if (unboxedClip.IsInBounds(it._pos))
{
if (scrolledDestination.IsInBounds(it._pos))
{
VERIFY_ARE_EQUAL(scroll, gci.AsCharInfo(*it));
}
else if (scrollArea.IsInBounds(it._pos))
{
VERIFY_ARE_EQUAL(fill, gci.AsCharInfo(*it));
}
else
{
VERIFY_ARE_EQUAL(background, gci.AsCharInfo(*it));
}
}
else
{
if (scrollArea.IsInBounds(it._pos))
{
VERIFY_ARE_EQUAL(scroll, gci.AsCharInfo(*it));
}
else
{
VERIFY_ARE_EQUAL(background, gci.AsCharInfo(*it));
}
}
}
// Move to next iterator position and check.
it++;
}
}
TEST_METHOD(ApiScrollConsoleScreenBufferW)
{
BEGIN_TEST_METHOD_PROPERTIES()
Remove unwanted DECSTBM clipping (#2764) The `DECSTBM` margins are meant to define the range of lines within which certain vertical scrolling operations take place. However, we were applying these margin restrictions in the `ScrollRegion` function, which is also used in a number of places that shouldn't be affected by `DECSTBM`. This includes the `ICH` and `DCH` escape sequences (which are only affected by the horizontal margins, which we don't yet support), the `ScrollConsoleScreenBuffer` API (which is public Console API, not meant to be affected by the VT terminal emulation), and the `CSI 3 J` erase scrollback extension (which isn't really scrolling as such, but uses the `ScrollRegion` function to manipulate the scrollback buffer). This commit moves the margin clipping out of the `ScrollRegion` function, so it can be applied exclusively in the places that need it. With the margin clipping removed from the `ScrollRegion` function, it now had to be applied manually in the places it was actually required. This included: * The `DoSrvPrivateReverseLineFeed` function (for the `RI` control): This was * just a matter of updating the bottom of the scroll rect to the bottom margin * (at least when the margins were actually set), since the top of the scroll * rect would always be the top of the viewport. The * `DoSrvPrivateModifyLinesImpl` function (for the `IL` and `DL` commands): * Again this was just a matter of updating the bottom of the scroll rect, since * the cursor position would always determine the top of the scroll rect. The * `AdaptDispatch::_ScrollMovement` method (for the `SU` and `SD` commands): * This required updating both the top and bottom coordinates of the scroll * rect, and also a simpler destination Y coordinate (the way the `ScrollRegion` * function worked before, the caller was expected to take the margins into * account when determining the destination). On the plus side, there was now no longer a need to override the margins when calling `ScrollRegion` in the `AdjustCursorPosition` function. In the first case, the margins had needed to be cleared (_stream.cpp 143-145), but that is now the default behaviour. In the second case, there had been a more complicated adjustment of the margins (_stream.cpp 196-209), but that code was never actually used so could be removed completely (to get to that point either _fScrollUp_ was true, so _scrollDownAtTop_ couldn't also be true, or _fScrollDown_ was true, but in that case there is a check to make sure _scrollDownAtTop_ is false). While testing, I also noticed that one of the `ScrollRegion` calls in the `AdjustCursorPosition` function was not setting the horizontal range correctly - the scrolling should always affect the full buffer width rather than just the viewport width - so I've fixed that now as well. ## Validation Steps Performed For commands like `RI`, `IL`, `DL`, etc. where we've changed the implementation but not the behaviour, there were already unit tests that could confirm that the new implementation was still producing the correct results. Where there has been a change in behaviour - namely for the `ICH` and `DCH` commands, and the `ScrollConsoleScreenBuffer` API - I've extended the existing unit tests to check that they still function correctly even when the `DECSTBM` margins are set (which would previously have caused them to fail). I've also tested manually with the test cases in issues #2543 and #2659, and confirmed that they now work as expected. Closes #2543 Closes #2659
2019-09-24 01:16:54 +02:00
TEST_METHOD_PROPERTY(L"data:setMargins", L"{false, true}")
TEST_METHOD_PROPERTY(L"data:checkClipped", L"{false, true}")
END_TEST_METHOD_PROPERTIES();
Remove unwanted DECSTBM clipping (#2764) The `DECSTBM` margins are meant to define the range of lines within which certain vertical scrolling operations take place. However, we were applying these margin restrictions in the `ScrollRegion` function, which is also used in a number of places that shouldn't be affected by `DECSTBM`. This includes the `ICH` and `DCH` escape sequences (which are only affected by the horizontal margins, which we don't yet support), the `ScrollConsoleScreenBuffer` API (which is public Console API, not meant to be affected by the VT terminal emulation), and the `CSI 3 J` erase scrollback extension (which isn't really scrolling as such, but uses the `ScrollRegion` function to manipulate the scrollback buffer). This commit moves the margin clipping out of the `ScrollRegion` function, so it can be applied exclusively in the places that need it. With the margin clipping removed from the `ScrollRegion` function, it now had to be applied manually in the places it was actually required. This included: * The `DoSrvPrivateReverseLineFeed` function (for the `RI` control): This was * just a matter of updating the bottom of the scroll rect to the bottom margin * (at least when the margins were actually set), since the top of the scroll * rect would always be the top of the viewport. The * `DoSrvPrivateModifyLinesImpl` function (for the `IL` and `DL` commands): * Again this was just a matter of updating the bottom of the scroll rect, since * the cursor position would always determine the top of the scroll rect. The * `AdaptDispatch::_ScrollMovement` method (for the `SU` and `SD` commands): * This required updating both the top and bottom coordinates of the scroll * rect, and also a simpler destination Y coordinate (the way the `ScrollRegion` * function worked before, the caller was expected to take the margins into * account when determining the destination). On the plus side, there was now no longer a need to override the margins when calling `ScrollRegion` in the `AdjustCursorPosition` function. In the first case, the margins had needed to be cleared (_stream.cpp 143-145), but that is now the default behaviour. In the second case, there had been a more complicated adjustment of the margins (_stream.cpp 196-209), but that code was never actually used so could be removed completely (to get to that point either _fScrollUp_ was true, so _scrollDownAtTop_ couldn't also be true, or _fScrollDown_ was true, but in that case there is a check to make sure _scrollDownAtTop_ is false). While testing, I also noticed that one of the `ScrollRegion` calls in the `AdjustCursorPosition` function was not setting the horizontal range correctly - the scrolling should always affect the full buffer width rather than just the viewport width - so I've fixed that now as well. ## Validation Steps Performed For commands like `RI`, `IL`, `DL`, etc. where we've changed the implementation but not the behaviour, there were already unit tests that could confirm that the new implementation was still producing the correct results. Where there has been a change in behaviour - namely for the `ICH` and `DCH` commands, and the `ScrollConsoleScreenBuffer` API - I've extended the existing unit tests to check that they still function correctly even when the `DECSTBM` margins are set (which would previously have caused them to fail). I've also tested manually with the test cases in issues #2543 and #2659, and confirmed that they now work as expected. Closes #2543 Closes #2659
2019-09-24 01:16:54 +02:00
bool setMargins;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"setMargins", setMargins), L"Get whether or not we should set the DECSTBM margins.");
bool checkClipped;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"checkClipped", checkClipped), L"Get whether or not we should check all the options using a clipping rectangle.");
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer();
VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional({ 5, 5 }), L"Make the buffer small so this doesn't take forever.");
Remove unwanted DECSTBM clipping (#2764) The `DECSTBM` margins are meant to define the range of lines within which certain vertical scrolling operations take place. However, we were applying these margin restrictions in the `ScrollRegion` function, which is also used in a number of places that shouldn't be affected by `DECSTBM`. This includes the `ICH` and `DCH` escape sequences (which are only affected by the horizontal margins, which we don't yet support), the `ScrollConsoleScreenBuffer` API (which is public Console API, not meant to be affected by the VT terminal emulation), and the `CSI 3 J` erase scrollback extension (which isn't really scrolling as such, but uses the `ScrollRegion` function to manipulate the scrollback buffer). This commit moves the margin clipping out of the `ScrollRegion` function, so it can be applied exclusively in the places that need it. With the margin clipping removed from the `ScrollRegion` function, it now had to be applied manually in the places it was actually required. This included: * The `DoSrvPrivateReverseLineFeed` function (for the `RI` control): This was * just a matter of updating the bottom of the scroll rect to the bottom margin * (at least when the margins were actually set), since the top of the scroll * rect would always be the top of the viewport. The * `DoSrvPrivateModifyLinesImpl` function (for the `IL` and `DL` commands): * Again this was just a matter of updating the bottom of the scroll rect, since * the cursor position would always determine the top of the scroll rect. The * `AdaptDispatch::_ScrollMovement` method (for the `SU` and `SD` commands): * This required updating both the top and bottom coordinates of the scroll * rect, and also a simpler destination Y coordinate (the way the `ScrollRegion` * function worked before, the caller was expected to take the margins into * account when determining the destination). On the plus side, there was now no longer a need to override the margins when calling `ScrollRegion` in the `AdjustCursorPosition` function. In the first case, the margins had needed to be cleared (_stream.cpp 143-145), but that is now the default behaviour. In the second case, there had been a more complicated adjustment of the margins (_stream.cpp 196-209), but that code was never actually used so could be removed completely (to get to that point either _fScrollUp_ was true, so _scrollDownAtTop_ couldn't also be true, or _fScrollDown_ was true, but in that case there is a check to make sure _scrollDownAtTop_ is false). While testing, I also noticed that one of the `ScrollRegion` calls in the `AdjustCursorPosition` function was not setting the horizontal range correctly - the scrolling should always affect the full buffer width rather than just the viewport width - so I've fixed that now as well. ## Validation Steps Performed For commands like `RI`, `IL`, `DL`, etc. where we've changed the implementation but not the behaviour, there were already unit tests that could confirm that the new implementation was still producing the correct results. Where there has been a change in behaviour - namely for the `ICH` and `DCH` commands, and the `ScrollConsoleScreenBuffer` API - I've extended the existing unit tests to check that they still function correctly even when the `DECSTBM` margins are set (which would previously have caused them to fail). I've also tested manually with the test cases in issues #2543 and #2659, and confirmed that they now work as expected. Closes #2543 Closes #2659
2019-09-24 01:16:54 +02:00
// Tests are run both with and without the DECSTBM margins set. This should not alter
// the results, since ScrollConsoleScreenBuffer should not be affected by VT margins.
auto& stateMachine = si.GetStateMachine();
stateMachine.ProcessString(setMargins ? L"\x1b[2;4r" : L"\x1b[r");
// Make sure we clear the margins on exit so they can't break other tests.
auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); });
gci.LockConsole();
auto Unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
CHAR_INFO fill;
fill.Char.UnicodeChar = L'A';
fill.Attributes = FOREGROUND_RED;
// By default, we're going to use a nullopt clip rectangle.
// If this instance of the test is checking clipping, we'll assign a clip value
// prior to each call variation.
std::optional<SMALL_RECT> clipRectangle = std::nullopt;
std::optional<Viewport> clipViewport = std::nullopt;
const auto bufferSize = si.GetBufferSize();
SMALL_RECT scroll = bufferSize.ToInclusive();
COORD destination{ 0, -2 }; // scroll up.
Log::Comment(L"Fill screen with green Zs. Scroll all up by two, backfilling with red As. Confirm every cell.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
CHAR_INFO background;
background.Char.UnicodeChar = L'Z';
background.Attributes = FOREGROUND_GREEN;
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
if (checkClipped)
{
// for scrolling up and down, we're going to clip to only modify the left half of the buffer
COORD clipRectDimensions = bufferSize.Dimensions();
clipRectDimensions.X /= 2;
clipViewport = Viewport::FromDimensions({ 0, 0 }, clipRectDimensions);
clipRectangle = clipViewport.value().ToInclusive();
}
// Scroll everything up and backfill with red As.
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
ValidateScreen(si, background, fill, destination, clipViewport);
Log::Comment(L"Fill screen with green Zs. Scroll all down by two, backfilling with red As. Confirm every cell.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Scroll everything down and backfill with red As.
destination = { 0, 2 };
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
ValidateScreen(si, background, fill, destination, clipViewport);
if (checkClipped)
{
// for scrolling left and right, we're going to clip to only modify the top half of the buffer
COORD clipRectDimensions = bufferSize.Dimensions();
clipRectDimensions.Y /= 2;
clipViewport = Viewport::FromDimensions({ 0, 0 }, clipRectDimensions);
clipRectangle = clipViewport.value().ToInclusive();
}
Log::Comment(L"Fill screen with green Zs. Scroll all left by two, backfilling with red As. Confirm every cell.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Scroll everything left and backfill with red As.
destination = { -2, 0 };
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
ValidateScreen(si, background, fill, destination, clipViewport);
Log::Comment(L"Fill screen with green Zs. Scroll all right by two, backfilling with red As. Confirm every cell.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Scroll everything right and backfill with red As.
destination = { 2, 0 };
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
ValidateScreen(si, background, fill, destination, clipViewport);
Log::Comment(L"Fill screen with green Zs. Move everything down and right by two, backfilling with red As. Confirm every cell.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Scroll everything down and right and backfill with red As.
destination = { 2, 2 };
if (checkClipped)
{
// Clip out the left most and top most column.
clipViewport = Viewport::FromDimensions({ 1, 1 }, { 4, 4 });
clipRectangle = clipViewport.value().ToInclusive();
}
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
ValidateScreen(si, background, fill, destination, clipViewport);
Log::Comment(L"Fill screen with green Zs. Move everything up and left by two, backfilling with red As. Confirm every cell.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Scroll everything up and left and backfill with red As.
destination = { -2, -2 };
if (checkClipped)
{
// Clip out the bottom most and right most column
clipViewport = Viewport::FromDimensions({ 0, 0 }, { 4, 4 });
clipRectangle = clipViewport.value().ToInclusive();
}
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
ValidateScreen(si, background, fill, destination, clipViewport);
Log::Comment(L"Scroll everything completely off the screen.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Scroll everything way off the screen.
destination = { 0, -10 };
if (checkClipped)
{
// for scrolling up and down, we're going to clip to only modify the left half of the buffer
COORD clipRectDimensions = bufferSize.Dimensions();
clipRectDimensions.X /= 2;
clipViewport = Viewport::FromDimensions({ 0, 0 }, clipRectDimensions);
clipRectangle = clipViewport.value().ToInclusive();
}
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
ValidateScreen(si, background, fill, destination, clipViewport);
Log::Comment(L"Scroll everything completely off the screen but use a null fill and confirm it is replaced with default attribute spaces.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Scroll everything way off the screen.
destination = { -10, -10 };
CHAR_INFO nullFill = { 0 };
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, nullFill.Char.UnicodeChar, nullFill.Attributes));
CHAR_INFO fillExpected;
fillExpected.Char.UnicodeChar = UNICODE_SPACE;
fillExpected.Attributes = si.GetAttributes().GetLegacyAttributes();
ValidateScreen(si, background, fillExpected, destination, clipViewport);
if (checkClipped)
{
// If we're doing clipping here, we're going to clip the scrolled area (after Bs are filled onto field of Zs)
// to only the 3rd and 4th columns of the pattern.
clipViewport = Viewport::FromDimensions({ 2, 0 }, { 2, 5 });
clipRectangle = clipViewport.value().ToInclusive();
}
Log::Comment(L"Scroll a small portion of the screen in an overlapping fashion.");
scroll.Top = 1;
scroll.Bottom = 2;
scroll.Left = 1;
scroll.Right = 2;
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Screen now looks like:
// ZZZZZ
// ZZZZZ
// ZZZZZ
// ZZZZZ
// ZZZZZ
// Fill the scroll rectangle with Blue Bs.
CHAR_INFO scrollRect;
scrollRect.Char.UnicodeChar = L'B';
scrollRect.Attributes = FOREGROUND_BLUE;
si.GetActiveBuffer().WriteRect(OutputCellIterator(scrollRect), Viewport::FromInclusive(scroll));
// Screen now looks like:
// ZZZZZ
// ZBBZZ
// ZBBZZ
// ZZZZZ
// ZZZZZ
// We're going to move our little embedded rectangle of Blue Bs inside the field of Green Zs down and to the right just one.
destination = { scroll.Left + 1, scroll.Top + 1 };
// Move rectangle and backfill with red As.
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
// Screen should now look like either:
// (with no clip rectangle):
// ZZZZZ
// ZAAZZ
// ZABBZ
// ZZBBZ
// ZZZZZ
// or with clip rectangle (of 3rd and 4th columns only, defined above)
// ZZZZZ
// ZBAZZ
// ZBBBZ
// ZZBBZ
// ZZZZZ
ValidateComplexScreen(si, background, fill, scrollRect, Viewport::FromInclusive(scroll), destination, clipViewport);
Log::Comment(L"Scroll a small portion of the screen in a non-overlapping fashion.");
si.GetActiveBuffer().ClearTextData(); // Clean out screen
si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs.
// Screen now looks like:
// ZZZZZ
// ZZZZZ
// ZZZZZ
// ZZZZZ
// ZZZZZ
// Fill the scroll rectangle with Blue Bs.
si.GetActiveBuffer().WriteRect(OutputCellIterator(scrollRect), Viewport::FromInclusive(scroll));
// Screen now looks like:
// ZZZZZ
// ZBBZZ
// ZBBZZ
// ZZZZZ
// ZZZZZ
// We're going to move our little embedded rectangle of Blue Bs inside the field of Green Zs down and to the right by two.
destination = { scroll.Left + 2, scroll.Top + 2 };
// Move rectangle and backfill with red As.
VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes));
// Screen should now look like either:
// (with no clip rectangle):
// ZZZZZ
// ZAAZZ
// ZAAZZ
// ZZZBB
// ZZZBB
// or with clip rectangle (of 3rd and 4th columns only, defined above)
// ZZZZZ
// ZBAZZ
// ZBAZZ
// ZZZBZ
// ZZZBZ
ValidateComplexScreen(si, background, fill, scrollRect, Viewport::FromInclusive(scroll), destination, clipViewport);
}
};