diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 9bfac7f1463..b1d30c45999 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -938,6 +938,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation if (point.Properties().IsLeftButtonPressed()) { + auto lock = _terminal->LockForWriting(); + const auto cursorPosition = point.Position(); const auto terminalPosition = _GetTerminalPosition(cursorPosition); @@ -979,6 +981,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation _lastMouseClickTimestamp = point.Timestamp(); _lastMouseClickPos = cursorPosition; } + _renderer->TriggerSelection(); } else if (point.Properties().IsRightButtonPressed()) @@ -1037,6 +1040,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation if (point.Properties().IsLeftButtonPressed()) { + auto lock = _terminal->LockForWriting(); + const auto cursorPosition = point.Position(); if (_singleClickTouchdownPos) diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 457fc2c2d98..2da24a11d82 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -780,7 +780,11 @@ void Terminal::_AdjustCursorPosition(const COORD proposedPosition) if (notifyScroll) { - _buffer->GetRenderTarget().TriggerRedrawAll(); + // We have to report the delta here because we might have circled the text buffer. + // That didn't change the viewport and therefore the TriggerScroll(void) + // method can't detect the delta on its own. + COORD delta{ 0, -gsl::narrow(newRows) }; + _buffer->GetRenderTarget().TriggerScroll(&delta); _NotifyScrollEvent(); } @@ -789,13 +793,20 @@ void Terminal::_AdjustCursorPosition(const COORD proposedPosition) void Terminal::UserScrollViewport(const int viewTop) { + // we're going to modify state here that the renderer could be reading. + auto lock = LockForWriting(); + const auto clampedNewTop = std::max(0, viewTop); const auto realTop = ViewStartIndex(); const auto newDelta = realTop - clampedNewTop; // if viewTop > realTop, we want the offset to be 0. _scrollOffset = std::max(0, newDelta); - _buffer->GetRenderTarget().TriggerRedrawAll(); + + // We can use the void variant of TriggerScroll here because + // we adjusted the viewport so it can detect the difference + // from the previous frame drawn. + _buffer->GetRenderTarget().TriggerScroll(); } int Terminal::GetScrollOffset() noexcept diff --git a/src/inc/til.h b/src/inc/til.h index e83d2cffe4f..bbafefe0cd7 100644 --- a/src/inc/til.h +++ b/src/inc/til.h @@ -11,8 +11,8 @@ #include "til/some.h" #include "til/size.h" #include "til/point.h" -#include "til/rectangle.h" #include "til/operators.h" +#include "til/rectangle.h" #include "til/bitmap.h" #include "til/u8u16convert.h" diff --git a/src/inc/til/bitmap.h b/src/inc/til/bitmap.h index ca589f42fed..acafbaf1560 100644 --- a/src/inc/til/bitmap.h +++ b/src/inc/til/bitmap.h @@ -273,7 +273,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" THROW_HR_IF(E_INVALIDARG, !_rc.contains(pt)); _runs.reset(); // reset cached runs on any non-const method - til::at(_bits, _rc.index_of(pt)) = true; + _bits.set(_rc.index_of(pt)); _dirty |= til::rectangle{ pt }; } @@ -283,9 +283,9 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" THROW_HR_IF(E_INVALIDARG, !_rc.contains(rc)); _runs.reset(); // reset cached runs on any non-const method - for (const auto pt : rc) + for (auto row = rc.top(); row < rc.bottom(); ++row) { - til::at(_bits, _rc.index_of(pt)) = true; + _bits.set(_rc.index_of(til::point{ rc.left(), row }), rc.width(), true); } _dirty |= rc; @@ -378,6 +378,11 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return _dirty == _rc; } + constexpr til::size size() const noexcept + { + return _sz; + } + std::wstring to_string() const { std::wstringstream wss; diff --git a/src/inc/til/operators.h b/src/inc/til/operators.h index 109cd3e3576..7e18e33498a 100644 --- a/src/inc/til/operators.h +++ b/src/inc/til/operators.h @@ -3,10 +3,6 @@ #pragma once -#include "rectangle.h" -#include "size.h" -#include "bitmap.h" - namespace til // Terminal Implementation Library. Also: "Today I Learned" { // Operators go here when they involve two headers that can't/don't include each other. diff --git a/src/inc/til/point.h b/src/inc/til/point.h index 57be2448c8f..fa9a869a6e9 100644 --- a/src/inc/til/point.h +++ b/src/inc/til/point.h @@ -163,6 +163,19 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return *this; } + template + point scale(TilMath, const float scale) const + { + struct + { + float x, y; + } pt; + THROW_HR_IF(E_ABORT, !base::CheckMul(scale, _x).AssignIfValid(&pt.x)); + THROW_HR_IF(E_ABORT, !base::CheckMul(scale, _y).AssignIfValid(&pt.y)); + + return til::point(TilMath(), pt); + } + point operator/(const point& other) const { ptrdiff_t x; diff --git a/src/inc/til/rectangle.h b/src/inc/til/rectangle.h index 19f684f589f..ba488ea7ea9 100644 --- a/src/inc/til/rectangle.h +++ b/src/inc/til/rectangle.h @@ -3,10 +3,6 @@ #pragma once -#include "point.h" -#include "size.h" -#include "some.h" - #ifdef UNIT_TESTING class RectangleTests; #endif @@ -179,6 +175,22 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { } + // This template will convert to rectangle from anything that has a Left, Top, Right, and Bottom field that are floating-point; + // a math type is required. + template + constexpr rectangle(TilMath, const TOther& other, std::enable_if_t().Left)> && std::is_floating_point_v().Top)> && std::is_floating_point_v().Right)> && std::is_floating_point_v().Bottom)>, int> /*sentinel*/ = 0) : + rectangle(til::point{ TilMath::template cast(other.Left), TilMath::template cast(other.Top) }, til::point{ TilMath::template cast(other.Right), TilMath::template cast(other.Bottom) }) + { + } + + // This template will convert to rectangle from anything that has a left, top, right, and bottom field that are floating-point; + // a math type is required. + template + constexpr rectangle(TilMath, const TOther& other, std::enable_if_t().left)> && std::is_floating_point_v().top)> && std::is_floating_point_v().right)> && std::is_floating_point_v().bottom)>, int> /*sentinel*/ = 0) : + rectangle(til::point{ TilMath::template cast(other.left), TilMath::template cast(other.top) }, til::point{ TilMath::template cast(other.right), TilMath::template cast(other.bottom) }) + { + } + constexpr bool operator==(const rectangle& other) const noexcept { return _topLeft == other._topLeft && @@ -636,6 +648,38 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return *this; } + // scale_up will scale the entire rectangle up by the size factor + // This includes moving the origin. + rectangle scale_up(const size& size) const + { + const auto topLeft = _topLeft * size; + const auto bottomRight = _bottomRight * size; + return til::rectangle{ topLeft, bottomRight }; + } + + // scale_down will scale the entire rectangle down by the size factor, + // but rounds the bottom-right corner out. + // This includes moving the origin. + rectangle scale_down(const size& size) const + { + auto topLeft = _topLeft; + auto bottomRight = _bottomRight; + topLeft = topLeft / size; + + // Move bottom right point into a size + // Use size specialization of divide_ceil to round up against the size given. + // Add leading addition to point to convert it back into a point. + bottomRight = til::point{} + til::size{ right(), bottom() }.divide_ceil(size); + + return til::rectangle{ topLeft, bottomRight }; + } + + template + rectangle scale(TilMath, const float scale) const + { + return til::rectangle{ _topLeft.scale(TilMath{}, scale), _bottomRight.scale(TilMath{}, scale) }; + } + #pragma endregion constexpr ptrdiff_t top() const noexcept diff --git a/src/inc/til/size.h b/src/inc/til/size.h index 67a41023822..b7d71dbd46b 100644 --- a/src/inc/til/size.h +++ b/src/inc/til/size.h @@ -126,6 +126,19 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return size{ width, height }; } + template + size scale(TilMath, const float scale) const + { + struct + { + float Width, Height; + } sz; + THROW_HR_IF(E_ABORT, !base::CheckMul(scale, _width).AssignIfValid(&sz.Width)); + THROW_HR_IF(E_ABORT, !base::CheckMul(scale, _height).AssignIfValid(&sz.Height)); + + return til::size(TilMath(), sz); + } + size operator/(const size& other) const { ptrdiff_t width; diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index dfe4bdd3a6d..812c966dfbd 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -303,6 +303,22 @@ void Renderer::TriggerSelection() // Get selection rectangles const auto rects = _GetSelectionRects(); + // Restrict all previous selection rectangles to inside the current viewport bounds + for (auto& sr : _previousSelection) + { + // Make the exclusive SMALL_RECT into a til::rectangle. + til::rectangle rc{ Viewport::FromExclusive(sr).ToInclusive() }; + + // Make a viewport representing the coordinates that are currently presentable. + const til::rectangle viewport{ til::size{ _pData->GetViewport().Dimensions() } }; + + // Intersect them so we only invalidate things that are still visible. + rc &= viewport; + + // Convert back into the exclusive SMALL_RECT and store in the vector. + sr = Viewport::FromInclusive(rc).ToExclusive(); + } + std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) { LOG_IF_FAILED(pEngine->InvalidateSelection(_previousSelection)); LOG_IF_FAILED(pEngine->InvalidateSelection(rects)); @@ -330,13 +346,26 @@ bool Renderer::_CheckViewportAndScroll() coordDelta.X = srOldViewport.Left - srNewViewport.Left; coordDelta.Y = srOldViewport.Top - srNewViewport.Top; - std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) { - LOG_IF_FAILED(pEngine->UpdateViewport(srNewViewport)); - LOG_IF_FAILED(pEngine->InvalidateScroll(&coordDelta)); - }); + for (auto engine : _rgpEngines) + { + LOG_IF_FAILED(engine->UpdateViewport(srNewViewport)); + } + _srViewportPrevious = srNewViewport; - return coordDelta.X != 0 || coordDelta.Y != 0; + if (coordDelta.X != 0 || coordDelta.Y != 0) + { + for (auto engine : _rgpEngines) + { + LOG_IF_FAILED(engine->InvalidateScroll(&coordDelta)); + } + + _ScrollPreviousSelection(coordDelta); + + return true; + } + + return false; } // Routine Description: @@ -369,6 +398,8 @@ void Renderer::TriggerScroll(const COORD* const pcoordDelta) LOG_IF_FAILED(pEngine->InvalidateScroll(pcoordDelta)); }); + _ScrollPreviousSelection(*pcoordDelta); + _NotifyPaintFrame(); } @@ -927,10 +958,13 @@ void Renderer::_PaintSelection(_In_ IRenderEngine* const pEngine) { for (auto dirtyRect : dirtyAreas) { + // Make a copy as `TrimToViewport` will manipulate it and + // can destroy it for the next dirtyRect to test against. + auto rectCopy = rect; Viewport dirtyView = Viewport::FromInclusive(dirtyRect); - if (dirtyView.TrimToViewport(&rect)) + if (dirtyView.TrimToViewport(&rectCopy)) { - LOG_IF_FAILED(pEngine->PaintSelection(rect)); + LOG_IF_FAILED(pEngine->PaintSelection(rectCopy)); } } } @@ -1002,6 +1036,33 @@ std::vector Renderer::_GetSelectionRects() const return result; } +// Method Description: +// - Offsets all of the selection rectangles we might be holding onto +// as the previously selected area. If the whole viewport scrolls, +// we need to scroll these areas also to ensure they're invalidated +// properly when the selection further changes. +// Arguments: +// - delta - The scroll delta +// Return Value: +// - - Updates internal state instead. +void Renderer::_ScrollPreviousSelection(const til::point delta) +{ + if (delta != til::point{ 0, 0 }) + { + for (auto& sr : _previousSelection) + { + // Get a rectangle representing this piece of the selection. + til::rectangle rc = Viewport::FromExclusive(sr).ToInclusive(); + + // Offset the entire existing rectangle by the delta. + rc += delta; + + // Store it back into the vector. + sr = Viewport::FromInclusive(rc).ToExclusive(); + } + } +} + // Method Description: // - Adds another Render engine to this renderer. Future rendering calls will // also be sent to the new renderer. diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index 3e02cc7aea1..594763edd19 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -119,6 +119,7 @@ namespace Microsoft::Console::Render SMALL_RECT _srViewportPrevious; std::vector _GetSelectionRects() const; + void _ScrollPreviousSelection(const til::point delta); std::vector _previousSelection; [[nodiscard]] HRESULT _PaintTitle(IRenderEngine* const pEngine); diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index 77c5bffb755..99a420246f1 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -65,9 +65,10 @@ using namespace Microsoft::Console::Types; // TODO GH 2683: The default constructor should not throw. DxEngine::DxEngine() : RenderEngineBase(), - _isInvalidUsed{ false }, - _invalidRect{ 0 }, - _invalidScroll{ 0 }, + _invalidateFullRows{ true }, + _invalidMap{}, + _invalidScroll{}, + _firstFrame{ true }, _presentParams{ 0 }, _presentReady{ false }, _presentScroll{ 0 }, @@ -75,16 +76,16 @@ DxEngine::DxEngine() : _presentOffset{ 0 }, _isEnabled{ false }, _isPainting{ false }, - _displaySizePixels{ 0 }, + _displaySizePixels{}, _foregroundColor{ 0 }, _backgroundColor{ 0 }, _selectionBackground{}, - _glyphCell{ 0 }, + _glyphCell{}, _haveDeviceResources{ false }, _retroTerminalEffects{ false }, _antialiasingMode{ D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE }, _hwndTarget{ static_cast(INVALID_HANDLE_VALUE) }, - _sizeTarget{ 0 }, + _sizeTarget{}, _dpi{ USER_DEFAULT_SCREEN_DPI }, _scale{ 1.0f }, _chainMode{ SwapChainMode::ForComposition }, @@ -238,8 +239,8 @@ HRESULT DxEngine::_SetupTerminalEffects() // Setup the viewport. D3D11_VIEWPORT vp; - vp.Width = static_cast(_displaySizePixels.cx); - vp.Height = static_cast(_displaySizePixels.cy); + vp.Width = _displaySizePixels.width(); + vp.Height = _displaySizePixels.height(); vp.MinDepth = 0.0f; vp.MaxDepth = 1.0f; vp.TopLeftX = 0; @@ -349,6 +350,7 @@ void DxEngine::_ComputePixelShaderSettings() noexcept // Return Value: // - Could be any DirectX/D3D/D2D/DXGI/DWrite error or memory issue. [[nodiscard]] HRESULT DxEngine::_CreateDeviceResources(const bool createSwapChain) noexcept +try { if (_haveDeviceResources) { @@ -420,76 +422,75 @@ void DxEngine::_ComputePixelShaderSettings() noexcept SwapChainDesc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED; SwapChainDesc.Scaling = DXGI_SCALING_NONE; - try + switch (_chainMode) { - switch (_chainMode) - { - case SwapChainMode::ForHwnd: - { - // use the HWND's dimensions for the swap chain dimensions. - RECT rect = { 0 }; - RETURN_IF_WIN32_BOOL_FALSE(GetClientRect(_hwndTarget, &rect)); - - SwapChainDesc.Width = rect.right - rect.left; - SwapChainDesc.Height = rect.bottom - rect.top; - - // We can't do alpha for HWNDs. Set to ignore. It will fail otherwise. - SwapChainDesc.AlphaMode = DXGI_ALPHA_MODE_IGNORE; - const auto createSwapChainResult = _dxgiFactory2->CreateSwapChainForHwnd(_d3dDevice.Get(), - _hwndTarget, - &SwapChainDesc, - nullptr, - nullptr, - &_dxgiSwapChain); - if (FAILED(createSwapChainResult)) - { - SwapChainDesc.Scaling = DXGI_SCALING_STRETCH; - RETURN_IF_FAILED(_dxgiFactory2->CreateSwapChainForHwnd(_d3dDevice.Get(), - _hwndTarget, - &SwapChainDesc, - nullptr, - nullptr, - &_dxgiSwapChain)); - } - - break; - } - case SwapChainMode::ForComposition: + case SwapChainMode::ForHwnd: + { + // use the HWND's dimensions for the swap chain dimensions. + RECT rect = { 0 }; + RETURN_IF_WIN32_BOOL_FALSE(GetClientRect(_hwndTarget, &rect)); + + SwapChainDesc.Width = rect.right - rect.left; + SwapChainDesc.Height = rect.bottom - rect.top; + + // We can't do alpha for HWNDs. Set to ignore. It will fail otherwise. + SwapChainDesc.AlphaMode = DXGI_ALPHA_MODE_IGNORE; + const auto createSwapChainResult = _dxgiFactory2->CreateSwapChainForHwnd(_d3dDevice.Get(), + _hwndTarget, + &SwapChainDesc, + nullptr, + nullptr, + &_dxgiSwapChain); + if (FAILED(createSwapChainResult)) { - // Use the given target size for compositions. - SwapChainDesc.Width = _displaySizePixels.cx; - SwapChainDesc.Height = _displaySizePixels.cy; - - // We're doing advanced composition pretty much for the purpose of pretty alpha, so turn it on. - SwapChainDesc.AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED; - // It's 100% required to use scaling mode stretch for composition. There is no other choice. SwapChainDesc.Scaling = DXGI_SCALING_STRETCH; - - RETURN_IF_FAILED(_dxgiFactory2->CreateSwapChainForComposition(_d3dDevice.Get(), - &SwapChainDesc, - nullptr, - &_dxgiSwapChain)); - break; - } - default: - THROW_HR(E_NOTIMPL); + RETURN_IF_FAILED(_dxgiFactory2->CreateSwapChainForHwnd(_d3dDevice.Get(), + _hwndTarget, + &SwapChainDesc, + nullptr, + nullptr, + &_dxgiSwapChain)); } - if (_retroTerminalEffects) + break; + } + case SwapChainMode::ForComposition: + { + // Use the given target size for compositions. + SwapChainDesc.Width = _displaySizePixels.width(); + SwapChainDesc.Height = _displaySizePixels.height(); + + // We're doing advanced composition pretty much for the purpose of pretty alpha, so turn it on. + SwapChainDesc.AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED; + // It's 100% required to use scaling mode stretch for composition. There is no other choice. + SwapChainDesc.Scaling = DXGI_SCALING_STRETCH; + + RETURN_IF_FAILED(_dxgiFactory2->CreateSwapChainForComposition(_d3dDevice.Get(), + &SwapChainDesc, + nullptr, + &_dxgiSwapChain)); + break; + } + default: + THROW_HR(E_NOTIMPL); + } + + if (_retroTerminalEffects) + { + const HRESULT hr = _SetupTerminalEffects(); + if (FAILED(hr)) { - const HRESULT hr = _SetupTerminalEffects(); - if (FAILED(hr)) - { - _retroTerminalEffects = false; - LOG_HR_MSG(hr, "Failed to setup terminal effects. Disabling."); - } + _retroTerminalEffects = false; + LOG_HR_MSG(hr, "Failed to setup terminal effects. Disabling."); } } - CATCH_RETURN(); // With a new swap chain, mark the entire thing as invalid. RETURN_IF_FAILED(InvalidateAll()); + // This is our first frame on this new target. + _firstFrame = true; + RETURN_IF_FAILED(_PrepareRenderTarget()); } @@ -515,6 +516,7 @@ void DxEngine::_ComputePixelShaderSettings() noexcept return S_OK; } +CATCH_RETURN(); [[nodiscard]] HRESULT DxEngine::_PrepareRenderTarget() noexcept { @@ -569,7 +571,6 @@ void DxEngine::_ComputePixelShaderSettings() noexcept ::Microsoft::WRL::ComPtr sc2; RETURN_IF_FAILED(_dxgiSwapChain.As(&sc2)); - RETURN_IF_FAILED(sc2->SetMatrixTransform(&inverseScale)); } return S_OK; @@ -629,14 +630,16 @@ void DxEngine::_ReleaseDeviceResources() noexcept _In_reads_(stringLength) PCWCHAR string, _In_ size_t stringLength, _Out_ IDWriteTextLayout** ppTextLayout) noexcept +try { return _dwriteFactory->CreateTextLayout(string, gsl::narrow(stringLength), _dwriteTextFormat.Get(), - gsl::narrow(_displaySizePixels.cx), - _glyphCell.cy != 0 ? _glyphCell.cy : gsl::narrow(_displaySizePixels.cy), + _displaySizePixels.width(), + _glyphCell.height() != 0 ? _glyphCell.height() : _displaySizePixels.height(), ppTextLayout); } +CATCH_RETURN() // Routine Description: // - Sets the target window handle for our display pipeline @@ -653,13 +656,13 @@ void DxEngine::_ReleaseDeviceResources() noexcept } [[nodiscard]] HRESULT DxEngine::SetWindowSize(const SIZE Pixels) noexcept +try { _sizeTarget = Pixels; - - RETURN_IF_FAILED(InvalidateAll()); - + _invalidMap.resize(_sizeTarget / _glyphCell, true); return S_OK; } +CATCH_RETURN(); void DxEngine::SetCallback(std::function pfn) { @@ -681,6 +684,18 @@ Microsoft::WRL::ComPtr DxEngine::GetSwapChain() return _dxgiSwapChain; } +void DxEngine::_InvalidateRectangle(const til::rectangle& rc) +{ + auto invalidate = rc; + + if (_invalidateFullRows) + { + invalidate = til::rectangle{ til::point{ static_cast(0), rc.top() }, til::size{ _invalidMap.size().width(), rc.height() } }; + } + + _invalidMap.set(invalidate); +} + // Routine Description: // - Invalidates a rectangle described in characters // Arguments: @@ -688,12 +703,15 @@ Microsoft::WRL::ComPtr DxEngine::GetSwapChain() // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::Invalidate(const SMALL_RECT* const psrRegion) noexcept +try { RETURN_HR_IF_NULL(E_INVALIDARG, psrRegion); - _InvalidOr(*psrRegion); + _InvalidateRectangle(Viewport::FromExclusive(*psrRegion).ToInclusive()); + return S_OK; } +CATCH_RETURN() // Routine Description: // - Invalidates one specific character coordinate @@ -702,12 +720,15 @@ Microsoft::WRL::ComPtr DxEngine::GetSwapChain() // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::InvalidateCursor(const COORD* const pcoordCursor) noexcept +try { RETURN_HR_IF_NULL(E_INVALIDARG, pcoordCursor); - const SMALL_RECT sr = Microsoft::Console::Types::Viewport::FromCoord(*pcoordCursor).ToInclusive(); - return Invalidate(&sr); + _InvalidateRectangle(til::rectangle{ *pcoordCursor, til::size{ 1, 1 } }); + + return S_OK; } +CATCH_RETURN() // Routine Description: // - Invalidates a rectangle describing a pixel area on the display @@ -716,13 +737,17 @@ Microsoft::WRL::ComPtr DxEngine::GetSwapChain() // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::InvalidateSystem(const RECT* const prcDirtyClient) noexcept +try { RETURN_HR_IF_NULL(E_INVALIDARG, prcDirtyClient); - _InvalidOr(*prcDirtyClient); + // Dirty client is in pixels. Use divide specialization against glyph factor to make conversion + // to cells. + _InvalidateRectangle(til::rectangle{ *prcDirtyClient }.scale_down(_glyphCell)); return S_OK; } +CATCH_RETURN(); // Routine Description: // - Invalidates a series of character rectangles @@ -748,50 +773,22 @@ Microsoft::WRL::ComPtr DxEngine::GetSwapChain() // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::InvalidateScroll(const COORD* const pcoordDelta) noexcept +try { - if (pcoordDelta->X != 0 || pcoordDelta->Y != 0) - { - try - { - POINT delta = { 0 }; - delta.x = pcoordDelta->X * _glyphCell.cx; - delta.y = pcoordDelta->Y * _glyphCell.cy; - - _InvalidOffset(delta); - - _invalidScroll.cx += delta.x; - _invalidScroll.cy += delta.y; - - // Add the revealed portion of the screen from the scroll to the invalid area. - const RECT display = _GetDisplayRect(); - RECT reveal = display; + RETURN_HR_IF(E_INVALIDARG, !pcoordDelta); - // X delta first - OffsetRect(&reveal, delta.x, 0); - IntersectRect(&reveal, &reveal, &display); - SubtractRect(&reveal, &display, &reveal); - - if (!IsRectEmpty(&reveal)) - { - _InvalidOr(reveal); - } + const til::point deltaCells{ *pcoordDelta }; - // Y delta second (subtract rect won't work if you move both) - reveal = display; - OffsetRect(&reveal, 0, delta.y); - IntersectRect(&reveal, &reveal, &display); - SubtractRect(&reveal, &display, &reveal); - - if (!IsRectEmpty(&reveal)) - { - _InvalidOr(reveal); - } - } - CATCH_RETURN(); + if (deltaCells != til::point{ 0, 0 }) + { + // Shift the contents of the map and fill in revealed area. + _invalidMap.translate(deltaCells, true); + _invalidScroll += deltaCells; } return S_OK; } +CATCH_RETURN(); // Routine Description: // - Invalidates the entire window area @@ -800,12 +797,12 @@ Microsoft::WRL::ComPtr DxEngine::GetSwapChain() // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::InvalidateAll() noexcept +try { - const RECT screen = _GetDisplayRect(); - _InvalidOr(screen); - + _invalidMap.set_all(); return S_OK; } +CATCH_RETURN(); // Routine Description: // - This currently has no effect in this renderer. @@ -827,7 +824,7 @@ Microsoft::WRL::ComPtr DxEngine::GetSwapChain() // - // Return Value: // - X by Y area in pixels of the surface -[[nodiscard]] SIZE DxEngine::_GetClientSize() const noexcept +[[nodiscard]] til::size DxEngine::_GetClientSize() const { switch (_chainMode) { @@ -836,18 +833,11 @@ Microsoft::WRL::ComPtr DxEngine::GetSwapChain() RECT clientRect = { 0 }; LOG_IF_WIN32_BOOL_FALSE(GetClientRect(_hwndTarget, &clientRect)); - SIZE clientSize = { 0 }; - clientSize.cx = clientRect.right - clientRect.left; - clientSize.cy = clientRect.bottom - clientRect.top; - - return clientSize; + return til::rectangle{ clientRect }.size(); } case SwapChainMode::ForComposition: { - SIZE size = _sizeTarget; - size.cx = static_cast(size.cx * _scale); - size.cy = static_cast(size.cy * _scale); - return size; + return _sizeTarget.scale(til::math::ceiling, _scale); } default: FAIL_FAST_HR(E_NOTIMPL); @@ -870,90 +860,6 @@ void _ScaleByFont(RECT& cellsToPixels, SIZE fontSize) noexcept cellsToPixels.bottom *= fontSize.cy; } -// Routine Description: -// - Retrieves a rectangle representation of the pixel size of the -// surface we are drawing on -// Arguments: -// - -// Return Value; -// - Origin-placed rectangle representing the pixel size of the surface -[[nodiscard]] RECT DxEngine::_GetDisplayRect() const noexcept -{ - return { 0, 0, _displaySizePixels.cx, _displaySizePixels.cy }; -} - -// Routine Description: -// - Helper to shift the existing dirty rectangle by a pixel offset -// and crop it to still be within the bounds of the display surface -// Arguments: -// - delta - Adjustment distance in pixels -// - -Y is up, Y is down, -X is left, X is right. -// Return Value: -// - -void DxEngine::_InvalidOffset(POINT delta) -{ - if (_isInvalidUsed) - { - // Copy the existing invalid rect - RECT invalidNew = _invalidRect; - - // Offset it to the new position - THROW_IF_WIN32_BOOL_FALSE(OffsetRect(&invalidNew, delta.x, delta.y)); - - // Get the rect representing the display - const RECT rectScreen = _GetDisplayRect(); - - // Ensure that the new invalid rectangle is still on the display - IntersectRect(&invalidNew, &invalidNew, &rectScreen); - - _invalidRect = invalidNew; - } -} - -// Routine description: -// - Adds the given character rectangle to the total dirty region -// - Will scale internally to pixels based on the current font. -// Arguments: -// - sr - character rectangle -// Return Value: -// - -void DxEngine::_InvalidOr(SMALL_RECT sr) noexcept -{ - RECT region; - region.left = sr.Left; - region.top = sr.Top; - region.right = sr.Right; - region.bottom = sr.Bottom; - _ScaleByFont(region, _glyphCell); - - region.right += _glyphCell.cx; - region.bottom += _glyphCell.cy; - - _InvalidOr(region); -} - -// Routine Description: -// - Adds the given pixel rectangle to the total dirty region -// Arguments: -// - rc - Dirty pixel rectangle -// Return Value: -// - -void DxEngine::_InvalidOr(RECT rc) noexcept -{ - if (_isInvalidUsed) - { - UnionRect(&_invalidRect, &_invalidRect, &rc); - - const RECT rcScreen = _GetDisplayRect(); - IntersectRect(&_invalidRect, &_invalidRect, &rcScreen); - } - else - { - _invalidRect = rc; - _isInvalidUsed = true; - } -} - // Routine Description: // - This is unused by this renderer. // Arguments: @@ -975,67 +881,78 @@ void DxEngine::_InvalidOr(RECT rc) noexcept // Return Value: // - Any DirectX error, a memory error, etc. [[nodiscard]] HRESULT DxEngine::StartPaint() noexcept +try { - FAIL_FAST_IF_FAILED(InvalidateAll()); RETURN_HR_IF(E_NOT_VALID_STATE, _isPainting); // invalid to start a paint while painting. + // If retro terminal effects are on, we must invalidate everything for them to draw correctly. + // Yes, this will further impact the performance of retro terminal effects. + // But we're talking about running the entire display pipeline through a shader for + // cosmetic effect, so performance isn't likely the top concern with this feature. + if (_retroTerminalEffects) + { + _invalidMap.set_all(); + } + + // If we're doing High DPI, we must invalidate everything for it to draw correctly. + // TODO: GH: 5320 - Remove implicit DPI scaling in D2D target to enable pixel perfect High DPI + if (_scale != 1.0f) + { + _invalidMap.set_all(); + } + + if (TraceLoggingProviderEnabled(g_hDxRenderProvider, WINEVENT_LEVEL_VERBOSE, 0)) + { + const auto invalidatedStr = _invalidMap.to_string(); + const auto invalidated = invalidatedStr.c_str(); + #pragma warning(suppress : 26477 26485 26494 26482 26446 26447) // We don't control TraceLoggingWrite - TraceLoggingWrite(g_hDxRenderProvider, - "Invalid", - TraceLoggingInt32(_invalidRect.bottom - _invalidRect.top, "InvalidHeight"), - TraceLoggingInt32((_invalidRect.bottom - _invalidRect.top) / _glyphCell.cy, "InvalidHeightChars"), - TraceLoggingInt32(_invalidRect.right - _invalidRect.left, "InvalidWidth"), - TraceLoggingInt32((_invalidRect.right - _invalidRect.left) / _glyphCell.cx, "InvalidWidthChars"), - TraceLoggingInt32(_invalidRect.left, "InvalidX"), - TraceLoggingInt32(_invalidRect.left / _glyphCell.cx, "InvalidXChars"), - TraceLoggingInt32(_invalidRect.top, "InvalidY"), - TraceLoggingInt32(_invalidRect.top / _glyphCell.cy, "InvalidYChars"), - TraceLoggingInt32(_invalidScroll.cx, "ScrollWidth"), - TraceLoggingInt32(_invalidScroll.cx / _glyphCell.cx, "ScrollWidthChars"), - TraceLoggingInt32(_invalidScroll.cy, "ScrollHeight"), - TraceLoggingInt32(_invalidScroll.cy / _glyphCell.cy, "ScrollHeightChars")); + TraceLoggingWrite(g_hDxRenderProvider, + "Invalid", + TraceLoggingWideString(invalidated), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + } if (_isEnabled) { - try + const auto clientSize = _GetClientSize(); + if (!_haveDeviceResources) { - const auto clientSize = _GetClientSize(); - if (!_haveDeviceResources) - { - RETURN_IF_FAILED(_CreateDeviceResources(true)); - } - else if (_displaySizePixels.cy != clientSize.cy || - _displaySizePixels.cx != clientSize.cx) - { - // OK, we're going to play a dangerous game here for the sake of optimizing resize - // First, set up a complete clear of all device resources if something goes terribly wrong. - auto resetDeviceResourcesOnFailure = wil::scope_exit([&]() noexcept { - _ReleaseDeviceResources(); - }); + RETURN_IF_FAILED(_CreateDeviceResources(true)); + } + else if (_displaySizePixels != clientSize) + { + // OK, we're going to play a dangerous game here for the sake of optimizing resize + // First, set up a complete clear of all device resources if something goes terribly wrong. + auto resetDeviceResourcesOnFailure = wil::scope_exit([&]() noexcept { + _ReleaseDeviceResources(); + }); - // Now let go of a few of the device resources that get in the way of resizing buffers in the swap chain - _dxgiSurface.Reset(); - _d2dRenderTarget.Reset(); + // Now let go of a few of the device resources that get in the way of resizing buffers in the swap chain + _dxgiSurface.Reset(); + _d2dRenderTarget.Reset(); - // Change the buffer size and recreate the render target (and surface) - RETURN_IF_FAILED(_dxgiSwapChain->ResizeBuffers(2, clientSize.cx, clientSize.cy, DXGI_FORMAT_B8G8R8A8_UNORM, 0)); - RETURN_IF_FAILED(_PrepareRenderTarget()); + // Change the buffer size and recreate the render target (and surface) + RETURN_IF_FAILED(_dxgiSwapChain->ResizeBuffers(2, clientSize.width(), clientSize.height(), DXGI_FORMAT_B8G8R8A8_UNORM, 0)); + RETURN_IF_FAILED(_PrepareRenderTarget()); - // OK we made it past the parts that can cause errors. We can release our failure handler. - resetDeviceResourcesOnFailure.release(); + // OK we made it past the parts that can cause errors. We can release our failure handler. + resetDeviceResourcesOnFailure.release(); - // And persist the new size. - _displaySizePixels = clientSize; - } + // And persist the new size. + _displaySizePixels = clientSize; - _d2dRenderTarget->BeginDraw(); - _isPainting = true; + // Mark this as the first frame on the new target. We can't use incremental drawing on the first frame. + _firstFrame = true; } - CATCH_RETURN(); + + _d2dRenderTarget->BeginDraw(); + _isPainting = true; } return S_OK; } +CATCH_RETURN() // Routine Description: // - Ends batch drawing and captures any state necessary for presentation @@ -1044,6 +961,7 @@ void DxEngine::_InvalidOr(RECT rc) noexcept // Return Value: // - Any DirectX error, a memory error, etc. [[nodiscard]] HRESULT DxEngine::EndPaint() noexcept +try { RETURN_HR_IF(E_INVALIDARG, !_isPainting); // invalid to end paint when we're not painting @@ -1057,21 +975,42 @@ void DxEngine::_InvalidOr(RECT rc) noexcept if (SUCCEEDED(hr)) { - if (_invalidScroll.cy != 0 || _invalidScroll.cx != 0) + if (_invalidScroll != til::point{ 0, 0 }) { - _presentDirty = _invalidRect; + // Copy `til::rectangles` into RECT map. + _presentDirty.assign(_invalidMap.begin(), _invalidMap.end()); + + // Scale all dirty rectangles into pixels + std::transform(_presentDirty.begin(), _presentDirty.end(), _presentDirty.begin(), [&](til::rectangle rc) { + return rc.scale_up(_glyphCell).scale(til::math::rounding, _scale); + }); + + // Invalid scroll is in characters, convert it to pixels. + const auto scrollPixels = (_invalidScroll * _glyphCell).scale(til::math::rounding, _scale); + + // The scroll rect is the entire field of cells, but in pixels. + til::rectangle scrollArea{ _invalidMap.size() * _glyphCell }; - const RECT display = _GetDisplayRect(); - SubtractRect(&_presentScroll, &display, &_presentDirty); - _presentOffset.x = _invalidScroll.cx; - _presentOffset.y = _invalidScroll.cy; + scrollArea = scrollArea.scale(til::math::ceiling, _scale); - _presentParams.DirtyRectsCount = 1; - _presentParams.pDirtyRects = &_presentDirty; + // Reduce the size of the rectangle by the scroll. + scrollArea -= til::size{} - scrollPixels; + + // Assign the area to the present storage + _presentScroll = scrollArea; + + // Pass the offset. + _presentOffset = scrollPixels; + + // Now fill up the parameters structure from the member variables. + _presentParams.DirtyRectsCount = gsl::narrow(_presentDirty.size()); + _presentParams.pDirtyRects = _presentDirty.data(); _presentParams.pScrollOffset = &_presentOffset; _presentParams.pScrollRect = &_presentScroll; + // The scroll rect will be empty if we scrolled >= 1 full screen size. + // Present1 doesn't like that. So clear it out. Everything will be dirty anyway. if (IsRectEmpty(&_presentScroll)) { _presentParams.pScrollRect = nullptr; @@ -1088,13 +1027,13 @@ void DxEngine::_InvalidOr(RECT rc) noexcept } } - _invalidRect = { 0 }; - _isInvalidUsed = false; + _invalidMap.reset_all(); - _invalidScroll = { 0 }; + _invalidScroll = {}; return hr; } +CATCH_RETURN() // Routine Description: // - Copies the front surface of the swap chain (the one being displayed) @@ -1146,27 +1085,65 @@ void DxEngine::_InvalidOr(RECT rc) noexcept { HRESULT hr = S_OK; - hr = _dxgiSwapChain->Present(1, 0); - /*hr = _dxgiSwapChain->Present1(1, 0, &_presentParams);*/ + bool recreate = false; - if (FAILED(hr)) + // On anything but the first frame, try partial presentation. + // We'll do it first because if it fails, we'll try again with full presentation. + if (!_firstFrame) + { + hr = _dxgiSwapChain->Present1(1, 0, &_presentParams); + + // These two error codes are indicated for destroy-and-recreate + // If we were told to destroy-and-recreate, we're going to skip straight into doing that + // and not try again with full presentation. + recreate = hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET; + + // Log this as we actually don't expect it to happen, we just will try again + // below for robustness of our drawing. + if (FAILED(hr) && !recreate) + { + LOG_HR(hr); + } + } + + // If it's the first frame through, we cannot do partial presentation. + // Also if partial presentation failed above and we weren't told to skip straight to + // device recreation. + // In both of these circumstances, do a full presentation. + if (_firstFrame || (FAILED(hr) && !recreate)) { + hr = _dxgiSwapChain->Present(1, 0); + _firstFrame = false; + // These two error codes are indicated for destroy-and-recreate - if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) + recreate = hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET; + } + + // Now check for failure cases from either presentation mode. + if (FAILED(hr)) + { + // If we were told to recreate the device surface, do that. + if (recreate) { // We don't need to end painting here, as the renderer has done it for us. _ReleaseDeviceResources(); FAIL_FAST_IF_FAILED(InvalidateAll()); return E_PENDING; // Indicate a retry to the renderer. } - - FAIL_FAST_HR(hr); + // Otherwise, we don't know what to do with this error. Report it. + else + { + FAIL_FAST_HR(hr); + } } + // Finally copy the front image (being presented now) onto the backing buffer + // (where we are about to draw the next frame) so we can draw only the differences + // next frame. RETURN_IF_FAILED(_CopyFrontToBack()); _presentReady = false; - _presentDirty = { 0 }; + _presentDirty.clear(); _presentOffset = { 0 }; _presentScroll = { 0 }; _presentParams = { 0 }; @@ -1195,25 +1172,41 @@ void DxEngine::_InvalidOr(RECT rc) noexcept // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::PaintBackground() noexcept +try { - switch (_chainMode) - { - case SwapChainMode::ForHwnd: - _d2dRenderTarget->FillRectangle(D2D1::RectF(static_cast(_invalidRect.left), - static_cast(_invalidRect.top), - static_cast(_invalidRect.right), - static_cast(_invalidRect.bottom)), - _d2dBrushBackground.Get()); - break; - case SwapChainMode::ForComposition: - D2D1_COLOR_F nothing = { 0 }; + D2D1_COLOR_F nothing = { 0 }; + // If the entire thing is invalid, just use one big clear operation. + // This will also hit the gutters outside the usual paintable area. + // Invalidating everything is supposed to happen with resizes of the + // entire canvas, changes of the font, and other such adjustments. + if (_invalidMap.all()) + { _d2dRenderTarget->Clear(nothing); - break; + } + else + { + // Runs are counts of cells. + // Use a transform by the size of one cell to convert cells-to-pixels + // as we clear. + _d2dRenderTarget->SetTransform(D2D1::Matrix3x2F::Scale(_glyphCell)); + for (const auto rect : _invalidMap.runs()) + { + // Use aliased. + // For graphics reasons, it'll look better because it will ensure that + // the edges are cut nice and sharp (not blended by anti-aliasing). + // For performance reasons, it takes a lot less work to not + // do anti-alias blending. + _d2dRenderTarget->PushAxisAlignedClip(rect, D2D1_ANTIALIAS_MODE_ALIASED); + _d2dRenderTarget->Clear(nothing); + _d2dRenderTarget->PopAxisAlignedClip(); + } + _d2dRenderTarget->SetTransform(D2D1::Matrix3x2F::Identity()); } return S_OK; } +CATCH_RETURN() // Routine Description: // - Places one line of text onto the screen at the given position @@ -1227,42 +1220,38 @@ void DxEngine::_InvalidOr(RECT rc) noexcept COORD const coord, const bool /*trimLeft*/, const bool /*lineWrapped*/) noexcept +try { - try - { - // Calculate positioning of our origin. - D2D1_POINT_2F origin; - origin.x = static_cast(coord.X * _glyphCell.cx); - origin.y = static_cast(coord.Y * _glyphCell.cy); - - // Create the text layout - CustomTextLayout layout(_dwriteFactory.Get(), - _dwriteTextAnalyzer.Get(), - _dwriteTextFormat.Get(), - _dwriteFontFace.Get(), - clusters, - _glyphCell.cx); - - // Get the baseline for this font as that's where we draw from - DWRITE_LINE_SPACING spacing; - RETURN_IF_FAILED(_dwriteTextFormat->GetLineSpacing(&spacing.method, &spacing.height, &spacing.baseline)); - - // Assemble the drawing context information - DrawingContext context(_d2dRenderTarget.Get(), - _d2dBrushForeground.Get(), - _d2dBrushBackground.Get(), - _dwriteFactory.Get(), - spacing, - D2D1::SizeF(gsl::narrow(_glyphCell.cx), gsl::narrow(_glyphCell.cy)), - D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT); - - // Layout then render the text - RETURN_IF_FAILED(layout.Draw(&context, _customRenderer.Get(), origin.x, origin.y)); - } - CATCH_RETURN(); + // Calculate positioning of our origin. + const D2D1_POINT_2F origin = til::point{ coord } * _glyphCell; + + // Create the text layout + CustomTextLayout layout(_dwriteFactory.Get(), + _dwriteTextAnalyzer.Get(), + _dwriteTextFormat.Get(), + _dwriteFontFace.Get(), + clusters, + _glyphCell.width()); + + // Get the baseline for this font as that's where we draw from + DWRITE_LINE_SPACING spacing; + RETURN_IF_FAILED(_dwriteTextFormat->GetLineSpacing(&spacing.method, &spacing.height, &spacing.baseline)); + + // Assemble the drawing context information + DrawingContext context(_d2dRenderTarget.Get(), + _d2dBrushForeground.Get(), + _d2dBrushBackground.Get(), + _dwriteFactory.Get(), + spacing, + _glyphCell, + D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT); + + // Layout then render the text + RETURN_IF_FAILED(layout.Draw(&context, _customRenderer.Get(), origin.x, origin.y)); return S_OK; } +CATCH_RETURN() // Routine Description: // - Paints lines around cells (draws in pieces of the grid) @@ -1278,16 +1267,15 @@ void DxEngine::_InvalidOr(RECT rc) noexcept COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept +try { const auto existingColor = _d2dBrushForeground->GetColor(); const auto restoreBrushOnExit = wil::scope_exit([&]() noexcept { _d2dBrushForeground->SetColor(existingColor); }); _d2dBrushForeground->SetColor(_ColorFFromColorRef(color)); - const auto font = _GetFontSize(); - D2D_POINT_2F target; - target.x = static_cast(coordTarget.X) * font.X; - target.y = static_cast(coordTarget.Y) * font.Y; + const auto font = _glyphCell; + D2D_POINT_2F target = til::point{ coordTarget } * font; D2D_POINT_2F start = { 0 }; D2D_POINT_2F end = { 0 }; @@ -1300,7 +1288,7 @@ void DxEngine::_InvalidOr(RECT rc) noexcept if (lines & GridLines::Top) { end = start; - end.x += font.X; + end.x += font.width(); _d2dRenderTarget->DrawLine(start, end, _d2dBrushForeground.Get(), 1.0f, _strokeStyle.Get()); } @@ -1308,7 +1296,7 @@ void DxEngine::_InvalidOr(RECT rc) noexcept if (lines & GridLines::Left) { end = start; - end.y += font.Y; + end.y += font.height(); _d2dRenderTarget->DrawLine(start, end, _d2dBrushForeground.Get(), 1.0f, _strokeStyle.Get()); } @@ -1321,32 +1309,33 @@ void DxEngine::_InvalidOr(RECT rc) noexcept // The top right corner inclusive is at 7,0 which is X (0) + Font Height (8) - 1 = 7. // 0.5 pixel offset for crisp lines; -0.5 on the Y to fit _in_ the cell, not outside it. - start = { target.x + 0.5f, target.y + font.Y - 0.5f }; + start = { target.x + 0.5f, target.y + font.height() - 0.5f }; if (lines & GridLines::Bottom) { end = start; - end.x += font.X - 1.f; + end.x += font.width() - 1.f; _d2dRenderTarget->DrawLine(start, end, _d2dBrushForeground.Get(), 1.0f, _strokeStyle.Get()); } - start = { target.x + font.X - 0.5f, target.y + 0.5f }; + start = { target.x + font.width() - 0.5f, target.y + 0.5f }; if (lines & GridLines::Right) { end = start; - end.y += font.Y - 1.f; + end.y += font.height() - 1.f; _d2dRenderTarget->DrawLine(start, end, _d2dBrushForeground.Get(), 1.0f, _strokeStyle.Get()); } // Move to the next character in this run. - target.x += font.X; + target.x += font.width(); } return S_OK; } +CATCH_RETURN() // Routine Description: // - Paints an overlay highlight on a portion of the frame to represent selected text @@ -1355,28 +1344,20 @@ void DxEngine::_InvalidOr(RECT rc) noexcept // Return Value: // - S_OK or relevant DirectX error. [[nodiscard]] HRESULT DxEngine::PaintSelection(const SMALL_RECT rect) noexcept +try { const auto existingColor = _d2dBrushForeground->GetColor(); _d2dBrushForeground->SetColor(_selectionBackground); const auto resetColorOnExit = wil::scope_exit([&]() noexcept { _d2dBrushForeground->SetColor(existingColor); }); - RECT pixels; - pixels.left = rect.Left * _glyphCell.cx; - pixels.top = rect.Top * _glyphCell.cy; - pixels.right = rect.Right * _glyphCell.cx; - pixels.bottom = rect.Bottom * _glyphCell.cy; - - D2D1_RECT_F draw = { 0 }; - draw.left = static_cast(pixels.left); - draw.top = static_cast(pixels.top); - draw.right = static_cast(pixels.right); - draw.bottom = static_cast(pixels.bottom); + const D2D1_RECT_F draw = til::rectangle{ Viewport::FromExclusive(rect).ToInclusive() }.scale_up(_glyphCell); _d2dRenderTarget->FillRectangle(draw, _d2dBrushForeground.Get()); return S_OK; } +CATCH_RETURN() // Helper to choose which Direct2D method to use when drawing the cursor rectangle enum class CursorPaintType @@ -1393,6 +1374,7 @@ enum class CursorPaintType // Return Value: // - S_OK or relevant DirectX error. [[nodiscard]] HRESULT DxEngine::PaintCursor(const IRenderEngine::CursorOptions& options) noexcept +try { // if the cursor is off, do nothing - it should not be visible. if (!options.isOn) @@ -1400,16 +1382,12 @@ enum class CursorPaintType return S_FALSE; } // Create rectangular block representing where the cursor can fill. - D2D1_RECT_F rect = { 0 }; - rect.left = static_cast(options.coordCursor.X * _glyphCell.cx); - rect.top = static_cast(options.coordCursor.Y * _glyphCell.cy); - rect.right = static_cast(rect.left + _glyphCell.cx); - rect.bottom = static_cast(rect.top + _glyphCell.cy); + D2D1_RECT_F rect = til::rectangle{ til::point{ options.coordCursor } }.scale_up(_glyphCell); // If we're double-width, make it one extra glyph wider if (options.fIsDoubleWidth) { - rect.right += _glyphCell.cx; + rect.right += _glyphCell.width(); } CursorPaintType paintType = CursorPaintType::Fill; @@ -1420,8 +1398,7 @@ enum class CursorPaintType { // Enforce min/max cursor height ULONG ulHeight = std::clamp(options.ulCursorHeightPercent, s_ulMinCursorHeightPercent, s_ulMaxCursorHeightPercent); - - ulHeight = gsl::narrow((_glyphCell.cy * ulHeight) / 100); + ulHeight = (_glyphCell.height() * ulHeight) / 100; rect.top = rect.bottom - ulHeight; break; } @@ -1484,6 +1461,7 @@ enum class CursorPaintType return S_OK; } +CATCH_RETURN() // Routine Description: // - Paint terminal effects. @@ -1581,6 +1559,7 @@ CATCH_RETURN() // Return Value: // - S_OK or relevant DirectX error [[nodiscard]] HRESULT DxEngine::UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo) noexcept +try { RETURN_IF_FAILED(_GetProposedFont(pfiFontInfoDesired, fiFontInfo, @@ -1589,22 +1568,16 @@ CATCH_RETURN() _dwriteTextAnalyzer, _dwriteFontFace)); - try - { - const auto size = fiFontInfo.GetSize(); - - _glyphCell.cx = size.X; - _glyphCell.cy = size.Y; - } - CATCH_RETURN(); + _glyphCell = fiFontInfo.GetSize(); return S_OK; } +CATCH_RETURN(); [[nodiscard]] Viewport DxEngine::GetViewportInCharacters(const Viewport& viewInPixels) noexcept { - const short widthInChars = gsl::narrow_cast(viewInPixels.Width() / _glyphCell.cx); - const short heightInChars = gsl::narrow_cast(viewInPixels.Height() / _glyphCell.cy); + const short widthInChars = gsl::narrow_cast(viewInPixels.Width() / _glyphCell.width()); + const short heightInChars = gsl::narrow_cast(viewInPixels.Height() / _glyphCell.height()); return Viewport::FromDimensions(viewInPixels.Origin(), { widthInChars, heightInChars }); } @@ -1693,29 +1666,7 @@ float DxEngine::GetScaling() const noexcept // - Rectangle describing dirty area in characters. [[nodiscard]] std::vector DxEngine::GetDirtyArea() { - SMALL_RECT r; - r.Top = gsl::narrow(floor(_invalidRect.top / _glyphCell.cy)); - r.Left = gsl::narrow(floor(_invalidRect.left / _glyphCell.cx)); - r.Bottom = gsl::narrow(floor(_invalidRect.bottom / _glyphCell.cy)); - r.Right = gsl::narrow(floor(_invalidRect.right / _glyphCell.cx)); - - // Exclusive to inclusive - r.Bottom--; - r.Right--; - - return { r }; -} - -// Routine Description: -// - Gets COORD packed with shorts of each glyph (character) cell's -// height and width. -// Arguments: -// - -// Return Value: -// - Nearest integer short x and y values for each cell. -[[nodiscard]] COORD DxEngine::_GetFontSize() const noexcept -{ - return { gsl::narrow(_glyphCell.cx), gsl::narrow(_glyphCell.cy) }; + return _invalidMap.runs(); } // Routine Description: @@ -1725,10 +1676,12 @@ float DxEngine::GetScaling() const noexcept // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::GetFontSize(_Out_ COORD* const pFontSize) noexcept +try { - *pFontSize = _GetFontSize(); + *pFontSize = _glyphCell; return S_OK; } +CATCH_RETURN(); // Routine Description: // - Currently unused by this renderer. @@ -1738,30 +1691,28 @@ float DxEngine::GetScaling() const noexcept // Return Value: // - S_OK or relevant DirectWrite error. [[nodiscard]] HRESULT DxEngine::IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept +try { RETURN_HR_IF_NULL(E_INVALIDARG, pResult); - try - { - const Cluster cluster(glyph, 0); // columns don't matter, we're doing analysis not layout. + const Cluster cluster(glyph, 0); // columns don't matter, we're doing analysis not layout. - // Create the text layout - CustomTextLayout layout(_dwriteFactory.Get(), - _dwriteTextAnalyzer.Get(), - _dwriteTextFormat.Get(), - _dwriteFontFace.Get(), - { &cluster, 1 }, - _glyphCell.cx); + // Create the text layout + CustomTextLayout layout(_dwriteFactory.Get(), + _dwriteTextAnalyzer.Get(), + _dwriteTextFormat.Get(), + _dwriteFontFace.Get(), + { &cluster, 1 }, + _glyphCell.width()); - UINT32 columns = 0; - RETURN_IF_FAILED(layout.GetColumns(&columns)); + UINT32 columns = 0; + RETURN_IF_FAILED(layout.GetColumns(&columns)); - *pResult = columns != 1; - } - CATCH_RETURN(); + *pResult = columns != 1; return S_OK; } +CATCH_RETURN(); // Method Description: // - Updates the window's title string. diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index b8521d9b1ee..6fd98eb076b 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -121,7 +121,7 @@ namespace Microsoft::Console::Render SwapChainMode _chainMode; HWND _hwndTarget; - SIZE _sizeTarget; + til::size _sizeTarget; int _dpi; float _scale; @@ -130,8 +130,8 @@ namespace Microsoft::Console::Render bool _isEnabled; bool _isPainting; - SIZE _displaySizePixels; - SIZE _glyphCell; + til::size _displaySizePixels; + til::size _glyphCell; D2D1_COLOR_F _defaultForegroundColor; D2D1_COLOR_F _defaultBackgroundColor; @@ -140,19 +140,13 @@ namespace Microsoft::Console::Render D2D1_COLOR_F _backgroundColor; D2D1_COLOR_F _selectionBackground; - [[nodiscard]] RECT _GetDisplayRect() const noexcept; - - bool _isInvalidUsed; - RECT _invalidRect; - SIZE _invalidScroll; - - void _InvalidOr(SMALL_RECT sr) noexcept; - void _InvalidOr(RECT rc) noexcept; - - void _InvalidOffset(POINT pt); + bool _firstFrame; + bool _invalidateFullRows; + til::bitmap _invalidMap; + til::point _invalidScroll; bool _presentReady; - RECT _presentDirty; + std::vector _presentDirty; RECT _presentScroll; POINT _presentOffset; DXGI_PRESENT_PARAMETERS _presentParams; @@ -244,9 +238,9 @@ namespace Microsoft::Console::Render ::Microsoft::WRL::ComPtr& textAnalyzer, ::Microsoft::WRL::ComPtr& fontFace) const noexcept; - [[nodiscard]] COORD _GetFontSize() const noexcept; + [[nodiscard]] til::size _GetClientSize() const; - [[nodiscard]] SIZE _GetClientSize() const noexcept; + void _InvalidateRectangle(const til::rectangle& rc); [[nodiscard]] D2D1_COLOR_F _ColorFFromColorRef(const COLORREF color) noexcept; diff --git a/src/renderer/dx/precomp.h b/src/renderer/dx/precomp.h index 858413f4b40..d77791ddc34 100644 --- a/src/renderer/dx/precomp.h +++ b/src/renderer/dx/precomp.h @@ -4,9 +4,11 @@ #pragma once // This includes support libraries from the CRT, STL, WIL, and GSL +#define BLOCK_TIL // We want to include it later, after DX. #include "LibraryIncludes.h" #include +#include #include "..\host\conddkrefs.h" #include @@ -34,4 +36,7 @@ #include #include +// Re-include TIL at the bottom to gain DX superpowers. +#include "til.h" + #pragma hdrstop diff --git a/src/til/ut_til/BitmapTests.cpp b/src/til/ut_til/BitmapTests.cpp index 648b32c7340..cb560d240aa 100644 --- a/src/til/ut_til/BitmapTests.cpp +++ b/src/til/ut_til/BitmapTests.cpp @@ -844,6 +844,14 @@ class BitmapTests VERIFY_IS_FALSE(bitmap.all()); } + TEST_METHOD(Size) + { + til::size sz{ 5, 10 }; + til::bitmap map{ sz }; + + VERIFY_ARE_EQUAL(sz, map.size()); + } + TEST_METHOD(Runs) { // This map --> Those runs diff --git a/src/til/ut_til/PointTests.cpp b/src/til/ut_til/PointTests.cpp index ffb9702944c..994f2df4bdd 100644 --- a/src/til/ut_til/PointTests.cpp +++ b/src/til/ut_til/PointTests.cpp @@ -417,6 +417,33 @@ class PointTests } } + TEST_METHOD(ScaleByFloat) + { + Log::Comment(L"0.) Scale that should be in bounds."); + { + const til::point pt{ 5, 10 }; + const float scale = 1.783f; + + const til::point expected{ static_cast(ceil(5 * scale)), static_cast(ceil(10 * scale)) }; + + const auto actual = pt.scale(til::math::ceiling, scale); + + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"1.) Scale results in value that is too large."); + { + const til::point pt{ 5, 10 }; + constexpr float scale = std::numeric_limits().max(); + + auto fn = [&]() { + pt.scale(til::math::ceiling, scale); + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + } + TEST_METHOD(Division) { Log::Comment(L"0.) Division of two things that should be in bounds."); diff --git a/src/til/ut_til/RectangleTests.cpp b/src/til/ut_til/RectangleTests.cpp index 82aa5a69a75..b8bb7859937 100644 --- a/src/til/ut_til/RectangleTests.cpp +++ b/src/til/ut_til/RectangleTests.cpp @@ -797,6 +797,79 @@ class RectangleTests } } + TEST_METHOD(ScaleUpSize) + { + const til::rectangle start{ 10, 20, 30, 40 }; + + Log::Comment(L"1.) Multiply by size to scale from cells to pixels"); + { + const til::size scale{ 3, 7 }; + const til::rectangle expected{ 10 * 3, 20 * 7, 30 * 3, 40 * 7 }; + const auto actual = start.scale_up(scale); + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"2.) Multiply by size with width way too big."); + { + const til::size scale{ std::numeric_limits().max(), static_cast(7) }; + + auto fn = [&]() { + const auto actual = start.scale_up(scale); + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + + Log::Comment(L"3.) Multiply by size with height way too big."); + { + const til::size scale{ static_cast(3), std::numeric_limits().max() }; + + auto fn = [&]() { + const auto actual = start.scale_up(scale); + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + } + + TEST_METHOD(ScaleDownSize) + { + const til::rectangle start{ 10, 20, 29, 40 }; + + Log::Comment(L"0.) Division by size to scale from pixels to cells"); + { + const til::size scale{ 3, 7 }; + + // Division is special. The top and left round down. + // The bottom and right round up. This is to ensure that the cells + // the smaller rectangle represents fully cover all the pixels + // of the larger rectangle. + // L: 10 / 3 = 3.333 --> round down --> 3 + // T: 20 / 7 = 2.857 --> round down --> 2 + // R: 29 / 3 = 9.667 --> round up ----> 10 + // B: 40 / 7 = 5.714 --> round up ----> 6 + const til::rectangle expected{ 3, 2, 10, 6 }; + const auto actual = start.scale_down(scale); + VERIFY_ARE_EQUAL(expected, actual); + } + } + + TEST_METHOD(ScaleByFloat) + { + const til::rectangle start{ 10, 20, 30, 40 }; + + const float scale = 1.45f; + + // This is not a test of the various TilMath rounding methods + // so we're only checking one here. + // Expected here is written based on the "ceiling" outcome. + const til::rectangle expected{ 15, 29, 44, 58 }; + + const auto actual = start.scale(til::math::ceiling, scale); + + VERIFY_ARE_EQUAL(actual, expected); + } + TEST_METHOD(Top) { const til::rectangle rc{ 5, 10, 15, 20 }; @@ -1361,4 +1434,101 @@ class RectangleTests } #pragma endregion + + template + struct RectangleTypeWithLowercase + { + T left, top, right, bottom; + }; + template + struct RectangleTypeWithCapitalization + { + T Left, Top, Right, Bottom; + }; + TEST_METHOD(CastFromFloatWithMathTypes) + { + RectangleTypeWithLowercase lowerFloatIntegral{ 1.f, 2.f, 3.f, 4.f }; + RectangleTypeWithLowercase lowerFloat{ 1.6f, 2.4f, 3.2f, 4.8f }; + RectangleTypeWithCapitalization capitalDoubleIntegral{ 3., 4., 5., 6. }; + RectangleTypeWithCapitalization capitalDouble{ 3.6, 4.4, 5.7, 6.3 }; + Log::Comment(L"0.) Ceiling"); + { + { + til::rectangle converted{ til::math::ceiling, lowerFloatIntegral }; + VERIFY_ARE_EQUAL((til::rectangle{ 1, 2, 3, 4 }), converted); + } + { + til::rectangle converted{ til::math::ceiling, lowerFloat }; + VERIFY_ARE_EQUAL((til::rectangle{ 2, 3, 4, 5 }), converted); + } + { + til::rectangle converted{ til::math::ceiling, capitalDoubleIntegral }; + VERIFY_ARE_EQUAL((til::rectangle{ 3, 4, 5, 6 }), converted); + } + { + til::rectangle converted{ til::math::ceiling, capitalDouble }; + VERIFY_ARE_EQUAL((til::rectangle{ 4, 5, 6, 7 }), converted); + } + } + + Log::Comment(L"1.) Flooring"); + { + { + til::rectangle converted{ til::math::flooring, lowerFloatIntegral }; + VERIFY_ARE_EQUAL((til::rectangle{ 1, 2, 3, 4 }), converted); + } + { + til::rectangle converted{ til::math::flooring, lowerFloat }; + VERIFY_ARE_EQUAL((til::rectangle{ 1, 2, 3, 4 }), converted); + } + { + til::rectangle converted{ til::math::flooring, capitalDoubleIntegral }; + VERIFY_ARE_EQUAL((til::rectangle{ 3, 4, 5, 6 }), converted); + } + { + til::rectangle converted{ til::math::flooring, capitalDouble }; + VERIFY_ARE_EQUAL((til::rectangle{ 3, 4, 5, 6 }), converted); + } + } + + Log::Comment(L"2.) Rounding"); + { + { + til::rectangle converted{ til::math::rounding, lowerFloatIntegral }; + VERIFY_ARE_EQUAL((til::rectangle{ 1, 2, 3, 4 }), converted); + } + { + til::rectangle converted{ til::math::rounding, lowerFloat }; + VERIFY_ARE_EQUAL((til::rectangle{ 2, 2, 3, 5 }), converted); + } + { + til::rectangle converted{ til::math::rounding, capitalDoubleIntegral }; + VERIFY_ARE_EQUAL((til::rectangle{ 3, 4, 5, 6 }), converted); + } + { + til::rectangle converted{ til::math::rounding, capitalDouble }; + VERIFY_ARE_EQUAL((til::rectangle{ 4, 4, 6, 6 }), converted); + } + } + + Log::Comment(L"3.) Truncating"); + { + { + til::rectangle converted{ til::math::truncating, lowerFloatIntegral }; + VERIFY_ARE_EQUAL((til::rectangle{ 1, 2, 3, 4 }), converted); + } + { + til::rectangle converted{ til::math::truncating, lowerFloat }; + VERIFY_ARE_EQUAL((til::rectangle{ 1, 2, 3, 4 }), converted); + } + { + til::rectangle converted{ til::math::truncating, capitalDoubleIntegral }; + VERIFY_ARE_EQUAL((til::rectangle{ 3, 4, 5, 6 }), converted); + } + { + til::rectangle converted{ til::math::truncating, capitalDouble }; + VERIFY_ARE_EQUAL((til::rectangle{ 3, 4, 5, 6 }), converted); + } + } + } }; diff --git a/src/til/ut_til/SizeTests.cpp b/src/til/ut_til/SizeTests.cpp index 4b708edf373..fb0bb1ecfc7 100644 --- a/src/til/ut_til/SizeTests.cpp +++ b/src/til/ut_til/SizeTests.cpp @@ -309,6 +309,33 @@ class SizeTests } } + TEST_METHOD(ScaleByFloat) + { + Log::Comment(L"0.) Scale that should be in bounds."); + { + const til::size sz{ 5, 10 }; + const float scale = 1.783f; + + const til::size expected{ static_cast(ceil(5 * scale)), static_cast(ceil(10 * scale)) }; + + const auto actual = sz.scale(til::math::ceiling, scale); + + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"1.) Scale results in value that is too large."); + { + const til::size sz{ 5, 10 }; + constexpr float scale = std::numeric_limits().max(); + + auto fn = [&]() { + sz.scale(til::math::ceiling, scale); + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + } + TEST_METHOD(Division) { Log::Comment(L"0.) Division of two things that should be in bounds.");