Browse Source

Layout performance improvements (#18315)

pull/18558/head
Julien Lebosquain 11 months ago
committed by GitHub
parent
commit
aac83dcd53
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 176
      src/Avalonia.Base/Layout/LayoutHelper.cs
  2. 100
      src/Avalonia.Base/Layout/Layoutable.cs
  3. 51
      src/Avalonia.Base/Layout/MinMax.cs
  4. 15
      src/Avalonia.Base/Size.cs
  5. 2
      src/Avalonia.Base/StyledElement.cs
  6. 11
      src/Avalonia.Base/Threading/Dispatcher.cs
  7. 25
      src/Avalonia.Base/Utilities/MathUtilities.cs
  8. 16
      src/Avalonia.Base/Visual.cs
  9. 2
      src/Avalonia.Controls/Border.cs
  10. 18
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  11. 2
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  12. 3
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  13. 8
      src/Avalonia.Controls/TextBlock.cs
  14. 27
      src/Avalonia.Controls/TopLevel.cs
  15. 14
      tests/Avalonia.Benchmarks/Layout/Measure.cs
  16. 5
      tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
  17. 6
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
  18. 11
      tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
  19. 1
      tests/Avalonia.Controls.UnitTests/WindowTests.cs

176
src/Avalonia.Base/Layout/LayoutHelper.cs

@ -1,4 +1,6 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -25,21 +27,20 @@ namespace Avalonia.Layout
/// <param name="constraints">The space available for the control.</param> /// <param name="constraints">The space available for the control.</param>
/// <returns>The control's size.</returns> /// <returns>The control's size.</returns>
public static Size ApplyLayoutConstraints(Layoutable control, Size constraints) public static Size ApplyLayoutConstraints(Layoutable control, Size constraints)
{ => ApplyLayoutConstraints(new MinMax(control), constraints);
var minmax = new MinMax(control);
return new Size( internal static Size ApplyLayoutConstraints(MinMax minMax, Size constraints)
MathUtilities.Clamp(constraints.Width, minmax.MinWidth, minmax.MaxWidth), => new(
MathUtilities.Clamp(constraints.Height, minmax.MinHeight, minmax.MaxHeight)); MathUtilities.Clamp(constraints.Width, minMax.MinWidth, minMax.MaxWidth),
} MathUtilities.Clamp(constraints.Height, minMax.MinHeight, minMax.MaxHeight));
public static Size MeasureChild(Layoutable? control, Size availableSize, Thickness padding, public static Size MeasureChild(Layoutable? control, Size availableSize, Thickness padding,
Thickness borderThickness) Thickness borderThickness)
{ {
if (IsParentLayoutRounded(control, out double scale)) if (IsParentLayoutRounded(control, out double scale))
{ {
padding = RoundLayoutThickness(padding, scale, scale); padding = RoundLayoutThickness(padding, scale);
borderThickness = RoundLayoutThickness(borderThickness, scale, scale); borderThickness = RoundLayoutThickness(borderThickness, scale);
} }
if (control != null) if (control != null)
@ -55,7 +56,7 @@ namespace Avalonia.Layout
{ {
if (IsParentLayoutRounded(control, out double scale)) if (IsParentLayoutRounded(control, out double scale))
{ {
padding = RoundLayoutThickness(padding, scale, scale); padding = RoundLayoutThickness(padding, scale);
} }
if (control != null) if (control != null)
@ -71,8 +72,8 @@ namespace Avalonia.Layout
{ {
if (IsParentLayoutRounded(child, out double scale)) if (IsParentLayoutRounded(child, out double scale))
{ {
padding = RoundLayoutThickness(padding, scale, scale); padding = RoundLayoutThickness(padding, scale);
borderThickness = RoundLayoutThickness(borderThickness, scale, scale); borderThickness = RoundLayoutThickness(borderThickness, scale);
} }
return ArrangeChildInternal(child, availableSize, padding + borderThickness); return ArrangeChildInternal(child, availableSize, padding + borderThickness);
@ -81,7 +82,7 @@ namespace Avalonia.Layout
public static Size ArrangeChild(Layoutable? child, Size availableSize, Thickness padding) public static Size ArrangeChild(Layoutable? child, Size availableSize, Thickness padding)
{ {
if(IsParentLayoutRounded(child, out double scale)) if(IsParentLayoutRounded(child, out double scale))
padding = RoundLayoutThickness(padding, scale, scale); padding = RoundLayoutThickness(padding, scale);
return ArrangeChildInternal(child, availableSize, padding); return ArrangeChildInternal(child, availableSize, padding);
} }
@ -140,18 +141,7 @@ namespace Avalonia.Layout
/// <param name="control">The control.</param> /// <param name="control">The control.</param>
/// <exception cref="Exception">Thrown when control has no root or returned layout scaling is invalid.</exception> /// <exception cref="Exception">Thrown when control has no root or returned layout scaling is invalid.</exception>
public static double GetLayoutScale(Layoutable control) public static double GetLayoutScale(Layoutable control)
{ => control.VisualRoot is ILayoutRoot layoutRoot ? layoutRoot.LayoutScaling : 1.0;
var visualRoot = (control as Visual)?.VisualRoot;
var result = (visualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0;
if (result == 0 || double.IsNaN(result) || double.IsInfinity(result))
{
throw new Exception($"Invalid LayoutScaling returned from {visualRoot!.GetType()}");
}
return result;
}
/// <summary> /// <summary>
/// Rounds a size to integer values for layout purposes, compensating for high DPI screen /// Rounds a size to integer values for layout purposes, compensating for high DPI screen
@ -172,6 +162,20 @@ namespace Avalonia.Layout
return new Size(RoundLayoutValueUp(size.Width, dpiScaleX), RoundLayoutValueUp(size.Height, dpiScaleY)); return new Size(RoundLayoutValueUp(size.Width, dpiScaleX), RoundLayoutValueUp(size.Height, dpiScaleY));
} }
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "The DPI scale should have been normalized.")]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Size RoundLayoutSizeUp(Size size, double dpiScale)
{
// If DPI == 1, don't use DPI-aware rounding.
return dpiScale == 1.0 ?
new Size(
Math.Ceiling(size.Width),
Math.Ceiling(size.Height)) :
new Size(
Math.Ceiling(RoundTo8Digits(size.Width) * dpiScale) / dpiScale,
Math.Ceiling(RoundTo8Digits(size.Height) * dpiScale) / dpiScale);
}
/// <summary> /// <summary>
/// Rounds a thickness to integer values for layout purposes, compensating for high DPI screen /// Rounds a thickness to integer values for layout purposes, compensating for high DPI screen
/// coordinates. /// coordinates.
@ -196,6 +200,38 @@ namespace Avalonia.Layout
); );
} }
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "The DPI scale should have been normalized.")]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Thickness RoundLayoutThickness(Thickness thickness, double dpiScale)
{
// If DPI == 1, don't use DPI-aware rounding.
return dpiScale == 1.0 ?
new Thickness(
Math.Round(thickness.Left),
Math.Round(thickness.Top),
Math.Round(thickness.Right),
Math.Round(thickness.Bottom)) :
new Thickness(
Math.Round(thickness.Left * dpiScale) / dpiScale,
Math.Round(thickness.Top * dpiScale) / dpiScale,
Math.Round(thickness.Right * dpiScale) / dpiScale,
Math.Round(thickness.Bottom * dpiScale) / dpiScale);
}
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "The DPI scale should have been normalized.")]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Point RoundLayoutPoint(Point point, double dpiScale)
{
// If DPI == 1, don't use DPI-aware rounding.
return dpiScale == 1.0 ?
new Point(
Math.Round(point.X),
Math.Round(point.Y)) :
new Point(
Math.Round(point.X * dpiScale) / dpiScale,
Math.Round(point.Y * dpiScale) / dpiScale);
}
/// <summary> /// <summary>
/// Calculates the value to be used for layout rounding at high DPI by rounding the value /// Calculates the value to be used for layout rounding at high DPI by rounding the value
/// up or down to the nearest pixel. /// up or down to the nearest pixel.
@ -211,28 +247,10 @@ namespace Avalonia.Layout
/// </remarks> /// </remarks>
public static double RoundLayoutValue(double value, double dpiScale) public static double RoundLayoutValue(double value, double dpiScale)
{ {
double newValue;
// If DPI == 1, don't use DPI-aware rounding. // If DPI == 1, don't use DPI-aware rounding.
if (!MathUtilities.IsOne(dpiScale)) return MathUtilities.IsOne(dpiScale) ?
{ Math.Round(value) :
newValue = Math.Round(value * dpiScale) / dpiScale; Math.Round(value * dpiScale) / dpiScale;
// If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue),
// use the original value.
if (double.IsNaN(newValue) ||
double.IsInfinity(newValue) ||
MathUtilities.AreClose(newValue, double.MaxValue))
{
newValue = value;
}
}
else
{
newValue = Math.Round(value);
}
return newValue;
} }
/// <summary> /// <summary>
@ -250,73 +268,25 @@ namespace Avalonia.Layout
/// </remarks> /// </remarks>
public static double RoundLayoutValueUp(double value, double dpiScale) public static double RoundLayoutValueUp(double value, double dpiScale)
{ {
double newValue; // If DPI == 1, don't use DPI-aware rounding.
return MathUtilities.IsOne(dpiScale) ?
Math.Ceiling(value) :
Math.Ceiling(RoundTo8Digits(value) * dpiScale) / dpiScale;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double RoundTo8Digits(double value)
{
// Round the value to avoid FP errors. This is needed because if `value` has a floating // Round the value to avoid FP errors. This is needed because if `value` has a floating
// point precision error (e.g. 79.333333333333343) then when it's multiplied by // point precision error (e.g. 79.333333333333343) then when it's multiplied by
// `dpiScale` and rounded up, it will be rounded up to a value one greater than it // `dpiScale` and rounded up, it will be rounded up to a value one greater than it
// should be. // should be.
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
value = Math.Round(value, 8, MidpointRounding.ToZero); return Math.Round(value, 8, MidpointRounding.ToZero);
#else #else
// MidpointRounding.ToZero isn't available in netstandard2.0. // MidpointRounding.ToZero isn't available in netstandard2.0.
value = Math.Truncate(value * 1e8) / 1e8; return Math.Truncate(value * 1e8) / 1e8;
#endif #endif
// If DPI == 1, don't use DPI-aware rounding.
if (!MathUtilities.IsOne(dpiScale))
{
newValue = Math.Ceiling(value * dpiScale) / dpiScale;
// If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue),
// use the original value.
if (double.IsNaN(newValue) ||
double.IsInfinity(newValue) ||
MathUtilities.AreClose(newValue, double.MaxValue))
{
newValue = value;
}
}
else
{
newValue = Math.Ceiling(value);
}
return newValue;
}
/// <summary>
/// Calculates the min and max height for a control. Ported from WPF.
/// </summary>
private readonly struct MinMax
{
public MinMax(Layoutable e)
{
MaxHeight = e.MaxHeight;
MinHeight = e.MinHeight;
double l = e.Height;
double height = (double.IsNaN(l) ? double.PositiveInfinity : l);
MaxHeight = Math.Max(Math.Min(height, MaxHeight), MinHeight);
height = (double.IsNaN(l) ? 0 : l);
MinHeight = Math.Max(Math.Min(MaxHeight, height), MinHeight);
MaxWidth = e.MaxWidth;
MinWidth = e.MinWidth;
l = e.Width;
double width = (double.IsNaN(l) ? double.PositiveInfinity : l);
MaxWidth = Math.Max(Math.Min(width, MaxWidth), MinWidth);
width = (double.IsNaN(l) ? 0 : l);
MinWidth = Math.Max(Math.Min(MaxWidth, width), MinWidth);
}
public double MinWidth { get; }
public double MaxWidth { get; }
public double MinHeight { get; }
public double MaxHeight { get; }
} }
} }
} }

100
src/Avalonia.Base/Layout/Layoutable.cs

@ -2,6 +2,7 @@ using System;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
#nullable enable #nullable enable
@ -544,53 +545,43 @@ namespace Avalonia.Layout
if (useLayoutRounding) if (useLayoutRounding)
{ {
scale = LayoutHelper.GetLayoutScale(this); scale = LayoutHelper.GetLayoutScale(this);
margin = LayoutHelper.RoundLayoutThickness(margin, scale, scale); margin = LayoutHelper.RoundLayoutThickness(margin, scale);
} }
ApplyStyling(); ApplyStyling();
ApplyTemplate(); ApplyTemplate();
var minMax = new MinMax(this);
var constrained = LayoutHelper.ApplyLayoutConstraints( var constrained = LayoutHelper.ApplyLayoutConstraints(
this, minMax,
availableSize.Deflate(margin)); availableSize.Deflate(margin));
var measured = MeasureOverride(constrained); var measured = MeasureOverride(constrained);
var width = measured.Width; var width = MathUtilities.Clamp(measured.Width, minMax.MinWidth, minMax.MaxWidth);
var height = measured.Height; var height = MathUtilities.Clamp(measured.Height, minMax.MinHeight, minMax.MaxHeight);
if (useLayoutRounding)
{ {
double widthCache = Width; (width, height) = LayoutHelper.RoundLayoutSizeUp(new Size(width, height), scale);
if (!double.IsNaN(widthCache))
{
width = widthCache;
}
} }
width = Math.Min(width, MaxWidth); if (width > availableSize.Width)
width = Math.Max(width, MinWidth); width = availableSize.Width;
{ if (height > availableSize.Height)
double heightCache = Height; height = availableSize.Height;
if (!double.IsNaN(heightCache)) width += margin.Left + margin.Right;
{ height += margin.Top + margin.Bottom;
height = heightCache;
}
}
height = Math.Min(height, MaxHeight); if (width < 0)
height = Math.Max(height, MinHeight); width = 0;
if (useLayoutRounding) if (height < 0)
{ height = 0;
(width, height) = LayoutHelper.RoundLayoutSizeUp(new Size(width, height), scale, scale);
}
width = Math.Min(width, availableSize.Width);
height = Math.Min(height, availableSize.Height);
return NonNegative(new Size(width, height).Inflate(margin)); return new Size(width, height);
} }
else else
{ {
@ -618,8 +609,13 @@ namespace Avalonia.Layout
if (visual is Layoutable layoutable) if (visual is Layoutable layoutable)
{ {
layoutable.Measure(availableSize); layoutable.Measure(availableSize);
width = Math.Max(width, layoutable.DesiredSize.Width); var childSize = layoutable.DesiredSize;
height = Math.Max(height, layoutable.DesiredSize.Height);
if (childSize.Width > width)
width = childSize.Width;
if (childSize.Height > height)
height = childSize.Height;
} }
} }
@ -650,12 +646,19 @@ namespace Avalonia.Layout
// If the margin isn't pre-rounded some sizes will be offset by 1 pixel in certain scales. // If the margin isn't pre-rounded some sizes will be offset by 1 pixel in certain scales.
if (useLayoutRounding) if (useLayoutRounding)
{ {
margin = LayoutHelper.RoundLayoutThickness(margin, scale, scale); margin = LayoutHelper.RoundLayoutThickness(margin, scale);
} }
var availableSizeMinusMargins = new Size(
Math.Max(0, finalRect.Width - margin.Left - margin.Right), var availableWidthMinusMargins = finalRect.Width - margin.Left - margin.Right;
Math.Max(0, finalRect.Height - margin.Top - margin.Bottom)); if (availableWidthMinusMargins < 0)
availableWidthMinusMargins = 0;
var availableHeightMinusMargins = finalRect.Height - margin.Top - margin.Bottom;
if (availableHeightMinusMargins < 0)
availableHeightMinusMargins = 0;
var availableSizeMinusMargins = new Size(availableWidthMinusMargins, availableHeightMinusMargins);
var horizontalAlignment = HorizontalAlignment; var horizontalAlignment = HorizontalAlignment;
var verticalAlignment = VerticalAlignment; var verticalAlignment = VerticalAlignment;
var size = availableSizeMinusMargins; var size = availableSizeMinusMargins;
@ -670,12 +673,12 @@ namespace Avalonia.Layout
size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - margin.Top - margin.Bottom)); size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - margin.Top - margin.Bottom));
} }
size = LayoutHelper.ApplyLayoutConstraints(this, size); size = LayoutHelper.ApplyLayoutConstraints(new MinMax(this), size);
if (useLayoutRounding) if (useLayoutRounding)
{ {
size = LayoutHelper.RoundLayoutSizeUp(size, scale, scale); size = LayoutHelper.RoundLayoutSizeUp(size, scale);
availableSizeMinusMargins = LayoutHelper.RoundLayoutSizeUp(availableSizeMinusMargins, scale, scale); availableSizeMinusMargins = LayoutHelper.RoundLayoutSizeUp(availableSizeMinusMargins, scale);
} }
size = ArrangeOverride(size).Constrain(size); size = ArrangeOverride(size).Constrain(size);
@ -702,13 +705,14 @@ namespace Avalonia.Layout
break; break;
} }
var origin = new Point(originX, originY);
if (useLayoutRounding) if (useLayoutRounding)
{ {
originX = LayoutHelper.RoundLayoutValue(originX, scale); origin = LayoutHelper.RoundLayoutPoint(origin, scale);
originY = LayoutHelper.RoundLayoutValue(originY, scale);
} }
Bounds = new Rect(originX, originY, size.Width, size.Height); Bounds = new Rect(origin, size);
} }
} }
@ -887,11 +891,10 @@ namespace Avalonia.Layout
/// <returns>True if the rect is invalid; otherwise false.</returns> /// <returns>True if the rect is invalid; otherwise false.</returns>
private static bool IsInvalidRect(Rect rect) private static bool IsInvalidRect(Rect rect)
{ {
return rect.Width < 0 || rect.Height < 0 || return MathUtilities.IsNegativeOrNonFinite(rect.Width) ||
double.IsInfinity(rect.X) || double.IsInfinity(rect.Y) || MathUtilities.IsNegativeOrNonFinite(rect.Height) ||
double.IsInfinity(rect.Width) || double.IsInfinity(rect.Height) || !MathUtilities.IsFinite(rect.X) ||
double.IsNaN(rect.X) || double.IsNaN(rect.Y) || !MathUtilities.IsFinite(rect.Y);
double.IsNaN(rect.Width) || double.IsNaN(rect.Height);
} }
/// <summary> /// <summary>
@ -902,9 +905,8 @@ namespace Avalonia.Layout
/// <returns>True if the size is invalid; otherwise false.</returns> /// <returns>True if the size is invalid; otherwise false.</returns>
private static bool IsInvalidSize(Size size) private static bool IsInvalidSize(Size size)
{ {
return size.Width < 0 || size.Height < 0 || return MathUtilities.IsNegativeOrNonFinite(size.Width) ||
double.IsInfinity(size.Width) || double.IsInfinity(size.Height) || MathUtilities.IsNegativeOrNonFinite(size.Height);
double.IsNaN(size.Width) || double.IsNaN(size.Height);
} }
/// <summary> /// <summary>

51
src/Avalonia.Base/Layout/MinMax.cs

@ -0,0 +1,51 @@
using System.Runtime.CompilerServices;
namespace Avalonia.Layout;
internal struct MinMax
{
public double MinWidth;
public double MaxWidth;
public double MinHeight;
public double MaxHeight;
public MinMax(Layoutable e)
{
(MinWidth, MaxWidth) = CalcMinMax(e.Width, e.MinWidth, e.MaxWidth);
(MinHeight, MaxHeight) = CalcMinMax(e.Height, e.MinHeight, e.MaxHeight);
}
private static (double Min, double Max) CalcMinMax(double value, double min, double max)
{
double v0, v1;
if (double.IsNaN(value))
{
v0 = 0.0;
v1 = double.PositiveInfinity;
}
else
{
v0 = v1 = value;
}
max = ClampUnchecked(v1, min, max);
min = ClampUnchecked(v0, min, max);
return (min, max);
}
// Don't use Math.Clamp, it's possible for min to be greater than max here
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double ClampUnchecked(double value, double min, double max)
{
if (value > max)
value = max;
if (value < min)
value = min;
return value;
}
}

15
src/Avalonia.Base/Size.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Runtime.CompilerServices;
#if !BUILDTASK #if !BUILDTASK
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
#endif #endif
@ -187,11 +188,18 @@ namespace Avalonia
/// <param name="thickness">The thickness.</param> /// <param name="thickness">The thickness.</param>
/// <returns>The deflated size.</returns> /// <returns>The deflated size.</returns>
/// <remarks>The deflated size cannot be less than 0.</remarks> /// <remarks>The deflated size cannot be less than 0.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Size Deflate(Thickness thickness) public Size Deflate(Thickness thickness)
{ {
return new Size( var width = _width - thickness.Left - thickness.Right;
Math.Max(0, _width - thickness.Left - thickness.Right), if (width < 0)
Math.Max(0, _height - thickness.Top - thickness.Bottom)); width = 0;
var height = _height - thickness.Top - thickness.Bottom;
if (height < 0)
height = 0;
return new Size(width, height);
} }
/// <summary> /// <summary>
@ -247,6 +255,7 @@ namespace Avalonia
/// </summary> /// </summary>
/// <param name="thickness">The thickness.</param> /// <param name="thickness">The thickness.</param>
/// <returns>The inflated size.</returns> /// <returns>The inflated size.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Size Inflate(Thickness thickness) public Size Inflate(Thickness thickness)
{ {
return new Size( return new Size(

2
src/Avalonia.Base/StyledElement.cs

@ -85,7 +85,7 @@ namespace Avalonia
private string? _name; private string? _name;
private Classes? _classes; private Classes? _classes;
private ILogicalRoot? _logicalRoot; private ILogicalRoot? _logicalRoot;
private IAvaloniaList<ILogical>? _logicalChildren; private AvaloniaList<ILogical>? _logicalChildren;
private IResourceDictionary? _resources; private IResourceDictionary? _resources;
private Styles? _styles; private Styles? _styles;
private bool _stylesApplied; private bool _stylesApplied;

11
src/Avalonia.Base/Threading/Dispatcher.cs

@ -44,9 +44,18 @@ public partial class Dispatcher : IDispatcher
_exceptionFilterEventArgs = new DispatcherUnhandledExceptionFilterEventArgs(this); _exceptionFilterEventArgs = new DispatcherUnhandledExceptionFilterEventArgs(this);
} }
public static Dispatcher UIThread => s_uiThread ??= CreateUIThreadDispatcher(); public static Dispatcher UIThread
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
return s_uiThread ??= CreateUIThreadDispatcher();
}
}
public bool SupportsRunLoops => _controlledImpl != null; public bool SupportsRunLoops => _controlledImpl != null;
[MethodImpl(MethodImplOptions.NoInlining)]
private static Dispatcher CreateUIThreadDispatcher() private static Dispatcher CreateUIThreadDispatcher()
{ {
var impl = AvaloniaLocator.Current.GetService<IDispatcherImpl>(); var impl = AvaloniaLocator.Current.GetService<IDispatcherImpl>();

25
src/Avalonia.Base/Utilities/MathUtilities.cs

@ -208,6 +208,7 @@ namespace Avalonia.Utilities
/// <param name="min">The minimum value.</param> /// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param> /// <param name="max">The maximum value.</param>
/// <returns>The clamped value.</returns> /// <returns>The clamped value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double Clamp(double val, double min, double max) public static double Clamp(double val, double min, double max)
{ {
if (min > max) if (min > max)
@ -363,6 +364,28 @@ namespace Avalonia.Utilities
{ {
return GetMinMax(initialValue, initialValue + delta); return GetMinMax(initialValue, initialValue + delta);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsNegativeOrNonFinite(double d)
{
#if NET6_0_OR_GREATER
ulong bits = BitConverter.DoubleToUInt64Bits(d);
return bits >= 0x7FF0_0000_0000_0000;
#else
return d < 0 || !IsFinite(d);
#endif
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsFinite(double d)
{
#if NET6_0_OR_GREATER
return double.IsFinite(d);
#else
long bits = BitConverter.DoubleToInt64Bits(d);
return (bits & 0x7FFF_FFFF_FFFF_FFFF) < 0x7FF0_0000_0000_0000;
#endif
}
#if !BUILDTASK #if !BUILDTASK
internal static int WhichPolygonSideIntersects( internal static int WhichPolygonSideIntersects(
@ -451,7 +474,7 @@ namespace Avalonia.Utilities
return true; return true;
} }
#endif #endif
private static void ThrowCannotBeGreaterThanException<T>(T min, T max) private static void ThrowCannotBeGreaterThanException<T>(T min, T max)
{ {
throw new ArgumentException($"{min} cannot be greater than {max}."); throw new ArgumentException($"{min} cannot be greater than {max}.");

16
src/Avalonia.Base/Visual.cs

@ -148,6 +148,8 @@ namespace Avalonia
/// </summary> /// </summary>
public Visual() public Visual()
{ {
_visualRoot = this as IRenderRoot;
// Disable transitions until we're added to the visual tree. // Disable transitions until we're added to the visual tree.
DisableTransitions(); DisableTransitions();
@ -317,16 +319,12 @@ namespace Avalonia
/// <summary> /// <summary>
/// Gets the control's child visuals. /// Gets the control's child visuals.
/// </summary> /// </summary>
protected internal IAvaloniaList<Visual> VisualChildren protected internal IAvaloniaList<Visual> VisualChildren { get; }
{
get;
private set;
}
/// <summary> /// <summary>
/// Gets the root of the visual tree, if the control is attached to a visual tree. /// Gets the root of the visual tree, if the control is attached to a visual tree.
/// </summary> /// </summary>
protected internal IRenderRoot? VisualRoot => _visualRoot ?? (this as IRenderRoot); protected internal IRenderRoot? VisualRoot => _visualRoot;
internal RenderOptions RenderOptions { get; set; } internal RenderOptions RenderOptions { get; set; }
@ -544,7 +542,7 @@ namespace Avalonia
{ {
Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Detached from visual tree"); Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Detached from visual tree");
_visualRoot = null; _visualRoot = this as IRenderRoot;
RootedVisualChildrenCount--; RootedVisualChildrenCount--;
if (RenderTransform is IMutableTransform mutableTransform) if (RenderTransform is IMutableTransform mutableTransform)
@ -683,9 +681,9 @@ namespace Avalonia
var old = _visualParent; var old = _visualParent;
_visualParent = value; _visualParent = value;
if (_visualRoot != null) if (_visualRoot is not null && old is not null)
{ {
var e = new VisualTreeAttachmentEventArgs(old!, _visualRoot); var e = new VisualTreeAttachmentEventArgs(old, _visualRoot);
OnDetachedFromVisualTreeCore(e); OnDetachedFromVisualTreeCore(e);
} }

2
src/Avalonia.Controls/Border.cs

@ -156,7 +156,7 @@ namespace Avalonia.Controls
var borderThickness = BorderThickness; var borderThickness = BorderThickness;
if (UseLayoutRounding) if (UseLayoutRounding)
borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, _scale, _scale); borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, _scale);
_layoutThickness = borderThickness; _layoutThickness = borderThickness;
} }

18
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -525,7 +525,7 @@ namespace Avalonia.Controls.Presenters
var borderThickness = BorderThickness; var borderThickness = BorderThickness;
if (UseLayoutRounding) if (UseLayoutRounding)
borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, _scale, _scale); borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, _scale);
_layoutThickness = borderThickness; _layoutThickness = borderThickness;
} }
@ -618,8 +618,8 @@ namespace Avalonia.Controls.Presenters
if (useLayoutRounding) if (useLayoutRounding)
{ {
padding = LayoutHelper.RoundLayoutThickness(padding, scale, scale); padding = LayoutHelper.RoundLayoutThickness(padding, scale);
borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, scale, scale); borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, scale);
} }
padding += borderThickness; padding += borderThickness;
@ -642,8 +642,8 @@ namespace Avalonia.Controls.Presenters
if (useLayoutRounding) if (useLayoutRounding)
{ {
sizeForChild = LayoutHelper.RoundLayoutSizeUp(sizeForChild, scale, scale); sizeForChild = LayoutHelper.RoundLayoutSizeUp(sizeForChild, scale);
availableSize = LayoutHelper.RoundLayoutSizeUp(availableSize, scale, scale); availableSize = LayoutHelper.RoundLayoutSizeUp(availableSize, scale);
} }
switch (horizontalContentAlignment) switch (horizontalContentAlignment)
@ -666,14 +666,14 @@ namespace Avalonia.Controls.Presenters
break; break;
} }
var origin = new Point(originX, originY);
if (useLayoutRounding) if (useLayoutRounding)
{ {
originX = LayoutHelper.RoundLayoutValue(originX, scale); origin = LayoutHelper.RoundLayoutPoint(origin, scale);
originY = LayoutHelper.RoundLayoutValue(originY, scale);
} }
var boundsForChild = var boundsForChild = new Rect(origin, sizeForChild).Deflate(padding);
new Rect(originX, originY, sizeForChild.Width, sizeForChild.Height).Deflate(padding);
Child.Arrange(boundsForChild); Child.Arrange(boundsForChild);

2
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -511,7 +511,7 @@ namespace Avalonia.Controls.Presenters
if (Child.UseLayoutRounding) if (Child.UseLayoutRounding)
{ {
var scale = LayoutHelper.GetLayoutScale(Child); var scale = LayoutHelper.GetLayoutScale(Child);
childMargin = LayoutHelper.RoundLayoutThickness(childMargin, scale, scale); childMargin = LayoutHelper.RoundLayoutThickness(childMargin, scale);
} }
var extent = Child!.Bounds.Size.Inflate(childMargin); var extent = Child!.Bounds.Size.Inflate(childMargin);

3
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -291,7 +291,6 @@ namespace Avalonia.Controls.Primitives
public sealed override void ApplyTemplate() public sealed override void ApplyTemplate()
{ {
var template = Template; var template = Template;
var logical = (ILogical)this;
// Apply the template if it is not the same as the template already applied - except // Apply the template if it is not the same as the template already applied - except
// for in the case that the template is null and we're not attached to the logical // for in the case that the template is null and we're not attached to the logical
@ -299,7 +298,7 @@ namespace Avalonia.Controls.Primitives
// the template has been detached, so we want to wait until it's re-attached to the // the template has been detached, so we want to wait until it's re-attached to the
// logical tree as if it's re-attached to the same tree the template will be the same // logical tree as if it's re-attached to the same tree the template will be the same
// and we don't need to do anything. // and we don't need to do anything.
if (_appliedTemplate != template && (template != null || logical.IsAttachedToLogicalTree)) if (_appliedTemplate != template && (template != null || ((ILogical)this).IsAttachedToLogicalTree))
{ {
if (VisualChildren.Count > 0) if (VisualChildren.Count > 0)
{ {

8
src/Avalonia.Controls/TextBlock.cs

@ -605,7 +605,7 @@ namespace Avalonia.Controls
} }
var scale = LayoutHelper.GetLayoutScale(this); var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); var padding = LayoutHelper.RoundLayoutThickness(Padding, scale);
var top = padding.Top; var top = padding.Top;
var textHeight = TextLayout.Height; var textHeight = TextLayout.Height;
@ -709,7 +709,7 @@ namespace Avalonia.Controls
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
var scale = LayoutHelper.GetLayoutScale(this); var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); var padding = LayoutHelper.RoundLayoutThickness(Padding, scale);
var deflatedSize = availableSize.Deflate(padding); var deflatedSize = availableSize.Deflate(padding);
if (_constraint != deflatedSize) if (_constraint != deflatedSize)
@ -740,7 +740,7 @@ namespace Avalonia.Controls
//This implicitly recreated the TextLayout with a new constraint if we previously reset it. //This implicitly recreated the TextLayout with a new constraint if we previously reset it.
var textLayout = TextLayout; var textLayout = TextLayout;
var size = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.MinTextWidth, textLayout.Height).Inflate(padding), 1, 1); var size = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.MinTextWidth, textLayout.Height).Inflate(padding), 1);
return size; return size;
} }
@ -748,7 +748,7 @@ namespace Avalonia.Controls
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
var scale = LayoutHelper.GetLayoutScale(this); var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); var padding = LayoutHelper.RoundLayoutThickness(Padding, scale);
var availableSize = finalSize.Deflate(padding); var availableSize = finalSize.Deflate(padding);

27
src/Avalonia.Controls/TopLevel.cs

@ -137,6 +137,7 @@ namespace Avalonia.Controls
private readonly IDisposable? _pointerOverPreProcessorSubscription; private readonly IDisposable? _pointerOverPreProcessorSubscription;
private readonly IDisposable? _backGestureSubscription; private readonly IDisposable? _backGestureSubscription;
private readonly Dictionary<AvaloniaProperty, Action> _platformImplBindings = new(); private readonly Dictionary<AvaloniaProperty, Action> _platformImplBindings = new();
private double _scaling;
private Size _clientSize; private Size _clientSize;
private Size? _frameSize; private Size? _frameSize;
private WindowTransparencyLevel _actualTransparencyLevel; private WindowTransparencyLevel _actualTransparencyLevel;
@ -218,6 +219,7 @@ namespace Avalonia.Controls
PlatformImpl = impl ?? throw new InvalidOperationException( PlatformImpl = impl ?? throw new InvalidOperationException(
"Could not create window implementation: maybe no windowing subsystem was initialized?"); "Could not create window implementation: maybe no windowing subsystem was initialized?");
_scaling = ValidateScaling(impl.RenderScaling);
_actualTransparencyLevel = PlatformImpl.TransparencyLevel; _actualTransparencyLevel = PlatformImpl.TransparencyLevel;
dependencyResolver ??= AvaloniaLocator.Current; dependencyResolver ??= AvaloniaLocator.Current;
@ -555,11 +557,10 @@ namespace Avalonia.Controls
return control.GetValue(AutoSafeAreaPaddingProperty); return control.GetValue(AutoSafeAreaPaddingProperty);
} }
/// <inheritdoc/> double ILayoutRoot.LayoutScaling => _scaling;
double ILayoutRoot.LayoutScaling => PlatformImpl?.RenderScaling ?? 1;
/// <inheritdoc/> /// <inheritdoc/>
public double RenderScaling => PlatformImpl?.RenderScaling ?? 1; public double RenderScaling => _scaling;
IStyleHost IStyleHost.StylingParent => _globalStyles!; IStyleHost IStyleHost.StylingParent => _globalStyles!;
@ -717,6 +718,7 @@ namespace Avalonia.Controls
Debug.Assert(PlatformImpl != null); Debug.Assert(PlatformImpl != null);
// The PlatformImpl is completely invalid at this point // The PlatformImpl is completely invalid at this point
PlatformImpl = null; PlatformImpl = null;
_scaling = 1.0;
if (_globalStyles is object) if (_globalStyles is object)
{ {
@ -768,6 +770,7 @@ namespace Avalonia.Controls
/// <param name="scaling">The window scaling.</param> /// <param name="scaling">The window scaling.</param>
private void HandleScalingChanged(double scaling) private void HandleScalingChanged(double scaling)
{ {
_scaling = ValidateScaling(scaling);
LayoutHelper.InvalidateSelfAndChildrenMeasure(this); LayoutHelper.InvalidateSelfAndChildrenMeasure(this);
Dispatcher.UIThread.Send(_ => ScalingChanged?.Invoke(this, EventArgs.Empty)); Dispatcher.UIThread.Send(_ => ScalingChanged?.Invoke(this, EventArgs.Empty));
} }
@ -971,6 +974,24 @@ namespace Avalonia.Controls
ITextInputMethodImpl? ITextInputMethodRoot.InputMethod => PlatformImpl?.TryGetFeature<ITextInputMethodImpl>(); ITextInputMethodImpl? ITextInputMethodRoot.InputMethod => PlatformImpl?.TryGetFeature<ITextInputMethodImpl>();
private double ValidateScaling(double scaling)
{
if (MathUtilities.IsNegativeOrNonFinite(scaling) || MathUtilities.IsZero(scaling))
{
throw new InvalidOperationException(
$"Invalid {nameof(ITopLevelImpl.RenderScaling)} value {scaling} returned from {PlatformImpl?.GetType()}");
}
if (MathUtilities.IsOne(scaling))
{
// Ensure we've got exactly 1.0 and not an approximation,
// so we don't have to use MathUtilities.IsOne in various layout hot paths.
return 1.0;
}
return scaling;
}
/// <summary> /// <summary>
/// Provides layout pass timing from the layout manager to the renderer, for diagnostics purposes. /// Provides layout pass timing from the layout manager to the renderer, for diagnostics purposes.
/// </summary> /// </summary>

14
tests/Avalonia.Benchmarks/Layout/Measure.cs

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
@ -31,12 +32,23 @@ namespace Avalonia.Benchmarks.Layout
[Benchmark, MethodImpl(MethodImplOptions.NoInlining)] [Benchmark, MethodImpl(MethodImplOptions.NoInlining)]
public void Remeasure() public void Remeasure()
{ {
_root.InvalidateMeasure();
foreach (var control in _controls) foreach (var control in _controls)
{ {
control.InvalidateMeasure(); // Use an unsafe accessor instead of InvalidateMeasure, otherwise a lot of time is spent invalidating
// controls, which we don't want: this benchmark is supposed to be focused on Measure/Arrange.
SetIsMeasureValid(control, false);
SetIsArrangeValid(control, false);
} }
_root.LayoutManager.ExecuteLayoutPass(); _root.LayoutManager.ExecuteLayoutPass();
} }
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_" + nameof(Layoutable.IsMeasureValid))]
private static extern void SetIsMeasureValid(Layoutable layoutable, bool value);
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_" + nameof(Layoutable.IsArrangeValid))]
private static extern void SetIsArrangeValid(Layoutable layoutable, bool value);
} }
} }

5
tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs

@ -301,7 +301,10 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.Measure(new Size(1000, 1000)); target.Measure(new Size(1000, 1000));
target.Arrange(new Rect(0, 0, 1000, 1000)); target.Arrange(new Rect(0, 0, 1000, 1000));
Assert.Equal(new Size(176.00000000000003, 176.00000000000003), target.Child!.DesiredSize); var nonRoundedVieViewport = target.Child!.Bounds.Size.Inflate(
LayoutHelper.RoundLayoutThickness(target.Child.Margin, root.LayoutScaling));
Assert.Equal(new Size(176.00000000000003, 176.00000000000003), nonRoundedVieViewport);
Assert.Equal(new Size(176, 176), target.Viewport); Assert.Equal(new Size(176, 176), target.Viewport);
Assert.Equal(new Size(176, 176), target.Extent); Assert.Equal(new Size(176, 176), target.Extent);
} }

6
tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

@ -66,7 +66,7 @@ namespace Avalonia.Controls.UnitTests
var textLayout = textBlock.TextLayout; var textLayout = textBlock.TextLayout;
var constraint = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.MinTextWidth, textLayout.Height), 1, 1); var constraint = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.MinTextWidth, textLayout.Height), 1);
Assert.Equal(textBlock.DesiredSize, constraint); Assert.Equal(textBlock.DesiredSize, constraint);
} }
@ -83,7 +83,7 @@ namespace Avalonia.Controls.UnitTests
var textLayout = textBlock.TextLayout; var textLayout = textBlock.TextLayout;
var constraint = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.WidthIncludingTrailingWhitespace, textLayout.Height), 1, 1); var constraint = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.WidthIncludingTrailingWhitespace, textLayout.Height), 1);
textBlock.Arrange(new Rect(constraint)); textBlock.Arrange(new Rect(constraint));
@ -118,7 +118,7 @@ namespace Avalonia.Controls.UnitTests
var textLayout = textBlock.TextLayout; var textLayout = textBlock.TextLayout;
var constraint = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.WidthIncludingTrailingWhitespace, textLayout.Height), 1, 1); var constraint = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.WidthIncludingTrailingWhitespace, textLayout.Height), 1);
Assert.Equal(constraint, textBlock.DesiredSize); Assert.Equal(constraint, textBlock.DesiredSize);
} }

11
tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs

@ -239,6 +239,7 @@ namespace Avalonia.Controls.UnitTests
var renderer = new Mock<IWindowBaseImpl>(); var renderer = new Mock<IWindowBaseImpl>();
if (setupAllProperties) if (setupAllProperties)
renderer.SetupAllProperties(); renderer.SetupAllProperties();
renderer.Setup(x => x.RenderScaling).Returns(1.0);
renderer.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor()); renderer.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
return renderer; return renderer;
} }
@ -248,18 +249,10 @@ namespace Avalonia.Controls.UnitTests
public bool IsClosed { get; private set; } public bool IsClosed { get; private set; }
public TestWindowBase() public TestWindowBase()
: base(CreateWindowsBaseImplMock()) : base(CreateMockWindowBaseImpl().Object)
{ {
} }
private static IWindowBaseImpl CreateWindowsBaseImplMock()
{
var compositor = RendererMocks.CreateDummyCompositor();
return Mock.Of<IWindowBaseImpl>(x =>
x.RenderScaling == 1 &&
x.Compositor == compositor);
}
public TestWindowBase(IWindowBaseImpl impl) public TestWindowBase(IWindowBaseImpl impl)
: base(impl) : base(impl)
{ {

1
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -18,6 +18,7 @@ namespace Avalonia.Controls.UnitTests
public void Setting_Title_Should_Set_Impl_Title() public void Setting_Title_Should_Set_Impl_Title()
{ {
var windowImpl = new Mock<IWindowImpl>(); var windowImpl = new Mock<IWindowImpl>();
windowImpl.Setup(r => r.RenderScaling).Returns(1.0);
windowImpl.Setup(r => r.Compositor).Returns(RendererMocks.CreateDummyCompositor()); windowImpl.Setup(r => r.Compositor).Returns(RendererMocks.CreateDummyCompositor());
var windowingPlatform = new MockWindowingPlatform(() => windowImpl.Object); var windowingPlatform = new MockWindowingPlatform(() => windowImpl.Object);

Loading…
Cancel
Save