From b094699f760b1f760174d841fd3a5fddfb7e663d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 19:36:06 +0300 Subject: [PATCH 01/61] Compositor works with X11. Somewhat --- samples/ControlCatalog.NetCore/Program.cs | 1 + src/Avalonia.Base/Avalonia.Base.csproj | 5 +- .../Collections/IAvaloniaReadOnlyList.cs | 1 + .../Collections/Pooled/PooledList.cs | 2 +- src/Avalonia.Base/Matrix.cs | 39 + .../Animations/AnimatedValueStore.cs | 40 + .../Animations/CompositionAnimation.cs | 63 ++ .../Animations/CompositionAnimationGroup.cs | 26 + .../Animations/ExpressionAnimation.cs | 34 + .../Animations/ExpressionAnimationInstance.cs | 44 ++ .../Animations/IAnimationInstance.cs | 11 + .../Animations/ICompositionAnimationBase.cs | 12 + .../Animations/ImplicitAnimationCollection.cs | 73 ++ .../Composition/Animations/Interpolators.cs | 73 ++ .../Animations/KeyFrameAnimation.cs | 53 ++ .../Animations/KeyFrameAnimationInstance.cs | 139 ++++ .../Composition/Animations/KeyFrames.cs | 80 ++ .../Animations/PropertySetSnapshot.cs | 46 ++ .../Composition/CompositingRenderer.cs | 194 +++++ .../Composition/CompositionDrawListVisual.cs | 44 ++ .../Composition/CompositionEasingFunction.cs | 97 +++ .../Composition/CompositionGradientBrush.cs | 16 + .../Composition/CompositionObject.cs | 125 +++ .../Composition/CompositionPropertySet.cs | 132 ++++ .../Composition/CompositionTarget.cs | 107 +++ .../Rendering/Composition/Compositor.cs | 143 ++++ .../Composition/CompositorRenderLoopTask.cs | 20 + .../Rendering/Composition/ContainerVisual.cs | 20 + .../Rendering/Composition/CustomDrawVisual.cs | 56 ++ .../Drawing/CompositionDrawList.cs | 81 ++ .../Drawing/CompositionDrawingContext.cs | 383 +++++++++ .../Rendering/Composition/Enums.cs | 120 +++ .../Expressions/BuiltInExpressionFfi.cs | 234 ++++++ .../Expressions/DelegateExpressionFfi.cs | 181 +++++ .../Composition/Expressions/Expression.cs | 331 ++++++++ .../ExpressionEvaluationContext.cs | 31 + .../Expressions/ExpressionParseException.cs | 14 + .../Expressions/ExpressionParser.cs | 298 +++++++ .../Expressions/ExpressionVariant.cs | 739 ++++++++++++++++++ .../Composition/Expressions/TokenParser.cs | 256 ++++++ .../Composition/ICompositionSurface.cs | 9 + .../Rendering/Composition/MatrixUtils.cs | 46 ++ .../Composition/Server/DrawingContextProxy.cs | 142 ++++ .../Composition/Server/ReadbackIndices.cs | 46 ++ .../Server/ServerCompositionBrush.cs | 7 + .../Server/ServerCompositionDrawListVisual.cs | 41 + .../Server/ServerCompositionGradientBrush.cs | 15 + .../ServerCompositionLinearGradientBrush.cs | 22 + .../Server/ServerCompositionSurface.cs | 9 + .../Server/ServerCompositionTarget.cs | 53 ++ .../Composition/Server/ServerCompositor.cs | 90 +++ .../Server/ServerContainerVisual.cs | 29 + .../Server/ServerCustomDrawVisual.cs | 31 + .../Composition/Server/ServerList.cs | 46 ++ .../Composition/Server/ServerObject.cs | 36 + .../Server/ServerSolidColorVisual.cs | 16 + .../Composition/Server/ServerSpriteVisual.cs | 20 + .../Composition/Server/ServerVisual.cs | 92 +++ .../Rendering/Composition/Transport/Batch.cs | 37 + .../Rendering/Composition/Transport/Change.cs | 82 ++ .../Composition/Transport/ChangeSet.cs | 36 + .../Composition/Transport/ChangeSetPool.cs | 42 + .../Transport/CustomDrawVisualChanges.cs | 20 + .../Transport/DrawListVisualChanges.cs | 48 ++ .../Composition/Transport/ListChange.cs | 19 + .../Composition/Transport/ListChangeSet.cs | 25 + .../Transport/ServerListProxyHelper.cs | 92 +++ .../Composition/Transport/VisualChanges.cs | 16 + .../Composition/Utils/CubicBezier.cs | 302 +++++++ .../Rendering/Composition/Utils/MathExt.cs | 23 + .../Rendering/Composition/Visual.cs | 64 ++ .../Rendering/Composition/VisualCollection.cs | 64 ++ src/Avalonia.Base/Rendering/IRenderer.cs | 6 + .../SceneGraph/DeferredDrawingContextImpl.cs | 1 + src/Avalonia.Base/Size.cs | 11 + .../Threading/DispatcherPriority.cs | 14 +- src/Avalonia.Base/Visual.cs | 25 +- src/Avalonia.Base/composition-schema.xml | 70 ++ .../HeadlessPlatformRenderInterface.cs | 2 + .../CompositionRoslynGenerator.cs | 21 + .../CompositionGenerator/Config.cs | 116 +++ .../CompositionGenerator/Extensions.cs | 90 +++ .../Generator.KeyFrameAnimation.cs | 59 ++ .../Generator.ListProxy.cs | 121 +++ .../CompositionGenerator/Generator.Utils.cs | 66 ++ .../CompositionGenerator/Generator.cs | 504 ++++++++++++ src/Avalonia.X11/X11Platform.cs | 8 +- src/Avalonia.X11/X11Window.cs | 17 +- .../Media/DrawingContextImpl.cs | 7 + 89 files changed, 7106 insertions(+), 16 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionObject.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Compositor.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Enums.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/Change.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Visual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/VisualCollection.cs create mode 100644 src/Avalonia.Base/composition-schema.xml create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 4464413e63..2a0755b900 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -111,6 +111,7 @@ namespace ControlCatalog.NetCore EnableMultiTouch = true, UseDBusMenu = true, EnableIme = true, + UseCompositor = true }) .With(new Win32PlatformOptions { diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 8e4755b4b7..bcebdd504c 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -3,10 +3,13 @@ net6.0;netstandard2.0 Avalonia.Base Avalonia - True + True + true + $(BaseIntermediateOutputPath)\GeneratedFiles + diff --git a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs index a6a5953827..cefbf642be 100644 --- a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs +++ b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs index 200a52fb0d..ffda1bedca 100644 --- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -1434,7 +1434,7 @@ namespace Avalonia.Collections.Pooled /// /// Returns the internal buffers to the ArrayPool. /// - public void Dispose() + public virtual void Dispose() { ReturnArray(); _size = 0; diff --git a/src/Avalonia.Base/Matrix.cs b/src/Avalonia.Base/Matrix.cs index b08a0eb98a..6f00b08d13 100644 --- a/src/Avalonia.Base/Matrix.cs +++ b/src/Avalonia.Base/Matrix.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Numerics; using Avalonia.Utilities; namespace Avalonia @@ -106,6 +107,36 @@ namespace Avalonia (value1._m31 * value2.M12) + (value1._m32 * value2.M22) + value2._m32); } + public static Matrix operator +(Matrix value1, Matrix value2) + { + return new Matrix(value1.M11 + value2.M11, + value1.M12 + value2.M12, + value1.M21 + value2.M21, + value1.M22 + value2.M22, + value1.M31 + value2.M31, + value1.M32 + value2.M32); + } + + public static Matrix operator -(Matrix value1, Matrix value2) + { + return new Matrix(value1.M11 - value2.M11, + value1.M12 - value2.M12, + value1.M21 - value2.M21, + value1.M22 - value2.M22, + value1.M31 - value2.M31, + value1.M32 - value2.M32); + } + + public static Matrix operator *(Matrix value1, double value2) + { + return new Matrix(value1.M11 * value2, + value1.M12 * value2, + value1.M21 * value2, + value1.M22 * value2, + value1.M31 * value2, + value1.M32 * value2); + } + /// /// Negates the given matrix by multiplying all values by -1. /// @@ -427,6 +458,14 @@ namespace Avalonia return true; } +#if !BUILDTASK + public static implicit operator Matrix4x4(Matrix m) + { + return new Matrix4x4(new Matrix3x2((float)m._m11, (float)m._m12, (float)m._m21, (float)m._m22, + (float)m._m31, (float)m._m32)); + } +#endif + public struct Decomposed { public Vector Translate; diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs new file mode 100644 index 0000000000..180c45022f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs @@ -0,0 +1,40 @@ +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal struct AnimatedValueStore where T : struct + { + private T _direct; + private IAnimationInstance _animation; + private T? _lastAnimated; + + public T Direct => _direct; + + public T GetAnimated(ServerCompositor compositor) + { + if (_animation == null) + return _direct; + var v = _animation.Evaluate(compositor.ServerNow, ExpressionVariant.Create(_direct)) + .CastOrDefault(); + _lastAnimated = v; + return v; + } + + private T LastAnimated => _animation != null ? _lastAnimated ?? _direct : _direct; + + public bool IsAnimation => _animation != null; + + public void SetAnimation(ChangeSet cs, IAnimationInstance animation) + { + _animation = animation; + _animation.Start(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated)); + } + + public static implicit operator AnimatedValueStore(T value) => new AnimatedValueStore() + { + _direct = value + }; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs new file mode 100644 index 0000000000..9375faaaae --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -0,0 +1,63 @@ +// ReSharper disable InconsistentNaming +// ReSharper disable CheckNamespace + +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Animations +{ + public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase + { + private readonly CompositionPropertySet _propertySet; + internal CompositionAnimation(Compositor compositor) : base(compositor, null!) + { + _propertySet = new CompositionPropertySet(compositor); + } + + private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); + + public void ClearAllParameters() => _propertySet.ClearAll(); + + public void ClearParameter(string key) => _propertySet.Clear(key); + + void SetVariant(string key, ExpressionVariant value) => _propertySet.Set(key, value); + + public void SetColorParameter(string key, Avalonia.Media.Color value) => SetVariant(key, value); + + public void SetMatrix3x2Parameter(string key, Matrix3x2 value) => SetVariant(key, value); + + public void SetMatrix4x4Parameter(string key, Matrix4x4 value) => SetVariant(key, value); + + public void SetQuaternionParameter(string key, Quaternion value) => SetVariant(key, value); + + public void SetReferenceParameter(string key, CompositionObject compositionObject) => + _propertySet.Set(key, compositionObject); + + public void SetScalarParameter(string key, float value) => SetVariant(key, value); + + public void SetVector2Parameter(string key, Vector2 value) => SetVariant(key, value); + + public void SetVector3Parameter(string key, Vector3 value) => SetVariant(key, value); + + public void SetVector4Parameter(string key, Vector4 value) => SetVariant(key, value); + + // TODO: void SetExpressionReferenceParameter(string parameterName, IAnimationObject source) + + public string? Target { get; set; } + + internal abstract IAnimationInstance CreateInstance(ServerObject targetObject, + ExpressionVariant? finalValue); + + internal PropertySetSnapshot CreateSnapshot(bool server) + => _propertySet.Snapshot(server, 1); + + void ICompositionAnimationBase.InternalOnly() + { + + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs new file mode 100644 index 0000000000..833f7e498c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Transport; + + +namespace Avalonia.Rendering.Composition.Animations +{ + public class CompositionAnimationGroup : CompositionObject, ICompositionAnimationBase + { + internal List Animations { get; } = new List(); + void ICompositionAnimationBase.InternalOnly() + { + + } + + public void Add(CompositionAnimation value) => Animations.Add(value); + public void Remove(CompositionAnimation value) => Animations.Remove(value); + public void RemoveAll() => Animations.Clear(); + + public CompositionAnimationGroup(Compositor compositor) : base(compositor, null!) + { + } + + private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs new file mode 100644 index 0000000000..a6f24c2e35 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs @@ -0,0 +1,34 @@ +// ReSharper disable CheckNamespace +using System; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations +{ + public class ExpressionAnimation : CompositionAnimation + { + private string? _expression; + private Expression? _parsedExpression; + + internal ExpressionAnimation(Compositor compositor) : base(compositor) + { + } + + public string? Expression + { + get => _expression; + set + { + _expression = value; + _parsedExpression = null; + } + } + + private Expression ParsedExpression => _parsedExpression ??= ExpressionParser.Parse(_expression.AsSpan()); + + internal override IAnimationInstance CreateInstance( + ServerObject targetObject, ExpressionVariant? finalValue) + => new ExpressionAnimationInstance(ParsedExpression, + targetObject, finalValue, CreateSnapshot(true)); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs new file mode 100644 index 0000000000..47b947b2e9 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs @@ -0,0 +1,44 @@ +using System; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal class ExpressionAnimationInstance : IAnimationInstance + { + private readonly Expression _expression; + private readonly IExpressionObject _target; + private ExpressionVariant _startingValue; + private readonly ExpressionVariant? _finalValue; + private readonly PropertySetSnapshot _parameters; + + public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + { + var ctx = new ExpressionEvaluationContext + { + Parameters = _parameters, + Target = _target, + ForeignFunctionInterface = BuiltInExpressionFfi.Instance, + StartingValue = _startingValue, + FinalValue = _finalValue ?? _startingValue, + CurrentValue = currentValue + }; + return _expression.Evaluate(ref ctx); + } + + public void Start(TimeSpan startedAt, ExpressionVariant startingValue) + { + _startingValue = startingValue; + } + + public ExpressionAnimationInstance(Expression expression, + IExpressionObject target, + ExpressionVariant? finalValue, + PropertySetSnapshot parameters) + { + _expression = expression; + _target = target; + _finalValue = finalValue; + _parameters = parameters; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs new file mode 100644 index 0000000000..a0b066ae0c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs @@ -0,0 +1,11 @@ +using System; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal interface IAnimationInstance + { + ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue); + void Start(TimeSpan startedAt, ExpressionVariant startingValue); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs new file mode 100644 index 0000000000..bf40fd3ad2 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs @@ -0,0 +1,12 @@ +// ReSharper disable CheckNamespace + +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations +{ + public interface ICompositionAnimationBase + { + internal void InternalOnly(); + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs new file mode 100644 index 0000000000..be91352527 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Animations +{ + public class ImplicitAnimationCollection : CompositionObject, IDictionary + { + private Dictionary _inner = new Dictionary(); + private IDictionary _innerface; + internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null!) + { + _innerface = _inner; + } + + private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); + + public IEnumerator> GetEnumerator() => _inner.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _inner).GetEnumerator(); + + void ICollection>.Add(KeyValuePair item) => _innerface.Add(item); + + public void Clear() => _inner.Clear(); + + bool ICollection>.Contains(KeyValuePair item) => _innerface.Contains(item); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => _innerface.CopyTo(array, arrayIndex); + + bool ICollection>.Remove(KeyValuePair item) => _innerface.Remove(item); + + public int Count => _inner.Count; + + bool ICollection>.IsReadOnly => _innerface.IsReadOnly; + + public void Add(string key, ICompositionAnimationBase value) => _inner.Add(key, value); + + public bool ContainsKey(string key) => _inner.ContainsKey(key); + + public bool Remove(string key) => _inner.Remove(key); + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out ICompositionAnimationBase value) => + _inner.TryGetValue(key, out value); + + public ICompositionAnimationBase this[string key] + { + get => _inner[key]; + set => _inner[key] = value; + } + + ICollection IDictionary.Keys => _innerface.Keys; + + ICollection IDictionary.Values => + _innerface.Values; + + // UWP compat + public uint Size => (uint) Count; + + public IReadOnlyDictionary GetView() => + new Dictionary(this); + + public bool HasKey(string key) => ContainsKey(key); + public void Insert(string key, ICompositionAnimationBase animation) => Add(key, animation); + + public ICompositionAnimationBase? Lookup(string key) + { + _inner.TryGetValue(key, out var rv); + return rv; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs new file mode 100644 index 0000000000..62b790701a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs @@ -0,0 +1,73 @@ +using System; +using System.Numerics; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal interface IInterpolator + { + T Interpolate(T from, T to, float progress); + } + + class ScalarInterpolator : IInterpolator + { + public float Interpolate(float @from, float to, float progress) => @from + (to - @from) * progress; + + public static ScalarInterpolator Instance { get; } = new ScalarInterpolator(); + } + + class Vector2Interpolator : IInterpolator + { + public Vector2 Interpolate(Vector2 @from, Vector2 to, float progress) + => Vector2.Lerp(@from, to, progress); + + public static Vector2Interpolator Instance { get; } = new Vector2Interpolator(); + } + + class Vector3Interpolator : IInterpolator + { + public Vector3 Interpolate(Vector3 @from, Vector3 to, float progress) + => Vector3.Lerp(@from, to, progress); + + public static Vector3Interpolator Instance { get; } = new Vector3Interpolator(); + } + + class Vector4Interpolator : IInterpolator + { + public Vector4 Interpolate(Vector4 @from, Vector4 to, float progress) + => Vector4.Lerp(@from, to, progress); + + public static Vector4Interpolator Instance { get; } = new Vector4Interpolator(); + } + + class QuaternionInterpolator : IInterpolator + { + public Quaternion Interpolate(Quaternion @from, Quaternion to, float progress) + => Quaternion.Lerp(@from, to, progress); + + public static QuaternionInterpolator Instance { get; } = new QuaternionInterpolator(); + } + + class ColorInterpolator : IInterpolator + { + static byte Lerp(float a, float b, float p) => (byte) Math.Max(0, Math.Min(255, (p * (b - a) + a))); + + public static Avalonia.Media.Color + LerpRGB(Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => + new Avalonia.Media.Color(Lerp(to.A, @from.A, progress), + Lerp(to.R, @from.R, progress), + Lerp(to.G, @from.G, progress), + Lerp(to.B, @from.B, progress)); + + public Avalonia.Media.Color Interpolate(Avalonia.Media.Color @from, Avalonia.Media.Color to, float progress) + => LerpRGB(@from, to, progress); + + public static ColorInterpolator Instance { get; } = new ColorInterpolator(); + } + + class BooleanInterpolator : IInterpolator + { + public bool Interpolate(bool @from, bool to, float progress) => progress >= 1 ? to : @from; + + public static BooleanInterpolator Instance { get; } = new BooleanInterpolator(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs new file mode 100644 index 0000000000..065dfd7a8e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs @@ -0,0 +1,53 @@ +namespace Avalonia.Rendering.Composition.Animations +{ + public abstract class KeyFrameAnimation : CompositionAnimation + { + internal KeyFrameAnimation(Compositor compositor) : base(compositor) + { + } + + public AnimationDelayBehavior DelayBehavior { get; set; } + public System.TimeSpan DelayTime { get; set; } + public AnimationDirection Direction { get; set; } + public System.TimeSpan Duration { get; set; } + public AnimationIterationBehavior IterationBehavior { get; set; } + public int IterationCount { get; set; } = 1; + public AnimationStopBehavior StopBehavior { get; set; } + + private protected abstract IKeyFrames KeyFrames { get; } + + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, + CompositionEasingFunction easingFunction) => + KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, easingFunction); + + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value) + => KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, new LinearEasingFunction(Compositor)); + } + + public enum AnimationDelayBehavior + { + SetInitialValueAfterDelay, + SetInitialValueBeforeDelay + } + + public enum AnimationDirection + { + Normal, + Reverse, + Alternate, + AlternateReverse + } + + public enum AnimationIterationBehavior + { + Count, + Forever + } + + public enum AnimationStopBehavior + { + LeaveCurrentValue, + SetToInitialValue, + SetToFinalValue + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs new file mode 100644 index 0000000000..b90a02148d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs @@ -0,0 +1,139 @@ +using System; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + class KeyFrameAnimationInstance : IAnimationInstance where T : struct + { + private readonly IInterpolator _interpolator; + private readonly ServerKeyFrame[] _keyFrames; + private readonly PropertySetSnapshot _snapshot; + private readonly ExpressionVariant? _finalValue; + private readonly IExpressionObject _target; + private readonly AnimationDelayBehavior _delayBehavior; + private readonly TimeSpan _delayTime; + private readonly AnimationDirection _direction; + private readonly TimeSpan _duration; + private readonly AnimationIterationBehavior _iterationBehavior; + private readonly int _iterationCount; + private readonly AnimationStopBehavior _stopBehavior; + private TimeSpan _startedAt; + private T _startingValue; + + public KeyFrameAnimationInstance( + IInterpolator interpolator, ServerKeyFrame[] keyFrames, + PropertySetSnapshot snapshot, ExpressionVariant? finalValue, + IExpressionObject target, + AnimationDelayBehavior delayBehavior, TimeSpan delayTime, + AnimationDirection direction, TimeSpan duration, + AnimationIterationBehavior iterationBehavior, + int iterationCount, AnimationStopBehavior stopBehavior) + { + _interpolator = interpolator; + _keyFrames = keyFrames; + _snapshot = snapshot; + _finalValue = finalValue; + _target = target; + _delayBehavior = delayBehavior; + _delayTime = delayTime; + _direction = direction; + _duration = duration; + _iterationBehavior = iterationBehavior; + _iterationCount = iterationCount; + _stopBehavior = stopBehavior; + if (_keyFrames.Length == 0) + throw new InvalidOperationException("Animation has no key frames"); + if(_duration.Ticks <= 0) + throw new InvalidOperationException("Invalid animation duration"); + } + + public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + { + var elapsed = now - _startedAt; + var starting = ExpressionVariant.Create(_startingValue); + var ctx = new ExpressionEvaluationContext + { + Parameters = _snapshot, + Target = _target, + CurrentValue = currentValue, + FinalValue = _finalValue ?? starting, + StartingValue = starting, + ForeignFunctionInterface = BuiltInExpressionFfi.Instance + }; + + if (elapsed < _delayTime) + { + if (_delayBehavior == AnimationDelayBehavior.SetInitialValueBeforeDelay) + return ExpressionVariant.Create(GetKeyFrame(ref ctx, _keyFrames[0])); + return currentValue; + } + + elapsed -= _delayTime; + var iterationNumber = elapsed.Ticks / _duration.Ticks; + if (_iterationBehavior == AnimationIterationBehavior.Count + && iterationNumber >= _iterationCount) + return ExpressionVariant.Create(GetKeyFrame(ref ctx, _keyFrames[_keyFrames.Length - 1])); + + + var evenIterationNumber = iterationNumber % 2 == 0; + elapsed = TimeSpan.FromTicks(elapsed.Ticks % _duration.Ticks); + + var reverse = + _direction == AnimationDirection.Alternate + ? !evenIterationNumber + : _direction == AnimationDirection.AlternateReverse + ? evenIterationNumber + : _direction == AnimationDirection.Reverse; + + var iterationProgress = elapsed.TotalSeconds / _duration.TotalSeconds; + if (reverse) + iterationProgress = 1 - iterationProgress; + + var left = new ServerKeyFrame + { + Value = _startingValue + }; + var right = _keyFrames[_keyFrames.Length - 1]; + for (var c = 0; c < _keyFrames.Length; c++) + { + var kf = _keyFrames[c]; + if (kf.Key < iterationProgress) + { + // this is the last frame + if (c == _keyFrames.Length - 1) + return ExpressionVariant.Create(GetKeyFrame(ref ctx, kf)); + + left = kf; + right = _keyFrames[c + 1]; + break; + } + } + + var keyProgress = Math.Max(0, Math.Min(1, (iterationProgress - left.Key) / (right.Key - left.Key))); + + var easedKeyProgress = right.EasingFunction.Ease((float) keyProgress); + if (float.IsNaN(easedKeyProgress) || float.IsInfinity(easedKeyProgress)) + return currentValue; + + return ExpressionVariant.Create(_interpolator.Interpolate( + GetKeyFrame(ref ctx, left), + GetKeyFrame(ref ctx, right), + easedKeyProgress + )); + } + + T GetKeyFrame(ref ExpressionEvaluationContext ctx, ServerKeyFrame f) + { + if (f.Expression != null) + return f.Expression.Evaluate(ref ctx).CastOrDefault(); + else + return f.Value; + } + + public void Start(TimeSpan startedAt, ExpressionVariant startingValue) + { + _startedAt = startedAt; + _startingValue = startingValue.CastOrDefault(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs new file mode 100644 index 0000000000..d7f2504061 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + class KeyFrames : List>, IKeyFrames + { + void Validate(float key) + { + if (key < 0 || key > 1) + throw new ArgumentException("Key frame key"); + if (Count > 0 && this[Count - 1].NormalizedProgressKey > key) + throw new ArgumentException("Key frame key " + key + " is less than the previous one"); + } + + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, + CompositionEasingFunction easingFunction) + { + Validate(normalizedProgressKey); + Add(new KeyFrame + { + NormalizedProgressKey = normalizedProgressKey, + Expression = Expression.Parse(value), + EasingFunction = easingFunction + }); + } + + public void Insert(float normalizedProgressKey, T value, CompositionEasingFunction easingFunction) + { + Validate(normalizedProgressKey); + Add(new KeyFrame + { + NormalizedProgressKey = normalizedProgressKey, + Value = value, + EasingFunction = easingFunction + }); + } + + public ServerKeyFrame[] Snapshot() + { + var frames = new ServerKeyFrame[Count]; + for (var c = 0; c < Count; c++) + { + var f = this[c]; + frames[c] = new ServerKeyFrame + { + Expression = f.Expression, + Value = f.Value, + EasingFunction = f.EasingFunction.Snapshot(), + Key = f.NormalizedProgressKey + }; + } + return frames; + } + } + + struct KeyFrame + { + public float NormalizedProgressKey; + public T Value; + public Expression Expression; + public CompositionEasingFunction EasingFunction; + } + + struct ServerKeyFrame + { + public T Value; + public Expression Expression; + public IEasingFunction EasingFunction; + public float Key; + } + + + + interface IKeyFrames + { + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, CompositionEasingFunction easingFunction); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs new file mode 100644 index 0000000000..ca703dfc6f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal class PropertySetSnapshot : IExpressionParameterCollection, IExpressionObject + { + private readonly Dictionary _dic; + + public struct Value + { + public ExpressionVariant Variant; + public IExpressionObject Object; + + public Value(IExpressionObject o) + { + Object = o; + Variant = default; + } + + public static implicit operator Value(ExpressionVariant v) => new Value + { + Variant = v + }; + } + + public PropertySetSnapshot(Dictionary dic) + { + _dic = dic; + } + + public ExpressionVariant GetParameter(string name) + { + _dic.TryGetValue(name, out var v); + return v.Variant; + } + + public IExpressionObject GetObjectParameter(string name) + { + _dic.TryGetValue(name, out var v); + return v.Object; + } + + public ExpressionVariant GetProperty(string name) => GetParameter(name); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs new file mode 100644 index 0000000000..07ac54b634 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using Avalonia.Collections; +using Avalonia.Media; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.Composition; + +public class CompositingRenderer : RendererBase, IRendererWithCompositor +{ + private readonly IRenderRoot _root; + private readonly Compositor _compositor; + private readonly IDeferredRendererLock? _rendererLock; + CompositionDrawingContext _recorder = new(); + DrawingContext _recordingContext; + private HashSet _dirty = new(); + private HashSet _recalculateChildren = new(); + private readonly CompositionTarget _target; + private bool _queuedUpdate; + private Action _update; + + public CompositingRenderer(IRenderRoot root, + Compositor compositor, + IDeferredRendererLock? rendererLock = null) + { + _root = root; + _compositor = compositor; + _recordingContext = new DrawingContext(_recorder); + _rendererLock = rendererLock ?? new ManagedDeferredRendererLock(); + _target = compositor.CreateCompositionTarget(root.CreateRenderTarget); + _target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor); + _update = Update; + } + + public bool DrawFps { get; set; } + public bool DrawDirtyRects { get; set; } + public event EventHandler? SceneInvalidated; + + void QueueUpdate() + { + if(_queuedUpdate) + return; + _queuedUpdate = true; + Dispatcher.UIThread.Post(_update, DispatcherPriority.Composition); + } + public void AddDirty(IVisual visual) + { + _dirty.Add((Visual)visual); + QueueUpdate(); + } + + public IEnumerable HitTest(Point p, IVisual root, Func filter) + { + var res = _target.TryHitTest(new Vector2((float)p.X, (float)p.Y)); + if(res == null) + yield break; + for (var index = res.Count - 1; index >= 0; index--) + { + var v = res[index]; + if (v is CompositionDrawListVisual dv) + { + if (filter == null || filter(dv.Visual)) + yield return dv.Visual; + } + } + } + + public IVisual? HitTestFirst(Point p, IVisual root, Func filter) + { + // TODO: Optimize + return HitTest(p, root, filter).FirstOrDefault(); + } + + public void RecalculateChildren(IVisual visual) + { + _recalculateChildren.Add((Visual)visual); + QueueUpdate(); + } + + private void SyncChildren(Visual v) + { + //TODO: Optimize by moving that logic to Visual itself + if(v.CompositionVisual == null) + return; + var compositionChildren = v.CompositionVisual.Children; + var visualChildren = (AvaloniaList)v.GetVisualChildren(); + if (compositionChildren.Count == visualChildren.Count) + { + bool mismatch = false; + for(var c=0; c _target.IsEnabled = true; + + public void Stop() + { + _target.IsEnabled = true; + } + + public void Dispose() + { + Stop(); + _target.Dispose(); + // Wait for the composition batch to be applied and rendered to guarantee that + // render target is not used anymore and can be safely disposed + _compositor.RequestCommitAsync().Wait(); + } + + + public Compositor Compositor => _compositor; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs new file mode 100644 index 0000000000..9f02055412 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -0,0 +1,44 @@ +using System.Numerics; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition; + +internal class CompositionDrawListVisual : CompositionContainerVisual +{ + public Visual Visual { get; } + + private new DrawListVisualChanges Changes => (DrawListVisualChanges)base.Changes; + private CompositionDrawList? _drawList; + public CompositionDrawList? DrawList + { + get => _drawList; + set + { + _drawList?.Dispose(); + _drawList = value; + Changes.DrawCommands = value?.Clone(); + } + } + + private protected override IChangeSetPool ChangeSetPool => DrawListVisualChanges.Pool; + + internal CompositionDrawListVisual(Compositor compositor, ServerCompositionContainerVisual server, Visual visual) : base(compositor, server) + { + Visual = visual; + } + + internal override bool HitTest(Vector2 point) + { + if (DrawList == null) + return false; + var pt = new Point(point.X, point.Y); + if (Visual is ICustomHitTest custom) + return custom.HitTest(pt); + foreach (var op in DrawList) + if (op.Item.HitTest(pt)) + return true; + return false; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs new file mode 100644 index 0000000000..73db243e93 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs @@ -0,0 +1,97 @@ +using System; +using System.Numerics; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Rendering.Composition.Utils; + +namespace Avalonia.Rendering.Composition +{ + public abstract class CompositionEasingFunction : CompositionObject + { + internal CompositionEasingFunction(Compositor compositor) : base(compositor, null!) + { + } + + private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); + + internal abstract IEasingFunction Snapshot(); + } + + internal interface IEasingFunction + { + float Ease(float progress); + } + + public sealed class DelegateCompositionEasingFunction : CompositionEasingFunction + { + private readonly Easing _func; + + public delegate float EasingDelegate(float progress); + + internal DelegateCompositionEasingFunction(Compositor compositor, EasingDelegate func) : base(compositor) + { + _func = new Easing(func); + } + + class Easing : IEasingFunction + { + private readonly EasingDelegate _func; + + public Easing(EasingDelegate func) + { + _func = func; + } + + public float Ease(float progress) => _func(progress); + } + + internal override IEasingFunction Snapshot() => _func; + } + + public class LinearEasingFunction : CompositionEasingFunction + { + public LinearEasingFunction(Compositor compositor) : base(compositor) + { + } + + class Linear : IEasingFunction + { + public float Ease(float progress) => progress; + } + + private static readonly Linear Instance = new Linear(); + internal override IEasingFunction Snapshot() => Instance; + } + + public class CubicBezierEasingFunction : CompositionEasingFunction + { + private CubicBezier _bezier; + public Vector2 ControlPoint1 { get; } + public Vector2 ControlPoint2 { get; } + //cubic-bezier(0.25, 0.1, 0.25, 1.0) + internal CubicBezierEasingFunction(Compositor compositor, Vector2 controlPoint1, Vector2 controlPoint2) : base(compositor) + { + ControlPoint1 = controlPoint1; + ControlPoint2 = controlPoint2; + if (controlPoint1.X < 0 || controlPoint1.X > 1 || controlPoint2.X < 0 || controlPoint2.X > 1) + throw new ArgumentException(); + _bezier = new CubicBezier(controlPoint1.X, controlPoint1.Y, controlPoint2.X, controlPoint2.Y); + } + + class EasingFunction : IEasingFunction + { + private readonly CubicBezier _bezier; + + public EasingFunction(CubicBezier bezier) + { + _bezier = bezier; + } + + public float Ease(float progress) => (float)_bezier.Solve(progress); + } + + internal static IEasingFunction Ease { get; } = new EasingFunction(new CubicBezier(0.25, 0.1, 0.25, 1)); + + internal override IEasingFunction Snapshot() => new EasingFunction(_bezier); + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs new file mode 100644 index 0000000000..cf222550dd --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs @@ -0,0 +1,16 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public partial class CompositionGradientBrush : CompositionBrush + { + internal CompositionGradientBrush(Compositor compositor, ServerCompositionGradientBrush server) : base(compositor, server) + { + ColorStops = new CompositionGradientStopCollection(compositor, server.Stops); + } + + public CompositionGradientStopCollection ColorStops { get; } + } + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs new file mode 100644 index 0000000000..2417ecaba8 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -0,0 +1,125 @@ +using System; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition +{ + public abstract class CompositionObject : IDisposable, IExpressionObject + { + public ImplicitAnimationCollection? ImplicitAnimations { get; set; } + internal CompositionObject(Compositor compositor, ServerObject server) + { + Compositor = compositor; + Server = server; + } + + public Compositor Compositor { get; } + internal ServerObject Server { get; } + public bool IsDisposed { get; private set; } + private ChangeSet? _changes; + + private static void ThrowInvalidOperation() => + throw new InvalidOperationException("There is no server-side counterpart for this object"); + + private protected ChangeSet Changes + { + get + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (Server == null) ThrowInvalidOperation(); + var currentBatch = Compositor.CurrentBatch; + if (_changes != null && _changes.Batch != currentBatch) + _changes = null; + if (_changes == null) + { + _changes = ChangeSetPool.Get(Server!, currentBatch); + currentBatch.Changes!.Add(_changes); + Compositor.QueueImplicitBatchCommit(); + } + + return _changes; + } + } + + private protected abstract IChangeSetPool ChangeSetPool { get; } + + public void Dispose() + { + Changes.Dispose = true; + IsDisposed = true; + } + + internal virtual ExpressionVariant GetPropertyForAnimation(string name) + { + return default; + } + + ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); + + public void StartAnimation(string propertyName, CompositionAnimation animation) + => StartAnimation(propertyName, animation, null); + + internal virtual void StartAnimation(string propertyName, CompositionAnimation animation, ExpressionVariant? finalValue = null) + { + throw new ArgumentException("Unknown property " + propertyName); + } + + public void StartAnimationGroup(ICompositionAnimationBase grp) + { + if (grp is CompositionAnimation animation) + { + if(animation.Target == null) + throw new ArgumentException("Animation Target can't be null"); + StartAnimation(animation.Target, animation); + } + else if (grp is CompositionAnimationGroup group) + { + foreach (var a in group.Animations) + { + if (a.Target == null) + throw new ArgumentException("Animation Target can't be null"); + StartAnimation(a.Target, a); + } + } + } + + bool StartAnimationGroupPart(CompositionAnimation animation, string target, ExpressionVariant finalValue) + { + if(animation.Target == null) + throw new ArgumentException("Animation Target can't be null"); + if (animation.Target == target) + { + StartAnimation(animation.Target, animation, finalValue); + return true; + } + else + { + StartAnimation(animation.Target, animation); + return false; + } + } + + internal bool StartAnimationGroup(ICompositionAnimationBase grp, string target, ExpressionVariant finalValue) + { + if (grp is CompositionAnimation animation) + return StartAnimationGroupPart(animation, target, finalValue); + if (grp is CompositionAnimationGroup group) + { + var matched = false; + foreach (var a in group.Animations) + { + if (a.Target == null) + throw new ArgumentException("Animation Target can't be null"); + if (StartAnimationGroupPart(a, target, finalValue)) + matched = true; + } + + return matched; + } + + throw new ArgumentException(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs new file mode 100644 index 0000000000..004c2676ff --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition +{ + public class CompositionPropertySet : CompositionObject + { + private readonly Dictionary _variants = new Dictionary(); + private readonly Dictionary _objects = new Dictionary(); + + internal CompositionPropertySet(Compositor compositor) : base(compositor, null!) + { + } + + private protected override IChangeSetPool ChangeSetPool => throw new NotSupportedException(); + + internal void Set(string key, ExpressionVariant value) + { + _objects.Remove(key); + _variants[key] = value; + } + + internal void Set(string key, CompositionObject obj) + { + _objects[key] = obj ?? throw new ArgumentNullException(nameof(obj)); + _variants.Remove(key); + } + public void InsertColor(string propertyName, Avalonia.Media.Color value) => Set(propertyName, value); + + public void InsertMatrix3x2(string propertyName, Matrix3x2 value) => Set(propertyName, value); + + public void InsertMatrix4x4(string propertyName, Matrix4x4 value) => Set(propertyName, value); + + public void InsertQuaternion(string propertyName, Quaternion value) => Set(propertyName, value); + + public void InsertScalar(string propertyName, float value) => Set(propertyName, value); + public void InsertVector2(string propertyName, Vector2 value) => Set(propertyName, value); + + public void InsertVector3(string propertyName, Vector3 value) => Set(propertyName, value); + + public void InsertVector4(string propertyName, Vector4 value) => Set(propertyName, value); + + + CompositionGetValueStatus TryGetVariant(string key, out T value) where T : struct + { + value = default; + if (!_variants.TryGetValue(key, out var v)) + return _objects.ContainsKey(key) + ? CompositionGetValueStatus.TypeMismatch + : CompositionGetValueStatus.NotFound; + + return v.TryCast(out value) ? CompositionGetValueStatus.Succeeded : CompositionGetValueStatus.TypeMismatch; + } + + public CompositionGetValueStatus TryGetColor(string propertyName, out Avalonia.Media.Color value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetMatrix3x2(string propertyName, out Matrix3x2 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetMatrix4x4(string propertyName, out Matrix4x4 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetQuaternion(string propertyName, out Quaternion value) + => TryGetVariant(propertyName, out value); + + + public CompositionGetValueStatus TryGetScalar(string propertyName, out float value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetVector2(string propertyName, out Vector2 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetVector3(string propertyName, out Vector3 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetVector4(string propertyName, out Vector4 value) + => TryGetVariant(propertyName, out value); + + + public void InsertBoolean(string propertyName, bool value) => Set(propertyName, value); + + public CompositionGetValueStatus TryGetBoolean(string propertyName, out bool value) + => TryGetVariant(propertyName, out value); + + internal void ClearAll() + { + _objects.Clear(); + _variants.Clear(); + } + + internal void Clear(string key) + { + _objects.Remove(key); + _variants.Remove(key); + } + + internal PropertySetSnapshot Snapshot(bool server, int allowedNestingLevel) + { + var dic = new Dictionary(_objects.Count + _variants.Count); + foreach (var o in _objects) + { + if (o.Value is CompositionPropertySet ps) + { + if (allowedNestingLevel <= 0) + throw new InvalidOperationException("PropertySet depth limit reached"); + dic[o.Key] = new PropertySetSnapshot.Value(ps.Snapshot(server, allowedNestingLevel - 1)); + } + else if (o.Value.Server == null) + throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed"); + else + dic[o.Key] = new PropertySetSnapshot.Value(server ? (IExpressionObject) o.Value.Server : o.Value); + } + + foreach (var v in _variants) + dic[v.Key] = v.Value; + + return new PropertySetSnapshot(dic); + } + } + + public enum CompositionGetValueStatus + { + Succeeded, + TypeMismatch, + NotFound + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs new file mode 100644 index 0000000000..a8835ca668 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Collections.Pooled; + +namespace Avalonia.Rendering.Composition +{ + public partial class CompositionTarget + { + partial void OnRootChanged() + { + if (Root != null) + Root.Root = this; + } + + partial void OnRootChanging() + { + if (Root != null) + Root.Root = null; + } + + public PooledList? TryHitTest(Vector2 point) + { + Server.Readback.NextRead(); + if (Root == null) + return null; + var res = new PooledList(); + HitTestCore(Root, point, res); + return res; + } + + public Vector2? TryTransformToVisual(CompositionVisual visual, Vector2 point) + { + if (visual.Root != this) + return null; + var v = visual; + var m = Matrix3x2.Identity; + while (v != null) + { + if (!TryGetInvertedTransform(v, out var cm)) + return null; + m = m * cm; + v = v.Parent; + } + + return Vector2.Transform(point, m); + } + + bool TryGetInvertedTransform(CompositionVisual visual, out Matrix3x2 matrix) + { + var m = visual.TryGetServerTransform(); + if (m == null) + { + matrix = default; + return false; + } + + // TODO: Use Matrix3x3 + var m32 = new Matrix3x2(m.Value.M11, m.Value.M12, m.Value.M21, m.Value.M22, m.Value.M41, m.Value.M42); + + return Matrix3x2.Invert(m32, out matrix); + } + + bool TryTransformTo(CompositionVisual visual, ref Vector2 v) + { + if (TryGetInvertedTransform(visual, out var m)) + { + v = Vector2.Transform(v, m); + return true; + } + + return false; + } + + bool HitTestCore(CompositionVisual visual, Vector2 point, PooledList result) + { + //TODO: Check readback too + if (visual.Visible == false) + return false; + if (!TryTransformTo(visual, ref point)) + return false; + if (point.X >= 0 && point.Y >= 0 && point.X <= visual.Size.X && point.Y <= visual.Size.Y) + { + bool success = false; + // Hit-test the current node + if (visual.HitTest(point)) + { + result.Add(visual); + success = true; + } + + // Inspect children too + if(visual is CompositionContainerVisual cv) + for (var c = cv.Children.Count - 1; c >= 0; c--) + { + var ch = cv.Children[c]; + var hit = HitTestCore(ch, point, result); + if (hit) + return true; + } + + return success; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs new file mode 100644 index 0000000000..217d8dd803 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using System.Threading.Tasks; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Threading; + + +namespace Avalonia.Rendering.Composition +{ + public partial class Compositor + { + private ServerCompositor _server; + private Batch _currentBatch; + private bool _implicitBatchCommitQueued; + private Action _implicitBatchCommit; + + internal Batch CurrentBatch => _currentBatch; + internal ServerCompositor Server => _server; + internal CompositionEasingFunction DefaultEasing { get; } + + private Compositor(ServerCompositor server) + { + _server = server; + _currentBatch = new Batch(); + _implicitBatchCommit = ImplicitBatchCommit; + DefaultEasing = new CubicBezierEasingFunction(this, + new Vector2(0.25f, 0.1f), new Vector2(0.25f, 1f)); + } + + public CompositionTarget CreateCompositionTarget(Func renderTargetFactory) + { + return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory)); + } + + public Task RequestCommitAsync() + { + var batch = CurrentBatch; + _currentBatch = new Batch(); + batch.CommitedAt = Server.Clock.Elapsed; + _server.EnqueueBatch(batch); + return batch.Completed; + } + + public static Compositor Create(IRenderLoop timer) + { + return new Compositor(new ServerCompositor(timer)); + } + + public void Dispose() + { + + } + + public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); + + public CompositionSolidColorVisual CreateSolidColorVisual() => new CompositionSolidColorVisual(this, + new ServerCompositionSolidColorVisual(_server)); + + public CompositionSolidColorVisual CreateSolidColorVisual(Avalonia.Media.Color color) + { + var v = new CompositionSolidColorVisual(this, new ServerCompositionSolidColorVisual(_server)); + v.Color = color; + return v; + } + + public CompositionSpriteVisual CreateSpriteVisual() => new CompositionSpriteVisual(this, new ServerCompositionSpriteVisual(_server)); + + public CompositionLinearGradientBrush CreateLinearGradientBrush() + => new CompositionLinearGradientBrush(this, new ServerCompositionLinearGradientBrush(_server)); + + public CompositionColorGradientStop CreateColorGradientStop() + => new CompositionColorGradientStop(this, new ServerCompositionColorGradientStop(_server)); + + public CompositionColorGradientStop CreateColorGradientStop(float offset, Avalonia.Media.Color color) + { + var stop = CreateColorGradientStop(); + stop.Offset = offset; + stop.Color = color; + return stop; + } + + // We want to make it 100% async later + /* + public CompositionBitmapSurface LoadBitmapSurface(Stream stream) + { + var bmp = _server.Backend.LoadCpuMemoryBitmap(stream); + return new CompositionBitmapSurface(this, bmp); + } + + public async Task LoadBitmapSurfaceAsync(Stream stream) + { + var bmp = await Task.Run(() => _server.Backend.LoadCpuMemoryBitmap(stream)); + return new CompositionBitmapSurface(this, bmp); + } + */ + public CompositionColorBrush CreateColorBrush(Avalonia.Media.Color color) => + new CompositionColorBrush(this, new ServerCompositionColorBrush(_server)) {Color = color}; + + public CompositionSurfaceBrush CreateSurfaceBrush() => + new CompositionSurfaceBrush(this, new ServerCompositionSurfaceBrush(_server)); + + /* + public CompositionGaussianBlurEffectBrush CreateGaussianBlurEffectBrush() => + new CompositionGaussianBlurEffectBrush(this, new ServerCompositionGaussianBlurEffectBrush(_server)); + + public CompositionBackdropBrush CreateBackdropBrush() => + new CompositionBackdropBrush(this, new ServerCompositionBackdropBrush(Server));*/ + + public ExpressionAnimation CreateExpressionAnimation() => new ExpressionAnimation(this); + + public ExpressionAnimation CreateExpressionAnimation(string expression) => new ExpressionAnimation(this) + { + Expression = expression + }; + + public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this); + + public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this); + + internal CustomDrawVisual CreateCustomDrawVisual(ICustomDrawVisualRenderer renderer, + ICustomDrawVisualHitTest? hitTest = null) where T : IEquatable => + new CustomDrawVisual(this, renderer, hitTest); + + public void QueueImplicitBatchCommit() + { + if(_implicitBatchCommitQueued) + return; + _implicitBatchCommitQueued = true; + Dispatcher.UIThread.Post(_implicitBatchCommit, DispatcherPriority.CompositionBatch); + } + + private void ImplicitBatchCommit() + { + _implicitBatchCommitQueued = false; + RequestCommitAsync(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs b/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs new file mode 100644 index 0000000000..074c0a9ccf --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs @@ -0,0 +1,20 @@ +using System; + +namespace Avalonia.Rendering.Composition; + +partial class Compositor +{ + class CompositorRenderLoopTask : IRenderLoopTask + { + public bool NeedsUpdate { get; } + public void Update(TimeSpan time) + { + throw new NotImplementedException(); + } + + public void Render() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs new file mode 100644 index 0000000000..f650d3e995 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs @@ -0,0 +1,20 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public class CompositionContainerVisual : CompositionVisual + { + public CompositionVisualCollection Children { get; } + internal CompositionContainerVisual(Compositor compositor, ServerCompositionContainerVisual server) : base(compositor, server) + { + Children = new CompositionVisualCollection(this, server.Children); + } + + private protected override void OnRootChanged() + { + foreach (var ch in Children) + ch.Root = Root; + base.OnRootChanged(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs new file mode 100644 index 0000000000..0505d6a46c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs @@ -0,0 +1,56 @@ +using System; +using System.Numerics; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition +{ + internal class CustomDrawVisual : CompositionContainerVisual where TData : IEquatable + { + private readonly ICustomDrawVisualHitTest? _hitTest; + + internal CustomDrawVisual(Compositor compositor, ICustomDrawVisualRenderer renderer, + ICustomDrawVisualHitTest? hitTest) : base(compositor, + new ServerCustomDrawVisual(compositor.Server, renderer)) + { + _hitTest = hitTest; + } + + private TData? _data; + + static bool Eq(TData? left, TData? right) + { + if (left == null && right == null) + return true; + if (left == null) + return false; + return left.Equals(right); + } + + public TData? Data + { + get => _data; + set + { + if (!Eq(_data, value)) + { + ((CustomDrawVisualChanges) Changes).Data.Value = value; + _data = value; + } + } + } + + private protected override IChangeSetPool ChangeSetPool => CustomDrawVisualChanges.Pool; + } + + public interface ICustomDrawVisualRenderer + { + void Render(IDrawingContextImpl canvas, TData? data); + } + + public interface ICustomDrawVisualHitTest + { + bool HitTest(TData data, Vector2 vector2); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs new file mode 100644 index 0000000000..aca8ef7c46 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs @@ -0,0 +1,81 @@ +using System; +using Avalonia.Collections.Pooled; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal class CompositionDrawList : PooledList> +{ + public CompositionDrawList() + { + + } + + public CompositionDrawList(int capacity) : base(capacity) + { + + } + + public override void Dispose() + { + foreach(var item in this) + item.Dispose(); + base.Dispose(); + } + + public CompositionDrawList Clone() + { + var clone = new CompositionDrawList(Count); + foreach (var r in this) + clone.Add(r.Clone()); + return clone; + } +} + +internal class CompositionDrawListBuilder +{ + private CompositionDrawList? _operations; + private bool _owns; + + public void Reset(CompositionDrawList? previousOperations) + { + _operations = previousOperations; + _owns = false; + } + + public CompositionDrawList DrawOperations => _operations ?? new CompositionDrawList(); + + void MakeWritable(int atIndex) + { + if(_owns) + return; + _owns = true; + var newOps = new CompositionDrawList(_operations?.Count ?? Math.Max(1, atIndex)); + if (_operations != null) + { + for (var c = 0; c < atIndex; c++) + newOps.Add(_operations[c].Clone()); + } + + _operations = newOps; + } + + public void ReplaceDrawOperation(int index, IDrawOperation node) + { + MakeWritable(index); + DrawOperations.Add(RefCountable.Create(node)); + } + + public void AddDrawOperation(IDrawOperation node) + { + MakeWritable(DrawOperations.Count); + DrawOperations.Add(RefCountable.Create(node)); + } + + public void TrimTo(int count) + { + if (count < DrawOperations.Count) + DrawOperations.RemoveRange(count, DrawOperations.Count - count); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs new file mode 100644 index 0000000000..c8e5d9e064 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -0,0 +1,383 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; +using Avalonia.VisualTree; +namespace Avalonia.Rendering.Composition; + +internal class CompositionDrawingContext : IDrawingContextImpl +{ + private CompositionDrawListBuilder _builder = new(); + private int _drawOperationIndex; + + /// + public Matrix Transform { get; set; } = Matrix.Identity; + + /// + public void Clear(Color color) + { + // Cannot clear a deferred scene. + } + + /// + public void Dispose() + { + // Nothing to do here since we allocate no unmanaged resources. + } + + public void BeginUpdate(CompositionDrawList list) + { + _builder.Reset(list); + _drawOperationIndex = 0; + } + + public CompositionDrawList EndUpdate() + { + _builder.TrimTo(_drawOperationIndex); + return _builder.DrawOperations!; + } + + /// + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) + { + Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode) + { + var next = NextDrawAs(); + + if (next == null || + !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)) + { + Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) + { + // This method is currently only used to composite layers so shouldn't be called here. + throw new NotSupportedException(); + } + + /// + public void DrawLine(IPen pen, Point p1, Point p2) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) + { + Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, + BoxShadows boxShadows = default) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) + { + Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, material, rect)) + { + Add(new ExperimentalAcrylicNode(Transform, material, rect)); + } + else + { + ++_drawOperationIndex; + } + } + + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) + { + Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); + } + else + { + ++_drawOperationIndex; + } + } + + public void Custom(ICustomDrawOperation custom) + { + var next = NextDrawAs(); + if (next == null || !next.Item.Equals(Transform, custom)) + Add(new CustomDrawOperation(custom, Transform)); + else + ++_drawOperationIndex; + } + + /// + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) + { + Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); + } + + else + { + ++_drawOperationIndex; + } + } + + public IDrawingContextLayerImpl CreateLayer(Size size) + { + throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); + } + + /// + public void PopClip() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new ClipNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopGeometryClip() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new GeometryClipNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopBitmapBlendMode() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new BitmapBlendModeNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopOpacity() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new OpacityNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopOpacityMask() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null, null)) + { + Add(new OpacityMaskNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushClip(Rect clip) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new ClipNode(Transform, clip)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushClip(RoundedRect clip) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new ClipNode(Transform, clip)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushGeometryClip(IGeometryImpl? clip) + { + if (clip is null) + return; + + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new GeometryClipNode(Transform, clip)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushOpacity(double opacity) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(opacity)) + { + Add(new OpacityNode(opacity)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushOpacityMask(IBrush mask, Rect bounds) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(mask, bounds)) + { + Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(blendingMode)) + { + Add(new BitmapBlendModeNode(blendingMode)); + } + else + { + ++_drawOperationIndex; + } + } + + private void Add(T node) where T : class, IDrawOperation + { + if (_drawOperationIndex < _builder!.DrawOperations.Count) + { + _builder.ReplaceDrawOperation(_drawOperationIndex, node); + } + else + { + _builder.AddDrawOperation(node); + } + + ++_drawOperationIndex; + } + + private IRef? NextDrawAs() where T : class, IDrawOperation + { + return _drawOperationIndex < _builder!.DrawOperations.Count + ? _builder.DrawOperations[_drawOperationIndex] as IRef + : null; + } + + private IDictionary? CreateChildScene(IBrush? brush) + { + /* + var visualBrush = brush as VisualBrush; + + if (visualBrush != null) + { + var visual = visualBrush.Visual; + + if (visual != null) + { + (visual as IVisualBrushInitialize)?.EnsureInitialized(); + var scene = new Scene(visual); + _sceneBuilder.UpdateAll(scene); + return new Dictionary { { visualBrush.Visual, scene } }; + } + }*/ + + return null; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Enums.cs b/src/Avalonia.Base/Rendering/Composition/Enums.cs new file mode 100644 index 0000000000..e349845cbf --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Enums.cs @@ -0,0 +1,120 @@ +using System; + +namespace Avalonia.Rendering.Composition +{ + public enum CompositionBlendMode + { + /// No regions are enabled. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_clr.svg) + Clear, + + /// Only the source will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src.svg) + Src, + + /// Only the destination will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst.svg) + Dst, + + /// Source is placed over the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-over.svg) + SrcOver, + + /// Destination is placed over the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-over.svg) + DstOver, + + /// The source that overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-in.svg) + SrcIn, + + /// Destination which overlaps the source, replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-in.svg) + DstIn, + + /// Source is placed, where it falls outside of the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-out.svg) + SrcOut, + + /// Destination is placed, where it falls outside of the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-out.svg) + DstOut, + + /// Source which overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-atop.svg) + SrcATop, + + /// Destination which overlaps the source replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-atop.svg) + DstATop, + + /// The non-overlapping regions of source and destination are combined. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_xor.svg) + Xor, + + /// Display the sum of the source image and destination image. [Porter Duff Compositing Operators] + Plus, + + /// Multiplies all components (= alpha and color). [Separable Blend Modes] + Modulate, + + /// Multiplies the complements of the backdrop and source CompositionColorvalues, then complements the result. [Separable Blend Modes] + Screen, + + /// Multiplies or screens the colors, depending on the backdrop CompositionColorvalue. [Separable Blend Modes] + Overlay, + + /// Selects the darker of the backdrop and source colors. [Separable Blend Modes] + Darken, + + /// Selects the lighter of the backdrop and source colors. [Separable Blend Modes] + Lighten, + + /// Brightens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes] + ColorDodge, + + /// Darkens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes] + ColorBurn, + + /// Multiplies or screens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes] + HardLight, + + /// Darkens or lightens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes] + SoftLight, + + /// Subtracts the darker of the two constituent colors from the lighter color. [Separable Blend Modes] + Difference, + + /// Produces an effect similar to that of the Difference mode but lower in contrast. [Separable Blend Modes] + Exclusion, + + /// The source CompositionColoris multiplied by the destination CompositionColorand replaces the destination [Separable Blend Modes] + Multiply, + + /// Creates a CompositionColorwith the hue of the source CompositionColorand the saturation and luminosity of the backdrop color. [Non-Separable Blend Modes] + Hue, + + /// Creates a CompositionColorwith the saturation of the source CompositionColorand the hue and luminosity of the backdrop color. [Non-Separable Blend Modes] + Saturation, + + /// Creates a CompositionColorwith the hue and saturation of the source CompositionColorand the luminosity of the backdrop color. [Non-Separable Blend Modes] + Color, + + /// Creates a CompositionColorwith the luminosity of the source CompositionColorand the hue and saturation of the backdrop color. [Non-Separable Blend Modes] + Luminosity, + } + + public enum CompositionGradientExtendMode + { + Clamp, + Wrap, + Mirror + } + + [Flags] + public enum CompositionTileMode + { + None = 0, + TileX = 1, + TileY = 2, + FlipX = 4, + FlipY = 8, + Tile = TileX | TileY, + Flip = FlipX | FlipY + } + + public enum CompositionStretch + { + None = 0, + Fill = 1, + //TODO: Uniform, UniformToFill + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs new file mode 100644 index 0000000000..db9a26e301 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Utils; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class BuiltInExpressionFfi : IExpressionForeignFunctionInterface + { + private readonly DelegateExpressionFfi _registry; + + static float Lerp(float a, float b, float p) => p * (b - a) + a; + + static Matrix3x2 Inverse(Matrix3x2 m) + { + Matrix3x2.Invert(m, out var r); + return r; + } + + static Matrix4x4 Inverse(Matrix4x4 m) + { + Matrix4x4.Invert(m, out var r); + return r; + } + + static float SmoothStep(float edge0, float edge1, float x) + { + var t = MathExt.Clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + return t * t * (3.0f - 2.0f * t); + } + + static Vector2 SmoothStep(Vector2 edge0, Vector2 edge1, Vector2 x) + { + return new Vector2( + SmoothStep(edge0.X, edge1.X, x.X), + SmoothStep(edge0.Y, edge1.Y, x.Y) + + ); + } + static Vector3 SmoothStep(Vector3 edge0, Vector3 edge1, Vector3 x) + { + return new Vector3( + SmoothStep(edge0.X, edge1.X, x.X), + SmoothStep(edge0.Y, edge1.Y, x.Y), + SmoothStep(edge0.Z, edge1.Z, x.Z) + + ); + } + + static Vector4 SmoothStep(Vector4 edge0, Vector4 edge1, Vector4 x) + { + return new Vector4( + SmoothStep(edge0.X, edge1.X, x.X), + SmoothStep(edge0.Y, edge1.Y, x.Y), + SmoothStep(edge0.Z, edge1.Z, x.Z), + SmoothStep(edge0.W, edge1.W, x.W) + ); + } + + private BuiltInExpressionFfi() + { + _registry = new DelegateExpressionFfi + { + {"Abs", (float f) => Math.Abs(f)}, + {"Abs", (Vector2 v) => Vector2.Abs(v)}, + {"Abs", (Vector3 v) => Vector3.Abs(v)}, + {"Abs", (Vector4 v) => Vector4.Abs(v)}, + + {"ACos", (float f) => (float) Math.Acos(f)}, + {"ASin", (float f) => (float) Math.Asin(f)}, + {"ATan", (float f) => (float) Math.Atan(f)}, + {"Ceil", (float f) => (float) Math.Ceiling(f)}, + + {"Clamp", (float a1, float a2, float a3) => MathExt.Clamp(a1, a2, a3)}, + {"Clamp", (Vector2 a1, Vector2 a2, Vector2 a3) => Vector2.Clamp(a1, a2, a3)}, + {"Clamp", (Vector3 a1, Vector3 a2, Vector3 a3) => Vector3.Clamp(a1, a2, a3)}, + {"Clamp", (Vector4 a1, Vector4 a2, Vector4 a3) => Vector4.Clamp(a1, a2, a3)}, + + {"Concatenate", (Quaternion a1, Quaternion a2) => Quaternion.Concatenate(a1, a2)}, + {"Cos", (float a) => (float) Math.Cos(a)}, + + /* + TODO: + ColorHsl(Float h, Float s, Float l) + ColorLerpHSL(Color colorTo, CompositionColorcolorFrom, Float progress) + */ + + { + "ColorLerp", (Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => + ColorInterpolator.LerpRGB(to, from, progress) + }, + { + "ColorLerpRGB", (Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => + ColorInterpolator.LerpRGB(to, from, progress) + }, + { + "ColorRGB", (float a, float r, float g, float b) => Avalonia.Media.Color.FromArgb( + (byte) MathExt.Clamp(a, 0, 255), + (byte) MathExt.Clamp(r, 0, 255), + (byte) MathExt.Clamp(g, 0, 255), + (byte) MathExt.Clamp(b, 0, 255) + ) + }, + + {"Distance", (Vector2 a1, Vector2 a2) => Vector2.Distance(a1, a2)}, + {"Distance", (Vector3 a1, Vector3 a2) => Vector3.Distance(a1, a2)}, + {"Distance", (Vector4 a1, Vector4 a2) => Vector4.Distance(a1, a2)}, + + {"DistanceSquared", (Vector2 a1, Vector2 a2) => Vector2.DistanceSquared(a1, a2)}, + {"DistanceSquared", (Vector3 a1, Vector3 a2) => Vector3.DistanceSquared(a1, a2)}, + {"DistanceSquared", (Vector4 a1, Vector4 a2) => Vector4.DistanceSquared(a1, a2)}, + + {"Floor", (float v) => (float) Math.Floor(v)}, + + {"Inverse", (Matrix3x2 v) => Inverse(v)}, + {"Inverse", (Matrix4x4 v) => Inverse(v)}, + + + {"Length", (Vector2 a1) => a1.Length()}, + {"Length", (Vector3 a1) => a1.Length()}, + {"Length", (Vector4 a1) => a1.Length()}, + {"Length", (Quaternion a1) => a1.Length()}, + + {"LengthSquared", (Vector2 a1) => a1.LengthSquared()}, + {"LengthSquared", (Vector3 a1) => a1.LengthSquared()}, + {"LengthSquared", (Vector4 a1) => a1.LengthSquared()}, + {"LengthSquared", (Quaternion a1) => a1.LengthSquared()}, + + {"Lerp", (float a1, float a2, float a3) => Lerp(a1, a2, a3)}, + {"Lerp", (Vector2 a1, Vector2 a2, float a3) => Vector2.Lerp(a1, a2, a3)}, + {"Lerp", (Vector3 a1, Vector3 a2, float a3) => Vector3.Lerp(a1, a2, a3)}, + {"Lerp", (Vector4 a1, Vector4 a2, float a3) => Vector4.Lerp(a1, a2, a3)}, + + + {"Ln", (float f) => (float) Math.Log(f)}, + {"Log10", (float f) => (float) Math.Log10(f)}, + + {"Matrix3x2.CreateFromScale", (Vector2 v) => Matrix3x2.CreateScale(v)}, + {"Matrix3x2.CreateFromTranslation", (Vector2 v) => Matrix3x2.CreateTranslation(v)}, + {"Matrix3x2.CreateRotation", (float v) => Matrix3x2.CreateRotation(v)}, + {"Matrix3x2.CreateScale", (Vector2 v) => Matrix3x2.CreateScale(v)}, + {"Matrix3x2.CreateSkew", (float a1, float a2, Vector2 a3) => Matrix3x2.CreateSkew(a1, a2, a3)}, + {"Matrix3x2.CreateTranslation", (Vector2 v) => Matrix3x2.CreateScale(v)}, + { + "Matrix3x2", (float m11, float m12, float m21, float m22, float m31, float m32) => + new Matrix3x2(m11, m12, m21, m22, m31, m32) + }, + {"Matrix4x4.CreateFromAxisAngle", (Vector3 v, float angle) => Matrix4x4.CreateFromAxisAngle(v, angle)}, + {"Matrix4x4.CreateFromScale", (Vector3 v) => Matrix4x4.CreateScale(v)}, + {"Matrix4x4.CreateFromTranslation", (Vector3 v) => Matrix4x4.CreateTranslation(v)}, + {"Matrix4x4.CreateScale", (Vector3 v) => Matrix4x4.CreateScale(v)}, + {"Matrix4x4.CreateTranslation", (Vector3 v) => Matrix4x4.CreateScale(v)}, + {"Matrix4x4", (Matrix3x2 m) => new Matrix4x4(m)}, + { + "Matrix4x4", + (float m11, float m12, float m13, float m14, + float m21, float m22, float m23, float m24, + float m31, float m32, float m33, float m34, + float m41, float m42, float m43, float m44) => + new Matrix4x4( + m11, m12, m13, m14, + m21, m22, m23, m24, + m31, m32, m33, m34, + m41, m42, m43, m44) + }, + + + {"Max", (float a1, float a2) => Math.Max(a1, a2)}, + {"Max", (Vector2 a1, Vector2 a2) => Vector2.Max(a1, a2)}, + {"Max", (Vector3 a1, Vector3 a2) => Vector3.Max(a1, a2)}, + {"Max", (Vector4 a1, Vector4 a2) => Vector4.Max(a1, a2)}, + + + {"Min", (float a1, float a2) => Math.Min(a1, a2)}, + {"Min", (Vector2 a1, Vector2 a2) => Vector2.Min(a1, a2)}, + {"Min", (Vector3 a1, Vector3 a2) => Vector3.Min(a1, a2)}, + {"Min", (Vector4 a1, Vector4 a2) => Vector4.Min(a1, a2)}, + + {"Mod", (float a, float b) => a % b}, + + {"Normalize", (Quaternion a) => Quaternion.Normalize(a)}, + {"Normalize", (Vector2 a) => Vector2.Normalize(a)}, + {"Normalize", (Vector3 a) => Vector3.Normalize(a)}, + {"Normalize", (Vector4 a) => Vector4.Normalize(a)}, + + {"Pow", (float a, float b) => (float) Math.Pow(a, b)}, + {"Quaternion.CreateFromAxisAngle", (Vector3 a, float b) => Quaternion.CreateFromAxisAngle(a, b)}, + {"Quaternion", (float a, float b, float c, float d) => new Quaternion(a, b, c, d)}, + + {"Round", (float a) => (float) Math.Round(a)}, + + {"Scale", (Matrix3x2 a, float b) => a * b}, + {"Scale", (Matrix4x4 a, float b) => a * b}, + {"Scale", (Vector2 a, float b) => a * b}, + {"Scale", (Vector3 a, float b) => a * b}, + {"Scale", (Vector4 a, float b) => a * b}, + + {"Sin", (float a) => (float) Math.Sin(a)}, + + {"SmoothStep", (float a1, float a2, float a3) => SmoothStep(a1, a2, a3)}, + {"SmoothStep", (Vector2 a1, Vector2 a2, Vector2 a3) => SmoothStep(a1, a2, a3)}, + {"SmoothStep", (Vector3 a1, Vector3 a2, Vector3 a3) => SmoothStep(a1, a2, a3)}, + {"SmoothStep", (Vector4 a1, Vector4 a2, Vector4 a3) => SmoothStep(a1, a2, a3)}, + + // I have no idea how to do a spherical interpolation for a scalar value, so we are doing a linear one + {"Slerp", (float a1, float a2, float a3) => Lerp(a1, a2, a3)}, + {"Slerp", (Quaternion a1, Quaternion a2, float a3) => Quaternion.Slerp(a1, a2, a3)}, + + {"Sqrt", (float a) => (float) Math.Sqrt(a)}, + {"Square", (float a) => a * a}, + {"Tan", (float a) => (float) Math.Tan(a)}, + + {"ToRadians", (float a) => (float) (a * Math.PI / 180)}, + {"ToDegrees", (float a) => (float) (a * 180d / Math.PI)}, + + {"Transform", (Vector2 a, Matrix3x2 b) => Vector2.Transform(a, b)}, + {"Transform", (Vector3 a, Matrix4x4 b) => Vector3.Transform(a, b)}, + + {"Vector2", (float a, float b) => new Vector2(a, b)}, + {"Vector3", (float a, float b, float c) => new Vector3(a, b, c)}, + {"Vector3", (Vector2 v2, float z) => new Vector3(v2, z)}, + {"Vector4", (float a, float b, float c, float d) => new Vector4(a, b, c, d)}, + {"Vector4", (Vector2 v2, float z, float w) => new Vector4(v2, z, w)}, + {"Vector4", (Vector3 v3, float w) => new Vector4(v3, w)}, + }; + } + + public bool Call(string name, IReadOnlyList arguments, out ExpressionVariant result) => + _registry.Call(name, arguments, out result); + + public static BuiltInExpressionFfi Instance { get; } = new BuiltInExpressionFfi(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs new file mode 100644 index 0000000000..002cf37522 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Avalonia.Media; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class DelegateExpressionFfi : IExpressionForeignFunctionInterface, IEnumerable + { + struct FfiRecord + { + public VariantType[] Types; + public Func, ExpressionVariant> Delegate; + } + + private readonly Dictionary>> + _registry = new Dictionary>>(); + + public bool Call(string name, IReadOnlyList arguments, out ExpressionVariant result) + { + result = default; + if (!_registry.TryGetValue(name, out var nameGroup)) + return false; + if (!nameGroup.TryGetValue(arguments.Count, out var countGroup)) + return false; + foreach (var record in countGroup) + { + var match = true; + for (var c = 0; c < arguments.Count; c++) + { + if (record.Types[c] != arguments[c].Type) + { + match = false; + break; + } + } + + if (match) + { + result = record.Delegate(arguments); + return true; + } + } + + return false; + } + + // Stub for collection initializer + IEnumerator IEnumerable.GetEnumerator() => Array.Empty().GetEnumerator(); + + void Add(string name, Func, ExpressionVariant> cb, + params Type[] types) + { + if (!_registry.TryGetValue(name, out var nameGroup)) + _registry[name] = nameGroup = + new Dictionary>(); + if (!nameGroup.TryGetValue(types.Length, out var countGroup)) + nameGroup[types.Length] = countGroup = new List(); + + countGroup.Add(new FfiRecord + { + Types = types.Select(t => TypeMap[t]).ToArray(), + Delegate = cb + }); + } + + static readonly Dictionary TypeMap = new Dictionary + { + [typeof(bool)] = VariantType.Boolean, + [typeof(float)] = VariantType.Scalar, + [typeof(Vector2)] = VariantType.Vector2, + [typeof(Vector3)] = VariantType.Vector3, + [typeof(Vector4)] = VariantType.Vector4, + [typeof(Matrix3x2)] = VariantType.Matrix3x2, + [typeof(Matrix4x4)] = VariantType.Matrix4x4, + [typeof(Quaternion)] = VariantType.Quaternion, + [typeof(Color)] = VariantType.Color + }; + + public void Add(string name, Func cb) where T1 : struct + { + Add(name, args => cb(args[0].CastOrDefault()), typeof(T1)); + } + + public void Add(string name, Func cb) where T1 : struct where T2 : struct + { + Add(name, args => cb(args[0].CastOrDefault(), args[1].CastOrDefault()), typeof(T1), typeof(T2)); + } + + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct + { + Add(name, args => cb(args[0].CastOrDefault(), args[1].CastOrDefault(), args[2].CastOrDefault()), typeof(T1), typeof(T2), + typeof(T3)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6)); + } + + + public void Add(string name, + Func cb) + where T1 : struct + where T2 : struct + where T3 : struct + where T4 : struct + where T5 : struct + where T6 : struct + where T7 : struct + where T8 : struct + where T9 : struct + where T10 : struct + where T11 : struct + where T12 : struct + where T13 : struct + where T14 : struct + where T15 : struct + where T16 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault() + ), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), + typeof(T5), typeof(T6), typeof(T7), typeof(T8), + typeof(T9), typeof(T10), typeof(T11), typeof(T12), + typeof(T13), typeof(T14), typeof(T15), typeof(T16) + ); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs new file mode 100644 index 0000000000..5577d2b52a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -0,0 +1,331 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal abstract class Expression + { + public abstract ExpressionType Type { get; } + public static Expression Parse(string expression) + { + return ExpressionParser.Parse(expression.AsSpan()); + } + + public abstract ExpressionVariant Evaluate(ref ExpressionEvaluationContext context); + + protected abstract string Print(); + public override string ToString() => Print(); + + internal static string OperatorName(ExpressionType t) + { + var attr = typeof(ExpressionType).GetMember(t.ToString())[0] + .GetCustomAttribute(); + if (attr != null) + return attr.Name; + return t.ToString(); + } + } + + internal class PrettyPrintStringAttribute : Attribute + { + public string Name { get; } + + public PrettyPrintStringAttribute(string name) + { + Name = name; + } + } + + internal enum ExpressionType + { + // Binary operators + [PrettyPrintString("+")] + Add, + [PrettyPrintString("-")] + Subtract, + [PrettyPrintString("/")] + Divide, + [PrettyPrintString("*")] + Multiply, + [PrettyPrintString(">")] + MoreThan, + [PrettyPrintString("<")] + LessThan, + [PrettyPrintString(">=")] + MoreThanOrEqual, + [PrettyPrintString("<=")] + LessThanOrEqual, + [PrettyPrintString("&&")] + LogicalAnd, + [PrettyPrintString("||")] + LogicalOr, + [PrettyPrintString("%")] + Remainder, + [PrettyPrintString("==")] + Equals, + [PrettyPrintString("!=")] + NotEquals, + // Unary operators + [PrettyPrintString("!")] + Not, + [PrettyPrintString("-")] + UnaryMinus, + // The rest + MemberAccess, + Parameter, + FunctionCall, + Keyword, + Constant, + ConditionalExpression + } + + internal enum ExpressionKeyword + { + StartingValue, + CurrentValue, + FinalValue, + Target, + Pi, + True, + False + } + + internal class ConditionalExpression : Expression + { + public Expression Condition { get; } + public Expression TruePart { get; } + public Expression FalsePart { get; } + public override ExpressionType Type => ExpressionType.ConditionalExpression; + + public ConditionalExpression(Expression condition, Expression truePart, Expression falsePart) + { + Condition = condition; + TruePart = truePart; + FalsePart = falsePart; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + var cond = Condition.Evaluate(ref context); + if (cond.Type == VariantType.Boolean && cond.Boolean) + return TruePart.Evaluate(ref context); + return FalsePart.Evaluate(ref context); + } + + protected override string Print() => $"({Condition}) ? ({TruePart}) : ({FalsePart})"; + } + + internal class ConstantExpression : Expression + { + public float Constant { get; } + public override ExpressionType Type => ExpressionType.Constant; + + public ConstantExpression(float constant) + { + Constant = constant; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) => Constant; + protected override string Print() => Constant.ToString(CultureInfo.InvariantCulture); + } + + internal class FunctionCallExpression : Expression + { + public string Name { get; } + public List Parameters { get; } + public override ExpressionType Type => ExpressionType.FunctionCall; + + public FunctionCallExpression(string name, List parameters) + { + Name = name; + Parameters = parameters; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (context.ForeignFunctionInterface == null) + return default; + var args = new List(); + foreach (var expr in Parameters) + args.Add(expr.Evaluate(ref context)); + if (!context.ForeignFunctionInterface.Call(Name, args, out var res)) + return default; + return res; + } + + protected override string Print() + { + return Name + "( (" + string.Join("), (", Parameters) + ") )"; + } + } + + internal class MemberAccessExpression : Expression + { + public override ExpressionType Type => ExpressionType.MemberAccess; + public Expression Target { get; } + public string Member { get; } + + public MemberAccessExpression(Expression target, string member) + { + Target = target; + Member = string.Intern(member); + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (Target is KeywordExpression ke + && ke.Keyword == ExpressionKeyword.Target) + return context.Target.GetProperty(Member); + if (Target is ParameterExpression pe) + { + var obj = context.Parameters?.GetObjectParameter(pe.Name); + if (obj != null) + return obj.GetProperty(Member); + } + + return Target.Evaluate(ref context).GetProperty(Member); + } + + protected override string Print() + { + return "(" + Target.ToString() + ")." + Member; + } + } + + internal class ParameterExpression : Expression + { + public string Name { get; } + public override ExpressionType Type => ExpressionType.Parameter; + + public ParameterExpression(string name) + { + Name = name; + } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + return context.Parameters?.GetParameter(Name) ?? default; + } + + protected override string Print() + { + return "{" + Name + "}"; + } + } + + internal class KeywordExpression : Expression + { + public override ExpressionType Type => ExpressionType.Keyword; + public ExpressionKeyword Keyword { get; } + + public KeywordExpression(ExpressionKeyword keyword) + { + Keyword = keyword; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (Keyword == ExpressionKeyword.StartingValue) + return context.StartingValue; + if (Keyword == ExpressionKeyword.CurrentValue) + return context.CurrentValue; + if (Keyword == ExpressionKeyword.FinalValue) + return context.FinalValue; + if (Keyword == ExpressionKeyword.Target) + // should be handled by MemberAccess + return default; + if (Keyword == ExpressionKeyword.True) + return true; + if (Keyword == ExpressionKeyword.False) + return false; + if (Keyword == ExpressionKeyword.Pi) + return (float) Math.PI; + return default; + } + + protected override string Print() + { + return "[" + Keyword + "]"; + } + } + + internal class UnaryExpression : Expression + { + public Expression Parameter { get; } + public override ExpressionType Type { get; } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (Type == ExpressionType.Not) + return !Parameter.Evaluate(ref context); + if (Type == ExpressionType.UnaryMinus) + return -Parameter.Evaluate(ref context); + return default; + } + + protected override string Print() + { + return OperatorName(Type) + Parameter; + } + + public UnaryExpression(Expression parameter, ExpressionType type) + { + Parameter = parameter; + Type = type; + } + } + + internal class BinaryExpression : Expression + { + public Expression Left { get; } + public Expression Right { get; } + public override ExpressionType Type { get; } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + var left = Left.Evaluate(ref context); + var right = Right.Evaluate(ref context); + if (Type == ExpressionType.Add) + return left + right; + if (Type == ExpressionType.Subtract) + return left - right; + if (Type == ExpressionType.Multiply) + return left * right; + if (Type == ExpressionType.Divide) + return left / right; + if (Type == ExpressionType.Remainder) + return left % right; + if (Type == ExpressionType.MoreThan) + return left > right; + if (Type == ExpressionType.LessThan) + return left < right; + if (Type == ExpressionType.MoreThanOrEqual) + return left > right; + if (Type == ExpressionType.LessThanOrEqual) + return left < right; + if (Type == ExpressionType.LogicalAnd) + return left.And(right); + if (Type == ExpressionType.LogicalOr) + return left.Or(right); + if (Type == ExpressionType.Equals) + return left.EqualsTo(right); + if (Type == ExpressionType.NotEquals) + return left.NotEqualsTo(right); + return default; + } + + protected override string Print() + { + return "(" + Left + OperatorName(Type) + Right + ")"; + } + + public BinaryExpression(Expression left, Expression right, ExpressionType type) + { + Left = left; + Right = right; + Type = type; + } + } + + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs new file mode 100644 index 0000000000..a7ddabd70d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal struct ExpressionEvaluationContext + { + public ExpressionVariant StartingValue { get; set; } + public ExpressionVariant CurrentValue { get; set; } + public ExpressionVariant FinalValue { get; set; } + public IExpressionObject Target { get; set; } + public IExpressionParameterCollection Parameters { get; set; } + public IExpressionForeignFunctionInterface ForeignFunctionInterface { get; set; } + } + + internal interface IExpressionObject + { + ExpressionVariant GetProperty(string name); + } + + internal interface IExpressionParameterCollection + { + public ExpressionVariant GetParameter(string name); + + public IExpressionObject GetObjectParameter(string name); + } + + internal interface IExpressionForeignFunctionInterface + { + bool Call(string name, IReadOnlyList arguments, out ExpressionVariant result); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs new file mode 100644 index 0000000000..6a207a3bf7 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class ExpressionParseException : Exception + { + public int Position { get; } + + public ExpressionParseException(string message, int position) : base(message) + { + Position = position; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs new file mode 100644 index 0000000000..5924bb8f1b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +// ReSharper disable StringLiteralTypo + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class ExpressionParser + { + public static Expression Parse(ReadOnlySpan s) + { + var p = new TokenParser(s); + var parsed = ParseTillTerminator(ref p, "", false, false, out _); + p.SkipWhitespace(); + if (p.Length != 0) + throw new ExpressionParseException("Unexpected data ", p.Position); + return parsed; + } + + private static ReadOnlySpan Dot => ".".AsSpan(); + static bool TryParseAtomic(ref TokenParser parser, + [MaybeNullWhen(returnValue: false)] out Expression expr) + { + // We can parse keywords, parameter names and constants + expr = null; + if (parser.TryParseKeywordLowerCase("this.startingvalue")) + expr = new KeywordExpression(ExpressionKeyword.StartingValue); + else if(parser.TryParseKeywordLowerCase("this.currentvalue")) + expr = new KeywordExpression(ExpressionKeyword.CurrentValue); + else if(parser.TryParseKeywordLowerCase("this.finalvalue")) + expr = new KeywordExpression(ExpressionKeyword.FinalValue); + else if(parser.TryParseKeywordLowerCase("pi")) + expr = new KeywordExpression(ExpressionKeyword.Pi); + else if(parser.TryParseKeywordLowerCase("true")) + expr = new KeywordExpression(ExpressionKeyword.True); + else if(parser.TryParseKeywordLowerCase("false")) + expr = new KeywordExpression(ExpressionKeyword.False); + else if (parser.TryParseKeywordLowerCase("this.target")) + expr = new KeywordExpression(ExpressionKeyword.Target); + + if (expr != null) + return true; + + if (parser.TryParseIdentifier(out var identifier)) + { + expr = new ParameterExpression(identifier.ToString()); + return true; + } + + if(parser.TryParseFloat(out var scalar)) + { + expr = new ConstantExpression(scalar); + return true; + } + + return false; + + } + + static bool TryParseOperator(ref TokenParser parser, out ExpressionType op) + { + op = (ExpressionType) (-1); + if (parser.TryConsume("||")) + op = ExpressionType.LogicalOr; + else if (parser.TryConsume("&&")) + op = ExpressionType.LogicalAnd; + else if (parser.TryConsume(">=")) + op = ExpressionType.MoreThanOrEqual; + else if (parser.TryConsume("<=")) + op = ExpressionType.LessThanOrEqual; + else if (parser.TryConsume("==")) + op = ExpressionType.Equals; + else if (parser.TryConsume("!=")) + op = ExpressionType.NotEquals; + else if (parser.TryConsumeAny("+-/*><%".AsSpan(), out var sop)) + { +#pragma warning disable CS8509 + op = sop switch +#pragma warning restore CS8509 + { + '+' => ExpressionType.Add, + '-' => ExpressionType.Subtract, + '/' => ExpressionType.Divide, + '*' => ExpressionType.Multiply, + '<' => ExpressionType.LessThan, + '>' => ExpressionType.MoreThan, + '%' => ExpressionType.Remainder + }; + } + else + return false; + + return true; + } + + + struct ExpressionOperatorGroup + { + private List _expressions; + private List _operators; + private Expression? _first; + + public bool NotEmpty => !Empty; + public bool Empty => _expressions == null && _first == null; + + public void AppendFirst(Expression expr) + { + if (NotEmpty) + throw new InvalidOperationException(); + _first = expr; + } + + public void AppendWithOperator(Expression expr, ExpressionType op) + { + if (_expressions == null) + { + if (_first == null) + throw new InvalidOperationException(); + _expressions = new List(); + _expressions.Add(_first); + _first = null; + _operators = new List(); + } + _expressions.Add(expr); + _operators.Add(op); + } + + // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/ + private static readonly ExpressionType[][] OperatorPrecedenceGroups = new[] + { + // multiplicative + new[] {ExpressionType.Multiply, ExpressionType.Divide, ExpressionType.Remainder}, + // additive + new[] {ExpressionType.Add, ExpressionType.Subtract}, + // relational + new[] {ExpressionType.MoreThan, ExpressionType.MoreThanOrEqual, ExpressionType.LessThan, ExpressionType.LessThanOrEqual}, + // equality + new[] {ExpressionType.Equals, ExpressionType.NotEquals}, + // conditional AND + new[] {ExpressionType.LogicalAnd}, + // conditional OR + new[]{ ExpressionType.LogicalOr}, + }; + + private static readonly ExpressionType[][] OperatorPrecedenceGroupsReversed = + OperatorPrecedenceGroups.Reverse().ToArray(); + + // a*b+c [a,b,c] [*,+], call with (0, 2) + // ToExpression(a*b) + ToExpression(c) + // a+b*c -> ToExpression(a) + ToExpression(b*c) + Expression ToExpression(int from, int to) + { + if (to - from == 0) + return _expressions[from]; + + if (to - from == 1) + return new BinaryExpression(_expressions[from], _expressions[to], _operators[from]); + + foreach (var grp in OperatorPrecedenceGroupsReversed) + { + for (var c = from; c < to; c++) + { + var currentOperator = _operators[c]; + foreach(var operatorFromGroup in grp) + if (currentOperator == operatorFromGroup) + { + // We are dividing the expression right here + var left = ToExpression(from, c); + var right = ToExpression(c + 1, to); + return new BinaryExpression(left, right, currentOperator); + } + } + } + + // We shouldn't ever get here, if we are, there is something wrong in the code + throw new ExpressionParseException("Expression parsing algorithm bug in ToExpression", 0); + } + + public Expression ToExpression() + { + if (_expressions == null) + return _first ?? throw new InvalidOperationException(); + return ToExpression(0, _expressions.Count - 1); + } + } + + static Expression ParseTillTerminator(ref TokenParser parser, string terminatorChars, + bool throwOnTerminator, + bool throwOnEnd, + out char? token) + { + ExpressionOperatorGroup left = default; + token = null; + while (true) + { + if (parser.TryConsumeAny(terminatorChars.AsSpan(), out var consumedToken)) + { + if (throwOnTerminator || left.Empty) + throw new ExpressionParseException($"Unexpected '{token}'", parser.Position - 1); + token = consumedToken; + return left.ToExpression(); + } + parser.SkipWhitespace(); + if (parser.Length == 0) + { + if (throwOnEnd || left.Empty) + throw new ExpressionParseException("Unexpected end of expression", parser.Position); + return left.ToExpression(); + } + + ExpressionType? op = null; + if (left.NotEmpty) + { + if (parser.TryConsume('?')) + { + var truePart = ParseTillTerminator(ref parser, ":", + false, true, out _); + // pass through the current parsing rules to consume the rest + var falsePart = ParseTillTerminator(ref parser, terminatorChars, throwOnTerminator, throwOnEnd, + out token); + + return new ConditionalExpression(left.ToExpression(), truePart, falsePart); + } + + // We expect a binary operator here + if (!TryParseOperator(ref parser, out var sop)) + throw new ExpressionParseException("Unexpected token", parser.Position); + op = sop; + } + + // We expect an expression to be parsed (either due to expecting a binary operator or parsing the first part + var applyNegation = false; + while (parser.TryConsume('!')) + applyNegation = !applyNegation; + + var applyUnaryMinus = false; + while (parser.TryConsume('-')) + applyUnaryMinus = !applyUnaryMinus; + + Expression? parsed; + + if (parser.TryConsume('(')) + parsed = ParseTillTerminator(ref parser, ")", false, true, out _); + else if (parser.TryParseCall(out var functionName)) + { + var parameterList = new List(); + while (true) + { + parameterList.Add(ParseTillTerminator(ref parser, ",)", false, true, out var closingToken)); + if (closingToken == ')') + break; + if (closingToken != ',') + throw new ExpressionParseException("Unexpected end of the expression", parser.Position); + } + + parsed = new FunctionCallExpression(functionName.ToString(), parameterList); + } + else if (TryParseAtomic(ref parser, out parsed)) + { + // do nothing + } + else + throw new ExpressionParseException("Unexpected token", parser.Position); + + + // Parse any following member accesses + while (parser.TryConsume('.')) + { + if(!parser.TryParseIdentifier(out var memberName)) + throw new ExpressionParseException("Unexpected token", parser.Position); + + parsed = new MemberAccessExpression(parsed, memberName.ToString()); + } + + // Apply ! operator + if (applyNegation) + parsed = new UnaryExpression(parsed, ExpressionType.Not); + + if (applyUnaryMinus) + { + if(parsed is ConstantExpression constexpr) + parsed = new ConstantExpression(-constexpr.Constant); + else parsed = new UnaryExpression(parsed, ExpressionType.UnaryMinus); + } + + if (left.Empty) + left.AppendFirst(parsed); + else + left.AppendWithOperator(parsed, op!.Value); + } + + + + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs new file mode 100644 index 0000000000..8c6af5cb0c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs @@ -0,0 +1,739 @@ +using System; +using System.Globalization; +using System.Numerics; +using System.Runtime.InteropServices; +using Avalonia.Media; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal enum VariantType + { + Invalid, + Boolean, + Scalar, + Double, + Vector2, + Vector3, + Vector4, + AvaloniaMatrix, + Matrix3x2, + Matrix4x4, + Quaternion, + Color + } + + [StructLayout(LayoutKind.Explicit)] + internal struct ExpressionVariant + { + [FieldOffset(0)] public VariantType Type; + + [FieldOffset(4)] public bool Boolean; + [FieldOffset(4)] public float Scalar; + [FieldOffset(4)] public double Double; + [FieldOffset(4)] public Vector2 Vector2; + [FieldOffset(4)] public Vector3 Vector3; + [FieldOffset(4)] public Vector4 Vector4; + [FieldOffset(4)] public Matrix AvaloniaMatrix; + [FieldOffset(4)] public Matrix3x2 Matrix3x2; + [FieldOffset(4)] public Matrix4x4 Matrix4x4; + [FieldOffset(4)] public Quaternion Quaternion; + [FieldOffset(4)] public Color Color; + + + public ExpressionVariant GetProperty(string property) + { + if (Type == VariantType.Vector2) + { + if (ReferenceEquals(property, "X")) + return Vector2.X; + if (ReferenceEquals(property, "Y")) + return Vector2.Y; + return default; + } + + if (Type == VariantType.Vector3) + { + if (ReferenceEquals(property, "X")) + return Vector3.X; + if (ReferenceEquals(property, "Y")) + return Vector3.Y; + if (ReferenceEquals(property, "Z")) + return Vector3.Z; + if(ReferenceEquals(property, "XY")) + return new Vector2(Vector3.X, Vector3.Y); + if(ReferenceEquals(property, "YX")) + return new Vector2(Vector3.Y, Vector3.X); + if(ReferenceEquals(property, "XZ")) + return new Vector2(Vector3.X, Vector3.Z); + if(ReferenceEquals(property, "ZX")) + return new Vector2(Vector3.Z, Vector3.X); + if(ReferenceEquals(property, "YZ")) + return new Vector2(Vector3.Y, Vector3.Z); + if(ReferenceEquals(property, "ZY")) + return new Vector2(Vector3.Z, Vector3.Y); + return default; + } + + if (Type == VariantType.Vector4) + { + if (ReferenceEquals(property, "X")) + return Vector4.X; + if (ReferenceEquals(property, "Y")) + return Vector4.Y; + if (ReferenceEquals(property, "Z")) + return Vector4.Z; + if (ReferenceEquals(property, "W")) + return Vector4.W; + return default; + } + + if (Type == VariantType.Matrix3x2) + { + if (ReferenceEquals(property, "M11")) + return Matrix3x2.M11; + if (ReferenceEquals(property, "M12")) + return Matrix3x2.M12; + if (ReferenceEquals(property, "M21")) + return Matrix3x2.M21; + if (ReferenceEquals(property, "M22")) + return Matrix3x2.M22; + if (ReferenceEquals(property, "M31")) + return Matrix3x2.M31; + if (ReferenceEquals(property, "M32")) + return Matrix3x2.M32; + return default; + } + + if (Type == VariantType.AvaloniaMatrix) + { + if (ReferenceEquals(property, "M11")) + return AvaloniaMatrix.M11; + if (ReferenceEquals(property, "M12")) + return AvaloniaMatrix.M12; + if (ReferenceEquals(property, "M21")) + return AvaloniaMatrix.M21; + if (ReferenceEquals(property, "M22")) + return AvaloniaMatrix.M22; + if (ReferenceEquals(property, "M31")) + return AvaloniaMatrix.M31; + if (ReferenceEquals(property, "M32")) + return AvaloniaMatrix.M32; + return default; + } + + if (Type == VariantType.Matrix4x4) + { + if (ReferenceEquals(property, "M11")) + return Matrix4x4.M11; + if (ReferenceEquals(property, "M12")) + return Matrix4x4.M12; + if (ReferenceEquals(property, "M13")) + return Matrix4x4.M13; + if (ReferenceEquals(property, "M14")) + return Matrix4x4.M14; + if (ReferenceEquals(property, "M21")) + return Matrix4x4.M21; + if (ReferenceEquals(property, "M22")) + return Matrix4x4.M22; + if (ReferenceEquals(property, "M23")) + return Matrix4x4.M23; + if (ReferenceEquals(property, "M24")) + return Matrix4x4.M24; + if (ReferenceEquals(property, "M31")) + return Matrix4x4.M31; + if (ReferenceEquals(property, "M32")) + return Matrix4x4.M32; + if (ReferenceEquals(property, "M33")) + return Matrix4x4.M33; + if (ReferenceEquals(property, "M34")) + return Matrix4x4.M34; + if (ReferenceEquals(property, "M41")) + return Matrix4x4.M41; + if (ReferenceEquals(property, "M42")) + return Matrix4x4.M42; + if (ReferenceEquals(property, "M43")) + return Matrix4x4.M43; + if (ReferenceEquals(property, "M44")) + return Matrix4x4.M44; + return default; + } + + if (Type == VariantType.Quaternion) + { + if (ReferenceEquals(property, "X")) + return Quaternion.X; + if (ReferenceEquals(property, "Y")) + return Quaternion.Y; + if (ReferenceEquals(property, "Z")) + return Quaternion.Z; + if (ReferenceEquals(property, "W")) + return Quaternion.W; + return default; + } + + if (Type == VariantType.Color) + { + if (ReferenceEquals(property, "A")) + return Color.A; + if (ReferenceEquals(property, "R")) + return Color.R; + if (ReferenceEquals(property, "G")) + return Color.G; + if (ReferenceEquals(property, "B")) + return Color.B; + return default; + } + + return default; + } + + public static implicit operator ExpressionVariant(bool value) => + new ExpressionVariant + { + Type = VariantType.Boolean, + Boolean = value + }; + + public static implicit operator ExpressionVariant(float scalar) => + new ExpressionVariant + { + Type = VariantType.Scalar, + Scalar = scalar + }; + + public static implicit operator ExpressionVariant(double d) => + new ExpressionVariant + { + Type = VariantType.Double, + Double = d + }; + + + public static implicit operator ExpressionVariant(Vector2 value) => + new ExpressionVariant + { + Type = VariantType.Vector2, + Vector2 = value + }; + + + public static implicit operator ExpressionVariant(Vector3 value) => + new ExpressionVariant + { + Type = VariantType.Vector3, + Vector3 = value + }; + + + public static implicit operator ExpressionVariant(Vector4 value) => + new ExpressionVariant + { + Type = VariantType.Vector4, + Vector4 = value + }; + + public static implicit operator ExpressionVariant(Matrix3x2 value) => + new ExpressionVariant + { + Type = VariantType.Matrix3x2, + Matrix3x2 = value + }; + + public static implicit operator ExpressionVariant(Matrix value) => + new ExpressionVariant + { + Type = VariantType.Matrix3x2, + AvaloniaMatrix = value + }; + + public static implicit operator ExpressionVariant(Matrix4x4 value) => + new ExpressionVariant + { + Type = VariantType.Matrix4x4, + Matrix4x4 = value + }; + + public static implicit operator ExpressionVariant(Quaternion value) => + new ExpressionVariant + { + Type = VariantType.Quaternion, + Quaternion = value + }; + + public static implicit operator ExpressionVariant(Avalonia.Media.Color value) => + new ExpressionVariant + { + Type = VariantType.Color, + Color = value + }; + + public static ExpressionVariant operator +(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type != right.Type || left.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar) + return left.Scalar + right.Scalar; + + if (left.Type == VariantType.Double) + return left.Double + right.Double; + + if (left.Type == VariantType.Vector2) + return left.Vector2 + right.Vector2; + + if (left.Type == VariantType.Vector3) + return left.Vector3 + right.Vector3; + + if (left.Type == VariantType.Vector4) + return left.Vector4 + right.Vector4; + + if (left.Type == VariantType.Matrix3x2) + return left.Matrix3x2 + right.Matrix3x2; + + if (left.Type == VariantType.AvaloniaMatrix) + return left.AvaloniaMatrix + right.AvaloniaMatrix; + + if (left.Type == VariantType.Matrix4x4) + return left.Matrix4x4 + right.Matrix4x4; + + if (left.Type == VariantType.Quaternion) + return left.Quaternion + right.Quaternion; + + return default; + } + + public static ExpressionVariant operator -(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type != right.Type || left.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar) + return left.Scalar - right.Scalar; + + if (left.Type == VariantType.Double) + return left.Double - right.Double; + + if (left.Type == VariantType.Vector2) + return left.Vector2 - right.Vector2; + + if (left.Type == VariantType.Vector3) + return left.Vector3 - right.Vector3; + + if (left.Type == VariantType.Vector4) + return left.Vector4 - right.Vector4; + + if (left.Type == VariantType.Matrix3x2) + return left.Matrix3x2 - right.Matrix3x2; + + if (left.Type == VariantType.AvaloniaMatrix) + return left.AvaloniaMatrix - right.AvaloniaMatrix; + + if (left.Type == VariantType.Matrix4x4) + return left.Matrix4x4 - right.Matrix4x4; + + if (left.Type == VariantType.Quaternion) + return left.Quaternion - right.Quaternion; + + return default; + } + + public static ExpressionVariant operator -(ExpressionVariant left) + { + + if (left.Type == VariantType.Scalar) + return -left.Scalar; + + if (left.Type == VariantType.Double) + return -left.Double; + + if (left.Type == VariantType.Vector2) + return -left.Vector2; + + if (left.Type == VariantType.Vector3) + return -left.Vector3; + + if (left.Type == VariantType.Vector4) + return -left.Vector4; + + if (left.Type == VariantType.Matrix3x2) + return -left.Matrix3x2; + + if (left.Type == VariantType.AvaloniaMatrix) + return -left.AvaloniaMatrix; + + if (left.Type == VariantType.Matrix4x4) + return -left.Matrix4x4; + + if (left.Type == VariantType.Quaternion) + return -left.Quaternion; + + return default; + } + + public static ExpressionVariant operator *(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Invalid || right.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar * right.Scalar; + + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double * right.Double; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2) + return left.Vector2 * right.Vector2; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar) + return left.Vector2 * right.Scalar; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3) + return left.Vector3 * right.Vector3; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar) + return left.Vector3 * right.Scalar; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4) + return left.Vector4 * right.Vector4; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Scalar) + return left.Vector4 * right.Scalar; + + if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Matrix3x2) + return left.Matrix3x2 * right.Matrix3x2; + + if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Scalar) + return left.Matrix3x2 * right.Scalar; + + if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.AvaloniaMatrix) + return left.AvaloniaMatrix * right.AvaloniaMatrix; + + if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Scalar) + return left.AvaloniaMatrix * (double)right.Scalar; + + if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Double) + return left.AvaloniaMatrix * right.Double; + + if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Matrix4x4) + return left.Matrix4x4 * right.Matrix4x4; + + if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Scalar) + return left.Matrix4x4 * right.Scalar; + + if (left.Type == VariantType.Quaternion && right.Type == VariantType.Quaternion) + return left.Quaternion * right.Quaternion; + + if (left.Type == VariantType.Quaternion && right.Type == VariantType.Scalar) + return left.Quaternion * right.Scalar; + + return default; + } + + public static ExpressionVariant operator /(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Invalid || right.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar / right.Scalar; + + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double / right.Double; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2) + return left.Vector2 / right.Vector2; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar) + return left.Vector2 / right.Scalar; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3) + return left.Vector3 / right.Vector3; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar) + return left.Vector3 / right.Scalar; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4) + return left.Vector4 / right.Vector4; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Scalar) + return left.Vector4 / right.Scalar; + + if (left.Type == VariantType.Quaternion && right.Type == VariantType.Quaternion) + return left.Quaternion / right.Quaternion; + + return default; + } + + public ExpressionVariant EqualsTo(ExpressionVariant right) + { + if (Type != right.Type || Type == VariantType.Invalid) + return default; + + if (Type == VariantType.Scalar) + return Scalar == right.Scalar; + + + if (Type == VariantType.Double) + return Double == right.Double; + + if (Type == VariantType.Vector2) + return Vector2 == right.Vector2; + + if (Type == VariantType.Vector3) + return Vector3 == right.Vector3; + + if (Type == VariantType.Vector4) + return Vector4 == right.Vector4; + + if (Type == VariantType.Boolean) + return Boolean == right.Boolean; + + if (Type == VariantType.Matrix3x2) + return Matrix3x2 == right.Matrix3x2; + + if (Type == VariantType.AvaloniaMatrix) + return AvaloniaMatrix == right.AvaloniaMatrix; + + if (Type == VariantType.Matrix4x4) + return Matrix4x4 == right.Matrix4x4; + + if (Type == VariantType.Quaternion) + return Quaternion == right.Quaternion; + + return default; + } + + public ExpressionVariant NotEqualsTo(ExpressionVariant right) + { + var r = EqualsTo(right); + if (r.Type == VariantType.Boolean) + return !r.Boolean; + return default; + } + + public static ExpressionVariant operator !(ExpressionVariant v) + { + if (v.Type == VariantType.Boolean) + return !v.Boolean; + return default; + } + + public static ExpressionVariant operator %(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar % right.Scalar; + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double % right.Double; + return default; + } + + public static ExpressionVariant operator <(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar < right.Scalar; + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double < right.Double; + return default; + } + + public static ExpressionVariant operator >(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar > right.Scalar; + + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double > right.Double; + return default; + } + + public ExpressionVariant And(ExpressionVariant right) + { + if (Type == VariantType.Boolean && right.Type == VariantType.Boolean) + return Boolean && right.Boolean; + return default; + } + + public ExpressionVariant Or(ExpressionVariant right) + { + if (Type == VariantType.Boolean && right.Type == VariantType.Boolean) + return Boolean && right.Boolean; + return default; + } + + public bool TryCast(out T res) where T : struct + { + if (typeof(T) == typeof(bool)) + { + if (Type == VariantType.Boolean) + { + res = (T) (object) Boolean; + return true; + } + } + + if (typeof(T) == typeof(float)) + { + if (Type == VariantType.Scalar) + { + res = (T) (object) Scalar; + return true; + } + } + + if (typeof(T) == typeof(double)) + { + if (Type == VariantType.Double) + { + res = (T) (object) Double; + return true; + } + } + + if (typeof(T) == typeof(Vector2)) + { + if (Type == VariantType.Vector2) + { + res = (T) (object) Vector2; + return true; + } + } + + if (typeof(T) == typeof(Vector3)) + { + if (Type == VariantType.Vector3) + { + res = (T) (object) Vector3; + return true; + } + } + + if (typeof(T) == typeof(Vector4)) + { + if (Type == VariantType.Vector4) + { + res = (T) (object) Vector4; + return true; + } + } + + if (typeof(T) == typeof(Matrix3x2)) + { + if (Type == VariantType.Matrix3x2) + { + res = (T) (object) Matrix3x2; + return true; + } + } + + if (typeof(T) == typeof(Matrix)) + { + if (Type == VariantType.AvaloniaMatrix) + { + res = (T) (object) Matrix3x2; + return true; + } + } + + if (typeof(T) == typeof(Matrix4x4)) + { + if (Type == VariantType.Matrix4x4) + { + res = (T) (object) Matrix4x4; + return true; + } + } + + if (typeof(T) == typeof(Quaternion)) + { + if (Type == VariantType.Quaternion) + { + res = (T) (object) Quaternion; + return true; + } + } + + if (typeof(T) == typeof(Avalonia.Media.Color)) + { + if (Type == VariantType.Color) + { + res = (T) (object) Color; + return true; + } + } + + res = default(T); + return false; + } + + public static ExpressionVariant Create(T v) where T : struct + { + if (typeof(T) == typeof(bool)) + return (bool) (object) v; + + if (typeof(T) == typeof(float)) + return (float) (object) v; + + if (typeof(T) == typeof(Vector2)) + return (Vector2) (object) v; + + if (typeof(T) == typeof(Vector3)) + return (Vector3) (object) v; + + if (typeof(T) == typeof(Vector4)) + return (Vector4) (object) v; + + if (typeof(T) == typeof(Matrix3x2)) + return (Matrix3x2) (object) v; + + if (typeof(T) == typeof(Matrix)) + return (Matrix) (object) v; + + if (typeof(T) == typeof(Matrix4x4)) + return (Matrix4x4) (object) v; + + if (typeof(T) == typeof(Quaternion)) + return (Quaternion) (object) v; + + if (typeof(T) == typeof(Avalonia.Media.Color)) + return (Avalonia.Media.Color) (object) v; + + throw new ArgumentException("Invalid variant type: " + typeof(T)); + } + + public T CastOrDefault() where T : struct + { + TryCast(out var r); + return r; + } + + public override string ToString() + { + if (Type == VariantType.Boolean) + return Boolean.ToString(); + if (Type == VariantType.Scalar) + return Scalar.ToString(CultureInfo.InvariantCulture); + if (Type == VariantType.Double) + return Double.ToString(CultureInfo.InvariantCulture); + if (Type == VariantType.Vector2) + return Vector2.ToString(); + if (Type == VariantType.Vector3) + return Vector3.ToString(); + if (Type == VariantType.Vector4) + return Vector4.ToString(); + if (Type == VariantType.Quaternion) + return Quaternion.ToString(); + if (Type == VariantType.Matrix3x2) + return Matrix3x2.ToString(); + if (Type == VariantType.AvaloniaMatrix) + return AvaloniaMatrix.ToString(); + if (Type == VariantType.Matrix4x4) + return Matrix4x4.ToString(); + if (Type == VariantType.Color) + return Color.ToString(); + if (Type == VariantType.Invalid) + return "Invalid"; + return "Unknown"; + } + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs new file mode 100644 index 0000000000..1050c7274c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs @@ -0,0 +1,256 @@ +using System; +using System.Globalization; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal ref struct TokenParser + { + private ReadOnlySpan _s; + public int Position { get; private set; } + public TokenParser(ReadOnlySpan s) + { + _s = s; + Position = 0; + } + + public void SkipWhitespace() + { + while (true) + { + if (_s.Length > 0 && char.IsWhiteSpace(_s[0])) + Advance(1); + else + return; + } + } + + static bool IsAlphaNumeric(char ch) => (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z'); + + public bool TryConsume(char c) + { + SkipWhitespace(); + if (_s.Length == 0 || _s[0] != c) + return false; + + Advance(1); + return true; + } + public bool TryConsume(string s) + { + SkipWhitespace(); + if (_s.Length < s.Length) + return false; + for (var c = 0; c < s.Length; c++) + { + if (_s[c] != s[c]) + return false; + } + + Advance(s.Length); + return true; + } + + public bool TryConsumeAny(ReadOnlySpan chars, out char token) + { + SkipWhitespace(); + token = default; + if (_s.Length == 0) + return false; + + foreach (var c in chars) + { + if (c == _s[0]) + { + token = c; + Advance(1); + return true; + } + } + + return false; + } + + + public bool TryParseKeyword(string keyword) + { + SkipWhitespace(); + if (keyword.Length > _s.Length) + return false; + for(var c=0; c keyword.Length && IsAlphaNumeric(_s[keyword.Length])) + return false; + + Advance(keyword.Length); + return true; + } + + public bool TryParseKeywordLowerCase(string keywordInLowerCase) + { + SkipWhitespace(); + if (keywordInLowerCase.Length > _s.Length) + return false; + for(var c=0; c keywordInLowerCase.Length && IsAlphaNumeric(_s[keywordInLowerCase.Length])) + return false; + + Advance(keywordInLowerCase.Length); + return true; + } + + public void Advance(int c) + { + _s = _s.Slice(c); + Position += c; + } + + public int Length => _s.Length; + + public bool TryParseIdentifier(ReadOnlySpan extraValidChars, out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if (IsAlphaNumeric(ch)) + len++; + else + { + var found = false; + foreach(var vc in extraValidChars) + if (vc == ch) + { + found = true; + break; + } + + if (found) + len++; + else + break; + } + } + + res = _s.Slice(0, len); + Advance(len); + return true; + } + + public bool TryParseIdentifier(out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if (IsAlphaNumeric(ch)) + len++; + else + break; + } + + res = _s.Slice(0, len); + Advance(len); + return true; + } + + public bool TryParseCall(out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch<= 'Z') || ch == '.') + len++; + else + break; + } + + res = _s.Slice(0, len); + + // Find '(' + for (var c = len; c < _s.Length; c++) + { + if(char.IsWhiteSpace(_s[c])) + continue; + if(_s[c]=='(') + { + Advance(c + 1); + return true; + } + + return false; + + } + + return false; + + } + + + public bool TryParseFloat(out float res) + { + res = 0; + SkipWhitespace(); + if (_s.Length == 0) + return false; + + var len = 0; + var dotCount = 0; + for (var c = 0; c < _s.Length; c++) + { + var ch = _s[c]; + if (ch >= '0' && ch <= '9') + len = c + 1; + else if (ch == '.' && dotCount == 0) + { + len = c + 1; + dotCount++; + } + else + break; + } + + var span = _s.Slice(0, len); + +#if NETSTANDARD2_0 + if (!float.TryParse(span.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out res)) + return false; +#else + if (!float.TryParse(span, NumberStyles.Number, CultureInfo.InvariantCulture, out res)) + return false; +#endif + Advance(len); + return true; + } + + public override string ToString() => _s.ToString(); + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs new file mode 100644 index 0000000000..9ef31c30e0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs @@ -0,0 +1,9 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public interface ICompositionSurface + { + internal ServerCompositionSurface Server { get; } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs new file mode 100644 index 0000000000..5e91bcb3d4 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs @@ -0,0 +1,46 @@ +using System.Numerics; + +namespace Avalonia.Rendering.Composition +{ + static class MatrixUtils + { + public static Matrix4x4 ComputeTransform(Vector2 size, Vector2 anchorPoint, Vector3 centerPoint, + Matrix4x4 transformMatrix, Vector3 scale, float rotationAngle, Quaternion orientation, Vector3 offset) + { + // The math here follows the *observed* UWP behavior since there are no docs on how it's supposed to work + + var anchor = size * anchorPoint; + var mat = Matrix4x4.CreateTranslation(-anchor.X, -anchor.Y, 0); + + var center = new Vector3(centerPoint.X, centerPoint.Y, centerPoint.Z); + + if (!transformMatrix.IsIdentity) + mat = transformMatrix * mat; + + + if (scale != new Vector3(1, 1, 1)) + mat *= Matrix4x4.CreateScale(scale, center); + + //TODO: RotationAxis support + if (rotationAngle != 0) + mat *= Matrix4x4.CreateRotationZ(rotationAngle, center); + + if (orientation != Quaternion.Identity) + { + if (centerPoint != default) + { + mat *= Matrix4x4.CreateTranslation(-center) + * Matrix4x4.CreateFromQuaternion(orientation) + * Matrix4x4.CreateTranslation(center); + } + else + mat *= Matrix4x4.CreateFromQuaternion(orientation); + } + + if (offset != default) + mat *= Matrix4x4.CreateTranslation(offset); + + return mat; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs new file mode 100644 index 0000000000..f5dfa92897 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -0,0 +1,142 @@ +using System.Numerics; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class CompositorDrawingContextProxy : IDrawingContextImpl +{ + private IDrawingContextImpl _impl; + + public CompositorDrawingContextProxy(IDrawingContextImpl impl) + { + _impl = impl; + } + + public Matrix PreTransform { get; set; } = Matrix.Identity; + + public void Dispose() + { + _impl.Dispose(); + } + + Matrix _transform; + public Matrix Transform + { + get => _transform; + set => _impl.Transform = PreTransform * (_transform = value); + } + + public void Clear(Color color) + { + _impl.Clear(color); + } + + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + { + _impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode); + } + + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + { + _impl.DrawBitmap(source, opacityMask, opacityMaskRect, destRect); + } + + public void DrawLine(IPen pen, Point p1, Point p2) + { + _impl.DrawLine(pen, p1, p2); + } + + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + _impl.DrawGeometry(brush, pen, geometry); + } + + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) + { + _impl.DrawRectangle(brush, pen, rect, boxShadows); + } + + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + _impl.DrawEllipse(brush, pen, rect); + } + + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + { + _impl.DrawGlyphRun(foreground, glyphRun); + } + + public IDrawingContextLayerImpl CreateLayer(Size size) + { + return _impl.CreateLayer(size); + } + + public void PushClip(Rect clip) + { + _impl.PushClip(clip); + } + + public void PushClip(RoundedRect clip) + { + _impl.PushClip(clip); + } + + public void PopClip() + { + _impl.PopClip(); + } + + public void PushOpacity(double opacity) + { + _impl.PushOpacity(opacity); + } + + public void PopOpacity() + { + _impl.PopOpacity(); + } + + public void PushOpacityMask(IBrush mask, Rect bounds) + { + _impl.PushOpacityMask(mask, bounds); + } + + public void PopOpacityMask() + { + _impl.PopOpacityMask(); + } + + public void PushGeometryClip(IGeometryImpl clip) + { + _impl.PushGeometryClip(clip); + } + + public void PopGeometryClip() + { + _impl.PopGeometryClip(); + } + + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + _impl.PushBitmapBlendMode(blendingMode); + } + + public void PopBitmapBlendMode() + { + _impl.PopBitmapBlendMode(); + } + + public void Custom(ICustomDrawOperation custom) + { + _impl.Custom(custom); + } + + public Matrix CutTransform(Matrix4x4 transform) => new Matrix(transform.M11, transform.M12, transform.M21, + transform.M22, transform.M41, + transform.M42); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs new file mode 100644 index 0000000000..372fa4d9ce --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs @@ -0,0 +1,46 @@ +namespace Avalonia.Rendering.Composition.Server +{ + internal class ReadbackIndices + { + private readonly object _lock = new object(); + public int ReadIndex { get; private set; } = 0; + public int WriteIndex { get; private set; } = -1; + public ulong ReadRevision { get; private set; } + public ulong WriteRevision { get; private set; } + private ulong[] _revisions = new ulong[3]; + + + public void NextRead() + { + lock (_lock) + { + for (var c = 0; c < 3; c++) + { + if (c != WriteIndex && c != ReadIndex && _revisions[c] > ReadRevision) + { + ReadIndex = c; + ReadRevision = _revisions[c]; + return; + } + } + } + } + + public void NextWrite(ulong revision) + { + lock (_lock) + { + for (var c = 0; c < 3; c++) + { + if (c != WriteIndex && c != ReadIndex) + { + WriteIndex = c; + WriteRevision = revision; + _revisions[c] = revision; + return; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs new file mode 100644 index 0000000000..eb041aaf88 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs @@ -0,0 +1,7 @@ +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract partial class ServerCompositionBrush : ServerObject + { + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs new file mode 100644 index 0000000000..397c968d04 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -0,0 +1,41 @@ +using System.Numerics; +using Avalonia.Collections.Pooled; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual +{ + private CompositionDrawList? _renderCommands; + + public ServerCompositionDrawListVisual(ServerCompositor compositor) : base(compositor) + { + } + + protected override void ApplyCore(ChangeSet changes) + { + var ch = (DrawListVisualChanges)changes; + if (ch.DrawCommandsIsSet) + { + _renderCommands?.Dispose(); + _renderCommands = ch.AcquireDrawCommands(); + } + base.ApplyCore(changes); + } + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + if (_renderCommands != null) + { + foreach (var cmd in _renderCommands) + { + cmd.Item.Render(canvas); + } + } + base.RenderCore(canvas, transform); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs new file mode 100644 index 0000000000..0948b9692f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs @@ -0,0 +1,15 @@ +using System; + +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract partial class ServerCompositionGradientBrush : ServerCompositionBrush + { + public ServerCompositionGradientStopCollection Stops { get; } + public ServerCompositionGradientBrush(ServerCompositor compositor) : base(compositor) + { + Stops = new ServerCompositionGradientStopCollection(compositor); + } + + public override long LastChangedBy => Math.Max(base.LastChangedBy, (long)Stops.LastChangedBy); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs new file mode 100644 index 0000000000..c421cdcfb0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs @@ -0,0 +1,22 @@ +using Avalonia.Media; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionLinearGradientBrush + { + /* + protected override void UpdateBackendBrush(ICbBrush brush) + { + var stopColors = new Color[Stops.List.Count]; + var offsets = new float[Stops.List.Count]; + for (var c = 0; c < Stops.List.Count; c++) + { + stopColors[c] = Stops.List[c].Color; + offsets[c] = Stops.List[c].Offset; + } + + ((ICbLinearGradientBrush) brush).Update(StartPoint, EndPoint, stopColors, offsets, ExtendMode); + }*/ + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs new file mode 100644 index 0000000000..462a193a86 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract class ServerCompositionSurface : ServerObject + { + protected ServerCompositionSurface(ServerCompositor compositor) : base(compositor) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs new file mode 100644 index 0000000000..493529e111 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -0,0 +1,53 @@ +using System; +using System.Numerics; +using System.Threading; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionTarget + { + private readonly ServerCompositor _compositor; + private readonly Func _renderTargetFactory; + private static long s_nextId = 1; + public long Id { get; } + private ulong _frame = 1; + private IRenderTarget? _renderTarget; + + public ReadbackIndices Readback { get; } = new(); + + public ServerCompositionTarget(ServerCompositor compositor, Func renderTargetFactory) : + base(compositor) + { + _compositor = compositor; + _renderTargetFactory = renderTargetFactory; + Id = Interlocked.Increment(ref s_nextId); + } + + partial void OnIsEnabledChanged() + { + if (IsEnabled) + _compositor.AddCompositionTarget(this); + else + _compositor.RemoveCompositionTarget(this); + } + + public void Render() + { + if (Root == null) + return; + _renderTarget ??= _renderTargetFactory(); + + Compositor.UpdateServerTime(); + using (var context = _renderTarget.CreateDrawingContext(null)) + { + context.Clear(Colors.Transparent); + Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + } + + Readback.NextWrite(_frame); + _frame++; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs new file mode 100644 index 0000000000..e56f85acdf --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + internal class ServerCompositor : IRenderLoopTask + { + private readonly IRenderLoop _renderLoop; + private readonly Queue _batches = new Queue(); + public long LastBatchId { get; private set; } + public Stopwatch Clock { get; } = Stopwatch.StartNew(); + public TimeSpan ServerNow { get; private set; } + private List _activeTargets = new(); + + public ServerCompositor(IRenderLoop renderLoop) + { + _renderLoop = renderLoop; + _renderLoop.Add(this); + } + + public void EnqueueBatch(Batch batch) + { + lock (_batches) + _batches.Enqueue(batch); + } + + internal void UpdateServerTime() => ServerNow = Clock.Elapsed; + + List _reusableToCompleteList = new(); + void ApplyPendingBatches() + { + while (true) + { + Batch batch; + lock (_batches) + { + if(_batches.Count == 0) + break; + batch = _batches.Dequeue(); + } + + foreach (var change in batch.Changes) + { + if (change.Dispose) + { + //TODO + } + change.Target!.Apply(change); + change.Reset(); + } + + _reusableToCompleteList.Add(batch); + LastBatchId = batch.SequenceId; + } + } + + void CompletePendingBatches() + { + foreach(var batch in _reusableToCompleteList) + batch.Complete(); + _reusableToCompleteList.Clear(); + } + + bool IRenderLoopTask.NeedsUpdate => false; + void IRenderLoopTask.Update(TimeSpan time) + { + } + + void IRenderLoopTask.Render() + { + ApplyPendingBatches(); + foreach (var t in _activeTargets) + t.Render(); + + CompletePendingBatches(); + } + + public void AddCompositionTarget(ServerCompositionTarget target) + { + _activeTargets.Add(target); + } + + public void RemoveCompositionTarget(ServerCompositionTarget target) + { + _activeTargets.Remove(target); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs new file mode 100644 index 0000000000..ac112b846f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -0,0 +1,29 @@ +using System.Numerics; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal class ServerCompositionContainerVisual : ServerCompositionVisual + { + public ServerCompositionVisualCollection Children { get; } + + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + base.RenderCore(canvas, transform); + + foreach (var ch in Children) + { + var t = transform; + + t = ch.CombinedTransformMatrix * t; + ch.Render(canvas, t); + } + } + + public ServerCompositionContainerVisual(ServerCompositor compositor) : base(compositor) + { + Children = new ServerCompositionVisualCollection(compositor); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs new file mode 100644 index 0000000000..5f3eb051a4 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs @@ -0,0 +1,31 @@ +using System.Numerics; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + class ServerCustomDrawVisual : ServerCompositionContainerVisual + { + private readonly ICustomDrawVisualRenderer _renderer; + private TData? _data; + public ServerCustomDrawVisual(ServerCompositor compositor, ICustomDrawVisualRenderer renderer) : base(compositor) + { + _renderer = renderer; + } + + protected override void ApplyCore(ChangeSet changes) + { + var c = (CustomDrawVisualChanges) changes; + if (c.Data.IsSet) + _data = c.Data.Value; + + base.ApplyCore(changes); + } + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + _renderer.Render(canvas, _data); + base.RenderCore(canvas, transform); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs new file mode 100644 index 0000000000..09ef119e6b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + class ServerList : ServerObject where T : ServerObject + { + public List List { get; } = new List(); + protected override void ApplyCore(ChangeSet changes) + { + var c = (ListChangeSet) changes; + if (c.HasListChanges) + { + foreach (var lc in c.ListChanges) + { + if(lc.Action == ListChangeAction.Clear) + List.Clear(); + if(lc.Action == ListChangeAction.RemoveAt) + List.RemoveAt(lc.Index); + if(lc.Action == ListChangeAction.InsertAt) + List.Insert(lc.Index, lc.Added!); + if (lc.Action == ListChangeAction.ReplaceAt) + List[lc.Index] = lc.Added!; + } + } + } + + public override long LastChangedBy + { + get + { + var seq = base.LastChangedBy; + foreach (var i in List) + seq = Math.Max(i.LastChangedBy, seq); + return seq; + } + } + + public List.Enumerator GetEnumerator() => List.GetEnumerator(); + + public ServerList(ServerCompositor compositor) : base(compositor) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs new file mode 100644 index 0000000000..072377cd7e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -0,0 +1,36 @@ +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract class ServerObject : IExpressionObject + { + public ServerCompositor Compositor { get; } + + public virtual long LastChangedBy => ItselfLastChangedBy; + public long ItselfLastChangedBy { get; private set; } + + public ServerObject(ServerCompositor compositor) + { + Compositor = compositor; + } + + protected virtual void ApplyCore(ChangeSet changes) + { + + } + + public void Apply(ChangeSet changes) + { + ApplyCore(changes); + ItselfLastChangedBy = changes.Batch!.SequenceId; + } + + public virtual ExpressionVariant GetPropertyForAnimation(string name) + { + return default; + } + + ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs new file mode 100644 index 0000000000..60569867de --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs @@ -0,0 +1,16 @@ +using System.Numerics; +using Avalonia.Media.Immutable; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionSolidColorVisual + { + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + canvas.Transform = canvas.CutTransform(transform); + canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new RoundedRect(new Rect(new Size(Size)))); + base.RenderCore(canvas, transform); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs new file mode 100644 index 0000000000..c658dc8ae3 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs @@ -0,0 +1,20 @@ +using System.Numerics; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionSpriteVisual + { + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + if (Brush != null) + { + //SetTransform(canvas, transform); + //canvas.FillRect((Vector2)Size, (ICbBrush)Brush.Brush!); + } + + base.RenderCore(canvas, transform); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs new file mode 100644 index 0000000000..37e188fb47 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -0,0 +1,92 @@ +using System.Numerics; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + unsafe partial class ServerCompositionVisual : ServerObject + { + protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + + } + + public void Render(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + if(Visible == false) + return; + if(Opacity == 0) + return; + canvas.PreTransform = canvas.CutTransform(transform); + canvas.Transform = Matrix.Identity; + if (Opacity != 1) + canvas.PushOpacity(Opacity); + if(ClipToBounds) + canvas.PushClip(new Rect(new Size(Size.X, Size.Y))); + if (Clip != null) + canvas.PushGeometryClip(Clip); + + RenderCore(canvas, transform); + + if (Clip != null) + canvas.PopGeometryClip(); + if (ClipToBounds) + canvas.PopClip(); + if(Opacity != 1) + canvas.PopOpacity(); + } + + private ReadbackData _readback0, _readback1, _readback2; + + + public ref ReadbackData GetReadback(int idx) + { + if (idx == 0) + return ref _readback0; + if (idx == 1) + return ref _readback1; + return ref _readback2; + } + + public Matrix4x4 CombinedTransformMatrix + { + get + { + if (Root == null) + return default; + + var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, + Scale, RotationAngle, Orientation, Offset); + var i = Root.Readback; + ref var readback = ref GetReadback(i.WriteIndex); + readback.Revision = i.WriteRevision; + readback.Matrix = res; + readback.TargetId = Root.Id; + + return res; + } + } + + public struct ReadbackData + { + public Matrix4x4 Matrix; + public bool Visible; + public ulong Revision; + public long TargetId; + } + + partial void ApplyChangesExtra(CompositionVisualChanges c) + { + if (c.Parent.IsSet) + Parent = c.Parent.Value; + if (c.Root.IsSet) + Root = c.Root.Value; + } + + public ServerCompositionTarget? Root { get; private set; } + + public ServerCompositionVisual? Parent { get; private set; } + } + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs new file mode 100644 index 0000000000..7b64c01d09 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalonia.Rendering.Composition.Transport +{ + internal class Batch + { + private static long _nextSequenceId = 1; + private static ConcurrentBag> _pool = new ConcurrentBag>(); + public long SequenceId { get; } + + public Batch() + { + SequenceId = Interlocked.Increment(ref _nextSequenceId); + if (!_pool.TryTake(out var lst)) + lst = new List(); + Changes = lst; + } + private TaskCompletionSource _tcs = new TaskCompletionSource(); + public List Changes { get; private set; } + public TimeSpan CommitedAt { get; set; } + + public void Complete() + { + Changes.Clear(); + _pool.Add(Changes); + Changes = null!; + + _tcs.TrySetResult(0); + } + + public Task Completed => _tcs.Task; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs new file mode 100644 index 0000000000..cbee350ab3 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs @@ -0,0 +1,82 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Rendering.Composition.Animations; + +namespace Avalonia.Rendering.Composition.Transport +{ + struct Change + { + private T? _value; + + public bool IsSet { get; private set; } + + public T? Value + { + get + { + if(!IsSet) + throw new InvalidOperationException(); + return _value; + } + set + { + IsSet = true; + _value = value; + } + } + + public void Reset() + { + _value = default; + IsSet = false; + } + } + + struct AnimatedChange + { + private T? _value; + private IAnimationInstance? _animation; + + public bool IsValue { get; private set; } + public bool IsAnimation { get; private set; } + + public T Value + { + get + { + if(!IsValue) + throw new InvalidOperationException(); + return _value!; + } + set + { + IsAnimation = false; + _animation = null; + IsValue = true; + _value = value; + } + } + + public IAnimationInstance Animation + { + get + { + if(!IsAnimation) + throw new InvalidOperationException(); + return _animation!; + } + set + { + IsValue = false; + _value = default; + IsAnimation = true; + _animation = value; + } + } + + public void Reset() + { + this = default; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs new file mode 100644 index 0000000000..898885dce6 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs @@ -0,0 +1,36 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + internal abstract class ChangeSet + { + private readonly IChangeSetPool _pool; + public Batch Batch = null!; + public ServerObject? Target; + public bool Dispose; + + public ChangeSet(IChangeSetPool pool) + { + _pool = pool; + } + + public virtual void Reset() + { + Batch = null!; + Target = null; + Dispose = false; + } + + public void Return() + { + _pool.Return(this); + } + } + + internal class CompositionObjectChanges : ChangeSet + { + public CompositionObjectChanges(IChangeSetPool pool) : base(pool) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs new file mode 100644 index 0000000000..ea97cd7d44 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Concurrent; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + interface IChangeSetPool + { + void Return(ChangeSet changes); + ChangeSet Get(ServerObject target, Batch batch); + } + + class ChangeSetPool : IChangeSetPool where T : ChangeSet + { + private readonly Func _factory; + private readonly ConcurrentBag _pool = new ConcurrentBag(); + + public ChangeSetPool(Func factory) + { + _factory = factory; + } + + public void Return(T changes) + { + changes.Reset(); + _pool.Add(changes); + } + + void IChangeSetPool.Return(ChangeSet changes) => Return((T) changes); + ChangeSet IChangeSetPool.Get(ServerObject target, Batch batch) => Get(target, batch); + + public T Get(ServerObject target, Batch batch) + { + if (!_pool.TryTake(out var res)) + res = _factory(this); + res.Target = target; + res.Batch = batch; + res.Dispose = false; + return res; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs new file mode 100644 index 0000000000..aed041b62e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs @@ -0,0 +1,20 @@ +namespace Avalonia.Rendering.Composition.Transport +{ + class CustomDrawVisualChanges : CompositionVisualChanges + { + public CustomDrawVisualChanges(IChangeSetPool pool) : base(pool) + { + } + + public Change Data; + + public override void Reset() + { + Data.Reset(); + base.Reset(); + } + + public new static ChangeSetPool> Pool { get; } = + new ChangeSetPool>(pool => new CustomDrawVisualChanges(pool)); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs new file mode 100644 index 0000000000..215c03b229 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Avalonia.Collections.Pooled; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Transport; + +internal class DrawListVisualChanges : CompositionVisualChanges +{ + private CompositionDrawList? _drawCommands; + + public DrawListVisualChanges(IChangeSetPool pool) : base(pool) + { + } + + public CompositionDrawList? DrawCommands + { + get => _drawCommands; + set + { + _drawCommands?.Dispose(); + _drawCommands = value; + DrawCommandsIsSet = true; + } + } + + public bool DrawCommandsIsSet { get; private set; } + + public CompositionDrawList? AcquireDrawCommands() + { + var rv = _drawCommands; + _drawCommands = null; + DrawCommandsIsSet = false; + return rv; + } + + public override void Reset() + { + _drawCommands?.Dispose(); + _drawCommands = null; + DrawCommandsIsSet = false; + base.Reset(); + } + + public new static ChangeSetPool Pool { get; } = + new ChangeSetPool(pool => new(pool)); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs new file mode 100644 index 0000000000..ee6e4231f8 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs @@ -0,0 +1,19 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + internal class ListChange where T : ServerObject + { + public int Index; + public ListChangeAction Action; + public T? Added; + } + + internal enum ListChangeAction + { + InsertAt, + RemoveAt, + Clear, + ReplaceAt + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs new file mode 100644 index 0000000000..9bb101a080 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + class ListChangeSet : ChangeSet where T : ServerObject + { + private List>? _listChanges; + public List> ListChanges => _listChanges ??= new List>(); + public bool HasListChanges => _listChanges != null; + + public override void Reset() + { + _listChanges?.Clear(); + base.Reset(); + } + + public ListChangeSet(IChangeSetPool pool) : base(pool) + { + } + + public static readonly ChangeSetPool> Pool = + new ChangeSetPool>(pool => new ListChangeSet(pool)); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs new file mode 100644 index 0000000000..1add3aa990 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs @@ -0,0 +1,92 @@ +using System.Collections; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + class ServerListProxyHelper : IList + where TServer : ServerObject + where TClient : CompositionObject + { + private readonly IGetChanges _parent; + private readonly List _list = new List(); + + public interface IGetChanges + { + ListChangeSet GetChanges(); + } + + public ServerListProxyHelper(IGetChanges parent) + { + _parent = parent; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(TClient item) => Insert(_list.Count, item); + + public void Clear() + { + _list.Clear(); + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.Clear + }); + } + + public bool Contains(TClient item) => _list.Contains(item); + + public void CopyTo(TClient[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + + public bool Remove(TClient item) + { + var idx = _list.IndexOf(item); + if (idx == -1) + return false; + RemoveAt(idx); + return true; + } + + public int Count => _list.Count; + public bool IsReadOnly => false; + public int IndexOf(TClient item) => _list.IndexOf(item); + + public void Insert(int index, TClient item) + { + _list.Insert(index, item); + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.InsertAt, + Index = index, + Added = (TServer) item.Server + }); + } + + public void RemoveAt(int index) + { + _list.RemoveAt(index); + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.RemoveAt, + Index = index + }); + } + + public TClient this[int index] + { + get => _list[index]; + set + { + _list[index] = value; + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.ReplaceAt, + Index = index, + Added = (TServer) value.Server + }); + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs new file mode 100644 index 0000000000..c87fb96967 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs @@ -0,0 +1,16 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + partial class CompositionVisualChanges + { + public Change Parent; + public Change Root; + + partial void ResetExtra() + { + Parent.Reset(); + Root.Reset(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs b/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs new file mode 100644 index 0000000000..8c85d7978b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs @@ -0,0 +1,302 @@ +// ReSharper disable InconsistentNaming +// Ported from Chromium project https://github.com/chromium/chromium/blob/374d31b7704475fa59f7b2cb836b3b68afdc3d79/ui/gfx/geometry/cubic_bezier.cc + +using System; + +// ReSharper disable CompareOfFloatsByEqualityOperator +// ReSharper disable CommentTypo +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable TooWideLocalVariableScope +// ReSharper disable UnusedMember.Global +#pragma warning disable 649 + +namespace Avalonia.Rendering.Composition.Utils +{ + internal unsafe struct CubicBezier + { + const int CUBIC_BEZIER_SPLINE_SAMPLES = 11; + double ax_; + double bx_; + double cx_; + + double ay_; + double by_; + double cy_; + + double start_gradient_; + double end_gradient_; + + double range_min_; + double range_max_; + private bool monotonically_increasing_; + + fixed double spline_samples_[CUBIC_BEZIER_SPLINE_SAMPLES]; + + public CubicBezier(double p1x, double p1y, double p2x, double p2y) : this() + { + InitCoefficients(p1x, p1y, p2x, p2y); + InitGradients(p1x, p1y, p2x, p2y); + InitRange(p1y, p2y); + InitSpline(); + } + + public readonly double SampleCurveX(double t) + { + // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule. + return ((ax_ * t + bx_) * t + cx_) * t; + } + + readonly double SampleCurveY(double t) + { + return ((ay_ * t + by_) * t + cy_) * t; + } + + readonly double SampleCurveDerivativeX(double t) + { + return (3.0 * ax_ * t + 2.0 * bx_) * t + cx_; + } + + readonly double SampleCurveDerivativeY(double t) + { + return (3.0 * ay_ * t + 2.0 * by_) * t + cy_; + } + + public readonly double SolveWithEpsilon(double x, double epsilon) + { + if (x < 0.0) + return 0.0 + start_gradient_ * x; + if (x > 1.0) + return 1.0 + end_gradient_ * (x - 1.0); + return SampleCurveY(SolveCurveX(x, epsilon)); + } + + void InitCoefficients(double p1x, + double p1y, + double p2x, + double p2y) + { + // Calculate the polynomial coefficients, implicit first and last control + // points are (0,0) and (1,1). + cx_ = 3.0 * p1x; + bx_ = 3.0 * (p2x - p1x) - cx_; + ax_ = 1.0 - cx_ - bx_; + + cy_ = 3.0 * p1y; + by_ = 3.0 * (p2y - p1y) - cy_; + ay_ = 1.0 - cy_ - by_; + +#if DEBUG + // Bezier curves with x-coordinates outside the range [0,1] for internal + // control points may have multiple values for t for a given value of x. + // In this case, calls to SolveCurveX may produce ambiguous results. + monotonically_increasing_ = p1x >= 0 && p1x <= 1 && p2x >= 0 && p2x <= 1; +#endif + } + + void InitGradients(double p1x, + double p1y, + double p2x, + double p2y) + { + // End-point gradients are used to calculate timing function results + // outside the range [0, 1]. + // + // There are four possibilities for the gradient at each end: + // (1) the closest control point is not horizontally coincident with regard to + // (0, 0) or (1, 1). In this case the line between the end point and + // the control point is tangent to the bezier at the end point. + // (2) the closest control point is coincident with the end point. In + // this case the line between the end point and the far control + // point is tangent to the bezier at the end point. + // (3) both internal control points are coincident with an endpoint. There + // are two special case that fall into this category: + // CubicBezier(0, 0, 0, 0) and CubicBezier(1, 1, 1, 1). Both are + // equivalent to linear. + // (4) the closest control point is horizontally coincident with the end + // point, but vertically distinct. In this case the gradient at the + // end point is Infinite. However, this causes issues when + // interpolating. As a result, we break down to a simple case of + // 0 gradient under these conditions. + + if (p1x > 0) + start_gradient_ = p1y / p1x; + else if (p1y == 0 && p2x > 0) + start_gradient_ = p2y / p2x; + else if (p1y == 0 && p2y == 0) + start_gradient_ = 1; + else + start_gradient_ = 0; + + if (p2x < 1) + end_gradient_ = (p2y - 1) / (p2x - 1); + else if (p2y == 1 && p1x < 1) + end_gradient_ = (p1y - 1) / (p1x - 1); + else if (p2y == 1 && p1y == 1) + end_gradient_ = 1; + else + end_gradient_ = 0; + } + + const double kBezierEpsilon = 1e-7; + + void InitRange(double p1y, double p2y) + { + range_min_ = 0; + range_max_ = 1; + if (0 <= p1y && p1y < 1 && 0 <= p2y && p2y <= 1) + return; + + double epsilon = kBezierEpsilon; + + // Represent the function's derivative in the form at^2 + bt + c + // as in sampleCurveDerivativeY. + // (Technically this is (dy/dt)*(1/3), which is suitable for finding zeros + // but does not actually give the slope of the curve.) + double a = 3.0 * ay_; + double b = 2.0 * by_; + double c = cy_; + + // Check if the derivative is constant. + if (Math.Abs(a) < epsilon && Math.Abs(b) < epsilon) + return; + + // Zeros of the function's derivative. + double t1; + double t2 = 0; + + if (Math.Abs(a) < epsilon) + { + // The function's derivative is linear. + t1 = -c / b; + } + else + { + // The function's derivative is a quadratic. We find the zeros of this + // quadratic using the quadratic formula. + double discriminant = b * b - 4 * a * c; + if (discriminant < 0) + return; + double discriminant_sqrt = Math.Sqrt(discriminant); + t1 = (-b + discriminant_sqrt) / (2 * a); + t2 = (-b - discriminant_sqrt) / (2 * a); + } + + double sol1 = 0; + double sol2 = 0; + + // If the solution is in the range [0,1] then we include it, otherwise we + // ignore it. + + // An interesting fact about these beziers is that they are only + // actually evaluated in [0,1]. After that we take the tangent at that point + // and linearly project it out. + if (0 < t1 && t1 < 1) + sol1 = SampleCurveY(t1); + + if (0 < t2 && t2 < 1) + sol2 = SampleCurveY(t2); + + range_min_ = Math.Min(Math.Min(range_min_, sol1), sol2); + range_max_ = Math.Max(Math.Max(range_max_, sol1), sol2); + } + + void InitSpline() + { + double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); + for (int i = 0; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) + { + spline_samples_[i] = SampleCurveX(i * delta_t); + } + } + + const int kMaxNewtonIterations = 4; + + + public readonly double SolveCurveX(double x, double epsilon) + { + if (x < 0 || x > 1) + throw new ArgumentException(); + + double t0 = 0; + double t1 = 0; + double t2 = x; + double x2 = 0; + double d2; + int i; + +#if DEBUG + if (!monotonically_increasing_) + throw new InvalidOperationException(); +#endif + + // Linear interpolation of spline curve for initial guess. + double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); + for (i = 1; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) + { + if (x <= spline_samples_[i]) + { + t1 = delta_t * i; + t0 = t1 - delta_t; + t2 = t0 + (t1 - t0) * (x - spline_samples_[i - 1]) / + (spline_samples_[i] - spline_samples_[i - 1]); + break; + } + } + + // Perform a few iterations of Newton's method -- normally very fast. + // See https://en.wikipedia.org/wiki/Newton%27s_method. + double newton_epsilon = Math.Min(kBezierEpsilon, epsilon); + for (i = 0; i < kMaxNewtonIterations; i++) + { + x2 = SampleCurveX(t2) - x; + if (Math.Abs(x2) < newton_epsilon) + return t2; + d2 = SampleCurveDerivativeX(t2); + if (Math.Abs(d2) < kBezierEpsilon) + break; + t2 = t2 - x2 / d2; + } + + if (Math.Abs(x2) < epsilon) + return t2; + + // Fall back to the bisection method for reliability. + while (t0 < t1) + { + x2 = SampleCurveX(t2); + if (Math.Abs(x2 - x) < epsilon) + return t2; + if (x > x2) + t0 = t2; + else + t1 = t2; + t2 = (t1 + t0) * .5; + } + + // Failure. + return t2; + } + + public readonly double Solve(double x) + { + return SolveWithEpsilon(x, kBezierEpsilon); + } + + public readonly double SlopeWithEpsilon(double x, double epsilon) + { + x = MathExt.Clamp(x, 0.0, 1.0); + double t = SolveCurveX(x, epsilon); + double dx = SampleCurveDerivativeX(t); + double dy = SampleCurveDerivativeY(t); + return dy / dx; + } + + public readonly double Slope(double x) + { + return SlopeWithEpsilon(x, kBezierEpsilon); + } + + public readonly double RangeMin => range_min_; + public readonly double RangeMax => range_max_; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs b/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs new file mode 100644 index 0000000000..0be19a8e9d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs @@ -0,0 +1,23 @@ +using System; + +namespace Avalonia.Rendering.Composition.Utils +{ + static class MathExt + { + public static float Clamp(float value, float min, float max) + { + var amax = Math.Max(min, max); + var amin = Math.Min(min, max); + return Math.Min(Math.Max(value, amin), amax); + } + + public static double Clamp(double value, double min, double max) + { + var amax = Math.Max(min, max); + var amin = Math.Min(min, max); + return Math.Min(Math.Max(value, amin), amax); + } + + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs new file mode 100644 index 0000000000..1e6d7f8abb --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -0,0 +1,64 @@ +using System.Numerics; + +namespace Avalonia.Rendering.Composition +{ + public abstract partial class CompositionVisual + { + private CompositionVisual? _parent; + private CompositionTarget? _root; + + public CompositionVisual? Parent + { + get => _parent; + internal set + { + if (_parent == value) + return; + _parent = value; + Changes.Parent.Value = value?.Server; + Root = _parent?.Root; + } + } + + // TODO: hide behind private-ish API + public CompositionTarget? Root + { + get => _root; + internal set + { + var changed = _root != value; + _root = value; + Changes.Root.Value = value?.Server; + if (changed) + OnRootChanged(); + } + } + + private protected virtual void OnRootChanged() + { + } + + + internal Matrix4x4? TryGetServerTransform() + { + if (Root == null) + return null; + var i = Root.Server.Readback; + ref var readback = ref Server.GetReadback(i.ReadIndex); + + // CompositionVisual wasn't visible + if (readback.Revision < i.ReadRevision) + return null; + + // CompositionVisual was reparented (potential race here) + if (readback.TargetId != Root.Server.Id) + return null; + + return readback.Matrix; + } + + internal object? Tag { get; set; } + + internal virtual bool HitTest(Vector2 point) => true; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs new file mode 100644 index 0000000000..35f33c3b38 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -0,0 +1,64 @@ +using System; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public partial class CompositionVisualCollection : CompositionObject + { + private CompositionVisual _owner; + internal CompositionVisualCollection(CompositionVisual parent, ServerCompositionVisualCollection server) : base(parent.Compositor, server) + { + _owner = parent; + InitializeDefaults(); + } + + public void InsertAbove(CompositionVisual newChild, CompositionVisual sibling) + { + var idx = _list.IndexOf(sibling); + if (idx == -1) + throw new InvalidOperationException(); + + Insert(idx + 1, newChild); + } + + public void InsertBelow(CompositionVisual newChild, CompositionVisual sibling) + { + var idx = _list.IndexOf(sibling); + if (idx == -1) + throw new InvalidOperationException(); + Insert(idx, newChild); + } + + public void InsertAtTop(CompositionVisual newChild) => Insert(_list.Count, newChild); + + public void InsertAtBottom(CompositionVisual newChild) => Insert(0, newChild); + + public void RemoveAll() => Clear(); + + partial void OnAdded(CompositionVisual item) => item.Parent = _owner; + + partial void OnBeforeReplace(CompositionVisual oldItem, CompositionVisual newItem) + { + if (oldItem != newItem) + OnBeforeAdded(newItem); + } + + partial void OnReplace(CompositionVisual oldItem, CompositionVisual newItem) + { + if (oldItem != newItem) + { + OnRemoved(oldItem); + OnAdded(newItem); + } + } + + partial void OnRemoved(CompositionVisual item) => item.Parent = null; + + + partial void OnBeforeAdded(CompositionVisual item) + { + if (item.Parent != null) + throw new InvalidOperationException("Visual already has a parent"); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs index e998f78d5c..8d6aabf440 100644 --- a/src/Avalonia.Base/Rendering/IRenderer.cs +++ b/src/Avalonia.Base/Rendering/IRenderer.cs @@ -1,6 +1,7 @@ using System; using Avalonia.VisualTree; using System.Collections.Generic; +using Avalonia.Rendering.Composition; namespace Avalonia.Rendering { @@ -87,4 +88,9 @@ namespace Avalonia.Rendering /// void Stop(); } + + public interface IRendererWithCompositor : IRenderer + { + Compositor Compositor { get; } + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 5225b85020..3f495c619c 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Utilities; diff --git a/src/Avalonia.Base/Size.cs b/src/Avalonia.Base/Size.cs index 69c3ae7319..5f20206200 100644 --- a/src/Avalonia.Base/Size.cs +++ b/src/Avalonia.Base/Size.cs @@ -52,6 +52,17 @@ namespace Avalonia _width = width; _height = height; } + +#if !BUILDTASK + /// + /// Initializes a new instance of the structure. + /// + /// The vector to take values from. + public Size(System.Numerics.Vector2 vector2) : this(vector2.X, vector2.Y) + { + + } +#endif /// /// Gets the aspect ratio of the size. diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index a93e4f406d..b4bf603f74 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -62,10 +62,20 @@ namespace Avalonia.Threading /// public static readonly DispatcherPriority Render = new(5); + /// + /// The job will be processed with the same priority as composition batch commit. + /// + public static readonly DispatcherPriority CompositionBatch = new(6); + + /// + /// The job will be processed with the same priority as composition updates. + /// + public static readonly DispatcherPriority Composition = new(7); + /// /// The job will be processed with the same priority as render. /// - public static readonly DispatcherPriority Layout = new(6); + public static readonly DispatcherPriority Layout = new(8); /// /// The job will be processed with the same priority as data binding. @@ -75,7 +85,7 @@ namespace Avalonia.Threading /// /// The job will be processed before other asynchronous operations. /// - public static readonly DispatcherPriority Send = new(7); + public static readonly DispatcherPriority Send = new(9); /// /// Maximum possible priority diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 4fd21f02f9..64341be0c7 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -1,3 +1,7 @@ + + +#nullable enable + using System; using System.Collections; using System.Collections.Specialized; @@ -8,11 +12,10 @@ using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Server; using Avalonia.Utilities; using Avalonia.VisualTree; - -#nullable enable - namespace Avalonia { /// @@ -288,6 +291,8 @@ namespace Avalonia /// protected IRenderRoot? VisualRoot => _visualRoot ?? (this as IRenderRoot); + internal CompositionDrawListVisual? CompositionVisual { get; private set; } + /// /// Gets a value indicating whether this control is attached to a visual root. /// @@ -432,6 +437,10 @@ namespace Avalonia } EnableTransitions(); + if (_visualRoot.Renderer is IRendererWithCompositor compositingRenderer) + { + AttachToCompositor(compositingRenderer.Compositor); + } OnAttachedToVisualTree(e); AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); @@ -452,6 +461,14 @@ namespace Avalonia } } + internal CompositionVisual AttachToCompositor(Compositor compositor) + { + if (CompositionVisual == null || CompositionVisual.Compositor != compositor) + CompositionVisual = new CompositionDrawListVisual(compositor, + new ServerCompositionDrawListVisual(compositor.Server), this); + return CompositionVisual; + } + /// /// Calls the method /// for this control and all of its visual descendants. @@ -564,7 +581,7 @@ namespace Avalonia { newValue.Changed += sender.RenderTransformChanged; } - + sender.InvalidateVisual(); } } diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml new file mode 100644 index 0000000000..cf864dac2d --- /dev/null +++ b/src/Avalonia.Base/composition-schema.xml @@ -0,0 +1,70 @@ + + + System.Numerics + Avalonia.Rendering.Composition.Server + Avalonia.Rendering.Composition.Transport + Avalonia.Rendering.Composition.Animations + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index addc248d58..984f76adb6 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Numerics; using System.Runtime.InteropServices; using Avalonia.Media; using Avalonia.Platform; @@ -344,6 +345,7 @@ namespace Avalonia.Headless } public Matrix Transform { get; set; } + public void Clear(Color color) { diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs new file mode 100644 index 0000000000..f079a339df --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs @@ -0,0 +1,21 @@ +using System.IO; +using System.Xml.Serialization; +using Microsoft.CodeAnalysis; + +namespace Avalonia.SourceGenerator.CompositionGenerator; + +[Generator(LanguageNames.CSharp)] +public class CompositionRoslynGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var schema = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml")); + var configs = schema.Select((t, _) => + (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString()))); + context.RegisterSourceOutput(configs, (spc, config) => + { + var generator = new Generator(spc, config); + generator.Generate(); + }); + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs new file mode 100644 index 0000000000..8b6aca33cd --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs @@ -0,0 +1,116 @@ +#nullable disable +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + + + [XmlRoot("NComposition")] + public class GConfig + { + [XmlElement("Using")] + public List Usings { get; set; } = new List(); + + [XmlElement(typeof(GManualClass), ElementName = "Manual")] + public List ManualClasses { get; set; } = new List(); + + [XmlElement(typeof(GClass), ElementName = "Object")] + [XmlElement(typeof(GBrush), ElementName = "Brush")] + [XmlElement(typeof(GList), ElementName = "List")] + public List Classes { get; set; } = new List(); + + [XmlElement(typeof(GAnimationType), ElementName = "KeyFrameAnimation")] + public List KeyFrameAnimations { get; set; } = new List(); + } + + public class GUsing + { + [XmlText] + public string Name { get; set; } + } + + public class GManualClass + { + [XmlAttribute] + public string Name { get; set; } + + [XmlAttribute] + public string ServerName { get; set; } + } + + public class GImplements + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string ServerName { get; set; } + } + + public class GClass + { + [XmlAttribute] + public string Name { get; set; } + + [XmlAttribute] + public string Inherits { get; set; } + + [XmlAttribute] + public string ChangesBase { get; set; } + + [XmlAttribute] + public string ServerBase { get; set; } + + [XmlAttribute] + public bool CustomCtor { get; set; } + + [XmlAttribute] + public bool CustomServerCtor { get; set; } + + [XmlElement(typeof(GImplements), ElementName = "Implements")] + public List Implements { get; set; } = new List(); + + [XmlAttribute] + public bool Abstract { get; set; } + + [XmlElement(typeof(GProperty), ElementName = "Property")] + public List Properties { get; set; } = new List(); + } + + public class GBrush : GClass + { + [XmlAttribute] + public bool CustomUpdate { get; set; } + + public GBrush() + { + Inherits = "CompositionBrush"; + } + } + + public class GList : GClass + { + [XmlAttribute] + public string ItemType { get; set; } + } + + public class GProperty + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string Type { get; set; } + [XmlAttribute] + public string DefaultValue { get; set; } + [XmlAttribute] + public bool Animated { get; set; } + } + + public class GAnimationType + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string Type { get; set; } + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs new file mode 100644 index 0000000000..43a4a4afa7 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs @@ -0,0 +1,90 @@ +using System; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public static class Extensions + { + public static ClassDeclarationSyntax AddModifiers(this ClassDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static MethodDeclarationSyntax AddModifiers(this MethodDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static PropertyDeclarationSyntax AddModifiers(this PropertyDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static ConstructorDeclarationSyntax AddModifiers(this ConstructorDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static AccessorDeclarationSyntax AddModifiers(this AccessorDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static string WithLowerFirst(this string s) + { + if (string.IsNullOrEmpty(s)) + return s; + return char.ToLowerInvariant(s[0]) + s.Substring(1); + } + + public static ExpressionSyntax MemberAccess(params string[] identifiers) + { + if (identifiers == null || identifiers.Length == 0) + throw new ArgumentException(); + var expr = (ExpressionSyntax)IdentifierName(identifiers[0]); + for (var c = 1; c < identifiers.Length; c++) + expr = MemberAccess(expr, identifiers[c]); + return expr; + } + + public static ExpressionSyntax MemberAccess(ExpressionSyntax expr, params string[] identifiers) + { + foreach (var i in identifiers) + expr = MemberAccess(expr, i); + return expr; + } + + public static MemberAccessExpressionSyntax MemberAccess(ExpressionSyntax expr, string identifier) => + MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, expr, IdentifierName(identifier)); + + public static ExpressionSyntax ConditionalMemberAccess(ExpressionSyntax expr, string member, bool checkNull) + { + if (checkNull) + return ConditionalAccessExpression(expr, MemberBindingExpression(IdentifierName(member))); + return MemberAccess(expr, member); + } + + public static ClassDeclarationSyntax WithBaseType(this ClassDeclarationSyntax cl, string bt) + { + return cl.AddBaseListTypes(SimpleBaseType(SyntaxFactory.ParseTypeName(bt))); + } + + public static string StripPrefix(this string s, string prefix) => string.IsNullOrEmpty(s) + ? s + : s.StartsWith(prefix) + ? s.Substring(prefix.Length) + : s; + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs new file mode 100644 index 0000000000..7d5146c5f5 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs @@ -0,0 +1,59 @@ +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + void GenerateAnimations() + { + var code = $@"using System.Numerics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition +{{ +"; + + foreach (var a in _config.KeyFrameAnimations) + { + var name = a.Name ?? a.Type; + + code += $@" + public class {name}KeyFrameAnimation : KeyFrameAnimation + {{ + public {name}KeyFrameAnimation(Compositor compositor) : base(compositor) + {{ + }} + + internal override IAnimationInstance CreateInstance(Avalonia.Rendering.Composition.Server.ServerObject targetObject, ExpressionVariant? finalValue) + {{ + return new KeyFrameAnimationInstance<{a.Type}>({name}Interpolator.Instance, _keyFrames.Snapshot(), CreateSnapshot(true), + finalValue?.CastOrDefault<{a.Type}>(), targetObject, + DelayBehavior, DelayTime, Direction, Duration, IterationBehavior, + IterationCount, StopBehavior); + }} + + private KeyFrames<{a.Type}> _keyFrames = new KeyFrames<{a.Type}>(); + private protected override IKeyFrames KeyFrames => _keyFrames; + + public void InsertKeyFrame(float normalizedProgressKey, {a.Type} value, CompositionEasingFunction easingFunction) + {{ + _keyFrames.Insert(normalizedProgressKey, value, easingFunction); + }} + + public void InsertKeyFrame(float normalizedProgressKey, {a.Type} value) + {{ + _keyFrames.Insert(normalizedProgressKey, value, Compositor.DefaultEasing); + }} + }} + + public partial class Compositor + {{ + public {name}KeyFrameAnimation Create{name}KeyFrameAnimation() => new {name}KeyFrameAnimation(this); + }} +"; + } + + code += "}"; + _output.AddSource("CompositionAnimations.cs", code); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs new file mode 100644 index 0000000000..593386f713 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs @@ -0,0 +1,121 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + private const string ListProxyTemplate = @" +class Template +{ + private ServerListProxyHelper _list = null!; + + ListChangeSet + ServerListProxyHelper.IGetChanges. + GetChanges() => Changes; + + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _list).GetEnumerator(); + + public void Add(ItemTypeName item) + { + OnBeforeAdded(item); + _list.Add(item); + OnAdded(item); + } + + public void Clear() + { + OnBeforeClear(); + _list.Clear(); + OnClear(); + } + + public bool Contains(ItemTypeName item) => _list.Contains(item); + + public void CopyTo(ItemTypeName[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + + public bool Remove(ItemTypeName item) + { + var removed = _list.Remove(item); + if(removed) + OnRemoved(item); + return removed; + } + + public int Count => _list.Count; + + public bool IsReadOnly => _list.IsReadOnly; + + public int IndexOf(ItemTypeName item) => _list.IndexOf(item); + + public void Insert(int index, ItemTypeName item) + { + OnBeforeAdded(item); + _list.Insert(index, item); + OnAdded(item); + } + + public void RemoveAt(int index) + { + var item = _list[index]; + _list.RemoveAt(index); + OnRemoved(item); + } + + public ItemTypeName this[int index] + { + get => _list[index]; + set + { + var old = _list[index]; + OnBeforeReplace(old, value); + _list[index] = value; + OnReplace(old, value); + } + } + + partial void OnBeforeAdded(ItemTypeName item); + partial void OnAdded(ItemTypeName item); + partial void OnRemoved(ItemTypeName item); + partial void OnBeforeClear(); + partial void OnBeforeReplace(ItemTypeName oldItem, ItemTypeName newItem); + partial void OnReplace(ItemTypeName oldItem, ItemTypeName newItem); + partial void OnClear(); +} +"; + + private ClassDeclarationSyntax AppendListProxy(GList list, ClassDeclarationSyntax cl) + { + + var itemType = list.ItemType; + var serverItemType = ServerName(itemType); + + cl = cl.AddBaseListTypes(SimpleBaseType( + ParseTypeName("ServerListProxyHelper<" + itemType + ", " + serverItemType + ">.IGetChanges")), + SimpleBaseType(ParseTypeName("IList<" + itemType + ">")) + ); + var code = ListProxyTemplate.Replace("ListTypeName", list.Name) + .Replace("ItemTypeName", itemType); + + var parsed = ParseCompilationUnit(code); + var parsedClass = (ClassDeclarationSyntax)parsed.Members.First(); + + cl = cl.AddMembers(parsedClass.Members.ToArray()); + + var defs = cl.Members.OfType().First(m => m.Identifier.Text == "InitializeDefaults"); + + cl = cl.ReplaceNode(defs.Body, defs.Body.AddStatements( + + ParseStatement($"_list = new ServerListProxyHelper<{itemType}, {serverItemType}>(this);"))); + + return cl; + } + + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs new file mode 100644 index 0000000000..b53c247991 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs @@ -0,0 +1,66 @@ +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + static void CleanDirectory(string path) + { + Directory.CreateDirectory(path); + Directory.Delete(path, true); + Directory.CreateDirectory(path); + } + + CompilationUnitSyntax Unit() + => CompilationUnit().WithUsings(List(new[] + { + "System", + "System.Text", + "System.Collections", + "System.Collections.Generic" + } + .Concat(_config.Usings + .Select(x => x.Name)).Select(u => UsingDirective(IdentifierName(u))))); + + void SaveTo(CompilationUnitSyntax unit, params string[] path) + { + var text = @" +#nullable enable +#pragma warning disable CS0108, CS0114 + +" + + + unit.NormalizeWhitespace().ToFullString(); + _output.AddSource(string.Join("_", path), text); + } + + + SyntaxToken Semicolon() => Token(SyntaxKind.SemicolonToken); + + + FieldDeclarationSyntax DeclareConstant(string type, string name, LiteralExpressionSyntax value) + => FieldDeclaration( + VariableDeclaration(ParseTypeName(type), + SingletonSeparatedList( + VariableDeclarator(name).WithInitializer(EqualsValueClause(value)) + )) + ).WithSemicolonToken(Semicolon()) + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.ConstKeyword))); + + FieldDeclarationSyntax DeclareField(string type, string name, params SyntaxKind[] modifiers) => + DeclareField(type, name, null, modifiers); + + FieldDeclarationSyntax DeclareField(string type, string name, EqualsValueClauseSyntax initializer, + params SyntaxKind[] modifiers) => + FieldDeclaration( + VariableDeclaration(ParseTypeName(type), + SingletonSeparatedList( + VariableDeclarator(name).WithInitializer(initializer)))) + .WithSemicolonToken(Semicolon()) + .WithModifiers(TokenList(modifiers.Select(x => Token(x)))); + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs new file mode 100644 index 0000000000..43ef4a96e8 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs @@ -0,0 +1,504 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Avalonia.SourceGenerator.CompositionGenerator.Extensions; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + partial class Generator + { + private readonly SourceProductionContext _output; + private readonly GConfig _config; + private readonly HashSet _objects; + private readonly HashSet _brushes; + private readonly Dictionary _manuals; + public Generator(SourceProductionContext output, GConfig config) + { + _output = output; + _config = config; + _manuals = _config.ManualClasses.ToDictionary(x => x.Name); + _objects = new HashSet(_config.ManualClasses.Select(x => x.Name) + .Concat(_config.Classes.Select(x => x.Name))); + _brushes = new HashSet(_config.Classes.OfType().Select(x => x.Name)) {"CompositionBrush"}; + } + + + + public void Generate() + { + foreach (var cl in _config.Classes) + GenerateClass(cl); + + GenerateAnimations(); + } + + + + string ServerName(string c) => c != null ? ("Server" + c) : "ServerObject"; + string ChangesName(string c) => c != null ? (c + "Changes") : "ChangeSet"; + + void GenerateClass(GClass cl) + { + var list = cl as GList; + + var unit = Unit(); + + var clientNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition")); + var serverNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition.Server")); + var transportNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition.Transport")); + + var inherits = cl.Inherits ?? "CompositionObject"; + var abstractModifier = cl.Abstract ? new[] {SyntaxKind.AbstractKeyword} : null; + + var client = ClassDeclaration(cl.Name) + .AddModifiers(abstractModifier) + .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(inherits); + + var serverName = ServerName(cl.Name); + var serverBase = cl.ServerBase ?? ServerName(cl.Inherits); + if (list != null) + serverBase = "ServerList<" + ServerName(list.ItemType) + ">"; + + var server = ClassDeclaration(serverName) + .AddModifiers(abstractModifier) + .AddModifiers(SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(serverBase); + + string changesName = ChangesName(cl.Name); + var changesBase = ChangesName(cl.ChangesBase ?? cl.Inherits); + + if (list != null) + changesBase = "ListChangeSet<" + ServerName(list.ItemType) + ">"; + + var changeSetPoolType = "ChangeSetPool<" + changesName + ">"; + var transport = ClassDeclaration(changesName) + .AddModifiers(SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(changesBase) + .AddMembers(DeclareField(changeSetPoolType, "Pool", + EqualsValueClause( + ParseExpression($"new {changeSetPoolType}(pool => new {changesName}(pool))") + ), + SyntaxKind.PublicKeyword, + SyntaxKind.StaticKeyword, SyntaxKind.ReadOnlyKeyword)) + .AddMembers(ParseMemberDeclaration($"public {changesName}(IChangeSetPool pool) : base(pool){{}}")); + + client = client + .AddMembers( + PropertyDeclaration(ParseTypeName("IChangeSetPool"), "ChangeSetPool") + .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.ProtectedKeyword, + SyntaxKind.OverrideKeyword) + .WithExpressionBody( + ArrowExpressionClause(MemberAccess(changesName, "Pool"))) + .WithSemicolonToken(Semicolon())) + .AddMembers(PropertyDeclaration(ParseTypeName(changesName), "Changes") + .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.NewKeyword) + .WithExpressionBody(ArrowExpressionClause(CastExpression(ParseTypeName(changesName), + MemberAccess(BaseExpression(), "Changes")))) + .WithSemicolonToken(Semicolon())); + + if (!cl.CustomCtor) + { + client = client.AddMembers(PropertyDeclaration(ParseTypeName(serverName), "Server") + .AddModifiers(SyntaxKind.InternalKeyword, SyntaxKind.NewKeyword) + .AddAccessorListAccessors(AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Semicolon()))); + client = client.AddMembers( + ConstructorDeclaration(cl.Name) + .AddModifiers(SyntaxKind.InternalKeyword) + .WithParameterList(ParameterList(SeparatedList(new[] + { + Parameter(Identifier("compositor")).WithType(ParseTypeName("Compositor")), + Parameter(Identifier("server")).WithType(ParseTypeName(serverName)), + }))) + .WithInitializer(ConstructorInitializer(SyntaxKind.BaseConstructorInitializer, + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("compositor")), + Argument(IdentifierName("server")), + })))).WithBody(Block( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("Server"), + CastExpression(ParseTypeName(serverName), IdentifierName("server")))), + ExpressionStatement(InvocationExpression(IdentifierName("InitializeDefaults"))) + ))); + } + + if (!cl.CustomServerCtor) + { + server = server.AddMembers( + ConstructorDeclaration(serverName) + .AddModifiers(SyntaxKind.InternalKeyword) + .WithParameterList(ParameterList(SeparatedList(new[] + { + Parameter(Identifier("compositor")).WithType(ParseTypeName("ServerCompositor")), + }))) + .WithInitializer(ConstructorInitializer(SyntaxKind.BaseConstructorInitializer, + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("compositor")), + })))).WithBody(Block())); + } + + + var changesVarName = "c"; + var changesVar = IdentifierName(changesVarName); + + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "ApplyChangesExtra") + .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName(changesName))) + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + transport = transport.AddMembers( + MethodDeclaration(ParseTypeName("void"), "ResetExtra") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + var applyMethodBody = Block( + ExpressionStatement(InvocationExpression(MemberAccess(IdentifierName("base"), "ApplyCore"), + ArgumentList(SeparatedList(new[] {Argument(IdentifierName("changes"))})))), + LocalDeclarationStatement(VariableDeclaration(ParseTypeName("var")) + .WithVariables(SingletonSeparatedList( + VariableDeclarator(changesVarName) + .WithInitializer(EqualsValueClause(CastExpression(ParseTypeName(changesName), + IdentifierName("changes"))))))), + ExpressionStatement(InvocationExpression(IdentifierName("ApplyChangesExtra")) + .AddArgumentListArguments(Argument(IdentifierName("c")))) + ); + + var resetBody = Block(); + var startAnimationBody = Block(); + var getPropertyBody = Block(); + var serverGetPropertyBody = Block(); + + var defaultsMethodBody = Block(); + + foreach (var prop in cl.Properties) + { + var fieldName = "_" + prop.Name.WithLowerFirst(); + var propType = ParseTypeName(prop.Type); + var filteredPropertyType = prop.Type.TrimEnd('?'); + var isObject = _objects.Contains(filteredPropertyType); + var isNullable = prop.Type.EndsWith("?"); + + + + + client = client + .AddMembers(DeclareField(prop.Type, fieldName)) + .AddMembers(PropertyDeclaration(propType, prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, + Block(ReturnStatement(IdentifierName(fieldName)))), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, + Block( + ParseStatement("var changed = false;"), + IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, + IdentifierName(fieldName), + IdentifierName("value")), + Block( + ParseStatement("On" + prop.Name + "Changing();"), + ParseStatement("changed = true;"), + GeneratePropertySetterAssignment(prop, fieldName, isObject, isNullable)) + ), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), IdentifierName("value"))), + ParseStatement($"if(changed) On" + prop.Name + "Changed();") + )) + )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + + var animatedServer = prop.Animated; + + var serverPropertyType = ((isObject ? "Server" : "") + prop.Type); + if (_manuals.TryGetValue(filteredPropertyType, out var manual) && manual.ServerName != null) + serverPropertyType = manual.ServerName + (isNullable ? "?" : ""); + + + transport = transport + .AddMembers(DeclareField((animatedServer ? "Animated" : "") + "Change<" + serverPropertyType + ">", + prop.Name, SyntaxKind.PublicKeyword)); + + if (animatedServer) + server = server.AddMembers( + DeclareField("AnimatedValueStore<" + serverPropertyType + ">", fieldName), + PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .WithExpressionBody(ArrowExpressionClause( + InvocationExpression(MemberAccess(fieldName, "GetAnimated"), + ArgumentList(SingletonSeparatedList(Argument(IdentifierName("Compositor"))))))) + .WithSemicolonToken(Semicolon()) + ); + else + { + server = server + .AddMembers(DeclareField(serverPropertyType, fieldName)) + .AddMembers(PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, + Block(ReturnStatement(IdentifierName(fieldName)))), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, + Block( + ParseStatement("var changed = false;"), + IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, + IdentifierName(fieldName), + IdentifierName("value")), + Block( + ParseStatement("On" + prop.Name + "Changing();"), + ParseStatement($"changed = true;")) + ), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), IdentifierName("value"))), + ParseStatement($"if(changed) On" + prop.Name + "Changed();") + )) + )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + } + + if (animatedServer) + applyMethodBody = applyMethodBody.AddStatements( + IfStatement(MemberAccess(changesVar, prop.Name, "IsValue"), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), MemberAccess(changesVar, prop.Name, "Value")))), + IfStatement(MemberAccess(changesVar, prop.Name, "IsAnimation"), + ExpressionStatement( + InvocationExpression(MemberAccess(fieldName, "SetAnimation"), + ArgumentList(SeparatedList(new[] + { + Argument(changesVar), + Argument(MemberAccess(changesVar, prop.Name, "Animation")) + }))))) + ); + else + applyMethodBody = applyMethodBody.AddStatements( + IfStatement(MemberAccess(changesVar, prop.Name, "IsSet"), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(prop.Name), MemberAccess(changesVar, prop.Name, "Value")))) + + ); + + + resetBody = resetBody.AddStatements( + ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); + + if (animatedServer) + startAnimationBody = ApplyStartAnimation(startAnimationBody, prop, fieldName); + + getPropertyBody = ApplyGetProperty(getPropertyBody, prop); + serverGetPropertyBody = ApplyGetProperty(getPropertyBody, prop); + + if (prop.DefaultValue != null) + { + defaultsMethodBody = defaultsMethodBody.AddStatements( + ExpressionStatement( + AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(prop.Name), ParseExpression(prop.DefaultValue)))); + } + } + + if (cl is GBrush brush && !cl.Abstract) + { + var brushName = brush.Name.StripPrefix("Composition"); + /* + server = server.AddMembers( + MethodDeclaration(ParseTypeName("ICbBrush"), "CreateBackendBrush") + .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) + .WithExpressionBody(ArrowExpressionClause( + InvocationExpression(MemberAccess("Compositor", "Backend", "Create" + brushName)) + )).WithSemicolonToken(Semicolon()) + ); + if (!brush.CustomUpdate) + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "UpdateBackendBrush") + .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) + .AddParameterListParameters(Parameter(Identifier("brush")) + .WithType(ParseTypeName("ICbBrush"))) + .AddBodyStatements( + ExpressionStatement( + InvocationExpression( + MemberAccess( + ParenthesizedExpression( + CastExpression(ParseTypeName("ICb" + brushName), IdentifierName("brush"))), "Update"), + ArgumentList(SeparatedList(cl.Properties.Select(x => + { + if(x.Type.TrimEnd('?') == "ICompositionSurface") + return Argument( + ConditionalAccessExpression(IdentifierName(x.Name), + MemberBindingExpression(IdentifierName("BackendSurface"))) + ); + if (_brushes.Contains(x.Type)) + return Argument( + ConditionalAccessExpression(IdentifierName(x.Name), + MemberBindingExpression(IdentifierName("Brush"))) + ); + return Argument(IdentifierName(x.Name)); + })))) + ))); + +*/ + } + + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "ApplyCore") + .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) + .AddParameterListParameters( + Parameter(Identifier("changes")).WithType(ParseTypeName("ChangeSet"))) + .WithBody(applyMethodBody)); + + client = client.AddMembers( + MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)); + + transport = transport.AddMembers(MethodDeclaration(ParseTypeName("void"), "Reset") + .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword) + .WithBody(resetBody.AddStatements( + ExpressionStatement(InvocationExpression(IdentifierName("ResetExtra"))), + ExpressionStatement(InvocationExpression(MemberAccess("base", "Reset")))))); + + if (list != null) + client = AppendListProxy(list, client); + + if (startAnimationBody.Statements.Count != 0) + client = WithStartAnimation(client, startAnimationBody); + + client = WithGetProperty(client, getPropertyBody, false); + server = WithGetProperty(server, serverGetPropertyBody, true); + + if(cl.Implements.Count > 0) + foreach (var impl in cl.Implements) + { + client = client.WithBaseList(client.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.Name)))); + if (impl.ServerName != null) + server = server.WithBaseList( + server.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName)))); + + client = client.AddMembers( + ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;")); + } + + + SaveTo(unit.AddMembers(clientNs.AddMembers(client)), + cl.Name + ".generated.cs"); + SaveTo(unit.AddMembers(serverNs.AddMembers(server)), + "Server", "Server" + cl.Name + ".generated.cs"); + SaveTo(unit.AddMembers(transportNs.AddMembers(transport)), + "Transport", cl.Name + "Changes.generated.cs"); + } + + StatementSyntax GeneratePropertySetterAssignment(GProperty prop, string fieldName, bool isObject, bool isNullable) + { + var normalChangesAssignment = (StatementSyntax)ExpressionStatement(AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + MemberAccess((ExpressionSyntax) IdentifierName("Changes"), prop.Name, + "Value"), + isObject + ? + ConditionalMemberAccess(IdentifierName("value"), "Server", isNullable) + : IdentifierName("value"))); + if (!prop.Animated) + return normalChangesAssignment; + + var code = $@" +{{ + if(animation is CompositionAnimation a) + Changes.{prop.Name}.Animation = a.CreateInstance(this.Server, value); + else + {{ + var saved = Changes.{prop.Name}; + if(!StartAnimationGroup(animation, ""{prop.Name}"", value)) + Changes.{prop.Name}.Value = value; + }} +}} + +"; + + return IfStatement( + ParseExpression( + $"ImplicitAnimations != null && ImplicitAnimations.TryGetValue(\"{prop.Name}\", out var animation) == true"), + ParseStatement(code), + ElseClause(normalChangesAssignment) + ); + } + + BlockSyntax ApplyStartAnimation(BlockSyntax body, GProperty prop, string fieldName) + { + var code = $@" +if (propertyName == ""{prop.Name}"") +{{ +var current = {fieldName}; +var server = animation.CreateInstance(this.Server, finalValue); +Changes.{prop.Name}.Animation = server; +return; +}} +"; + return body.AddStatements(ParseStatement(code)); + } + + private static HashSet VariantPropertyTypes = new HashSet + { + "bool", + "float", + "Vector2", + "Vector3", + "Vector4", + "Matrix3x2", + "Matrix4x4", + "Quaternion", + "CompositionColor" + }; + + BlockSyntax ApplyGetProperty(BlockSyntax body, GProperty prop) + { + if (VariantPropertyTypes.Contains(prop.Type)) + return body.AddStatements( + ParseStatement($"if(name == \"{prop.Name}\")\n return {prop.Name};\n") + ); + + return body; + } + + ClassDeclarationSyntax WithGetProperty(ClassDeclarationSyntax cl, BlockSyntax body, bool server) + { + if (body.Statements.Count == 0) + return cl; + body = body.AddStatements( + ParseStatement("return base.GetPropertyForAnimation(name);")); + var method = ((MethodDeclarationSyntax) ParseMemberDeclaration( + $"{(server ? "public" : "internal")} override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")) + .WithBody(body); + + return cl.AddMembers(method); + } + + ClassDeclarationSyntax WithStartAnimation(ClassDeclarationSyntax cl, BlockSyntax body) + { + body = body.AddStatements( + ExpressionStatement(InvocationExpression(MemberAccess("base", "StartAnimation"), + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("propertyName")), + Argument(IdentifierName("animation")), + Argument(IdentifierName("finalValue")), + })))) + ); + return cl.AddMembers( + ((MethodDeclarationSyntax) ParseMemberDeclaration( + "internal override void StartAnimation(string propertyName, CompositionAnimation animation, Avalonia.Rendering.Composition.Expressions.ExpressionVariant? finalValue){}")) + .WithBody(body)); + + + } + + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index ec3f29c806..7a64b39575 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -13,6 +13,7 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.X11; using Avalonia.X11.Glx; using Avalonia.X11.NativeDialogs; @@ -29,6 +30,7 @@ namespace Avalonia.X11 public XI2Manager XI2; public X11Info Info { get; private set; } public IX11Screens X11Screens { get; private set; } + public Compositor Compositor { get; private set; } public IScreenImpl Screens { get; private set; } public X11PlatformOptions Options { get; private set; } public IntPtr OrphanedWindow { get; private set; } @@ -101,7 +103,9 @@ namespace Avalonia.X11 GlxPlatformOpenGlInterface.TryInitialize(Info, Options.GlProfiles); } - + if (options.UseCompositor) + Compositor = Compositor.Create(AvaloniaLocator.Current.GetService()!); + } public IntPtr DeferredDisplay { get; set; } @@ -222,6 +226,8 @@ namespace Avalonia /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; + + public bool UseCompositor { get; set; } /// /// Determines whether to use IME. diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 066156a652..37cfeb0624 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -18,6 +18,7 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.X11.Glx; using static Avalonia.X11.XLib; @@ -360,13 +361,15 @@ namespace Avalonia.X11 if (customRendererFactory != null) return customRendererFactory.Create(root, loop); - - return _platform.Options.UseDeferredRendering ? - new DeferredRenderer(root, loop) - { - RenderOnlyOnRenderThread = true - } : - (IRenderer)new X11ImmediateRendererProxy(root, loop); + + return _platform.Options.UseDeferredRendering + ? _platform.Options.UseCompositor + ? new CompositingRenderer(root, this._platform.Compositor) + : new DeferredRenderer(root, loop) + { + RenderOnlyOnRenderThread = true + } + : (IRenderer)new X11ImmediateRendererProxy(root, loop); } void OnEvent(ref XEvent ev) diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index a259d8fab9..1243c90214 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; @@ -73,6 +74,12 @@ namespace Avalonia.Direct2D1.Media set { _deviceContext.Transform = value.ToDirect2D(); } } + public Matrix4x4 Transform4x4 + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + /// public void Clear(Color color) { From 4274e78a321b37412c77915638535e1d12304c6e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 21:43:22 +0300 Subject: [PATCH 02/61] Win32 --- samples/ControlCatalog.NetCore/Program.cs | 3 ++- .../Rendering/Composition/CompositingRenderer.cs | 4 ++-- src/Windows/Avalonia.Win32/Win32Platform.cs | 8 ++++++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 4 ++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 2a0755b900..6196aac153 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -115,7 +115,8 @@ namespace ControlCatalog.NetCore }) .With(new Win32PlatformOptions { - EnableMultitouch = true + EnableMultitouch = true, + UseCompositor = true }) .UseSkia() .AfterSetup(builder => diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 07ac54b634..65ed0d17ad 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -177,7 +177,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public void Stop() { - _target.IsEnabled = true; + _target.IsEnabled = false; } public void Dispose() @@ -191,4 +191,4 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public Compositor Compositor => _compositor; -} \ No newline at end of file +} diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index dc5e5324c4..32705a2cc6 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -14,6 +14,7 @@ using Avalonia.Input.Platform; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Win32.Input; @@ -48,6 +49,8 @@ namespace Avalonia /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; + + public bool UseCompositor { get; set; } /// /// Enables ANGLE for Windows. For every Windows version that is above Windows 7, the default is true otherwise it's false. @@ -132,6 +135,8 @@ namespace Avalonia.Win32 public static bool UseDeferredRendering => Options.UseDeferredRendering; internal static bool UseOverlayPopups => Options.OverlayPopups; public static Win32PlatformOptions Options { get; private set; } + + internal static Compositor Compositor { get; private set; } public Size DoubleClickSize => new Size( UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXDOUBLECLK), @@ -181,6 +186,9 @@ namespace Avalonia.Win32 if (OleContext.Current != null) AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); + + if (Options.UseCompositor) + Compositor = Compositor.Create(AvaloniaLocator.Current.GetRequiredService()); } public bool HasMessages() diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index f0036236ec..e61121f23e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -14,6 +14,7 @@ using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Win32.Automation; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; @@ -500,6 +501,9 @@ namespace Avalonia.Win32 if (customRendererFactory != null) return customRendererFactory.Create(root, loop); + if (Win32Platform.Compositor != null) + return new CompositingRenderer(root, Win32Platform.Compositor); + return Win32Platform.UseDeferredRendering ? _isUsingComposition ? new DeferredRenderer(root, loop) From 7a3b5f051f02e2e0689df3232ffac5b76f6d986b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 23:20:39 +0300 Subject: [PATCH 03/61] Make ThreadSafeObjectPool actually thread safe --- src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs index c6845485dc..05995f2069 100644 --- a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs +++ b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs @@ -5,7 +5,6 @@ namespace Avalonia.Threading public class ThreadSafeObjectPool where T : class, new() { private Stack _stack = new Stack(); - private object _lock = new object(); public static ThreadSafeObjectPool Default { get; } = new ThreadSafeObjectPool(); public T Get() @@ -20,7 +19,7 @@ namespace Avalonia.Threading public void Return(T obj) { - lock (_stack) + lock (_lock) { _stack.Push(obj); } From 6d46006d075e05a4de377e8f85b1b93199e348f3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 23:30:02 +0300 Subject: [PATCH 04/61] Removed some matrix operations --- .../Composition/Expressions/ExpressionVariant.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs index 8c6af5cb0c..086c8ce276 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs @@ -290,9 +290,6 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.Matrix3x2) return left.Matrix3x2 + right.Matrix3x2; - if (left.Type == VariantType.AvaloniaMatrix) - return left.AvaloniaMatrix + right.AvaloniaMatrix; - if (left.Type == VariantType.Matrix4x4) return left.Matrix4x4 + right.Matrix4x4; @@ -325,9 +322,6 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.Matrix3x2) return left.Matrix3x2 - right.Matrix3x2; - if (left.Type == VariantType.AvaloniaMatrix) - return left.AvaloniaMatrix - right.AvaloniaMatrix; - if (left.Type == VariantType.Matrix4x4) return left.Matrix4x4 - right.Matrix4x4; @@ -407,12 +401,6 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.AvaloniaMatrix) return left.AvaloniaMatrix * right.AvaloniaMatrix; - - if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Scalar) - return left.AvaloniaMatrix * (double)right.Scalar; - - if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Double) - return left.AvaloniaMatrix * right.Double; if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Matrix4x4) return left.Matrix4x4 * right.Matrix4x4; From 16dde385d0d8c6b9f7711a801b67a8f4440b3a57 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 11 May 2022 00:59:00 +0300 Subject: [PATCH 05/61] WIP: Matrix3x3, some invalidation support, dirty rects drawing --- samples/RenderDemo/App.xaml.cs | 4 + .../Composition/CompositingRenderer.cs | 26 ++++-- .../Composition/CompositionDrawListVisual.cs | 3 +- .../Composition/CompositionTarget.cs | 24 +++--- .../Rendering/Composition/Compositor.cs | 1 + .../Rendering/Composition/MatrixUtils.cs | 20 +++++ .../Composition/Server/DrawingContextProxy.cs | 4 - .../Composition/Server/FpsCounter.cs | 73 ++++++++++++++++ .../Server/ServerCompositionDrawListVisual.cs | 20 +++++ .../Server/ServerCompositionTarget.cs | 83 ++++++++++++++++++- .../Server/ServerContainerVisual.cs | 7 ++ .../Server/ServerSolidColorVisual.cs | 2 +- .../Composition/Server/ServerVisual.cs | 52 ++++++++---- .../Transport/CompositionTargetChanges.cs | 11 +++ .../Rendering/Composition/Visual.cs | 2 +- .../Rendering/Composition/VisualCollection.cs | 5 ++ .../Threading/ThreadSafeObjectPool.cs | 4 +- src/Avalonia.Base/composition-schema.xml | 4 + 18 files changed, 297 insertions(+), 48 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs diff --git a/samples/RenderDemo/App.xaml.cs b/samples/RenderDemo/App.xaml.cs index 8054b06964..8f4e02df01 100644 --- a/samples/RenderDemo/App.xaml.cs +++ b/samples/RenderDemo/App.xaml.cs @@ -29,6 +29,10 @@ namespace RenderDemo .With(new Win32PlatformOptions { OverlayPopups = true, + }) + .With(new X11PlatformOptions + { + UseCompositor = true }) .UsePlatformDetect() .LogToTrace(); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 07ac54b634..22fc6a77d4 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -37,9 +37,19 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor _target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor); _update = Update; } - - public bool DrawFps { get; set; } - public bool DrawDirtyRects { get; set; } + + public bool DrawFps + { + get => _target.DrawFps; + set => _target.DrawFps = value; + } + + public bool DrawDirtyRects + { + get => _target.DrawDirtyRects; + set => _target.DrawDirtyRects = value; + } + public event EventHandler? SceneInvalidated; void QueueUpdate() @@ -57,7 +67,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public IEnumerable HitTest(Point p, IVisual root, Func filter) { - var res = _target.TryHitTest(new Vector2((float)p.X, (float)p.Y)); + var res = _target.TryHitTest(p); if(res == null) yield break; for (var index = res.Count - 1; index >= 0; index--) @@ -146,7 +156,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor renderTransform *= mirrorMatrix; } - comp.TransformMatrix = renderTransform; + comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform); _recorder.BeginUpdate(comp.DrawList ?? new CompositionDrawList()); visual.Render(_recordingContext); @@ -159,6 +169,8 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor SyncChildren(v); _dirty.Clear(); _recalculateChildren.Clear(); + _target.Size = _root.ClientSize; + _target.Scaling = _root.RenderScaling; } public void Resized(Size size) @@ -169,7 +181,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor { // We render only on the render thread for now Update(); - + _target.RequestRedraw(); Compositor.RequestCommitAsync().Wait(); } @@ -177,7 +189,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public void Stop() { - _target.IsEnabled = true; + _target.IsEnabled = false; } public void Dispose() diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index 9f02055412..b19c311663 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -29,11 +29,10 @@ internal class CompositionDrawListVisual : CompositionContainerVisual Visual = visual; } - internal override bool HitTest(Vector2 point) + internal override bool HitTest(Point pt) { if (DrawList == null) return false; - var pt = new Point(point.X, point.Y); if (Visual is ICustomHitTest custom) return custom.HitTest(pt); foreach (var op in DrawList) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index a8835ca668..8d052389c2 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -18,7 +18,7 @@ namespace Avalonia.Rendering.Composition Root.Root = null; } - public PooledList? TryHitTest(Vector2 point) + public PooledList? TryHitTest(Point point) { Server.Readback.NextRead(); if (Root == null) @@ -28,12 +28,12 @@ namespace Avalonia.Rendering.Composition return res; } - public Vector2? TryTransformToVisual(CompositionVisual visual, Vector2 point) + public Point? TryTransformToVisual(CompositionVisual visual, Point point) { if (visual.Root != this) return null; var v = visual; - var m = Matrix3x2.Identity; + var m = Matrix.Identity; while (v != null) { if (!TryGetInvertedTransform(v, out var cm)) @@ -42,10 +42,10 @@ namespace Avalonia.Rendering.Composition v = v.Parent; } - return Vector2.Transform(point, m); + return point * m; } - bool TryGetInvertedTransform(CompositionVisual visual, out Matrix3x2 matrix) + bool TryGetInvertedTransform(CompositionVisual visual, out Matrix matrix) { var m = visual.TryGetServerTransform(); if (m == null) @@ -54,24 +54,22 @@ namespace Avalonia.Rendering.Composition return false; } - // TODO: Use Matrix3x3 - var m32 = new Matrix3x2(m.Value.M11, m.Value.M12, m.Value.M21, m.Value.M22, m.Value.M41, m.Value.M42); - - return Matrix3x2.Invert(m32, out matrix); + var m33 = MatrixUtils.ToMatrix(m.Value); + return m33.TryInvert(out matrix); } - bool TryTransformTo(CompositionVisual visual, ref Vector2 v) + bool TryTransformTo(CompositionVisual visual, ref Point v) { if (TryGetInvertedTransform(visual, out var m)) { - v = Vector2.Transform(v, m); + v = v * m; return true; } return false; } - bool HitTestCore(CompositionVisual visual, Vector2 point, PooledList result) + bool HitTestCore(CompositionVisual visual, Point point, PooledList result) { //TODO: Check readback too if (visual.Visible == false) @@ -103,5 +101,7 @@ namespace Avalonia.Rendering.Composition return false; } + + public void RequestRedraw() => Changes.RedrawRequested.Value = true; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 217d8dd803..14d779dbc4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Numerics; using System.Threading.Tasks; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; diff --git a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs index 5e91bcb3d4..2cb500cae4 100644 --- a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs +++ b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs @@ -42,5 +42,25 @@ namespace Avalonia.Rendering.Composition return mat; } + + public static Matrix4x4 ToMatrix4x4(Matrix matrix) => + new Matrix4x4( + (float)matrix.M11, (float)matrix.M12, 0, (float)matrix.M13, + (float)matrix.M21, (float)matrix.M22, 0, (float)matrix.M23, + 0, 0, 1, 0, + (float)matrix.M31, (float)matrix.M32, 0, (float)matrix.M33 + ); + + public static Matrix ToMatrix(Matrix4x4 matrix44) => + new Matrix( + matrix44.M11, + matrix44.M12, + matrix44.M14, + matrix44.M21, + matrix44.M22, + matrix44.M24, + matrix44.M41, + matrix44.M42, + matrix44.M44); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index f5dfa92897..7d061e86a9 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -135,8 +135,4 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl { _impl.Custom(custom); } - - public Matrix CutTransform(Matrix4x4 transform) => new Matrix(transform.M11, transform.M12, transform.M21, - transform.M22, transform.M41, - transform.M42); } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs new file mode 100644 index 0000000000..8c2e6e774a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -0,0 +1,73 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class FpsCounter +{ + private readonly GlyphTypeface _typeface; + private readonly bool _useManualFpsCounting; + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private int _framesThisSecond; + private int _fps; + private TimeSpan _lastFpsUpdate; + private GlyphRun[] _runs = new GlyphRun[10]; + + public FpsCounter(GlyphTypeface typeface, bool useManualFpsCounting = false) + { + for (var c = 0; c <= 9; c++) + { + var s = c.ToString(); + var glyph = typeface.GetGlyph((uint)(s[0])); + _runs[c] = new GlyphRun(typeface, 18, new ReadOnlySlice(s.AsMemory()), new ushort[] { glyph }); + } + _typeface = typeface; + _useManualFpsCounting = useManualFpsCounting; + } + + public void FpsTick() => _framesThisSecond++; + + public void RenderFps(IDrawingContextImpl context) + { + var now = _stopwatch.Elapsed; + var elapsed = now - _lastFpsUpdate; + + if (!_useManualFpsCounting) + ++_framesThisSecond; + + if (elapsed.TotalSeconds > 1) + { + _fps = (int)(_framesThisSecond / elapsed.TotalSeconds); + _framesThisSecond = 0; + _lastFpsUpdate = now; + } + + var fpsLine = _fps.ToString("000"); + double width = 0; + double height = 0; + foreach (var ch in fpsLine) + { + var run = _runs[ch - '0']; + width += run.Size.Width; + height = Math.Max(height, run.Size.Height); + } + + var rect = new Rect(0, 0, width + 3, height + 3); + + context.DrawRectangle(Brushes.Black, null, rect); + + double offset = 0; + foreach (var ch in fpsLine) + { + var run = _runs[ch - '0']; + context.Transform = Matrix.CreateTranslation(offset, 0); + context.DrawGlyphRun(Brushes.White, run); + offset += run.Size.Width; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index 397c968d04..f0384c36fc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -16,6 +16,25 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { } + Rect? _contentBounds; + + public override Rect ContentBounds + { + get + { + if (_contentBounds == null) + { + var rect = Rect.Empty; + if(_renderCommands!=null) + foreach (var cmd in _renderCommands) + rect = rect.Union(cmd.Item.Bounds); + _contentBounds = rect; + } + + return _contentBounds.Value; + } + } + protected override void ApplyCore(ChangeSet changes) { var ch = (DrawListVisualChanges)changes; @@ -23,6 +42,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { _renderCommands?.Dispose(); _renderCommands = ch.AcquireDrawCommands(); + _contentBounds = null; } base.ApplyCore(changes); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 493529e111..04ec711455 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -2,7 +2,10 @@ using System; using System.Numerics; using System.Threading; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server { @@ -14,6 +17,13 @@ namespace Avalonia.Rendering.Composition.Server public long Id { get; } private ulong _frame = 1; private IRenderTarget? _renderTarget; + private FpsCounter _fpsCounter = new FpsCounter(Typeface.Default.GlyphTypeface); + private Rect _dirtyRect; + private Random _random = new(); + private Size _layerSize; + private IDrawingContextLayerImpl? _layer; + private bool _redrawRequested; + public ReadbackIndices Readback { get; } = new(); @@ -33,6 +43,11 @@ namespace Avalonia.Rendering.Composition.Server _compositor.RemoveCompositionTarget(this); } + partial void ApplyChangesExtra(CompositionTargetChanges c) + { + _redrawRequested = true; + } + public void Render() { if (Root == null) @@ -40,14 +55,76 @@ namespace Avalonia.Rendering.Composition.Server _renderTarget ??= _renderTargetFactory(); Compositor.UpdateServerTime(); - using (var context = _renderTarget.CreateDrawingContext(null)) + + Root.Update(this, Matrix4x4.Identity); + + if(_dirtyRect.IsEmpty && !_redrawRequested) + return; + _redrawRequested = false; + using (var targetContext = _renderTarget.CreateDrawingContext(null)) { - context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + var layerSize = Size * Scaling; + if (layerSize != _layerSize || _layer == null) + { + _layer?.Dispose(); + _layer = null; + _layer = targetContext.CreateLayer(layerSize); + _layerSize = layerSize; + } + + if (!_dirtyRect.IsEmpty) + { + using (var context = _layer.CreateDrawingContext(null)) + { + context.PushClip(_dirtyRect); + context.Clear(Colors.Transparent); + Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + context.PopClip(); + } + } + + targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, new Rect(_layerSize), + new Rect(_layerSize)); + + + if (DrawDirtyRects) + { + targetContext.DrawRectangle(new ImmutableSolidColorBrush( + new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), + (byte)_random.Next(255))) + , null, _dirtyRect); + } + + if(DrawFps) + _fpsCounter.RenderFps(targetContext); + _dirtyRect = Rect.Empty; + } Readback.NextWrite(_frame); _frame++; } + + private static Rect SnapToDevicePixels(Rect rect, double scale) + { + return new Rect( + new Point( + Math.Floor(rect.X * scale) / scale, + Math.Floor(rect.Y * scale) / scale), + new Point( + Math.Ceiling(rect.Right * scale) / scale, + Math.Ceiling(rect.Bottom * scale) / scale)); + } + + public void AddDirtyRect(Rect rect) + { + var snapped = SnapToDevicePixels(rect, Scaling); + _dirtyRect = _dirtyRect.Union(snapped); + } + + public void Invalidate() + { + _redrawRequested = true; + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs index ac112b846f..3f0995b257 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -21,6 +21,13 @@ namespace Avalonia.Rendering.Composition.Server } } + public override void Update(ServerCompositionTarget root, Matrix4x4 transform) + { + base.Update(root, transform); + foreach (var child in Children) + child.Update(root, GlobalTransformMatrix); + } + public ServerCompositionContainerVisual(ServerCompositor compositor) : base(compositor) { Children = new ServerCompositionVisualCollection(compositor); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs index 60569867de..786779bb2d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.Composition.Server { protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { - canvas.Transform = canvas.CutTransform(transform); + canvas.Transform = MatrixUtils.ToMatrix(transform); canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new RoundedRect(new Rect(new Size(Size)))); base.RenderCore(canvas, transform); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 37e188fb47..90b580bfa7 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -6,6 +6,7 @@ namespace Avalonia.Rendering.Composition.Server { unsafe partial class ServerCompositionVisual : ServerObject { + private bool _isDirty; protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -17,7 +18,7 @@ namespace Avalonia.Rendering.Composition.Server return; if(Opacity == 0) return; - canvas.PreTransform = canvas.CutTransform(transform); + canvas.PreTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; if (Opacity != 1) canvas.PushOpacity(Opacity); @@ -25,7 +26,8 @@ namespace Avalonia.Rendering.Composition.Server canvas.PushClip(new Rect(new Size(Size.X, Size.Y))); if (Clip != null) canvas.PushGeometryClip(Clip); - + + //TODO: Check clip RenderCore(canvas, transform); if (Clip != null) @@ -48,22 +50,31 @@ namespace Avalonia.Rendering.Composition.Server return ref _readback2; } - public Matrix4x4 CombinedTransformMatrix + public Matrix4x4 CombinedTransformMatrix { get; private set; } + public Matrix4x4 GlobalTransformMatrix { get; private set; } + + public virtual void Update(ServerCompositionTarget root, Matrix4x4 transform) { - get + var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, + Scale, RotationAngle, Orientation, Offset); + var i = Root!.Readback; + ref var readback = ref GetReadback(i.WriteIndex); + readback.Revision = i.WriteRevision; + readback.Matrix = res; + readback.TargetId = Root.Id; + //TODO: check effective opacity too + IsVisibleInFrame = Visible && Opacity > 0; + CombinedTransformMatrix = res; + GlobalTransformMatrix = res * transform; + //TODO: Cache + TransformedBounds = ContentBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); + + if (!IsVisibleInFrame) + _isDirty = false; + else if (_isDirty) { - if (Root == null) - return default; - - var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, - Scale, RotationAngle, Orientation, Offset); - var i = Root.Readback; - ref var readback = ref GetReadback(i.WriteIndex); - readback.Revision = i.WriteRevision; - readback.Matrix = res; - readback.TargetId = Root.Id; - - return res; + Root.AddDirtyRect(TransformedBounds); + _isDirty = false; } } @@ -81,11 +92,20 @@ namespace Avalonia.Rendering.Composition.Server Parent = c.Parent.Value; if (c.Root.IsSet) Root = c.Root.Value; + _isDirty = true; + + if (IsVisibleInFrame) + Root?.AddDirtyRect(TransformedBounds); + else + Root?.Invalidate(); } public ServerCompositionTarget? Root { get; private set; } public ServerCompositionVisual? Parent { get; private set; } + public bool IsVisibleInFrame { get; set; } + public Rect TransformedBounds { get; set; } + public virtual Rect ContentBounds => new Rect(0, 0, Size.X, Size.Y); } diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs new file mode 100644 index 0000000000..014adc7bbe --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs @@ -0,0 +1,11 @@ +namespace Avalonia.Rendering.Composition.Transport; + +partial class CompositionTargetChanges +{ + public Change RedrawRequested; + + partial void ResetExtra() + { + RedrawRequested.Reset(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index 1e6d7f8abb..25fce01de3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -59,6 +59,6 @@ namespace Avalonia.Rendering.Composition internal object? Tag { get; set; } - internal virtual bool HitTest(Vector2 point) => true; + internal virtual bool HitTest(Point point) => true; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs index 35f33c3b38..fef4caf675 100644 --- a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -54,6 +54,11 @@ namespace Avalonia.Rendering.Composition partial void OnRemoved(CompositionVisual item) => item.Parent = null; + partial void OnBeforeClear() + { + foreach (var i in this) + i.Parent = null; + } partial void OnBeforeAdded(CompositionVisual item) { diff --git a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs index 05995f2069..827a02334a 100644 --- a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs +++ b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs @@ -9,7 +9,7 @@ namespace Avalonia.Threading public T Get() { - lock (_lock) + lock (_stack) { if(_stack.Count == 0) return new T(); @@ -19,7 +19,7 @@ namespace Avalonia.Threading public void Return(T obj) { - lock (_lock) + lock (_stack) { _stack.Push(obj); } diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index cf864dac2d..eb1ffe1922 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -25,6 +25,10 @@ + + + + From 0e8672017b4ba40c78413dc73a1e2ba0dcad971a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 11 May 2022 20:08:14 +0300 Subject: [PATCH 06/61] Workaround for #8118 --- src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 90b580bfa7..bf5b8bf292 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -30,6 +30,9 @@ namespace Avalonia.Rendering.Composition.Server //TODO: Check clip RenderCore(canvas, transform); + canvas.PreTransform = MatrixUtils.ToMatrix(transform); + canvas.Transform = Matrix.Identity; + if (Clip != null) canvas.PopGeometryClip(); if (ClipToBounds) From 9ac37065dca2a23006617dc649debd3392e23b38 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 24 May 2022 19:49:43 +0300 Subject: [PATCH 07/61] Animations now work... more or less? --- samples/ControlCatalog/MainView.xaml | 3 + .../Pages/CompositionPage.axaml | 45 +++++ .../Pages/CompositionPage.axaml.cs | 157 ++++++++++++++++++ .../Animations/AnimatedValueStore.cs | 88 +++++++++- .../Animations/AnimationInstanceBase.cs | 77 +++++++++ .../Animations/CompositionAnimation.cs | 2 +- .../Animations/ExpressionAnimationInstance.cs | 25 +-- .../Animations/IAnimationInstance.cs | 7 +- .../Animations/KeyFrameAnimationInstance.cs | 59 +++++-- .../Composition/Animations/KeyFrames.cs | 2 +- .../Composition/CompositionPropertySet.cs | 12 +- .../Composition/ElementCompositionPreview.cs | 6 + .../Composition/Expressions/Expression.cs | 45 ++++- .../ExpressionEvaluationContext.cs | 1 + .../Expressions/ExpressionTrackedValues.cs | 57 +++++++ .../Composition/Server/FpsCounter.cs | 27 ++- .../Server/ServerCompositionTarget.cs | 5 +- .../Composition/Server/ServerCompositor.cs | 20 +++ .../Composition/Server/ServerObject.cs | 82 ++++++++- .../Composition/Server/ServerVisual.cs | 22 ++- .../Rendering/Composition/Visual.cs | 1 - .../Utilities/RefTrackingDictionary.cs | 67 ++++++++ .../Avalonia.SourceGenerator.csproj | 1 + .../CompositionRoslynGenerator.cs | 26 +-- .../CompositionGenerator/Generator.cs | 102 ++++++++++-- .../ICompositionGeneratorSink.cs | 6 + .../RoslynCompositionGeneratorSink.cs | 15 ++ 27 files changed, 873 insertions(+), 87 deletions(-) create mode 100644 samples/ControlCatalog/Pages/CompositionPage.axaml create mode 100644 samples/ControlCatalog/Pages/CompositionPage.axaml.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs create mode 100644 src/Avalonia.Base/Utilities/RefTrackingDictionary.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 59d724db69..2ce5ab3934 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -13,6 +13,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml b/samples/ControlCatalog/Pages/CompositionPage.axaml new file mode 100644 index 0000000000..592290fde5 --- /dev/null +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml @@ -0,0 +1,45 @@ + + + Implicit animations + + + + + + + + + + + + + + + + + + + + + + + Resize me + + + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs new file mode 100644 index 0000000000..b37231243d --- /dev/null +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Media; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages; + +public partial class CompositionPage : UserControl +{ + private ImplicitAnimationCollection _implicitAnimations; + + public CompositionPage() + { + AvaloniaXamlLoader.Load(this); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + this.FindControl("Items").Items = CreateColorItems(); + } + + private List CreateColorItems() + { + var list = new List(); + + list.Add(new ColorItem(Color.FromArgb(255, 255, 185, 0))); + list.Add(new ColorItem(Color.FromArgb(255, 231, 72, 86))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 120, 215))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 153, 188))); + list.Add(new ColorItem(Color.FromArgb(255, 122, 117, 116))); + list.Add(new ColorItem(Color.FromArgb(255, 118, 118, 118))); + list.Add(new ColorItem(Color.FromArgb(255, 255, 141, 0))); + list.Add(new ColorItem(Color.FromArgb(255, 232, 17, 35))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 99, 177))); + list.Add(new ColorItem(Color.FromArgb(255, 45, 125, 154))); + list.Add(new ColorItem(Color.FromArgb(255, 93, 90, 88))); + list.Add(new ColorItem(Color.FromArgb(255, 76, 74, 72))); + list.Add(new ColorItem(Color.FromArgb(255, 247, 99, 12))); + list.Add(new ColorItem(Color.FromArgb(255, 234, 0, 94))); + list.Add(new ColorItem(Color.FromArgb(255, 142, 140, 216))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 183, 195))); + list.Add(new ColorItem(Color.FromArgb(255, 104, 118, 138))); + list.Add(new ColorItem(Color.FromArgb(255, 105, 121, 126))); + list.Add(new ColorItem(Color.FromArgb(255, 202, 80, 16))); + list.Add(new ColorItem(Color.FromArgb(255, 195, 0, 82))); + list.Add(new ColorItem(Color.FromArgb(255, 107, 105, 214))); + list.Add(new ColorItem(Color.FromArgb(255, 3, 131, 135))); + list.Add(new ColorItem(Color.FromArgb(255, 81, 92, 107))); + list.Add(new ColorItem(Color.FromArgb(255, 74, 84, 89))); + list.Add(new ColorItem(Color.FromArgb(255, 218, 59, 1))); + list.Add(new ColorItem(Color.FromArgb(255, 227, 0, 140))); + list.Add(new ColorItem(Color.FromArgb(255, 135, 100, 184))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 178, 148))); + list.Add(new ColorItem(Color.FromArgb(255, 86, 124, 115))); + list.Add(new ColorItem(Color.FromArgb(255, 100, 124, 100))); + list.Add(new ColorItem(Color.FromArgb(255, 239, 105, 80))); + list.Add(new ColorItem(Color.FromArgb(255, 191, 0, 119))); + list.Add(new ColorItem(Color.FromArgb(255, 116, 77, 169))); + list.Add(new ColorItem(Color.FromArgb(255, 1, 133, 116))); + list.Add(new ColorItem(Color.FromArgb(255, 72, 104, 96))); + list.Add(new ColorItem(Color.FromArgb(255, 82, 94, 84))); + list.Add(new ColorItem(Color.FromArgb(255, 209, 52, 56))); + list.Add(new ColorItem(Color.FromArgb(255, 194, 57, 179))); + list.Add(new ColorItem(Color.FromArgb(255, 177, 70, 194))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 204, 106))); + list.Add(new ColorItem(Color.FromArgb(255, 73, 130, 5))); + list.Add(new ColorItem(Color.FromArgb(255, 132, 117, 69))); + list.Add(new ColorItem(Color.FromArgb(255, 255, 67, 67))); + list.Add(new ColorItem(Color.FromArgb(255, 154, 0, 137))); + list.Add(new ColorItem(Color.FromArgb(255, 136, 23, 152))); + list.Add(new ColorItem(Color.FromArgb(255, 16, 137, 62))); + list.Add(new ColorItem(Color.FromArgb(255, 16, 124, 16))); + list.Add(new ColorItem(Color.FromArgb(255, 126, 115, 95))); + + return list; + } + + public class ColorItem + { + public Color Color { get; private set; } + + public SolidColorBrush ColorBrush + { + get { return new SolidColorBrush(Color); } + } + + public String ColorHexValue + { + get { return Color.ToString().Substring(3).ToUpperInvariant(); } + } + + public ColorItem(Color color) + { + Color = color; + } + } + + private void EnsureImplicitAnimations() + { + if (_implicitAnimations == null) + { + var compositor = ElementCompositionPreview.GetElementVisual(this)!.Compositor; + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(400); + + var rotationAnimation = compositor.CreateScalarKeyFrameAnimation(); + rotationAnimation.Target = "RotationAngle"; + rotationAnimation.InsertKeyFrame(.5f, 0.160f); + rotationAnimation.InsertKeyFrame(1f, 0f); + rotationAnimation.Duration = TimeSpan.FromMilliseconds(400); + + var animationGroup = compositor.CreateAnimationGroup(); + animationGroup.Add(offsetAnimation); + animationGroup.Add(rotationAnimation); + + _implicitAnimations = compositor.CreateImplicitAnimationCollection(); + _implicitAnimations["Offset"] = animationGroup; + } + } + + public static void SetEnableAnimations(Border border, bool value) + { + + var page = border.FindAncestorOfType(); + if (page == null) + { + border.AttachedToVisualTree += delegate { SetEnableAnimations(border, true); }; + return; + } + + if (ElementCompositionPreview.GetElementVisual(page) == null) + return; + + page.EnsureImplicitAnimations(); + ElementCompositionPreview.GetElementVisual((Visual)border.GetVisualParent()).ImplicitAnimations = + page._implicitAnimations; + } + + + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs index 180c45022f..e877b50b20 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs @@ -1,19 +1,62 @@ +using System.Runtime.InteropServices; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Animations { - internal struct AnimatedValueStore where T : struct + internal struct ServerObjectSubscriptionStore { + public bool IsValid; + public RefTrackingDictionary Subscribers; + + public void Invalidate() + { + if (IsValid) + return; + IsValid = false; + if (Subscribers != null) + foreach (var sub in Subscribers) + sub.Key.Invalidate(); + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct ServerValueStore + { + // HAS TO BE THE FIRST FIELD, accessed by field offset from ServerObject + private ServerObjectSubscriptionStore Subscriptions; + private T _value; + public T Value + { + set + { + _value = value; + Subscriptions.Invalidate(); + } + get + { + Subscriptions.IsValid = true; + return _value; + } + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct ServerAnimatedValueStore where T : struct + { + // HAS TO BE THE FIRST FIELD, accessed by field offset from ServerObject + private ServerObjectSubscriptionStore Subscriptions; + private IAnimationInstance? _animation; private T _direct; - private IAnimationInstance _animation; private T? _lastAnimated; public T Direct => _direct; public T GetAnimated(ServerCompositor compositor) { + Subscriptions.IsValid = true; if (_animation == null) return _direct; var v = _animation.Evaluate(compositor.ServerNow, ExpressionVariant.Create(_direct)) @@ -22,19 +65,50 @@ namespace Avalonia.Rendering.Composition.Animations return v; } + public void Activate(ServerObject parent) + { + if (_animation != null) + _animation.Activate(); + } + + public void Deactivate(ServerObject parent) + { + if (_animation != null) + _animation.Deactivate(); + } + private T LastAnimated => _animation != null ? _lastAnimated ?? _direct : _direct; public bool IsAnimation => _animation != null; - public void SetAnimation(ChangeSet cs, IAnimationInstance animation) + public void SetAnimation(ServerObject target, ChangeSet cs, IAnimationInstance animation, int storeOffset) { + _direct = default; + if (_animation != null) + { + if (target.IsActive) + _animation.Deactivate(); + } + _animation = animation; - _animation.Start(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated)); + _animation.Initialize(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated), storeOffset); + if (target.IsActive) + _animation.Activate(); + + Subscriptions.Invalidate(); } - public static implicit operator AnimatedValueStore(T value) => new AnimatedValueStore() + public void SetValue(ServerObject target, T value) { - _direct = value - }; + if (_animation != null) + { + if (target.IsActive) + _animation.Deactivate(); + } + + _animation = null; + _direct = value; + Subscriptions.Invalidate(); + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs new file mode 100644 index 0000000000..212237049f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations; + +internal abstract class AnimationInstanceBase : IAnimationInstance +{ + private List<(ServerObject obj, int member)>? _trackedObjects; + protected PropertySetSnapshot Parameters { get; } + public ServerObject TargetObject { get; } + protected int StoreOffset { get; private set; } + private bool _invalidated; + + public AnimationInstanceBase(ServerObject target, PropertySetSnapshot parameters) + { + Parameters = parameters; + TargetObject = target; + } + + protected void Initialize(int storeOffset, HashSet<(string name, string member)> trackedObjects) + { + if (trackedObjects.Count > 0) + { + _trackedObjects = new (); + foreach (var t in trackedObjects) + { + var obj = Parameters.GetObjectParameter(t.name); + if (obj is ServerObject tracked) + { + var off = tracked.GetFieldOffset(t.member); + if (off == null) +#if DEBUG + throw new InvalidCastException("Attempting to subscribe to unknown field"); +#else + continue; +#endif + _trackedObjects.Add((tracked, off.Value)); + } + } + } + + StoreOffset = storeOffset; + } + + public abstract void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset); + protected abstract ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue); + + public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + { + _invalidated = false; + return EvaluateCore(now, currentValue); + } + + public virtual void Activate() + { + if (_trackedObjects != null) + foreach (var tracked in _trackedObjects) + tracked.obj.SubscribeToInvalidation(tracked.member, this); + } + + public virtual void Deactivate() + { + if (_trackedObjects != null) + foreach (var tracked in _trackedObjects) + tracked.obj.UnsubscribeFromInvalidation(tracked.member, this); + } + + public void Invalidate() + { + if (_invalidated) + return; + _invalidated = true; + TargetObject.NotifyAnimatedValueChanged(StoreOffset); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index 9375faaaae..fe20115b38 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -53,7 +53,7 @@ namespace Avalonia.Rendering.Composition.Animations ExpressionVariant? finalValue); internal PropertySetSnapshot CreateSnapshot(bool server) - => _propertySet.Snapshot(server, 1); + => _propertySet.Snapshot(server); void ICompositionAnimationBase.InternalOnly() { diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs index 47b947b2e9..7944fe7990 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs @@ -1,22 +1,22 @@ using System; +using System.Collections.Generic; using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { - internal class ExpressionAnimationInstance : IAnimationInstance + internal class ExpressionAnimationInstance : AnimationInstanceBase, IAnimationInstance { private readonly Expression _expression; - private readonly IExpressionObject _target; private ExpressionVariant _startingValue; private readonly ExpressionVariant? _finalValue; - private readonly PropertySetSnapshot _parameters; - public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue) { var ctx = new ExpressionEvaluationContext { - Parameters = _parameters, - Target = _target, + Parameters = Parameters, + Target = TargetObject, ForeignFunctionInterface = BuiltInExpressionFfi.Instance, StartingValue = _startingValue, FinalValue = _finalValue ?? _startingValue, @@ -25,20 +25,21 @@ namespace Avalonia.Rendering.Composition.Animations return _expression.Evaluate(ref ctx); } - public void Start(TimeSpan startedAt, ExpressionVariant startingValue) + public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset) { _startingValue = startingValue; + var hs = new HashSet<(string, string)>(); + _expression.CollectReferences(hs); + base.Initialize(storeOffset, hs); } - + public ExpressionAnimationInstance(Expression expression, - IExpressionObject target, + ServerObject target, ExpressionVariant? finalValue, - PropertySetSnapshot parameters) + PropertySetSnapshot parameters) : base(target, parameters) { _expression = expression; - _target = target; _finalValue = finalValue; - _parameters = parameters; } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs index a0b066ae0c..05d1b50953 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs @@ -1,11 +1,16 @@ using System; using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { internal interface IAnimationInstance { + ServerObject TargetObject { get; } ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue); - void Start(TimeSpan startedAt, ExpressionVariant startingValue); + void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset); + void Activate(); + void Deactivate(); + void Invalidate(); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs index b90a02148d..9571cef0b4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs @@ -1,15 +1,15 @@ using System; +using System.Collections.Generic; using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { - class KeyFrameAnimationInstance : IAnimationInstance where T : struct + class KeyFrameAnimationInstance : AnimationInstanceBase, IAnimationInstance where T : struct { private readonly IInterpolator _interpolator; private readonly ServerKeyFrame[] _keyFrames; - private readonly PropertySetSnapshot _snapshot; private readonly ExpressionVariant? _finalValue; - private readonly IExpressionObject _target; private readonly AnimationDelayBehavior _delayBehavior; private readonly TimeSpan _delayTime; private readonly AnimationDirection _direction; @@ -19,21 +19,21 @@ namespace Avalonia.Rendering.Composition.Animations private readonly AnimationStopBehavior _stopBehavior; private TimeSpan _startedAt; private T _startingValue; + private readonly TimeSpan _totalDuration; + private bool _finished; public KeyFrameAnimationInstance( IInterpolator interpolator, ServerKeyFrame[] keyFrames, PropertySetSnapshot snapshot, ExpressionVariant? finalValue, - IExpressionObject target, + ServerObject target, AnimationDelayBehavior delayBehavior, TimeSpan delayTime, AnimationDirection direction, TimeSpan duration, AnimationIterationBehavior iterationBehavior, - int iterationCount, AnimationStopBehavior stopBehavior) + int iterationCount, AnimationStopBehavior stopBehavior) : base(target, snapshot) { _interpolator = interpolator; _keyFrames = keyFrames; - _snapshot = snapshot; _finalValue = finalValue; - _target = target; _delayBehavior = delayBehavior; _delayTime = delayTime; _direction = direction; @@ -41,26 +41,43 @@ namespace Avalonia.Rendering.Composition.Animations _iterationBehavior = iterationBehavior; _iterationCount = iterationCount; _stopBehavior = stopBehavior; + if (_iterationBehavior == AnimationIterationBehavior.Count) + _totalDuration = delayTime.Add(TimeSpan.FromTicks(iterationCount * _duration.Ticks)); if (_keyFrames.Length == 0) throw new InvalidOperationException("Animation has no key frames"); if(_duration.Ticks <= 0) throw new InvalidOperationException("Invalid animation duration"); } - public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + + protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue) { - var elapsed = now - _startedAt; var starting = ExpressionVariant.Create(_startingValue); var ctx = new ExpressionEvaluationContext { - Parameters = _snapshot, - Target = _target, + Parameters = Parameters, + Target = TargetObject, CurrentValue = currentValue, FinalValue = _finalValue ?? starting, StartingValue = starting, ForeignFunctionInterface = BuiltInExpressionFfi.Instance }; + var elapsed = now - _startedAt; + var res = EvaluateImpl(elapsed, currentValue, ref ctx); + if (_iterationBehavior == AnimationIterationBehavior.Count + && !_finished + && elapsed > _totalDuration) + { + // Active check? + TargetObject.Compositor.RemoveFromClock(this); + _finished = true; + } + return res; + } + + private ExpressionVariant EvaluateImpl(TimeSpan elapsed, ExpressionVariant currentValue, ref ExpressionEvaluationContext ctx) + { if (elapsed < _delayTime) { if (_delayBehavior == AnimationDelayBehavior.SetInitialValueBeforeDelay) @@ -130,10 +147,28 @@ namespace Avalonia.Rendering.Composition.Animations return f.Value; } - public void Start(TimeSpan startedAt, ExpressionVariant startingValue) + public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset) { _startedAt = startedAt; _startingValue = startingValue.CastOrDefault(); + var hs = new HashSet<(string, string)>(); + + // TODO: Update subscriptions based on the current keyframe rather than keeping subscriptions to all of them + foreach (var frame in _keyFrames) + frame.Expression?.CollectReferences(hs); + Initialize(storeOffset, hs); + } + + public override void Activate() + { + TargetObject.Compositor.AddToClock(this); + base.Activate(); + } + + public override void Deactivate() + { + TargetObject.Compositor.RemoveFromClock(this); + base.Deactivate(); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs index d7f2504061..26ba35409d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs @@ -66,7 +66,7 @@ namespace Avalonia.Rendering.Composition.Animations struct ServerKeyFrame { public T Value; - public Expression Expression; + public Expression? Expression; public IEasingFunction EasingFunction; public float Key; } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs index 004c2676ff..bc0ce804dc 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -24,11 +24,16 @@ namespace Avalonia.Rendering.Composition _variants[key] = value; } + /* + For INTERNAL USE by CompositionAnimation ONLY, we DON'T support expression + paths like SomeParam.SomePropertyObject.SomeValue + */ internal void Set(string key, CompositionObject obj) { _objects[key] = obj ?? throw new ArgumentNullException(nameof(obj)); _variants.Remove(key); } + public void InsertColor(string propertyName, Avalonia.Media.Color value) => Set(propertyName, value); public void InsertMatrix3x2(string propertyName, Matrix3x2 value) => Set(propertyName, value); @@ -99,7 +104,10 @@ namespace Avalonia.Rendering.Composition _variants.Remove(key); } - internal PropertySetSnapshot Snapshot(bool server, int allowedNestingLevel) + internal PropertySetSnapshot Snapshot(bool server) => + SnapshotCore(server, 1); + + private PropertySetSnapshot SnapshotCore(bool server, int allowedNestingLevel) { var dic = new Dictionary(_objects.Count + _variants.Count); foreach (var o in _objects) @@ -108,7 +116,7 @@ namespace Avalonia.Rendering.Composition { if (allowedNestingLevel <= 0) throw new InvalidOperationException("PropertySet depth limit reached"); - dic[o.Key] = new PropertySetSnapshot.Value(ps.Snapshot(server, allowedNestingLevel - 1)); + dic[o.Key] = new PropertySetSnapshot.Value(ps.SnapshotCore(server, allowedNestingLevel - 1)); } else if (o.Value.Server == null) throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed"); diff --git a/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs b/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs new file mode 100644 index 0000000000..afda314276 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs @@ -0,0 +1,6 @@ +namespace Avalonia.Rendering.Composition; + +public static class ElementCompositionPreview +{ + public static CompositionVisual? GetElementVisual(Visual visual) => visual.CompositionVisual; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs index 5577d2b52a..088771e1ba 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Reflection; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Expressions { @@ -15,6 +16,11 @@ namespace Avalonia.Rendering.Composition.Expressions public abstract ExpressionVariant Evaluate(ref ExpressionEvaluationContext context); + public virtual void CollectReferences(HashSet<(string parameter, string property)> references) + { + + } + protected abstract string Print(); public override string ToString() => Print(); @@ -114,6 +120,13 @@ namespace Avalonia.Rendering.Composition.Expressions return FalsePart.Evaluate(ref context); } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Condition.CollectReferences(references); + TruePart.CollectReferences(references); + FalsePart.CollectReferences(references); + } + protected override string Print() => $"({Condition}) ? ({TruePart}) : ({FalsePart})"; } @@ -128,6 +141,7 @@ namespace Avalonia.Rendering.Composition.Expressions } public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) => Constant; + protected override string Print() => Constant.ToString(CultureInfo.InvariantCulture); } @@ -155,6 +169,12 @@ namespace Avalonia.Rendering.Composition.Expressions return res; } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + foreach(var arg in Parameters) + arg.CollectReferences(references); + } + protected override string Print() { return Name + "( (" + string.Join("), (", Parameters) + ") )"; @@ -173,18 +193,30 @@ namespace Avalonia.Rendering.Composition.Expressions Member = string.Intern(member); } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Target.CollectReferences(references); + if (Target is ParameterExpression pe) + references.Add((pe.Name, Member)); + } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) { if (Target is KeywordExpression ke && ke.Keyword == ExpressionKeyword.Target) + { return context.Target.GetProperty(Member); + } + if (Target is ParameterExpression pe) { var obj = context.Parameters?.GetObjectParameter(pe.Name); if (obj != null) + { return obj.GetProperty(Member); + } } - + // Those are considered immutable return Target.Evaluate(ref context).GetProperty(Member); } @@ -263,6 +295,11 @@ namespace Avalonia.Rendering.Composition.Expressions return default; } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Parameter.CollectReferences(references); + } + protected override string Print() { return OperatorName(Type) + Parameter; @@ -313,6 +350,12 @@ namespace Avalonia.Rendering.Composition.Expressions return default; } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Left.CollectReferences(references); + Right.CollectReferences(references); + } + protected override string Print() { return "(" + Left + OperatorName(Type) + Right + ")"; diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs index a7ddabd70d..9d23551e43 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Expressions { diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs new file mode 100644 index 0000000000..334f975aa0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs @@ -0,0 +1,57 @@ +using System.Collections; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Expressions; + +internal class ExpressionTrackedObjects : IEnumerable +{ + private List _list = new(); + private HashSet _hashSet = new(); + + public void Add(IExpressionObject obj, string member) + { + if (_hashSet.Add(obj)) + _list.Add(obj); + } + + public void Clear() + { + _list.Clear(); + _hashSet.Clear(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _list.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_list).GetEnumerator(); + } + + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + + public struct Pool + { + private Stack _stack = new(); + + public Pool() + { + } + + public ExpressionTrackedObjects Get() + { + if (_stack.Count > 0) + return _stack.Pop(); + return new ExpressionTrackedObjects(); + } + + public void Return(ExpressionTrackedObjects obj) + { + _stack.Clear(); + _stack.Push(obj); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index 8c2e6e774a..a60084d8f3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -10,24 +10,23 @@ namespace Avalonia.Rendering.Composition.Server; internal class FpsCounter { - private readonly GlyphTypeface _typeface; - private readonly bool _useManualFpsCounting; private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private int _framesThisSecond; + private int _totalFrames; private int _fps; private TimeSpan _lastFpsUpdate; - private GlyphRun[] _runs = new GlyphRun[10]; + const int FirstChar = 32; + const int LastChar = 126; + private GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1]; - public FpsCounter(GlyphTypeface typeface, bool useManualFpsCounting = false) + public FpsCounter(GlyphTypeface typeface) { - for (var c = 0; c <= 9; c++) + for (var c = FirstChar; c <= LastChar; c++) { - var s = c.ToString(); + var s = new string((char)c, 1); var glyph = typeface.GetGlyph((uint)(s[0])); - _runs[c] = new GlyphRun(typeface, 18, new ReadOnlySlice(s.AsMemory()), new ushort[] { glyph }); + _runs[c - FirstChar] = new GlyphRun(typeface, 18, new ReadOnlySlice(s.AsMemory()), new ushort[] { glyph }); } - _typeface = typeface; - _useManualFpsCounting = useManualFpsCounting; } public void FpsTick() => _framesThisSecond++; @@ -37,8 +36,8 @@ internal class FpsCounter var now = _stopwatch.Elapsed; var elapsed = now - _lastFpsUpdate; - if (!_useManualFpsCounting) - ++_framesThisSecond; + ++_framesThisSecond; + ++_totalFrames; if (elapsed.TotalSeconds > 1) { @@ -47,12 +46,12 @@ internal class FpsCounter _lastFpsUpdate = now; } - var fpsLine = _fps.ToString("000"); + var fpsLine = $"Frame #{_totalFrames:00000000} FPS: {_fps:000}"; double width = 0; double height = 0; foreach (var ch in fpsLine) { - var run = _runs[ch - '0']; + var run = _runs[ch - FirstChar]; width += run.Size.Width; height = Math.Max(height, run.Size.Height); } @@ -64,7 +63,7 @@ internal class FpsCounter double offset = 0; foreach (var ch in fpsLine) { - var run = _runs[ch - '0']; + var run = _runs[ch - FirstChar]; context.Transform = Matrix.CreateTranslation(offset, 0); context.DrawGlyphRun(Brushes.White, run); offset += run.Size.Width; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 04ec711455..dab65fc8ed 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -56,10 +56,11 @@ namespace Avalonia.Rendering.Composition.Server Compositor.UpdateServerTime(); - Root.Update(this, Matrix4x4.Identity); - if(_dirtyRect.IsEmpty && !_redrawRequested) return; + + Root.Update(this, Matrix4x4.Identity); + _redrawRequested = false; using (var targetContext = _renderTarget.CreateDrawingContext(null)) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index e56f85acdf..5dbe9cfb17 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition.Server @@ -13,6 +15,8 @@ namespace Avalonia.Rendering.Composition.Server public Stopwatch Clock { get; } = Stopwatch.StartNew(); public TimeSpan ServerNow { get; private set; } private List _activeTargets = new(); + private HashSet _activeAnimations = new(); + private List _animationsToUpdate = new(); public ServerCompositor(IRenderLoop renderLoop) { @@ -64,6 +68,7 @@ namespace Avalonia.Rendering.Composition.Server } bool IRenderLoopTask.NeedsUpdate => false; + void IRenderLoopTask.Update(TimeSpan time) { } @@ -71,6 +76,15 @@ namespace Avalonia.Rendering.Composition.Server void IRenderLoopTask.Render() { ApplyPendingBatches(); + + foreach(var animation in _activeAnimations) + _animationsToUpdate.Add(animation); + + foreach(var animation in _animationsToUpdate) + animation.Invalidate(); + + _animationsToUpdate.Clear(); + foreach (var t in _activeTargets) t.Render(); @@ -86,5 +100,11 @@ namespace Avalonia.Rendering.Composition.Server { _activeTargets.Remove(target); } + + public void AddToClock(IAnimationInstance animationInstance) => + _activeAnimations.Add(animationInstance); + + public void RemoveFromClock(IAnimationInstance animationInstance) => + _activeAnimations.Remove(animationInstance); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index 072377cd7e..5b2f58b186 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -1,5 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server { @@ -9,7 +15,9 @@ namespace Avalonia.Rendering.Composition.Server public virtual long LastChangedBy => ItselfLastChangedBy; public long ItselfLastChangedBy { get; private set; } - + private uint _activationCount; + public bool IsActive => _activationCount != 0; + public ServerObject(ServerCompositor compositor) { Compositor = compositor; @@ -23,6 +31,7 @@ namespace Avalonia.Rendering.Composition.Server public void Apply(ChangeSet changes) { ApplyCore(changes); + ValuesInvalidated(); ItselfLastChangedBy = changes.Batch!.SequenceId; } @@ -32,5 +41,76 @@ namespace Avalonia.Rendering.Composition.Server } ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); + + public void Activate() + { + _activationCount++; + if (_activationCount == 1) + Activated(); + } + + public void Deactivate() + { +#if DEBUG + if (_activationCount == 0) + throw new InvalidOperationException(); +#endif + _activationCount--; + if (_activationCount == 0) + Deactivated(); + } + + protected virtual void Activated() + { + + } + + protected virtual void Deactivated() + { + + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected int GetOffset(ref T field) where T : struct + { + return Unsafe.ByteOffset(ref Unsafe.As(ref _activationCount), + ref Unsafe.As(ref field)) + .ToInt32(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ref ServerObjectSubscriptionStore GetStoreFromOffset(int offset) + { + return ref Unsafe.As(ref Unsafe.AddByteOffset(ref _activationCount, + new IntPtr(offset))); + } + + public void NotifyAnimatedValueChanged(int offset) + { + ref var store = ref GetStoreFromOffset(offset); + store.Invalidate(); + ValuesInvalidated(); + } + + protected virtual void ValuesInvalidated() + { + + } + + public void SubscribeToInvalidation(int member, IAnimationInstance animation) + { + ref var store = ref GetStoreFromOffset(member); + if (store.Subscribers.AddRef(animation)) + Activate(); + } + + public void UnsubscribeFromInvalidation(int member, IAnimationInstance animation) + { + ref var store = ref GetStoreFromOffset(member); + if (store.Subscribers.ReleaseRef(animation)) + Deactivate(); + } + + public virtual int? GetFieldOffset(string fieldName) => null; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index bf5b8bf292..05b63a7a73 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -7,6 +7,7 @@ namespace Avalonia.Rendering.Composition.Server unsafe partial class ServerCompositionVisual : ServerObject { private bool _isDirty; + private ServerCompositionTarget? _root; protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -95,16 +96,31 @@ namespace Avalonia.Rendering.Composition.Server Parent = c.Parent.Value; if (c.Root.IsSet) Root = c.Root.Value; - _isDirty = true; + ValuesInvalidated(); + } + + public ServerCompositionTarget? Root + { + get => _root; + private set + { + if(_root != null) + Deactivate(); + _root = value; + if (_root != null) + Activate(); + } + } + protected override void ValuesInvalidated() + { + _isDirty = true; if (IsVisibleInFrame) Root?.AddDirtyRect(TransformedBounds); else Root?.Invalidate(); } - public ServerCompositionTarget? Root { get; private set; } - public ServerCompositionVisual? Parent { get; private set; } public bool IsVisibleInFrame { get; set; } public Rect TransformedBounds { get; set; } diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index 25fce01de3..f7c8078073 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -38,7 +38,6 @@ namespace Avalonia.Rendering.Composition { } - internal Matrix4x4? TryGetServerTransform() { if (Root == null) diff --git a/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs b/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs new file mode 100644 index 0000000000..71305a8305 --- /dev/null +++ b/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Avalonia.Utilities; + +internal class RefTrackingDictionary : Dictionary where TKey : class +{ + /// + /// Increase reference count for a key by 1. + /// + /// true if key was added to the dictionary, false otherwise + public bool AddRef(TKey key) + { +#if NET5_0_OR_GREATER + ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(this, key, out var _); + count++; +#else + TryGetValue(key, out var count); + count++; + this[key] = count; +#endif + return count == 1; + } + + /// + /// Decrease reference count for a key by 1. + /// + /// true if key was removed to the dictionary, false otherwise + public bool ReleaseRef(TKey key) + { +#if NET5_0_OR_GREATER + ref var count = ref CollectionsMarshal.GetValueRefOrNullRef(this, key); + if (Unsafe.IsNullRef(ref count)) +#if DEBUG + throw new InvalidOperationException("Attempting to release a non-referenced object"); +#else + return false; +#endif // DEBUG + count--; + if (count == 0) + { + Remove(key); + return true; + } + + return false; +#else + if (!TryGetValue(key, out var count)) +#if DEBUG + throw new InvalidOperationException("Attempting to release a non-referenced object"); +#else + return false; +#endif // DEBUG + count--; + if (count == 0) + { + Remove(key); + return true; + } + + this[key] = count; + return false; +#endif + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj index 97e58f8a64..3312f7a619 100644 --- a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj +++ b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj @@ -3,6 +3,7 @@ netstandard2.0 enable + false diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs index f079a339df..72311b4d18 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs @@ -2,20 +2,22 @@ using System.IO; using System.Xml.Serialization; using Microsoft.CodeAnalysis; -namespace Avalonia.SourceGenerator.CompositionGenerator; - -[Generator(LanguageNames.CSharp)] -public class CompositionRoslynGenerator : IIncrementalGenerator +namespace Avalonia.SourceGenerator.CompositionGenerator { - public void Initialize(IncrementalGeneratorInitializationContext context) + [Generator(LanguageNames.CSharp)] + public class CompositionRoslynGenerator : IIncrementalGenerator { - var schema = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml")); - var configs = schema.Select((t, _) => - (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString()))); - context.RegisterSourceOutput(configs, (spc, config) => + public void Initialize(IncrementalGeneratorInitializationContext context) { - var generator = new Generator(spc, config); - generator.Generate(); - }); + var schema = + context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml")); + var configs = schema.Select((t, _) => + (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString()))); + context.RegisterSourceOutput(configs, (spc, config) => + { + var generator = new Generator(new RoslynCompositionGeneratorSink(spc), config); + generator.Generate(); + }); + } } } \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs index 43ef4a96e8..5a514a4eff 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs @@ -7,14 +7,14 @@ using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Avalonia.SourceGenerator.CompositionGenerator.Extensions; namespace Avalonia.SourceGenerator.CompositionGenerator { - partial class Generator + public partial class Generator { - private readonly SourceProductionContext _output; + private readonly ICompositionGeneratorSink _output; private readonly GConfig _config; private readonly HashSet _objects; private readonly HashSet _brushes; private readonly Dictionary _manuals; - public Generator(SourceProductionContext output, GConfig config) + public Generator(ICompositionGeneratorSink output, GConfig config) { _output = output; _config = config; @@ -168,17 +168,35 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ExpressionStatement(InvocationExpression(IdentifierName("ApplyChangesExtra")) .AddArgumentListArguments(Argument(IdentifierName("c")))) ); + + var uninitializedObjectName = "dummy"; + var serverStaticCtorBody = cl.Abstract + ? Block() + : Block( + ParseStatement( + $"var dummy = ({serverName})System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof({serverName}));"), + ParseStatement($"System.GC.SuppressFinalize(dummy);"), + ParseStatement("InitializeFieldOffsets(dummy);") + ); + + var initializeFieldOffsetsBody = cl.ServerBase == null + ? Block() + : Block(ParseStatement($"{cl.ServerBase}.InitializeFieldOffsets(dummy);")); var resetBody = Block(); var startAnimationBody = Block(); var getPropertyBody = Block(); var serverGetPropertyBody = Block(); + var serverGetFieldOffsetBody = Block(); + var activatedBody = Block(ParseStatement("base.Activated();")); + var deactivatedBody = Block(ParseStatement("base.Deactivated();")); var defaultsMethodBody = Block(); foreach (var prop in cl.Properties) { var fieldName = "_" + prop.Name.WithLowerFirst(); + var fieldOffsetName = "s_OffsetOf" + fieldName; var propType = ParseTypeName(prop.Type); var filteredPropertyType = prop.Type.TrimEnd('?'); var isObject = _objects.Contains(filteredPropertyType); @@ -229,7 +247,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if (animatedServer) server = server.AddMembers( - DeclareField("AnimatedValueStore<" + serverPropertyType + ">", fieldName), + DeclareField("ServerAnimatedValueStore<" + serverPropertyType + ">", fieldName), PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) .AddModifiers(SyntaxKind.PublicKeyword) .WithExpressionBody(ArrowExpressionClause( @@ -240,24 +258,24 @@ namespace Avalonia.SourceGenerator.CompositionGenerator else { server = server - .AddMembers(DeclareField(serverPropertyType, fieldName)) + .AddMembers(DeclareField("ServerValueStore<" + serverPropertyType + ">", fieldName)) .AddMembers(PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) .AddModifiers(SyntaxKind.PublicKeyword) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, - Block(ReturnStatement(IdentifierName(fieldName)))), + Block(ReturnStatement(MemberAccess(IdentifierName(fieldName), "Value")))), AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, Block( ParseStatement("var changed = false;"), IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, - IdentifierName(fieldName), + MemberAccess(IdentifierName(fieldName), "Value"), IdentifierName("value")), Block( ParseStatement("On" + prop.Name + "Changing();"), ParseStatement($"changed = true;")) ), ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, - IdentifierName(fieldName), IdentifierName("value"))), + MemberAccess(IdentifierName(fieldName), "Value"), IdentifierName("value"))), ParseStatement($"if(changed) On" + prop.Name + "Changed();") )) )) @@ -270,15 +288,22 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if (animatedServer) applyMethodBody = applyMethodBody.AddStatements( IfStatement(MemberAccess(changesVar, prop.Name, "IsValue"), - ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, - IdentifierName(fieldName), MemberAccess(changesVar, prop.Name, "Value")))), + ExpressionStatement( + InvocationExpression(MemberAccess(fieldName, "SetValue"), + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("this")), + Argument(MemberAccess(changesVar, prop.Name, "Value")), + }))))), IfStatement(MemberAccess(changesVar, prop.Name, "IsAnimation"), ExpressionStatement( InvocationExpression(MemberAccess(fieldName, "SetAnimation"), ArgumentList(SeparatedList(new[] { + Argument(IdentifierName("this")), Argument(changesVar), - Argument(MemberAccess(changesVar, prop.Name, "Animation")) + Argument(MemberAccess(changesVar, prop.Name, "Animation")), + Argument(IdentifierName(fieldOffsetName)) }))))) ); else @@ -288,16 +313,28 @@ namespace Avalonia.SourceGenerator.CompositionGenerator IdentifierName(prop.Name), MemberAccess(changesVar, prop.Name, "Value")))) ); - resetBody = resetBody.AddStatements( ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); if (animatedServer) + { startAnimationBody = ApplyStartAnimation(startAnimationBody, prop, fieldName); + activatedBody = activatedBody.AddStatements(ParseStatement($"{fieldName}.Activate(this);")); + deactivatedBody = deactivatedBody.AddStatements(ParseStatement($"{fieldName}.Deactivate(this);")); + } + getPropertyBody = ApplyGetProperty(getPropertyBody, prop); - serverGetPropertyBody = ApplyGetProperty(getPropertyBody, prop); + serverGetPropertyBody = ApplyGetProperty(serverGetPropertyBody, prop); + serverGetFieldOffsetBody = ApplyGetProperty(serverGetFieldOffsetBody, prop, fieldOffsetName); + + server = server.AddMembers(DeclareField("int", fieldOffsetName, SyntaxKind.StaticKeyword)); + initializeFieldOffsetsBody = initializeFieldOffsetsBody.AddStatements(ExpressionStatement( + AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, IdentifierName(fieldOffsetName), + InvocationExpression(MemberAccess(IdentifierName(uninitializedObjectName), "GetOffset"), + ArgumentList(SingletonSeparatedList(Argument( + RefExpression(MemberAccess(IdentifierName(uninitializedObjectName), fieldName))))))))); if (prop.DefaultValue != null) { @@ -357,6 +394,21 @@ namespace Avalonia.SourceGenerator.CompositionGenerator Parameter(Identifier("changes")).WithType(ParseTypeName("ChangeSet"))) .WithBody(applyMethodBody)); + server = server.AddMembers(ConstructorDeclaration(serverName) + .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) + .WithBody(serverStaticCtorBody)); + + server = server.AddMembers( + ((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected static void InitializeFieldOffsets({serverName} dummy){{}}")!) + .WithBody(initializeFieldOffsetsBody)); + + server = server + .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected override void Activated(){{}}")!).WithBody(activatedBody)) + .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected override void Deactivated(){{}}")!).WithBody(deactivatedBody)); + client = client.AddMembers( MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)); @@ -374,7 +426,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator client = WithGetProperty(client, getPropertyBody, false); server = WithGetProperty(server, serverGetPropertyBody, true); - + server = WithGetFieldOffset(server, serverGetFieldOffsetBody); + if(cl.Implements.Count > 0) foreach (var impl in cl.Implements) { @@ -452,17 +505,19 @@ return; "Vector2", "Vector3", "Vector4", + "Matrix", "Matrix3x2", "Matrix4x4", "Quaternion", - "CompositionColor" + "Color", + "Avalonia.Media.Color" }; - BlockSyntax ApplyGetProperty(BlockSyntax body, GProperty prop) + BlockSyntax ApplyGetProperty(BlockSyntax body, GProperty prop, string? expr = null) { if (VariantPropertyTypes.Contains(prop.Type)) return body.AddStatements( - ParseStatement($"if(name == \"{prop.Name}\")\n return {prop.Name};\n") + ParseStatement($"if(name == \"{prop.Name}\")\n return {expr ?? prop.Name};\n") ); return body; @@ -480,6 +535,19 @@ return; return cl.AddMembers(method); } + + ClassDeclarationSyntax WithGetFieldOffset(ClassDeclarationSyntax cl, BlockSyntax body) + { + if (body.Statements.Count == 0) + return cl; + body = body.AddStatements( + ParseStatement("return base.GetFieldOffset(name);")); + var method = ((MethodDeclarationSyntax)ParseMemberDeclaration( + $"public override int? GetFieldOffset(string name){{}}")) + .WithBody(body); + + return cl.AddMembers(method); + } ClassDeclarationSyntax WithStartAnimation(ClassDeclarationSyntax cl, BlockSyntax body) { diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs new file mode 100644 index 0000000000..085a4041be --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs @@ -0,0 +1,6 @@ +namespace Avalonia.SourceGenerator.CompositionGenerator; + +public interface ICompositionGeneratorSink +{ + void AddSource(string name, string code); +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs new file mode 100644 index 0000000000..6fec3faf93 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs @@ -0,0 +1,15 @@ +using Microsoft.CodeAnalysis; + +namespace Avalonia.SourceGenerator.CompositionGenerator; + +class RoslynCompositionGeneratorSink : ICompositionGeneratorSink +{ + private readonly SourceProductionContext _ctx; + + public RoslynCompositionGeneratorSink(SourceProductionContext ctx) + { + _ctx = ctx; + } + + public void AddSource(string name, string code) => _ctx.AddSource(name, code); +} \ No newline at end of file From bcbd86eca3bda0b3e6aeee03fb063a2ba6811c28 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 25 May 2022 21:15:45 +0300 Subject: [PATCH 08/61] Added VisualBrush support --- .../Composition/CompositionDrawListVisual.cs | 2 +- .../Drawing/CompositionDrawList.cs | 16 +++++++++- .../Drawing/CompositionDrawingContext.cs | 27 ++++++++++------- .../Composition/Server/DrawingContextProxy.cs | 30 ++++++++++++++++++- .../Server/ServerCompositionDrawListVisual.cs | 5 +--- .../Server/ServerCompositionTarget.cs | 5 ++-- .../Rendering/DeferredRenderer.cs | 6 ++-- .../SceneGraph/BrushDrawOperation.cs | 16 +++++++--- .../SceneGraph/DeferredDrawingContextImpl.cs | 4 +-- .../Rendering/SceneGraph/EllipseNode.cs | 7 ++--- .../Rendering/SceneGraph/GeometryNode.cs | 11 +++---- .../Rendering/SceneGraph/GlyphRunNode.cs | 11 +++---- .../Rendering/SceneGraph/LineNode.cs | 8 ++--- .../Rendering/SceneGraph/OpacityMaskNode.cs | 12 ++++---- .../Rendering/SceneGraph/RectangleNode.cs | 11 +++---- 15 files changed, 104 insertions(+), 67 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index b19c311663..e4ed0abd29 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -24,7 +24,7 @@ internal class CompositionDrawListVisual : CompositionContainerVisual private protected override IChangeSetPool ChangeSetPool => DrawListVisualChanges.Pool; - internal CompositionDrawListVisual(Compositor compositor, ServerCompositionContainerVisual server, Visual visual) : base(compositor, server) + internal CompositionDrawListVisual(Compositor compositor, ServerCompositionDrawListVisual server, Visual visual) : base(compositor, server) { Visual = visual; } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs index aca8ef7c46..1d416f5a8a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections.Pooled; +using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; @@ -7,6 +8,8 @@ namespace Avalonia.Rendering.Composition.Drawing; internal class CompositionDrawList : PooledList> { + public Size? Size { get; set; } + public CompositionDrawList() { @@ -26,11 +29,22 @@ internal class CompositionDrawList : PooledList> public CompositionDrawList Clone() { - var clone = new CompositionDrawList(Count); + var clone = new CompositionDrawList(Count) { Size = Size }; foreach (var r in this) clone.Add(r.Clone()); return clone; } + + public void Render(CompositorDrawingContextProxy canvas) + { + foreach (var cmd in this) + { + canvas.VisualBrushDrawList = (cmd.Item as BrushDrawOperation)?.Aux as CompositionDrawList; + cmd.Item.Render(canvas); + } + + canvas.VisualBrushDrawList = null; + } } internal class CompositionDrawListBuilder diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index c8e5d9e064..96c0e22d56 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -359,25 +359,30 @@ internal class CompositionDrawingContext : IDrawingContextImpl ? _builder.DrawOperations[_drawOperationIndex] as IRef : null; } - - private IDictionary? CreateChildScene(IBrush? brush) + + private IDisposable? CreateChildScene(IBrush? brush) { - /* - var visualBrush = brush as VisualBrush; - - if (visualBrush != null) + if (brush is VisualBrush visualBrush) { var visual = visualBrush.Visual; if (visual != null) { + // TODO: This is a temporary solution to make visual brush to work like it does with DeferredRenderer + // We should directly reference the corresponding CompositionVisual (which should + // be attached to the same composition target) like UWP does. + // Render-able visuals shouldn't be dangling unattached (visual as IVisualBrushInitialize)?.EnsureInitialized(); - var scene = new Scene(visual); - _sceneBuilder.UpdateAll(scene); - return new Dictionary { { visualBrush.Visual, scene } }; - } - }*/ + var drawList = new CompositionDrawList() { Size = visual.Bounds.Size }; + var recorder = new CompositionDrawingContext(); + recorder.BeginUpdate(drawList); + ImmediateRenderer.Render(visual, new DrawingContext(recorder)); + recorder.EndUpdate(); + + return drawList; + } + } return null; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 7d061e86a9..8b6ac5b0c2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -2,6 +2,7 @@ using System.Numerics; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; @@ -10,12 +11,21 @@ namespace Avalonia.Rendering.Composition.Server; internal class CompositorDrawingContextProxy : IDrawingContextImpl { private IDrawingContextImpl _impl; + private readonly VisualBrushRenderer _visualBrushRenderer; - public CompositorDrawingContextProxy(IDrawingContextImpl impl) + public CompositorDrawingContextProxy(IDrawingContextImpl impl, VisualBrushRenderer visualBrushRenderer) { _impl = impl; + _visualBrushRenderer = visualBrushRenderer; } + // This is a hack to make it work with the current way of handling visual brushes + public CompositionDrawList? VisualBrushDrawList + { + get => _visualBrushRenderer.VisualBrushDrawList; + set => _visualBrushRenderer.VisualBrushDrawList = value; + } + public Matrix PreTransform { get; set; } = Matrix.Identity; public void Dispose() @@ -135,4 +145,22 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl { _impl.Custom(custom); } + + public class VisualBrushRenderer : IVisualBrushRenderer + { + public CompositionDrawList? VisualBrushDrawList { get; set; } + public Size GetRenderTargetSize(IVisualBrush brush) + { + return VisualBrushDrawList?.Size ?? Size.Empty; + } + + public void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) + { + if (VisualBrushDrawList != null) + { + foreach (var cmd in VisualBrushDrawList) + cmd.Item.Render(context); + } + } + } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index f0384c36fc..ba18211459 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -51,10 +51,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { if (_renderCommands != null) { - foreach (var cmd in _renderCommands) - { - cmd.Item.Render(canvas); - } + _renderCommands.Render(canvas); } base.RenderCore(canvas, transform); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index dab65fc8ed..d8a5de4f54 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -75,11 +75,12 @@ namespace Avalonia.Rendering.Composition.Server if (!_dirtyRect.IsEmpty) { - using (var context = _layer.CreateDrawingContext(null)) + var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer(); + using (var context = _layer.CreateDrawingContext(visualBrushHelper)) { context.PushClip(_dirtyRect); context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), Root.CombinedTransformMatrix); context.PopClip(); } } diff --git a/src/Avalonia.Base/Rendering/DeferredRenderer.cs b/src/Avalonia.Base/Rendering/DeferredRenderer.cs index 82be0a1a0f..4236763e3b 100644 --- a/src/Avalonia.Base/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Base/Rendering/DeferredRenderer.cs @@ -272,16 +272,18 @@ namespace Avalonia.Rendering } } + Scene? TryGetChildScene(IRef? op) => (op?.Item as BrushDrawOperation)?.Aux as Scene; + /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { - return (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty; + return TryGetChildScene(_currentDraw)?.Size ?? Size.Empty; } /// void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) { - var childScene = (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]; + var childScene = TryGetChildScene(_currentDraw); if (childScene != null) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs index cd3dac699a..e81966ce81 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.VisualTree; @@ -9,14 +10,21 @@ namespace Avalonia.Rendering.SceneGraph /// internal abstract class BrushDrawOperation : DrawOperation { - public BrushDrawOperation(Rect bounds, Matrix transform) + public BrushDrawOperation(Rect bounds, Matrix transform, IDisposable? aux) : base(bounds, transform) { + Aux = aux; } /// - /// Gets a collection of child scenes that are needed to draw visual brushes. + /// Auxiliary data required to draw the brush /// - public abstract IDictionary? ChildScenes { get; } + public IDisposable? Aux { get; } + + public override void Dispose() + { + Aux?.Dispose(); + base.Dispose(); + } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 3f495c619c..07082e4ac3 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -457,7 +457,7 @@ namespace Avalonia.Rendering.SceneGraph return _drawOperationindex < _node!.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as IRef : null; } - private IDictionary? CreateChildScene(IBrush? brush) + private IDisposable? CreateChildScene(IBrush? brush) { var visualBrush = brush as VisualBrush; @@ -470,7 +470,7 @@ namespace Avalonia.Rendering.SceneGraph (visual as IVisualBrushInitialize)?.EnsureInitialized(); var scene = new Scene(visual); _sceneBuilder.UpdateAll(scene); - return new Dictionary { { visualBrush.Visual, scene } }; + return scene; } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs index c1fc6a81f6..4600653b9d 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs @@ -17,14 +17,13 @@ namespace Avalonia.Rendering.SceneGraph IBrush? brush, IPen? pen, Rect rect, - IDictionary? childScenes = null) - : base(rect.Inflate(pen?.Thickness ?? 0), transform) + IDisposable? aux = null) + : base(rect.Inflate(pen?.Thickness ?? 0), transform, aux) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; - ChildScenes = childScenes; } /// @@ -47,8 +46,6 @@ namespace Avalonia.Rendering.SceneGraph /// public Rect Rect { get; } - public override IDictionary? ChildScenes { get; } - public bool Equals(Matrix transform, IBrush? brush, IPen? pen, Rect rect) { return transform == Transform && diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index 70748989d6..4b43f93aee 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -23,14 +24,13 @@ namespace Avalonia.Rendering.SceneGraph IBrush? brush, IPen? pen, IGeometryImpl geometry, - IDictionary? childScenes = null) - : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform) + IDisposable? aux) + : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, aux) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Geometry = geometry; - ChildScenes = childScenes; } /// @@ -53,9 +53,6 @@ namespace Avalonia.Rendering.SceneGraph /// public IGeometryImpl Geometry { get; } - /// - public override IDictionary? ChildScenes { get; } - /// /// Determines if this draw operation equals another. /// diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index d6da087120..9199611ed6 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; @@ -23,13 +24,12 @@ namespace Avalonia.Rendering.SceneGraph Matrix transform, IBrush foreground, GlyphRun glyphRun, - IDictionary? childScenes = null) - : base(new Rect(glyphRun.Size), transform) + IDisposable? aux = null) + : base(new Rect(glyphRun.Size), transform, aux) { Transform = transform; Foreground = foreground.ToImmutable(); GlyphRun = glyphRun; - ChildScenes = childScenes; } /// @@ -47,9 +47,6 @@ namespace Avalonia.Rendering.SceneGraph /// public GlyphRun GlyphRun { get; } - /// - public override IDictionary? ChildScenes { get; } - /// public override void Render(IDrawingContextImpl context) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index a9e1ce8ed7..ee5ec0a5fc 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -25,14 +25,13 @@ namespace Avalonia.Rendering.SceneGraph IPen pen, Point p1, Point p2, - IDictionary? childScenes = null) - : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform) + IDisposable? aux = null) + : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform, aux) { Transform = transform; Pen = pen.ToImmutable(); P1 = p1; P2 = p2; - ChildScenes = childScenes; } /// @@ -55,9 +54,6 @@ namespace Avalonia.Rendering.SceneGraph /// public Point P2 { get; } - /// - public override IDictionary? ChildScenes { get; } - /// /// Determines if this draw operation equals another. /// diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 4b6e7d2254..549c1fd7de 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Platform; using Avalonia.VisualTree; @@ -17,12 +18,11 @@ namespace Avalonia.Rendering.SceneGraph /// The opacity mask to push. /// The bounds of the mask. /// Child scenes for drawing visual brushes. - public OpacityMaskNode(IBrush mask, Rect bounds, IDictionary? childScenes = null) - : base(Rect.Empty, Matrix.Identity) + public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) + : base(Rect.Empty, Matrix.Identity, aux) { Mask = mask.ToImmutable(); MaskBounds = bounds; - ChildScenes = childScenes; } /// @@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph /// opacity mask pop. /// public OpacityMaskNode() - : base(Rect.Empty, Matrix.Identity) + : base(Rect.Empty, Matrix.Identity, null) { } @@ -44,8 +44,6 @@ namespace Avalonia.Rendering.SceneGraph /// public Rect? MaskBounds { get; } - /// - public override IDictionary? ChildScenes { get; } /// public override bool HitTest(Point p) => false; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index 3279c3a549..7b79c446f9 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -26,14 +27,13 @@ namespace Avalonia.Rendering.SceneGraph IPen? pen, RoundedRect rect, BoxShadows boxShadows, - IDictionary? childScenes = null) - : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform) + IDisposable? aux = null) + : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, aux) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; - ChildScenes = childScenes; BoxShadows = boxShadows; } @@ -62,9 +62,6 @@ namespace Avalonia.Rendering.SceneGraph /// public BoxShadows BoxShadows { get; } - /// - public override IDictionary? ChildScenes { get; } - /// /// Determines if this draw operation equals another. /// From 7e4fa1d84b7a8eac22fa72fb2d0c9c612e743bf3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 25 May 2022 21:46:52 +0300 Subject: [PATCH 09/61] Fixed hit-testing for the first frame --- .../Composition/Server/ReadbackIndices.cs | 25 ++++++++----------- .../Server/ServerCompositionTarget.cs | 10 +++++--- .../Composition/Server/ServerVisual.cs | 3 +-- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs index 372fa4d9ce..1971451811 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs @@ -4,29 +4,24 @@ namespace Avalonia.Rendering.Composition.Server { private readonly object _lock = new object(); public int ReadIndex { get; private set; } = 0; - public int WriteIndex { get; private set; } = -1; + public int WriteIndex { get; private set; } = 1; + public int WrittenIndex { get; private set; } = 0; public ulong ReadRevision { get; private set; } - public ulong WriteRevision { get; private set; } - private ulong[] _revisions = new ulong[3]; - - + public ulong LastWrittenRevision { get; private set; } + public void NextRead() { lock (_lock) { - for (var c = 0; c < 3; c++) + if (ReadRevision < LastWrittenRevision) { - if (c != WriteIndex && c != ReadIndex && _revisions[c] > ReadRevision) - { - ReadIndex = c; - ReadRevision = _revisions[c]; - return; - } + ReadIndex = WrittenIndex; + ReadRevision = LastWrittenRevision; } } } - public void NextWrite(ulong revision) + public void CompleteWrite(ulong writtenRevision) { lock (_lock) { @@ -34,9 +29,9 @@ namespace Avalonia.Rendering.Composition.Server { if (c != WriteIndex && c != ReadIndex) { + WrittenIndex = WriteIndex; + LastWrittenRevision = writtenRevision; WriteIndex = c; - WriteRevision = revision; - _revisions[c] = revision; return; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index d8a5de4f54..a50562eabc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -15,7 +15,7 @@ namespace Avalonia.Rendering.Composition.Server private readonly Func _renderTargetFactory; private static long s_nextId = 1; public long Id { get; } - private ulong _frame = 1; + public ulong Revision { get; private set; } private IRenderTarget? _renderTarget; private FpsCounter _fpsCounter = new FpsCounter(Typeface.Default.GlyphTypeface); private Rect _dirtyRect; @@ -58,9 +58,14 @@ namespace Avalonia.Rendering.Composition.Server if(_dirtyRect.IsEmpty && !_redrawRequested) return; + + Revision++; + // Update happens in a separate phase to extend dirty rect if needed Root.Update(this, Matrix4x4.Identity); + Readback.CompleteWrite(Revision); + _redrawRequested = false; using (var targetContext = _renderTarget.CreateDrawingContext(null)) { @@ -102,9 +107,6 @@ namespace Avalonia.Rendering.Composition.Server _dirtyRect = Rect.Empty; } - - Readback.NextWrite(_frame); - _frame++; } private static Rect SnapToDevicePixels(Rect rect, double scale) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 05b63a7a73..4e320c34be 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -63,7 +63,7 @@ namespace Avalonia.Rendering.Composition.Server Scale, RotationAngle, Orientation, Offset); var i = Root!.Readback; ref var readback = ref GetReadback(i.WriteIndex); - readback.Revision = i.WriteRevision; + readback.Revision = root.Revision; readback.Matrix = res; readback.TargetId = Root.Id; //TODO: check effective opacity too @@ -85,7 +85,6 @@ namespace Avalonia.Rendering.Composition.Server public struct ReadbackData { public Matrix4x4 Matrix; - public bool Visible; public ulong Revision; public long TargetId; } From 7a9d9ea304e3e96f0b857afb9b3c14bb864fd6b9 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 25 May 2022 22:17:35 +0300 Subject: [PATCH 10/61] Invalidate visual's rect if it's moved in the global coordinate space --- .../Rendering/Composition/Server/ServerVisual.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 4e320c34be..9bfc909fe4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -69,7 +69,14 @@ namespace Avalonia.Rendering.Composition.Server //TODO: check effective opacity too IsVisibleInFrame = Visible && Opacity > 0; CombinedTransformMatrix = res; - GlobalTransformMatrix = res * transform; + var newTransform = res * transform; + if (GlobalTransformMatrix != newTransform) + { + // Visual was moved alongside with its parent + _isDirty = true; + Root.AddDirtyRect(TransformedBounds); + } + GlobalTransformMatrix = newTransform; //TODO: Cache TransformedBounds = ContentBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); From f0989357a01a1df28ad5b522aad180e08d5e539f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 15:29:56 +0300 Subject: [PATCH 11/61] Visiblity check fix --- src/Avalonia.Base/Rendering/Composition/Visual.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index f7c8078073..fa8d5d8f3b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -45,8 +45,8 @@ namespace Avalonia.Rendering.Composition var i = Root.Server.Readback; ref var readback = ref Server.GetReadback(i.ReadIndex); - // CompositionVisual wasn't visible - if (readback.Revision < i.ReadRevision) + // CompositionVisual wasn't visible or wasn't even attached to the composition target during the lat frame + if (!readback.Visible || readback.Revision < i.ReadRevision) return null; // CompositionVisual was reparented (potential race here) From f974859323c06b493226dc8d85de66349a5ac4a9 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 15:30:12 +0300 Subject: [PATCH 12/61] Change stream WIP --- .../Composition/Server/ServerVisual.cs | 51 ++++-- .../Composition/Transport/BatchStream.cs | 173 ++++++++++++++++++ .../Transport/BatchStreamArrayPool.cs | 144 +++++++++++++++ 3 files changed, 354 insertions(+), 14 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 9bfc909fe4..801bfb2f65 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -8,6 +8,7 @@ namespace Avalonia.Rendering.Composition.Server { private bool _isDirty; private ServerCompositionTarget? _root; + private bool _isBackface; protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -59,23 +60,35 @@ namespace Avalonia.Rendering.Composition.Server public virtual void Update(ServerCompositionTarget root, Matrix4x4 transform) { - var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, + // Calculate new parent-relative transform + CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, Scale, RotationAngle, Orientation, Offset); - var i = Root!.Readback; - ref var readback = ref GetReadback(i.WriteIndex); - readback.Revision = root.Revision; - readback.Matrix = res; - readback.TargetId = Root.Id; - //TODO: check effective opacity too - IsVisibleInFrame = Visible && Opacity > 0; - CombinedTransformMatrix = res; - var newTransform = res * transform; + + var newTransform = CombinedTransformMatrix * transform; + + // Check if visual was moved and recalculate face orientation + var positionChanged = false; if (GlobalTransformMatrix != newTransform) { - // Visual was moved alongside with its parent - _isDirty = true; - Root.AddDirtyRect(TransformedBounds); + _isBackface = Vector3.Transform( + new Vector3(0, 0, float.PositiveInfinity), GlobalTransformMatrix).Z <= 0; + positionChanged = true; + } + + var wasVisible = IsVisibleInFrame; + //TODO: check effective opacity too + IsVisibleInFrame = Visible && Opacity > 0 && !_isBackface; + + // Invalidate previous rect and queue new rect based on visibility + if (positionChanged) + { + if(wasVisible) + Root!.AddDirtyRect(TransformedBounds); + + if (IsVisibleInFrame) + _isDirty = true; } + GlobalTransformMatrix = newTransform; //TODO: Cache TransformedBounds = ContentBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); @@ -84,9 +97,18 @@ namespace Avalonia.Rendering.Composition.Server _isDirty = false; else if (_isDirty) { - Root.AddDirtyRect(TransformedBounds); + Root!.AddDirtyRect(TransformedBounds); _isDirty = false; } + + // Update readback indices + var i = Root!.Readback; + ref var readback = ref GetReadback(i.WriteIndex); + readback.Revision = root.Revision; + readback.Matrix = CombinedTransformMatrix; + readback.TargetId = Root.Id; + readback.Visible = IsVisibleInFrame; + } public struct ReadbackData @@ -94,6 +116,7 @@ namespace Avalonia.Rendering.Composition.Server public Matrix4x4 Matrix; public ulong Revision; public long TargetId; + public bool Visible; } partial void ApplyChangesExtra(CompositionVisualChanges c) diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs new file mode 100644 index 0000000000..feb892d134 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport; + +internal class BatchStreamData +{ + public Queue> Objects { get; } = new(); + public Queue> Structs { get; } = new(); +} + +public struct BatchStreamSegment +{ + public TData Data { get; set; } + public int ElementCount { get; set; } +} + +internal class BatchStreamWriter : IDisposable +{ + private readonly BatchStreamData _output; + private readonly BatchStreamMemoryPool _memoryPool; + private readonly BatchStreamObjectPool _objectPool; + + private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentDataSegment; + + public BatchStreamWriter(BatchStreamData output, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + { + _output = output; + _memoryPool = memoryPool; + _objectPool = objectPool; + } + + void CommitDataSegment() + { + if (_currentDataSegment.Data != IntPtr.Zero) + _output.Structs.Enqueue(_currentDataSegment); + _currentDataSegment = new (); + } + + void NextDataSegment() + { + CommitDataSegment(); + _currentDataSegment.Data = _memoryPool.Get(); + } + + void CommitObjectSegment() + { + if (_currentObjectSegment.Data != null) + _output.Objects.Enqueue(_currentObjectSegment!); + _currentObjectSegment = new(); + } + + void NextObjectSegment() + { + CommitObjectSegment(); + _currentObjectSegment.Data = _objectPool.Get(); + } + + public unsafe void Write(T item) where T : unmanaged + { + var size = Unsafe.SizeOf(); + if (_currentDataSegment.Data == IntPtr.Zero || _currentDataSegment.ElementCount + size > _memoryPool.BufferSize) + NextDataSegment(); + *(T*)((byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount) = item; + _currentDataSegment.ElementCount += size; + } + + public void Write(ServerObject item) + { + if (_currentObjectSegment.Data == null || + _currentObjectSegment.ElementCount >= _currentObjectSegment.Data.Length) + NextObjectSegment(); + _currentObjectSegment.Data![_currentObjectSegment.ElementCount] = item; + _currentObjectSegment.ElementCount++; + } + + public void Dispose() + { + CommitDataSegment(); + CommitObjectSegment(); + } +} + +internal class BatchStreamReader : IDisposable +{ + private readonly BatchStreamData _input; + private readonly BatchStreamMemoryPool _memoryPool; + private readonly BatchStreamObjectPool _objectPool; + + private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentDataSegment; + private int _memoryOffset, _objectOffset; + + public BatchStreamReader(BatchStreamData _input, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + { + this._input = _input; + _memoryPool = memoryPool; + _objectPool = objectPool; + } + + public unsafe T Read() where T : unmanaged + { + var size = Unsafe.SizeOf(); + if (_currentDataSegment.Data == IntPtr.Zero) + { + if (_input.Structs.Count == 0) + throw new EndOfStreamException(); + _currentDataSegment = _input.Structs.Dequeue(); + _memoryOffset = 0; + } + + if (_memoryOffset + size > _currentDataSegment.ElementCount) + throw new InvalidOperationException("Attempted to read more memory then left in the current segment"); + + var rv = *(T*)((byte*)_currentDataSegment.Data + size); + _memoryOffset += size; + if (_memoryOffset == _currentDataSegment.ElementCount) + { + _memoryPool.Return(_currentDataSegment.Data); + _currentDataSegment = new(); + } + + return rv; + } + + public ServerObject ReadObject() + { + if (_currentObjectSegment.Data == null) + { + if (_input.Objects.Count == 0) + throw new EndOfStreamException(); + _currentObjectSegment = _input.Objects.Dequeue()!; + _objectOffset = 0; + } + + var rv = _currentObjectSegment.Data![_objectOffset]; + _objectOffset++; + if (_objectOffset == _currentObjectSegment.ElementCount) + { + _objectPool.Return(_currentObjectSegment.Data); + _currentObjectSegment = new(); + } + + return rv; + } + + public bool IsStructEof => _currentDataSegment.Data == IntPtr.Zero && _input.Structs.Count == 0; + + public void Dispose() + { + if (_currentDataSegment.Data != IntPtr.Zero) + { + _memoryPool.Return(_currentDataSegment.Data); + _currentDataSegment = new(); + } + + while (_input.Structs.Count > 0) + _memoryPool.Return(_input.Structs.Dequeue().Data); + + if (_currentObjectSegment.Data != null) + { + _objectPool.Return(_currentObjectSegment.Data); + _currentObjectSegment = new(); + } + + while (_input.Objects.Count > 0) + _objectPool.Return(_input.Objects.Dequeue().Data); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs new file mode 100644 index 0000000000..913958765a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using Avalonia.Threading; + +namespace Avalonia.Rendering.Composition.Transport; + +/// +/// A pool that keeps a number of elements that was used in the last 10 seconds +/// +internal abstract class BatchStreamPoolBase : IDisposable +{ + readonly Stack _pool = new(); + bool _disposed; + int _usage; + readonly int[] _usageStatistics = new int[10]; + int _usageStatisticsSlot; + + public BatchStreamPoolBase(bool needsFinalize = false) + { + if(!needsFinalize) + GC.SuppressFinalize(needsFinalize); + + var updateRef = new WeakReference>(this); + StartUpdateTimer(updateRef); + } + + static void StartUpdateTimer(WeakReference> updateRef) + { + DispatcherTimer.Run(() => + { + if (updateRef.TryGetTarget(out var target)) + { + target.UpdateStatistics(); + return true; + } + return false; + + }, TimeSpan.FromSeconds(1)); + } + + private void UpdateStatistics() + { + lock (_pool) + { + var maximumUsage = _usageStatistics.Max(); + var recentlyUsedPooledSlots = maximumUsage - _usage; + while (recentlyUsedPooledSlots < _pool.Count) + DestroyItem(_pool.Pop()); + + _usageStatistics[_usage] = 0; + _usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length; + } + } + + protected abstract T CreateItem(); + + protected virtual void DestroyItem(T item) + { + + } + + public T Get() + { + lock (_pool) + { + _usage++; + if (_usageStatistics[_usageStatisticsSlot] < _usage) + _usageStatistics[_usageStatisticsSlot] = _usage; + + if (_pool.Count != 0) + return _pool.Pop(); + } + + return CreateItem(); + } + + public void Return(T item) + { + lock (_pool) + { + _usage--; + if (!_disposed) + { + _pool.Push(item); + return; + } + } + + DestroyItem(item); + } + + public void Dispose() + { + lock (_pool) + { + _disposed = true; + foreach (var item in _pool) + DestroyItem(item); + _pool.Clear(); + } + } + + ~BatchStreamPoolBase() + { + Dispose(); + } +} + +internal sealed class BatchStreamObjectPool : BatchStreamPoolBase where T : class +{ + private readonly int _arraySize; + + public BatchStreamObjectPool(int arraySize = 1024) + { + _arraySize = arraySize; + } + + protected override T[] CreateItem() + { + return new T[_arraySize]; + } + + protected override void DestroyItem(T[] item) + { + Array.Clear(item, 0, item.Length); + } +} + +internal sealed class BatchStreamMemoryPool : BatchStreamPoolBase +{ + public int BufferSize { get; } + + public BatchStreamMemoryPool(int bufferSize = 16384) + { + BufferSize = bufferSize; + } + + protected override IntPtr CreateItem() => Marshal.AllocHGlobal(BufferSize); + + protected override void DestroyItem(IntPtr item) => Marshal.FreeHGlobal(item); +} \ No newline at end of file From 4991d4f370acd7503260258722bcad328e619d65 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 21:35:50 +0300 Subject: [PATCH 13/61] Switched to byte-stream based transport --- .../Animations/AnimatedValueStore.cs | 5 +- .../Animations/CompositionAnimation.cs | 5 +- .../Animations/CompositionAnimationGroup.cs | 2 - .../Animations/ExpressionAnimation.cs | 2 +- .../Animations/ImplicitAnimationCollection.cs | 2 - .../Composition/CompositionDrawListVisual.cs | 16 +- .../Composition/CompositionEasingFunction.cs | 4 +- .../Composition/CompositionObject.cs | 57 ++- .../Composition/CompositionPropertySet.cs | 12 +- .../Composition/CompositionTarget.cs | 2 +- .../Rendering/Composition/Compositor.cs | 50 ++- .../Rendering/Composition/ContainerVisual.cs | 13 +- .../Rendering/Composition/CustomDrawVisual.cs | 56 --- .../Server/ServerCompositionDrawListVisual.cs | 10 +- .../Server/ServerCompositionTarget.cs | 4 +- .../Composition/Server/ServerCompositor.cs | 23 +- .../Server/ServerContainerVisual.cs | 9 +- .../Server/ServerCustomDrawVisual.cs | 31 -- .../Composition/Server/ServerList.cs | 24 +- .../Composition/Server/ServerObject.cs | 28 +- .../Composition/Server/ServerVisual.cs | 31 +- .../Rendering/Composition/Transport/Batch.cs | 7 +- .../Composition/Transport/BatchStream.cs | 27 +- .../Transport/BatchStreamArrayPool.cs | 24 +- .../Transport/BatchStreamDebugMarker.cs | 9 + .../Rendering/Composition/Transport/Change.cs | 82 ---- .../Composition/Transport/ChangeSet.cs | 36 -- .../Composition/Transport/ChangeSetPool.cs | 42 -- .../Transport/CompositionTargetChanges.cs | 11 - .../Transport/CustomDrawVisualChanges.cs | 20 - .../Transport/DrawListVisualChanges.cs | 48 -- .../Composition/Transport/ListChange.cs | 19 - .../Composition/Transport/ListChangeSet.cs | 25 -- .../Transport/ServerListProxyHelper.cs | 55 +-- .../Composition/Transport/VisualChanges.cs | 16 - .../Rendering/Composition/Visual.cs | 35 +- .../Rendering/Composition/VisualCollection.cs | 1 + src/Avalonia.Base/composition-schema.xml | 7 +- .../CompositionGenerator/Config.cs | 6 + .../CompositionGenerator/Extensions.cs | 9 + .../Generator.KeyFrameAnimation.cs | 2 +- .../Generator.ListProxy.cs | 12 +- .../CompositionGenerator/Generator.cs | 413 ++++++++++-------- src/Avalonia.X11/X11Platform.cs | 2 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 +- .../Composition/BatchStreamTests.cs | 45 ++ 46 files changed, 532 insertions(+), 809 deletions(-) delete mode 100644 src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/Change.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs create mode 100644 tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs index e877b50b20..95bc384743 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs @@ -1,3 +1,4 @@ +using System; using System.Runtime.InteropServices; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Server; @@ -81,7 +82,7 @@ namespace Avalonia.Rendering.Composition.Animations public bool IsAnimation => _animation != null; - public void SetAnimation(ServerObject target, ChangeSet cs, IAnimationInstance animation, int storeOffset) + public void SetAnimation(ServerObject target, TimeSpan commitedAt, IAnimationInstance animation, int storeOffset) { _direct = default; if (_animation != null) @@ -91,7 +92,7 @@ namespace Avalonia.Rendering.Composition.Animations } _animation = animation; - _animation.Initialize(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated), storeOffset); + _animation.Initialize(commitedAt, ExpressionVariant.Create(LastAnimated), storeOffset); if (target.IsActive) _animation.Activate(); diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index fe20115b38..cf81c6e656 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -18,8 +18,6 @@ namespace Avalonia.Rendering.Composition.Animations _propertySet = new CompositionPropertySet(compositor); } - private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); - public void ClearAllParameters() => _propertySet.ClearAll(); public void ClearParameter(string key) => _propertySet.Clear(key); @@ -52,8 +50,7 @@ namespace Avalonia.Rendering.Composition.Animations internal abstract IAnimationInstance CreateInstance(ServerObject targetObject, ExpressionVariant? finalValue); - internal PropertySetSnapshot CreateSnapshot(bool server) - => _propertySet.Snapshot(server); + internal PropertySetSnapshot CreateSnapshot() => _propertySet.Snapshot(); void ICompositionAnimationBase.InternalOnly() { diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs index 833f7e498c..89f8ba411d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs @@ -20,7 +20,5 @@ namespace Avalonia.Rendering.Composition.Animations public CompositionAnimationGroup(Compositor compositor) : base(compositor, null!) { } - - private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs index a6f24c2e35..6a2c07e6ef 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs @@ -29,6 +29,6 @@ namespace Avalonia.Rendering.Composition.Animations internal override IAnimationInstance CreateInstance( ServerObject targetObject, ExpressionVariant? finalValue) => new ExpressionAnimationInstance(ParsedExpression, - targetObject, finalValue, CreateSnapshot(true)); + targetObject, finalValue, CreateSnapshot()); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs index be91352527..fa5b69dae9 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs @@ -15,8 +15,6 @@ namespace Avalonia.Rendering.Composition.Animations _innerface = _inner; } - private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); - public IEnumerator> GetEnumerator() => _inner.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _inner).GetEnumerator(); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index e4ed0abd29..069d888fbb 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -9,7 +9,7 @@ internal class CompositionDrawListVisual : CompositionContainerVisual { public Visual Visual { get; } - private new DrawListVisualChanges Changes => (DrawListVisualChanges)base.Changes; + private bool _drawListChanged; private CompositionDrawList? _drawList; public CompositionDrawList? DrawList { @@ -18,11 +18,21 @@ internal class CompositionDrawListVisual : CompositionContainerVisual { _drawList?.Dispose(); _drawList = value; - Changes.DrawCommands = value?.Clone(); + _drawListChanged = true; + RegisterForSerialization(); } } - private protected override IChangeSetPool ChangeSetPool => DrawListVisualChanges.Pool; + private protected override void SerializeChangesCore(BatchStreamWriter writer) + { + writer.Write((byte)(_drawListChanged ? 1 : 0)); + if (_drawListChanged) + { + writer.WriteObject(DrawList?.Clone()); + _drawListChanged = false; + } + base.SerializeChangesCore(writer); + } internal CompositionDrawListVisual(Compositor compositor, ServerCompositionDrawListVisual server, Visual visual) : base(compositor, server) { diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs index 73db243e93..90b2bec268 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs @@ -10,9 +10,7 @@ namespace Avalonia.Rendering.Composition internal CompositionEasingFunction(Compositor compositor) : base(compositor, null!) { } - - private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); - + internal abstract IEasingFunction Snapshot(); } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs index 2417ecaba8..d561338a36 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -6,7 +6,7 @@ using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition { - public abstract class CompositionObject : IDisposable, IExpressionObject + public abstract class CompositionObject : IDisposable { public ImplicitAnimationCollection? ImplicitAnimations { get; set; } internal CompositionObject(Compositor compositor, ServerObject server) @@ -18,46 +18,17 @@ namespace Avalonia.Rendering.Composition public Compositor Compositor { get; } internal ServerObject Server { get; } public bool IsDisposed { get; private set; } - private ChangeSet? _changes; + private bool _registeredForSerialization; private static void ThrowInvalidOperation() => throw new InvalidOperationException("There is no server-side counterpart for this object"); - - private protected ChangeSet Changes - { - get - { - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (Server == null) ThrowInvalidOperation(); - var currentBatch = Compositor.CurrentBatch; - if (_changes != null && _changes.Batch != currentBatch) - _changes = null; - if (_changes == null) - { - _changes = ChangeSetPool.Get(Server!, currentBatch); - currentBatch.Changes!.Add(_changes); - Compositor.QueueImplicitBatchCommit(); - } - - return _changes; - } - } - - private protected abstract IChangeSetPool ChangeSetPool { get; } public void Dispose() { - Changes.Dispose = true; + //Changes.Dispose = true; IsDisposed = true; } - internal virtual ExpressionVariant GetPropertyForAnimation(string name) - { - return default; - } - - ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); - public void StartAnimation(string propertyName, CompositionAnimation animation) => StartAnimation(propertyName, animation, null); @@ -121,5 +92,27 @@ namespace Avalonia.Rendering.Composition throw new ArgumentException(); } + + protected void RegisterForSerialization() + { + if (Server == null) + throw new InvalidOperationException("The object doesn't have an associated server counterpart"); + + if(_registeredForSerialization) + return; + _registeredForSerialization = true; + Compositor.RegisterForSerialization(this); + } + + internal void SerializeChanges(BatchStreamWriter writer) + { + _registeredForSerialization = false; + SerializeChangesCore(writer); + } + + private protected virtual void SerializeChangesCore(BatchStreamWriter writer) + { + + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs index bc0ce804dc..584969cbc0 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -16,8 +16,6 @@ namespace Avalonia.Rendering.Composition { } - private protected override IChangeSetPool ChangeSetPool => throw new NotSupportedException(); - internal void Set(string key, ExpressionVariant value) { _objects.Remove(key); @@ -104,10 +102,10 @@ namespace Avalonia.Rendering.Composition _variants.Remove(key); } - internal PropertySetSnapshot Snapshot(bool server) => - SnapshotCore(server, 1); + internal PropertySetSnapshot Snapshot() => + SnapshotCore(1); - private PropertySetSnapshot SnapshotCore(bool server, int allowedNestingLevel) + private PropertySetSnapshot SnapshotCore(int allowedNestingLevel) { var dic = new Dictionary(_objects.Count + _variants.Count); foreach (var o in _objects) @@ -116,12 +114,12 @@ namespace Avalonia.Rendering.Composition { if (allowedNestingLevel <= 0) throw new InvalidOperationException("PropertySet depth limit reached"); - dic[o.Key] = new PropertySetSnapshot.Value(ps.SnapshotCore(server, allowedNestingLevel - 1)); + dic[o.Key] = new PropertySetSnapshot.Value(ps.SnapshotCore(allowedNestingLevel - 1)); } else if (o.Value.Server == null) throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed"); else - dic[o.Key] = new PropertySetSnapshot.Value(server ? (IExpressionObject) o.Value.Server : o.Value); + dic[o.Key] = new PropertySetSnapshot.Value(o.Value.Server); } foreach (var v in _variants) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index 8d052389c2..c5cfaeacce 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -102,6 +102,6 @@ namespace Avalonia.Rendering.Composition return false; } - public void RequestRedraw() => Changes.RedrawRequested.Value = true; + public void RequestRedraw() => RegisterForSerialization(); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 14d779dbc4..96564f0800 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -16,18 +16,17 @@ namespace Avalonia.Rendering.Composition public partial class Compositor { private ServerCompositor _server; - private Batch _currentBatch; private bool _implicitBatchCommitQueued; private Action _implicitBatchCommit; - - internal Batch CurrentBatch => _currentBatch; + private BatchStreamObjectPool _batchObjectPool = new(); + private BatchStreamMemoryPool _batchMemoryPool = new(); + private List _objectsForSerialization = new(); internal ServerCompositor Server => _server; internal CompositionEasingFunction DefaultEasing { get; } - - private Compositor(ServerCompositor server) + + public Compositor(IRenderLoop loop) { - _server = server; - _currentBatch = new Batch(); + _server = new ServerCompositor(loop, _batchObjectPool, _batchMemoryPool); _implicitBatchCommit = ImplicitBatchCommit; DefaultEasing = new CubicBezierEasingFunction(this, new Vector2(0.25f, 0.1f), new Vector2(0.25f, 1f)); @@ -40,18 +39,27 @@ namespace Avalonia.Rendering.Composition public Task RequestCommitAsync() { - var batch = CurrentBatch; - _currentBatch = new Batch(); + var batch = new Batch(); + + using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool)) + { + foreach (var obj in _objectsForSerialization) + { + writer.WriteObject(obj.Server); + obj.SerializeChanges(writer); +#if DEBUG_COMPOSITOR_SERIALIZATION + writer.Write(BatchStreamDebugMarkers.ObjectEndMagic); + writer.WriteObject(BatchStreamDebugMarkers.ObjectEndMarker); +#endif + } + _objectsForSerialization.Clear(); + } + batch.CommitedAt = Server.Clock.Elapsed; _server.EnqueueBatch(batch); return batch.Completed; } - public static Compositor Create(IRenderLoop timer) - { - return new Compositor(new ServerCompositor(timer)); - } - public void Dispose() { @@ -122,12 +130,8 @@ namespace Avalonia.Rendering.Composition public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this); public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this); - - internal CustomDrawVisual CreateCustomDrawVisual(ICustomDrawVisualRenderer renderer, - ICustomDrawVisualHitTest? hitTest = null) where T : IEquatable => - new CustomDrawVisual(this, renderer, hitTest); - - public void QueueImplicitBatchCommit() + + private void QueueImplicitBatchCommit() { if(_implicitBatchCommitQueued) return; @@ -140,5 +144,11 @@ namespace Avalonia.Rendering.Composition _implicitBatchCommitQueued = false; RequestCommitAsync(); } + + internal void RegisterForSerialization(CompositionObject compositionObject) + { + _objectsForSerialization.Add(compositionObject); + QueueImplicitBatchCommit(); + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs index f650d3e995..5b2a4be1bc 100644 --- a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs @@ -2,19 +2,20 @@ using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition { - public class CompositionContainerVisual : CompositionVisual + public partial class CompositionContainerVisual : CompositionVisual { - public CompositionVisualCollection Children { get; } - internal CompositionContainerVisual(Compositor compositor, ServerCompositionContainerVisual server) : base(compositor, server) + public CompositionVisualCollection Children { get; private set; } = null!; + + partial void InitializeDefaultsExtra() { - Children = new CompositionVisualCollection(this, server.Children); + Children = new CompositionVisualCollection(this, Server.Children); } - private protected override void OnRootChanged() + private protected override void OnRootChangedCore() { foreach (var ch in Children) ch.Root = Root; - base.OnRootChanged(); + base.OnRootChangedCore(); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs deleted file mode 100644 index 0505d6a46c..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Numerics; -using Avalonia.Platform; -using Avalonia.Rendering.Composition.Server; -using Avalonia.Rendering.Composition.Transport; - -namespace Avalonia.Rendering.Composition -{ - internal class CustomDrawVisual : CompositionContainerVisual where TData : IEquatable - { - private readonly ICustomDrawVisualHitTest? _hitTest; - - internal CustomDrawVisual(Compositor compositor, ICustomDrawVisualRenderer renderer, - ICustomDrawVisualHitTest? hitTest) : base(compositor, - new ServerCustomDrawVisual(compositor.Server, renderer)) - { - _hitTest = hitTest; - } - - private TData? _data; - - static bool Eq(TData? left, TData? right) - { - if (left == null && right == null) - return true; - if (left == null) - return false; - return left.Equals(right); - } - - public TData? Data - { - get => _data; - set - { - if (!Eq(_data, value)) - { - ((CustomDrawVisualChanges) Changes).Data.Value = value; - _data = value; - } - } - } - - private protected override IChangeSetPool ChangeSetPool => CustomDrawVisualChanges.Pool; - } - - public interface ICustomDrawVisualRenderer - { - void Render(IDrawingContextImpl canvas, TData? data); - } - - public interface ICustomDrawVisualHitTest - { - bool HitTest(TData data, Vector2 vector2); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index ba18211459..f1b5032cd3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using Avalonia.Collections.Pooled; using Avalonia.Platform; @@ -35,16 +36,15 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua } } - protected override void ApplyCore(ChangeSet changes) + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) { - var ch = (DrawListVisualChanges)changes; - if (ch.DrawCommandsIsSet) + if (reader.Read() == 1) { _renderCommands?.Dispose(); - _renderCommands = ch.AcquireDrawCommands(); + _renderCommands = reader.ReadObject(); _contentBounds = null; } - base.ApplyCore(changes); + base.DeserializeChangesCore(reader, commitedAt); } protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index a50562eabc..7567eba534 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -42,8 +42,8 @@ namespace Avalonia.Rendering.Composition.Server else _compositor.RemoveCompositionTarget(this); } - - partial void ApplyChangesExtra(CompositionTargetChanges c) + + partial void DeserializeChangesExtra(BatchStreamReader c) { _redrawRequested = true; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 5dbe9cfb17..f7de704b23 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -17,10 +17,14 @@ namespace Avalonia.Rendering.Composition.Server private List _activeTargets = new(); private HashSet _activeAnimations = new(); private List _animationsToUpdate = new(); + private BatchStreamObjectPool _batchObjectPool; + private BatchStreamMemoryPool _batchMemoryPool; - public ServerCompositor(IRenderLoop renderLoop) + public ServerCompositor(IRenderLoop renderLoop, BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) { _renderLoop = renderLoop; + _batchObjectPool = batchObjectPool; + _batchMemoryPool = batchMemoryPool; _renderLoop.Add(this); } @@ -45,14 +49,21 @@ namespace Avalonia.Rendering.Composition.Server batch = _batches.Dequeue(); } - foreach (var change in batch.Changes) + using (var stream = new BatchStreamReader(batch.Changes, _batchMemoryPool, _batchObjectPool)) { - if (change.Dispose) + while (!stream.IsObjectEof) { - //TODO + var target = (ServerObject)stream.ReadObject()!; + target.DeserializeChanges(stream, batch); +#if DEBUG_COMPOSITOR_SERIALIZATION + if (stream.ReadObject() != BatchStreamDebugMarkers.ObjectEndMarker) + throw new InvalidOperationException( + $"Object {target.GetType()} failed to deserialize properly on object stream"); + if(stream.Read() != BatchStreamDebugMarkers.ObjectEndMagic) + throw new InvalidOperationException( + $"Object {target.GetType()} failed to deserialize properly on data stream"); +#endif } - change.Target!.Apply(change); - change.Reset(); } _reusableToCompleteList.Add(batch); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs index 3f0995b257..a277450214 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -3,10 +3,9 @@ using Avalonia.Platform; namespace Avalonia.Rendering.Composition.Server { - internal class ServerCompositionContainerVisual : ServerCompositionVisual + internal partial class ServerCompositionContainerVisual : ServerCompositionVisual { - public ServerCompositionVisualCollection Children { get; } - + public ServerCompositionVisualCollection Children { get; private set; } = null!; protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -28,9 +27,9 @@ namespace Avalonia.Rendering.Composition.Server child.Update(root, GlobalTransformMatrix); } - public ServerCompositionContainerVisual(ServerCompositor compositor) : base(compositor) + partial void Initialize() { - Children = new ServerCompositionVisualCollection(compositor); + Children = new ServerCompositionVisualCollection(Compositor); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs deleted file mode 100644 index 5f3eb051a4..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Numerics; -using Avalonia.Platform; -using Avalonia.Rendering.Composition.Transport; - -namespace Avalonia.Rendering.Composition.Server -{ - class ServerCustomDrawVisual : ServerCompositionContainerVisual - { - private readonly ICustomDrawVisualRenderer _renderer; - private TData? _data; - public ServerCustomDrawVisual(ServerCompositor compositor, ICustomDrawVisualRenderer renderer) : base(compositor) - { - _renderer = renderer; - } - - protected override void ApplyCore(ChangeSet changes) - { - var c = (CustomDrawVisualChanges) changes; - if (c.Data.IsSet) - _data = c.Data.Value; - - base.ApplyCore(changes); - } - - protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) - { - _renderer.Render(canvas, _data); - base.RenderCore(canvas, transform); - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs index 09ef119e6b..4beea4715b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs @@ -7,25 +7,19 @@ namespace Avalonia.Rendering.Composition.Server class ServerList : ServerObject where T : ServerObject { public List List { get; } = new List(); - protected override void ApplyCore(ChangeSet changes) + + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) { - var c = (ListChangeSet) changes; - if (c.HasListChanges) + if (reader.Read() == 1) { - foreach (var lc in c.ListChanges) - { - if(lc.Action == ListChangeAction.Clear) - List.Clear(); - if(lc.Action == ListChangeAction.RemoveAt) - List.RemoveAt(lc.Index); - if(lc.Action == ListChangeAction.InsertAt) - List.Insert(lc.Index, lc.Added!); - if (lc.Action == ListChangeAction.ReplaceAt) - List[lc.Index] = lc.Added!; - } + List.Clear(); + var count = reader.Read(); + for (var c = 0; c < count; c++) + List.Add(reader.ReadObject()); } + base.DeserializeChangesCore(reader, commitedAt); } - + public override long LastChangedBy { get diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index 5b2f58b186..16f57d9059 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -23,18 +23,6 @@ namespace Avalonia.Rendering.Composition.Server Compositor = compositor; } - protected virtual void ApplyCore(ChangeSet changes) - { - - } - - public void Apply(ChangeSet changes) - { - ApplyCore(changes); - ValuesInvalidated(); - ItselfLastChangedBy = changes.Batch!.SequenceId; - } - public virtual ExpressionVariant GetPropertyForAnimation(string name) { return default; @@ -81,6 +69,10 @@ namespace Avalonia.Rendering.Composition.Server [MethodImpl(MethodImplOptions.AggressiveInlining)] private ref ServerObjectSubscriptionStore GetStoreFromOffset(int offset) { +#if DEBUG + if (offset == 0) + throw new InvalidOperationException(); +#endif return ref Unsafe.As(ref Unsafe.AddByteOffset(ref _activationCount, new IntPtr(offset))); } @@ -112,5 +104,17 @@ namespace Avalonia.Rendering.Composition.Server } public virtual int? GetFieldOffset(string fieldName) => null; + + protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) + { + + } + + public void DeserializeChanges(BatchStreamReader reader, Batch batch) + { + DeserializeChangesCore(reader, batch.CommitedAt); + ValuesInvalidated(); + ItselfLastChangedBy = batch.SequenceId; + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 801bfb2f65..5717ab2f8c 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -7,7 +7,6 @@ namespace Avalonia.Rendering.Composition.Server unsafe partial class ServerCompositionVisual : ServerObject { private bool _isDirty; - private ServerCompositionTarget? _root; private bool _isBackface; protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -119,28 +118,24 @@ namespace Avalonia.Rendering.Composition.Server public bool Visible; } - partial void ApplyChangesExtra(CompositionVisualChanges c) + + partial void DeserializeChangesExtra(BatchStreamReader c) { - if (c.Parent.IsSet) - Parent = c.Parent.Value; - if (c.Root.IsSet) - Root = c.Root.Value; ValuesInvalidated(); } - public ServerCompositionTarget? Root + partial void OnRootChanging() { - get => _root; - private set - { - if(_root != null) - Deactivate(); - _root = value; - if (_root != null) - Activate(); - } + if(Root != null) + Deactivate(); } - + + partial void OnRootChanged() + { + if (Root != null) + Activate(); + } + protected override void ValuesInvalidated() { _isDirty = true; @@ -149,8 +144,6 @@ namespace Avalonia.Rendering.Composition.Server else Root?.Invalidate(); } - - public ServerCompositionVisual? Parent { get; private set; } public bool IsVisibleInFrame { get; set; } public Rect TransformedBounds { get; set; } public virtual Rect ContentBounds => new Rect(0, 0, Size.X, Size.Y); diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs index 7b64c01d09..0714db5781 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs @@ -9,23 +9,22 @@ namespace Avalonia.Rendering.Composition.Transport internal class Batch { private static long _nextSequenceId = 1; - private static ConcurrentBag> _pool = new ConcurrentBag>(); + private static ConcurrentBag _pool = new(); public long SequenceId { get; } public Batch() { SequenceId = Interlocked.Increment(ref _nextSequenceId); if (!_pool.TryTake(out var lst)) - lst = new List(); + lst = new BatchStreamData(); Changes = lst; } private TaskCompletionSource _tcs = new TaskCompletionSource(); - public List Changes { get; private set; } + public BatchStreamData Changes { get; private set; } public TimeSpan CommitedAt { get; set; } public void Complete() { - Changes.Clear(); _pool.Add(Changes); Changes = null!; diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs index feb892d134..9e9ed739fb 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -2,13 +2,14 @@ using System; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; +using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Transport; internal class BatchStreamData { - public Queue> Objects { get; } = new(); + public Queue> Objects { get; } = new(); public Queue> Structs { get; } = new(); } @@ -22,12 +23,12 @@ internal class BatchStreamWriter : IDisposable { private readonly BatchStreamData _output; private readonly BatchStreamMemoryPool _memoryPool; - private readonly BatchStreamObjectPool _objectPool; + private readonly BatchStreamObjectPool _objectPool; - private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentObjectSegment; private BatchStreamSegment _currentDataSegment; - public BatchStreamWriter(BatchStreamData output, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + public BatchStreamWriter(BatchStreamData output, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) { _output = output; _memoryPool = memoryPool; @@ -69,7 +70,7 @@ internal class BatchStreamWriter : IDisposable _currentDataSegment.ElementCount += size; } - public void Write(ServerObject item) + public void WriteObject(object? item) { if (_currentObjectSegment.Data == null || _currentObjectSegment.ElementCount >= _currentObjectSegment.Data.Length) @@ -89,15 +90,15 @@ internal class BatchStreamReader : IDisposable { private readonly BatchStreamData _input; private readonly BatchStreamMemoryPool _memoryPool; - private readonly BatchStreamObjectPool _objectPool; + private readonly BatchStreamObjectPool _objectPool; - private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentObjectSegment; private BatchStreamSegment _currentDataSegment; private int _memoryOffset, _objectOffset; - public BatchStreamReader(BatchStreamData _input, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + public BatchStreamReader(BatchStreamData input, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) { - this._input = _input; + _input = input; _memoryPool = memoryPool; _objectPool = objectPool; } @@ -116,7 +117,7 @@ internal class BatchStreamReader : IDisposable if (_memoryOffset + size > _currentDataSegment.ElementCount) throw new InvalidOperationException("Attempted to read more memory then left in the current segment"); - var rv = *(T*)((byte*)_currentDataSegment.Data + size); + var rv = *(T*)((byte*)_currentDataSegment.Data + _memoryOffset); _memoryOffset += size; if (_memoryOffset == _currentDataSegment.ElementCount) { @@ -127,7 +128,9 @@ internal class BatchStreamReader : IDisposable return rv; } - public ServerObject ReadObject() + public T ReadObject() where T : class? => (T)ReadObject()!; + + public object? ReadObject() { if (_currentObjectSegment.Data == null) { @@ -148,6 +151,8 @@ internal class BatchStreamReader : IDisposable return rv; } + public bool IsObjectEof => _currentObjectSegment.Data == null && _input.Objects.Count == 0; + public bool IsStructEof => _currentDataSegment.Data == IntPtr.Zero && _input.Structs.Count == 0; public void Dispose() diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs index 913958765a..d76a9c609e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -18,27 +18,31 @@ internal abstract class BatchStreamPoolBase : IDisposable readonly int[] _usageStatistics = new int[10]; int _usageStatisticsSlot; - public BatchStreamPoolBase(bool needsFinalize = false) + public BatchStreamPoolBase(bool needsFinalize, Action>? startTimer = null) { if(!needsFinalize) GC.SuppressFinalize(needsFinalize); var updateRef = new WeakReference>(this); - StartUpdateTimer(updateRef); + StartUpdateTimer(startTimer, updateRef); } - static void StartUpdateTimer(WeakReference> updateRef) + static void StartUpdateTimer(Action>? startTimer, WeakReference> updateRef) { - DispatcherTimer.Run(() => + Func timerProc = () => { if (updateRef.TryGetTarget(out var target)) { target.UpdateStatistics(); return true; } - return false; - }, TimeSpan.FromSeconds(1)); + return false; + }; + if (startTimer != null) + startTimer(timerProc); + else + DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1)); } private void UpdateStatistics() @@ -50,7 +54,7 @@ internal abstract class BatchStreamPoolBase : IDisposable while (recentlyUsedPooledSlots < _pool.Count) DestroyItem(_pool.Pop()); - _usageStatistics[_usage] = 0; + _usageStatistics[_usageStatisticsSlot] = 0; _usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length; } } @@ -109,11 +113,11 @@ internal abstract class BatchStreamPoolBase : IDisposable } } -internal sealed class BatchStreamObjectPool : BatchStreamPoolBase where T : class +internal sealed class BatchStreamObjectPool : BatchStreamPoolBase where T : class? { private readonly int _arraySize; - public BatchStreamObjectPool(int arraySize = 1024) + public BatchStreamObjectPool(int arraySize = 1024, Action>? startTimer = null) : base(false, startTimer) { _arraySize = arraySize; } @@ -133,7 +137,7 @@ internal sealed class BatchStreamMemoryPool : BatchStreamPoolBase { public int BufferSize { get; } - public BatchStreamMemoryPool(int bufferSize = 16384) + public BatchStreamMemoryPool(int bufferSize = 16384, Action>? startTimer = null) : base(true, startTimer) { BufferSize = bufferSize; } diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs new file mode 100644 index 0000000000..7d21b03f24 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.Rendering.Composition.Transport; + +internal class BatchStreamDebugMarkers +{ + public static object ObjectEndMarker = new object(); + public static Guid ObjectEndMagic = Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs deleted file mode 100644 index cbee350ab3..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Rendering.Composition.Animations; - -namespace Avalonia.Rendering.Composition.Transport -{ - struct Change - { - private T? _value; - - public bool IsSet { get; private set; } - - public T? Value - { - get - { - if(!IsSet) - throw new InvalidOperationException(); - return _value; - } - set - { - IsSet = true; - _value = value; - } - } - - public void Reset() - { - _value = default; - IsSet = false; - } - } - - struct AnimatedChange - { - private T? _value; - private IAnimationInstance? _animation; - - public bool IsValue { get; private set; } - public bool IsAnimation { get; private set; } - - public T Value - { - get - { - if(!IsValue) - throw new InvalidOperationException(); - return _value!; - } - set - { - IsAnimation = false; - _animation = null; - IsValue = true; - _value = value; - } - } - - public IAnimationInstance Animation - { - get - { - if(!IsAnimation) - throw new InvalidOperationException(); - return _animation!; - } - set - { - IsValue = false; - _value = default; - IsAnimation = true; - _animation = value; - } - } - - public void Reset() - { - this = default; - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs deleted file mode 100644 index 898885dce6..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - internal abstract class ChangeSet - { - private readonly IChangeSetPool _pool; - public Batch Batch = null!; - public ServerObject? Target; - public bool Dispose; - - public ChangeSet(IChangeSetPool pool) - { - _pool = pool; - } - - public virtual void Reset() - { - Batch = null!; - Target = null; - Dispose = false; - } - - public void Return() - { - _pool.Return(this); - } - } - - internal class CompositionObjectChanges : ChangeSet - { - public CompositionObjectChanges(IChangeSetPool pool) : base(pool) - { - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs deleted file mode 100644 index ea97cd7d44..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Concurrent; -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - interface IChangeSetPool - { - void Return(ChangeSet changes); - ChangeSet Get(ServerObject target, Batch batch); - } - - class ChangeSetPool : IChangeSetPool where T : ChangeSet - { - private readonly Func _factory; - private readonly ConcurrentBag _pool = new ConcurrentBag(); - - public ChangeSetPool(Func factory) - { - _factory = factory; - } - - public void Return(T changes) - { - changes.Reset(); - _pool.Add(changes); - } - - void IChangeSetPool.Return(ChangeSet changes) => Return((T) changes); - ChangeSet IChangeSetPool.Get(ServerObject target, Batch batch) => Get(target, batch); - - public T Get(ServerObject target, Batch batch) - { - if (!_pool.TryTake(out var res)) - res = _factory(this); - res.Target = target; - res.Batch = batch; - res.Dispose = false; - return res; - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs deleted file mode 100644 index 014adc7bbe..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Avalonia.Rendering.Composition.Transport; - -partial class CompositionTargetChanges -{ - public Change RedrawRequested; - - partial void ResetExtra() - { - RedrawRequested.Reset(); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs deleted file mode 100644 index aed041b62e..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Avalonia.Rendering.Composition.Transport -{ - class CustomDrawVisualChanges : CompositionVisualChanges - { - public CustomDrawVisualChanges(IChangeSetPool pool) : base(pool) - { - } - - public Change Data; - - public override void Reset() - { - Data.Reset(); - base.Reset(); - } - - public new static ChangeSetPool> Pool { get; } = - new ChangeSetPool>(pool => new CustomDrawVisualChanges(pool)); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs deleted file mode 100644 index 215c03b229..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Collections.Pooled; -using Avalonia.Rendering.Composition.Drawing; -using Avalonia.Rendering.SceneGraph; -using Avalonia.Utilities; - -namespace Avalonia.Rendering.Composition.Transport; - -internal class DrawListVisualChanges : CompositionVisualChanges -{ - private CompositionDrawList? _drawCommands; - - public DrawListVisualChanges(IChangeSetPool pool) : base(pool) - { - } - - public CompositionDrawList? DrawCommands - { - get => _drawCommands; - set - { - _drawCommands?.Dispose(); - _drawCommands = value; - DrawCommandsIsSet = true; - } - } - - public bool DrawCommandsIsSet { get; private set; } - - public CompositionDrawList? AcquireDrawCommands() - { - var rv = _drawCommands; - _drawCommands = null; - DrawCommandsIsSet = false; - return rv; - } - - public override void Reset() - { - _drawCommands?.Dispose(); - _drawCommands = null; - DrawCommandsIsSet = false; - base.Reset(); - } - - public new static ChangeSetPool Pool { get; } = - new ChangeSetPool(pool => new(pool)); -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs deleted file mode 100644 index ee6e4231f8..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - internal class ListChange where T : ServerObject - { - public int Index; - public ListChangeAction Action; - public T? Added; - } - - internal enum ListChangeAction - { - InsertAt, - RemoveAt, - Clear, - ReplaceAt - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs deleted file mode 100644 index 9bb101a080..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - class ListChangeSet : ChangeSet where T : ServerObject - { - private List>? _listChanges; - public List> ListChanges => _listChanges ??= new List>(); - public bool HasListChanges => _listChanges != null; - - public override void Reset() - { - _listChanges?.Clear(); - base.Reset(); - } - - public ListChangeSet(IChangeSetPool pool) : base(pool) - { - } - - public static readonly ChangeSetPool> Pool = - new ChangeSetPool>(pool => new ListChangeSet(pool)); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs index 1add3aa990..2399bd71d7 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs @@ -8,19 +8,21 @@ namespace Avalonia.Rendering.Composition.Transport where TServer : ServerObject where TClient : CompositionObject { - private readonly IGetChanges _parent; - private readonly List _list = new List(); + private readonly IRegisterForSerialization _parent; + private bool _changed; - public interface IGetChanges + public interface IRegisterForSerialization { - ListChangeSet GetChanges(); + void RegisterForSerialization(); } - public ServerListProxyHelper(IGetChanges parent) + public ServerListProxyHelper(IRegisterForSerialization parent) { _parent = parent; } - + + private readonly List _list = new List(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public List.Enumerator GetEnumerator() => _list.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -30,10 +32,8 @@ namespace Avalonia.Rendering.Composition.Transport public void Clear() { _list.Clear(); - _parent.GetChanges().ListChanges.Add(new ListChange - { - Action = ListChangeAction.Clear - }); + _changed = true; + _parent.RegisterForSerialization(); } public bool Contains(TClient item) => _list.Contains(item); @@ -56,22 +56,15 @@ namespace Avalonia.Rendering.Composition.Transport public void Insert(int index, TClient item) { _list.Insert(index, item); - _parent.GetChanges().ListChanges.Add(new ListChange - { - Action = ListChangeAction.InsertAt, - Index = index, - Added = (TServer) item.Server - }); + _changed = true; + _parent.RegisterForSerialization(); } public void RemoveAt(int index) { _list.RemoveAt(index); - _parent.GetChanges().ListChanges.Add(new ListChange - { - Action = ListChangeAction.RemoveAt, - Index = index - }); + _changed = true; + _parent.RegisterForSerialization(); } public TClient this[int index] @@ -80,13 +73,21 @@ namespace Avalonia.Rendering.Composition.Transport set { _list[index] = value; - _parent.GetChanges().ListChanges.Add(new ListChange - { - Action = ListChangeAction.ReplaceAt, - Index = index, - Added = (TServer) value.Server - }); + _changed = true; + _parent.RegisterForSerialization(); + } + } + + public void Serialize(BatchStreamWriter writer) + { + writer.Write((byte)(_changed ? 1 : 0)); + if (_changed) + { + writer.Write(_list.Count); + foreach (var el in _list) + writer.WriteObject(el.Server); } + _changed = false; } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs deleted file mode 100644 index c87fb96967..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - partial class CompositionVisualChanges - { - public Change Parent; - public Change Root; - - partial void ResetExtra() - { - Parent.Reset(); - Root.Reset(); - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index fa8d5d8f3b..5bf5dcee74 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -4,39 +4,14 @@ namespace Avalonia.Rendering.Composition { public abstract partial class CompositionVisual { - private CompositionVisual? _parent; - private CompositionTarget? _root; - - public CompositionVisual? Parent - { - get => _parent; - internal set - { - if (_parent == value) - return; - _parent = value; - Changes.Parent.Value = value?.Server; - Root = _parent?.Root; - } - } - - // TODO: hide behind private-ish API - public CompositionTarget? Root + private protected virtual void OnRootChangedCore() { - get => _root; - internal set - { - var changed = _root != value; - _root = value; - Changes.Root.Value = value?.Server; - if (changed) - OnRootChanged(); - } } - private protected virtual void OnRootChanged() - { - } + partial void OnRootChanged() => OnRootChangedCore(); + + partial void OnParentChanged() => Root = Parent?.Root; + internal Matrix4x4? TryGetServerTransform() { diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs index fef4caf675..42226a8b4d 100644 --- a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -64,6 +64,7 @@ namespace Avalonia.Rendering.Composition { if (item.Parent != null) throw new InvalidOperationException("Visual already has a parent"); + item.Parent = item; } } } \ No newline at end of file diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index eb1ffe1922..a7ae341bb3 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -4,10 +4,12 @@ Avalonia.Rendering.Composition.Server Avalonia.Rendering.Composition.Transport Avalonia.Rendering.Composition.Animations - - + + + + @@ -21,6 +23,7 @@ + diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs index 8b6aca33cd..096864e52a 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs @@ -35,6 +35,10 @@ namespace Avalonia.SourceGenerator.CompositionGenerator [XmlAttribute] public string Name { get; set; } + + [XmlAttribute] + public bool Passthrough { get; set; } + [XmlAttribute] public string ServerName { get; set; } } @@ -104,6 +108,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator public string DefaultValue { get; set; } [XmlAttribute] public bool Animated { get; set; } + [XmlAttribute] + public bool InternalSet { get; set; } } public class GAnimationType diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs index 43a4a4afa7..d88e9b4600 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs @@ -41,6 +41,15 @@ namespace Avalonia.SourceGenerator.CompositionGenerator return cl; return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); } + + public static EnumDeclarationSyntax AddModifiers(this EnumDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static string WithLowerFirst(this string s) { diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs index 7d5146c5f5..314ac1acbf 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs @@ -25,7 +25,7 @@ namespace Avalonia.Rendering.Composition internal override IAnimationInstance CreateInstance(Avalonia.Rendering.Composition.Server.ServerObject targetObject, ExpressionVariant? finalValue) {{ - return new KeyFrameAnimationInstance<{a.Type}>({name}Interpolator.Instance, _keyFrames.Snapshot(), CreateSnapshot(true), + return new KeyFrameAnimationInstance<{a.Type}>({name}Interpolator.Instance, _keyFrames.Snapshot(), CreateSnapshot(), finalValue?.CastOrDefault<{a.Type}>(), targetObject, DelayBehavior, DelayTime, Direction, Duration, IterationBehavior, IterationCount, StopBehavior); diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs index 593386f713..e0ea5b20ae 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs @@ -11,9 +11,7 @@ class Template { private ServerListProxyHelper _list = null!; - ListChangeSet - ServerListProxyHelper.IGetChanges. - GetChanges() => Changes; + void ServerListProxyHelper.IRegisterForSerialization.RegisterForSerialization() => RegisterForSerialization(); public List.Enumerator GetEnumerator() => _list.GetEnumerator(); @@ -87,7 +85,11 @@ class Template partial void OnBeforeReplace(ItemTypeName oldItem, ItemTypeName newItem); partial void OnReplace(ItemTypeName oldItem, ItemTypeName newItem); partial void OnClear(); -} + private protected override void SerializeChangesCore(BatchStreamWriter writer) + {{ + _list.Serialize(writer); + base.SerializeChangesCore(writer); + }} "; private ClassDeclarationSyntax AppendListProxy(GList list, ClassDeclarationSyntax cl) @@ -97,7 +99,7 @@ class Template var serverItemType = ServerName(itemType); cl = cl.AddBaseListTypes(SimpleBaseType( - ParseTypeName("ServerListProxyHelper<" + itemType + ", " + serverItemType + ">.IGetChanges")), + ParseTypeName("ServerListProxyHelper<" + itemType + ", " + serverItemType + ">.IRegisterForSerialization")), SimpleBaseType(ParseTypeName("IList<" + itemType + ">")) ); var code = ListProxyTemplate.Replace("ListTypeName", list.Name) diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs index 5a514a4eff..3c38c0331e 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; @@ -38,7 +39,12 @@ namespace Avalonia.SourceGenerator.CompositionGenerator string ServerName(string c) => c != null ? ("Server" + c) : "ServerObject"; string ChangesName(string c) => c != null ? (c + "Changes") : "ChangeSet"; - + string ChangedFieldsTypeName(GClass c) => c.Name + "ChangedFields"; + string ChangedFieldsFieldName(GClass c) => "_changedFieldsOf" + c.Name; + string PropertyBackingFieldName(GProperty prop) => "_" + prop.Name.WithLowerFirst(); + string ServerPropertyOffsetFieldName(GProperty prop) => "s_OffsetOf" + PropertyBackingFieldName(prop); + string PropertyPendingAnimationFieldName(GProperty prop) => "_pendingAnimationFor" + prop.Name; + void GenerateClass(GClass cl) { var list = cl as GList; @@ -68,36 +74,13 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .WithBaseType(serverBase); string changesName = ChangesName(cl.Name); - var changesBase = ChangesName(cl.ChangesBase ?? cl.Inherits); + string changedFieldsTypeName = ChangedFieldsTypeName(cl); + string changedFieldsName = ChangedFieldsFieldName(cl); - if (list != null) - changesBase = "ListChangeSet<" + ServerName(list.ItemType) + ">"; - - var changeSetPoolType = "ChangeSetPool<" + changesName + ">"; - var transport = ClassDeclaration(changesName) - .AddModifiers(SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) - .WithBaseType(changesBase) - .AddMembers(DeclareField(changeSetPoolType, "Pool", - EqualsValueClause( - ParseExpression($"new {changeSetPoolType}(pool => new {changesName}(pool))") - ), - SyntaxKind.PublicKeyword, - SyntaxKind.StaticKeyword, SyntaxKind.ReadOnlyKeyword)) - .AddMembers(ParseMemberDeclaration($"public {changesName}(IChangeSetPool pool) : base(pool){{}}")); - - client = client - .AddMembers( - PropertyDeclaration(ParseTypeName("IChangeSetPool"), "ChangeSetPool") - .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.ProtectedKeyword, - SyntaxKind.OverrideKeyword) - .WithExpressionBody( - ArrowExpressionClause(MemberAccess(changesName, "Pool"))) - .WithSemicolonToken(Semicolon())) - .AddMembers(PropertyDeclaration(ParseTypeName(changesName), "Changes") - .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.NewKeyword) - .WithExpressionBody(ArrowExpressionClause(CastExpression(ParseTypeName(changesName), - MemberAccess(BaseExpression(), "Changes")))) - .WithSemicolonToken(Semicolon())); + if (cl.Properties.Count > 0) + client = client + .AddMembers(DeclareField(changedFieldsTypeName, changedFieldsName)); + if (!cl.CustomCtor) { @@ -105,7 +88,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .AddModifiers(SyntaxKind.InternalKeyword, SyntaxKind.NewKeyword) .AddAccessorListAccessors(AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithSemicolonToken(Semicolon()))); - client = client.AddMembers( + client = client.AddMembers( ConstructorDeclaration(cl.Name) .AddModifiers(SyntaxKind.InternalKeyword) .WithParameterList(ParameterList(SeparatedList(new[] @@ -141,20 +124,20 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ArgumentList(SeparatedList(new[] { Argument(IdentifierName("compositor")), - })))).WithBody(Block())); + })))).WithBody(Block(ParseStatement("Initialize();")))); } + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "Initialize") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + var changesVarName = "c"; var changesVar = IdentifierName(changesVarName); server = server.AddMembers( - MethodDeclaration(ParseTypeName("void"), "ApplyChangesExtra") - .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName(changesName))) - .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); - - transport = transport.AddMembers( - MethodDeclaration(ParseTypeName("void"), "ResetExtra") + MethodDeclaration(ParseTypeName("void"), "DeserializeChangesExtra") + .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName("BatchStreamReader"))) .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); var applyMethodBody = Block( @@ -168,7 +151,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ExpressionStatement(InvocationExpression(IdentifierName("ApplyChangesExtra")) .AddArgumentListArguments(Argument(IdentifierName("c")))) ); - + var uninitializedObjectName = "dummy"; var serverStaticCtorBody = cl.Abstract ? Block() @@ -179,72 +162,51 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ParseStatement("InitializeFieldOffsets(dummy);") ); - var initializeFieldOffsetsBody = cl.ServerBase == null + var initializeFieldOffsetsBody = cl.Inherits == null ? Block() - : Block(ParseStatement($"{cl.ServerBase}.InitializeFieldOffsets(dummy);")); + : Block(ParseStatement($"Server{cl.Inherits}.InitializeFieldOffsets(dummy);")); var resetBody = Block(); var startAnimationBody = Block(); - var getPropertyBody = Block(); var serverGetPropertyBody = Block(); var serverGetFieldOffsetBody = Block(); var activatedBody = Block(ParseStatement("base.Activated();")); var deactivatedBody = Block(ParseStatement("base.Deactivated();")); + var serializeMethodBody = SerializeChangesPrologue(cl); + var deserializeMethodBody = DeserializeChangesPrologue(cl); - var defaultsMethodBody = Block(); + var defaultsMethodBody = Block(ParseStatement("InitializeDefaultsExtra();")); foreach (var prop in cl.Properties) { - var fieldName = "_" + prop.Name.WithLowerFirst(); - var fieldOffsetName = "s_OffsetOf" + fieldName; + var fieldName = PropertyBackingFieldName(prop); + var animatedFieldName = PropertyPendingAnimationFieldName(prop); + var fieldOffsetName = ServerPropertyOffsetFieldName(prop); var propType = ParseTypeName(prop.Type); var filteredPropertyType = prop.Type.TrimEnd('?'); var isObject = _objects.Contains(filteredPropertyType); var isNullable = prop.Type.EndsWith("?"); - - - - - client = client - .AddMembers(DeclareField(prop.Type, fieldName)) - .AddMembers(PropertyDeclaration(propType, prop.Name) - .AddModifiers(SyntaxKind.PublicKeyword) - .AddAccessorListAccessors( - AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, - Block(ReturnStatement(IdentifierName(fieldName)))), - AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, - Block( - ParseStatement("var changed = false;"), - IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, - IdentifierName(fieldName), - IdentifierName("value")), - Block( - ParseStatement("On" + prop.Name + "Changing();"), - ParseStatement("changed = true;"), - GeneratePropertySetterAssignment(prop, fieldName, isObject, isNullable)) - ), - ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, - IdentifierName(fieldName), IdentifierName("value"))), - ParseStatement($"if(changed) On" + prop.Name + "Changed();") - )) - )) - .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") - .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) - .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") - .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + bool isPassthrough = false; + if (prop.Animated) + client = client.AddMembers(DeclareField("IAnimationInstance?", animatedFieldName)); + client = GenerateClientProperty(client, cl, prop, propType, isObject, isNullable); var animatedServer = prop.Animated; var serverPropertyType = ((isObject ? "Server" : "") + prop.Type); - if (_manuals.TryGetValue(filteredPropertyType, out var manual) && manual.ServerName != null) - serverPropertyType = manual.ServerName + (isNullable ? "?" : ""); - - - transport = transport - .AddMembers(DeclareField((animatedServer ? "Animated" : "") + "Change<" + serverPropertyType + ">", - prop.Name, SyntaxKind.PublicKeyword)); + if (_manuals.TryGetValue(filteredPropertyType, out var manual)) + { + if (manual.Passthrough) + { + isPassthrough = true; + serverPropertyType = prop.Type; + } + if (manual.ServerName != null) + serverPropertyType = manual.ServerName + (isNullable ? "?" : ""); + } + if (animatedServer) server = server.AddMembers( DeclareField("ServerAnimatedValueStore<" + serverPropertyType + ">", fieldName), @@ -301,7 +263,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ArgumentList(SeparatedList(new[] { Argument(IdentifierName("this")), - Argument(changesVar), + Argument(ParseExpression("c.Batch.CommitedAt")), Argument(MemberAccess(changesVar, prop.Name, "Animation")), Argument(IdentifierName(fieldOffsetName)) }))))) @@ -316,16 +278,18 @@ namespace Avalonia.SourceGenerator.CompositionGenerator resetBody = resetBody.AddStatements( ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); - + + serializeMethodBody = ApplySerializeField(serializeMethodBody,cl, prop, isObject, isPassthrough); + deserializeMethodBody = ApplyDeserializeField(deserializeMethodBody,cl, prop, serverPropertyType, isObject); + if (animatedServer) { - startAnimationBody = ApplyStartAnimation(startAnimationBody, prop, fieldName); + startAnimationBody = ApplyStartAnimation(startAnimationBody, cl, prop); activatedBody = activatedBody.AddStatements(ParseStatement($"{fieldName}.Activate(this);")); deactivatedBody = deactivatedBody.AddStatements(ParseStatement($"{fieldName}.Deactivate(this);")); } - - getPropertyBody = ApplyGetProperty(getPropertyBody, prop); + serverGetPropertyBody = ApplyGetProperty(serverGetPropertyBody, prop); serverGetFieldOffsetBody = ApplyGetProperty(serverGetFieldOffsetBody, prop, fieldOffsetName); @@ -345,55 +309,6 @@ namespace Avalonia.SourceGenerator.CompositionGenerator } } - if (cl is GBrush brush && !cl.Abstract) - { - var brushName = brush.Name.StripPrefix("Composition"); - /* - server = server.AddMembers( - MethodDeclaration(ParseTypeName("ICbBrush"), "CreateBackendBrush") - .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) - .WithExpressionBody(ArrowExpressionClause( - InvocationExpression(MemberAccess("Compositor", "Backend", "Create" + brushName)) - )).WithSemicolonToken(Semicolon()) - ); - if (!brush.CustomUpdate) - server = server.AddMembers( - MethodDeclaration(ParseTypeName("void"), "UpdateBackendBrush") - .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) - .AddParameterListParameters(Parameter(Identifier("brush")) - .WithType(ParseTypeName("ICbBrush"))) - .AddBodyStatements( - ExpressionStatement( - InvocationExpression( - MemberAccess( - ParenthesizedExpression( - CastExpression(ParseTypeName("ICb" + brushName), IdentifierName("brush"))), "Update"), - ArgumentList(SeparatedList(cl.Properties.Select(x => - { - if(x.Type.TrimEnd('?') == "ICompositionSurface") - return Argument( - ConditionalAccessExpression(IdentifierName(x.Name), - MemberBindingExpression(IdentifierName("BackendSurface"))) - ); - if (_brushes.Contains(x.Type)) - return Argument( - ConditionalAccessExpression(IdentifierName(x.Name), - MemberBindingExpression(IdentifierName("Brush"))) - ); - return Argument(IdentifierName(x.Name)); - })))) - ))); - -*/ - } - - server = server.AddMembers( - MethodDeclaration(ParseTypeName("void"), "ApplyCore") - .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) - .AddParameterListParameters( - Parameter(Identifier("changes")).WithType(ParseTypeName("ChangeSet"))) - .WithBody(applyMethodBody)); - server = server.AddMembers(ConstructorDeclaration(serverName) .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) .WithBody(serverStaticCtorBody)); @@ -408,26 +323,32 @@ namespace Avalonia.SourceGenerator.CompositionGenerator $"protected override void Activated(){{}}")!).WithBody(activatedBody)) .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( $"protected override void Deactivated(){{}}")!).WithBody(deactivatedBody)); + if (cl.Properties.Count > 0) + server = server.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt){{}}") + !) + .WithBody(deserializeMethodBody)); client = client.AddMembers( - MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)); - - transport = transport.AddMembers(MethodDeclaration(ParseTypeName("void"), "Reset") - .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword) - .WithBody(resetBody.AddStatements( - ExpressionStatement(InvocationExpression(IdentifierName("ResetExtra"))), - ExpressionStatement(InvocationExpression(MemberAccess("base", "Reset")))))); - + MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)) + .AddMembers( + MethodDeclaration(ParseTypeName("void"), "InitializeDefaultsExtra") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + if (cl.Properties.Count > 0) + client = client.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"private protected override void SerializeChangesCore(BatchStreamWriter writer){{}}")!) + .WithBody(serializeMethodBody)); + if (list != null) client = AppendListProxy(list, client); if (startAnimationBody.Statements.Count != 0) client = WithStartAnimation(client, startAnimationBody); - - client = WithGetProperty(client, getPropertyBody, false); - server = WithGetProperty(server, serverGetPropertyBody, true); + + server = WithGetPropertyForAnimation(server, serverGetPropertyBody); server = WithGetFieldOffset(server, serverGetFieldOffsetBody); - + if(cl.Implements.Count > 0) foreach (var impl in cl.Implements) { @@ -441,57 +362,121 @@ namespace Avalonia.SourceGenerator.CompositionGenerator } + SaveTo(unit.AddMembers(GenerateChangedFieldsEnum(cl)), "Transport", + ChangedFieldsTypeName(cl) + ".generated.cs"); + SaveTo(unit.AddMembers(clientNs.AddMembers(client)), cl.Name + ".generated.cs"); SaveTo(unit.AddMembers(serverNs.AddMembers(server)), "Server", "Server" + cl.Name + ".generated.cs"); - SaveTo(unit.AddMembers(transportNs.AddMembers(transport)), - "Transport", cl.Name + "Changes.generated.cs"); + } + + private ClassDeclarationSyntax GenerateClientProperty(ClassDeclarationSyntax client, GClass cl, GProperty prop, + TypeSyntax propType, bool isObject, bool isNullable) + { + var fieldName = PropertyBackingFieldName(prop); + return client + .AddMembers(DeclareField(prop.Type, fieldName)) + .AddMembers(PropertyDeclaration(propType, prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, + Block(ReturnStatement(IdentifierName(fieldName)))), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, + Block( + ParseStatement("var changed = false;"), + IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, + IdentifierName(fieldName), + IdentifierName("value")), + Block( + ParseStatement("On" + prop.Name + "Changing();"), + ParseStatement("changed = true;"), + GeneratePropertySetterAssignment(cl, prop, isObject, isNullable)) + ), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), IdentifierName("value"))), + ParseStatement($"if(changed) On" + prop.Name + "Changed();") + )).WithModifiers(TokenList(prop.InternalSet ? new[]{Token(SyntaxKind.InternalKeyword)} : Array.Empty())) + )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); } - StatementSyntax GeneratePropertySetterAssignment(GProperty prop, string fieldName, bool isObject, bool isNullable) + EnumDeclarationSyntax GenerateChangedFieldsEnum(GClass cl) { - var normalChangesAssignment = (StatementSyntax)ExpressionStatement(AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - MemberAccess((ExpressionSyntax) IdentifierName("Changes"), prop.Name, - "Value"), - isObject - ? - ConditionalMemberAccess(IdentifierName("value"), "Server", isNullable) - : IdentifierName("value"))); - if (!prop.Animated) - return normalChangesAssignment; + var changedFieldsEnum = EnumDeclaration(Identifier(ChangedFieldsTypeName(cl))); + int count = 0; - var code = $@" -{{ - if(animation is CompositionAnimation a) - Changes.{prop.Name}.Animation = a.CreateInstance(this.Server, value); - else + void AddValue(string name) + { + var value = 1ul << count; + changedFieldsEnum = changedFieldsEnum.AddMembers( + EnumMemberDeclaration(name) + .WithEqualsValue(EqualsValueClause(ParseExpression(value.ToString())))); + count++; + } + + foreach (var prop in cl.Properties) + { + AddValue(prop.Name); + + if (prop.Animated) + AddValue(prop.Name + "Animated"); + } + + var baseType = count <= 8 ? "byte" : count <= 16 ? "ushort" : count <= 32 ? "uint" : "ulong"; + return changedFieldsEnum.AddBaseListTypes(SimpleBaseType(ParseTypeName(baseType))) + .AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("System.Flags"))))); + } + + StatementSyntax GeneratePropertySetterAssignment(GClass cl, GProperty prop, bool isObject, bool isNullable) + { + var pendingAnimationField = PropertyPendingAnimationFieldName(prop); + + var code = @$" + // Update the backing value + {PropertyBackingFieldName(prop)} = value; + + // Register object for serialization in the next batch + {ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}; + RegisterForSerialization(); +"; + if (prop.Animated) + { + code += @$" + // Reset previous animation if any + {pendingAnimationField} = null; + {ChangedFieldsFieldName(cl)} &= ~{ChangedFieldsTypeName(cl)}.{prop.Name}Animated; + // Check for implicit animations + if(ImplicitAnimations != null && ImplicitAnimations.TryGetValue(""{prop.Name}"", out var animation) == true) {{ - var saved = Changes.{prop.Name}; - if(!StartAnimationGroup(animation, ""{prop.Name}"", value)) - Changes.{prop.Name}.Value = value; + // Animation affects only current property + if(animation is CompositionAnimation a) + {{ + {ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated; + {pendingAnimationField} = a.CreateInstance(this.Server, value); + }} + // Animation is triggered by the current field, but does not necessary affects it + StartAnimationGroup(animation, ""{prop.Name}"", value); }} -}} - "; - - return IfStatement( - ParseExpression( - $"ImplicitAnimations != null && ImplicitAnimations.TryGetValue(\"{prop.Name}\", out var animation) == true"), - ParseStatement(code), - ElseClause(normalChangesAssignment) - ); + } + + return ParseStatement("{\n" + code + "\n}"); } - BlockSyntax ApplyStartAnimation(BlockSyntax body, GProperty prop, string fieldName) + BlockSyntax ApplyStartAnimation(BlockSyntax body, GClass cl, GProperty prop) { var code = $@" if (propertyName == ""{prop.Name}"") {{ -var current = {fieldName}; +var current = {PropertyBackingFieldName(prop)}; var server = animation.CreateInstance(this.Server, finalValue); -Changes.{prop.Name}.Animation = server; +{PropertyPendingAnimationFieldName(prop)} = server; +{ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated; +RegisterForSerialization(); return; }} "; @@ -522,15 +507,75 @@ return; return body; } + + private BlockSyntax SerializeChangesPrologue(GClass cl) + { + return Block( + ParseStatement("base.SerializeChangesCore(writer);"), + ParseStatement($"writer.Write({ChangedFieldsFieldName(cl)});") + ); + } + + BlockSyntax ApplySerializeField(BlockSyntax body, GClass cl, GProperty prop, bool isObject, bool isPassthrough) + { + var changedFields = ChangedFieldsFieldName(cl); + var changedFieldsType = ChangedFieldsTypeName(cl); + + var code = ""; + if (prop.Animated) + { + code = $@" + if(({changedFields} & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) + writer.WriteObject({PropertyPendingAnimationFieldName(prop)}); + else "; + } + + code += $@" + if(({changedFields} & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name}) + writer.Write{(isObject ? "Object" : "")}({PropertyBackingFieldName(prop)}{(isObject && !isPassthrough ? "?.Server!":"")}); +"; + return body.AddStatements(ParseStatement(code)); + } + + private BlockSyntax DeserializeChangesPrologue(GClass cl) + { + return Block(ParseStatement($@" +base.DeserializeChangesCore(reader, commitedAt); +DeserializeChangesExtra(reader); +var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); +")); + } + + BlockSyntax ApplyDeserializeField(BlockSyntax body, GClass cl, GProperty prop, string serverType, bool isObject) + { + var changedFieldsType = ChangedFieldsTypeName(cl); + var code = ""; + if (prop.Animated) + { + code = $@" + if((changed & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) + {PropertyBackingFieldName(prop)}.SetAnimation(this, commitedAt, reader.ReadObject(), {ServerPropertyOffsetFieldName(prop)}); + else "; + } + + var readValueCode = $"reader.Read{(isObject ? "Object" : "")}<{serverType}>()"; + code += $@" + if((changed & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name}) +"; + if (prop.Animated) + code += $"{PropertyBackingFieldName(prop)}.SetValue(this, {readValueCode});"; + else code += $"{prop.Name} = {readValueCode};"; + return body.AddStatements(ParseStatement(code)); + } - ClassDeclarationSyntax WithGetProperty(ClassDeclarationSyntax cl, BlockSyntax body, bool server) + ClassDeclarationSyntax WithGetPropertyForAnimation(ClassDeclarationSyntax cl, BlockSyntax body) { if (body.Statements.Count == 0) return cl; body = body.AddStatements( ParseStatement("return base.GetPropertyForAnimation(name);")); var method = ((MethodDeclarationSyntax) ParseMemberDeclaration( - $"{(server ? "public" : "internal")} override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")) + $"public override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")) .WithBody(body); return cl.AddMembers(method); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 7a64b39575..9c82288c8e 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -104,7 +104,7 @@ namespace Avalonia.X11 } if (options.UseCompositor) - Compositor = Compositor.Create(AvaloniaLocator.Current.GetService()!); + Compositor = new Compositor(AvaloniaLocator.Current.GetService()!); } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 32705a2cc6..4b0350f40f 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -188,7 +188,7 @@ namespace Avalonia.Win32 AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); if (Options.UseCompositor) - Compositor = Compositor.Create(AvaloniaLocator.Current.GetRequiredService()); + Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService()); } public bool HasMessages() diff --git a/tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs b/tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs new file mode 100644 index 0000000000..a1b55257e6 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks.Dataflow; +using Avalonia.Rendering.Composition.Transport; +using Xunit; + +namespace Avalonia.Base.UnitTests.Composition; + +public class BatchStreamTests +{ + [Fact] + public void BatchStreamCorrectlyWritesAndReadsData() + { + var data = new BatchStreamData(); + var memPool = new BatchStreamMemoryPool(100, _ => { }); + var objPool = new BatchStreamObjectPool(10, _ => { }); + + var guids = new List(); + var objects = new List(); + for (var c = 0; c < 453; c++) + { + guids.Add(Guid.NewGuid()); + objects.Add(new object()); + } + + using (var writer = new BatchStreamWriter(data, memPool, objPool)) + { + foreach(var guid in guids) + writer.Write(guid); + foreach (var obj in objects) + writer.WriteObject(obj); + } + + using (var reader = new BatchStreamReader(data, memPool, objPool)) + { + foreach (var guid in guids) + Assert.Equal(guid, reader.Read()); + foreach (var obj in objects) + Assert.Equal(obj, reader.ReadObject()); + } + + + + } +} \ No newline at end of file From 2d8be0dff68dc4036bc6e8b5c40894f0367e6271 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 21:51:40 +0300 Subject: [PATCH 14/61] Fixes --- .../Composition/Transport/BatchStreamArrayPool.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs index d76a9c609e..97d05704af 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -51,11 +51,12 @@ internal abstract class BatchStreamPoolBase : IDisposable { var maximumUsage = _usageStatistics.Max(); var recentlyUsedPooledSlots = maximumUsage - _usage; - while (recentlyUsedPooledSlots < _pool.Count) + var keepSlots = Math.Max(recentlyUsedPooledSlots, 10); + while (keepSlots < _pool.Count) DestroyItem(_pool.Pop()); - _usageStatistics[_usageStatisticsSlot] = 0; _usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length; + _usageStatistics[_usageStatisticsSlot] = 0; } } @@ -137,7 +138,7 @@ internal sealed class BatchStreamMemoryPool : BatchStreamPoolBase { public int BufferSize { get; } - public BatchStreamMemoryPool(int bufferSize = 16384, Action>? startTimer = null) : base(true, startTimer) + public BatchStreamMemoryPool(int bufferSize = 1024, Action>? startTimer = null) : base(true, startTimer) { BufferSize = bufferSize; } From dbbed2c70bcb7abff176edffa7a7b045a0dd914d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 23:14:13 +0300 Subject: [PATCH 15/61] Added Dispose for CompositionTarget --- .../Composition/CompositionObject.cs | 5 ++-- .../Server/ServerCompositionTarget.cs | 27 ++++++++++++++++++- .../Composition/Server/ServerCompositor.cs | 2 ++ .../Composition/Server/ServerObject.cs | 4 ++- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs index d561338a36..baf1bfcddf 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -25,7 +25,7 @@ namespace Avalonia.Rendering.Composition public void Dispose() { - //Changes.Dispose = true; + RegisterForSerialization(); IsDisposed = true; } @@ -112,7 +112,8 @@ namespace Avalonia.Rendering.Composition private protected virtual void SerializeChangesCore(BatchStreamWriter writer) { - + if (Server is IDisposable) + writer.Write((byte)(IsDisposed ? 1 : 0)); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 7567eba534..9513fb58fa 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -9,7 +9,7 @@ using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server { - internal partial class ServerCompositionTarget + internal partial class ServerCompositionTarget : IDisposable { private readonly ServerCompositor _compositor; private readonly Func _renderTargetFactory; @@ -23,6 +23,7 @@ namespace Avalonia.Rendering.Composition.Server private Size _layerSize; private IDrawingContextLayerImpl? _layer; private bool _redrawRequested; + private bool _disposed; public ReadbackIndices Readback { get; } = new(); @@ -50,6 +51,12 @@ namespace Avalonia.Rendering.Composition.Server public void Render() { + if (_disposed) + { + Compositor.RemoveCompositionTarget(this); + return; + } + if (Root == null) return; _renderTarget ??= _renderTargetFactory(); @@ -69,6 +76,12 @@ namespace Avalonia.Rendering.Composition.Server _redrawRequested = false; using (var targetContext = _renderTarget.CreateDrawingContext(null)) { + // This is a hack to safely dispose layer created by some other render target + // because we can only dispose layers with the corresponding GPU context being + // active on the current thread + while (Compositor.LayersToDispose.Count > 0) + Compositor.LayersToDispose.Dequeue().Dispose(); + var layerSize = Size * Scaling; if (layerSize != _layerSize || _layer == null) { @@ -130,5 +143,17 @@ namespace Avalonia.Rendering.Composition.Server { _redrawRequested = true; } + + public void Dispose() + { + _disposed = true; + if (_layer != null) + { + Compositor.LayersToDispose.Enqueue(_layer); + _layer = null; + } + _renderTarget?.Dispose(); + _renderTarget = null; + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index f7de704b23..241be479ff 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Transport; @@ -19,6 +20,7 @@ namespace Avalonia.Rendering.Composition.Server private List _animationsToUpdate = new(); private BatchStreamObjectPool _batchObjectPool; private BatchStreamMemoryPool _batchMemoryPool; + public Queue LayersToDispose { get; } = new(); public ServerCompositor(IRenderLoop renderLoop, BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index 16f57d9059..dde711c3b5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -107,7 +107,9 @@ namespace Avalonia.Rendering.Composition.Server protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) { - + if (this is IDisposable disp + && reader.Read() == 1) + disp.Dispose(); } public void DeserializeChanges(BatchStreamReader reader, Batch batch) From 7790a513065b0e91676ba675bea4587ff428d402 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 23:15:39 +0300 Subject: [PATCH 16/61] Fixed hittest filtering --- .../Rendering/Composition/CompositingRenderer.cs | 6 +++--- .../Composition/CompositionDrawListVisual.cs | 6 +++++- .../Rendering/Composition/CompositionTarget.cs | 13 ++++++++----- src/Avalonia.Base/Rendering/Composition/Visual.cs | 4 +++- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 88c6948e48..b83f804a8f 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -65,9 +65,9 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor QueueUpdate(); } - public IEnumerable HitTest(Point p, IVisual root, Func filter) + public IEnumerable HitTest(Point p, IVisual root, Func? filter) { - var res = _target.TryHitTest(p); + var res = _target.TryHitTest(p, filter); if(res == null) yield break; for (var index = res.Count - 1; index >= 0; index--) @@ -81,7 +81,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor } } - public IVisual? HitTestFirst(Point p, IVisual root, Func filter) + public IVisual? HitTestFirst(Point p, IVisual root, Func? filter) { // TODO: Optimize return HitTest(p, root, filter).FirstOrDefault(); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index 069d888fbb..cc2b411822 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -1,7 +1,9 @@ +using System; using System.Numerics; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; +using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition; @@ -39,10 +41,12 @@ internal class CompositionDrawListVisual : CompositionContainerVisual Visual = visual; } - internal override bool HitTest(Point pt) + internal override bool HitTest(Point pt, Func? filter) { if (DrawList == null) return false; + if (filter != null && !filter(Visual)) + return false; if (Visual is ICustomHitTest custom) return custom.HitTest(pt); foreach (var op in DrawList) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index c5cfaeacce..3243934932 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.Numerics; using Avalonia.Collections.Pooled; +using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition { @@ -18,13 +20,13 @@ namespace Avalonia.Rendering.Composition Root.Root = null; } - public PooledList? TryHitTest(Point point) + public PooledList? TryHitTest(Point point, Func? filter) { Server.Readback.NextRead(); if (Root == null) return null; var res = new PooledList(); - HitTestCore(Root, point, res); + HitTestCore(Root, point, res, filter); return res; } @@ -69,7 +71,8 @@ namespace Avalonia.Rendering.Composition return false; } - bool HitTestCore(CompositionVisual visual, Point point, PooledList result) + bool HitTestCore(CompositionVisual visual, Point point, PooledList result, + Func? filter) { //TODO: Check readback too if (visual.Visible == false) @@ -80,7 +83,7 @@ namespace Avalonia.Rendering.Composition { bool success = false; // Hit-test the current node - if (visual.HitTest(point)) + if (visual.HitTest(point, filter)) { result.Add(visual); success = true; @@ -91,7 +94,7 @@ namespace Avalonia.Rendering.Composition for (var c = cv.Children.Count - 1; c >= 0; c--) { var ch = cv.Children[c]; - var hit = HitTestCore(ch, point, result); + var hit = HitTestCore(ch, point, result, filter); if (hit) return true; } diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index 5bf5dcee74..3d6e3fdaeb 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -1,4 +1,6 @@ +using System; using System.Numerics; +using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition { @@ -33,6 +35,6 @@ namespace Avalonia.Rendering.Composition internal object? Tag { get; set; } - internal virtual bool HitTest(Point point) => true; + internal virtual bool HitTest(Point point, Func? filter) => true; } } \ No newline at end of file From 0d7adc70d9e69334d2109b04a8d2ebade8c6461b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 3 Jun 2022 15:47:01 +0300 Subject: [PATCH 17/61] Implemented ZIndex support --- .../Composition/CompositingRenderer.cs | 69 ++++++++++++++++--- .../Rendering/ImmediateRenderer.cs | 6 +- .../Rendering/SceneGraph/SceneBuilder.cs | 44 +++++++----- src/Avalonia.Base/Visual.cs | 8 +++ src/Avalonia.Base/VisualTree/IVisual.cs | 5 ++ 5 files changed, 104 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index b83f804a8f..5aded281b3 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using Avalonia.Collections; +using Avalonia.Collections.Pooled; using Avalonia.Media; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.Composition.Server; @@ -100,26 +101,74 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor return; var compositionChildren = v.CompositionVisual.Children; var visualChildren = (AvaloniaList)v.GetVisualChildren(); + + PooledList<(IVisual visual, int index)>? sortedChildren = null; + if (v.HasNonUniformZIndexChildren && visualChildren.Count > 1) + { + sortedChildren = new (visualChildren.Count); + for (var c = 0; c < visualChildren.Count; c++) + sortedChildren.Add((visualChildren[c], c)); + + // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. + sortedChildren.Sort(static (lhs, rhs) => + { + var result = lhs.visual.ZIndex.CompareTo(rhs.visual.ZIndex); + return result == 0 ? lhs.index.CompareTo(rhs.index) : result; + }); + } + if (compositionChildren.Count == visualChildren.Count) { bool mismatch = false; - for(var c=0; c x, ZIndexComparer.Instance)) + var childrenEnumerable = visual.HasNonUniformZIndexChildren + ? visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance) + : (IEnumerable)visual.VisualChildren; + + foreach (var child in childrenEnumerable) { var childBounds = GetTransformedBounds(child); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs index 019c3e0e9b..cc18400a4c 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs @@ -272,26 +272,36 @@ namespace Avalonia.Rendering.SceneGraph else if (visualChildren.Count > 1) { var count = visualChildren.Count; - var sortedChildren = new (IVisual visual, int index)[count]; - for (var i = 0; i < count; i++) + if (visual.HasNonUniformZIndexChildren) { - sortedChildren[i] = (visualChildren[i], i); - } - - // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. - Array.Sort(sortedChildren, (lhs, rhs) => - { - var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual); - - return result == 0 ? lhs.index.CompareTo(rhs.index) : result; - }); - - foreach (var child in sortedChildren) - { - var childNode = GetOrCreateChildNode(scene, child.Item1, node); - Update(context, scene, (VisualNode)childNode, clip, forceRecurse); + var sortedChildren = new (IVisual visual, int index)[count]; + + for (var i = 0; i < count; i++) + { + sortedChildren[i] = (visualChildren[i], i); + } + + // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. + Array.Sort(sortedChildren, (lhs, rhs) => + { + var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual); + + return result == 0 ? lhs.index.CompareTo(rhs.index) : result; + }); + + foreach (var child in sortedChildren) + { + var childNode = GetOrCreateChildNode(scene, child.Item1, node); + Update(context, scene, (VisualNode)childNode, clip, forceRecurse); + } } + else + foreach (var child in visualChildren) + { + var childNode = GetOrCreateChildNode(scene, child, node); + Update(context, scene, (VisualNode)childNode, clip, forceRecurse); + } } node.SubTreeUpdated = true; diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 64341be0c7..c11ea2c40a 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -293,6 +293,8 @@ namespace Avalonia internal CompositionDrawListVisual? CompositionVisual { get; private set; } + public bool HasNonUniformZIndexChildren { get; private set; } + /// /// Gets a value indicating whether this control is attached to a visual root. /// @@ -445,6 +447,9 @@ namespace Avalonia AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); + if (ZIndex != 0 && this.GetVisualParent() is Visual parent) + parent.HasNonUniformZIndexChildren = true; + var visualChildren = VisualChildren; if (visualChildren != null) @@ -611,6 +616,9 @@ namespace Avalonia { var sender = e.Sender as IVisual; var parent = sender?.VisualParent; + if (sender?.ZIndex != 0 && parent is Visual parentVisual) + parentVisual.HasNonUniformZIndexChildren = true; + sender?.InvalidateVisual(); parent?.VisualRoot?.Renderer?.RecalculateChildren(parent); } diff --git a/src/Avalonia.Base/VisualTree/IVisual.cs b/src/Avalonia.Base/VisualTree/IVisual.cs index 3b053fab38..fdd2d187b8 100644 --- a/src/Avalonia.Base/VisualTree/IVisual.cs +++ b/src/Avalonia.Base/VisualTree/IVisual.cs @@ -79,6 +79,11 @@ namespace Avalonia.VisualTree /// Gets a value indicating whether to apply mirror transform on this control. /// bool HasMirrorTransform { get; } + + /// + /// Gets a value indicating whether to sort children when rendering this control + /// + bool HasNonUniformZIndexChildren { get; } /// /// Gets or sets the render transform of the control. From 32c1eb41e44a097898355893ce53aa6f23e2319d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 3 Jun 2022 15:58:21 +0300 Subject: [PATCH 18/61] Hit-testing clip --- .../Composition/CompositionTarget.cs | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index 3243934932..31431a4e36 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -79,30 +79,33 @@ namespace Avalonia.Rendering.Composition return false; if (!TryTransformTo(visual, ref point)) return false; - if (point.X >= 0 && point.Y >= 0 && point.X <= visual.Size.X && point.Y <= visual.Size.Y) + + if (visual.ClipToBounds + && (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y)) + return false; + if (visual.Clip?.FillContains(point) == false) + return false; + + bool success = false; + // Hit-test the current node + if (visual.HitTest(point, filter)) { - bool success = false; - // Hit-test the current node - if (visual.HitTest(point, filter)) + result.Add(visual); + success = true; + } + + // Inspect children too + if (visual is CompositionContainerVisual cv) + for (var c = cv.Children.Count - 1; c >= 0; c--) { - result.Add(visual); - success = true; + var ch = cv.Children[c]; + var hit = HitTestCore(ch, point, result, filter); + if (hit) + return true; } - - // Inspect children too - if(visual is CompositionContainerVisual cv) - for (var c = cv.Children.Count - 1; c >= 0; c--) - { - var ch = cv.Children[c]; - var hit = HitTestCore(ch, point, result, filter); - if (hit) - return true; - } - return success; - } + return success; - return false; } public void RequestRedraw() => RegisterForSerialization(); From 10247f3dc2f80e00584e098964715e09a8d832f0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 3 Jun 2022 16:03:05 +0300 Subject: [PATCH 19/61] Stop animations from ticking when window is hidden --- .../Server/ServerCompositionTarget.cs | 22 +++++++++++++++++++ .../Composition/Server/ServerVisual.cs | 6 ++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 9513fb58fa..e8c417a98d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Numerics; using System.Threading; using Avalonia.Media; @@ -24,6 +25,7 @@ namespace Avalonia.Rendering.Composition.Server private IDrawingContextLayerImpl? _layer; private bool _redrawRequested; private bool _disposed; + private HashSet _attachedVisuals = new(); public ReadbackIndices Readback { get; } = new(); @@ -39,9 +41,17 @@ namespace Avalonia.Rendering.Composition.Server partial void OnIsEnabledChanged() { if (IsEnabled) + { _compositor.AddCompositionTarget(this); + foreach (var v in _attachedVisuals) + v.Activate(); + } else + { _compositor.RemoveCompositionTarget(this); + foreach (var v in _attachedVisuals) + v.Deactivate(); + } } partial void DeserializeChangesExtra(BatchStreamReader c) @@ -155,5 +165,17 @@ namespace Avalonia.Rendering.Composition.Server _renderTarget?.Dispose(); _renderTarget = null; } + + public void AddVisual(ServerCompositionVisual visual) + { + if (_attachedVisuals.Add(visual) && IsEnabled) + visual.Activate(); + } + + public void RemoveVisual(ServerCompositionVisual visual) + { + if (_attachedVisuals.Remove(visual) && IsEnabled) + visual.Deactivate(); + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 5717ab2f8c..5c7067bfb1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -126,14 +126,14 @@ namespace Avalonia.Rendering.Composition.Server partial void OnRootChanging() { - if(Root != null) - Deactivate(); + if (Root != null) + Root.RemoveVisual(this); } partial void OnRootChanged() { if (Root != null) - Activate(); + Root.AddVisual(this); } protected override void ValuesInvalidated() From 6e2214b6d42e6bf8f1913d9dd5fc509098769094 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 3 Jun 2022 17:22:02 +0300 Subject: [PATCH 20/61] Added adorner support --- .../Server/ServerCompositionDrawListVisual.cs | 19 +++++++++++++++--- .../Server/ServerCompositionTarget.cs | 14 ++++++++++++- .../Server/ServerContainerVisual.cs | 18 +++++++++-------- .../Server/ServerSolidColorVisual.cs | 5 ++--- .../Composition/Server/ServerSpriteVisual.cs | 4 ++-- .../Composition/Server/ServerVisual.cs | 12 +++++++---- src/Avalonia.Base/Visual.cs | 2 +- src/Avalonia.Base/composition-schema.xml | 1 + .../Primitives/AdornerLayer.cs | 20 ++++++++++++++----- .../CompositionGenerator/Config.cs | 2 ++ .../CompositionGenerator/Generator.cs | 2 +- 11 files changed, 71 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index f1b5032cd3..45062632c7 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -11,10 +11,16 @@ namespace Avalonia.Rendering.Composition.Server; internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual { +#if DEBUG + public readonly Visual UiVisual; +#endif private CompositionDrawList? _renderCommands; - public ServerCompositionDrawListVisual(ServerCompositor compositor) : base(compositor) + public ServerCompositionDrawListVisual(ServerCompositor compositor, Visual v) : base(compositor) { +#if DEBUG + UiVisual = v; +#endif } Rect? _contentBounds; @@ -47,12 +53,19 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua base.DeserializeChangesCore(reader, commitedAt); } - protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + protected override void RenderCore(CompositorDrawingContextProxy canvas) { if (_renderCommands != null) { _renderCommands.Render(canvas); } - base.RenderCore(canvas, transform); + base.RenderCore(canvas); } + +#if DEBUG + public override string ToString() + { + return UiVisual.GetType().ToString(); + } +#endif } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index e8c417a98d..fb0d6fce60 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -26,6 +26,7 @@ namespace Avalonia.Rendering.Composition.Server private bool _redrawRequested; private bool _disposed; private HashSet _attachedVisuals = new(); + private Queue _adornerUpdateQueue = new(); public ReadbackIndices Readback { get; } = new(); @@ -80,6 +81,12 @@ namespace Avalonia.Rendering.Composition.Server // Update happens in a separate phase to extend dirty rect if needed Root.Update(this, Matrix4x4.Identity); + + while (_adornerUpdateQueue.Count > 0) + { + var adorner = _adornerUpdateQueue.Dequeue(); + adorner.Update(this, adorner.AdornedVisual?.GlobalTransformMatrix ?? Matrix4x4.Identity); + } Readback.CompleteWrite(Revision); @@ -108,7 +115,7 @@ namespace Avalonia.Rendering.Composition.Server { context.PushClip(_dirtyRect); context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), Root.CombinedTransformMatrix); + Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper)); context.PopClip(); } } @@ -147,6 +154,7 @@ namespace Avalonia.Rendering.Composition.Server { var snapped = SnapToDevicePixels(rect, Scaling); _dirtyRect = _dirtyRect.Union(snapped); + _redrawRequested = true; } public void Invalidate() @@ -176,6 +184,10 @@ namespace Avalonia.Rendering.Composition.Server { if (_attachedVisuals.Remove(visual) && IsEnabled) visual.Deactivate(); + if(visual.IsVisibleInFrame) + AddDirtyRect(visual.TransformedBounds); } + + public void EnqueueAdornerUpdate(ServerCompositionVisual visual) => _adornerUpdateQueue.Enqueue(visual); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs index a277450214..bcfcfbe4f2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -7,24 +7,26 @@ namespace Avalonia.Rendering.Composition.Server { public ServerCompositionVisualCollection Children { get; private set; } = null!; - protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + protected override void RenderCore(CompositorDrawingContextProxy canvas) { - base.RenderCore(canvas, transform); + base.RenderCore(canvas); foreach (var ch in Children) { - var t = transform; - - t = ch.CombinedTransformMatrix * t; - ch.Render(canvas, t); + ch.Render(canvas); } } public override void Update(ServerCompositionTarget root, Matrix4x4 transform) { base.Update(root, transform); - foreach (var child in Children) - child.Update(root, GlobalTransformMatrix); + foreach (var child in Children) + { + if (child.AdornedVisual != null) + root.EnqueueAdornerUpdate(child); + else + child.Update(root, GlobalTransformMatrix); + } } partial void Initialize() diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs index 786779bb2d..5720d80304 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs @@ -6,11 +6,10 @@ namespace Avalonia.Rendering.Composition.Server { internal partial class ServerCompositionSolidColorVisual { - protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + protected override void RenderCore(CompositorDrawingContextProxy canvas) { - canvas.Transform = MatrixUtils.ToMatrix(transform); canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new RoundedRect(new Rect(new Size(Size)))); - base.RenderCore(canvas, transform); + base.RenderCore(canvas); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs index c658dc8ae3..2f4c446cfa 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs @@ -6,7 +6,7 @@ namespace Avalonia.Rendering.Composition.Server internal partial class ServerCompositionSpriteVisual { - protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + protected override void RenderCore(CompositorDrawingContextProxy canvas) { if (Brush != null) { @@ -14,7 +14,7 @@ namespace Avalonia.Rendering.Composition.Server //canvas.FillRect((Vector2)Size, (ICbBrush)Brush.Brush!); } - base.RenderCore(canvas, transform); + base.RenderCore(canvas); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 5c7067bfb1..0b2ba6922b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -8,17 +8,18 @@ namespace Avalonia.Rendering.Composition.Server { private bool _isDirty; private bool _isBackface; - protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + protected virtual void RenderCore(CompositorDrawingContextProxy canvas) { } - public void Render(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + public void Render(CompositorDrawingContextProxy canvas) { if(Visible == false) return; if(Opacity == 0) return; + var transform = GlobalTransformMatrix; canvas.PreTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; if (Opacity != 1) @@ -29,8 +30,9 @@ namespace Avalonia.Rendering.Composition.Server canvas.PushGeometryClip(Clip); //TODO: Check clip - RenderCore(canvas, transform); + RenderCore(canvas); + // Hack to force invalidation of SKMatrix canvas.PreTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; @@ -60,7 +62,9 @@ namespace Avalonia.Rendering.Composition.Server public virtual void Update(ServerCompositionTarget root, Matrix4x4 transform) { // Calculate new parent-relative transform - CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, + CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, + // HACK: Ignore RenderTransform set by the adorner layer + AdornedVisual != null ? Matrix4x4.Identity : TransformMatrix, Scale, RotationAngle, Orientation, Offset); var newTransform = CombinedTransformMatrix * transform; diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index c11ea2c40a..2f4fbbd3f0 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -470,7 +470,7 @@ namespace Avalonia { if (CompositionVisual == null || CompositionVisual.Compositor != compositor) CompositionVisual = new CompositionDrawListVisual(compositor, - new ServerCompositionDrawListVisual(compositor.Server), this); + new ServerCompositionDrawListVisual(compositor.Server, this), this); return CompositionVisual; } diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index a7ae341bb3..b86ce35d5f 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -22,6 +22,7 @@ + diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 5ad4e39baf..57fb7226e8 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -164,6 +164,9 @@ namespace Avalonia.Controls.Primitives private void UpdateAdornedElement(Visual adorner, Visual? adorned) { + if (adorner.CompositionVisual != null) + adorner.CompositionVisual.AdornedVisual = adorned?.CompositionVisual; + var info = adorner.GetValue(s_adornedElementInfoProperty); if (info != null) @@ -184,11 +187,18 @@ namespace Avalonia.Controls.Primitives adorner.SetValue(s_adornedElementInfoProperty, info); } - info.Subscription = adorned.GetObservable(TransformedBoundsProperty).Subscribe(x => - { - info.Bounds = x; - InvalidateMeasure(); - }); + if (adorner.CompositionVisual != null) + info.Subscription = adorned.GetObservable(BoundsProperty).Subscribe(x => + { + info.Bounds = new TransformedBounds(new Rect(adorned.Bounds.Size), new Rect(adorned.Bounds.Size), Matrix.Identity); + InvalidateMeasure(); + }); + else + info.Subscription = adorned.GetObservable(TransformedBoundsProperty).Subscribe(x => + { + info.Bounds = x; + InvalidateMeasure(); + }); } } diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs index 096864e52a..d1fc691a8b 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs @@ -110,6 +110,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator public bool Animated { get; set; } [XmlAttribute] public bool InternalSet { get; set; } + [XmlAttribute] + public bool Internal { get; set; } } public class GAnimationType diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs index 3c38c0331e..f8f86265cf 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs @@ -378,7 +378,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator return client .AddMembers(DeclareField(prop.Type, fieldName)) .AddMembers(PropertyDeclaration(propType, prop.Name) - .AddModifiers(SyntaxKind.PublicKeyword) + .AddModifiers(prop.Internal ? SyntaxKind.InternalKeyword : SyntaxKind.PublicKeyword) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, Block(ReturnStatement(IdentifierName(fieldName)))), From 223a67543335365de9e7746ad5e9ad568a64f319 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 3 Jun 2022 18:50:32 +0300 Subject: [PATCH 21/61] IPlatformGpu --- src/Avalonia.Base/Platform/IPlatformGpu.cs | 16 +++++ .../Rendering/Composition/Compositor.cs | 4 +- .../Server/ServerCompositionTarget.cs | 22 +++---- .../Composition/Server/ServerCompositor.cs | 6 +- .../AvaloniaNativePlatformOpenGlInterface.cs | 2 + .../Egl/EglPlatformOpenGlInterface.cs | 2 + src/Avalonia.OpenGL/IGlContext.cs | 3 +- .../IPlatformOpenGlInterface.cs | 6 +- src/Avalonia.X11/Glx/GlxPlatformFeature.cs | 2 + src/Avalonia.X11/X11Platform.cs | 6 +- .../OpenGl/WglPlatformOpenGlInterface.cs | 2 + src/Windows/Avalonia.Win32/Win32GlManager.cs | 61 +++++++++++-------- src/Windows/Avalonia.Win32/Win32Platform.cs | 6 +- src/iOS/Avalonia.iOS/EaglDisplay.cs | 2 + 14 files changed, 92 insertions(+), 48 deletions(-) create mode 100644 src/Avalonia.Base/Platform/IPlatformGpu.cs diff --git a/src/Avalonia.Base/Platform/IPlatformGpu.cs b/src/Avalonia.Base/Platform/IPlatformGpu.cs new file mode 100644 index 0000000000..0507dea1d7 --- /dev/null +++ b/src/Avalonia.Base/Platform/IPlatformGpu.cs @@ -0,0 +1,16 @@ +using System; +using Avalonia.Metadata; + +namespace Avalonia.Platform; + +[Unstable] +public interface IPlatformGpu +{ + IPlatformGpuContext PrimaryContext { get; } +} + +[Unstable] +public interface IPlatformGpuContext : IDisposable +{ + IDisposable EnsureCurrent(); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 96564f0800..03aebba907 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -24,9 +24,9 @@ namespace Avalonia.Rendering.Composition internal ServerCompositor Server => _server; internal CompositionEasingFunction DefaultEasing { get; } - public Compositor(IRenderLoop loop) + public Compositor(IRenderLoop loop, IPlatformGpu? gpu) { - _server = new ServerCompositor(loop, _batchObjectPool, _batchMemoryPool); + _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool); _implicitBatchCommit = ImplicitBatchCommit; DefaultEasing = new CubicBezierEasingFunction(this, new Vector2(0.25f, 0.1f), new Vector2(0.25f, 1f)); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index fb0d6fce60..d1d7e421a4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -93,12 +93,6 @@ namespace Avalonia.Rendering.Composition.Server _redrawRequested = false; using (var targetContext = _renderTarget.CreateDrawingContext(null)) { - // This is a hack to safely dispose layer created by some other render target - // because we can only dispose layers with the corresponding GPU context being - // active on the current thread - while (Compositor.LayersToDispose.Count > 0) - Compositor.LayersToDispose.Dequeue().Dispose(); - var layerSize = Size * Scaling; if (layerSize != _layerSize || _layer == null) { @@ -164,14 +158,20 @@ namespace Avalonia.Rendering.Composition.Server public void Dispose() { + if(_disposed) + return; _disposed = true; - if (_layer != null) + using (_compositor.GpuContext?.EnsureCurrent()) { - Compositor.LayersToDispose.Enqueue(_layer); - _layer = null; + if (_layer != null) + { + _layer.Dispose(); + _layer = null; + } + + _renderTarget?.Dispose(); + _renderTarget = null; } - _renderTarget?.Dispose(); - _renderTarget = null; } public void AddVisual(ServerCompositionVisual visual) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 241be479ff..c44150f756 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -20,10 +20,12 @@ namespace Avalonia.Rendering.Composition.Server private List _animationsToUpdate = new(); private BatchStreamObjectPool _batchObjectPool; private BatchStreamMemoryPool _batchMemoryPool; - public Queue LayersToDispose { get; } = new(); + public IPlatformGpuContext? GpuContext { get; } - public ServerCompositor(IRenderLoop renderLoop, BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) + public ServerCompositor(IRenderLoop renderLoop, IPlatformGpu? platformGpu, + BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) { + GpuContext = platformGpu?.PrimaryContext; _renderLoop = renderLoop; _batchObjectPool = batchObjectPool; _batchMemoryPool = batchMemoryPool; diff --git a/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs b/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs index 3b3d8836fd..14d27a90e9 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs @@ -3,6 +3,7 @@ using Avalonia.OpenGL; using Avalonia.Native.Interop; using System.Drawing; using Avalonia.OpenGL.Surfaces; +using Avalonia.Platform; using Avalonia.Threading; namespace Avalonia.Native @@ -37,6 +38,7 @@ namespace Avalonia.Native internal GlContext MainContext { get; } public IGlContext PrimaryContext => MainContext; + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; public bool CanShareContexts => true; public bool CanCreateContexts => true; diff --git a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs index 476f65a774..a6d8c1e98d 100644 --- a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Logging; +using Avalonia.Platform; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl @@ -12,6 +13,7 @@ namespace Avalonia.OpenGL.Egl public EglContext PrimaryEglContext { get; } public IGlContext PrimaryContext => PrimaryEglContext; + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; public EglPlatformOpenGlInterface(EglDisplay display) { diff --git a/src/Avalonia.OpenGL/IGlContext.cs b/src/Avalonia.OpenGL/IGlContext.cs index 50868db873..a52a6535da 100644 --- a/src/Avalonia.OpenGL/IGlContext.cs +++ b/src/Avalonia.OpenGL/IGlContext.cs @@ -1,8 +1,9 @@ using System; +using Avalonia.Platform; namespace Avalonia.OpenGL { - public interface IGlContext : IDisposable + public interface IGlContext : IPlatformGpuContext { GlVersion Version { get; } GlInterface GlInterface { get; } diff --git a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs index 5ee5df1e85..4ff7997b03 100644 --- a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs +++ b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs @@ -1,8 +1,10 @@ +using Avalonia.Platform; + namespace Avalonia.OpenGL { - public interface IPlatformOpenGlInterface + public interface IPlatformOpenGlInterface : IPlatformGpu { - IGlContext PrimaryContext { get; } + new IGlContext PrimaryContext { get; } IGlContext CreateSharedContext(); bool CanShareContexts { get; } bool CanCreateContexts { get; } diff --git a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs index 6735a32ffe..0968adc799 100644 --- a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs +++ b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Avalonia.Logging; using Avalonia.OpenGL; +using Avalonia.Platform; namespace Avalonia.X11.Glx { @@ -14,6 +15,7 @@ namespace Avalonia.X11.Glx public IGlContext CreateSharedContext() => Display.CreateContext(PrimaryContext); public GlxContext DeferredContext { get; private set; } public IGlContext PrimaryContext => DeferredContext; + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; public static bool TryInitialize(X11Info x11, IList glProfiles) { diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 9c82288c8e..d882450259 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -103,8 +103,12 @@ namespace Avalonia.X11 GlxPlatformOpenGlInterface.TryInitialize(Info, Options.GlProfiles); } + var gl = AvaloniaLocator.Current.GetService(); + if (gl != null) + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + if (options.UseCompositor) - Compositor = new Compositor(AvaloniaLocator.Current.GetService()!); + Compositor = new Compositor(AvaloniaLocator.Current.GetService()!, gl); } diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs index b948495b99..1d0880a468 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs @@ -2,12 +2,14 @@ using System; using System.Linq; using Avalonia.Logging; using Avalonia.OpenGL; +using Avalonia.Platform; namespace Avalonia.Win32.OpenGl { class WglPlatformOpenGlInterface : IPlatformOpenGlInterface { public WglContext PrimaryContext { get; } + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; IGlContext IPlatformOpenGlInterface.PrimaryContext => PrimaryContext; public IGlContext CreateSharedContext() => WglDisplay.CreateContext(new[] { PrimaryContext.Version }, PrimaryContext); diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 2a3f4a3384..39a742d1ac 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -1,6 +1,7 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; +using Avalonia.Platform; using Avalonia.Win32.OpenGl; using Avalonia.Win32.WinRT.Composition; @@ -9,45 +10,53 @@ namespace Avalonia.Win32 static class Win32GlManager { - public static void Initialize() + public static IPlatformOpenGlInterface Initialize() { - AvaloniaLocator.CurrentMutable.Bind().ToLazy(() => + var gl = InitializeCore(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + return gl; + } + + static IPlatformOpenGlInterface InitializeCore() + { + + var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); + if (opts.UseWgl) { - var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); - if (opts.UseWgl) - { - var wgl = WglPlatformOpenGlInterface.TryCreate(); - return wgl; - } + var wgl = WglPlatformOpenGlInterface.TryCreate(); + return wgl; + } - if (opts.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) - { - var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); + if (opts.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) + { + var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); - if (egl != null) + if (egl != null) + { + if (opts.EglRendererBlacklist != null) { - if (opts.EglRendererBlacklist != null) + foreach (var item in opts.EglRendererBlacklist) { - foreach (var item in opts.EglRendererBlacklist) + if (egl.PrimaryEglContext.GlInterface.Renderer.Contains(item)) { - if (egl.PrimaryEglContext.GlInterface.Renderer.Contains(item)) - { - return null; - } + return null; } } - - if (opts.UseWindowsUIComposition) - { - WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); - } } - return egl; + if (opts.UseWindowsUIComposition) + { + WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); + } } - return null; - }); + return egl; + } + + return null; } + + } } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 4b0350f40f..af38a92e26 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -180,15 +180,15 @@ namespace Avalonia.Win32 .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()) .Bind().ToConstant(s_instance); - Win32GlManager.Initialize(); + var gl = Win32GlManager.Initialize(); _uiThread = Thread.CurrentThread; if (OleContext.Current != null) AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); - + if (Options.UseCompositor) - Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService()); + Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), gl); } public bool HasMessages() diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/EaglDisplay.cs index bd1969081d..906bbc29e7 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/EaglDisplay.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Disposables; using Avalonia.OpenGL; +using Avalonia.Platform; using OpenGLES; namespace Avalonia.iOS @@ -9,6 +10,7 @@ namespace Avalonia.iOS { public IGlContext PrimaryContext => Context; public IGlContext CreateSharedContext() => throw new NotSupportedException(); + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; public bool CanShareContexts => false; public bool CanCreateContexts => false; public IGlContext CreateContext() => throw new System.NotSupportedException(); From f772e5fb5a09c0fdb30a916a0eb21b04a1a73524 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 4 Jun 2022 01:19:31 +0300 Subject: [PATCH 22/61] Don't transitively activate server objects, only objects directly attached to a composition target should receive updates --- .../Rendering/Composition/Server/ServerObject.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index dde711c3b5..f55c9439e4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -92,15 +92,13 @@ namespace Avalonia.Rendering.Composition.Server public void SubscribeToInvalidation(int member, IAnimationInstance animation) { ref var store = ref GetStoreFromOffset(member); - if (store.Subscribers.AddRef(animation)) - Activate(); + store.Subscribers.AddRef(animation); } public void UnsubscribeFromInvalidation(int member, IAnimationInstance animation) { ref var store = ref GetStoreFromOffset(member); - if (store.Subscribers.ReleaseRef(animation)) - Deactivate(); + store.Subscribers.ReleaseRef(animation); } public virtual int? GetFieldOffset(string fieldName) => null; From 95b18fdf164622681c712c6043cc7e2b676615db Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 Jun 2022 10:09:56 +0300 Subject: [PATCH 23/61] macOS support? --- .../Composition/CompositingRenderer.cs | 16 +++++++++------ .../Composition/CompositionTarget.cs | 6 ++++++ .../Composition/Server/ServerCompositor.cs | 11 +++++++++- src/Avalonia.Native/AvaloniaNativePlatform.cs | 20 +++++++++++++++---- .../AvaloniaNativePlatformExtensions.cs | 5 +++++ src/Avalonia.Native/WindowImplBase.cs | 13 +++++++----- 6 files changed, 55 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 5aded281b3..5bdd7847c4 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -17,7 +17,6 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor { private readonly IRenderRoot _root; private readonly Compositor _compositor; - private readonly IDeferredRendererLock? _rendererLock; CompositionDrawingContext _recorder = new(); DrawingContext _recordingContext; private HashSet _dirty = new(); @@ -26,14 +25,17 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor private bool _queuedUpdate; private Action _update; + /// + /// Forces the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered + /// + public bool RenderOnlyOnRenderThread { get; set; } = true; + public CompositingRenderer(IRenderRoot root, - Compositor compositor, - IDeferredRendererLock? rendererLock = null) + Compositor compositor) { _root = root; _compositor = compositor; _recordingContext = new DrawingContext(_recorder); - _rendererLock = rendererLock ?? new ManagedDeferredRendererLock(); _target = compositor.CreateCompositionTarget(root.CreateRenderTarget); _target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor); _update = Update; @@ -228,10 +230,12 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public void Paint(Rect rect) { - // We render only on the render thread for now Update(); _target.RequestRedraw(); - Compositor.RequestCommitAsync().Wait(); + if(RenderOnlyOnRenderThread) + Compositor.RequestCommitAsync().Wait(); + else + _target.ImmediateUIThreadRender(); } public void Start() => _target.IsEnabled = true; diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index 31431a4e36..7e50c43ac1 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -109,5 +109,11 @@ namespace Avalonia.Rendering.Composition } public void RequestRedraw() => RegisterForSerialization(); + + internal void ImmediateUIThreadRender() + { + Compositor.RequestCommitAsync(); + Compositor.Server.Render(); + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index c44150f756..811fffd8eb 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -20,6 +20,7 @@ namespace Avalonia.Rendering.Composition.Server private List _animationsToUpdate = new(); private BatchStreamObjectPool _batchObjectPool; private BatchStreamMemoryPool _batchMemoryPool; + private object _lock = new object(); public IPlatformGpuContext? GpuContext { get; } public ServerCompositor(IRenderLoop renderLoop, IPlatformGpu? platformGpu, @@ -88,7 +89,15 @@ namespace Avalonia.Rendering.Composition.Server { } - void IRenderLoopTask.Render() + public void Render() + { + lock (_lock) + { + RenderCore(); + } + } + + private void RenderCore() { ApplyPendingBatches(); diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 4802fed554..7b08ed00dc 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -8,6 +8,8 @@ using Avalonia.Native.Interop; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using JetBrains.Annotations; namespace Avalonia.Native { @@ -21,6 +23,7 @@ namespace Avalonia.Native static extern IntPtr CreateAvaloniaNative(); internal static readonly KeyboardDevice KeyboardDevice = new KeyboardDevice(); + [CanBeNull] internal static Compositor Compositor { get; private set; } public Size DoubleClickSize => new Size(4, 4); @@ -101,6 +104,7 @@ namespace Avalonia.Native _factory.MacOptions.SetShowInDock(macOpts.ShowInDock ? 1 : 0); } + var renderLoop = new RenderLoop(); AvaloniaLocator.CurrentMutable .Bind() .ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface())) @@ -110,7 +114,7 @@ namespace Avalonia.Native .Bind().ToConstant(this) .Bind().ToConstant(this) .Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) - .Bind().ToConstant(new RenderLoop()) + .Bind().ToConstant(renderLoop) .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta, wholeWordTextActionModifiers: KeyModifiers.Alt)) @@ -124,19 +128,27 @@ namespace Avalonia.Native hotkeys.MoveCursorToTheStartOfLineWithSelection.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); hotkeys.MoveCursorToTheEndOfLine.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers)); hotkeys.MoveCursorToTheEndOfLineWithSelection.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); - + if (_options.UseGpu) { try { - AvaloniaLocator.CurrentMutable.Bind() - .ToConstant(_platformGl = new AvaloniaNativePlatformOpenGlInterface(_factory.ObtainGlDisplay())); + _platformGl = new AvaloniaNativePlatformOpenGlInterface(_factory.ObtainGlDisplay()); + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(_platformGl) + .Bind().ToConstant(_platformGl); + } catch (Exception) { // ignored } } + + if (_options.UseDeferredRendering && _options.UseCompositor) + { + Compositor = new Compositor(renderLoop, _platformGl); + } } public ITrayIconImpl CreateTrayIcon() diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 10619d675b..1320903ca2 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -39,6 +39,11 @@ namespace Avalonia /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; + + /// + /// Enables new compositing rendering with UWP-like API + /// + public bool UseCompositor { get; set; } /// /// Determines whether to use GPU for rendering in your project. The default value is true. diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 94a3a5ed9b..922750fbb0 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -12,6 +12,7 @@ using Avalonia.Native.Interop; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; namespace Avalonia.Native @@ -363,13 +364,15 @@ namespace Avalonia.Native public IRenderer CreateRenderer(IRenderRoot root) { + var customRendererFactory = AvaloniaLocator.Current.GetService(); + var loop = AvaloniaLocator.Current.GetService(); + if (customRendererFactory != null) + return customRendererFactory.Create(root, loop); + if (_deferredRendering) { - var loop = AvaloniaLocator.Current.GetService(); - var customRendererFactory = AvaloniaLocator.Current.GetService(); - - if (customRendererFactory != null) - return customRendererFactory.Create(root, loop); + if (AvaloniaNativePlatform.Compositor != null) + return new CompositingRenderer(root, AvaloniaNativePlatform.Compositor); return new DeferredRenderer(root, loop); } From b6b08155d87fc3d743d09f8437f94cf6715de6fa Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 Jun 2022 11:40:21 +0300 Subject: [PATCH 24/61] [WIP] Run render tests for compositing renderer --- .../Avalonia.Android/ChoreographerTimer.cs | 3 + .../Composition/CompositingRenderer.cs | 6 +- .../Rendering/Composition/Compositor.cs | 7 +-- .../Rendering/DefaultRenderTimer.cs | 2 + src/Avalonia.Base/Rendering/IRenderLoop.cs | 2 + src/Avalonia.Base/Rendering/IRenderTimer.cs | 5 ++ src/Avalonia.Base/Rendering/RenderLoop.cs | 2 + .../Rendering/SleepLoopRenderTimer.cs | 2 + src/Avalonia.Base/Visual.cs | 3 + .../ManualTriggerRenderTimer.cs | 1 + .../Composition/WinUICompositorConnection.cs | 1 + src/iOS/Avalonia.iOS/DisplayLinkTimer.cs | 2 + .../Avalonia.RenderTests/ManualRenderTimer.cs | 19 ++++++ tests/Avalonia.RenderTests/TestBase.cs | 58 ++++++++++++++++++- tests/Avalonia.RenderTests/TestRenderRoot.cs | 48 +++++++++++++++ 15 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 tests/Avalonia.RenderTests/ManualRenderTimer.cs create mode 100644 tests/Avalonia.RenderTests/TestRenderRoot.cs diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs index 1d898261a3..19dc7b4ab6 100644 --- a/src/Android/Avalonia.Android/ChoreographerTimer.cs +++ b/src/Android/Avalonia.Android/ChoreographerTimer.cs @@ -29,6 +29,9 @@ namespace Avalonia.Android _thread = new Thread(Loop); _thread.Start(); } + + + public bool RunsInBackground => true; public event Action Tick { diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 5bdd7847c4..c67142648a 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -232,7 +232,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor { Update(); _target.RequestRedraw(); - if(RenderOnlyOnRenderThread) + if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground) Compositor.RequestCommitAsync().Wait(); else _target.ImmediateUIThreadRender(); @@ -249,9 +249,11 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor { Stop(); _target.Dispose(); + // Wait for the composition batch to be applied and rendered to guarantee that // render target is not used anymore and can be safely disposed - _compositor.RequestCommitAsync().Wait(); + if (Compositor.Loop.RunsInBackground) + _compositor.RequestCommitAsync().Wait(); } diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 03aebba907..e92b69bd60 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -15,6 +15,7 @@ namespace Avalonia.Rendering.Composition { public partial class Compositor { + internal IRenderLoop Loop { get; } private ServerCompositor _server; private bool _implicitBatchCommitQueued; private Action _implicitBatchCommit; @@ -26,6 +27,7 @@ namespace Avalonia.Rendering.Composition public Compositor(IRenderLoop loop, IPlatformGpu? gpu) { + Loop = loop; _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool); _implicitBatchCommit = ImplicitBatchCommit; DefaultEasing = new CubicBezierEasingFunction(this, @@ -60,11 +62,6 @@ namespace Avalonia.Rendering.Composition return batch.Completed; } - public void Dispose() - { - - } - public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); public CompositionSolidColorVisual CreateSolidColorVisual() => new CompositionSolidColorVisual(this, diff --git a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs index 82d3892975..d0d3dd9715 100644 --- a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs @@ -59,6 +59,8 @@ namespace Avalonia.Rendering } } + public bool RunsInBackground => true; + /// /// Starts the timer. /// diff --git a/src/Avalonia.Base/Rendering/IRenderLoop.cs b/src/Avalonia.Base/Rendering/IRenderLoop.cs index 9838967261..e500ecdf8b 100644 --- a/src/Avalonia.Base/Rendering/IRenderLoop.cs +++ b/src/Avalonia.Base/Rendering/IRenderLoop.cs @@ -27,5 +27,7 @@ namespace Avalonia.Rendering /// /// The update task. void Remove(IRenderLoopTask i); + + bool RunsInBackground { get; } } } diff --git a/src/Avalonia.Base/Rendering/IRenderTimer.cs b/src/Avalonia.Base/Rendering/IRenderTimer.cs index ee74c345be..07af7eeec8 100644 --- a/src/Avalonia.Base/Rendering/IRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/IRenderTimer.cs @@ -18,5 +18,10 @@ namespace Avalonia.Rendering /// switch execution to the right thread. /// event Action Tick; + + /// + /// Indicates if the timer ticks on a non-UI thread + /// + bool RunsInBackground { get; } } } diff --git a/src/Avalonia.Base/Rendering/RenderLoop.cs b/src/Avalonia.Base/Rendering/RenderLoop.cs index a5d7e15f93..c66fec92aa 100644 --- a/src/Avalonia.Base/Rendering/RenderLoop.cs +++ b/src/Avalonia.Base/Rendering/RenderLoop.cs @@ -87,6 +87,8 @@ namespace Avalonia.Rendering } } + public bool RunsInBackground => Timer.RunsInBackground; + private void TimerTick(TimeSpan time) { if (Interlocked.CompareExchange(ref _inTick, 1, 0) == 0) diff --git a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs index 86595754e9..8544de4bbc 100644 --- a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs @@ -43,6 +43,8 @@ namespace Avalonia.Rendering } } + public bool RunsInBackground => true; + void LoopProc() { var lastTick = _st.Elapsed; diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 2f4fbbd3f0..0bfdd6d516 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -469,8 +469,11 @@ namespace Avalonia internal CompositionVisual AttachToCompositor(Compositor compositor) { if (CompositionVisual == null || CompositionVisual.Compositor != compositor) + { CompositionVisual = new CompositionDrawListVisual(compositor, new ServerCompositionDrawListVisual(compositor.Server, this), this); + } + return CompositionVisual; } diff --git a/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs b/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs index 1a06d47744..7b9feab2e3 100644 --- a/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs +++ b/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs @@ -12,5 +12,6 @@ namespace Avalonia.Web.Blazor public void RaiseTick() => Tick?.Invoke(s_sw.Elapsed); public event Action? Tick; + public bool RunsInBackground => false; } } diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index 76af12e8ca..5334b90d62 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -302,5 +302,6 @@ namespace Avalonia.Win32.WinRT.Composition } public event Action Tick; + public bool RunsInBackground => true; } } diff --git a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs b/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs index df73479a65..eb124fd450 100644 --- a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs +++ b/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs @@ -28,6 +28,8 @@ namespace Avalonia.iOS } public Thread TimerThread { get; } + + public bool RunsInBackground => true; private void OnLinkTick() { diff --git a/tests/Avalonia.RenderTests/ManualRenderTimer.cs b/tests/Avalonia.RenderTests/ManualRenderTimer.cs new file mode 100644 index 0000000000..0dc994aaa5 --- /dev/null +++ b/tests/Avalonia.RenderTests/ManualRenderTimer.cs @@ -0,0 +1,19 @@ +using Avalonia.Rendering; +using System.Threading.Tasks; +using System; + + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests +#endif +{ + public class ManualRenderTimer : IRenderTimer + { + public event Action Tick; + public bool RunsInBackground => false; + public void TriggerTick() => Tick?.Invoke(TimeSpan.Zero); + public Task TriggerBackgroundTick() => Task.Run(TriggerTick); + } +} \ No newline at end of file diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index 39250f2aa7..4d6b313ffc 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -8,8 +8,13 @@ using Xunit; using Avalonia.Platform; using System.Threading.Tasks; using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; using System.Threading; using Avalonia.Media; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using SixLabors.ImageSharp.PixelFormats; using Image = SixLabors.ImageSharp.Image; @@ -38,7 +43,7 @@ namespace Avalonia.Direct2D1.RenderTests new TestThreadingInterface(); private static readonly IAssetLoader assetLoader = new AssetLoader(); - + static TestBase() { #if AVALONIA_SKIA @@ -84,6 +89,7 @@ namespace Avalonia.Direct2D1.RenderTests var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png"); var deferredPath = Path.Combine(OutputPath, testName + ".deferred.out.png"); + var compositedPath = Path.Combine(OutputPath, testName + ".composited.out.png"); var factory = AvaloniaLocator.Current.GetService(); var pixelSize = new PixelSize((int)target.Width, (int)target.Height); var size = new Size(target.Width, target.Height); @@ -96,7 +102,8 @@ namespace Avalonia.Direct2D1.RenderTests bitmap.Render(target); bitmap.Save(immediatePath); } - + + using (var rtb = factory.CreateRenderTargetBitmap(pixelSize, dpiVector)) using (var renderer = new DeferredRenderer(target, rtb)) { @@ -107,9 +114,30 @@ namespace Avalonia.Direct2D1.RenderTests // Do the deferred render on a background thread to expose any threading errors in // the deferred rendering path. await Task.Run((Action)renderer.UnitTestRender); + threadingInterface.MainThread = Thread.CurrentThread; rtb.Save(deferredPath); } + + var timer = new ManualRenderTimer(); + + var compositor = new Compositor(new RenderLoop(timer, Dispatcher.UIThread), null); + using (var rtb = factory.CreateRenderTargetBitmap(pixelSize, dpiVector)) + { + var root = new TestRenderRoot(dpiVector.X / 96, rtb); + using (var renderer = new CompositingRenderer(root, compositor) { RenderOnlyOnRenderThread = false}) + { + root.Initialize(renderer, target); + renderer.Start(); + Dispatcher.UIThread.RunJobs(); + timer.TriggerTick(); + } + + // Free pools + for (var c = 0; c < 11; c++) + TestThreadingInterface.RunTimers(); + rtb.Save(compositedPath); + } } protected void CompareImages([CallerMemberName] string testName = "") @@ -117,13 +145,16 @@ namespace Avalonia.Direct2D1.RenderTests var expectedPath = Path.Combine(OutputPath, testName + ".expected.png"); var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png"); var deferredPath = Path.Combine(OutputPath, testName + ".deferred.out.png"); + var compositedPath = Path.Combine(OutputPath, testName + ".composited.out.png"); using (var expected = Image.Load(expectedPath)) using (var immediate = Image.Load(immediatePath)) using (var deferred = Image.Load(deferredPath)) + using (var composited = Image.Load(compositedPath)) { var immediateError = CompareImages(immediate, expected); var deferredError = CompareImages(deferred, expected); + var compositedError = CompareImages(composited, expected); if (immediateError > 0.022) { @@ -134,6 +165,11 @@ namespace Avalonia.Direct2D1.RenderTests { Assert.True(false, deferredPath + ": Error = " + deferredError); } + + if (compositedError > 0.022) + { + Assert.True(false, compositedPath + ": Error = " + compositedError); + } } } @@ -233,9 +269,25 @@ namespace Avalonia.Direct2D1.RenderTests // No-op } + private static List s_timers = new(); + + public static void RunTimers() + { + lock (s_timers) + { + foreach(var t in s_timers.ToList()) + t.Invoke(); + } + } + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { - throw new NotImplementedException(); + var act = () => tick(); + lock (s_timers) s_timers.Add(act); + return Disposable.Create(() => + { + lock (s_timers) s_timers.Remove(act); + }); } } } diff --git a/tests/Avalonia.RenderTests/TestRenderRoot.cs b/tests/Avalonia.RenderTests/TestRenderRoot.cs new file mode 100644 index 0000000000..8f2b324d9c --- /dev/null +++ b/tests/Avalonia.RenderTests/TestRenderRoot.cs @@ -0,0 +1,48 @@ +using Avalonia.Rendering; +using System.Threading.Tasks; +using System; +using Avalonia.Controls; +using Avalonia.Platform; + + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests +#endif +{ + public class TestRenderRoot : Decorator, IRenderRoot + { + private readonly IRenderTarget _renderTarget; + public Size ClientSize { get; private set; } + public IRenderer Renderer { get; private set; } + public double RenderScaling { get; } + + public TestRenderRoot(double scaling, IRenderTarget renderTarget) + { + _renderTarget = renderTarget; + RenderScaling = scaling; + } + + public void Initialize(IRenderer renderer, Control child) + { + Renderer = renderer; + Child = child; + Width = child.Width; + Height = child.Height; + ClientSize = new Size(Width, Height); + Measure(ClientSize); + Arrange(new Rect(ClientSize)); + } + + public IRenderTarget CreateRenderTarget() => _renderTarget; + + public void Invalidate(Rect rect) + { + } + + public Point PointToClient(PixelPoint point) => point.ToPoint(RenderScaling); + + public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling); + } +} \ No newline at end of file From 92a20ee26fedb10a39f91c1e4529fc3ec24b3357 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 Jun 2022 11:53:54 +0300 Subject: [PATCH 25/61] Added support for opacity mask --- .../Rendering/Composition/CompositingRenderer.cs | 1 + .../Rendering/Composition/Server/ServerVisual.cs | 9 +++++++-- src/Avalonia.Base/Rendering/Composition/Visual.cs | 13 +++++++++++++ src/Avalonia.Base/composition-schema.xml | 5 ++++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index c67142648a..8cb7daca6b 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -189,6 +189,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor comp.Opacity = (float)visual.Opacity; comp.ClipToBounds = visual.ClipToBounds; comp.Clip = visual.Clip?.PlatformImpl; + comp.OpacityMask = visual.OpacityMask; var renderTransform = Matrix.Identity; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 0b2ba6922b..d42bcae7e7 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -24,10 +24,13 @@ namespace Avalonia.Rendering.Composition.Server canvas.Transform = Matrix.Identity; if (Opacity != 1) canvas.PushOpacity(Opacity); + var boundsRect = new Rect(new Size(Size.X, Size.Y)); if(ClipToBounds) - canvas.PushClip(new Rect(new Size(Size.X, Size.Y))); + canvas.PushClip(boundsRect); if (Clip != null) canvas.PushGeometryClip(Clip); + if(OpacityMaskBrush != null) + canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); //TODO: Check clip RenderCore(canvas); @@ -35,7 +38,9 @@ namespace Avalonia.Rendering.Composition.Server // Hack to force invalidation of SKMatrix canvas.PreTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; - + + if (OpacityMaskBrush != null) + canvas.PopOpacityMask(); if (Clip != null) canvas.PopGeometryClip(); if (ClipToBounds) diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index 3d6e3fdaeb..f092029457 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -1,11 +1,14 @@ using System; using System.Numerics; +using Avalonia.Media; using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition { public abstract partial class CompositionVisual { + private IBrush? _opacityMask; + private protected virtual void OnRootChangedCore() { } @@ -14,6 +17,16 @@ namespace Avalonia.Rendering.Composition partial void OnParentChanged() => Root = Parent?.Root; + public IBrush? OpacityMask + { + get => _opacityMask; + set + { + if (_opacityMask == value) + return; + OpacityMaskBrush = (_opacityMask = value)?.ToImmutable(); + } + } internal Matrix4x4? TryGetServerTransform() { diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index b86ce35d5f..2b555359c6 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -6,13 +6,15 @@ Avalonia.Rendering.Composition.Animations + - + + @@ -23,6 +25,7 @@ + From bd4aa13ad163cac3f5e1b8de4a7e63940713b202 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 Jun 2022 12:12:35 +0300 Subject: [PATCH 26/61] Fixed visual brush and scaling --- .../Composition/Drawing/CompositionDrawingContext.cs | 10 +++++----- .../Composition/Server/ServerCompositionTarget.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index 96c0e22d56..0c5a98b239 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -30,7 +30,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl // Nothing to do here since we allocate no unmanaged resources. } - public void BeginUpdate(CompositionDrawList list) + public void BeginUpdate(CompositionDrawList? list) { _builder.Reset(list); _drawOperationIndex = 0; @@ -373,12 +373,12 @@ internal class CompositionDrawingContext : IDrawingContextImpl // be attached to the same composition target) like UWP does. // Render-able visuals shouldn't be dangling unattached (visual as IVisualBrushInitialize)?.EnsureInitialized(); - - var drawList = new CompositionDrawList() { Size = visual.Bounds.Size }; + var recorder = new CompositionDrawingContext(); - recorder.BeginUpdate(drawList); + recorder.BeginUpdate(null); ImmediateRenderer.Render(visual, new DrawingContext(recorder)); - recorder.EndUpdate(); + var drawList = recorder.EndUpdate(); + drawList.Size = visual.Bounds.Size; return drawList; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index d1d7e421a4..24a5f0294a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -115,7 +115,7 @@ namespace Avalonia.Rendering.Composition.Server } targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, new Rect(_layerSize), - new Rect(_layerSize)); + new Rect(Size)); if (DrawDirtyRects) From 70e77880f54079b1312cbaa50bbacde7b42fb992 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 Jun 2022 12:44:54 +0300 Subject: [PATCH 27/61] Avalonia.SourceGenerator -> DevGenerators --- Avalonia.sln | 13 +++++++------ build/SourceGenerators.props | 2 +- .../CompositionRoslynGenerator.cs | 0 .../DevGenerators}/CompositionGenerator/Config.cs | 0 .../CompositionGenerator/Extensions.cs | 0 .../Generator.KeyFrameAnimation.cs | 0 .../CompositionGenerator/Generator.ListProxy.cs | 0 .../CompositionGenerator/Generator.Utils.cs | 0 .../CompositionGenerator/Generator.cs | 0 .../ICompositionGeneratorSink.cs | 0 .../RoslynCompositionGeneratorSink.cs | 0 .../DevGenerators/DevGenerators.csproj} | 2 +- .../DevGenerators}/IsExternalInit.cs | 0 .../DevGenerators}/SubtypesFactoryGenerator.cs | 0 14 files changed, 9 insertions(+), 8 deletions(-) rename src/{Avalonia.SourceGenerator => tools/DevGenerators}/CompositionGenerator/CompositionRoslynGenerator.cs (100%) rename src/{Avalonia.SourceGenerator => tools/DevGenerators}/CompositionGenerator/Config.cs (100%) rename src/{Avalonia.SourceGenerator => tools/DevGenerators}/CompositionGenerator/Extensions.cs (100%) rename src/{Avalonia.SourceGenerator => tools/DevGenerators}/CompositionGenerator/Generator.KeyFrameAnimation.cs (100%) rename src/{Avalonia.SourceGenerator => tools/DevGenerators}/CompositionGenerator/Generator.ListProxy.cs (100%) rename src/{Avalonia.SourceGenerator => tools/DevGenerators}/CompositionGenerator/Generator.Utils.cs (100%) rename src/{Avalonia.SourceGenerator => tools/DevGenerators}/CompositionGenerator/Generator.cs (100%) rename src/{Avalonia.SourceGenerator => tools/DevGenerators}/CompositionGenerator/ICompositionGeneratorSink.cs (100%) rename src/{Avalonia.SourceGenerator => tools/DevGenerators}/CompositionGenerator/RoslynCompositionGeneratorSink.cs (100%) rename src/{Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj => tools/DevGenerators/DevGenerators.csproj} (89%) rename src/{Avalonia.SourceGenerator => tools/DevGenerators}/IsExternalInit.cs (100%) rename src/{Avalonia.SourceGenerator => tools/DevGenerators}/SubtypesFactoryGenerator.cs (100%) diff --git a/Avalonia.sln b/Avalonia.sln index 25c7daf080..d3dce17e02 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -205,14 +205,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.SourceGenerator", "src\Avalonia.SourceGenerator\Avalonia.SourceGenerator.csproj", "{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPicker", "src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj", "{7BF6C69D-FC14-43EB-9ED0-782C16F3D5D9}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{EABE2161-989B-42BF-BD8D-1E34B20C21F1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -485,10 +485,6 @@ Global {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|Any CPU.Build.0 = Debug|Any CPU {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.ActiveCfg = Release|Any CPU {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.Build.0 = Release|Any CPU - {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.Build.0 = Release|Any CPU {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.Build.0 = Debug|Any CPU {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -501,6 +497,10 @@ Global {EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Debug|Any CPU.Build.0 = Debug|Any CPU {EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Release|Any CPU.ActiveCfg = Release|Any CPU {EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Release|Any CPU.Build.0 = Release|Any CPU + {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -557,6 +557,7 @@ Global {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/build/SourceGenerators.props b/build/SourceGenerators.props index d000af1bf6..4929578b60 100644 --- a/build/SourceGenerators.props +++ b/build/SourceGenerators.props @@ -1,7 +1,7 @@ diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs b/src/tools/DevGenerators/CompositionGenerator/CompositionRoslynGenerator.cs similarity index 100% rename from src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs rename to src/tools/DevGenerators/CompositionGenerator/CompositionRoslynGenerator.cs diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs b/src/tools/DevGenerators/CompositionGenerator/Config.cs similarity index 100% rename from src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs rename to src/tools/DevGenerators/CompositionGenerator/Config.cs diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs b/src/tools/DevGenerators/CompositionGenerator/Extensions.cs similarity index 100% rename from src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs rename to src/tools/DevGenerators/CompositionGenerator/Extensions.cs diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.KeyFrameAnimation.cs similarity index 100% rename from src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs rename to src/tools/DevGenerators/CompositionGenerator/Generator.KeyFrameAnimation.cs diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs similarity index 100% rename from src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs rename to src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs similarity index 100% rename from src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs rename to src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs similarity index 100% rename from src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs rename to src/tools/DevGenerators/CompositionGenerator/Generator.cs diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs b/src/tools/DevGenerators/CompositionGenerator/ICompositionGeneratorSink.cs similarity index 100% rename from src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs rename to src/tools/DevGenerators/CompositionGenerator/ICompositionGeneratorSink.cs diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs b/src/tools/DevGenerators/CompositionGenerator/RoslynCompositionGeneratorSink.cs similarity index 100% rename from src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs rename to src/tools/DevGenerators/CompositionGenerator/RoslynCompositionGeneratorSink.cs diff --git a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj b/src/tools/DevGenerators/DevGenerators.csproj similarity index 89% rename from src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj rename to src/tools/DevGenerators/DevGenerators.csproj index 3312f7a619..04ab33bf9e 100644 --- a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj +++ b/src/tools/DevGenerators/DevGenerators.csproj @@ -12,7 +12,7 @@ all - + diff --git a/src/Avalonia.SourceGenerator/IsExternalInit.cs b/src/tools/DevGenerators/IsExternalInit.cs similarity index 100% rename from src/Avalonia.SourceGenerator/IsExternalInit.cs rename to src/tools/DevGenerators/IsExternalInit.cs diff --git a/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs b/src/tools/DevGenerators/SubtypesFactoryGenerator.cs similarity index 100% rename from src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs rename to src/tools/DevGenerators/SubtypesFactoryGenerator.cs From 27e87857af8c631d1adee0a3aa311404ec951a7b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 Jun 2022 15:28:55 +0300 Subject: [PATCH 28/61] Some docs and refactorings --- .../Pages/CompositionPage.axaml.cs | 6 +- .../Easings}/CubicBezier.cs | 8 +- .../Animation/Easings/CubicBezierEasing.cs | 29 +++++ src/Avalonia.Base/Avalonia.Base.csproj | 6 +- .../Animations/AnimatedValueStore.cs | 20 +++- .../Animations/AnimationInstanceBase.cs | 5 + .../Animations/CompositionAnimation.cs | 109 +++++++++-------- .../Animations/ExpressionAnimation.cs | 19 +++ .../Animations/ExpressionAnimationInstance.cs | 4 + .../Animations/ICompositionAnimationBase.cs | 3 + .../Animations/ImplicitAnimationCollection.cs | 11 ++ .../Composition/Animations/Interpolators.cs | 3 + .../Animations/KeyFrameAnimation.cs | 111 +++++++++++++++--- .../Animations/KeyFrameAnimationInstance.cs | 16 ++- .../Composition/Animations/KeyFrames.cs | 27 +++-- .../Animations/PropertySetSnapshot.cs | 3 + .../Composition/CompositingRenderer.cs | 29 ++++- .../Composition/CompositionDrawListVisual.cs | 11 ++ .../Composition/CompositionEasingFunction.cs | 95 --------------- .../Composition/CompositionGradientBrush.cs | 16 --- .../Composition/CompositionObject.cs | 21 +++- .../Composition/CompositionPropertySet.cs | 9 ++ .../Composition/CompositionTarget.cs | 19 +++ .../Rendering/Composition/Compositor.cs | 100 +++++++--------- .../Composition/CompositorRenderLoopTask.cs | 20 ---- .../Rendering/Composition/ContainerVisual.cs | 3 + .../Drawing/CompositionDrawList.cs | 6 + .../Drawing/CompositionDrawingContext.cs | 3 + .../Composition/ElementCompositionPreview.cs | 10 +- .../Expressions/BuiltInExpressionFfi.cs | 17 +-- .../Expressions/DelegateExpressionFfi.cs | 3 + .../Composition/Expressions/Expression.cs | 3 + .../Expressions/ExpressionVariant.cs | 3 + .../Composition/Expressions/TokenParser.cs | 3 + .../Composition/ICompositionSurface.cs | 9 -- .../Composition/Server/DrawingContextProxy.cs | 7 ++ .../Composition/Server/FpsCounter.cs | 4 + .../Composition/Server/ReadbackIndices.cs | 5 + .../Server/ServerCompositionBrush.cs | 7 -- .../Server/ServerCompositionDrawListVisual.cs | 4 + .../Server/ServerCompositionGradientBrush.cs | 15 --- .../Server/ServerCompositionTarget.cs | 5 + .../Composition/Server/ServerCompositor.cs | 6 + .../Server/ServerContainerVisual.cs | 5 + .../Composition/Server/ServerList.cs | 4 + .../Composition/Server/ServerObject.cs | 8 +- .../Server/ServerSolidColorVisual.cs | 15 --- .../Composition/Server/ServerSpriteVisual.cs | 20 ---- .../Composition/Server/ServerVisual.cs | 17 ++- .../Rendering/Composition/Transport/Batch.cs | 3 + .../Composition/Transport/BatchStream.cs | 6 + .../Transport/ServerListProxyHelper.cs | 5 + .../Rendering/Composition/Utils/MathExt.cs | 23 ---- .../Rendering/Composition/Visual.cs | 3 + .../Rendering/Composition/VisualCollection.cs | 3 + src/Avalonia.Base/Utilities/MathUtilities.cs | 14 +++ .../Utilities/RefTrackingDictionary.cs | 3 + src/Avalonia.Base/composition-schema.xml | 35 ------ .../Markup/Parsers/ExpressionParser.cs | 3 + .../Generator.KeyFrameAnimation.cs | 2 +- 60 files changed, 565 insertions(+), 417 deletions(-) rename src/Avalonia.Base/{Rendering/Composition/Utils => Animation/Easings}/CubicBezier.cs (97%) create mode 100644 src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs index b37231243d..74ee43ed92 100644 --- a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs @@ -111,7 +111,7 @@ public partial class CompositionPage : UserControl { if (_implicitAnimations == null) { - var compositor = ElementCompositionPreview.GetElementVisual(this)!.Compositor; + var compositor = ElementComposition.GetElementVisual(this)!.Compositor; var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); offsetAnimation.Target = "Offset"; @@ -143,11 +143,11 @@ public partial class CompositionPage : UserControl return; } - if (ElementCompositionPreview.GetElementVisual(page) == null) + if (ElementComposition.GetElementVisual(page) == null) return; page.EnsureImplicitAnimations(); - ElementCompositionPreview.GetElementVisual((Visual)border.GetVisualParent()).ImplicitAnimations = + ElementComposition.GetElementVisual((Visual)border.GetVisualParent()).ImplicitAnimations = page._implicitAnimations; } diff --git a/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs b/src/Avalonia.Base/Animation/Easings/CubicBezier.cs similarity index 97% rename from src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs rename to src/Avalonia.Base/Animation/Easings/CubicBezier.cs index 8c85d7978b..5c2487a516 100644 --- a/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs +++ b/src/Avalonia.Base/Animation/Easings/CubicBezier.cs @@ -2,6 +2,7 @@ // Ported from Chromium project https://github.com/chromium/chromium/blob/374d31b7704475fa59f7b2cb836b3b68afdc3d79/ui/gfx/geometry/cubic_bezier.cc using System; +using Avalonia.Utilities; // ReSharper disable CompareOfFloatsByEqualityOperator // ReSharper disable CommentTypo @@ -10,8 +11,11 @@ using System; // ReSharper disable UnusedMember.Global #pragma warning disable 649 -namespace Avalonia.Rendering.Composition.Utils +namespace Avalonia.Animation.Easings { + /// + /// Represents a cubic bezier curve and can compute Y coordinate for a given X + /// internal unsafe struct CubicBezier { const int CUBIC_BEZIER_SPLINE_SAMPLES = 11; @@ -284,7 +288,7 @@ namespace Avalonia.Rendering.Composition.Utils public readonly double SlopeWithEpsilon(double x, double epsilon) { - x = MathExt.Clamp(x, 0.0, 1.0); + x = MathUtilities.Clamp(x, 0.0, 1.0); double t = SolveCurveX(x, epsilon); double dx = SampleCurveDerivativeX(t); double dy = SampleCurveDerivativeY(t); diff --git a/src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs b/src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs new file mode 100644 index 0000000000..e51c35c4e6 --- /dev/null +++ b/src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs @@ -0,0 +1,29 @@ +using System; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Utils; + +namespace Avalonia.Animation.Easings; + +public class CubicBezierEasing : IEasing +{ + private CubicBezier _bezier; + //cubic-bezier(0.25, 0.1, 0.25, 1.0) + internal CubicBezierEasing(Point controlPoint1, Point controlPoint2) + { + ControlPoint1 = controlPoint1; + ControlPoint2 = controlPoint2; + if (controlPoint1.X < 0 || controlPoint1.X > 1 || controlPoint2.X < 0 || controlPoint2.X > 1) + throw new ArgumentException(); + _bezier = new CubicBezier(controlPoint1.X, controlPoint1.Y, controlPoint2.X, controlPoint2.Y); + } + + public Point ControlPoint2 { get; set; } + public Point ControlPoint1 { get; set; } + + internal static IEasing Ease { get; } = new CubicBezierEasing(new Point(0.25, 0.1), new Point(0.25, 1)); + + double IEasing.Ease(double progress) + { + return _bezier.Solve(progress); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 38d114eca2..4c988c8ae1 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -35,6 +35,10 @@ - + + + + + diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs index 95bc384743..f4735040d5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs @@ -7,10 +7,15 @@ using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Animations { + /// + /// This is the first element of both animated and non-animated value stores. + /// It's used to propagate property invalidation to subscribers + /// + internal struct ServerObjectSubscriptionStore { public bool IsValid; - public RefTrackingDictionary Subscribers; + public RefTrackingDictionary? Subscribers; public void Invalidate() { @@ -23,6 +28,10 @@ namespace Avalonia.Rendering.Composition.Animations } } + /// + /// The value store for non-animated values that can still be referenced by animations. + /// Simply stores the value and notifies subscribers + /// [StructLayout(LayoutKind.Sequential)] internal struct ServerValueStore { @@ -43,7 +52,12 @@ namespace Avalonia.Rendering.Composition.Animations } } } - + + /// + /// Value store for potentially animated values. Can hold both direct value and animation instance. + /// Is also responsible for activating/deactivating the animation when container object is activated/deactivated + /// + /// [StructLayout(LayoutKind.Sequential)] internal struct ServerAnimatedValueStore where T : struct { @@ -53,8 +67,6 @@ namespace Avalonia.Rendering.Composition.Animations private T _direct; private T? _lastAnimated; - public T Direct => _direct; - public T GetAnimated(ServerCompositor compositor) { Subscriptions.IsValid = true; diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs index 212237049f..35aa8de1bc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs @@ -5,6 +5,11 @@ using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations; + +/// +/// The base class for both key-frame and expression animation instances +/// Is responsible for activation tracking and for subscribing to properties used in dependencies +/// internal abstract class AnimationInstanceBase : IAnimationInstance { private List<(ServerObject obj, int member)>? _trackedObjects; diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index cf81c6e656..c5102a2d7d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -10,51 +10,66 @@ using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition.Animations { - public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase - { - private readonly CompositionPropertySet _propertySet; - internal CompositionAnimation(Compositor compositor) : base(compositor, null!) - { - _propertySet = new CompositionPropertySet(compositor); - } - - public void ClearAllParameters() => _propertySet.ClearAll(); - - public void ClearParameter(string key) => _propertySet.Clear(key); - - void SetVariant(string key, ExpressionVariant value) => _propertySet.Set(key, value); - - public void SetColorParameter(string key, Avalonia.Media.Color value) => SetVariant(key, value); - - public void SetMatrix3x2Parameter(string key, Matrix3x2 value) => SetVariant(key, value); - - public void SetMatrix4x4Parameter(string key, Matrix4x4 value) => SetVariant(key, value); - - public void SetQuaternionParameter(string key, Quaternion value) => SetVariant(key, value); - - public void SetReferenceParameter(string key, CompositionObject compositionObject) => - _propertySet.Set(key, compositionObject); - - public void SetScalarParameter(string key, float value) => SetVariant(key, value); - - public void SetVector2Parameter(string key, Vector2 value) => SetVariant(key, value); - - public void SetVector3Parameter(string key, Vector3 value) => SetVariant(key, value); - - public void SetVector4Parameter(string key, Vector4 value) => SetVariant(key, value); - - // TODO: void SetExpressionReferenceParameter(string parameterName, IAnimationObject source) - - public string? Target { get; set; } - - internal abstract IAnimationInstance CreateInstance(ServerObject targetObject, - ExpressionVariant? finalValue); - - internal PropertySetSnapshot CreateSnapshot() => _propertySet.Snapshot(); - - void ICompositionAnimationBase.InternalOnly() - { - - } - } + /// + /// This is the base class for ExpressionAnimation and KeyFrameAnimation. + /// + /// + /// Use the method to start the animation. + /// Value parameters (as opposed to reference parameters which are set using ) + /// are copied and "embedded" into an expression at the time CompositionObject.StartAnimation is called. + /// Changing the value of the variable after is called will not affect + /// the value of the ExpressionAnimation. + /// See the remarks section of ExpressionAnimation for additional information. + /// + public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase + { + private readonly CompositionPropertySet _propertySet; + internal CompositionAnimation(Compositor compositor) : base(compositor, null!) + { + _propertySet = new CompositionPropertySet(compositor); + } + + /// + /// Clears all of the parameters of the animation. + /// + public void ClearAllParameters() => _propertySet.ClearAll(); + + /// + /// Clears a parameter from the animation. + /// + public void ClearParameter(string key) => _propertySet.Clear(key); + + void SetVariant(string key, ExpressionVariant value) => _propertySet.Set(key, value); + + public void SetColorParameter(string key, Media.Color value) => SetVariant(key, value); + + public void SetMatrix3x2Parameter(string key, Matrix3x2 value) => SetVariant(key, value); + + public void SetMatrix4x4Parameter(string key, Matrix4x4 value) => SetVariant(key, value); + + public void SetQuaternionParameter(string key, Quaternion value) => SetVariant(key, value); + + public void SetReferenceParameter(string key, CompositionObject compositionObject) => + _propertySet.Set(key, compositionObject); + + public void SetScalarParameter(string key, float value) => SetVariant(key, value); + + public void SetVector2Parameter(string key, Vector2 value) => SetVariant(key, value); + + public void SetVector3Parameter(string key, Vector3 value) => SetVariant(key, value); + + public void SetVector4Parameter(string key, Vector4 value) => SetVariant(key, value); + + public string? Target { get; set; } + + internal abstract IAnimationInstance CreateInstance(ServerObject targetObject, + ExpressionVariant? finalValue); + + internal PropertySetSnapshot CreateSnapshot() => _propertySet.Snapshot(); + + void ICompositionAnimationBase.InternalOnly() + { + + } + } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs index 6a2c07e6ef..163f4e99ba 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs @@ -5,6 +5,17 @@ using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { + /// + /// A Composition Animation that uses a mathematical equation to calculate the value for an animating property every frame. + /// + /// + /// The core of ExpressionAnimations allows a developer to define a mathematical equation that can be used to calculate the value + /// of a targeted animating property each frame. + /// This contrasts s, which use an interpolator to define how the animating + /// property changes over time. The mathematical equation can be defined using references to properties + /// of Composition objects, mathematical functions and operators and Input. + /// Use the method to start the animation. + /// public class ExpressionAnimation : CompositionAnimation { private string? _expression; @@ -14,6 +25,14 @@ namespace Avalonia.Rendering.Composition.Animations { } + /// + /// The mathematical equation specifying how the animated value is calculated each frame. + /// The Expression is the core of an and represents the equation + /// the system will use to calculate the value of the animation property each frame. + /// The equation is set on this property in the form of a string. + /// Although expressions can be defined by simple mathematical equations such as "2+2", + /// the real power lies in creating mathematical relationships where the input values can change frame over frame. + /// public string? Expression { get => _expression; diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs index 7944fe7990..445cef9a08 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs @@ -5,6 +5,10 @@ using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { + + /// + /// Server-side counterpart of with values baked-in. + /// internal class ExpressionAnimationInstance : AnimationInstanceBase, IAnimationInstance { private readonly Expression _expression; diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs index bf40fd3ad2..87e5ad757a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs @@ -4,6 +4,9 @@ using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { + /// + /// Base class for composition animations. + /// public interface ICompositionAnimationBase { internal void InternalOnly(); diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs index fa5b69dae9..f4bcc6ff38 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs @@ -6,6 +6,17 @@ using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition.Animations { + /// + /// A collection of animations triggered when a condition is met. + /// + /// + /// Implicit animations let you drive animations by specifying trigger conditions rather than requiring the manual definition of animation behavior. + /// They help decouple animation start logic from core app logic. You define animations and the events that should trigger these animations. + /// Currently the only available trigger is animated property change. + /// + /// When expression is used in ImplicitAnimationCollection a special keyword `this.FinalValue` will represent + /// the final value of the animated property that was changed + /// public class ImplicitAnimationCollection : CompositionObject, IDictionary { private Dictionary _inner = new Dictionary(); diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs index 62b790701a..a4eeacef32 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs @@ -3,6 +3,9 @@ using System.Numerics; namespace Avalonia.Rendering.Composition.Animations { + /// + /// An interface to define interpolation logic for a particular type + /// internal interface IInterpolator { T Interpolate(T from, T to, float progress); diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs index 065dfd7a8e..49b3ab753a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs @@ -1,53 +1,134 @@ +using System; +using Avalonia.Animation; +using Avalonia.Animation.Easings; + namespace Avalonia.Rendering.Composition.Animations { + + /// + /// A time-based animation with one or more key frames. + /// These frames are markers, allowing developers to specify values at specific times for the animating property. + /// KeyFrame animations can be further customized by specifying how the animation interpolates between keyframes. + /// public abstract class KeyFrameAnimation : CompositionAnimation { + private TimeSpan _duration = TimeSpan.FromMilliseconds(1); + internal KeyFrameAnimation(Compositor compositor) : base(compositor) { } + /// + /// The delay behavior of the key frame animation. + /// public AnimationDelayBehavior DelayBehavior { get; set; } + + /// + /// Delay before the animation starts after is called. + /// public System.TimeSpan DelayTime { get; set; } - public AnimationDirection Direction { get; set; } - public System.TimeSpan Duration { get; set; } + + /// + /// The direction the animation is playing. + /// The Direction property allows you to drive your animation from start to end or end to start or alternate + /// between start and end or end to start if animation has an greater than one. + /// This gives an easy way for customizing animation definitions. + /// + public PlaybackDirection Direction { get; set; } + + /// + /// The duration of the animation. + /// Minimum allowed value is 1ms and maximum allowed value is 24 days. + /// + public TimeSpan Duration + { + get => _duration; + set + { + if (_duration < TimeSpan.FromMilliseconds(1) || _duration > TimeSpan.FromDays(1)) + throw new ArgumentException("Minimum allowed value is 1ms and maximum allowed value is 24 days."); + _duration = value; + } + } + + /// + /// The iteration behavior for the key frame animation. + /// public AnimationIterationBehavior IterationBehavior { get; set; } + + /// + /// The number of times to repeat the key frame animation. + /// public int IterationCount { get; set; } = 1; + + /// + /// Specifies how to set the property value when animation is stopped + /// public AnimationStopBehavior StopBehavior { get; set; } private protected abstract IKeyFrames KeyFrames { get; } + /// + /// Inserts an expression keyframe. + /// + /// + /// The time the key frame should occur at, expressed as a percentage of the animation Duration. Allowed value is from 0.0 to 1.0. + /// + /// The expression used to calculate the value of the key frame. + /// The easing function to use when interpolating between frames. public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, - CompositionEasingFunction easingFunction) => - KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, easingFunction); - - public void InsertExpressionKeyFrame(float normalizedProgressKey, string value) - => KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, new LinearEasingFunction(Compositor)); + Easing? easingFunction = null) => + KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, easingFunction ?? Compositor.DefaultEasing); } + /// + /// Specifies the animation delay behavior. + /// public enum AnimationDelayBehavior { + /// + /// If a DelayTime is specified, it delays starting the animation according to delay time and after delay + /// has expired it applies animation to the object property. + /// SetInitialValueAfterDelay, + /// + /// Applies the initial value of the animation (i.e. the value at Keyframe 0) to the object before the delay time + /// is elapsed (when there is a DelayTime specified), it then delays starting the animation according to the DelayTime. + /// SetInitialValueBeforeDelay } - public enum AnimationDirection - { - Normal, - Reverse, - Alternate, - AlternateReverse - } - + /// + /// Specifies if the animation should loop. + /// public enum AnimationIterationBehavior { + /// + /// The animation should loop the specified number of times. + /// Count, + /// + /// The animation should loop forever. + /// Forever } + /// + /// Specifies the behavior of an animation when it stops. + /// public enum AnimationStopBehavior { + /// + /// Leave the animation at its current value. + /// LeaveCurrentValue, + /// + /// Reset the animation to its initial value. + /// SetToInitialValue, + /// + /// Set the animation to its final value. + /// SetToFinalValue } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs index 9571cef0b4..7268780298 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs @@ -1,10 +1,14 @@ using System; using System.Collections.Generic; +using Avalonia.Animation; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { + /// + /// Server-side counterpart of KeyFrameAnimation with values baked-in + /// class KeyFrameAnimationInstance : AnimationInstanceBase, IAnimationInstance where T : struct { private readonly IInterpolator _interpolator; @@ -12,7 +16,7 @@ namespace Avalonia.Rendering.Composition.Animations private readonly ExpressionVariant? _finalValue; private readonly AnimationDelayBehavior _delayBehavior; private readonly TimeSpan _delayTime; - private readonly AnimationDirection _direction; + private readonly PlaybackDirection _direction; private readonly TimeSpan _duration; private readonly AnimationIterationBehavior _iterationBehavior; private readonly int _iterationCount; @@ -27,7 +31,7 @@ namespace Avalonia.Rendering.Composition.Animations PropertySetSnapshot snapshot, ExpressionVariant? finalValue, ServerObject target, AnimationDelayBehavior delayBehavior, TimeSpan delayTime, - AnimationDirection direction, TimeSpan duration, + PlaybackDirection direction, TimeSpan duration, AnimationIterationBehavior iterationBehavior, int iterationCount, AnimationStopBehavior stopBehavior) : base(target, snapshot) { @@ -96,11 +100,11 @@ namespace Avalonia.Rendering.Composition.Animations elapsed = TimeSpan.FromTicks(elapsed.Ticks % _duration.Ticks); var reverse = - _direction == AnimationDirection.Alternate + _direction == PlaybackDirection.Alternate ? !evenIterationNumber - : _direction == AnimationDirection.AlternateReverse + : _direction == PlaybackDirection.AlternateReverse ? evenIterationNumber - : _direction == AnimationDirection.Reverse; + : _direction == PlaybackDirection.Reverse; var iterationProgress = elapsed.TotalSeconds / _duration.TotalSeconds; if (reverse) @@ -128,7 +132,7 @@ namespace Avalonia.Rendering.Composition.Animations var keyProgress = Math.Max(0, Math.Min(1, (iterationProgress - left.Key) / (right.Key - left.Key))); - var easedKeyProgress = right.EasingFunction.Ease((float) keyProgress); + var easedKeyProgress = (float)right.EasingFunction.Ease(keyProgress); if (float.IsNaN(easedKeyProgress) || float.IsInfinity(easedKeyProgress)) return currentValue; diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs index 26ba35409d..369cc80b95 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs @@ -1,9 +1,15 @@ using System; using System.Collections.Generic; +using Avalonia.Animation.Easings; using Avalonia.Rendering.Composition.Expressions; namespace Avalonia.Rendering.Composition.Animations { + + /// + /// Collection of composition animation key frames + /// + /// class KeyFrames : List>, IKeyFrames { void Validate(float key) @@ -14,8 +20,7 @@ namespace Avalonia.Rendering.Composition.Animations throw new ArgumentException("Key frame key " + key + " is less than the previous one"); } - public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, - CompositionEasingFunction easingFunction) + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, IEasing easingFunction) { Validate(normalizedProgressKey); Add(new KeyFrame @@ -26,7 +31,7 @@ namespace Avalonia.Rendering.Composition.Animations }); } - public void Insert(float normalizedProgressKey, T value, CompositionEasingFunction easingFunction) + public void Insert(float normalizedProgressKey, T value, IEasing easingFunction) { Validate(normalizedProgressKey); Add(new KeyFrame @@ -47,7 +52,7 @@ namespace Avalonia.Rendering.Composition.Animations { Expression = f.Expression, Value = f.Value, - EasingFunction = f.EasingFunction.Snapshot(), + EasingFunction = f.EasingFunction, Key = f.NormalizedProgressKey }; } @@ -55,26 +60,30 @@ namespace Avalonia.Rendering.Composition.Animations } } + /// + /// Composition animation key frame + /// struct KeyFrame { public float NormalizedProgressKey; public T Value; public Expression Expression; - public CompositionEasingFunction EasingFunction; + public IEasing EasingFunction; } + /// + /// Server-side composition animation key frame + /// struct ServerKeyFrame { public T Value; public Expression? Expression; - public IEasingFunction EasingFunction; + public IEasing EasingFunction; public float Key; } - - interface IKeyFrames { - public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, CompositionEasingFunction easingFunction); + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, IEasing easingFunction); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs index ca703dfc6f..fc6cfc9f3d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs @@ -3,6 +3,9 @@ using Avalonia.Rendering.Composition.Expressions; namespace Avalonia.Rendering.Composition.Animations { + /// + /// A snapshot of properties used by an animation + /// internal class PropertySetSnapshot : IExpressionParameterCollection, IExpressionObject { private readonly Dictionary _dic; diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 8cb7daca6b..5e8e9c24f9 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -13,7 +13,10 @@ using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition; -public class CompositingRenderer : RendererBase, IRendererWithCompositor +/// +/// A renderer that utilizes to render the visual tree +/// +public class CompositingRenderer : IRendererWithCompositor { private readonly IRenderRoot _root; private readonly Compositor _compositor; @@ -24,9 +27,10 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor private readonly CompositionTarget _target; private bool _queuedUpdate; private Action _update; + private Action _invalidateScene; /// - /// Forces the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered + /// Asks the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered. /// public bool RenderOnlyOnRenderThread { get; set; } = true; @@ -39,20 +43,24 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor _target = compositor.CreateCompositionTarget(root.CreateRenderTarget); _target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor); _update = Update; + _invalidateScene = InvalidateScene; } + /// public bool DrawFps { get => _target.DrawFps; set => _target.DrawFps = value; } - + + /// public bool DrawDirtyRects { get => _target.DrawDirtyRects; set => _target.DrawDirtyRects = value; } + /// public event EventHandler? SceneInvalidated; void QueueUpdate() @@ -62,12 +70,15 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor _queuedUpdate = true; Dispatcher.UIThread.Post(_update, DispatcherPriority.Composition); } + + /// public void AddDirty(IVisual visual) { _dirty.Add((Visual)visual); QueueUpdate(); } + /// public IEnumerable HitTest(Point p, IVisual root, Func? filter) { var res = _target.TryHitTest(p, filter); @@ -84,12 +95,14 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor } } + /// public IVisual? HitTestFirst(Point p, IVisual root, Func? filter) { // TODO: Optimize return HitTest(p, root, filter).FirstOrDefault(); } + /// public void RecalculateChildren(IVisual visual) { _recalculateChildren.Add((Visual)visual); @@ -172,7 +185,10 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor compositionChildren.Add(compositionChild); } } - + + private void InvalidateScene() => + SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize))); + private void Update() { _queuedUpdate = false; @@ -223,6 +239,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor _recalculateChildren.Clear(); _target.Size = _root.ClientSize; _target.Scaling = _root.RenderScaling; + Compositor.InvokeOnNextCommit(_invalidateScene); } public void Resized(Size size) @@ -257,6 +274,8 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor _compositor.RequestCommitAsync().Wait(); } - + /// + /// The associated object + /// public Compositor Compositor => _compositor; } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index cc2b411822..47cfcd325b 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -7,12 +7,23 @@ using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition; + +/// +/// A composition visual that holds a list of drawing commands issued by +/// internal class CompositionDrawListVisual : CompositionContainerVisual { + /// + /// The associated + /// public Visual Visual { get; } private bool _drawListChanged; private CompositionDrawList? _drawList; + + /// + /// The list of drawing commands + /// public CompositionDrawList? DrawList { get => _drawList; diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs deleted file mode 100644 index 90b2bec268..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Numerics; -using Avalonia.Rendering.Composition.Transport; -using Avalonia.Rendering.Composition.Utils; - -namespace Avalonia.Rendering.Composition -{ - public abstract class CompositionEasingFunction : CompositionObject - { - internal CompositionEasingFunction(Compositor compositor) : base(compositor, null!) - { - } - - internal abstract IEasingFunction Snapshot(); - } - - internal interface IEasingFunction - { - float Ease(float progress); - } - - public sealed class DelegateCompositionEasingFunction : CompositionEasingFunction - { - private readonly Easing _func; - - public delegate float EasingDelegate(float progress); - - internal DelegateCompositionEasingFunction(Compositor compositor, EasingDelegate func) : base(compositor) - { - _func = new Easing(func); - } - - class Easing : IEasingFunction - { - private readonly EasingDelegate _func; - - public Easing(EasingDelegate func) - { - _func = func; - } - - public float Ease(float progress) => _func(progress); - } - - internal override IEasingFunction Snapshot() => _func; - } - - public class LinearEasingFunction : CompositionEasingFunction - { - public LinearEasingFunction(Compositor compositor) : base(compositor) - { - } - - class Linear : IEasingFunction - { - public float Ease(float progress) => progress; - } - - private static readonly Linear Instance = new Linear(); - internal override IEasingFunction Snapshot() => Instance; - } - - public class CubicBezierEasingFunction : CompositionEasingFunction - { - private CubicBezier _bezier; - public Vector2 ControlPoint1 { get; } - public Vector2 ControlPoint2 { get; } - //cubic-bezier(0.25, 0.1, 0.25, 1.0) - internal CubicBezierEasingFunction(Compositor compositor, Vector2 controlPoint1, Vector2 controlPoint2) : base(compositor) - { - ControlPoint1 = controlPoint1; - ControlPoint2 = controlPoint2; - if (controlPoint1.X < 0 || controlPoint1.X > 1 || controlPoint2.X < 0 || controlPoint2.X > 1) - throw new ArgumentException(); - _bezier = new CubicBezier(controlPoint1.X, controlPoint1.Y, controlPoint2.X, controlPoint2.Y); - } - - class EasingFunction : IEasingFunction - { - private readonly CubicBezier _bezier; - - public EasingFunction(CubicBezier bezier) - { - _bezier = bezier; - } - - public float Ease(float progress) => (float)_bezier.Solve(progress); - } - - internal static IEasingFunction Ease { get; } = new EasingFunction(new CubicBezier(0.25, 0.1, 0.25, 1)); - - internal override IEasingFunction Snapshot() => new EasingFunction(_bezier); - } - -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs deleted file mode 100644 index cf222550dd..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition -{ - public partial class CompositionGradientBrush : CompositionBrush - { - internal CompositionGradientBrush(Compositor compositor, ServerCompositionGradientBrush server) : base(compositor, server) - { - ColorStops = new CompositionGradientStopCollection(compositor, server.Stops); - } - - public CompositionGradientStopCollection ColorStops { get; } - } - - -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs index baf1bfcddf..0ed9a3c75d 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -6,8 +6,16 @@ using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition { + /// + /// Base class of the composition API representing a node in the visual tree structure. + /// Composition objects are the visual tree structure on which all other features of the composition API use and build on. + /// The API allows developers to define and create one or many objects each representing a single node in a Visual tree. + /// public abstract class CompositionObject : IDisposable { + /// + /// The collection of implicit animations attached to this object. + /// public ImplicitAnimationCollection? ImplicitAnimations { get; set; } internal CompositionObject(Compositor compositor, ServerObject server) { @@ -15,6 +23,9 @@ namespace Avalonia.Rendering.Composition Server = server; } + /// + /// The associated Compositor + /// public Compositor Compositor { get; } internal ServerObject Server { get; } public bool IsDisposed { get; private set; } @@ -29,14 +40,22 @@ namespace Avalonia.Rendering.Composition IsDisposed = true; } + /// + /// Connects an animation with the specified property of the object and starts the animation. + /// public void StartAnimation(string propertyName, CompositionAnimation animation) => StartAnimation(propertyName, animation, null); - internal virtual void StartAnimation(string propertyName, CompositionAnimation animation, ExpressionVariant? finalValue = null) + internal virtual void StartAnimation(string propertyName, CompositionAnimation animation, ExpressionVariant? finalValue) { throw new ArgumentException("Unknown property " + propertyName); } + /// + /// Starts an animation group. + /// The StartAnimationGroup method on CompositionObject lets you start CompositionAnimationGroup. + /// All the animations in the group will be started at the same time on the object. + /// public void StartAnimationGroup(ICompositionAnimationBase grp) { if (grp is CompositionAnimation animation) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs index 584969cbc0..ee4552d154 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -7,6 +7,15 @@ using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition { + /// + /// s are s that allow storage of key values pairs + /// that can be shared across the application and are not tied to the lifetime of another composition object. + /// s are most commonly used with animations, where they maintain key-value pairs + /// that are referenced to drive portions of composition animations. s + /// provide the ability to insert key-value pairs or retrieve a value for a given key. + /// does not support a delete function – ensure you use + /// to store values that will be shared across the application. + /// public class CompositionPropertySet : CompositionObject { private readonly Dictionary _variants = new Dictionary(); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index 7e50c43ac1..25bbd4dc88 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -6,6 +6,9 @@ using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition { + /// + /// Represents the composition output (e. g. a window, embedded control, entire screen) + /// public partial class CompositionTarget { partial void OnRootChanged() @@ -20,6 +23,12 @@ namespace Avalonia.Rendering.Composition Root.Root = null; } + /// + /// Attempts to perform a hit-tst + /// + /// + /// + /// public PooledList? TryHitTest(Point point, Func? filter) { Server.Readback.NextRead(); @@ -30,6 +39,10 @@ namespace Avalonia.Rendering.Composition return res; } + /// + /// Attempts to transform a point to a particular CompositionVisual coordinate space + /// + /// public Point? TryTransformToVisual(CompositionVisual visual, Point point) { if (visual.Root != this) @@ -108,8 +121,14 @@ namespace Avalonia.Rendering.Composition } + /// + /// Registers the composition target for explicit redraw + /// public void RequestRedraw() => RegisterForSerialization(); + /// + /// Performs composition directly on the UI thread + /// internal void ImmediateUIThreadRender() { Compositor.RequestCommitAsync(); diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index e92b69bd60..28c81dfb91 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Numerics; using System.Threading.Tasks; +using Avalonia.Animation.Easings; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; @@ -13,6 +14,10 @@ using Avalonia.Threading; namespace Avalonia.Rendering.Composition { + /// + /// The Compositor class manages communication between UI-thread and render-thread parts of the composition engine. + /// It also serves as a factory to create UI-thread parts of various composition objects + /// public partial class Compositor { internal IRenderLoop Loop { get; } @@ -23,22 +28,38 @@ namespace Avalonia.Rendering.Composition private BatchStreamMemoryPool _batchMemoryPool = new(); private List _objectsForSerialization = new(); internal ServerCompositor Server => _server; - internal CompositionEasingFunction DefaultEasing { get; } - + internal IEasing DefaultEasing { get; } + private List? _invokeOnNextCommit; + private readonly Stack> _invokeListPool = new(); + + /// + /// Creates a new compositor on a specified render loop that would use a particular GPU + /// + /// + /// public Compositor(IRenderLoop loop, IPlatformGpu? gpu) { Loop = loop; _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool); _implicitBatchCommit = ImplicitBatchCommit; - DefaultEasing = new CubicBezierEasingFunction(this, - new Vector2(0.25f, 0.1f), new Vector2(0.25f, 1f)); + + DefaultEasing = new CubicBezierEasing(new Point(0.25f, 0.1f), new Point(0.25f, 1f)); } + /// + /// Creates a new CompositionTarget + /// + /// A factory method to create IRenderTarget to be called from the render thread + /// public CompositionTarget CreateCompositionTarget(Func renderTargetFactory) { return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory)); } + /// + /// Requests pending changes in the composition objects to be serialized and sent to the render thread + /// + /// A task that completes when sent changes are applied and rendered on the render thread public Task RequestCommitAsync() { var batch = new Batch(); @@ -59,64 +80,25 @@ namespace Avalonia.Rendering.Composition batch.CommitedAt = Server.Clock.Elapsed; _server.EnqueueBatch(batch); + if (_invokeOnNextCommit != null) + ScheduleCommitCallbacks(batch.Completed); + return batch.Completed; } - public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); - - public CompositionSolidColorVisual CreateSolidColorVisual() => new CompositionSolidColorVisual(this, - new ServerCompositionSolidColorVisual(_server)); - - public CompositionSolidColorVisual CreateSolidColorVisual(Avalonia.Media.Color color) - { - var v = new CompositionSolidColorVisual(this, new ServerCompositionSolidColorVisual(_server)); - v.Color = color; - return v; - } - - public CompositionSpriteVisual CreateSpriteVisual() => new CompositionSpriteVisual(this, new ServerCompositionSpriteVisual(_server)); - - public CompositionLinearGradientBrush CreateLinearGradientBrush() - => new CompositionLinearGradientBrush(this, new ServerCompositionLinearGradientBrush(_server)); - - public CompositionColorGradientStop CreateColorGradientStop() - => new CompositionColorGradientStop(this, new ServerCompositionColorGradientStop(_server)); - - public CompositionColorGradientStop CreateColorGradientStop(float offset, Avalonia.Media.Color color) - { - var stop = CreateColorGradientStop(); - stop.Offset = offset; - stop.Color = color; - return stop; - } - - // We want to make it 100% async later - /* - public CompositionBitmapSurface LoadBitmapSurface(Stream stream) + async void ScheduleCommitCallbacks(Task task) { - var bmp = _server.Backend.LoadCpuMemoryBitmap(stream); - return new CompositionBitmapSurface(this, bmp); + var list = _invokeOnNextCommit; + _invokeOnNextCommit = null; + await task; + foreach (var i in list!) + i(); + list.Clear(); + _invokeListPool.Push(list); } - public async Task LoadBitmapSurfaceAsync(Stream stream) - { - var bmp = await Task.Run(() => _server.Backend.LoadCpuMemoryBitmap(stream)); - return new CompositionBitmapSurface(this, bmp); - } - */ - public CompositionColorBrush CreateColorBrush(Avalonia.Media.Color color) => - new CompositionColorBrush(this, new ServerCompositionColorBrush(_server)) {Color = color}; - - public CompositionSurfaceBrush CreateSurfaceBrush() => - new CompositionSurfaceBrush(this, new ServerCompositionSurfaceBrush(_server)); - - /* - public CompositionGaussianBlurEffectBrush CreateGaussianBlurEffectBrush() => - new CompositionGaussianBlurEffectBrush(this, new ServerCompositionGaussianBlurEffectBrush(_server)); - - public CompositionBackdropBrush CreateBackdropBrush() => - new CompositionBackdropBrush(this, new ServerCompositionBackdropBrush(Server));*/ - + public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); + public ExpressionAnimation CreateExpressionAnimation() => new ExpressionAnimation(this); public ExpressionAnimation CreateExpressionAnimation(string expression) => new ExpressionAnimation(this) @@ -147,5 +129,11 @@ namespace Avalonia.Rendering.Composition _objectsForSerialization.Add(compositionObject); QueueImplicitBatchCommit(); } + + internal void InvokeOnNextCommit(Action action) + { + _invokeOnNextCommit ??= _invokeListPool.Count > 0 ? _invokeListPool.Pop() : new(); + _invokeOnNextCommit.Add(action); + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs b/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs deleted file mode 100644 index 074c0a9ccf..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Avalonia.Rendering.Composition; - -partial class Compositor -{ - class CompositorRenderLoopTask : IRenderLoopTask - { - public bool NeedsUpdate { get; } - public void Update(TimeSpan time) - { - throw new NotImplementedException(); - } - - public void Render() - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs index 5b2a4be1bc..caf074dd6b 100644 --- a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs @@ -2,6 +2,9 @@ using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition { + /// + /// A node in the visual tree that can have children. + /// public partial class CompositionContainerVisual : CompositionVisual { public CompositionVisualCollection Children { get; private set; } = null!; diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs index 1d416f5a8a..315faf2c86 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs @@ -6,6 +6,9 @@ using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Drawing; +/// +/// A list of serialized drawing commands +/// internal class CompositionDrawList : PooledList> { public Size? Size { get; set; } @@ -47,6 +50,9 @@ internal class CompositionDrawList : PooledList> } } +/// +/// An helper class for building +/// internal class CompositionDrawListBuilder { private CompositionDrawList? _operations; diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index 0c5a98b239..e678a85fcf 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -10,6 +10,9 @@ using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition; +/// +/// An IDrawingContextImpl implementation that builds +/// internal class CompositionDrawingContext : IDrawingContextImpl { private CompositionDrawListBuilder _builder = new(); diff --git a/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs b/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs index afda314276..1397a20fb6 100644 --- a/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs +++ b/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs @@ -1,6 +1,14 @@ namespace Avalonia.Rendering.Composition; -public static class ElementCompositionPreview +/// +/// Enables access to composition visual objects that back XAML elements in the XAML composition tree. +/// +public static class ElementComposition { + /// + /// Gets CompositionVisual that backs a Visual + /// + /// + /// public static CompositionVisual? GetElementVisual(Visual visual) => visual.CompositionVisual; } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs index db9a26e301..44347d2c7a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs @@ -2,10 +2,13 @@ using System; using System.Collections.Generic; using System.Numerics; using Avalonia.Rendering.Composition.Animations; -using Avalonia.Rendering.Composition.Utils; +using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Expressions { + /// + /// Built-in functions for Foreign Function Interface available from composition animation expressions + /// internal class BuiltInExpressionFfi : IExpressionForeignFunctionInterface { private readonly DelegateExpressionFfi _registry; @@ -26,7 +29,7 @@ namespace Avalonia.Rendering.Composition.Expressions static float SmoothStep(float edge0, float edge1, float x) { - var t = MathExt.Clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + var t = MathUtilities.Clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); return t * t * (3.0f - 2.0f * t); } @@ -72,7 +75,7 @@ namespace Avalonia.Rendering.Composition.Expressions {"ATan", (float f) => (float) Math.Atan(f)}, {"Ceil", (float f) => (float) Math.Ceiling(f)}, - {"Clamp", (float a1, float a2, float a3) => MathExt.Clamp(a1, a2, a3)}, + {"Clamp", (float a1, float a2, float a3) => MathUtilities.Clamp(a1, a2, a3)}, {"Clamp", (Vector2 a1, Vector2 a2, Vector2 a3) => Vector2.Clamp(a1, a2, a3)}, {"Clamp", (Vector3 a1, Vector3 a2, Vector3 a3) => Vector3.Clamp(a1, a2, a3)}, {"Clamp", (Vector4 a1, Vector4 a2, Vector4 a3) => Vector4.Clamp(a1, a2, a3)}, @@ -96,10 +99,10 @@ namespace Avalonia.Rendering.Composition.Expressions }, { "ColorRGB", (float a, float r, float g, float b) => Avalonia.Media.Color.FromArgb( - (byte) MathExt.Clamp(a, 0, 255), - (byte) MathExt.Clamp(r, 0, 255), - (byte) MathExt.Clamp(g, 0, 255), - (byte) MathExt.Clamp(b, 0, 255) + (byte) MathUtilities.Clamp(a, 0, 255), + (byte) MathUtilities.Clamp(r, 0, 255), + (byte) MathUtilities.Clamp(g, 0, 255), + (byte) MathUtilities.Clamp(b, 0, 255) ) }, diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs index 002cf37522..85c6141409 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs @@ -7,6 +7,9 @@ using Avalonia.Media; namespace Avalonia.Rendering.Composition.Expressions { + /// + /// Foreign function interface for composition animations based on calling delegates + /// internal class DelegateExpressionFfi : IExpressionForeignFunctionInterface, IEnumerable { struct FfiRecord diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs index 088771e1ba..5abba00365 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -6,6 +6,9 @@ using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Expressions { + /// + /// A parsed composition expression + /// internal abstract class Expression { public abstract ExpressionType Type { get; } diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs index 086c8ce276..7b900534d8 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs @@ -22,6 +22,9 @@ namespace Avalonia.Rendering.Composition.Expressions Color } + /// + /// A VARIANT type used in expression animations. Can represent multiple value types + /// [StructLayout(LayoutKind.Explicit)] internal struct ExpressionVariant { diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs index 1050c7274c..27782c8c2c 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs @@ -3,6 +3,9 @@ using System.Globalization; namespace Avalonia.Rendering.Composition.Expressions { + /// + /// Helper class for composition expression parser + /// internal ref struct TokenParser { private ReadOnlySpan _s; diff --git a/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs deleted file mode 100644 index 9ef31c30e0..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition -{ - public interface ICompositionSurface - { - internal ServerCompositionSurface Server { get; } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 8b6ac5b0c2..1b85159b02 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -8,6 +8,13 @@ using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server; +/// +/// A bunch of hacks to make the existing rendering operations and IDrawingContext +/// to work with composition rendering infrastructure. +/// 1) Keeps and applies the transform of the current visual since drawing operations think that +/// they have information about the full render transform (they are not) +/// 2) Keeps the draw list for the VisualBrush contents of the current drawing operation. +/// internal class CompositorDrawingContextProxy : IDrawingContextImpl { private IDrawingContextImpl _impl; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index a60084d8f3..a09de2c0ff 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -8,6 +8,9 @@ using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server; +/// +/// An FPS counter helper that can draw itself on the render thread +/// internal class FpsCounter { private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); @@ -17,6 +20,7 @@ internal class FpsCounter private TimeSpan _lastFpsUpdate; const int FirstChar = 32; const int LastChar = 126; + // ASCII chars private GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1]; public FpsCounter(GlyphTypeface typeface) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs index 1971451811..c9592b70ab 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs @@ -1,5 +1,10 @@ namespace Avalonia.Rendering.Composition.Server { + /// + /// A helper class used to manage the current slots for writing data from the render thread + /// and reading it from the UI thread. + /// Used mostly by hit-testing which needs to know the last transform of the visual + /// internal class ReadbackIndices { private readonly object _lock = new object(); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs deleted file mode 100644 index eb041aaf88..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Avalonia.Rendering.Composition.Server -{ - internal abstract partial class ServerCompositionBrush : ServerObject - { - - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index 45062632c7..eff1af65e2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -9,9 +9,13 @@ using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server; +/// +/// Server-side counterpart of +/// internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual { #if DEBUG + // This is needed for debugging purposes so we could see inspect the associated visual from debugger public readonly Visual UiVisual; #endif private CompositionDrawList? _renderCommands; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs deleted file mode 100644 index 0948b9692f..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Avalonia.Rendering.Composition.Server -{ - internal abstract partial class ServerCompositionGradientBrush : ServerCompositionBrush - { - public ServerCompositionGradientStopCollection Stops { get; } - public ServerCompositionGradientBrush(ServerCompositor compositor) : base(compositor) - { - Stops = new ServerCompositionGradientStopCollection(compositor); - } - - public override long LastChangedBy => Math.Max(base.LastChangedBy, (long)Stops.LastChangedBy); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 24a5f0294a..8b903c4382 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -10,6 +10,10 @@ using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server { + /// + /// Server-side counterpart of the + /// That's the place where we update visual transforms, track dirty rects and actually do rendering + /// internal partial class ServerCompositionTarget : IDisposable { private readonly ServerCompositor _compositor; @@ -172,6 +176,7 @@ namespace Avalonia.Rendering.Composition.Server _renderTarget?.Dispose(); _renderTarget = null; } + _compositor.RemoveCompositionTarget(this); } public void AddVisual(ServerCompositionVisual visual) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 811fffd8eb..73792fdf98 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -8,6 +8,12 @@ using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition.Server { + /// + /// Server-side counterpart of the . + /// 1) manages deserialization of changes received from the UI thread + /// 2) triggers animation ticks + /// 3) asks composition targets to render themselves + /// internal class ServerCompositor : IRenderLoopTask { private readonly IRenderLoop _renderLoop; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs index bcfcfbe4f2..136ebc1d63 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -3,6 +3,11 @@ using Avalonia.Platform; namespace Avalonia.Rendering.Composition.Server { + /// + /// Server-side counterpart of . + /// Mostly propagates update and render calls, but is also responsible + /// for updating adorners in deferred manner + /// internal partial class ServerCompositionContainerVisual : ServerCompositionVisual { public ServerCompositionVisualCollection Children { get; private set; } = null!; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs index 4beea4715b..39d6a8dc70 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs @@ -4,6 +4,10 @@ using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition.Server { + /// + /// A server-side list container capable of receiving changes from the UI thread + /// Right now it's quite dumb since it always receives the full list + /// class ServerList : ServerObject where T : ServerObject { public List List { get; } = new List(); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index f55c9439e4..e5ae7fa859 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -9,6 +9,10 @@ using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server { + /// + /// Server-side counterpart. + /// Is responsible for animation activation and invalidation + /// internal abstract class ServerObject : IExpressionObject { public ServerCompositor Compositor { get; } @@ -92,13 +96,15 @@ namespace Avalonia.Rendering.Composition.Server public void SubscribeToInvalidation(int member, IAnimationInstance animation) { ref var store = ref GetStoreFromOffset(member); + if (store.Subscribers == null) + store.Subscribers = new(); store.Subscribers.AddRef(animation); } public void UnsubscribeFromInvalidation(int member, IAnimationInstance animation) { ref var store = ref GetStoreFromOffset(member); - store.Subscribers.ReleaseRef(animation); + store.Subscribers?.ReleaseRef(animation); } public virtual int? GetFieldOffset(string fieldName) => null; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs deleted file mode 100644 index 5720d80304..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Numerics; -using Avalonia.Media.Immutable; -using Avalonia.Platform; - -namespace Avalonia.Rendering.Composition.Server -{ - internal partial class ServerCompositionSolidColorVisual - { - protected override void RenderCore(CompositorDrawingContextProxy canvas) - { - canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new RoundedRect(new Rect(new Size(Size)))); - base.RenderCore(canvas); - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs deleted file mode 100644 index 2f4c446cfa..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Numerics; -using Avalonia.Platform; - -namespace Avalonia.Rendering.Composition.Server -{ - internal partial class ServerCompositionSpriteVisual - { - - protected override void RenderCore(CompositorDrawingContextProxy canvas) - { - if (Brush != null) - { - //SetTransform(canvas, transform); - //canvas.FillRect((Vector2)Size, (ICbBrush)Brush.Brush!); - } - - base.RenderCore(canvas); - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index d42bcae7e7..0c58647319 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -4,7 +4,13 @@ using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition.Server { - unsafe partial class ServerCompositionVisual : ServerObject + /// + /// Server-side counterpart. + /// Is responsible for computing the transformation matrix, for applying various visual + /// properties before calling visual-specific drawing code and for notifying the + /// for new dirty rects + /// + partial class ServerCompositionVisual : ServerObject { private bool _isDirty; private bool _isBackface; @@ -51,7 +57,10 @@ namespace Avalonia.Rendering.Composition.Server private ReadbackData _readback0, _readback1, _readback2; - + /// + /// Obtains "readback" data - the data that is sent from the render thread to the UI thread + /// in non-blocking manner. Used mostly by hit-testing + /// public ref ReadbackData GetReadback(int idx) { if (idx == 0) @@ -119,6 +128,9 @@ namespace Avalonia.Rendering.Composition.Server } + /// + /// Data that can be read from the UI thread + /// public struct ReadbackData { public Matrix4x4 Matrix; @@ -126,7 +138,6 @@ namespace Avalonia.Rendering.Composition.Server public long TargetId; public bool Visible; } - partial void DeserializeChangesExtra(BatchStreamReader c) { diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs index 0714db5781..e69768d3bf 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs @@ -6,6 +6,9 @@ using System.Threading.Tasks; namespace Avalonia.Rendering.Composition.Transport { + /// + /// Represents a group of serialized changes from the UI thread to be atomically applied at the render thread + /// internal class Batch { private static long _nextSequenceId = 1; diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs index 9e9ed739fb..65237473fb 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -7,6 +7,12 @@ using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Transport; +/// +/// The batch data is separated into 2 "streams": +/// - objects: CLR reference types that are references to either server-side or common objects +/// - structs: blittable types like int, Matrix, Color +/// Each "stream" consists of memory segments that are pooled +/// internal class BatchStreamData { public Queue> Objects { get; } = new(); diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs index 2399bd71d7..e295c3c2c8 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs @@ -4,6 +4,11 @@ using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Transport { + /// + /// A helper class used from generated UI-thread-side collections of composition objects. + /// + // NOTE: This should probably be a base class since TServer isn't used anymore and it was the reason why + // it couldn't be exposed as a base class class ServerListProxyHelper : IList where TServer : ServerObject where TClient : CompositionObject diff --git a/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs b/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs deleted file mode 100644 index 0be19a8e9d..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace Avalonia.Rendering.Composition.Utils -{ - static class MathExt - { - public static float Clamp(float value, float min, float max) - { - var amax = Math.Max(min, max); - var amin = Math.Min(min, max); - return Math.Min(Math.Max(value, amin), amax); - } - - public static double Clamp(double value, double min, double max) - { - var amax = Math.Max(min, max); - var amin = Math.Min(min, max); - return Math.Min(Math.Max(value, amin), amax); - } - - - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index f092029457..f9e1eae2ab 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -5,6 +5,9 @@ using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition { + /// + /// The base visual object in the composition visual hierarchy. + /// public abstract partial class CompositionVisual { private IBrush? _opacityMask; diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs index 42226a8b4d..60ebd9271c 100644 --- a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -3,6 +3,9 @@ using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition { + /// + /// A collection of CompositionVisual objects + /// public partial class CompositionVisualCollection : CompositionObject { private CompositionVisual _owner; diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 596cbf1d7e..3d5be806e1 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -251,6 +251,20 @@ namespace Avalonia.Utilities return val; } } + + /// + /// Clamps a value between a minimum and maximum value. + /// + /// The value. + /// The minimum value. + /// The maximum value. + /// The clamped value. + public static float Clamp(float value, float min, float max) + { + var amax = Math.Max(min, max); + var amin = Math.Min(min, max); + return Math.Min(Math.Max(value, amin), amax); + } /// /// Clamps a value between a minimum and maximum value. diff --git a/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs b/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs index 71305a8305..9400e37f21 100644 --- a/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs +++ b/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs @@ -5,6 +5,9 @@ using System.Runtime.InteropServices; namespace Avalonia.Utilities; +/// +/// Maintains a set of objects with reference counts +/// internal class RefTrackingDictionary : Dictionary where TKey : class { /// diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index 2b555359c6..e0e177da44 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -7,7 +7,6 @@ - @@ -37,40 +36,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 297eef9e80..a88bfc3651 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -8,6 +8,9 @@ using Avalonia.Controls; namespace Avalonia.Markup.Parsers { + /// + /// Parser for composition expressions + /// internal class ExpressionParser { private readonly bool _enableValidation; diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.KeyFrameAnimation.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.KeyFrameAnimation.cs index 314ac1acbf..7ad40f68e4 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.KeyFrameAnimation.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.KeyFrameAnimation.cs @@ -34,7 +34,7 @@ namespace Avalonia.Rendering.Composition private KeyFrames<{a.Type}> _keyFrames = new KeyFrames<{a.Type}>(); private protected override IKeyFrames KeyFrames => _keyFrames; - public void InsertKeyFrame(float normalizedProgressKey, {a.Type} value, CompositionEasingFunction easingFunction) + public void InsertKeyFrame(float normalizedProgressKey, {a.Type} value, Avalonia.Animation.Easings.IEasing easingFunction) {{ _keyFrames.Insert(normalizedProgressKey, value, easingFunction); }} From c9a523db57a6da88d17991443af5f9bf50848e60 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 Jun 2022 15:55:34 +0300 Subject: [PATCH 29/61] compile --- src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs b/src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs index e51c35c4e6..71582fa448 100644 --- a/src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs +++ b/src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs @@ -1,6 +1,4 @@ using System; -using Avalonia.Rendering.Composition; -using Avalonia.Rendering.Composition.Utils; namespace Avalonia.Animation.Easings; From 5ca48a6b325d35cd22210c95321d6fdccc99d45b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 Jun 2022 16:20:02 +0300 Subject: [PATCH 30/61] DataType --- .../Pages/CompositionPage.axaml | 2 +- .../Pages/CompositionPage.axaml.cs | 140 +++++++++--------- 2 files changed, 69 insertions(+), 73 deletions(-) diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml b/samples/ControlCatalog/Pages/CompositionPage.axaml index 592290fde5..22c5c88941 100644 --- a/samples/ControlCatalog/Pages/CompositionPage.axaml +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml @@ -12,7 +12,7 @@ - + ("Items").Items = CreateColorItems(); } - private List CreateColorItems() + private List CreateColorItems() { - var list = new List(); - - list.Add(new ColorItem(Color.FromArgb(255, 255, 185, 0))); - list.Add(new ColorItem(Color.FromArgb(255, 231, 72, 86))); - list.Add(new ColorItem(Color.FromArgb(255, 0, 120, 215))); - list.Add(new ColorItem(Color.FromArgb(255, 0, 153, 188))); - list.Add(new ColorItem(Color.FromArgb(255, 122, 117, 116))); - list.Add(new ColorItem(Color.FromArgb(255, 118, 118, 118))); - list.Add(new ColorItem(Color.FromArgb(255, 255, 141, 0))); - list.Add(new ColorItem(Color.FromArgb(255, 232, 17, 35))); - list.Add(new ColorItem(Color.FromArgb(255, 0, 99, 177))); - list.Add(new ColorItem(Color.FromArgb(255, 45, 125, 154))); - list.Add(new ColorItem(Color.FromArgb(255, 93, 90, 88))); - list.Add(new ColorItem(Color.FromArgb(255, 76, 74, 72))); - list.Add(new ColorItem(Color.FromArgb(255, 247, 99, 12))); - list.Add(new ColorItem(Color.FromArgb(255, 234, 0, 94))); - list.Add(new ColorItem(Color.FromArgb(255, 142, 140, 216))); - list.Add(new ColorItem(Color.FromArgb(255, 0, 183, 195))); - list.Add(new ColorItem(Color.FromArgb(255, 104, 118, 138))); - list.Add(new ColorItem(Color.FromArgb(255, 105, 121, 126))); - list.Add(new ColorItem(Color.FromArgb(255, 202, 80, 16))); - list.Add(new ColorItem(Color.FromArgb(255, 195, 0, 82))); - list.Add(new ColorItem(Color.FromArgb(255, 107, 105, 214))); - list.Add(new ColorItem(Color.FromArgb(255, 3, 131, 135))); - list.Add(new ColorItem(Color.FromArgb(255, 81, 92, 107))); - list.Add(new ColorItem(Color.FromArgb(255, 74, 84, 89))); - list.Add(new ColorItem(Color.FromArgb(255, 218, 59, 1))); - list.Add(new ColorItem(Color.FromArgb(255, 227, 0, 140))); - list.Add(new ColorItem(Color.FromArgb(255, 135, 100, 184))); - list.Add(new ColorItem(Color.FromArgb(255, 0, 178, 148))); - list.Add(new ColorItem(Color.FromArgb(255, 86, 124, 115))); - list.Add(new ColorItem(Color.FromArgb(255, 100, 124, 100))); - list.Add(new ColorItem(Color.FromArgb(255, 239, 105, 80))); - list.Add(new ColorItem(Color.FromArgb(255, 191, 0, 119))); - list.Add(new ColorItem(Color.FromArgb(255, 116, 77, 169))); - list.Add(new ColorItem(Color.FromArgb(255, 1, 133, 116))); - list.Add(new ColorItem(Color.FromArgb(255, 72, 104, 96))); - list.Add(new ColorItem(Color.FromArgb(255, 82, 94, 84))); - list.Add(new ColorItem(Color.FromArgb(255, 209, 52, 56))); - list.Add(new ColorItem(Color.FromArgb(255, 194, 57, 179))); - list.Add(new ColorItem(Color.FromArgb(255, 177, 70, 194))); - list.Add(new ColorItem(Color.FromArgb(255, 0, 204, 106))); - list.Add(new ColorItem(Color.FromArgb(255, 73, 130, 5))); - list.Add(new ColorItem(Color.FromArgb(255, 132, 117, 69))); - list.Add(new ColorItem(Color.FromArgb(255, 255, 67, 67))); - list.Add(new ColorItem(Color.FromArgb(255, 154, 0, 137))); - list.Add(new ColorItem(Color.FromArgb(255, 136, 23, 152))); - list.Add(new ColorItem(Color.FromArgb(255, 16, 137, 62))); - list.Add(new ColorItem(Color.FromArgb(255, 16, 124, 16))); - list.Add(new ColorItem(Color.FromArgb(255, 126, 115, 95))); + var list = new List(); + + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 185, 0))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 231, 72, 86))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 120, 215))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 153, 188))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 122, 117, 116))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 118, 118, 118))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 141, 0))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 232, 17, 35))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 99, 177))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 45, 125, 154))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 93, 90, 88))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 76, 74, 72))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 247, 99, 12))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 234, 0, 94))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 142, 140, 216))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 183, 195))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 104, 118, 138))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 105, 121, 126))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 202, 80, 16))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 195, 0, 82))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 107, 105, 214))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 3, 131, 135))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 81, 92, 107))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 74, 84, 89))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 218, 59, 1))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 227, 0, 140))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 135, 100, 184))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 178, 148))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 86, 124, 115))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 100, 124, 100))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 239, 105, 80))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 191, 0, 119))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 116, 77, 169))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 1, 133, 116))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 72, 104, 96))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 82, 94, 84))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 209, 52, 56))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 194, 57, 179))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 177, 70, 194))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 204, 106))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 73, 130, 5))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 132, 117, 69))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 67, 67))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 154, 0, 137))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 136, 23, 152))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 16, 137, 62))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 16, 124, 16))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 126, 115, 95))); return list; } - - public class ColorItem - { - public Color Color { get; private set; } - - public SolidColorBrush ColorBrush - { - get { return new SolidColorBrush(Color); } - } - - public String ColorHexValue - { - get { return Color.ToString().Substring(3).ToUpperInvariant(); } - } - - public ColorItem(Color color) - { - Color = color; - } - } - + private void EnsureImplicitAnimations() { if (_implicitAnimations == null) @@ -150,8 +130,24 @@ public partial class CompositionPage : UserControl ElementComposition.GetElementVisual((Visual)border.GetVisualParent()).ImplicitAnimations = page._implicitAnimations; } +} +public class CompositionPageColorItem +{ + public Color Color { get; private set; } + public SolidColorBrush ColorBrush + { + get { return new SolidColorBrush(Color); } + } + public String ColorHexValue + { + get { return Color.ToString().Substring(3).ToUpperInvariant(); } + } + public CompositionPageColorItem(Color color) + { + Color = color; + } } \ No newline at end of file From ca483b087b8044312ffaf9acb6ed63a5344628ad Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 Jun 2022 19:32:01 +0300 Subject: [PATCH 31/61] Apply mirror transform before render transform --- .../Rendering/Composition/CompositingRenderer.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 5e8e9c24f9..5da256475a 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -209,20 +209,17 @@ public class CompositingRenderer : IRendererWithCompositor var renderTransform = Matrix.Identity; + if (visual.HasMirrorTransform) + renderTransform = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0); + if (visual.RenderTransform != null) { var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); var offset = Matrix.CreateTranslation(origin); - renderTransform = (-offset) * visual.RenderTransform.Value * (offset); + renderTransform *= (-offset) * visual.RenderTransform.Value * (offset); } - - if (visual.HasMirrorTransform) - { - var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0); - renderTransform *= mirrorMatrix; - } comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform); From 19338dd9b8720b81a99a0e28abbd759a4a176792 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 23 Jun 2022 11:11:53 +0300 Subject: [PATCH 32/61] Properly clear the render target before blitting the rendered frame --- .../Rendering/Composition/Server/ServerCompositionTarget.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 8b903c4382..3faba5095f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -118,6 +118,7 @@ namespace Avalonia.Rendering.Composition.Server } } + targetContext.Clear(Colors.Transparent); targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, new Rect(_layerSize), new Rect(Size)); From 9ecffa5183a1cda5b7b834d71312845d8822bde5 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 23 Jun 2022 11:12:10 +0300 Subject: [PATCH 33/61] Enable acryllic material --- .../Composition/Drawing/CompositionDrawingContext.cs | 2 +- .../Rendering/Composition/Server/DrawingContextProxy.cs | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index e678a85fcf..c5a970acde 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -13,7 +13,7 @@ namespace Avalonia.Rendering.Composition; /// /// An IDrawingContextImpl implementation that builds /// -internal class CompositionDrawingContext : IDrawingContextImpl +internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { private CompositionDrawListBuilder _builder = new(); private int _drawOperationIndex; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 1b85159b02..2fd87f6620 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -15,7 +15,7 @@ namespace Avalonia.Rendering.Composition.Server; /// they have information about the full render transform (they are not) /// 2) Keeps the draw list for the VisualBrush contents of the current drawing operation. /// -internal class CompositorDrawingContextProxy : IDrawingContextImpl +internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { private IDrawingContextImpl _impl; private readonly VisualBrushRenderer _visualBrushRenderer; @@ -170,4 +170,10 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl } } } + + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + if (_impl is IDrawingContextWithAcrylicLikeSupport acrylic) + acrylic.DrawRectangle(material, rect); + } } \ No newline at end of file From 7d0651f643059c7c909528bd54d47249a8385f26 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 23 Jun 2022 12:42:16 +0300 Subject: [PATCH 34/61] Fix opacity (and other composition-affected properties) invalidation --- .../Server/ServerCompositionDrawListVisual.cs | 2 +- .../Server/ServerCompositionTarget.cs | 2 +- .../Composition/Server/ServerVisual.cs | 73 +++++++++++++++---- 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index eff1af65e2..df3c78b1bc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -29,7 +29,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua Rect? _contentBounds; - public override Rect ContentBounds + public override Rect OwnContentBounds { get { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 3faba5095f..9fd1a9f191 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -191,7 +191,7 @@ namespace Avalonia.Rendering.Composition.Server if (_attachedVisuals.Remove(visual) && IsEnabled) visual.Deactivate(); if(visual.IsVisibleInFrame) - AddDirtyRect(visual.TransformedBounds); + AddDirtyRect(visual.TransformedOwnContentBounds); } public void EnqueueAdornerUpdate(ServerCompositionVisual visual) => _adornerUpdateQueue.Enqueue(visual); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 0c58647319..f2b6b4d880 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -1,4 +1,6 @@ +using System; using System.Numerics; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering.Composition.Transport; @@ -13,6 +15,8 @@ namespace Avalonia.Rendering.Composition.Server partial class ServerCompositionVisual : ServerObject { private bool _isDirty; + private bool _isDirtyComposition; + private CompositionProperties _oldCompositionProperties; private bool _isBackface; protected virtual void RenderCore(CompositorDrawingContextProxy canvas) { @@ -21,7 +25,8 @@ namespace Avalonia.Rendering.Composition.Server public void Render(CompositorDrawingContextProxy canvas) { - if(Visible == false) + _isDirtyComposition = false; + if(Visible == false || IsVisibleInFrame == false) return; if(Opacity == 0) return; @@ -38,7 +43,6 @@ namespace Avalonia.Rendering.Composition.Server if(OpacityMaskBrush != null) canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); - //TODO: Check clip RenderCore(canvas); // Hack to force invalidation of SKMatrix @@ -93,30 +97,34 @@ namespace Avalonia.Rendering.Composition.Server } var wasVisible = IsVisibleInFrame; - //TODO: check effective opacity too - IsVisibleInFrame = Visible && Opacity > 0 && !_isBackface; + + EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1); + IsVisibleInFrame = Visible && EffectiveOpacity > 0.04 && !_isBackface; // Invalidate previous rect and queue new rect based on visibility if (positionChanged) { if(wasVisible) - Root!.AddDirtyRect(TransformedBounds); + Root!.AddDirtyRect(TransformedOwnContentBounds); if (IsVisibleInFrame) _isDirty = true; } + + if (wasVisible != IsVisibleInFrame) + _isDirty = true; + + if (_parent.Value?._isDirtyComposition == true) + _isDirty = true; GlobalTransformMatrix = newTransform; //TODO: Cache - TransformedBounds = ContentBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); + TransformedOwnContentBounds = OwnContentBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); - if (!IsVisibleInFrame) - _isDirty = false; - else if (_isDirty) - { - Root!.AddDirtyRect(TransformedBounds); - _isDirty = false; - } + if (IsVisibleInFrame && _isDirty) + Root!.AddDirtyRect(TransformedOwnContentBounds); + + _isDirty = false; // Update readback indices var i = Root!.Readback; @@ -126,6 +134,14 @@ namespace Avalonia.Rendering.Composition.Server readback.TargetId = Root.Id; readback.Visible = IsVisibleInFrame; + // Forcefully mark any children visuals are dirty if any of the composition + // properties were changed since the last update + var newProps = GetCompositionProperties(); + if (!newProps.Equals(_oldCompositionProperties)) + { + _isDirtyComposition = true; + _oldCompositionProperties = newProps; + } } /// @@ -160,13 +176,38 @@ namespace Avalonia.Rendering.Composition.Server { _isDirty = true; if (IsVisibleInFrame) - Root?.AddDirtyRect(TransformedBounds); + Root?.AddDirtyRect(TransformedOwnContentBounds); else Root?.Invalidate(); } + + struct CompositionProperties + { + public double EffectiveOpacity { get; set; } + public Vector2? ClipSize { get; set; } + public IGeometryImpl? Clip { get; set; } + public IBrush? OpacityMaskBrush { get; set; } + + public bool Equals(CompositionProperties other) => + EffectiveOpacity == other.EffectiveOpacity + && ClipSize == other.ClipSize + && Clip == other.Clip + && OpacityMaskBrush == other.OpacityMaskBrush; + } + + private CompositionProperties GetCompositionProperties() => new CompositionProperties + { + EffectiveOpacity = EffectiveOpacity, + Clip = Clip, + ClipSize = ClipToBounds ? Size : null, + OpacityMaskBrush = OpacityMaskBrush + }; + + public bool IsVisibleInFrame { get; set; } - public Rect TransformedBounds { get; set; } - public virtual Rect ContentBounds => new Rect(0, 0, Size.X, Size.Y); + public double EffectiveOpacity { get; set; } + public Rect TransformedOwnContentBounds { get; set; } + public virtual Rect OwnContentBounds => new Rect(0, 0, Size.X, Size.Y); } From 61506919480c80b691c312e4df9730ab07a1ee39 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 23 Jun 2022 14:19:25 +0300 Subject: [PATCH 35/61] Don't allocate a draw list if visual doesn't render anything --- .../Rendering/Composition/CompositingRenderer.cs | 2 +- .../Composition/Drawing/CompositionDrawList.cs | 13 +++++++------ .../Drawing/CompositionDrawingContext.cs | 6 +++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 5da256475a..282973c26a 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -223,7 +223,7 @@ public class CompositingRenderer : IRendererWithCompositor comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform); - _recorder.BeginUpdate(comp.DrawList ?? new CompositionDrawList()); + _recorder.BeginUpdate(comp.DrawList); visual.Render(_recordingContext); comp.DrawList = _recorder.EndUpdate(); diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs index 315faf2c86..432a0832f2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs @@ -64,7 +64,8 @@ internal class CompositionDrawListBuilder _owns = false; } - public CompositionDrawList DrawOperations => _operations ?? new CompositionDrawList(); + public int Count => _operations?.Count ?? 0; + public CompositionDrawList? DrawOperations => _operations; void MakeWritable(int atIndex) { @@ -84,18 +85,18 @@ internal class CompositionDrawListBuilder public void ReplaceDrawOperation(int index, IDrawOperation node) { MakeWritable(index); - DrawOperations.Add(RefCountable.Create(node)); + DrawOperations!.Add(RefCountable.Create(node)); } public void AddDrawOperation(IDrawOperation node) { - MakeWritable(DrawOperations.Count); - DrawOperations.Add(RefCountable.Create(node)); + MakeWritable(Count); + DrawOperations!.Add(RefCountable.Create(node)); } public void TrimTo(int count) { - if (count < DrawOperations.Count) - DrawOperations.RemoveRange(count, DrawOperations.Count - count); + if (count < Count) + _operations!.RemoveRange(count, _operations.Count - count); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index c5a970acde..d7c1ef125d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -344,7 +344,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW private void Add(T node) where T : class, IDrawOperation { - if (_drawOperationIndex < _builder!.DrawOperations.Count) + if (_drawOperationIndex < _builder.Count) { _builder.ReplaceDrawOperation(_drawOperationIndex, node); } @@ -358,8 +358,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW private IRef? NextDrawAs() where T : class, IDrawOperation { - return _drawOperationIndex < _builder!.DrawOperations.Count - ? _builder.DrawOperations[_drawOperationIndex] as IRef + return _drawOperationIndex < _builder.Count + ? _builder.DrawOperations![_drawOperationIndex] as IRef : null; } From 19f6f12fe4b52a8adf4746ca9e686f7fecaa938f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 24 Jun 2022 03:12:22 +0300 Subject: [PATCH 36/61] Apparently LayoutKind.Sequential is ignored for generic structs --- .../Composition/Animations/AnimatedValueStore.cs | 7 ++----- .../Rendering/Composition/Server/ServerObject.cs | 8 ++++---- .../Rendering/Composition/Server/ServerVisual.cs | 2 ++ src/tools/DevGenerators/CompositionGenerator/Generator.cs | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs index f4735040d5..92aa1f35bb 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs @@ -32,11 +32,9 @@ namespace Avalonia.Rendering.Composition.Animations /// The value store for non-animated values that can still be referenced by animations. /// Simply stores the value and notifies subscribers /// - [StructLayout(LayoutKind.Sequential)] internal struct ServerValueStore { - // HAS TO BE THE FIRST FIELD, accessed by field offset from ServerObject - private ServerObjectSubscriptionStore Subscriptions; + public ServerObjectSubscriptionStore Subscriptions; private T _value; public T Value { @@ -61,8 +59,7 @@ namespace Avalonia.Rendering.Composition.Animations [StructLayout(LayoutKind.Sequential)] internal struct ServerAnimatedValueStore where T : struct { - // HAS TO BE THE FIRST FIELD, accessed by field offset from ServerObject - private ServerObjectSubscriptionStore Subscriptions; + public ServerObjectSubscriptionStore Subscriptions; private IAnimationInstance? _animation; private T _direct; private T? _lastAnimated; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index e5ae7fa859..b53e31c9cf 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -63,15 +63,15 @@ namespace Avalonia.Rendering.Composition.Server } [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected int GetOffset(ref T field) where T : struct + protected int GetOffset(ref ServerObjectSubscriptionStore field) { - return Unsafe.ByteOffset(ref Unsafe.As(ref _activationCount), - ref Unsafe.As(ref field)) + return Unsafe.ByteOffset(ref _activationCount, + ref Unsafe.As(ref field)) .ToInt32(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private ref ServerObjectSubscriptionStore GetStoreFromOffset(int offset) + protected ref ServerObjectSubscriptionStore GetStoreFromOffset(int offset) { #if DEBUG if (offset == 0) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index f2b6b4d880..04eba72594 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -2,7 +2,9 @@ using System; using System.Numerics; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server { diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index f8f86265cf..ad7f2200a6 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -298,7 +298,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, IdentifierName(fieldOffsetName), InvocationExpression(MemberAccess(IdentifierName(uninitializedObjectName), "GetOffset"), ArgumentList(SingletonSeparatedList(Argument( - RefExpression(MemberAccess(IdentifierName(uninitializedObjectName), fieldName))))))))); + RefExpression(MemberAccess(MemberAccess(IdentifierName(uninitializedObjectName), fieldName), "Subscriptions"))))))))); if (prop.DefaultValue != null) { From 567466391d1255caa437096a4a2559c2186813c2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 24 Jun 2022 03:39:54 +0300 Subject: [PATCH 37/61] Fixed CreateLayer usage --- .../Composition/Server/ServerCompositionTarget.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 9fd1a9f191..749665156d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Numerics; using System.Threading; using Avalonia.Media; +using Avalonia.Media.Imaging; using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.Rendering.Composition.Transport; @@ -102,7 +103,7 @@ namespace Avalonia.Rendering.Composition.Server { _layer?.Dispose(); _layer = null; - _layer = targetContext.CreateLayer(layerSize); + _layer = targetContext.CreateLayer(Size); _layerSize = layerSize; } @@ -119,8 +120,10 @@ namespace Avalonia.Rendering.Composition.Server } targetContext.Clear(Colors.Transparent); - targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, new Rect(_layerSize), - new Rect(Size)); + targetContext.Transform = Matrix.Identity; + targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, + new Rect(_layerSize), + new Rect(Size), BitmapInterpolationMode.LowQuality); if (DrawDirtyRects) From 579dbaa25cf74ee4ebc8830ad5de1675cd8d3e66 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 24 Jun 2022 04:41:51 +0300 Subject: [PATCH 38/61] Report renderer transport pool memory usage in fps line --- src/Avalonia.Base/Avalonia.Base.csproj | 1 + .../Rendering/Composition/Server/FpsCounter.cs | 4 ++-- .../Composition/Server/ServerCompositionTarget.cs | 15 ++++++++++++--- .../Composition/Server/ServerCompositor.cs | 10 +++++----- .../Composition/Transport/BatchStreamArrayPool.cs | 11 +++++++---- .../Utilities}/ByteSizeHelper.cs | 9 +++++---- src/Avalonia.Dialogs/FileSizeStringConverter.cs | 2 +- src/Avalonia.Dialogs/ManagedFileChooserSources.cs | 3 ++- 8 files changed, 35 insertions(+), 20 deletions(-) rename src/{Avalonia.Dialogs => Avalonia.Base/Utilities}/ByteSizeHelper.cs (70%) diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 4c988c8ae1..15feed388b 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -35,6 +35,7 @@ + diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index a09de2c0ff..7585710540 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -35,7 +35,7 @@ internal class FpsCounter public void FpsTick() => _framesThisSecond++; - public void RenderFps(IDrawingContextImpl context) + public void RenderFps(IDrawingContextImpl context, string aux) { var now = _stopwatch.Elapsed; var elapsed = now - _lastFpsUpdate; @@ -50,7 +50,7 @@ internal class FpsCounter _lastFpsUpdate = now; } - var fpsLine = $"Frame #{_totalFrames:00000000} FPS: {_fps:000}"; + var fpsLine = $"Frame #{_totalFrames:00000000} FPS: {_fps:000} " + aux; double width = 0; double height = 0; foreach (var ch in fpsLine) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 749665156d..5ec5df8416 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -134,10 +134,19 @@ namespace Avalonia.Rendering.Composition.Server , null, _dirtyRect); } - if(DrawFps) - _fpsCounter.RenderFps(targetContext); + if (DrawFps) + { + var nativeMem = ByteSizeHelper.ToString((ulong)( + (Compositor.BatchMemoryPool.CurrentUsage + Compositor.BatchMemoryPool.CurrentPool) * + Compositor.BatchMemoryPool.BufferSize), false); + var managedMem = ByteSizeHelper.ToString((ulong)( + (Compositor.BatchObjectPool.CurrentUsage + Compositor.BatchObjectPool.CurrentPool) * + Compositor.BatchObjectPool.ArraySize * + IntPtr.Size), false); + _fpsCounter.RenderFps(targetContext, $"M:{managedMem} / N:{nativeMem}"); + } + _dirtyRect = Rect.Empty; - } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 73792fdf98..564f792ebe 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -24,8 +24,8 @@ namespace Avalonia.Rendering.Composition.Server private List _activeTargets = new(); private HashSet _activeAnimations = new(); private List _animationsToUpdate = new(); - private BatchStreamObjectPool _batchObjectPool; - private BatchStreamMemoryPool _batchMemoryPool; + internal BatchStreamObjectPool BatchObjectPool; + internal BatchStreamMemoryPool BatchMemoryPool; private object _lock = new object(); public IPlatformGpuContext? GpuContext { get; } @@ -34,8 +34,8 @@ namespace Avalonia.Rendering.Composition.Server { GpuContext = platformGpu?.PrimaryContext; _renderLoop = renderLoop; - _batchObjectPool = batchObjectPool; - _batchMemoryPool = batchMemoryPool; + BatchObjectPool = batchObjectPool; + BatchMemoryPool = batchMemoryPool; _renderLoop.Add(this); } @@ -60,7 +60,7 @@ namespace Avalonia.Rendering.Composition.Server batch = _batches.Dequeue(); } - using (var stream = new BatchStreamReader(batch.Changes, _batchMemoryPool, _batchObjectPool)) + using (var stream = new BatchStreamReader(batch.Changes, BatchMemoryPool, BatchObjectPool)) { while (!stream.IsObjectEof) { diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs index 97d05704af..32b4ed3026 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -18,6 +18,9 @@ internal abstract class BatchStreamPoolBase : IDisposable readonly int[] _usageStatistics = new int[10]; int _usageStatisticsSlot; + public int CurrentUsage => _usage; + public int CurrentPool => _pool.Count; + public BatchStreamPoolBase(bool needsFinalize, Action>? startTimer = null) { if(!needsFinalize) @@ -116,16 +119,16 @@ internal abstract class BatchStreamPoolBase : IDisposable internal sealed class BatchStreamObjectPool : BatchStreamPoolBase where T : class? { - private readonly int _arraySize; + public int ArraySize { get; } - public BatchStreamObjectPool(int arraySize = 1024, Action>? startTimer = null) : base(false, startTimer) + public BatchStreamObjectPool(int arraySize = 128, Action>? startTimer = null) : base(false, startTimer) { - _arraySize = arraySize; + ArraySize = arraySize; } protected override T[] CreateItem() { - return new T[_arraySize]; + return new T[ArraySize]; } protected override void DestroyItem(T[] item) diff --git a/src/Avalonia.Dialogs/ByteSizeHelper.cs b/src/Avalonia.Base/Utilities/ByteSizeHelper.cs similarity index 70% rename from src/Avalonia.Dialogs/ByteSizeHelper.cs rename to src/Avalonia.Base/Utilities/ByteSizeHelper.cs index d849e33399..edaf94231b 100644 --- a/src/Avalonia.Dialogs/ByteSizeHelper.cs +++ b/src/Avalonia.Base/Utilities/ByteSizeHelper.cs @@ -1,10 +1,11 @@ using System; -namespace Avalonia.Dialogs +namespace Avalonia.Utilities { internal static class ByteSizeHelper { - private const string formatTemplate = "{0}{1:0.#} {2}"; + private const string formatTemplateSeparated = "{0}{1:0.#} {2}"; + private const string formatTemplate = "{0}{1:0.#}{2}"; private static readonly string[] Prefixes = { @@ -19,11 +20,11 @@ namespace Avalonia.Dialogs "YB" }; - public static string ToString(ulong bytes) + public static string ToString(ulong bytes, bool separate) { if (bytes == 0) { - return string.Format(formatTemplate, null, 0, Prefixes[0]); + return string.Format(separate ? formatTemplateSeparated : formatTemplate, null, 0, Prefixes[0]); } var absSize = Math.Abs((double)bytes); diff --git a/src/Avalonia.Dialogs/FileSizeStringConverter.cs b/src/Avalonia.Dialogs/FileSizeStringConverter.cs index c2cdf1e502..144f50fd9c 100644 --- a/src/Avalonia.Dialogs/FileSizeStringConverter.cs +++ b/src/Avalonia.Dialogs/FileSizeStringConverter.cs @@ -12,7 +12,7 @@ namespace Avalonia.Dialogs { if (value is long size && size > 0) { - return ByteSizeHelper.ToString((ulong)size); + return Avalonia.Utilities.ByteSizeHelper.ToString((ulong)size, true); } return ""; diff --git a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs index a217a67bc6..a76a84ba5a 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs @@ -6,6 +6,7 @@ using System.Reactive.Linq; using System.Runtime.InteropServices; using Avalonia.Controls.Platform; using Avalonia.Threading; +using Avalonia.Utilities; namespace Avalonia.Dialogs { @@ -60,7 +61,7 @@ namespace Avalonia.Dialogs if (displayName == null & x.VolumeSizeBytes > 0) { - displayName = $"{ByteSizeHelper.ToString(x.VolumeSizeBytes)} Volume"; + displayName = $"{ByteSizeHelper.ToString(x.VolumeSizeBytes, true)} Volume"; }; try From 2f8dcda203d6c88ef9f53d352f787b7bd9019426 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 24 Jun 2022 14:35:49 +0300 Subject: [PATCH 39/61] Imporve dirty rect tracking, optimize invalidation --- .../Server/ServerCompositionDrawListVisual.cs | 4 +- .../Server/ServerCompositionTarget.cs | 12 +- .../Server/ServerContainerVisual.cs | 14 +- .../Composition/Server/ServerObject.cs | 2 +- .../Server/ServerVisual.DirtyProperties.cs | 76 +++++++++ .../Composition/Server/ServerVisual.cs | 160 ++++++++++-------- .../CompositionGenerator/Generator.cs | 13 +- 7 files changed, 199 insertions(+), 82 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index df3c78b1bc..93a5226f83 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -57,13 +57,13 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua base.DeserializeChangesCore(reader, commitedAt); } - protected override void RenderCore(CompositorDrawingContextProxy canvas) + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) { if (_renderCommands != null) { _renderCommands.Render(canvas); } - base.RenderCore(canvas); + base.RenderCore(canvas, currentTransformedClip); } #if DEBUG diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 5ec5df8416..691cdf57c5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -35,6 +35,7 @@ namespace Avalonia.Rendering.Composition.Server public ReadbackIndices Readback { get; } = new(); + public int RenderedVisuals { get; set; } public ServerCompositionTarget(ServerCompositor compositor, Func renderTargetFactory) : base(compositor) @@ -85,12 +86,12 @@ namespace Avalonia.Rendering.Composition.Server Revision++; // Update happens in a separate phase to extend dirty rect if needed - Root.Update(this, Matrix4x4.Identity); + Root.Update(this); while (_adornerUpdateQueue.Count > 0) { var adorner = _adornerUpdateQueue.Dequeue(); - adorner.Update(this, adorner.AdornedVisual?.GlobalTransformMatrix ?? Matrix4x4.Identity); + adorner.Update(this); } Readback.CompleteWrite(Revision); @@ -114,7 +115,7 @@ namespace Avalonia.Rendering.Composition.Server { context.PushClip(_dirtyRect); context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper)); + Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), _dirtyRect); context.PopClip(); } } @@ -143,8 +144,9 @@ namespace Avalonia.Rendering.Composition.Server (Compositor.BatchObjectPool.CurrentUsage + Compositor.BatchObjectPool.CurrentPool) * Compositor.BatchObjectPool.ArraySize * IntPtr.Size), false); - _fpsCounter.RenderFps(targetContext, $"M:{managedMem} / N:{nativeMem}"); + _fpsCounter.RenderFps(targetContext, $"M:{managedMem} / N:{nativeMem} R:{RenderedVisuals:0000}"); } + RenderedVisuals = 0; _dirtyRect = Rect.Empty; } @@ -163,6 +165,8 @@ namespace Avalonia.Rendering.Composition.Server public void AddDirtyRect(Rect rect) { + if(rect.IsEmpty) + return; var snapped = SnapToDevicePixels(rect, Scaling); _dirtyRect = _dirtyRect.Union(snapped); _redrawRequested = true; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs index 136ebc1d63..f7152293cc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -12,26 +12,28 @@ namespace Avalonia.Rendering.Composition.Server { public ServerCompositionVisualCollection Children { get; private set; } = null!; - protected override void RenderCore(CompositorDrawingContextProxy canvas) + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) { - base.RenderCore(canvas); + base.RenderCore(canvas, currentTransformedClip); foreach (var ch in Children) { - ch.Render(canvas); + ch.Render(canvas, currentTransformedClip); } } - public override void Update(ServerCompositionTarget root, Matrix4x4 transform) + public override void Update(ServerCompositionTarget root) { - base.Update(root, transform); + base.Update(root); foreach (var child in Children) { if (child.AdornedVisual != null) root.EnqueueAdornerUpdate(child); else - child.Update(root, GlobalTransformMatrix); + child.Update(root); } + + IsDirtyComposition = false; } partial void Initialize() diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index b53e31c9cf..a2af34c7f0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -81,7 +81,7 @@ namespace Avalonia.Rendering.Composition.Server new IntPtr(offset))); } - public void NotifyAnimatedValueChanged(int offset) + public virtual void NotifyAnimatedValueChanged(int offset) { ref var store = ref GetStoreFromOffset(offset); store.Invalidate(); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs new file mode 100644 index 0000000000..e434b97b2f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs @@ -0,0 +1,76 @@ +namespace Avalonia.Rendering.Composition.Server; + +partial class ServerCompositionVisual +{ + protected bool IsDirtyComposition; + private bool _combinedTransformDirty; + private bool _clipSizeDirty; + + private const CompositionVisualChangedFields CompositionFieldsMask + = CompositionVisualChangedFields.Opacity + | CompositionVisualChangedFields.OpacityAnimated + | CompositionVisualChangedFields.OpacityMaskBrush + | CompositionVisualChangedFields.Clip + | CompositionVisualChangedFields.ClipToBounds + | CompositionVisualChangedFields.ClipToBoundsAnimated + | CompositionVisualChangedFields.Size + | CompositionVisualChangedFields.SizeAnimated; + + private const CompositionVisualChangedFields CombinedTransformFieldsMask = + CompositionVisualChangedFields.Size + | CompositionVisualChangedFields.SizeAnimated + | CompositionVisualChangedFields.AnchorPoint + | CompositionVisualChangedFields.AnchorPointAnimated + | CompositionVisualChangedFields.CenterPoint + | CompositionVisualChangedFields.CenterPointAnimated + | CompositionVisualChangedFields.AdornedVisual + | CompositionVisualChangedFields.TransformMatrix + | CompositionVisualChangedFields.Scale + | CompositionVisualChangedFields.ScaleAnimated + | CompositionVisualChangedFields.RotationAngle + | CompositionVisualChangedFields.RotationAngleAnimated + | CompositionVisualChangedFields.Orientation + | CompositionVisualChangedFields.OrientationAnimated + | CompositionVisualChangedFields.Offset + | CompositionVisualChangedFields.OffsetAnimated; + + private const CompositionVisualChangedFields ClipSizeDirtyMask = + CompositionVisualChangedFields.Size + | CompositionVisualChangedFields.SizeAnimated + | CompositionVisualChangedFields.ClipToBounds + | CompositionVisualChangedFields.ClipToBoundsAnimated; + + partial void OnFieldsDeserialized(CompositionVisualChangedFields changed) + { + if ((changed & CompositionFieldsMask) != 0) + IsDirtyComposition = true; + if ((changed & CombinedTransformFieldsMask) != 0) + _combinedTransformDirty = true; + if ((changed & ClipSizeDirtyMask) != 0) + _clipSizeDirty = true; + } + + public override void NotifyAnimatedValueChanged(int offset) + { + base.NotifyAnimatedValueChanged(offset); + if (offset == s_OffsetOf_clipToBounds + || offset == s_OffsetOf_opacity + || offset == s_OffsetOf_size) + IsDirtyComposition = true; + + if (offset == s_OffsetOf_size + || offset == s_OffsetOf_anchorPoint + || offset == s_OffsetOf_centerPoint + || offset == s_OffsetOf_adornedVisual + || offset == s_OffsetOf_transformMatrix + || offset == s_OffsetOf_scale + || offset == s_OffsetOf_rotationAngle + || offset == s_OffsetOf_orientation + || offset == s_OffsetOf_offset) + _combinedTransformDirty = true; + + if (offset == s_OffsetOf_clipToBounds + || offset == s_OffsetOf_size) + _clipSizeDirty = true; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 04eba72594..7d98b3b246 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -16,22 +16,30 @@ namespace Avalonia.Rendering.Composition.Server /// partial class ServerCompositionVisual : ServerObject { - private bool _isDirty; - private bool _isDirtyComposition; - private CompositionProperties _oldCompositionProperties; + private bool _isDirtyForUpdate; + private Rect _oldOwnContentBounds; private bool _isBackface; - protected virtual void RenderCore(CompositorDrawingContextProxy canvas) + private Rect? _transformedClipBounds; + private Rect _combinedTransformedClipBounds; + + protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) { } - - public void Render(CompositorDrawingContextProxy canvas) + + public void Render(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) { - _isDirtyComposition = false; if(Visible == false || IsVisibleInFrame == false) return; if(Opacity == 0) return; + + currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds); + if(currentTransformedClip.IsEmpty) + return; + + Root!.RenderedVisuals++; + var transform = GlobalTransformMatrix; canvas.PreTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; @@ -45,7 +53,7 @@ namespace Avalonia.Rendering.Composition.Server if(OpacityMaskBrush != null) canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); - RenderCore(canvas); + RenderCore(canvas, currentTransformedClip); // Hack to force invalidation of SKMatrix canvas.PreTransform = MatrixUtils.ToMatrix(transform); @@ -76,18 +84,29 @@ namespace Avalonia.Rendering.Composition.Server return ref _readback2; } - public Matrix4x4 CombinedTransformMatrix { get; private set; } + public Matrix4x4 CombinedTransformMatrix { get; private set; } = Matrix4x4.Identity; public Matrix4x4 GlobalTransformMatrix { get; private set; } - public virtual void Update(ServerCompositionTarget root, Matrix4x4 transform) + public virtual void Update(ServerCompositionTarget root) { - // Calculate new parent-relative transform - CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, - // HACK: Ignore RenderTransform set by the adorner layer - AdornedVisual != null ? Matrix4x4.Identity : TransformMatrix, - Scale, RotationAngle, Orientation, Offset); + if(Parent == null && Root == null) + return; - var newTransform = CombinedTransformMatrix * transform; + var wasVisible = IsVisibleInFrame; + + // Calculate new parent-relative transform + if (_combinedTransformDirty) + { + CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, + // HACK: Ignore RenderTransform set by the adorner layer + AdornedVisual != null ? Matrix4x4.Identity : TransformMatrix, + Scale, RotationAngle, Orientation, Offset); + _combinedTransformDirty = false; + } + + var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix4x4.Identity; + + var newTransform = CombinedTransformMatrix * parentTransform; // Check if visual was moved and recalculate face orientation var positionChanged = false; @@ -97,36 +116,69 @@ namespace Avalonia.Rendering.Composition.Server new Vector3(0, 0, float.PositiveInfinity), GlobalTransformMatrix).Z <= 0; positionChanged = true; } + + var oldTransformedContentBounds = TransformedOwnContentBounds; + var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds; + + + if (_parent.Value?.IsDirtyComposition == true) + { + IsDirtyComposition = true; + _isDirtyForUpdate = true; + } + + GlobalTransformMatrix = newTransform; - var wasVisible = IsVisibleInFrame; + var ownBounds = OwnContentBounds; + if (ownBounds != _oldOwnContentBounds || positionChanged) + { + _oldOwnContentBounds = ownBounds; + if (ownBounds.IsEmpty) + TransformedOwnContentBounds = default; + else + TransformedOwnContentBounds = + ownBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); + } + if (_clipSizeDirty || positionChanged) + { + _transformedClipBounds = ClipToBounds + ? new Rect(new Size(Size.X, Size.Y)) + .TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)) + : null; + + _clipSizeDirty = false; + } + + _combinedTransformedClipBounds = Parent?._combinedTransformedClipBounds ?? new Rect(Root!.Size); + if (_transformedClipBounds != null) + _combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value); + EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1); - IsVisibleInFrame = Visible && EffectiveOpacity > 0.04 && !_isBackface; + + IsVisibleInFrame = Visible && EffectiveOpacity > 0.04 && !_isBackface && + !_combinedTransformedClipBounds.IsEmpty; + + if (wasVisible != IsVisibleInFrame) + _isDirtyForUpdate = true; // Invalidate previous rect and queue new rect based on visibility if (positionChanged) { if(wasVisible) - Root!.AddDirtyRect(TransformedOwnContentBounds); + AddDirtyRect(oldTransformedContentBounds.Intersect(oldCombinedTransformedClipBounds)); if (IsVisibleInFrame) - _isDirty = true; + _isDirtyForUpdate = true; } + + // Invalidate new bounds + if (IsVisibleInFrame && _isDirtyForUpdate) + AddDirtyRect(TransformedOwnContentBounds.Intersect(_combinedTransformedClipBounds)); - if (wasVisible != IsVisibleInFrame) - _isDirty = true; - - if (_parent.Value?._isDirtyComposition == true) - _isDirty = true; - GlobalTransformMatrix = newTransform; - //TODO: Cache - TransformedOwnContentBounds = OwnContentBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); - if (IsVisibleInFrame && _isDirty) - Root!.AddDirtyRect(TransformedOwnContentBounds); - - _isDirty = false; + _isDirtyForUpdate = false; // Update readback indices var i = Root!.Readback; @@ -135,15 +187,13 @@ namespace Avalonia.Rendering.Composition.Server readback.Matrix = CombinedTransformMatrix; readback.TargetId = Root.Id; readback.Visible = IsVisibleInFrame; + } - // Forcefully mark any children visuals are dirty if any of the composition - // properties were changed since the last update - var newProps = GetCompositionProperties(); - if (!newProps.Equals(_oldCompositionProperties)) - { - _isDirtyComposition = true; - _oldCompositionProperties = newProps; - } + void AddDirtyRect(Rect rc) + { + if(rc == Rect.Empty) + return; + Root?.AddDirtyRect(rc); } /// @@ -176,36 +226,10 @@ namespace Avalonia.Rendering.Composition.Server protected override void ValuesInvalidated() { - _isDirty = true; - if (IsVisibleInFrame) - Root?.AddDirtyRect(TransformedOwnContentBounds); - else - Root?.Invalidate(); + _isDirtyForUpdate = true; + Root?.Invalidate(); } - struct CompositionProperties - { - public double EffectiveOpacity { get; set; } - public Vector2? ClipSize { get; set; } - public IGeometryImpl? Clip { get; set; } - public IBrush? OpacityMaskBrush { get; set; } - - public bool Equals(CompositionProperties other) => - EffectiveOpacity == other.EffectiveOpacity - && ClipSize == other.ClipSize - && Clip == other.Clip - && OpacityMaskBrush == other.OpacityMaskBrush; - } - - private CompositionProperties GetCompositionProperties() => new CompositionProperties - { - EffectiveOpacity = EffectiveOpacity, - Clip = Clip, - ClipSize = ClipToBounds ? Size : null, - OpacityMaskBrush = OpacityMaskBrush - }; - - public bool IsVisibleInFrame { get; set; } public double EffectiveOpacity { get; set; } public Rect TransformedOwnContentBounds { get; set; } diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index ad7f2200a6..4df5a91c4b 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -324,10 +324,16 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( $"protected override void Deactivated(){{}}")!).WithBody(deactivatedBody)); if (cl.Properties.Count > 0) + { server = server.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( $"protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt){{}}") !) - .WithBody(deserializeMethodBody)); + .WithBody(ApplyDeserializeChangesEpilogue(deserializeMethodBody, cl))); + server = server.AddMembers(MethodDeclaration(ParseTypeName("void"), "OnFieldsDeserialized") + .WithParameterList(ParameterList(SingletonSeparatedList(Parameter(Identifier("changed")) + .WithType(ParseTypeName(ChangedFieldsTypeName(cl)))))) + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + } client = client.AddMembers( MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)) @@ -545,6 +551,11 @@ DeserializeChangesExtra(reader); var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); ")); } + + private BlockSyntax ApplyDeserializeChangesEpilogue(BlockSyntax body, GClass cl) + { + return body.AddStatements(ParseStatement("OnFieldsDeserialized(changed);")); + } BlockSyntax ApplyDeserializeField(BlockSyntax body, GClass cl, GProperty prop, string serverType, bool isObject) { From 2654b311faf8b4b9125baa57e6eb16da3d0fb6f9 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 24 Jun 2022 14:45:03 +0300 Subject: [PATCH 40/61] Actually enable enforced UI-thread rendering for macOS --- src/Avalonia.Native/WindowImplBase.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 922750fbb0..5ac8bd05c7 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -372,7 +372,10 @@ namespace Avalonia.Native if (_deferredRendering) { if (AvaloniaNativePlatform.Compositor != null) - return new CompositingRenderer(root, AvaloniaNativePlatform.Compositor); + return new CompositingRenderer(root, AvaloniaNativePlatform.Compositor) + { + RenderOnlyOnRenderThread = false + }; return new DeferredRenderer(root, loop); } From def87fc2bb1965e92bdf4d70dc57b8d490cc34b7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 24 Jun 2022 14:45:18 +0300 Subject: [PATCH 41/61] Enable compositor for macOS by default --- src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 1320903ca2..61889aa9e4 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -43,7 +43,7 @@ namespace Avalonia /// /// Enables new compositing rendering with UWP-like API /// - public bool UseCompositor { get; set; } + public bool UseCompositor { get; set; } = true; /// /// Determines whether to use GPU for rendering in your project. The default value is true. From c1ed0ae3351063aadad5125dd6fccac19078638e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 24 Jun 2022 14:45:54 +0300 Subject: [PATCH 42/61] Make SleepLoopRenderTimer to actually tick with requested rate --- src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs index 8544de4bbc..cd43a3ef20 100644 --- a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs @@ -53,7 +53,7 @@ namespace Avalonia.Rendering var now = _st.Elapsed; var timeTillNextTick = lastTick + _timeBetweenTicks - now; if (timeTillNextTick.TotalMilliseconds > 1) Thread.Sleep(timeTillNextTick); - lastTick = now; + lastTick = now = _st.Elapsed; lock (_lock) { if (_count == 0) From 85c5377307c6af2d97d912ce553364315844b8e8 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 24 Jun 2022 15:00:07 +0300 Subject: [PATCH 43/61] Glitches --- .../Server/ServerCompositionTarget.cs | 2 ++ .../Composition/Server/ServerVisual.cs | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 691cdf57c5..3351caf0c6 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -152,6 +152,8 @@ namespace Avalonia.Rendering.Composition.Server } } + public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling); + private static Rect SnapToDevicePixels(Rect rect, double scale) { return new Rect( diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 7d98b3b246..06f087258b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -47,7 +47,7 @@ namespace Avalonia.Rendering.Composition.Server canvas.PushOpacity(Opacity); var boundsRect = new Rect(new Size(Size.X, Size.Y)); if(ClipToBounds) - canvas.PushClip(boundsRect); + canvas.PushClip(Root!.SnapToDevicePixels(boundsRect)); if (Clip != null) canvas.PushGeometryClip(Clip); if(OpacityMaskBrush != null) @@ -162,22 +162,28 @@ namespace Avalonia.Rendering.Composition.Server if (wasVisible != IsVisibleInFrame) _isDirtyForUpdate = true; + var dirtyOldBounds = false; // Invalidate previous rect and queue new rect based on visibility if (positionChanged) { - if(wasVisible) - AddDirtyRect(oldTransformedContentBounds.Intersect(oldCombinedTransformedClipBounds)); + if (wasVisible) + dirtyOldBounds = true; if (IsVisibleInFrame) _isDirtyForUpdate = true; } // Invalidate new bounds - if (IsVisibleInFrame && _isDirtyForUpdate) + if (IsVisibleInFrame && _isDirtyForUpdate) + { + dirtyOldBounds = true; AddDirtyRect(TransformedOwnContentBounds.Intersect(_combinedTransformedClipBounds)); + } + + if (dirtyOldBounds && wasVisible) + AddDirtyRect(oldTransformedContentBounds.Intersect(oldCombinedTransformedClipBounds)); + - - _isDirtyForUpdate = false; // Update readback indices From 77ff52d21840627f99dc2312916c6dcf8cceceb8 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 26 Jun 2022 02:30:56 +0300 Subject: [PATCH 44/61] Use a zeroed huge object instead of uninitialized one --- .../Composition/Server/ServerObject.cs | 21 +++++++++++-- .../CompositionGenerator/Generator.cs | 31 +++++++------------ 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index a2af34c7f0..4c358605fc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -61,11 +61,28 @@ namespace Avalonia.Rendering.Composition.Server { } + + [StructLayout(LayoutKind.Sequential)] + protected class OffsetDummy + { +#pragma warning disable CS0649 + public FillerStruct Filler; +#pragma warning restore CS0649 + } + [StructLayout(LayoutKind.Sequential)] + protected unsafe struct FillerStruct + { + public fixed byte FillerData[8192]; + } + + private static readonly object s_OffsetDummy = new OffsetDummy(); + protected static T GetOffsetDummy() where T : ServerObject => Unsafe.As(s_OffsetDummy); + [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected int GetOffset(ref ServerObjectSubscriptionStore field) + protected static int GetOffset(ServerObject obj, ref ServerObjectSubscriptionStore field) { - return Unsafe.ByteOffset(ref _activationCount, + return Unsafe.ByteOffset(ref obj._activationCount, ref Unsafe.As(ref field)) .ToInt32(); } diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index 4df5a91c4b..0bb4e30f75 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -153,18 +153,10 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ); var uninitializedObjectName = "dummy"; - var serverStaticCtorBody = cl.Abstract - ? Block() - : Block( - ParseStatement( - $"var dummy = ({serverName})System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof({serverName}));"), - ParseStatement($"System.GC.SuppressFinalize(dummy);"), - ParseStatement("InitializeFieldOffsets(dummy);") - ); + var serverStaticCtorBody = Block( + ParseStatement($"var dummy = GetOffsetDummy<{serverName}>();") + ); - var initializeFieldOffsetsBody = cl.Inherits == null - ? Block() - : Block(ParseStatement($"Server{cl.Inherits}.InitializeFieldOffsets(dummy);")); var resetBody = Block(); var startAnimationBody = Block(); @@ -294,11 +286,15 @@ namespace Avalonia.SourceGenerator.CompositionGenerator serverGetFieldOffsetBody = ApplyGetProperty(serverGetFieldOffsetBody, prop, fieldOffsetName); server = server.AddMembers(DeclareField("int", fieldOffsetName, SyntaxKind.StaticKeyword)); - initializeFieldOffsetsBody = initializeFieldOffsetsBody.AddStatements(ExpressionStatement( + serverStaticCtorBody = serverStaticCtorBody.AddStatements(ExpressionStatement( AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, IdentifierName(fieldOffsetName), - InvocationExpression(MemberAccess(IdentifierName(uninitializedObjectName), "GetOffset"), - ArgumentList(SingletonSeparatedList(Argument( - RefExpression(MemberAccess(MemberAccess(IdentifierName(uninitializedObjectName), fieldName), "Subscriptions"))))))))); + InvocationExpression(IdentifierName("GetOffset"), + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName(uninitializedObjectName)), + Argument(RefExpression(MemberAccess( + MemberAccess(IdentifierName(uninitializedObjectName), fieldName), "Subscriptions"))) + })))))); if (prop.DefaultValue != null) { @@ -312,11 +308,6 @@ namespace Avalonia.SourceGenerator.CompositionGenerator server = server.AddMembers(ConstructorDeclaration(serverName) .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) .WithBody(serverStaticCtorBody)); - - server = server.AddMembers( - ((MethodDeclarationSyntax)ParseMemberDeclaration( - $"protected static void InitializeFieldOffsets({serverName} dummy){{}}")!) - .WithBody(initializeFieldOffsetsBody)); server = server .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( From 38469f0629e9ba235bc87d04808cdb64e9fbe560 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 26 Jun 2022 17:29:58 +0300 Subject: [PATCH 45/61] Added composition support to the headless platform --- samples/ControlCatalog.NetCore/Program.cs | 14 +++++++------ .../HeadlessVncPlatformExtensions.cs | 6 +++++- .../AvaloniaHeadlessPlatform.cs | 20 ++++++++++++++----- .../HeadlessPlatformRenderInterface.cs | 1 - src/Avalonia.Headless/HeadlessWindowImpl.cs | 5 ++++- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 32a22210c6..e5ae883d5e 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -53,7 +53,11 @@ namespace ControlCatalog.NetCore else if (args.Contains("--full-headless")) { return builder - .UseHeadless(true) + .UseHeadless(new AvaloniaHeadlessPlatformOptions + { + UseHeadlessDrawing = true, + UseComposition = false + }) .AfterSetup(_ => { DispatcherTimer.RunOnce(async () => @@ -63,12 +67,11 @@ namespace ControlCatalog.NetCore var tc = window.GetLogicalDescendants().OfType().First(); foreach (var page in tc.Items.Cast().ToList()) { - // Skip DatePicker because of some layout bug in grid - if (page.Header.ToString() == "DatePicker") + if (page.Header.ToString() == "DatePicker" || page.Header.ToString() == "TreeView") continue; Console.WriteLine("Selecting " + page.Header); tc.SelectedItem = page; - await Task.Delay(500); + await Task.Delay(50); } Console.WriteLine("Selecting the first page"); tc.SelectedItem = tc.Items.OfType().First(); @@ -77,7 +80,7 @@ namespace ControlCatalog.NetCore for (var c = 0; c < 3; c++) { GC.Collect(2, GCCollectionMode.Forced); - await Task.Delay(500); + await Task.Delay(00); } void FormatMem(string metric, long bytes) @@ -87,7 +90,6 @@ namespace ControlCatalog.NetCore FormatMem("GC allocated bytes", GC.GetTotalMemory(true)); FormatMem("WorkingSet64", Process.GetCurrentProcess().WorkingSet64); - }, TimeSpan.FromSeconds(1)); }) .StartWithClassicDesktopLifetime(args); diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs b/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs index bd215296c8..02a1e57701 100644 --- a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs +++ b/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs @@ -20,7 +20,11 @@ namespace Avalonia var tcpServer = new TcpListener(host == null ? IPAddress.Loopback : IPAddress.Parse(host), port); tcpServer.Start(); return builder - .UseHeadless(false) + .UseHeadless(new AvaloniaHeadlessPlatformOptions + { + UseComposition = true, + UseHeadlessDrawing = false + }) .AfterSetup(_ => { var lt = ((IClassicDesktopStyleApplicationLifetime)builder.Instance.ApplicationLifetime); diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 0ca2733cde..d957b99747 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -7,12 +7,14 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; namespace Avalonia.Headless { public static class AvaloniaHeadlessPlatform { + internal static Compositor Compositor { get; private set; } class RenderTimer : DefaultRenderTimer { private readonly int _framesPerSecond; @@ -55,7 +57,7 @@ namespace Avalonia.Headless public ITrayIconImpl CreateTrayIcon() => null; } - internal static void Initialize() + internal static void Initialize(AvaloniaHeadlessPlatformOptions opts) { AvaloniaLocator.CurrentMutable .Bind().ToConstant(new HeadlessPlatformThreadingInterface()) @@ -71,6 +73,8 @@ namespace Avalonia.Headless .Bind().ToSingleton() .Bind().ToConstant(new HeadlessWindowingPlatform()) .Bind().ToSingleton(); + if (opts.UseComposition) + Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), null); } @@ -82,15 +86,21 @@ namespace Avalonia.Headless } } - + + public class AvaloniaHeadlessPlatformOptions + { + public bool UseComposition { get; set; } = true; + public bool UseHeadlessDrawing { get; set; } = true; + } + public static class AvaloniaHeadlessPlatformExtensions { - public static T UseHeadless(this T builder, bool headlessDrawing = true) + public static T UseHeadless(this T builder, AvaloniaHeadlessPlatformOptions opts) where T : AppBuilderBase, new() { - if (headlessDrawing) + if(opts.UseHeadlessDrawing) builder.UseRenderingSubsystem(HeadlessPlatformRenderInterface.Initialize, "Headless"); - return builder.UseWindowingSubsystem(AvaloniaHeadlessPlatform.Initialize, "Headless"); + return builder.UseWindowingSubsystem(() => AvaloniaHeadlessPlatform.Initialize(opts), "Headless"); } } } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index b3c56e1ea0..5576368240 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -418,7 +418,6 @@ namespace Avalonia.Headless public void DrawLine(IPen pen, Point p1, Point p2) { - throw new NotImplementedException(); } public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index c976921bc3..d0f5e2ac2f 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -10,6 +10,7 @@ using Avalonia.Input.Raw; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.Utilities; @@ -52,7 +53,9 @@ namespace Avalonia.Headless public Action ScalingChanged { get; set; } public IRenderer CreateRenderer(IRenderRoot root) - => new DeferredRenderer(root, AvaloniaLocator.Current.GetService()); + => AvaloniaHeadlessPlatform.Compositor != null + ? new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor) + : new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService()); public void Invalidate(Rect rect) { From 1251ede74af0a51d0e173003c4906740f8e00e1d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 26 Jun 2022 17:36:44 +0300 Subject: [PATCH 46/61] Release CompositionVisual reference when Visual is detached --- src/Avalonia.Base/Visual.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 0bfdd6d516..7db218d2df 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -495,6 +495,7 @@ namespace Avalonia DisableTransitions(); OnDetachedFromVisualTree(e); + CompositionVisual = null; DetachedFromVisualTree?.Invoke(this, e); e.Root?.Renderer?.AddDirty(this); From 62571a69b613b7ff6be762da2c742fdde1380b03 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 26 Jun 2022 17:37:09 +0300 Subject: [PATCH 47/61] Fix crashes in headless memory benchmark --- samples/ControlCatalog.NetCore/Program.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index e5ae883d5e..df7ae67ef4 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -56,10 +56,16 @@ namespace ControlCatalog.NetCore .UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = true, - UseComposition = false + UseComposition = true }) .AfterSetup(_ => { + static Task LowPriorityDelay(int ms) + { + return Task.Delay(ms).ContinueWith(_ => + Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Background)) + .Unwrap(); + } DispatcherTimer.RunOnce(async () => { var window = ((IClassicDesktopStyleApplicationLifetime)Application.Current.ApplicationLifetime) @@ -71,7 +77,7 @@ namespace ControlCatalog.NetCore continue; Console.WriteLine("Selecting " + page.Header); tc.SelectedItem = page; - await Task.Delay(50); + await LowPriorityDelay(20); } Console.WriteLine("Selecting the first page"); tc.SelectedItem = tc.Items.OfType().First(); From 1849ca4caa660a172fa16a3e616a58be84d12340 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 28 Jun 2022 23:50:22 +0300 Subject: [PATCH 48/61] Fixed matrix multiplication order --- .../Rendering/Composition/Server/DrawingContextProxy.cs | 4 ++-- .../Rendering/Composition/Server/ServerVisual.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 2fd87f6620..e261507f60 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -33,7 +33,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont set => _visualBrushRenderer.VisualBrushDrawList = value; } - public Matrix PreTransform { get; set; } = Matrix.Identity; + public Matrix PostTransform { get; set; } = Matrix.Identity; public void Dispose() { @@ -44,7 +44,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont public Matrix Transform { get => _transform; - set => _impl.Transform = PreTransform * (_transform = value); + set => _impl.Transform = (_transform = value) * PostTransform; } public void Clear(Color color) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 06f087258b..3b36dfb87e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -41,7 +41,7 @@ namespace Avalonia.Rendering.Composition.Server Root!.RenderedVisuals++; var transform = GlobalTransformMatrix; - canvas.PreTransform = MatrixUtils.ToMatrix(transform); + canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; if (Opacity != 1) canvas.PushOpacity(Opacity); @@ -56,7 +56,7 @@ namespace Avalonia.Rendering.Composition.Server RenderCore(canvas, currentTransformedClip); // Hack to force invalidation of SKMatrix - canvas.PreTransform = MatrixUtils.ToMatrix(transform); + canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; if (OpacityMaskBrush != null) From f6506f19e83d6f8456a86d749324b18bb1cba4ee Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 29 Jun 2022 23:57:03 +0300 Subject: [PATCH 49/61] Fixed Ref finalizer --- src/Avalonia.Base/Utilities/Ref.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/Ref.cs b/src/Avalonia.Base/Utilities/Ref.cs index 7209f02720..95a1c23883 100644 --- a/src/Avalonia.Base/Utilities/Ref.cs +++ b/src/Avalonia.Base/Utilities/Ref.cs @@ -159,7 +159,7 @@ namespace Avalonia.Utilities ~Ref() { - _counter?.Release(); + Dispose(); } public T Item From b4512e0da8f2334b1a1b2826e600f2386a30a4af Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 30 Jun 2022 00:05:10 +0300 Subject: [PATCH 50/61] Clear the draw list before letting go of the visual --- src/Avalonia.Base/Visual.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 7db218d2df..716b5f261d 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -495,7 +495,12 @@ namespace Avalonia DisableTransitions(); OnDetachedFromVisualTree(e); - CompositionVisual = null; + if (CompositionVisual != null) + { + CompositionVisual.DrawList = null; + CompositionVisual = null; + } + DetachedFromVisualTree?.Invoke(this, e); e.Root?.Renderer?.AddDirty(this); From 77dca2c5356b544e57dab77ca563885e00989f0f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 30 Jun 2022 15:53:07 +0300 Subject: [PATCH 51/61] VerifyAccess --- src/Avalonia.Base/Rendering/Composition/Compositor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 28c81dfb91..1bdae44cb9 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -62,6 +62,7 @@ namespace Avalonia.Rendering.Composition /// A task that completes when sent changes are applied and rendered on the render thread public Task RequestCommitAsync() { + Dispatcher.UIThread.VerifyAccess(); var batch = new Batch(); using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool)) @@ -126,6 +127,7 @@ namespace Avalonia.Rendering.Composition internal void RegisterForSerialization(CompositionObject compositionObject) { + Dispatcher.UIThread.VerifyAccess(); _objectsForSerialization.Add(compositionObject); QueueImplicitBatchCommit(); } From 5b77b7d24bab22929c38f5303cd5bd34262ff474 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 2 Jul 2022 13:55:09 +0300 Subject: [PATCH 52/61] UseComposition -> UseCompositor --- samples/ControlCatalog.NetCore/Program.cs | 2 +- src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs | 2 +- src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index df7ae67ef4..7423428a0a 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -56,7 +56,7 @@ namespace ControlCatalog.NetCore .UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = true, - UseComposition = true + UseCompositor = true }) .AfterSetup(_ => { diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs b/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs index 02a1e57701..cc7d5ef30d 100644 --- a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs +++ b/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs @@ -22,7 +22,7 @@ namespace Avalonia return builder .UseHeadless(new AvaloniaHeadlessPlatformOptions { - UseComposition = true, + UseCompositor = true, UseHeadlessDrawing = false }) .AfterSetup(_ => diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index d957b99747..fe9283b26f 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -73,7 +73,7 @@ namespace Avalonia.Headless .Bind().ToSingleton() .Bind().ToConstant(new HeadlessWindowingPlatform()) .Bind().ToSingleton(); - if (opts.UseComposition) + if (opts.UseCompositor) Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), null); } @@ -89,7 +89,7 @@ namespace Avalonia.Headless public class AvaloniaHeadlessPlatformOptions { - public bool UseComposition { get; set; } = true; + public bool UseCompositor { get; set; } = true; public bool UseHeadlessDrawing { get; set; } = true; } From 862b318906b30a9a73f945e64597cf8997801581 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 12 Jul 2022 13:40:44 +0300 Subject: [PATCH 53/61] Use dictionaries for storing composition animation infrastructure --- .../Animations/AnimatedValueStore.cs | 124 ------------ .../Animations/AnimationInstanceBase.cs | 16 +- .../Animations/ExpressionAnimationInstance.cs | 4 +- .../Animations/IAnimationInstance.cs | 2 +- .../Animations/KeyFrameAnimationInstance.cs | 4 +- .../Composition/CompositionObject.cs | 3 + .../Composition/Server/CompositionProperty.cs | 15 ++ .../Composition/Server/ServerObject.cs | 101 ++++++---- .../Server/ServerVisual.DirtyProperties.cs | 30 +-- .../Composition/Server/ServerVisual.cs | 2 +- .../Utilities/SmallDictionary.cs | 181 ++++++++++++++++++ .../CompositionGenerator/Generator.cs | 154 +++++---------- 12 files changed, 348 insertions(+), 288 deletions(-) delete mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs create mode 100644 src/Avalonia.Base/Utilities/SmallDictionary.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs deleted file mode 100644 index 92aa1f35bb..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Avalonia.Rendering.Composition.Expressions; -using Avalonia.Rendering.Composition.Server; -using Avalonia.Rendering.Composition.Transport; -using Avalonia.Utilities; - -namespace Avalonia.Rendering.Composition.Animations -{ - /// - /// This is the first element of both animated and non-animated value stores. - /// It's used to propagate property invalidation to subscribers - /// - - internal struct ServerObjectSubscriptionStore - { - public bool IsValid; - public RefTrackingDictionary? Subscribers; - - public void Invalidate() - { - if (IsValid) - return; - IsValid = false; - if (Subscribers != null) - foreach (var sub in Subscribers) - sub.Key.Invalidate(); - } - } - - /// - /// The value store for non-animated values that can still be referenced by animations. - /// Simply stores the value and notifies subscribers - /// - internal struct ServerValueStore - { - public ServerObjectSubscriptionStore Subscriptions; - private T _value; - public T Value - { - set - { - _value = value; - Subscriptions.Invalidate(); - } - get - { - Subscriptions.IsValid = true; - return _value; - } - } - } - - /// - /// Value store for potentially animated values. Can hold both direct value and animation instance. - /// Is also responsible for activating/deactivating the animation when container object is activated/deactivated - /// - /// - [StructLayout(LayoutKind.Sequential)] - internal struct ServerAnimatedValueStore where T : struct - { - public ServerObjectSubscriptionStore Subscriptions; - private IAnimationInstance? _animation; - private T _direct; - private T? _lastAnimated; - - public T GetAnimated(ServerCompositor compositor) - { - Subscriptions.IsValid = true; - if (_animation == null) - return _direct; - var v = _animation.Evaluate(compositor.ServerNow, ExpressionVariant.Create(_direct)) - .CastOrDefault(); - _lastAnimated = v; - return v; - } - - public void Activate(ServerObject parent) - { - if (_animation != null) - _animation.Activate(); - } - - public void Deactivate(ServerObject parent) - { - if (_animation != null) - _animation.Deactivate(); - } - - private T LastAnimated => _animation != null ? _lastAnimated ?? _direct : _direct; - - public bool IsAnimation => _animation != null; - - public void SetAnimation(ServerObject target, TimeSpan commitedAt, IAnimationInstance animation, int storeOffset) - { - _direct = default; - if (_animation != null) - { - if (target.IsActive) - _animation.Deactivate(); - } - - _animation = animation; - _animation.Initialize(commitedAt, ExpressionVariant.Create(LastAnimated), storeOffset); - if (target.IsActive) - _animation.Activate(); - - Subscriptions.Invalidate(); - } - - public void SetValue(ServerObject target, T value) - { - if (_animation != null) - { - if (target.IsActive) - _animation.Deactivate(); - } - - _animation = null; - _direct = value; - Subscriptions.Invalidate(); - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs index 35aa8de1bc..80e64118ee 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs @@ -12,10 +12,10 @@ namespace Avalonia.Rendering.Composition.Animations; /// internal abstract class AnimationInstanceBase : IAnimationInstance { - private List<(ServerObject obj, int member)>? _trackedObjects; + private List<(ServerObject obj, CompositionProperty member)>? _trackedObjects; protected PropertySetSnapshot Parameters { get; } public ServerObject TargetObject { get; } - protected int StoreOffset { get; private set; } + protected CompositionProperty Property { get; private set; } = null!; private bool _invalidated; public AnimationInstanceBase(ServerObject target, PropertySetSnapshot parameters) @@ -24,7 +24,7 @@ internal abstract class AnimationInstanceBase : IAnimationInstance TargetObject = target; } - protected void Initialize(int storeOffset, HashSet<(string name, string member)> trackedObjects) + protected void Initialize(CompositionProperty property, HashSet<(string name, string member)> trackedObjects) { if (trackedObjects.Count > 0) { @@ -34,22 +34,22 @@ internal abstract class AnimationInstanceBase : IAnimationInstance var obj = Parameters.GetObjectParameter(t.name); if (obj is ServerObject tracked) { - var off = tracked.GetFieldOffset(t.member); + var off = tracked.GetCompositionProperty(t.member); if (off == null) #if DEBUG throw new InvalidCastException("Attempting to subscribe to unknown field"); #else continue; #endif - _trackedObjects.Add((tracked, off.Value)); + _trackedObjects.Add((tracked, off)); } } } - StoreOffset = storeOffset; + Property = property; } - public abstract void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset); + public abstract void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property); protected abstract ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue); public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) @@ -77,6 +77,6 @@ internal abstract class AnimationInstanceBase : IAnimationInstance if (_invalidated) return; _invalidated = true; - TargetObject.NotifyAnimatedValueChanged(StoreOffset); + TargetObject.NotifyAnimatedValueChanged(Property); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs index 445cef9a08..764bac9931 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs @@ -29,12 +29,12 @@ namespace Avalonia.Rendering.Composition.Animations return _expression.Evaluate(ref ctx); } - public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset) + public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property) { _startingValue = startingValue; var hs = new HashSet<(string, string)>(); _expression.CollectReferences(hs); - base.Initialize(storeOffset, hs); + base.Initialize(property, hs); } public ExpressionAnimationInstance(Expression expression, diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs index 05d1b50953..4e1972f2c6 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.Composition.Animations { ServerObject TargetObject { get; } ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue); - void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset); + void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property); void Activate(); void Deactivate(); void Invalidate(); diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs index 7268780298..0c0fcfaf2b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs @@ -151,7 +151,7 @@ namespace Avalonia.Rendering.Composition.Animations return f.Value; } - public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset) + public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property) { _startedAt = startedAt; _startingValue = startingValue.CastOrDefault(); @@ -160,7 +160,7 @@ namespace Avalonia.Rendering.Composition.Animations // TODO: Update subscriptions based on the current keyframe rather than keeping subscriptions to all of them foreach (var frame in _keyFrames) frame.Expression?.CollectReferences(hs); - Initialize(storeOffset, hs); + Initialize(property, hs); } public override void Activate() diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs index 0ed9a3c75d..f529ee9cff 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -3,6 +3,7 @@ using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Rendering.Composition { @@ -17,6 +18,8 @@ namespace Avalonia.Rendering.Composition /// The collection of implicit animations attached to this object. /// public ImplicitAnimationCollection? ImplicitAnimations { get; set; } + + private protected InlineDictionary PendingAnimations; internal CompositionObject(Compositor compositor, ServerObject server) { Compositor = compositor; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs b/src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs new file mode 100644 index 0000000000..282c0e113d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; + +namespace Avalonia.Rendering.Composition.Server; + +internal class CompositionProperty +{ + private static volatile int s_NextId = 1; + public int Id { get; private set; } + + public static CompositionProperty Register() => new() + { + Id = Interlocked.Increment(ref s_NextId) + }; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index 4c358605fc..c6b468a32f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -21,7 +21,25 @@ namespace Avalonia.Rendering.Composition.Server public long ItselfLastChangedBy { get; private set; } private uint _activationCount; public bool IsActive => _activationCount != 0; + private InlineDictionary _subscriptions; + private InlineDictionary _animations; + + private class ServerObjectSubscriptionStore + { + public bool IsValid; + public RefTrackingDictionary? Subscribers; + public void Invalidate() + { + if (IsValid) + return; + IsValid = false; + if (Subscribers != null) + foreach (var sub in Subscribers) + sub.Key.Invalidate(); + } + } + public ServerObject(ServerCompositor compositor) { Compositor = compositor; @@ -62,46 +80,62 @@ namespace Avalonia.Rendering.Composition.Server } - [StructLayout(LayoutKind.Sequential)] - protected class OffsetDummy + void InvalidateSubscriptions(CompositionProperty property) { -#pragma warning disable CS0649 - public FillerStruct Filler; -#pragma warning restore CS0649 + if(_subscriptions.TryGetValue(property, out var subs)) + subs.Invalidate(); } - - [StructLayout(LayoutKind.Sequential)] - protected unsafe struct FillerStruct + + protected void SetValue(CompositionProperty prop, out T field, T value) { - public fixed byte FillerData[8192]; + field = value; + InvalidateSubscriptions(prop); } - private static readonly object s_OffsetDummy = new OffsetDummy(); - protected static T GetOffsetDummy() where T : ServerObject => Unsafe.As(s_OffsetDummy); + protected T GetValue(CompositionProperty prop, ref T field) + { + if (_subscriptions.TryGetValue(prop, out var subs)) + subs.IsValid = true; + return field; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static int GetOffset(ServerObject obj, ref ServerObjectSubscriptionStore field) + protected void SetAnimatedValue(CompositionProperty prop, ref T field, + TimeSpan commitedAt, IAnimationInstance animation) where T : struct { - return Unsafe.ByteOffset(ref obj._activationCount, - ref Unsafe.As(ref field)) - .ToInt32(); + if (IsActive && _animations.TryGetValue(prop, out var oldAnimation)) + oldAnimation.Deactivate(); + _animations[prop] = animation; + + animation.Initialize(commitedAt, ExpressionVariant.Create(field), prop); + if(IsActive) + animation.Activate(); + + InvalidateSubscriptions(prop); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected ref ServerObjectSubscriptionStore GetStoreFromOffset(int offset) + protected void SetAnimatedValue(CompositionProperty property, out T field, T value) { -#if DEBUG - if (offset == 0) - throw new InvalidOperationException(); -#endif - return ref Unsafe.As(ref Unsafe.AddByteOffset(ref _activationCount, - new IntPtr(offset))); + if (_animations.TryGetAndRemoveValue(property, out var animation) && IsActive) + animation.Deactivate(); + field = value; + InvalidateSubscriptions(property); } + + protected T GetAnimatedValue(CompositionProperty property, ref T field) where T : struct + { + if (_subscriptions.TryGetValue(property, out var subscriptions)) + subscriptions.IsValid = true; + + if (_animations.TryGetValue(property, out var animation)) + field = animation.Evaluate(Compositor.ServerNow, ExpressionVariant.Create(field)) + .CastOrDefault(); - public virtual void NotifyAnimatedValueChanged(int offset) + return field; + } + + public virtual void NotifyAnimatedValueChanged(CompositionProperty prop) { - ref var store = ref GetStoreFromOffset(offset); - store.Invalidate(); + InvalidateSubscriptions(prop); ValuesInvalidated(); } @@ -110,21 +144,22 @@ namespace Avalonia.Rendering.Composition.Server } - public void SubscribeToInvalidation(int member, IAnimationInstance animation) + public void SubscribeToInvalidation(CompositionProperty member, IAnimationInstance animation) { - ref var store = ref GetStoreFromOffset(member); + if (!_subscriptions.TryGetValue(member, out var store)) + _subscriptions[member] = store = new ServerObjectSubscriptionStore(); if (store.Subscribers == null) store.Subscribers = new(); store.Subscribers.AddRef(animation); } - public void UnsubscribeFromInvalidation(int member, IAnimationInstance animation) + public void UnsubscribeFromInvalidation(CompositionProperty member, IAnimationInstance animation) { - ref var store = ref GetStoreFromOffset(member); - store.Subscribers?.ReleaseRef(animation); + if(_subscriptions.TryGetValue(member, out var store)) + store.Subscribers?.ReleaseRef(animation); } - public virtual int? GetFieldOffset(string fieldName) => null; + public virtual CompositionProperty? GetCompositionProperty(string fieldName) => null; protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs index e434b97b2f..c5af74e2dd 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs @@ -50,27 +50,27 @@ partial class ServerCompositionVisual _clipSizeDirty = true; } - public override void NotifyAnimatedValueChanged(int offset) + public override void NotifyAnimatedValueChanged(CompositionProperty offset) { base.NotifyAnimatedValueChanged(offset); - if (offset == s_OffsetOf_clipToBounds - || offset == s_OffsetOf_opacity - || offset == s_OffsetOf_size) + if (offset == s_IdOfClipToBoundsProperty + || offset == s_IdOfOpacityProperty + || offset == s_IdOfSizeProperty) IsDirtyComposition = true; - if (offset == s_OffsetOf_size - || offset == s_OffsetOf_anchorPoint - || offset == s_OffsetOf_centerPoint - || offset == s_OffsetOf_adornedVisual - || offset == s_OffsetOf_transformMatrix - || offset == s_OffsetOf_scale - || offset == s_OffsetOf_rotationAngle - || offset == s_OffsetOf_orientation - || offset == s_OffsetOf_offset) + if (offset == s_IdOfSizeProperty + || offset == s_IdOfAnchorPointProperty + || offset == s_IdOfCenterPointProperty + || offset == s_IdOfAdornedVisualProperty + || offset == s_IdOfTransformMatrixProperty + || offset == s_IdOfScaleProperty + || offset == s_IdOfRotationAngleProperty + || offset == s_IdOfOrientationProperty + || offset == s_IdOfOffsetProperty) _combinedTransformDirty = true; - if (offset == s_OffsetOf_clipToBounds - || offset == s_OffsetOf_size) + if (offset == s_IdOfClipToBoundsProperty + || offset == s_IdOfSizeProperty) _clipSizeDirty = true; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 3b36dfb87e..0ba63c70bc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -121,7 +121,7 @@ namespace Avalonia.Rendering.Composition.Server var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds; - if (_parent.Value?.IsDirtyComposition == true) + if (_parent?.IsDirtyComposition == true) { IsDirtyComposition = true; _isDirtyForUpdate = true; diff --git a/src/Avalonia.Base/Utilities/SmallDictionary.cs b/src/Avalonia.Base/Utilities/SmallDictionary.cs new file mode 100644 index 0000000000..b8f532c747 --- /dev/null +++ b/src/Avalonia.Base/Utilities/SmallDictionary.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Avalonia.Utilities; + +public struct InlineDictionary where TKey : class where TValue : class +{ + object? _data; + TValue? _value; + + void SetCore(TKey key, TValue value, bool overwrite) + { + if (_data == null) + { + _data = key; + _value = value; + } + else if (_data is KeyValuePair[] arr) + { + var free = -1; + for (var c = 0; c < arr.Length; c++) + { + if (arr[c].Key == key) + { + if (overwrite) + { + arr[c] = new(key, value); + return; + } + else + throw new ArgumentException("Key already exists in dictionary"); + } + + if (arr[c].Key == null) + free = c; + } + + if (free != -1) + { + arr[free] = new KeyValuePair(key, value); + return; + } + + // Upgrade to dictionary + var newDic = new Dictionary(); + foreach (var kvp in arr) + newDic.Add(kvp.Key!, kvp.Value!); + newDic.Add(key, value); + _data = newDic; + } + else if (_data is Dictionary dic) + { + if (overwrite) + dic[key] = value; + else + dic.Add(key, value); + } + else + { + // We have a single element, upgrade to array + arr = new KeyValuePair[6]; + arr[0] = new KeyValuePair((TKey)_data, _value); + arr[1] = new KeyValuePair(key, value); + _data = arr; + _value = null; + } + } + + public void Add(TKey key, TValue value) => SetCore(key, value, false); + public void Set(TKey key, TValue value) => SetCore(key, value, true); + + public TValue this[TKey key] + { + get + { + if (TryGetValue(key, out var rv)) + return rv; + throw new KeyNotFoundException(); + } + set => Set(key, value); + } + + public bool Remove(TKey key) + { + if (_data == key) + { + _data = null; + _value = null; + return true; + } + else if (_data is KeyValuePair[] arr) + { + for (var c = 0; c < arr.Length; c++) + { + if (arr[c].Key == key) + { + arr[c] = default; + return true; + } + } + + return false; + } + else if (_data is Dictionary dic) + return dic.Remove(key); + + return false; + } + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)]out TValue value) + { + if (_data == key) + { + value = _value!; + return true; + } + else if (_data is KeyValuePair[] arr) + { + for (var c = 0; c < arr.Length; c++) + { + if (arr[c].Key == key) + { + value = arr[c].Value!; + return true; + } + } + + value = null; + return false; + } + else if (_data is Dictionary dic) + return dic.TryGetValue(key, out value); + + value = null; + return false; + } + + + public bool TryGetAndRemoveValue(TKey key, [MaybeNullWhen(false)]out TValue value) + { + if (_data == key) + { + value = _value!; + _value = null; + _data = null; + return true; + } + else if (_data is KeyValuePair[] arr) + { + for (var c = 0; c < arr.Length; c++) + { + if (arr[c].Key == key) + { + value = arr[c].Value!; + arr[c] = default; + return true; + } + } + + value = null; + return false; + } + else if (_data is Dictionary dic) + { + if (!dic.TryGetValue(key, out value)) + return false; + dic.Remove(key); + } + + value = null; + return false; + } + + public TValue GetAndRemove(TKey key) + { + if (TryGetAndRemoveValue(key, out var v)) + return v; + throw new KeyNotFoundException(); + } +} \ No newline at end of file diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index 0bb4e30f75..18f1d1c1e5 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -42,8 +42,10 @@ namespace Avalonia.SourceGenerator.CompositionGenerator string ChangedFieldsTypeName(GClass c) => c.Name + "ChangedFields"; string ChangedFieldsFieldName(GClass c) => "_changedFieldsOf" + c.Name; string PropertyBackingFieldName(GProperty prop) => "_" + prop.Name.WithLowerFirst(); - string ServerPropertyOffsetFieldName(GProperty prop) => "s_OffsetOf" + PropertyBackingFieldName(prop); - string PropertyPendingAnimationFieldName(GProperty prop) => "_pendingAnimationFor" + prop.Name; + string CompositionPropertyField(GProperty prop) => "s_IdOf" + prop.Name + "Property"; + + ExpressionSyntax ClientProperty(GClass c, GProperty p) => + MemberAccess(ServerName(c.Name), CompositionPropertyField(p)); void GenerateClass(GClass cl) { @@ -140,30 +142,10 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName("BatchStreamReader"))) .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); - var applyMethodBody = Block( - ExpressionStatement(InvocationExpression(MemberAccess(IdentifierName("base"), "ApplyCore"), - ArgumentList(SeparatedList(new[] {Argument(IdentifierName("changes"))})))), - LocalDeclarationStatement(VariableDeclaration(ParseTypeName("var")) - .WithVariables(SingletonSeparatedList( - VariableDeclarator(changesVarName) - .WithInitializer(EqualsValueClause(CastExpression(ParseTypeName(changesName), - IdentifierName("changes"))))))), - ExpressionStatement(InvocationExpression(IdentifierName("ApplyChangesExtra")) - .AddArgumentListArguments(Argument(IdentifierName("c")))) - ); - - var uninitializedObjectName = "dummy"; - var serverStaticCtorBody = Block( - ParseStatement($"var dummy = GetOffsetDummy<{serverName}>();") - ); - - var resetBody = Block(); var startAnimationBody = Block(); var serverGetPropertyBody = Block(); - var serverGetFieldOffsetBody = Block(); - var activatedBody = Block(ParseStatement("base.Activated();")); - var deactivatedBody = Block(ParseStatement("base.Deactivated();")); + var serverGetCompositionPropertyBody = Block(); var serializeMethodBody = SerializeChangesPrologue(cl); var deserializeMethodBody = DeserializeChangesPrologue(cl); @@ -172,16 +154,12 @@ namespace Avalonia.SourceGenerator.CompositionGenerator foreach (var prop in cl.Properties) { var fieldName = PropertyBackingFieldName(prop); - var animatedFieldName = PropertyPendingAnimationFieldName(prop); - var fieldOffsetName = ServerPropertyOffsetFieldName(prop); var propType = ParseTypeName(prop.Type); var filteredPropertyType = prop.Type.TrimEnd('?'); var isObject = _objects.Contains(filteredPropertyType); var isNullable = prop.Type.EndsWith("?"); bool isPassthrough = false; - if (prop.Animated) - client = client.AddMembers(DeclareField("IAnimationInstance?", animatedFieldName)); client = GenerateClientProperty(client, cl, prop, propType, isObject, isNullable); var animatedServer = prop.Animated; @@ -201,35 +179,50 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if (animatedServer) server = server.AddMembers( - DeclareField("ServerAnimatedValueStore<" + serverPropertyType + ">", fieldName), + DeclareField(serverPropertyType, fieldName), PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) .AddModifiers(SyntaxKind.PublicKeyword) .WithExpressionBody(ArrowExpressionClause( - InvocationExpression(MemberAccess(fieldName, "GetAnimated"), - ArgumentList(SingletonSeparatedList(Argument(IdentifierName("Compositor"))))))) + InvocationExpression(IdentifierName("GetAnimatedValue"), + ArgumentList(SeparatedList(new[]{ + Argument(IdentifierName(CompositionPropertyField(prop))), + Argument(null, Token(SyntaxKind.RefKeyword), IdentifierName(fieldName)) + } + ))))) .WithSemicolonToken(Semicolon()) ); else { server = server - .AddMembers(DeclareField("ServerValueStore<" + serverPropertyType + ">", fieldName)) + .AddMembers(DeclareField(serverPropertyType, fieldName)) .AddMembers(PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) .AddModifiers(SyntaxKind.PublicKeyword) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, - Block(ReturnStatement(MemberAccess(IdentifierName(fieldName), "Value")))), + Block(ReturnStatement( + InvocationExpression(IdentifierName("GetValue"), + ArgumentList(SeparatedList(new[]{ + Argument(IdentifierName(CompositionPropertyField(prop))), + Argument(null, Token(SyntaxKind.RefKeyword), IdentifierName(fieldName)) + } + )))))), AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, Block( ParseStatement("var changed = false;"), IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, - MemberAccess(IdentifierName(fieldName), "Value"), + IdentifierName(fieldName), IdentifierName("value")), Block( ParseStatement("On" + prop.Name + "Changing();"), ParseStatement($"changed = true;")) ), - ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, - MemberAccess(IdentifierName(fieldName), "Value"), IdentifierName("value"))), + ExpressionStatement(InvocationExpression(IdentifierName("SetValue"), + ArgumentList(SeparatedList(new[]{ + Argument(IdentifierName(CompositionPropertyField(prop))), + Argument(null, Token(SyntaxKind.OutKeyword), IdentifierName(fieldName)), + Argument(IdentifierName("value")) + } + )))), ParseStatement($"if(changed) On" + prop.Name + "Changed();") )) )) @@ -238,36 +231,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); } - - if (animatedServer) - applyMethodBody = applyMethodBody.AddStatements( - IfStatement(MemberAccess(changesVar, prop.Name, "IsValue"), - ExpressionStatement( - InvocationExpression(MemberAccess(fieldName, "SetValue"), - ArgumentList(SeparatedList(new[] - { - Argument(IdentifierName("this")), - Argument(MemberAccess(changesVar, prop.Name, "Value")), - }))))), - IfStatement(MemberAccess(changesVar, prop.Name, "IsAnimation"), - ExpressionStatement( - InvocationExpression(MemberAccess(fieldName, "SetAnimation"), - ArgumentList(SeparatedList(new[] - { - Argument(IdentifierName("this")), - Argument(ParseExpression("c.Batch.CommitedAt")), - Argument(MemberAccess(changesVar, prop.Name, "Animation")), - Argument(IdentifierName(fieldOffsetName)) - }))))) - ); - else - applyMethodBody = applyMethodBody.AddStatements( - IfStatement(MemberAccess(changesVar, prop.Name, "IsSet"), - ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, - IdentifierName(prop.Name), MemberAccess(changesVar, prop.Name, "Value")))) - - ); - + resetBody = resetBody.AddStatements( ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); @@ -277,24 +241,15 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if (animatedServer) { startAnimationBody = ApplyStartAnimation(startAnimationBody, cl, prop); - activatedBody = activatedBody.AddStatements(ParseStatement($"{fieldName}.Activate(this);")); - deactivatedBody = deactivatedBody.AddStatements(ParseStatement($"{fieldName}.Deactivate(this);")); } serverGetPropertyBody = ApplyGetProperty(serverGetPropertyBody, prop); - serverGetFieldOffsetBody = ApplyGetProperty(serverGetFieldOffsetBody, prop, fieldOffsetName); + serverGetCompositionPropertyBody = ApplyGetProperty(serverGetCompositionPropertyBody, prop, CompositionPropertyField(prop)); - server = server.AddMembers(DeclareField("int", fieldOffsetName, SyntaxKind.StaticKeyword)); - serverStaticCtorBody = serverStaticCtorBody.AddStatements(ExpressionStatement( - AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, IdentifierName(fieldOffsetName), - InvocationExpression(IdentifierName("GetOffset"), - ArgumentList(SeparatedList(new[] - { - Argument(IdentifierName(uninitializedObjectName)), - Argument(RefExpression(MemberAccess( - MemberAccess(IdentifierName(uninitializedObjectName), fieldName), "Subscriptions"))) - })))))); + server = server.AddMembers(DeclareField("CompositionProperty", CompositionPropertyField(prop), + EqualsValueClause(ParseExpression("CompositionProperty.Register()")), + SyntaxKind.InternalKeyword, SyntaxKind.StaticKeyword)); if (prop.DefaultValue != null) { @@ -304,16 +259,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator IdentifierName(prop.Name), ParseExpression(prop.DefaultValue)))); } } - - server = server.AddMembers(ConstructorDeclaration(serverName) - .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) - .WithBody(serverStaticCtorBody)); - - server = server - .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( - $"protected override void Activated(){{}}")!).WithBody(activatedBody)) - .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( - $"protected override void Deactivated(){{}}")!).WithBody(deactivatedBody)); + if (cl.Properties.Count > 0) { server = server.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( @@ -331,12 +277,15 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .AddMembers( MethodDeclaration(ParseTypeName("void"), "InitializeDefaultsExtra") .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); - + if (cl.Properties.Count > 0) + { + serializeMethodBody = serializeMethodBody.AddStatements(SerializeChangesEpilogue(cl)); client = client.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( $"private protected override void SerializeChangesCore(BatchStreamWriter writer){{}}")!) .WithBody(serializeMethodBody)); - + } + if (list != null) client = AppendListProxy(list, client); @@ -344,7 +293,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator client = WithStartAnimation(client, startAnimationBody); server = WithGetPropertyForAnimation(server, serverGetPropertyBody); - server = WithGetFieldOffset(server, serverGetFieldOffsetBody); + server = WithGetCompositionProperty(server, serverGetCompositionPropertyBody); if(cl.Implements.Count > 0) foreach (var impl in cl.Implements) @@ -430,8 +379,6 @@ namespace Avalonia.SourceGenerator.CompositionGenerator StatementSyntax GeneratePropertySetterAssignment(GClass cl, GProperty prop, bool isObject, bool isNullable) { - var pendingAnimationField = PropertyPendingAnimationFieldName(prop); - var code = @$" // Update the backing value {PropertyBackingFieldName(prop)} = value; @@ -444,7 +391,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator { code += @$" // Reset previous animation if any - {pendingAnimationField} = null; + PendingAnimations.Remove({ClientProperty(cl, prop)}); {ChangedFieldsFieldName(cl)} &= ~{ChangedFieldsTypeName(cl)}.{prop.Name}Animated; // Check for implicit animations if(ImplicitAnimations != null && ImplicitAnimations.TryGetValue(""{prop.Name}"", out var animation) == true) @@ -453,7 +400,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if(animation is CompositionAnimation a) {{ {ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated; - {pendingAnimationField} = a.CreateInstance(this.Server, value); + PendingAnimations[{ClientProperty(cl, prop)}] = a.CreateInstance(this.Server, value); }} // Animation is triggered by the current field, but does not necessary affects it StartAnimationGroup(animation, ""{prop.Name}"", value); @@ -471,7 +418,7 @@ if (propertyName == ""{prop.Name}"") {{ var current = {PropertyBackingFieldName(prop)}; var server = animation.CreateInstance(this.Server, finalValue); -{PropertyPendingAnimationFieldName(prop)} = server; +PendingAnimations[{ClientProperty(cl, prop)}] = server; {ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated; RegisterForSerialization(); return; @@ -512,6 +459,9 @@ return; ParseStatement($"writer.Write({ChangedFieldsFieldName(cl)});") ); } + + private BlockSyntax SerializeChangesEpilogue(GClass cl) => + Block(ParseStatement(ChangedFieldsFieldName(cl) + " = default;")); BlockSyntax ApplySerializeField(BlockSyntax body, GClass cl, GProperty prop, bool isObject, bool isPassthrough) { @@ -523,7 +473,7 @@ return; { code = $@" if(({changedFields} & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) - writer.WriteObject({PropertyPendingAnimationFieldName(prop)}); + writer.WriteObject(PendingAnimations.GetAndRemove({ClientProperty(cl, prop)})); else "; } @@ -556,7 +506,7 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); { code = $@" if((changed & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) - {PropertyBackingFieldName(prop)}.SetAnimation(this, commitedAt, reader.ReadObject(), {ServerPropertyOffsetFieldName(prop)}); + SetAnimatedValue({CompositionPropertyField(prop)}, ref {PropertyBackingFieldName(prop)}, commitedAt, reader.ReadObject()); else "; } @@ -565,7 +515,7 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); if((changed & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name}) "; if (prop.Animated) - code += $"{PropertyBackingFieldName(prop)}.SetValue(this, {readValueCode});"; + code += $"SetAnimatedValue({CompositionPropertyField(prop)}, out {PropertyBackingFieldName(prop)}, {readValueCode});"; else code += $"{prop.Name} = {readValueCode};"; return body.AddStatements(ParseStatement(code)); } @@ -583,14 +533,14 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); return cl.AddMembers(method); } - ClassDeclarationSyntax WithGetFieldOffset(ClassDeclarationSyntax cl, BlockSyntax body) + ClassDeclarationSyntax WithGetCompositionProperty(ClassDeclarationSyntax cl, BlockSyntax body) { if (body.Statements.Count == 0) return cl; body = body.AddStatements( - ParseStatement("return base.GetFieldOffset(name);")); + ParseStatement("return base.GetCompositionProperty(name);")); var method = ((MethodDeclarationSyntax)ParseMemberDeclaration( - $"public override int? GetFieldOffset(string name){{}}")) + $"public override CompositionProperty? GetCompositionProperty(string name){{}}")) .WithBody(body); return cl.AddMembers(method); From 35e18ce765bbb20e831029f885f067c1c0f52119 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 12 Jul 2022 13:56:11 +0300 Subject: [PATCH 54/61] Fixed yet another invalidation case --- .../Rendering/Composition/Server/ServerVisual.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 0ba63c70bc..6fdf105e58 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -120,11 +120,12 @@ namespace Avalonia.Rendering.Composition.Server var oldTransformedContentBounds = TransformedOwnContentBounds; var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds; - + var dirtyOldBounds = false; if (_parent?.IsDirtyComposition == true) { IsDirtyComposition = true; _isDirtyForUpdate = true; + dirtyOldBounds = true; } GlobalTransformMatrix = newTransform; @@ -161,8 +162,7 @@ namespace Avalonia.Rendering.Composition.Server if (wasVisible != IsVisibleInFrame) _isDirtyForUpdate = true; - - var dirtyOldBounds = false; + // Invalidate previous rect and queue new rect based on visibility if (positionChanged) { From 70a86fccbfe362d30fe125090d9a237baf16ec1b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 12 Jul 2022 15:39:44 +0300 Subject: [PATCH 55/61] Fixed initialization order --- src/Avalonia.Native/AvaloniaNativePlatform.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 7b08ed00dc..aedfc74fa2 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -104,7 +104,6 @@ namespace Avalonia.Native _factory.MacOptions.SetShowInDock(macOpts.ShowInDock ? 1 : 0); } - var renderLoop = new RenderLoop(); AvaloniaLocator.CurrentMutable .Bind() .ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface())) @@ -114,7 +113,6 @@ namespace Avalonia.Native .Bind().ToConstant(this) .Bind().ToConstant(this) .Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) - .Bind().ToConstant(renderLoop) .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta, wholeWordTextActionModifiers: KeyModifiers.Alt)) @@ -123,6 +121,9 @@ namespace Avalonia.Native .Bind().ToConstant(applicationPlatform) .Bind().ToConstant(new MacOSNativeMenuCommands(_factory.CreateApplicationCommands())); + var renderLoop = new RenderLoop(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(renderLoop); + var hotkeys = AvaloniaLocator.Current.GetService(); hotkeys.MoveCursorToTheStartOfLine.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers)); hotkeys.MoveCursorToTheStartOfLineWithSelection.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); @@ -145,6 +146,7 @@ namespace Avalonia.Native } } + if (_options.UseDeferredRendering && _options.UseCompositor) { Compositor = new Compositor(renderLoop, _platformGl); From 21ccb8d43aa35ec4aa66a70bfd7660e31e4c7252 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 12 Jul 2022 16:13:56 +0300 Subject: [PATCH 56/61] Fixed animation activation --- .../Composition/Server/ServerObject.cs | 10 +- .../Utilities/SmallDictionary.cs | 101 +++++++++++++++++- 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index c6b468a32f..93ea8e8dee 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -70,14 +70,16 @@ namespace Avalonia.Rendering.Composition.Server Deactivated(); } - protected virtual void Activated() + protected void Activated() { - + foreach(var kp in _animations) + kp.Value.Activate(); } - protected virtual void Deactivated() + protected void Deactivated() { - + foreach(var kp in _animations) + kp.Value.Deactivate(); } void InvalidateSubscriptions(CompositionProperty property) diff --git a/src/Avalonia.Base/Utilities/SmallDictionary.cs b/src/Avalonia.Base/Utilities/SmallDictionary.cs index b8f532c747..7d6a21c136 100644 --- a/src/Avalonia.Base/Utilities/SmallDictionary.cs +++ b/src/Avalonia.Base/Utilities/SmallDictionary.cs @@ -1,16 +1,19 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace Avalonia.Utilities; -public struct InlineDictionary where TKey : class where TValue : class +public struct InlineDictionary : IEnumerable> where TKey : class where TValue : class { object? _data; TValue? _value; void SetCore(TKey key, TValue value, bool overwrite) { + if (key == null) + throw new ArgumentNullException(); if (_data == null) { _data = key; @@ -178,4 +181,98 @@ public struct InlineDictionary where TKey : class where TValue : c return v; throw new KeyNotFoundException(); } -} \ No newline at end of file + + public struct Enumerator : IEnumerator> + { + private Dictionary.Enumerator _inner; + private readonly KeyValuePair[]? _arr; + private KeyValuePair _first; + private int _index; + private Type _type; + enum Type + { + Empty, Single, Array, Dictionary + } + + public Enumerator(InlineDictionary parent) + { + _arr = null; + _first = default; + _index = -1; + _inner = default; + if (parent._data is Dictionary inner) + { + _inner = inner.GetEnumerator(); + _type = Type.Dictionary; + } + else if (parent._data is KeyValuePair[] arr) + { + _type = Type.Array; + _arr = arr; + } + else if (parent._data != null) + { + _type = Type.Single; + _first = new((TKey)parent._data!, parent._value!); + } + else + _type = Type.Empty; + + } + + public bool MoveNext() + { + if (_type == Type.Single) + { + if (_index != -1) + return false; + _index = 0; + } + else if (_type == Type.Array) + { + var next = _index + 1; + if (_arr!.Length - 1 < next || _arr[next].Key == null) + return false; + _index = next; + return true; + } + else if (_type == Type.Dictionary) + return _inner.MoveNext(); + + return false; + } + + public void Reset() + { + _index = -1; + if(_type == Type.Dictionary) + ((IEnumerator)_inner).Reset(); + } + + public KeyValuePair Current + { + get + { + if (_type == Type.Single) + return _first!; + if (_type == Type.Array) + return _arr![_index]!; + if (_type == Type.Dictionary) + return _inner.Current; + throw new InvalidOperationException(); + } + } + + object IEnumerator.Current => Current; + + public void Dispose() + { + } + } + + public Enumerator GetEnumerator() => new Enumerator(this); + + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + +} From ecca63949f3377d566197712a512c42431fca3aa Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 12 Jul 2022 19:43:24 +0300 Subject: [PATCH 57/61] Use Blit when available --- .../Composition/Server/ServerCompositionTarget.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 3351caf0c6..0fde86e484 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -122,9 +122,12 @@ namespace Avalonia.Rendering.Composition.Server targetContext.Clear(Colors.Transparent); targetContext.Transform = Matrix.Identity; - targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, - new Rect(_layerSize), - new Rect(Size), BitmapInterpolationMode.LowQuality); + if (_layer.CanBlit) + _layer.Blit(targetContext); + else + targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, + new Rect(_layerSize), + new Rect(Size), BitmapInterpolationMode.LowQuality); if (DrawDirtyRects) From 551f35a760f08062be6e8d41f05c4d58495a8493 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 13 Jul 2022 07:41:55 +0300 Subject: [PATCH 58/61] Cleanup --- ...cs => ServerCompositionContainerVisual.cs} | 0 .../ServerCompositionLinearGradientBrush.cs | 22 ------------------- ...erverCompositionVisual.DirtyProperties.cs} | 0 ...erVisual.cs => ServerCompositionVisual.cs} | 0 4 files changed, 22 deletions(-) rename src/Avalonia.Base/Rendering/Composition/Server/{ServerContainerVisual.cs => ServerCompositionContainerVisual.cs} (100%) delete mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs rename src/Avalonia.Base/Rendering/Composition/Server/{ServerVisual.DirtyProperties.cs => ServerCompositionVisual.DirtyProperties.cs} (100%) rename src/Avalonia.Base/Rendering/Composition/Server/{ServerVisual.cs => ServerCompositionVisual.cs} (100%) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs similarity index 100% rename from src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs rename to src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs deleted file mode 100644 index c421cdcfb0..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Avalonia.Media; - -namespace Avalonia.Rendering.Composition.Server -{ - internal partial class ServerCompositionLinearGradientBrush - { - /* - protected override void UpdateBackendBrush(ICbBrush brush) - { - var stopColors = new Color[Stops.List.Count]; - var offsets = new float[Stops.List.Count]; - for (var c = 0; c < Stops.List.Count; c++) - { - stopColors[c] = Stops.List[c].Color; - offsets[c] = Stops.List[c].Offset; - } - - ((ICbLinearGradientBrush) brush).Update(StartPoint, EndPoint, stopColors, offsets, ExtendMode); - }*/ - - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs similarity index 100% rename from src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs rename to src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs similarity index 100% rename from src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs rename to src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs From 221a0f134148914ccaff159e60fbc1859fef5924 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 13 Jul 2022 07:44:05 +0300 Subject: [PATCH 59/61] Removed workaround from headless platform memory measurement code --- samples/ControlCatalog.NetCore/Program.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 07d514df6b..cfa016d814 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -60,12 +60,6 @@ namespace ControlCatalog.NetCore }) .AfterSetup(_ => { - static Task LowPriorityDelay(int ms) - { - return Task.Delay(ms).ContinueWith(_ => - Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Background)) - .Unwrap(); - } DispatcherTimer.RunOnce(async () => { var window = ((IClassicDesktopStyleApplicationLifetime)Application.Current.ApplicationLifetime) @@ -77,7 +71,7 @@ namespace ControlCatalog.NetCore continue; Console.WriteLine("Selecting " + page.Header); tc.SelectedItem = page; - await LowPriorityDelay(20); + await Task.Delay(50); } Console.WriteLine("Selecting the first page"); tc.SelectedItem = tc.Items.OfType().First(); @@ -86,7 +80,7 @@ namespace ControlCatalog.NetCore for (var c = 0; c < 3; c++) { GC.Collect(2, GCCollectionMode.Forced); - await Task.Delay(00); + await Task.Delay(50); } void FormatMem(string metric, long bytes) From ebf464ad93173cccb46470c4363b0ea322101c9e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 14 Jul 2022 13:12:16 +0300 Subject: [PATCH 60/61] Enable compositing renderer by default for desktop platforms --- samples/ControlCatalog.NetCore/Program.cs | 6 ++---- src/Avalonia.X11/X11Platform.cs | 4 ++-- src/Windows/Avalonia.Win32/Win32Platform.cs | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index cfa016d814..d98a068d84 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -113,13 +113,11 @@ namespace ControlCatalog.NetCore { EnableMultiTouch = true, UseDBusMenu = true, - EnableIme = true, - UseCompositor = true + EnableIme = true }) .With(new Win32PlatformOptions { - EnableMultitouch = true, - UseCompositor = true + EnableMultitouch = true }) .UseSkia() .AfterSetup(builder => diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index b5fbebca4b..edb320d4f0 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -235,8 +235,8 @@ namespace Avalonia /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; - - public bool UseCompositor { get; set; } + + public bool UseCompositor { get; set; } = true; /// /// Determines whether to use IME. diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index f71d514e38..73ef50052c 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -49,8 +49,8 @@ namespace Avalonia /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; - - public bool UseCompositor { get; set; } + + public bool UseCompositor { get; set; } = true; /// /// Enables ANGLE for Windows. For every Windows version that is above Windows 7, the default is true otherwise it's false. From b9998dd4839b27516d2006c3633ec302c72495c5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Jul 2022 15:56:59 +0200 Subject: [PATCH 61/61] Don't use TransformedBounds for automation. --- .../Automation/Peers/ControlAutomationPeer.cs | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index 28cb3e34b2..a93d3fa7dd 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -146,7 +146,7 @@ namespace Avalonia.Automation.Peers protected override string? GetAccessKeyCore() => AutomationProperties.GetAccessKey(Owner); protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom; protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name; - protected override Rect GetBoundingRectangleCore() => GetBounds(Owner.TransformedBounds); + protected override Rect GetBoundingRectangleCore() => GetBounds(Owner); protected override string GetClassNameCore() => Owner.GetType().Name; protected override bool HasKeyboardFocusCore() => Owner.IsFocused; protected override bool IsContentElementCore() => AutomationProperties.GetAccessibilityView(Owner) >= AccessibilityView.Content; @@ -160,9 +160,19 @@ namespace Avalonia.Automation.Peers return AutomationProperties.GetControlTypeOverride(Owner) ?? GetAutomationControlTypeCore(); } - private static Rect GetBounds(TransformedBounds? bounds) + private static Rect GetBounds(Control control) { - return bounds?.Bounds.TransformToAABB(bounds!.Value.Transform) ?? default; + var root = control.GetVisualRoot(); + + if (root is null) + return default; + + var transform = control.TransformToVisual(root); + + if (!transform.HasValue) + return default; + + return new Rect(control.Bounds.Size).TransformToAABB(transform.Value); } private void Initialize() @@ -182,12 +192,14 @@ namespace Avalonia.Automation.Peers if (parent is Control c) (GetOrCreate(c) as ControlAutomationPeer)?.InvalidateChildren(); } - else if (e.Property == Visual.TransformedBoundsProperty) + else if (e.Property == Visual.BoundsProperty || + e.Property == Visual.RenderTransformProperty || + e.Property == Visual.RenderTransformOriginProperty) { RaisePropertyChangedEvent( AutomationElementIdentifiers.BoundingRectangleProperty, - GetBounds((TransformedBounds?)e.OldValue), - GetBounds((TransformedBounds?)e.NewValue)); + null, + GetBounds(Owner)); } else if (e.Property == Visual.VisualParentProperty) {