From 79684bf82177dfa52bd1eca84476a2950d765bb9 Mon Sep 17 00:00:00 2001 From: Michael Niksa Date: Mon, 13 Apr 2020 13:09:02 -0700 Subject: [PATCH] Render row-by-row instead of invalidating entire screen (#5185) ## Summary of the Pull Request Adjusts DirectX renderer to use `til::bitmap` to track invalidation regions. Uses special modification to invalidate a row-at-a-time to ensure ligatures and NxM glyphs continue to work. ## References Likely helps #1064 ## PR Checklist * [x] Closes #778 * [x] I work here. * [x] Manual testing performed. See Performance traces in #778. * [x] Automated tests for `til` changes. * [x] Am core contributor. And discussed with @DHowett-MSFT. ## Detailed Description of the Pull Request / Additional comments - Applies `til::bitmap` as the new invalidation scheme inside the DirectX renderer and updates all entrypoints for collecting invalidation data to coalesce into this structure. - Semi-permanently routes all invalidations through a helper method `_InvalidateRectangle` that will expand any invalidation to cover the entire line. This ensures that ligatures and NxM glyphs will continue to render appropriately while still allowing us to dramatically reduce the number of lines drawn overall. In the future, we may come up with a tighter solution than line-by-line invalidation and can modify this helper method appropriately at that later date to further scope the invalid region. - Ensures that the `experimental.retroTerminalEffects` feature continues to invalidate the entire display on start of frame as the shader is applied at the end of the frame composition and will stack on itself in an amusing fashion when we only redraw part of the display. - Moves many member variables inside the DirectX renderer into the new `til::size`, `til::point`, and `til::rectangle` methods to facilitate easier management and mathematical operations. Consequently adds `try/catch` blocks around many of the already-existing `noexcept` methods to deal with mathematical or casting failures now detected by using the support classes. - Corrects `TerminalCore` redraw triggers to appropriately communicate scrolling circumstances to the renderer so it can optimize the draw regions appropriately. - Fixes an issue in the base `Renderer` that was causing overlapping scroll regions due to behavior of `Viewport::TrimToViewport` modifying the local. This fix is "good enough" for now and should go away when `Viewport` is fully migrated to `til::rectangle`. - Adds multiplication and division operators to `til::rectangle` and supporting tests. These operates will help scale back and forth between a cell-based rectangle and a pixel-based rectangle. They take special care to ensure that a pixel rectangle being divided downward back to cells will expand (with the ceiling division methods) to cover a full cell when even one pixel inside the cell is touched (as is how a redraw would have to occur). - Blocks off trace logging of invalid regions if no one is listening to optimize performance. - Restores full usage of `IDXGISwapChain1::Present1` to accurately and fully communicate dirty and scroll regions to the underlying DirectX framework. This additional information allows the framework to optimize drawing between frames by eliminating data transfer of regions that aren't modified and shuffling frames in place. See [Remarks](https://docs.microsoft.com/en-us/windows/win32/api/dxgi1_2/nf-dxgi1_2-idxgiswapchain1-present1#remarks) for more details. - Updates `til::bitmap` set methods to use more optimized versions of the setters on the `dynamic_bitset<>` that can bulk fill bits as the existing algorithm was noticeably slow after applying the "expand-to-row" helper to the DirectX renderer invalidation. - All `til` import hierarchy is now handled in the parent `til.h` file and not in the child files to prevent circular imports from happening. We don't expect the import of any individual library file, only the base one. So this should be OK for now. ## Validation Steps Performed - Ran `cmatrix`, `cmatrix -u0`, and `cacafire` after changes were made. - Made a bunch of ligatures with `Cascadia Code` in the Terminal before/after the changes and confirmed they still ligate. - Ran `dir` in Powershell and fixed the scrolling issues - Clicked all over the place and dragged to make sure selection works. - Checked retro terminal effect manually with Powershell. --- src/cascadia/TerminalControl/TermControl.cpp | 5 + src/cascadia/TerminalCore/Terminal.cpp | 15 +- src/inc/til.h | 2 +- src/inc/til/bitmap.h | 11 +- src/inc/til/operators.h | 4 - src/inc/til/point.h | 13 + src/inc/til/rectangle.h | 52 +- src/inc/til/size.h | 13 + src/renderer/base/renderer.cpp | 75 +- src/renderer/base/renderer.hpp | 1 + src/renderer/dx/DxRenderer.cpp | 739 +++++++++---------- src/renderer/dx/DxRenderer.hpp | 26 +- src/renderer/dx/precomp.h | 5 + src/til/ut_til/BitmapTests.cpp | 8 + src/til/ut_til/PointTests.cpp | 27 + src/til/ut_til/RectangleTests.cpp | 170 +++++ src/til/ut_til/SizeTests.cpp | 27 + 17 files changed, 762 insertions(+), 431 deletions(-) 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.");