Browse Source

Don't involve the animation system in every single property read (#15110)

pull/15153/head
Nikita Tsukanov 2 years ago
committed by GitHub
parent
commit
e79d62a66b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs
  2. 6
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs
  3. 110
      src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs
  4. 19
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
  5. 38
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs
  6. 8
      src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs
  7. 107
      src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs
  8. 175
      src/Avalonia.Base/Rendering/Composition/Server/ServerObjectAnimations.cs
  9. 29
      src/tools/DevGenerators/CompositionGenerator/Generator.cs

6
src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs

@ -61,14 +61,14 @@ internal abstract class AnimationInstanceBase : IAnimationInstance
{
if (_trackedObjects != null)
foreach (var tracked in _trackedObjects)
tracked.obj.SubscribeToInvalidation(tracked.member, this);
tracked.obj.GetOrCreateAnimations().SubscribeToInvalidation(tracked.member, this);
}
public virtual void Deactivate()
{
if (_trackedObjects != null)
foreach (var tracked in _trackedObjects)
tracked.obj.UnsubscribeFromInvalidation(tracked.member, this);
tracked.obj.Animations?.UnsubscribeFromInvalidation(tracked.member, this);
}
public void Invalidate()
@ -76,7 +76,7 @@ internal abstract class AnimationInstanceBase : IAnimationInstance
if (_invalidated)
return;
_invalidated = true;
TargetObject.NotifyAnimatedValueChanged(Property);
TargetObject.Animations?.NotifyAnimationInstanceInvalidated(Property);
}
public void OnTick() => Invalidate();

6
src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs

@ -74,7 +74,7 @@ namespace Avalonia.Rendering.Composition.Animations
&& elapsed > _totalDuration)
{
// Active check?
TargetObject.Compositor.RemoveFromClock(this);
TargetObject.Compositor.Animations.RemoveFromClock(this);
_finished = true;
}
return res;
@ -177,13 +177,13 @@ namespace Avalonia.Rendering.Composition.Animations
{
return;
}
TargetObject.Compositor.AddToClock(this);
TargetObject.Compositor.Animations.AddToClock(this);
base.Activate();
}
public override void Deactivate()
{
TargetObject.Compositor.RemoveFromClock(this);
TargetObject.Compositor.Animations.RemoveFromClock(this);
base.Deactivate();
}
}

110
src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs

@ -1,15 +1,115 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Avalonia.Rendering.Composition.Expressions;
namespace Avalonia.Rendering.Composition.Server;
internal class CompositionProperty
{
private static volatile int s_NextId = 1;
public int Id { get; private set; }
private static int s_nextId = 1;
private static readonly object _lock = new();
public static CompositionProperty Register() => new()
private static Dictionary<Type, List<CompositionProperty>> s_dynamicRegistry = new();
class ReadOnlyRegistry : Dictionary<Type, IReadOnlyDictionary<string, CompositionProperty>>
{
}
private static volatile ReadOnlyRegistry? s_ReadOnlyRegistry;
public CompositionProperty(int id, string name, Type owner, Func<SimpleServerObject, ExpressionVariant>? getVariant)
{
Id = id;
Name = name;
Owner = owner;
GetVariant = getVariant;
}
public int Id { get; }
public string Name { get; }
public Type Owner { get; }
public Func<SimpleServerObject, ExpressionVariant>? GetVariant { get; }
public static CompositionProperty<TField> Register<TOwner, TField>(string name, Func<SimpleServerObject, TField> getField, Action<SimpleServerObject, TField> setField,
Func<SimpleServerObject, ExpressionVariant>? getVariant)
{
CompositionProperty<TField> prop;
lock (_lock)
{
var id = s_nextId++;
prop = new CompositionProperty<TField>(id, name, typeof(TOwner), getField, setField, getVariant);
}
s_ReadOnlyRegistry = null;
return prop;
}
static void PopulatePropertiesForType(Type type, List<CompositionProperty> l)
{
Id = Interlocked.Increment(ref s_NextId)
};
Type? t = type;
while (t != null && t != typeof(object))
{
if (s_dynamicRegistry.TryGetValue(t, out var lst))
l.AddRange(lst);
t = t.BaseType;
}
}
static ReadOnlyRegistry Build()
{
var reg = new ReadOnlyRegistry();
foreach (var type in s_dynamicRegistry.Keys)
{
var lst = new List<CompositionProperty>();
PopulatePropertiesForType(type, lst);
reg[type] = lst.ToDictionary(x => x.Name);
}
return reg;
}
public static IReadOnlyDictionary<string, CompositionProperty>? TryGetPropertiesForType(Type t)
{
GetRegistry().TryGetValue(t, out var rv);
return rv;
}
public static CompositionProperty? Find(Type owner, string name)
{
if (TryGetPropertiesForType(owner)?.TryGetValue(name, out var prop) == true)
return prop;
return null;
}
static ReadOnlyRegistry GetRegistry()
{
var reg = s_ReadOnlyRegistry;
if (reg != null)
return reg;
lock (_lock)
{
// ReSharper disable once NonAtomicCompoundOperator
// This is the only line ever that would set the field to a not-null value, and we are inside of a lock
return s_ReadOnlyRegistry ??= Build();
}
}
}
internal class CompositionProperty<T> : CompositionProperty
{
public Func<SimpleServerObject, T> GetField { get; }
public Action<SimpleServerObject, T> SetField { get; }
public CompositionProperty(int id, string name, Type owner,
Func<SimpleServerObject, T> getField,
Action<SimpleServerObject, T> setField,
Func<SimpleServerObject, ExpressionVariant>? getVariant)
: base(id, name, owner, getVariant)
{
GetField = getField;
SetField = setField;
}
}

19
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs

@ -27,8 +27,6 @@ namespace Avalonia.Rendering.Composition.Server
public Stopwatch Clock { get; } = Stopwatch.StartNew();
public TimeSpan ServerNow { get; private set; }
private readonly List<ServerCompositionTarget> _activeTargets = new();
private readonly HashSet<IServerClockItem> _clockItems = new();
private readonly List<IServerClockItem> _clockItemsToUpdate = new();
internal BatchStreamObjectPool<object?> BatchObjectPool;
internal BatchStreamMemoryPool BatchMemoryPool;
private readonly object _lock = new object();
@ -39,12 +37,14 @@ namespace Avalonia.Rendering.Composition.Server
internal static readonly object RenderThreadJobsStartMarker = new();
internal static readonly object RenderThreadJobsEndMarker = new();
public CompositionOptions Options { get; }
public ServerCompositorAnimations Animations { get; }
public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics,
CompositionOptions options,
BatchStreamObjectPool<object?> batchObjectPool, BatchStreamMemoryPool batchMemoryPool)
{
Options = options;
Animations = new();
_renderLoop = renderLoop;
RenderInterface = new PlatformRenderInterfaceContextManager(platformGraphics);
RenderInterface.ContextDisposed += RT_OnContextDisposed;
@ -210,14 +210,9 @@ namespace Avalonia.Rendering.Composition.Server
UpdateServerTime();
ApplyPendingBatches();
NotifyBatchesProcessed();
foreach(var animation in _clockItems)
_clockItemsToUpdate.Add(animation);
foreach (var animation in _clockItemsToUpdate)
animation.OnTick();
_clockItemsToUpdate.Clear();
Animations.Process();
ApplyEnqueuedRenderResourceChanges();
@ -244,12 +239,6 @@ namespace Avalonia.Rendering.Composition.Server
_activeTargets.Remove(target);
}
public void AddToClock(IServerClockItem item) =>
_clockItems.Add(item);
public void RemoveFromClock(IServerClockItem item) =>
_clockItems.Remove(item);
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
{
using (RenderInterface.EnsureCurrent())

38
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs

@ -0,0 +1,38 @@
using System.Collections.Generic;
namespace Avalonia.Rendering.Composition.Server;
internal class ServerCompositorAnimations
{
private readonly HashSet<IServerClockItem> _clockItems = new();
private readonly List<IServerClockItem> _clockItemsToUpdate = new();
private readonly HashSet<ServerObjectAnimations> _dirtyAnimatedObjects = new();
private readonly Queue<ServerObjectAnimations> _dirtyAnimatedObjectQueue = new();
public void AddToClock(IServerClockItem item) =>
_clockItems.Add(item);
public void RemoveFromClock(IServerClockItem item) =>
_clockItems.Remove(item);
public void Process()
{
foreach (var animation in _clockItems)
_clockItemsToUpdate.Add(animation);
foreach (var animation in _clockItemsToUpdate)
animation.OnTick();
_clockItemsToUpdate.Clear();
while (_dirtyAnimatedObjectQueue.Count > 0)
_dirtyAnimatedObjectQueue.Dequeue().EvaluateAnimations();
_dirtyAnimatedObjects.Clear();
}
public void AddDirtyAnimatedObject(ServerObjectAnimations obj)
{
if (_dirtyAnimatedObjects.Add(obj))
_dirtyAnimatedObjectQueue.Enqueue(obj);
}
}

8
src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs

@ -38,7 +38,7 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer
_wantsNextAnimationFrameAfterTick = false;
_handler.OnAnimationFrameUpdate();
if (!_wantsNextAnimationFrameAfterTick)
Compositor.RemoveFromClock(this);
Compositor.Animations.RemoveFromClock(this);
}
public override Rect OwnContentBounds => _handler.GetRenderBounds();
@ -46,13 +46,13 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer
protected override void OnAttachedToRoot(ServerCompositionTarget target)
{
if (_wantsNextAnimationFrameAfterTick)
Compositor.AddToClock(this);
Compositor.Animations.AddToClock(this);
base.OnAttachedToRoot(target);
}
protected override void OnDetachedFromRoot(ServerCompositionTarget target)
{
Compositor.RemoveFromClock(this);
Compositor.Animations.RemoveFromClock(this);
base.OnDetachedFromRoot(target);
}
@ -67,7 +67,7 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer
{
_wantsNextAnimationFrameAfterTick = true;
if (Root != null)
Compositor.AddToClock(this);
Compositor.Animations.AddToClock(this);
}
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,

107
src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs

@ -16,37 +16,15 @@ namespace Avalonia.Rendering.Composition.Server
internal abstract class ServerObject : SimpleServerObject, IExpressionObject
{
private uint _activationCount;
private ServerObjectAnimations? _animations;
public ServerObjectAnimations? Animations => _animations;
public ServerObjectAnimations GetOrCreateAnimations() => _animations ??= new(this);
public bool IsActive => _activationCount != 0;
private InlineDictionary<CompositionProperty, ServerObjectSubscriptionStore> _subscriptions;
private InlineDictionary<CompositionProperty, IAnimationInstance> _animations;
private class ServerObjectSubscriptionStore
{
public bool IsValid;
public RefTrackingDictionary<IAnimationInstance>? Subscribers;
public void Invalidate()
{
if (!IsValid)
return;
IsValid = false;
if (Subscribers != null)
foreach (var sub in Subscribers)
sub.Key.Invalidate();
}
}
public ServerObject(ServerCompositor compositor) : base(compositor)
{
}
public virtual ExpressionVariant GetPropertyForAnimation(string name)
{
return default;
}
ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name);
public void Activate()
{
_activationCount++;
@ -65,92 +43,37 @@ namespace Avalonia.Rendering.Composition.Server
Deactivated();
}
protected void Activated()
{
foreach(var kp in _animations)
kp.Value.Activate();
}
protected void Deactivated()
{
foreach(var kp in _animations)
kp.Value.Deactivate();
}
private void Activated() => _animations?.Activated();
void InvalidateSubscriptions(CompositionProperty property)
{
if(_subscriptions.TryGetValue(property, out var subs))
subs.Invalidate();
}
private void Deactivated() => _animations?.Deactivated();
protected new void SetValue<T>(CompositionProperty prop, ref T field, T value)
{
field = value;
InvalidateSubscriptions(prop);
}
protected new T GetValue<T>(CompositionProperty prop, ref T field)
{
if (_subscriptions.TryGetValue(prop, out var subs))
subs.IsValid = true;
return field;
_animations?.OnSetDirectValue(prop);
}
protected void SetAnimatedValue<T>(CompositionProperty prop, ref T field,
protected void SetAnimatedValue<T>(CompositionProperty<T> prop, ref T field,
TimeSpan committedAt, IAnimationInstance animation) where T : struct
{
if (IsActive && _animations.TryGetValue(prop, out var oldAnimation))
oldAnimation.Deactivate();
_animations[prop] = animation;
animation.Initialize(committedAt, ExpressionVariant.Create(field), prop);
if(IsActive)
animation.Activate();
InvalidateSubscriptions(prop);
GetOrCreateAnimations().OnSetAnimatedValue(prop, ref field, committedAt, animation);
}
protected void SetAnimatedValue<T>(CompositionProperty property, out T field, T value)
{
if (_animations.TryGetAndRemoveValue(property, out var animation) && IsActive)
animation.Deactivate();
field = value;
InvalidateSubscriptions(property);
_animations?.RemoveAnimationForProperty(property);
}
protected T GetAnimatedValue<T>(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<T>();
return field;
}
public virtual void NotifyAnimatedValueChanged(CompositionProperty prop) => ValuesInvalidated();
public virtual void NotifyAnimatedValueChanged(CompositionProperty prop)
{
InvalidateSubscriptions(prop);
ValuesInvalidated();
}
public void SubscribeToInvalidation(CompositionProperty member, IAnimationInstance animation)
public virtual CompositionProperty? GetCompositionProperty(string fieldName) => null;
ExpressionVariant IExpressionObject.GetProperty(string name)
{
if (!_subscriptions.TryGetValue(member, out var store))
_subscriptions[member] = store = new ServerObjectSubscriptionStore();
if (store.Subscribers == null)
store.Subscribers = new();
store.Subscribers.AddRef(animation);
}
if (_animations == null)
return CompositionProperty.Find(this.GetType(), name)?.GetVariant?.Invoke(this) ?? default;
public void UnsubscribeFromInvalidation(CompositionProperty member, IAnimationInstance animation)
{
if(_subscriptions.TryGetValue(member, out var store))
store.Subscribers?.ReleaseRef(animation);
return _animations.GetPropertyForAnimation(name);
}
public virtual CompositionProperty? GetCompositionProperty(string fieldName) => null;
}
}

175
src/Avalonia.Base/Rendering/Composition/Server/ServerObjectAnimations.cs

@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Expressions;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Server;
class ServerObjectAnimations
{
private readonly ServerObject _owner;
private InlineDictionary<CompositionProperty, ServerObjectSubscriptionStore> _subscriptions;
private InlineDictionary<CompositionProperty, ServerObjectAnimationInstance> _animations;
private readonly IReadOnlyDictionary<string, CompositionProperty> _properties;
public ServerObjectAnimations(ServerObject owner)
{
_owner = owner;
_properties = CompositionProperty.TryGetPropertiesForType(owner.GetType()) ??
new Dictionary<string, CompositionProperty>();
}
private class ServerObjectSubscriptionStore
{
public bool IsValid;
public RefTrackingDictionary<IAnimationInstance>? Subscribers;
public void Invalidate()
{
if (!IsValid)
return;
IsValid = false;
if (Subscribers != null)
foreach (var sub in Subscribers)
sub.Key.Invalidate();
}
}
abstract class ServerObjectAnimationInstance
{
public ServerObjectAnimations Owner { get; }
private ExpressionVariant _cachedVariant;
public bool IsDirty { get; set; } = true;
public bool NeedsUpdate { get; set; } = true;
public IAnimationInstance Animation { get; }
public ServerObjectAnimationInstance(ServerObjectAnimations owner, IAnimationInstance animation)
{
Animation = animation;
Owner = owner;
}
public ExpressionVariant GetVariant()
{
var compositor = Owner._owner.Compositor;
if (!IsDirty)
return _cachedVariant;
// We are setting this _before_ evaluating animation to prevent stack overflows due to potential
// cyclic references
IsDirty = false;
return _cachedVariant = Animation.Evaluate(Owner._owner.Compositor.ServerNow, _cachedVariant);
}
public abstract void UpdateTargetProperty();
}
class ServerObjectAnimationInstance<T> : ServerObjectAnimationInstance where T : struct
{
private readonly CompositionProperty<T> _property;
public ServerObjectAnimationInstance(ServerObjectAnimations owner, IAnimationInstance animation,
CompositionProperty<T> property) : base(owner, animation)
{
_property = property;
}
public override void UpdateTargetProperty()
{
if (NeedsUpdate)
{
NeedsUpdate = false;
_property.SetField(Owner._owner, GetVariant().CastOrDefault<T>());
Owner._owner.NotifyAnimatedValueChanged(_property);
}
}
}
public void Activated()
{
foreach(var kp in _animations)
kp.Value.Animation.Activate();
}
public void Deactivated()
{
foreach(var kp in _animations)
kp.Value.Animation.Deactivate();
}
public void OnSetDirectValue(CompositionProperty property)
{
if(_subscriptions.TryGetValue(property, out var subs))
subs.Invalidate();
}
public void OnSetAnimatedValue<T>(CompositionProperty<T> prop, ref T field, TimeSpan committedAt, IAnimationInstance animation) where T : struct
{
if (_owner.IsActive && _animations.TryGetValue(prop, out var oldAnimation))
oldAnimation.Animation.Deactivate();
_animations[prop] = new ServerObjectAnimationInstance<T>(this, animation, prop);
animation.Initialize(committedAt, ExpressionVariant.Create(field), prop);
if(_owner.IsActive)
animation.Activate();
OnSetDirectValue(prop);
}
public void RemoveAnimationForProperty(CompositionProperty property)
{
if (_animations.TryGetAndRemoveValue(property, out var animation) && _owner.IsActive)
animation.Animation.Deactivate();
OnSetDirectValue(property);
}
public void SubscribeToInvalidation(CompositionProperty member, IAnimationInstance animation)
{
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(CompositionProperty member, IAnimationInstance animation)
{
if(_subscriptions.TryGetValue(member, out var store))
store.Subscribers?.ReleaseRef(animation);
}
public ExpressionVariant GetPropertyForAnimation(string name)
{
if (!_properties.TryGetValue(name, out var prop))
return default;
if (_subscriptions.TryGetValue(prop, out var subs))
subs.IsValid = true;
if (_animations.TryGetValue(prop, out var animation))
return animation.GetVariant();
return prop.GetVariant?.Invoke(_owner) ?? default;
}
public void EvaluateAnimations()
{
foreach (var animation in _animations)
if (animation.Value.IsDirty)
animation.Value.UpdateTargetProperty();
}
public void NotifyAnimationInstanceInvalidated(CompositionProperty property)
{
if (_animations.TryGetValue(property, out var instance))
{
instance.IsDirty = instance.NeedsUpdate = true;
_owner.Compositor.Animations.AddDirtyAnimatedObject(this);
}
else
Debug.Assert(false);
}
}

29
src/tools/DevGenerators/CompositionGenerator/Generator.cs

@ -166,15 +166,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
.AddModifiers(SyntaxKind.PublicKeyword)
.AddAccessorListAccessors(
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithExpressionBody(
ArrowExpressionClause(
InvocationExpression(IdentifierName("GetAnimatedValue"),
ArgumentList(SeparatedList(new[]
{
Argument(IdentifierName(CompositionPropertyField(prop))),
Argument(null, Token(SyntaxKind.RefKeyword),
IdentifierName(fieldName))
}
))))).WithSemicolonToken(Semicolon()),
ArrowExpressionClause(IdentifierName(fieldName))).WithSemicolonToken(Semicolon()),
AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
.WithExpressionBody(ArrowExpressionClause(
ParseExpression($"SetAnimatedValue({CompositionPropertyField(prop)}, out {PropertyBackingFieldName(prop)}, value)")))
@ -187,13 +179,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
.AddModifiers(SyntaxKind.PublicKeyword)
.AddAccessorListAccessors(
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration,
Block(ReturnStatement(
InvocationExpression(IdentifierName("GetValue"),
ArgumentList(SeparatedList(new[]{
Argument(IdentifierName(CompositionPropertyField(prop))),
Argument(null, Token(SyntaxKind.RefKeyword), IdentifierName(fieldName))
}
)))))),
Block(ReturnStatement(IdentifierName(fieldName)))),
AccessorDeclaration(SyntaxKind.SetAccessorDeclaration,
Block(
ParseStatement("var changed = false;"),
@ -235,8 +221,13 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
serverGetPropertyBody = ApplyGetProperty(serverGetPropertyBody, prop);
serverGetCompositionPropertyBody = ApplyGetProperty(serverGetCompositionPropertyBody, prop, CompositionPropertyField(prop));
server = server.AddMembers(DeclareField("CompositionProperty", CompositionPropertyField(prop),
EqualsValueClause(ParseExpression("CompositionProperty.Register()")),
string compositionPropertyVariantGetter = "null";
if(VariantPropertyTypes.Contains(prop.Type))
compositionPropertyVariantGetter = $"obj => (({serverName})obj).{fieldName}";
server = server.AddMembers(DeclareField($"CompositionProperty<{serverPropertyType}>", CompositionPropertyField(prop),
EqualsValueClause(ParseExpression(
$"CompositionProperty.Register<{serverName}, {serverPropertyType}>(\"{prop.Name}\", obj => (({serverName})obj).{fieldName}, (obj, v) => (({serverName})obj).{fieldName} = v, {compositionPropertyVariantGetter})")),
SyntaxKind.InternalKeyword, SyntaxKind.StaticKeyword));
if (prop.DefaultValue != null)
@ -282,7 +273,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
if (!cl.ServerOnly)
{
server = WithGetPropertyForAnimation(server, serverGetPropertyBody);
//server = WithGetPropertyForAnimation(server, serverGetPropertyBody);
server = WithGetCompositionProperty(server, serverGetCompositionPropertyBody);
}

Loading…
Cancel
Save