Browse Source

Merge branch 'master' into feature/7120-fluent-control-themes

Steven Kirk 3 years ago
parent
commit
ffe55020bc
100 changed files with 7327 additions and 321 deletions
  1. 0 0
      .ncrunch/ControlCatalog.net6.0.v3.ncrunchproject
  2. 5 0
      .ncrunch/ControlCatalog.netstandard2.0.v3.ncrunchproject
  3. 9 6
      Avalonia.sln
  4. 21 2
      azure-pipelines-integrationtests.yml
  5. 1 1
      build/SourceGenerators.props
  6. 5 1
      native/Avalonia.Native/src/OSX/AvnView.mm
  7. 6 0
      native/Avalonia.Native/src/OSX/rendertarget.mm
  8. 2 2
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  9. 1 2
      samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs
  10. 2 7
      samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs
  11. 11 8
      samples/ControlCatalog.NetCore/Program.cs
  12. 1 0
      samples/ControlCatalog/App.xaml
  13. 3 0
      samples/ControlCatalog/MainView.xaml
  14. 11 33
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  15. 45 0
      samples/ControlCatalog/Pages/CompositionPage.axaml
  16. 153 0
      samples/ControlCatalog/Pages/CompositionPage.axaml.cs
  17. 21 6
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  18. 4 0
      samples/RenderDemo/App.xaml.cs
  19. 3 0
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  20. 306 0
      src/Avalonia.Base/Animation/Easings/CubicBezier.cs
  21. 27 0
      src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs
  22. 10 2
      src/Avalonia.Base/Avalonia.Base.csproj
  23. 1 0
      src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs
  24. 1 1
      src/Avalonia.Base/Collections/Pooled/PooledList.cs
  25. 52 4
      src/Avalonia.Base/Controls/Classes.cs
  26. 14 0
      src/Avalonia.Base/Controls/IClassesChangedListener.cs
  27. 7 0
      src/Avalonia.Base/Controls/IPseudoClasses.cs
  28. 1 1
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  29. 1 4
      src/Avalonia.Base/Input/Cursor.cs
  30. 0 18
      src/Avalonia.Base/Input/DragEventArgs.cs
  31. 0 11
      src/Avalonia.Base/Input/GotFocusEventArgs.cs
  32. 0 5
      src/Avalonia.Base/Input/IInputRoot.cs
  33. 0 13
      src/Avalonia.Base/Input/IKeyboardDevice.cs
  34. 0 12
      src/Avalonia.Base/Input/IMouseDevice.cs
  35. 0 14
      src/Avalonia.Base/Input/IPointerDevice.cs
  36. 0 2
      src/Avalonia.Base/Input/KeyEventArgs.cs
  37. 1 11
      src/Avalonia.Base/Input/KeyGesture.cs
  38. 1 53
      src/Avalonia.Base/Input/MouseDevice.cs
  39. 1 18
      src/Avalonia.Base/Input/PenDevice.cs
  40. 3 59
      src/Avalonia.Base/Input/PointerEventArgs.cs
  41. 2 0
      src/Avalonia.Base/Input/PointerOverPreProcessor.cs
  42. 0 5
      src/Avalonia.Base/Input/Raw/RawDragEvent.cs
  43. 0 3
      src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs
  44. 0 11
      src/Avalonia.Base/Input/TouchDevice.cs
  45. 4 4
      src/Avalonia.Base/Layout/LayoutManager.cs
  46. 2 2
      src/Avalonia.Base/Metadata/IAddChild.cs
  47. 16 0
      src/Avalonia.Base/Platform/IPlatformGpu.cs
  48. 82 0
      src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs
  49. 75 0
      src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs
  50. 24 0
      src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs
  51. 53 0
      src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs
  52. 49 0
      src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs
  53. 16 0
      src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs
  54. 15 0
      src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs
  55. 82 0
      src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs
  56. 76 0
      src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs
  57. 134 0
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs
  58. 178 0
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs
  59. 89 0
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs
  60. 49 0
      src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs
  61. 278 0
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  62. 75 0
      src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
  63. 141 0
      src/Avalonia.Base/Rendering/Composition/CompositionObject.cs
  64. 147 0
      src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs
  65. 130 0
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  66. 141 0
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  67. 24 0
      src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs
  68. 102 0
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs
  69. 391 0
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
  70. 14 0
      src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs
  71. 120 0
      src/Avalonia.Base/Rendering/Composition/Enums.cs
  72. 237 0
      src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs
  73. 184 0
      src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs
  74. 377 0
      src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs
  75. 32 0
      src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs
  76. 14 0
      src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs
  77. 298 0
      src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs
  78. 57 0
      src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs
  79. 730 0
      src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs
  80. 259 0
      src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs
  81. 6 0
      src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
  82. 66 0
      src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs
  83. 15 0
      src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs
  84. 179 0
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  85. 76 0
      src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs
  86. 46 0
      src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs
  87. 44 0
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs
  88. 75 0
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs
  89. 9 0
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs
  90. 221 0
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  91. 76 0
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs
  92. 237 0
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  93. 140 0
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
  94. 44 0
      src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs
  95. 180 0
      src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs
  96. 39 0
      src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs
  97. 184 0
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs
  98. 156 0
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs
  99. 9 0
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs
  100. 98 0
      src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs

+ 0 - 0
.ncrunch/ControlCatalog.v3.ncrunchproject → .ncrunch/ControlCatalog.net6.0.v3.ncrunchproject


+ 5 - 0
.ncrunch/ControlCatalog.netstandard2.0.v3.ncrunchproject

@@ -0,0 +1,5 @@
+<ProjectConfiguration>
+  <Settings>
+    <IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
+  </Settings>
+</ProjectConfiguration>

+ 9 - 6
Avalonia.sln

@@ -38,6 +38,7 @@ EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}"
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}"
 	ProjectSection(SolutionItems) = preProject
 	ProjectSection(SolutionItems) = preProject
 		.editorconfig = .editorconfig
 		.editorconfig = .editorconfig
+		src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs
 		src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
 		src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
 		src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs
 		src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs
 	EndProjectSection
 	EndProjectSection
@@ -205,14 +206,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\S
 EndProject
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}"
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}"
 EndProject
 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}"
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}"
 EndProject
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPicker", "src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj", "{7BF6C69D-FC14-43EB-9ED0-782C16F3D5D9}"
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPicker", "src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj", "{7BF6C69D-FC14-43EB-9ED0-782C16F3D5D9}"
 EndProject
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{EABE2161-989B-42BF-BD8D-1E34B20C21F1}"
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{EABE2161-989B-42BF-BD8D-1E34B20C21F1}"
 EndProject
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}"
+EndProject
 Global
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
 		Debug|Any CPU = Debug|Any CPU
@@ -485,10 +486,6 @@ Global
 		{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{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.ActiveCfg = Release|Any CPU
 		{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
 		{2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.Build.0 = 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
 		{2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -501,6 +498,10 @@ Global
 		{EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{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.ActiveCfg = Release|Any CPU
 		{EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Release|Any CPU.Build.0 = 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
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
 		HideSolutionNode = FALSE
@@ -557,6 +558,8 @@ Global
 		{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
 		{2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
 		{EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 		{EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+		{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
+		{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
 		SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

+ 21 - 2
azure-pipelines-integrationtests.yml

@@ -12,8 +12,27 @@ jobs:
     name: 'AvaloniaMacPool'
     name: 'AvaloniaMacPool'
 
 
   steps:
   steps:
-  - script: ./tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh
-    displayName: 'run integration tests'
+  - script: system_profiler SPDisplaysDataType |grep Resolution
+  
+  - script: |
+      pkill node
+      appium &
+      pkill IntegrationTestApp
+      ./build.sh CompileNative
+      rm -rf $(osascript -e "POSIX path of (path to application id \"net.avaloniaui.avalonia.integrationtestapp\")")
+      pkill IntegrationTestApp
+      ./samples/IntegrationTestApp/bundle.sh
+      open -n ./samples/IntegrationTestApp/bin/Debug/net6.0/osx-arm64/publish/IntegrationTestApp.app
+      pkill IntegrationTestApp
+
+  - task: DotNetCoreCLI@2
+    inputs:
+      command: 'test'
+      projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj'
+
+  - script: |
+      pkill IntegrationTestApp
+      pkill node
 
 
 
 
 - job: Windows
 - job: Windows

+ 1 - 1
build/SourceGenerators.props

@@ -1,7 +1,7 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
   <ItemGroup>
     <ProjectReference 
     <ProjectReference 
-      Include="$(MSBuildThisFileDirectory)/../src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj" 
+      Include="$(MSBuildThisFileDirectory)/../src/tools/DevGenerators/DevGenerators.csproj"
       OutputItemType="Analyzer" 
       OutputItemType="Analyzer" 
       ReferenceOutputAssembly="false"
       ReferenceOutputAssembly="false"
       PrivateAssets="all" />
       PrivateAssets="all" />

+ 5 - 1
native/Avalonia.Native/src/OSX/AvnView.mm

@@ -127,7 +127,11 @@
         [self updateRenderTarget];
         [self updateRenderTarget];
 
 
         auto reason = [self inLiveResize] ? ResizeUser : _resizeReason;
         auto reason = [self inLiveResize] ? ResizeUser : _resizeReason;
-        _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
+        
+        if(_parent->IsShown())
+        {
+            _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
+        }
     }
     }
 }
 }
 
 

+ 6 - 0
native/Avalonia.Native/src/OSX/rendertarget.mm

@@ -13,6 +13,7 @@
 {
 {
     @public IOSurfaceRef surface;
     @public IOSurfaceRef surface;
     @public AvnPixelSize size;
     @public AvnPixelSize size;
+    @public bool hasContent;
     @public float scale;
     @public float scale;
     ComPtr<IAvnGlContext> _context;
     ComPtr<IAvnGlContext> _context;
     GLuint _framebuffer, _texture, _renderbuffer;
     GLuint _framebuffer, _texture, _renderbuffer;
@@ -41,6 +42,7 @@
     self->scale = scale;
     self->scale = scale;
     self->size = size;
     self->size = size;
     self->_context = context;
     self->_context = context;
+    self->hasContent = false;
     return self;
     return self;
 }
 }
 
 
@@ -92,6 +94,7 @@
     _context->MakeCurrent(release.getPPV());
     _context->MakeCurrent(release.getPPV());
     glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
     glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
     glFlush();
     glFlush();
+    self->hasContent = true;
 }
 }
 
 
 -(void) dealloc
 -(void) dealloc
@@ -170,6 +173,8 @@ static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* ta
         @synchronized (lock) {
         @synchronized (lock) {
             if(_layer == nil)
             if(_layer == nil)
                 return;
                 return;
+            if(!surface->hasContent)
+                return;
             [CATransaction begin];
             [CATransaction begin];
             [_layer setContents: nil];
             [_layer setContents: nil];
             if(surface != nil)
             if(surface != nil)
@@ -213,6 +218,7 @@ static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* ta
             memcpy(pSurface + y*sstride, pFb + y*fstride, wbytes);
             memcpy(pSurface + y*sstride, pFb + y*fstride, wbytes);
         }
         }
         IOSurfaceUnlock(surf, 0, nil);
         IOSurfaceUnlock(surf, 0, nil);
+        surface->hasContent = true;
         [self updateLayer];
         [self updateLayer];
         return S_OK;
         return S_OK;
     }
     }

+ 2 - 2
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@@ -21,12 +21,12 @@
     <RunAOTCompilation>True</RunAOTCompilation>
     <RunAOTCompilation>True</RunAOTCompilation>
   </PropertyGroup>
   </PropertyGroup>
   
   
-  <PropertyGroup Condition="'$(RunAOTCompilation)'=='True'">
+  <!-- PropertyGroup Condition="'$(RunAOTCompilation)'=='True'">
     <EnableLLVM>True</EnableLLVM>
     <EnableLLVM>True</EnableLLVM>
     <AndroidAotAdditionalArguments>no-write-symbols,nodebug</AndroidAotAdditionalArguments>
     <AndroidAotAdditionalArguments>no-write-symbols,nodebug</AndroidAotAdditionalArguments>
     <AndroidAotMode>Hybrid</AndroidAotMode>
     <AndroidAotMode>Hybrid</AndroidAotMode>
     <AndroidGenerateJniMarshalMethods>True</AndroidGenerateJniMarshalMethods>
     <AndroidGenerateJniMarshalMethods>True</AndroidGenerateJniMarshalMethods>
-  </PropertyGroup>
+  </PropertyGroup -->
 
 
   <PropertyGroup Condition="'$(AndroidEnableProfiler)'=='True'">
   <PropertyGroup Condition="'$(AndroidEnableProfiler)'=='True'">
     <IsEmulator Condition="'$(IsEmulator)' == ''">True</IsEmulator>
     <IsEmulator Condition="'$(IsEmulator)' == ''">True</IsEmulator>

+ 1 - 2
samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs

@@ -22,8 +22,7 @@ public class EmbedSampleGtk : INativeDemoControl
 
 
         var control = createDefault();
         var control = createDefault();
         var nodes = Path.GetFullPath(Path.Combine(typeof(EmbedSample).Assembly.GetModules()[0].FullyQualifiedName,
         var nodes = Path.GetFullPath(Path.Combine(typeof(EmbedSample).Assembly.GetModules()[0].FullyQualifiedName,
-            "..",
-            "nodes.mp4"));
+            "..", "NativeControls", "Gtk", "nodes.mp4"));
         _mplayer = Process.Start(new ProcessStartInfo("mplayer",
         _mplayer = Process.Start(new ProcessStartInfo("mplayer",
             $"-vo x11 -zoom -loop 0 -wid {control.Handle.ToInt64()} \"{nodes}\"")
             $"-vo x11 -zoom -loop 0 -wid {control.Handle.ToInt64()} \"{nodes}\"")
         {
         {

+ 2 - 7
samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs

@@ -2,6 +2,7 @@ using System;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Avalonia.Controls.Platform;
 using Avalonia.Controls.Platform;
 using Avalonia.Platform.Interop;
 using Avalonia.Platform.Interop;
+using Avalonia.X11.Interop;
 using Avalonia.X11.NativeDialogs;
 using Avalonia.X11.NativeDialogs;
 using static Avalonia.X11.NativeDialogs.Gtk;
 using static Avalonia.X11.NativeDialogs.Gtk;
 using static Avalonia.X11.NativeDialogs.Glib;
 using static Avalonia.X11.NativeDialogs.Glib;
@@ -10,8 +11,6 @@ namespace ControlCatalog.NetCore;
 
 
 internal class GtkHelper
 internal class GtkHelper
 {
 {
-    private static Task<bool> s_gtkTask;
-
     class FileChooser : INativeControlHostDestroyableControlHandle
     class FileChooser : INativeControlHostDestroyableControlHandle
     {
     {
         private readonly IntPtr _widget;
         private readonly IntPtr _widget;
@@ -38,11 +37,7 @@ internal class GtkHelper
 
 
     public static INativeControlHostDestroyableControlHandle CreateGtkFileChooser(IntPtr parentXid)
     public static INativeControlHostDestroyableControlHandle CreateGtkFileChooser(IntPtr parentXid)
     {
     {
-        if (s_gtkTask == null)
-            s_gtkTask = StartGtk();
-        if (!s_gtkTask.Result)
-            return null;
-        return RunOnGlibThread(() =>
+        return GtkInteropHelper.RunOnGlibThread(() =>
         {
         {
             using (var title = new Utf8Buffer("Embedded"))
             using (var title = new Utf8Buffer("Embedded"))
             {
             {

+ 11 - 8
samples/ControlCatalog.NetCore/Program.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Diagnostics;
 using System.Diagnostics;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
@@ -53,7 +53,11 @@ namespace ControlCatalog.NetCore
             else if (args.Contains("--full-headless"))
             else if (args.Contains("--full-headless"))
             {
             {
                 return builder
                 return builder
-                    .UseHeadless(true)
+                    .UseHeadless(new AvaloniaHeadlessPlatformOptions
+                    {
+                        UseHeadlessDrawing = true,
+                        UseCompositor = true
+                    })
                     .AfterSetup(_ =>
                     .AfterSetup(_ =>
                     {
                     {
                         DispatcherTimer.RunOnce(async () =>
                         DispatcherTimer.RunOnce(async () =>
@@ -63,12 +67,11 @@ namespace ControlCatalog.NetCore
                             var tc = window.GetLogicalDescendants().OfType<TabControl>().First();
                             var tc = window.GetLogicalDescendants().OfType<TabControl>().First();
                             foreach (var page in tc.Items.Cast<TabItem>().ToList())
                             foreach (var page in tc.Items.Cast<TabItem>().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;
                                     continue;
                                 Console.WriteLine("Selecting " + page.Header);
                                 Console.WriteLine("Selecting " + page.Header);
                                 tc.SelectedItem = page;
                                 tc.SelectedItem = page;
-                                await Task.Delay(500);
+                                await Task.Delay(50);
                             }
                             }
                             Console.WriteLine("Selecting the first page");
                             Console.WriteLine("Selecting the first page");
                             tc.SelectedItem = tc.Items.OfType<object>().First();
                             tc.SelectedItem = tc.Items.OfType<object>().First();
@@ -77,7 +80,7 @@ namespace ControlCatalog.NetCore
                             for (var c = 0; c < 3; c++)
                             for (var c = 0; c < 3; c++)
                             {
                             {
                                 GC.Collect(2, GCCollectionMode.Forced);
                                 GC.Collect(2, GCCollectionMode.Forced);
-                                await Task.Delay(500);
+                                await Task.Delay(50);
                             }
                             }
 
 
                             void FormatMem(string metric, long bytes)
                             void FormatMem(string metric, long bytes)
@@ -87,7 +90,6 @@ namespace ControlCatalog.NetCore
 
 
                             FormatMem("GC allocated bytes", GC.GetTotalMemory(true));
                             FormatMem("GC allocated bytes", GC.GetTotalMemory(true));
                             FormatMem("WorkingSet64", Process.GetCurrentProcess().WorkingSet64);
                             FormatMem("WorkingSet64", Process.GetCurrentProcess().WorkingSet64);
-
                         }, TimeSpan.FromSeconds(1));
                         }, TimeSpan.FromSeconds(1));
                     })
                     })
                     .StartWithClassicDesktopLifetime(args);
                     .StartWithClassicDesktopLifetime(args);
@@ -111,10 +113,11 @@ namespace ControlCatalog.NetCore
                 {
                 {
                     EnableMultiTouch = true,
                     EnableMultiTouch = true,
                     UseDBusMenu = true,
                     UseDBusMenu = true,
-                    EnableIme = true,
+                    EnableIme = true
                 })
                 })
                 .With(new Win32PlatformOptions
                 .With(new Win32PlatformOptions
                 {
                 {
+                    EnableMultitouch = true
                 })
                 })
                 .UseSkia()
                 .UseSkia()
                 .AfterSetup(builder =>
                 .AfterSetup(builder =>

+ 1 - 0
samples/ControlCatalog/App.xaml

@@ -49,6 +49,7 @@
                 <NativeMenuItemSeparator />
                 <NativeMenuItemSeparator />
                 <NativeMenuItem Header="Option 3" ToggleType="CheckBox" IsChecked="True" Command="{Binding ToggleCommand}" />
                 <NativeMenuItem Header="Option 3" ToggleType="CheckBox" IsChecked="True" Command="{Binding ToggleCommand}" />
                 <NativeMenuItem Icon="/Assets/test_icon.ico" Header="Restore Defaults" Command="{Binding ToggleCommand}" />
                 <NativeMenuItem Icon="/Assets/test_icon.ico" Header="Restore Defaults" Command="{Binding ToggleCommand}" />
+                <NativeMenuItem Header="Disabled option" IsEnabled="False" />
               </NativeMenu>
               </NativeMenu>
             </NativeMenuItem>
             </NativeMenuItem>
             <NativeMenuItem Header="Exit" Command="{Binding ExitCommand}" />
             <NativeMenuItem Header="Exit" Command="{Binding ExitCommand}" />

+ 3 - 0
samples/ControlCatalog/MainView.xaml

@@ -13,6 +13,9 @@
       </Style>
       </Style>
     </Grid.Styles>
     </Grid.Styles>
     <controls:HamburgerMenu Name="Sidebar">
     <controls:HamburgerMenu Name="Sidebar">
+      <TabItem Header="Composition">
+        <pages:CompositionPage/>
+      </TabItem>
       <TabItem Header="Acrylic">
       <TabItem Header="Acrylic">
         <pages:AcrylicPage />
         <pages:AcrylicPage />
       </TabItem>
       </TabItem>

+ 11 - 33
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@@ -13,8 +13,14 @@
     <pc:ThirdComponentConverter x:Key="ThirdComponent" />
     <pc:ThirdComponentConverter x:Key="ThirdComponent" />
   </UserControl.Resources>
   </UserControl.Resources>
 
 
-  <Grid ColumnDefinitions="Auto,10,Auto">
-    <Grid Grid.Column="0"
+  <Grid ColumnDefinitions="Auto,10,Auto,10,Auto"
+        RowDefinitions="Auto,Auto">
+    <ColorPicker Grid.Column="0"
+                 Grid.Row="1" />
+    <ColorView Grid.Column="0"
+               Grid.Row="0"
+               ColorSpectrumShape="Ring" />
+    <Grid Grid.Column="2"
           Grid.Row="0"
           Grid.Row="0"
           RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
           RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
       <ColorSpectrum x:Name="ColorSpectrum1"
       <ColorSpectrum x:Name="ColorSpectrum1"
@@ -41,39 +47,11 @@
                    ColorModel="Hsva"
                    ColorModel="Hsva"
                    HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
                    HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
       <ColorPreviewer Grid.Row="5"
       <ColorPreviewer Grid.Row="5"
-                      ShowAccentColors="True"
+                      IsAccentColorsVisible="True"
                       HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
                       HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
     </Grid>
     </Grid>
-    <Grid Grid.Column="2"
-          Grid.Row="0"
-          ColumnDefinitions="Auto,Auto,Auto"
-          RowDefinitions="Auto,Auto">
-      <ColorSlider Grid.Column="0"
-                   Grid.Row="0"
-                   IsAlphaMaxForced="True"
-                   IsSaturationValueMaxForced="False"
-                   ColorComponent="{Binding Components, ElementName=ColorSpectrum2, Converter={StaticResource ThirdComponent}}"
-                   ColorModel="Hsva"
-                   Orientation="Vertical"
-                   HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
-      <ColorSpectrum x:Name="ColorSpectrum2"
-                     Grid.Column="1"
-                     Grid.Row="0"
-                     Color="Green"
-                     Shape="Ring"
-                     Height="256"
-                     Width="256" />
-      <ColorSlider Grid.Column="2"
-                   Grid.Row="0"
-                   ColorComponent="Alpha"
-                   ColorModel="Hsva"
-                   Orientation="Vertical"
-                   HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
-      <ColorPreviewer Grid.Column="0"
-                      Grid.ColumnSpan="3"
-                      Grid.Row="1"
-                      ShowAccentColors="True"
-                      HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
+    <Grid Grid.Column="4"
+          Grid.Row="0">
     </Grid>
     </Grid>
   </Grid>
   </Grid>
 </UserControl>
 </UserControl>

+ 45 - 0
samples/ControlCatalog/Pages/CompositionPage.axaml

@@ -0,0 +1,45 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:pages="clr-namespace:ControlCatalog.Pages"
+             x:Class="ControlCatalog.Pages.CompositionPage">
+    <StackPanel>
+        <TextBlock Classes="h1">Implicit animations</TextBlock>
+        <Grid ColumnDefinitions="*,10,40" Margin="0 0 40 0">
+            <ItemsControl x:Name="Items">
+                <ItemsControl.ItemsPanel>
+                    <ItemsPanelTemplate>
+                        <WrapPanel/>
+                    </ItemsPanelTemplate>
+                </ItemsControl.ItemsPanel>
+                <ItemsControl.DataTemplates>
+                    <DataTemplate DataType="pages:CompositionPageColorItem">
+                        <Border 
+                            pages:CompositionPage.EnableAnimations="True"
+                            Padding="10" BorderBrush="Gray" BorderThickness="2"
+                            Background="{Binding ColorBrush}" Width="100" Height="100" Margin="10">
+                            <TextBlock Text="{Binding ColorHexValue}"/>
+                        </Border>
+                    </DataTemplate>
+                </ItemsControl.DataTemplates>
+            </ItemsControl>
+            <GridSplitter Margin="2" BorderThickness="1" BorderBrush="Gray" 
+                          Background="#e0e0e0" Grid.Column="1"
+                          ResizeDirection="Columns" ResizeBehavior="PreviousAndNext"
+            />
+            <Border Grid.Column="2">
+                <LayoutTransformControl
+                    HorizontalAlignment="Center"
+            
+                    MinWidth="30">
+                    <LayoutTransformControl.LayoutTransform>
+                        <RotateTransform Angle="90"/>
+                    </LayoutTransformControl.LayoutTransform>
+            
+                    <TextBlock>Resize me</TextBlock>
+                </LayoutTransformControl>
+            </Border>
+        </Grid>
+
+     
+    </StackPanel>
+</UserControl>

+ 153 - 0
samples/ControlCatalog/Pages/CompositionPage.axaml.cs

@@ -0,0 +1,153 @@
+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<ItemsControl>("Items").Items = CreateColorItems();
+    }
+
+    private List<CompositionPageColorItem> CreateColorItems()
+    {
+        var list = new List<CompositionPageColorItem>();
+
+        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;
+    }
+    
+    private void EnsureImplicitAnimations()
+    {
+        if (_implicitAnimations == null)
+        {
+            var compositor = ElementComposition.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<CompositionPage>();
+        if (page == null)
+        {
+            border.AttachedToVisualTree += delegate { SetEnableAnimations(border, true); };
+            return;
+        }
+
+        if (ElementComposition.GetElementVisual(page) == null)
+            return;
+
+        page.EnsureImplicitAnimations();
+        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;
+    }
+}

+ 21 - 6
samples/ControlCatalog/Pages/ProgressBarPage.xaml

@@ -1,22 +1,37 @@
 <UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.Pages.ProgressBarPage">
 <UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.Pages.ProgressBarPage">
   <StackPanel Orientation="Vertical" Spacing="4">
   <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h2">A progress bar control</TextBlock>
     <TextBlock Classes="h2">A progress bar control</TextBlock>
-    <StackPanel>
+    <StackPanel Spacing="5">
+      <StackPanel Orientation="Horizontal" Spacing="5">
+        <TextBlock VerticalAlignment="Center">Maximum</TextBlock>
+        <NumericUpDown x:Name="maximum" Value="100" VerticalAlignment="Center"/>
+      </StackPanel>
+      <StackPanel Orientation="Horizontal" Spacing="5">
+        <TextBlock VerticalAlignment="Center">Minimum</TextBlock>
+        <NumericUpDown x:Name="minimum" Value="0" VerticalAlignment="Center"/>
+      </StackPanel>
+      <StackPanel Orientation="Horizontal" Spacing="5">
+        <TextBlock VerticalAlignment="Center">Progress Text Format</TextBlock>
+        <TextBox x:Name="stringFormat" Text="{}{0:0}%" VerticalAlignment="Center"/>
+      </StackPanel>
       <CheckBox x:Name="showProgress" Margin="10,16,0,0" Content="Show Progress Text" />
       <CheckBox x:Name="showProgress" Margin="10,16,0,0" Content="Show Progress Text" />
       <CheckBox x:Name="isIndeterminate" Margin="10,16,0,0" Content="Toggle Indeterminate" />
       <CheckBox x:Name="isIndeterminate" Margin="10,16,0,0" Content="Toggle Indeterminate" />
       <StackPanel Orientation="Horizontal" Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16">
       <StackPanel Orientation="Horizontal" Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16">
         <StackPanel Spacing="16">
         <StackPanel Spacing="16">
-          <ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #hprogress.Value}" />
+          <ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #hprogress.Value}" 
+                       Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" ProgressTextFormat="{Binding #stringFormat.Text}"/>
         </StackPanel>
         </StackPanel>
-        <ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #vprogress.Value}" Orientation="Vertical" />
+        <ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #vprogress.Value}" Orientation="Vertical" 
+                     Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" ProgressTextFormat="{Binding #stringFormat.Text}"/>
       </StackPanel>
       </StackPanel>
       <StackPanel Margin="16">
       <StackPanel Margin="16">
-        <Slider Name="hprogress" Maximum="100" Value="40" />
-        <Slider Name="vprogress" Maximum="100" Value="60" />
+        <Slider Name="hprogress" Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" Value="40" />
+        <Slider Name="vprogress" Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" Value="60" />
       </StackPanel>
       </StackPanel>
 
 
       <StackPanel Spacing="10">
       <StackPanel Spacing="10">
-        <ProgressBar VerticalAlignment="Center" IsIndeterminate="True" />
+        <ProgressBar VerticalAlignment="Center" IsIndeterminate="True" 
+                     Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.value}"/>
         <ProgressBar VerticalAlignment="Center" Value="5" Maximum="10" />
         <ProgressBar VerticalAlignment="Center" Value="5" Maximum="10" />
         <ProgressBar VerticalAlignment="Center" Value="50" />
         <ProgressBar VerticalAlignment="Center" Value="50" />
         <ProgressBar VerticalAlignment="Center" Value="50" Minimum="25" Maximum="75" />
         <ProgressBar VerticalAlignment="Center" Value="50" Minimum="25" Maximum="75" />

+ 4 - 0
samples/RenderDemo/App.xaml.cs

@@ -29,6 +29,10 @@ namespace RenderDemo
                .With(new Win32PlatformOptions
                .With(new Win32PlatformOptions
                {
                {
                    OverlayPopups = true,
                    OverlayPopups = true,
+               })
+               .With(new X11PlatformOptions
+               {
+                   UseCompositor = true
                })
                })
                 .UsePlatformDetect()
                 .UsePlatformDetect()
                 .LogToTrace();
                 .LogToTrace();

+ 3 - 0
src/Android/Avalonia.Android/ChoreographerTimer.cs

@@ -29,6 +29,9 @@ namespace Avalonia.Android
             _thread = new Thread(Loop);
             _thread = new Thread(Loop);
             _thread.Start();
             _thread.Start();
         }
         }
+        
+        
+        public bool RunsInBackground => true;
 
 
         public event Action<TimeSpan> Tick
         public event Action<TimeSpan> Tick
         {
         {

+ 306 - 0
src/Avalonia.Base/Animation/Easings/CubicBezier.cs

@@ -0,0 +1,306 @@
+// ReSharper disable InconsistentNaming
+// 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
+// ReSharper disable MemberCanBePrivate.Global
+// ReSharper disable TooWideLocalVariableScope
+// ReSharper disable UnusedMember.Global
+#pragma warning disable 649
+
+namespace Avalonia.Animation.Easings
+{
+    /// <summary>
+    /// Represents a cubic bezier curve and can compute Y coordinate for a given X
+    /// </summary>
+    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 = MathUtilities.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_;
+    }
+}

+ 27 - 0
src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs

@@ -0,0 +1,27 @@
+using System;
+
+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);
+    }
+}

+ 10 - 2
src/Avalonia.Base/Avalonia.Base.csproj

@@ -3,10 +3,13 @@
     <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
     <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
     <AssemblyName>Avalonia.Base</AssemblyName>
     <AssemblyName>Avalonia.Base</AssemblyName>
     <RootNamespace>Avalonia</RootNamespace>
     <RootNamespace>Avalonia</RootNamespace>
-    <AllowUnsafeBlocks>True</AllowUnsafeBlocks>    
+    <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
+    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
+    <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
   </PropertyGroup>
   </PropertyGroup>
   <ItemGroup>
   <ItemGroup>
     <EmbeddedResource Include="Assets\*.trie" />
     <EmbeddedResource Include="Assets\*.trie" />
+    <AdditionalFiles Include="composition-schema.xml" />
   </ItemGroup>
   </ItemGroup>
   <Import Project="..\..\build\Base.props" />
   <Import Project="..\..\build\Base.props" />
   <Import Project="..\..\build\Binding.props" />
   <Import Project="..\..\build\Binding.props" />
@@ -32,6 +35,11 @@
     <InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Web.Blazor, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Web.Blazor, PublicKey=$(AvaloniaPublicKey)" />
-    <InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7"/>
+    <InternalsVisibleTo Include="Avalonia.Dialogs, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Folder Include="Rendering\Composition\Utils" />
   </ItemGroup>
   </ItemGroup>
 </Project>
 </Project>

+ 1 - 0
src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs

@@ -1,3 +1,4 @@
+using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.Collections.Specialized;
 using System.ComponentModel;
 using System.ComponentModel;

+ 1 - 1
src/Avalonia.Base/Collections/Pooled/PooledList.cs

@@ -1434,7 +1434,7 @@ namespace Avalonia.Collections.Pooled
         /// <summary>
         /// <summary>
         /// Returns the internal buffers to the ArrayPool.
         /// Returns the internal buffers to the ArrayPool.
         /// </summary>
         /// </summary>
-        public void Dispose()
+        public virtual void Dispose()
         {
         {
             ReturnArray();
             ReturnArray();
             _size = 0;
             _size = 0;

+ 52 - 4
src/Avalonia.Base/Controls/Classes.cs

@@ -1,8 +1,7 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using Avalonia.Collections;
 using Avalonia.Collections;
-
-#nullable enable
+using Avalonia.Utilities;
 
 
 namespace Avalonia.Controls
 namespace Avalonia.Controls
 {
 {
@@ -14,6 +13,8 @@ namespace Avalonia.Controls
     /// </remarks>
     /// </remarks>
     public class Classes : AvaloniaList<string>, IPseudoClasses
     public class Classes : AvaloniaList<string>, IPseudoClasses
     {
     {
+        private SafeEnumerableList<IClassesChangedListener>? _listeners;
+
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="Classes"/> class.
         /// Initializes a new instance of the <see cref="Classes"/> class.
         /// </summary>
         /// </summary>
@@ -39,6 +40,11 @@ namespace Avalonia.Controls
         {            
         {            
         }
         }
 
 
+        /// <summary>
+        /// Gets the number of listeners subscribed to this collection for unit testing purposes.
+        /// </summary>
+        internal int ListenerCount => _listeners?.Count ?? 0;
+
         /// <summary>
         /// <summary>
         /// Parses a classes string.
         /// Parses a classes string.
         /// </summary>
         /// </summary>
@@ -62,6 +68,7 @@ namespace Avalonia.Controls
             if (!Contains(name))
             if (!Contains(name))
             {
             {
                 base.Add(name);
                 base.Add(name);
+                NotifyChanged();
             }
             }
         }
         }
 
 
@@ -89,6 +96,7 @@ namespace Avalonia.Controls
             }
             }
 
 
             base.AddRange(c);
             base.AddRange(c);
+            NotifyChanged();
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -103,6 +111,8 @@ namespace Avalonia.Controls
                     RemoveAt(i);
                     RemoveAt(i);
                 }
                 }
             }
             }
+
+            NotifyChanged();
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -122,6 +132,7 @@ namespace Avalonia.Controls
             if (!Contains(name))
             if (!Contains(name))
             {
             {
                 base.Insert(index, name);
                 base.Insert(index, name);
+                NotifyChanged();
             }
             }
         }
         }
 
 
@@ -154,6 +165,7 @@ namespace Avalonia.Controls
             if (toInsert != null)
             if (toInsert != null)
             {
             {
                 base.InsertRange(index, toInsert);
                 base.InsertRange(index, toInsert);
+                NotifyChanged();
             }
             }
         }
         }
 
 
@@ -169,7 +181,14 @@ namespace Avalonia.Controls
         public override bool Remove(string name)
         public override bool Remove(string name)
         {
         {
             ThrowIfPseudoclass(name, "removed");
             ThrowIfPseudoclass(name, "removed");
-            return base.Remove(name);
+
+            if (base.Remove(name))
+            {
+                NotifyChanged();
+                return true;
+            }
+
+            return false;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -197,6 +216,7 @@ namespace Avalonia.Controls
             if (toRemove != null)
             if (toRemove != null)
             {
             {
                 base.RemoveAll(toRemove);
                 base.RemoveAll(toRemove);
+                NotifyChanged();
             }
             }
         }
         }
 
 
@@ -214,6 +234,7 @@ namespace Avalonia.Controls
             var name = this[index];
             var name = this[index];
             ThrowIfPseudoclass(name, "removed");
             ThrowIfPseudoclass(name, "removed");
             base.RemoveAt(index);
             base.RemoveAt(index);
+            NotifyChanged();
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -224,6 +245,7 @@ namespace Avalonia.Controls
         public override void RemoveRange(int index, int count)
         public override void RemoveRange(int index, int count)
         {
         {
             base.RemoveRange(index, count);
             base.RemoveRange(index, count);
+            NotifyChanged();
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -255,6 +277,7 @@ namespace Avalonia.Controls
             }
             }
 
 
             base.AddRange(source);
             base.AddRange(source);
+            NotifyChanged();
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
@@ -263,13 +286,38 @@ namespace Avalonia.Controls
             if (!Contains(name))
             if (!Contains(name))
             {
             {
                 base.Add(name);
                 base.Add(name);
+                NotifyChanged();
             }
             }
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
         bool IPseudoClasses.Remove(string name)
         bool IPseudoClasses.Remove(string name)
         {
         {
-            return base.Remove(name);
+            if (base.Remove(name))
+            {
+                NotifyChanged();
+                return true;
+            }
+
+            return false;
+        }
+
+        internal void AddListener(IClassesChangedListener listener)
+        {
+            (_listeners ??= new()).Add(listener);
+        }
+
+        internal void RemoveListener(IClassesChangedListener listener)
+        {
+            _listeners?.Remove(listener);
+        }
+
+        private void NotifyChanged()
+        {
+            if (_listeners is null)
+                return;
+            foreach (var listener in _listeners)
+                listener.Changed();
         }
         }
 
 
         private void ThrowIfPseudoclass(string name, string operation)
         private void ThrowIfPseudoclass(string name, string operation)

+ 14 - 0
src/Avalonia.Base/Controls/IClassesChangedListener.cs

@@ -0,0 +1,14 @@
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Internal interface for listening to changes in <see cref="Classes"/> in a more
+    /// performant manner than subscribing to CollectionChanged.
+    /// </summary>
+    internal interface IClassesChangedListener
+    {
+        /// <summary>
+        /// Notifies the listener that the <see cref="Classes"/> collection has changed.
+        /// </summary>
+        void Changed();
+    }
+}

+ 7 - 0
src/Avalonia.Base/Controls/IPseudoClasses.cs

@@ -19,5 +19,12 @@ namespace Avalonia.Controls
         /// </summary>
         /// </summary>
         /// <param name="name">The pseudoclass name.</param>
         /// <param name="name">The pseudoclass name.</param>
         bool Remove(string name);
         bool Remove(string name);
+
+        /// <summary>
+        /// Returns whether a pseudoclass is present in the collection.
+        /// </summary>
+        /// <param name="name">The pseudoclass name.</param>
+        /// <returns>Whether the pseudoclass is present.</returns>
+        bool Contains(string name);
     }
     }
 }
 }

+ 1 - 1
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@@ -55,7 +55,7 @@ namespace Avalonia.Data.Core.Plugins
         
         
         private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName)
         private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName)
         {
         {
-            if (instance is IReflectableType reflectableType)
+            if (instance is IReflectableType reflectableType && instance is not Type)
                 return reflectableType.GetTypeInfo().GetProperty(propertyName, PropertyBindingFlags);
                 return reflectableType.GetTypeInfo().GetProperty(propertyName, PropertyBindingFlags);
 
 
             var type = instance.GetType();
             var type = instance.GetType();

+ 1 - 4
src/Avalonia.Base/Input/Cursor.cs

@@ -32,10 +32,7 @@ namespace Avalonia.Input
         DragCopy,
         DragCopy,
         DragLink,
         DragLink,
         None,
         None,
-
-        [Obsolete("Use BottomSide")]
-        BottomSize = BottomSide
-
+        
         // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ 
         // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ 
         // We might enable them later, preferably, by loading pixmax directly from theme with fallback image
         // We might enable them later, preferably, by loading pixmax directly from theme with fallback image
         // SizeNorthWestSouthEast,
         // SizeNorthWestSouthEast,

+ 0 - 18
src/Avalonia.Base/Input/DragEventArgs.cs

@@ -13,9 +13,6 @@ namespace Avalonia.Input
 
 
         public IDataObject Data { get; private set; }
         public IDataObject Data { get; private set; }
 
 
-        [Obsolete("Use KeyModifiers")]
-        public InputModifiers Modifiers { get; private set; }
-
         public KeyModifiers KeyModifiers { get; private set; }
         public KeyModifiers KeyModifiers { get; private set; }
 
 
         public Point GetPosition(IVisual relativeTo)
         public Point GetPosition(IVisual relativeTo)
@@ -35,17 +32,6 @@ namespace Avalonia.Input
             return point;
             return point;
         }
         }
 
 
-        [Obsolete("Use constructor taking KeyModifiers")]
-        public DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data, Interactive target, Point targetLocation, InputModifiers modifiers)
-            : base(routedEvent)
-        {
-            Data = data;
-            _target = target;
-            _targetLocation = targetLocation;
-            Modifiers = modifiers;
-            KeyModifiers = (KeyModifiers)(((int)modifiers) & 0xF);
-        }
-
         public DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers)
         public DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers)
             : base(routedEvent)
             : base(routedEvent)
         {
         {
@@ -53,10 +39,6 @@ namespace Avalonia.Input
             _target = target;
             _target = target;
             _targetLocation = targetLocation;
             _targetLocation = targetLocation;
             KeyModifiers = keyModifiers;
             KeyModifiers = keyModifiers;
-#pragma warning disable CS0618 // Type or member is obsolete
-            Modifiers = (InputModifiers)keyModifiers;
-#pragma warning restore CS0618 // Type or member is obsolete
         }
         }
-
     }
     }
 }
 }

+ 0 - 11
src/Avalonia.Base/Input/GotFocusEventArgs.cs

@@ -1,4 +1,3 @@
-using System;
 using Avalonia.Interactivity;
 using Avalonia.Interactivity;
 
 
 namespace Avalonia.Input
 namespace Avalonia.Input
@@ -13,16 +12,6 @@ namespace Avalonia.Input
         /// </summary>
         /// </summary>
         public NavigationMethod NavigationMethod { get; set; }
         public NavigationMethod NavigationMethod { get; set; }
 
 
-        /// <summary>
-        /// Gets or sets any input modifiers active at the time of focus.
-        /// </summary>
-        [Obsolete("Use KeyModifiers")]
-        public InputModifiers InputModifiers
-        {
-            get => (InputModifiers)KeyModifiers;
-            set => KeyModifiers = (KeyModifiers)((int)value & 0xF);
-        }
-
         /// <summary>
         /// <summary>
         /// Gets or sets any key modifiers active at the time of focus.
         /// Gets or sets any key modifiers active at the time of focus.
         /// </summary>
         /// </summary>

+ 0 - 5
src/Avalonia.Base/Input/IInputRoot.cs

@@ -27,10 +27,5 @@ namespace Avalonia.Input
         /// Gets or sets a value indicating whether access keys are shown in the window.
         /// Gets or sets a value indicating whether access keys are shown in the window.
         /// </summary>
         /// </summary>
         bool ShowAccessKeys { get; set; }
         bool ShowAccessKeys { get; set; }
-
-        /// <summary>
-        /// Gets associated mouse device
-        /// </summary>
-        IMouseDevice? MouseDevice { get; }
     }
     }
 }
 }

+ 0 - 13
src/Avalonia.Base/Input/IKeyboardDevice.cs

@@ -4,19 +4,6 @@ using Avalonia.Metadata;
 
 
 namespace Avalonia.Input
 namespace Avalonia.Input
 {
 {
-    [Flags, Obsolete("Use KeyModifiers and PointerPointProperties")]
-    public enum InputModifiers
-    {
-        None = 0,
-        Alt = 1,
-        Control = 2,
-        Shift = 4,
-        Windows = 8,
-        LeftMouseButton = 16,
-        RightMouseButton = 32,
-        MiddleMouseButton = 64
-    }
-
     [Flags]
     [Flags]
     public enum KeyModifiers
     public enum KeyModifiers
     {
     {

+ 0 - 12
src/Avalonia.Base/Input/IMouseDevice.cs

@@ -1,4 +1,3 @@
-using System;
 using Avalonia.Metadata;
 using Avalonia.Metadata;
 
 
 namespace Avalonia.Input
 namespace Avalonia.Input
@@ -9,16 +8,5 @@ namespace Avalonia.Input
     [NotClientImplementable]
     [NotClientImplementable]
     public interface IMouseDevice : IPointerDevice
     public interface IMouseDevice : IPointerDevice
     {
     {
-        /// <summary>
-        /// Gets the mouse position, in screen coordinates.
-        /// </summary>
-        [Obsolete("Use PointerEventArgs.GetPosition")]
-        PixelPoint Position { get; }
-
-        [Obsolete]
-        void TopLevelClosed(IInputRoot root);
-
-        [Obsolete]
-        void SceneInvalidated(IInputRoot root, Rect rect);
     }
     }
 }
 }

+ 0 - 14
src/Avalonia.Base/Input/IPointerDevice.cs

@@ -1,5 +1,3 @@
-using System;
-using Avalonia.VisualTree;
 using Avalonia.Input.Raw;
 using Avalonia.Input.Raw;
 using Avalonia.Metadata;
 using Avalonia.Metadata;
 
 
@@ -8,18 +6,6 @@ namespace Avalonia.Input
     [NotClientImplementable]
     [NotClientImplementable]
     public interface IPointerDevice : IInputDevice
     public interface IPointerDevice : IInputDevice
     {
     {
-        /// <inheritdoc cref="IPointer.Captured" />
-        [Obsolete("Use IPointer")]
-        IInputElement? Captured { get; }
-
-        /// <inheritdoc cref="IPointer.Capture(IInputElement?)" />
-        [Obsolete("Use IPointer")]
-        void Capture(IInputElement? control);
-
-        /// <inheritdoc cref="PointerEventArgs.GetPosition(IVisual?)" />
-        [Obsolete("Use PointerEventArgs.GetPosition")]
-        Point GetPosition(IVisual relativeTo);
-
         /// <summary>
         /// <summary>
         /// Gets a pointer for specific event args.
         /// Gets a pointer for specific event args.
         /// </summary>
         /// </summary>

+ 0 - 2
src/Avalonia.Base/Input/KeyEventArgs.cs

@@ -9,8 +9,6 @@ namespace Avalonia.Input
 
 
         public Key Key { get; set; }
         public Key Key { get; set; }
 
 
-        [Obsolete("Use KeyModifiers")]
-        public InputModifiers Modifiers => (InputModifiers)KeyModifiers;
         public KeyModifiers KeyModifiers { get; set; }
         public KeyModifiers KeyModifiers { get; set; }
     }
     }
 }
 }

+ 1 - 11
src/Avalonia.Base/Input/KeyGesture.cs

@@ -15,13 +15,6 @@ namespace Avalonia.Input
             { "+", Key.OemPlus }, { "-", Key.OemMinus }, { ".", Key.OemPeriod }, { ",", Key.OemComma }
             { "+", Key.OemPlus }, { "-", Key.OemMinus }, { ".", Key.OemPeriod }, { ",", Key.OemComma }
         };
         };
 
 
-        [Obsolete("Use constructor taking KeyModifiers")]
-        public KeyGesture(Key key, InputModifiers modifiers)
-        {
-            Key = key;
-            KeyModifiers = (KeyModifiers)(((int)modifiers) & 0xf);
-        }
-
         public KeyGesture(Key key, KeyModifiers modifiers = KeyModifiers.None)
         public KeyGesture(Key key, KeyModifiers modifiers = KeyModifiers.None)
         {
         {
             Key = key;
             Key = key;
@@ -63,10 +56,7 @@ namespace Avalonia.Input
         }
         }
 
 
         public Key Key { get; }
         public Key Key { get; }
-
-        [Obsolete("Use KeyModifiers")]
-        public InputModifiers Modifiers => (InputModifiers)KeyModifiers;
-
+        
         public KeyModifiers KeyModifiers { get; }
         public KeyModifiers KeyModifiers { get; }
 
 
         public static KeyGesture Parse(string gesture)
         public static KeyGesture Parse(string gesture)

+ 1 - 53
src/Avalonia.Base/Input/MouseDevice.cs

@@ -21,7 +21,6 @@ namespace Avalonia.Input
 
 
         private readonly Pointer _pointer;
         private readonly Pointer _pointer;
         private bool _disposed;
         private bool _disposed;
-        private PixelPoint? _position;
         private MouseButton _lastMouseDownButton;
         private MouseButton _lastMouseDownButton;
 
 
         public MouseDevice(Pointer? pointer = null)
         public MouseDevice(Pointer? pointer = null)
@@ -29,43 +28,6 @@ namespace Avalonia.Input
             _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
             _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
         }
         }
 
 
-        [Obsolete("Use IPointer instead")]
-        public IInputElement? Captured => _pointer.Captured;
-
-        [Obsolete("Use events instead")]
-        public PixelPoint Position
-        {
-            get => _position ?? new PixelPoint(-1, -1);
-            protected set => _position = value;
-        }
-
-        [Obsolete("Use IPointer instead")]
-        public void Capture(IInputElement? control)
-        {
-            _pointer.Capture(control);
-        }
-
-        /// <summary>
-        /// Gets the mouse position relative to a control.
-        /// </summary>
-        /// <param name="relativeTo">The control.</param>
-        /// <returns>The mouse position in the control's coordinates.</returns>
-        public Point GetPosition(IVisual relativeTo)
-        {
-            relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo));
-
-            if (relativeTo.VisualRoot == null)
-            {
-                throw new InvalidOperationException("Control is not attached to visual tree.");
-            }
-
-#pragma warning disable CS0618 // Type or member is obsolete
-            var rootPoint = relativeTo.VisualRoot.PointToClient(Position);
-#pragma warning restore CS0618 // Type or member is obsolete
-            var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo);
-            return rootPoint * transform!.Value;
-        }
-
         public void ProcessRawEvent(RawInputEventArgs e)
         public void ProcessRawEvent(RawInputEventArgs e)
         {
         {
             if (!e.Handled && e is RawPointerEventArgs margs)
             if (!e.Handled && e is RawPointerEventArgs margs)
@@ -96,7 +58,6 @@ namespace Avalonia.Input
             if(mouse._disposed)
             if(mouse._disposed)
                 return;
                 return;
 
 
-            _position = e.Root.PointToScreen(e.Position);
             var props = CreateProperties(e);
             var props = CreateProperties(e);
             var keyModifiers = e.InputModifiers.ToKeyModifiers();
             var keyModifiers = e.InputModifiers.ToKeyModifiers();
             switch (e.Type)
             switch (e.Type)
@@ -145,7 +106,6 @@ namespace Avalonia.Input
 
 
         private void LeaveWindow()
         private void LeaveWindow()
         {
         {
-            _position = null;
         }
         }
 
 
         PointerPointProperties CreateProperties(RawPointerEventArgs args)
         PointerPointProperties CreateProperties(RawPointerEventArgs args)
@@ -324,19 +284,7 @@ namespace Avalonia.Input
             _disposed = true;
             _disposed = true;
             _pointer?.Dispose();
             _pointer?.Dispose();
         }
         }
-
-        [Obsolete]
-        public void TopLevelClosed(IInputRoot root)
-        {
-            // no-op
-        }
-
-        [Obsolete]
-        public void SceneInvalidated(IInputRoot root, Rect rect)
-        {
-            // no-op
-        }
-
+        
         public IPointer? TryGetPointer(RawPointerEventArgs ev)
         public IPointer? TryGetPointer(RawPointerEventArgs ev)
         {
         {
             return _pointer;
             return _pointer;

+ 1 - 18
src/Avalonia.Base/Input/PenDevice.cs

@@ -1,10 +1,8 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
-using System.Reactive.Linq;
 using Avalonia.Input.Raw;
 using Avalonia.Input.Raw;
 using Avalonia.Platform;
 using Avalonia.Platform;
-using Avalonia.VisualTree;
 
 
 namespace Avalonia.Input
 namespace Avalonia.Input
 {
 {
@@ -14,7 +12,6 @@ namespace Avalonia.Input
     public class PenDevice : IPenDevice, IDisposable
     public class PenDevice : IPenDevice, IDisposable
     {
     {
         private readonly Dictionary<long, Pointer> _pointers = new();
         private readonly Dictionary<long, Pointer> _pointers = new();
-        private readonly Dictionary<long, PixelPoint> _lastPositions = new();
         private int _clickCount;
         private int _clickCount;
         private Rect _lastClickRect;
         private Rect _lastClickRect;
         private ulong _lastClickTime;
         private ulong _lastClickTime;
@@ -41,9 +38,7 @@ namespace Avalonia.Input
                 _pointers[e.RawPointerId] = pointer = new Pointer(Pointer.GetNextFreeId(),
                 _pointers[e.RawPointerId] = pointer = new Pointer(Pointer.GetNextFreeId(),
                     PointerType.Pen, _pointers.Count == 0);
                     PointerType.Pen, _pointers.Count == 0);
             }
             }
-
-            _lastPositions[e.RawPointerId] = e.Root.PointToScreen(e.Position);
-
+            
             var props = new PointerPointProperties(e.InputModifiers, e.Type.ToUpdateKind(),
             var props = new PointerPointProperties(e.InputModifiers, e.Type.ToUpdateKind(),
                 e.Point.Twist, e.Point.Pressure, e.Point.XTilt, e.Point.YTilt);
                 e.Point.Twist, e.Point.Pressure, e.Point.XTilt, e.Point.YTilt);
             var keyModifiers = e.InputModifiers.ToKeyModifiers();
             var keyModifiers = e.InputModifiers.ToKeyModifiers();
@@ -69,7 +64,6 @@ namespace Avalonia.Input
             {
             {
                 pointer.Dispose();
                 pointer.Dispose();
                 _pointers.Remove(e.RawPointerId);
                 _pointers.Remove(e.RawPointerId);
-                _lastPositions.Remove(e.RawPointerId);
             }
             }
         }
         }
 
 
@@ -153,17 +147,6 @@ namespace Avalonia.Input
                 p.Dispose();
                 p.Dispose();
         }
         }
 
 
-        [Obsolete]
-        IInputElement? IPointerDevice.Captured => _pointers.Values
-            .FirstOrDefault(p => p.IsPrimary)?.Captured;
-
-        [Obsolete]
-        void IPointerDevice.Capture(IInputElement? control) => _pointers.Values
-            .FirstOrDefault(p => p.IsPrimary)?.Capture(control);
-
-        [Obsolete]
-        Point IPointerDevice.GetPosition(IVisual relativeTo) => new Point(-1, -1);
-
         public IPointer? TryGetPointer(RawPointerEventArgs ev)
         public IPointer? TryGetPointer(RawPointerEventArgs ev)
         {
         {
             return _pointers.TryGetValue(ev.RawPointerId, out var pointer)
             return _pointers.TryGetValue(ev.RawPointerId, out var pointer)

+ 3 - 59
src/Avalonia.Base/Input/PointerEventArgs.cs

@@ -11,7 +11,7 @@ namespace Avalonia.Input
         private readonly IVisual? _rootVisual;
         private readonly IVisual? _rootVisual;
         private readonly Point _rootVisualPosition;
         private readonly Point _rootVisualPosition;
         private readonly PointerPointProperties _properties;
         private readonly PointerPointProperties _properties;
-        private Lazy<IReadOnlyList<RawPointerPoint>?>? _previousPoints;
+        private readonly Lazy<IReadOnlyList<RawPointerPoint>?>? _previousPoints;
 
 
         public PointerEventArgs(RoutedEvent routedEvent,
         public PointerEventArgs(RoutedEvent routedEvent,
             IInteractive? source,
             IInteractive? source,
@@ -43,29 +43,6 @@ namespace Avalonia.Input
         {
         {
             _previousPoints = previousPoints;
             _previousPoints = previousPoints;
         }
         }
-        
-
-        class EmulatedDevice : IPointerDevice
-        {
-            private readonly PointerEventArgs _ev;
-
-            public EmulatedDevice(PointerEventArgs ev)
-            {
-                _ev = ev;
-            }
-            
-            public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException();
-
-            public IInputElement? Captured => _ev.Pointer.Captured;
-            public void Capture(IInputElement? control)
-            {
-                _ev.Pointer.Capture(control);
-            }
-
-            public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo);
-
-            public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer;
-        }
 
 
         /// <summary>
         /// <summary>
         /// Gets specific pointer generated by input device.
         /// Gets specific pointer generated by input device.
@@ -77,28 +54,6 @@ namespace Avalonia.Input
         /// </summary>
         /// </summary>
         public ulong Timestamp { get; }
         public ulong Timestamp { get; }
 
 
-        private IPointerDevice? _device;
-
-        [Obsolete("Use Pointer to get pointer-specific information")]
-        public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this));
-
-        [Obsolete("Use KeyModifiers and PointerPointProperties")]
-        public InputModifiers InputModifiers 
-        {
-            get
-            {
-                var mods = (InputModifiers)KeyModifiers;
-                if (_properties.IsLeftButtonPressed)
-                    mods |= InputModifiers.LeftMouseButton;
-                if (_properties.IsMiddleButtonPressed)
-                    mods |= InputModifiers.MiddleMouseButton;
-                if (_properties.IsRightButtonPressed)
-                    mods |= InputModifiers.RightMouseButton;
-                
-                return mods;
-            }
-        }
-
         /// <summary>
         /// <summary>
         /// Gets a value that indicates which key modifiers were active at the time that the pointer event was initiated.
         /// Gets a value that indicates which key modifiers were active at the time that the pointer event was initiated.
         /// </summary>
         /// </summary>
@@ -120,9 +75,6 @@ namespace Avalonia.Input
         /// <returns>The pointer position in the control's coordinates.</returns>
         /// <returns>The pointer position in the control's coordinates.</returns>
         public Point GetPosition(IVisual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo);
         public Point GetPosition(IVisual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo);
 
 
-        [Obsolete("Use GetCurrentPoint")]
-        public PointerPoint GetPointerPoint(IVisual? relativeTo) => GetCurrentPoint(relativeTo);
-        
         /// <summary>
         /// <summary>
         /// Returns the PointerPoint associated with the current event
         /// Returns the PointerPoint associated with the current event
         /// </summary>
         /// </summary>
@@ -171,8 +123,6 @@ namespace Avalonia.Input
 
 
     public class PointerPressedEventArgs : PointerEventArgs
     public class PointerPressedEventArgs : PointerEventArgs
     {
     {
-        private readonly int _clickCount;
-
         public PointerPressedEventArgs(
         public PointerPressedEventArgs(
             IInteractive source,
             IInteractive source,
             IPointer pointer,
             IPointer pointer,
@@ -184,13 +134,10 @@ namespace Avalonia.Input
             : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition,
             : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition,
                 timestamp, properties, modifiers)
                 timestamp, properties, modifiers)
         {
         {
-            _clickCount = clickCount;
+            ClickCount = clickCount;
         }
         }
 
 
-        public int ClickCount => _clickCount;
-
-        [Obsolete("Use PointerPressedEventArgs.GetCurrentPoint(this).Properties")]
-        public MouseButton MouseButton => Properties.PointerUpdateKind.GetMouseButton();
+        public int ClickCount { get; }
     }
     }
 
 
     public class PointerReleasedEventArgs : PointerEventArgs
     public class PointerReleasedEventArgs : PointerEventArgs
@@ -210,9 +157,6 @@ namespace Avalonia.Input
         /// Gets the mouse button that triggered the corresponding PointerPressed event
         /// Gets the mouse button that triggered the corresponding PointerPressed event
         /// </summary>
         /// </summary>
         public MouseButton InitialPressMouseButton { get; }
         public MouseButton InitialPressMouseButton { get; }
-
-        [Obsolete("Use InitialPressMouseButton")]
-        public MouseButton MouseButton => InitialPressMouseButton;
     }
     }
 
 
     public class PointerCaptureLostEventArgs : RoutedEventArgs
     public class PointerCaptureLostEventArgs : RoutedEventArgs

+ 2 - 0
src/Avalonia.Base/Input/PointerOverPreProcessor.cs

@@ -15,6 +15,8 @@ namespace Avalonia.Input
             _inputRoot = inputRoot ?? throw new ArgumentNullException(nameof(inputRoot));
             _inputRoot = inputRoot ?? throw new ArgumentNullException(nameof(inputRoot));
         }
         }
 
 
+        public PixelPoint? LastPosition => _lastPointer?.position;
+        
         public void OnCompleted()
         public void OnCompleted()
         {
         {
             ClearPointerOver();
             ClearPointerOver();

+ 0 - 5
src/Avalonia.Base/Input/Raw/RawDragEvent.cs

@@ -8,8 +8,6 @@ namespace Avalonia.Input.Raw
         public IDataObject Data { get; }
         public IDataObject Data { get; }
         public DragDropEffects Effects { get; set; }
         public DragDropEffects Effects { get; set; }
         public RawDragEventType Type { get; }
         public RawDragEventType Type { get; }
-        [Obsolete("Use KeyModifiers")]
-        public InputModifiers Modifiers { get; }
         public KeyModifiers KeyModifiers { get; }
         public KeyModifiers KeyModifiers { get; }
 
 
         public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, 
         public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, 
@@ -21,9 +19,6 @@ namespace Avalonia.Input.Raw
             Data = data;
             Data = data;
             Effects = effects;
             Effects = effects;
             KeyModifiers = modifiers.ToKeyModifiers();
             KeyModifiers = modifiers.ToKeyModifiers();
-#pragma warning disable CS0618 // Type or member is obsolete
-            Modifiers = (InputModifiers)modifiers;
-#pragma warning restore CS0618 // Type or member is obsolete
         }
         }
     }
     }
 }
 }

+ 0 - 3
src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs

@@ -19,8 +19,5 @@ namespace Avalonia.Input.Raw
         {
         {
             RawPointerId = rawPointerId;
             RawPointerId = rawPointerId;
         }
         }
-
-        [Obsolete("Use RawPointerId")]
-        public long TouchPointId { get => RawPointerId; set => RawPointerId = value; }
     }
     }
 }
 }

+ 0 - 11
src/Avalonia.Base/Input/TouchDevice.cs

@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using Avalonia.Input.Raw;
 using Avalonia.Input.Raw;
 using Avalonia.Platform;
 using Avalonia.Platform;
-using Avalonia.VisualTree;
 
 
 namespace Avalonia.Input
 namespace Avalonia.Input
 {
 {
@@ -20,9 +19,6 @@ namespace Avalonia.Input
         private int _clickCount;
         private int _clickCount;
         private Rect _lastClickRect;
         private Rect _lastClickRect;
         private ulong _lastClickTime;
         private ulong _lastClickTime;
-        private Pointer? _lastPointer;
-
-        IInputElement? IPointerDevice.Captured => _lastPointer?.Captured;
 
 
         RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown)
         RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown)
         {
         {
@@ -32,10 +28,6 @@ namespace Avalonia.Input
             return rv;
             return rv;
         }
         }
 
 
-        void IPointerDevice.Capture(IInputElement? control) => _lastPointer?.Capture(control);
-
-        Point IPointerDevice.GetPosition(IVisual relativeTo) => default;
-
         public void ProcessRawEvent(RawInputEventArgs ev)
         public void ProcessRawEvent(RawInputEventArgs ev)
         {
         {
             if (ev.Handled || _disposed)
             if (ev.Handled || _disposed)
@@ -51,7 +43,6 @@ namespace Avalonia.Input
                     PointerType.Touch, _pointers.Count == 0);
                     PointerType.Touch, _pointers.Count == 0);
                 pointer.Capture(hit);
                 pointer.Capture(hit);
             }
             }
-            _lastPointer = pointer;
 
 
             var target = pointer.Captured ?? args.Root;
             var target = pointer.Captured ?? args.Root;
             var updateKind = args.Type.ToUpdateKind();
             var updateKind = args.Type.ToUpdateKind();
@@ -96,7 +87,6 @@ namespace Avalonia.Input
                         new PointerPointProperties(GetModifiers(args.InputModifiers, false), updateKind),
                         new PointerPointProperties(GetModifiers(args.InputModifiers, false), updateKind),
                         keyModifier, MouseButton.Left));
                         keyModifier, MouseButton.Left));
                 }
                 }
-                _lastPointer = null;
             }
             }
 
 
             if (args.Type == RawPointerEventType.TouchCancel)
             if (args.Type == RawPointerEventType.TouchCancel)
@@ -104,7 +94,6 @@ namespace Avalonia.Input
                 _pointers.Remove(args.RawPointerId);
                 _pointers.Remove(args.RawPointerId);
                 using (pointer)
                 using (pointer)
                     pointer.Capture(null);
                     pointer.Capture(null);
-                _lastPointer = null;
             }
             }
 
 
             if (args.Type == RawPointerEventType.TouchUpdate)
             if (args.Type == RawPointerEventType.TouchUpdate)

+ 4 - 4
src/Avalonia.Base/Layout/LayoutManager.cs

@@ -350,7 +350,7 @@ namespace Avalonia.Layout
                 {
                 {
                     for (var i = 0; i < count; ++i)
                     for (var i = 0; i < count; ++i)
                     {
                     {
-                        var l = _effectiveViewportChangedListeners[i];
+                        var l = listeners[i];
 
 
                         if (!l.Listener.IsAttachedToVisualTree)
                         if (!l.Listener.IsAttachedToVisualTree)
                         {
                         {
@@ -362,7 +362,7 @@ namespace Avalonia.Layout
                         if (viewport != l.Viewport)
                         if (viewport != l.Viewport)
                         {
                         {
                             l.Listener.EffectiveViewportChanged(new EffectiveViewportChangedEventArgs(viewport));
                             l.Listener.EffectiveViewportChanged(new EffectiveViewportChangedEventArgs(viewport));
-                            _effectiveViewportChangedListeners[i] = new EffectiveViewportChangedListener(l.Listener, viewport);
+                            l.Viewport = viewport;
                         }
                         }
                     }
                     }
                 }
                 }
@@ -414,7 +414,7 @@ namespace Avalonia.Layout
             }
             }
         }
         }
 
 
-        private readonly struct EffectiveViewportChangedListener
+        private class EffectiveViewportChangedListener
         {
         {
             public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport)
             public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport)
             {
             {
@@ -423,7 +423,7 @@ namespace Avalonia.Layout
             }
             }
 
 
             public ILayoutable Listener { get; }
             public ILayoutable Listener { get; }
-            public Rect Viewport { get; }
+            public Rect Viewport { get; set; }
         }
         }
     }
     }
 }
 }

+ 2 - 2
src/Markup/Avalonia.Markup.Xaml/IAddChild.cs → src/Avalonia.Base/Metadata/IAddChild.cs

@@ -1,11 +1,11 @@
-namespace Avalonia.Markup.Xaml
+namespace Avalonia.Metadata
 {
 {
     public interface IAddChild
     public interface IAddChild
     {
     {
         void AddChild(object child);
         void AddChild(object child);
     }
     }
 
 
-    public interface IAddChild<T> : IAddChild
+    public interface IAddChild<T>
     {
     {
         void AddChild(T child);
         void AddChild(T child);
     }
     }

+ 16 - 0
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();
+}

+ 82 - 0
src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs

@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations;
+
+
+/// <summary>
+/// The base class for both key-frame and expression animation instances
+/// Is responsible for activation tracking and for subscribing to properties used in dependencies
+/// </summary>
+internal abstract class AnimationInstanceBase : IAnimationInstance
+{
+    private List<(ServerObject obj, CompositionProperty member)>? _trackedObjects;
+    protected PropertySetSnapshot Parameters { get; }
+    public ServerObject TargetObject { get; }
+    protected CompositionProperty Property { get; private set; } = null!;
+    private bool _invalidated;
+
+    public AnimationInstanceBase(ServerObject target, PropertySetSnapshot parameters)
+    {
+        Parameters = parameters;
+        TargetObject = target;
+    }
+
+    protected void Initialize(CompositionProperty property, 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.GetCompositionProperty(t.member);
+                    if (off == null)
+#if DEBUG
+                        throw new InvalidCastException("Attempting to subscribe to unknown field");
+#else
+                        continue;
+#endif
+                    _trackedObjects.Add((tracked, off));
+                }
+            }
+        }
+
+        Property = property;
+    }
+
+    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)
+    {
+        _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(Property);
+    }
+}

+ 75 - 0
src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs

@@ -0,0 +1,75 @@
+// 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
+{
+    /// <summary>
+    /// This is the base class for ExpressionAnimation and KeyFrameAnimation.
+    /// </summary>
+    /// <remarks>
+    /// Use the <see cref="CompositionObject.StartAnimation"/> method to start the animation.
+    /// Value parameters (as opposed to reference parameters which are set using <see cref="SetReferenceParameter"/>)
+    /// are copied and "embedded" into an expression at the time CompositionObject.StartAnimation is called.
+    /// Changing the value of the variable after <see cref="CompositionObject.StartAnimation"/> is called will not affect
+    /// the value of the ExpressionAnimation.
+    /// See the remarks section of ExpressionAnimation for additional information.
+    /// </remarks>
+    public abstract class CompositionAnimation : CompositionObject,  ICompositionAnimationBase
+    {
+        private readonly CompositionPropertySet _propertySet;
+        internal CompositionAnimation(Compositor compositor) : base(compositor, null!)
+        {
+            _propertySet = new CompositionPropertySet(compositor);
+        }
+        
+        /// <summary>
+        /// Clears all of the parameters of the animation.
+        /// </summary>
+        public void ClearAllParameters() => _propertySet.ClearAll();
+
+        /// <summary>
+        /// Clears a parameter from the animation.
+        /// </summary>
+        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()
+        {
+            
+        }
+    }
+}

+ 24 - 0
src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs

@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Transport;
+
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+    public class CompositionAnimationGroup : CompositionObject, ICompositionAnimationBase
+    {
+        internal List<CompositionAnimation> Animations { get; } = new List<CompositionAnimation>();
+        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!)
+        {
+        }
+    }
+}

+ 53 - 0
src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs

@@ -0,0 +1,53 @@
+// ReSharper disable CheckNamespace
+using System;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+    /// <summary>
+    /// A Composition Animation that uses a mathematical equation to calculate the value for an animating property every frame.
+    /// </summary>
+    /// <remarks>
+    /// 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 <see cref="KeyFrameAnimation"/>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 <see cref="CompositionObject.StartAnimation"/> method to start the animation.
+    /// </remarks>
+    public class ExpressionAnimation : CompositionAnimation
+    {
+        private string? _expression;
+        private Expression? _parsedExpression;
+        
+        internal ExpressionAnimation(Compositor compositor) : base(compositor)
+        {
+        }
+
+        /// <summary>
+        /// The mathematical equation specifying how the animated value is calculated each frame.
+        /// The Expression is the core of an <see cref="ExpressionAnimation"/> 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.
+        /// </summary>
+        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());
+    }
+}

+ 49 - 0
src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs

@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+    
+    /// <summary>
+    /// Server-side counterpart of <see cref="ExpressionAnimation"/> with values baked-in.
+    /// </summary>
+    internal class ExpressionAnimationInstance : AnimationInstanceBase, IAnimationInstance
+    {
+        private readonly Expression _expression;
+        private ExpressionVariant _startingValue;
+        private readonly ExpressionVariant? _finalValue;
+
+        protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue)
+        {
+            var ctx = new ExpressionEvaluationContext
+            {
+                Parameters = Parameters,
+                Target = TargetObject,
+                ForeignFunctionInterface = BuiltInExpressionFfi.Instance,
+                StartingValue = _startingValue,
+                FinalValue = _finalValue ?? _startingValue,
+                CurrentValue = currentValue
+            };
+            return _expression.Evaluate(ref ctx);
+        }
+
+        public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property)
+        {
+            _startingValue = startingValue;
+            var hs = new HashSet<(string, string)>();
+            _expression.CollectReferences(hs);
+            base.Initialize(property, hs);
+        }
+        
+        public ExpressionAnimationInstance(Expression expression,
+            ServerObject target,
+            ExpressionVariant? finalValue,
+            PropertySetSnapshot parameters) : base(target, parameters)
+        {
+            _expression = expression;
+            _finalValue = finalValue;
+        }
+    }
+}

+ 16 - 0
src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs

@@ -0,0 +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 Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property);
+        void Activate();
+        void Deactivate();
+        void Invalidate();
+    }
+}

+ 15 - 0
src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs

@@ -0,0 +1,15 @@
+// ReSharper disable CheckNamespace
+
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+    /// <summary>
+    /// Base class for composition animations.
+    /// </summary>
+    public interface ICompositionAnimationBase
+    {
+        internal void InternalOnly();
+    }
+
+}

+ 82 - 0
src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs

@@ -0,0 +1,82 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Rendering.Composition.Transport;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+    /// <summary>
+    /// A collection of animations triggered when a condition is met.
+    /// </summary>
+    /// <remarks>
+    /// 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 
+    /// </remarks>
+    public class ImplicitAnimationCollection : CompositionObject, IDictionary<string, ICompositionAnimationBase>
+    {
+        private Dictionary<string, ICompositionAnimationBase> _inner = new Dictionary<string, ICompositionAnimationBase>();
+        private IDictionary<string, ICompositionAnimationBase> _innerface;
+        internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null!)
+        {
+            _innerface = _inner;
+        }
+
+        public IEnumerator<KeyValuePair<string, ICompositionAnimationBase>> GetEnumerator() => _inner.GetEnumerator();
+
+        IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _inner).GetEnumerator();
+
+        void ICollection<KeyValuePair<string, ICompositionAnimationBase>>.Add(KeyValuePair<string, ICompositionAnimationBase> item) => _innerface.Add(item);
+
+        public void Clear() => _inner.Clear();
+
+        bool ICollection<KeyValuePair<string, ICompositionAnimationBase>>.Contains(KeyValuePair<string, ICompositionAnimationBase> item) => _innerface.Contains(item);
+
+        void ICollection<KeyValuePair<string, ICompositionAnimationBase>>.CopyTo(KeyValuePair<string, ICompositionAnimationBase>[] array, int arrayIndex) => _innerface.CopyTo(array, arrayIndex);
+
+        bool ICollection<KeyValuePair<string, ICompositionAnimationBase>>.Remove(KeyValuePair<string, ICompositionAnimationBase> item) => _innerface.Remove(item);
+
+        public int Count => _inner.Count;
+
+        bool ICollection<KeyValuePair<string, ICompositionAnimationBase>>.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<string> IDictionary<string, ICompositionAnimationBase>.Keys => _innerface.Keys;
+
+        ICollection<ICompositionAnimationBase> IDictionary<string, ICompositionAnimationBase>.Values =>
+            _innerface.Values;
+        
+        // UWP compat
+        public uint Size => (uint) Count;
+
+        public IReadOnlyDictionary<string, ICompositionAnimationBase> GetView() =>
+            new Dictionary<string, ICompositionAnimationBase>(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;
+        }
+    }
+}

+ 76 - 0
src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs

@@ -0,0 +1,76 @@
+using System;
+using System.Numerics;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+    /// <summary>
+    ///  An interface to define interpolation logic for a particular type
+    /// </summary>
+    internal interface IInterpolator<T>
+    {
+        T Interpolate(T from, T to, float progress);
+    }
+
+    class ScalarInterpolator : IInterpolator<float>
+    {
+        public float Interpolate(float @from, float to, float progress) => @from + (to - @from) * progress;
+        
+        public static ScalarInterpolator Instance { get; } = new ScalarInterpolator();
+    }
+
+    class Vector2Interpolator : IInterpolator<Vector2>
+    {
+        public Vector2 Interpolate(Vector2 @from, Vector2 to, float progress) 
+            => Vector2.Lerp(@from, to, progress);
+        
+        public static Vector2Interpolator Instance { get; } = new Vector2Interpolator();
+    }
+    
+    class Vector3Interpolator : IInterpolator<Vector3>
+    {
+        public Vector3 Interpolate(Vector3 @from, Vector3 to, float progress) 
+            => Vector3.Lerp(@from, to, progress);
+        
+        public static Vector3Interpolator Instance { get; } = new Vector3Interpolator();
+    }
+    
+    class Vector4Interpolator : IInterpolator<Vector4>
+    {
+        public Vector4 Interpolate(Vector4 @from, Vector4 to, float progress) 
+            => Vector4.Lerp(@from, to, progress);
+        
+        public static Vector4Interpolator Instance { get; } = new Vector4Interpolator();
+    }
+    
+    class QuaternionInterpolator : IInterpolator<Quaternion>
+    {
+        public Quaternion Interpolate(Quaternion @from, Quaternion to, float progress) 
+            => Quaternion.Lerp(@from, to, progress);
+
+        public static QuaternionInterpolator Instance { get; } = new QuaternionInterpolator();
+    }
+    
+    class ColorInterpolator : IInterpolator<Avalonia.Media.Color>
+    {
+        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<bool>
+    {
+        public bool Interpolate(bool @from, bool to, float progress) => progress >= 1 ? to : @from;
+        
+        public static BooleanInterpolator Instance { get; } = new BooleanInterpolator();
+    }
+}

+ 134 - 0
src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs

@@ -0,0 +1,134 @@
+using System;
+using Avalonia.Animation;
+using Avalonia.Animation.Easings;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+    
+    /// <summary>
+    /// 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.
+    /// </summary>
+    public abstract class KeyFrameAnimation : CompositionAnimation
+    {
+        private TimeSpan _duration = TimeSpan.FromMilliseconds(1);
+
+        internal KeyFrameAnimation(Compositor compositor) : base(compositor)
+        {
+        }
+        
+        /// <summary>
+        /// The delay behavior of the key frame animation.
+        /// </summary>
+        public AnimationDelayBehavior DelayBehavior { get; set; }
+        
+        /// <summary>
+        /// Delay before the animation starts after <see cref="CompositionObject.StartAnimation"/> is called.
+        /// </summary>
+        public System.TimeSpan DelayTime { get; set; }
+        
+        /// <summary>
+        /// 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 <see cref="IterationCount"/> greater than one.
+        /// This gives an easy way for customizing animation definitions.
+        /// </summary>
+        public PlaybackDirection Direction { get; set; }
+
+        /// <summary>
+        /// The duration of the animation.
+        /// Minimum allowed value is 1ms and maximum allowed value is 24 days.
+        /// </summary>
+        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;
+            }
+        }
+        
+        /// <summary>
+        /// The iteration behavior for the key frame animation.
+        /// </summary>
+        public AnimationIterationBehavior IterationBehavior { get; set; }
+        
+        /// <summary>
+        /// The number of times to repeat the key frame animation.
+        /// </summary>
+        public int IterationCount { get; set; } = 1;
+        
+        /// <summary>
+        /// Specifies how to set the property value when animation is stopped
+        /// </summary>
+        public AnimationStopBehavior StopBehavior { get; set; }
+        
+        private protected abstract IKeyFrames KeyFrames { get; }
+        
+        /// <summary>
+        /// Inserts an expression keyframe.
+        /// </summary>
+        /// <param name="normalizedProgressKey">
+        /// 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.
+        /// </param>
+        /// <param name="value">The expression used to calculate the value of the key frame.</param>
+        /// <param name="easingFunction">The easing function to use when interpolating between frames.</param>
+        public void InsertExpressionKeyFrame(float normalizedProgressKey, string value,
+            Easing? easingFunction = null) =>
+            KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, easingFunction ?? Compositor.DefaultEasing);
+    }
+
+    /// <summary>
+    /// Specifies the animation delay behavior.
+    /// </summary>
+    public enum AnimationDelayBehavior
+    {
+        /// <summary>
+        /// 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.
+        /// </summary>
+        SetInitialValueAfterDelay,
+        /// <summary>
+        /// 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.
+        /// </summary>
+        SetInitialValueBeforeDelay
+    }
+
+    /// <summary>
+    /// Specifies if the animation should loop.
+    /// </summary>
+    public enum AnimationIterationBehavior
+    {
+        /// <summary>
+        /// The animation should loop the specified number of times.
+        /// </summary>
+        Count,
+        /// <summary>
+        /// The animation should loop forever.
+        /// </summary>
+        Forever
+    }
+
+    /// <summary>
+    /// Specifies the behavior of an animation when it stops.
+    /// </summary>
+    public enum AnimationStopBehavior
+    {
+        /// <summary>
+        /// Leave the animation at its current value.
+        /// </summary>
+        LeaveCurrentValue,
+        /// <summary>
+        /// Reset the animation to its initial value.
+        /// </summary>
+        SetToInitialValue,
+        /// <summary>
+        /// Set the animation to its final value.
+        /// </summary>
+        SetToFinalValue
+    }
+}

+ 178 - 0
src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs

@@ -0,0 +1,178 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Animation;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+    /// <summary>
+    /// Server-side counterpart of KeyFrameAnimation with values baked-in
+    /// </summary>
+    class KeyFrameAnimationInstance<T> : AnimationInstanceBase, IAnimationInstance where T : struct
+    {
+        private readonly IInterpolator<T> _interpolator;
+        private readonly ServerKeyFrame<T>[] _keyFrames;
+        private readonly ExpressionVariant? _finalValue;
+        private readonly AnimationDelayBehavior _delayBehavior;
+        private readonly TimeSpan _delayTime;
+        private readonly PlaybackDirection _direction;
+        private readonly TimeSpan _duration;
+        private readonly AnimationIterationBehavior _iterationBehavior;
+        private readonly int _iterationCount;
+        private readonly AnimationStopBehavior _stopBehavior;
+        private TimeSpan _startedAt;
+        private T _startingValue;
+        private readonly TimeSpan _totalDuration;
+        private bool _finished;
+
+        public KeyFrameAnimationInstance(
+            IInterpolator<T> interpolator, ServerKeyFrame<T>[] keyFrames,
+            PropertySetSnapshot snapshot, ExpressionVariant? finalValue,
+            ServerObject target,
+            AnimationDelayBehavior delayBehavior, TimeSpan delayTime,
+            PlaybackDirection direction, TimeSpan duration,
+            AnimationIterationBehavior iterationBehavior,
+            int iterationCount, AnimationStopBehavior stopBehavior) : base(target, snapshot)
+        {
+            _interpolator = interpolator;
+            _keyFrames = keyFrames;
+            _finalValue = finalValue;
+            _delayBehavior = delayBehavior;
+            _delayTime = delayTime;
+            _direction = direction;
+            _duration = duration;
+            _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");
+        }
+
+
+        protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue)
+        {
+            var starting = ExpressionVariant.Create(_startingValue);
+            var ctx = new ExpressionEvaluationContext
+            {
+                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)
+                    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 == PlaybackDirection.Alternate
+                    ? !evenIterationNumber
+                    : _direction == PlaybackDirection.AlternateReverse
+                        ? evenIterationNumber
+                        : _direction == PlaybackDirection.Reverse;
+
+            var iterationProgress = elapsed.TotalSeconds / _duration.TotalSeconds;
+            if (reverse)
+                iterationProgress = 1 - iterationProgress;
+
+            var left = new ServerKeyFrame<T>
+            {
+                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 = (float)right.EasingFunction.Ease(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<T> f)
+        {
+            if (f.Expression != null)
+                return f.Expression.Evaluate(ref ctx).CastOrDefault<T>();
+            else
+                return f.Value;
+        }
+
+        public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property)
+        {
+            _startedAt = startedAt;
+            _startingValue = startingValue.CastOrDefault<T>();
+            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(property, hs);
+        }
+
+        public override void Activate()
+        {
+            TargetObject.Compositor.AddToClock(this);
+            base.Activate();
+        }
+
+        public override void Deactivate()
+        {
+            TargetObject.Compositor.RemoveFromClock(this);
+            base.Deactivate();
+        }
+    }
+}

+ 89 - 0
src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs

@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Animation.Easings;
+using Avalonia.Rendering.Composition.Expressions;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+    
+    /// <summary>
+    /// Collection of composition animation key frames
+    /// </summary>
+    /// <typeparam name="T"></typeparam>
+    class KeyFrames<T> : List<KeyFrame<T>>, 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, IEasing easingFunction)
+        {
+            Validate(normalizedProgressKey);
+            Add(new KeyFrame<T>
+            {
+                NormalizedProgressKey = normalizedProgressKey,
+                Expression = Expression.Parse(value),
+                EasingFunction = easingFunction
+            });
+        }
+
+        public void Insert(float normalizedProgressKey, T value, IEasing easingFunction)
+        {
+            Validate(normalizedProgressKey);
+            Add(new KeyFrame<T>
+            {
+                NormalizedProgressKey = normalizedProgressKey,
+                Value = value,
+                EasingFunction = easingFunction
+            });
+        }
+
+        public ServerKeyFrame<T>[] Snapshot()
+        {
+            var frames = new ServerKeyFrame<T>[Count];
+            for (var c = 0; c < Count; c++)
+            {
+                var f = this[c];
+                frames[c] = new ServerKeyFrame<T>
+                {
+                    Expression = f.Expression,
+                    Value = f.Value,
+                    EasingFunction = f.EasingFunction,
+                    Key = f.NormalizedProgressKey
+                };
+            }
+            return frames;
+        }
+    }
+
+    /// <summary>
+    /// Composition animation key frame
+    /// </summary>
+    struct KeyFrame<T>
+    {
+        public float NormalizedProgressKey;
+        public T Value;
+        public Expression Expression;
+        public IEasing EasingFunction;
+    }
+    
+    /// <summary>
+    /// Server-side composition animation key frame
+    /// </summary>
+    struct ServerKeyFrame<T>
+    {
+        public T Value;
+        public Expression? Expression;
+        public IEasing EasingFunction;
+        public float Key;
+    }
+    
+    interface IKeyFrames
+    {
+        public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, IEasing easingFunction);
+    }
+}

+ 49 - 0
src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs

@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Expressions;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+    /// <summary>
+    /// A snapshot of properties used by an animation
+    /// </summary>
+    internal class PropertySetSnapshot : IExpressionParameterCollection, IExpressionObject
+    {
+        private readonly Dictionary<string, Value> _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<string, Value> 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);
+    }
+}

+ 278 - 0
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@@ -0,0 +1,278 @@
+using System;
+using System.Collections.Generic;
+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;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.Composition;
+
+/// <summary>
+/// A renderer that utilizes <see cref="Avalonia.Rendering.Composition.Compositor"/> to render the visual tree 
+/// </summary>
+public class CompositingRenderer : IRendererWithCompositor
+{
+    private readonly IRenderRoot _root;
+    private readonly Compositor _compositor;
+    CompositionDrawingContext _recorder = new();
+    DrawingContext _recordingContext;
+    private HashSet<Visual> _dirty = new();
+    private HashSet<Visual> _recalculateChildren = new();
+    private bool _queuedUpdate;
+    private Action _update;
+    private Action _invalidateScene;
+
+    internal CompositionTarget CompositionTarget;
+    
+    /// <summary>
+    /// Asks the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered.
+    /// </summary>
+    public bool RenderOnlyOnRenderThread { get; set; } = true;
+
+    public CompositingRenderer(IRenderRoot root,
+        Compositor compositor)
+    {
+        _root = root;
+        _compositor = compositor;
+        _recordingContext = new DrawingContext(_recorder);
+        CompositionTarget = compositor.CreateCompositionTarget(root.CreateRenderTarget);
+        CompositionTarget.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor);
+        _update = Update;
+        _invalidateScene = InvalidateScene;
+    }
+
+    /// <inheritdoc/>
+    public bool DrawFps
+    {
+        get => CompositionTarget.DrawFps;
+        set => CompositionTarget.DrawFps = value;
+    }
+    
+    /// <inheritdoc/>
+    public bool DrawDirtyRects
+    {
+        get => CompositionTarget.DrawDirtyRects;
+        set => CompositionTarget.DrawDirtyRects = value;
+    }
+
+    /// <inheritdoc/>
+    public event EventHandler<SceneInvalidatedEventArgs>? SceneInvalidated;
+
+    void QueueUpdate()
+    {
+        if(_queuedUpdate)
+            return;
+        _queuedUpdate = true;
+        Dispatcher.UIThread.Post(_update, DispatcherPriority.Composition);
+    }
+    
+    /// <inheritdoc/>
+    public void AddDirty(IVisual visual)
+    {
+        _dirty.Add((Visual)visual);
+        QueueUpdate();
+    }
+
+    /// <inheritdoc/>
+    public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool>? filter)
+    {
+        var res = CompositionTarget.TryHitTest(p, filter);
+        if(res == null)
+            yield break;
+        foreach(var v in res)
+        {
+            if (v is CompositionDrawListVisual dv)
+            {
+                if (filter == null || filter(dv.Visual))
+                    yield return dv.Visual;
+            }
+        }
+    }
+
+    /// <inheritdoc/>
+    public IVisual? HitTestFirst(Point p, IVisual root, Func<IVisual, bool>? filter)
+    {
+        // TODO: Optimize
+        return HitTest(p, root, filter).FirstOrDefault();
+    }
+
+    /// <inheritdoc/>
+    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<IVisual>)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;
+            if (v.HasNonUniformZIndexChildren)
+            {
+                
+                
+            }
+
+            if (sortedChildren != null)
+                for (var c = 0; c < visualChildren.Count; c++)
+                {
+                    if (!ReferenceEquals(compositionChildren[c], ((Visual)sortedChildren[c].visual).CompositionVisual))
+                    {
+                        mismatch = true;
+                        break;
+                    }
+                }
+            else
+                for (var c = 0; c < visualChildren.Count; c++)
+                    if (!ReferenceEquals(compositionChildren[c], ((Visual)visualChildren[c]).CompositionVisual))
+                    {
+                        mismatch = true;
+                        break;
+                    }
+
+
+            if (!mismatch)
+            {
+                sortedChildren?.Dispose();
+                return;
+            }
+        }
+        
+        compositionChildren.Clear();
+        if (sortedChildren != null)
+        {
+            foreach (var ch in sortedChildren)
+            {
+                var compositionChild = ((Visual)ch.visual).CompositionVisual;
+                if (compositionChild != null)
+                    compositionChildren.Add(compositionChild);
+            }
+            sortedChildren.Dispose();
+        }
+        else
+            foreach (var ch in v.GetVisualChildren())
+            {
+                var compositionChild = ((Visual)ch).CompositionVisual;
+                if (compositionChild != null)
+                    compositionChildren.Add(compositionChild);
+            }
+    }
+
+    private void InvalidateScene() =>
+        SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize)));
+
+    private void Update()
+    {
+        _queuedUpdate = false;
+        foreach (var visual in _dirty)
+        {
+            var comp = visual.CompositionVisual;
+            if(comp == null)
+                continue;
+            
+            // TODO: Optimize all of that by moving to the Visual itself, so we won't have to recalculate every time
+            comp.Offset = new Vector3((float)visual.Bounds.Left, (float)visual.Bounds.Top, 0);
+            comp.Size = new Vector2((float)visual.Bounds.Width, (float)visual.Bounds.Height);
+            comp.Visible = visual.IsVisible;
+            comp.Opacity = (float)visual.Opacity;
+            comp.ClipToBounds = visual.ClipToBounds;
+            comp.Clip = visual.Clip?.PlatformImpl;
+            comp.OpacityMask = visual.OpacityMask;
+            
+            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);
+            }
+
+
+
+            comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform);
+
+            _recorder.BeginUpdate(comp.DrawList);
+            visual.Render(_recordingContext);
+            comp.DrawList = _recorder.EndUpdate();
+
+            SyncChildren(visual);
+        }
+        foreach(var v in _recalculateChildren)
+            if (!_dirty.Contains(v))
+                SyncChildren(v);
+        _dirty.Clear();
+        _recalculateChildren.Clear();
+        CompositionTarget.Size = _root.ClientSize;
+        CompositionTarget.Scaling = _root.RenderScaling;
+        Compositor.InvokeOnNextCommit(_invalidateScene);
+    }
+    
+    public void Resized(Size size)
+    {
+    }
+
+    public void Paint(Rect rect)
+    {
+        Update();
+        CompositionTarget.RequestRedraw();
+        if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground)
+            Compositor.RequestCommitAsync().Wait();
+        else
+            CompositionTarget.ImmediateUIThreadRender();
+    }
+
+    public void Start() => CompositionTarget.IsEnabled = true;
+
+    public void Stop()
+    {
+        CompositionTarget.IsEnabled = false;
+    }
+    
+    public void Dispose()
+    {
+        Stop();
+        CompositionTarget.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
+        if (Compositor.Loop.RunsInBackground)
+            _compositor.RequestCommitAsync().Wait();
+    }
+
+    /// <summary>
+    /// The associated <see cref="Avalonia.Rendering.Composition.Compositor"/> object
+    /// </summary>
+    public Compositor Compositor => _compositor;
+}

+ 75 - 0
src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs

@@ -0,0 +1,75 @@
+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;
+
+
+/// <summary>
+/// A composition visual that holds a list of drawing commands issued by <see cref="Avalonia.Visual"/>
+/// </summary>
+internal class CompositionDrawListVisual : CompositionContainerVisual
+{
+    /// <summary>
+    /// The associated <see cref="Avalonia.Visual"/>
+    /// </summary>
+    public Visual Visual { get; }
+
+    private bool _drawListChanged;
+    private CompositionDrawList? _drawList;
+    
+    /// <summary>
+    /// The list of drawing commands
+    /// </summary>
+    public CompositionDrawList? DrawList
+    {
+        get => _drawList;
+        set
+        {
+            _drawList?.Dispose();
+            _drawList = value;
+            _drawListChanged = true;
+            RegisterForSerialization();
+        }
+    }
+
+    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)
+    {
+        Visual = visual;
+    }
+
+    internal override bool HitTest(Point pt, Func<IVisual, bool>? filter)
+    {
+        var custom = Visual as ICustomHitTest;
+        if (DrawList == null && custom == null)
+            return false;
+        if (filter != null && !filter(Visual))
+            return false;
+        if (custom != null)
+        {
+            // Simulate the old behavior
+            // TODO: Change behavior once legacy renderers are removed
+            pt += new Point(Offset.X, Offset.Y);
+            return custom.HitTest(pt);
+        }
+
+        foreach (var op in DrawList!)
+            if (op.Item.HitTest(pt))
+                return true;
+        return false;
+    }
+}

+ 141 - 0
src/Avalonia.Base/Rendering/Composition/CompositionObject.cs

@@ -0,0 +1,141 @@
+using System;
+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
+{
+    /// <summary>
+    /// 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 <see cref="CompositionVisual" /> objects each representing a single node in a Visual tree.
+    /// </summary>
+    public abstract class CompositionObject : IDisposable
+    {
+        /// <summary>
+        /// The collection of implicit animations attached to this object.
+        /// </summary>
+        public ImplicitAnimationCollection? ImplicitAnimations { get; set; }
+
+        private protected InlineDictionary<CompositionProperty, IAnimationInstance> PendingAnimations;
+        internal CompositionObject(Compositor compositor, ServerObject server)
+        {
+            Compositor = compositor;
+            Server = server;
+        }
+        
+        /// <summary>
+        /// The associated Compositor
+        /// </summary>
+        public Compositor Compositor { get; }
+        internal ServerObject Server { get; }
+        public bool IsDisposed { get; private set; }
+        private bool _registeredForSerialization;
+
+        private static void ThrowInvalidOperation() =>
+            throw new InvalidOperationException("There is no server-side counterpart for this object");
+
+        public void Dispose()
+        {
+            RegisterForSerialization();
+            IsDisposed = true;
+        }
+
+        /// <summary>
+        /// Connects an animation with the specified property of the object and starts the animation.
+        /// </summary>
+        public void StartAnimation(string propertyName, CompositionAnimation animation)
+            => StartAnimation(propertyName, animation, null);
+        
+        internal virtual void StartAnimation(string propertyName, CompositionAnimation animation, ExpressionVariant? finalValue)
+        {
+            throw new ArgumentException("Unknown property " + propertyName);
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        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();
+        }
+
+        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)
+        {
+            if (Server is IDisposable)
+                writer.Write((byte)(IsDisposed ? 1 : 0));
+        }
+    }
+}

+ 147 - 0
src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs

@@ -0,0 +1,147 @@
+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
+{
+    /// <summary>
+    /// <see cref="CompositionPropertySet"/>s are <see cref="CompositionObject"/>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.
+    /// <see cref="CompositionPropertySet"/>s are most commonly used with animations, where they maintain key-value pairs
+    /// that are referenced to drive portions of composition animations. <see cref="CompositionPropertySet"/>s
+    /// provide the ability to insert key-value pairs or retrieve a value for a given key.
+    /// <see cref="CompositionPropertySet"/> does not support a delete function – ensure you use <see cref="CompositionPropertySet"/>
+    /// to store values that will be shared across the application.
+    /// </summary>
+    public class CompositionPropertySet : CompositionObject
+    {
+        private readonly Dictionary<string, ExpressionVariant> _variants = new Dictionary<string, ExpressionVariant>();
+        private readonly Dictionary<string, CompositionObject> _objects = new Dictionary<string, CompositionObject>();
+        
+        internal CompositionPropertySet(Compositor compositor) : base(compositor, null!)
+        {
+        }
+
+        internal void Set(string key, ExpressionVariant value)
+        {
+            _objects.Remove(key);
+            _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);
+
+        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<T>(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() =>
+            SnapshotCore(1);
+        
+        private PropertySetSnapshot SnapshotCore(int allowedNestingLevel)
+        {
+            var dic = new Dictionary<string, PropertySetSnapshot.Value>(_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.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(o.Value.Server);
+            }
+
+            foreach (var v in _variants)
+                dic[v.Key] = v.Value;
+            
+            return new PropertySetSnapshot(dic);
+        }
+    }
+
+    public enum CompositionGetValueStatus
+    {
+        Succeeded,
+        TypeMismatch,
+        NotFound
+    }
+}

+ 130 - 0
src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs

@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using Avalonia.Collections.Pooled;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.Composition
+{
+    /// <summary>
+    /// Represents the composition output (e. g. a window, embedded control, entire screen)
+    /// </summary>
+    public partial class CompositionTarget
+    {
+        partial void OnRootChanged()
+        {
+            if (Root != null)
+                Root.Root = this;
+        }
+
+        partial void OnRootChanging()
+        {
+            if (Root != null)
+                Root.Root = null;
+        }
+        
+        /// <summary>
+        /// Attempts to perform a hit-tst
+        /// </summary>
+        /// <param name="point"></param>
+        /// <param name="filter"></param>
+        /// <returns></returns>
+        public PooledList<CompositionVisual>? TryHitTest(Point point, Func<IVisual, bool>? filter)
+        {
+            Server.Readback.NextRead();
+            if (Root == null)
+                return null;
+            var res = new PooledList<CompositionVisual>();
+            HitTestCore(Root, point, res, filter);
+            return res;
+        }
+
+        /// <summary>
+        /// Attempts to transform a point to a particular CompositionVisual coordinate space
+        /// </summary>
+        /// <returns></returns>
+        public Point? TryTransformToVisual(CompositionVisual visual, Point point)
+        {
+            if (visual.Root != this)
+                return null;
+            var v = visual;
+            var m = Matrix.Identity;
+            while (v != null)
+            {
+                if (!TryGetInvertedTransform(v, out var cm))
+                    return null;
+                m = m * cm;
+                v = v.Parent;
+            }
+
+            return point * m;
+        }
+
+        bool TryGetInvertedTransform(CompositionVisual visual, out Matrix matrix)
+        {
+            var m = visual.TryGetServerGlobalTransform();
+            if (m == null)
+            {
+                matrix = default;
+                return false;
+            }
+
+            var m33 = MatrixUtils.ToMatrix(m.Value);
+            return m33.TryInvert(out matrix);
+        }
+
+        bool TryTransformTo(CompositionVisual visual, Point globalPoint, out Point v)
+        {
+            v = default;
+            if (TryGetInvertedTransform(visual, out var m))
+            {
+                v = globalPoint * m;
+                return true;
+            }
+
+            return false;
+        }
+        
+        void HitTestCore(CompositionVisual visual, Point globalPoint, PooledList<CompositionVisual> result,
+            Func<IVisual, bool>? filter)
+        {
+            if (visual.Visible == false)
+                return;
+            if (!TryTransformTo(visual, globalPoint, out var point))
+                return;
+
+            if (visual.ClipToBounds
+                && (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y))
+                return;
+
+            if (visual.Clip?.FillContains(point) == false)
+                return;
+            
+            // Inspect children
+            if (visual is CompositionContainerVisual cv)
+                for (var c = cv.Children.Count - 1; c >= 0; c--)
+                {
+                    var ch = cv.Children[c];
+                    HitTestCore(ch, globalPoint, result, filter);
+                }
+            
+            // Hit-test the current node
+            if (visual.HitTest(point, filter)) 
+                result.Add(visual);
+        }
+
+        /// <summary>
+        /// Registers the composition target for explicit redraw
+        /// </summary>
+        public void RequestRedraw() => RegisterForSerialization();
+
+        /// <summary>
+        /// Performs composition directly on the UI thread 
+        /// </summary>
+        internal void ImmediateUIThreadRender()
+        {
+            Compositor.RequestCommitAsync();
+            Compositor.Server.Render();
+        }
+    }
+}

+ 141 - 0
src/Avalonia.Base/Rendering/Composition/Compositor.cs

@@ -0,0 +1,141 @@
+using System;
+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;
+using Avalonia.Rendering.Composition.Server;
+using Avalonia.Rendering.Composition.Transport;
+using Avalonia.Threading;
+
+
+namespace Avalonia.Rendering.Composition
+{
+    /// <summary>
+    /// 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 
+    /// </summary>
+    public partial class Compositor
+    {
+        internal IRenderLoop Loop { get; }
+        private ServerCompositor _server;
+        private bool _implicitBatchCommitQueued;
+        private Action _implicitBatchCommit;
+        private BatchStreamObjectPool<object?> _batchObjectPool = new();
+        private BatchStreamMemoryPool _batchMemoryPool = new();
+        private List<CompositionObject> _objectsForSerialization = new();
+        internal ServerCompositor Server => _server;
+        internal IEasing DefaultEasing { get; }
+        private List<Action>? _invokeOnNextCommit;
+        private readonly Stack<List<Action>> _invokeListPool = new();
+
+        /// <summary>
+        /// Creates a new compositor on a specified render loop that would use a particular GPU
+        /// </summary>
+        /// <param name="loop"></param>
+        /// <param name="gpu"></param>
+        public Compositor(IRenderLoop loop, IPlatformGpu? gpu)
+        {
+            Loop = loop;
+            _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool);
+            _implicitBatchCommit = ImplicitBatchCommit;
+
+            DefaultEasing = new CubicBezierEasing(new Point(0.25f, 0.1f), new Point(0.25f, 1f));
+        }
+
+        /// <summary>
+        /// Creates a new CompositionTarget
+        /// </summary>
+        /// <param name="renderTargetFactory">A factory method to create IRenderTarget to be called from the render thread</param>
+        /// <returns></returns>
+        public CompositionTarget CreateCompositionTarget(Func<IRenderTarget> renderTargetFactory)
+        {
+            return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory));
+        }
+
+        /// <summary>
+        /// Requests pending changes in the composition objects to be serialized and sent to the render thread
+        /// </summary>
+        /// <returns>A task that completes when sent changes are applied and rendered on the render thread</returns>
+        public Task RequestCommitAsync()
+        {
+            Dispatcher.UIThread.VerifyAccess();
+            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);
+            if (_invokeOnNextCommit != null) 
+                ScheduleCommitCallbacks(batch.Completed);
+            
+            return batch.Completed;
+        }
+
+        async void ScheduleCommitCallbacks(Task task)
+        {
+            var list = _invokeOnNextCommit;
+            _invokeOnNextCommit = null;
+            await task;
+            foreach (var i in list!)
+                i();
+            list.Clear();
+            _invokeListPool.Push(list);
+        }
+
+        public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_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);
+        
+        private void QueueImplicitBatchCommit()
+        {
+            if(_implicitBatchCommitQueued)
+                return;
+            _implicitBatchCommitQueued = true;
+            Dispatcher.UIThread.Post(_implicitBatchCommit, DispatcherPriority.CompositionBatch);
+        }
+
+        private void ImplicitBatchCommit()
+        {
+            _implicitBatchCommitQueued = false;
+            RequestCommitAsync();
+        }
+
+        internal void RegisterForSerialization(CompositionObject compositionObject)
+        {
+            Dispatcher.UIThread.VerifyAccess();
+            _objectsForSerialization.Add(compositionObject);
+            QueueImplicitBatchCommit();
+        }
+
+        internal void InvokeOnNextCommit(Action action)
+        {
+            _invokeOnNextCommit ??= _invokeListPool.Count > 0 ? _invokeListPool.Pop() : new();
+            _invokeOnNextCommit.Add(action);
+        }
+    }
+}

+ 24 - 0
src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs

@@ -0,0 +1,24 @@
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition
+{
+    /// <summary>
+    /// A node in the visual tree that can have children.
+    /// </summary>
+    public partial class CompositionContainerVisual : CompositionVisual
+    {
+        public CompositionVisualCollection Children { get; private set; } = null!;
+
+        partial void InitializeDefaultsExtra()
+        {
+            Children = new CompositionVisualCollection(this, Server.Children);
+        }
+
+        private protected override void OnRootChangedCore()
+        {
+            foreach (var ch in Children)
+                ch.Root = Root;
+            base.OnRootChangedCore();
+        }
+    }
+}

+ 102 - 0
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs

@@ -0,0 +1,102 @@
+using System;
+using Avalonia.Collections.Pooled;
+using Avalonia.Rendering.Composition.Server;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Utilities;
+
+namespace Avalonia.Rendering.Composition.Drawing;
+
+/// <summary>
+/// A list of serialized drawing commands
+/// </summary>
+internal class CompositionDrawList : PooledList<IRef<IDrawOperation>>
+{
+    public Size? Size { get; set; }
+    
+    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) { 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;
+    }
+}
+
+/// <summary>
+/// An helper class for building <see cref="CompositionDrawList"/>
+/// </summary>
+internal class CompositionDrawListBuilder
+{
+    private CompositionDrawList? _operations;
+    private bool _owns;
+
+    public void Reset(CompositionDrawList? previousOperations)
+    {
+        _operations = previousOperations;
+        _owns = false;
+    }
+
+    public int Count => _operations?.Count ?? 0;
+    public CompositionDrawList? DrawOperations => _operations;
+
+    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(Count);
+        DrawOperations!.Add(RefCountable.Create(node));
+    }
+
+    public void TrimTo(int count)
+    {
+        if (count < Count)
+            _operations!.RemoveRange(count, _operations.Count - count);
+    }
+}

+ 391 - 0
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs

@@ -0,0 +1,391 @@
+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;
+
+/// <summary>
+/// An IDrawingContextImpl implementation that builds <see cref="CompositionDrawList"/>
+/// </summary>
+internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
+{
+    private CompositionDrawListBuilder _builder = new();
+    private int _drawOperationIndex;
+
+    /// <inheritdoc/>
+    public Matrix Transform { get; set; } = Matrix.Identity;
+
+    /// <inheritdoc/>
+    public void Clear(Color color)
+    {
+        // Cannot clear a deferred scene.
+    }
+
+    /// <inheritdoc/>
+    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!;
+    }
+
+    /// <inheritdoc/>
+    public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
+    {
+        var next = NextDrawAs<GeometryNode>();
+
+        if (next == null || !next.Item.Equals(Transform, brush, pen, geometry))
+        {
+            Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush)));
+        }
+        else
+        {
+            ++_drawOperationIndex;
+        }
+    }
+
+    /// <inheritdoc/>
+    public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
+        BitmapInterpolationMode bitmapInterpolationMode)
+    {
+        var next = NextDrawAs<ImageNode>();
+
+        if (next == null ||
+            !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode))
+        {
+            Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode));
+        }
+        else
+        {
+            ++_drawOperationIndex;
+        }
+    }
+
+    /// <inheritdoc/>
+    public void DrawBitmap(IRef<IBitmapImpl> 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();
+    }
+
+    /// <inheritdoc/>
+    public void DrawLine(IPen pen, Point p1, Point p2)
+    {
+        var next = NextDrawAs<LineNode>();
+
+        if (next == null || !next.Item.Equals(Transform, pen, p1, p2))
+        {
+            Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush)));
+        }
+        else
+        {
+            ++_drawOperationIndex;
+        }
+    }
+
+    /// <inheritdoc/>
+    public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect,
+        BoxShadows boxShadows = default)
+    {
+        var next = NextDrawAs<RectangleNode>();
+
+        if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows))
+        {
+            Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush)));
+        }
+        else
+        {
+            ++_drawOperationIndex;
+        }
+    }
+
+    /// <inheritdoc/>
+    public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
+    {
+        var next = NextDrawAs<ExperimentalAcrylicNode>();
+
+        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<EllipseNode>();
+
+        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<CustomDrawOperation>();
+        if (next == null || !next.Item.Equals(Transform, custom))
+            Add(new CustomDrawOperation(custom, Transform));
+        else
+            ++_drawOperationIndex;
+    }
+
+    /// <inheritdoc/>
+    public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
+    {
+        var next = NextDrawAs<GlyphRunNode>();
+
+        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");
+    }
+
+    /// <inheritdoc/>
+    public void PopClip()
+    {
+        var next = NextDrawAs<ClipNode>();
+
+        if (next == null || !next.Item.Equals(null))
+        {
+            Add(new ClipNode());
+        }
+        else
+        {
+            ++_drawOperationIndex;
+        }
+    }
+
+    /// <inheritdoc/>
+    public void PopGeometryClip()
+    {
+        var next = NextDrawAs<GeometryClipNode>();
+
+        if (next == null || !next.Item.Equals(null))
+        {
+            Add(new GeometryClipNode());
+        }
+        else
+        {
+            ++_drawOperationIndex;
+        }
+    }
+
+    /// <inheritdoc/>
+    public void PopBitmapBlendMode()
+    {
+        var next = NextDrawAs<BitmapBlendModeNode>();
+
+        if (next == null || !next.Item.Equals(null))
+        {
+            Add(new BitmapBlendModeNode());
+        }
+        else
+        {
+            ++_drawOperationIndex;
+        }
+    }
+
+    /// <inheritdoc/>
+    public void PopOpacity()
+    {
+        var next = NextDrawAs<OpacityNode>();
+
+        if (next == null || !next.Item.Equals(null))
+        {
+            Add(new OpacityNode());
+        }
+        else
+        {
+            ++_drawOperationIndex;
+        }
+    }
+
+    /// <inheritdoc/>
+    public void PopOpacityMask()
+    {
+        var next = NextDrawAs<OpacityMaskNode>();
+
+        if (next == null || !next.Item.Equals(null, null))
+        {
+            Add(new OpacityMaskNode());
+        }
+        else
+        {
+            ++_drawOperationIndex;
+        }
+    }
+
+    /// <inheritdoc/>
+    public void PushClip(Rect clip)
+    {
+        var next = NextDrawAs<ClipNode>();
+
+        if (next == null || !next.Item.Equals(Transform, clip))
+        {
+            Add(new ClipNode(Transform, clip));
+        }
+        else
+        {
+            ++_drawOperationIndex;
+        }
+    }
+
+    /// <inheritdoc />
+    public void PushClip(RoundedRect clip)
+    {
+        var next = NextDrawAs<ClipNode>();
+
+        if (next == null || !next.Item.Equals(Transform, clip))
+        {
+            Add(new ClipNode(Transform, clip));
+        }
+        else
+        {
+            ++_drawOperationIndex;
+        }
+    }
+
+    /// <inheritdoc/>
+    public void PushGeometryClip(IGeometryImpl? clip)
+    {
+        if (clip is null)
+            return;
+
+        var next = NextDrawAs<GeometryClipNode>();
+
+        if (next == null || !next.Item.Equals(Transform, clip))
+        {
+            Add(new GeometryClipNode(Transform, clip));
+        }
+        else
+        {
+            ++_drawOperationIndex;
+        }
+    }
+
+    /// <inheritdoc/>
+    public void PushOpacity(double opacity)
+    {
+        var next = NextDrawAs<OpacityNode>();
+
+        if (next == null || !next.Item.Equals(opacity))
+        {
+            Add(new OpacityNode(opacity));
+        }
+        else
+        {
+            ++_drawOperationIndex;
+        }
+    }
+
+    /// <inheritdoc/>
+    public void PushOpacityMask(IBrush mask, Rect bounds)
+    {
+        var next = NextDrawAs<OpacityMaskNode>();
+
+        if (next == null || !next.Item.Equals(mask, bounds))
+        {
+            Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask)));
+        }
+        else
+        {
+            ++_drawOperationIndex;
+        }
+    }
+
+    /// <inheritdoc/>
+    public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
+    {
+        var next = NextDrawAs<BitmapBlendModeNode>();
+
+        if (next == null || !next.Item.Equals(blendingMode))
+        {
+            Add(new BitmapBlendModeNode(blendingMode));
+        }
+        else
+        {
+            ++_drawOperationIndex;
+        }
+    }
+
+    private void Add<T>(T node) where T : class, IDrawOperation
+    {
+        if (_drawOperationIndex < _builder.Count)
+        {
+            _builder.ReplaceDrawOperation(_drawOperationIndex, node);
+        }
+        else
+        {
+            _builder.AddDrawOperation(node);
+        }
+
+        ++_drawOperationIndex;
+    }
+
+    private IRef<T>? NextDrawAs<T>() where T : class, IDrawOperation
+    {
+        return _drawOperationIndex < _builder.Count
+            ? _builder.DrawOperations![_drawOperationIndex] as IRef<T>
+            : null;
+    }
+    
+    private IDisposable? CreateChildScene(IBrush? brush)
+    {
+        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 recorder = new CompositionDrawingContext();
+                recorder.BeginUpdate(null);
+                ImmediateRenderer.Render(visual, new DrawingContext(recorder));
+                var drawList = recorder.EndUpdate();
+                drawList.Size = visual.Bounds.Size;
+
+                return drawList;
+            }
+        }
+        return null;
+    }
+}

+ 14 - 0
src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs

@@ -0,0 +1,14 @@
+namespace Avalonia.Rendering.Composition;
+
+/// <summary>
+/// Enables access to composition visual objects that back XAML elements in the XAML composition tree.
+/// </summary>
+public static class ElementComposition
+{
+    /// <summary>
+    /// Gets CompositionVisual that backs a Visual
+    /// </summary>
+    /// <param name="visual"></param>
+    /// <returns></returns>
+    public static CompositionVisual? GetElementVisual(Visual visual) => visual.CompositionVisual;
+}

+ 120 - 0
src/Avalonia.Base/Rendering/Composition/Enums.cs

@@ -0,0 +1,120 @@
+using System;
+
+namespace Avalonia.Rendering.Composition
+{
+    public enum CompositionBlendMode
+    {
+        /// <summary>No regions are enabled. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_clr.svg)</summary>
+        Clear,
+
+        /// <summary>Only the source will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src.svg)</summary>
+        Src,
+
+        /// <summary>Only the destination will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst.svg)</summary>
+        Dst,
+
+        /// <summary>Source is placed over the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-over.svg)</summary>
+        SrcOver,
+
+        /// <summary>Destination is placed over the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-over.svg)</summary>
+        DstOver,
+
+        /// <summary>The source that overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-in.svg)</summary>
+        SrcIn,
+
+        /// <summary>Destination which overlaps the source, replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-in.svg)</summary>
+        DstIn,
+
+        /// <summary>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)</summary>
+        SrcOut,
+
+        /// <summary>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)</summary>
+        DstOut,
+
+        /// <summary>Source which overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-atop.svg)</summary>
+        SrcATop,
+
+        /// <summary>Destination which overlaps the source replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-atop.svg)</summary>
+        DstATop,
+
+        /// <summary>The non-overlapping regions of source and destination are combined. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_xor.svg)</summary>
+        Xor,
+
+        /// <summary>Display the sum of the source image and destination image. [Porter Duff Compositing Operators]</summary>
+        Plus,
+
+        /// <summary>Multiplies all components (= alpha and color). [Separable Blend Modes]</summary>
+        Modulate,
+
+        /// <summary>Multiplies the complements of the backdrop and source CompositionColorvalues, then complements the result. [Separable Blend Modes]</summary>
+        Screen,
+
+        /// <summary>Multiplies or screens the colors, depending on the backdrop CompositionColorvalue. [Separable Blend Modes]</summary>
+        Overlay,
+
+        /// <summary>Selects the darker of the backdrop and source colors. [Separable Blend Modes]</summary>
+        Darken,
+
+        /// <summary>Selects the lighter of the backdrop and source colors. [Separable Blend Modes]</summary>
+        Lighten,
+
+        /// <summary>Brightens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes]</summary>
+        ColorDodge,
+
+        /// <summary>Darkens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes]</summary>
+        ColorBurn,
+
+        /// <summary>Multiplies or screens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes]</summary>
+        HardLight,
+
+        /// <summary>Darkens or lightens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes]</summary>
+        SoftLight,
+
+        /// <summary>Subtracts the darker of the two constituent colors from the lighter color. [Separable Blend Modes]</summary>
+        Difference,
+
+        /// <summary>Produces an effect similar to that of the Difference mode but lower in contrast. [Separable Blend Modes]</summary>
+        Exclusion,
+
+        /// <summary>The source CompositionColoris multiplied by the destination CompositionColorand replaces the destination [Separable Blend Modes]</summary>
+        Multiply,
+
+        /// <summary>Creates a CompositionColorwith the hue of the source CompositionColorand the saturation and luminosity of the backdrop color. [Non-Separable Blend Modes]</summary>
+        Hue,
+
+        /// <summary>Creates a CompositionColorwith the saturation of the source CompositionColorand the hue and luminosity of the backdrop color. [Non-Separable Blend Modes]</summary>
+        Saturation,
+
+        /// <summary>Creates a CompositionColorwith the hue and saturation of the source CompositionColorand the luminosity of the backdrop color. [Non-Separable Blend Modes]</summary>
+        Color,
+
+        /// <summary>Creates a CompositionColorwith the luminosity of the source CompositionColorand the hue and saturation of the backdrop color. [Non-Separable Blend Modes]</summary>
+        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
+    }
+}

+ 237 - 0
src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs

@@ -0,0 +1,237 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using Avalonia.Rendering.Composition.Animations;
+using Avalonia.Utilities;
+
+namespace Avalonia.Rendering.Composition.Expressions
+{
+    /// <summary>
+    /// Built-in functions for Foreign Function Interface available from composition animation expressions
+    /// </summary>
+    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 = MathUtilities.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) => 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)},
+
+                {"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) MathUtilities.Clamp(a, 0, 255),
+                        (byte) MathUtilities.Clamp(r, 0, 255),
+                        (byte) MathUtilities.Clamp(g, 0, 255),
+                        (byte) MathUtilities.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<ExpressionVariant> arguments, out ExpressionVariant result) =>
+            _registry.Call(name, arguments, out result);
+
+        public static BuiltInExpressionFfi Instance { get; } = new BuiltInExpressionFfi();
+    }
+}

+ 184 - 0
src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs

@@ -0,0 +1,184 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using Avalonia.Media;
+
+namespace Avalonia.Rendering.Composition.Expressions
+{
+    /// <summary>
+    /// Foreign function interface for composition animations based on calling delegates
+    /// </summary>
+    internal class DelegateExpressionFfi : IExpressionForeignFunctionInterface, IEnumerable
+    {
+        struct FfiRecord
+        {
+            public VariantType[] Types;
+            public Func<IReadOnlyList<ExpressionVariant>, ExpressionVariant> Delegate;
+        }
+
+        private readonly Dictionary<string, Dictionary<int, List<FfiRecord>>>
+            _registry = new Dictionary<string, Dictionary<int, List<FfiRecord>>>();
+
+        public bool Call(string name, IReadOnlyList<ExpressionVariant> 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<object>().GetEnumerator();
+
+        void Add(string name, Func<IReadOnlyList<ExpressionVariant>, ExpressionVariant> cb,
+            params Type[] types)
+        {
+            if (!_registry.TryGetValue(name, out var nameGroup))
+                _registry[name] = nameGroup =
+                    new Dictionary<int, List<FfiRecord>>();
+            if (!nameGroup.TryGetValue(types.Length, out var countGroup))
+                nameGroup[types.Length] = countGroup = new List<FfiRecord>();
+
+            countGroup.Add(new FfiRecord
+            {
+                Types = types.Select(t => TypeMap[t]).ToArray(),
+                Delegate = cb
+            });
+        }
+
+        static readonly Dictionary<Type, VariantType> TypeMap = new Dictionary<Type, VariantType>
+        {
+            [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<T1>(string name, Func<T1, ExpressionVariant> cb) where T1 : struct
+        {
+            Add(name, args => cb(args[0].CastOrDefault<T1>()), typeof(T1));
+        }
+
+        public void Add<T1, T2>(string name, Func<T1, T2, ExpressionVariant> cb) where T1 : struct where T2 : struct
+        {
+            Add(name, args => cb(args[0].CastOrDefault<T1>(), args[1].CastOrDefault<T2>()), typeof(T1), typeof(T2));
+        }
+
+
+        public void Add<T1, T2, T3>(string name, Func<T1, T2, T3, ExpressionVariant> cb)
+            where T1 : struct where T2 : struct where T3 : struct
+        {
+            Add(name, args => cb(args[0].CastOrDefault<T1>(), args[1].CastOrDefault<T2>(), args[2].CastOrDefault<T3>()), typeof(T1), typeof(T2),
+                typeof(T3));
+        }
+        
+        public void Add<T1, T2, T3, T4>(string name, Func<T1, T2, T3, T4, ExpressionVariant> cb)
+            where T1 : struct where T2 : struct where T3 : struct where T4 : struct
+        {
+            Add(name, args => cb(
+                    args[0].CastOrDefault<T1>(),
+                    args[1].CastOrDefault<T2>(), 
+                    args[2].CastOrDefault<T3>(),
+                    args[3].CastOrDefault<T4>()),
+                typeof(T1), typeof(T2), typeof(T3), typeof(T4));
+        }
+        
+        public void Add<T1, T2, T3, T4, T5>(string name, Func<T1, T2, T3, T4, T5, ExpressionVariant> cb)
+            where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct
+        {
+            Add(name, args => cb(
+                    args[0].CastOrDefault<T1>(),
+                    args[1].CastOrDefault<T2>(), 
+                    args[2].CastOrDefault<T3>(),
+                    args[3].CastOrDefault<T4>(),
+                    args[4].CastOrDefault<T5>()),
+                typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5));
+        }
+        
+        public void Add<T1, T2, T3, T4, T5, T6>(string name, Func<T1, T2, T3, T4, T5, T6, ExpressionVariant> 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<T1>(),
+                    args[1].CastOrDefault<T2>(), 
+                    args[2].CastOrDefault<T3>(),
+                    args[3].CastOrDefault<T4>(),
+                    args[4].CastOrDefault<T5>(),
+                    args[4].CastOrDefault<T6>()),
+                typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6));
+        }
+
+
+        public void Add<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(string name,
+            Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, ExpressionVariant> 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<T1>(),
+                    args[1].CastOrDefault<T2>(),
+                    args[2].CastOrDefault<T3>(),
+                    args[3].CastOrDefault<T4>(),
+                    args[4].CastOrDefault<T5>(),
+                    args[4].CastOrDefault<T6>(),
+                    args[4].CastOrDefault<T7>(),
+                    args[4].CastOrDefault<T8>(),
+                    args[4].CastOrDefault<T9>(),
+                    args[4].CastOrDefault<T10>(),
+                    args[4].CastOrDefault<T11>(),
+                    args[4].CastOrDefault<T12>(),
+                    args[4].CastOrDefault<T13>(),
+                    args[4].CastOrDefault<T14>(),
+                    args[4].CastOrDefault<T15>(),
+                    args[4].CastOrDefault<T16>()
+                ),
+                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)
+            );
+        }
+    }
+}

+ 377 - 0
src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs

@@ -0,0 +1,377 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reflection;
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Expressions
+{
+    /// <summary>
+    /// A parsed composition expression
+    /// </summary>
+    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);
+
+        public virtual void CollectReferences(HashSet<(string parameter, string property)> references)
+        {
+            
+        }
+
+        protected abstract string Print();
+        public override string ToString() => Print();
+
+        internal static string OperatorName(ExpressionType t)
+        {
+            var attr = typeof(ExpressionType).GetMember(t.ToString())[0]
+                .GetCustomAttribute<PrettyPrintStringAttribute>();
+            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);
+        }
+
+        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})";
+    }
+    
+    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<Expression> Parameters { get; }
+        public override ExpressionType Type => ExpressionType.FunctionCall;
+
+        public FunctionCallExpression(string name, List<Expression> parameters)
+        {
+            Name = name;
+            Parameters = parameters;
+        }
+        
+        public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context)
+        {
+            if (context.ForeignFunctionInterface == null)
+                return default;
+            var args = new List<ExpressionVariant>();
+            foreach (var expr in Parameters)
+                args.Add(expr.Evaluate(ref context));
+            if (!context.ForeignFunctionInterface.Call(Name, args, out var res))
+                return default;
+            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) + ") )";
+        }
+    }
+    
+    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 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);
+        }
+
+        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;
+        }
+
+        public override void CollectReferences(HashSet<(string parameter, string property)> references)
+        {
+            Parameter.CollectReferences(references);
+        }
+
+        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;
+        }
+
+        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 + ")";
+        }
+
+        public BinaryExpression(Expression left, Expression right, ExpressionType type)
+        {
+            Left = left;
+            Right = right;
+            Type = type;
+        }
+    }
+
+
+
+}

+ 32 - 0
src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs

@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Server;
+
+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<ExpressionVariant> arguments, out ExpressionVariant result);
+    }
+}

+ 14 - 0
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;
+        }
+    }
+}

+ 298 - 0
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<char> 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<char> 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<Expression> _expressions;
+            private List<ExpressionType> _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<Expression>();
+                    _expressions.Add(_first);
+                    _first = null;
+                    _operators = new List<ExpressionType>();
+                }
+                _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<Expression>();
+                    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);
+            }
+            
+            
+            
+        }
+    }
+}

+ 57 - 0
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<IExpressionObject>
+{
+    private List<IExpressionObject> _list = new();
+    private HashSet<IExpressionObject> _hashSet = new();
+    
+    public void Add(IExpressionObject obj, string member)
+    {
+        if (_hashSet.Add(obj))
+            _list.Add(obj);
+    }
+
+    public void Clear()
+    {
+        _list.Clear();
+        _hashSet.Clear();
+    }
+
+    IEnumerator<IExpressionObject> IEnumerable<IExpressionObject>.GetEnumerator()
+    {
+        return _list.GetEnumerator();
+    }
+
+    IEnumerator IEnumerable.GetEnumerator()
+    {
+        return ((IEnumerable)_list).GetEnumerator();
+    }
+
+    public List<IExpressionObject>.Enumerator GetEnumerator() => _list.GetEnumerator();
+    
+    public struct Pool
+    {
+        private Stack<ExpressionTrackedObjects> _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);
+        }
+    }
+}

+ 730 - 0
src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs

@@ -0,0 +1,730 @@
+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
+    }
+
+    /// <summary>
+    /// A VARIANT type used in expression animations. Can represent multiple value types
+    /// </summary>
+    [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.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.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.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<T>(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>(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<T>() where T : struct
+        {
+            TryCast<T>(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";
+        }
+    }
+
+}

+ 259 - 0
src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs

@@ -0,0 +1,259 @@
+using System;
+using System.Globalization;
+
+namespace Avalonia.Rendering.Composition.Expressions
+{
+    /// <summary>
+    /// Helper class for composition expression parser
+    /// </summary>
+    internal ref struct TokenParser
+    {
+        private ReadOnlySpan<char> _s;
+        public int Position { get; private set; }
+        public TokenParser(ReadOnlySpan<char> 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<char> 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;c++)
+                if (keyword[c] != _s[c])
+                    return false;
+
+            if (_s.Length > 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;c++)
+                if (keywordInLowerCase[c] != char.ToLowerInvariant(_s[c]))
+                    return false;
+            
+            if (_s.Length > 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<char> extraValidChars, out ReadOnlySpan<char> res)
+        {
+            res = ReadOnlySpan<char>.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<char> res)
+        {
+            res = ReadOnlySpan<char>.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<char> res)
+        {
+            res = ReadOnlySpan<char>.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();
+
+    }
+}

+ 6 - 0
src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs

@@ -0,0 +1,6 @@
+namespace Avalonia.Rendering.Composition;
+
+internal interface ICompositionTargetDebugEvents
+{
+    void RectInvalidated(Rect rc);
+}

+ 66 - 0
src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs

@@ -0,0 +1,66 @@
+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;
+        }
+
+        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);
+    }
+}

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

+ 179 - 0
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@@ -0,0 +1,179 @@
+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;
+
+namespace Avalonia.Rendering.Composition.Server;
+
+/// <summary>
+/// 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.
+/// </summary>
+internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
+{
+    private IDrawingContextImpl _impl;
+    private readonly VisualBrushRenderer _visualBrushRenderer;
+
+    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 PostTransform { get; set; } = Matrix.Identity;
+    
+    public void Dispose()
+    {
+        _impl.Dispose();
+    }
+
+    Matrix _transform;    
+    public Matrix Transform
+    {
+        get => _transform;
+        set => _impl.Transform = (_transform = value) * PostTransform;
+    }
+
+    public void Clear(Color color)
+    {
+        _impl.Clear(color);
+    }
+
+    public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
+        BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
+    {
+        _impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode);
+    }
+
+    public void DrawBitmap(IRef<IBitmapImpl> 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 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);
+            }
+        }
+    }
+
+    public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
+    {
+        if (_impl is IDrawingContextWithAcrylicLikeSupport acrylic) 
+            acrylic.DrawRectangle(material, rect);
+    }
+}

+ 76 - 0
src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs

@@ -0,0 +1,76 @@
+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;
+
+/// <summary>
+/// An FPS counter helper that can draw itself on the render thread
+/// </summary>
+internal class FpsCounter
+{
+    private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
+    private int _framesThisSecond;
+    private int _totalFrames;
+    private int _fps;
+    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)
+    {
+        for (var c = FirstChar; c <= LastChar; c++)
+        {
+            var s = new string((char)c, 1);
+            var glyph = typeface.GetGlyph((uint)(s[0]));
+            _runs[c - FirstChar] = new GlyphRun(typeface, 18, new ReadOnlySlice<char>(s.AsMemory()), new ushort[] { glyph });
+        }
+    }
+
+    public void FpsTick() => _framesThisSecond++;
+
+    public void RenderFps(IDrawingContextImpl context, string aux)
+    {
+        var now = _stopwatch.Elapsed;
+        var elapsed = now - _lastFpsUpdate;
+
+        ++_framesThisSecond;
+        ++_totalFrames;
+
+        if (elapsed.TotalSeconds > 1)
+        {
+            _fps = (int)(_framesThisSecond / elapsed.TotalSeconds);
+            _framesThisSecond = 0;
+            _lastFpsUpdate = now;
+        }
+
+        var fpsLine = $"Frame #{_totalFrames:00000000} FPS: {_fps:000} " + aux;
+        double width = 0;
+        double height = 0;
+        foreach (var ch in fpsLine)
+        {
+            var run = _runs[ch - FirstChar];
+            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 - FirstChar];
+            context.Transform = Matrix.CreateTranslation(offset, 0);
+            context.DrawGlyphRun(Brushes.White, run);
+            offset += run.Size.Width;
+        }
+    }
+}

+ 46 - 0
src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs

@@ -0,0 +1,46 @@
+namespace Avalonia.Rendering.Composition.Server
+{
+    /// <summary>
+    /// 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
+    /// </summary>
+    internal class ReadbackIndices
+    {
+        private readonly object _lock = new object();
+        public int ReadIndex { get; private set; } = 0;
+        public int WriteIndex { get; private set; } = 1;
+        public int WrittenIndex { get; private set; } = 0;
+        public ulong ReadRevision { get; private set; }
+        public ulong LastWrittenRevision { get; private set; }
+        
+        public void NextRead()
+        {
+            lock (_lock)
+            {
+                if (ReadRevision < LastWrittenRevision)
+                {
+                    ReadIndex = WrittenIndex;
+                    ReadRevision = LastWrittenRevision;
+                }
+            }
+        }
+
+        public void CompleteWrite(ulong writtenRevision)
+        {
+            lock (_lock)
+            {
+                for (var c = 0; c < 3; c++)
+                {
+                    if (c != WriteIndex && c != ReadIndex)
+                    {
+                        WrittenIndex = WriteIndex;
+                        LastWrittenRevision = writtenRevision;
+                        WriteIndex = c;
+                        return;
+                    }
+                }
+            }
+        }
+    }
+}

+ 44 - 0
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs

@@ -0,0 +1,44 @@
+using System.Numerics;
+using Avalonia.Platform;
+
+namespace Avalonia.Rendering.Composition.Server
+{
+    /// <summary>
+    /// Server-side counterpart of <see cref="CompositionContainerVisual"/>.
+    /// Mostly propagates update and render calls, but is also responsible
+    /// for updating adorners in deferred manner
+    /// </summary>
+    internal partial class ServerCompositionContainerVisual : ServerCompositionVisual
+    {
+        public ServerCompositionVisualCollection Children { get; private set; } = null!;
+        
+        protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
+        {
+            base.RenderCore(canvas, currentTransformedClip);
+
+            foreach (var ch in Children)
+            {
+                ch.Render(canvas, currentTransformedClip);
+            }
+        }
+
+        public override void Update(ServerCompositionTarget root)
+        {
+            base.Update(root);
+            foreach (var child in Children)
+            {
+                if (child.AdornedVisual != null)
+                    root.EnqueueAdornerUpdate(child);
+                else
+                    child.Update(root);
+            }
+
+            IsDirtyComposition = false;
+        }
+
+        partial void Initialize()
+        {
+            Children = new ServerCompositionVisualCollection(Compositor);
+        }
+    }
+}

+ 75 - 0
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs

@@ -0,0 +1,75 @@
+using System;
+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;
+
+/// <summary>
+/// Server-side counterpart of <see cref="CompositionDrawListVisual"/>
+/// </summary>
+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;
+    
+    public ServerCompositionDrawListVisual(ServerCompositor compositor, Visual v) : base(compositor)
+    {
+#if DEBUG
+        UiVisual = v;
+#endif
+    }
+
+    Rect? _contentBounds;
+
+    public override Rect OwnContentBounds
+    {
+        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 DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt)
+    {
+        if (reader.Read<byte>() == 1)
+        {
+            _renderCommands?.Dispose();
+            _renderCommands = reader.ReadObject<CompositionDrawList?>();
+            _contentBounds = null;
+        }
+        base.DeserializeChangesCore(reader, commitedAt);
+    }
+
+    protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
+    {
+        if (_renderCommands != null)
+        {
+            _renderCommands.Render(canvas);
+        }
+        base.RenderCore(canvas, currentTransformedClip);
+    }
+    
+#if DEBUG
+    public override string ToString()
+    {
+        return UiVisual.GetType().ToString();
+    }
+#endif
+}

+ 9 - 0
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)
+        {
+        }
+    }
+}

+ 221 - 0
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@@ -0,0 +1,221 @@
+using System;
+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;
+using Avalonia.Utilities;
+
+namespace Avalonia.Rendering.Composition.Server
+{
+    /// <summary>
+    /// Server-side counterpart of the <see cref="CompositionTarget"/>
+    /// That's the place where we update visual transforms, track dirty rects and actually do rendering
+    /// </summary>
+    internal partial class ServerCompositionTarget : IDisposable
+    {
+        private readonly ServerCompositor _compositor;
+        private readonly Func<IRenderTarget> _renderTargetFactory;
+        private static long s_nextId = 1;
+        public long Id { get; }
+        public ulong Revision { get; private set; }
+        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;
+        private bool _disposed;
+        private HashSet<ServerCompositionVisual> _attachedVisuals = new();
+        private Queue<ServerCompositionVisual> _adornerUpdateQueue = new();
+
+        public ICompositionTargetDebugEvents? DebugEvents { get; set; }
+        public ReadbackIndices Readback { get; } = new();
+        public int RenderedVisuals { get; set; }
+
+        public ServerCompositionTarget(ServerCompositor compositor, Func<IRenderTarget> renderTargetFactory) :
+            base(compositor)
+        {
+            _compositor = compositor;
+            _renderTargetFactory = renderTargetFactory;
+            Id = Interlocked.Increment(ref s_nextId);
+        }
+
+        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)
+        {
+            _redrawRequested = true;
+        }
+
+        public void Render()
+        {
+            if (_disposed)
+            {
+                Compositor.RemoveCompositionTarget(this);
+                return;
+            }
+
+            if (Root == null) 
+                return;
+            _renderTarget ??= _renderTargetFactory();
+
+            Compositor.UpdateServerTime();
+            
+            if(_dirtyRect.IsEmpty && !_redrawRequested)
+                return;
+
+            Revision++;
+            
+            // Update happens in a separate phase to extend dirty rect if needed
+            Root.Update(this);
+
+            while (_adornerUpdateQueue.Count > 0)
+            {
+                var adorner = _adornerUpdateQueue.Dequeue();
+                adorner.Update(this);
+            }
+            
+            Readback.CompleteWrite(Revision);
+
+            _redrawRequested = false;
+            using (var targetContext = _renderTarget.CreateDrawingContext(null))
+            {
+                var layerSize = Size * Scaling;
+                if (layerSize != _layerSize || _layer == null)
+                {
+                    _layer?.Dispose();
+                    _layer = null;
+                    _layer = targetContext.CreateLayer(Size);
+                    _layerSize = layerSize;
+                }
+
+                if (!_dirtyRect.IsEmpty)
+                {
+                    var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer();
+                    using (var context = _layer.CreateDrawingContext(visualBrushHelper))
+                    {
+                        context.PushClip(_dirtyRect);
+                        context.Clear(Colors.Transparent);
+                        Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), _dirtyRect);
+                        context.PopClip();
+                    }
+                }
+
+                targetContext.Clear(Colors.Transparent);
+                targetContext.Transform = Matrix.Identity;
+                if (_layer.CanBlit)
+                    _layer.Blit(targetContext);
+                else
+                    targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1,
+                        new Rect(_layerSize),
+                        new Rect(Size), BitmapInterpolationMode.LowQuality);
+                
+                
+                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)
+                {
+                    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} R:{RenderedVisuals:0000}");
+                }
+                RenderedVisuals = 0;
+
+                _dirtyRect = Rect.Empty;
+            }
+        }
+
+        public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling);
+        
+        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)
+        {
+            if(rect.IsEmpty)
+                return;
+            var snapped = SnapToDevicePixels(rect, Scaling);
+            DebugEvents?.RectInvalidated(rect);
+            _dirtyRect = _dirtyRect.Union(snapped);
+            _redrawRequested = true;
+        }
+
+        public void Invalidate()
+        {
+            _redrawRequested = true;
+        }
+
+        public void Dispose()
+        {
+            if(_disposed)
+                return;
+            _disposed = true;
+            using (_compositor.GpuContext?.EnsureCurrent())
+            {
+                if (_layer != null)
+                {
+                    _layer.Dispose();
+                    _layer = null;
+                }
+
+                _renderTarget?.Dispose();
+                _renderTarget = null;
+            }
+            _compositor.RemoveCompositionTarget(this);
+        }
+
+        public void AddVisual(ServerCompositionVisual visual)
+        {
+            if (_attachedVisuals.Add(visual) && IsEnabled)
+                visual.Activate();
+        }
+
+        public void RemoveVisual(ServerCompositionVisual visual)
+        {
+            if (_attachedVisuals.Remove(visual) && IsEnabled)
+                visual.Deactivate();
+            if(visual.IsVisibleInFrame)
+                AddDirtyRect(visual.TransformedOwnContentBounds);
+        }
+
+        public void EnqueueAdornerUpdate(ServerCompositionVisual visual) => _adornerUpdateQueue.Enqueue(visual);
+    }
+}

+ 76 - 0
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.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(CompositionProperty offset)
+    {
+        base.NotifyAnimatedValueChanged(offset);
+        if (offset == s_IdOfClipToBoundsProperty
+            || offset == s_IdOfOpacityProperty
+            || offset == s_IdOfSizeProperty)
+            IsDirtyComposition = true;
+
+        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_IdOfClipToBoundsProperty
+            || offset == s_IdOfSizeProperty)
+            _clipSizeDirty = true;
+    }
+}

+ 237 - 0
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@@ -0,0 +1,237 @@
+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
+{
+    /// <summary>
+    /// Server-side <see cref="CompositionVisual"/> counterpart.
+    /// Is responsible for computing the transformation matrix, for applying various visual
+    /// properties before calling visual-specific drawing code and for notifying the
+    /// <see cref="ServerCompositionTarget"/> for new dirty rects
+    /// </summary>
+    partial class ServerCompositionVisual : ServerObject
+    {
+        private bool _isDirtyForUpdate;
+        private Rect _oldOwnContentBounds;
+        private bool _isBackface;
+        private Rect? _transformedClipBounds;
+        private Rect _combinedTransformedClipBounds;
+        
+        protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
+        {
+            
+        }
+
+        public void Render(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
+        {
+            if(Visible == false || IsVisibleInFrame == false)
+                return;
+            if(Opacity == 0)
+                return;
+
+            currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds);
+            if(currentTransformedClip.IsEmpty)
+                return;
+
+            Root!.RenderedVisuals++;
+            
+            var transform = GlobalTransformMatrix;
+            canvas.PostTransform = MatrixUtils.ToMatrix(transform);
+            canvas.Transform = Matrix.Identity;
+            if (Opacity != 1)
+                canvas.PushOpacity(Opacity);
+            var boundsRect = new Rect(new Size(Size.X, Size.Y));
+            if(ClipToBounds)
+                canvas.PushClip(Root!.SnapToDevicePixels(boundsRect));
+            if (Clip != null) 
+                canvas.PushGeometryClip(Clip);
+            if(OpacityMaskBrush != null)
+                canvas.PushOpacityMask(OpacityMaskBrush, boundsRect);
+            
+            RenderCore(canvas, currentTransformedClip);
+            
+            // Hack to force invalidation of SKMatrix
+            canvas.PostTransform = MatrixUtils.ToMatrix(transform);
+            canvas.Transform = Matrix.Identity;
+
+            if (OpacityMaskBrush != null)
+                canvas.PopOpacityMask();
+            if (Clip != null)
+                canvas.PopGeometryClip();
+            if (ClipToBounds)
+                canvas.PopClip();
+            if(Opacity != 1)
+                canvas.PopOpacity();
+        }
+        
+        private ReadbackData _readback0, _readback1, _readback2;
+
+        /// <summary>
+        /// 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
+        /// </summary>
+        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; private set; } = Matrix4x4.Identity;
+        public Matrix4x4 GlobalTransformMatrix { get; private set; }
+
+        public virtual void Update(ServerCompositionTarget root)
+        {
+            if(Parent == null && Root == null)
+                return;
+            
+            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;
+            if (GlobalTransformMatrix != newTransform)
+            {
+                _isBackface = Vector3.Transform(
+                    new Vector3(0, 0, float.PositiveInfinity), GlobalTransformMatrix).Z <= 0;
+                positionChanged = true;
+            }
+
+            var oldTransformedContentBounds = TransformedOwnContentBounds;
+            var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds;
+            
+            if (_parent?.IsDirtyComposition == true)
+            {
+                IsDirtyComposition = true;
+                _isDirtyForUpdate = true;
+            }
+            
+            var invalidateOldBounds = _isDirtyForUpdate;
+            var invalidateNewBounds = _isDirtyForUpdate;
+
+            GlobalTransformMatrix = newTransform;
+            
+            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 = _parent?.IsVisibleInFrame != false && Visible && EffectiveOpacity > 0.04 && !_isBackface &&
+                               !_combinedTransformedClipBounds.IsEmpty;
+
+            if (wasVisible != IsVisibleInFrame || positionChanged)
+            {
+                invalidateOldBounds |= wasVisible;
+                invalidateNewBounds |= IsVisibleInFrame;
+            }
+
+            // Invalidate new bounds
+            if (invalidateNewBounds)
+                AddDirtyRect(TransformedOwnContentBounds.Intersect(_combinedTransformedClipBounds));
+
+            if (invalidateOldBounds)
+                AddDirtyRect(oldTransformedContentBounds.Intersect(oldCombinedTransformedClipBounds));
+
+
+            _isDirtyForUpdate = false;
+            
+            // Update readback indices
+            var i = Root!.Readback;
+            ref var readback = ref GetReadback(i.WriteIndex);
+            readback.Revision = root.Revision;
+            readback.Matrix = GlobalTransformMatrix;
+            readback.TargetId = Root.Id;
+            readback.Visible = IsVisibleInFrame;
+        }
+
+        void AddDirtyRect(Rect rc)
+        {
+            if(rc == Rect.Empty)
+                return;
+            Root?.AddDirtyRect(rc);
+        }
+        
+        /// <summary>
+        /// Data that can be read from the UI thread
+        /// </summary>
+        public struct ReadbackData
+        {
+            public Matrix4x4 Matrix;
+            public ulong Revision;
+            public long TargetId;
+            public bool Visible;
+        }
+        
+        partial void DeserializeChangesExtra(BatchStreamReader c)
+        {
+            ValuesInvalidated();
+        }
+
+        partial void OnRootChanging()
+        {
+            if (Root != null)
+                Root.RemoveVisual(this);
+        }
+        
+        partial void OnRootChanged()
+        {
+            if (Root != null)
+                Root.AddVisual(this);
+        }
+        
+        protected override void ValuesInvalidated()
+        {
+            _isDirtyForUpdate = true;
+            Root?.Invalidate();
+        }
+
+        public bool IsVisibleInFrame { get; set; }
+        public double EffectiveOpacity { get; set; }
+        public Rect TransformedOwnContentBounds { get; set; }
+        public virtual Rect OwnContentBounds => new Rect(0, 0, Size.X, Size.Y);
+    }
+
+
+}

+ 140 - 0
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs

@@ -0,0 +1,140 @@
+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;
+
+namespace Avalonia.Rendering.Composition.Server
+{
+    /// <summary>
+    /// Server-side counterpart of the <see cref="Compositor"/>.
+    /// 1) manages deserialization of changes received from the UI thread
+    /// 2) triggers animation ticks
+    /// 3) asks composition targets to render themselves
+    /// </summary>
+    internal class ServerCompositor : IRenderLoopTask
+    {
+        private readonly IRenderLoop _renderLoop;
+        private readonly Queue<Batch> _batches = new Queue<Batch>(); 
+        public long LastBatchId { get; private set; }
+        public Stopwatch Clock { get; } = Stopwatch.StartNew();
+        public TimeSpan ServerNow { get; private set; }
+        private List<ServerCompositionTarget> _activeTargets = new();
+        private HashSet<IAnimationInstance> _activeAnimations = new();
+        private List<IAnimationInstance> _animationsToUpdate = new();
+        internal BatchStreamObjectPool<object?> BatchObjectPool;
+        internal BatchStreamMemoryPool BatchMemoryPool;
+        private object _lock = new object();
+        public IPlatformGpuContext? GpuContext { get; }
+
+        public ServerCompositor(IRenderLoop renderLoop, IPlatformGpu? platformGpu,
+            BatchStreamObjectPool<object?> batchObjectPool, BatchStreamMemoryPool batchMemoryPool)
+        {
+            GpuContext = platformGpu?.PrimaryContext;
+            _renderLoop = renderLoop;
+            BatchObjectPool = batchObjectPool;
+            BatchMemoryPool = batchMemoryPool;
+            _renderLoop.Add(this);
+        }
+
+        public void EnqueueBatch(Batch batch)
+        {
+            lock (_batches) 
+                _batches.Enqueue(batch);
+        }
+
+        internal void UpdateServerTime() => ServerNow = Clock.Elapsed;
+
+        List<Batch> _reusableToCompleteList = new();
+        void ApplyPendingBatches()
+        {
+            while (true)
+            {
+                Batch batch;
+                lock (_batches)
+                {
+                    if(_batches.Count == 0)
+                        break;
+                    batch = _batches.Dequeue();
+                }
+
+                using (var stream = new BatchStreamReader(batch.Changes, BatchMemoryPool, BatchObjectPool))
+                {
+                    while (!stream.IsObjectEof)
+                    {
+                        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<Guid>() != BatchStreamDebugMarkers.ObjectEndMagic)
+                            throw new InvalidOperationException(
+                                $"Object {target.GetType()} failed to deserialize properly on data stream");
+#endif
+                    }
+                }
+
+                _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)
+        {
+        }
+
+        public void Render()
+        {
+            lock (_lock)
+            {
+                RenderCore();
+            }
+        }
+        
+        private void RenderCore()
+        {
+            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();
+            
+            CompletePendingBatches();
+        }
+
+        public void AddCompositionTarget(ServerCompositionTarget target)
+        {
+            _activeTargets.Add(target);
+        }
+
+        public void RemoveCompositionTarget(ServerCompositionTarget target)
+        {
+            _activeTargets.Remove(target);
+        }
+        
+        public void AddToClock(IAnimationInstance animationInstance) =>
+            _activeAnimations.Add(animationInstance);
+
+        public void RemoveFromClock(IAnimationInstance animationInstance) =>
+            _activeAnimations.Remove(animationInstance);
+    }
+}

+ 44 - 0
src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Transport;
+
+namespace Avalonia.Rendering.Composition.Server
+{
+    /// <summary>
+    /// 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
+    /// </summary>
+    class ServerList<T> : ServerObject where T : ServerObject
+    {
+        public List<T> List { get; } = new List<T>();
+
+        protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt)
+        {
+            if (reader.Read<byte>() == 1)
+            {
+                List.Clear();
+                var count = reader.Read<int>();
+                for (var c = 0; c < count; c++) 
+                    List.Add(reader.ReadObject<T>());
+            }
+            base.DeserializeChangesCore(reader, commitedAt);
+        }
+
+        public override long LastChangedBy
+        {
+            get
+            {
+                var seq = base.LastChangedBy;
+                foreach (var i in List)
+                    seq = Math.Max(i.LastChangedBy, seq);
+                return seq;
+            }
+        }
+
+        public List<T>.Enumerator GetEnumerator() => List.GetEnumerator();
+
+        public ServerList(ServerCompositor compositor) : base(compositor)
+        {
+        }
+    }
+}

+ 180 - 0
src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs

@@ -0,0 +1,180 @@
+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
+{
+    /// <summary>
+    /// Server-side <see cref="CompositionObject" /> counterpart.
+    /// Is responsible for animation activation and invalidation
+    /// </summary>
+    internal abstract class ServerObject : IExpressionObject
+    {
+        public ServerCompositor Compositor { get; }
+
+        public virtual long LastChangedBy => ItselfLastChangedBy;
+        public long ItselfLastChangedBy { get; private set; }
+        private uint _activationCount;
+        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)
+        {
+            Compositor = compositor;
+        }
+
+        public virtual ExpressionVariant GetPropertyForAnimation(string name)
+        {
+            return default;
+        }
+
+        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 void Activated()
+        {
+            foreach(var kp in _animations)
+                kp.Value.Activate();
+        }
+
+        protected void Deactivated()
+        {
+            foreach(var kp in _animations)
+                kp.Value.Deactivate();
+        }
+
+        void InvalidateSubscriptions(CompositionProperty property)
+        {
+            if(_subscriptions.TryGetValue(property, out var subs))
+                subs.Invalidate();
+        }
+
+        protected void SetValue<T>(CompositionProperty prop, out T field, T value)
+        {
+            field = value;
+            InvalidateSubscriptions(prop);
+        }
+
+        protected T GetValue<T>(CompositionProperty prop, ref T field)
+        {
+            if (_subscriptions.TryGetValue(prop, out var subs))
+                subs.IsValid = true;
+            return field;
+        }
+
+        protected void SetAnimatedValue<T>(CompositionProperty prop, ref T field,
+            TimeSpan commitedAt, IAnimationInstance animation) where T : struct
+        {
+            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);
+        }
+
+        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);
+        }
+        
+        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)
+        {
+            InvalidateSubscriptions(prop);
+            ValuesInvalidated();
+        }
+
+        protected virtual void ValuesInvalidated()
+        {
+            
+        }
+
+        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 virtual CompositionProperty? GetCompositionProperty(string fieldName) => null;
+
+        protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt)
+        {
+            if (this is IDisposable disp
+                && reader.Read<byte>() == 1)
+                disp.Dispose();
+        }
+        
+        public void DeserializeChanges(BatchStreamReader reader, Batch batch)
+        {
+            DeserializeChangesCore(reader, batch.CommitedAt);
+            ValuesInvalidated();
+            ItselfLastChangedBy = batch.SequenceId;
+        }
+    }
+}

+ 39 - 0
src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs

@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Avalonia.Rendering.Composition.Transport
+{
+    /// <summary>
+    /// Represents a group of serialized changes from the UI thread to be atomically applied at the render thread
+    /// </summary>
+    internal class Batch
+    {
+        private static long _nextSequenceId = 1;
+        private static ConcurrentBag<BatchStreamData> _pool = new();
+        public long SequenceId { get; }
+        
+        public Batch()
+        {
+            SequenceId = Interlocked.Increment(ref _nextSequenceId);
+            if (!_pool.TryTake(out var lst))
+                lst = new BatchStreamData();
+            Changes = lst;
+        }
+        private TaskCompletionSource<int> _tcs = new TaskCompletionSource<int>();
+        public BatchStreamData Changes { get; private set; }
+        public TimeSpan CommitedAt { get; set; }
+        
+        public void Complete()
+        {
+            _pool.Add(Changes);
+            Changes = null!;
+
+            _tcs.TrySetResult(0);
+        }
+
+        public Task Completed => _tcs.Task;
+    }
+}

+ 184 - 0
src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs

@@ -0,0 +1,184 @@
+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;
+
+/// <summary>
+/// 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 
+/// </summary>
+internal class BatchStreamData
+{
+    public Queue<BatchStreamSegment<object?[]>> Objects { get; } = new();
+    public Queue<BatchStreamSegment<IntPtr>> Structs { get; } = new();
+}
+
+public struct BatchStreamSegment<TData>
+{
+    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<object?> _objectPool;
+
+    private BatchStreamSegment<object?[]?> _currentObjectSegment;
+    private BatchStreamSegment<IntPtr> _currentDataSegment;
+    
+    public BatchStreamWriter(BatchStreamData output, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool<object?> 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>(T item) where T : unmanaged
+    {
+        var size = Unsafe.SizeOf<T>();
+        if (_currentDataSegment.Data == IntPtr.Zero || _currentDataSegment.ElementCount + size > _memoryPool.BufferSize)
+            NextDataSegment();
+        Unsafe.WriteUnaligned<T>((byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount, item);
+        _currentDataSegment.ElementCount += size;
+    }
+
+    public void WriteObject(object? 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<object?> _objectPool;
+
+    private BatchStreamSegment<object?[]?> _currentObjectSegment;
+    private BatchStreamSegment<IntPtr> _currentDataSegment;
+    private int _memoryOffset, _objectOffset;
+    
+    public BatchStreamReader(BatchStreamData input, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool<object?> objectPool)
+    {
+        _input = input;
+        _memoryPool = memoryPool;
+        _objectPool = objectPool;
+    }
+
+    public unsafe T Read<T>() where T : unmanaged
+    {
+        var size = Unsafe.SizeOf<T>();
+        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 = Unsafe.ReadUnaligned<T>((byte*)_currentDataSegment.Data + _memoryOffset);
+        _memoryOffset += size;
+        if (_memoryOffset == _currentDataSegment.ElementCount)
+        {
+            _memoryPool.Return(_currentDataSegment.Data);
+            _currentDataSegment = new();
+        }
+
+        return rv;
+    }
+
+    public T ReadObject<T>() where T : class? => (T)ReadObject()!;
+    
+    public object? 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 IsObjectEof => _currentObjectSegment.Data == null && _input.Objects.Count == 0;
+    
+    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);
+    }
+}

+ 156 - 0
src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs

@@ -0,0 +1,156 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Avalonia.Platform;
+using Avalonia.Threading;
+
+namespace Avalonia.Rendering.Composition.Transport;
+
+/// <summary>
+/// A pool that keeps a number of elements that was used in the last 10 seconds 
+/// </summary>
+internal abstract class BatchStreamPoolBase<T> : IDisposable
+{
+    readonly Stack<T> _pool = new();
+    bool _disposed;
+    int _usage;
+    readonly int[] _usageStatistics = new int[10];
+    int _usageStatisticsSlot;
+    bool _reclaimImmediately;
+
+    public int CurrentUsage => _usage;
+    public int CurrentPool => _pool.Count;
+
+    public BatchStreamPoolBase(bool needsFinalize, Action<Func<bool>>? startTimer = null)
+    {
+        if(!needsFinalize)
+            GC.SuppressFinalize(needsFinalize);
+
+        var updateRef = new WeakReference<BatchStreamPoolBase<T>>(this);
+        if (AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>() == null)
+            _reclaimImmediately = true;
+        else
+            StartUpdateTimer(startTimer, updateRef);
+    }
+
+    static void StartUpdateTimer(Action<Func<bool>>? startTimer, WeakReference<BatchStreamPoolBase<T>> updateRef)
+    {
+        Func<bool> timerProc = () =>
+        {
+            if (updateRef.TryGetTarget(out var target))
+            {
+                target.UpdateStatistics();
+                return true;
+            }
+
+            return false;
+        };
+        if (startTimer != null)
+            startTimer(timerProc);
+        else
+            DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1));
+    }
+
+    private void UpdateStatistics()
+    {
+        lock (_pool)
+        {
+            var maximumUsage = _usageStatistics.Max();
+            var recentlyUsedPooledSlots = maximumUsage - _usage;
+            var keepSlots = Math.Max(recentlyUsedPooledSlots, 10);
+            while (keepSlots < _pool.Count) 
+                DestroyItem(_pool.Pop());
+
+            _usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length;
+            _usageStatistics[_usageStatisticsSlot] = 0;
+        }
+    }
+
+    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 && !_reclaimImmediately)
+            {
+                _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<T> : BatchStreamPoolBase<T[]> where T : class?
+{
+    public int ArraySize { get; }
+
+    public BatchStreamObjectPool(int arraySize = 128, Action<Func<bool>>? startTimer = null) : base(false, startTimer)
+    {
+        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<IntPtr>
+{
+    public int BufferSize { get; }
+
+    public BatchStreamMemoryPool(int bufferSize = 1024, Action<Func<bool>>? startTimer = null) : base(true, startTimer)
+    {
+        BufferSize = bufferSize;
+    }
+    
+    protected override IntPtr CreateItem() => Marshal.AllocHGlobal(BufferSize);
+
+    protected override void DestroyItem(IntPtr item) => Marshal.FreeHGlobal(item);
+}

+ 9 - 0
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();
+}

+ 98 - 0
src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs

@@ -0,0 +1,98 @@
+using System.Collections;
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Transport
+{
+    /// <summary>
+    /// A helper class used from generated UI-thread-side collections of composition objects.
+    /// </summary>
+    // 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<TClient, TServer> : IList<TClient>
+        where TServer : ServerObject
+        where TClient : CompositionObject
+    {
+        private readonly IRegisterForSerialization _parent;
+        private bool _changed;
+
+        public interface IRegisterForSerialization
+        {
+            void RegisterForSerialization();
+        }
+
+        public ServerListProxyHelper(IRegisterForSerialization parent)
+        {
+            _parent = parent;
+        }
+        
+        private readonly List<TClient> _list = new List<TClient>();
+        
+        IEnumerator<TClient> IEnumerable<TClient>.GetEnumerator() => GetEnumerator();
+        public List<TClient>.Enumerator GetEnumerator() => _list.GetEnumerator();
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+        public void Add(TClient item) => Insert(_list.Count, item);
+
+        public void Clear()
+        {
+            _list.Clear();
+            _changed = true;
+            _parent.RegisterForSerialization();
+        }
+
+        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);
+            _changed = true;
+            _parent.RegisterForSerialization();
+        }
+
+        public void RemoveAt(int index)
+        {
+            _list.RemoveAt(index);
+            _changed = true;
+            _parent.RegisterForSerialization();
+        }
+
+        public TClient this[int index]
+        {
+            get => _list[index];
+            set
+            {
+                _list[index] = value;
+                _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;
+        }
+    }
+}

Some files were not shown because too many files changed in this diff