Browse Source

Use dictionaries for storing composition animation infrastructure

pull/8105/head
Nikita Tsukanov 4 years ago
parent
commit
862b318906
  1. 124
      src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs
  2. 16
      src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs
  3. 4
      src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs
  4. 2
      src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs
  5. 4
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs
  6. 3
      src/Avalonia.Base/Rendering/Composition/CompositionObject.cs
  7. 15
      src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs
  8. 101
      src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs
  9. 30
      src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs
  10. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs
  11. 181
      src/Avalonia.Base/Utilities/SmallDictionary.cs
  12. 154
      src/tools/DevGenerators/CompositionGenerator/Generator.cs

124
src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs

@ -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
{
/// <summary>
/// This is the first element of both animated and non-animated value stores.
/// It's used to propagate property invalidation to subscribers
/// </summary>
internal struct 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();
}
}
/// <summary>
/// The value store for non-animated values that can still be referenced by animations.
/// Simply stores the value and notifies subscribers
/// </summary>
internal struct ServerValueStore<T>
{
public ServerObjectSubscriptionStore Subscriptions;
private T _value;
public T Value
{
set
{
_value = value;
Subscriptions.Invalidate();
}
get
{
Subscriptions.IsValid = true;
return _value;
}
}
}
/// <summary>
/// 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
/// </summary>
/// <typeparam name="T"></typeparam>
[StructLayout(LayoutKind.Sequential)]
internal struct ServerAnimatedValueStore<T> 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<T>();
_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();
}
}
}

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

@ -12,10 +12,10 @@ namespace Avalonia.Rendering.Composition.Animations;
/// </summary> /// </summary>
internal abstract class AnimationInstanceBase : IAnimationInstance internal abstract class AnimationInstanceBase : IAnimationInstance
{ {
private List<(ServerObject obj, int member)>? _trackedObjects; private List<(ServerObject obj, CompositionProperty member)>? _trackedObjects;
protected PropertySetSnapshot Parameters { get; } protected PropertySetSnapshot Parameters { get; }
public ServerObject TargetObject { get; } public ServerObject TargetObject { get; }
protected int StoreOffset { get; private set; } protected CompositionProperty Property { get; private set; } = null!;
private bool _invalidated; private bool _invalidated;
public AnimationInstanceBase(ServerObject target, PropertySetSnapshot parameters) public AnimationInstanceBase(ServerObject target, PropertySetSnapshot parameters)
@ -24,7 +24,7 @@ internal abstract class AnimationInstanceBase : IAnimationInstance
TargetObject = target; 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) if (trackedObjects.Count > 0)
{ {
@ -34,22 +34,22 @@ internal abstract class AnimationInstanceBase : IAnimationInstance
var obj = Parameters.GetObjectParameter(t.name); var obj = Parameters.GetObjectParameter(t.name);
if (obj is ServerObject tracked) if (obj is ServerObject tracked)
{ {
var off = tracked.GetFieldOffset(t.member); var off = tracked.GetCompositionProperty(t.member);
if (off == null) if (off == null)
#if DEBUG #if DEBUG
throw new InvalidCastException("Attempting to subscribe to unknown field"); throw new InvalidCastException("Attempting to subscribe to unknown field");
#else #else
continue; continue;
#endif #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); protected abstract ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue);
public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue)
@ -77,6 +77,6 @@ internal abstract class AnimationInstanceBase : IAnimationInstance
if (_invalidated) if (_invalidated)
return; return;
_invalidated = true; _invalidated = true;
TargetObject.NotifyAnimatedValueChanged(StoreOffset); TargetObject.NotifyAnimatedValueChanged(Property);
} }
} }

4
src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs

@ -29,12 +29,12 @@ namespace Avalonia.Rendering.Composition.Animations
return _expression.Evaluate(ref ctx); 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; _startingValue = startingValue;
var hs = new HashSet<(string, string)>(); var hs = new HashSet<(string, string)>();
_expression.CollectReferences(hs); _expression.CollectReferences(hs);
base.Initialize(storeOffset, hs); base.Initialize(property, hs);
} }
public ExpressionAnimationInstance(Expression expression, public ExpressionAnimationInstance(Expression expression,

2
src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs

@ -8,7 +8,7 @@ namespace Avalonia.Rendering.Composition.Animations
{ {
ServerObject TargetObject { get; } ServerObject TargetObject { get; }
ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue); 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 Activate();
void Deactivate(); void Deactivate();
void Invalidate(); void Invalidate();

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

@ -151,7 +151,7 @@ namespace Avalonia.Rendering.Composition.Animations
return f.Value; 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; _startedAt = startedAt;
_startingValue = startingValue.CastOrDefault<T>(); _startingValue = startingValue.CastOrDefault<T>();
@ -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 // TODO: Update subscriptions based on the current keyframe rather than keeping subscriptions to all of them
foreach (var frame in _keyFrames) foreach (var frame in _keyFrames)
frame.Expression?.CollectReferences(hs); frame.Expression?.CollectReferences(hs);
Initialize(storeOffset, hs); Initialize(property, hs);
} }
public override void Activate() public override void Activate()

3
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.Expressions;
using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport; using Avalonia.Rendering.Composition.Transport;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition namespace Avalonia.Rendering.Composition
{ {
@ -17,6 +18,8 @@ namespace Avalonia.Rendering.Composition
/// The collection of implicit animations attached to this object. /// The collection of implicit animations attached to this object.
/// </summary> /// </summary>
public ImplicitAnimationCollection? ImplicitAnimations { get; set; } public ImplicitAnimationCollection? ImplicitAnimations { get; set; }
private protected InlineDictionary<CompositionProperty, IAnimationInstance> PendingAnimations;
internal CompositionObject(Compositor compositor, ServerObject server) internal CompositionObject(Compositor compositor, ServerObject server)
{ {
Compositor = compositor; Compositor = compositor;

15
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)
};
}

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

@ -21,7 +21,25 @@ namespace Avalonia.Rendering.Composition.Server
public long ItselfLastChangedBy { get; private set; } public long ItselfLastChangedBy { get; private set; }
private uint _activationCount; private uint _activationCount;
public bool IsActive => _activationCount != 0; 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) public ServerObject(ServerCompositor compositor)
{ {
Compositor = compositor; Compositor = compositor;
@ -62,46 +80,62 @@ namespace Avalonia.Rendering.Composition.Server
} }
[StructLayout(LayoutKind.Sequential)] void InvalidateSubscriptions(CompositionProperty property)
protected class OffsetDummy
{ {
#pragma warning disable CS0649 if(_subscriptions.TryGetValue(property, out var subs))
public FillerStruct Filler; subs.Invalidate();
#pragma warning restore CS0649
} }
[StructLayout(LayoutKind.Sequential)] protected void SetValue<T>(CompositionProperty prop, out T field, T value)
protected unsafe struct FillerStruct
{ {
public fixed byte FillerData[8192]; field = value;
InvalidateSubscriptions(prop);
} }
private static readonly object s_OffsetDummy = new OffsetDummy(); protected T GetValue<T>(CompositionProperty prop, ref T field)
protected static T GetOffsetDummy<T>() where T : ServerObject => Unsafe.As<T>(s_OffsetDummy); {
if (_subscriptions.TryGetValue(prop, out var subs))
subs.IsValid = true;
return field;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] protected void SetAnimatedValue<T>(CompositionProperty prop, ref T field,
protected static int GetOffset(ServerObject obj, ref ServerObjectSubscriptionStore field) TimeSpan commitedAt, IAnimationInstance animation) where T : struct
{ {
return Unsafe.ByteOffset(ref obj._activationCount, if (IsActive && _animations.TryGetValue(prop, out var oldAnimation))
ref Unsafe.As<ServerObjectSubscriptionStore, uint>(ref field)) oldAnimation.Deactivate();
.ToInt32(); _animations[prop] = animation;
animation.Initialize(commitedAt, ExpressionVariant.Create(field), prop);
if(IsActive)
animation.Activate();
InvalidateSubscriptions(prop);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] protected void SetAnimatedValue<T>(CompositionProperty property, out T field, T value)
protected ref ServerObjectSubscriptionStore GetStoreFromOffset(int offset)
{ {
#if DEBUG if (_animations.TryGetAndRemoveValue(property, out var animation) && IsActive)
if (offset == 0) animation.Deactivate();
throw new InvalidOperationException(); field = value;
#endif InvalidateSubscriptions(property);
return ref Unsafe.As<uint, ServerObjectSubscriptionStore>(ref Unsafe.AddByteOffset(ref _activationCount,
new IntPtr(offset)));
} }
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>();
public virtual void NotifyAnimatedValueChanged(int offset) return field;
}
public virtual void NotifyAnimatedValueChanged(CompositionProperty prop)
{ {
ref var store = ref GetStoreFromOffset(offset); InvalidateSubscriptions(prop);
store.Invalidate();
ValuesInvalidated(); 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) if (store.Subscribers == null)
store.Subscribers = new(); store.Subscribers = new();
store.Subscribers.AddRef(animation); store.Subscribers.AddRef(animation);
} }
public void UnsubscribeFromInvalidation(int member, IAnimationInstance animation) public void UnsubscribeFromInvalidation(CompositionProperty member, IAnimationInstance animation)
{ {
ref var store = ref GetStoreFromOffset(member); if(_subscriptions.TryGetValue(member, out var store))
store.Subscribers?.ReleaseRef(animation); 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) protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt)
{ {

30
src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs

@ -50,27 +50,27 @@ partial class ServerCompositionVisual
_clipSizeDirty = true; _clipSizeDirty = true;
} }
public override void NotifyAnimatedValueChanged(int offset) public override void NotifyAnimatedValueChanged(CompositionProperty offset)
{ {
base.NotifyAnimatedValueChanged(offset); base.NotifyAnimatedValueChanged(offset);
if (offset == s_OffsetOf_clipToBounds if (offset == s_IdOfClipToBoundsProperty
|| offset == s_OffsetOf_opacity || offset == s_IdOfOpacityProperty
|| offset == s_OffsetOf_size) || offset == s_IdOfSizeProperty)
IsDirtyComposition = true; IsDirtyComposition = true;
if (offset == s_OffsetOf_size if (offset == s_IdOfSizeProperty
|| offset == s_OffsetOf_anchorPoint || offset == s_IdOfAnchorPointProperty
|| offset == s_OffsetOf_centerPoint || offset == s_IdOfCenterPointProperty
|| offset == s_OffsetOf_adornedVisual || offset == s_IdOfAdornedVisualProperty
|| offset == s_OffsetOf_transformMatrix || offset == s_IdOfTransformMatrixProperty
|| offset == s_OffsetOf_scale || offset == s_IdOfScaleProperty
|| offset == s_OffsetOf_rotationAngle || offset == s_IdOfRotationAngleProperty
|| offset == s_OffsetOf_orientation || offset == s_IdOfOrientationProperty
|| offset == s_OffsetOf_offset) || offset == s_IdOfOffsetProperty)
_combinedTransformDirty = true; _combinedTransformDirty = true;
if (offset == s_OffsetOf_clipToBounds if (offset == s_IdOfClipToBoundsProperty
|| offset == s_OffsetOf_size) || offset == s_IdOfSizeProperty)
_clipSizeDirty = true; _clipSizeDirty = true;
} }
} }

2
src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs

@ -121,7 +121,7 @@ namespace Avalonia.Rendering.Composition.Server
var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds; var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds;
if (_parent.Value?.IsDirtyComposition == true) if (_parent?.IsDirtyComposition == true)
{ {
IsDirtyComposition = true; IsDirtyComposition = true;
_isDirtyForUpdate = true; _isDirtyForUpdate = true;

181
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<TKey, TValue> 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<TKey?, TValue?>[] 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<TKey?, TValue?>(key, value);
return;
}
// Upgrade to dictionary
var newDic = new Dictionary<TKey, TValue?>();
foreach (var kvp in arr)
newDic.Add(kvp.Key!, kvp.Value!);
newDic.Add(key, value);
_data = newDic;
}
else if (_data is Dictionary<TKey, TValue?> dic)
{
if (overwrite)
dic[key] = value;
else
dic.Add(key, value);
}
else
{
// We have a single element, upgrade to array
arr = new KeyValuePair<TKey?, TValue?>[6];
arr[0] = new KeyValuePair<TKey?, TValue?>((TKey)_data, _value);
arr[1] = new KeyValuePair<TKey?, TValue?>(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<TKey?, TValue?>[] 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<TKey, TValue?> 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<TKey?, TValue?>[] 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<TKey, TValue?> 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<TKey?, TValue?>[] 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<TKey, TValue?> 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();
}
}

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

@ -42,8 +42,10 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
string ChangedFieldsTypeName(GClass c) => c.Name + "ChangedFields"; string ChangedFieldsTypeName(GClass c) => c.Name + "ChangedFields";
string ChangedFieldsFieldName(GClass c) => "_changedFieldsOf" + c.Name; string ChangedFieldsFieldName(GClass c) => "_changedFieldsOf" + c.Name;
string PropertyBackingFieldName(GProperty prop) => "_" + prop.Name.WithLowerFirst(); string PropertyBackingFieldName(GProperty prop) => "_" + prop.Name.WithLowerFirst();
string ServerPropertyOffsetFieldName(GProperty prop) => "s_OffsetOf" + PropertyBackingFieldName(prop); string CompositionPropertyField(GProperty prop) => "s_IdOf" + prop.Name + "Property";
string PropertyPendingAnimationFieldName(GProperty prop) => "_pendingAnimationFor" + prop.Name;
ExpressionSyntax ClientProperty(GClass c, GProperty p) =>
MemberAccess(ServerName(c.Name), CompositionPropertyField(p));
void GenerateClass(GClass cl) void GenerateClass(GClass cl)
{ {
@ -140,30 +142,10 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
.AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName("BatchStreamReader"))) .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName("BatchStreamReader")))
.AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); .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 resetBody = Block();
var startAnimationBody = Block(); var startAnimationBody = Block();
var serverGetPropertyBody = Block(); var serverGetPropertyBody = Block();
var serverGetFieldOffsetBody = Block(); var serverGetCompositionPropertyBody = Block();
var activatedBody = Block(ParseStatement("base.Activated();"));
var deactivatedBody = Block(ParseStatement("base.Deactivated();"));
var serializeMethodBody = SerializeChangesPrologue(cl); var serializeMethodBody = SerializeChangesPrologue(cl);
var deserializeMethodBody = DeserializeChangesPrologue(cl); var deserializeMethodBody = DeserializeChangesPrologue(cl);
@ -172,16 +154,12 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
foreach (var prop in cl.Properties) foreach (var prop in cl.Properties)
{ {
var fieldName = PropertyBackingFieldName(prop); var fieldName = PropertyBackingFieldName(prop);
var animatedFieldName = PropertyPendingAnimationFieldName(prop);
var fieldOffsetName = ServerPropertyOffsetFieldName(prop);
var propType = ParseTypeName(prop.Type); var propType = ParseTypeName(prop.Type);
var filteredPropertyType = prop.Type.TrimEnd('?'); var filteredPropertyType = prop.Type.TrimEnd('?');
var isObject = _objects.Contains(filteredPropertyType); var isObject = _objects.Contains(filteredPropertyType);
var isNullable = prop.Type.EndsWith("?"); var isNullable = prop.Type.EndsWith("?");
bool isPassthrough = false; bool isPassthrough = false;
if (prop.Animated)
client = client.AddMembers(DeclareField("IAnimationInstance?", animatedFieldName));
client = GenerateClientProperty(client, cl, prop, propType, isObject, isNullable); client = GenerateClientProperty(client, cl, prop, propType, isObject, isNullable);
var animatedServer = prop.Animated; var animatedServer = prop.Animated;
@ -201,35 +179,50 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
if (animatedServer) if (animatedServer)
server = server.AddMembers( server = server.AddMembers(
DeclareField("ServerAnimatedValueStore<" + serverPropertyType + ">", fieldName), DeclareField(serverPropertyType, fieldName),
PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name)
.AddModifiers(SyntaxKind.PublicKeyword) .AddModifiers(SyntaxKind.PublicKeyword)
.WithExpressionBody(ArrowExpressionClause( .WithExpressionBody(ArrowExpressionClause(
InvocationExpression(MemberAccess(fieldName, "GetAnimated"), InvocationExpression(IdentifierName("GetAnimatedValue"),
ArgumentList(SingletonSeparatedList(Argument(IdentifierName("Compositor"))))))) ArgumentList(SeparatedList(new[]{
Argument(IdentifierName(CompositionPropertyField(prop))),
Argument(null, Token(SyntaxKind.RefKeyword), IdentifierName(fieldName))
}
)))))
.WithSemicolonToken(Semicolon()) .WithSemicolonToken(Semicolon())
); );
else else
{ {
server = server server = server
.AddMembers(DeclareField("ServerValueStore<" + serverPropertyType + ">", fieldName)) .AddMembers(DeclareField(serverPropertyType, fieldName))
.AddMembers(PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) .AddMembers(PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name)
.AddModifiers(SyntaxKind.PublicKeyword) .AddModifiers(SyntaxKind.PublicKeyword)
.AddAccessorListAccessors( .AddAccessorListAccessors(
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, 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, AccessorDeclaration(SyntaxKind.SetAccessorDeclaration,
Block( Block(
ParseStatement("var changed = false;"), ParseStatement("var changed = false;"),
IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression,
MemberAccess(IdentifierName(fieldName), "Value"), IdentifierName(fieldName),
IdentifierName("value")), IdentifierName("value")),
Block( Block(
ParseStatement("On" + prop.Name + "Changing();"), ParseStatement("On" + prop.Name + "Changing();"),
ParseStatement($"changed = true;")) ParseStatement($"changed = true;"))
), ),
ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, ExpressionStatement(InvocationExpression(IdentifierName("SetValue"),
MemberAccess(IdentifierName(fieldName), "Value"), IdentifierName("value"))), ArgumentList(SeparatedList(new[]{
Argument(IdentifierName(CompositionPropertyField(prop))),
Argument(null, Token(SyntaxKind.OutKeyword), IdentifierName(fieldName)),
Argument(IdentifierName("value"))
}
)))),
ParseStatement($"if(changed) On" + prop.Name + "Changed();") ParseStatement($"if(changed) On" + prop.Name + "Changed();")
)) ))
)) ))
@ -238,36 +231,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
.AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing")
.AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); .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( resetBody = resetBody.AddStatements(
ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset"))));
@ -277,24 +241,15 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
if (animatedServer) if (animatedServer)
{ {
startAnimationBody = ApplyStartAnimation(startAnimationBody, cl, prop); startAnimationBody = ApplyStartAnimation(startAnimationBody, cl, prop);
activatedBody = activatedBody.AddStatements(ParseStatement($"{fieldName}.Activate(this);"));
deactivatedBody = deactivatedBody.AddStatements(ParseStatement($"{fieldName}.Deactivate(this);"));
} }
serverGetPropertyBody = ApplyGetProperty(serverGetPropertyBody, prop); serverGetPropertyBody = ApplyGetProperty(serverGetPropertyBody, prop);
serverGetFieldOffsetBody = ApplyGetProperty(serverGetFieldOffsetBody, prop, fieldOffsetName); serverGetCompositionPropertyBody = ApplyGetProperty(serverGetCompositionPropertyBody, prop, CompositionPropertyField(prop));
server = server.AddMembers(DeclareField("int", fieldOffsetName, SyntaxKind.StaticKeyword)); server = server.AddMembers(DeclareField("CompositionProperty", CompositionPropertyField(prop),
serverStaticCtorBody = serverStaticCtorBody.AddStatements(ExpressionStatement( EqualsValueClause(ParseExpression("CompositionProperty.Register()")),
AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, IdentifierName(fieldOffsetName), SyntaxKind.InternalKeyword, SyntaxKind.StaticKeyword));
InvocationExpression(IdentifierName("GetOffset"),
ArgumentList(SeparatedList(new[]
{
Argument(IdentifierName(uninitializedObjectName)),
Argument(RefExpression(MemberAccess(
MemberAccess(IdentifierName(uninitializedObjectName), fieldName), "Subscriptions")))
}))))));
if (prop.DefaultValue != null) if (prop.DefaultValue != null)
{ {
@ -304,16 +259,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
IdentifierName(prop.Name), ParseExpression(prop.DefaultValue)))); 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) if (cl.Properties.Count > 0)
{ {
server = server.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( server = server.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration(
@ -331,12 +277,15 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
.AddMembers( .AddMembers(
MethodDeclaration(ParseTypeName("void"), "InitializeDefaultsExtra") MethodDeclaration(ParseTypeName("void"), "InitializeDefaultsExtra")
.AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon()));
if (cl.Properties.Count > 0) if (cl.Properties.Count > 0)
{
serializeMethodBody = serializeMethodBody.AddStatements(SerializeChangesEpilogue(cl));
client = client.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( client = client.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration(
$"private protected override void SerializeChangesCore(BatchStreamWriter writer){{}}")!) $"private protected override void SerializeChangesCore(BatchStreamWriter writer){{}}")!)
.WithBody(serializeMethodBody)); .WithBody(serializeMethodBody));
}
if (list != null) if (list != null)
client = AppendListProxy(list, client); client = AppendListProxy(list, client);
@ -344,7 +293,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
client = WithStartAnimation(client, startAnimationBody); client = WithStartAnimation(client, startAnimationBody);
server = WithGetPropertyForAnimation(server, serverGetPropertyBody); server = WithGetPropertyForAnimation(server, serverGetPropertyBody);
server = WithGetFieldOffset(server, serverGetFieldOffsetBody); server = WithGetCompositionProperty(server, serverGetCompositionPropertyBody);
if(cl.Implements.Count > 0) if(cl.Implements.Count > 0)
foreach (var impl in cl.Implements) foreach (var impl in cl.Implements)
@ -430,8 +379,6 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
StatementSyntax GeneratePropertySetterAssignment(GClass cl, GProperty prop, bool isObject, bool isNullable) StatementSyntax GeneratePropertySetterAssignment(GClass cl, GProperty prop, bool isObject, bool isNullable)
{ {
var pendingAnimationField = PropertyPendingAnimationFieldName(prop);
var code = @$" var code = @$"
// Update the backing value // Update the backing value
{PropertyBackingFieldName(prop)} = value; {PropertyBackingFieldName(prop)} = value;
@ -444,7 +391,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
{ {
code += @$" code += @$"
// Reset previous animation if any // Reset previous animation if any
{pendingAnimationField} = null; PendingAnimations.Remove({ClientProperty(cl, prop)});
{ChangedFieldsFieldName(cl)} &= ~{ChangedFieldsTypeName(cl)}.{prop.Name}Animated; {ChangedFieldsFieldName(cl)} &= ~{ChangedFieldsTypeName(cl)}.{prop.Name}Animated;
// Check for implicit animations // Check for implicit animations
if(ImplicitAnimations != null && ImplicitAnimations.TryGetValue(""{prop.Name}"", out var animation) == true) if(ImplicitAnimations != null && ImplicitAnimations.TryGetValue(""{prop.Name}"", out var animation) == true)
@ -453,7 +400,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
if(animation is CompositionAnimation a) if(animation is CompositionAnimation a)
{{ {{
{ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated; {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 // Animation is triggered by the current field, but does not necessary affects it
StartAnimationGroup(animation, ""{prop.Name}"", value); StartAnimationGroup(animation, ""{prop.Name}"", value);
@ -471,7 +418,7 @@ if (propertyName == ""{prop.Name}"")
{{ {{
var current = {PropertyBackingFieldName(prop)}; var current = {PropertyBackingFieldName(prop)};
var server = animation.CreateInstance(this.Server, finalValue); var server = animation.CreateInstance(this.Server, finalValue);
{PropertyPendingAnimationFieldName(prop)} = server; PendingAnimations[{ClientProperty(cl, prop)}] = server;
{ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated; {ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated;
RegisterForSerialization(); RegisterForSerialization();
return; return;
@ -512,6 +459,9 @@ return;
ParseStatement($"writer.Write({ChangedFieldsFieldName(cl)});") 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) BlockSyntax ApplySerializeField(BlockSyntax body, GClass cl, GProperty prop, bool isObject, bool isPassthrough)
{ {
@ -523,7 +473,7 @@ return;
{ {
code = $@" code = $@"
if(({changedFields} & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) if(({changedFields} & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated)
writer.WriteObject({PropertyPendingAnimationFieldName(prop)}); writer.WriteObject(PendingAnimations.GetAndRemove({ClientProperty(cl, prop)}));
else "; else ";
} }
@ -556,7 +506,7 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>();
{ {
code = $@" code = $@"
if((changed & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) if((changed & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated)
{PropertyBackingFieldName(prop)}.SetAnimation(this, commitedAt, reader.ReadObject<IAnimationInstance>(), {ServerPropertyOffsetFieldName(prop)}); SetAnimatedValue({CompositionPropertyField(prop)}, ref {PropertyBackingFieldName(prop)}, commitedAt, reader.ReadObject<IAnimationInstance>());
else "; else ";
} }
@ -565,7 +515,7 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>();
if((changed & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name}) if((changed & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name})
"; ";
if (prop.Animated) if (prop.Animated)
code += $"{PropertyBackingFieldName(prop)}.SetValue(this, {readValueCode});"; code += $"SetAnimatedValue({CompositionPropertyField(prop)}, out {PropertyBackingFieldName(prop)}, {readValueCode});";
else code += $"{prop.Name} = {readValueCode};"; else code += $"{prop.Name} = {readValueCode};";
return body.AddStatements(ParseStatement(code)); return body.AddStatements(ParseStatement(code));
} }
@ -583,14 +533,14 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>();
return cl.AddMembers(method); return cl.AddMembers(method);
} }
ClassDeclarationSyntax WithGetFieldOffset(ClassDeclarationSyntax cl, BlockSyntax body) ClassDeclarationSyntax WithGetCompositionProperty(ClassDeclarationSyntax cl, BlockSyntax body)
{ {
if (body.Statements.Count == 0) if (body.Statements.Count == 0)
return cl; return cl;
body = body.AddStatements( body = body.AddStatements(
ParseStatement("return base.GetFieldOffset(name);")); ParseStatement("return base.GetCompositionProperty(name);"));
var method = ((MethodDeclarationSyntax)ParseMemberDeclaration( var method = ((MethodDeclarationSyntax)ParseMemberDeclaration(
$"public override int? GetFieldOffset(string name){{}}")) $"public override CompositionProperty? GetCompositionProperty(string name){{}}"))
.WithBody(body); .WithBody(body);
return cl.AddMembers(method); return cl.AddMembers(method);

Loading…
Cancel
Save