Adjusts High DPI scaling to enable differential rendering (#5345)

## Summary of the Pull Request
- Adjusts scaling practices in `DxEngine` (and related scaling practices in `TerminalControl`) for pixel-perfect row baselines and spacing at High DPI such that differential row-by-row rendering can be applied at High DPI.

## References
- #5185 

## PR Checklist
* [x] Closes #5320, closes #3515, closes #1064
* [x] I work here.
* [x] Manually tested.
* [x] No doc.
* [x] Am core contributor. Also discussed with some of them already via Teams.

## Detailed Description of the Pull Request / Additional comments

**WAS:**
- We were using implicit DPI scaling on the `ID2D1RenderTarget` and running all of our processing in DIPs (Device-Independent Pixels). That's all well and good for getting things bootstrapped quickly, but it leaves the actual scaling of the draw commands up to the discretion of the rendering target.
- When we don't get to explicitly choose exactly how many pixels tall/wide and our X/Y placement perfectly, the nature of floating point multiplication and division required to do the presentation can cause us to drift off slightly out of our control depending on what the final display resolution actually is.
- Differential drawing cannot work unless we can know the exact integer pixels that need to be copied/moved/preserved/replaced between frames to give to the `IDXGISwapChain1::Present1` method. If things spill into fractional pixels or the sizes of rows/columns vary as they are rounded up and down implicitly, then we cannot do the differential rendering.

**NOW:**
- When deciding on a font, the `DxEngine` will take the scale factor into account and adjust the proposed height of the requested font. Then the remainder of the existing code that adjusts the baseline and integer-ifies each character cell will run naturally from there. That code already works correctly to align the height at normal DPI and scale out the font heights and advances to take an exact integer of pixels.
- `TermControl` has to use the scale now, in some places, and stop scaling in other places. This has to do with how the target's nature used to be implicit and is now explicit. For instance, determining where the cursor click hits must be scaled now. And determining the pixel size of the display canvas must no longer be scaled.
- `DxEngine` will no longer attempt to scale the invalid regions per my attempts in #5185 because the cell size is scaled. So it should work the same as at 96 DPI.
- The block is removed from the `DxEngine` that was causing a full invalidate on every frame at High DPI.
- A TODO was removed from `TermControl` that was invalidating everything when the DPI changed because the underlying renderer will already do that.

## Validation Steps Performed
* [x] Check at 150% DPI. Print text, scroll text down and up, do selection.
* [x] Check at 100% DPI. Print text, scroll text down and up, do selection.
* [x] Span two different DPI monitors and drag between them.
* [x] Giant pile of tests in https://github.com/microsoft/terminal/pull/5345#issuecomment-614127648

Co-authored-by: Dustin Howett <duhowett@microsoft.com>
Co-authored-by: Mike Griese <migrie@microsoft.com>
This commit is contained in:
Michael Niksa 2020-04-22 14:59:51 -07:00 committed by GitHub
parent 0741cfdaec
commit 8ea9b327f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 356 additions and 166 deletions

View file

@ -724,7 +724,7 @@ HRESULT HwndTerminal::_CopyTextToSystemClipboard(const TextBuffer::TextAndColor&
if (fAlsoCopyFormatting)
{
const auto& fontData = _actualFont;
int const iFontHeightPoints = fontData.GetUnscaledSize().Y * 72 / this->_currentDpi;
int const iFontHeightPoints = fontData.GetUnscaledSize().Y; // this renderer uses points already
const COLORREF bgColor = _terminal->GetBackgroundColor(_terminal->GetDefaultBrushColors());
std::string HTMLToPlaceOnClip = TextBuffer::GenHTML(rows, iFontHeightPoints, fontData.GetFaceName(), bgColor, "Hwnd Console Host");

View file

@ -175,52 +175,69 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
auto fontArgs = winrt::make_self<FontInfoEventArgs>();
_CurrentFontInfoHandlers(*this, *fontArgs);
const auto fontWidth = fontArgs->FontSize().Width;
const auto fontHeight = fontArgs->FontSize().Height;
const til::size fontSize{ til::math::flooring, fontArgs->FontSize() };
// Convert text buffer cursor position to client coordinate position within the window
COORD clientCursorPos;
clientCursorPos.X = ::base::ClampMul(_currentTerminalCursorPos.x(), ::base::ClampedNumeric<ptrdiff_t>(fontWidth));
clientCursorPos.Y = ::base::ClampMul(_currentTerminalCursorPos.y(), ::base::ClampedNumeric<ptrdiff_t>(fontHeight));
// Convert text buffer cursor position to client coordinate position
// within the window. This point is in _pixels_
const til::point clientCursorPos{ _currentTerminalCursorPos * fontSize };
// position textblock to cursor position
Canvas().SetLeft(TextBlock(), clientCursorPos.X);
Canvas().SetTop(TextBlock(), clientCursorPos.Y);
// Get scale factor for view
const double scaleFactor = DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel();
// calculate FontSize in pixels from DPIs
const double fontSizePx = (fontHeight * 72) / USER_DEFAULT_SCREEN_DPI;
TextBlock().FontSize(fontSizePx);
const til::point clientCursorInDips{ clientCursorPos / scaleFactor };
// Position our TextBlock at the cursor position
Canvas().SetLeft(TextBlock(), clientCursorInDips.x<double>());
Canvas().SetTop(TextBlock(), clientCursorInDips.y<double>());
// calculate FontSize in pixels from Points
const double fontSizePx = (fontSize.height<double>() * 72) / USER_DEFAULT_SCREEN_DPI;
// Make sure to unscale the font size to correct for DPI! XAML needs
// things in DIPs, and the fontSize is in pixels.
TextBlock().FontSize(fontSizePx / scaleFactor);
TextBlock().FontFamily(Media::FontFamily(fontArgs->FontFace()));
const auto widthToTerminalEnd = _currentCanvasWidth - ::base::ClampedNumeric<double>(clientCursorPos.X);
const auto widthToTerminalEnd = _currentCanvasWidth - clientCursorInDips.x<double>();
// Make sure that we're setting the MaxWidth to a positive number - a
// negative number here will crash us in mysterious ways with a useless
// stack trace
const auto newMaxWidth = std::max<double>(0.0, widthToTerminalEnd);
TextBlock().MaxWidth(newMaxWidth);
// Get window in screen coordinates, this is the entire window including tabs
const auto windowBounds = CoreWindow::GetForCurrentThread().Bounds();
// Get window in screen coordinates, this is the entire window including
// tabs. THIS IS IN DIPs
const auto windowBounds{ CoreWindow::GetForCurrentThread().Bounds() };
const til::point windowOrigin{ til::math::flooring, windowBounds };
// Convert from client coordinate to screen coordinate by adding window position
COORD screenCursorPos;
screenCursorPos.X = ::base::ClampAdd(clientCursorPos.X, ::base::ClampedNumeric<short>(windowBounds.X));
screenCursorPos.Y = ::base::ClampAdd(clientCursorPos.Y, ::base::ClampedNumeric<short>(windowBounds.Y));
// Get the offset (margin + tabs, etc..) of the control within the window
const til::point controlOrigin{ til::math::flooring,
this->TransformToVisual(nullptr).TransformPoint(Point(0, 0)) };
// get any offset (margin + tabs, etc..) of the control within the window
const auto offsetPoint = this->TransformToVisual(nullptr).TransformPoint(winrt::Windows::Foundation::Point(0, 0));
// The controlAbsoluteOrigin is the origin of the control relative to
// the origin of the displays. THIS IS IN DIPs
const til::point controlAbsoluteOrigin{ windowOrigin + controlOrigin };
// add the margin offsets if any
screenCursorPos.X = ::base::ClampAdd(screenCursorPos.X, ::base::ClampedNumeric<short>(offsetPoint.X));
screenCursorPos.Y = ::base::ClampAdd(screenCursorPos.Y, ::base::ClampedNumeric<short>(offsetPoint.Y));
// Convert the control origin to pixels
const til::point scaledFrameOrigin = controlAbsoluteOrigin * scaleFactor;
// Get scale factor for view
const double scaleFactor = DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel();
const auto yOffset = ::base::ClampedNumeric<float>(_currentTextBlockHeight) - fontHeight;
const auto textBottom = ::base::ClampedNumeric<float>(screenCursorPos.Y) + yOffset;
// Get the location of the cursor in the display, in pixels.
til::point screenCursorPos{ scaledFrameOrigin + clientCursorPos };
_currentTextBounds = ScaleRect(Rect(screenCursorPos.X, textBottom, 0, fontHeight), scaleFactor);
_currentControlBounds = ScaleRect(Rect(screenCursorPos.X, screenCursorPos.Y, 0, fontHeight), scaleFactor);
// GH #5007 - make sure to account for wrapping the IME composition at
// the right side of the viewport.
const ptrdiff_t textBlockHeight = ::base::ClampMul(_currentTextBlockHeight, scaleFactor);
// Get the bounds of the composition text, in pixels.
const til::rectangle textBounds{ til::point{ screenCursorPos.x(), screenCursorPos.y() },
til::size{ 0, textBlockHeight } };
_currentTextBounds = textBounds;
_currentControlBounds = Rect(screenCursorPos.x<float>(),
screenCursorPos.y<float>(),
0,
fontSize.height<float>());
}
// Method Description:

View file

@ -225,17 +225,12 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_terminal->UpdateSettings(_settings);
// Refresh our font with the renderer
const auto actualFontOldSize = _actualFont.GetSize();
_UpdateFont();
const auto width = SwapChainPanel().ActualWidth();
const auto height = SwapChainPanel().ActualHeight();
if (width != 0 && height != 0)
const auto actualFontNewSize = _actualFont.GetSize();
if (actualFontNewSize != actualFontOldSize)
{
// If the font size changed, or the _swapchainPanel's size changed
// for any reason, we'll need to make sure to also resize the
// buffer. _DoResize will invalidate everything for us.
auto lock = _terminal->LockForWriting();
_DoResize(width, height);
_RefreshSize();
}
}
}
@ -514,8 +509,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
return false;
}
const auto windowWidth = SwapChainPanel().ActualWidth(); // Width() and Height() are NaN?
const auto windowHeight = SwapChainPanel().ActualHeight();
const auto actualWidth = SwapChainPanel().ActualWidth();
const auto actualHeight = SwapChainPanel().ActualHeight();
const auto windowWidth = actualWidth * SwapChainPanel().CompositionScaleX(); // Width() and Height() are NaN?
const auto windowHeight = actualHeight * SwapChainPanel().CompositionScaleY();
if (windowWidth == 0 || windowHeight == 0)
{
@ -1056,8 +1054,10 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// Figure out if the user's moved a quarter of a cell's smaller axis away from the clickdown point
auto& touchdownPoint{ *_singleClickTouchdownPos };
auto distance{ std::sqrtf(std::powf(cursorPosition.X - touchdownPoint.X, 2) + std::powf(cursorPosition.Y - touchdownPoint.Y, 2)) };
const auto fontSize{ _actualFont.GetSize() };
if (distance >= (std::min(fontSize.X, fontSize.Y) / 4.f))
const til::size fontSize{ _actualFont.GetSize() };
const auto fontSizeInDips = fontSize.scale(til::math::rounding, 1.0f / _renderEngine->GetScaling());
if (distance >= (std::min(fontSizeInDips.width(), fontSizeInDips.height()) / 4.f))
{
_terminal->SetSelectionAnchor(_GetTerminalPosition(touchdownPoint));
// stop tracking the touchdown point
@ -1097,19 +1097,22 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
winrt::Windows::Foundation::Point newTouchPoint{ contactRect.X, contactRect.Y };
const auto anchor = _touchAnchor.value();
// Get the difference between the point we've dragged to and the start of the touch.
const float fontHeight = float(_actualFont.GetSize().Y);
// Our _actualFont's size is in pixels, convert to DIPs, which the
// rest of the Points here are in.
const til::size fontSize{ _actualFont.GetSize() };
const auto fontSizeInDips = fontSize.scale(til::math::rounding, 1.0f / _renderEngine->GetScaling());
// Get the difference between the point we've dragged to and the start of the touch.
const float dy = newTouchPoint.Y - anchor.Y;
// Start viewport scroll after we've moved more than a half row of text
if (std::abs(dy) > (fontHeight / 2.0f))
if (std::abs(dy) > (fontSizeInDips.height<float>() / 2.0f))
{
// Multiply by -1, because moving the touch point down will
// create a positive delta, but we want the viewport to move up,
// so we'll need a negative scroll amount (and the inverse for
// panning down)
const float numRows = -1.0f * (dy / fontHeight);
const float numRows = -1.0f * (dy / fontSizeInDips.height<float>());
const auto currentOffset = ::base::ClampedNumeric<double>(ScrollBar().Value());
const auto newValue = numRows + currentOffset;
@ -1648,12 +1651,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// Refresh our font with the renderer
_UpdateFont();
auto lock = _terminal->LockForWriting();
// Resize the terminal's BUFFER to match the new font size. This does
// NOT change the size of the window, because that can lead to more
// problems (like what happens when you change the font size while the
// window is maximized?)
_DoResize(SwapChainPanel().ActualWidth(), SwapChainPanel().ActualHeight());
_RefreshSize();
}
CATCH_LOG();
}
@ -1673,22 +1675,84 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
auto lock = _terminal->LockForWriting();
const auto foundationSize = e.NewSize();
const auto newSize = e.NewSize();
const auto currentScaleX = SwapChainPanel().CompositionScaleX();
const auto currentEngineScale = _renderEngine->GetScaling();
auto foundationSize = newSize;
// A strange thing can happen here. If you have two tabs open, and drag
// across a DPI boundary, then switch to the other tab, that tab will
// receive two events: First, a SizeChanged, then a ScaleChanged. In the
// SizeChanged event handler, the SwapChainPanel's CompositionScale will
// _already_ be the new scaling, but the engine won't have that value
// yet. If we scale by the CompositionScale here, we'll end up in a
// weird torn state. I'm not totally sure why.
//
// Fortunately we will be getting that following ScaleChanged event, and
// we'll end up resizing again, so we don't terribly need to worry about
// this.
foundationSize.Width *= currentEngineScale;
foundationSize.Height *= currentEngineScale;
_DoResize(foundationSize.Width, foundationSize.Height);
}
// Method Description:
// - Triggered when the swapchain changes DPI. When this happens, we're
// going to receive 3 events:
// - 1. First, a CompositionScaleChanged _for the original scale_. I don't
// know why this event happens first. **It also doesn't always happen.**
// However, when it does happen, it doesn't give us any useful
// information.
// - 2. Then, a SizeChanged. During that SizeChanged, either:
// - the CompositionScale will still be the original DPI. This happens
// when the control is visible as the DPI changes.
// - The CompositionScale will be the new DPI. This happens when the
// control wasn't focused as the window's DPI changed, so it only got
// these messages after XAML updated it's scaling.
// - 3. Finally, a CompositionScaleChanged with the _new_ DPI.
// - 4. We'll usually get another SizeChanged some time after this last
// ScaleChanged. This usually seems to happen after something triggers
// the UI to re-layout, like hovering over the scrollbar. This event
// doesn't reliably happen immediately after a scale change, so we can't
// depend on it (despite the fact that both the scale and size state is
// definitely correct in it)
// - In the 3rd event, we're going to update our font size for the new DPI.
// At that point, we know how big the font should be for the new DPI, and
// how big the SwapChainPanel will be. If these sizes are different, we'll
// need to resize the buffer to fit in the new window.
// Arguments:
// - sender: The SwapChainPanel who's DPI changed. This is our _swapchainPanel.
// - args: This param is unused in the CompositionScaleChanged event.
void TermControl::_SwapChainScaleChanged(Windows::UI::Xaml::Controls::SwapChainPanel const& sender,
Windows::Foundation::IInspectable const& /*args*/)
{
if (_renderEngine)
{
const auto scale = sender.CompositionScaleX();
const auto dpi = (int)(scale * USER_DEFAULT_SCREEN_DPI);
const auto scaleX = sender.CompositionScaleX();
const auto scaleY = sender.CompositionScaleY();
const auto dpi = (float)(scaleX * USER_DEFAULT_SCREEN_DPI);
const auto currentEngineScale = _renderEngine->GetScaling();
// TODO: MSFT: 21169071 - Shouldn't this all happen through _renderer and trigger the invalidate automatically on DPI change?
THROW_IF_FAILED(_renderEngine->UpdateDpi(dpi));
_renderer->TriggerRedrawAll();
// If we're getting a notification to change to the DPI we already
// have, then we're probably just beginning the DPI change. Since
// we'll get _another_ event with the real DPI, do nothing here for
// now. We'll also skip the next resize in _SwapChainSizeChanged.
const bool dpiWasUnchanged = currentEngineScale == scaleX;
if (dpiWasUnchanged)
{
return;
}
const auto actualFontOldSize = _actualFont.GetSize();
_renderer->TriggerFontChange(::base::saturated_cast<int>(dpi), _desiredFont, _actualFont);
const auto actualFontNewSize = _actualFont.GetSize();
if (actualFontNewSize != actualFontOldSize)
{
_RefreshSize();
}
}
}
@ -1727,6 +1791,31 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_selectionNeedsToBeCopied = true;
}
// Method Description:
// - Perform a resize for the current size of the swapchainpanel. If the
// font size changed, we'll need to resize the buffer to fit the existing
// swapchain size. This helper will call _DoResize with the current size
// of the swapchain, accounting for scaling due to DPI.
// - Note that a DPI change will also trigger a font size change, and will call into here.
// Arguments:
// - <none>
// Return Value:
// - <none>
void TermControl::_RefreshSize()
{
const auto currentScaleX = SwapChainPanel().CompositionScaleX();
const auto currentScaleY = SwapChainPanel().CompositionScaleY();
const auto actualWidth = SwapChainPanel().ActualWidth();
const auto actualHeight = SwapChainPanel().ActualHeight();
const auto widthInPixels = actualWidth * currentScaleX;
const auto heightInPixels = actualHeight * currentScaleY;
// Grab the lock, because we might be changing the buffer size here.
auto lock = _terminal->LockForWriting();
_DoResize(widthInPixels, heightInPixels);
}
// Method Description:
// - Process a resize event that was initiated by the user. This can either
// be due to the user resizing the window (causing the swapchain to
@ -1762,11 +1851,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels);
// If this function succeeds with S_FALSE, then the terminal didn't
// actually change size. No need to notify the connection of this
// no-op.
// TODO: MSFT:20642295 Resizing the buffer will corrupt it
// I believe we'll need support for CSI 2J, and additionally I think
// we're resetting the viewport to the top
// actually change size. No need to notify the connection of this no-op.
const HRESULT hr = _terminal->UserResize({ vp.Width(), vp.Height() });
if (SUCCEEDED(hr) && hr != S_FALSE)
{
@ -2093,23 +2178,14 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
THROW_IF_FAILED(dxEngine->UpdateDpi(dpi));
THROW_IF_FAILED(dxEngine->UpdateFont(desiredFont, actualFont));
const float scale = dxEngine->GetScaling();
const auto scale = dxEngine->GetScaling();
const auto fontSize = actualFont.GetSize();
// Manually multiply by the scaling factor. The DX engine doesn't
// actually store the scaled font size in the fontInfo.GetSize()
// property when the DX engine is in Composition mode (which it is for
// the Terminal). At runtime, this is fine, as we'll transform
// everything by our scaling, so it'll work out. However, right now we
// need to get the exact pixel count.
const float fFontWidth = gsl::narrow_cast<float>(fontSize.X * scale);
const float fFontHeight = gsl::narrow_cast<float>(fontSize.Y * scale);
// UWP XAML scrollbars aren't guaranteed to be the same size as the
// ComCtl scrollbars, but it's certainly close enough.
const auto scrollbarSize = GetSystemMetricsForDpi(SM_CXVSCROLL, dpi);
double width = cols * fFontWidth;
double width = cols * fontSize.X;
// Reserve additional space if scrollbar is intended to be visible
if (settings.ScrollState() == ScrollbarState::Visible)
@ -2117,7 +2193,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
width += scrollbarSize;
}
double height = rows * fFontHeight;
double height = rows * fontSize.Y;
auto thickness = _ParseThicknessFromPadding(settings.Padding());
// GH#2061 - make sure to account for the size the padding _will be_ scaled to
width += scale * (thickness.Left + thickness.Right);
@ -2311,21 +2387,21 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// - the corresponding viewport terminal position for the given Point parameter
const COORD TermControl::_GetTerminalPosition(winrt::Windows::Foundation::Point cursorPosition)
{
// Exclude padding from cursor position calculation
COORD terminalPosition = {
static_cast<SHORT>(cursorPosition.X - SwapChainPanel().Margin().Left),
static_cast<SHORT>(cursorPosition.Y - SwapChainPanel().Margin().Top)
};
// cursorPosition is DIPs, relative to SwapChainPanel origin
const til::point cursorPosInDIPs{ til::math::rounding, cursorPosition };
const til::size marginsInDips{ til::math::rounding, SwapChainPanel().Margin().Left, SwapChainPanel().Margin().Top };
const auto fontSize = _actualFont.GetSize();
FAIL_FAST_IF(fontSize.X == 0);
FAIL_FAST_IF(fontSize.Y == 0);
// This point is the location of the cursor within the actual grid of characters, in DIPs
const til::point relativeToMarginInDIPs = cursorPosInDIPs - marginsInDips;
// Normalize to terminal coordinates by using font size
terminalPosition.X /= fontSize.X;
terminalPosition.Y /= fontSize.Y;
// Convert it to pixels
const til::point relativeToMarginInPixels{ relativeToMarginInDIPs * SwapChainPanel().CompositionScaleX() };
return terminalPosition;
// Get the size of the font, which is in pixels
const til::size fontSize{ _actualFont.GetSize() };
// Convert the location in pixels to characters within the current viewport.
return til::point{ relativeToMarginInPixels / fontSize };
}
// Method Description:

View file

@ -198,6 +198,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void _SwapChainSizeChanged(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::SizeChangedEventArgs const& e);
void _SwapChainScaleChanged(Windows::UI::Xaml::Controls::SwapChainPanel const& sender, Windows::Foundation::IInspectable const& args);
void _DoResize(const double newWidth, const double newHeight);
void _RefreshSize();
void _TerminalTitleChanged(const std::wstring_view& wstr);
winrt::fire_and_forget _TerminalScrollPositionChanged(const int viewTop, const int viewHeight, const int bufferSize);
winrt::fire_and_forget _TerminalCursorPositionChanged();

View file

@ -1229,6 +1229,20 @@ void SCREEN_INFORMATION::_InternalSetViewportSize(const COORD* const pcoordSize,
_viewport = newViewport;
UpdateBottom();
Tracing::s_TraceWindowViewport(_viewport);
// In Conpty mode, call TriggerScroll here without params. By not providing
// params, the renderer will make sure to update the VtEngine with the
// updated viewport size. If we don't do this, the engine can get into a
// torn state on this frame.
//
// Without this statement, the engine won't be told about the new view size
// till the start of the next frame. If any other text gets output before
// that frame starts, there's a very real chance that it'll cause errors as
// the engine tries to invalidate those regions.
if (gci.IsInVtIoMode() && ServiceLocator::LocateGlobals().pRender)
{
ServiceLocator::LocateGlobals().pRender->TriggerScroll();
}
}
// Routine Description:

View file

@ -135,7 +135,6 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
_sz{},
_rc{},
_bits{},
_dirty{},
_runs{}
{
}
@ -149,7 +148,6 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
_sz(sz),
_rc(sz),
_bits(_sz.area()),
_dirty(fill ? sz : til::rectangle{}),
_runs{}
{
if (fill)
@ -162,7 +160,6 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
return _sz == other._sz &&
_rc == other._rc &&
_dirty == other._dirty && // dirty is before bits because it's a rough estimate of bits and a faster comparison.
_bits == other._bits;
// _runs excluded because it's a cache of generated state.
}
@ -187,15 +184,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
// If we don't have cached runs, rebuild.
if (!_runs.has_value())
{
// If there's only one square dirty, quick save it off and be done.
if (one())
{
_runs.emplace({ _dirty });
}
else
{
_runs.emplace(begin(), end());
}
_runs.emplace(begin(), end());
}
// Return a reference to the runs.
@ -274,8 +263,6 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
_runs.reset(); // reset cached runs on any non-const method
_bits.set(_rc.index_of(pt));
_dirty |= til::rectangle{ pt };
}
void set(const til::rectangle rc)
@ -287,22 +274,18 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
_bits.set(_rc.index_of(til::point{ rc.left(), row }), rc.width(), true);
}
_dirty |= rc;
}
void set_all() noexcept
{
_runs.reset(); // reset cached runs on any non-const method
_bits.set();
_dirty = _rc;
}
void reset_all() noexcept
{
_runs.reset(); // reset cached runs on any non-const method
_bits.reset();
_dirty = {};
}
// True if we resized. False if it was the same size as before.
@ -358,9 +341,9 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
}
}
bool one() const
constexpr bool one() const noexcept
{
return _dirty.size() == til::size{ 1, 1 };
return _bits.count() == 1;
}
constexpr bool any() const noexcept
@ -370,12 +353,12 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
constexpr bool none() const noexcept
{
return _dirty.empty();
return _bits.none();
}
constexpr bool all() const noexcept
{
return _dirty == _rc;
return _bits.all();
}
constexpr til::size size() const noexcept
@ -399,7 +382,6 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
}
private:
til::rectangle _dirty;
til::size _sz;
til::rectangle _rc;
dynamic_bitset<> _bits;

View file

@ -53,8 +53,18 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
}
// This template will convert to point from floating-point args;
// a math type is required. If you _don't_ provide one, you're going to
// get a compile-time error about "cannot convert from initializer-list to til::point"
template<typename TilMath, typename TOther>
constexpr point(TilMath, const TOther& x, const TOther& y, std::enable_if_t<std::is_floating_point_v<TOther>, int> /*sentinel*/ = 0) :
point(TilMath::template cast<ptrdiff_t>(x), TilMath::template cast<ptrdiff_t>(y))
{
}
// This template will convert to size from anything that has a X and a Y field that are floating-point;
// a math type is required.
// a math type is required. If you _don't_ provide one, you're going to
// get a compile-time error about "cannot convert from initializer-list to til::point"
template<typename TilMath, typename TOther>
constexpr point(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().X)> && std::is_floating_point_v<decltype(std::declval<TOther>().Y)>, int> /*sentinel*/ = 0) :
point(TilMath::template cast<ptrdiff_t>(other.X), TilMath::template cast<ptrdiff_t>(other.Y))
@ -62,7 +72,8 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
}
// This template will convert to size from anything that has a x and a y field that are floating-point;
// a math type is required.
// a math type is required. If you _don't_ provide one, you're going to
// get a compile-time error about "cannot convert from initializer-list to til::point"
template<typename TilMath, typename TOther>
constexpr point(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().x)> && std::is_floating_point_v<decltype(std::declval<TOther>().y)>, int> /*sentinel*/ = 0) :
point(TilMath::template cast<ptrdiff_t>(other.x), TilMath::template cast<ptrdiff_t>(other.y))

View file

@ -863,6 +863,18 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
}
#endif
#ifdef WINRT_Windows_Foundation_H
operator winrt::Windows::Foundation::Rect() const
{
winrt::Windows::Foundation::Rect ret;
THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(left()).AssignIfValid(&ret.X));
THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(top()).AssignIfValid(&ret.Y));
THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(width()).AssignIfValid(&ret.Width));
THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(height()).AssignIfValid(&ret.Height));
return ret;
}
#endif
std::wstring to_string() const
{
return wil::str_printf<std::wstring>(L"(L:%td, T:%td, R:%td, B:%td) [W:%td, H:%td]", left(), top(), right(), bottom(), width(), height());

View file

@ -25,6 +25,14 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
size(static_cast<ptrdiff_t>(width), static_cast<ptrdiff_t>(height))
{
}
constexpr size(ptrdiff_t width, int height) noexcept :
size(width, static_cast<ptrdiff_t>(height))
{
}
constexpr size(int width, ptrdiff_t height) noexcept :
size(static_cast<ptrdiff_t>(width), height)
{
}
#endif
size(size_t width, size_t height)
@ -54,7 +62,8 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
}
// This template will convert to size from anything that has a X and a Y field that are floating-point;
// a math type is required.
// a math type is required. If you _don't_ provide one, you're going to
// get a compile-time error about "cannot convert from initializer-list to til::size"
template<typename TilMath, typename TOther>
constexpr size(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().X)> && std::is_floating_point_v<decltype(std::declval<TOther>().Y)>, int> /*sentinel*/ = 0) :
size(TilMath::template cast<ptrdiff_t>(other.X), TilMath::template cast<ptrdiff_t>(other.Y))
@ -62,7 +71,8 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
}
// This template will convert to size from anything that has a cx and a cy field that are floating-point;
// a math type is required.
// a math type is required. If you _don't_ provide one, you're going to
// get a compile-time error about "cannot convert from initializer-list to til::size"
template<typename TilMath, typename TOther>
constexpr size(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().cx)> && std::is_floating_point_v<decltype(std::declval<TOther>().cy)>, int> /*sentinel*/ = 0) :
size(TilMath::template cast<ptrdiff_t>(other.cx), TilMath::template cast<ptrdiff_t>(other.cy))
@ -70,13 +80,23 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
}
// This template will convert to size from anything that has a Width and a Height field that are floating-point;
// a math type is required.
// a math type is required. If you _don't_ provide one, you're going to
// get a compile-time error about "cannot convert from initializer-list to til::size"
template<typename TilMath, typename TOther>
constexpr size(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().Width)> && std::is_floating_point_v<decltype(std::declval<TOther>().Height)>, int> /*sentinel*/ = 0) :
size(TilMath::template cast<ptrdiff_t>(other.Width), TilMath::template cast<ptrdiff_t>(other.Height))
{
}
// This template will convert to size from floating-point args;
// a math type is required. If you _don't_ provide one, you're going to
// get a compile-time error about "cannot convert from initializer-list to til::size"
template<typename TilMath, typename TOther>
constexpr size(TilMath, const TOther& width, const TOther& height, std::enable_if_t<std::is_floating_point_v<TOther>, int> /*sentinel*/ = 0) :
size(TilMath::template cast<ptrdiff_t>(width), TilMath::template cast<ptrdiff_t>(height))
{
}
constexpr bool operator==(const size& other) const noexcept
{
return _width == other._width &&

View file

@ -88,6 +88,7 @@ DxEngine::DxEngine() :
_sizeTarget{},
_dpi{ USER_DEFAULT_SCREEN_DPI },
_scale{ 1.0f },
_prevScale{ 1.0f },
_chainMode{ SwapChainMode::ForComposition },
_customRenderer{ ::Microsoft::WRL::Make<CustomTextRenderer>() }
{
@ -562,9 +563,6 @@ CATCH_RETURN();
// If in composition mode, apply scaling factor matrix
if (_chainMode == SwapChainMode::ForComposition)
{
const auto fdpi = static_cast<float>(_dpi);
_d2dRenderTarget->SetDpi(fdpi, fdpi);
DXGI_MATRIX_3X2_F inverseScale = { 0 };
inverseScale._11 = 1.0f / _scale;
inverseScale._22 = inverseScale._11;
@ -573,6 +571,8 @@ CATCH_RETURN();
RETURN_IF_FAILED(_dxgiSwapChain.As(&sc2));
RETURN_IF_FAILED(sc2->SetMatrixTransform(&inverseScale));
}
_prevScale = _scale;
return S_OK;
}
CATCH_RETURN();
@ -800,6 +800,16 @@ CATCH_RETURN();
try
{
_invalidMap.set_all();
// Since everything is invalidated here, mark this as a "first frame", so
// that we won't use incremental drawing on it. The caller of this intended
// for _everything_ to get redrawn, so setting _firstFrame will force us to
// redraw the entire frame. This will make sure that things like the gutters
// get cleared correctly.
//
// Invalidating everything is supposed to happen with resizes of the
// entire canvas, changes of the font, and other such adjustments.
_firstFrame = true;
return S_OK;
}
CATCH_RETURN();
@ -837,7 +847,7 @@ CATCH_RETURN();
}
case SwapChainMode::ForComposition:
{
return _sizeTarget.scale(til::math::ceiling, _scale);
return _sizeTarget;
}
default:
FAIL_FAST_HR(E_NOTIMPL);
@ -894,13 +904,6 @@ try
_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();
@ -920,7 +923,7 @@ try
{
RETURN_IF_FAILED(_CreateDeviceResources(true));
}
else if (_displaySizePixels != clientSize)
else if (_displaySizePixels != clientSize || _prevScale != _scale)
{
// 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.
@ -982,17 +985,15 @@ try
// 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);
return rc.scale_up(_glyphCell);
});
// Invalid scroll is in characters, convert it to pixels.
const auto scrollPixels = (_invalidScroll * _glyphCell).scale(til::math::rounding, _scale);
const auto scrollPixels = (_invalidScroll * _glyphCell);
// The scroll rect is the entire field of cells, but in pixels.
til::rectangle scrollArea{ _invalidMap.size() * _glyphCell };
scrollArea = scrollArea.scale(til::math::ceiling, _scale);
// Reduce the size of the rectangle by the scroll.
scrollArea -= til::size{} - scrollPixels;
@ -1177,9 +1178,6 @@ try
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);
@ -1971,12 +1969,8 @@ CATCH_RETURN();
// The advance is the number of pixels left-to-right (X dimension) for the given font.
// We're finding a proportional factor here with the design units in "ems", not an actual pixel measurement.
// For HWND swap chains, we play trickery with the font size. For others, we use inherent scaling.
// For composition swap chains, we scale by the DPI later during drawing and presentation.
if (_chainMode == SwapChainMode::ForHwnd)
{
heightDesired *= (static_cast<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI));
}
// Now we play trickery with the font size. Scale by the DPI to get the height we expect.
heightDesired *= (static_cast<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI));
const float widthAdvance = static_cast<float>(advanceInDesignUnits) / fontMetrics.designUnitsPerEm;

View file

@ -124,6 +124,7 @@ namespace Microsoft::Console::Render
til::size _sizeTarget;
int _dpi;
float _scale;
float _prevScale;
std::function<void()> _pfn;

View file

@ -35,6 +35,38 @@ Abstract:
// (before TIL so its support lights up)
#include <dcommon.h>
// Include some things structs from WinRT without including WinRT
// because I just want to make sure it fills structs correctly.
// Adapted from C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\winrt\windows.foundation.h
#define WINRT_Windows_Foundation_H
namespace winrt {
namespace Windows {
namespace Foundation {
struct Rect
{
FLOAT X;
FLOAT Y;
FLOAT Width;
FLOAT Height;
};
struct Point
{
FLOAT X;
FLOAT Y;
};
struct Size
{
FLOAT Width;
FLOAT Height;
};
} /* Foundation */
} /* Windows */
} /* winrt */
// Include TIL after Wex to get test comparators.
#include "til.h"

View file

@ -22,25 +22,6 @@ class BitmapTests
void _checkBits(const std::vector<til::rectangle>& bitsOn,
const til::bitmap& map)
{
Log::Comment(L"Check dirty rectangles.");
// Union up all the dirty rectangles into one big one.
if (bitsOn.empty())
{
VERIFY_ARE_EQUAL(til::rectangle{}, map._dirty);
}
else
{
auto dirtyExpected = bitsOn.front();
for (auto it = bitsOn.cbegin() + 1; it < bitsOn.cend(); ++it)
{
dirtyExpected |= *it;
}
// Check if it matches.
VERIFY_ARE_EQUAL(dirtyExpected, map._dirty);
}
Log::Comment(L"Check all bits in map.");
// For every point in the map...
for (const auto pt : map._rc)
@ -71,7 +52,6 @@ class BitmapTests
VERIFY_ARE_EQUAL(expectedSize, bitmap._sz);
VERIFY_ARE_EQUAL(expectedRect, bitmap._rc);
VERIFY_ARE_EQUAL(0u, bitmap._bits.size());
VERIFY_ARE_EQUAL(til::rectangle{}, bitmap._dirty);
// The find will go from begin to end in the bits looking for a "true".
// It should miss so the result should be "cend" and turn out true here.
@ -86,7 +66,6 @@ class BitmapTests
VERIFY_ARE_EQUAL(expectedSize, bitmap._sz);
VERIFY_ARE_EQUAL(expectedRect, bitmap._rc);
VERIFY_ARE_EQUAL(50u, bitmap._bits.size());
VERIFY_ARE_EQUAL(til::rectangle{}, bitmap._dirty);
// The find will go from begin to end in the bits looking for a "true".
// It should miss so the result should be "cend" and turn out true here.
@ -112,12 +91,10 @@ class BitmapTests
if (!fill)
{
VERIFY_IS_TRUE(bitmap._bits.none());
VERIFY_ARE_EQUAL(til::rectangle{}, bitmap._dirty);
}
else
{
VERIFY_IS_TRUE(bitmap._bits.all());
VERIFY_ARE_EQUAL(expectedRect, bitmap._dirty);
}
}

View file

@ -27,6 +27,13 @@ class PointTests
VERIFY_ARE_EQUAL(10, pt._y);
}
TEST_METHOD(RawFloatingConstruct)
{
const til::point pt{ til::math::rounding, 3.2f, 7.6f };
VERIFY_ARE_EQUAL(3, pt._x);
VERIFY_ARE_EQUAL(8, pt._y);
}
TEST_METHOD(UnsignedConstruct)
{
Log::Comment(L"0.) Normal unsigned construct.");

View file

@ -1350,6 +1350,23 @@ class RectangleTests
// All ptrdiff_ts fit into a float, so there's no exception tests.
}
TEST_METHOD(CastToWindowsFoundationRect)
{
Log::Comment(L"0.) Typical situation.");
{
const til::rectangle rc{ 5, 10, 15, 20 };
winrt::Windows::Foundation::Rect val = rc;
VERIFY_ARE_EQUAL(5.f, val.X);
VERIFY_ARE_EQUAL(10.f, val.Y);
VERIFY_ARE_EQUAL(10.f, val.Width);
VERIFY_ARE_EQUAL(10.f, val.Height);
}
// All ptrdiff_ts fit into a float, so there's no exception tests.
// The only other exceptions come from things that don't fit into width() or height()
// and those have explicit tests elsewhere in this file.
}
#pragma region iterator
TEST_METHOD(Begin)
{

View file

@ -27,6 +27,13 @@ class SizeTests
VERIFY_ARE_EQUAL(10, sz._height);
}
TEST_METHOD(RawFloatingConstruct)
{
const til::size sz{ til::math::rounding, 3.2f, 7.8f };
VERIFY_ARE_EQUAL(3, sz._width);
VERIFY_ARE_EQUAL(8, sz._height);
}
TEST_METHOD(UnsignedConstruct)
{
Log::Comment(L"0.) Normal unsigned construct.");
@ -74,6 +81,26 @@ class SizeTests
VERIFY_ARE_EQUAL(height, sz._height);
}
TEST_METHOD(MixedRawTypeConstruct)
{
const ptrdiff_t a = -5;
const int b = -10;
Log::Comment(L"Case 1: ptrdiff_t/int");
{
const til::size sz{ a, b };
VERIFY_ARE_EQUAL(a, sz._width);
VERIFY_ARE_EQUAL(b, sz._height);
}
Log::Comment(L"Case 2: int/ptrdiff_t");
{
const til::size sz{ b, a };
VERIFY_ARE_EQUAL(b, sz._width);
VERIFY_ARE_EQUAL(a, sz._height);
}
}
TEST_METHOD(CoordConstruct)
{
COORD coord{ -5, 10 };

View file

@ -98,9 +98,10 @@ void TermControlUiaTextRange::_TranslatePointToScreen(LPPOINT clientPoint) const
const gsl::not_null<TermControlUiaProvider*> provider = static_cast<TermControlUiaProvider*>(_pProvider);
const auto includeOffsets = [](long clientPos, double termControlPos, double padding, double scaleFactor) {
auto result = base::ClampedNumeric<double>(clientPos);
result += padding;
auto result = base::ClampedNumeric<double>(padding);
// only the padding is in DIPs now
result *= scaleFactor;
result += clientPos;
result += termControlPos;
return result;
};
@ -131,10 +132,11 @@ void TermControlUiaTextRange::_TranslatePointFromScreen(LPPOINT screenPoint) con
const gsl::not_null<TermControlUiaProvider*> provider = static_cast<TermControlUiaProvider*>(_pProvider);
const auto includeOffsets = [](long screenPos, double termControlPos, double padding, double scaleFactor) {
auto result = base::ClampedNumeric<double>(screenPos);
result -= termControlPos;
auto result = base::ClampedNumeric<double>(padding);
// only the padding is in DIPs now
result /= scaleFactor;
result -= padding;
result -= screenPos;
result -= termControlPos;
return result;
};